[
  {
    "path": ".changeset/config.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@3.0.1/schema.json\",\n  \"changelog\": false,\n  \"commit\": false,\n  \"fixed\": [],\n  \"linked\": [],\n  \"access\": \"restricted\",\n  \"baseBranch\": \"v5\",\n  \"updateInternalDependencies\": \"patch\",\n  \"ignore\": [\"@antv/g6-site\", \"bundle\"]\n}\n"
  },
  {
    "path": ".codecov.yml",
    "content": "# Setting coverage targets per flag\ncoverage:\n  round: down\n  range: 60..90\n  precision: 2\n  status:\n    patch: off\n    project:\n      default: off\n      g6:\n        threshold: 1%\n        flags:\n          - g6\n\nflags:\n  g6:\n    paths:\n      # filter the folder(s) you wish to measure by that flag\n      - packages/g6\n\ncomment:\n  layout: \"reach, diff, flags, files\"\n  behavior: default\n  require_changes: true # only post the comment if coverage changes\n\ngithub_checks:\n  annotations: false\n\nflag_management:\n  default_rules:\n    carryforward: false"
  },
  {
    "path": ".commitlintrc.js",
    "content": "module.exports = {\n  extends: ['@commitlint/config-conventional'],\n  rules: {\n    'type-enum': [\n      2,\n      'always',\n      ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test', 'wip'],\n    ],\n  },\n};\n"
  },
  {
    "path": ".cursor/rules/translation.mdc",
    "content": "---\ndescription: 翻译\nglobs:\nalwaysApply: false\n---\n# Translation Guidelines for site/docs\n\nWhen translating files under the `site/docs` directory, please adhere to the following guidelines:\n\n1. **Consistency in Terminology**: Ensure that terminology is consistent throughout the document. Use a glossary if available to maintain uniformity in terms.\n\n   **Glossary**:\n\n   - 画布 (Canvas)\n   - 元素 (Element)\n   - 节点 (Node)\n   - 边 (Edge)\n   - 组合 (Combo)\n   - 交互 (Behavior)\n   - 布局 (Layout)\n   - 插件 (Plugin)\n   - 动画 (Animation)\n   - 数据处理 (Transform)\n   - 色板 (Palette)\n   - 配置项 (Option)\n   - 图数据 (Graph Data)\n   - 树图 (Tree Graph)\n   - 属性 (Property)\n   - 描述 (Description)\n   - 类型 (Type)\n   - 默认值 (Default Value)\n   - 必选 (Required)\n\n2. **Adjust Hyperlinks**: Review and adjust hyperlinks to ensure they point to the correct translated sections or documents. Verify that all links are functional and correctly formatted.\n\n   - **Internal Links**: In the English version, all internal links should have a `/en` prefix, while the Chinese version should not have any prefix. Ensure this prefix is added to all internal links in English documents to avoid any oversight.\n   - **Anchor Points**: For anchor points following a `#`, if they contain Chinese characters, they should be adjusted to match the corresponding title in the English version rather than being directly translated.\n   - **External Links**: Convert external links appropriately to ensure they align with the language and context of the document.\n\n3. **Direct Writing to Translated Documents**: Translations should be stored in corresponding `.en.md` or `.zh.md` files within the same directory. Ensure that the translated content is placed in the correct location within the document.\n\n   - When translating from Chinese to English, create or update the `.en.md` file in the same directory.\n   - When translating from English to Chinese, create or update the `.zh.md` file in the same directory.\n\n4. **Support for Partial Content Translation**: Allow for the selection and translation of specific sections of content. Translated sections should be inserted into the appropriate location within the document, maintaining the logical flow and structure.\n\n   - **Full Document Translation**: If the entire document is selected for translation, replace the entire content with the translated version.\n   - **Partial Content Translation**: If only specific sections are selected, find the appropriate place to replace or insert the translated content, ensuring the document's logical flow and structure are maintained.\n\n5. **Contextual Translation**: Avoid literal translations. Ensure that the translation fits the English context and conveys the intended meaning accurately.\n\n6. **Direct Modification**: Translations should be directly modified in the corresponding `.en.md` or `.zh.md` files without returning the translated content separately. Ensure that the changes are saved in the correct file and location.\n\n7. **Preserve Metadata Order**: Do not modify the `order` attribute in the page metadata during translation. This ensures that the document order remains consistent across different language versions.\n\n8. **Add '/en' Prefix to Internal Links**: Ensure that all internal links in English documentation have the '/en' prefix to maintain consistency and correct navigation.\n\nBy following these guidelines, translations will be more accurate and consistent, facilitating easier review and integration into the documentation.\n"
  },
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[Makefile]\nindent_style = tab"
  },
  {
    "path": ".eslintignore",
    "content": "dist\nes\nlib\nnode_modules"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  env: {\n    browser: true,\n    es2021: true,\n    node: true,\n    commonjs: true,\n    jest: true,\n  },\n  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:jsdoc/recommended-error'],\n  overrides: [\n    {\n      env: {\n        node: true,\n      },\n      files: ['.eslintrc.{js,cjs}'],\n      parserOptions: {\n        sourceType: 'script',\n      },\n    },\n    {\n      files: ['**/__tests__/**', '*.js', '*.mjs'],\n      rules: {\n        'jsdoc/require-jsdoc': 0,\n      },\n    },\n    {\n      files: ['./packages/g6/src/plugins/hull/!(index).ts', '*.js', '*.mjs', '*.ts'],\n      rules: {\n        'jsdoc/require-jsdoc': 0,\n      },\n    },\n    {\n      files: ['**/demo-to-test/**'],\n      rules: {\n        '@typescript-eslint/no-var-requires': 'off',\n      },\n    },\n    {\n      files: ['./packages/site/**', './scripts/**'],\n      rules: {\n        'no-console': 'off',\n      },\n    },\n  ],\n  parser: '@typescript-eslint/parser',\n  parserOptions: {\n    ecmaVersion: 'latest',\n    sourceType: 'module',\n  },\n  plugins: ['@typescript-eslint', 'jsdoc'],\n  rules: {\n    // indent: ['error', 2, { SwitchCase: 1 }],\n    'linebreak-style': ['error', 'unix'],\n    quotes: ['error', 'single', { allowTemplateLiterals: true, avoidEscape: true }],\n    semi: ['error', 'always'],\n    '@typescript-eslint/no-unused-vars': ['warn', { ignoreRestSiblings: true }],\n    'jsdoc/require-param-type': 0,\n    '@typescript-eslint/no-this-alias': 'off',\n    'no-console': 'error',\n\n    // TODO: rules below will be set to 2 in the future\n    'jsdoc/require-jsdoc': 1,\n    'jsdoc/check-access': 1,\n    'jsdoc/valid-types': 0,\n    /**\n     * js plugin rules\n     */\n    'jsdoc/check-tag-names': [\n      'error',\n      {\n        // Allow TSDoc tags @remarks, @defaultValue\n        // Custom tags: @apiCategory for Graph API\n        definedTags: ['remarks', 'defaultValue', 'apiCategory'],\n      },\n    ],\n    'jsdoc/require-description': 1,\n    'jsdoc/require-param': 1,\n    'jsdoc/check-param-names': 1,\n    'jsdoc/require-param-description': 1,\n    'jsdoc/require-returns': 1,\n    'jsdoc/require-returns-type': 0,\n    'jsdoc/require-returns-description': 1,\n\n    // TODO: rules below are not recommended, and will be removed in the future\n    '@typescript-eslint/no-explicit-any': 1,\n    '@typescript-eslint/ban-types': 1,\n    '@typescript-eslint/ban-ts-comment': 1,\n  },\n};\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/1.bug_report.yml",
    "content": "name: '🐞 Bug Report'\ndescription: Create a report to help us improve, Ask questions in Discussions / 创建一个问题报告以帮助我们改进，提问请到 Discussions\ntitle: '[Bug]: '\nlabels: ['waiting for maintainer']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Report errors and exceptions found in the project.\n\n        Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already:\n\n        ---\n\n        反馈在项目中发现的错误、异常。\n\n        在提交新 issue 之前，先通过以下链接检查是否存在相同问题:\n\n        > [Issues](../issues) | [Closed Issues](../issues?q=is:issue+is:closed) | [Discussions](../discussions)\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the bug / 问题描述\n      placeholder: |\n        If there is a code block, please use Markdown syntax, such as:\n        如包含代码块，请使用 Markdown 语法，如：\n\n        ```javascript\n        // Your code here\n        ```\n    validations:\n      required: true\n  - type: input\n    id: link\n    attributes:\n      label: Reproduction link / 复现链接\n      placeholder: |\n        CodeSandbox / StackBlitz / ...\n    validations:\n      required: false\n  - type: textarea\n    id: steps\n    attributes:\n      label: Steps to Reproduce the Bug or Issue / 重现步骤\n    validations:\n      required: false\n  - type: dropdown\n    id: version\n    attributes:\n      label: Version / 版本\n      options:\n        - Please select / 请选择\n        - 🆕 5.x\n        - 4.x\n        - 3.x\n    validations:\n      required: true\n  - type: checkboxes\n    id: OS\n    attributes:\n      label: OS / 操作系统\n      options:\n        - label: macOS\n        - label: Windows\n        - label: Linux\n        - label: Others / 其他\n    validations:\n      required: true\n  - type: checkboxes\n    id: Browser\n    attributes:\n      label: Browser / 浏览器\n      options:\n        - label: Chrome\n        - label: Edge\n        - label: Firefox\n        - label: Safari (Limited support / 有限支持)\n        - label: IE (Nonsupport / 不支持)\n        - label: Others / 其他\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/2.feature_request.yml",
    "content": "name: '💡 Feature Request'\ndescription: I have a suggestion (and may want to implement it) / 我有一个建议（或者想参与贡献）\ntitle: '[Feat]: '\nlabels: ['waiting for maintainer']\nbody:\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the feature / 功能描述\n      description: 'What problem does this feature solve? / 这个功能解决什么问题？'\n      placeholder: |\n        I would like to see... because...\n        我希望能有... 因为...\n    validations:\n      required: true\n\n  - type: dropdown\n    attributes:\n      label: Are you willing to contribute? / 是否愿意参与贡献？\n      options:\n        - Please select / 请选择\n        - ✅ Yes / 是\n        - ❌ No / 否\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/3.docs_feedback.yml",
    "content": "name: '📖 Docs Feedback'\ndescription: 'Help us make our docs better! Share your thoughts and suggestions / 帮助我们改进文档！分享您的想法和建议'\nlabels: ['waiting for maintainer']\ntitle: '[Docs]: '\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ### 👋 Hello there! / 您好！\n\n        Thank you for helping us improve our documentation! Your feedback is invaluable to us and will help make our docs better for everyone.\n\n        感谢您帮助我们改进文档！您的反馈对我们来说非常宝贵，这将帮助我们为所有人提供更好的文档体验。\n\n  - type: input\n    id: page-url\n    attributes:\n      label: '📍 Which page are you reading?'\n      description: \"Please share the URL of the page you'd like to give feedback on / 请分享您想要反馈的页面链接\"\n      placeholder: 'https://docs.example.com/'\n    validations:\n      required: true\n\n  - type: dropdown\n    id: feedback-type\n    attributes:\n      label: \"💭 What's on your mind?\"\n      description: 'What kind of feedback would you like to share? / 您想分享什么类型的反馈？'\n      options:\n        - 'Could be clearer / 需要更清晰的解释'\n        - 'Missing information / 信息不完整'\n        - 'Example needs fixing / 示例需要修复'\n        - 'Content needs updating / 内容需要更新'\n        - 'Other suggestions / 其他建议'\n    validations:\n      required: true\n\n  - type: textarea\n    id: description\n    attributes:\n      label: '🤔 Tell us more'\n      description: |\n        Let us know what went wrong when you were using this documentation and what we could do to improve it | 请告知您在使用此文档时遇到的问题以及我们可以改进的地方\n      placeholder: |\n        Share your experience:\n        - What confused you?\n        - What were you looking for?\n        - What would have helped you understand better?\n\n        分享您的体验：\n        - 哪里让您感到困惑？\n        - 您在寻找什么信息？\n        - 什么样的改进能帮助您更好地理解？\n    validations:\n      required: true\n\n  - type: textarea\n    id: suggestion\n    attributes:\n      label: '💡 Got any suggestions?'\n      description: |\n        What are you trying to accomplish? Providing context helps us come up with a solution that is more useful in the real world | 您希望实现什么目标？提供上下文有助于我们提出更实用的解决方案\n      placeholder: |\n        Some ideas you might share:\n        - Adding more examples\n        - Including screenshots\n        - Providing step-by-step guides\n\n        您可以建议：\n        - 添加更多示例\n        - 包含截图说明\n        - 提供步骤指南\n    validations:\n      required: false\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        💝 Thanks for taking the time to fill out this form! Your feedback helps make our documentation better for everyone.\n\n        感谢您抽出宝贵时间填写这份反馈！您的建议将帮助我们为所有人提供更好的文档。\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/4.oscp.yml",
    "content": "name: '✏️ OSCP Season of Docs'\ndescription: '通过开源社区的力量，共同打造更友好、更易上手的 AntV 文档 | Contribute to AntV G6 documentation with the power of open source community'\nlabels: ['OSCP']\nprojects: ['2025 AntV OSCP Season of Docs']\ntitle: '[Docs]: '\nbody:\n  - type: textarea\n    id: 'summary'\n    attributes:\n      label: 任务介绍\n      value: |\n        > 此 ISSUE 为 [AntV 开源共建计划（AntV Open Source Contribution Plan，简称 AntV OSCP）Phase3 - 文档季](https://github.com/antvis/G6/issues/6882)的任务 ISSUE，欢迎社区开发者参与共建~\n        >  - 更多任务，可查看 [GitHub Project - 2025 AntV OSCP Season of Docs](https://github.com/orgs/antvis/projects/31)。\n\n        > This ISSUE is one of the tasks of the [AntV Open Source Contribution Plan (referred to as AntV OSCP) Pharse3 - Season of Docs](https://github.com/antvis/G6/issues/6882) . Welcome to join us in building it together!\n        > - For more tasks, you can check the [GitHub Project - 2025 AntV OSCP Season of Docs](https://github.com/orgs/antvis/projects/31).\n\n        ## 改造文档「xxx」\n\n        ### 任务介绍\n\n        - 任务名称：改造 [xxx](https://g6.antv.antgroup.com/manual/xxx) 文档\n        - 技术方向：g6 / docs\n        - 任务难度：新手友好 🌟 / 进阶 🌟🌟 / 专家 🌟🌟🌟\n        - 可获得积分：20分 / 30分 / 50分\n\n        ### 详细要求\n\n        - 文档规范：\n          - 参考示例：\n            - [插件 - 内置插件 - 背景](https://g6.antv.antgroup.com/manual/plugin/build-in/background)\n            - [插件 - 内置插件 - 工具栏](https://g6.antv.antgroup.com/manual/plugin/build-in/toolbar)\n          - 文档结构：「GridLine」部分的文档至少应该包括 **概述**、**使用场景**、**配置项**，**示例代码** 几个部分。\n          - 内容规范：\n            - 属性表格需要包含【属性】【描述】【类型】【默认值】【必选】列，所有的配置项包括绘图属性需要罗列完整。\n            - 复杂类型单独解释说明\n            - 必要时可配上示意图\n\n        ### 能力要求\n\n        ```\n        - 对 G6 有一定了解，能阅读 G6 源码，编写示例。\n        ```\n\n        ### 执行路径\n\n        #### 1. 认领任务\n\n        选择感兴趣的且没有 Assignee 的任务，按格式回复，该任务 assign 给你后即为成功认领~\n        - 认领回复格式：【@GitHub ID + Give it to me】\n        - eg：【@yvonnyx Give it to me】\n\n        #### 2. 做任务\n\n        1. clone  g6 代码\n\n        ```bash\n        git clone https://github.com/antvis/G6.git\n        ```\n\n        2. 拉取所有线上分支\n\n        ```bash\n        git fetch\n        ```\n\n        3. 切换到 v5 分支\n\n        ```bash\n        git checkout v5\n        ```\n\n        4. 安装依赖\n\n        ```bash\n        pnpm install\n        ```\n\n        5. 进入 site 包\n\n        ```bash\n        cd packages/site\n        ```\n\n        6. 本地启动 site 站点\n\n        ```bash\n        pnpm run dev\n        ```\n\n        7. 优化文档并预览效果\n\n        对应文件位于 `packages/site/docs/manual/xxx.zh.md`\n\n        #### 3. 提交 PR\n\n        > 请保证文档语意通顺、格式正确、代码示例完整且能够正确编译，否则该 PR 将不会被 review 和 merge，此 issue 将被重新释放。\n\n        1. 提交 Pull Request，等待 Code Review\n\n        - PR 标题参考 `docs: 任务名称` ，如 `docs: 改造点击选中交互文档` ，并关联 `OSCP` 标签，以便快速进入 PR review 阶段。\n        - PR 与对应任务 ISSUE 进行关联，方式：在 PR 正文中，通过 `- Fixed: #任务 ISSUE 号` 即可实现关联，eg：\n\n        ![Image](https://github.com/user-attachments/assets/a05cc8f5-d42b-47fd-b3e2-be796a8b8017)\n\n        2. 根据（多次） Code Review 建议修改\n\n        3 等待合并入 v5 分支后，积分生效\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "# Ref: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser\nblank_issues_enabled: false\ncontact_links:\n  - name: 📝 Question / 问题咨询\n    url: https://github.com/antvis/g6/discussions/new?category=q-a\n    about: Discuss G6 usage / 讨论 G6 使用问题\n  - name: 💬 Join Discussion Group / 加入讨论群\n    url: https://qr.dingtalk.com/action/joingroup?code=v1,k1,rQHsK/OOTPX8ixM/DaXcL3goIYpnpKr/AFIonmA1SOM=&_dt_no_comment=1&origin=11?\n    about: Join DingTalk discussion group / 加入钉钉讨论群\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/oscp.yml",
    "content": "# name: 'AntV OSCP 计划 / AntV OSCP Plan'\n# description: AntV 开源共建计划(仅供管理员使用) / AntV Open Source Contribution Plan(For administrators only)\n# body:\n#   - type: checkboxes\n#     id: AntV_OSCP_program\n#     attributes:\n#       label: AntV Open Source Contribution Plan（可选/Optional）\n#       description: |\n#         AntV 开源共建计划期望可以基于 AntV 的开源 Roadmap 开放具体开发任务到社区，以社区共建任务的形式推动“AntV” 的开源发展，也期望有更多社区伙伴各各种形式参与到 AntV 的开源共建中，共同参与数据可视化开源生态的持续建设。\n#         AntV Open Source Contribution Plan expects to open specific development tasks to the community based on AntV's open source roadmap, promote the open source development of \"AntV\" in the form of community co-construction tasks, and also hope that more community partners will participate in AntV's open source co-construction in various forms, and jointly participate in the continuous construction of the data visualization open source ecosystem.\n\n#         若有感兴趣想要认领的任务，可直接回复认领，如果你是首次认领可先完成初级入门任务。\n#         If you are interested in claiming a task, you can directly reply to claim it. If you are claiming for the first time, you can complete the primary entry task first.\n\n#       options:\n#         - label: 我同意将这个 Issue 参与 OSCP 计划 / I agree to participate in the OSCP plan\n#     validations:\n#       required: false\n#   - type: dropdown\n#     id: issue_oscp_difficulty\n#     attributes:\n#       label: Issue 类型 / Issue Type\n#       options:\n#         - 初级任务 / Primary Task\n#         - 中级任务 / Intermediate Task\n#         - 高级任务 / Advanced Task\n#         - 专家任务 / Expert Task\n#     validations:\n#       required: false\n#   - type: textarea\n#     id: oscp_task_description\n#     attributes:\n#       label: 任务介绍 / Task Description\n#       description: |\n#         简单描述任务背景信息，为了解决哪些问题\n#         Briefly describe the background information of the task and what problems to solve\n#     validations:\n#       required: false\n#   - type: textarea\n#     id: oscp_task_info_ref\n#     attributes:\n#       label: 参考说明 / Reference Description\n#       description: |\n#         提供一些可参考的 demo，相关教程辅助用户解决问题\n#         Provide some demos and related tutorials for users to solve problems\n#     validations:\n#       required: false\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: build\n\non:\n  push:\n    paths-ignore:\n      - '**/*.md'\n  pull_request:\n    paths-ignore:\n      - '**/*.md'\n\nconcurrency:\n  group: ${{github.workflow}}-${{github.event_name}}-${{github.ref}}\n  cancel-in-progress: true\n\njobs:\n  lint-and-build-g6:\n    runs-on: macos-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Install Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20\n\n      - name: Install Dependencies\n        run: |\n          brew install python-setuptools pkg-config cairo pango libpng jpeg giflib librsvg\n\n      - uses: pnpm/action-setup@v4\n        name: Install pnpm\n        with:\n          version: 9\n          run_install: false\n\n      - name: Install Dependencies\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Run CI\n        run: |\n          npm run ci\n\n      - name: Run Playwright tests\n        run: |\n          pnpm exec playwright install chromium\n          pnpm exec playwright test\n\n      - name: Upload blob report to GitHub Actions Artifacts\n        if: always()\n        uses: actions/upload-artifact@v4\n        with:\n          name: report\n          path: |\n            packages/g6/__tests__/snapshots/**/*-actual.svg\n            playwright-report/\n          retention-days: 1\n\n      - name: Upload coverage to Codecov\n        uses: codecov/codecov-action@v5\n        with:\n          files: ./packages/g6/coverage/coverage-final.json\n          flags: g6\n        env:\n          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deploy\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - v5\n\njobs:\n  deploy-site:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Setup node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18\n      - uses: pnpm/action-setup@v4\n        with:\n          version: 9\n      - uses: actions/checkout@v2\n      - run: pnpm install\n      - run: pnpm build\n      - run: |\n          cd ./packages/site\n          pnpm run build\n      - run: cp ./packages/site/CNAME ./packages/site/dist/CNAME\n      - run: |\n          cd ./packages/site/dist\n          git init\n          git config --local user.name antv\n          git config --local user.email antv@antfin.com\n          git add .\n          git commit -m \"update by release action\"\n      - uses: ad-m/github-push-action@master\n        with:\n          github_token: ${{secrets.PERSONAL_ACCESS_TOKEN}}\n          directory: ./packages/site/dist\n          branch: gh-pages\n          force: true\n"
  },
  {
    "path": ".github/workflows/ensure-triage-label.yml",
    "content": "name: Ensure Triage Label is Present\n\non:\n  label:\n    types:\n      - deleted\n  issues:\n    types:\n      - opened\n\npermissions: {}\n\njobs:\n  label_issues:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n    steps:\n      - uses: actions/github-script@v7.0.1\n        with:\n          script: |\n            const labelToTriage = 'waiting for maintainer';\n\n            const { data: labels } = await github.rest.issues.listLabelsOnIssue({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n            });\n\n            if (labels.length <= 0) {\n              await github.rest.issues.addLabels({\n                issue_number: context.issue.number,\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                labels: [labelToTriage]\n              })\n            }\n"
  },
  {
    "path": ".github/workflows/issue-automated.yml",
    "content": "name: Issue Automated Processing\n\non:\n  issues:\n    types: [opened, reopened, edited]\n  workflow_dispatch:\n    inputs:\n      issue_number:\n        description: 'Issue number to process'\n        required: true\n        type: number\n\npermissions:\n  issues: write\n  pull-requests: read\n  contents: read\n\njobs:\n  issue-response:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v3\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20\n\n      - name: Install dependencies\n        run: |\n          yarn install\n          yarn add openai @antv/mcp-server-antv -W\n\n      - name: Process Issue\n        uses: actions/github-script@v7\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} #\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            let issue = context.payload.issue;\n\n            if (context.eventName === 'workflow_dispatch') {\n              const issueNumber = context.payload.inputs.issue_number;\n              const { data: issueData } = await github.rest.issues.get({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: issueNumber\n              });\n              issue = issueData;\n            }\n\n            const script = require('./.github/workflows/scripts/issue-automated.js');\n            await script({\n              github,\n              core,\n              context,\n              issue\n            });\n"
  },
  {
    "path": ".github/workflows/issue_translate.yml",
    "content": "name: Translate Issue Title\n\non:\n  issues:\n    types: [opened, edited]\n\njobs:\n  run:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20\n\n      - name: Translate\n        uses: Aarebecca/issue-translator@1.0.0\n        with:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          AZURE_TRANSLATE_KEY: ${{ secrets.AZURE_TRANSLATE_KEY }}\n          AZURE_TRANSLATE_ENDPOINT: 'https://api.cognitive.microsofttranslator.com'\n          AZURE_TRANSLATE_LOCATION: 'eastus'\n          AZURE_TRANSLATE_TARGET: 'en'\n"
  },
  {
    "path": ".github/workflows/manage-labeled.yml",
    "content": "name: Manage Labeled Issue\n\non:\n  issues:\n    types: [labeled]\n\npermissions: {}\n\njobs:\n  manage-labels:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n    steps:\n      # 当添加分类标签时，移除 'waiting for maintainer' 标签\n      - name: Remove `waiting for maintainer` label when other triage labels are added\n        uses: actions/github-script@v7.0.1\n        with:\n          script: |\n            const labelsToCheck = ['waiting for author', 'need improvement', 'bug 🐛', 'documentation 📖', 'feature 💡', 'question 💬', 'notabug', 'stale', 'wontfix', 'duplicate'];\n\n            const labelToRemove = 'waiting for maintainer';\n            const newLabel = context.payload.label.name;\n\n            if (labelsToCheck.includes(newLabel)) {\n                const { data: labels } = await github.rest.issues.listLabelsOnIssue({\n                  issue_number: context.issue.number,\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                });\n                if (labels.some(label => label.name === labelToRemove)) {\n                  await github.rest.issues.removeLabel({\n                    issue_number: context.issue.number,\n                    owner: context.repo.owner,\n                    repo: context.repo.repo,\n                    name: labelToRemove,\n                  });\n                }\n            }\n\n      # 当添加 'need improvement' 标签时，同时添加 'waiting for author' 标签\n      - name: Append label if `need improvement` is added\n        if: github.event.label.name == 'need improvement'\n        uses: actions-cool/issues-helper@v3.6.0\n        with:\n          actions: \"add-labels\"\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          labels: \"waiting for author\"\n\n      # 当添加 'need improvement' 标签时，发送提醒评论\n      - name: Warn bad issue when `need improvement` label is added\n        if: github.event.label.name == 'need improvement'\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: \"create-comment\"\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          body: |\n            📝 To help us better understand and address your issue, **please provide more information, or use the standard format**, otherwise we will not process this issue.\n\n            Reference document: \n\n              - [Creating an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/creating-an-issue)\n              - [Basic writing and formatting syntax](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax)\n\n            ---\n\n            📝 为了帮助我们更好地理解和解决你的问题，**请提供更多信息，或者使用规范的格式**，否则我们不会处理这个 issue。\n\n            参考文档：\n\n              - [创建议题](https://docs.github.com/zh/issues/tracking-your-work-with-issues/using-issues/creating-an-issue)\n              - [基本撰写和格式语法](https://docs.github.com/zh/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax)\n          emoji: \"heart\"\n\n      # 处理 stale 标签\n      - name: Add stale issue comment before closing\n        if: github.event.label.name == 'stale'\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: \"create-comment\"\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          body: |\n            ⚠️ This issue has been automatically closed due to inactivity. \n\n            - If the issue is still relevant and important to you, feel free to:\n              1. Reopen with additional information\n              2. Create a new issue with updated context\n              3. Reference any related issues or discussions\n\n            We close inactive issues to keep our backlog manageable and focused on active issues.\n\n            Your contribution makes our project better! 🌟\n\n            ---\n\n            ⚠️ 由于长期无活动，此 issue 已被自动关闭。\n\n            - 如果这个问题对您来说仍然重要，您可以：\n              1. 重新打开并提供补充信息\n              2. 创建一个新的 issue 并更新相关背景\n              3. 关联相关的 issue 或讨论\n\n            为了更好地维护项目，我们需要定期清理不活跃的问题。\n\n            感谢您为开源添砖加瓦！🌟\n          emoji: \"heart\"\n\n      - name: Close stale issue\n        if: github.event.label.name == 'stale'\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: \"close-issue\"\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n\n      # 处理 wontfix 标签\n      - name: Add wontfix issue comment before closing\n        if: github.event.label.name == 'wontfix'\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: \"create-comment\"\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          body: |\n            🚫 This issue has been marked as \"Won't Fix\". Here's why:\n\n            - The described behavior is working as intended\n            - The request falls outside our project scope/goals\n            - The cost/benefit ratio doesn't justify the change\n\n            If you have new information that might change this decision, feel free to:\n            1. Share your additional context\n            2. Propose alternative solutions\n            3. Start a discussion to explore different approaches\n\n            Thank you for your understanding and engagement! 🙏\n\n            ---\n\n            🚫 此 issue 被标记为\"不予修复\"，原因如下：\n\n            - 当前行为符合设计预期\n            - 该请求超出项目范围/目标\n            - 投入产出比不足以支持此变更\n\n            如果您有任何新的想法或建议，欢迎：\n            1. 分享更多上下文\n            2. 提出替代方案\n            3. 发起讨论以探索不同思路\n\n            感谢理解与支持！🙏\n          emoji: \"heart\"\n\n      - name: Close wontfix issue\n        if: github.event.label.name == 'wontfix'\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: \"close-issue\"\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n\n      # 处理 notabug 标签\n      - name: Add notabug issue comment before closing\n        if: github.event.label.name == 'notabug'\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: \"create-comment\"\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n          body: |\n            ✅ After careful review, we've determined this is not a bug. Here's why:\n\n            - The current behavior is working as designed\n            - This might be a misunderstanding of the feature\n            - The issue cannot be reproduced with the provided information\n\n            If you still believe this is a bug, please:\n            1. Provide a minimal reproduction\n            2. Share your expected behavior\n            3. Include more detailed environment information\n\n            Thank you for helping us improve our project! 💫\n\n            ---\n\n            ✅ 经过仔细核查，这并非一个 bug，原因如下：\n\n            - 当前表现符合设计预期\n            - 可能是对功能理解有所偏差\n            - 基于已提供信息无法复现问题\n\n            如果您仍认为这是一个 bug，建议：\n            1. 提供最小复现示例\n            2. 说明您期望的表现\n            3. 补充更详细的环境信息\n\n            期待您的反馈！💫\n          emoji: \"heart\"\n\n      - name: Close notabug issue\n        if: github.event.label.name == 'notabug'\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: \"close-issue\"\n          token: ${{ secrets.GITHUB_TOKEN }}\n          issue-number: ${{ github.event.issue.number }}\n"
  },
  {
    "path": ".github/workflows/mark-duplicate.yml",
    "content": "name: Mark Duplicate Issue\n\non:\n  issue_comment:\n    types: [created, edited]\n\npermissions: {}\n\njobs:\n  mark-duplicate:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      issues: write\n    steps:\n      - name: Mark duplicate issue\n        uses: actions-cool/issues-helper@v3.6.0\n        with:\n          actions: \"mark-duplicate\"\n          token: ${{ secrets.GITHUB_TOKEN }}\n          duplicate-labels: \"duplicate\"\n          remove-labels: \"waiting for maintainer\"\n          close-issue: true\n"
  },
  {
    "path": ".github/workflows/no-response.yml",
    "content": "name: No Response\n\n# `issues`.`closed`, `issue_comment`.`created`, and `scheduled` event types are required for this Action\n# to work properly.\non:\n  issues:\n    types:\n      - closed\n  issue_comment:\n    types:\n      - created\n  schedule:\n    # These runs in our repos are spread evenly throughout the day to avoid hitting rate limits.\n    # If you change this schedule, consider changing the remaining repositories as well.\n    # Runs at 12 am, 12 pm\n    - cron: \"0 0,12 * * *\"\n\npermissions: {}\n\njobs:\n  noResponse:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      issues: write\n    steps:\n      - uses: MBilalShafi/no-response-add-label@v0.0.6\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          # Number of days of inactivity before an Issue is closed for lack of response\n          daysUntilClose: 7\n          # Label requiring a response\n          responseRequiredLabel: \"waiting for author\"\n          # Label to add back when required label is removed\n          optionalFollowupLabel: \"waiting for maintainer\"\n          # Comment to post when closing an Issue for lack of response. Set to `false` to disable\n          closeComment: |\n            ⚠️ This issue has been automatically closed due to inactivity. \n\n            - If the issue is still relevant and important to you, feel free to:\n              1. Reopen with additional information\n              2. Create a new issue with updated context\n              3. Reference any related issues or discussions\n\n            We close inactive issues to keep our backlog manageable and focused on active issues. \n\n            Your contribution makes our project better! 🌟\n\n            ---\n\n            ⚠️ 由于长期无活动，此 issue 已被自动关闭。\n\n            - 如果这个问题对您来说仍然重要，您可以：\n              1. 重新打开并提供补充信息\n              2. 创建一个新的 issue 并更新相关背景\n              3. 关联相关的 issue 或讨论\n\n            为了更好地维护项目，我们需要定期清理不活跃的问题。\n\n            感谢您为开源添砖加瓦！🌟\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "# 当具有 publish 标签的 PR 被合并时，自动发布新版本\n# Automatically publish a new version when a PR with the publish label is merged\nname: Auto Publish\non:\n  pull_request:\n    types: [closed]\n    branches:\n      - v5\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    if: contains(github.event.pull_request.labels.*.name, 'publish') && github.event.pull_request.merged == true\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Install Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18\n\n      - name: Install pnpm and dependencies\n        uses: pnpm/action-setup@v4\n        with:\n          version: 9\n          run_install: true\n\n      - name: Build\n        run: npm run build\n\n      - name: Publish\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n        run: pnpm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} & pnpm run publish\n"
  },
  {
    "path": ".github/workflows/resolved-pending-release.yml",
    "content": "name: Resolved Pending Release\n\non:\n  release:\n    types: [published]\n\npermissions: {}\n\njobs:\n  comment-on-issues:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          # Check this repository out, otherwise the script won't be available,\n          # as it otherwise checks out the repository where the workflow caller is located\n          repository: antvis/github-config\n\n      - name: Comment on issues\n        uses: actions/github-script@v7.0.1\n        with:\n          script: |\n            const script = require('./.github/workflows/scripts/closeOnRelease.js');\n            await script({core, github, context});\n"
  },
  {
    "path": ".github/workflows/scripts/closeOnRelease.js",
    "content": "/**\n * @param {Object} param\n * @param {import('@actions/core')} param.core\n * @param {ReturnType<import('@actions/github').getOctokit>} param.github\n * @param {import('@actions/github').context} param.context\n */\nmodule.exports = async ({ core, github, context }) => {\n  try {\n    const owner = context.repo.owner;\n    const repo = context.repo.repo;\n\n    const label = 'resolved pending release';\n    const resolvedLabel = 'resolved';\n\n    const issuesPendingRelease = (\n      await github.paginate(github.rest.issues.listForRepo, {\n        owner,\n        repo,\n        state: 'open',\n        per_page: 100,\n      })\n    ).filter((i) => i.pull_request === undefined && i.labels.map((l) => l.name).includes(label));\n\n    let failedIssues = 0;\n\n    for (const issue of issuesPendingRelease) {\n      const number = issue.number;\n\n      // slow down how often we send requests if there are lots of issues.\n      await new Promise((resolve) => setTimeout(resolve, 250));\n\n      const { data: releases } = await github.rest.repos.listReleases({\n        owner,\n        repo,\n      });\n      const release = releases.length > 0 ? releases[0] : undefined;\n\n      if (release === undefined) {\n        throw new Error('There is no release available');\n      }\n\n      const message = `:tada: This issue has been resolved and is now available in the [${release.tag_name}](${release.html_url}) release! :tada:`;\n\n      try {\n        // Remove the `resolved pending release` label.\n        await github.rest.issues.removeLabel({\n          owner,\n          repo,\n          issue_number: number,\n          name: label,\n        });\n\n        // Add the `resolved` label.\n        await github.rest.issues.addLabels({\n          owner,\n          repo,\n          issue_number: number,\n          labels: [resolvedLabel],\n        });\n\n        // Comment on the issue that we will close.\n        await github.rest.issues.createComment({\n          owner,\n          repo,\n          issue_number: number,\n          body: message,\n        });\n\n        // Close the issue.\n        await github.rest.issues.update({\n          owner,\n          repo,\n          issue_number: number,\n          state: 'closed',\n        });\n      } catch (error) {\n        console.error(`Failed to comment on and/or close issue #${number}`, error);\n        failedIssues++;\n      }\n\n      console.log(`Closed #${number}`);\n    }\n\n    if (failedIssues > 0) {\n      core.setFailed(`Failed to comment on ${failedIssues} PRs`);\n    }\n  } catch (error) {\n    console.error(error);\n    core.setFailed(error.message);\n  }\n};\n"
  },
  {
    "path": ".github/workflows/scripts/issue-automated.js",
    "content": "const { OpenAI } = require(\"openai\");\nconst { QueryAntVDocumentTool, ExtractAntVTopicTool }=  require('@antv/mcp-server-antv/build/tools');\n\n\n/**\n * @param {Object} param\n * @param {import('@actions/github').GitHub} param.github\n * @param {import('@actions/core')} param.core\n * @param {Object} param.context GitHub Action context\n */\nmodule.exports = async ({ github, core, context, issue }) => {\n  try {\n    core.info('开始处理 issue...', context.repo.repo);\n    const library = `g6`;\n    if (!issue) {\n      core.setFailed('找不到 issue 信息');\n      return;\n    }\n\n    const issueNumber = issue.number;\n    const issueTitle = issue.title;\n\n    core.info(`处理 issue #${issueNumber}: ${issueTitle}`);\n\n    const combinedQuery = prepareAIPrompt(context, issue);\n\n\n    const topicExtractionResult = await ExtractAntVTopicTool.run({ query: combinedQuery });\n\n    const aiResponse = await getAIResponse(core,  topicExtractionResult.content[0].text);\n    const jsonMatch = aiResponse.match(/```json\\s*(\\{[\\s\\S]*?\\})\\s*```/);\n    const processedTopicContent = JSON.parse(jsonMatch[1]);\n\n    const queryDocumentParams = {\n        library,\n        query: combinedQuery,\n        topic: processedTopicContent.topic,\n        intent: processedTopicContent.intent,\n        tokens: 5000,\n        ...(processedTopicContent.subTasks && { subTasks: processedTopicContent.subTasks }),\n      };\n\n      const documentationResult = await QueryAntVDocumentTool.run(queryDocumentParams);\n\n      const response = await getAIResponse(core,  documentationResult.content[0].text);\n\n        await github.rest.issues.createComment({\n            issue_number: issue.number,\n            owner: context.repo.owner,\n            repo: context.repo.repo,\n            body: `@${issue.user.login} 您好！以下是关于您问题的自动回复：\\n\\n${response}\\n\\n---\\n*此回复由 AI 助手自动生成。如有任何问题，我们的团队会尽快跟进。*`\n        });\n\n    core.info('Issue 处理完成');\n\n  } catch (error) {\n    core.setFailed(`处理 issue 失败: ${error.message}`);\n    core.error(error.stack);\n  }\n};\n\nfunction prepareAIPrompt(context, issue) {\n    return `\n    你是 ${context.repo.repo} 项目的智能助手。这是一个处理 GitHub issue 的自动回复系统。\n    请分析以下 issue 并提供专业、有帮助的回复。\n\n    ## 当前 Issue\n    - 标题: ${issue.title}\n    - 内容: ${issue.body}\n\n    请提供完整、有帮助的回复，但不要过于冗长。回复应该条理清晰，使用适当的 Markdown 格式。\n`;\n}\n\n/**\n * 调用 GitHub AI API 获取回复\n */\nasync function getAIResponse(core, userQuestion) {\n  try {\n    core.info('正在调用 GitHub AI API...');\n\n    const token = process.env.GH_TOKEN;\n\n    if (!token) {\n      throw new Error('未找到 GH_TOKEN 环境变量');\n    }\n\n    const endpoint = \"https://models.github.ai/inference\";\n    const model = \"openai/gpt-4.1\";\n\n    const client = new OpenAI({\n      baseURL: endpoint,\n      apiKey: token\n    });\n\n    const response = await client.chat.completions.create({\n      messages: [\n        { role: \"user\", content: userQuestion }\n      ],\n      temperature: 0.7,\n      top_p: 1.0,\n      model: model\n    });\n\n    core.info('成功获取 AI 响应');\n    core.info(JSON.stringify(response));\n    return response.choices[0].message.content;\n\n  } catch (error) {\n    core.warning(`调用 GitHub AI API 失败: ${error.message}`);\n    // 默认回复\n    return `\n    感谢您提交这个 issue！\n\n    我们的团队会尽快查看您的问题。为了帮助我们更快地解决，请确保您提供了:\n\n    - 问题的详细描述\n    - 复现步骤 (如果是 bug)\n    - 预期行为和实际行为\n    - 使用的版本信息\n\n    谢谢您的理解与支持！\n`;\n  }\n}\n"
  },
  {
    "path": ".github/workflows/scripts/updateYuque.js",
    "content": "const fs = require('fs');\nconst path = require('path');\n\n/**\n * @param {Object} param\n * @param {import('@actions/core')} param.core\n * @param {import('@actions/core').InputOptions} param.inputs\n */\nmodule.exports = async ({ core, inputs }) => {\n  try {\n    const API_BASE = 'https://www.yuque.com/api/v2';\n    const group_login = 'antv'; // 知识库所属组织\n    const { token, book_slug, site_slug } = inputs;\n\n    // 存储创建的文档ID，用于后续更新目录\n    const createdDocIds = {\n      tutorials: [],\n      examples: [],\n    };\n\n    core.info('开始更新语雀文档...');\n\n    // 删除知识库中的所有文档\n    async function clearAllDocs() {\n      core.info('获取知识库文档列表...');\n\n      try {\n        let allDocs = [];\n        let offset = 0;\n        const limit = 100; // 语雀API每页最大条数\n        let hasMore = true;\n\n        // 循环获取所有文档\n        while (hasMore) {\n          core.info(`获取文档列表，偏移量: ${offset}, 数量: ${limit}...`);\n\n          const response = await fetch(\n            `${API_BASE}/repos/${group_login}/${book_slug}/docs?offset=${offset}&limit=${limit}`,\n            {\n              method: 'GET',\n              headers: {\n                'Content-Type': 'application/json',\n                'X-Auth-Token': token,\n              },\n            },\n          );\n\n          if (!response.ok) {\n            throw new Error(`获取文档列表失败: ${response.statusText}`);\n          }\n\n          const data = await response.json();\n          const docs = data.data;\n\n          if (docs && docs.length > 0) {\n            allDocs = allDocs.concat(docs);\n            offset += docs.length;\n\n            // 如果返回的文档数量小于请求的限制，说明已经没有更多文档了\n            if (docs.length < limit) {\n              hasMore = false;\n            }\n          } else {\n            hasMore = false;\n          }\n        }\n\n        core.info(`共找到 ${allDocs.length} 个文档，准备删除...`);\n\n        // 删除所有文档\n        for (const doc of allDocs) {\n          core.info(`删除文档: ${doc.title} (${doc.id})...`);\n\n          const deleteResponse = await fetch(`${API_BASE}/repos/${group_login}/${book_slug}/docs/${doc.id}`, {\n            method: 'DELETE',\n            headers: {\n              'Content-Type': 'application/json',\n              'X-Auth-Token': token,\n            },\n          });\n\n          if (!deleteResponse.ok) {\n            core.warning(`删除文档 ${doc.title} 失败: ${deleteResponse.statusText}`);\n          } else {\n            core.info(`已删除文档: ${doc.title}`);\n          }\n        }\n\n        core.info('所有文档已删除');\n      } catch (error) {\n        core.error('删除文档过程中出错:' + error.message);\n        throw error;\n      }\n    }\n\n    // 创建单个文档\n    async function createDoc(title, body, type) {\n      try {\n        core.info(`创建文档: ${title}...`);\n\n        const response = await fetch(`${API_BASE}/repos/${group_login}/${book_slug}/docs`, {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n            'X-Auth-Token': token,\n          },\n          body: JSON.stringify({\n            title: title,\n            public: 1,\n            format: 'lake',\n            body: body,\n          }),\n        });\n\n        if (!response.ok) {\n          throw new Error(`创建文档失败: ${response.statusText}`);\n        }\n\n        const data = await response.json();\n        core.info(`文档已创建: ${title} (ID: ${data.data.id})`);\n\n        // 存储文档ID用于后续更新目录\n        if (type === 'tutorial') {\n          createdDocIds.tutorials.push(data.data.id);\n        } else {\n          createdDocIds.examples.push(data.data.id);\n        }\n\n        return data.data.id;\n      } catch (error) {\n        core.error(`创建文档 ${title} 时出错: ${error.message}`);\n        return null;\n      }\n    }\n\n    // 更新知识库目录\n    async function updateToc() {\n      try {\n        core.info('更新知识库目录...');\n\n        const response = await fetch(`${API_BASE}/repos/${group_login}/${book_slug}/toc`, {\n          method: 'PUT',\n          headers: {\n            'Content-Type': 'application/json',\n            'X-Auth-Token': token,\n          },\n          body: JSON.stringify({\n            action: 'appendNode',\n            action_mode: 'child',\n            doc_ids: [...createdDocIds.tutorials, ...createdDocIds.examples],\n            type: 'DOC',\n          }),\n        });\n\n        if (!response.ok) {\n          throw new Error(`更新目录失败: ${response.statusText}`);\n        }\n\n        core.info('知识库目录已更新');\n      } catch (error) {\n        core.error('更新目录时出错: ' + error.message);\n        throw error;\n      }\n    }\n\n    // 递归获取所有 .zh.md 文件并创建文档\n    async function processMarkdownFiles() {\n      core.info('处理Markdown文档...');\n\n      function getAllMarkdownFiles(dir) {\n        let results = [];\n\n        if (!fs.existsSync(dir)) {\n          core.warning(`目录不存在: ${dir}`);\n          return results;\n        }\n\n        const list = fs.readdirSync(dir);\n        list.forEach((file) => {\n          const filePath = path.join(dir, file);\n          const stat = fs.statSync(filePath);\n          if (stat && stat.isDirectory()) {\n            results = results.concat(getAllMarkdownFiles(filePath));\n          } else if (file.endsWith('.zh.md')) {\n            results.push(filePath);\n          }\n        });\n        return results;\n      }\n\n      try {\n        // 获取文档目录\n        const docsDir = path.join(process.cwd(), site_slug, 'docs');\n        core.info(`搜索文档目录: ${docsDir}`);\n\n        const files = getAllMarkdownFiles(docsDir);\n        core.info(`找到 ${files.length} 个中文 Markdown 文件`);\n\n        // 为每个文件创建文档\n        for (const file of files) {\n          let content = fs.readFileSync(file, 'utf-8');\n          const fileName = path.basename(file, '.zh.md');\n          const title = `教程-${fileName}`;\n\n          await createDoc(title, content, 'tutorial');\n        }\n      } catch (error) {\n        core.error('处理 Markdown 文件时出错: ' + error.message);\n        throw error;\n      }\n    }\n\n    // 处理示例代码文件\n    async function processExampleFiles() {\n      core.info('处理示例代码...');\n\n      function traverseDirectory(dir) {\n        const metaFiles = [];\n\n        if (!fs.existsSync(dir)) {\n          core.warning(`目录不存在: ${dir}`);\n          return metaFiles;\n        }\n\n        function findMetaFiles(directory) {\n          try {\n            fs.readdirSync(directory, { withFileTypes: true }).forEach((dirent) => {\n              const fullPath = path.join(directory, dirent.name);\n              if (dirent.isDirectory()) {\n                findMetaFiles(fullPath);\n              } else if (dirent.name === 'meta.json') {\n                metaFiles.push(fullPath);\n              }\n            });\n          } catch (err) {\n            core.warning(`读取目录 ${directory} 时出错: ${err.message}`);\n          }\n        }\n\n        findMetaFiles(dir);\n        return metaFiles;\n      }\n\n      async function processMetaJson(metaFilePath) {\n        try {\n          const dir = path.dirname(metaFilePath);\n          const folderName = path.basename(dir);\n          const metaContent = fs.readFileSync(metaFilePath, 'utf-8');\n          const metaJson = JSON.parse(metaContent);\n\n          if (Array.isArray(metaJson.demos)) {\n            for (const demo of metaJson.demos) {\n              const demoFilePath = path.join(dir, demo.filename);\n              if (fs.existsSync(demoFilePath) && fs.statSync(demoFilePath).isFile()) {\n                await processDemoFile(demoFilePath, demo.title.zh, folderName);\n              }\n            }\n          }\n        } catch (error) {\n          core.error(`处理 ${metaFilePath} 时出错: ${error.message}`);\n        }\n      }\n\n      async function processDemoFile(filePath, title, category) {\n        try {\n          let content = fs.readFileSync(filePath, 'utf-8');\n          // 移除HTML标签\n          content = removeHtmlTags(content);\n\n          const docTitle = `${category}-${title}`;\n          await createDoc(docTitle, `// ${title}\\n${content}`, 'example');\n        } catch (error) {\n          core.error(`处理 ${filePath} 时出错: ${error.message}`);\n        }\n      }\n\n      // 移除HTML标签的函数\n      function removeHtmlTags(code) {\n        // 找到模板字符串中的HTML标签\n        const templateStringRegex = /`([\\s\\S]*?)`/g;\n\n        return code.replace(templateStringRegex, (match, templateContent) => {\n          // 移除模板字符串中的HTML标签\n          const cleanTemplate = templateContent\n            .replace(/<[^>]*>[\\s\\S]*?<\\/[^>]*>/g, '')\n            .replace(/<[^>]*\\/>/g, '') // 移除自闭合标签\n            .replace(/<[^>]*>/g, ''); // 移除单个开放标签\n\n          return '`' + cleanTemplate + '`';\n        });\n      }\n\n      try {\n        const rootDir = process.cwd();\n        core.info(`搜索示例文件，根目录: ${rootDir}`);\n\n        const metaFiles = traverseDirectory(rootDir);\n        core.info(`找到 ${metaFiles.length} 个 meta.json 文件`);\n\n        for (const metaFile of metaFiles) {\n          await processMetaJson(metaFile);\n        }\n      } catch (error) {\n        core.error('处理示例文件时出错: ' + error.message);\n        throw error;\n      }\n    }\n\n    // 主执行函数\n    try {\n      core.startGroup('清除现有文档');\n      await clearAllDocs();\n      core.endGroup();\n\n      core.startGroup('处理示例文件');\n      await processExampleFiles();\n      core.endGroup();\n\n      core.startGroup('处理文档文件');\n      await processMarkdownFiles();\n      core.endGroup();\n\n      core.startGroup('更新目录');\n      await updateToc();\n      core.endGroup();\n\n      core.info('文档更新处理完成');\n    } catch (error) {\n      core.setFailed(`更新语雀文档失败: ${error.message}`);\n    }\n  } catch (error) {\n    core.setFailed(`脚本执行失败: ${error.message}`);\n  }\n};\n"
  },
  {
    "path": ".github/workflows/update-yuque.yml",
    "content": "name: Update Documentation on Yuque\n\non:\n  pull_request:\n    types: [closed]\n    branches:\n      - v5\n    paths:\n      - '**/*.md'\n\njobs:\n  check-and-update:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v3\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20\n\n      - name: Run Yuque update script\n        uses: actions/github-script@v7.0.1\n        with:\n          script: |\n            const script = require('./.github/workflows/scripts/updateYuque.js');\n            await script({\n              core,\n              inputs: {\n                token: '${{ secrets.YUQUE_TOKEN }}', // 语雀 Token\n                book_slug: 'osbmvn', // 外网语雀知识库\n                site_slug: 'packages/site', // 文档所在位置\n              }\n            });\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\nyarn.lock\npackage-lock.json\npnpm-lock.yaml\n\n# Sys\n.DS_Store\n.idea\n\n# Node\nnode_modules/\n.npmrc\n\n# Build\ndist\nlib\nesm\n\n# Test\ncoverage\n\n# Bundle visualizer\nstats.html\n\n# Tools\n.turbo\n**/tmp/\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\n\n# IDE\n.history/\n.lh/\n"
  },
  {
    "path": ".husky/commit-msg",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpx --no-install commitlint --edit \"$1\"\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nprotected_branches=\"master v5\"\ncurrent_branch=$(git rev-parse --abbrev-ref HEAD)\nfor branch in $protected_branches; do\n  if [ \"$current_branch\" == \"$branch\" ]; then\n    echo \"\\033[31mDirect commit to '$branch' branch are not allowed!\\033[0m\"\n    exit 1\n  fi\ndone\n\nnpx lint-staged\n"
  },
  {
    "path": ".prettierignore",
    "content": "dist\nes\nlib\nnode_modules"
  },
  {
    "path": ".prettierrc.js",
    "content": "module.exports = {\n  plugins: [\n    require.resolve('prettier-plugin-organize-imports'),\n    require.resolve('prettier-plugin-packagejson'),\n  ],\n  printWidth: 120,\n  proseWrap: 'never',\n  singleQuote: true,\n  trailingComma: 'all',\n  overrides: [\n    {\n      files: '*.md',\n      options: {\n        proseWrap: 'preserve',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"cSpell.words\": [\n    \"AABB\",\n    \"afteranimate\",\n    \"aftercanvasinit\",\n    \"afterdestroy\",\n    \"afterdraw\",\n    \"afterelementcreate\",\n    \"afterelementdestroy\",\n    \"afterelementstatechange\",\n    \"afterelementtranslate\",\n    \"afterelementupdate\",\n    \"afterlayout\",\n    \"afterrender\",\n    \"afterrendererchange\",\n    \"aftersizechange\",\n    \"afterstagelayout\",\n    \"aftertransform\",\n    \"afterviewportanimate\",\n    \"antv\",\n    \"autosize\",\n    \"batchend\",\n    \"batchstart\",\n    \"bbox\",\n    \"beforeanimate\",\n    \"beforecanvasinit\",\n    \"beforedestroy\",\n    \"beforedraw\",\n    \"beforeelementcreate\",\n    \"beforeelementdestroy\",\n    \"beforeelementstatechange\",\n    \"beforeelementtranslate\",\n    \"beforeelementupdate\",\n    \"beforelayout\",\n    \"beforerender\",\n    \"beforerendererchange\",\n    \"beforesizechange\",\n    \"beforestagelayout\",\n    \"beforetransform\",\n    \"beforeviewportanimate\",\n    \"betweenness\",\n    \"Bezier\",\n    \"bubblesets\",\n    \"cancelviewportanimate\",\n    \"convexhull\",\n    \"Dagre\",\n    \"dendrogram\",\n    \"elementstatechange\",\n    \"elementtranslate\",\n    \"elementvisibilitychange\",\n    \"Forceatlas\",\n    \"Fruchterman\",\n    \"Fullscreen\",\n    \"gforce\",\n    \"graphlib\",\n    \"GSHAPE\",\n    \"mindmap\",\n    \"onframe\",\n    \"pagerank\",\n    \"Phong\",\n    \"pinchend\",\n    \"pinchmove\",\n    \"pinchstart\",\n    \"pointset\",\n    \"Polyline\",\n    \"ranksep\",\n    \"Snapline\",\n    \"Timebar\"\n  ],\n  \"javascript.preferences.importModuleSpecifier\": \"relative\",\n  \"typescript.preferences.importModuleSpecifier\": \"relative\",\n  \"svg.preview.background\": \"transparent\"\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# ChangeLog\n\n### 4.8.0\n\n- fix: destroy graph and call layout problem, closes: #4126;\n- fix: remove duplicated event emit, closes: #4043;\n- fix: mousedown on other DOMs and mouseup on canvas, click is triggered unexpectly, closes: #2922;\n- fix: mousemove and mouseup are not triggered with drag and dragend, closes: #3086;\n- fix: replace DOMMouseScroll and mousewheel with wheel event, closes: #3256;\n- perf: refresh item when updateChild, updateChildren, addChild, removeChild for TreeGraph;\n\n### 4.7.17\n\n- fix: expandCombo and the edges of the children are not refreshed, closes: #3250;\n- fix: the item param of the afterremoveitem for combo should be data;\n- fix: add type to the parameter list of beforeremoveitem event;\n- fix: edge update with destroyed end items, closes: #3925;\n- perf: take the max value of padding array for circle combo, closes: #4113;\n- feat: support top-center for rect combo label position, closes: #3750;\n- feat: createCombo and uncombo support stack, closes: #3695, #3323;\n\n### 4.7.16\n\n- feat: allowDragOnItem config for scroll-canvas, closes: #3062;\n- feat: allow to setTextWaterMarker and setImageWaterMarker with an undefined parameter to remove the watermarker, closes: #3478;\n- feat: hideEdge config for minimap to enhance the performance, closes: #3158;\n- fix: minimap has incorrect shape zIndex with keyShape type and delegate type, closes: #3132;\n- fix: minimap viewport dragging problem in firefox and safari, closes: #2939;\n- docs: add sequence demo to site, closes: #3027;\n- perf: unify the formats of shouldBegin, shouldUpdate, and shouldEnd in behaviors, closes: #3028;\n- perf: fitView and fitCenter according to the corner ndoes insead of getCanvasBBox to avoid maximum call stack size exceeded, closes: #2447;\n- fix: treeGraph changeData with node properties lost, closes: #3215;\n- fix: error occurs while calling updateLayout from gpu layout to a cpu layout, closes: #3272;\n- fix: error occurs while calling changeData to remove a node in a combo, closes: #3293;\n\n### 4.7.15\n\n- fix: dagre layout for collapsed combos;\n- perf: give layout algorithm vedges;\n\n### 4.7.14\n\n- fix: error occurs while dragging combo with drag-node behavior;\n\n### 4.7.13\n\n- fix: unexpected move with fitCenter with animation;\n- fix: update modelRect with rendering error, closes: #4041;\n\n### 4.7.12\n\n- fix: drag-canvas incorrectly stopped by right click;\n- fix: createCombo with nodes which already has parent combos;\n- fix: setItemState on node, related edges's linking positions are not refreshed;\n- perf: combo animate inherit from graph's animate config;\n- perf: improve the performance of setItemState and active-relations again;\n- feat: graph supports optimizeThreshold to control the number threshold of nodes to enable the optimization on rendering and interaction, currently only affects the edges' refresh while the related node state style changed;\n\n### 4.7.11\n\n- perf: improve the performance of setItemState and active-relations;\n- perf: keyShape is hiden when a combo is collapsed with collapsedSubstituIcon;\n- fix: drag-node incorrectly stopped by right click;\n- fix: timebar plugin destroy problem, closes: #3998;\n- fix: controllerCfg does not take effect in timebar with tick type, closes: #3843;\n- feat: timebar plugin supports config the default time type;\n- feat: timebar with play and pause API;\n- chore: use addItem and removeItem instead of changeData in timebar;\n\n### 4.7.10\n\n- perf: force layout with animation calls graph refreshPositions instead positionsAnimate while refreshing;\n\n### 4.7.9\n\n- perf: init node positions when the node has no x and y in the origin data;\n\n### 4.7.8\n\n- feat: pointPadding config for loop edges with non-circle nodes, closes: #3974;\n- fix: image lost while updating the size for an image node, closes: #3938;\n\n### 4.7.7\n\n- feat: getContentPlaceholder and getTitlePlaceholder for Annotation plugin;\n\n### 4.7.6\n\n- fix: Annotation readData with inexistent item;\n- perf: improve the performance for updating;\n\n### 4.7.5\n\n- perf: Annotation support updating positions for outside cards by calling updateOutsideCards;\n\n### 4.7.4\n\n- perf: Annotation min-width and input width;\n\n### 4.7.3\n\n- feat: beforechangedata and afterchagnedata for graph changeData;\n- feat: Annotation supports icon events callbacks;\n- feat: Annotation supports defaultBegin position configuration for new annotation cards;\n- perf: Annotation updated automatically when graph data changed and graph item visibility changed;\n- fix: Destroy legend canvas when the plugin is destroyed, closes: #3931;\n\n### 4.7.2\n\n- feat: Annotation plugin supports configuring behaviors for collapse and close icon;\n- feat: Annotation plugin supports canvas annotation;\n- fix: gForce layout has animation by default;\n- fix: createCombo creates vedges asynchronously, closes: #3912;\n- fix: strange polyline path edge related to combo, closes: #3913;\n\n#### 4.7.1\n\n- feat: Annotation plugin;\n- fix: combo and drag-node with heap maximum problem, closes: #3886;\n- fix: combo and graph re-read problem, closes: 3902;\n- fix: d3 force layout with default animate;\n- perf: bundling plugin ts problem, closes: #3904;\n\n#### 4.7.0\n\n- fix: combo collapsed edge problems, closes: #3839;\n\n#### 4.7.0-beta\n\n- feat: force2 from graphin-force;\n- feat: preset for layout;\n- feat: tweak incremental layout init for force like layouts;\n\n#### 4.6.18\n\n- feat: updateLayout from no pipes to pipes, closes: #3726;\n- fix: relayout with pipes;\n\n#### 4.6.17\n\n- fix: legend changeData problem, closes: #3561;\n- fix: redo and undo with an image node, closes: #3782;\n- fix: call refreshPositions instead of positionsAnimate while there is no layout configuration;\n\n#### 4.6.16\n\n- feat: ID check;\n- feat: fitView with animation;\n- feat: findAllByState with additional filter;\n- fix: wrong dropped position for drag-combo with enableDelegate, closes: #3810;\n- fix: stack for drag-combo with onlyChangeComboSize, closes: #3801;\n- fix: stack updateLayout, closes: #3765;\n- fix: drag-canvas and zoom-canvas with enableOptimize show a hidden shape which is controlled by state, closes: #3635;\n- fix: typing problem for react node;\n\n#### 4.6.15\n\n- fix: fitView does not zoom the graph with animate true;\n\n#### 4.6.14\n\n- perf: optimize the performance of combo graph;\n\n#### 4.6.12\n\n- perf: optimize the performance of combo graph first rendering;\n\n#### 4.6.11\n\n- fix: star node with leftBottom linkPoint show and hide problem;\n- fix: relayout does not execute onAllLayoutEnd problem;\n- fix: combo edge state update problem, closes: #3639;\n\n#### 4.6.10\n\n- feat: maxLength for labelCfg;\n- fix: custom layout warning and layout failed problem;\n- fix: upgrade layout to fix DagreLayoutOptions type error;\n- fix: upgrade layout to fix comboCombined with original node infomations problem;\n\n#### 4.6.8\n\n- fix: spelling error for 'nodeselectChange', closes: #3736;\n- fix: update node icon from show false to show true;\n- fix: afterrender should be emitted when the layout is not configured;\n- perf: update related edges while drag-combo;\n- feat: combo supports collapsedSubstituteIcon showing after collapsed;\n- feat: remove animations while first rendering with (collapsed)combos;\n- refactor: toolbar plugin functions;\n\n#### 4.6.6\n\n- fix: destroyLayout error, closes: #3727;\n- fix: drag combo with stack problem, closes: #3699;\n- fix: updateLayout does not take effect if update layout with same type as graph instance configuration, closes: #3706;\n- fix: legendStateStyles typo, closes: #3705;\n- perf: zoom-canvas take the maximum and minimum values instead of return directly;\n- perf: minimap cursor move;\n- feat: fitView and fitCenter with animation;\n- feat: addItems to add multiple items into graph in the same time;\n- feat: enable edge selection in click-select;\n\n#### 4.6.4\n\n- chore: improve the types of graph events;\n- fix: position animate considers origin attributes;\n\n#### 4.6.3\n\n- feat: shouldDeselect param for lasso-select;\n- fix: initial collapsed combos with unexpected size;\n\n#### 4.6.1\n\n- fix: layoutController is null problem;\n\n#### 4.6.0-beta\n\n- feat: comboCombined Layout from @antv/layout;\n- feat: combo supports position configurations for any situations;\n- fix: run layout promise only when the layout is configured;\n\n#### 4.5.5\n\n- fix: tooltip with wrong duplicated child DOM nodes;\n\n#### 4.5.4\n\n- feat: tooltip plugin supports dynamic dom configurations;\n- feat: context menu plugin supports mobile touch event;\n- feat: allow enabling stack operations at runtime;\n- fix: use origin data when changeData without data param, closes: #3459;\n- feat: shouldBegin for canvas click in click-select behavior;\n\n#### 4.5.3\n\n- fix: import G6 in head and call getComputedStyle, the document body is not exist;\n\n#### 4.5.2\n\n- fix: node update from no icon to iconfont icon failed;\n- fix: getUpdateType with type error;\n- fix: edge label background with clearItemStates problem;\n- fix: edge label with autoRotate false and padding problem;\n- fix: changeData in the process of create-edge behavior, an error occurs, closes: #3384;\n- fix: node update from no icon to iconfont icon failed;\n\n#### 4.5.1\n\n- feat: translate graph with animation;\n- feat: zoom graph with animation;\n- feat: timebar supports filterItemTypes to configure the types of the graph items to be filtered; only nodes can be filtered before;\n- feat: timebar supports to configure the rotate of the tick labels by tickLabelStyle[dot]rotate;\n- feat: timebar supports container CSS configuration by containerCSS;\n- feat: timebar supports a function getDate to returns the date value according to each node or edge by user;\n- feat: timebar supports afunction getValue to returns the value (for trend line of trend timebar) according to each node or edge by user;\n- feat: timebar supports to configure a boolean changeData to control the filter way, true means filters by graph[dot]changeData, false means filters by graph[dot]showItem and graph[dot]hideItem;\n- feat: timebar supports to configure a function shouldIgnore to return true or false by user to decide whether the node or the edge should be ignored while filtering;\n- fix: simple timebar silder text position strategy and expand the lineAppendWidth for the slider;\n- fix: edge label padding bug, closes: #3346;\n- fix: update node with iconfont icon, the icon is updated to a wrong position, closes: #3348;\n\n#### 4.5.0\n\n- fix: add item type to the parameter of afterremoveitem event;\n\n#### 4.4.1\n\n- feat: zoom with animation, contributed by @Blakko;\n\n#### 4.4.0-beta.1\n\n- fix: drag-combo and drag-node with wrongly calling shouldUpdate;\n\n#### 4.4.0-beta.0\n\n- feat: better performance for item drawing;\n- fix: disable the capture of hull shape to enhance the performance of dragging canvas with hulls;\n- fix: uncombo an empty combo, fix: #3248;\n- fix: upgrade layout to beta 5 to solve proxy problem for IE;\n\n#### 4.3.11\n\n#### 4.3.9\n\n- fix: update edge to be horizontal and the label is on wrong position for min file;\n\n#### 4.3.9\n\n- fix: addBehavior with behavior string name, closes: #3020;\n- fix: drag-node shouldEnd does not stop the dragging node behavior, closes: #3173;\n- fix: drag-combo fails to merge combo with enableDelegate, closes: #3137;\n- fix: uncombo does not trigger afterremoveitem event, closes: #3179;\n- fix: error label background position when the edge label has position start, closes: #3129;\n- fix: destroyed graph judgement, closes: #3203;\n- fix: edge click event will not be triggered when the contextmenu is configure with trigger click, closes: #3201;\n- feat: drag-combo with shouldEnd, closes: #3202;\n- chore: information for failing to download image, closes: #2980;\n\n#### 4.3.7\n\n- fix: update edge to be horizontal and the label is on wrong position;\n\n#### 4.3.6\n\n- fix: drag-node on mobile, closes: #3127;\n- fix: removeBehaviors drag-canvas cause canvas:drag event cannot be listened;\n- fix: drag-node with unexpected offseted edge end points, closes: #3118;\n- fix: delete node with combo, closes: #3141;\n- fix: update node position with wrong position;\n- feat: enableStack for drag-node behavior, closes: #3128;\n\n#### 4.3.5\n\n- fix: drag a node without comboId by drag-node with onlyChangeComboSize;\n- fix: gpu layout with async;\n- fix: minimap with delegate type cannot reach the top of the canvas, closes: #2885;\n- feat: improve the performance for updating nodes;\n- feat: updateLayout with align and alignPoint;\n\n#### 4.3.4\n\n- fix: when select a node with click-select, selected combos should be deselected;\n- fix: contextmenu with click trigger does not show the menu up, closes: #2982;\n- fix: layout with collapsed combo, closes: #2988;\n- fix: zoom-canvas with optimizeZoom, drag-canvas shows the node shapes hiden by zoom-canvas optimizeZoom, closes: #2996;\n\n#### 4.3.3\n\n- fix: uncombo with id, closes: #2924;\n- fix: image node with state changing, closes: #2923;\n- fix: mouseentering tooltip DOM hides the DOM;\n- feat: moveTo with animate, closes: #2252;\n\n#### 4.3.2\n\n- fix: upgrade the layout package to 0.1.14 to solve the different results from gpu and cpu problem in gForce layout, closes: #2902;\n- fix: auto fitting container without width and height for graph problem, closes: #2901;\n- fix: minimap with zoomingproblem, closes: #2863\n- feat: fx and fy for fruchterman and gForce layout in both gpu and cpu version;\n- feat: barWidth for interval bar chart for TimeBar plugin, closes: #2989;\n- feat: click trigger for context munu, closes: #2686;\n\n#### 4.3.0\n\n- fix: empty object for TreeGraph data;\n- fix: combo edge arrow error with state styles;\n- fix: depth problem for addItem with comboId, closes: #2888;\n- feat: focus edge item;\n- feat: legend plugin;\n- feat: allow to new a tree layout independently;\n\n#### 4.2.7\n\n- fix: edges disappear when collapsing combo, closes: #2816;\n- fix: drag-node with edge key, closes: #2819;\n- fix: failed to update startArrow to be false, closes #2814;\n- fix: createCombo and add combId or parentId to the related nodes or combos, closes #2815;\n- feat: no animation when first rendering with collapsed combos, closes: #2826;\n\n#### 4.2.6\n\n- feat: scroll-canvas behavior;\n- feat: iconfont for node icon;\n- feat: percentage of scalable range for drag-canvas;\n- fix: missing brushStyle in type ModeOption;\n- fix: the comboId remains in the node after uncombo(), closes #2801;\n- fix: disappearing edges when combos are expanded/collapsed, closes #2798;\n- fix: invisible nodes and edges should not be selected by brush-select and lasso-select, closes #2810;\n\n#### 4.2.5\n\n- feat: donut node;\n- feat: downloadImage with watermarker;\n- fix: multiple layout calling error;\n- fix: combo collapse and related edges diappearing;\n- fix: forceAtlas2 with descrete node error;\n\n#### 4.2.4\n\n- fix: change data with dulplicated name between nodes and combos;\n- fix: pixelRatio for graph types;\n\n#### 4.2.3\n\n- fix: layout with fitView;\n\n#### 4.2.2\n\n- feat: pipe layouts for subgraphs;\n\n#### 4.2.1\n\n- fix: circle combo edge linking position problem;\n- fix: drag minimap viewport with forbidden icon in chrome on windows;\n- fix: show node without node position problem;\n- fix: addItem and getNodeDgree with wrong result problem;\n- fix: timebar data filtering problem;\n- fix: update endArrow to be false and set state problem;\n- feat: pass comb and comboEdge data for layout;\n- feat: tooltip with fixToItem to avoid following the mouse when moving;\n- feat: getViewPortCenterPoint and getGraphCenterPoint API;\n- feat: tooltip with trigger configuration, supports mouseenter and click;\n\n#### 4.2.0\n\n#### 4.1.14\n\n- fix: combo edge link position problem;\n- fix: activate-relations with combo and combo edges problem;\n- feat: support config TimeBar handler, background, foreground, tick label, tick line style;\n\n#### 4.1.16\n\n- fix: webworker in dist;\n\n#### 4.1.15\n\n- fix: cubic-x problem, closes: #2698;\n\n#### 4.1.14\n\n- fix: gridSize for polyline;\n- fix: create-edge undo problem;\n- fix: tslib spreadArray problem;\n- fix: rect combo position with state problem;\n- feat: simple polyline for better performance;\n- fix: gridSize for polyline;\n- fix: cubic-x problem, closes: #2698;\n- fix: create-edge undo problem;\n- fix: tslib spreadArray problem;\n- fix: rect combo position with state problem;\n- feat: simple polyline for better performance;\n\n#### 4.1.13\n\n- fix: getHulls with error type;\n- fix: createHull with destroyed hullMap problem;\n- fix: refining TimeBar minor problems;\n- fix: tooltip with display none to avoid enlarging graph container;\n- feat: TimeBar supports controller style configuration;\n- feat: TimeBar supports filtering edges;\n- feat: dagre with nested combo;\n\n#### 4.1.13-beta\n\n- chore: update layout and register in G6;\n- fix: performance problem in create-edge with polyline;\n- fix: performance for polyline;\n- fix: debounce updating the polyline edges in drag-node behavior;\n- fix: toolbar redo undo max clone in drag-node behavior;\n- feat: dagre layout with combo;\n- feat: cubic-vertical and cubic-horizontal with curveOffset and minCurveOffset\n\n#### 4.1.12\n\n- chore: update layout with alpha gwebgpu;\n- chore: update algorithm with fixed publicPath problem;\n\n#### 4.1.11\n\n- chore: link correct core;\n\n#### 4.1.10\n\n- chore: update algorithm;\n\n#### 4.1.9\n\n- feat: allowDragOnItem for drag-canvas behavior;\n- fix: drag-canvas with two fingers on mobile affects zoom-canvas;\n\n#### 4.1.8\n\n- fix: shouldBegin false for zoom-canvas behavior;\n- fix: shouldBegin originScale from graph zoom;\n- fix: error in collapse-expand with touch on canvas;\n\n#### 4.1.7\n\n- fix: polyline with negative endpoints;\n- fix: polyline direction when linkCenter;\n- fix: remove g6-core browser since it has no umd output;\n- feat: custom texts for the time range and time point text in timeBar plugin;\n- chore: types for strict mode;\n\n#### 4.1.6\n\n- fix: webworker problem after removing broswer in pc and g6;\n\n#### 4.1.5\n\n- fix: wrong style for modelRect after updating and state changing, closes: #2613;\n- fix: drag-canvas with shouldBegin false, closes: #2571;\n- fix: pack plugin with es module, closes: #2577;\n- feat: dijkstra with multiple shortest paths, closes: #2297;\n- fix: setMode while the delegates of brush-select and drag-node is on the canvas, closes: #2607;\n- docs: update the english TimeBar docs, closes: #2597;\n- fix: TimeBar time point switch text configurable, closes: #2597;\n\n#### 4.1.4\n\n- fix: drag-canvas with touch on mobile;\n\n#### 4.1.2\n\n- fix: registerBehavior export problem;\n- fix: shouldEnd of create-edge with groupByTypes as false;\n- fix: collapse and expand a combo with an empty sub combo error;\n- fix: update padding of rect combo;\n- fix: the graph in the minimap with circular layout is not centered, closes: #2555;\n- fix: edge background displays on a wrong place when autoRotate is true;\n\n#### 4.1.1\n\n- fix: soomth-convex hull with one line nodes leads to unshift problem;\n- fix: zoom-canvas to optimizeZoom and hide the label, the label will not show up any more problem;\n- fix: the ts type for parameter of timing event listener, closes: #2499;\n\n#### 4.1.0\n\n- chore: ts lint;\n- feat: getEdgeConfig for create-edge behavior;\n- fix: uniqueId with timestamp and random;\n- fix: fix zoom-canvas and drag-canvas with enableOptimize conflict problem shrink the settimeout;\n\n#### 4.1.0-beta.1\n\n- chore: unpack the g6 into core, pc, element, plugin, mobile, and exported by g6;\n- feat: layout with onLayoutEnd and custom layout with tag;\n- feat: emit beforecollapseexpandcombo and aftercollapseexpandcombo;\n- fix: toolbar for firefox and other browsers;\n- fix: edge label position with state problem;\n- fix: set item state to false at the first time;\n- fix: hull with one node;\n- fix: combo state size problem;\n- fix: state with fontSize changed problem;\n- fix: edge label with background when the two end nodes are overlapped;\n- fix: text rasidual of timebar;\n- fix: maximum stack size problem for image node type, fix: #2383;\n\n#### 4.0.3\n\n- fix: state style restore for non-circle shapes;\n\n#### 4.0.2\n\n- fix: node and edge state style with update problem;\n- fix: import lib problem;\n- fix: import node module problem;\n- fix: hidden shapes show up after zoom-canvas or drag-canvas with enableOptimize;\n- fix: tooltip for combo;\n- fix: update edge with false endArrow and startArrow;\n\n#### 4.0.1\n\n- fix: glslang problem;\n\n#### 4.0.0-beta.0\n\n- feat: fruchterman and gforce layout with gpu;\n- feat: gforce;\n- feat: updateChildren API for TreeGraph;\n- feat: louvain clustering algorithm;\n- feat: container of plugins with dom id;\n- feat: label propagation clustering algorithm;\n- feat: get color sets by subject color array;\n- feat: canvas context menu;\n- feat: stop gforce;\n- feat: dark rules for colors;\n- fix: text redidual problem, closes: #2045 #2193;\n- fix: graph on callback parameter type problem, closes: #2250;\n- fix: combo zIndex problem;\n- fix: webworker updateLayoutCfg problem;\n- fix: drag-canvas and click node on mobile;\n\n#### 3.8.5\n\n- fix: get fontFamily of the window in global leads to DOM depending when using bigfish;\n\n#### 3.8.4\n\n- feat: new version of basic styles for light version;\n- feat: shortcuts-call behavior for calling a Graph function by shortcuts;\n- feat: color generate util function getColorsWithSubjectColor;\n- fix: drag-canvas on mobile problem;\n- fix: style update problem with stateStyles in the options of registerNode;\n\n#### 3.8.3\n\n- feat: drag the viewport of the minimap out of the the view;\n- fix: extend modelRect with description problem, closes: #2235;\n\n#### 3.8.2\n\n- feat: graph.setImageWaterMarker, graph.setTextWaterMarker API;\n- feat: zoom-canvas support mobile;\n- fix: drag-canvas behavior support scalable range, closes: #2136;\n- fix: TreeGraph changeData clear all states, closes: #2173;\n- chore: auto zoom tooltip & contextMenu component when zoom-canvas;\n- chore: upgrade @antv/g-canvas;\n- feat: destroyLayout API for graph, closes: #2140;\n- feat: clustering for force layout, closes: #2196;\n- fix: svg renderer minimap hidden elements probem, closes: #2174;\n- feat: add extra parameter graph for menu plugin, closes: #2204;\n- fix: tooltip plugin, crossing different shape cant execute the getContent function, closes: #2153;\n- feat: add edgeConfig for create-edge behavior, closes: #2195;\n- fix: remove the source node while creat-edge;\n- feat: create-edge for combo, closes: #2211;\n- fix: update the typings for G6Event;\n\n#### 3.8.1\n\n- fix: update edge states with updateItem problem, closes: #2142;\n- fix: create-edge behavior with polyline problem, closes: #2165;\n- fix: console.warn show duplicate ID, closes: #2163;\n- feat: support the drag-canvas behavior on the mobile device, closes: #816;\n- chore: timeBar component docs;\n\n#### 3.8.0\n\n- fix: treeGraph render with addItem and stack problem, closes: #2084;\n- feat: G6 Interactive Document GraphMarker;\n- feat: registerNode with jsx support afterDraw and setState;\n- feat: edge filter lens plugin;\n- feat: timebar plugin;\n\n#### 3.7.3\n\n- fix: update G to fix the shape disappear when it has been dragged out of the view port problem, closes: #2078, #2030, #2007;\n- fix: redo undo with treeGraph problem;\n- fix: remove item with itemType problem, closes: #2096.\n\n#### 3.7.2\n\n- fix: toolbar redo undo addItem with type problem, closes #2043;\n- fix: optimized drag-canvas with hidden items;\n- fix: state style with 0 value problem, closes: #2039;\n- fix: layout with webworker leads to twice beforelayout, closes: #2052;\n- fix: context menu with sibling doms of graph container leads to position problem, closes: #2053;\n- fix: changeData with combos problem, closes: #2064;\n- fix: improve the position of the context menu before showing up;\n- feat: fisheye allows user to config the trigger of scaling range(r) and magnify factor(d) by scaleRBy and scaleDBy respectively;\n- feat: add the percent text of magnify factor(d) for fisheye and users are allowed to configure it by show showDPercent.\n\n#### 3.7.1\n\n- fix: hide the tooltip plugin when drag node and contextmenu, closes #1975;\n- fix: processParellelEdges without edge id problem;\n- fix: label background with left, right position problem, closes #1861;\n- fix: create-edge and redo undo problem, #1976;\n- fix: tooltip plugin with shouldBegin problem, closes #2006;\n- fix: tooltip behavior with shouldBegin problem, closes #2016;\n- fix: the position of grid plugins when there is something on the top of the canvas, closes: #2012;\n- fix: fisheye destroy and new problem, closes: #2018;\n- fix: node event with wrong canvasX and canvasY problem, closes: #2027;\n- fix: drag combo and drag node to drop on canvas/combo/node problem;\n- feat: improve the performance on the combos;\n- fix: redo and undo problem when update item after additem, closes #2019;\n- feat: hide shapes beside keyShape while zooming;\n- feat: improve the performance on the combos.\n\n#### 3.7.0\n\n- feat: chart node;\n- feat: bubble set;\n- feat: custom node with JSX;\n- feat: minimum spanning tree algorithm;\n- feat: path finding algorithm;\n- feat: cycle finding algorithm;\n- chore: update antv/hierarchy to fix indented tree with dropCap problem.\n\n#### 3.6.2\n\n- feat: find all paths and the shortest path between two nodes;\n- feat: fisheye with dragging;\n- feat: fisheye with scaling range and d;\n- feat: click and drag to create an edge by create-edge behavior;\n- feat: process multiple parallel edges to quadratic with proper curveOffset;\n- fix: polyline with rect and radius=0 problem;\n- fix: arrow state & linkpoint;\n- fix: the position of the tooltip plugin;\n- fix: drop a node onto a sub node of a combo;\n- chore: update hierarchy to solve the children ordering problem for indented tree layout;\n- chore: extract the public calculation to enhance the performance of fisheye.\n\n#### 3.6.1-beta\n\n- chore: update g-canvas to support quickHit and pruning the rendering of the graph outside the viewport;\n- feat: add statistical chart nodes;\n- feat: add hull for create smooth contour to include specific items;\n- fix: clear combos before render;\n- fix: menu plugin with clickHandler problem.\n\n#### 3.6.1\n\n- feat: image minimap;\n- feat: visible can be controlled in the data;\n- feat: item type for tooltip plugin;\n- feat: menu plugin with shouldUpdate;\n- fix: tooltip plugin position and hidden by removeItem;\n- fix: tooltip behavior hidden by removeItem;\n- fix: menu plugin with clicking on canvas problem;\n- fix: menu plugin with clickHandler problem;\n- fix: createCombo with double nodes problem.\n\n#### 3.6.0\n\n- feat: fisheye lens plugin;\n- feat: lasso-select behavior;\n- feat: TimeBar plugin;\n- feat: ToolBar plugin.\n\n#### 3.5.12\n\n- fix: node:click is triggered twice while clicking a node;\n- fix: update combo edge when drag node out of it problem;\n- feat: animate configuration for combo, true by default;\n- fix: calling canvas.on('\\*', ...) instead of origin way in event controller leads to malposition while dragging nodes with zoomed graph.\n\n#### 3.5.11\n\n- feat: graph.priorityState api;\n- feat: graph.on support name:event mode.\n- fix: combo edge with uncorrect end points;\n- fix: combo polyline edge with wrong path;\n- fix: getViewCenter with padding problem;\n- fix: cannot read property 'getModel' of null problem on contextmenu when the target is not an item;\n- feat: allow user to configure the initial positions for empty combos;\n- feat: optimize by hiding edges and shapes which are not keyShape while dragging canvas;\n- feat: fix the initial positions by equably distributing for layout to produce similar result.\n\n#### 3.5.10\n\n- fix: fitView and fitCenter with animate in the initial state;\n- fix: dulplicated edges in nodeselectchange event of brush-select;\n- fix: triple click and drag canvas problem;\n- fix: sync the minZoom and maxZoom in drag-canvas and graph;\n- fix: integrate getSourceNeighbors and getTargetNeighbors to getNeighbors(node, type);\n- feat: initial x and y for combo data;\n- feat: dagre layout supports sortByCombo;\n- feat: allow user to disable relayout in collapse-expand-combo behavior;\n- feat: dijkstra shortest path lenght algorithm.\n\n#### 3.5.9\n\n- fix: multiple animate update shape for combo;\n- fix: removeItem from a combo.\n\n#### 3.5.8\n\n- fix: combo edge problem, issues #1722;\n- feat: adjacency matrix algorithm;\n- feat: Floyd Warshall shortest path algorithm;\n- feat: built-in arrows;\n- feat: built-in markers;\n- fix: force layout with addItem and relayout;\n- fix: create combo with parentId problem;\n- feat: allow user to configure the pixelRatio for Canvas;\n- chore: update G to resolve the blur canvas problem.\n\n#### 3.5.7\n\n- feat: shouldBegin for click-select behavior;\n- feat: graph.getGroup, graph.getContainer, graph.getMinZoom, graph.setMinZoom, graph.getMaxZoom, graph.setMaxZoom, graph.getWidth, graph.getHeight API;\n- fix: combo edge dashLine attribute;\n- fix: combo collapse and expand with edges problem;\n- fix: destroy the tooltip DOMs when destroy the graph;\n- fix: unify the shape names for custom node and extended node;\n- fix: update the edges after first render with collapsed combos.\n\n#### 3.5.6\n\n- feat: dropCap for indented TreeGraph layout.\n\n#### 3.5.5\n\n- fix: custom node with setState problem;\n- fix: validationCombo in drag-combo and drag-node.\n\n#### 3.5.3\n\n- feat: focusItem with animation;\n- feat: generate the image url of the full graph by graph.toFullDataUrl;\n- fix: graph dispears after being dragged out of the canvas and back;\n- fix: the graph cannot be dragged back if it is already out of the view;\n- fix: size and radius of the linkPoints problem;\n- fix: combo graph with unused state name in comboStateStyles;\n- fix: preventDefault in drag-canvas behavior.\n\n#### 3.5.2\n\n- feat: degree algorithm;\n- feat: graph.getNodeDegree;\n- fix: downloadFullImage changes the matrix of the graph problem;\n- fix: circular layout modifies the origin data with infinite hierarchy problem.\n\n#### 3.5.1\n\n- feat: graph.fitCenter to align the graph center to canvas center;\n- fix: getType is not a function error occurs when addItem with point;\n- fix: checking comboTrees avaiability;\n- fix: error occurs when createCombo into the graph without any combos;\n- fix: endPoint and startPoint are missing in modelConfig type;\n- fix: edge background leads to empty canvas when the autoRotate is false;\n- fix: combo state style bug.\n\n#### 3.5.0\n\n- feat: combo and combo layout;\n- feat: graph algorithms: DFS, BFS and circle detection;\n- feat: add `getNeighbors`, `getSourceNeighbors`, `getTargetNeighbors` methods on Graph and Node;\n- feat: add `getID` method on Item;\n- fix: All Configuration type declarations are migrated to types folder, refer [here](https://github.com/antvis/G6/commit/3691cb51264df8529f75222147ac3f248b71f2f6?diff=unified#diff-76cf0eb5e3d8032945f1ac79ffc5e815R6);\n- fix: Some configuration type declarations have removed the `I` prefix, refer [here](https://github.com/antvis/G6/commit/3691cb51264df8529f75222147ac3f248b71f2f6?diff=unified#diff-aa582974831cee2972b8c96cfcce503aR16);\n- feat: Util.getLetterWidth and Util.getTextSize.\n\n#### 3.4.10\n\n- fix: TreeGraphData type with style and stateStyles;\n- fix: wrong controlpoint position for bezier curves with getControlPoint.\n\n#### 3.4.9\n\n- fix: transplie d3-force to support IE11.\n\n#### 3.4.8\n\n- feat: update the keyShape type minimap when the node or edge's style is updated;\n- fix: problem about switching to another applications or browser menu and then switch back, the drag-canvas does not take effect;\n- fix: fix the problem about fail to render the graph when the animate and fitView are true by turn off the animate for rendering temporary;\n- fix: curveOffset for arc, quadratic, cubic edge.\n\n#### 3.4.7\n\n- feat: downloadFullImage when the (part of) graph is out of the screen;\n- feat: With pre-graph has no layout configurations and no positions in data, calling changeData to change into a new data with positions, results in show the node with positions in data;\n- feat: allow user to assign curveOffset and curvePostion for Bezier curves;\n- fix: moveTo wrong logic problem;\n- fix: removeItem to update the minimap.\n\n#### 3.4.6\n\n- same as 3.4.5, published wrongly.\n\n#### 3.4.5\n\n- feat: background of the label on node or edge;\n- feat: better performance of minimap;\n- fix: minimap viewport displace problem;\n- feat: offset of tooltip;\n- fix: the length of the node's name affects the tree layout;\n- fix: toFront does not work for svg renderer;\n- fix: error occurs when the fontSize is smaller than 12 with svg renderer;\n- fix: changeData clears states;\n- fix: state does not work when default labelCfg is not assigned.\n\n#### 3.4.4\n\n- feat: background color for downloadImage and toDataURL;\n- feat: support configure image for grid plugin;\n- fix: initial position for fruchterman layout;\n- refactor: clip for image node.\n- fix: cubic with only one controlPoint error;\n- fix: polyline without L attributes.\n\n#### 3.4.3\n\n- fix: support extends BehaviorOption;\n- fix: click-select Behavior support multiple selection using ctrl key.\n\n#### 3.4.2\n\n- feat: zoom-canvas behavior supports hiding non-keyshape elements when scaling canvas;\n- refactor: when the second parameter is null, clearItemStates will clear all states of the item;\n- fix: [changeData bug](https://github.com/antvis/G6/issues/1323);\n- fix: update antv/hierarchy to fix fixedRoot for TreeGraph;\n- fix: problem of a graph has multiple polyline edges;\n- fix: problem of dagre with controlPoints and loop edges.\n\n#### 3.4.1\n\n- feat: force layout clone original data model to allow the customized properties;\n- fix: BehaviorOptions type error;\n- fix: fitView the graph with data whose nodes and edges are empty arrays;\n- fix: rect node positions are changed after calling graph.changeData;\n- fix: drag behavior is disabled when the keys are released invalidly;\n- refactor: update G and the fill of custom arrow should be assigned by user.\n\n#### 3.4.0\n\n- feat: SVG renderer;\n- refactor: new state mechanism with multiple values, sub graphics shape style settings.\n\n#### 3.3.7\n\n- feat: beforeaddchild and afteraddchild emit for TreeGraph;\n- feat: built-in nodes' labels can be captured;\n- fix: drag shadow caused by localRefresh, update the g-canvas version;\n- fix: abnormal polyline bendding;\n- fix: collapse-expand trigger problem;\n- fix: update nodes with empty string label;\n- fix: abnormal rendering when a graph has image nodes and other type nodes.\n\n#### 3.3.6\n\n- feat: support edge weight for dagre layout;\n- feat: automatically add draggable to keyShape, users do not need to assign it when custom a node or an edge;\n- fix: cannot read 0 or null problem in getPointByCanvas;\n- fix: brush-select bug;\n- fix: set autoDraw to canvas when graph's setAutoPaint is called;\n- fix: modify the usage of bbox in view controller since the interface is chagned by G;\n- fix: the shape.attr error in updateShapeStyle;\n- fix: local refresh influence on changeData;\n- refactor: upgrade g-canvas to 0.3.23 to solve lacking of removeChild function;\n- doc: update the demo fo custom behavior doc;\n- doc: add plugin demos and cases for site;\n- doc: fix shouldUpdate problem in treeWithLargeData demo on the site.\n\n#### 3.3.5\n\n- fix: 3.3.4 is not published successfully;\n\n#### 3.3.4\n\n- fix: 3.3.3 is not published successfully;\n- fix: delegate or keyShape type minimap does not display bug;\n- fix: dragging bug on minimap with a graph whose bbox is nagtive;\n- fix: null matrix bug, create a unit matrix for null.\n\n#### 3.3.3\n\n- fix: delegate or keyShape type minimap does not display bug;\n- fix: null matrix in focus() and getLoopCfgs() bug.\n\n#### 3.3.2\n\n- fix: ts type export problem;\n- fix: edge with endArrow and autoRotate label bug;\n- fix: code prettier;\n- fix: line with control points bug;\n- fix: matrix null bug.\n\n#### 3.3.1\n\n- fix: resolve 3.3.0 compatibility problem.\n\n#### 3.3.0\n\n- Graph API\n  - refactor: delete removeEvent function, use off;\n- refactor: parameters of Shape animate changed, shape.animate(toAttrs, animateCfg) or shape.animate(onFrame, animateCfg);\n- feat: descriptionCfg for modelRect to define the style of description by user;\n- feat: update a node from without some shapes to with them, such as linkPoints, label, logo icon and state icon for modelRect;\n- feat: the callback paramter of event nodeselectchange is changed to { target, selectedItems, ... };\n- feat: support stateStyles in node and edge data;\n- feat: calculate pixelRatio by G automatically, user do not need to assign it to graph instance;\n- chore: G 4.0\n- refactor: refreshLayout of TreeGraph is renamed as layout\n- fix: no fan shape in G any more\n- feat: recommand to assign name for each shape when addShape\n- fix: do not support SVG renderer anymore. no renderer for graph configuration anymore\n- refactor: plugins usage is changed into new G6.PluginName()\n\n#### 3.2.7\n\n- feat: supports create the group without nodes in node-group;\n- fix: supports destoryed properties and fix issue 1094;\n\n#### 3.2.6\n\n- feat: supports sort the nodes on one circle according to the data ordering or some attribute in radial layout\n- fix: grid layout with cols and rows\n- feat: fix the nodes with position information in their original data and random the positions of others when the layout is not defined for graph\n\n#### 3.2.5\n\n- fix: click-select trigger error\n- fix: solved position problem for minimap\n\n#### 3.2.4\n\n- fix: typescript compile error\n- fix: delete sankey lib\n\n#### 3.2.3\n\n- fix: group position error\n- fix: supports not set layout type\n\n#### 3.1.5\n\n- feat: supports g6 types file\n- fix: set brush-select trigger param to ctrl not work\n- fix: when set fitView to true, drag-group Behavior not get desired positon\n\n#### 3.1.3\n\n- feat: radial layout nonoverlap iterations can be controlled by user\n- feat: add lock, unlock and hasLocked function, supports lock and unlock node\n- fix: mds with discrete points problem\n- fix: fruchterman-group layout title position for rect groups\n\n#### 3.1.2\n\n- feat: default behavior supports configuration trigger mode\n- feat: node combining supports configuration title\n- fix: update demo state styles\n\n#### 3.1.1\n\n- fix: update node use custom config\n- fix: update demo\n- feat: default node implement getShapeStyle function\n\n#### 3.1.0\n\n- feat: support for rich layouts：random, radial, mds, circular, fruchterman, force, dagre\n- feat: more flexible configuration for shape\n- feat: build-in rich default nodes\n- feat: cases that provide layout and default nodes\n\n#### 3.0.7-beta.1\n\n`2019-09-11`\n\n- fix: zoom-canvas support IE and Firefox\n\n#### 3.0.6\n\n`2019-09-11`\n\n- fix: group data util function use module.exports\n- feat: update @antv/hierarchy version\n\n#### 3.0.5\n\n`2019-09-10`\n\n- feat: support add and remove group\n- feat: support collapse and expand group\n- feat: add graph api: collapseGroup and expandGroup\n\n#### 3.0.5-beta.12\n\n- feat: add rect group\n- feat: add rect group demo\n- feat: add chart node\n\n---\n\n#### 3.0.5-beta.10\n\n- feat: add 5 chart node\n- feat: collapse-expand tree support click and dblclick by trigger option\n- fix: drag group bug fix\n\n#### 3.0.5-beta.10\n\n- feat: support render group\n- feat: support drag group, collapse and expand group, drag node in/out group\n- feat: add drag-group、collapse-expand-group and drag-node-with-group behavior\n- feat: add drag-group and collapse-expand-group demo\n- feat: add register list node demo\n\n#### 3.0.5-beta.8\n\n`2019-07-19`\n\n- feat: add five demos\n- refactor: update three behaviors\n\n#### 2.2.5\n\n`2018-12-20`\n\n- feat: add saveimage limitRatio\n\n#### 2.2.4\n\n`2018-12-20`\n\n- fix: bug fix\n\n#### 2.2.3\n\n`2018-12-10`\n\n- fix: bug fix\n\n#### 2.2.2\n\n`2018-11-30`\n\n- fix: tree remove guide will not getEdges.closes #521\n\n#### 2.2.1\n\n`2018-11-25`\n\n- fix: Compatible with MOUSEWHEEL\n- fix: fadeIn aniamtion\n- fix: fix wheelZoom behaviour by removing the deprecated mousewheel event\n\n#### 2.2.0\n\n`2018-11-22`\n\n- fix: Graph read zIndex\n- refactor: Animation\n\n#### 2.1.5\n\n`2018-10-26`\n\n- fix: svg pixelRatio bug\n- feat: add wheel event\n\n#### 2.1.4\n\n`2018-10-06`\n\n- fix: custom math.sign to compatible with ie browser.Closes #516.\n- fix: legend component from @antv/component\n- feat: update svg minimap && fix svg dom event\n\n#### 2.1.3\n\n`2018-09-27`\n\n- feat: add label rotate\n- feat: if there is no items the graph box equal canvas size\n\n#### 2.1.2\n\n`2018-09-19`\n\n- fix: dom getShape bug.Closes #472\n- fix: template.maxSpanningForest bug\n\n#### 2.1.1\n\n`2018-09-17`\n\n- fix: tool.highlightSubgraph calculate box bug\n- fix: plugin.grid.Closes #479\n- chore(dev): upgrade babel & torchjs\n\n#### 2.1.0\n\n`2018-09-03`\n\n- feat: svg render\n- feat: plugin.layout.forceAtlas2\n- feat: plugin.tool.fisheye\n- feat: plugin.tool.textDisplay\n- feat: plugin.tool.grid\n- feat: plugin.template.tableSankey\n- feat: plugin.edge.polyline\n\n#### 2.0.5\n\n`2018-07-12`\n\n- improve: add g6 arrow\n\n#### 2.0.4\n\n`2018-07-12`\n\n- feat: layout export group.Closes #355\n- feat(plugin): add tool.tooltip. Closes #360.\n- style: change the calling way of forceAtlas2 on template.maxSpanningForest\n- fix: origin tree data collapsed is true tree edge visible bug.Closes #357\n- fix: remove the forceAtlas.js in template.maxSpanningForest, use forceAtlas from layout.forceAtlas2\n- fix: add demos: plugin-fisheye, plugin-forceAtlas2, gallery-graphanalyzer\n- fix: add demos: plugin-forceAtlas2, plugin-fisheye\n\n#### 2.0.3\n\n`2018-06-29`\n\n- feat: update g to 3.0.x. Closes #346\n- fix: group should use rect intersect box. Close #297\n- fix(plugin): dagre edge controlpoints remove start point and end point\n- style: remove some annotations\n- chore: update torchjs && improve demo name\n\n#### 2.0.2\n\n`2018-06-13`\n\n- chore(plugin): require g6 by src/index\n- chore(dev test): remove useless test script\n- fix(plugin) minimap destroy Closes #308\n- fix(saveImage) saveImage bug\n- fix(event): fix dom coord. Closes #305\n\n#### 2.0.1\n\n`2018-06-11`\n\n- fix: reDraw edge after layout\n- feat: add quadraticCurve config cpd\n- feat: add beforelayout && afterlayout event\n- chore: .travis.yml add add Node.js\n- chore: .travis.yml cache node_modules\n\n#### 2.0.0\n\n`2018-06-06`\n\n- refactor: refactor architecture && code\n\n#### 1.2.1\n\n`2018-03-15`\n\n- feat: layout interface\n\n#### 1.2.0\n\n`2018-01-15`\n\n- fix: nodeActivedBoxStyle spelling error\n- fix: error when deleting a circle\n- fix: trigger dragstart while right clicking and moveing\n- feat: Unify Layout mechanism\n- feat: Plugin mechanism\n- feat: Data filter mechanism\n- feat: Activated interface\n- feat: Action wheelZoomAutoLabel\n- feat: configuration of graph -- preciseAnchor\n- remove: Global.preciseAnchor\n- remove: Layout.Flow、Layout.Force\n- improve: html container strategy\n\n#### 1.1.6\n\n`2017-10-15`\n\n- fix: pack problem in layout algorithm\n\n#### 1.1.5\n\n`2017-09-15`\n\n- fix: dragCanvas is effective while mousemove, prevent it from affecting click events\n- fix: unactivate pick-up in activeRectBox of node\n\n#### 1.1.4\n\n`2017-08-15`\n\n- feat: graph.invertPoint()\n- feat: third configuration of anchor to support style setting, float style, connection\n- feat: item.getGroup()\n- feat: events -- afteritemrender、itemremove、itemadd\n- feat: behaviourSignal\n- improve: mouseWheel is affective after focusing the canvas\n\n#### 1.1.3\n\n`2017-08-8`\n\n- feat: Graph configuration -- useNodeSortGroup\n- feat: Global.nodeDelegationStyle, Global.edgeDelegationStyle, isolate the delegation of edge and node on graph\n- fix: itemremove is triggered before destroying a graph\n\n#### 1.1.2\n\n`2017-08-01`\n\n- feat: dragBlankX dragBlankY\n\n#### 1.1.1\n\n`2017-07-18`\n\n- improve: dragNode protect mechanism\n\n#### 1.1.0\n\n`2017-07-05`\n\n- feat: HTML node\n- feat: mapper support callback function\n- feat: Graph interfaces -- updateMatrix、changeSize、showAnchor、hideAnchor、updataNodesPosition\n- feat: tool functions -- Util.isNode()、Util.isEdge()\n- feat: Shape polyLineFlow\n- feat: dragEdgeEndHideAnchor、dragNodeEndHideAnchor、hoverAnchorSetActived、hoverNodeShowAnchor\n\n#### 1.0.7\n\n`2017-06-21`\n\n- fix: draw one more time in 16ms after first draw\n- improve: add zoom by scroll in edit mode\n\n#### 1.0.6\n\n`2017-06-15`\n\n- fix: compatible in chrome in windows. triggering mousemove after first click leads to wrong click event.\n- feat: support fix size graphics\n- feat: analysis mode\n- feat: updateNodesPositon update a set of nodes' position\n- improve: change useAnchor to be a configuration of edge\n\n#### 1.0.5\n\n`2017-06-01`\n\n- feat: downloadImage support saving with name\n- feat: automatically detect tooltip padding\n- improve: stop the action while mouse dragging out of the canvas\n\n#### 1.0.4\n\n`2017-05-20`\n\n- fix: tree changeData Bug\n- fix: when getAnchorPoints returns auto, anchor is the intersection of edge and the bounding box\n- fix: generate node label according to isNull\n- feat: viewport parameters -- tl、tc、tr、rc、br、bc、bl、lc、cc\n- improve: reduce tolerance to improve the accuracy of interception\n- improve: improve tooltip event mechanisom to enhance performance\n\n#### 1.0.3\n\n`2017-05-10`\n\n- feat: graph.guide().link()\n\n#### 1.0.2\n\n`2017-05-10`\n\n- fix: Object.values => Util.getObjectValues\n- fix: when anchorPoints is auto, there is only anchorpoint on edge, it will also return the intersection\n- fix: tree update interface Bug\n- improve: represent positions information by group.transfrom()\n\n#### 1.0.1\n\n`2017-04-22`\n\n- fix: copy and paste bug\n- feat: draw once in 16ms\n- feat: itemactived itemunactived itemhover itemupdate itemmouseenter itemmouseleave\n- improve: be clear the status of graphics before activating graphics by frame selection\n- improve: dragAddEdge, linkable to anchor\n- improve: performance of animation\n\n#### 1.0.0\n\n`2017-03-31`\n\n- feat: fitView configurations\n- feat: graph.zoom()\n- feat: wheelZoomHideEdges hide the edges while zooming by wheel\n- feat: dragHideEdges hide the edge while dragging edge\n- feat: graph.filterBehaviour()\n- feat: graph.addBehaviour()\n- feat: graph.changeLayout()\n- feat: read interface, re-define save interface\n- feat: graph.snapshot, graph.downloadImage\n- feat: graph.autoSize()\n- feat: graph.focusPoint()\n- feat: tree graph、net graph\n- feat: interaction mechanism -- event => action => mode\n- feat: animation mechanism\n- feat: itemmouseleave、itemmouseenter\n- remove: graph.refresh()\n- remove: graph.changeNodes()\n- remove: graph attributes -- zoomable、draggable、resizeable、selectable\n- improve: anchor mechanism\n- improve: hide G6.GraphUtil functions, unified in G6.Util\n- improve: replace g-canvas-core to g-canvas to improve performance\n- improve: Global.nodeAcitveBoxStyle instead of Global.nodeBoxStyle\n- improve: afterAdd => afteradd\n- improve: G6.Graph to be an abstract class\n\n#### 0.2.3\n\n`2017-03-2`\n\n- fix: draggable for controlling draggable under default mode\n- feat: graph.converPoint()\n- feat: graph.autoSize()\n- feat: rightmousedown leftmousedown wheeldown\n- improve: use try catch to prevent the length of getPoint of path equals zero\n\n#### 0.2.2\n\n`2017-02-24`\n\n- fix: add px totooltip css padding\n- fix: tooltip mapping error\n- fix: accurate intersection\n- fix: zoom error on double accuracy screen\n- fix: buonding box extended from keyShape\n- feat: afterAdd\n- feat: dblclick\n- improve: width、height default null\n- improve: remove hovershape on node\n- improve: tooltip defense mechanism\n\n#### 0.2.1\n\n`2017-02-14`\n\n- fix: rollback when add node\n- fix: apply tranformation of parent container while calculating bounding box\n- feat: waterPath\n- feat: tooltip tip information\n- feat: mouseover\n- feat: multiSelectable, default false\n- feat: set forceFit to true while width is undefined\n- improve: zoomable、draggable、resizeable、selectable default true\n\n#### 0.2.0\n\n`2017-02-07`\n\n- feat: accurate anchor mechanism\n- feat: GraphUtil.getEllipsePath\n- feat: GraphUtil.pointsToPolygon\n- feat: GraphUtil.pointsToBezier\n- feat: GraphUtil.snapPreciseAnchor\n- feat: GraphUtil.arrowTo\n- feat: GraphUtil.drawEdge\n- feat: bezierQuadratic\n- feat: node.show\n- feat: node.hide\n- feat: node.getLinkNodes\n- feat: node.getUnLinkNodes\n- feat: node.getRelativeItems\n- feat: node.getUnRelativeItems\n- feat: edge.show\n- feat: edge.hide\n- feat: Shape afterDraw\n- improve: the controlling point positions of Bezier Curve 改进贝塞尔曲线控制点位置\n- improve: grpah.delete => graph.del\n- improve: error when adding id\n\n#### 0.1.4\n\n`2017-01-17`\n\n- fix: delegator of dragging a node is the center of bbox\n- fix: use cardinality sort for all the sorting algorithm\n- fix: random id on edges\n- feat: level sort on edges, edge labels on the top level\n- feat: while extending shape is undefined when register an edge, find the extending shaoe automatically\n\n#### 0.1.3\n\n`2017-01-15`\n\n- fix: judge the existance of the object while operating assistGrid\n- feat: rollback judgement, default unactivate\n- feat: style mapping channel\n- feat: return the intersections while getAnchorPoints is null or returns false\n- feat: bezierHorizontal、bezierVertical\n- improve: 'eventEnd'\n\n#### 0.1.2\n\n`2017-01-12`\n\n- fix: judge the configuration before updating grid\n- fix: the size of graphContainer in unsetable, setted by inner canvas\n- fix: will not add an edge if the target or source is undefined\n- fix: changeSize() maximum tolerance for error\n- feat: graph.get('el') to get canvas DOM\n- feat: event exposures shape\n\n#### 0.1.1\n\n`2017-01-09`\n\n- feat: entrance of graph is G6.Graph\n\n#### 0.1.0\n\n`2017-01-07`\n\n- feat: color calculation library\n- feat: hot key\n- feat: updo, redo\n- feat: copy, paste\n- feat: reset zoom, auto zoom\n- feat: tree graph, linear graph, sankey graph, flow laout\n- feat: flow chart package\n- feat: timing diagram package\n- feat: single selection, frame selection\n- feat: node deformation\n- feat: edge deformation\n- feat: drag node and edge\n- feat: link edge and node\n- feat: drag canvas\n- feat: zoom\n- feat: select mode\n- feat: integrate g-graph\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Alipay.inc\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "PUBLISH.md",
    "content": "This project uses changeset to manage version release, and the specific release process is as follows:\n\n1. Complete related development work\n2. Create a branch from v5 (any name you want)\n3. Run `npm run version` command, fill in the information according to the prompt, and the version number will be updated automatically\n4. Commit the changes to the remote repository\n5. Create a PR on GitHub, add the `publish` label, and merge the branch to v5\n6. After the branch is merged, GitHub Actions will be triggered automatically, and the package will be published to npm\n7. After the release, the Release note needs to be updated. Execute \"pnpm tag\" in the packages/g6\n8. Fill in the tag information on the newly opened Github link. First, select the previous tag, and then select the current tag to obtain the changes. After confirming that there are no issues, release it.\n\n---\n\n本项目通过 changeset 来管理版本发布，具体的发布流程如下：\n\n1. 完成相关的开发工作\n2. 从 v5 分支创建一个分支（任意分支名均可）\n3. 根目录执行 `npm run version` 命令，根据提示填写相关信息，会自动更新版本号\n4. 将变更提交到远程仓库\n5. 在 GitHub 上创建一个 PR，并添加 `publish` 标签，将该分支合并到 v5 分支\n6. 分支合并后，会自动触发 GitHub Actions，发布到 npm\n7. 发布后，需更新 Release note，在 packages/g6 目录下执行 pnpm tag\n8. 在新打开的 Github 链接填写 tag 信息，先选择前一个 tag, 然后选择当前 tag 后得到变更，确认没有问题后发布\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"https://gw.alipayobjects.com/zos/antfincdn/R8sN%24GNdh6/language.svg\" width=\"18\"> English | [简体中文](./README.zh-CN.md)\n\n<h1 align=\"center\">\n<b>G6: A Graph Visualization Framework in TypeScript</b>\n</h1>\n\n![](https://user-images.githubusercontent.com/6113694/45008751-ea465300-b036-11e8-8e2a-166cbb338ce2.png)\n\n<p align=\"center\">\n<a href=\"https://trendshift.io/repositories/9709\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/9709\" alt=\"antvis%2FG6 | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n</p>\n\n[![npm Version](https://img.shields.io/npm/v/@antv/g6.svg)](https://www.npmjs.com/package/@antv/g6)\n[![Build Status](https://github.com/antvis/G6/actions/workflows/build.yml/badge.svg)](https://github.com/antvis/G6/actions/workflows/build.yml)\n[![codecov](https://codecov.io/gh/antvis/G6/graph/badge.svg?token=OvIk06tCPa)](https://codecov.io/gh/antvis/G6)\n[![npm Download](https://img.shields.io/npm/dm/@antv/g6.svg)](https://www.npmjs.com/package/@antv/g6)\n![typescript](https://img.shields.io/badge/language-typescript-blue.svg)\n[![npm License](https://img.shields.io/npm/l/@antv/g6.svg)](https://www.npmjs.com/package/@antv/g6)\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/antvis/G6)\n\n<p align=\"center\">\n  <a href=\"https://g6.antv.antgroup.com/en\">Introduction</a> •\n  <a href=\"https://g6.antv.antgroup.com/en/examples\">Examples</a> •\n  <a href=\"https://g6.antv.antgroup.com/en/manual/getting-started/quick-start\">Quick Start</a> •\n  <a href=\"https://g6.antv.antgroup.com/en/api/graph/method\">API</a>\n</p>\n\n[G6](https://github.com/antvis/g6) is a graph visualization engine. It provides basic capabilities for graph visualization and analysis such as drawing, layout, analysis, interaction, animation, themes, and plugins. With G6, users can quickly build their own graph visualization and analysis applications, making relational data simple, transparent, and meaningful.\n\n<img src='https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*_PJ5SZELwq0AAAAAAAAAAAAADmJ7AQ/original' width=550 alt='' />\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*zTjwQaXokeQAAAAAAAAAAABkARQnAQ' width=550 alt='' />\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*zau8QJcVpDQAAAAAAAAAAABkARQnAQ' height=200 alt='' /><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*RIlETY_S6IoAAAAAAAAAAABkARQnAQ' height=200 alt='' />\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*cDzXR4jIWr8AAAAAAAAAAABkARQnAQ' height=150 alt='' /><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*HTasSJGC4koAAAAAAAAAAABkARQnAQ' height=150 alt='' />\n\n<img src=\"https://user-images.githubusercontent.com/6113694/44995293-02858600-afd5-11e8-840c-349e4730d63d.gif\" height=150 alt='' /><img src=\"https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*I9OdTbXJIi0AAAAAAAAAAABkARQnAQ\" height=150 alt='' /><img src=\"https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*xoufSYcjK2AAAAAAAAAAAABkARQnAQ\" height=150 alt='' />\n\n## ✨ Features\n\nG6, as a professional graph visualization engine, boasts the following features:\n\n- **Rich Elements**: It comes with a variety of built-in node, edge, and Combo UI elements with extensive style configurations, supports data callbacks, and has a flexible mechanism for extending custom elements.\n- **Controllable Interactions**: It includes more than 10 built-in interaction behaviors and offers a rich array of events, facilitating the expansion of custom interactive behaviors.\n- **High-Performance Layout**: The engine features more than 10 common graph layouts, some of which leverage GPU and Rust parallel computing for enhanced performance, and it supports custom layout development.\n- **Convenient Plugins**: Optimized built-in plugin functionality and performance, with flexible extensibility, making it easier to implement customized business capabilities.\n- **Multiple Theme and Palettes**: Provides two sets of built-in themes, light and dark, that integrate over 20 popular community color palettes based on the AntV new color scheme.\n- **Multi-Environment Rendering**: Harnessing the power of [G](https://github.com/antvis/g), it supports rendering in Canvas, SVG, and WebGL, as well as server-side rendering with Node.js; it also offers plugin packages that provide powerful 3D rendering and spatial interactions based on WebGL.\n- **React Ecosystem**: By utilizing the React front-end ecosystem, it supports React nodes, significantly enriching the presentational styles of G6 nodes.\n\n## 🔨 Getting Started\n\nG6 is usually installed via a package manager such as npm or Yarn.\n\n```bash\n$ npm install @antv/g6\n```\n\nThe `Graph` object then can be imported from G6.\n\n```html\n<div id=\"container\"></div>\n```\n\n```js\nimport { Graph } from '@antv/g6';\n\n// Get the Data.\nconst data = {\n  nodes: [\n    /* your nodes data */\n  ],\n  edges: [\n    /* your edges data */\n  ],\n};\n\n// Create the Graph instance.\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    palette: {\n      type: 'group',\n      field: 'cluster',\n    },\n  },\n  layout: {\n    type: 'force',\n  },\n  behaviors: ['drag-canvas', 'drag-node'],\n});\n\n// Render the Graph.\ngraph.render();\n```\n\nAll goes well, you can get the following lovely graph!\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ue4iTYurc6sAAAAAAAAAAAAADmJ7AQ/fmt.webp\" height=\"300\" />\n\n## 🌍 Ecosystem\n\n- **Ant Design Charts**: A React chart library based on G2, G6, X6, L7.\n- **Graphin**: A simple React wrapper based on G6, as well as an SDK for developing graph visualization applications.\n\nFor more ecosystem open-source projects, contributions are welcome. Please feel free to submit a PR for inclusion.\n\n## 📮 Contributing\n\nThis project exists thanks to all the people who contribute.\n\nAnd thank you to all our backers! 🙏\n\n<a href=\"https://openomy.app/github/antvis/G6\" target=\"_blank\" style=\"display: block; width: 100%;\" align=\"center\">\n  <img src=\"https://openomy.app/svg?repo=antvis/G6&chart=bubble&latestMonth=3\" target=\"_blank\" alt=\"Contribution Leaderboard\" style=\"display: block; width: 100%;\" />\n </a>\n\n- **Issue Reporting**: If you encounter any issues with G6 during use, please feel free to submit an issue, along with the minimal sample code that can reproduce the problem.\n- **Contribution Guide**: Information on how to get involved in the [development and contribution](https://g6.antv.antgroup.com/en/manual/contribute) to G6.\n- **Ideas Discussion**: Discuss your ideas on GitHub Discussions or in the DingTalk group.\n\n<div align=\"center\">\n  <img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*CQoGSoFBzaUAAAAAAAAAAAAADmJ7AQ/fmt.webp\" height=\"256\" />\n</div>\n\n## 📄 License\n\n[MIT](./LICENSE).\n"
  },
  {
    "path": "README.zh-CN.md",
    "content": "<img src=\"https://gw.alipayobjects.com/zos/antfincdn/R8sN%24GNdh6/language.svg\" width=\"18\"> [English](./README.md) | 简体中文\n\n<h1 align=\"center\">\n<b>G6：图可视分析引擎</b>\n</h1>\n\n![](https://user-images.githubusercontent.com/6113694/45008751-ea465300-b036-11e8-8e2a-166cbb338ce2.png)\n\n<p align=\"center\">\n<a href=\"https://trendshift.io/repositories/9709\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/9709\" alt=\"antvis%2FG6 | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n</p>\n\n[![npm Version](https://img.shields.io/npm/v/@antv/g6.svg)](https://www.npmjs.com/package/@antv/g6)\n[![Build Status](https://github.com/antvis/G6/actions/workflows/build.yml/badge.svg)](https://github.com/antvis/G6/actions/workflows/build.yml)\n[![codecov](https://codecov.io/gh/antvis/G6/graph/badge.svg?token=OvIk06tCPa)](https://codecov.io/gh/antvis/G6)\n[![npm Download](https://img.shields.io/npm/dm/@antv/g6.svg)](https://www.npmjs.com/package/@antv/g6)\n![typescript](https://img.shields.io/badge/language-typescript-blue.svg)\n[![npm License](https://img.shields.io/npm/l/@antv/g6.svg)](https://www.npmjs.com/package/@antv/g6)\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/antvis/G6)\n\n<p align=\"center\">\n  <a href=\"https://g6.antv.antgroup.com/manual/introduction\">简介</a> •\n  <a href=\"https://g6.antv.antgroup.com/examples\">图表示例</a> •\n  <a href=\"https://g6.antv.antgroup.com/manual/getting-started/quick-start\">快速开始</a> •\n  <a href=\"https://g6.antv.antgroup.com/api/graph/method\">API</a>\n</p>\n\n[G6](https://github.com/antvis/g6) 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画、主题、插件等图可视化和分析的基础能力。基于 G6，用户可以快速搭建自己的图可视化分析应用，让关系数据变得简单，透明，有意义。\n\n<img src='https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*_PJ5SZELwq0AAAAAAAAAAAAADmJ7AQ/original' width=550 alt='' />\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*zTjwQaXokeQAAAAAAAAAAABkARQnAQ' width=550 alt='' />\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*zau8QJcVpDQAAAAAAAAAAABkARQnAQ' height=200 alt='' /><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*RIlETY_S6IoAAAAAAAAAAABkARQnAQ' height=200 alt='' />\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*cDzXR4jIWr8AAAAAAAAAAABkARQnAQ' height=150 alt='' /><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*HTasSJGC4koAAAAAAAAAAABkARQnAQ' height=150 alt='' />\n\n<img src=\"https://user-images.githubusercontent.com/6113694/44995293-02858600-afd5-11e8-840c-349e4730d63d.gif\" height=150 alt='' /><img src=\"https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*I9OdTbXJIi0AAAAAAAAAAABkARQnAQ\" height=150 alt='' /><img src=\"https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*xoufSYcjK2AAAAAAAAAAAABkARQnAQ\" height=150 alt='' />\n\n## ✨ 特性\n\nG6 作为一款专业的图可视化引擎，具有以下特性：\n\n- **丰富的元素**：内置丰富的节点、边、Combo UI 元素，样式配置丰富，支持数据回调，且具备有灵活扩展自定义元素的机制。\n- **可控的交互**：内置 10+ 交互行为，且提供丰富的各类事件，便于扩展自定义的交互行为。\n- **高性能布局**：内置 10+ 常用的图布局，部分基于 GPU、Rust 并行计算提升性能，支持自定义布局。\n- **便捷的组件**：优化内置组件功能及性能，且有灵活的扩展性，便于业务实现定制能力。\n- **多主题色板**：提供了亮色、暗色两套内置主题，在 AntV 新色板前提下，融入 20+ 常用社区色板。\n- **多环境渲染**：发挥 [G](https://github.com/antvis/g) 能力， 支持 Canvas、SVG 以及 WebGL，和 Node.js 服务端渲染；基于 WebGL 提供强大 3D 渲染和空间交互的插件包。\n- **React 体系**：利用 React 前端生态，支持 React 节点，大大丰富 G6 的节点呈现样式。\n\n## 🔨 开始使用\n\n可以通过 NPM 或 Yarn 等包管理器来安装。\n\n```bash\n$ npm install @antv/g6\n```\n\n成功安装之后，可以通过 import 导入 `Graph` 对象。\n\n```html\n<div id=\"container\"></div>\n```\n\n```js\nimport { Graph } from '@antv/g6';\n\n// 准备数据\nconst data = {\n  nodes: [\n    /* your nodes data */\n  ],\n  edges: [\n    /* your edges data */\n  ],\n};\n\n// 初始化图表实例\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    palette: {\n      type: 'group',\n      field: 'cluster',\n    },\n  },\n  layout: {\n    type: 'force',\n  },\n  behaviors: ['drag-canvas', 'drag-node'],\n});\n\n// 渲染图\ngraph.render();\n```\n\n一切顺利，你可以得到下面的力导图!\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ue4iTYurc6sAAAAAAAAAAAAADmJ7AQ/fmt.webp\" height=\"300\" />\n\n## 🌍 生态\n\n- **Ant Design Charts**： React 图表库，基于 G2、G6、X6、L7。\n- **Graphin**：基于 G6 的 React 简单封装，以及图可视化应用研发的 SDK。\n\n更多生态开源项目，欢迎 PR 收录进来。\n\n## 📮 贡献\n\n感谢所有为这个项目做出贡献的人，感谢所有支持者！🙏\n\n<a href=\"https://openomy.app/github/antvis/G6\" target=\"_blank\" style=\"display: block; width: 100%;\" align=\"center\">\n  <img src=\"https://openomy.app/svg?repo=antvis/G6&chart=bubble&latestMonth=3\" target=\"_blank\" alt=\"Contribution Leaderboard\" style=\"display: block; width: 100%;\" />\n </a>\n\n- **问题反馈**：使用过程遇到的 G6 的问题，欢迎提交 Issue，并附上可以复现问题的最小案例代码。\n- **贡献指南**：如何参与到 G6 的[开发和贡献](https://g6.antv.antgroup.com/manual/contribute)。\n- **想法讨论**：在 GitHub Discussion 上或者钉钉群里面讨论。\n\n<div>\n  <img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*CQoGSoFBzaUAAAAAAAAAAAAADmJ7AQ/fmt.webp\" height=\"256\" />\n</div>\n\n## 📄 License\n\n[MIT](./LICENSE).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\nCould the maintainers please create and publish a security.md with security policy that indicates the process for submitting vulnerabilities, tracking, and expectations for users of remediation of vulnerabilities?\n\n\n## Supported Versions\n\nUse this section to tell people about which versions of your project are\ncurrently being supported with security updates.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 5.1.x   | :white_check_mark: |\n| 5.0.x   | :x:                |\n| 4.0.x   | :white_check_mark: |\n| < 4.0   | :x:                |\n\n## Reporting a Vulnerability\n\nUse this section to tell people how to report a vulnerability.\n\nTell them where to go, how often they can expect to get an update on a\nreported vulnerability, what to expect if the vulnerability is accepted or\ndeclined, etc.\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"g6\",\n  \"private\": true,\n  \"repository\": \"https://github.com/antvis/G6.git\",\n  \"scripts\": {\n    \"build\": \"turbo build --filter=!@antv/g6-site\",\n    \"ci\": \"turbo run ci --filter=!@antv/g6-site\",\n    \"dev:g6\": \"cd ./packages/g6 && npm run dev\",\n    \"postinstall\": \"husky install\",\n    \"perf\": \"npm run perf\",\n    \"prepare\": \"husky install\",\n    \"publish\": \"pnpm publish -r --publish-branch v5\",\n    \"site\": \"pnpm -r --stream --filter=./packages/site run dev\",\n    \"version\": \"./scripts/version.sh\",\n    \"watch\": \"pnpm -r --stream --filter=!./site run start\"\n  },\n  \"commitlint\": {\n    \"extends\": [\n      \"@commitlint/config-conventional\"\n    ]\n  },\n  \"lint-staged\": {\n    \"*.{ts,tsx}\": [\n      \"eslint --fix\",\n      \"prettier --write\"\n    ],\n    \"*.{json,md}\": [\n      \"prettier --write\"\n    ]\n  },\n  \"devDependencies\": {\n    \"@antv/g-canvas\": \"^2.2.0\",\n    \"@antv/g-plugin-rough-canvas-renderer\": \"^2.1.1\",\n    \"@babel/core\": \"^7.28.6\",\n    \"@babel/plugin-transform-typescript\": \"^7.28.6\",\n    \"@changesets/cli\": \"^2.29.8\",\n    \"@commitlint/cli\": \"^18.6.1\",\n    \"@commitlint/config-conventional\": \"^18.6.3\",\n    \"@playwright/test\": \"^1.58.0\",\n    \"@rollup/plugin-commonjs\": \"^25.0.8\",\n    \"@rollup/plugin-json\": \"^6.1.0\",\n    \"@rollup/plugin-node-resolve\": \"^15.3.1\",\n    \"@rollup/plugin-terser\": \"^0.4.4\",\n    \"@rollup/plugin-typescript\": \"^11.1.6\",\n    \"@swc/core\": \"^1.15.11\",\n    \"@swc/jest\": \"^0.2.39\",\n    \"@types/d3-hierarchy\": \"^3.1.7\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/jsdom\": \"^21.1.7\",\n    \"@types/node\": \"^20.19.30\",\n    \"@types/stats.js\": \"^0.17.4\",\n    \"@typescript-eslint/eslint-plugin\": \"^6.21.0\",\n    \"@typescript-eslint/parser\": \"^6.21.0\",\n    \"chalk\": \"^4.1.2\",\n    \"d3-hierarchy\": \"^3.1.2\",\n    \"eslint\": \"^8.57.1\",\n    \"eslint-plugin-jsdoc\": \"^46.10.1\",\n    \"husky\": \"^8.0.3\",\n    \"iperf\": \"0.1.0-beta.14\",\n    \"jest\": \"^29.7.0\",\n    \"jest-environment-jsdom\": \"^29.7.0\",\n    \"jsdom\": \"^23.2.0\",\n    \"lil-gui\": \"^0.19.2\",\n    \"limit-size\": \"^0.1.4\",\n    \"lint-staged\": \"^15.5.2\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"open\": \"^10.2.0\",\n    \"prettier\": \"^3.8.1\",\n    \"prettier-plugin-organize-imports\": \"^3.2.4\",\n    \"prettier-plugin-packagejson\": \"^2.5.22\",\n    \"rimraf\": \"^5.0.10\",\n    \"rollup\": \"^4.57.0\",\n    \"rollup-plugin-polyfill-node\": \"^0.13.0\",\n    \"rollup-plugin-visualizer\": \"^5.14.0\",\n    \"stats.js\": \"^0.17.0\",\n    \"svgo\": \"^3.3.2\",\n    \"ts-node\": \"^10.9.2\",\n    \"tslib\": \"^2.8.1\",\n    \"turbo\": \"^1.13.4\",\n    \"typescript\": \"^5.9.3\",\n    \"vite\": \"^5.4.21\"\n  },\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [\n      \"canvas\"\n    ],\n    \"overrides\": {\n      \"@umijs/mako\": \"0.9.2\"\n    },\n    \"ignoredBuiltDependencies\": [\n      \"@parcel/watcher\",\n      \"@swc/core\",\n      \"core-js\",\n      \"core-js-pure\",\n      \"esbuild\",\n      \"iperf\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/bundle/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>G6 Bundler Test</title>\n  </head>\n  <body>\n    <select id=\"switch-bundler\">\n      <option value=\"\">None</option>\n      <option value=\"webpack\">Webpack</option>\n      <option value=\"vite\">Vite</option>\n      <option value=\"rollup\">Rollup</option>\n    </select>\n    <div id=\"container\"></div>\n\n    <script>\n      const select = document.getElementById('switch-bundler');\n\n      function setBundler(value) {\n        if (!value) return;\n        const container = document.getElementById('container');\n        container.innerHTML = '';\n\n        const currentScript = document.getElementById('g6-script');\n        if (currentScript) document.body.removeChild(currentScript);\n\n        const script = document.createElement('script');\n        script.id = 'g6-script';\n        script.src = `./dist/${value}/g6.umd.js`;\n        document.body.appendChild(script);\n      }\n\n      select.addEventListener('change', (e) => {\n        setBundler(e.target.value);\n        console.log('switch bundler:', e.target.value);\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/bundle/package.json",
    "content": "{\n  \"name\": \"bundle\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"run-s build:*\",\n    \"build:rollup\": \"rollup -c\",\n    \"build:vite\": \"vite build\",\n    \"build:webpack\": \"webpack\",\n    \"ci\": \"npm run build\"\n  },\n  \"dependencies\": {\n    \"@antv/g6\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^25.0.8\",\n    \"@rollup/plugin-node-resolve\": \"^15.3.1\",\n    \"@rollup/plugin-terser\": \"^0.4.4\",\n    \"@rollup/plugin-typescript\": \"^11.1.6\",\n    \"rollup\": \"^4.40.2\",\n    \"rollup-plugin-polyfill-node\": \"^0.13.0\",\n    \"swc\": \"^1.0.11\",\n    \"vite\": \"^5.4.19\",\n    \"webpack\": \"^5.99.8\",\n    \"webpack-cli\": \"^5.1.4\"\n  }\n}\n"
  },
  {
    "path": "packages/bundle/rollup.config.mjs",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport terser from '@rollup/plugin-terser';\nimport typescript from '@rollup/plugin-typescript';\nimport nodePolyfills from 'rollup-plugin-polyfill-node';\n\nexport default {\n  input: 'src/index.ts',\n  output: {\n    file: 'dist/rollup/g6.umd.js',\n    name: 'g6',\n    format: 'umd',\n    sourcemap: false,\n  },\n  plugins: [nodePolyfills(), resolve(), commonjs(), typescript(), terser()],\n};\n"
  },
  {
    "path": "packages/bundle/src/index.ts",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0' },\n    { id: '1' },\n    { id: '2' },\n    { id: '3' },\n    { id: '4' },\n    { id: '5' },\n    { id: '6' },\n    { id: '7' },\n    { id: '8' },\n    { id: '9' },\n  ],\n  edges: [\n    { source: '0', target: '1' },\n    { source: '0', target: '2' },\n    { source: '1', target: '4' },\n    { source: '0', target: '3' },\n    { source: '3', target: '4' },\n    { source: '4', target: '5' },\n    { source: '4', target: '6' },\n    { source: '5', target: '7' },\n    { source: '5', target: '8' },\n    { source: '8', target: '9' },\n    { source: '2', target: '9' },\n    { source: '3', target: '9' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'view',\n  animation: false,\n  data,\n  layout: {\n    type: 'antv-dagre',\n    nodeSize: [60, 30],\n    nodesep: 60,\n    ranksep: 40,\n    controlPoints: true,\n  },\n  node: {\n    type: 'rect',\n    style: {\n      size: [60, 30],\n      radius: 8,\n      labelText: (d) => d.id,\n      labelBackground: true,\n    },\n  },\n  edge: {\n    type: 'polyline',\n  },\n  behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/bundle/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"outDir\": \"lib\",\n    \"paths\": {\n      \"@antv/g6\": [\"../g6/src/index.ts\"]\n    }\n  },\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "packages/bundle/vite.config.js",
    "content": "import { defineConfig } from 'vite';\n\nexport default defineConfig({\n  build: {\n    lib: {\n      entry: 'src/index.ts',\n      name: 'g6',\n      fileName: 'g6',\n      formats: ['umd'],\n    },\n    outDir: 'dist/vite',\n  },\n});\n"
  },
  {
    "path": "packages/bundle/webpack.config.js",
    "content": "const path = require('path');\n\nmodule.exports = {\n  entry: './src/index.ts',\n  output: {\n    filename: 'g6.umd.js',\n    path: path.resolve(__dirname, 'dist/webpack'),\n  },\n};\n"
  },
  {
    "path": "packages/cli/CHANGELOG.md",
    "content": "# @antv/g6-cli\n\n## 0.0.2\n\n### Patch Changes\n\n- chore, feat, bugfix\n"
  },
  {
    "path": "packages/cli/README.md",
    "content": "# @antv/g6-cli\n\n`@antv/g6-cli` is a G6 template generation tool that comes with several templates.\n\nCurrently, it owns a built-in template called `extension`. This template handles the boilerplate setup, which encompasses a seamless local development environment, linting, code formatting, Jest for snapshot testing and bundling with Rollup etc.\n\n`@antv/g6-cli` i\n\n## Getting Started\n\nTo start using `@antv/g6-cli`, you'll first need to install it globally.\n\n```bash\nnpm i @antv/g6-cli -g\n```\n\nOnce installed, you can easily scaffold a new project:\n\n```bash\ncreate-g6\n```\n\nThen follow the prompts!\n\n![prompts](https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*09BKQrIcZUMAAAAAAAAAAAAADmJ7AQ/original)\n\nYou can also directly specify the project name and the template you want to use via additional command line options. For example, to scaffold a **G6 Extension** project, run:\n\n```bash\ncreate-g6 g6-extension-test --template extension\n```\n"
  },
  {
    "path": "packages/cli/build.config.ts",
    "content": "import { defineBuildConfig } from 'unbuild';\n\nexport default defineBuildConfig({\n  entries: ['src/index'],\n  clean: true,\n  rollup: {\n    inlineDependencies: true,\n    esbuild: {\n      target: 'node18',\n      minify: true,\n    },\n  },\n});\n"
  },
  {
    "path": "packages/cli/index.js",
    "content": "#!/usr/bin/env node\n\nimport './dist/index.mjs';\n"
  },
  {
    "path": "packages/cli/package.json",
    "content": "{\n  \"name\": \"@antv/g6-cli\",\n  \"version\": \"0.0.3\",\n  \"description\": \"Scaffolding Your Extension for G6\",\n  \"keywords\": [\n    \"antv\",\n    \"g6\",\n    \"extension\",\n    \"template\"\n  ],\n  \"repository\": \"https://github.com/antvis/G6.git\",\n  \"license\": \"MIT\",\n  \"author\": \"yvonneyx\",\n  \"type\": \"module\",\n  \"main\": \"index.js\",\n  \"bin\": {\n    \"create-g6\": \"index.js\"\n  },\n  \"files\": [\n    \"index.js\",\n    \"template-*\",\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"unbuild\",\n    \"dev\": \"unbuild --stub\",\n    \"prepublishOnly\": \"npm run build\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@types/lodash\": \"^4.17.16\",\n    \"@types/minimist\": \"^1.2.5\",\n    \"@types/prompts\": \"^2.4.9\",\n    \"kolorist\": \"^1.8.0\",\n    \"minimist\": \"^1.2.8\",\n    \"prompts\": \"^2.4.2\",\n    \"unbuild\": \"^2.0.0\"\n  },\n  \"engines\": {\n    \"node\": \"^18.0.0 || >=20.0.0\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/src/index.ts",
    "content": "/* eslint-disable jsdoc/require-jsdoc */\nimport { red, reset, yellow } from 'kolorist';\nimport minimist from 'minimist';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport prompts from 'prompts';\n\nconst argv = minimist<{\n  t?: string;\n  template?: string;\n}>(process.argv.slice(2), { string: ['_'] });\nconst cwd = process.cwd();\n\nconst renameFiles: Record<string, string | undefined> = {};\n\nconst defaultTargetDir = 'g6-extension-test';\n\nconst TEMPLATES = [\n  {\n    name: 'extension',\n    display: 'Extension',\n    color: yellow,\n  },\n];\n\nconst TEMPLATE_NAMES = TEMPLATES.map((template) => template.name);\n\nasync function init() {\n  const argTargetDir = formatTargetDir(argv._[0]);\n  const argTemplate = argv.template || argv.t;\n\n  let targetDir = argTargetDir || defaultTargetDir;\n  const getProjectName = () => (targetDir === '.' ? path.basename(path.resolve()) : targetDir);\n\n  let result: prompts.Answers<'template' | 'projectName' | 'overwrite' | 'author'>;\n\n  prompts.override({\n    overwrite: argv.overwrite,\n  });\n\n  try {\n    result = await prompts(\n      [\n        {\n          type: argTemplate && TEMPLATE_NAMES.includes(argTemplate) ? null : 'select',\n          name: 'template',\n          message:\n            typeof argTemplate === 'string' && !TEMPLATE_NAMES.includes(argTemplate)\n              ? reset(`\"${argTemplate}\" isn't a valid template. Please choose from below: `)\n              : reset('Select a template:'),\n          initial: 0,\n          choices: TEMPLATES.map((template) => {\n            const templateColor = template.color;\n            return {\n              title: templateColor(template.display || template.name),\n              value: template,\n            };\n          }),\n        },\n        {\n          type: () => (!fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'select'),\n          name: 'overwrite',\n          message: () =>\n            (targetDir === '.' ? 'Current directory' : `Target directory \"${targetDir}\"`) +\n            ` is not empty. Please choose how to proceed:`,\n          initial: 0,\n          choices: [\n            {\n              title: 'Remove existing files and continue',\n              value: 'yes',\n            },\n            {\n              title: 'Cancel operation',\n              value: 'no',\n            },\n            {\n              title: 'Ignore files and continue',\n              value: 'ignore',\n            },\n          ],\n        },\n        {\n          type: (_, { overwrite }: { overwrite?: string }) => {\n            if (overwrite === 'no') {\n              throw new Error(red('✖') + ' Operation cancelled');\n            }\n            return null;\n          },\n          name: 'overwriteChecker',\n        },\n        {\n          type: argTargetDir ? null : 'text',\n          name: 'projectName',\n          message: reset('Project name:'),\n          initial: defaultTargetDir,\n          onState: (state) => {\n            targetDir = formatTargetDir(state.value) || defaultTargetDir;\n          },\n        },\n        {\n          type: 'text',\n          name: 'author',\n          message: reset('Author'),\n        },\n      ],\n      {\n        onCancel: () => {\n          throw new Error(red('✖') + ' Operation cancelled');\n        },\n      },\n    );\n  } catch (cancelled: any) {\n    console.log(cancelled.message);\n    return;\n  }\n\n  // user choice associated with prompts\n  const { template, overwrite, projectName = getProjectName(), author } = result;\n\n  const variables = {\n    '{{projectName}}': projectName,\n  };\n\n  const root = path.join(cwd, targetDir);\n\n  if (overwrite === 'yes') {\n    emptyDir(root);\n  } else if (!fs.existsSync(root)) {\n    fs.mkdirSync(root, { recursive: true });\n  }\n\n  const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent);\n\n  const pkgManager = pkgInfo ? pkgInfo.name : 'npm';\n\n  console.log(`\\nScaffolding project in ${root}...`);\n\n  const templateDir = path.resolve(fileURLToPath(import.meta.url), '../..', `template-${template.name}`);\n\n  const write = (file: string, variables: Record<string, string>, content?: string) => {\n    const targetPath = path.join(root, renameFiles[file] ?? file);\n    if (content) {\n      fs.writeFileSync(targetPath, content);\n    } else {\n      copy(path.join(templateDir, file), targetPath, variables);\n    }\n  };\n\n  const files = fs.readdirSync(templateDir);\n\n  for (const file of files.filter((f) => f !== 'package.json')) {\n    write(file, variables);\n  }\n\n  const pkg = JSON.parse(fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8'));\n\n  pkg.name = projectName;\n  pkg.author = author;\n\n  write('package.json', variables, JSON.stringify(pkg, null, 2) + '\\n');\n\n  const cdProjectName = path.relative(cwd, root);\n  console.log(`\\nDone. Now run:\\n`);\n  if (root !== cwd) {\n    console.log(`  cd ${cdProjectName.includes(' ') ? `\"${cdProjectName}\"` : cdProjectName}`);\n  }\n  switch (pkgManager) {\n    case 'yarn':\n      console.log('  yarn');\n      console.log('  yarn dev');\n      break;\n    default:\n      console.log(`  ${pkgManager} install`);\n      console.log(`  ${pkgManager} run dev`);\n      break;\n  }\n  console.log();\n}\n\nfunction formatTargetDir(targetDir: string | undefined) {\n  return targetDir?.trim().replace(/\\/+$/g, '');\n}\n\nfunction copy(src: string, dest: string, variables: Record<string, string>) {\n  const stat = fs.statSync(src);\n  if (stat.isDirectory()) {\n    copyDir(src, dest, variables);\n  } else {\n    const templateContent = fs.readFileSync(src, 'utf-8');\n    const content = replaceTemplateVariables(templateContent, variables);\n    fs.writeFileSync(dest, content);\n  }\n}\n\nfunction copyDir(srcDir: string, destDir: string, variables: Record<string, string>) {\n  fs.mkdirSync(destDir, { recursive: true });\n  for (const file of fs.readdirSync(srcDir)) {\n    const srcFile = path.resolve(srcDir, file);\n    const destFile = path.resolve(destDir, file);\n    copy(srcFile, destFile, variables);\n  }\n}\n\nfunction isEmpty(path: string) {\n  const files = fs.readdirSync(path);\n  return files.length === 0 || (files.length === 1 && files[0] === '.git');\n}\n\nfunction emptyDir(dir: string) {\n  if (!fs.existsSync(dir)) {\n    return;\n  }\n  for (const file of fs.readdirSync(dir)) {\n    if (file === '.git') {\n      continue;\n    }\n    fs.rmSync(path.resolve(dir, file), { recursive: true, force: true });\n  }\n}\n\nfunction pkgFromUserAgent(userAgent: string | undefined) {\n  if (!userAgent) return undefined;\n  const pkgSpec = userAgent.split(' ')[0];\n  const pkgSpecArr = pkgSpec.split('/');\n  return {\n    name: pkgSpecArr[0],\n    version: pkgSpecArr[1],\n  };\n}\n\nfunction replaceTemplateVariables(content: string, variables: Record<string, string>) {\n  Object.keys(variables).forEach((key) => {\n    const regex = new RegExp(key, 'g');\n    content = content.replace(regex, variables[key]);\n  });\n  return content;\n}\n\ninit().catch((e) => {\n  console.error(e);\n});\n"
  },
  {
    "path": "packages/cli/template-extension/.commitlintrc.js",
    "content": "module.exports = {\n  extends: ['@commitlint/config-conventional'],\n  rules: {\n    'type-enum': [\n      2,\n      'always',\n      ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test', 'wip'],\n    ],\n  },\n};\n"
  },
  {
    "path": "packages/cli/template-extension/.editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[Makefile]\nindent_style = tab\n"
  },
  {
    "path": "packages/cli/template-extension/.eslintignore",
    "content": "dist\nes\nlib\nnode_modules"
  },
  {
    "path": "packages/cli/template-extension/.eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  env: {\n    browser: true,\n    es2021: true,\n    node: true,\n    commonjs: true,\n    jest: true,\n  },\n  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],\n  overrides: [\n    {\n      env: {\n        node: true,\n      },\n      files: ['.eslintrc.{js,cjs}'],\n      parserOptions: {\n        sourceType: 'script',\n      },\n    },\n  ],\n  parser: '@typescript-eslint/parser',\n  parserOptions: {\n    ecmaVersion: 'latest',\n    sourceType: 'module',\n  },\n  plugins: ['@typescript-eslint', 'jsdoc'],\n  rules: {\n    quotes: ['error', 'single', { allowTemplateLiterals: true, avoidEscape: true }],\n    semi: ['error', 'always'],\n  },\n};\n"
  },
  {
    "path": "packages/cli/template-extension/.gitignore",
    "content": "# Node\nnode_modules/\n\n# Build\ndist\nlib\nesm\n\n\n"
  },
  {
    "path": "packages/cli/template-extension/.prettierignore",
    "content": "dist\nes\nlib\nnode_modules\n"
  },
  {
    "path": "packages/cli/template-extension/.prettierrc.js",
    "content": "module.exports = {\n  plugins: [require.resolve('prettier-plugin-organize-imports'), require.resolve('prettier-plugin-packagejson')],\n  printWidth: 120,\n  proseWrap: 'never',\n  singleQuote: true,\n  trailingComma: 'all',\n};\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/demos/element-node-extend.ts",
    "content": "import { ExtendNode } from '@/src';\nimport { ExtensionCategory, Graph, register } from '@antv/g6';\n\nexport const elementNodeExtend: TestCase = async (context) => {\n  register(ExtensionCategory.NODE, 'extend-node', ExtendNode);\n\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [{ id: 'node1', style: { x: 100, y: 100 } }],\n    },\n    node: { type: 'extend-node' },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/demos/index.ts",
    "content": "export * from './element-node-extend';\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>{{projectName}}</title>\n    <style>\n      body {\n        margin: 0;\n      }\n\n      #container {\n        width: 500px;\n        height: 500px;\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"container\"></div>\n    <script type=\"module\" src=\"./main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/main.ts",
    "content": "import type { Controller } from 'lil-gui';\nimport GUI from 'lil-gui';\nimport _ from 'lodash';\nimport * as demos from './demos';\n\nconst { toUpper, snakeCase } = _;\nconst demoNames = Object.keys(demos);\n\nconst options = {\n  demo: '',\n};\n\nconst customForm: Controller[] = [];\n\nconst panel = new GUI({ autoPlace: true });\nconst __STORAGE__ = `__` + toUpper(snakeCase('{{projectName}}')) + `_DEMO__`;\nconst load = () => {\n  const data = localStorage.getItem(__STORAGE__);\n  if (data) panel.load(JSON.parse(data));\n};\nconst save = () => {\n  localStorage.setItem(__STORAGE__, JSON.stringify(panel.save()));\n};\npanel\n  .add(options, 'demo', demoNames)\n  .name('Demo')\n  .onChange((name: string) => {\n    render(name);\n    save();\n  });\nload();\n\nfunction initContainer() {\n  const container = document.getElementById('container')!;\n  container.innerHTML = '';\n  return container;\n}\n\nfunction initContext() {\n  const container = initContainer();\n  return { container, width: 500, height: 500 };\n}\n\nasync function render(name: string) {\n  destroyForm();\n  const context = initContext();\n  const demo = demos[name as keyof typeof demos];\n  const graph = await demo(context);\n  customForm.push(...(demo?.form?.(panel) || []));\n  Object.assign(window, { graph });\n}\n\nfunction destroyForm() {\n  customForm.forEach((controller) => controller.destroy());\n  customForm.length = 0;\n}\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/setup.ts",
    "content": "import './utils/use-snapshot-matchers';\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/types.d.ts",
    "content": "import type { Graph, GraphOptions } from '@antv/g6';\nimport type { Controller, GUI } from 'lil-gui';\n\ndeclare global {\n  export interface TestCase {\n    (context: GraphOptions): Promise<Graph>;\n    form?: (gui: GUI) => Controller[];\n  }\n\n  export type TestContext = GraphOptions;\n}\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/unit/default.spec.ts",
    "content": "describe('suite', () => {\n  it('case', () => {\n    expect(1).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/unit/elements/nodes/extend.spec.ts",
    "content": "import { elementNodeExtend } from '@@/demos';\nimport { createDemoGraph } from '@@/utils/index';\nimport type { Graph } from '@antv/g6';\n\ndescribe('element node circle', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(elementNodeExtend);\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('should render an extended node', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n});\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/utils/create.ts",
    "content": "import { resetEntityCounter } from '@antv/g';\nimport { Renderer as CanvasRenderer } from '@antv/g-canvas';\nimport { Renderer as SVGRenderer } from '@antv/g-svg';\nimport { Graph } from '@antv/g6';\nimport { OffscreenCanvasContext } from './offscreen-canvas-context';\n\nfunction getRenderer(renderer: string) {\n  switch (renderer) {\n    case 'svg':\n      return new SVGRenderer();\n    case 'webgl':\n    case 'canvas':\n      return new CanvasRenderer();\n    default:\n      return new SVGRenderer();\n  }\n}\n\n/**\n * Create graph canvas with config.\n * @param dom - dom\n * @param width - width\n * @param height - height\n * @param renderer - render\n * @returns instance\n */\nexport function createGraphCanvas(\n  dom?: null | HTMLElement,\n  width: number = 500,\n  height: number = 500,\n  renderer: string = 'svg',\n) {\n  const container = dom || document.createElement('div');\n  container.style.width = `${width}px`;\n  container.style.height = `${height}px`;\n\n  resetEntityCounter();\n  const offscreenNodeCanvas = {\n    getContext: () => context,\n  } as unknown as HTMLCanvasElement;\n  const context = new OffscreenCanvasContext(offscreenNodeCanvas);\n\n  return {\n    container,\n    width,\n    height,\n    renderer: () => getRenderer(renderer),\n    document: container.ownerDocument,\n    offscreenCanvas: offscreenNodeCanvas,\n  };\n}\n\nexport async function createDemoGraph(demo: TestCase, context?: Partial<TestContext>): Promise<Graph> {\n  const canvasOptions = createGraphCanvas(document.getElementById('container'));\n  return demo({ animation: false, ...canvasOptions, theme: 'light', ...context });\n}\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/utils/dir.ts",
    "content": "import path from 'path';\n\n/**\n * <zh/> 获取快照目录\n *\n * <en/> Get snapshot directory\n * @param dir - __filename\n * @param detail - <zh/> 快照详情 | <en/> snapshot detail\n * @returns <zh/> 快照目录 | <en/> snapshot directory\n */\nexport function getSnapshotDir(dir: string, detail: string = 'default'): [string, string] {\n  const root = process.cwd();\n  const subDir = dir.replace(root, '').replace('__tests__/unit/', '').replace('.spec.ts', '');\n  const outputDir = path.join(root, '__tests__', 'snapshots', subDir);\n  return [outputDir, detail];\n}\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/utils/index.ts",
    "content": "export { createDemoGraph } from './create';\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/utils/offscreen-canvas-context.ts",
    "content": "// Computed as round(measureText(text).width * 10) at 10px system-ui. For\n// characters that are not represented in this map, we’d ideally want to use a\n// weighted average of what we expect to see. But since we don’t really know\n// what that is, using “e” seems reasonable.\nconst defaultWidthMap: Record<string, number> = {\n  a: 56,\n  b: 63,\n  c: 57,\n  d: 63,\n  e: 58,\n  f: 37,\n  g: 62,\n  h: 60,\n  i: 26,\n  j: 26,\n  k: 55,\n  l: 26,\n  m: 88,\n  n: 60,\n  o: 60,\n  p: 62,\n  q: 62,\n  r: 39,\n  s: 54,\n  t: 38,\n  u: 60,\n  v: 55,\n  w: 79,\n  x: 54,\n  y: 55,\n  z: 55,\n  A: 69,\n  B: 67,\n  C: 73,\n  D: 74,\n  E: 61,\n  F: 58,\n  G: 76,\n  H: 75,\n  I: 28,\n  J: 55,\n  K: 67,\n  L: 58,\n  M: 89,\n  N: 75,\n  O: 78,\n  P: 65,\n  Q: 78,\n  R: 67,\n  S: 65,\n  T: 65,\n  U: 75,\n  V: 69,\n  W: 98,\n  X: 69,\n  Y: 67,\n  Z: 67,\n  0: 64,\n  1: 48,\n  2: 62,\n  3: 64,\n  4: 66,\n  5: 63,\n  6: 65,\n  7: 58,\n  8: 65,\n  9: 65,\n  ' ': 29,\n  '!': 32,\n  '\"': 49,\n  \"'\": 31,\n  '(': 39,\n  ')': 39,\n  ',': 31,\n  '-': 48,\n  '.': 31,\n  '/': 32,\n  ':': 31,\n  ';': 31,\n  '?': 52,\n  '‘': 31,\n  '’': 31,\n  '“': 47,\n  '”': 47,\n  '…': 82,\n};\n\nexport function measureText(text: string, fontSize: number) {\n  let sum = 0;\n  for (let i = 0; i < text.length; i++) {\n    sum += ((defaultWidthMap[text[i]] ?? 100) * fontSize) / 100;\n  }\n  return sum;\n}\n\nexport class OffscreenCanvasContext {\n  private fontSize!: number;\n\n  constructor(public canvas: HTMLCanvasElement) {}\n\n  set font(font: string) {\n    // `${fontStyle} ${fontVariant} ${fontWeight} ${fontSizeString}\n    const [, , , fontSizeString] = font.split(' ');\n    const fontSize = parseFloat(fontSizeString.replace('px', ''));\n    this.fontSize = fontSize;\n  }\n\n  fillRect() {}\n  fillText() {}\n  getImageData(sx: number, sy: number, sw: number, sh: number) {\n    return {\n      // ignore ascent and descent\n      data: new Uint8ClampedArray(sw * sh * 4).fill(0),\n    };\n  }\n\n  measureText(text: string) {\n    return {\n      width: measureText(text, this.fontSize),\n      actualBoundingBoxAscent: 0,\n      actualBoundingBoxDescent: 0,\n      actualBoundingBoxLeft: 0,\n      actualBoundingBoxRight: 0,\n      fontBoundingBoxAscent: 0,\n      fontBoundingBoxDescent: 0,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/utils/sleep.ts",
    "content": "export function sleep(n: number) {\n  return new Promise((resolve) => {\n    setTimeout(resolve, n);\n  });\n}\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/utils/svg-transformer.js",
    "content": "module.exports = {\n  process() {\n    return {\n      code: `module.exports = {};`,\n    };\n  },\n};\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/utils/to-match-svg-snapshot.ts",
    "content": "import type { Canvas, IAnimation } from '@antv/g';\nimport type { Graph, IAnimateEvent } from '@antv/g6';\nimport chalk from 'chalk';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport format from 'xml-formatter';\nimport xmlserializer from 'xmlserializer';\nimport { getSnapshotDir } from './dir';\nimport { sleep } from './sleep';\n\nexport type ToMatchSVGSnapshotOptions = {\n  fileFormat?: string;\n  keepSVGElementId?: boolean;\n};\nconst formatSVG = (svg: string, keepSVGElementId: boolean) => {\n  return (keepSVGElementId ? svg : svg.replace(/ *id=\"[^\"]*\" */g, ' ').replace(/clip-path=\"[^\"]*\"/g, '')).replace(\n    '\\r\\n',\n    '\\n',\n  );\n};\n\n// @see https://jestjs.io/docs/26.x/expect#expectextendmatchers\nexport async function toMatchSVGSnapshot(\n  gCanvas: Canvas | Canvas[],\n  dir: string,\n  name: string,\n  options: ToMatchSVGSnapshotOptions = {},\n): Promise<{ message: () => string; pass: boolean }> {\n  await sleep(300);\n\n  const { fileFormat = 'svg', keepSVGElementId = false } = options;\n  const namePath = path.join(dir, name);\n  const actualPath = path.join(dir, `${name}-actual.${fileFormat}`);\n  const expectedPath = path.join(dir, `${name}.${fileFormat}`);\n  const gCanvases = Array.isArray(gCanvas) ? gCanvas : [gCanvas];\n\n  let actual: string = '';\n\n  // Clone <svg>\n  const svg = (gCanvases[0].getContextService().getDomElement() as unknown as SVGElement).cloneNode(true) as SVGElement;\n  const gRoot = svg.querySelector('#g-root');\n\n  gCanvases.slice(1).forEach((gCanvas) => {\n    const dom = (gCanvas.getContextService().getDomElement() as unknown as SVGElement).cloneNode(true) as SVGElement;\n    // @ts-expect-error dom is SVGElement\n    gRoot?.append(...(dom.querySelector('#g-root')?.childNodes || []));\n  });\n\n  actual += svg\n    ? formatSVG(format(xmlserializer.serializeToString(svg as any), { indentation: '  ' }), keepSVGElementId)\n    : '';\n\n  try {\n    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n    if (!fs.existsSync(expectedPath)) {\n      if (process.env.CI === 'true') {\n        throw new Error(`Please generate golden image for ${namePath}`);\n      }\n      console.warn(`! generate ${namePath}`);\n      fs.writeFileSync(expectedPath, actual);\n      return {\n        message: () => `generate ${namePath}`,\n        pass: true,\n      };\n    } else {\n      const expected = fs.readFileSync(expectedPath, {\n        encoding: 'utf8',\n        flag: 'r',\n      });\n      if (actual === expected) {\n        if (fs.existsSync(actualPath)) fs.unlinkSync(actualPath);\n        return {\n          message: () => `match ${namePath}`,\n          pass: true,\n        };\n      }\n\n      // Perverse actual file.\n      if (actual) fs.writeFileSync(actualPath, actual);\n\n      const formatPath = (p: string) => p.split('/g6/')[1];\n      return {\n        message: () =>\n          `mismatch: \\n expected: ${chalk.green(formatPath(expectedPath))}\\n received: ${chalk.red(formatPath(actualPath))}`,\n        pass: false,\n      };\n    }\n  } catch (e) {\n    return {\n      message: () => `${e}`,\n      pass: false,\n    };\n  }\n}\n\nexport async function toMatchSnapshot(\n  graph: Graph,\n  dir: string,\n  detail?: string,\n  options: ToMatchSVGSnapshotOptions = {},\n) {\n  return await toMatchSVGSnapshot(\n    Object.values(graph.getCanvas().getLayers()),\n    ...getSnapshotDir(dir, detail),\n    options,\n  );\n}\n\nexport async function toMatchAnimation(\n  graph: Graph,\n  dir: string,\n  frames: number[],\n  operation: () => void | Promise<void>,\n  detail = 'default',\n  options: ToMatchSVGSnapshotOptions = {},\n) {\n  const animationPromise = new Promise<IAnimation>((resolve) => {\n    graph.once<IAnimateEvent>('beforeanimate', (e) => {\n      resolve(e.animation!);\n    });\n  });\n\n  await operation();\n\n  const animation = await animationPromise;\n\n  animation.pause();\n\n  for (const frame of frames) {\n    animation.currentTime = frame;\n    await sleep(32);\n    const result = await toMatchSVGSnapshot(\n      Object.values(graph.getCanvas().getCanvases().canvas),\n      ...getSnapshotDir(dir, `${detail}-${frame}`),\n      options,\n    );\n\n    if (!result.pass) {\n      return result;\n    }\n  }\n\n  return {\n    message: () => `match ${detail}`,\n    pass: true,\n  };\n}\n"
  },
  {
    "path": "packages/cli/template-extension/__tests__/utils/use-snapshot-matchers.ts",
    "content": "import {\n  ToMatchSVGSnapshotOptions,\n  toMatchAnimation,\n  toMatchSVGSnapshot,\n  toMatchSnapshot,\n} from './to-match-svg-snapshot';\n\ndeclare global {\n  // eslint-disable-next-line @typescript-eslint/no-namespace\n  namespace jest {\n    interface Matchers<R> {\n      toMatchSVGSnapshot(dir: string, name: string, options?: ToMatchSVGSnapshotOptions): Promise<R>;\n      toMatchSnapshot(dir: string, detail?: string, options?: ToMatchSVGSnapshotOptions): Promise<R>;\n      toMatchAnimation(\n        dir: string,\n        frames: number[],\n        operation: () => void | Promise<void>,\n        detail?: string,\n        options?: ToMatchSVGSnapshotOptions,\n      ): Promise<R>;\n    }\n  }\n}\n\nexpect.extend({\n  toMatchSVGSnapshot,\n  toMatchSnapshot,\n  toMatchAnimation,\n});\n"
  },
  {
    "path": "packages/cli/template-extension/jest.config.js",
    "content": "// Installing third-party modules by tnpm or cnpm will name modules with underscore as prefix.\n// In this case _{module} is also necessary.\nconst esm = ['internmap', 'd3-*', 'lodash-es', 'chalk'].map((d) => `_${d}|${d}`).join('|');\n\nmodule.exports = {\n  testTimeout: 100000,\n  testEnvironment: 'jsdom',\n  setupFilesAfterEnv: ['./__tests__/setup.ts'],\n  transform: {\n    '^.+\\\\.[tj]s$': ['@swc/jest'],\n    '^.+\\\\.svg$': ['<rootDir>/__tests__/utils/svg-transformer.js'],\n  },\n  collectCoverageFrom: ['src/**/*.ts'],\n  moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],\n  collectCoverage: false,\n  testRegex: '(/__tests__/.*\\\\.(test|spec))\\\\.(ts|tsx|js)$',\n  // Transform esm to cjs.\n  transformIgnorePatterns: [`<rootDir>/node_modules/(?!(${esm}))`],\n  testPathIgnorePatterns: ['/(lib|esm)/__tests__/'],\n  moduleNameMapper: {\n    '^@@/(.*)$': '<rootDir>/__tests__/$1',\n    '^@/(.*)$': '<rootDir>/$1',\n  },\n};\n"
  },
  {
    "path": "packages/cli/template-extension/package.json",
    "content": "{\n  \"name\": \"g6-extension-test\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Extension for G6\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"\"\n  },\n  \"license\": \"MIT\",\n  \"main\": \"lib/index.js\",\n  \"module\": \"esm/index.js\",\n  \"types\": \"lib/index.d.ts\",\n  \"scripts\": {\n    \"build:cjs\": \"rimraf ./lib && tsc --module commonjs --outDir lib -p tsconfig.build.json\",\n    \"build:esm\": \"rimraf ./esm && tsc --module ESNext --outDir esm -p tsconfig.build.json\",\n    \"build:umd\": \"rimraf ./dist && rollup -c\",\n    \"build\": \"run-p build:*\",\n    \"dev\": \"vite\",\n    \"fix\": \"eslint ./src ./__tests__ --fix && prettier ./src __tests__ --write \",\n    \"lint\": \"eslint ./src __tests__ --quiet && prettier ./src __tests__ --check\",\n    \"test\": \"jest\"\n  },\n  \"commitlint\": {\n    \"extends\": [\n      \"@commitlint/config-conventional\"\n    ]\n  },\n  \"lint-staged\": {\n    \"*.{ts,tsx}\": [\n      \"eslint --fix\",\n      \"prettier --write\"\n    ]\n  },\n  \"dependencies\": {\n    \"@antv/g6\": \"^5.0.0\"\n  },\n  \"devDependencies\": {\n    \"@antv/g\": \"^6.1.2\",\n    \"@antv/g-canvas\": \"^2.0.18\",\n    \"@antv/g-svg\": \"^2.0.15\",\n    \"@commitlint/config-conventional\": \"^19.2.2\",\n    \"@rollup/plugin-commonjs\": \"^25.0.7\",\n    \"@rollup/plugin-node-resolve\": \"^15.2.3\",\n    \"@rollup/plugin-terser\": \"^0.4.4\",\n    \"@rollup/plugin-typescript\": \"^11.1.6\",\n    \"@swc/jest\": \"^0.2.36\",\n    \"@types/jest\": \"^29.5.12\",\n    \"@types/node\": \"^20.12.12\",\n    \"@types/xmlserializer\": \"^0.6.6\",\n    \"@typescript-eslint/eslint-plugin\": \"^7.9.0\",\n    \"@typescript-eslint/parser\": \"^7.9.0\",\n    \"chalk\": \"^5.3.0\",\n    \"eslint\": \"^8.57.0\",\n    \"jest\": \"^29.7.0\",\n    \"jest-environment-jsdom\": \"^29.7.0\",\n    \"jsdom\": \"^23.2.0\",\n    \"lil-gui\": \"^0.19.2\",\n    \"lodash\": \"^4.17.21\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"prettier\": \"^3.2.5\",\n    \"prettier-plugin-organize-imports\": \"^3.2.4\",\n    \"prettier-plugin-packagejson\": \"^2.5.0\",\n    \"rimraf\": \"^5.0.7\",\n    \"rollup\": \"^4.17.2\",\n    \"rollup-plugin-polyfill-node\": \"^0.13.0\",\n    \"rollup-plugin-visualizer\": \"^5.12.0\",\n    \"ts-node\": \"^10.9.2\",\n    \"typescript\": \"^5.4.5\",\n    \"vite\": \"^5.2.11\",\n    \"xmlserializer\": \"^0.6.1\"\n  },\n  \"peerDependencies\": {\n    \"@antv/g\": \"^6.1.2\",\n    \"@antv/g-canvas\": \"^2.0.18\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/template-extension/rollup.config.mjs",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport terser from '@rollup/plugin-terser';\nimport typescript from '@rollup/plugin-typescript';\nimport _ from 'lodash';\nimport nodePolyfills from 'rollup-plugin-polyfill-node';\nimport { visualizer } from 'rollup-plugin-visualizer';\n\nconst { camelCase, upperFirst } = _;\nconst isBundleVis = !!process.env.BUNDLE_VIS;\n\nexport default [\n  {\n    input: 'src/index.ts',\n    output: {\n      file: 'dist/{{projectName}}.min.js',\n      name: upperFirst(camelCase('{{projectName}}')),\n      format: 'umd',\n      sourcemap: false,\n    },\n    plugins: [\n      nodePolyfills(),\n      resolve(),\n      commonjs(),\n      typescript({\n        tsconfig: 'tsconfig.build.json',\n      }),\n      terser(),\n      ...(isBundleVis ? [visualizer()] : []),\n    ],\n  },\n];\n"
  },
  {
    "path": "packages/cli/template-extension/src/elements/index.ts",
    "content": "export * from './nodes';\n"
  },
  {
    "path": "packages/cli/template-extension/src/elements/nodes/extend-node.ts",
    "content": "import type { CircleStyleProps } from '@antv/g6';\nimport { Circle } from '@antv/g6';\n\nexport interface ExtendNodeStyleProps extends CircleStyleProps {}\n\nexport class ExtendNode extends Circle {}\n"
  },
  {
    "path": "packages/cli/template-extension/src/elements/nodes/index.ts",
    "content": "export { ExtendNode } from './extend-node';\n\nexport type { ExtendNodeStyleProps } from './extend-node';\n"
  },
  {
    "path": "packages/cli/template-extension/src/exports.ts",
    "content": "export { ExtendNode } from './elements';\n"
  },
  {
    "path": "packages/cli/template-extension/src/index.ts",
    "content": "export * from './exports';\n"
  },
  {
    "path": "packages/cli/template-extension/tsconfig.build.json",
    "content": "{\n  \"compilerOptions\": {\n    \"paths\": {}\n  },\n  \"include\": [\"src/**/*\"],\n  \"extends\": \"./tsconfig.json\"\n}\n"
  },
  {
    "path": "packages/cli/template-extension/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowSyntheticDefaultImports\": true,\n    \"baseUrl\": \".\",\n    \"declaration\": true,\n    \"esModuleInterop\": true,\n    \"experimentalDecorators\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"isolatedModules\": true,\n    \"lib\": [\"DOM\", \"ESNext\"],\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"Node\",\n    \"outDir\": \"lib\",\n    \"pretty\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"sourceMap\": true,\n    \"sourceRoot\": \"src\",\n    \"strict\": true,\n    \"target\": \"ES6\",\n    \"types\": [\"@types/jest\", \"node\"],\n    \"paths\": {\n      \"@/*\": [\"./*\"],\n      \"@@/*\": [\"__tests__/*\"]\n    }\n  },\n  \"exclude\": [\"node_modules\", \"dist\", \"lib\", \"esm\"],\n  \"include\": [\"src/**/*\", \"__tests__/**/*\"]\n}\n"
  },
  {
    "path": "packages/cli/template-extension/vite.config.js",
    "content": "import path from 'path';\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n  root: './__tests__',\n  server: {\n    port: 8080,\n    open: '/',\n  },\n  plugins: [\n    {\n      name: 'isolation',\n      configureServer(server) {\n        server.middlewares.use((_req, res, next) => {\n          res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');\n          res.setHeader('Cross-Origin-Embedder-Policy', 'same-origin');\n          next();\n        });\n      },\n    },\n  ],\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, '.'),\n      '@@': path.resolve(__dirname, './__tests__'),\n    },\n  },\n});\n"
  },
  {
    "path": "packages/cli/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"build.config.ts\", \"src\"],\n  \"compilerOptions\": {}\n}\n"
  },
  {
    "path": "packages/g6/.gitignore",
    "content": "__tests__/**/*-actual.*\n\nREADME.*"
  },
  {
    "path": "packages/g6/__tests__/.eslintrc",
    "content": "{\n  \"rules\": {\n    \"no-console\": \"off\"\n  }\n}"
  },
  {
    "path": "packages/g6/__tests__/bugs/api-expand-element-z-index.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('api expand element z-index', () => {\n  it('when expand element, the z-index of descendant elements should be updated', async () => {\n    const graph = createGraph({\n      animation: false,\n      data: {\n        nodes: [{ id: 'node-1' }, { id: 'node-2', combo: 'combo-2' }],\n        combos: [\n          { id: 'combo-1', style: { collapsed: true } },\n          { id: 'combo-2', combo: 'combo-1', style: { collapsed: true } },\n        ],\n      },\n    });\n\n    await graph.draw();\n\n    const getZIndexOf = (id: string): number => {\n      // @ts-expect-error context is private\n      const context = graph.context;\n      return context.element!.getElement(id)!.style.zIndex;\n    };\n\n    expect(getZIndexOf('combo-1')).toBe(0);\n    expect(getZIndexOf('node-1')).toBe(0);\n    expect(graph.getComboData('combo-2').style?.zIndex).toBe(1);\n    expect(graph.getNodeData('node-2').style?.zIndex).toBe(2);\n\n    graph.frontElement('node-1');\n\n    expect(getZIndexOf('node-1')).toBe(3);\n\n    graph.frontElement('combo-1');\n\n    expect(getZIndexOf('combo-1')).toBe(4);\n\n    await graph.expandElement('combo-1', false);\n    await graph.expandElement('combo-2', false);\n\n    expect(getZIndexOf('combo-1')).toBe(4);\n    expect(getZIndexOf('combo-2')).toBe(5);\n    expect(getZIndexOf('node-2')).toBe(6);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/api-focus-element-edge.spec.ts",
    "content": "import { behaviorDragNode } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\nit('api focusElement edge', async () => {\n  const graph = await createDemoGraph(behaviorDragNode, { animation: false });\n\n  graph.translateBy([100, 100]);\n  graph.zoomBy(2);\n\n  graph.focusElement('node-3');\n\n  await expect(graph).toMatchSnapshot(__filename);\n\n  graph.focusElement('node-3-node-4');\n\n  await expect(graph).toMatchSnapshot(__filename, 'focusElement edge');\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/behaviors-click-select-drag-node.spec.ts",
    "content": "import { CommonEvent, NodeEvent } from '@/src';\nimport { createGraph } from '@@/utils';\n\ndescribe('behavior drag-node with click select', () => {\n  const createDemoGraph = async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', style: { x: 100, y: 100 } },\n          { id: 'node-2', combo: 'combo-1', style: { x: 200, y: 100 } },\n          { id: 'node-3', style: { x: 100, y: 200 } },\n          { id: 'node-4', combo: 'combo-1', style: { x: 200, y: 200 } },\n        ],\n        edges: [\n          { source: 'node-1', target: 'node-2' },\n          { source: 'node-2', target: 'node-4' },\n          { source: 'node-1', target: 'node-3' },\n          { source: 'node-3', target: 'node-4' },\n        ],\n        combos: [{ id: 'combo-1' }],\n      },\n      node: { style: { size: 20 } },\n      edge: {\n        style: { endArrow: true },\n      },\n      behaviors: [{ type: 'drag-element' }, { type: 'click-select', multiple: true }],\n    });\n    await graph.render();\n    return graph;\n  };\n\n  it('drag unselected node', async () => {\n    const graph = await createDemoGraph();\n\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-1' }, targetType: 'node' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'click-node-1');\n\n    // drag node-2\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-2' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 20, dy: 20 });\n    graph.emit(NodeEvent.DRAG_END);\n\n    await expect(graph).toMatchSnapshot(__filename, 'drag-node-2');\n  });\n\n  it('drag selected node', async () => {\n    const graph = await createDemoGraph();\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'shift' });\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-1' }, targetType: 'node' });\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-2' }, targetType: 'node' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'shift' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'click-node-1-node-2');\n\n    // drag node-2\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-2' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 20, dy: 20 });\n    graph.emit(NodeEvent.DRAG_END);\n\n    await expect(graph).toMatchSnapshot(__filename, 'drag-node-1-node-2');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/behaviors-click-select.spec.ts",
    "content": "import { CommonEvent, NodeEvent } from '@/src';\nimport { behaviorClickSelect } from '@@/demos';\nimport { createDemoGraph, createGraph } from '@@/utils';\n\ndescribe('behavior click-select', () => {\n  it('multiple select with degree 1', async () => {\n    const graph = await createDemoGraph(behaviorClickSelect, { animation: false });\n    graph.updateBehavior({ key: 'click-select', degree: 1, multiple: true });\n\n    graph.emit(NodeEvent.CLICK, { target: { id: '29' }, targetType: 'node' });\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'shift' });\n    graph.emit(NodeEvent.CLICK, { target: { id: '6' }, targetType: 'node' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'shift' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'multiple-shift-degree-1');\n\n    graph.destroy();\n  });\n\n  it('update state by api', async () => {\n    // 通过 api 更新状态导致 click-select 状态不同步\n    // State updated by api causes click-select state to be out of sync\n\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1', type: 'rect', style: { x: 50, y: 100 } }],\n      },\n      behaviors: [{ key: 'click-select', type: 'click-select' }],\n    });\n\n    await graph.draw();\n\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-1' }, targetType: 'node' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'state-selected');\n\n    graph.setElementState({ 'node-1': [] });\n    graph.addNodeData([{ id: 'node-2', type: 'rect', style: { x: 200, y: 200 }, states: ['selected'] }]);\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename, 'add-node-2');\n\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-2' }, targetType: 'node' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'click-node-2');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/behaviors-collapse-expand.spec.ts",
    "content": "import { ComboEvent, GraphEvent } from '@/src';\nimport { layoutAntVDagreFlowCombo } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior collapse expand', () => {\n  it('collapse expand with no change element', async () => {\n    // https://github.com/antvis/G6/issues/5951\n    const graph = await createDemoGraph(layoutAntVDagreFlowCombo, { animation: true });\n\n    // @ts-expect-error private method\n    const comboA = graph.context.element.getElement('A');\n\n    const click = jest.fn(async () => {\n      await new Promise<void>((resolve) => {\n        graph.on(GraphEvent.AFTER_ANIMATE, () => {\n          resolve();\n        });\n\n        graph.emit(ComboEvent.DBLCLICK, { target: comboA, targetType: 'combo' });\n      });\n    });\n\n    expect(click).not.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/behaviors-drag-element-combo.spec.ts",
    "content": "import { ComboEvent, Graph } from '@/src';\nimport { layoutAntVDagreFlowCombo } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior drag element combo', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(layoutAntVDagreFlowCombo, { animation: false });\n  });\n\n  it('drag combo A over C', async () => {\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'A' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: 100, dy: 0 });\n    graph.emit(ComboEvent.DRAG_END, { target: { id: 'C' } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-A-over-C');\n  });\n\n  it('drag combo C over A', async () => {\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'C' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: -10, dy: 0 });\n    graph.emit(ComboEvent.DRAG_END, { target: { id: 'A' } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-C-over-A');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/behaviors-drag-rotated-canvas.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { CommonEvent, NodeEvent } from '@/src';\nimport { bugDragRotatedCanvas } from '@@/demos';\nimport { createDemoGraph, dispatchCanvasEvent } from '@@/utils';\n\nconst fixed2 = (num: number): number => {\n  return parseFloat(num.toFixed(2));\n};\n\ndescribe('behavior drag rotated canvas', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(bugDragRotatedCanvas, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('drag 30 rotated canvas', async () => {\n    await graph.rotateTo(30);\n    const [x, y] = graph.getPosition();\n\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { movement: { x: 10, y: 10 }, targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    expect(graph.getRotation()).toBe(30);\n    expect(graph.getPosition()).toBeCloseTo([x + 3.66, y + 13.66]);\n  });\n\n  it('drag 90 rotated canvas', async () => {\n    await graph.rotateTo(90);\n    const [x, y] = graph.getPosition();\n\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { movement: { x: 10, y: 20 }, targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    expect(graph.getRotation()).toBe(90);\n    expect(graph.getPosition()).toBeCloseTo([x - 20, y + 10]);\n  });\n\n  it('drag 180 rotated canvas', async () => {\n    await graph.rotateTo(180);\n    const [x, y] = graph.getPosition();\n\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { movement: { x: 10, y: 20 }, targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    expect(graph.getRotation()).toBe(180);\n    expect(graph.getPosition()).toBeCloseTo([x - 10, y - 20]);\n  });\n\n  it('drag 270 rotated canvas', async () => {\n    await graph.rotateTo(270);\n    const [x, y] = graph.getPosition();\n\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { movement: { x: 10, y: 20 }, targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    expect(graph.getRotation()).toBe(270);\n    expect(graph.getPosition()).toBeCloseTo([x + 20, y - 10]);\n  });\n\n  it.each([\n    { name: 'element', id: 'node1', targetType: 'node' },\n    { name: 'combo', id: 'comboA', targetType: 'combo' },\n  ])('drag $name when 30 rotated canvas', async ({ id, targetType }) => {\n    await graph.rotateTo(30);\n\n    const [x, y] = graph.getElementPosition(id);\n\n    graph.emit(NodeEvent.DRAG_START, { target: { id: id }, targetType });\n    graph.emit(NodeEvent.DRAG, { dx: 10, dy: 10 });\n    graph.emit(NodeEvent.DRAG_END, { target: { id: id }, targetType });\n\n    expect(graph.getRotation()).toBe(30);\n    const [nextX, nextY] = graph.getElementPosition(id);\n    expect(fixed2(nextX)).toBeCloseTo(fixed2(x + 3.66));\n    expect(fixed2(nextY)).toBeCloseTo(fixed2(y + 13.66));\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/behaviors-multiple-conflict.spec.ts",
    "content": "import { createGraph, dispatchCanvasEvent } from '@@/utils';\nimport { CommonEvent, NodeEvent } from '@antv/g6';\n\ndescribe('bugs:multiple-conflict', () => {\n  it('drag element, drag canvas', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1', style: { x: 50, y: 50, size: 20 } }],\n      },\n      behaviors: ['drag-element', 'drag-canvas'],\n    });\n\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename);\n\n    // drag canvas\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { movement: { x: 10, y: 10 }, targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n    await expect(graph).toMatchSnapshot(__filename, 'drag-canvas');\n\n    // drag element\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-1' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 10, dy: 10 });\n    graph.emit(NodeEvent.DRAG_END);\n    await expect(graph).toMatchSnapshot(__filename, 'drag-element');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/brush-select.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('BrushSelect clear states issue', () => {\n  it('Should not clear states except selection state', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', style: { x: 250, y: 150 } },\n          { id: 'node-3', style: { x: 250, y: 300 } },\n        ],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-3' }],\n      },\n      behaviors: ['brush-select'],\n    });\n\n    await graph.draw();\n    graph.setElementState({ 'edge-1': ['selected', 'active', 'custom-state'] });\n\n    const currentStates = graph.getElementState('edge-1');\n    expect(currentStates).toEqual(['selected', 'active', 'custom-state']);\n\n    await graph.emit('canvas:click');\n\n    const newStates = graph.getElementState('edge-1');\n    console.log(newStates);\n    expect(newStates).toEqual(['active', 'custom-state']);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/continuous-invoke.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('bugs:continuous-invoke', () => {\n  it('continuous invoke', () => {\n    const graph = createGraph({});\n    const fn = jest.fn(async () => {\n      graph.destroy();\n      await graph.render();\n    });\n\n    expect(fn).rejects.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/element-combo-drag.spec.ts",
    "content": "import { NodeEvent } from '@/src';\nimport { createGraph } from '@@/utils';\n\ndescribe('bugs:element-combo-drag', () => {\n  it('drag combo', async () => {\n    const graph = createGraph({\n      animation: false,\n      data: {\n        nodes: [\n          { id: 'node-0', combo: 'combo-0', style: { x: 100, y: 100 } },\n          { id: 'node-1', combo: 'combo-0', style: { x: 150, y: 100 } },\n          { id: 'node-2', style: { x: 250, y: 100 } },\n        ],\n        edges: [{ source: 'node-1', target: 'node-2' }],\n        combos: [{ id: 'combo-0' }],\n      },\n      behaviors: ['drag-element'],\n    });\n\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename);\n\n    await graph.collapseElement('combo-0');\n\n    await expect(graph).toMatchSnapshot(__filename, 'collapse-combo-0');\n\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-2' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 50, dy: 50 });\n    graph.emit(NodeEvent.DRAG_END, { target: { id: 'node-2' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-node-2');\n\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'combo-0' }, targetType: 'combo' });\n    graph.emit(NodeEvent.DRAG, { dx: 50, dy: 50 });\n    graph.emit(NodeEvent.DRAG_END, { target: { id: 'combo-0' }, targetType: 'combo' });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-0');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/element-custom-state-switch.spec.ts",
    "content": "import { createGraph } from '@@/utils';\nimport { CanvasEvent, CommonEvent, NodeEvent } from '@antv/g6';\n\ndescribe('bug: element-custom-state-switch', () => {\n  it('single select', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1', states: ['important'], style: { x: 100, y: 100 } }],\n      },\n      node: {\n        style: {\n          fill: 'red',\n        },\n        state: {\n          important: {\n            fill: 'green',\n          },\n        },\n      },\n      behaviors: [\n        {\n          type: 'click-select',\n        },\n      ],\n    });\n\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename, 'single');\n\n    expect(graph.getElementState('node-1')).toEqual(['important']);\n\n    // click\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-1' }, targetType: 'node' });\n\n    expect(graph.getElementState('node-1')).toEqual(['important', 'selected']);\n\n    await expect(graph).toMatchSnapshot(__filename, 'single-select');\n\n    // click again\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-1' }, targetType: 'node' });\n\n    expect(graph.getElementState('node-1')).toEqual(['important']);\n\n    await expect(graph).toMatchSnapshot(__filename, 'single');\n  });\n\n  it('multiple select', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', states: ['important'], style: { x: 100, y: 100 } },\n          { id: 'node-2', style: { x: 150, y: 100 } },\n        ],\n      },\n      node: {\n        style: {\n          fill: 'red',\n        },\n        state: {\n          important: {\n            fill: 'green',\n          },\n        },\n      },\n      behaviors: [\n        {\n          type: 'click-select',\n          multiple: true,\n        },\n      ],\n    });\n\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename, 'multiple');\n\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-2' }, targetType: 'node' });\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'shift' });\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-1' }, targetType: 'node' });\n    // graph.emit(CommonEvent.KEY_UP, { key: 'shift' });\n\n    expect(graph.getElementState('node-1')).toEqual(['important', 'selected']);\n    expect(graph.getElementState('node-2')).toEqual(['selected']);\n\n    await expect(graph).toMatchSnapshot(__filename, 'multiple-select');\n\n    // unselect\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-1' }, targetType: 'node' });\n    expect(graph.getElementState('node-1')).toEqual(['important']);\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-2' }, targetType: 'node' });\n    expect(graph.getElementState('node-2')).toEqual([]);\n\n    await expect(graph).toMatchSnapshot(__filename, 'multiple');\n\n    // reselect\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-1' }, targetType: 'node' });\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-2' }, targetType: 'node' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'shift' });\n\n    // click canvas\n    graph.emit(CanvasEvent.CLICK);\n    expect(graph.getElementState('node-1')).toEqual(['important']);\n    expect(graph.getElementState('node-2')).toEqual([]);\n\n    await expect(graph).toMatchSnapshot(__filename, 'multiple-unselect');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/element-edge-update-arrow.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('bug: element-edge-update-arrow', () => {\n  it('should update edge arrow', async () => {\n    const graph = createGraph({\n      animation: false,\n      data: {\n        nodes: [\n          { id: 'node-0', style: { x: 100, y: 100 } },\n          { id: 'node-1', style: { x: 200, y: 100 } },\n        ],\n        edges: [\n          {\n            source: 'node-0',\n            target: 'node-1',\n            style: { startArrow: true, startArrowFill: 'red', endArrow: true, endArrowFill: 'green' },\n          },\n        ],\n      },\n    });\n\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.updateEdgeData([\n      {\n        source: 'node-0',\n        target: 'node-1',\n        style: { startArrowFill: 'purple', startArrowStroke: 'blue', endArrowFill: 'pink', endArrowStroke: 'yellow' },\n      },\n    ]);\n\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename, 'update-arrow');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/element-node-collapse.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('bugs:element-node-collapse', () => {\n  it('collapse or expand a node should not throw error', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n          { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n          { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n        ],\n        combos: [\n          { id: 'combo1', combo: 'combo2' },\n          { id: 'combo2', style: {} },\n        ],\n      },\n      combo: {\n        style: {\n          labelText: (d) => d.id,\n          labelPadding: [1, 5],\n          labelFill: '#fff',\n          labelBackground: true,\n          labelBackgroundRadius: 10,\n          labelBackgroundFill: '#7863FF',\n        },\n      },\n    });\n\n    await graph.render();\n\n    const fn = async () => {\n      await graph.collapseElement('node1', false);\n      await graph.expandElement('node2', false);\n    };\n\n    expect(fn).not.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/element-node-icon-switch.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('bug: element-node-icon-switch', () => {\n  it('change node icon', async () => {\n    const graph = createGraph({\n      animation: false,\n      data: {\n        nodes: [{ id: 'node-1', style: { x: 50, y: 50, iconText: 'Text' } }],\n      },\n      node: {\n        style: {},\n      },\n    });\n\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename, 'text-icon');\n\n    graph.updateNodeData([\n      {\n        id: 'node-1',\n        style: {\n          iconText: '',\n          iconSrc: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*AzSISZeq81IAAAAAAAAAAAAADmJ7AQ/original',\n        },\n      },\n    ]);\n\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename, 'image-icon');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/element-node-update-badge.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('bug: element-node-update-badge', () => {\n  it('should update node badge', async () => {\n    const graph = createGraph({\n      animation: false,\n      node: {\n        style: {\n          badge: true,\n          badges: [{ text: '1' }],\n          badgeFill: 'white',\n          badgeBackgroundFill: 'red',\n        },\n      },\n      data: {\n        nodes: [{ id: 'node-0', style: { x: 100, y: 100 }, states: ['inactive'] }],\n      },\n    });\n\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.setElementState('node-0', []);\n\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename, 'update-node-badge');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/element-orth-router.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('element orth router', () => {\n  it('test polyline orth', async () => {\n    const graph = createGraph({\n      animation: true,\n      data: {\n        nodes: [\n          {\n            id: 'node-1',\n            style: { x: 310, y: 280, size: 80 },\n          },\n          {\n            id: 'node-2',\n            style: { x: 300, y: 175 },\n          },\n        ],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n      },\n      edge: {\n        type: 'polyline',\n        style: {\n          router: { type: 'orth' },\n        },\n      },\n    });\n\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/element-port-rotate.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('element port rotate', () => {\n  it('default', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],\n      },\n      node: {\n        type: 'rect',\n        style: {\n          size: [50, 150],\n          port: true,\n          portR: 3,\n          ports: [\n            { key: 'port-1', placement: [0, 0.15] },\n            { key: 'port-2', placement: 'left' },\n            { key: 'port-3', placement: [0, 0.85] },\n          ],\n          transform: [['rotate', 45]],\n        },\n      },\n    });\n\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/element-remove-combo.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('element remove combo', () => {\n  it('remove combo', async () => {\n    const graph = createGraph({\n      animation: true,\n      data: {\n        nodes: [\n          { id: 'node-1', data: {}, combo: 'combo-1' },\n          { id: 'node-2', data: {}, combo: 'combo-1' },\n          { id: 'node-3', data: {}, combo: 'combo-1' },\n        ],\n        combos: [\n          { id: 'combo-1', data: {}, combo: 'combo-2' },\n          { id: 'combo-2', data: {}, style: {} },\n        ],\n      },\n      layout: {\n        type: 'force',\n      },\n    });\n\n    await graph.draw();\n\n    graph.removeComboData(['combo-1']);\n\n    const draw = jest.fn(async () => {\n      await graph.draw();\n    });\n\n    expect(draw).not.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/element-set-position-to-origin.spec.ts",
    "content": "import type { ID } from '@/src';\nimport { createGraph } from '@@/utils';\n\ndescribe('element set position to origin', () => {\n  it('suite 1', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1' }],\n      },\n    });\n\n    await graph.draw();\n\n    // @ts-expect-error Property 'context' is protected\n    const getElementOf = (id: ID) => graph.context.element!.getElement(id)!;\n\n    expect(graph.getNodeData('node-1').style).toEqual({ zIndex: 0 });\n    expect(getElementOf('node-1').style.transform).toEqual([['translate', 0, 0]]);\n\n    graph.translateElementTo('node-1', [100, 100]);\n\n    expect(graph.getNodeData('node-1').style).toEqual({ x: 100, y: 100, z: 0, zIndex: 0 });\n    expect(getElementOf('node-1').style.transform).toEqual([['translate', 100, 100]]);\n\n    graph.translateElementTo('node-1', [0, 0]);\n\n    expect(graph.getNodeData('node-1').style).toEqual({ x: 0, y: 0, z: 0, zIndex: 0 });\n    expect(getElementOf('node-1').style.transform).toEqual([['translate', 0, 0]]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/fit-view.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('fit view', () => {\n  it('suite 1', async () => {\n    // https://github.com/antvis/G6/issues/5943\n    const graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', style: { x: 250, y: 150 } },\n          { id: 'node-2', style: { x: 350, y: 150 } },\n          { id: 'node-3', style: { x: 250, y: 300 } },\n        ],\n      },\n      behaviors: ['zoom-canvas'],\n    });\n\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename);\n\n    // wheel\n    graph.emit('wheel', { deltaY: 5 });\n    graph.emit('wheel', { deltaY: 5 });\n\n    await expect(graph).toMatchSnapshot(__filename, 'after-wheel');\n\n    // fit center\n    await graph.fitCenter();\n    await graph.fitCenter();\n    await expect(graph).toMatchSnapshot(__filename, 'after-fit-center');\n\n    // fit view\n    await graph.fitView();\n    await expect(graph).toMatchSnapshot(__filename, 'after-fit-view');\n\n    // fit center again\n    await graph.fitCenter();\n    await expect(graph).toMatchSnapshot(__filename, 'after-fit-center-again');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/focus-element.spec.ts",
    "content": "import { CommonEvent, NodeEvent } from '@/src';\nimport { createGraph, dispatchCanvasEvent } from '@@/utils';\n\ndescribe('focus element', () => {\n  it('focus after drag', async () => {\n    // https://github.com/antvis/G6/issues/5955\n    const graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', style: { x: 250, y: 150 } },\n          { id: 'node-2', style: { x: 350, y: 150 } },\n          { id: 'node-3', style: { x: 250, y: 300 } },\n        ],\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n    });\n\n    await graph.draw();\n\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { movement: { x: 100, y: 100 }, targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    await expect(graph).toMatchSnapshot(__filename, 'focus-before-drag');\n\n    await graph.focusElement('node-1');\n\n    await expect(graph).toMatchSnapshot(__filename, 'focus-after-drag');\n  });\n\n  it('hover after focus', async () => {\n    // https://github.com/antvis/G6/issues/5925\n    const graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', style: { x: 250, y: 150 } },\n          { id: 'node-2', style: { x: 350, y: 150 } },\n          { id: 'node-3', style: { x: 250, y: 300 } },\n        ],\n      },\n      behaviors: ['zoom-canvas', 'hover-activate'],\n    });\n\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename, 'hover-before-focus');\n\n    await graph.focusElement('node-1');\n\n    graph.emit(NodeEvent.POINTER_ENTER, {\n      target: { id: 'node-2' },\n      targetType: 'node',\n      type: CommonEvent.POINTER_ENTER,\n    });\n\n    await expect(graph).toMatchSnapshot(__filename, 'hover-after-focus');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/graph-draw-after-clear.spec.ts",
    "content": "import { elementEdgeLine } from '@@/demos';\nimport { createDemoGraph, sleep } from '@@/utils';\n\nit('graph draw after clear', async () => {\n  const graph = await createDemoGraph(elementEdgeLine);\n\n  const data = graph.getData();\n\n  await graph.clear();\n\n  await expect(graph).toMatchSnapshot(__filename, 'blank');\n\n  await sleep(200);\n\n  graph.addData(data);\n  await graph.draw();\n\n  await expect(graph).toMatchSnapshot(__filename);\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/model-add-edge-in-combo.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('add edge in combo', () => {\n  it('add edge in combo without zIndex', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', combo: 'combo-1', style: { x: 100, y: 100 } },\n          { id: 'node-2', combo: 'combo-1', style: { x: 200, y: 200 } },\n        ],\n        combos: [{ id: 'combo-1' }],\n      },\n    });\n\n    await graph.draw();\n\n    expect(graph.getComboData('combo-1').style?.zIndex).toBe(0);\n    expect(graph.getNodeData('node-1').style?.zIndex).toBe(1);\n\n    graph.addEdgeData([{ id: 'edge', source: 'node-1', target: 'node-2' }]);\n    await graph.draw();\n\n    expect(graph.getEdgeData('edge').style?.zIndex).toBe(0);\n    // @ts-expect-error skip the type check\n    expect(graph.context.element?.getElement('edge')?.style.zIndex).toBe(0);\n  });\n\n  it('add edge in combo with zIndex', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', combo: 'combo-1', style: { x: 100, y: 100 } },\n          { id: 'node-2', combo: 'combo-1', style: { x: 200, y: 200 } },\n          { id: 'node-3', style: { x: 300, y: 300, zIndex: 5 } },\n        ],\n        combos: [{ id: 'combo-1' }],\n      },\n    });\n\n    await graph.draw();\n\n    expect(graph.getComboData('combo-1').style?.zIndex).toBe(0);\n\n    await graph.frontElement('combo-1');\n\n    expect(graph.getComboData('combo-1').style?.zIndex).toBe(5 + 1);\n    expect(graph.getNodeData('node-1').style?.zIndex).toBe(5 + 1 + 1);\n\n    graph.addEdgeData([{ id: 'edge', source: 'node-1', target: 'node-2' }]);\n    await graph.draw();\n\n    expect(graph.getEdgeData('edge').style?.zIndex).toBe(5 + 1);\n    // @ts-expect-error skip the type check\n    expect(graph.context.element?.getElement('edge')?.style.zIndex).toBe(5 + 1);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/model-remove-parent.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\nit('model remove parent', async () => {\n  const data = {\n    nodes: [\n      { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n      { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n      { id: 'node3', combo: 'combo1', style: { x: 250, y: 300 } },\n    ],\n    edges: [],\n    combos: [{ id: 'combo1' }],\n  };\n\n  const graph = createGraph({\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    combo: {\n      type: 'rect',\n    },\n  });\n\n  await graph.render();\n\n  await expect(graph).toMatchSnapshot(__filename);\n\n  graph.updateNodeData([{ id: 'node3', combo: null }]);\n  await graph.draw();\n\n  await expect(graph).toMatchSnapshot(__dirname, 'remove-parent');\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/plugin-history-align-fields.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('bug: plugin-history-align-fields', () => {\n  it('fix alignFields util', async () => {\n    const graph = createGraph({\n      plugins: [{ type: 'history', key: 'history' }],\n      data: {\n        nodes: [\n          {\n            id: 'node-1',\n            type: 'rect',\n            style: { x: 50, y: 100 },\n            data: {\n              aaa: {\n                bbb: false,\n                ccc: true,\n                ddd: '1234',\n              },\n            },\n          },\n        ],\n      },\n    });\n\n    await graph.render();\n\n    expect(graph.getNodeData('node-1').style).toEqual({ x: 50, y: 100, zIndex: 0 });\n    expect(graph.getNodeData('node-1').data!.aaa).toEqual({\n      bbb: false,\n      ccc: true,\n      ddd: '1234',\n    });\n\n    await graph.translateElementBy('node-1', [100, 100]);\n    expect(graph.getNodeData('node-1').style).toEqual({ x: 150, y: 200, z: 0, zIndex: 0 });\n    expect(graph.getNodeData('node-1').data!.aaa).toEqual({\n      bbb: false,\n      ccc: true,\n      ddd: '1234',\n    });\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/plugin-hull-three-collinear-dots.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('bug: plugin-hull-three-collinear-dots', () => {\n  it('fully enclosed', async () => {\n    const graph = createGraph({\n      animation: false,\n      data: {\n        nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }],\n      },\n      layout: {\n        type: 'grid',\n        rows: 3,\n        cols: 1,\n      },\n      plugins: [\n        {\n          key: 'hull',\n          type: 'hull',\n          members: ['node1', 'node2', 'node3'],\n        },\n      ],\n    });\n\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/plugin-minimap-combo-collapsed.spec.ts",
    "content": "import { createGraph, sleep } from '@@/utils';\n\ndescribe('bug: plugin-minimap-combo-collapsed', () => {\n  it('should be collapsed', async () => {\n    const graph = createGraph({\n      animation: false,\n      data: {\n        nodes: [{ id: 'node1', combo: 'combo1' }, { id: 'node2' }],\n        edges: [{ source: 'node1', target: 'node2' }],\n        combos: [{ id: 'combo1' }],\n      },\n      layout: {\n        type: 'grid',\n      },\n      plugins: [\n        {\n          key: 'minimap',\n          type: 'minimap',\n          size: [240, 160],\n        },\n      ],\n    });\n\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename);\n\n    await sleep(1000);\n    graph.collapseElement('combo1');\n    graph.translateElementBy('combo1', [100, 100]);\n\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename, 'update-collapsed-combo');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/render-change-combo.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('render change combo', () => {\n  it('bug', async () => {\n    const graph = createGraph({});\n    await graph.render();\n\n    let count = 1;\n    const operation = async () => {\n      graph.setData({\n        nodes: [\n          { id: 'node1', combo: `${count}A`, style: { x: 250, y: 150 } },\n          { id: 'node2', combo: `${count}b`, style: { x: 350, y: 150 } },\n          { id: 'node3', style: { x: 250, y: 300 } },\n        ],\n        edges: [],\n        combos: [\n          { id: `${count}A`, style: { labelText: `${count}A` } },\n          { id: `${count}b`, style: { labelText: `${count}B` } },\n        ],\n      });\n      await graph.render();\n      count++;\n    };\n\n    await operation();\n\n    await operation();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/render-deleted-data.spec.ts",
    "content": "import { layoutCompactBoxBasic } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('render deleted data', () => {\n  it('bug', async () => {\n    const graph = await createDemoGraph(layoutCompactBoxBasic);\n\n    const render = jest.fn(async () => {\n      graph.removeNodeData(['Classification']);\n      await graph.render();\n    });\n\n    expect(render).not.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/tree-update-collapsed-node.spec.ts",
    "content": "import { idOf, treeToGraphData } from '@/src';\nimport { createGraph } from '@@/utils';\n\ndescribe('bug: tree-update-collapsed-node', () => {\n  it('should be collapsed', async () => {\n    const graph = createGraph({\n      animation: false,\n      x: 50,\n      y: 50,\n      data: treeToGraphData({\n        id: 'root',\n        children: [\n          { id: '1-1', style: { collapsed: true }, children: [{ id: '1-1-1' }] },\n          { id: '1-2', children: [{ id: '1-2-1' }] },\n        ],\n      }),\n      layout: {\n        type: 'indented',\n      },\n    });\n\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename);\n\n    // set parent of 1-2 to 1-1\n    const edges = graph\n      .getEdgeData()\n      .filter((edge) => edge.target === '1-2')\n      .map(idOf);\n    graph.removeEdgeData(edges);\n    graph.updateNodeData([\n      { id: 'root', children: ['1-1'] },\n      { id: '1-1', children: ['1-1-1', '1-2'] },\n    ]);\n    graph.addEdgeData([{ source: '1-1', target: '1-2' }]);\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename, 'update collapsed node');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/bugs/utils-set-visibility.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('bug: utils-set-visibility', () => {\n  it('should set correct', async () => {\n    const graph = createGraph({\n      animation: true,\n      data: {\n        nodes: [\n          {\n            id: 'node-0',\n            style: {\n              x: 100,\n              y: 100,\n              labelText: 'label',\n              iconText: 'icon',\n              badges: [{ text: 'b1', placement: 'right-top' }],\n            },\n          },\n        ],\n      },\n    });\n\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename);\n\n    await graph.hideElement('node-0');\n\n    await expect(graph).toMatchSnapshot(__filename, 'hidden');\n\n    await graph.showElement('node-0');\n\n    await expect(graph).toMatchSnapshot(__filename, 'visible');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/algorithm-category.json",
    "content": "{\n  \"id\": \"Modeling Methods\",\n  \"children\": [\n    {\n      \"id\": \"Classification\",\n      \"children\": [\n        { \"id\": \"Logistic regression\" },\n        { \"id\": \"Linear discriminant analysis\" },\n        { \"id\": \"Rules\" },\n        { \"id\": \"Decision trees\" },\n        { \"id\": \"Naive Bayes\" },\n        { \"id\": \"K nearest neighbor\" },\n        { \"id\": \"Probabilistic neural network\" },\n        { \"id\": \"Support vector machine\" }\n      ]\n    },\n    {\n      \"id\": \"Consensus\",\n      \"children\": [\n        {\n          \"id\": \"Models diversity\",\n          \"children\": [\n            { \"id\": \"Different initializations\" },\n            { \"id\": \"Different parameter choices\" },\n            { \"id\": \"Different architectures\" },\n            { \"id\": \"Different modeling methods\" },\n            { \"id\": \"Different training sets\" },\n            { \"id\": \"Different feature sets\" }\n          ]\n        },\n        {\n          \"id\": \"Methods\",\n          \"children\": [{ \"id\": \"Classifier selection\" }, { \"id\": \"Classifier fusion\" }]\n        },\n        {\n          \"id\": \"Common\",\n          \"children\": [{ \"id\": \"Bagging\" }, { \"id\": \"Boosting\" }, { \"id\": \"AdaBoost\" }]\n        }\n      ]\n    },\n    {\n      \"id\": \"Regression\",\n      \"children\": [\n        { \"id\": \"Multiple linear regression\" },\n        { \"id\": \"Partial least squares\" },\n        { \"id\": \"Multi-layer feed forward neural network\" },\n        { \"id\": \"General regression neural network\" },\n        { \"id\": \"Support vector regression\" }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/circular.json",
    "content": "{\n  \"nodes\": [\n    {\n      \"id\": \"0\",\n      \"label\": \"0\"\n    },\n    {\n      \"id\": \"1\",\n      \"label\": \"1\"\n    },\n    {\n      \"id\": \"2\",\n      \"label\": \"2\"\n    },\n    {\n      \"id\": \"3\",\n      \"label\": \"3\"\n    },\n    {\n      \"id\": \"4\",\n      \"label\": \"4\"\n    },\n    {\n      \"id\": \"5\",\n      \"label\": \"5\"\n    },\n    {\n      \"id\": \"6\",\n      \"label\": \"6\"\n    },\n    {\n      \"id\": \"7\",\n      \"label\": \"7\"\n    },\n    {\n      \"id\": \"8\",\n      \"label\": \"8\"\n    },\n    {\n      \"id\": \"9\",\n      \"label\": \"9\"\n    },\n    {\n      \"id\": \"10\",\n      \"label\": \"10\"\n    },\n    {\n      \"id\": \"11\",\n      \"label\": \"11\"\n    },\n    {\n      \"id\": \"12\",\n      \"label\": \"12\"\n    },\n    {\n      \"id\": \"13\",\n      \"label\": \"13\"\n    },\n    {\n      \"id\": \"14\",\n      \"label\": \"14\"\n    },\n    {\n      \"id\": \"15\",\n      \"label\": \"15\"\n    },\n    {\n      \"id\": \"16\",\n      \"label\": \"16\"\n    },\n    {\n      \"id\": \"17\",\n      \"label\": \"17\"\n    },\n    {\n      \"id\": \"18\",\n      \"label\": \"18\"\n    },\n    {\n      \"id\": \"19\",\n      \"label\": \"19\"\n    },\n    {\n      \"id\": \"20\",\n      \"label\": \"20\"\n    },\n    {\n      \"id\": \"21\",\n      \"label\": \"21\"\n    },\n    {\n      \"id\": \"22\",\n      \"label\": \"22\"\n    },\n    {\n      \"id\": \"23\",\n      \"label\": \"23\"\n    },\n    {\n      \"id\": \"24\",\n      \"label\": \"24\"\n    },\n    {\n      \"id\": \"25\",\n      \"label\": \"25\"\n    },\n    {\n      \"id\": \"26\",\n      \"label\": \"26\"\n    },\n    {\n      \"id\": \"27\",\n      \"label\": \"27\"\n    },\n    {\n      \"id\": \"28\",\n      \"label\": \"28\"\n    },\n    {\n      \"id\": \"29\",\n      \"label\": \"29\"\n    },\n    {\n      \"id\": \"30\",\n      \"label\": \"30\"\n    },\n    {\n      \"id\": \"31\",\n      \"label\": \"31\"\n    },\n    {\n      \"id\": \"32\",\n      \"label\": \"32\"\n    },\n    {\n      \"id\": \"33\",\n      \"label\": \"33\"\n    }\n  ],\n  \"edges\": [\n    {\n      \"source\": \"0\",\n      \"target\": \"1\"\n    },\n    {\n      \"source\": \"0\",\n      \"target\": \"2\"\n    },\n    {\n      \"source\": \"0\",\n      \"target\": \"3\"\n    },\n    {\n      \"source\": \"0\",\n      \"target\": \"4\"\n    },\n    {\n      \"source\": \"0\",\n      \"target\": \"5\"\n    },\n    {\n      \"source\": \"0\",\n      \"target\": \"7\"\n    },\n    {\n      \"source\": \"0\",\n      \"target\": \"8\"\n    },\n    {\n      \"source\": \"0\",\n      \"target\": \"9\"\n    },\n    {\n      \"source\": \"0\",\n      \"target\": \"10\"\n    },\n    {\n      \"source\": \"0\",\n      \"target\": \"11\"\n    },\n    {\n      \"source\": \"0\",\n      \"target\": \"13\"\n    },\n    {\n      \"source\": \"0\",\n      \"target\": \"14\"\n    },\n    {\n      \"source\": \"0\",\n      \"target\": \"15\"\n    },\n    {\n      \"source\": \"0\",\n      \"target\": \"16\"\n    },\n    {\n      \"source\": \"2\",\n      \"target\": \"3\"\n    },\n    {\n      \"source\": \"4\",\n      \"target\": \"5\"\n    },\n    {\n      \"source\": \"4\",\n      \"target\": \"6\"\n    },\n    {\n      \"source\": \"5\",\n      \"target\": \"6\"\n    },\n    {\n      \"source\": \"7\",\n      \"target\": \"13\"\n    },\n    {\n      \"source\": \"8\",\n      \"target\": \"14\"\n    },\n    {\n      \"source\": \"9\",\n      \"target\": \"10\"\n    },\n    {\n      \"source\": \"10\",\n      \"target\": \"22\"\n    },\n    {\n      \"source\": \"10\",\n      \"target\": \"14\"\n    },\n    {\n      \"source\": \"10\",\n      \"target\": \"12\"\n    },\n    {\n      \"source\": \"10\",\n      \"target\": \"24\"\n    },\n    {\n      \"source\": \"10\",\n      \"target\": \"21\"\n    },\n    {\n      \"source\": \"10\",\n      \"target\": \"20\"\n    },\n    {\n      \"source\": \"11\",\n      \"target\": \"24\"\n    },\n    {\n      \"source\": \"11\",\n      \"target\": \"22\"\n    },\n    {\n      \"source\": \"11\",\n      \"target\": \"14\"\n    },\n    {\n      \"source\": \"12\",\n      \"target\": \"13\"\n    },\n    {\n      \"source\": \"16\",\n      \"target\": \"17\"\n    },\n    {\n      \"source\": \"16\",\n      \"target\": \"18\"\n    },\n    {\n      \"source\": \"16\",\n      \"target\": \"21\"\n    },\n    {\n      \"source\": \"16\",\n      \"target\": \"22\"\n    },\n    {\n      \"source\": \"17\",\n      \"target\": \"18\"\n    },\n    {\n      \"source\": \"17\",\n      \"target\": \"20\"\n    },\n    {\n      \"source\": \"18\",\n      \"target\": \"19\"\n    },\n    {\n      \"source\": \"19\",\n      \"target\": \"20\"\n    },\n    {\n      \"source\": \"19\",\n      \"target\": \"33\"\n    },\n    {\n      \"source\": \"19\",\n      \"target\": \"22\"\n    },\n    {\n      \"source\": \"19\",\n      \"target\": \"23\"\n    },\n    {\n      \"source\": \"20\",\n      \"target\": \"21\"\n    },\n    {\n      \"source\": \"21\",\n      \"target\": \"22\"\n    },\n    {\n      \"source\": \"22\",\n      \"target\": \"24\"\n    },\n    {\n      \"source\": \"22\",\n      \"target\": \"25\"\n    },\n    {\n      \"source\": \"22\",\n      \"target\": \"26\"\n    },\n    {\n      \"source\": \"22\",\n      \"target\": \"23\"\n    },\n    {\n      \"source\": \"22\",\n      \"target\": \"28\"\n    },\n    {\n      \"source\": \"22\",\n      \"target\": \"30\"\n    },\n    {\n      \"source\": \"22\",\n      \"target\": \"31\"\n    },\n    {\n      \"source\": \"22\",\n      \"target\": \"32\"\n    },\n    {\n      \"source\": \"22\",\n      \"target\": \"33\"\n    },\n    {\n      \"source\": \"23\",\n      \"target\": \"28\"\n    },\n    {\n      \"source\": \"23\",\n      \"target\": \"27\"\n    },\n    {\n      \"source\": \"23\",\n      \"target\": \"29\"\n    },\n    {\n      \"source\": \"23\",\n      \"target\": \"30\"\n    },\n    {\n      \"source\": \"23\",\n      \"target\": \"31\"\n    },\n    {\n      \"source\": \"23\",\n      \"target\": \"33\"\n    },\n    {\n      \"source\": \"32\",\n      \"target\": \"33\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/cluster.json",
    "content": "{\n  \"nodes\": [\n    { \"id\": \"0\", \"data\": { \"cluster\": \"a\" } },\n    { \"id\": \"1\", \"data\": { \"cluster\": \"a\" } },\n    { \"id\": \"2\", \"data\": { \"cluster\": \"a\" } },\n    { \"id\": \"3\", \"data\": { \"cluster\": \"a\" } },\n    { \"id\": \"4\", \"data\": { \"cluster\": \"a\" } },\n    { \"id\": \"5\", \"data\": { \"cluster\": \"a\" } },\n    { \"id\": \"6\", \"data\": { \"cluster\": \"a\" } },\n    { \"id\": \"7\", \"data\": { \"cluster\": \"a\" } },\n    { \"id\": \"8\", \"data\": { \"cluster\": \"a\" } },\n    { \"id\": \"9\", \"data\": { \"cluster\": \"a\" } },\n    { \"id\": \"10\", \"data\": { \"cluster\": \"a\" } },\n    { \"id\": \"11\", \"data\": { \"cluster\": \"a\" } },\n    { \"id\": \"12\", \"data\": { \"cluster\": \"a\" } },\n    { \"id\": \"13\", \"data\": { \"cluster\": \"b\" } },\n    { \"id\": \"14\", \"data\": { \"cluster\": \"b\" } },\n    { \"id\": \"15\", \"data\": { \"cluster\": \"b\" } },\n    { \"id\": \"16\", \"data\": { \"cluster\": \"b\" } },\n    { \"id\": \"17\", \"data\": { \"cluster\": \"b\" } },\n    { \"id\": \"18\", \"data\": { \"cluster\": \"c\" } },\n    { \"id\": \"19\", \"data\": { \"cluster\": \"c\" } },\n    { \"id\": \"20\", \"data\": { \"cluster\": \"c\" } },\n    { \"id\": \"21\", \"data\": { \"cluster\": \"c\" } },\n    { \"id\": \"22\", \"data\": { \"cluster\": \"c\" } },\n    { \"id\": \"23\", \"data\": { \"cluster\": \"c\" } },\n    { \"id\": \"24\", \"data\": { \"cluster\": \"c\" } },\n    { \"id\": \"25\", \"data\": { \"cluster\": \"c\" } },\n    { \"id\": \"26\", \"data\": { \"cluster\": \"c\" } },\n    { \"id\": \"27\", \"data\": { \"cluster\": \"c\" } },\n    { \"id\": \"28\", \"data\": { \"cluster\": \"c\" } },\n    { \"id\": \"29\", \"data\": { \"cluster\": \"c\" } },\n    { \"id\": \"30\", \"data\": { \"cluster\": \"c\" } },\n    { \"id\": \"31\", \"data\": { \"cluster\": \"d\" } },\n    { \"id\": \"32\", \"data\": { \"cluster\": \"d\" } },\n    { \"id\": \"33\", \"data\": { \"cluster\": \"d\" } }\n  ],\n  \"edges\": [\n    { \"source\": \"0\", \"target\": \"1\" },\n    { \"source\": \"0\", \"target\": \"2\" },\n    { \"source\": \"0\", \"target\": \"3\" },\n    { \"source\": \"0\", \"target\": \"4\" },\n    { \"source\": \"0\", \"target\": \"5\" },\n    { \"source\": \"0\", \"target\": \"7\" },\n    { \"source\": \"0\", \"target\": \"8\" },\n    { \"source\": \"0\", \"target\": \"9\" },\n    { \"source\": \"0\", \"target\": \"10\" },\n    { \"source\": \"0\", \"target\": \"11\" },\n    { \"source\": \"0\", \"target\": \"13\" },\n    { \"source\": \"0\", \"target\": \"14\" },\n    { \"source\": \"0\", \"target\": \"15\" },\n    { \"source\": \"0\", \"target\": \"16\" },\n    { \"source\": \"2\", \"target\": \"3\" },\n    { \"source\": \"4\", \"target\": \"5\" },\n    { \"source\": \"4\", \"target\": \"6\" },\n    { \"source\": \"5\", \"target\": \"6\" },\n    { \"source\": \"7\", \"target\": \"13\" },\n    { \"source\": \"8\", \"target\": \"14\" },\n    { \"source\": \"9\", \"target\": \"10\" },\n    { \"source\": \"10\", \"target\": \"22\" },\n    { \"source\": \"10\", \"target\": \"14\" },\n    { \"source\": \"10\", \"target\": \"12\" },\n    { \"source\": \"10\", \"target\": \"24\" },\n    { \"source\": \"10\", \"target\": \"21\" },\n    { \"source\": \"10\", \"target\": \"20\" },\n    { \"source\": \"11\", \"target\": \"24\" },\n    { \"source\": \"11\", \"target\": \"22\" },\n    { \"source\": \"11\", \"target\": \"14\" },\n    { \"source\": \"12\", \"target\": \"13\" },\n    { \"source\": \"16\", \"target\": \"17\" },\n    { \"source\": \"16\", \"target\": \"18\" },\n    { \"source\": \"16\", \"target\": \"21\" },\n    { \"source\": \"16\", \"target\": \"22\" },\n    { \"source\": \"17\", \"target\": \"18\" },\n    { \"source\": \"17\", \"target\": \"20\" },\n    { \"source\": \"18\", \"target\": \"19\" },\n    { \"source\": \"19\", \"target\": \"20\" },\n    { \"source\": \"19\", \"target\": \"33\" },\n    { \"source\": \"19\", \"target\": \"22\" },\n    { \"source\": \"19\", \"target\": \"23\" },\n    { \"source\": \"20\", \"target\": \"21\" },\n    { \"source\": \"21\", \"target\": \"22\" },\n    { \"source\": \"22\", \"target\": \"24\" },\n    { \"source\": \"22\", \"target\": \"25\" },\n    { \"source\": \"22\", \"target\": \"26\" },\n    { \"source\": \"22\", \"target\": \"23\" },\n    { \"source\": \"22\", \"target\": \"28\" },\n    { \"source\": \"22\", \"target\": \"30\" },\n    { \"source\": \"22\", \"target\": \"31\" },\n    { \"source\": \"22\", \"target\": \"32\" },\n    { \"source\": \"22\", \"target\": \"33\" },\n    { \"source\": \"23\", \"target\": \"28\" },\n    { \"source\": \"23\", \"target\": \"27\" },\n    { \"source\": \"23\", \"target\": \"29\" },\n    { \"source\": \"23\", \"target\": \"30\" },\n    { \"source\": \"23\", \"target\": \"31\" },\n    { \"source\": \"23\", \"target\": \"33\" },\n    { \"source\": \"32\", \"target\": \"33\" }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/combo.json",
    "content": "{\n  \"nodes\": [\n    { \"id\": \"0\", \"combo\": \"a\" },\n    { \"id\": \"1\", \"combo\": \"a\" },\n    { \"id\": \"2\", \"combo\": \"a\" },\n    { \"id\": \"3\", \"combo\": \"a\" },\n    { \"id\": \"4\", \"combo\": \"a\" },\n    { \"id\": \"5\", \"combo\": \"a\" },\n    { \"id\": \"6\", \"combo\": \"a\" },\n    { \"id\": \"7\", \"combo\": \"a\" },\n    { \"id\": \"8\", \"combo\": \"a\" },\n    { \"id\": \"9\", \"combo\": \"a\" },\n    { \"id\": \"10\", \"combo\": \"a\" },\n    { \"id\": \"11\", \"combo\": \"a\" },\n    { \"id\": \"12\", \"combo\": \"a\" },\n    { \"id\": \"13\", \"combo\": \"a\" },\n    { \"id\": \"14\", \"combo\": \"a\" },\n    { \"id\": \"15\", \"combo\": \"a\" },\n    { \"id\": \"16\", \"combo\": \"b\" },\n    { \"id\": \"17\", \"combo\": \"b\" },\n    { \"id\": \"18\", \"combo\": \"b\" },\n    { \"id\": \"19\", \"combo\": \"b\" },\n    { \"id\": \"20\" },\n    { \"id\": \"21\" },\n    { \"id\": \"22\" },\n    { \"id\": \"23\", \"combo\": \"c\" },\n    { \"id\": \"24\", \"combo\": \"a\" },\n    { \"id\": \"25\" },\n    { \"id\": \"26\" },\n    { \"id\": \"27\", \"combo\": \"c\" },\n    { \"id\": \"28\", \"combo\": \"c\" },\n    { \"id\": \"29\", \"combo\": \"c\" },\n    { \"id\": \"30\", \"combo\": \"c\" },\n    { \"id\": \"31\", \"combo\": \"c\" },\n    { \"id\": \"32\", \"combo\": \"d\" },\n    { \"id\": \"33\", \"combo\": \"d\" }\n  ],\n  \"edges\": [\n    { \"source\": \"0\", \"target\": \"1\" },\n    { \"source\": \"0\", \"target\": \"2\" },\n    { \"source\": \"0\", \"target\": \"3\" },\n    { \"source\": \"0\", \"target\": \"4\" },\n    { \"source\": \"0\", \"target\": \"5\" },\n    { \"source\": \"0\", \"target\": \"7\" },\n    { \"source\": \"0\", \"target\": \"8\" },\n    { \"source\": \"0\", \"target\": \"9\" },\n    { \"source\": \"0\", \"target\": \"10\" },\n    { \"source\": \"0\", \"target\": \"11\" },\n    { \"source\": \"0\", \"target\": \"13\" },\n    { \"source\": \"0\", \"target\": \"14\" },\n    { \"source\": \"0\", \"target\": \"15\" },\n    { \"source\": \"0\", \"target\": \"16\" },\n    { \"source\": \"2\", \"target\": \"3\" },\n    { \"source\": \"4\", \"target\": \"5\" },\n    { \"source\": \"4\", \"target\": \"6\" },\n    { \"source\": \"5\", \"target\": \"6\" },\n    { \"source\": \"7\", \"target\": \"13\" },\n    { \"source\": \"8\", \"target\": \"14\" },\n    { \"source\": \"9\", \"target\": \"10\" },\n    { \"source\": \"10\", \"target\": \"22\" },\n    { \"source\": \"10\", \"target\": \"14\" },\n    { \"source\": \"10\", \"target\": \"12\" },\n    { \"source\": \"10\", \"target\": \"24\" },\n    { \"source\": \"10\", \"target\": \"21\" },\n    { \"source\": \"10\", \"target\": \"20\" },\n    { \"source\": \"11\", \"target\": \"24\" },\n    { \"source\": \"11\", \"target\": \"22\" },\n    { \"source\": \"11\", \"target\": \"14\" },\n    { \"source\": \"12\", \"target\": \"13\" },\n    { \"source\": \"16\", \"target\": \"17\" },\n    { \"source\": \"16\", \"target\": \"18\" },\n    { \"source\": \"16\", \"target\": \"21\" },\n    { \"source\": \"16\", \"target\": \"22\" },\n    { \"source\": \"17\", \"target\": \"18\" },\n    { \"source\": \"17\", \"target\": \"20\" },\n    { \"source\": \"18\", \"target\": \"19\" },\n    { \"source\": \"19\", \"target\": \"20\" },\n    { \"source\": \"19\", \"target\": \"33\" },\n    { \"source\": \"19\", \"target\": \"22\" },\n    { \"source\": \"19\", \"target\": \"23\" },\n    { \"source\": \"20\", \"target\": \"21\" },\n    { \"source\": \"21\", \"target\": \"22\" },\n    { \"source\": \"22\", \"target\": \"24\" },\n    { \"source\": \"22\", \"target\": \"25\" },\n    { \"source\": \"22\", \"target\": \"26\" },\n    { \"source\": \"22\", \"target\": \"23\" },\n    { \"source\": \"22\", \"target\": \"28\" },\n    { \"source\": \"22\", \"target\": \"30\" },\n    { \"source\": \"22\", \"target\": \"31\" },\n    { \"source\": \"22\", \"target\": \"32\" },\n    { \"source\": \"22\", \"target\": \"33\" },\n    { \"source\": \"23\", \"target\": \"28\" },\n    { \"source\": \"23\", \"target\": \"27\" },\n    { \"source\": \"23\", \"target\": \"29\" },\n    { \"source\": \"23\", \"target\": \"30\" },\n    { \"source\": \"23\", \"target\": \"31\" },\n    { \"source\": \"23\", \"target\": \"33\" },\n    { \"source\": \"32\", \"target\": \"33\" }\n  ],\n  \"combos\": [\n    { \"id\": \"a\", \"data\": { \"label\": \"Combo A\" } },\n    { \"id\": \"b\", \"data\": { \"label\": \"Combo B\" } },\n    { \"id\": \"c\", \"data\": { \"label\": \"Combo D\" } },\n    { \"id\": \"d\", \"combo\": \"b\", \"data\": { \"label\": \"Combo D\" } }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/dagre-combo.json",
    "content": "{\n  \"nodes\": [\n    { \"id\": \"0\" },\n    { \"id\": \"1\" },\n    { \"id\": \"2\" },\n    { \"id\": \"3\" },\n    { \"id\": \"4\", \"combo\": \"A\" },\n    { \"id\": \"5\", \"combo\": \"B\" },\n    { \"id\": \"6\", \"combo\": \"A\" },\n    { \"id\": \"7\", \"combo\": \"C\" },\n    { \"id\": \"8\", \"combo\": \"C\" },\n    { \"id\": \"9\", \"combo\": \"A\" },\n    { \"id\": \"10\", \"combo\": \"B\" },\n    { \"id\": \"11\", \"combo\": \"B\" }\n  ],\n  \"edges\": [\n    { \"id\": \"edge-102\", \"source\": \"0\", \"target\": \"1\" },\n    { \"id\": \"edge-161\", \"source\": \"0\", \"target\": \"2\" },\n    { \"id\": \"edge-237\", \"source\": \"1\", \"target\": \"4\" },\n    { \"id\": \"edge-253\", \"source\": \"0\", \"target\": \"3\" },\n    { \"id\": \"edge-133\", \"source\": \"3\", \"target\": \"4\" },\n    { \"id\": \"edge-320\", \"source\": \"2\", \"target\": \"5\" },\n    { \"id\": \"edge-355\", \"source\": \"1\", \"target\": \"6\" },\n    { \"id\": \"edge-823\", \"source\": \"1\", \"target\": \"7\" },\n    { \"id\": \"edge-665\", \"source\": \"3\", \"target\": \"8\" },\n    { \"id\": \"edge-884\", \"source\": \"3\", \"target\": \"9\" },\n    { \"id\": \"edge-536\", \"source\": \"5\", \"target\": \"10\" },\n    { \"id\": \"edge-401\", \"source\": \"5\", \"target\": \"11\" }\n  ],\n  \"combos\": [\n    { \"id\": \"A\", \"style\": { \"type\": \"rect\" } },\n    { \"id\": \"B\", \"style\": { \"type\": \"rect\" } },\n    { \"id\": \"C\", \"style\": { \"type\": \"rect\" } }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/dagre.json",
    "content": "{\n  \"nodes\": [\n    { \"id\": \"0\", \"data\": { \"label\": \"0\" } },\n    { \"id\": \"1\", \"data\": { \"label\": \"1\" } },\n    { \"id\": \"2\", \"data\": { \"label\": \"2\" } },\n    { \"id\": \"3\", \"data\": { \"label\": \"3\" } },\n    { \"id\": \"4\", \"data\": { \"label\": \"4\" } },\n    { \"id\": \"5\", \"data\": { \"label\": \"5\" } },\n    { \"id\": \"6\", \"data\": { \"label\": \"6\" } },\n    { \"id\": \"7\", \"data\": { \"label\": \"7\" } },\n    { \"id\": \"8\", \"data\": { \"label\": \"8\" } },\n    { \"id\": \"9\", \"data\": { \"label\": \"9\" } }\n  ],\n  \"edges\": [\n    { \"source\": \"0\", \"target\": \"1\", \"data\": {} },\n    { \"source\": \"0\", \"target\": \"2\", \"data\": {} },\n    { \"source\": \"1\", \"target\": \"4\", \"data\": {} },\n    { \"source\": \"0\", \"target\": \"3\", \"data\": {} },\n    { \"source\": \"3\", \"target\": \"4\", \"data\": {} },\n    { \"source\": \"4\", \"target\": \"5\", \"data\": {} },\n    { \"source\": \"4\", \"target\": \"6\", \"data\": {} },\n    { \"source\": \"5\", \"target\": \"7\", \"data\": {} },\n    { \"source\": \"5\", \"target\": \"8\", \"data\": {} },\n    { \"source\": \"8\", \"target\": \"9\", \"data\": {} },\n    { \"source\": \"2\", \"target\": \"9\", \"data\": {} },\n    { \"source\": \"3\", \"target\": \"9\", \"data\": {} }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/decision-tree.json",
    "content": "{\n  \"id\": \"g1\",\n  \"name\": \"Name1\",\n  \"count\": 123456,\n  \"label\": \"538.90\",\n  \"currency\": \"Yuan\",\n  \"rate\": 1.0,\n  \"status\": \"B\",\n  \"variableName\": \"V1\",\n  \"variableValue\": 0.341,\n  \"variableUp\": false,\n  \"children\": [\n    {\n      \"id\": \"g12\",\n      \"name\": \"Deal with LONG label LONG label LONG label LONG label\",\n      \"count\": 123456,\n      \"label\": \"338.00\",\n      \"rate\": 0.627,\n      \"status\": \"R\",\n      \"currency\": \"Yuan\",\n      \"variableName\": \"V2\",\n      \"variableValue\": 0.179,\n      \"variableUp\": true,\n      \"children\": [\n        {\n          \"id\": \"g121\",\n          \"name\": \"Name3\",\n          \"collapsed\": true,\n          \"count\": 123456,\n          \"label\": \"138.00\",\n          \"rate\": 0.123,\n          \"status\": \"B\",\n          \"currency\": \"Yuan\",\n          \"variableName\": \"V2\",\n          \"variableValue\": 0.27,\n          \"variableUp\": true,\n          \"children\": [\n            {\n              \"id\": \"g1211\",\n              \"name\": \"Name4\",\n              \"count\": 123456,\n              \"label\": \"138.00\",\n              \"rate\": 1.0,\n              \"status\": \"B\",\n              \"currency\": \"Yuan\",\n              \"variableName\": \"V1\",\n              \"variableValue\": 0.164,\n              \"variableUp\": false,\n              \"children\": []\n            }\n          ]\n        },\n        {\n          \"id\": \"g122\",\n          \"name\": \"Name5\",\n          \"collapsed\": true,\n          \"count\": 123456,\n          \"label\": \"100.00\",\n          \"rate\": 0.296,\n          \"status\": \"G\",\n          \"currency\": \"Yuan\",\n          \"variableName\": \"V1\",\n          \"variableValue\": 0.259,\n          \"variableUp\": true,\n          \"children\": [\n            {\n              \"id\": \"g1221\",\n              \"name\": \"Name6\",\n              \"count\": 123456,\n              \"label\": \"40.00\",\n              \"rate\": 0.4,\n              \"status\": \"G\",\n              \"currency\": \"Yuan\",\n              \"variableName\": \"V1\",\n              \"variableValue\": 0.135,\n              \"variableUp\": true,\n              \"children\": [\n                {\n                  \"id\": \"g12211\",\n                  \"name\": \"Name6-1\",\n                  \"count\": 123456,\n                  \"label\": \"40.00\",\n                  \"rate\": 1.0,\n                  \"status\": \"R\",\n                  \"currency\": \"Yuan\",\n                  \"variableName\": \"V1\",\n                  \"variableValue\": 0.181,\n                  \"variableUp\": true,\n                  \"children\": []\n                }\n              ]\n            },\n            {\n              \"id\": \"g1222\",\n              \"name\": \"Name7\",\n              \"count\": 123456,\n              \"label\": \"60.00\",\n              \"rate\": 0.6,\n              \"status\": \"G\",\n              \"currency\": \"Yuan\",\n              \"variableName\": \"V1\",\n              \"variableValue\": 0.239,\n              \"variableUp\": false,\n              \"children\": []\n            }\n          ]\n        },\n        {\n          \"id\": \"g123\",\n          \"name\": \"Name8\",\n          \"collapsed\": true,\n          \"count\": 123456,\n          \"label\": \"100.00\",\n          \"rate\": 0.296,\n          \"status\": \"DI\",\n          \"currency\": \"Yuan\",\n          \"variableName\": \"V2\",\n          \"variableValue\": 0.131,\n          \"variableUp\": false,\n          \"children\": [\n            {\n              \"id\": \"g1231\",\n              \"name\": \"Name8-1\",\n              \"count\": 123456,\n              \"label\": \"100.00\",\n              \"rate\": 1.0,\n              \"status\": \"DI\",\n              \"currency\": \"Yuan\",\n              \"variableName\": \"V2\",\n              \"variableValue\": 0.131,\n              \"variableUp\": false,\n              \"children\": []\n            }\n          ]\n        }\n      ]\n    },\n    {\n      \"id\": \"g13\",\n      \"name\": \"Name9\",\n      \"count\": 123456,\n      \"label\": \"100.90\",\n      \"rate\": 0.187,\n      \"status\": \"B\",\n      \"currency\": \"Yuan\",\n      \"variableName\": \"V2\",\n      \"variableValue\": 0.221,\n      \"variableUp\": true,\n      \"children\": [\n        {\n          \"id\": \"g131\",\n          \"name\": \"Name10\",\n          \"count\": 123456,\n          \"label\": \"33.90\",\n          \"rate\": 0.336,\n          \"status\": \"R\",\n          \"currency\": \"Yuan\",\n          \"variableName\": \"V1\",\n          \"variableValue\": 0.12,\n          \"variableUp\": true,\n          \"children\": []\n        },\n        {\n          \"id\": \"g132\",\n          \"name\": \"Name11\",\n          \"count\": 123456,\n          \"label\": \"67.00\",\n          \"rate\": 0.664,\n          \"status\": \"G\",\n          \"currency\": \"Yuan\",\n          \"variableName\": \"V1\",\n          \"variableValue\": 0.241,\n          \"variableUp\": false,\n          \"children\": []\n        }\n      ]\n    },\n    {\n      \"id\": \"g14\",\n      \"name\": \"Name12\",\n      \"count\": 123456,\n      \"label\": \"100.00\",\n      \"rate\": 0.186,\n      \"status\": \"G\",\n      \"currency\": \"Yuan\",\n      \"variableName\": \"V2\",\n      \"variableValue\": 0.531,\n      \"variableUp\": true,\n      \"children\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/element-edges.json",
    "content": "{\n  \"nodes\": [\n    { \"id\": \"node1\" },\n    { \"id\": \"node2\" },\n    { \"id\": \"node3\" },\n    { \"id\": \"node4\" },\n    { \"id\": \"node5\" },\n    { \"id\": \"node6\" }\n  ],\n  \"edges\": [\n    { \"id\": \"line-default\", \"source\": \"node1\", \"target\": \"node2\" },\n    { \"id\": \"line-active\", \"source\": \"node1\", \"target\": \"node3\", \"states\": [\"active\"] },\n    { \"id\": \"line-selected\", \"source\": \"node1\", \"target\": \"node4\", \"states\": [\"selected\"] },\n    { \"id\": \"line-highlight\", \"source\": \"node1\", \"target\": \"node5\", \"states\": [\"highlight\"] },\n    { \"id\": \"line-inactive\", \"source\": \"node1\", \"target\": \"node6\", \"states\": [\"inactive\"] }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/element-nodes.json",
    "content": "{\n  \"nodes\": [\n    { \"id\": \"default\" },\n    { \"id\": \"halo\", \"style\": { \"halo\": true } },\n    {\n      \"id\": \"badges\",\n      \"style\": {\n        \"badges\": [\n          {\n            \"text\": \"\\ue603\",\n            \"fontFamily\": \"iconfont\",\n            \"backgroundWidth\": 12,\n            \"backgroundHeight\": 12,\n            \"placement\": \"right-top\",\n            \"offsetX\": -3\n          },\n          { \"text\": \"Important\", \"placement\": \"right\" },\n          { \"text\": \"Notice\", \"placement\": \"right-bottom\" }\n        ],\n        \"badgeFontSize\": 8,\n        \"badgePadding\": [1, 4]\n      }\n    },\n    {\n      \"id\": \"icon\",\n      \"style\": {\n        \"iconSrc\": \"https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg\"\n      }\n    },\n    {\n      \"id\": \"ports\",\n      \"style\": {\n        \"portR\": 3,\n        \"ports\": [{ \"placement\": \"left\" }, { \"placement\": \"right\" }, { \"placement\": \"top\" }, { \"placement\": \"bottom\" }]\n      }\n    },\n    { \"id\": \"active\", \"states\": [\"active\"] },\n    { \"id\": \"selected\", \"states\": [\"selected\"] },\n    { \"id\": \"highlight\", \"states\": [\"highlight\"] },\n    { \"id\": \"inactive\", \"states\": [\"inactive\"] },\n    {\n      \"id\": \"disabled\",\n      \"states\": [\"disabled\"]\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/file-system.json",
    "content": "{\n  \"id\": \"src\",\n  \"children\": [\n    { \"id\": \"animations\" },\n    { \"id\": \"behaviors\" },\n    { \"id\": \"elements\", \"children\": [{ \"id\": \"nodes\" }, { \"id\": \"edges\" }, { \"id\": \"combos\" }] },\n    { \"id\": \"layouts\" },\n    {\n      \"id\": \"runtime\",\n      \"children\": [\n        { \"id\": \"canvas\" },\n        { \"id\": \"data\" },\n        { \"id\": \"element\" },\n        { \"id\": \"graph\" },\n        { \"id\": \"layout\" },\n        { \"id\": \"viewport\" }\n      ]\n    },\n    { \"id\": \"spec\" },\n    { \"id\": \"themes\", \"children\": [{ \"id\": \"dark\" }, { \"id\": \"light\" }] }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/flare.json",
    "content": "{\n  \"id\": \"flare\",\n  \"children\": [\n    {\n      \"id\": \"analytics\",\n      \"children\": [\n        {\n          \"id\": \"cluster\",\n          \"children\": [\n            { \"id\": \"AgglomerativeCluster\", \"value\": 3938 },\n            { \"id\": \"CommunityStructure\", \"value\": 3812 },\n            { \"id\": \"HierarchicalCluster\", \"value\": 6714 },\n            { \"id\": \"MergeEdge\", \"value\": 743 }\n          ]\n        },\n        {\n          \"id\": \"graph\",\n          \"children\": [\n            { \"id\": \"BetweennessCentrality\", \"value\": 3534 },\n            { \"id\": \"LinkDistance\", \"value\": 5731 },\n            { \"id\": \"MaxFlowMinCut\", \"value\": 7840 },\n            { \"id\": \"ShortestPaths\", \"value\": 5914 },\n            { \"id\": \"SpanningTree\", \"value\": 3416 }\n          ]\n        },\n        {\n          \"id\": \"optimization\",\n          \"children\": [{ \"id\": \"AspectRatioBanker\", \"value\": 7074 }]\n        }\n      ]\n    },\n    {\n      \"id\": \"animate\",\n      \"children\": [\n        { \"id\": \"Easing\", \"value\": 17010 },\n        { \"id\": \"FunctionSequence\", \"value\": 5842 },\n        {\n          \"id\": \"interpolate\",\n          \"children\": [\n            { \"id\": \"ArrayInterpolator\", \"value\": 1983 },\n            { \"id\": \"ColorInterpolator\", \"value\": 2047 },\n            { \"id\": \"DateInterpolator\", \"value\": 1375 },\n            { \"id\": \"Interpolator\", \"value\": 8746 },\n            { \"id\": \"MatrixInterpolator\", \"value\": 2202 },\n            { \"id\": \"NumberInterpolator\", \"value\": 1382 },\n            { \"id\": \"ObjectInterpolator\", \"value\": 1629 },\n            { \"id\": \"PointInterpolator\", \"value\": 1675 },\n            { \"id\": \"RectangleInterpolator\", \"value\": 2042 }\n          ]\n        },\n        { \"id\": \"ISchedulable\", \"value\": 1041 },\n        { \"id\": \"Parallel\", \"value\": 5176 },\n        { \"id\": \"Pause\", \"value\": 449 },\n        { \"id\": \"Scheduler\", \"value\": 5593 },\n        { \"id\": \"Sequence\", \"value\": 5534 },\n        { \"id\": \"Transition\", \"value\": 9201 },\n        { \"id\": \"Transitioner\", \"value\": 19975 },\n        { \"id\": \"TransitionEvent\", \"value\": 1116 },\n        { \"id\": \"Tween\", \"value\": 6006 }\n      ]\n    },\n    {\n      \"id\": \"display\",\n      \"children\": [\n        { \"id\": \"DirtySprite\", \"value\": 8833 },\n        { \"id\": \"LineSprite\", \"value\": 1732 },\n        { \"id\": \"RectSprite\", \"value\": 3623 },\n        { \"id\": \"TextSprite\", \"value\": 10066 }\n      ]\n    },\n    {\n      \"id\": \"flex\",\n      \"children\": [{ \"id\": \"FlareVis\", \"value\": 4116 }]\n    },\n    {\n      \"id\": \"physics\",\n      \"children\": [\n        { \"id\": \"DragForce\", \"value\": 1082 },\n        { \"id\": \"GravityForce\", \"value\": 1336 },\n        { \"id\": \"IForce\", \"value\": 319 },\n        { \"id\": \"NBodyForce\", \"value\": 10498 },\n        { \"id\": \"Particle\", \"value\": 2822 },\n        { \"id\": \"Simulation\", \"value\": 9983 },\n        { \"id\": \"Spring\", \"value\": 2213 },\n        { \"id\": \"SpringForce\", \"value\": 1681 }\n      ]\n    },\n    {\n      \"id\": \"query\",\n      \"children\": [\n        { \"id\": \"AggregateExpression\", \"value\": 1616 },\n        { \"id\": \"And\", \"value\": 1027 },\n        { \"id\": \"Arithmetic\", \"value\": 3891 },\n        { \"id\": \"Average\", \"value\": 891 },\n        { \"id\": \"BinaryExpression\", \"value\": 2893 },\n        { \"id\": \"Comparison\", \"value\": 5103 },\n        { \"id\": \"CompositeExpression\", \"value\": 3677 },\n        { \"id\": \"Count\", \"value\": 781 },\n        { \"id\": \"DateUtil\", \"value\": 4141 },\n        { \"id\": \"Distinct\", \"value\": 933 },\n        { \"id\": \"Expression\", \"value\": 5130 },\n        { \"id\": \"ExpressionIterator\", \"value\": 3617 },\n        { \"id\": \"Fn\", \"value\": 3240 },\n        { \"id\": \"If\", \"value\": 2732 },\n        { \"id\": \"IsA\", \"value\": 2039 },\n        { \"id\": \"Literal\", \"value\": 1214 },\n        { \"id\": \"Match\", \"value\": 3748 },\n        { \"id\": \"Maximum\", \"value\": 843 },\n        {\n          \"id\": \"methods\",\n          \"children\": [\n            { \"id\": \"add\", \"value\": 593 },\n            { \"id\": \"and\", \"value\": 330 },\n            { \"id\": \"average\", \"value\": 287 },\n            { \"id\": \"count\", \"value\": 277 },\n            { \"id\": \"distinct\", \"value\": 292 },\n            { \"id\": \"div\", \"value\": 595 },\n            { \"id\": \"eq\", \"value\": 594 },\n            { \"id\": \"fn\", \"value\": 460 },\n            { \"id\": \"gt\", \"value\": 603 },\n            { \"id\": \"gte\", \"value\": 625 },\n            { \"id\": \"iff\", \"value\": 748 },\n            { \"id\": \"isa\", \"value\": 461 },\n            { \"id\": \"lt\", \"value\": 597 },\n            { \"id\": \"lte\", \"value\": 619 },\n            { \"id\": \"max\", \"value\": 283 },\n            { \"id\": \"min\", \"value\": 283 },\n            { \"id\": \"mod\", \"value\": 591 },\n            { \"id\": \"mul\", \"value\": 603 },\n            { \"id\": \"neq\", \"value\": 599 },\n            { \"id\": \"not\", \"value\": 386 },\n            { \"id\": \"or\", \"value\": 323 },\n            { \"id\": \"orderby\", \"value\": 307 },\n            { \"id\": \"range\", \"value\": 772 },\n            { \"id\": \"select\", \"value\": 296 },\n            { \"id\": \"stddev\", \"value\": 363 },\n            { \"id\": \"sub\", \"value\": 600 },\n            { \"id\": \"sum\", \"value\": 280 },\n            { \"id\": \"update\", \"value\": 307 },\n            { \"id\": \"variance\", \"value\": 335 },\n            { \"id\": \"where\", \"value\": 299 },\n            { \"id\": \"xor\", \"value\": 354 },\n            { \"id\": \"-\", \"value\": 264 }\n          ]\n        },\n        { \"id\": \"Minimum\", \"value\": 843 },\n        { \"id\": \"Not\", \"value\": 1554 },\n        { \"id\": \"Or\", \"value\": 970 },\n        { \"id\": \"Query\", \"value\": 13896 },\n        { \"id\": \"Range\", \"value\": 1594 },\n        { \"id\": \"StringUtil\", \"value\": 4130 },\n        { \"id\": \"Sum\", \"value\": 791 },\n        { \"id\": \"Variable\", \"value\": 1124 },\n        { \"id\": \"Variance\", \"value\": 1876 },\n        { \"id\": \"Xor\", \"value\": 1101 }\n      ]\n    },\n    {\n      \"id\": \"scale\",\n      \"children\": [\n        { \"id\": \"IScaleMap\", \"value\": 2105 },\n        { \"id\": \"LinearScale\", \"value\": 1316 },\n        { \"id\": \"LogScale\", \"value\": 3151 },\n        { \"id\": \"OrdinalScale\", \"value\": 3770 },\n        { \"id\": \"QuantileScale\", \"value\": 2435 },\n        { \"id\": \"QuantitativeScale\", \"value\": 4839 },\n        { \"id\": \"RootScale\", \"value\": 1756 },\n        { \"id\": \"Scale\", \"value\": 4268 },\n        { \"id\": \"ScaleType\", \"value\": 1821 },\n        { \"id\": \"TimeScale\", \"value\": 5833 }\n      ]\n    },\n    {\n      \"id\": \"util\",\n      \"children\": [\n        { \"id\": \"Arrays\", \"value\": 8258 },\n        { \"id\": \"Colors\", \"value\": 10001 },\n        { \"id\": \"Dates\", \"value\": 8217 },\n        { \"id\": \"Displays\", \"value\": 12555 },\n        { \"id\": \"Filter\", \"value\": 2324 },\n        { \"id\": \"Geometry\", \"value\": 10993 },\n        {\n          \"id\": \"heap\",\n          \"children\": [\n            { \"id\": \"FibonacciHeap\", \"value\": 9354 },\n            { \"id\": \"HeapNode\", \"value\": 1233 }\n          ]\n        },\n        { \"id\": \"IEvaluable\", \"value\": 335 },\n        { \"id\": \"IPredicate\", \"value\": 383 },\n        { \"id\": \"IValueProxy\", \"value\": 874 },\n        {\n          \"id\": \"math\",\n          \"children\": [\n            { \"id\": \"DenseMatrix\", \"value\": 3165 },\n            { \"id\": \"IMatrix\", \"value\": 2815 },\n            { \"id\": \"SparseMatrix\", \"value\": 3366 }\n          ]\n        },\n        { \"id\": \"Maths\", \"value\": 17705 },\n        { \"id\": \"Orientation\", \"value\": 1486 },\n        {\n          \"id\": \"palette\",\n          \"children\": [\n            { \"id\": \"ColorPalette\", \"value\": 6367 },\n            { \"id\": \"Palette\", \"value\": 1229 },\n            { \"id\": \"ShapePalette\", \"value\": 2059 },\n            { \"id\": \"SizePalette\", \"value\": 2291 }\n          ]\n        },\n        { \"id\": \"Property\", \"value\": 5559 },\n        { \"id\": \"Shapes\", \"value\": 19118 },\n        { \"id\": \"Sort\", \"value\": 6887 },\n        { \"id\": \"Stats\", \"value\": 6557 },\n        { \"id\": \"Strings\", \"value\": 22026 }\n      ]\n    },\n    {\n      \"id\": \"vis\",\n      \"children\": [\n        {\n          \"id\": \"axis\",\n          \"children\": [\n            { \"id\": \"Axes\", \"value\": 1302 },\n            { \"id\": \"Axis\", \"value\": 24593 },\n            { \"id\": \"AxisGridLine\", \"value\": 652 },\n            { \"id\": \"AxisLabel\", \"value\": 636 },\n            { \"id\": \"CartesianAxes\", \"value\": 6703 }\n          ]\n        },\n        {\n          \"id\": \"controls\",\n          \"children\": [\n            { \"id\": \"AnchorControl\", \"value\": 2138 },\n            { \"id\": \"ClickControl\", \"value\": 3824 },\n            { \"id\": \"Control\", \"value\": 1353 },\n            { \"id\": \"ControlList\", \"value\": 4665 },\n            { \"id\": \"DragControl\", \"value\": 2649 },\n            { \"id\": \"ExpandControl\", \"value\": 2832 },\n            { \"id\": \"HoverControl\", \"value\": 4896 },\n            { \"id\": \"IControl\", \"value\": 763 },\n            { \"id\": \"PanZoomControl\", \"value\": 5222 },\n            { \"id\": \"SelectionControl\", \"value\": 7862 },\n            { \"id\": \"TooltipControl\", \"value\": 8435 }\n          ]\n        },\n        {\n          \"id\": \"data\",\n          \"children\": [\n            { \"id\": \"Data\", \"value\": 20544 },\n            { \"id\": \"DataList\", \"value\": 19788 },\n            { \"id\": \"DataSprite\", \"value\": 10349 },\n            { \"id\": \"EdgeSprite\", \"value\": 3301 },\n            { \"id\": \"NodeSprite\", \"value\": 19382 },\n            {\n              \"id\": \"render\",\n              \"children\": [\n                { \"id\": \"ArrowType\", \"value\": 698 },\n                { \"id\": \"EdgeRenderer\", \"value\": 5569 },\n                { \"id\": \"IRenderer\", \"value\": 353 },\n                { \"id\": \"ShapeRenderer\", \"value\": 2247 }\n              ]\n            },\n            { \"id\": \"ScaleBinding\", \"value\": 11275 },\n            { \"id\": \"Tree\", \"value\": 7147 },\n            { \"id\": \"TreeBuilder\", \"value\": 9930 }\n          ]\n        },\n        {\n          \"id\": \"events\",\n          \"children\": [\n            { \"id\": \"DataEvent\", \"value\": 2313 },\n            { \"id\": \"SelectionEvent\", \"value\": 1880 },\n            { \"id\": \"TooltipEvent\", \"value\": 1701 },\n            { \"id\": \"VisualizationEvent\", \"value\": 1117 }\n          ]\n        },\n        {\n          \"id\": \"legend\",\n          \"children\": [\n            { \"id\": \"Legend\", \"value\": 20859 },\n            { \"id\": \"LegendItem\", \"value\": 4614 },\n            { \"id\": \"LegendRange\", \"value\": 10530 }\n          ]\n        },\n        {\n          \"id\": \"operator\",\n          \"children\": [\n            {\n              \"id\": \"distortion\",\n              \"children\": [\n                { \"id\": \"BifocalDistortion\", \"value\": 4461 },\n                { \"id\": \"Distortion\", \"value\": 6314 },\n                { \"id\": \"FisheyeDistortion\", \"value\": 3444 }\n              ]\n            },\n            {\n              \"id\": \"encoder\",\n              \"children\": [\n                { \"id\": \"ColorEncoder\", \"value\": 3179 },\n                { \"id\": \"Encoder\", \"value\": 4060 },\n                { \"id\": \"PropertyEncoder\", \"value\": 4138 },\n                { \"id\": \"ShapeEncoder\", \"value\": 1690 },\n                { \"id\": \"SizeEncoder\", \"value\": 1830 }\n              ]\n            },\n            {\n              \"id\": \"filter\",\n              \"children\": [\n                { \"id\": \"FisheyeTreeFilter\", \"value\": 5219 },\n                { \"id\": \"GraphDistanceFilter\", \"value\": 3165 },\n                { \"id\": \"VisibilityFilter\", \"value\": 3509 }\n              ]\n            },\n            { \"id\": \"IOperator\", \"value\": 1286 },\n            {\n              \"id\": \"label\",\n              \"children\": [\n                { \"id\": \"Labeler\", \"value\": 9956 },\n                { \"id\": \"RadialLabeler\", \"value\": 3899 },\n                { \"id\": \"StackedAreaLabeler\", \"value\": 3202 }\n              ]\n            },\n            {\n              \"id\": \"layout\",\n              \"children\": [\n                { \"id\": \"AxisLayout\", \"value\": 6725 },\n                { \"id\": \"BundledEdgeRouter\", \"value\": 3727 },\n                { \"id\": \"CircleLayout\", \"value\": 9317 },\n                { \"id\": \"CirclePackingLayout\", \"value\": 12003 },\n                { \"id\": \"DendrogramLayout\", \"value\": 4853 },\n                { \"id\": \"ForceDirectedLayout\", \"value\": 8411 },\n                { \"id\": \"IcicleTreeLayout\", \"value\": 4864 },\n                { \"id\": \"IndentedTreeLayout\", \"value\": 3174 },\n                { \"id\": \"Layout\", \"value\": 7881 },\n                { \"id\": \"NodeLinkTreeLayout\", \"value\": 12870 },\n                { \"id\": \"PieLayout\", \"value\": 2728 },\n                { \"id\": \"RadialTreeLayout\", \"value\": 12348 },\n                { \"id\": \"RandomLayout\", \"value\": 870 },\n                { \"id\": \"StackedAreaLayout\", \"value\": 9121 },\n                { \"id\": \"TreeMapLayout\", \"value\": 9191 }\n              ]\n            },\n            { \"id\": \"Operator\", \"value\": 2490 },\n            { \"id\": \"OperatorList\", \"value\": 5248 },\n            { \"id\": \"OperatorSequence\", \"value\": 4190 },\n            { \"id\": \"OperatorSwitch\", \"value\": 2581 },\n            { \"id\": \"SortOperator\", \"value\": 2023 }\n          ]\n        },\n        { \"id\": \"Visualization\", \"value\": 16540 }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/force.json",
    "content": "{\n  \"nodes\": [\n    {\n      \"id\": \"node0\",\n      \"style\": {\n        \"size\": 50\n      },\n      \"data\": {\n        \"cluster\": \"node0\"\n      }\n    },\n    {\n      \"id\": \"node1\",\n      \"style\": {\n        \"size\": 30\n      },\n      \"data\": {\n        \"cluster\": \"node1\"\n      }\n    },\n    {\n      \"id\": \"node2\",\n      \"style\": {\n        \"size\": 30\n      },\n      \"data\": {\n        \"cluster\": \"node2\"\n      }\n    },\n    {\n      \"id\": \"node3\",\n      \"style\": {\n        \"size\": 30\n      },\n      \"data\": {\n        \"cluster\": \"node3\"\n      }\n    },\n    {\n      \"id\": \"node4\",\n      \"style\": {\n        \"size\": 30\n      },\n      \"data\": {\n        \"cluster\": \"node4\"\n      }\n    },\n    {\n      \"id\": \"node5\",\n      \"style\": {\n        \"size\": 30\n      },\n      \"data\": {\n        \"isLeaf\": true,\n        \"cluster\": \"node5\"\n      }\n    },\n    {\n      \"id\": \"node6\",\n      \"style\": {\n        \"size\": 15\n      },\n      \"data\": {\n        \"isLeaf\": true,\n        \"cluster\": \"node6\"\n      }\n    },\n    {\n      \"id\": \"node7\",\n      \"style\": {\n        \"size\": 15\n      },\n      \"data\": {\n        \"isLeaf\": true,\n        \"cluster\": \"node7\"\n      }\n    },\n    {\n      \"id\": \"node8\",\n      \"style\": {\n        \"size\": 15\n      },\n      \"data\": {\n        \"isLeaf\": true,\n        \"cluster\": \"node8\"\n      }\n    },\n    {\n      \"id\": \"node9\",\n      \"style\": {\n        \"size\": 15\n      },\n      \"data\": {\n        \"isLeaf\": true,\n        \"cluster\": \"node9\"\n      }\n    },\n    {\n      \"id\": \"node10\",\n      \"style\": {\n        \"size\": 15\n      },\n      \"data\": {\n        \"isLeaf\": true,\n        \"cluster\": \"node10\"\n      }\n    },\n    {\n      \"id\": \"node11\",\n      \"style\": {\n        \"size\": 15\n      },\n      \"data\": {\n        \"isLeaf\": true,\n        \"cluster\": \"node11\"\n      }\n    },\n    {\n      \"id\": \"node12\",\n      \"style\": {\n        \"size\": 15\n      },\n      \"data\": {\n        \"isLeaf\": true,\n        \"cluster\": \"node12\"\n      }\n    },\n    {\n      \"id\": \"node13\",\n      \"style\": {\n        \"size\": 15\n      },\n      \"data\": {\n        \"isLeaf\": true,\n        \"cluster\": \"node13\"\n      }\n    },\n    {\n      \"id\": \"node14\",\n      \"style\": {\n        \"size\": 15\n      },\n      \"data\": {\n        \"isLeaf\": true,\n        \"cluster\": \"node14\"\n      }\n    },\n    {\n      \"id\": \"node15\",\n      \"style\": {\n        \"size\": 15\n      },\n      \"data\": {\n        \"isLeaf\": true,\n        \"cluster\": \"node15\"\n      }\n    },\n    {\n      \"id\": \"node16\",\n      \"style\": {\n        \"size\": 15\n      },\n      \"data\": {\n        \"isLeaf\": true,\n        \"cluster\": \"node16\"\n      }\n    }\n  ],\n  \"edges\": [\n    {\n      \"source\": \"node0\",\n      \"target\": \"node1\"\n    },\n    {\n      \"source\": \"node0\",\n      \"target\": \"node2\"\n    },\n    {\n      \"source\": \"node0\",\n      \"target\": \"node3\"\n    },\n    {\n      \"source\": \"node0\",\n      \"target\": \"node4\"\n    },\n    {\n      \"source\": \"node0\",\n      \"target\": \"node5\"\n    },\n    {\n      \"source\": \"node1\",\n      \"target\": \"node6\"\n    },\n    {\n      \"source\": \"node1\",\n      \"target\": \"node7\"\n    },\n    {\n      \"source\": \"node2\",\n      \"target\": \"node8\"\n    },\n    {\n      \"source\": \"node2\",\n      \"target\": \"node9\"\n    },\n    {\n      \"source\": \"node2\",\n      \"target\": \"node10\"\n    },\n    {\n      \"source\": \"node2\",\n      \"target\": \"node11\"\n    },\n    {\n      \"source\": \"node2\",\n      \"target\": \"node12\"\n    },\n    {\n      \"source\": \"node2\",\n      \"target\": \"node13\"\n    },\n    {\n      \"source\": \"node3\",\n      \"target\": \"node14\"\n    },\n    {\n      \"source\": \"node3\",\n      \"target\": \"node15\"\n    },\n    {\n      \"source\": \"node3\",\n      \"target\": \"node16\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/gene.json",
    "content": "{\n  \"nodes\": [\n    {\n      \"id\": \"HIRA\",\n      \"data\": {\n        \"rank\": 148\n      }\n    },\n    {\n      \"id\": \"SERPINE1\",\n      \"data\": {\n        \"rank\": 5\n      }\n    },\n    {\n      \"id\": \"FAS\",\n      \"data\": {\n        \"rank\": 38\n      }\n    },\n    {\n      \"id\": \"H1F0\",\n      \"data\": {\n        \"rank\": 179\n      }\n    },\n    {\n      \"id\": \"CHEK2\",\n      \"data\": {\n        \"rank\": 42\n      }\n    },\n    {\n      \"id\": \"COL18A1\",\n      \"data\": {\n        \"rank\": 157\n      }\n    },\n    {\n      \"id\": \"CREBBP\",\n      \"data\": {\n        \"rank\": 24\n      }\n    },\n    {\n      \"id\": \"FDXR\",\n      \"data\": {\n        \"rank\": 147\n      }\n    },\n    {\n      \"id\": \"SMYD2\",\n      \"data\": {\n        \"rank\": 175\n      }\n    },\n    {\n      \"id\": \"ATR\",\n      \"data\": {\n        \"rank\": 52\n      }\n    },\n    {\n      \"id\": \"HGF\",\n      \"data\": {\n        \"rank\": 4\n      }\n    },\n    {\n      \"id\": \"ATM\",\n      \"data\": {\n        \"rank\": 25\n      }\n    },\n    {\n      \"id\": \"TP63\",\n      \"data\": {\n        \"rank\": 37\n      }\n    },\n    {\n      \"id\": \"GPX1\",\n      \"data\": {\n        \"rank\": 13\n      }\n    },\n    {\n      \"id\": \"TRIAP1\",\n      \"data\": {\n        \"rank\": 183\n      }\n    },\n    {\n      \"id\": \"HIST1H1E\",\n      \"data\": {\n        \"rank\": 167\n      }\n    },\n    {\n      \"id\": \"HIST1H1D\",\n      \"data\": {\n        \"rank\": 184\n      }\n    },\n    {\n      \"id\": \"HIST1H1C\",\n      \"data\": {\n        \"rank\": 174\n      }\n    },\n    {\n      \"id\": \"HIST1H1B\",\n      \"data\": {\n        \"rank\": 96\n      }\n    },\n    {\n      \"id\": \"HIST1H1A\",\n      \"data\": {\n        \"rank\": 173\n      }\n    },\n    {\n      \"id\": \"TP53\",\n      \"data\": {\n        \"rank\": 0\n      }\n    },\n    {\n      \"id\": \"GADD45A\",\n      \"data\": {\n        \"rank\": 98\n      }\n    },\n    {\n      \"id\": \"PML\",\n      \"data\": {\n        \"rank\": 35\n      }\n    },\n    {\n      \"id\": \"SUMO1\",\n      \"data\": {\n        \"rank\": 36\n      }\n    },\n    {\n      \"id\": \"PPP2CA\",\n      \"data\": {\n        \"rank\": 146\n      }\n    },\n    {\n      \"id\": \"PPP2CB\",\n      \"data\": {\n        \"rank\": 185\n      }\n    },\n    {\n      \"id\": \"PRKAA1\",\n      \"data\": {\n        \"rank\": 78\n      }\n    },\n    {\n      \"id\": \"PRKAA2\",\n      \"data\": {\n        \"rank\": 29\n      }\n    },\n    {\n      \"id\": \"MAPK11\",\n      \"data\": {\n        \"rank\": 127\n      }\n    },\n    {\n      \"id\": \"TP73\",\n      \"data\": {\n        \"rank\": 116\n      }\n    },\n    {\n      \"id\": \"CCNB1\",\n      \"data\": {\n        \"rank\": 62\n      }\n    },\n    {\n      \"id\": \"MAPK12\",\n      \"data\": {\n        \"rank\": 131\n      }\n    },\n    {\n      \"id\": \"MAPK13\",\n      \"data\": {\n        \"rank\": 126\n      }\n    },\n    {\n      \"id\": \"MAPK14\",\n      \"data\": {\n        \"rank\": 23\n      }\n    },\n    {\n      \"id\": \"PRKAB2\",\n      \"data\": {\n        \"rank\": 189\n      }\n    },\n    {\n      \"id\": \"PRKAB1\",\n      \"data\": {\n        \"rank\": 73\n      }\n    },\n    {\n      \"id\": \"EP300\",\n      \"data\": {\n        \"rank\": 19\n      }\n    },\n    {\n      \"id\": \"IGBP1\",\n      \"data\": {\n        \"rank\": 125\n      }\n    },\n    {\n      \"id\": \"FBXO11\",\n      \"data\": {\n        \"rank\": 188\n      }\n    },\n    {\n      \"id\": \"HMGB1\",\n      \"data\": {\n        \"rank\": 54\n      }\n    },\n    {\n      \"id\": \"NEDD8\",\n      \"data\": {\n        \"rank\": 205\n      }\n    },\n    {\n      \"id\": \"ASF1A\",\n      \"data\": {\n        \"rank\": 161\n      }\n    },\n    {\n      \"id\": \"KAT8\",\n      \"data\": {\n        \"rank\": 199\n      }\n    },\n    {\n      \"id\": \"HTT\",\n      \"data\": {\n        \"rank\": 43\n      }\n    },\n    {\n      \"id\": \"WRN\",\n      \"data\": {\n        \"rank\": 53\n      }\n    },\n    {\n      \"id\": \"CDKN1A\",\n      \"data\": {\n        \"rank\": 9\n      }\n    },\n    {\n      \"id\": \"PLK3\",\n      \"data\": {\n        \"rank\": 115\n      }\n    },\n    {\n      \"id\": \"DYRK1A\",\n      \"data\": {\n        \"rank\": 128\n      }\n    },\n    {\n      \"id\": \"PRKAG1\",\n      \"data\": {\n        \"rank\": 177\n      }\n    },\n    {\n      \"id\": \"CASP6\",\n      \"data\": {\n        \"rank\": 104\n      }\n    },\n    {\n      \"id\": \"MAX\",\n      \"data\": {\n        \"rank\": 158\n      }\n    },\n    {\n      \"id\": \"FOS\",\n      \"data\": {\n        \"rank\": 61\n      }\n    },\n    {\n      \"id\": \"TP53I3\",\n      \"data\": {\n        \"rank\": 152\n      }\n    },\n    {\n      \"id\": \"PRMT1\",\n      \"data\": {\n        \"rank\": 206\n      }\n    },\n    {\n      \"id\": \"PCBP4\",\n      \"data\": {\n        \"rank\": 165\n      }\n    },\n    {\n      \"id\": \"PRMT5\",\n      \"data\": {\n        \"rank\": 118\n      }\n    },\n    {\n      \"id\": \"E4F1\",\n      \"data\": {\n        \"rank\": 150\n      }\n    },\n    {\n      \"id\": \"CASP1\",\n      \"data\": {\n        \"rank\": 64\n      }\n    },\n    {\n      \"id\": \"PRKCA\",\n      \"data\": {\n        \"rank\": 84\n      }\n    },\n    {\n      \"id\": \"CDK1\",\n      \"data\": {\n        \"rank\": 27\n      }\n    },\n    {\n      \"id\": \"CDK9\",\n      \"data\": {\n        \"rank\": 66\n      }\n    },\n    {\n      \"id\": \"RB1\",\n      \"data\": {\n        \"rank\": 15\n      }\n    },\n    {\n      \"id\": \"CDK7\",\n      \"data\": {\n        \"rank\": 72\n      }\n    },\n    {\n      \"id\": \"HMGA2\",\n      \"data\": {\n        \"rank\": 82\n      }\n    },\n    {\n      \"id\": \"DAPK3\",\n      \"data\": {\n        \"rank\": 112\n      }\n    },\n    {\n      \"id\": \"HMGA1\",\n      \"data\": {\n        \"rank\": 111\n      }\n    },\n    {\n      \"id\": \"PRKCD\",\n      \"data\": {\n        \"rank\": 122\n      }\n    },\n    {\n      \"id\": \"UBN1\",\n      \"data\": {\n        \"rank\": 180\n      }\n    },\n    {\n      \"id\": \"CDK5\",\n      \"data\": {\n        \"rank\": 44\n      }\n    },\n    {\n      \"id\": \"CDK2\",\n      \"data\": {\n        \"rank\": 34\n      }\n    },\n    {\n      \"id\": \"DAPK1\",\n      \"data\": {\n        \"rank\": 49\n      }\n    },\n    {\n      \"id\": \"RFWD2\",\n      \"data\": {\n        \"rank\": 143\n      }\n    },\n    {\n      \"id\": \"PPM1J\",\n      \"data\": {\n        \"rank\": 191\n      }\n    },\n    {\n      \"id\": \"BTG2\",\n      \"data\": {\n        \"rank\": 97\n      }\n    },\n    {\n      \"id\": \"CD82\",\n      \"data\": {\n        \"rank\": 69\n      }\n    },\n    {\n      \"id\": \"HIPK2\",\n      \"data\": {\n        \"rank\": 70\n      }\n    },\n    {\n      \"id\": \"DDB2\",\n      \"data\": {\n        \"rank\": 102\n      }\n    },\n    {\n      \"id\": \"MDM2\",\n      \"data\": {\n        \"rank\": 7\n      }\n    },\n    {\n      \"id\": \"CTSD\",\n      \"data\": {\n        \"rank\": 85\n      }\n    },\n    {\n      \"id\": \"NGFR\",\n      \"data\": {\n        \"rank\": 80\n      }\n    },\n    {\n      \"id\": \"CARM1\",\n      \"data\": {\n        \"rank\": 207\n      }\n    },\n    {\n      \"id\": \"TYRP1\",\n      \"data\": {\n        \"rank\": 87\n      }\n    },\n    {\n      \"id\": \"PRKDC\",\n      \"data\": {\n        \"rank\": 74\n      }\n    },\n    {\n      \"id\": \"HIC1\",\n      \"data\": {\n        \"rank\": 99\n      }\n    },\n    {\n      \"id\": \"TAP1\",\n      \"data\": {\n        \"rank\": 63\n      }\n    },\n    {\n      \"id\": \"PYCARD\",\n      \"data\": {\n        \"rank\": 89\n      }\n    },\n    {\n      \"id\": \"APC\",\n      \"data\": {\n        \"rank\": 22\n      }\n    },\n    {\n      \"id\": \"RNF144B\",\n      \"data\": {\n        \"rank\": 190\n      }\n    },\n    {\n      \"id\": \"KAT2B\",\n      \"data\": {\n        \"rank\": 55\n      }\n    },\n    {\n      \"id\": \"MSH2\",\n      \"data\": {\n        \"rank\": 21\n      }\n    },\n    {\n      \"id\": \"PPP1R13L\",\n      \"data\": {\n        \"rank\": 140\n      }\n    },\n    {\n      \"id\": \"SIRT1\",\n      \"data\": {\n        \"rank\": 46\n      }\n    },\n    {\n      \"id\": \"CASP10\",\n      \"data\": {\n        \"rank\": 100\n      }\n    },\n    {\n      \"id\": \"SP1\",\n      \"data\": {\n        \"rank\": 204\n      }\n    },\n    {\n      \"id\": \"IRF5\",\n      \"data\": {\n        \"rank\": 101\n      }\n    },\n    {\n      \"id\": \"BAX\",\n      \"data\": {\n        \"rank\": 51\n      }\n    },\n    {\n      \"id\": \"CABIN1\",\n      \"data\": {\n        \"rank\": 132\n      }\n    },\n    {\n      \"id\": \"SETD7\",\n      \"data\": {\n        \"rank\": 156\n      }\n    },\n    {\n      \"id\": \"SETD8\",\n      \"data\": {\n        \"rank\": 164\n      }\n    },\n    {\n      \"id\": \"APAF1\",\n      \"data\": {\n        \"rank\": 83\n      }\n    },\n    {\n      \"id\": \"GDF15\",\n      \"data\": {\n        \"rank\": 91\n      }\n    }\n  ],\n  \"edges\": [\n    {\n      \"id\": \"TP53-controls-state-change-of-H1F0\",\n      \"source\": \"TP53\",\n      \"target\": \"H1F0\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-IRF5\",\n      \"source\": \"TP53\",\n      \"target\": \"IRF5\"\n    },\n    {\n      \"id\": \"KAT8-controls-state-change-of-TP53\",\n      \"source\": \"KAT8\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"FBXO11-controls-state-change-of-TP53\",\n      \"source\": \"FBXO11\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"ATR-controls-state-change-of-TP53\",\n      \"source\": \"ATR\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"FBXO11-controls-state-change-of-NEDD8\",\n      \"source\": \"FBXO11\",\n      \"target\": \"NEDD8\"\n    },\n    {\n      \"id\": \"ATM-controls-state-change-of-TP53\",\n      \"source\": \"ATM\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"SMYD2-controls-state-change-of-TP53\",\n      \"source\": \"SMYD2\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"NGFR-controls-state-change-of-TP53\",\n      \"source\": \"NGFR\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"MAX-controls-expression-of-TP53\",\n      \"source\": \"MAX\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-PRKAB1\",\n      \"source\": \"TP53\",\n      \"target\": \"PRKAB1\"\n    },\n    {\n      \"id\": \"MDM2-controls-state-change-of-TP53\",\n      \"source\": \"MDM2\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"SETD8-controls-state-change-of-TP53\",\n      \"source\": \"SETD8\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"SETD7-controls-state-change-of-TP53\",\n      \"source\": \"SETD7\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-TYRP1\",\n      \"source\": \"TP53\",\n      \"target\": \"TYRP1\"\n    },\n    {\n      \"id\": \"ATR-controls-state-change-of-TP63\",\n      \"source\": \"ATR\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"PRKCA-controls-state-change-of-TP53\",\n      \"source\": \"PRKCA\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"ATM-controls-state-change-of-TP63\",\n      \"source\": \"ATM\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"CHEK2-controls-state-change-of-TP73\",\n      \"source\": \"CHEK2\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-DDB2\",\n      \"source\": \"TP53\",\n      \"target\": \"DDB2\"\n    },\n    {\n      \"id\": \"PRKCD-controls-state-change-of-TP53\",\n      \"source\": \"PRKCD\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-TP63\",\n      \"source\": \"TP53\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-PML\",\n      \"source\": \"TP53\",\n      \"target\": \"PML\"\n    },\n    {\n      \"id\": \"ATM-controls-state-change-of-TP73\",\n      \"source\": \"ATM\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"WRN-controls-state-change-of-TP63\",\n      \"source\": \"WRN\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"ATR-controls-state-change-of-TP73\",\n      \"source\": \"ATR\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-PCBP4\",\n      \"source\": \"TP53\",\n      \"target\": \"PCBP4\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-GADD45A\",\n      \"source\": \"TP53\",\n      \"target\": \"GADD45A\"\n    },\n    {\n      \"id\": \"PRKDC-controls-state-change-of-TP53\",\n      \"source\": \"PRKDC\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-GDF15\",\n      \"source\": \"TP53\",\n      \"target\": \"GDF15\"\n    },\n    {\n      \"id\": \"WRN-controls-state-change-of-TP73\",\n      \"source\": \"WRN\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-SERPINE1\",\n      \"source\": \"TP53\",\n      \"target\": \"SERPINE1\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-PPM1J\",\n      \"source\": \"TP53\",\n      \"target\": \"PPM1J\"\n    },\n    {\n      \"id\": \"HTT-controls-state-change-of-TP63\",\n      \"source\": \"HTT\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-CASP1\",\n      \"source\": \"TP53\",\n      \"target\": \"CASP1\"\n    },\n    {\n      \"id\": \"MAPK14-controls-state-change-of-TP63\",\n      \"source\": \"MAPK14\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"HMGB1-controls-state-change-of-TP73\",\n      \"source\": \"HMGB1\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-CASP6\",\n      \"source\": \"TP53\",\n      \"target\": \"CASP6\"\n    },\n    {\n      \"id\": \"MAPK13-controls-state-change-of-TP63\",\n      \"source\": \"MAPK13\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"HTT-controls-state-change-of-TP53\",\n      \"source\": \"HTT\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"MAPK12-controls-state-change-of-TP63\",\n      \"source\": \"MAPK12\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"MAPK11-controls-state-change-of-TP63\",\n      \"source\": \"MAPK11\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"SIRT1-controls-expression-of-CDKN1A\",\n      \"source\": \"SIRT1\",\n      \"target\": \"CDKN1A\"\n    },\n    {\n      \"id\": \"PML-controls-state-change-of-TP53\",\n      \"source\": \"PML\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"CREBBP-controls-state-change-of-TP53\",\n      \"source\": \"CREBBP\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-TAP1\",\n      \"source\": \"TP53\",\n      \"target\": \"TAP1\"\n    },\n    {\n      \"id\": \"MAPK13-controls-state-change-of-TP53\",\n      \"source\": \"MAPK13\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"MAPK12-controls-state-change-of-TP53\",\n      \"source\": \"MAPK12\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"MAPK14-controls-state-change-of-TP53\",\n      \"source\": \"MAPK14\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"MAPK11-controls-state-change-of-TP53\",\n      \"source\": \"MAPK11\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-state-change-of-BAX\",\n      \"source\": \"TP53\",\n      \"target\": \"BAX\"\n    },\n    {\n      \"id\": \"HMGB1-controls-state-change-of-TP63\",\n      \"source\": \"HMGB1\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-TP73\",\n      \"source\": \"TP53\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"PML-controls-state-change-of-TP63\",\n      \"source\": \"PML\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"PPP2CA-controls-state-change-of-TP73\",\n      \"source\": \"PPP2CA\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"PPP2CB-controls-state-change-of-TP73\",\n      \"source\": \"PPP2CB\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"FOS-controls-expression-of-TP53\",\n      \"source\": \"FOS\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"CREBBP-controls-expression-of-GADD45A\",\n      \"source\": \"CREBBP\",\n      \"target\": \"GADD45A\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-CDKN1A\",\n      \"source\": \"TP53\",\n      \"target\": \"CDKN1A\"\n    },\n    {\n      \"id\": \"PML-controls-state-change-of-TP73\",\n      \"source\": \"PML\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"HMGB1-controls-state-change-of-TP53\",\n      \"source\": \"HMGB1\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"IGBP1-controls-state-change-of-TP73\",\n      \"source\": \"IGBP1\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"HIPK2-controls-state-change-of-TP53\",\n      \"source\": \"HIPK2\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-RNF144B\",\n      \"source\": \"TP53\",\n      \"target\": \"RNF144B\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-HIC1\",\n      \"source\": \"TP53\",\n      \"target\": \"HIC1\"\n    },\n    {\n      \"id\": \"TP53-controls-state-change-of-HIRA\",\n      \"source\": \"TP53\",\n      \"target\": \"HIRA\"\n    },\n    {\n      \"id\": \"MAPK14-controls-state-change-of-TP73\",\n      \"source\": \"MAPK14\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"TP53-controls-state-change-of-UBN1\",\n      \"source\": \"TP53\",\n      \"target\": \"UBN1\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-RB1\",\n      \"source\": \"TP53\",\n      \"target\": \"RB1\"\n    },\n    {\n      \"id\": \"MAPK11-controls-state-change-of-TP73\",\n      \"source\": \"MAPK11\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"TP53-controls-state-change-of-HMGA2\",\n      \"source\": \"TP53\",\n      \"target\": \"HMGA2\"\n    },\n    {\n      \"id\": \"TP53-controls-state-change-of-HMGA1\",\n      \"source\": \"TP53\",\n      \"target\": \"HMGA1\"\n    },\n    {\n      \"id\": \"MAPK13-controls-state-change-of-TP73\",\n      \"source\": \"MAPK13\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"MAPK12-controls-state-change-of-TP73\",\n      \"source\": \"MAPK12\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"IGBP1-controls-state-change-of-TP63\",\n      \"source\": \"IGBP1\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-TP53I3\",\n      \"source\": \"TP53\",\n      \"target\": \"TP53I3\"\n    },\n    {\n      \"id\": \"DYRK1A-controls-state-change-of-TP53\",\n      \"source\": \"DYRK1A\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"SUMO1-controls-state-change-of-TP73\",\n      \"source\": \"SUMO1\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"PRKAG1-controls-state-change-of-TP63\",\n      \"source\": \"PRKAG1\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"SIRT1-controls-state-change-of-TP73\",\n      \"source\": \"SIRT1\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"PPP2CB-controls-state-change-of-TP63\",\n      \"source\": \"PPP2CB\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"CHEK2-controls-state-change-of-TP53\",\n      \"source\": \"CHEK2\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"PPP2CA-controls-state-change-of-TP63\",\n      \"source\": \"PPP2CA\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"CDK7-controls-state-change-of-TP53\",\n      \"source\": \"CDK7\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"CDK9-controls-state-change-of-TP53\",\n      \"source\": \"CDK9\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"CDK2-controls-state-change-of-TP53\",\n      \"source\": \"CDK2\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"CDK5-controls-state-change-of-TP53\",\n      \"source\": \"CDK5\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"IGBP1-controls-state-change-of-TP53\",\n      \"source\": \"IGBP1\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-BAX\",\n      \"source\": \"TP53\",\n      \"target\": \"BAX\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-CCNB1\",\n      \"source\": \"TP53\",\n      \"target\": \"CCNB1\"\n    },\n    {\n      \"id\": \"SIRT1-controls-state-change-of-TP63\",\n      \"source\": \"SIRT1\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"PRKAG1-controls-state-change-of-TP73\",\n      \"source\": \"PRKAG1\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-MDM2\",\n      \"source\": \"TP53\",\n      \"target\": \"MDM2\"\n    },\n    {\n      \"id\": \"PPP2CB-controls-state-change-of-TP53\",\n      \"source\": \"PPP2CB\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"PPP2CA-controls-state-change-of-TP53\",\n      \"source\": \"PPP2CA\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"CHEK2-controls-state-change-of-TP63\",\n      \"source\": \"CHEK2\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-CD82\",\n      \"source\": \"TP53\",\n      \"target\": \"CD82\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-FDXR\",\n      \"source\": \"TP53\",\n      \"target\": \"FDXR\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-HTT\",\n      \"source\": \"TP53\",\n      \"target\": \"HTT\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-CTSD\",\n      \"source\": \"TP53\",\n      \"target\": \"CTSD\"\n    },\n    {\n      \"id\": \"SIRT1-controls-state-change-of-TP53\",\n      \"source\": \"SIRT1\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-CASP10\",\n      \"source\": \"TP53\",\n      \"target\": \"CASP10\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-APAF1\",\n      \"source\": \"TP53\",\n      \"target\": \"APAF1\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-HGF\",\n      \"source\": \"TP53\",\n      \"target\": \"HGF\"\n    },\n    {\n      \"id\": \"PRMT5-controls-state-change-of-TP53\",\n      \"source\": \"PRMT5\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-APC\",\n      \"source\": \"TP53\",\n      \"target\": \"APC\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-BTG2\",\n      \"source\": \"TP53\",\n      \"target\": \"BTG2\"\n    },\n    {\n      \"id\": \"PRMT1-controls-expression-of-GADD45A\",\n      \"source\": \"PRMT1\",\n      \"target\": \"GADD45A\"\n    },\n    {\n      \"id\": \"TP53-controls-state-change-of-ASF1A\",\n      \"source\": \"TP53\",\n      \"target\": \"ASF1A\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-FAS\",\n      \"source\": \"TP53\",\n      \"target\": \"FAS\"\n    },\n    {\n      \"id\": \"HTT-controls-state-change-of-TP73\",\n      \"source\": \"HTT\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"PRKAG1-controls-state-change-of-TP53\",\n      \"source\": \"PRKAG1\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-TRIAP1\",\n      \"source\": \"TP53\",\n      \"target\": \"TRIAP1\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-RFWD2\",\n      \"source\": \"TP53\",\n      \"target\": \"RFWD2\"\n    },\n    {\n      \"id\": \"EP300-controls-state-change-of-TP63\",\n      \"source\": \"EP300\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-COL18A1\",\n      \"source\": \"TP53\",\n      \"target\": \"COL18A1\"\n    },\n    {\n      \"id\": \"EP300-controls-expression-of-GADD45A\",\n      \"source\": \"EP300\",\n      \"target\": \"GADD45A\"\n    },\n    {\n      \"id\": \"PRKAB2-controls-state-change-of-TP73\",\n      \"source\": \"PRKAB2\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"EP300-controls-state-change-of-TP53\",\n      \"source\": \"EP300\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"PRKAB1-controls-state-change-of-TP73\",\n      \"source\": \"PRKAB1\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"TP53-controls-state-change-of-HIST1H1B\",\n      \"source\": \"TP53\",\n      \"target\": \"HIST1H1B\"\n    },\n    {\n      \"id\": \"EP300-controls-state-change-of-TP73\",\n      \"source\": \"EP300\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"TP53-controls-state-change-of-HIST1H1C\",\n      \"source\": \"TP53\",\n      \"target\": \"HIST1H1C\"\n    },\n    {\n      \"id\": \"TP53-controls-state-change-of-HIST1H1A\",\n      \"source\": \"TP53\",\n      \"target\": \"HIST1H1A\"\n    },\n    {\n      \"id\": \"CARM1-controls-expression-of-GADD45A\",\n      \"source\": \"CARM1\",\n      \"target\": \"GADD45A\"\n    },\n    {\n      \"id\": \"TP53-controls-state-change-of-HIST1H1D\",\n      \"source\": \"TP53\",\n      \"target\": \"HIST1H1D\"\n    },\n    {\n      \"id\": \"TP53-controls-state-change-of-HIST1H1E\",\n      \"source\": \"TP53\",\n      \"target\": \"HIST1H1E\"\n    },\n    {\n      \"id\": \"WRN-controls-state-change-of-TP53\",\n      \"source\": \"WRN\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-MSH2\",\n      \"source\": \"TP53\",\n      \"target\": \"MSH2\"\n    },\n    {\n      \"id\": \"KAT2B-controls-state-change-of-TP53\",\n      \"source\": \"KAT2B\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-PYCARD\",\n      \"source\": \"TP53\",\n      \"target\": \"PYCARD\"\n    },\n    {\n      \"id\": \"PRKAA2-controls-state-change-of-TP53\",\n      \"source\": \"PRKAA2\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"SP1-controls-expression-of-CCNB1\",\n      \"source\": \"SP1\",\n      \"target\": \"CCNB1\"\n    },\n    {\n      \"id\": \"PRKAA1-controls-state-change-of-TP53\",\n      \"source\": \"PRKAA1\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-GPX1\",\n      \"source\": \"TP53\",\n      \"target\": \"GPX1\"\n    },\n    {\n      \"id\": \"TP53-controls-state-change-of-CABIN1\",\n      \"source\": \"TP53\",\n      \"target\": \"CABIN1\"\n    },\n    {\n      \"id\": \"MDM2-controls-state-change-of-NEDD8\",\n      \"source\": \"MDM2\",\n      \"target\": \"NEDD8\"\n    },\n    {\n      \"id\": \"PLK3-controls-state-change-of-TP53\",\n      \"source\": \"PLK3\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"PRKAB1-controls-state-change-of-TP53\",\n      \"source\": \"PRKAB1\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"SUMO1-controls-state-change-of-TP53\",\n      \"source\": \"SUMO1\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"PRKAA1-controls-state-change-of-TP63\",\n      \"source\": \"PRKAA1\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"PPP1R13L-controls-state-change-of-TP53\",\n      \"source\": \"PPP1R13L\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"TP53-controls-expression-of-PLK3\",\n      \"source\": \"TP53\",\n      \"target\": \"PLK3\"\n    },\n    {\n      \"id\": \"PRKAA2-controls-state-change-of-TP63\",\n      \"source\": \"PRKAA2\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"PRKAB2-controls-state-change-of-TP53\",\n      \"source\": \"PRKAB2\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"DAPK1-controls-state-change-of-TP53\",\n      \"source\": \"DAPK1\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"CDK1-controls-state-change-of-TP53\",\n      \"source\": \"CDK1\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"DAPK3-controls-state-change-of-TP53\",\n      \"source\": \"DAPK3\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"PRKAB1-controls-state-change-of-TP63\",\n      \"source\": \"PRKAB1\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"SUMO1-controls-state-change-of-TP63\",\n      \"source\": \"SUMO1\",\n      \"target\": \"TP63\"\n    },\n    {\n      \"id\": \"PRKAA1-controls-state-change-of-TP73\",\n      \"source\": \"PRKAA1\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"E4F1-controls-state-change-of-TP53\",\n      \"source\": \"E4F1\",\n      \"target\": \"TP53\"\n    },\n    {\n      \"id\": \"PRKAA2-controls-state-change-of-TP73\",\n      \"source\": \"PRKAA2\",\n      \"target\": \"TP73\"\n    },\n    {\n      \"id\": \"PRKAB2-controls-state-change-of-TP63\",\n      \"source\": \"PRKAB2\",\n      \"target\": \"TP63\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/language-tree.json",
    "content": "{\n  \"nodes\": [\n    {\n      \"id\": \"Proto Indo-European\",\n      \"children\": [\n        \"Balto-Slavic\",\n        \"Germanic\",\n        \"Celtic\",\n        \"Italic\",\n        \"Hellenic\",\n        \"Anatolian\",\n        \"Indo-Iranian\",\n        \"Tocharian\",\n        \"Phrygian\",\n        \"Armenian\",\n        \"Albanian\",\n        \"Thracian\"\n      ]\n    },\n    {\n      \"id\": \"Balto-Slavic\",\n      \"children\": [\"Baltic\", \"Slavic\"]\n    },\n    {\n      \"id\": \"Baltic\",\n      \"children\": [\"Old Prussian\", \"Lithuanian\", \"Latvian\"]\n    },\n    {\n      \"id\": \"Old Prussian\"\n    },\n    {\n      \"id\": \"Lithuanian\"\n    },\n    {\n      \"id\": \"Latvian\"\n    },\n    {\n      \"id\": \"Slavic\",\n      \"children\": [\"East Slavic\", \"West Slavic\", \"South Slavic\"]\n    },\n    {\n      \"id\": \"East Slavic\",\n      \"children\": [\"Bulgarian\", \"Old Church Slavonic\", \"Macedonian\", \"Serbo-Croatian\", \"Slovene\"]\n    },\n    {\n      \"id\": \"Bulgarian\"\n    },\n    {\n      \"id\": \"Old Church Slavonic\"\n    },\n    {\n      \"id\": \"Macedonian\"\n    },\n    {\n      \"id\": \"Serbo-Croatian\"\n    },\n    {\n      \"id\": \"Slovene\"\n    },\n    {\n      \"id\": \"West Slavic\",\n      \"children\": [\"Polish\", \"Slovak\", \"Czech\", \"Wendish\"]\n    },\n    {\n      \"id\": \"Polish\"\n    },\n    {\n      \"id\": \"Slovak\"\n    },\n    {\n      \"id\": \"Czech\"\n    },\n    {\n      \"id\": \"Wendish\"\n    },\n    {\n      \"id\": \"South Slavic\",\n      \"children\": [\"Russian\", \"Ukrainian\", \"Belarusian\", \"Rusyn\"]\n    },\n    {\n      \"id\": \"Russian\"\n    },\n    {\n      \"id\": \"Ukrainian\"\n    },\n    {\n      \"id\": \"Belarusian\"\n    },\n    {\n      \"id\": \"Rusyn\"\n    },\n    {\n      \"id\": \"Germanic\",\n      \"children\": [\"North Germanic\", \"West Germanic\", \"East Germanic\"]\n    },\n    {\n      \"id\": \"North Germanic\",\n      \"children\": [\"Old Norse\", \"Old Swedish\", \"Old Danish\"]\n    },\n    {\n      \"id\": \"Old Norse\",\n      \"children\": [\"Old Icelandic\", \"Old Norwegian\", \"Faroese\"]\n    },\n    {\n      \"id\": \"Old Icelandic\",\n      \"children\": [\"Icelandic\"]\n    },\n    {\n      \"id\": \"Icelandic\"\n    },\n    {\n      \"id\": \"Old Norwegian\",\n      \"children\": [\"Middle Norwegian\"]\n    },\n    {\n      \"id\": \"Middle Norwegian\",\n      \"children\": [\"Norwegian\"]\n    },\n    {\n      \"id\": \"Norwegian\"\n    },\n    {\n      \"id\": \"Faroese\"\n    },\n    {\n      \"id\": \"Old Swedish\",\n      \"children\": [\"Middle Swedish\"]\n    },\n    {\n      \"id\": \"Middle Swedish\",\n      \"children\": [\"Swedish\"]\n    },\n    {\n      \"id\": \"Swedish\"\n    },\n    {\n      \"id\": \"Old Danish\",\n      \"children\": [\"Middle Danish\"]\n    },\n    {\n      \"id\": \"Middle Danish\",\n      \"children\": [\"Danish\"]\n    },\n    {\n      \"id\": \"Danish\"\n    },\n    {\n      \"id\": \"West Germanic\",\n      \"children\": [\"Old English\", \"Old Frisian\", \"Old Dutch\", \"Old Low German\", \"Old High German\"]\n    },\n    {\n      \"id\": \"Old English\",\n      \"children\": [\"Middle English\"]\n    },\n    {\n      \"id\": \"Middle English\",\n      \"children\": [\"English\"]\n    },\n    {\n      \"id\": \"English\"\n    },\n    {\n      \"id\": \"Old Frisian\",\n      \"children\": [\"Frisian\"]\n    },\n    {\n      \"id\": \"Frisian\"\n    },\n    {\n      \"id\": \"Old Dutch\",\n      \"children\": [\"Middle Dutch\"]\n    },\n    {\n      \"id\": \"Middle Dutch\",\n      \"children\": [\"Hollandic\", \"Flemish\", \"Dutch\", \"Limburgish\", \"Brabantian\", \"Rhinelandic\"]\n    },\n    {\n      \"id\": \"Hollandic\"\n    },\n    {\n      \"id\": \"Flemish\"\n    },\n    {\n      \"id\": \"Dutch\"\n    },\n    {\n      \"id\": \"Limburgish\"\n    },\n    {\n      \"id\": \"Brabantian\"\n    },\n    {\n      \"id\": \"Rhinelandic\"\n    },\n    {\n      \"id\": \"Old Low German\",\n      \"children\": [\"Middle Low German\"]\n    },\n    {\n      \"id\": \"Middle Low German\",\n      \"children\": [\"Low German\"]\n    },\n    {\n      \"id\": \"Low German\"\n    },\n    {\n      \"id\": \"Old High German\",\n      \"children\": [\"Middle High German\"]\n    },\n    {\n      \"id\": \"Middle High German\",\n      \"children\": [\"(High) German\", \"Yiddish\"]\n    },\n    {\n      \"id\": \"(High) German\"\n    },\n    {\n      \"id\": \"Yiddish\"\n    },\n    {\n      \"id\": \"East Germanic\",\n      \"children\": [\"Gothic\"]\n    },\n    {\n      \"id\": \"Gothic\"\n    },\n    {\n      \"id\": \"Celtic\",\n      \"children\": [\"Brythonic\", \"Goidelic\"]\n    },\n    {\n      \"id\": \"Brythonic\",\n      \"children\": [\"Welsh\", \"Breton\", \"Cornish\", \"Cuymbric\"]\n    },\n    {\n      \"id\": \"Welsh\"\n    },\n    {\n      \"id\": \"Breton\"\n    },\n    {\n      \"id\": \"Cornish\"\n    },\n    {\n      \"id\": \"Cuymbric\"\n    },\n    {\n      \"id\": \"Goidelic\",\n      \"children\": [\"Modern Irish\", \"Scottish Gaelic\", \"Manx\"]\n    },\n    {\n      \"id\": \"Modern Irish\"\n    },\n    {\n      \"id\": \"Scottish Gaelic\"\n    },\n    {\n      \"id\": \"Manx\"\n    },\n    {\n      \"id\": \"Italic\",\n      \"children\": [\"Osco-Umbrian\", \"Latino-Faliscan\"]\n    },\n    {\n      \"id\": \"Osco-Umbrian\",\n      \"children\": [\"Umbrian\", \"Oscan\"]\n    },\n    {\n      \"id\": \"Umbrian\"\n    },\n    {\n      \"id\": \"Oscan\"\n    },\n    {\n      \"id\": \"Latino-Faliscan\",\n      \"children\": [\"Latin\", \"Faliscan\"]\n    },\n    {\n      \"id\": \"Latin\",\n      \"children\": [\n        \"Portugese\",\n        \"Spanish\",\n        \"French\",\n        \"Romanian\",\n        \"Italian\",\n        \"Catalan\",\n        \"Franco-Provençal\",\n        \"Rhaeto-Romance\"\n      ]\n    },\n    {\n      \"id\": \"Portugese\"\n    },\n    {\n      \"id\": \"Spanish\"\n    },\n    {\n      \"id\": \"French\"\n    },\n    {\n      \"id\": \"Romanian\"\n    },\n    {\n      \"id\": \"Italian\"\n    },\n    {\n      \"id\": \"Catalan\"\n    },\n    {\n      \"id\": \"Franco-Provençal\"\n    },\n    {\n      \"id\": \"Rhaeto-Romance\"\n    },\n    {\n      \"id\": \"Faliscan\"\n    },\n    {\n      \"id\": \"Hellenic\",\n      \"children\": [\"Greek\"]\n    },\n    {\n      \"id\": \"Greek\"\n    },\n    {\n      \"id\": \"Anatolian\",\n      \"children\": [\"Hittite\", \"Palaic\", \"Luwic\", \"Lydian\"]\n    },\n    {\n      \"id\": \"Hittite\"\n    },\n    {\n      \"id\": \"Palaic\"\n    },\n    {\n      \"id\": \"Luwic\"\n    },\n    {\n      \"id\": \"Lydian\"\n    },\n    {\n      \"id\": \"Indo-Iranian\",\n      \"children\": [\"Dardic\", \"Indic\", \"Iranian\"]\n    },\n    {\n      \"id\": \"Dardic\",\n      \"children\": [\"Dard\"]\n    },\n    {\n      \"id\": \"Dard\"\n    },\n    {\n      \"id\": \"Indic\",\n      \"children\": [\"Sanskrit\"]\n    },\n    {\n      \"id\": \"Sanskrit\",\n      \"children\": [\n        \"Sindhi\",\n        \"Romani\",\n        \"Urdu\",\n        \"Hindi\",\n        \"Bihari\",\n        \"Assamese\",\n        \"Bengali\",\n        \"Marathi\",\n        \"Gujarati\",\n        \"Punjabi\",\n        \"Sinhalese\"\n      ]\n    },\n    {\n      \"id\": \"Sindhi\"\n    },\n    {\n      \"id\": \"Romani\"\n    },\n    {\n      \"id\": \"Urdu\"\n    },\n    {\n      \"id\": \"Hindi\"\n    },\n    {\n      \"id\": \"Bihari\"\n    },\n    {\n      \"id\": \"Assamese\"\n    },\n    {\n      \"id\": \"Bengali\"\n    },\n    {\n      \"id\": \"Marathi\"\n    },\n    {\n      \"id\": \"Gujarati\"\n    },\n    {\n      \"id\": \"Punjabi\"\n    },\n    {\n      \"id\": \"Sinhalese\"\n    },\n    {\n      \"id\": \"Iranian\",\n      \"children\": [\"Old Persian\", \"Balochi\", \"Kurdish\", \"Pashto\", \"Sogdian\"]\n    },\n    {\n      \"id\": \"Old Persian\",\n      \"children\": [\"Middle Persian\", \"Pahlavi\"]\n    },\n    {\n      \"id\": \"Middle Persian\",\n      \"children\": [\"Persian\"]\n    },\n    {\n      \"id\": \"Persian\"\n    },\n    {\n      \"id\": \"Pahlavi\"\n    },\n    {\n      \"id\": \"Balochi\"\n    },\n    {\n      \"id\": \"Kurdish\"\n    },\n    {\n      \"id\": \"Pashto\"\n    },\n    {\n      \"id\": \"Sogdian\"\n    },\n    {\n      \"id\": \"Tocharian\",\n      \"children\": [\"Tocharian A\", \"Tocharian B\"]\n    },\n    {\n      \"id\": \"Tocharian A\"\n    },\n    {\n      \"id\": \"Tocharian B\"\n    },\n    {\n      \"id\": \"Phrygian\"\n    },\n    {\n      \"id\": \"Armenian\"\n    },\n    {\n      \"id\": \"Albanian\"\n    },\n    {\n      \"id\": \"Thracian\"\n    }\n  ],\n  \"edges\": [\n    {\n      \"source\": \"Proto Indo-European\",\n      \"target\": \"Balto-Slavic\"\n    },\n    {\n      \"source\": \"Proto Indo-European\",\n      \"target\": \"Germanic\"\n    },\n    {\n      \"source\": \"Proto Indo-European\",\n      \"target\": \"Celtic\"\n    },\n    {\n      \"source\": \"Proto Indo-European\",\n      \"target\": \"Italic\"\n    },\n    {\n      \"source\": \"Proto Indo-European\",\n      \"target\": \"Hellenic\"\n    },\n    {\n      \"source\": \"Proto Indo-European\",\n      \"target\": \"Anatolian\"\n    },\n    {\n      \"source\": \"Proto Indo-European\",\n      \"target\": \"Indo-Iranian\"\n    },\n    {\n      \"source\": \"Proto Indo-European\",\n      \"target\": \"Tocharian\"\n    },\n    {\n      \"source\": \"Proto Indo-European\",\n      \"target\": \"Phrygian\"\n    },\n    {\n      \"source\": \"Proto Indo-European\",\n      \"target\": \"Armenian\"\n    },\n    {\n      \"source\": \"Proto Indo-European\",\n      \"target\": \"Albanian\"\n    },\n    {\n      \"source\": \"Proto Indo-European\",\n      \"target\": \"Thracian\"\n    },\n    {\n      \"source\": \"Balto-Slavic\",\n      \"target\": \"Baltic\"\n    },\n    {\n      \"source\": \"Balto-Slavic\",\n      \"target\": \"Slavic\"\n    },\n    {\n      \"source\": \"Baltic\",\n      \"target\": \"Old Prussian\"\n    },\n    {\n      \"source\": \"Baltic\",\n      \"target\": \"Lithuanian\"\n    },\n    {\n      \"source\": \"Baltic\",\n      \"target\": \"Latvian\"\n    },\n    {\n      \"source\": \"Slavic\",\n      \"target\": \"East Slavic\"\n    },\n    {\n      \"source\": \"Slavic\",\n      \"target\": \"West Slavic\"\n    },\n    {\n      \"source\": \"Slavic\",\n      \"target\": \"South Slavic\"\n    },\n    {\n      \"source\": \"East Slavic\",\n      \"target\": \"Bulgarian\"\n    },\n    {\n      \"source\": \"East Slavic\",\n      \"target\": \"Old Church Slavonic\"\n    },\n    {\n      \"source\": \"East Slavic\",\n      \"target\": \"Macedonian\"\n    },\n    {\n      \"source\": \"East Slavic\",\n      \"target\": \"Serbo-Croatian\"\n    },\n    {\n      \"source\": \"East Slavic\",\n      \"target\": \"Slovene\"\n    },\n    {\n      \"source\": \"West Slavic\",\n      \"target\": \"Polish\"\n    },\n    {\n      \"source\": \"West Slavic\",\n      \"target\": \"Slovak\"\n    },\n    {\n      \"source\": \"West Slavic\",\n      \"target\": \"Czech\"\n    },\n    {\n      \"source\": \"West Slavic\",\n      \"target\": \"Wendish\"\n    },\n    {\n      \"source\": \"South Slavic\",\n      \"target\": \"Russian\"\n    },\n    {\n      \"source\": \"South Slavic\",\n      \"target\": \"Ukrainian\"\n    },\n    {\n      \"source\": \"South Slavic\",\n      \"target\": \"Belarusian\"\n    },\n    {\n      \"source\": \"South Slavic\",\n      \"target\": \"Rusyn\"\n    },\n    {\n      \"source\": \"Germanic\",\n      \"target\": \"North Germanic\"\n    },\n    {\n      \"source\": \"Germanic\",\n      \"target\": \"West Germanic\"\n    },\n    {\n      \"source\": \"Germanic\",\n      \"target\": \"East Germanic\"\n    },\n    {\n      \"source\": \"North Germanic\",\n      \"target\": \"Old Norse\"\n    },\n    {\n      \"source\": \"North Germanic\",\n      \"target\": \"Old Swedish\"\n    },\n    {\n      \"source\": \"North Germanic\",\n      \"target\": \"Old Danish\"\n    },\n    {\n      \"source\": \"Old Norse\",\n      \"target\": \"Old Icelandic\"\n    },\n    {\n      \"source\": \"Old Norse\",\n      \"target\": \"Old Norwegian\"\n    },\n    {\n      \"source\": \"Old Norse\",\n      \"target\": \"Faroese\"\n    },\n    {\n      \"source\": \"Old Icelandic\",\n      \"target\": \"Icelandic\"\n    },\n    {\n      \"source\": \"Old Norwegian\",\n      \"target\": \"Middle Norwegian\"\n    },\n    {\n      \"source\": \"Middle Norwegian\",\n      \"target\": \"Norwegian\"\n    },\n    {\n      \"source\": \"Old Swedish\",\n      \"target\": \"Middle Swedish\"\n    },\n    {\n      \"source\": \"Middle Swedish\",\n      \"target\": \"Swedish\"\n    },\n    {\n      \"source\": \"Old Danish\",\n      \"target\": \"Middle Danish\"\n    },\n    {\n      \"source\": \"Middle Danish\",\n      \"target\": \"Danish\"\n    },\n    {\n      \"source\": \"West Germanic\",\n      \"target\": \"Old English\"\n    },\n    {\n      \"source\": \"West Germanic\",\n      \"target\": \"Old Frisian\"\n    },\n    {\n      \"source\": \"West Germanic\",\n      \"target\": \"Old Dutch\"\n    },\n    {\n      \"source\": \"West Germanic\",\n      \"target\": \"Old Low German\"\n    },\n    {\n      \"source\": \"West Germanic\",\n      \"target\": \"Old High German\"\n    },\n    {\n      \"source\": \"Old English\",\n      \"target\": \"Middle English\"\n    },\n    {\n      \"source\": \"Middle English\",\n      \"target\": \"English\"\n    },\n    {\n      \"source\": \"Old Frisian\",\n      \"target\": \"Frisian\"\n    },\n    {\n      \"source\": \"Old Dutch\",\n      \"target\": \"Middle Dutch\"\n    },\n    {\n      \"source\": \"Middle Dutch\",\n      \"target\": \"Hollandic\"\n    },\n    {\n      \"source\": \"Middle Dutch\",\n      \"target\": \"Flemish\"\n    },\n    {\n      \"source\": \"Middle Dutch\",\n      \"target\": \"Dutch\"\n    },\n    {\n      \"source\": \"Middle Dutch\",\n      \"target\": \"Limburgish\"\n    },\n    {\n      \"source\": \"Middle Dutch\",\n      \"target\": \"Brabantian\"\n    },\n    {\n      \"source\": \"Middle Dutch\",\n      \"target\": \"Rhinelandic\"\n    },\n    {\n      \"source\": \"Old Low German\",\n      \"target\": \"Middle Low German\"\n    },\n    {\n      \"source\": \"Middle Low German\",\n      \"target\": \"Low German\"\n    },\n    {\n      \"source\": \"Old High German\",\n      \"target\": \"Middle High German\"\n    },\n    {\n      \"source\": \"Middle High German\",\n      \"target\": \"(High) German\"\n    },\n    {\n      \"source\": \"Middle High German\",\n      \"target\": \"Yiddish\"\n    },\n    {\n      \"source\": \"East Germanic\",\n      \"target\": \"Gothic\"\n    },\n    {\n      \"source\": \"Celtic\",\n      \"target\": \"Brythonic\"\n    },\n    {\n      \"source\": \"Celtic\",\n      \"target\": \"Goidelic\"\n    },\n    {\n      \"source\": \"Brythonic\",\n      \"target\": \"Welsh\"\n    },\n    {\n      \"source\": \"Brythonic\",\n      \"target\": \"Breton\"\n    },\n    {\n      \"source\": \"Brythonic\",\n      \"target\": \"Cornish\"\n    },\n    {\n      \"source\": \"Brythonic\",\n      \"target\": \"Cuymbric\"\n    },\n    {\n      \"source\": \"Goidelic\",\n      \"target\": \"Modern Irish\"\n    },\n    {\n      \"source\": \"Goidelic\",\n      \"target\": \"Scottish Gaelic\"\n    },\n    {\n      \"source\": \"Goidelic\",\n      \"target\": \"Manx\"\n    },\n    {\n      \"source\": \"Italic\",\n      \"target\": \"Osco-Umbrian\"\n    },\n    {\n      \"source\": \"Italic\",\n      \"target\": \"Latino-Faliscan\"\n    },\n    {\n      \"source\": \"Osco-Umbrian\",\n      \"target\": \"Umbrian\"\n    },\n    {\n      \"source\": \"Osco-Umbrian\",\n      \"target\": \"Oscan\"\n    },\n    {\n      \"source\": \"Latino-Faliscan\",\n      \"target\": \"Latin\"\n    },\n    {\n      \"source\": \"Latino-Faliscan\",\n      \"target\": \"Faliscan\"\n    },\n    {\n      \"source\": \"Latin\",\n      \"target\": \"Portugese\"\n    },\n    {\n      \"source\": \"Latin\",\n      \"target\": \"Spanish\"\n    },\n    {\n      \"source\": \"Latin\",\n      \"target\": \"French\"\n    },\n    {\n      \"source\": \"Latin\",\n      \"target\": \"Romanian\"\n    },\n    {\n      \"source\": \"Latin\",\n      \"target\": \"Italian\"\n    },\n    {\n      \"source\": \"Latin\",\n      \"target\": \"Catalan\"\n    },\n    {\n      \"source\": \"Latin\",\n      \"target\": \"Franco-Provençal\"\n    },\n    {\n      \"source\": \"Latin\",\n      \"target\": \"Rhaeto-Romance\"\n    },\n    {\n      \"source\": \"Hellenic\",\n      \"target\": \"Greek\"\n    },\n    {\n      \"source\": \"Anatolian\",\n      \"target\": \"Hittite\"\n    },\n    {\n      \"source\": \"Anatolian\",\n      \"target\": \"Palaic\"\n    },\n    {\n      \"source\": \"Anatolian\",\n      \"target\": \"Luwic\"\n    },\n    {\n      \"source\": \"Anatolian\",\n      \"target\": \"Lydian\"\n    },\n    {\n      \"source\": \"Indo-Iranian\",\n      \"target\": \"Dardic\"\n    },\n    {\n      \"source\": \"Indo-Iranian\",\n      \"target\": \"Indic\"\n    },\n    {\n      \"source\": \"Indo-Iranian\",\n      \"target\": \"Iranian\"\n    },\n    {\n      \"source\": \"Dardic\",\n      \"target\": \"Dard\"\n    },\n    {\n      \"source\": \"Indic\",\n      \"target\": \"Sanskrit\"\n    },\n    {\n      \"source\": \"Sanskrit\",\n      \"target\": \"Sindhi\"\n    },\n    {\n      \"source\": \"Sanskrit\",\n      \"target\": \"Romani\"\n    },\n    {\n      \"source\": \"Sanskrit\",\n      \"target\": \"Urdu\"\n    },\n    {\n      \"source\": \"Sanskrit\",\n      \"target\": \"Hindi\"\n    },\n    {\n      \"source\": \"Sanskrit\",\n      \"target\": \"Bihari\"\n    },\n    {\n      \"source\": \"Sanskrit\",\n      \"target\": \"Assamese\"\n    },\n    {\n      \"source\": \"Sanskrit\",\n      \"target\": \"Bengali\"\n    },\n    {\n      \"source\": \"Sanskrit\",\n      \"target\": \"Marathi\"\n    },\n    {\n      \"source\": \"Sanskrit\",\n      \"target\": \"Gujarati\"\n    },\n    {\n      \"source\": \"Sanskrit\",\n      \"target\": \"Punjabi\"\n    },\n    {\n      \"source\": \"Sanskrit\",\n      \"target\": \"Sinhalese\"\n    },\n    {\n      \"source\": \"Iranian\",\n      \"target\": \"Old Persian\"\n    },\n    {\n      \"source\": \"Iranian\",\n      \"target\": \"Balochi\"\n    },\n    {\n      \"source\": \"Iranian\",\n      \"target\": \"Kurdish\"\n    },\n    {\n      \"source\": \"Iranian\",\n      \"target\": \"Pashto\"\n    },\n    {\n      \"source\": \"Iranian\",\n      \"target\": \"Sogdian\"\n    },\n    {\n      \"source\": \"Old Persian\",\n      \"target\": \"Middle Persian\"\n    },\n    {\n      \"source\": \"Old Persian\",\n      \"target\": \"Pahlavi\"\n    },\n    {\n      \"source\": \"Middle Persian\",\n      \"target\": \"Persian\"\n    },\n    {\n      \"source\": \"Tocharian\",\n      \"target\": \"Tocharian A\"\n    },\n    {\n      \"source\": \"Tocharian\",\n      \"target\": \"Tocharian B\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/organization-chart.json",
    "content": "{\n  \"nodes\": [\n    {\n      \"id\": \"0\",\n      \"data\": {\n        \"email\": \"ejoplin@yoyodyne.com\",\n        \"fax\": \"555-0101\",\n        \"name\": \"Eric Joplin\",\n        \"phone\": \"555-0100\",\n        \"position\": \"Chief Executive Officer\",\n        \"status\": \"online\"\n      }\n    },\n    {\n      \"id\": \"1\",\n      \"data\": {\n        \"email\": \"groberts@yoyodyne.com\",\n        \"fax\": \"555-0101\",\n        \"name\": \"Gary Roberts\",\n        \"phone\": \"555-0100\",\n        \"position\": \"Chief Executive Assistant\",\n        \"status\": \"busy\"\n      }\n    },\n    {\n      \"id\": \"2\",\n      \"data\": {\n        \"email\": \"aburns@yoyodyne.com\",\n        \"fax\": \"555-0103\",\n        \"name\": \"Alex Burns\",\n        \"phone\": \"555-0102\",\n        \"position\": \"Senior Executive Assistant\",\n        \"status\": \"offline\"\n      }\n    },\n    {\n      \"id\": \"4\",\n      \"data\": {\n        \"email\": \"msmith@yoyodyne.com\",\n        \"fax\": \"555-0115\",\n        \"name\": \"Mary Smith\",\n        \"phone\": \"555-0114\",\n        \"position\": \"Finance Manager\",\n        \"status\": \"busy\"\n      }\n    },\n    {\n      \"id\": \"5\",\n      \"data\": {\n        \"email\": \"bwhite@yoyodyne.com\",\n        \"fax\": \"555-0117\",\n        \"name\": \"Bob White\",\n        \"phone\": \"555-0116\",\n        \"position\": \"HR Manager\",\n        \"status\": \"online\"\n      }\n    },\n    {\n      \"id\": \"6\",\n      \"data\": {\n        \"email\": \"jjones@yoyodyne.com\",\n        \"fax\": \"555-0119\",\n        \"name\": \"John Jones\",\n        \"phone\": \"555-0118\",\n        \"position\": \"IT Manager\",\n        \"status\": \"offline\"\n      }\n    },\n    {\n      \"id\": \"7\",\n      \"data\": {\n        \"email\": \"klee@yoyodyne.com\",\n        \"fax\": \"555-0121\",\n        \"name\": \"Karen Lee\",\n        \"phone\": \"555-0120\",\n        \"position\": \"Marketing Manager\",\n        \"status\": \"online\"\n      }\n    },\n    {\n      \"id\": \"8\",\n      \"data\": {\n        \"email\": \"dmiller@yoyodyne.com\",\n        \"fax\": \"555-0123\",\n        \"name\": \"David Miller\",\n        \"phone\": \"555-0122\",\n        \"position\": \"Sales Manager\",\n        \"status\": \"busy\"\n      }\n    },\n    {\n      \"id\": \"9\",\n      \"data\": {\n        \"email\": \"rjoe@yoyodyne.com\",\n        \"fax\": \"555-0125\",\n        \"name\": \"Rachel Joe\",\n        \"phone\": \"555-0124\",\n        \"position\": \"Operations Manager\",\n        \"status\": \"offline\"\n      }\n    },\n    {\n      \"id\": \"10\",\n      \"data\": {\n        \"email\": \"tadams@yoyodyne.com\",\n        \"fax\": \"555-0127\",\n        \"name\": \"Tom Adams\",\n        \"phone\": \"555-0126\",\n        \"position\": \"Product Manager\",\n        \"status\": \"online\"\n      }\n    },\n    {\n      \"id\": \"11\",\n      \"data\": {\n        \"email\": \"wbrown@yoyodyne.com\",\n        \"fax\": \"555-0129\",\n        \"name\": \"Will Brown\",\n        \"phone\": \"555-0128\",\n        \"position\": \"Customer Support Manager\",\n        \"status\": \"busy\"\n      }\n    },\n    {\n      \"id\": \"12\",\n      \"data\": {\n        \"email\": \"dmartin@yoyodyne.com\",\n        \"fax\": \"555-0131\",\n        \"name\": \"Diana Martin\",\n        \"phone\": \"555-0130\",\n        \"position\": \"Compliance Officer\",\n        \"status\": \"offline\"\n      }\n    },\n    {\n      \"id\": \"13\",\n      \"data\": {\n        \"email\": \"jwilson@yoyodyne.com\",\n        \"fax\": \"555-0133\",\n        \"name\": \"Jim Wilson\",\n        \"phone\": \"555-0132\",\n        \"position\": \"Legal Counsel\",\n        \"status\": \"online\"\n      }\n    },\n    {\n      \"id\": \"14\",\n      \"data\": {\n        \"email\": \"charris@yoyodyne.com\",\n        \"fax\": \"555-0135\",\n        \"name\": \"Cathy Harris\",\n        \"phone\": \"555-0134\",\n        \"position\": \"Procurement Manager\",\n        \"status\": \"busy\"\n      }\n    },\n    {\n      \"id\": \"15\",\n      \"data\": {\n        \"email\": \"eblack@yoyodyne.com\",\n        \"fax\": \"555-0137\",\n        \"name\": \"Evan Black\",\n        \"phone\": \"555-0136\",\n        \"position\": \"Logistics Manager\",\n        \"status\": \"offline\"\n      }\n    },\n    {\n      \"id\": \"16\",\n      \"data\": {\n        \"email\": \"ywang@yoyodyne.com\",\n        \"fax\": \"555-0139\",\n        \"name\": \"Yvonne Wang\",\n        \"phone\": \"555-0138\",\n        \"position\": \"Research and Development Manager\",\n        \"status\": \"online\"\n      }\n    },\n    {\n      \"id\": \"17\",\n      \"data\": {\n        \"email\": \"jsanchez@yoyodyne.com\",\n        \"fax\": \"555-0141\",\n        \"name\": \"Juan Sanchez\",\n        \"phone\": \"555-0140\",\n        \"position\": \"Chief Technology Officer\",\n        \"status\": \"busy\"\n      }\n    },\n    {\n      \"id\": \"18\",\n      \"data\": {\n        \"email\": \"mjones@yoyodyne.com\",\n        \"fax\": \"555-0143\",\n        \"name\": \"Molly Jones\",\n        \"phone\": \"555-0142\",\n        \"position\": \"Chief Financial Officer\",\n        \"status\": \"offline\"\n      }\n    },\n    {\n      \"id\": \"19\",\n      \"data\": {\n        \"email\": \"rking@yoyodyne.com\",\n        \"fax\": \"555-0145\",\n        \"name\": \"Richard King\",\n        \"phone\": \"555-0144\",\n        \"position\": \"Chief Operating Officer\",\n        \"status\": \"online\"\n      }\n    }\n  ],\n  \"edges\": [\n    { \"source\": \"0\", \"target\": \"1\" },\n    { \"source\": \"1\", \"target\": \"2\" },\n    { \"source\": \"0\", \"target\": \"17\" },\n    { \"source\": \"0\", \"target\": \"18\" },\n    { \"source\": \"0\", \"target\": \"19\" },\n    { \"source\": \"17\", \"target\": \"6\" },\n    { \"source\": \"17\", \"target\": \"16\" },\n    { \"source\": \"18\", \"target\": \"4\" },\n    { \"source\": \"18\", \"target\": \"12\" },\n    { \"source\": \"19\", \"target\": \"9\" },\n    { \"source\": \"19\", \"target\": \"15\" },\n    { \"source\": \"4\", \"target\": \"5\" },\n    { \"source\": \"4\", \"target\": \"10\" },\n    { \"source\": \"5\", \"target\": \"8\" },\n    { \"source\": \"6\", \"target\": \"11\" },\n    { \"source\": \"9\", \"target\": \"7\" },\n    { \"source\": \"15\", \"target\": \"13\" },\n    { \"source\": \"15\", \"target\": \"14\" }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/parallel-edges.json",
    "content": "{\n  \"nodes\": [\n    {\n      \"id\": \"node1\",\n      \"style\": {\n        \"x\": 260,\n        \"y\": 220\n      }\n    },\n    {\n      \"id\": \"node2\",\n      \"style\": {\n        \"x\": 186,\n        \"y\": 342\n      }\n    },\n    {\n      \"id\": \"node3\",\n      \"style\": {\n        \"x\": 131,\n        \"y\": 194\n      }\n    },\n    {\n      \"id\": \"node4\",\n      \"style\": {\n        \"x\": 258,\n        \"y\": 80\n      }\n    },\n    {\n      \"id\": \"node5\",\n      \"style\": {\n        \"x\": 395,\n        \"y\": 186\n      }\n    },\n    {\n      \"id\": \"node6\",\n      \"style\": {\n        \"x\": 333,\n        \"y\": 337\n      }\n    }\n  ],\n  \"edges\": [\n    {\n      \"id\": \"edge1\",\n      \"source\": \"node1\",\n      \"target\": \"node4\",\n      \"style\": { \"stroke\": \"#8576FF\", \"lineWidth\": 2, \"startArrow\": true }\n    },\n    {\n      \"id\": \"edge2\",\n      \"source\": \"node4\",\n      \"target\": \"node1\",\n      \"style\": { \"endArrow\": true }\n    },\n    { \"id\": \"edge3\", \"source\": \"node4\", \"target\": \"node1\" },\n    { \"id\": \"edge4\", \"source\": \"node1\", \"target\": \"node4\" },\n    { \"id\": \"edge5\", \"source\": \"node1\", \"target\": \"node2\" },\n    { \"id\": \"edge6\", \"source\": \"node1\", \"target\": \"node2\" },\n    { \"id\": \"edge7\", \"source\": \"node1\", \"target\": \"node3\" },\n    { \"id\": \"edge8\", \"source\": \"node1\", \"target\": \"node5\" },\n    { \"id\": \"edge9\", \"source\": \"node1\", \"target\": \"node6\" },\n    { \"id\": \"edge10\", \"source\": \"node1\", \"target\": \"node6\" },\n    { \"id\": \"edge11\", \"source\": \"node1\", \"target\": \"node6\" },\n    { \"id\": \"loop1\", \"source\": \"node5\", \"target\": \"node5\", \"style\": { \"stroke\": \"#8576FF\", \"lineWidth\": 2 } },\n    { \"id\": \"loop2\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop3\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop4\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop5\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop6\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop7\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop8\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop9\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop10\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop11\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop12\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop13\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop14\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop15\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop16\", \"source\": \"node5\", \"target\": \"node5\" },\n    { \"id\": \"loop17\", \"source\": \"node5\", \"target\": \"node5\" }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/radial.json",
    "content": "{\n  \"nodes\": [\n    { \"id\": \"0\", \"data\": { \"label\": \"0\" } },\n    { \"id\": \"1\", \"data\": { \"label\": \"1\" } },\n    { \"id\": \"2\", \"data\": { \"label\": \"2\" } },\n    { \"id\": \"3\", \"data\": { \"label\": \"3\" } },\n    { \"id\": \"4\", \"data\": { \"label\": \"4\" } },\n    { \"id\": \"5\", \"data\": { \"label\": \"5\" } },\n    { \"id\": \"6\", \"data\": { \"label\": \"6\" } },\n    { \"id\": \"7\", \"data\": { \"label\": \"7\" } },\n    { \"id\": \"8\", \"data\": { \"label\": \"8\" } },\n    { \"id\": \"9\", \"data\": { \"label\": \"9\" } },\n    { \"id\": \"10\", \"data\": { \"label\": \"10\" } },\n    { \"id\": \"11\", \"data\": { \"label\": \"11\" } },\n    { \"id\": \"12\", \"data\": { \"label\": \"12\" } },\n    { \"id\": \"13\", \"data\": { \"label\": \"13\" } },\n    { \"id\": \"14\", \"data\": { \"label\": \"14\" } },\n    { \"id\": \"15\", \"data\": { \"label\": \"15\" } },\n    { \"id\": \"16\", \"data\": { \"label\": \"16\" } },\n    { \"id\": \"17\", \"data\": { \"label\": \"17\" } },\n    { \"id\": \"18\", \"data\": { \"label\": \"18\" } },\n    { \"id\": \"19\", \"data\": { \"label\": \"19\" } },\n    { \"id\": \"20\", \"data\": { \"label\": \"20\" } },\n    { \"id\": \"21\", \"data\": { \"label\": \"21\" } },\n    { \"id\": \"22\", \"data\": { \"label\": \"22\" } },\n    { \"id\": \"23\", \"data\": { \"label\": \"23\" } },\n    { \"id\": \"24\", \"data\": { \"label\": \"24\" } },\n    { \"id\": \"25\", \"data\": { \"label\": \"25\" } },\n    { \"id\": \"26\", \"data\": { \"label\": \"26\" } },\n    { \"id\": \"27\", \"data\": { \"label\": \"27\" } },\n    { \"id\": \"28\", \"data\": { \"label\": \"28\" } },\n    { \"id\": \"29\", \"data\": { \"label\": \"29\" } },\n    { \"id\": \"30\", \"data\": { \"label\": \"30\" } },\n    { \"id\": \"31\", \"data\": { \"label\": \"31\" } },\n    { \"id\": \"32\", \"data\": { \"label\": \"32\" } },\n    { \"id\": \"33\", \"data\": { \"label\": \"33\" } }\n  ],\n  \"edges\": [\n    { \"source\": \"0\", \"target\": \"1\" },\n    { \"source\": \"0\", \"target\": \"2\" },\n    { \"source\": \"0\", \"target\": \"3\" },\n    { \"source\": \"0\", \"target\": \"4\" },\n    { \"source\": \"0\", \"target\": \"5\" },\n    { \"source\": \"0\", \"target\": \"7\" },\n    { \"source\": \"0\", \"target\": \"8\" },\n    { \"source\": \"0\", \"target\": \"9\" },\n    { \"source\": \"0\", \"target\": \"10\" },\n    { \"source\": \"0\", \"target\": \"11\" },\n    { \"source\": \"0\", \"target\": \"13\" },\n    { \"source\": \"0\", \"target\": \"14\" },\n    { \"source\": \"0\", \"target\": \"15\" },\n    { \"source\": \"0\", \"target\": \"16\" },\n    { \"source\": \"2\", \"target\": \"3\" },\n    { \"source\": \"4\", \"target\": \"5\" },\n    { \"source\": \"4\", \"target\": \"6\" },\n    { \"source\": \"5\", \"target\": \"6\" },\n    { \"source\": \"7\", \"target\": \"13\" },\n    { \"source\": \"8\", \"target\": \"14\" },\n    { \"source\": \"10\", \"target\": \"22\" },\n    { \"source\": \"10\", \"target\": \"14\" },\n    { \"source\": \"10\", \"target\": \"12\" },\n    { \"source\": \"10\", \"target\": \"24\" },\n    { \"source\": \"10\", \"target\": \"21\" },\n    { \"source\": \"10\", \"target\": \"20\" },\n    { \"source\": \"11\", \"target\": \"24\" },\n    { \"source\": \"11\", \"target\": \"22\" },\n    { \"source\": \"11\", \"target\": \"14\" },\n    { \"source\": \"12\", \"target\": \"13\" },\n    { \"source\": \"16\", \"target\": \"17\" },\n    { \"source\": \"16\", \"target\": \"18\" },\n    { \"source\": \"16\", \"target\": \"21\" },\n    { \"source\": \"16\", \"target\": \"22\" },\n    { \"source\": \"17\", \"target\": \"18\" },\n    { \"source\": \"17\", \"target\": \"20\" },\n    { \"source\": \"18\", \"target\": \"19\" },\n    { \"source\": \"19\", \"target\": \"20\" },\n    { \"source\": \"19\", \"target\": \"33\" },\n    { \"source\": \"19\", \"target\": \"22\" },\n    { \"source\": \"19\", \"target\": \"23\" },\n    { \"source\": \"20\", \"target\": \"21\" },\n    { \"source\": \"21\", \"target\": \"22\" },\n    { \"source\": \"22\", \"target\": \"24\" },\n    { \"source\": \"22\", \"target\": \"26\" },\n    { \"source\": \"22\", \"target\": \"23\" },\n    { \"source\": \"22\", \"target\": \"28\" },\n    { \"source\": \"22\", \"target\": \"30\" },\n    { \"source\": \"22\", \"target\": \"31\" },\n    { \"source\": \"22\", \"target\": \"32\" },\n    { \"source\": \"22\", \"target\": \"33\" },\n    { \"source\": \"23\", \"target\": \"28\" },\n    { \"source\": \"23\", \"target\": \"27\" },\n    { \"source\": \"23\", \"target\": \"29\" },\n    { \"source\": \"23\", \"target\": \"30\" },\n    { \"source\": \"23\", \"target\": \"31\" },\n    { \"source\": \"23\", \"target\": \"33\" },\n    { \"source\": \"32\", \"target\": \"33\" }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/relations.json",
    "content": "{\n  \"nodes\": [\n    { \"id\": \"Myriel\", \"style\": { \"x\": 197.13154409979438, \"y\": 58.49567372045294 } },\n    { \"id\": \"Napoleon\", \"style\": { \"x\": 147.01896389692396, \"y\": 22.47017586685877 } },\n    { \"id\": \"Mlle.Baptistine\", \"style\": { \"x\": 225.53929622396657, \"y\": 141.52203994343503 } },\n    { \"id\": \"Mme.Magloire\", \"style\": { \"x\": 255.07906424356426, \"y\": 120.2538776202175 } },\n    { \"id\": \"CountessdeLo\", \"style\": { \"x\": 151.886941377147, \"y\": -3.5440526274605024 } },\n    { \"id\": \"Geborand\", \"style\": { \"x\": 136.99780912786676, \"y\": 41.74972346367764 } },\n    { \"id\": \"Champtercier\", \"style\": { \"x\": 227.06448529213904, \"y\": 8.803245731763797 } },\n    { \"id\": \"Cravatte\", \"style\": { \"x\": 172.28712104569624, \"y\": -10.28659385020346 } },\n    { \"id\": \"Count\", \"style\": { \"x\": 172.9776128536988, \"y\": 12.515280485950003 } },\n    { \"id\": \"OldMan\", \"style\": { \"x\": 198.7549153659034, \"y\": -6.143466139379697 } },\n    { \"id\": \"Labarre\", \"style\": { \"x\": 266.5746386228216, \"y\": 203.98384539788222 } },\n    { \"id\": \"Valjean\", \"style\": { \"x\": 322.22242753596396, \"y\": 221.58991461580462 } },\n    { \"id\": \"Marguerite\", \"style\": { \"x\": 265.1218339265034, \"y\": 171.59761511302105 } },\n    { \"id\": \"Mme.deR\", \"style\": { \"x\": 299.78639359854327, \"y\": 133.57398015667923 } },\n    { \"id\": \"Isabeau\", \"style\": { \"x\": 282.69786358028415, \"y\": 191.50678051232913 } },\n    { \"id\": \"Gervais\", \"style\": { \"x\": 334.4562033716733, \"y\": 148.86340203151713 } },\n    { \"id\": \"Tholomyes\", \"style\": { \"x\": 359.6758601570104, \"y\": 158.51932058679517 } },\n    { \"id\": \"Listolier\", \"style\": { \"x\": 308.6408107258377, \"y\": 80.08978211784734 } },\n    { \"id\": \"Fameuil\", \"style\": { \"x\": 329.1208783621155, \"y\": 89.50783923513406 } },\n    { \"id\": \"Blacheville\", \"style\": { \"x\": 351.31710942912247, \"y\": 95.62381874446997 } },\n    { \"id\": \"Favourite\", \"style\": { \"x\": 284.0990966456606, \"y\": 153.6649901350214 } },\n    { \"id\": \"Dahlia\", \"style\": { \"x\": 303.2794454950651, \"y\": 170.87469919068386 } },\n    { \"id\": \"Zephine\", \"style\": { \"x\": 286.9038607953858, \"y\": 94.82364610010669 } },\n    { \"id\": \"Fantine\", \"style\": { \"x\": 337.7295856292113, \"y\": 187.2760733153313 } },\n    { \"id\": \"Mme.Thenardier\", \"style\": { \"x\": 283.8431887426204, \"y\": 267.7101161193055 } },\n    { \"id\": \"Thenardier\", \"style\": { \"x\": 317.6539018281542, \"y\": 300.0586304481375 } },\n    { \"id\": \"Cosette\", \"style\": { \"x\": 343.4495217104461, \"y\": 248.14013534143953 } },\n    { \"id\": \"Javert\", \"style\": { \"x\": 368.6281356589531, \"y\": 263.5847126845181 } },\n    { \"id\": \"Fauchelevent\", \"style\": { \"x\": 377.3520676841103, \"y\": 176.72534157485532 } },\n    { \"id\": \"Bamatabois\", \"style\": { \"x\": 391.75313851634024, \"y\": 156.5212161097912 } },\n    { \"id\": \"Perpetue\", \"style\": { \"x\": 234.8199749437348, \"y\": 195.99976079362335 } },\n    { \"id\": \"Simplice\", \"style\": { \"x\": 286.4937544345336, \"y\": 227.73420851527578 } },\n    { \"id\": \"Scaufflaire\", \"style\": { \"x\": 250.02919011143416, \"y\": 231.2513211913802 } },\n    { \"id\": \"Woman1\", \"style\": { \"x\": 375.4668487891018, \"y\": 202.783515421686 } },\n    { \"id\": \"Judge\", \"style\": { \"x\": 370.1700307319093, \"y\": 139.4810861650384 } },\n    { \"id\": \"Champmathieu\", \"style\": { \"x\": 404.6422482933774, \"y\": 216.58364918349568 } },\n    { \"id\": \"Brevet\", \"style\": { \"x\": 399.2513775912632, \"y\": 183.03026453336724 } },\n    { \"id\": \"Chenildieu\", \"style\": { \"x\": 425.90996667472837, \"y\": 194.79658513642403 } },\n    { \"id\": \"Cochepaille\", \"style\": { \"x\": 419.38361105364334, \"y\": 148.69180823448008 } },\n    { \"id\": \"Pontmercy\", \"style\": { \"x\": 375.2946100421193, \"y\": 307.66682817782345 } },\n    { \"id\": \"Boulatruelle\", \"style\": { \"x\": 260.66757416917164, \"y\": 279.0949406815367 } },\n    { \"id\": \"Eponine\", \"style\": { \"x\": 268.68796660221636, \"y\": 365.8200533034293 } },\n    { \"id\": \"Anzelma\", \"style\": { \"x\": 234.53762633403787, \"y\": 303.08504254821366 } },\n    { \"id\": \"Woman2\", \"style\": { \"x\": 304.29126463264043, \"y\": 254.05392981470945 } },\n    { \"id\": \"MotherInnocent\", \"style\": { \"x\": 350.35613429759803, \"y\": 214.42252912270644 } },\n    { \"id\": \"Gribier\", \"style\": { \"x\": 437.51920169330805, \"y\": 160.14388411785757 } },\n    { \"id\": \"Jondrette\", \"style\": { \"x\": 510.1406569699257, \"y\": 327.7456828911454 } },\n    { \"id\": \"Mme.Burgon\", \"style\": { \"x\": 466.0856874797108, \"y\": 368.0210264990602 } },\n    { \"id\": \"Gavroche\", \"style\": { \"x\": 393.6973181801981, \"y\": 380.40382743216634 } },\n    { \"id\": \"Gillenormand\", \"style\": { \"x\": 338.1148595335302, \"y\": 286.4434006942807 } },\n    { \"id\": \"Magnon\", \"style\": { \"x\": 277.12320020410266, \"y\": 317.4384382481713 } },\n    { \"id\": \"Mlle.Gillenormand\", \"style\": { \"x\": 257.52167498720337, \"y\": 306.4604520400414 } },\n    { \"id\": \"Mme.Pontmercy\", \"style\": { \"x\": 307.71325168392366, \"y\": 318.0074114921048 } },\n    { \"id\": \"Mlle.Vaubois\", \"style\": { \"x\": 197.63137784390082, \"y\": 325.2999365859076 } },\n    { \"id\": \"Lt.Gillenormand\", \"style\": { \"x\": 294.4105849543593, \"y\": 296.53686533697186 } },\n    { \"id\": \"Marius\", \"style\": { \"x\": 336.3436812430268, \"y\": 350.8376519695578 } },\n    { \"id\": \"BaronessT\", \"style\": { \"x\": 390.6807729530675, \"y\": 322.9175698803163 } },\n    { \"id\": \"Mabeuf\", \"style\": { \"x\": 366.77554563642803, \"y\": 445.26666512175433 } },\n    { \"id\": \"Enjolras\", \"style\": { \"x\": 376.9421415192702, \"y\": 371.1750781444891 } },\n    { \"id\": \"Combeferre\", \"style\": { \"x\": 397.0516872015465, \"y\": 416.38478793328625 } },\n    { \"id\": \"Prouvaire\", \"style\": { \"x\": 309.0241345496318, \"y\": 426.44215271462605 } },\n    { \"id\": \"Feuilly\", \"style\": { \"x\": 314.71137563489117, \"y\": 456.80172690673896 } },\n    { \"id\": \"Courfeyrac\", \"style\": { \"x\": 332.8405296045364, \"y\": 435.8881866127797 } },\n    { \"id\": \"Bahorel\", \"style\": { \"x\": 343.1268360879219, \"y\": 466.9404473411801 } },\n    { \"id\": \"Bossuet\", \"style\": { \"x\": 305.84814130923144, \"y\": 382.89355947309724 } },\n    { \"id\": \"Joly\", \"style\": { \"x\": 371.447442010866, \"y\": 415.99688422022257 } },\n    { \"id\": \"Grantaire\", \"style\": { \"x\": 370.72651876919826, \"y\": 466.96671298340794 } },\n    { \"id\": \"MotherPlutarch\", \"style\": { \"x\": 424.04457501182867, \"y\": 461.9373924104361 } },\n    { \"id\": \"Gueulemer\", \"style\": { \"x\": 344.1315821958891, \"y\": 323.7890765583486 } },\n    { \"id\": \"Babet\", \"style\": { \"x\": 367.3969014122835, \"y\": 319.2359576043117 } },\n    { \"id\": \"Claquesous\", \"style\": { \"x\": 303.23885194199465, \"y\": 347.8041412708572 } },\n    { \"id\": \"Montparnasse\", \"style\": { \"x\": 322.6528688110919, \"y\": 330.01757397802925 } },\n    { \"id\": \"Toussaint\", \"style\": { \"x\": 306.6921797724685, \"y\": 277.05255454452566 } },\n    { \"id\": \"Child1\", \"style\": { \"x\": 361.1652068827243, \"y\": 387.9769951347244 } },\n    { \"id\": \"Child2\", \"style\": { \"x\": 415.98942162128606, \"y\": 432.37341762016945 } },\n    { \"id\": \"Brujon\", \"style\": { \"x\": 330.44198511493056, \"y\": 394.6025799878689 } },\n    { \"id\": \"Mme.Hucheloup\", \"style\": { \"x\": 394.43875881505835, \"y\": 450.4056149101193 } }\n  ],\n  \"edges\": [\n    { \"id\": \"0\", \"source\": \"Napoleon\", \"target\": \"Myriel\", \"data\": { \"value\": 1 } },\n    { \"id\": \"1\", \"source\": \"Mlle.Baptistine\", \"target\": \"Myriel\", \"data\": { \"value\": 8 } },\n    { \"id\": \"2\", \"source\": \"Mme.Magloire\", \"target\": \"Myriel\", \"data\": { \"value\": 10 } },\n    { \"id\": \"3\", \"source\": \"Mme.Magloire\", \"target\": \"Mlle.Baptistine\", \"data\": { \"value\": 6 } },\n    { \"id\": \"4\", \"source\": \"CountessdeLo\", \"target\": \"Myriel\", \"data\": { \"value\": 1 } },\n    { \"id\": \"5\", \"source\": \"Geborand\", \"target\": \"Myriel\", \"data\": { \"value\": 1 } },\n    { \"id\": \"6\", \"source\": \"Champtercier\", \"target\": \"Myriel\", \"data\": { \"value\": 1 } },\n    { \"id\": \"7\", \"source\": \"Cravatte\", \"target\": \"Myriel\", \"data\": { \"value\": 1 } },\n    { \"id\": \"8\", \"source\": \"Count\", \"target\": \"Myriel\", \"data\": { \"value\": 2 } },\n    { \"id\": \"9\", \"source\": \"OldMan\", \"target\": \"Myriel\", \"data\": { \"value\": 1 } },\n    { \"id\": \"10\", \"source\": \"Valjean\", \"target\": \"Labarre\", \"data\": { \"value\": 1 } },\n    { \"id\": \"11\", \"source\": \"Valjean\", \"target\": \"Mme.Magloire\", \"data\": { \"value\": 3 } },\n    { \"id\": \"12\", \"source\": \"Valjean\", \"target\": \"Mlle.Baptistine\", \"data\": { \"value\": 3 } },\n    { \"id\": \"13\", \"source\": \"Valjean\", \"target\": \"Myriel\", \"data\": { \"value\": 5 } },\n    { \"id\": \"14\", \"source\": \"Marguerite\", \"target\": \"Valjean\", \"data\": { \"value\": 1 } },\n    { \"id\": \"15\", \"source\": \"Mme.deR\", \"target\": \"Valjean\", \"data\": { \"value\": 1 } },\n    { \"id\": \"16\", \"source\": \"Isabeau\", \"target\": \"Valjean\", \"data\": { \"value\": 1 } },\n    { \"id\": \"17\", \"source\": \"Gervais\", \"target\": \"Valjean\", \"data\": { \"value\": 1 } },\n    { \"id\": \"18\", \"source\": \"Listolier\", \"target\": \"Tholomyes\", \"data\": { \"value\": 4 } },\n    { \"id\": \"19\", \"source\": \"Fameuil\", \"target\": \"Tholomyes\", \"data\": { \"value\": 4 } },\n    { \"id\": \"20\", \"source\": \"Fameuil\", \"target\": \"Listolier\", \"data\": { \"value\": 4 } },\n    { \"id\": \"21\", \"source\": \"Blacheville\", \"target\": \"Tholomyes\", \"data\": { \"value\": 4 } },\n    { \"id\": \"22\", \"source\": \"Blacheville\", \"target\": \"Listolier\", \"data\": { \"value\": 4 } },\n    { \"id\": \"23\", \"source\": \"Blacheville\", \"target\": \"Fameuil\", \"data\": { \"value\": 4 } },\n    { \"id\": \"24\", \"source\": \"Favourite\", \"target\": \"Tholomyes\", \"data\": { \"value\": 3 } },\n    { \"id\": \"25\", \"source\": \"Favourite\", \"target\": \"Listolier\", \"data\": { \"value\": 3 } },\n    { \"id\": \"26\", \"source\": \"Favourite\", \"target\": \"Fameuil\", \"data\": { \"value\": 3 } },\n    { \"id\": \"27\", \"source\": \"Favourite\", \"target\": \"Blacheville\", \"data\": { \"value\": 4 } },\n    { \"id\": \"28\", \"source\": \"Dahlia\", \"target\": \"Tholomyes\", \"data\": { \"value\": 3 } },\n    { \"id\": \"29\", \"source\": \"Dahlia\", \"target\": \"Listolier\", \"data\": { \"value\": 3 } },\n    { \"id\": \"30\", \"source\": \"Dahlia\", \"target\": \"Fameuil\", \"data\": { \"value\": 3 } },\n    { \"id\": \"31\", \"source\": \"Dahlia\", \"target\": \"Blacheville\", \"data\": { \"value\": 3 } },\n    { \"id\": \"32\", \"source\": \"Dahlia\", \"target\": \"Favourite\", \"data\": { \"value\": 5 } },\n    { \"id\": \"33\", \"source\": \"Zephine\", \"target\": \"Tholomyes\", \"data\": { \"value\": 3 } },\n    { \"id\": \"34\", \"source\": \"Zephine\", \"target\": \"Listolier\", \"data\": { \"value\": 3 } },\n    { \"id\": \"35\", \"source\": \"Zephine\", \"target\": \"Fameuil\", \"data\": { \"value\": 3 } },\n    { \"id\": \"36\", \"source\": \"Zephine\", \"target\": \"Blacheville\", \"data\": { \"value\": 3 } },\n    { \"id\": \"37\", \"source\": \"Zephine\", \"target\": \"Favourite\", \"data\": { \"value\": 4 } },\n    { \"id\": \"38\", \"source\": \"Zephine\", \"target\": \"Dahlia\", \"data\": { \"value\": 4 } },\n    { \"id\": \"39\", \"source\": \"Fantine\", \"target\": \"Tholomyes\", \"data\": { \"value\": 3 } },\n    { \"id\": \"40\", \"source\": \"Fantine\", \"target\": \"Listolier\", \"data\": { \"value\": 3 } },\n    { \"id\": \"41\", \"source\": \"Fantine\", \"target\": \"Fameuil\", \"data\": { \"value\": 3 } },\n    { \"id\": \"42\", \"source\": \"Fantine\", \"target\": \"Blacheville\", \"data\": { \"value\": 3 } },\n    { \"id\": \"43\", \"source\": \"Fantine\", \"target\": \"Favourite\", \"data\": { \"value\": 4 } },\n    { \"id\": \"44\", \"source\": \"Fantine\", \"target\": \"Dahlia\", \"data\": { \"value\": 4 } },\n    { \"id\": \"45\", \"source\": \"Fantine\", \"target\": \"Zephine\", \"data\": { \"value\": 4 } },\n    { \"id\": \"46\", \"source\": \"Fantine\", \"target\": \"Marguerite\", \"data\": { \"value\": 2 } },\n    { \"id\": \"47\", \"source\": \"Fantine\", \"target\": \"Valjean\", \"data\": { \"value\": 9 } },\n    { \"id\": \"48\", \"source\": \"Mme.Thenardier\", \"target\": \"Fantine\", \"data\": { \"value\": 2 } },\n    { \"id\": \"49\", \"source\": \"Mme.Thenardier\", \"target\": \"Valjean\", \"data\": { \"value\": 7 } },\n    { \"id\": \"50\", \"source\": \"Thenardier\", \"target\": \"Mme.Thenardier\", \"data\": { \"value\": 13 } },\n    { \"id\": \"51\", \"source\": \"Thenardier\", \"target\": \"Fantine\", \"data\": { \"value\": 1 } },\n    { \"id\": \"52\", \"source\": \"Thenardier\", \"target\": \"Valjean\", \"data\": { \"value\": 12 } },\n    { \"id\": \"53\", \"source\": \"Cosette\", \"target\": \"Mme.Thenardier\", \"data\": { \"value\": 4 } },\n    { \"id\": \"54\", \"source\": \"Cosette\", \"target\": \"Valjean\", \"data\": { \"value\": 31 } },\n    { \"id\": \"55\", \"source\": \"Cosette\", \"target\": \"Tholomyes\", \"data\": { \"value\": 1 } },\n    { \"id\": \"56\", \"source\": \"Cosette\", \"target\": \"Thenardier\", \"data\": { \"value\": 1 } },\n    { \"id\": \"57\", \"source\": \"Javert\", \"target\": \"Valjean\", \"data\": { \"value\": 17 } },\n    { \"id\": \"58\", \"source\": \"Javert\", \"target\": \"Fantine\", \"data\": { \"value\": 5 } },\n    { \"id\": \"59\", \"source\": \"Javert\", \"target\": \"Thenardier\", \"data\": { \"value\": 5 } },\n    { \"id\": \"60\", \"source\": \"Javert\", \"target\": \"Mme.Thenardier\", \"data\": { \"value\": 1 } },\n    { \"id\": \"61\", \"source\": \"Javert\", \"target\": \"Cosette\", \"data\": { \"value\": 1 } },\n    { \"id\": \"62\", \"source\": \"Fauchelevent\", \"target\": \"Valjean\", \"data\": { \"value\": 8 } },\n    { \"id\": \"63\", \"source\": \"Fauchelevent\", \"target\": \"Javert\", \"data\": { \"value\": 1 } },\n    { \"id\": \"64\", \"source\": \"Bamatabois\", \"target\": \"Fantine\", \"data\": { \"value\": 1 } },\n    { \"id\": \"65\", \"source\": \"Bamatabois\", \"target\": \"Javert\", \"data\": { \"value\": 1 } },\n    { \"id\": \"66\", \"source\": \"Bamatabois\", \"target\": \"Valjean\", \"data\": { \"value\": 2 } },\n    { \"id\": \"67\", \"source\": \"Perpetue\", \"target\": \"Fantine\", \"data\": { \"value\": 1 } },\n    { \"id\": \"68\", \"source\": \"Simplice\", \"target\": \"Perpetue\", \"data\": { \"value\": 2 } },\n    { \"id\": \"69\", \"source\": \"Simplice\", \"target\": \"Valjean\", \"data\": { \"value\": 3 } },\n    { \"id\": \"70\", \"source\": \"Simplice\", \"target\": \"Fantine\", \"data\": { \"value\": 2 } },\n    { \"id\": \"71\", \"source\": \"Simplice\", \"target\": \"Javert\", \"data\": { \"value\": 1 } },\n    { \"id\": \"72\", \"source\": \"Scaufflaire\", \"target\": \"Valjean\", \"data\": { \"value\": 1 } },\n    { \"id\": \"73\", \"source\": \"Woman1\", \"target\": \"Valjean\", \"data\": { \"value\": 2 } },\n    { \"id\": \"74\", \"source\": \"Woman1\", \"target\": \"Javert\", \"data\": { \"value\": 1 } },\n    { \"id\": \"75\", \"source\": \"Judge\", \"target\": \"Valjean\", \"data\": { \"value\": 3 } },\n    { \"id\": \"76\", \"source\": \"Judge\", \"target\": \"Bamatabois\", \"data\": { \"value\": 2 } },\n    { \"id\": \"77\", \"source\": \"Champmathieu\", \"target\": \"Valjean\", \"data\": { \"value\": 3 } },\n    { \"id\": \"78\", \"source\": \"Champmathieu\", \"target\": \"Judge\", \"data\": { \"value\": 3 } },\n    { \"id\": \"79\", \"source\": \"Champmathieu\", \"target\": \"Bamatabois\", \"data\": { \"value\": 2 } },\n    { \"id\": \"80\", \"source\": \"Brevet\", \"target\": \"Judge\", \"data\": { \"value\": 2 } },\n    { \"id\": \"81\", \"source\": \"Brevet\", \"target\": \"Champmathieu\", \"data\": { \"value\": 2 } },\n    { \"id\": \"82\", \"source\": \"Brevet\", \"target\": \"Valjean\", \"data\": { \"value\": 2 } },\n    { \"id\": \"83\", \"source\": \"Brevet\", \"target\": \"Bamatabois\", \"data\": { \"value\": 1 } },\n    { \"id\": \"84\", \"source\": \"Chenildieu\", \"target\": \"Judge\", \"data\": { \"value\": 2 } },\n    { \"id\": \"85\", \"source\": \"Chenildieu\", \"target\": \"Champmathieu\", \"data\": { \"value\": 2 } },\n    { \"id\": \"86\", \"source\": \"Chenildieu\", \"target\": \"Brevet\", \"data\": { \"value\": 2 } },\n    { \"id\": \"87\", \"source\": \"Chenildieu\", \"target\": \"Valjean\", \"data\": { \"value\": 2 } },\n    { \"id\": \"88\", \"source\": \"Chenildieu\", \"target\": \"Bamatabois\", \"data\": { \"value\": 1 } },\n    { \"id\": \"89\", \"source\": \"Cochepaille\", \"target\": \"Judge\", \"data\": { \"value\": 2 } },\n    { \"id\": \"90\", \"source\": \"Cochepaille\", \"target\": \"Champmathieu\", \"data\": { \"value\": 2 } },\n    { \"id\": \"91\", \"source\": \"Cochepaille\", \"target\": \"Brevet\", \"data\": { \"value\": 2 } },\n    { \"id\": \"92\", \"source\": \"Cochepaille\", \"target\": \"Chenildieu\", \"data\": { \"value\": 2 } },\n    { \"id\": \"93\", \"source\": \"Cochepaille\", \"target\": \"Valjean\", \"data\": { \"value\": 2 } },\n    { \"id\": \"94\", \"source\": \"Cochepaille\", \"target\": \"Bamatabois\", \"data\": { \"value\": 1 } },\n    { \"id\": \"95\", \"source\": \"Pontmercy\", \"target\": \"Thenardier\", \"data\": { \"value\": 1 } },\n    { \"id\": \"96\", \"source\": \"Boulatruelle\", \"target\": \"Thenardier\", \"data\": { \"value\": 1 } },\n    { \"id\": \"97\", \"source\": \"Eponine\", \"target\": \"Mme.Thenardier\", \"data\": { \"value\": 2 } },\n    { \"id\": \"98\", \"source\": \"Eponine\", \"target\": \"Thenardier\", \"data\": { \"value\": 3 } },\n    { \"id\": \"99\", \"source\": \"Anzelma\", \"target\": \"Eponine\", \"data\": { \"value\": 2 } },\n    { \"id\": \"100\", \"source\": \"Anzelma\", \"target\": \"Thenardier\", \"data\": { \"value\": 2 } },\n    { \"id\": \"101\", \"source\": \"Anzelma\", \"target\": \"Mme.Thenardier\", \"data\": { \"value\": 1 } },\n    { \"id\": \"102\", \"source\": \"Woman2\", \"target\": \"Valjean\", \"data\": { \"value\": 3 } },\n    { \"id\": \"103\", \"source\": \"Woman2\", \"target\": \"Cosette\", \"data\": { \"value\": 1 } },\n    { \"id\": \"104\", \"source\": \"Woman2\", \"target\": \"Javert\", \"data\": { \"value\": 1 } },\n    { \"id\": \"105\", \"source\": \"MotherInnocent\", \"target\": \"Fauchelevent\", \"data\": { \"value\": 3 } },\n    { \"id\": \"106\", \"source\": \"MotherInnocent\", \"target\": \"Valjean\", \"data\": { \"value\": 1 } },\n    { \"id\": \"107\", \"source\": \"Gribier\", \"target\": \"Fauchelevent\", \"data\": { \"value\": 2 } },\n    { \"id\": \"108\", \"source\": \"Mme.Burgon\", \"target\": \"Jondrette\", \"data\": { \"value\": 1 } },\n    { \"id\": \"109\", \"source\": \"Gavroche\", \"target\": \"Mme.Burgon\", \"data\": { \"value\": 2 } },\n    { \"id\": \"110\", \"source\": \"Gavroche\", \"target\": \"Thenardier\", \"data\": { \"value\": 1 } },\n    { \"id\": \"111\", \"source\": \"Gavroche\", \"target\": \"Javert\", \"data\": { \"value\": 1 } },\n    { \"id\": \"112\", \"source\": \"Gavroche\", \"target\": \"Valjean\", \"data\": { \"value\": 1 } },\n    { \"id\": \"113\", \"source\": \"Gillenormand\", \"target\": \"Cosette\", \"data\": { \"value\": 3 } },\n    { \"id\": \"114\", \"source\": \"Gillenormand\", \"target\": \"Valjean\", \"data\": { \"value\": 2 } },\n    { \"id\": \"115\", \"source\": \"Magnon\", \"target\": \"Gillenormand\", \"data\": { \"value\": 1 } },\n    { \"id\": \"116\", \"source\": \"Magnon\", \"target\": \"Mme.Thenardier\", \"data\": { \"value\": 1 } },\n    { \"id\": \"117\", \"source\": \"Mlle.Gillenormand\", \"target\": \"Gillenormand\", \"data\": { \"value\": 9 } },\n    { \"id\": \"118\", \"source\": \"Mlle.Gillenormand\", \"target\": \"Cosette\", \"data\": { \"value\": 2 } },\n    { \"id\": \"119\", \"source\": \"Mlle.Gillenormand\", \"target\": \"Valjean\", \"data\": { \"value\": 2 } },\n    { \"id\": \"120\", \"source\": \"Mme.Pontmercy\", \"target\": \"Mlle.Gillenormand\", \"data\": { \"value\": 1 } },\n    { \"id\": \"121\", \"source\": \"Mme.Pontmercy\", \"target\": \"Pontmercy\", \"data\": { \"value\": 1 } },\n    { \"id\": \"122\", \"source\": \"Mlle.Vaubois\", \"target\": \"Mlle.Gillenormand\", \"data\": { \"value\": 1 } },\n    { \"id\": \"123\", \"source\": \"Lt.Gillenormand\", \"target\": \"Mlle.Gillenormand\", \"data\": { \"value\": 2 } },\n    { \"id\": \"124\", \"source\": \"Lt.Gillenormand\", \"target\": \"Gillenormand\", \"data\": { \"value\": 1 } },\n    { \"id\": \"125\", \"source\": \"Lt.Gillenormand\", \"target\": \"Cosette\", \"data\": { \"value\": 1 } },\n    { \"id\": \"126\", \"source\": \"Marius\", \"target\": \"Mlle.Gillenormand\", \"data\": { \"value\": 6 } },\n    { \"id\": \"127\", \"source\": \"Marius\", \"target\": \"Gillenormand\", \"data\": { \"value\": 12 } },\n    { \"id\": \"128\", \"source\": \"Marius\", \"target\": \"Pontmercy\", \"data\": { \"value\": 1 } },\n    { \"id\": \"129\", \"source\": \"Marius\", \"target\": \"Lt.Gillenormand\", \"data\": { \"value\": 1 } },\n    { \"id\": \"130\", \"source\": \"Marius\", \"target\": \"Cosette\", \"data\": { \"value\": 21 } },\n    { \"id\": \"131\", \"source\": \"Marius\", \"target\": \"Valjean\", \"data\": { \"value\": 19 } },\n    { \"id\": \"132\", \"source\": \"Marius\", \"target\": \"Tholomyes\", \"data\": { \"value\": 1 } },\n    { \"id\": \"133\", \"source\": \"Marius\", \"target\": \"Thenardier\", \"data\": { \"value\": 2 } },\n    { \"id\": \"134\", \"source\": \"Marius\", \"target\": \"Eponine\", \"data\": { \"value\": 5 } },\n    { \"id\": \"135\", \"source\": \"Marius\", \"target\": \"Gavroche\", \"data\": { \"value\": 4 } },\n    { \"id\": \"136\", \"source\": \"BaronessT\", \"target\": \"Gillenormand\", \"data\": { \"value\": 1 } },\n    { \"id\": \"137\", \"source\": \"BaronessT\", \"target\": \"Marius\", \"data\": { \"value\": 1 } },\n    { \"id\": \"138\", \"source\": \"Mabeuf\", \"target\": \"Marius\", \"data\": { \"value\": 1 } },\n    { \"id\": \"139\", \"source\": \"Mabeuf\", \"target\": \"Eponine\", \"data\": { \"value\": 1 } },\n    { \"id\": \"140\", \"source\": \"Mabeuf\", \"target\": \"Gavroche\", \"data\": { \"value\": 1 } },\n    { \"id\": \"141\", \"source\": \"Enjolras\", \"target\": \"Marius\", \"data\": { \"value\": 7 } },\n    { \"id\": \"142\", \"source\": \"Enjolras\", \"target\": \"Gavroche\", \"data\": { \"value\": 7 } },\n    { \"id\": \"143\", \"source\": \"Enjolras\", \"target\": \"Javert\", \"data\": { \"value\": 6 } },\n    { \"id\": \"144\", \"source\": \"Enjolras\", \"target\": \"Mabeuf\", \"data\": { \"value\": 1 } },\n    { \"id\": \"145\", \"source\": \"Enjolras\", \"target\": \"Valjean\", \"data\": { \"value\": 4 } },\n    { \"id\": \"146\", \"source\": \"Combeferre\", \"target\": \"Enjolras\", \"data\": { \"value\": 15 } },\n    { \"id\": \"147\", \"source\": \"Combeferre\", \"target\": \"Marius\", \"data\": { \"value\": 5 } },\n    { \"id\": \"148\", \"source\": \"Combeferre\", \"target\": \"Gavroche\", \"data\": { \"value\": 6 } },\n    { \"id\": \"149\", \"source\": \"Combeferre\", \"target\": \"Mabeuf\", \"data\": { \"value\": 2 } },\n    { \"id\": \"150\", \"source\": \"Prouvaire\", \"target\": \"Gavroche\", \"data\": { \"value\": 1 } },\n    { \"id\": \"151\", \"source\": \"Prouvaire\", \"target\": \"Enjolras\", \"data\": { \"value\": 4 } },\n    { \"id\": \"152\", \"source\": \"Prouvaire\", \"target\": \"Combeferre\", \"data\": { \"value\": 2 } },\n    { \"id\": \"153\", \"source\": \"Feuilly\", \"target\": \"Gavroche\", \"data\": { \"value\": 2 } },\n    { \"id\": \"154\", \"source\": \"Feuilly\", \"target\": \"Enjolras\", \"data\": { \"value\": 6 } },\n    { \"id\": \"155\", \"source\": \"Feuilly\", \"target\": \"Prouvaire\", \"data\": { \"value\": 2 } },\n    { \"id\": \"156\", \"source\": \"Feuilly\", \"target\": \"Combeferre\", \"data\": { \"value\": 5 } },\n    { \"id\": \"157\", \"source\": \"Feuilly\", \"target\": \"Mabeuf\", \"data\": { \"value\": 1 } },\n    { \"id\": \"158\", \"source\": \"Feuilly\", \"target\": \"Marius\", \"data\": { \"value\": 1 } },\n    { \"id\": \"159\", \"source\": \"Courfeyrac\", \"target\": \"Marius\", \"data\": { \"value\": 9 } },\n    { \"id\": \"160\", \"source\": \"Courfeyrac\", \"target\": \"Enjolras\", \"data\": { \"value\": 17 } },\n    { \"id\": \"161\", \"source\": \"Courfeyrac\", \"target\": \"Combeferre\", \"data\": { \"value\": 13 } },\n    { \"id\": \"162\", \"source\": \"Courfeyrac\", \"target\": \"Gavroche\", \"data\": { \"value\": 7 } },\n    { \"id\": \"163\", \"source\": \"Courfeyrac\", \"target\": \"Mabeuf\", \"data\": { \"value\": 2 } },\n    { \"id\": \"164\", \"source\": \"Courfeyrac\", \"target\": \"Eponine\", \"data\": { \"value\": 1 } },\n    { \"id\": \"165\", \"source\": \"Courfeyrac\", \"target\": \"Feuilly\", \"data\": { \"value\": 6 } },\n    { \"id\": \"166\", \"source\": \"Courfeyrac\", \"target\": \"Prouvaire\", \"data\": { \"value\": 3 } },\n    { \"id\": \"167\", \"source\": \"Bahorel\", \"target\": \"Combeferre\", \"data\": { \"value\": 5 } },\n    { \"id\": \"168\", \"source\": \"Bahorel\", \"target\": \"Gavroche\", \"data\": { \"value\": 5 } },\n    { \"id\": \"169\", \"source\": \"Bahorel\", \"target\": \"Courfeyrac\", \"data\": { \"value\": 6 } },\n    { \"id\": \"170\", \"source\": \"Bahorel\", \"target\": \"Mabeuf\", \"data\": { \"value\": 2 } },\n    { \"id\": \"171\", \"source\": \"Bahorel\", \"target\": \"Enjolras\", \"data\": { \"value\": 4 } },\n    { \"id\": \"172\", \"source\": \"Bahorel\", \"target\": \"Feuilly\", \"data\": { \"value\": 3 } },\n    { \"id\": \"173\", \"source\": \"Bahorel\", \"target\": \"Prouvaire\", \"data\": { \"value\": 2 } },\n    { \"id\": \"174\", \"source\": \"Bahorel\", \"target\": \"Marius\", \"data\": { \"value\": 1 } },\n    { \"id\": \"175\", \"source\": \"Bossuet\", \"target\": \"Marius\", \"data\": { \"value\": 5 } },\n    { \"id\": \"176\", \"source\": \"Bossuet\", \"target\": \"Courfeyrac\", \"data\": { \"value\": 12 } },\n    { \"id\": \"177\", \"source\": \"Bossuet\", \"target\": \"Gavroche\", \"data\": { \"value\": 5 } },\n    { \"id\": \"178\", \"source\": \"Bossuet\", \"target\": \"Bahorel\", \"data\": { \"value\": 4 } },\n    { \"id\": \"179\", \"source\": \"Bossuet\", \"target\": \"Enjolras\", \"data\": { \"value\": 10 } },\n    { \"id\": \"180\", \"source\": \"Bossuet\", \"target\": \"Feuilly\", \"data\": { \"value\": 6 } },\n    { \"id\": \"181\", \"source\": \"Bossuet\", \"target\": \"Prouvaire\", \"data\": { \"value\": 2 } },\n    { \"id\": \"182\", \"source\": \"Bossuet\", \"target\": \"Combeferre\", \"data\": { \"value\": 9 } },\n    { \"id\": \"183\", \"source\": \"Bossuet\", \"target\": \"Mabeuf\", \"data\": { \"value\": 1 } },\n    { \"id\": \"184\", \"source\": \"Bossuet\", \"target\": \"Valjean\", \"data\": { \"value\": 1 } },\n    { \"id\": \"185\", \"source\": \"Joly\", \"target\": \"Bahorel\", \"data\": { \"value\": 5 } },\n    { \"id\": \"186\", \"source\": \"Joly\", \"target\": \"Bossuet\", \"data\": { \"value\": 7 } },\n    { \"id\": \"187\", \"source\": \"Joly\", \"target\": \"Gavroche\", \"data\": { \"value\": 3 } },\n    { \"id\": \"188\", \"source\": \"Joly\", \"target\": \"Courfeyrac\", \"data\": { \"value\": 5 } },\n    { \"id\": \"189\", \"source\": \"Joly\", \"target\": \"Enjolras\", \"data\": { \"value\": 5 } },\n    { \"id\": \"190\", \"source\": \"Joly\", \"target\": \"Feuilly\", \"data\": { \"value\": 5 } },\n    { \"id\": \"191\", \"source\": \"Joly\", \"target\": \"Prouvaire\", \"data\": { \"value\": 2 } },\n    { \"id\": \"192\", \"source\": \"Joly\", \"target\": \"Combeferre\", \"data\": { \"value\": 5 } },\n    { \"id\": \"193\", \"source\": \"Joly\", \"target\": \"Mabeuf\", \"data\": { \"value\": 1 } },\n    { \"id\": \"194\", \"source\": \"Joly\", \"target\": \"Marius\", \"data\": { \"value\": 2 } },\n    { \"id\": \"195\", \"source\": \"Grantaire\", \"target\": \"Bossuet\", \"data\": { \"value\": 3 } },\n    { \"id\": \"196\", \"source\": \"Grantaire\", \"target\": \"Enjolras\", \"data\": { \"value\": 3 } },\n    { \"id\": \"197\", \"source\": \"Grantaire\", \"target\": \"Combeferre\", \"data\": { \"value\": 1 } },\n    { \"id\": \"198\", \"source\": \"Grantaire\", \"target\": \"Courfeyrac\", \"data\": { \"value\": 2 } },\n    { \"id\": \"199\", \"source\": \"Grantaire\", \"target\": \"Joly\", \"data\": { \"value\": 2 } },\n    { \"id\": \"200\", \"source\": \"Grantaire\", \"target\": \"Gavroche\", \"data\": { \"value\": 1 } },\n    { \"id\": \"201\", \"source\": \"Grantaire\", \"target\": \"Bahorel\", \"data\": { \"value\": 1 } },\n    { \"id\": \"202\", \"source\": \"Grantaire\", \"target\": \"Feuilly\", \"data\": { \"value\": 1 } },\n    { \"id\": \"203\", \"source\": \"Grantaire\", \"target\": \"Prouvaire\", \"data\": { \"value\": 1 } },\n    { \"id\": \"204\", \"source\": \"MotherPlutarch\", \"target\": \"Mabeuf\", \"data\": { \"value\": 3 } },\n    { \"id\": \"205\", \"source\": \"Gueulemer\", \"target\": \"Thenardier\", \"data\": { \"value\": 5 } },\n    { \"id\": \"206\", \"source\": \"Gueulemer\", \"target\": \"Valjean\", \"data\": { \"value\": 1 } },\n    { \"id\": \"207\", \"source\": \"Gueulemer\", \"target\": \"Mme.Thenardier\", \"data\": { \"value\": 1 } },\n    { \"id\": \"208\", \"source\": \"Gueulemer\", \"target\": \"Javert\", \"data\": { \"value\": 1 } },\n    { \"id\": \"209\", \"source\": \"Gueulemer\", \"target\": \"Gavroche\", \"data\": { \"value\": 1 } },\n    { \"id\": \"210\", \"source\": \"Gueulemer\", \"target\": \"Eponine\", \"data\": { \"value\": 1 } },\n    { \"id\": \"211\", \"source\": \"Babet\", \"target\": \"Thenardier\", \"data\": { \"value\": 6 } },\n    { \"id\": \"212\", \"source\": \"Babet\", \"target\": \"Gueulemer\", \"data\": { \"value\": 6 } },\n    { \"id\": \"213\", \"source\": \"Babet\", \"target\": \"Valjean\", \"data\": { \"value\": 1 } },\n    { \"id\": \"214\", \"source\": \"Babet\", \"target\": \"Mme.Thenardier\", \"data\": { \"value\": 1 } },\n    { \"id\": \"215\", \"source\": \"Babet\", \"target\": \"Javert\", \"data\": { \"value\": 2 } },\n    { \"id\": \"216\", \"source\": \"Babet\", \"target\": \"Gavroche\", \"data\": { \"value\": 1 } },\n    { \"id\": \"217\", \"source\": \"Babet\", \"target\": \"Eponine\", \"data\": { \"value\": 1 } },\n    { \"id\": \"218\", \"source\": \"Claquesous\", \"target\": \"Thenardier\", \"data\": { \"value\": 4 } },\n    { \"id\": \"219\", \"source\": \"Claquesous\", \"target\": \"Babet\", \"data\": { \"value\": 4 } },\n    { \"id\": \"220\", \"source\": \"Claquesous\", \"target\": \"Gueulemer\", \"data\": { \"value\": 4 } },\n    { \"id\": \"221\", \"source\": \"Claquesous\", \"target\": \"Valjean\", \"data\": { \"value\": 1 } },\n    { \"id\": \"222\", \"source\": \"Claquesous\", \"target\": \"Mme.Thenardier\", \"data\": { \"value\": 1 } },\n    { \"id\": \"223\", \"source\": \"Claquesous\", \"target\": \"Javert\", \"data\": { \"value\": 1 } },\n    { \"id\": \"224\", \"source\": \"Claquesous\", \"target\": \"Eponine\", \"data\": { \"value\": 1 } },\n    { \"id\": \"225\", \"source\": \"Claquesous\", \"target\": \"Enjolras\", \"data\": { \"value\": 1 } },\n    { \"id\": \"226\", \"source\": \"Montparnasse\", \"target\": \"Javert\", \"data\": { \"value\": 1 } },\n    { \"id\": \"227\", \"source\": \"Montparnasse\", \"target\": \"Babet\", \"data\": { \"value\": 2 } },\n    { \"id\": \"228\", \"source\": \"Montparnasse\", \"target\": \"Gueulemer\", \"data\": { \"value\": 2 } },\n    { \"id\": \"229\", \"source\": \"Montparnasse\", \"target\": \"Claquesous\", \"data\": { \"value\": 2 } },\n    { \"id\": \"230\", \"source\": \"Montparnasse\", \"target\": \"Valjean\", \"data\": { \"value\": 1 } },\n    { \"id\": \"231\", \"source\": \"Montparnasse\", \"target\": \"Gavroche\", \"data\": { \"value\": 1 } },\n    { \"id\": \"232\", \"source\": \"Montparnasse\", \"target\": \"Eponine\", \"data\": { \"value\": 1 } },\n    { \"id\": \"233\", \"source\": \"Montparnasse\", \"target\": \"Thenardier\", \"data\": { \"value\": 1 } },\n    { \"id\": \"234\", \"source\": \"Toussaint\", \"target\": \"Cosette\", \"data\": { \"value\": 2 } },\n    { \"id\": \"235\", \"source\": \"Toussaint\", \"target\": \"Javert\", \"data\": { \"value\": 1 } },\n    { \"id\": \"236\", \"source\": \"Toussaint\", \"target\": \"Valjean\", \"data\": { \"value\": 1 } },\n    { \"id\": \"237\", \"source\": \"Child1\", \"target\": \"Gavroche\", \"data\": { \"value\": 2 } },\n    { \"id\": \"238\", \"source\": \"Child2\", \"target\": \"Gavroche\", \"data\": { \"value\": 2 } },\n    { \"id\": \"239\", \"source\": \"Child2\", \"target\": \"Child1\", \"data\": { \"value\": 3 } },\n    { \"id\": \"240\", \"source\": \"Brujon\", \"target\": \"Babet\", \"data\": { \"value\": 3 } },\n    { \"id\": \"241\", \"source\": \"Brujon\", \"target\": \"Gueulemer\", \"data\": { \"value\": 3 } },\n    { \"id\": \"242\", \"source\": \"Brujon\", \"target\": \"Thenardier\", \"data\": { \"value\": 3 } },\n    { \"id\": \"243\", \"source\": \"Brujon\", \"target\": \"Gavroche\", \"data\": { \"value\": 1 } },\n    { \"id\": \"244\", \"source\": \"Brujon\", \"target\": \"Eponine\", \"data\": { \"value\": 1 } },\n    { \"id\": \"245\", \"source\": \"Brujon\", \"target\": \"Claquesous\", \"data\": { \"value\": 1 } },\n    { \"id\": \"246\", \"source\": \"Brujon\", \"target\": \"Montparnasse\", \"data\": { \"value\": 1 } },\n    { \"id\": \"247\", \"source\": \"Mme.Hucheloup\", \"target\": \"Bossuet\", \"data\": { \"value\": 1 } },\n    { \"id\": \"248\", \"source\": \"Mme.Hucheloup\", \"target\": \"Joly\", \"data\": { \"value\": 1 } },\n    { \"id\": \"249\", \"source\": \"Mme.Hucheloup\", \"target\": \"Grantaire\", \"data\": { \"value\": 1 } },\n    { \"id\": \"250\", \"source\": \"Mme.Hucheloup\", \"target\": \"Bahorel\", \"data\": { \"value\": 1 } },\n    { \"id\": \"251\", \"source\": \"Mme.Hucheloup\", \"target\": \"Courfeyrac\", \"data\": { \"value\": 1 } },\n    { \"id\": \"252\", \"source\": \"Mme.Hucheloup\", \"target\": \"Gavroche\", \"data\": { \"value\": 1 } },\n    { \"id\": \"253\", \"source\": \"Mme.Hucheloup\", \"target\": \"Enjolras\", \"data\": { \"value\": 1 } }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/dataset/soccer.json",
    "content": "{\n  \"nodes\": [\n    { \"id\": \"Argentina\", \"data\": { \"name\": \"Argentina\" } },\n    { \"id\": \"Australia\", \"data\": { \"name\": \"Australia\" } },\n    { \"id\": \"Belgium\", \"data\": { \"name\": \"Belgium\" } },\n    { \"id\": \"Brazil\", \"data\": { \"name\": \"Brazil\" } },\n    { \"id\": \"Colombia\", \"data\": { \"name\": \"Colombia\" } },\n    { \"id\": \"Costa Rica\", \"data\": { \"name\": \"Costa Rica\" } },\n    { \"id\": \"Croatia\", \"data\": { \"name\": \"Croatia\" } },\n    { \"id\": \"Denmark\", \"data\": { \"name\": \"Denmark\" } },\n    { \"id\": \"Egypt\", \"data\": { \"name\": \"Egypt\" } },\n    { \"id\": \"England\", \"data\": { \"name\": \"England\" } },\n    { \"id\": \"France\", \"data\": { \"name\": \"France\" } },\n    { \"id\": \"Germany\", \"data\": { \"name\": \"Germany\" } },\n    { \"id\": \"Iceland\", \"data\": { \"name\": \"Iceland\" } },\n    { \"id\": \"IR Iran\", \"data\": { \"name\": \"IR Iran\" } },\n    { \"id\": \"Japan\", \"data\": { \"name\": \"Japan\" } },\n    { \"id\": \"Korea Republic\", \"data\": { \"name\": \"Korea Republic\" } },\n    { \"id\": \"Mexico\", \"data\": { \"name\": \"Mexico\" } },\n    { \"id\": \"Morocco\", \"data\": { \"name\": \"Morocco\" } },\n    { \"id\": \"Nigeria\", \"data\": { \"name\": \"Nigeria\" } },\n    { \"id\": \"Panama\", \"data\": { \"name\": \"Panama\" } },\n    { \"id\": \"Peru\", \"data\": { \"name\": \"Peru\" } },\n    { \"id\": \"Poland\", \"data\": { \"name\": \"Poland\" } },\n    { \"id\": \"Portugal\", \"data\": { \"name\": \"Portugal\" } },\n    { \"id\": \"Russia\", \"data\": { \"name\": \"Russia\" } },\n    { \"id\": \"Saudi Arabia\", \"data\": { \"name\": \"Saudi Arabia\" } },\n    { \"id\": \"Senegal\", \"data\": { \"name\": \"Senegal\" } },\n    { \"id\": \"Serbia\", \"data\": { \"name\": \"Serbia\" } },\n    { \"id\": \"Spain\", \"data\": { \"name\": \"Spain\" } },\n    { \"id\": \"Sweden\", \"data\": { \"name\": \"Sweden\" } },\n    { \"id\": \"Switzerland\", \"data\": { \"name\": \"Switzerland\" } },\n    { \"id\": \"Tunisia\", \"data\": { \"name\": \"Tunisia\" } },\n    { \"id\": \"Uruguay\", \"data\": { \"name\": \"Uruguay\" } }\n  ],\n  \"edges\": [\n    {\n      \"id\": \"0\",\n      \"target\": \"Russia\",\n      \"source\": \"Saudi Arabia\",\n      \"data\": { \"target_score\": 5, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"1\",\n      \"target\": \"Uruguay\",\n      \"source\": \"Egypt\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"2\",\n      \"target\": \"Russia\",\n      \"source\": \"Egypt\",\n      \"data\": { \"target_score\": 3, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"3\",\n      \"target\": \"Uruguay\",\n      \"source\": \"Saudi Arabia\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"4\",\n      \"target\": \"Uruguay\",\n      \"source\": \"Russia\",\n      \"data\": { \"target_score\": 3, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"5\",\n      \"target\": \"Saudi Arabia\",\n      \"source\": \"Egypt\",\n      \"data\": { \"target_score\": 2, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"6\",\n      \"target\": \"IR Iran\",\n      \"source\": \"Morocco\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"7\",\n      \"target\": \"Portugal\",\n      \"source\": \"Spain\",\n      \"data\": { \"target_score\": 3, \"source_score\": 3, \"directed\": false }\n    },\n    {\n      \"id\": \"8\",\n      \"target\": \"Portugal\",\n      \"source\": \"Morocco\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"9\",\n      \"target\": \"Spain\",\n      \"source\": \"IR Iran\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"10\",\n      \"target\": \"IR Iran\",\n      \"source\": \"Portugal\",\n      \"data\": { \"target_score\": 1, \"source_score\": 1, \"directed\": false }\n    },\n    {\n      \"id\": \"11\",\n      \"target\": \"Spain\",\n      \"source\": \"Morocco\",\n      \"data\": { \"target_score\": 2, \"source_score\": 2, \"directed\": false }\n    },\n    {\n      \"id\": \"12\",\n      \"target\": \"France\",\n      \"source\": \"Australia\",\n      \"data\": { \"target_score\": 2, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"13\",\n      \"target\": \"Denmark\",\n      \"source\": \"Peru\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"14\",\n      \"target\": \"Denmark\",\n      \"source\": \"Australia\",\n      \"data\": { \"target_score\": 1, \"source_score\": 1, \"directed\": false }\n    },\n    {\n      \"id\": \"15\",\n      \"target\": \"France\",\n      \"source\": \"Peru\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"16\",\n      \"target\": \"Denmark\",\n      \"source\": \"France\",\n      \"data\": { \"target_score\": 0, \"source_score\": 0, \"directed\": false }\n    },\n    {\n      \"id\": \"17\",\n      \"target\": \"Peru\",\n      \"source\": \"Australia\",\n      \"data\": { \"target_score\": 2, \"source_score\": 0, \"directed\": true }\n    },\n    { \"id\": \"18\", \"target\": \"Argentina\", \"source\": \"Iceland\", \"data\": { \"target_score\": 1, \"source_score\": 1 } },\n    {\n      \"id\": \"19\",\n      \"target\": \"Croatia\",\n      \"source\": \"Nigeria\",\n      \"data\": { \"target_score\": 2, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"20\",\n      \"target\": \"Croatia\",\n      \"source\": \"Argentina\",\n      \"data\": { \"target_score\": 3, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"21\",\n      \"target\": \"Nigeria\",\n      \"source\": \"Iceland\",\n      \"data\": { \"target_score\": 2, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"22\",\n      \"target\": \"Argentina\",\n      \"source\": \"Nigeria\",\n      \"data\": { \"target_score\": 2, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"23\",\n      \"target\": \"Croatia\",\n      \"source\": \"Iceland\",\n      \"data\": { \"target_score\": 2, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"24\",\n      \"target\": \"Serbia\",\n      \"source\": \"Costa Rica\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"25\",\n      \"target\": \"Brazil\",\n      \"source\": \"Switzerland\",\n      \"data\": { \"target_score\": 1, \"source_score\": 1, \"directed\": false }\n    },\n    {\n      \"id\": \"26\",\n      \"target\": \"Brazil\",\n      \"source\": \"Costa Rica\",\n      \"data\": { \"target_score\": 2, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"27\",\n      \"target\": \"Switzerland\",\n      \"source\": \"Serbia\",\n      \"data\": { \"target_score\": 2, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"28\",\n      \"target\": \"Brazil\",\n      \"source\": \"Serbia\",\n      \"data\": { \"target_score\": 2, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"29\",\n      \"target\": \"Switzerland\",\n      \"source\": \"Costa Rica\",\n      \"data\": { \"target_score\": 2, \"source_score\": 2, \"directed\": false }\n    },\n    {\n      \"id\": \"30\",\n      \"target\": \"Mexico\",\n      \"source\": \"Germany\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"31\",\n      \"target\": \"Sweden\",\n      \"source\": \"Korea Republic\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"32\",\n      \"target\": \"Mexico\",\n      \"source\": \"Korea Republic\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"33\",\n      \"target\": \"Germany\",\n      \"source\": \"Sweden\",\n      \"data\": { \"target_score\": 2, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"34\",\n      \"target\": \"Korea Republic\",\n      \"source\": \"Germany\",\n      \"data\": { \"target_score\": 2, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"35\",\n      \"target\": \"Sweden\",\n      \"source\": \"Mexico\",\n      \"data\": { \"target_score\": 3, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"36\",\n      \"target\": \"Belgium\",\n      \"source\": \"Panama\",\n      \"data\": { \"target_score\": 3, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"37\",\n      \"target\": \"England\",\n      \"source\": \"Tunisia\",\n      \"data\": { \"target_score\": 2, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"38\",\n      \"target\": \"Belgium\",\n      \"source\": \"Tunisia\",\n      \"data\": { \"target_score\": 5, \"source_score\": 2, \"directed\": true }\n    },\n    {\n      \"id\": \"39\",\n      \"target\": \"England\",\n      \"source\": \"Panama\",\n      \"data\": { \"target_score\": 6, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"40\",\n      \"target\": \"Belgium\",\n      \"source\": \"England\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"41\",\n      \"target\": \"Tunisia\",\n      \"source\": \"Panama\",\n      \"data\": { \"target_score\": 2, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"42\",\n      \"target\": \"Japan\",\n      \"source\": \"Colombia\",\n      \"data\": { \"target_score\": 2, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"43\",\n      \"target\": \"Senegal\",\n      \"source\": \"Poland\",\n      \"data\": { \"target_score\": 2, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"44\",\n      \"target\": \"Japan\",\n      \"source\": \"Senegal\",\n      \"data\": { \"target_score\": 2, \"source_score\": 2, \"directed\": false }\n    },\n    {\n      \"id\": \"45\",\n      \"target\": \"Colombia\",\n      \"source\": \"Poland\",\n      \"data\": { \"target_score\": 3, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"46\",\n      \"target\": \"Poland\",\n      \"source\": \"Japan\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"47\",\n      \"target\": \"Colombia\",\n      \"source\": \"Senegal\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"48\",\n      \"target\": \"Uruguay\",\n      \"source\": \"Portugal\",\n      \"data\": { \"target_score\": 2, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"49\",\n      \"target\": \"France\",\n      \"source\": \"Argentina\",\n      \"data\": { \"target_score\": 4, \"source_score\": 3, \"directed\": true }\n    },\n    {\n      \"id\": \"50\",\n      \"target\": \"Russia\",\n      \"source\": \"Spain\",\n      \"data\": { \"target_score\": 5, \"source_score\": 4, \"directed\": true }\n    },\n    {\n      \"id\": \"51\",\n      \"target\": \"Croatia\",\n      \"source\": \"Denmark\",\n      \"data\": { \"target_score\": 4, \"source_score\": 3, \"directed\": true }\n    },\n    {\n      \"id\": \"52\",\n      \"target\": \"Brazil\",\n      \"source\": \"Mexico\",\n      \"data\": { \"target_score\": 2, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"53\",\n      \"target\": \"Belgium\",\n      \"source\": \"Japan\",\n      \"data\": { \"target_score\": 3, \"source_score\": 2, \"directed\": true }\n    },\n    {\n      \"id\": \"54\",\n      \"target\": \"Sweden\",\n      \"source\": \"Switzerland\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"55\",\n      \"target\": \"England\",\n      \"source\": \"Colombia\",\n      \"data\": { \"target_score\": 4, \"source_score\": 3, \"directed\": true }\n    },\n    {\n      \"id\": \"56\",\n      \"target\": \"France\",\n      \"source\": \"Uruguay\",\n      \"data\": { \"target_score\": 2, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"57\",\n      \"target\": \"Belgium\",\n      \"source\": \"Brazil\",\n      \"data\": { \"target_score\": 2, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"58\",\n      \"target\": \"Croatia\",\n      \"source\": \"Russia\",\n      \"data\": { \"target_score\": 6, \"source_score\": 5, \"directed\": true }\n    },\n    {\n      \"id\": \"59\",\n      \"target\": \"England\",\n      \"source\": \"Sweden\",\n      \"data\": { \"target_score\": 2, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"60\",\n      \"target\": \"France\",\n      \"source\": \"Belgium\",\n      \"data\": { \"target_score\": 1, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"61\",\n      \"target\": \"Croatia\",\n      \"source\": \"England\",\n      \"data\": { \"target_score\": 2, \"source_score\": 1, \"directed\": true }\n    },\n    {\n      \"id\": \"62\",\n      \"target\": \"Belgium\",\n      \"source\": \"England\",\n      \"data\": { \"target_score\": 2, \"source_score\": 0, \"directed\": true }\n    },\n    {\n      \"id\": \"63\",\n      \"target\": \"France\",\n      \"source\": \"Croatia\",\n      \"data\": { \"target_score\": 4, \"source_score\": 2, \"directed\": true }\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/__tests__/demos/animation-element-edge-cubic.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const animationElementEdgeCubic: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 100, y: 100 } },\n        { id: 'node-2', style: { x: 350, y: 150 } },\n      ],\n      edges: [\n        {\n          id: 'edge-1',\n          source: 'node-1',\n          target: 'node-2',\n          style: {\n            curveOffset: 30,\n          },\n        },\n      ],\n    },\n    edge: {\n      type: 'cubic',\n      style: {\n        lineWidth: 2,\n        stroke: '#1890FF',\n        labelText: 'cubic-edge',\n        labelFontSize: 12,\n        endArrow: true,\n      },\n    },\n  });\n\n  await graph.draw();\n\n  animationElementEdgeCubic.form = (panel) => [\n    panel.add(\n      {\n        Play: () => {\n          graph.updateNodeData([{ id: 'node-2', style: { y: 300 } }]);\n          graph.updateEdgeData([{ id: 'edge-1', style: { curveOffset: 60 } }]);\n          graph.draw();\n        },\n      },\n      'Play',\n    ),\n  ];\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/animation-element-edge-line.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const animationEdgeLine: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 100, y: 150 } },\n        { id: 'node-2', style: { x: 300, y: 200 } },\n      ],\n      edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n    },\n    cursor: 'grab',\n    edge: {\n      style: {\n        lineWidth: 2,\n        lineDash: [10, 10],\n        stroke: '#1890FF',\n        halo: true,\n        haloOpacity: 0.25,\n        haloLineWidth: 12,\n        label: true,\n        labelText: 'line-edge',\n        labelFontSize: 12,\n        labelFill: '#000',\n        labelPadding: 0,\n        startArrow: true,\n        startArrowType: 'circle',\n        endArrow: true,\n        endArrowFill: 'red',\n      },\n    },\n  });\n\n  await graph.render();\n\n  animationEdgeLine.form = (panel) => [\n    panel.add(\n      {\n        Play: () => {\n          graph.updateNodeData([{ id: 'node-2', style: { x: 450, y: 350 } }]);\n          graph.draw();\n        },\n      },\n      'Play',\n    ),\n  ];\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/animation-element-edge-quadratic.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const animationElementEdgeQuadratic: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 100, y: 150 } },\n        { id: 'node-2', style: { x: 300, y: 200 } },\n      ],\n      edges: [\n        {\n          id: 'edge-1',\n          source: 'node-1',\n          target: 'node-2',\n        },\n      ],\n    },\n    edge: {\n      type: 'quadratic',\n      style: {\n        lineWidth: 2,\n        stroke: '#1890FF',\n        labelText: 'quadratic-edge',\n        labelFontSize: 12,\n        endArrow: true,\n      },\n    },\n  });\n\n  await graph.draw();\n\n  animationElementEdgeQuadratic.form = (panel) => [\n    panel.add(\n      {\n        Play: () => {\n          graph.updateNodeData([{ id: 'node-2', style: { x: 450, y: 350 } }]);\n          graph.draw();\n        },\n      },\n      'Play',\n    ),\n  ];\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/animation-element-position.ts",
    "content": "import type { GraphOptions } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nexport const animationElementPosition: TestCase = async (context) => {\n  const options: GraphOptions = {\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 250, y: 200 } },\n        { id: 'node-2', style: { x: 250, y: 200 } },\n        { id: 'node-3', style: { x: 250, y: 200 } },\n        { id: 'node-4', style: { x: 250, y: 200 } },\n        { id: 'node-5', style: { x: 250, y: 200 } },\n        { id: 'node-6', style: { x: 250, y: 200 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-2', target: 'node-3' },\n        { source: 'node-3', target: 'node-1' },\n        { source: 'node-3', target: 'node-5' },\n        { source: 'node-2', target: 'node-4' },\n        { source: 'node-2', target: 'node-5' },\n        { source: 'node-3', target: 'node-6' },\n        { source: 'node-4', target: 'node-5' },\n        { source: 'node-5', target: 'node-6' },\n      ],\n    },\n    node: {\n      style: {\n        size: 20,\n      },\n    },\n  };\n\n  const graph = new Graph(options);\n  await graph.render();\n\n  const play = () => {\n    graph.translateElementTo(\n      {\n        'node-1': [250, 100],\n        'node-2': [175, 200],\n        'node-3': [325, 200],\n        'node-4': [100, 300],\n        'node-5': [250, 300],\n        'node-6': [400, 300],\n      },\n      true,\n    );\n  };\n  animationElementPosition.form = (panel) => [panel.add({ play }, 'play').name('Play')];\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/animation-element-state-switch.ts",
    "content": "import type { GraphOptions } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nexport const animationElementStateSwitch: TestCase = async (context) => {\n  const options: GraphOptions = {\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', states: ['active', 'selected'], style: { x: 50, y: 50 } },\n        { id: 'node-2', style: { x: 200, y: 50 } },\n        { id: 'node-3', states: ['active'], style: { x: 125, y: 150 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2', states: ['active'] },\n        { source: 'node-2', target: 'node-3' },\n        { source: 'node-3', target: 'node-1' },\n      ],\n    },\n    theme: 'light',\n    node: {\n      style: {\n        lineWidth: 1,\n        size: 20,\n      },\n      state: {\n        active: {\n          lineWidth: 2,\n        },\n        selected: {\n          fill: 'pink',\n        },\n      },\n      animation: {\n        update: [{ fields: ['lineWidth', 'fill'] }],\n      },\n    },\n    edge: {\n      style: {\n        lineWidth: 1,\n      },\n      state: {\n        active: {\n          lineWidth: 2,\n          stroke: 'pink',\n        },\n      },\n      animation: {\n        update: [\n          {\n            fields: ['lineWidth', 'stroke'],\n          },\n        ],\n      },\n    },\n  };\n\n  const graph = new Graph(options);\n  await graph.render();\n\n  const play = () => {\n    graph.updateData({\n      nodes: [\n        { id: 'node-1', states: [] },\n        { id: 'node-2', states: ['active'] },\n        { id: 'node-3', states: ['selected'] },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2', states: [] },\n        { source: 'node-2', target: 'node-3', states: ['active'] },\n      ],\n    });\n    graph.draw();\n  };\n\n  animationElementStateSwitch.form = (panel) => [panel.add({ play }, 'play').name('Play')];\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/animation-element-state.ts",
    "content": "import { Circle, ExtensionCategory, Graph, Line, register } from '@antv/g6';\n\nexport const animationElementState: TestCase = async (context) => {\n  class BreathingCircle extends Circle {\n    onCreate() {\n      this.shapeMap.halo.animate([{ lineWidth: 5 }, { lineWidth: 10 }], {\n        duration: 1000,\n        iterations: Infinity,\n        direction: 'alternate',\n      });\n    }\n  }\n\n  class FlyLine extends Line {\n    onCreate() {\n      this.shapeMap.key.animate([{ lineDashOffset: -20 }, { lineDashOffset: 0 }], {\n        duration: 500,\n        iterations: Infinity,\n      });\n    }\n  }\n\n  register(ExtensionCategory.NODE, 'breathing-circle', BreathingCircle);\n  register(ExtensionCategory.EDGE, 'fly-line', FlyLine);\n\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 50, y: 50 } },\n        { id: 'node-2', style: { x: 200, y: 50 } },\n        { id: 'node-3', style: { x: 125, y: 150 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2', style: {} },\n        { source: 'node-2', target: 'node-3', style: {} },\n        { source: 'node-3', target: 'node-1', style: {} },\n      ],\n    },\n    node: {\n      type: 'breathing-circle',\n      style: {\n        halo: true,\n        haloLineWidth: 5,\n      },\n    },\n    edge: {\n      type: 'fly-line',\n      style: {\n        lineDash: [10, 10],\n      },\n    },\n    behaviors: ['drag-element'],\n  });\n\n  await graph.draw();\n\n  animationElementState.form = (panel) => [\n    panel.add(\n      {\n        Animate: () => {\n          graph.translateElementBy('node-2', [0, 50]);\n        },\n      },\n      'Animate',\n    ),\n  ];\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/animation-element-style-position.ts",
    "content": "import { Graph, type GraphOptions } from '@antv/g6';\n\nexport const animationElementStylePosition: TestCase = async (context) => {\n  const options: GraphOptions = {\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 50, y: 50 } },\n        { id: 'node-2', style: { x: 200, y: 50 } },\n        { id: 'node-3', style: { x: 125, y: 150 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-2', target: 'node-3' },\n        { source: 'node-3', target: 'node-1' },\n      ],\n    },\n    theme: 'light',\n    node: {\n      style: {\n        size: 20,\n      },\n    },\n    edge: {\n      style: {},\n    },\n  };\n\n  const graph = new Graph(options);\n  await graph.render();\n\n  const play = () => {\n    graph.addNodeData([\n      { id: 'node-4', style: { x: 50, y: 200, fill: 'orange' } },\n      { id: 'node-5', style: { x: 75, y: 150, fill: 'purple' } },\n      { id: 'node-6', style: { x: 200, y: 100, fill: 'cyan' } },\n    ]);\n    graph.removeNodeData(['node-1']);\n    graph.updateNodeData([{ id: 'node-2', style: { x: 200, y: 200, stroke: 'green' } }]);\n    graph.draw();\n  };\n\n  animationElementStylePosition.form = (panel) => [panel.add({ play }, 'play').name('Play')];\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-auto-adapt-label.ts",
    "content": "import data from '@@/dataset/language-tree.json';\nimport { Graph, IPointerEvent, type Element } from '@antv/g6';\n\nexport const behaviorAutoAdaptLabel: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    padding: 20,\n    theme: 'light',\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n        labelBackground: true,\n        labelFontFamily: 'Gill Sans',\n        labelFill: '#333',\n      },\n      state: {\n        active: {\n          label: true,\n        },\n      },\n      palette: {\n        type: 'group',\n        color: 'tableau',\n        field: 'group',\n      },\n    },\n    edge: {\n      style: {\n        stroke: '#E2E3E1',\n        endArrow: true,\n      },\n    },\n    behaviors: [\n      'drag-canvas',\n      'zoom-canvas',\n      function () {\n        return {\n          type: 'hover-activate',\n          degree: 0,\n          onHover: (e: IPointerEvent<Element>) => {\n            this.frontElement(e.target.id);\n          },\n        };\n      },\n      {\n        key: 'auto-adapt-label',\n        type: 'auto-adapt-label',\n      },\n    ],\n    layout: {\n      type: 'd3-force',\n      manyBody: { strength: -200 },\n      x: {},\n      y: {},\n    },\n    transforms: [\n      {\n        key: 'map-node-size',\n        type: 'map-node-size',\n        maxSize: 60,\n        minSize: 12,\n        scale: 'linear',\n        mapLabelSize: true,\n      },\n    ],\n    plugins: [{ type: 'background', background: '#fff' }],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-brush-select.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const behaviorBrushSelect: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node1', combo: 'combo1', style: { x: 150, y: 250, lineWidth: 0 } },\n        { id: 'node2', combo: 'combo1', style: { x: 250, y: 200, lineWidth: 0 } },\n        { id: 'node3', combo: 'combo1', style: { x: 350, y: 250, lineWidth: 0 } },\n        { id: 'node4', combo: 'combo1', style: { x: 250, y: 300, lineWidth: 0 } },\n      ],\n      edges: [\n        {\n          id: 'edge1',\n          source: 'node1',\n          target: 'node2',\n        },\n        {\n          id: 'edge2',\n          source: 'node2',\n          target: 'node3',\n        },\n        {\n          id: 'edge3',\n          source: 'node3',\n          target: 'node4',\n        },\n        {\n          id: 'edge4',\n          source: 'node1',\n          target: 'node4',\n        },\n      ],\n      combos: [{ id: 'combo1' }],\n    },\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    animation: false,\n    behaviors: [\n      {\n        type: 'brush-select',\n        key: 'brush-select',\n        trigger: 'drag',\n      },\n    ],\n  });\n\n  await graph.render();\n\n  behaviorBrushSelect.form = (panel) => {\n    const config = {\n      mode: 'default',\n    };\n    const handleChange = () => {\n      graph.updateBehavior({ key: 'brush-select', ...config });\n    };\n\n    return [panel.add(config, 'mode', ['union', 'default', 'intersect', 'diff']).onChange(handleChange)];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-click-select.ts",
    "content": "import type { ClickSelectOptions } from '@/src/behaviors';\nimport data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const behaviorClickSelect: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    layout: { type: 'd3-force' },\n    behaviors: [{ type: 'click-select', key: 'click-select' }, 'drag-element'],\n  });\n\n  await graph.render();\n\n  const config = {\n    multiple: false,\n    trigger: ['shift'],\n    degree: 0,\n    state: 'selected',\n    unselectedState: undefined,\n  };\n\n  const updateClickSelectOption = (options: Partial<ClickSelectOptions>) => {\n    graph.updateBehavior({ key: 'click-select', ...options });\n  };\n\n  behaviorClickSelect.form = (panel) => [\n    panel\n      .add(config, 'multiple')\n      .name('Multiple')\n      .onChange((multiple: boolean) => updateClickSelectOption({ multiple })),\n    panel\n      .add(config, 'trigger', ['Shift', 'Control', 'Alt', 'Meta'])\n      .name('Trigger')\n      .onChange((trigger: string) => updateClickSelectOption({ trigger: [trigger] })),\n    panel\n      .add(config, 'degree', [0, 1, 2, 3])\n      .name('Degree')\n      .onChange((degree: number) => updateClickSelectOption({ degree })),\n  ];\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-create-edge.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const behaviorCreateEdge: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n        { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n        { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n      ],\n      edges: [],\n      combos: [\n        {\n          id: 'combo1',\n        },\n        {\n          id: 'combo2',\n          style: {\n            // 指向中心\n            ports: [{ key: 'port-1', placement: [0.5, 0.5] }],\n          },\n        },\n      ],\n    },\n    node: { style: { size: 20 } },\n    edge: {\n      style: { endArrow: true },\n    },\n    behaviors: [{ type: 'create-edge' }],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-drag-canvas.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const behaviorDragCanvas: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    layout: {\n      type: 'd3-force',\n    },\n    node: {\n      style: {\n        size: 20,\n      },\n    },\n    behaviors: [\n      'drag-canvas',\n      {\n        type: 'drag-canvas',\n        key: 'drag-canvas',\n        trigger: {\n          up: ['ArrowUp'],\n          down: ['ArrowDown'],\n          right: ['ArrowRight'],\n          left: ['ArrowLeft'],\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-drag-element.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const behaviorDragNode: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 100, y: 100 } },\n        { id: 'node-2', combo: 'combo-1', style: { x: 200, y: 100 } },\n        { id: 'node-3', style: { x: 100, y: 200 } },\n        { id: 'node-4', combo: 'combo-1', style: { x: 200, y: 200 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-2', target: 'node-4' },\n        { source: 'node-1', target: 'node-3' },\n        { source: 'node-3', target: 'node-4' },\n      ],\n      combos: [{ id: 'combo-1' }],\n    },\n    node: { style: { size: 20 } },\n    edge: {\n      style: { endArrow: true },\n    },\n    behaviors: [{ type: 'drag-element' }],\n  });\n\n  await graph.render();\n\n  behaviorDragNode.form = (panel) => {\n    const config = {\n      enable: true,\n      hideEdge: 'none',\n      shadow: false,\n    };\n    const handleChange = () => {\n      graph.setBehaviors([{ type: 'drag-element', ...config }]);\n    };\n    return [\n      panel.add(config, 'enable').onChange(handleChange),\n      panel.add(config, 'hideEdge', ['none', 'in', 'out', 'both']).onChange(handleChange),\n      panel.add(config, 'shadow').onChange(handleChange),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-expand-collapse-combo.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const behaviorExpandCollapseCombo: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', combo: 'combo-2', style: { x: 120, y: 100 } },\n        { id: 'node-2', combo: 'combo-1', style: { x: 300, y: 200 } },\n        { id: 'node-3', combo: 'combo-1', style: { x: 200, y: 300 } },\n      ],\n      edges: [\n        { id: 'edge-1', source: 'node-1', target: 'node-2' },\n        { id: 'edge-2', source: 'node-2', target: 'node-3' },\n      ],\n      combos: [\n        {\n          id: 'combo-1',\n          type: 'rect',\n          combo: 'combo-2',\n          style: {\n            collapsed: true,\n          },\n        },\n        { id: 'combo-2' },\n      ],\n    },\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    combo: {\n      style: {\n        labelText: (d) => d.id,\n        lineDash: 0,\n      },\n    },\n    behaviors: [{ type: 'drag-element' }, 'collapse-expand', 'click-select'],\n  });\n\n  await graph.render();\n\n  behaviorExpandCollapseCombo.form = (panel) => {\n    const config = {\n      element: 'combo-1',\n      dropEffect: 'move',\n      collapse: () => graph.collapseElement(config.element),\n      expand: () => graph.expandElement(config.element),\n    };\n\n    return [\n      panel\n        .add(config, 'element', {\n          'combo-1': 'combo-1',\n          'combo-2': 'combo-2',\n          'combo-3': 'combo-3',\n          'combo-4': 'combo-4',\n        })\n        .name('Combo'),\n      panel.add(config, 'collapse').name('Collapse'),\n      panel.add(config, 'expand').name('Expand'),\n      panel.add(config, 'dropEffect', ['link', 'move', 'none']).onChange((value: string) => {\n        graph.setBehaviors((behaviors) => {\n          return behaviors.map((behavior) => {\n            if (typeof behavior === 'object' && behavior.type === 'drag-element') {\n              return {\n                ...behavior,\n                dropEffect: value,\n              };\n            }\n            return behavior;\n          });\n        });\n      }),\n    ];\n  };\n\n  Object.assign(window, { graph });\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-expand-collapse-node.ts",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nexport const behaviorExpandCollapseNode: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    x: 200,\n    y: 200,\n    data: treeToGraphData({\n      id: 'A',\n      children: [\n        { id: 'B', children: [{ id: 'D' }, { id: 'E' }] },\n        { id: 'C', style: { collapsed: true }, children: [{ id: 'F' }, { id: 'G' }] },\n      ],\n    }),\n    node: {\n      style: {\n        size: 32,\n        labelText: (d) => d.id,\n        labelPlacement: 'right',\n        ports: [{ position: 'center' }],\n      },\n    },\n    edge: {\n      type: 'cubic-horizontal',\n    },\n    layout: {\n      type: 'dendrogram',\n      nodeSep: 40,\n      rankSep: 100,\n      preLayout: false,\n    },\n    behaviors: [{ type: 'collapse-expand', trigger: 'click', align: false }, 'drag-element'],\n  });\n\n  await graph.render();\n\n  behaviorExpandCollapseNode.form = (panel) => {\n    const config = {\n      element: 'A',\n      collapse: () => graph.collapseElement(config.element),\n      expand: () => graph.expandElement(config.element),\n    };\n\n    return [\n      panel.add(config, 'element', ['A', 'B', 'C', 'D', 'E', 'F', 'G']).name('Node'),\n      panel.add(config, 'collapse').name('Collapse'),\n      panel.add(config, 'expand').name('Expand'),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-fix-element-size.ts",
    "content": "import { Graph, GraphData } from '@antv/g6';\n\nconst data: GraphData = {\n  nodes: [\n    { id: 'node0', size: 50, label: '0', style: { x: 326, y: 268 }, states: ['selected'] },\n    { id: 'node1', size: 30, label: '1', style: { x: 280, y: 384 }, states: ['selected'] },\n    { id: 'node2', size: 30, label: '2', style: { x: 234, y: 167 } },\n    { id: 'node3', size: 30, label: '3', style: { x: 391, y: 368 } },\n    { id: 'node4', size: 30, label: '4', style: { x: 444, y: 209 } },\n    { id: 'node5', size: 30, label: '5', style: { x: 378, y: 157 } },\n    { id: 'node6', size: 15, label: '6', style: { x: 229, y: 400 } },\n    { id: 'node7', size: 15, label: '7', style: { x: 281, y: 440 } },\n    { id: 'node8', size: 15, label: '8', style: { x: 188, y: 119 } },\n    { id: 'node9', size: 15, label: '9', style: { x: 287, y: 157 } },\n    { id: 'node10', size: 15, label: '10', style: { x: 185, y: 200 } },\n    { id: 'node11', size: 15, label: '11', style: { x: 238, y: 110 } },\n    { id: 'node12', size: 15, label: '12', style: { x: 239, y: 221 } },\n    { id: 'node13', size: 15, label: '13', style: { x: 176, y: 160 } },\n    { id: 'node14', size: 15, label: '14', style: { x: 389, y: 423 } },\n    { id: 'node15', size: 15, label: '15', style: { x: 441, y: 341 } },\n    { id: 'node16', size: 15, label: '16', style: { x: 442, y: 398 } },\n  ],\n  edges: [\n    { source: 'node0', target: 'node1', label: '0-1', states: ['selected'] },\n    { source: 'node0', target: 'node2', label: '0-2' },\n    { source: 'node0', target: 'node3', label: '0-3' },\n    { source: 'node0', target: 'node4', label: '0-4' },\n    { source: 'node0', target: 'node5', label: '0-5' },\n    { source: 'node1', target: 'node6', label: '1-6' },\n    { source: 'node1', target: 'node7', label: '1-7' },\n    { source: 'node2', target: 'node8', label: '2-8' },\n    { source: 'node2', target: 'node9', label: '2-9' },\n    { source: 'node2', target: 'node10', label: '2-10' },\n    { source: 'node2', target: 'node11', label: '2-11' },\n    { source: 'node2', target: 'node12', label: '2-12' },\n    { source: 'node2', target: 'node13', label: '2-13' },\n    { source: 'node3', target: 'node14', label: '3-14' },\n    { source: 'node3', target: 'node15', label: '3-15' },\n    { source: 'node3', target: 'node16', label: '3-16' },\n  ],\n};\n\nexport const behaviorFixElementSize: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.label,\n        size: (d) => d.size,\n        lineWidth: 1,\n      },\n    },\n    edge: { style: { labelText: (d) => d.label } },\n    behaviors: [\n      'zoom-canvas',\n      'drag-canvas',\n      {\n        type: 'fix-element-size',\n        key: 'fix-element-size',\n        state: 'selected',\n        reset: true,\n      },\n      { type: 'click-select', key: 'click-select', multiple: true },\n    ],\n    plugins: [{ key: 'history', type: 'history' }],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-focus-element.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const behaviorFocusElement: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 100, y: 100 } },\n        { id: 'node-2', combo: 'combo-1', style: { x: 200, y: 100 } },\n        { id: 'node-3', style: { x: 100, y: 200 } },\n        { id: 'node-4', combo: 'combo-1', style: { x: 200, y: 200 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-2', target: 'node-4' },\n        { source: 'node-1', target: 'node-3' },\n        { source: 'node-3', target: 'node-4' },\n      ],\n      combos: [{ id: 'combo-1' }],\n    },\n    node: { style: { size: 20 } },\n    edge: {\n      style: { endArrow: true },\n    },\n    behaviors: ['focus-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-hover-activate.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const behaviorHoverActivate: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: data,\n    layout: { type: 'd3-force' },\n    node: {\n      style: {\n        size: 20,\n      },\n    },\n    zoomRange: [0.5, 5],\n    behaviors: [{ type: 'hover-activate' }],\n  });\n\n  await graph.render();\n\n  const config = {\n    degree: 0,\n  };\n\n  behaviorHoverActivate.form = (panel) => [\n    panel\n      .add(config, 'degree', 0, 3, 1)\n      .name('Degree')\n      .onChange((degree: number) => graph.setBehaviors([{ type: 'hover-activate', degree }])),\n  ];\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-lasso-select.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const behaviorLassoSelect: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node1', combo: 'combo1', style: { x: 150, y: 250, lineWidth: 0 } },\n        { id: 'node2', combo: 'combo1', style: { x: 250, y: 200, lineWidth: 0 } },\n        { id: 'node3', combo: 'combo1', style: { x: 350, y: 250, lineWidth: 0 } },\n        { id: 'node4', combo: 'combo1', style: { x: 250, y: 300, lineWidth: 0 } },\n      ],\n      edges: [\n        {\n          id: 'edge1',\n          source: 'node1',\n          target: 'node2',\n        },\n        {\n          id: 'edge2',\n          source: 'node2',\n          target: 'node3',\n        },\n        {\n          id: 'edge3',\n          source: 'node3',\n          target: 'node4',\n        },\n        {\n          id: 'edge4',\n          source: 'node1',\n          target: 'node4',\n        },\n      ],\n      combos: [{ id: 'combo1' }],\n    },\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    animation: false,\n    behaviors: [{ type: 'lasso-select', key: 'lasso-select', trigger: 'drag' }],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-optimize-viewport-transform.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport type { DisplayObject } from '@antv/g';\nimport type { ElementType } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nexport const behaviorOptimizeViewportTransform: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    layout: {\n      type: 'd3-force',\n    },\n    node: {\n      style: {\n        iconSrc: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',\n        labelFontSize: 8,\n        labelText: (datum) => datum.id,\n        size: 20,\n      },\n    },\n    edge: {\n      style: {\n        labelFontSize: 8,\n        labelText: (datum) => datum.id,\n        startArrow: true,\n      },\n    },\n    behaviors: [\n      'drag-canvas',\n      'zoom-canvas',\n      'scroll-canvas',\n      {\n        key: 'optimize-viewport-transform',\n        type: 'optimize-viewport-transform',\n        shapes: (type: ElementType, shape: DisplayObject) => type === 'node' && shape.className === 'key',\n      },\n    ],\n    animation: false,\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-scroll-canvas.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const behaviorScrollCanvas: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    layout: {\n      type: 'd3-force',\n    },\n    node: {\n      style: {\n        size: 20,\n      },\n    },\n    behaviors: [\n      {\n        key: 'scroll-canvas',\n        type: 'scroll-canvas',\n      },\n    ],\n  });\n\n  behaviorScrollCanvas.form = (panel) => {\n    const config = {\n      direction: '',\n      sensitivity: 1,\n    };\n\n    panel.onChange(() => {\n      graph.updateBehavior({\n        key: 'scroll-canvas',\n        ...config,\n      });\n    });\n\n    return [panel.add(config, 'direction', ['xy', 'x', 'y']), panel.add(config, 'sensitivity', 1, 10, 1)];\n  };\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/behavior-zoom-canvas.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const behaviorZoomCanvas: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    layout: {\n      type: 'd3-force',\n    },\n    node: {\n      style: {\n        size: 20,\n      },\n    },\n    zoomRange: [0.5, 5],\n    behaviors: [{ type: 'zoom-canvas' }],\n  });\n\n  await graph.render();\n\n  behaviorZoomCanvas.form = (panel) => {\n    const config = {\n      DisableZoom: false,\n      addShortcutZoom: () => {\n        graph.setBehaviors((currBehaviors) => [\n          ...currBehaviors,\n          {\n            key: 'shortcut-zoom-canvas',\n            type: 'zoom-canvas',\n            trigger: {\n              zoomIn: ['Control', '='],\n              zoomOut: ['Control', '-'],\n              reset: ['Control', '0'],\n            },\n          },\n        ]);\n        alert('Zoom behavior added');\n      },\n      removeShortcutZoom: () => {\n        graph.setBehaviors((currBehaviors) => {\n          return currBehaviors.slice(0, 1);\n        });\n        alert('Zoom behavior removed');\n      },\n    };\n\n    return [\n      panel.add(config, 'DisableZoom').onChange((disable: boolean) => {\n        graph.setBehaviors((currBehaviors) => {\n          return currBehaviors.map((behavior, index) => {\n            if (index === 0 && typeof behavior === 'object') {\n              return { ...behavior, enable: !disable };\n            }\n            return behavior;\n          });\n        });\n      }),\n      panel.add(config, 'addShortcutZoom'),\n      panel.add(config, 'removeShortcutZoom'),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/bug-drag-rotated-canvas.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const bugDragRotatedCanvas: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node1', combo: 'comboA' },\n        { id: 'node2', combo: 'comboA' },\n        { id: 'node3' },\n        { id: 'node4' },\n        { id: 'node5' },\n      ],\n      combos: [{ id: 'comboA' }],\n      edges: [\n        { source: 'node1', target: 'node2' },\n        { source: 'node1', target: 'node3' },\n        { source: 'node1', target: 'node4' },\n        { source: 'node2', target: 'node3' },\n        { source: 'node3', target: 'node4' },\n        { source: 'node4', target: 'node5' },\n      ],\n    },\n    layout: {\n      type: 'grid',\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n  });\n\n  await graph.render();\n\n  graph.rotateTo(2160 + 60);\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/bug-drag-rotated-element-force.ts",
    "content": "// ref: https://observablehq.com/@d3/force-directed-lattice\nimport { Graph } from '@antv/g6';\n\nexport const bugDragRotatedElementForce: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: getData(),\n    layout: {\n      type: 'd3-force',\n      manyBody: {\n        strength: -30,\n      },\n      link: {\n        strength: 1,\n        distance: 20,\n        iterations: 10,\n      },\n    },\n    node: {\n      style: {\n        size: 10,\n        fill: '#000',\n      },\n    },\n    edge: {\n      style: {\n        stroke: '#000',\n      },\n    },\n    behaviors: [{ type: 'drag-element-force' }, 'zoom-canvas'],\n  });\n\n  await graph.render();\n\n  graph.rotateTo(2160 + 60);\n\n  return graph;\n};\n\nfunction getData(size = 10) {\n  const nodes = Array.from({ length: size * size }, (_, i) => ({ id: `${i}` }));\n  const edges = [];\n  for (let y = 0; y < size; ++y) {\n    for (let x = 0; x < size; ++x) {\n      if (y > 0) edges.push({ source: `${(y - 1) * size + x}`, target: `${y * size + x}` });\n      if (x > 0) edges.push({ source: `${y * size + (x - 1)}`, target: `${y * size + x}` });\n    }\n  }\n  return { nodes, edges };\n}\n"
  },
  {
    "path": "packages/g6/__tests__/demos/bug-process-parallel-edges-combo-fixed.ts",
    "content": "import type { IElementDragEvent } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\n/**\n * 测试 process-parallel-edges 与 collapse-expand 配合的修复效果\n * 确保修复后向后兼容，不影响原有功能\n * @param context\n */\nexport const bugProcessParallelEdgesComboFixed: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node1', combo: 'combo1', style: { x: 300, y: 100 } },\n        { id: 'node2', combo: 'combo1', style: { x: 300, y: 150 } },\n        { id: 'node3', combo: 'combo2', style: { x: 100, y: 100 } },\n        { id: 'node4', combo: 'combo2', style: { x: 50, y: 150 } },\n        { id: 'node5', combo: 'combo2', style: { x: 150, y: 150 } },\n      ],\n      edges: [\n        { source: 'node1', target: 'node2' },\n        { source: 'node3', target: 'node4' },\n        { source: 'node3', target: 'node5' },\n      ],\n      combos: [\n        { id: 'combo1', style: { labelText: '双击折叠', collapsed: true } },\n        { id: 'combo2', style: { labelText: '单击折叠 (无 process-parallel-edges)', collapsed: false } },\n      ],\n    },\n    node: {\n      style: {\n        labelText: (d) => d.id,\n        size: 20,\n      },\n    },\n    edge: {\n      type: 'quadratic',\n      state: {\n        highlighted: {\n          stroke: '#F5AD21',\n          labelFontWeight: 600,\n          labelFontSize: 18,\n        },\n      },\n      style: {\n        loop: false,\n        lineWidth: 2,\n        haloOpacity: 0.2,\n        endArrow: true,\n        endArrowType: 'vee',\n        stroke: '#C4CDE3',\n        labelText: 'fixed version',\n        labelFontSize: 14,\n        labelFontWeight: 500,\n        labelBackground: true,\n        labelBackgroundFill: '#FFFFFF',\n        labelBackgroundRadius: 4,\n        labelPadding: [4, 8],\n      },\n    },\n    combo: {\n      style: {\n        lineWidth: 2,\n        stroke: '#99ADD1',\n        fill: '#F3F6FF',\n        radius: 8,\n        padding: [10, 20, 30, 20],\n        labelFontSize: 12,\n        labelFill: '#666',\n      },\n    },\n    // 注意：这里添加了 process-parallel-edges 变换来测试修复效果\n    transforms: [\n      {\n        type: 'process-parallel-edges',\n        distance: 60,\n      },\n    ],\n    behaviors: [\n      {\n        type: 'drag-element',\n      },\n      {\n        type: 'collapse-expand',\n        trigger: 'dblclick',\n        enable: (event: IElementDragEvent) => event.targetType === 'combo' && event.target.id === 'combo1',\n      },\n      {\n        type: 'collapse-expand',\n        trigger: 'click',\n        enable: (event: IElementDragEvent) => event.targetType === 'combo' && event.target.id === 'combo2',\n      },\n    ],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/bug-tooltip-resize.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const bugTooltipResize: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n      edges: [\n        { source: 'node1', target: 'node2' },\n        { source: 'node1', target: 'node3' },\n        { source: 'node1', target: 'node4' },\n        { source: 'node2', target: 'node3' },\n        { source: 'node3', target: 'node4' },\n        { source: 'node4', target: 'node5' },\n      ],\n    },\n    layout: {\n      type: 'grid',\n    },\n    plugins: [\n      {\n        type: 'tooltip',\n        style: {\n          ['.tooltip']: {\n            transition: 'none',\n          },\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n\n  bugTooltipResize.form = (panel) => {\n    let width = 500;\n    return [\n      panel.add(\n        {\n          resize: () => {\n            const newWidth = width === 500 ? 300 : 500;\n            width = newWidth;\n            document.querySelector<HTMLDivElement>('#container')!.style.width = `${newWidth}px`;\n            graph.resize();\n            graph.fitView();\n          },\n        },\n        'resize',\n      ),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/canvas-cursor.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const canvasCursor: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],\n    },\n    cursor: 'progress',\n  });\n\n  await graph.render();\n\n  canvasCursor.form = (panel) => {\n    return [\n      panel\n        .add({ cursor: 'progress' }, 'cursor', [\n          'auto',\n          'default',\n          'none',\n          'context-menu',\n          'help',\n          'pointer',\n          'progress',\n          'wait',\n          'cell',\n          'crosshair',\n          'text',\n          'vertical-text',\n          'alias',\n          'copy',\n          'move',\n          'no-drop',\n          'not-allowed',\n          'grab',\n          'grabbing',\n          'all-scroll',\n          'col-resize',\n          'row-resize',\n          'n-resize',\n          'e-resize',\n          's-resize',\n          'w-resize',\n          'ne-resize',\n          'nw-resize',\n          'se-resize',\n          'sw-resize',\n          'ew-resize',\n          'ns-resize',\n          'nesw-resize',\n          'nwse-resize',\n          'zoom-in',\n          'zoom-out',\n        ])\n        .onChange((cursor: any) => {\n          graph.setOptions({ cursor });\n        }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/case-fishbone.ts",
    "content": "import type { TextStyleProps } from '@antv/g';\nimport { Text } from '@antv/g';\nimport {\n  BaseTransform,\n  BaseTransformOptions,\n  CategoricalPalette,\n  DrawData,\n  ExtensionCategory,\n  Graph,\n  register,\n  RuntimeContext,\n  treeToGraphData,\n} from '@antv/g6';\n\nconst data = {\n  id: '克服拖延',\n  children: [\n    { id: '完美主义情结', children: [{ id: '正确评估事情难度' }, { id: '先完成，再完善' }, { id: 'Just do it' }] },\n    {\n      id: '提高专注度',\n      children: [{ id: '番茄工作法' }, { id: '限时、限量，一次只做一件事' }, { id: '提高抗干扰能力，减少打断' }],\n    },\n    {\n      id: '设定清晰的任务管理流程',\n      children: [\n        { id: '设立完成事项的优先级' },\n        { id: '拆解具体可执行的目标' },\n        { id: '收集-整理-排序-执行反馈-总结' },\n      ],\n    },\n    {\n      id: '建立积极反馈',\n      children: [{ id: '做喜欢的事情' }, { id: '精神激励' }, { id: '物质激励' }],\n    },\n    {\n      id: '放松、享受',\n      children: [{ id: '注重过程而非结果' }, { id: '靠需求驱动而非焦虑' }, { id: '接受、理解' }],\n    },\n  ],\n};\n\ninterface AssignColorByBranchOptions extends BaseTransformOptions {\n  colors?: CategoricalPalette;\n}\n\nexport const caseFishbone: TestCase = async (context) => {\n  let textShape: Text | null;\n  const measureText = (style: TextStyleProps) => {\n    if (!textShape) textShape = new Text({ style });\n    textShape.attr(style);\n    return textShape.getBBox().width;\n  };\n\n  class AssignColorByBranch extends BaseTransform {\n    static defaultOptions: Partial<AssignColorByBranchOptions> = {\n      colors: [\n        '#1783FF',\n        '#F08F56',\n        '#D580FF',\n        '#00C9C9',\n        '#7863FF',\n        '#DB9D0D',\n        '#60C42D',\n        '#FF80CA',\n        '#2491B3',\n        '#17C76F',\n      ],\n    };\n\n    constructor(context: RuntimeContext, options: AssignColorByBranchOptions) {\n      super(context, Object.assign({}, AssignColorByBranch.defaultOptions, options));\n    }\n\n    beforeDraw(input: DrawData): DrawData {\n      const nodes = this.context.model.getNodeData();\n\n      if (nodes.length === 0) return input;\n\n      let colorIndex = 0;\n      const dfs = (nodeId: string, color?: string) => {\n        const node = nodes.find((datum) => datum.id == nodeId);\n        if (!node) return;\n\n        node.style ||= {};\n        node.style.color = color || this.options.colors[colorIndex++ % this.options.colors.length];\n        node.children?.forEach((childId) => dfs(childId, node.style?.color as string));\n      };\n\n      nodes.filter((node) => node.depth === 1).forEach((rootNode) => dfs(rootNode.id));\n\n      return input;\n    }\n  }\n\n  class ArrangeEdgeZIndex extends BaseTransform {\n    public beforeDraw(input: DrawData): DrawData {\n      const { model } = this.context;\n      const { nodes, edges } = model.getData();\n\n      const oneLevelNodes = nodes.filter((node) => node.depth === 1);\n      const oneLevelNodeIds = oneLevelNodes.map((node) => node.id);\n\n      edges.forEach((edge) => {\n        if (oneLevelNodeIds.includes(edge.target)) {\n          edge.style ||= {};\n          edge.style.zIndex = oneLevelNodes.length - oneLevelNodes.findIndex((node) => node.id === edge.target);\n        }\n      });\n\n      return input;\n    }\n  }\n\n  register(ExtensionCategory.TRANSFORM, 'assign-color-by-branch', AssignColorByBranch);\n  register(ExtensionCategory.TRANSFORM, 'arrange-edge-z-index', ArrangeEdgeZIndex);\n\n  const getNodeSize = (id: string, depth: number) => {\n    const FONT_FAMILY = 'system-ui, sans-serif';\n    return depth === 0\n      ? [measureText({ text: id, fontSize: 24, fontWeight: 'bold', fontFamily: FONT_FAMILY }) + 80, 58]\n      : depth === 1\n        ? [measureText({ text: id, fontSize: 18, fontFamily: FONT_FAMILY }) + 50, 42]\n        : [0, 30];\n  };\n\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    padding: 10,\n    data: treeToGraphData(data),\n    node: {\n      type: 'rect',\n      style: (d) => {\n        const style = {\n          radius: 8,\n          size: getNodeSize(d.id, d.depth!),\n          labelText: d.id,\n          labelPlacement: 'right',\n        };\n\n        if (d.depth === 0) {\n          Object.assign(style, {\n            fill: '#EFF0F0',\n            labelFill: '#262626',\n            labelFontWeight: 'bold',\n            labelFontSize: 24,\n            labelOffsetY: 8,\n            labelPlacement: 'center',\n          });\n        } else if (d.depth === 1) {\n          Object.assign(style, {\n            labelFontSize: 18,\n            labelFill: '#fff',\n            labelFillOpacity: 0.9,\n            labelOffsetY: 3,\n            labelPlacement: 'center',\n            fill: d.style?.color,\n          });\n        } else {\n          Object.assign(style, {\n            fill: 'transparent',\n            labelFontSize: 16,\n            labeFill: '#262626',\n          });\n        }\n        return style;\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        lineWidth: 3,\n        stroke: function (data) {\n          return (this.getNodeData(data.target).style!.color as string) || '#99ADD1';\n        },\n      },\n    },\n    layout: {\n      type: 'fishbone',\n      direction: 'LR',\n      hGap: 40,\n      vGap: 60,\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas'],\n    transforms: ['assign-color-by-branch', 'arrange-edge-z-index'],\n    animation: false,\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/case-fund-flow.ts",
    "content": "import data from '@@/dataset/decision-tree.json';\nimport type { RectStyleProps as GRectStyleProps, TextStyleProps as GTextStyleProps } from '@antv/g';\nimport { Rect as GRect, Group, Text as GText } from '@antv/g';\nimport type { BadgeStyleProps, LabelStyleProps, NodeData, RectStyleProps, TreeData } from '@antv/g6';\nimport { Badge, CommonEvent, ExtensionCategory, Graph, Label, Rect, register, treeToGraphData } from '@antv/g6';\n\nexport const caseFundFlow: TestCase = async (context) => {\n  const COLORS: Record<string, string> = {\n    B: '#1783FF',\n    R: '#F46649',\n    Y: '#DB9D0D',\n    G: '#60C42D',\n    DI: '#A7A7A7',\n  };\n  const GREY_COLOR = '#CED4D9';\n\n  class TreeNode extends Rect {\n    get data() {\n      return this.context.model.getNodeLikeDatum(this.id) as Record<string, string>;\n    }\n\n    get childrenData() {\n      return this.context.model.getChildrenData(this.id);\n    }\n\n    protected getLabelStyle(attributes: Required<RectStyleProps>): LabelStyleProps {\n      const [width, height] = this.getSize(attributes);\n      return {\n        x: -width / 2 + 8,\n        y: -height / 2 + 16,\n        text: this.data.name,\n        fontSize: 12,\n        opacity: 0.85,\n        fill: '#000',\n        cursor: 'pointer',\n      };\n    }\n\n    protected getPriceStyle(attributes: Required<RectStyleProps>): GTextStyleProps {\n      const [width, height] = this.getSize(attributes);\n      return {\n        x: -width / 2 + 8,\n        y: height / 2 - 8,\n        text: this.data.label,\n        fontSize: 16,\n        fill: '#000',\n        opacity: 0.85,\n      };\n    }\n\n    protected drawPriceShape(attributes: Required<RectStyleProps>, container: Group) {\n      const priceStyle = this.getPriceStyle(attributes);\n      this.upsert('price', GText, priceStyle, container);\n    }\n\n    protected getCurrencyStyle(attributes: Required<RectStyleProps>): GTextStyleProps {\n      const [, height] = this.getSize(attributes);\n      return {\n        x: this.shapeMap['price'].getLocalBounds().max[0] + 4,\n        y: height / 2 - 8,\n        text: this.data.currency,\n        fontSize: 12,\n        fill: '#000',\n        opacity: 0.75,\n      };\n    }\n\n    protected drawCurrencyShape(attributes: Required<RectStyleProps>, container: Group) {\n      const currencyStyle = this.getCurrencyStyle(attributes);\n      this.upsert('currency', GText, currencyStyle, container);\n    }\n\n    protected getPercentStyle(attributes: Required<RectStyleProps>): GTextStyleProps {\n      const [width, height] = this.getSize(attributes);\n      return {\n        x: width / 2 - 4,\n        y: height / 2 - 8,\n        text: `${((Number(this.data.variableValue) || 0) * 100).toFixed(2)}%`,\n        fontSize: 12,\n        textAlign: 'right',\n        fill: COLORS[this.data.status],\n      };\n    }\n\n    protected drawPercentShape(attributes: Required<RectStyleProps>, container: Group) {\n      const percentStyle = this.getPercentStyle(attributes);\n      this.upsert('percent', GText, percentStyle, container);\n    }\n\n    protected getTriangleStyle(attributes: Required<RectStyleProps>): LabelStyleProps {\n      const percentMinX = this.shapeMap['percent'].getLocalBounds().min[0];\n      const [, height] = this.getSize(attributes);\n      return {\n        fill: COLORS[this.data.status],\n        x: this.data.variableUp ? percentMinX - 18 : percentMinX,\n        y: height / 2 - 16,\n        fontFamily: 'iconfont',\n        fontSize: 16,\n        text: '\\ue62d',\n        transform: this.data.variableUp ? [] : [['rotate', 180]],\n      };\n    }\n\n    protected drawTriangleShape(attributes: Required<RectStyleProps>, container: Group) {\n      const triangleStyle = this.getTriangleStyle(attributes);\n      this.upsert('triangle', Label, triangleStyle, container);\n    }\n\n    protected getVariableStyle(attributes: Required<RectStyleProps>): GTextStyleProps {\n      const [, height] = this.getSize(attributes);\n      return {\n        fill: '#000',\n        fontSize: 12,\n        opacity: 0.45,\n        text: this.data.variableName,\n        textAlign: 'right',\n        x: this.shapeMap['triangle'].getLocalBounds().min[0] - 4,\n        y: height / 2 - 8,\n      };\n    }\n\n    protected drawVariableShape(attributes: Required<RectStyleProps>, container: Group) {\n      const variableStyle = this.getVariableStyle(attributes);\n      this.upsert('variable', GText, variableStyle, container);\n    }\n\n    protected getCollapseStyle(attributes: Required<RectStyleProps>): BadgeStyleProps | false {\n      if (this.childrenData.length === 0) return false;\n      const { collapsed } = attributes;\n      const [width, height] = this.getSize(attributes);\n      return {\n        backgroundFill: '#fff',\n        backgroundHeight: 16,\n        backgroundLineWidth: 1,\n        backgroundRadius: 0,\n        backgroundStroke: GREY_COLOR,\n        backgroundWidth: 16,\n        cursor: 'pointer',\n        fill: GREY_COLOR,\n        fontSize: 16,\n        text: collapsed ? '+' : '-',\n        textAlign: 'center',\n        textBaseline: 'middle',\n        x: width / 2,\n        y: 0,\n      };\n    }\n\n    protected drawCollapseShape(attributes: Required<RectStyleProps>, container: Group) {\n      const collapseStyle = this.getCollapseStyle(attributes);\n      const btn = this.upsert('collapse', Badge, collapseStyle, container);\n\n      if (btn && !Reflect.has(btn, '__bind__')) {\n        Reflect.set(btn, '__bind__', true);\n        btn.addEventListener(CommonEvent.CLICK, () => {\n          const { collapsed } = this.attributes;\n          const graph = this.context.graph;\n          if (collapsed) graph.expandElement(this.id);\n          else graph.collapseElement(this.id);\n        });\n      }\n    }\n\n    protected getProcessBarStyle(attributes: Required<RectStyleProps>): GRectStyleProps {\n      const { rate, status } = this.data;\n      // @ts-ignore\n      const { radius } = attributes;\n      const color = COLORS[status];\n      const percent = `${Number(rate) * 100}%`;\n      const [width, height] = this.getSize(attributes);\n      return {\n        x: -width / 2,\n        y: height / 2 - 4,\n        width: width,\n        height: 4,\n        radius: [0, 0, radius, radius],\n        fill: `linear-gradient(to right, ${color} ${percent}, ${GREY_COLOR} ${percent})`,\n      };\n    }\n\n    protected drawProcessBarShape(attributes: Required<RectStyleProps>, container: Group) {\n      const processBarStyle = this.getProcessBarStyle(attributes);\n      this.upsert('process-bar', GRect, processBarStyle, container);\n    }\n\n    protected getKeyStyle(attributes: Required<RectStyleProps>): GRectStyleProps {\n      const keyStyle = super.getKeyStyle(attributes);\n      return {\n        ...keyStyle,\n        fill: '#fff',\n        lineWidth: 1,\n        stroke: GREY_COLOR,\n      };\n    }\n\n    public render(attributes: Required<RectStyleProps> = this.parsedAttributes, container: Group) {\n      super.render(attributes, container);\n\n      this.drawPriceShape(attributes, container);\n      this.drawCurrencyShape(attributes, container);\n      this.drawPercentShape(attributes, container);\n      this.drawTriangleShape(attributes, container);\n      this.drawVariableShape(attributes, container);\n      this.drawProcessBarShape(attributes, container);\n      this.drawCollapseShape(attributes, container);\n    }\n  }\n\n  register(ExtensionCategory.NODE, 'tree-node', TreeNode);\n\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data: treeToGraphData(data, {\n      getNodeData: (datum: TreeData, depth: number) => {\n        if (!datum.style) datum.style = {};\n        datum.style.collapsed = depth >= 2;\n        if (!datum.children) return datum as NodeData;\n        const { children, ...restDatum } = datum;\n        return { ...restDatum, children: children.map((child) => child.id) } as NodeData;\n      },\n    }),\n    node: {\n      type: 'tree-node',\n      style: {\n        size: [202, 60],\n        ports: [{ placement: 'left' }, { placement: 'right' }],\n        radius: 4,\n      },\n    },\n    edge: {\n      type: 'cubic-horizontal',\n      style: {\n        stroke: GREY_COLOR,\n      },\n    },\n    layout: {\n      type: 'indented',\n      direction: 'LR',\n      dropCap: false,\n      indent: 300,\n      getHeight: () => 60,\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/case-indented-tree.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport type {\n  BaseStyleProps,\n  DisplayObject,\n  DisplayObjectConfig,\n  Group,\n  RectStyleProps,\n  TextStyleProps,\n} from '@antv/g';\nimport { Text as GText, Rect } from '@antv/g';\nimport type {\n  BadgeStyleProps,\n  BaseBehaviorOptions,\n  BaseNodeStyleProps,\n  Element,\n  ID,\n  IElementDragEvent,\n  IPointerEvent,\n  LabelStyleProps,\n  Node,\n  NodeData,\n  Point,\n  PolylineStyleProps,\n  Prefix,\n  RuntimeContext,\n  Vector2,\n} from '@antv/g6';\nimport {\n  Badge,\n  BaseBehavior,\n  BaseNode,\n  CommonEvent,\n  ExtensionCategory,\n  Graph,\n  NodeEvent,\n  Polyline,\n  idOf,\n  register,\n  subStyleProps,\n  treeToGraphData,\n} from '@antv/g6';\n\nexport const caseIndentedTree: TestCase = async (context) => {\n  const rootId = data.id;\n  const COLORS = [\n    '#5B8FF9',\n    '#F6BD16',\n    '#5AD8A6',\n    '#945FB9',\n    '#E86452',\n    '#6DC8EC',\n    '#FF99C3',\n    '#1E9493',\n    '#FF9845',\n    '#5D7092',\n  ];\n\n  let textShape: GText | null;\n  const measureText = (text: TextStyleProps) => {\n    if (!textShape) textShape = new GText({ style: text });\n    textShape.attr(text);\n    return textShape.getBBox().width;\n  };\n\n  interface IndentedNodeStyleProps extends BaseNodeStyleProps {\n    showIcon: boolean;\n    color: string;\n  }\n\n  const TreeEvent = {\n    COLLAPSE_EXPAND: 'collapse-expand',\n    ADD_CHILD: 'add-child',\n  };\n\n  class IndentedNode extends BaseNode<IndentedNodeStyleProps> {\n    static defaultStyleProps: Partial<IndentedNodeStyleProps> = {\n      ports: [\n        {\n          key: 'in',\n          placement: 'right-bottom',\n        },\n        {\n          key: 'out',\n          placement: 'left-bottom',\n        },\n      ],\n    };\n\n    constructor(options: DisplayObjectConfig<IndentedNodeStyleProps>) {\n      Object.assign(options.style!, IndentedNode.defaultStyleProps);\n      super(options);\n    }\n\n    protected get childrenData() {\n      return this.context!.model.getChildrenData(this.id);\n    }\n\n    protected getKeyStyle(attributes: Required<IndentedNodeStyleProps>): RectStyleProps {\n      const [width, height] = this.getSize(attributes);\n      const keyStyle = super.getKeyStyle(attributes);\n      return {\n        width,\n        height,\n        ...keyStyle,\n        fill: 'transparent',\n      };\n    }\n\n    protected drawKeyShape(attributes: Required<IndentedNodeStyleProps>, container: Group): Rect | undefined {\n      const keyStyle = this.getKeyStyle(attributes);\n      return this.upsert('key', Rect, keyStyle, container);\n    }\n\n    protected getLabelStyle(attributes: Required<IndentedNodeStyleProps>): false | LabelStyleProps {\n      if (attributes.label === false || !attributes.labelText) return false;\n      return subStyleProps<LabelStyleProps>(this.getGraphicStyle(attributes), 'label');\n    }\n\n    private drawIconArea(attributes: Required<IndentedNodeStyleProps>, container: Group) {\n      const [, h] = this.getSize(attributes);\n      const iconAreaStyle = {\n        fill: 'transparent',\n        height: 30,\n        width: 12,\n        x: -6,\n        y: h,\n        zIndex: -1,\n      };\n      this.upsert('icon-area', Rect, iconAreaStyle, container);\n    }\n\n    private forwardEvent(target: DisplayObject | undefined, type: string, listener: (event: any) => void) {\n      if (target && !Reflect.has(target, '__bind__')) {\n        Reflect.set(target, '__bind__', true);\n        target.addEventListener(type, listener);\n      }\n    }\n\n    private getCountStyle(attributes: Required<IndentedNodeStyleProps>): false | BadgeStyleProps {\n      const { collapsed, color } = attributes;\n      if (collapsed) {\n        const [, height] = this.getSize(attributes);\n        return {\n          backgroundFill: color,\n          cursor: 'pointer',\n          fill: '#fff',\n          fontSize: 8,\n          padding: [0, 10],\n          text: `${this.childrenData.length}`,\n          textAlign: 'center',\n          y: height + 8,\n        };\n      }\n\n      return false;\n    }\n\n    private drawCountShape(attributes: Required<IndentedNodeStyleProps>, container: Group) {\n      const countStyle = this.getCountStyle(attributes);\n      const btn = this.upsert('count', Badge, countStyle, container);\n\n      this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent) => {\n        event.stopPropagation();\n        this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {\n          id: this.id,\n          collapsed: false,\n        });\n      });\n    }\n\n    private isShowCollapse(attributes: Required<IndentedNodeStyleProps>) {\n      return !attributes.collapsed && this.childrenData.length > 0;\n    }\n\n    private getCollapseStyle(attributes: Required<IndentedNodeStyleProps>): false | BadgeStyleProps {\n      const { showIcon, color } = attributes;\n      if (!this.isShowCollapse(attributes)) return false;\n      const [, height] = this.getSize(attributes);\n      return {\n        visibility: showIcon ? 'visible' : 'hidden',\n        backgroundFill: color,\n        backgroundHeight: 12,\n        backgroundWidth: 12,\n        cursor: 'pointer',\n        fill: '#fff',\n        fontFamily: 'iconfont',\n        fontSize: 8,\n        text: '\\ue6e4',\n        textAlign: 'center',\n        x: -1, // half of edge line width\n        y: height + 8,\n      };\n    }\n\n    private drawCollapseShape(attributes: Required<IndentedNodeStyleProps>, container: Group) {\n      const iconStyle = this.getCollapseStyle(attributes);\n      const btn = this.upsert('collapse-expand', Badge, iconStyle, container);\n\n      this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent) => {\n        event.stopPropagation();\n        this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {\n          id: this.id,\n          collapsed: !attributes.collapsed,\n        });\n      });\n    }\n\n    private getAddStyle(attributes: Required<IndentedNodeStyleProps>): false | BadgeStyleProps {\n      const { collapsed, showIcon } = attributes;\n      if (collapsed) return false;\n      const [, height] = this.getSize(attributes);\n      const color = '#ddd';\n      const lineWidth = 1;\n\n      return {\n        visibility: showIcon ? 'visible' : 'hidden',\n        backgroundFill: '#fff',\n        backgroundHeight: 12,\n        backgroundLineWidth: lineWidth,\n        backgroundStroke: color,\n        backgroundWidth: 12,\n        cursor: 'pointer',\n        fill: color,\n        fontFamily: 'iconfont',\n        text: '\\ue664',\n        textAlign: 'center',\n        x: -1,\n        y: height + (this.isShowCollapse(attributes) ? 22 : 8),\n      };\n    }\n\n    private drawAddShape(attributes: Required<IndentedNodeStyleProps>, container: Group) {\n      const addStyle = this.getAddStyle(attributes);\n      const btn = this.upsert('add', Badge, addStyle, container);\n\n      this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent) => {\n        event.stopPropagation();\n        this.context.graph.emit(TreeEvent.ADD_CHILD, { id: this.id });\n      });\n    }\n\n    public render(attributes: Required<IndentedNodeStyleProps> = this.parsedAttributes, container: Group = this): void {\n      super.render(attributes, container);\n\n      this.drawCountShape(attributes, container);\n\n      this.drawIconArea(attributes, container);\n      this.drawCollapseShape(attributes, container);\n      this.drawAddShape(attributes, container);\n    }\n  }\n\n  class IndentedEdge extends Polyline {\n    protected getControlPoints(attributes: Required<PolylineStyleProps>) {\n      const [sourcePoint, targetPoint] = this.getEndpoints(attributes, false);\n      const [sx] = sourcePoint;\n      const [, ty] = targetPoint;\n      return [[sx, ty]] as Point[];\n    }\n  }\n\n  interface CollapseExpandTreeOptions extends BaseBehaviorOptions {\n    onCreateChild?: (parent: ID) => NodeData;\n  }\n\n  class CollapseExpandTree extends BaseBehavior<CollapseExpandTreeOptions> {\n    constructor(context: RuntimeContext, options: CollapseExpandTreeOptions) {\n      super(context, options);\n      this.bindEvents();\n    }\n\n    public update(options: Partial<CollapseExpandTreeOptions>) {\n      this.unbindEvents();\n      super.update(options);\n      this.bindEvents();\n    }\n\n    private bindEvents() {\n      const { graph } = this.context;\n\n      graph.on(NodeEvent.POINTER_ENTER, this.showIcon);\n      graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon);\n      graph.on(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);\n      graph.on(TreeEvent.ADD_CHILD, this.addChild);\n    }\n\n    private unbindEvents() {\n      const { graph } = this.context;\n\n      graph.off(NodeEvent.POINTER_ENTER, this.showIcon);\n      graph.off(NodeEvent.POINTER_LEAVE, this.hideIcon);\n      graph.off(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);\n      graph.off(TreeEvent.ADD_CHILD, this.addChild);\n    }\n\n    private status = 'idle';\n\n    private showIcon = (event: IPointerEvent<Node>) => {\n      this.setIcon(event, true);\n    };\n\n    private hideIcon = (event: IPointerEvent<Node>) => {\n      this.setIcon(event, false);\n    };\n\n    private setIcon = (event: IPointerEvent<Node>, show: boolean) => {\n      if (this.status !== 'idle') return;\n      const { target } = event;\n      const id = target.id;\n      const { graph, element } = this.context;\n      graph.updateNodeData([{ id, style: { showIcon: show } }]);\n      element!.draw({ animation: false, silence: true });\n    };\n\n    private onCollapseExpand = async (event: any) => {\n      this.status = 'busy';\n      const { id, collapsed } = event;\n      const { graph } = this.context;\n      await graph.frontElement(id);\n      if (collapsed) await graph.collapseElement(id);\n      else await graph.expandElement(id);\n      this.status = 'idle';\n    };\n\n    private addChild = async (event: any) => {\n      this.status = 'busy';\n      const {\n        onCreateChild = (id) => {\n          const parent = this.context.graph.getNodeData(id);\n          const { x = 0, y = 0 } = parent.style || {};\n          return { id: `${Date.now()}`, style: { x, y, labelText: 'new node' } };\n        },\n      } = this.options;\n      const { graph } = this.context;\n      const datum = onCreateChild(event.id);\n      const parent = graph.getNodeData(event.id);\n\n      graph.addNodeData([datum]);\n      graph.addEdgeData([{ source: event.id, target: datum.id }]);\n      graph.updateNodeData([\n        { id: event.id, children: [...(parent.children || []), datum.id], style: { collapsed: false } },\n      ]);\n      await graph.render();\n      this.status = 'idle';\n    };\n  }\n\n  interface DragBranchOptions extends BaseBehaviorOptions, Prefix<'shadow', BaseStyleProps> {\n    enable?: boolean | ((event: IElementDragEvent) => boolean);\n  }\n\n  /**\n   * <zh/> 支持拖拽节点到其他节点下作为子节点\n   *\n   * <en/> Support dragging nodes to other nodes as child nodes\n   */\n  class DragBranch extends BaseBehavior<DragBranchOptions> {\n    constructor(context: RuntimeContext, options: DragBranchOptions) {\n      super(context, options);\n      this.bindEvents();\n    }\n\n    public update(options: Partial<DragBranchOptions>) {\n      this.unbindEvents();\n      super.update(options);\n      this.bindEvents();\n    }\n\n    private bindEvents() {\n      const { graph } = this.context;\n\n      graph.on(NodeEvent.DRAG_START, this.onDragStart);\n      graph.on(NodeEvent.DRAG, this.onDrag);\n      graph.on(NodeEvent.DRAG_END, this.onDragEnd);\n      graph.on(NodeEvent.DRAG_ENTER, this.onDragEnter);\n      graph.on(NodeEvent.DRAG_LEAVE, this.onDragLeave);\n    }\n\n    private unbindEvents() {\n      const { graph } = this.context;\n\n      graph.off(NodeEvent.DRAG_START, this.onDragStart);\n      graph.off(NodeEvent.DRAG, this.onDrag);\n      graph.off(NodeEvent.DRAG_END, this.onDragEnd);\n      graph.off(NodeEvent.DRAG_ENTER, this.onDragEnter);\n      graph.off(NodeEvent.DRAG_LEAVE, this.onDragLeave);\n    }\n\n    private enable = true;\n\n    private validate(event: IElementDragEvent) {\n      if (this.destroyed) return false;\n      const { enable = (evt) => evt.target.id !== rootId } = this.options;\n      if (typeof enable === 'function') return enable(event);\n      return !!enable;\n    }\n\n    private shadow?: Rect;\n\n    private createShadow(target: Element) {\n      const shadowStyle = subStyleProps<RectStyleProps>(this.options, 'shadow');\n      const positionStyle = target.getShape('label').getBBox();\n\n      this.shadow = new Rect({\n        style: {\n          pointerEvents: 'none',\n          fill: '#F3F9FF',\n          fillOpacity: 0.5,\n          stroke: '#1890FF',\n          strokeOpacity: 0.9,\n          lineDash: [5, 5],\n          ...shadowStyle,\n          ...positionStyle,\n        },\n      });\n      this.context.canvas.appendChild(this.shadow);\n    }\n\n    private moveShadow(offset: Vector2) {\n      if (!this.shadow) return;\n      const [dx, dy] = offset;\n      this.shadow.translate(dx, dy);\n    }\n\n    private destroyShadow() {\n      this.shadow?.remove();\n      this.shadow = undefined;\n    }\n\n    private child?: Node;\n\n    private parent?: Node;\n\n    private onDragStart = (event: IElementDragEvent) => {\n      this.enable = this.validate(event);\n      if (!this.enable) return;\n\n      const { target } = event;\n      this.child = target as Node;\n      this.createShadow(target);\n    };\n\n    private getDelta(event: IElementDragEvent): Vector2 {\n      const zoom = this.context.graph.getZoom();\n      return [event.dx / zoom, event.dy / zoom];\n    }\n\n    private onDrag = (event: IElementDragEvent) => {\n      if (!this.enable) return;\n\n      const delta = this.getDelta(event);\n      this.moveShadow(delta);\n    };\n\n    private onDragEnd = () => {\n      this.destroyShadow();\n      if (this.child === undefined || this.parent === undefined) return;\n\n      const { graph } = this.context;\n      const childId = this.child.id;\n      const parentId = this.parent.id;\n\n      const originalParent = graph.getParentData(childId, 'tree') as NodeData;\n\n      // 前后父节点不应该相同\n      // The previous and current parent nodes should not be the same\n      if (idOf(originalParent) === parentId) return;\n\n      // 新的父节点不应该是当前节点的子节点\n      // The new parent node should not be a child node of the current node\n      const ancestors = graph.getAncestorsData(parentId, 'tree');\n      if (ancestors.some((ancestor) => ancestor.id === childId)) return;\n\n      const edges = graph\n        .getEdgeData()\n        .filter((edge) => edge.target === childId)\n        .map(idOf);\n      graph.removeEdgeData(edges);\n      graph.updateNodeData([\n        { id: idOf(originalParent), children: originalParent?.children?.filter((child) => child !== childId) },\n      ]);\n      const modifiedParent = graph.getNodeData(parentId);\n      graph.updateNodeData([{ id: parentId, children: [...(modifiedParent.children || []), childId] }]);\n      graph.addEdgeData([{ source: parentId, target: childId }]);\n      graph.render();\n    };\n\n    private onDragEnter = (event: IElementDragEvent) => {\n      const { graph, element } = this.context;\n      const targetId = event.target.id;\n      if (targetId === this.child?.id || targetId === rootId) {\n        if (targetId === rootId) this.parent = event.target as Node;\n        return;\n      }\n\n      this.parent = event.target as Node;\n      graph.updateNodeData([{ id: targetId, states: ['selected'] }]);\n      element!.draw({ animation: false, silence: true });\n    };\n\n    private onDragLeave = (event: IElementDragEvent) => {\n      const { graph, element } = this.context;\n      const targetId = event.target.id;\n\n      this.parent = undefined;\n      graph.updateNodeData([{ id: targetId, states: [] }]);\n      element!.draw({ animation: false, silence: true });\n    };\n  }\n\n  register(ExtensionCategory.NODE, 'indented', IndentedNode);\n  register(ExtensionCategory.EDGE, 'indented', IndentedEdge);\n  register(ExtensionCategory.BEHAVIOR, 'collapse-expand-tree', CollapseExpandTree);\n  register(ExtensionCategory.BEHAVIOR, 'drag-branch', DragBranch);\n\n  const graph: Graph = new Graph({\n    ...context,\n    data: treeToGraphData(data),\n    x: 100,\n    y: 100,\n    node: {\n      type: 'indented',\n      style: {\n        size: (d) => [measureText({ text: d.id, fontSize: 12 }) + 6, 20],\n        labelBackground: true,\n        labelBackgroundRadius: 0,\n        labelBackgroundFill: (d) => (d.id === rootId ? '#576286' : '#fff'),\n        labelFill: (d) => (d.id === rootId ? '#fff' : '#666'),\n        labelText: (d) => d.style?.labelText || d.id,\n        labelTextAlign: (d) => (d.id === rootId ? 'center' : 'left'),\n        labelTextBaseline: 'top',\n        color: (datum: NodeData) => {\n          const depth = graph.getAncestorsData(datum.id, 'tree').length - 1;\n          return COLORS[depth % COLORS.length] || '#576286';\n        },\n      },\n      state: {\n        selected: {\n          lineWidth: 0,\n          labelFill: '#40A8FF',\n          labelBackground: true,\n          labelFontWeight: 'normal',\n          labelBackgroundFill: '#e8f7ff',\n          labelBackgroundRadius: 10,\n        },\n      },\n    },\n    edge: {\n      type: 'indented',\n      style: {\n        radius: 16,\n        lineWidth: 2,\n        sourcePort: 'out',\n        targetPort: 'in',\n        stroke: (datum) => {\n          const depth = graph.getAncestorsData(datum.source, 'tree').length;\n          return COLORS[depth % COLORS.length];\n        },\n      },\n    },\n    layout: {\n      type: 'indented',\n      direction: 'LR',\n      isHorizontal: true,\n      indent: 40,\n      getHeight: () => 20,\n      getVGap: () => 10,\n    },\n    behaviors: [\n      'scroll-canvas',\n      'drag-branch',\n      'collapse-expand-tree',\n      {\n        type: 'click-select',\n        enable: (event: IPointerEvent<Node>) => event.targetType === 'node' && event.target.id !== rootId,\n      },\n    ],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/case-language-tree.ts",
    "content": "import { labelPropagation } from '@antv/algorithm';\nimport type { Element, IPointerEvent, NodeData } from '@antv/g6';\nimport { Graph } from '@antv/g6';\nimport data from '../dataset/language-tree.json';\n\nexport const caseLanguageTree: TestCase = async (context) => {\n  const size = (node: NodeData) => Math.max(...(node.style?.size as [number, number, number]));\n\n  const graph = new Graph({\n    ...context,\n    data: {\n      ...data,\n      nodes: labelPropagation(data).clusters.flatMap((cluster) => cluster.nodes),\n    },\n    node: {\n      style: {\n        label: true,\n        labelBackground: true,\n        labelPadding: [0, 4],\n        labelText: (d) => d.id,\n        icon: true,\n        iconSrc: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',\n      },\n      state: {\n        inactive: {\n          fill: '#E0E0E0',\n          fillOpacity: 1,\n          icon: false,\n          label: false,\n          labelBackground: false,\n        },\n      },\n      palette: {\n        field: (d) => d.clusterId,\n      },\n    },\n    edge: {\n      style: {\n        stroke: '#E0E0E0',\n        endArrow: true,\n      },\n    },\n    layout: {\n      type: 'd3-force',\n      link: {\n        distance: (edge: any) => size(edge.source) + size(edge.target),\n      },\n      collide: {\n        radius: (node: NodeData) => size(node) + 1,\n      },\n      manyBody: {\n        strength: (node: NodeData) => -4 * size(node),\n      },\n      animation: false,\n    },\n    transforms: [\n      {\n        key: 'map-node-size',\n        type: 'map-node-size',\n        scale: 'log',\n      },\n    ],\n    behaviors: [\n      'drag-canvas',\n      'zoom-canvas',\n      function () {\n        return {\n          key: 'hover-activate',\n          type: 'hover-activate',\n          enable: true,\n          degree: 1,\n          inactiveState: 'inactive',\n          onHover: (e: IPointerEvent<Element>) => {\n            this.frontElement(e.target.id);\n            e.view.setCursor('pointer');\n          },\n          onHoverEnd: (e: IPointerEvent<Element>) => {\n            e.view.setCursor('default');\n          },\n        };\n      },\n      {\n        key: 'fix-element-size',\n        type: 'fix-element-size',\n        enable: true,\n        node: [{ shape: 'label' }],\n      },\n      {\n        key: 'auto-adapt-label',\n        type: 'auto-adapt-label',\n      },\n    ],\n    plugins: [{ type: 'background', background: '#fff' }],\n    animation: false,\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/case-mindmap.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport type { DisplayObject, DisplayObjectConfig, Group, RectStyleProps, TextStyleProps } from '@antv/g';\nimport { Rect, Text } from '@antv/g';\nimport type {\n  BadgeStyleProps,\n  BaseBehaviorOptions,\n  BaseNodeStyleProps,\n  CubicStyleProps,\n  DrawData,\n  ID,\n  IPointerEvent,\n  Node,\n  NodeData,\n  PathArray,\n  RuntimeContext,\n} from '@antv/g6';\nimport {\n  Badge,\n  BaseBehavior,\n  BaseNode,\n  BaseTransform,\n  CommonEvent,\n  CubicHorizontal,\n  ExtensionCategory,\n  Graph,\n  idOf,\n  NodeEvent,\n  positionOf,\n  register,\n  treeToGraphData,\n} from '@antv/g6';\n\nexport const caseMindmap: TestCase = async (context) => {\n  const COLORS = [\n    '#1783FF',\n    '#00C9C9',\n    '#F08F56',\n    '#D580FF',\n    '#7863FF',\n    '#DB9D0D',\n    '#60C42D',\n    '#FF80CA',\n    '#2491B3',\n    '#17C76F',\n  ];\n\n  const RootNodeStyle = {\n    fill: '#EFF0F0',\n    labelFill: '#262626',\n    labelFontSize: 16,\n    labelFontWeight: 600,\n    labelPlacement: 'center',\n    ports: [{ placement: 'right' }, { placement: 'left' }],\n    radius: 4,\n  };\n\n  const NodeStyle = {\n    fill: 'transparent',\n    labelPlacement: 'center',\n    labelFontSize: 12,\n    ports: [{ placement: 'right-bottom' }, { placement: 'left-bottom' }],\n  };\n\n  const TreeEvent = {\n    COLLAPSE_EXPAND: 'collapse-expand',\n    ADD_CHILD: 'add-child',\n  };\n\n  let textShape: Text | null;\n  const measureText = (text: TextStyleProps) => {\n    if (!textShape) textShape = new Text({ style: text });\n    textShape.attr(text);\n    return textShape.getBBox().width;\n  };\n\n  const getNodeWidth = (nodeId: ID, isRoot: boolean) => {\n    return isRoot\n      ? measureText({ text: nodeId, fontSize: RootNodeStyle.labelFontSize }) + 20\n      : measureText({ text: nodeId, fontSize: NodeStyle.labelFontSize }) + 30;\n  };\n\n  interface MindmapNodeStyleProps extends BaseNodeStyleProps {\n    color: string;\n    showIcon: boolean;\n    direction: 'left' | 'right';\n  }\n\n  class MindmapNode extends BaseNode<MindmapNodeStyleProps> {\n    static defaultStyleProps: Partial<MindmapNodeStyleProps> = {\n      showIcon: false,\n    };\n\n    constructor(options: DisplayObjectConfig<MindmapNodeStyleProps>) {\n      Object.assign(options.style!, MindmapNode.defaultStyleProps);\n      super(options);\n    }\n\n    get childrenData() {\n      return this.context.model.getChildrenData(this.id);\n    }\n\n    get rootId() {\n      return idOf(this.context.model.getRootsData()[0]);\n    }\n\n    private isShowCollapse(attributes: Required<MindmapNodeStyleProps>) {\n      const { collapsed, showIcon } = attributes;\n      return !collapsed && showIcon && this.childrenData.length > 0;\n    }\n\n    protected getCollapseStyle(attributes: Required<MindmapNodeStyleProps>): BadgeStyleProps | false {\n      const { showIcon, color, direction } = attributes;\n      if (!this.isShowCollapse(attributes)) return false;\n      const [width, height] = this.getSize(attributes);\n\n      return {\n        backgroundFill: color,\n        backgroundHeight: 12,\n        backgroundWidth: 12,\n        cursor: 'pointer',\n        fill: '#fff',\n        fontFamily: 'iconfont',\n        fontSize: 8,\n        text: '\\ue6e4',\n        textAlign: 'center',\n        transform: direction === 'left' ? [['rotate', 90]] : [['rotate', -90]],\n        visibility: showIcon ? 'visible' : 'hidden',\n        x: direction === 'left' ? -6 : width + 6,\n        y: height,\n      };\n    }\n\n    protected drawCollapseShape(attributes: Required<MindmapNodeStyleProps>, container: Group) {\n      const iconStyle = this.getCollapseStyle(attributes);\n      const btn = this.upsert('collapse-expand', Badge, iconStyle, container);\n\n      this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent<Node>) => {\n        event.stopPropagation();\n        this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {\n          id: this.id,\n          collapsed: !attributes.collapsed,\n        });\n      });\n    }\n\n    protected getCountStyle(attributes: Required<MindmapNodeStyleProps>): BadgeStyleProps | false {\n      const { collapsed, color, direction } = attributes;\n      const count = this.context.model.getDescendantsData(this.id).length;\n      if (!collapsed || count === 0) return false;\n      const [width, height] = this.getSize(attributes);\n      return {\n        backgroundFill: color,\n        backgroundHeight: 12,\n        backgroundWidth: 12,\n        cursor: 'pointer',\n        fill: '#fff',\n        fontSize: 8,\n        text: count.toString(),\n        textAlign: 'center',\n        x: direction === 'left' ? -6 : width + 6,\n        y: height,\n      };\n    }\n\n    protected drawCountShape(attributes: Required<MindmapNodeStyleProps>, container: Group) {\n      const countStyle = this.getCountStyle(attributes);\n      const btn = this.upsert('count', Badge, countStyle, container);\n\n      this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent<Node>) => {\n        event.stopPropagation();\n        this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {\n          id: this.id,\n          collapsed: false,\n        });\n      });\n    }\n\n    protected getAddStyle(attributes: Required<MindmapNodeStyleProps>): BadgeStyleProps | false {\n      const { collapsed, showIcon, direction } = attributes;\n      if (collapsed || !showIcon) return false;\n      const [width, height] = this.getSize(attributes);\n      const color = '#ddd';\n\n      const offsetX = this.isShowCollapse(attributes) ? 24 : 12;\n      const isRoot = this.id === this.rootId;\n\n      return {\n        backgroundFill: '#fff',\n        backgroundHeight: 12,\n        backgroundLineWidth: 1,\n        backgroundStroke: color,\n        backgroundWidth: 12,\n        cursor: 'pointer',\n        fill: color,\n        fontFamily: 'iconfont',\n        fontSize: 8,\n        text: '\\ue664',\n        textAlign: 'center',\n        x: isRoot ? width + 12 : direction === 'left' ? -offsetX : width + offsetX,\n        y: isRoot ? height / 2 : height,\n      };\n    }\n\n    protected getAddBarStyle(attributes: Required<MindmapNodeStyleProps>): RectStyleProps | false {\n      const { collapsed, showIcon, direction, color = COLORS[0] } = attributes;\n      if (collapsed || !showIcon) return false;\n      const [width, height] = this.getSize(attributes);\n\n      const offsetX = this.isShowCollapse(attributes) ? 12 : 0;\n      const isRoot = this.id === this.rootId;\n\n      const HEIGHT = 2;\n      const WIDTH = 6;\n\n      return {\n        cursor: 'pointer',\n        fill:\n          direction === 'left'\n            ? `linear-gradient(180deg, #fff 20%, ${color})`\n            : `linear-gradient(0deg, #fff 20%, ${color})`,\n        height: HEIGHT,\n        width: WIDTH,\n        x: isRoot ? width : direction === 'left' ? -offsetX - WIDTH : width + offsetX,\n        y: isRoot ? height / 2 - HEIGHT / 2 : height - HEIGHT / 2,\n        zIndex: -1,\n      };\n    }\n\n    protected drawAddShape(attributes: Required<MindmapNodeStyleProps>, container: Group) {\n      const addStyle = this.getAddStyle(attributes);\n      const addBarStyle = this.getAddBarStyle(attributes);\n      this.upsert('add-bar', Rect, addBarStyle, container);\n      const btn = this.upsert('add', Badge, addStyle, container);\n\n      this.forwardEvent(btn, CommonEvent.CLICK, (event: IPointerEvent<Node>) => {\n        event.stopPropagation();\n        this.context.graph.emit(TreeEvent.ADD_CHILD, { id: this.id, direction: attributes.direction });\n      });\n    }\n\n    private forwardEvent(target: DisplayObject | undefined, type: string, listener: (event: any) => void) {\n      if (target && !Reflect.has(target, '__bind__')) {\n        Reflect.set(target, '__bind__', true);\n        target.addEventListener(type, listener);\n      }\n    }\n\n    protected getKeyStyle(attributes: Required<MindmapNodeStyleProps>): RectStyleProps {\n      const [width, height] = this.getSize(attributes);\n      const keyShape = super.getKeyStyle(attributes);\n      return { width, height, ...keyShape };\n    }\n\n    protected drawKeyShape(attributes: Required<MindmapNodeStyleProps>, container: Group) {\n      const keyStyle = this.getKeyStyle(attributes);\n      return this.upsert('key', Rect, keyStyle, container);\n    }\n\n    public render(attributes: Required<MindmapNodeStyleProps> = this.parsedAttributes, container: Group = this) {\n      super.render(attributes, container);\n\n      this.drawCollapseShape(attributes, container);\n      this.drawAddShape(attributes, container);\n\n      this.drawCountShape(attributes, container);\n    }\n  }\n\n  class MindmapEdge extends CubicHorizontal {\n    get rootId() {\n      return idOf(this.context.model.getRootsData()[0]);\n    }\n\n    protected getKeyPath(attributes: Required<CubicStyleProps>): PathArray {\n      const path = super.getKeyPath(attributes);\n      const isRoot = this.targetNode.id === this.rootId;\n      const labelWidth = getNodeWidth(this.targetNode.id, isRoot);\n\n      const [, tp] = this.getEndpoints(attributes);\n      const sign = this.sourceNode.getCenter()[0] < this.targetNode.getCenter()[0] ? 1 : -1;\n      return [...path, ['L', tp[0] + labelWidth * sign, tp[1]]] as PathArray;\n    }\n  }\n\n  interface CollapseExpandTreeOptions extends BaseBehaviorOptions {\n    onCreateChild?: (parent: ID) => NodeData;\n  }\n\n  class CollapseExpandTree extends BaseBehavior<CollapseExpandTreeOptions> {\n    constructor(context: RuntimeContext, options: Partial<CollapseExpandTreeOptions>) {\n      super(context, options);\n      this.bindEvents();\n    }\n\n    update(options: Partial<CollapseExpandTreeOptions>) {\n      this.unbindEvents();\n      super.update(options);\n      this.bindEvents();\n    }\n\n    bindEvents() {\n      const { graph } = this.context;\n\n      graph.on(NodeEvent.POINTER_ENTER, this.showIcon);\n      graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon);\n      graph.on(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);\n      graph.on(TreeEvent.ADD_CHILD, this.addChild);\n    }\n\n    unbindEvents() {\n      const { graph } = this.context;\n\n      graph.off(NodeEvent.POINTER_ENTER, this.showIcon);\n      graph.off(NodeEvent.POINTER_LEAVE, this.hideIcon);\n      graph.off(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);\n      graph.off(TreeEvent.ADD_CHILD, this.addChild);\n    }\n\n    status = 'idle';\n\n    showIcon = (event: IPointerEvent<Node>) => {\n      this.setIcon(event, true);\n    };\n\n    hideIcon = (event: IPointerEvent<Node>) => {\n      this.setIcon(event, false);\n    };\n\n    setIcon = (event: IPointerEvent<Node>, show: boolean) => {\n      if (this.status !== 'idle') return;\n      const { target } = event;\n      const id = target.id;\n      const { graph, element } = this.context;\n      graph.updateNodeData([{ id, style: { showIcon: show } }]);\n      element!.draw({ animation: false, silence: true });\n    };\n\n    onCollapseExpand = async (event: any) => {\n      this.status = 'busy';\n      const { id, collapsed } = event;\n      const { graph } = this.context;\n      await graph.frontElement(id);\n      if (collapsed) await graph.collapseElement(id);\n      else await graph.expandElement(id);\n      this.status = 'idle';\n    };\n\n    addChild = async (event: any) => {\n      this.status = 'busy';\n      const {\n        onCreateChild = () => {\n          const currentTime = new Date(Date.now()).toLocaleString();\n          return { id: `New Node in ${currentTime}` };\n        },\n      } = this.options;\n      const { graph } = this.context;\n      const datum = onCreateChild(event.id);\n      const parent = graph.getNodeData(event.id);\n\n      graph.addNodeData([datum]);\n      graph.addEdgeData([{ source: event.id, target: datum.id }]);\n      graph.updateNodeData([\n        {\n          id: event.id,\n          children: [...(parent.children || []), datum.id],\n          style: { collapsed: false, showIcon: false },\n        },\n      ]);\n      await graph.render();\n      await graph.focusElement(datum.id);\n      this.status = 'idle';\n    };\n  }\n\n  class AssignElementColor extends BaseTransform {\n    beforeDraw(data: DrawData): DrawData {\n      const { nodes = [], edges = [] } = this.context.graph.getData();\n\n      const nodeColorMap = new Map<string, string>();\n\n      let colorIndex = 0;\n      const dfs = (nodeId: string, color?: string) => {\n        const node = nodes.find((datum) => datum.id == nodeId)!;\n        if (!node) return;\n\n        if (node.depth !== 0) {\n          const nodeColor = color || COLORS[colorIndex++ % COLORS.length];\n          node.style ||= {};\n          node.style.color = nodeColor;\n          nodeColorMap.set(nodeId, nodeColor);\n        }\n\n        node.children?.forEach((childId) => dfs(childId, node.style!.color as string));\n      };\n\n      nodes.filter((node) => node.depth === 0).forEach((rootNode) => dfs(rootNode.id));\n\n      edges.forEach((edge) => {\n        edge.style ||= {};\n        edge.style.stroke = nodeColorMap.get(edge.target);\n      });\n\n      return data;\n    }\n  }\n\n  register(ExtensionCategory.NODE, 'mindmap', MindmapNode);\n  register(ExtensionCategory.EDGE, 'mindmap', MindmapEdge);\n  register(ExtensionCategory.BEHAVIOR, 'collapse-expand-tree', CollapseExpandTree);\n  register(ExtensionCategory.TRANSFORM, 'assign-element-color', AssignElementColor);\n\n  const rootId = data.id;\n\n  const getNodeSide = (nodeData: NodeData, parentData?: NodeData): 'center' | 'left' | 'right' => {\n    if (!parentData) return 'center';\n\n    const nodePositionX = positionOf(nodeData)[0];\n    const parentPositionX = positionOf(parentData)[0];\n    return parentPositionX > nodePositionX ? 'left' : 'right';\n  };\n\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    node: {\n      type: 'mindmap',\n      style: function (d: NodeData) {\n        const direction = getNodeSide(d, this.getParentData(idOf(d), 'tree'));\n        const isRoot = idOf(d) === rootId;\n\n        return {\n          direction,\n          labelText: idOf(d),\n          size: [getNodeWidth(idOf(d), isRoot), 30],\n          // 通过设置节点标签背景来扩大节点的交互区域\n          // Enlarge the interactive area of the node by setting label background\n          labelBackground: true,\n          labelBackgroundFill: 'transparent',\n          labelPadding: direction === 'left' ? [2, 0, 10, 40] : [2, 40, 10, 0],\n          ...(isRoot ? RootNodeStyle : NodeStyle),\n        };\n      },\n    },\n    edge: {\n      type: 'mindmap',\n      style: { lineWidth: 2 },\n    },\n    layout: {\n      type: 'mindmap',\n      direction: 'H',\n      getHeight: () => 30,\n      getWidth: (node: NodeData) => getNodeWidth(node.id, node.id === rootId),\n      getVGap: () => 6,\n      getHGap: () => 60,\n      animation: false,\n    },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'collapse-expand-tree'],\n    transforms: ['assign-element-color'],\n    animation: false,\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/case-org-chart.ts",
    "content": "import data from '@@/dataset/organization-chart.json';\nimport type { RectStyleProps as GRectStyleProps, Group } from '@antv/g';\nimport type { BaseBehaviorOptions, IViewportEvent, LabelStyleProps, RectStyleProps, RuntimeContext } from '@antv/g6';\nimport { Badge, BaseBehavior, ExtensionCategory, Graph, GraphEvent, Label, Rect, register } from '@antv/g6';\nenum ZoomLevel {\n  OVERVIEW = 'overview',\n  DETAILED = 'detailed',\n}\n\nconst statusColors: Record<string, string> = {\n  online: '#17BEBB',\n  busy: '#E36397',\n  offline: '#B7AD99',\n};\n\nconst DEFAULT_LEVEL = ZoomLevel.DETAILED;\n\nexport const caseOrgChart: TestCase = async (context) => {\n  /**\n   * Draw a chart node with different ui based on the zoom level.\n   */\n  class ChartNode extends Rect {\n    protected get data() {\n      return this.context.model.getElementDataById(this.id).data as Record<string, string>;\n    }\n\n    protected get level() {\n      return this.data.level || DEFAULT_LEVEL;\n    }\n\n    protected getLabelStyle(): false | LabelStyleProps {\n      const text = this.data.name as string;\n      const labelStyle =\n        this.level === ZoomLevel.OVERVIEW\n          ? {\n              fill: '#fff',\n              fontSize: 20,\n              fontWeight: 600,\n              textAlign: 'center',\n              transform: [['translate', 0, 0]],\n            }\n          : {\n              fill: '#2078B4',\n              fontSize: 14,\n              fontWeight: 400,\n              textAlign: 'left',\n              transform: [['translate', -65, -15]],\n            };\n      return { text, ...labelStyle } as LabelStyleProps;\n    }\n\n    protected getKeyStyle(attributes: Required<RectStyleProps>): GRectStyleProps {\n      return {\n        ...super.getKeyStyle(attributes),\n        fill: this.level === ZoomLevel.OVERVIEW ? statusColors[this.data.status] : '#fff',\n      };\n    }\n\n    protected getPositionStyle(attributes: Required<RectStyleProps>): false | LabelStyleProps {\n      if (this.level === ZoomLevel.OVERVIEW) return false;\n      return {\n        text: this.data.position,\n        fontSize: 8,\n        fontWeight: 400,\n        textTransform: 'uppercase',\n        fill: '#343f4a',\n        textAlign: 'left',\n        transform: [['translate', -65, 0]],\n      };\n    }\n\n    protected drawPositionShape(attributes: Required<RectStyleProps>, container: Group) {\n      const positionStyle = this.getPositionStyle(attributes);\n      this.upsert('position', Label, positionStyle, container);\n    }\n\n    protected getStatusStyle(attributes: Required<RectStyleProps>): false | LabelStyleProps {\n      if (this.level === ZoomLevel.OVERVIEW) return false;\n      return {\n        text: this.data.status,\n        fontSize: 8,\n        textAlign: 'left',\n        transform: [['translate', 40, -16]],\n        padding: [0, 4],\n        fill: '#fff',\n        backgroundFill: statusColors[this.data.status as string],\n      };\n    }\n\n    protected drawStatusShape(attributes: Required<RectStyleProps>, container: Group) {\n      const statusStyle = this.getStatusStyle(attributes);\n      this.upsert('status', Badge, statusStyle, container);\n    }\n\n    protected getPhoneStyle(attributes: Required<RectStyleProps>): false | LabelStyleProps {\n      if (this.level === ZoomLevel.OVERVIEW) return false;\n      return {\n        text: this.data.phone,\n        fontSize: 8,\n        fontWeight: 300,\n        textAlign: 'left',\n        transform: [['translate', -65, 20]],\n      };\n    }\n\n    protected drawPhoneShape(attributes: Required<RectStyleProps>, container: Group) {\n      const style = this.getPhoneStyle(attributes);\n      this.upsert('phone', Label, style, container);\n    }\n\n    public render(attributes: Required<RectStyleProps> = this.parsedAttributes, container: Group = this): void {\n      super.render(attributes, container);\n\n      this.drawPositionShape(attributes, container);\n\n      this.drawStatusShape(attributes, container);\n\n      this.drawPhoneShape(attributes, container);\n    }\n  }\n\n  /**\n   * Implement a level of detail rendering, which will show different details based on the zoom level.\n   */\n  class LevelOfDetail extends BaseBehavior {\n    private prevLevel: ZoomLevel = DEFAULT_LEVEL;\n    private levels: Record<ZoomLevel, [number, number]> = {\n      [ZoomLevel.OVERVIEW]: [0, 0.6],\n      [ZoomLevel.DETAILED]: [0.6, Infinity],\n    };\n\n    constructor(context: RuntimeContext, options: BaseBehaviorOptions) {\n      super(context, options);\n      this.bindEvents();\n    }\n\n    private updateZoomLevel = async (e: IViewportEvent) => {\n      if ('scale' in e.data) {\n        const scale = e.data.scale!;\n        const level = Object.entries(this.levels).find(\n          ([key, [min, max]]) => scale > min && scale <= max,\n        )?.[0] as ZoomLevel;\n        if (level && this.prevLevel !== level) {\n          const { graph } = this.context;\n          graph.updateNodeData((prev) => prev.map((node) => ({ ...node, data: { ...node.data, level } })));\n          await graph.draw();\n          this.prevLevel = level;\n        }\n      }\n    };\n\n    private bindEvents() {\n      const { graph } = this.context;\n      graph.on(GraphEvent.AFTER_TRANSFORM, this.updateZoomLevel);\n    }\n\n    private unbindEvents() {\n      const { graph } = this.context;\n      graph.off(GraphEvent.AFTER_TRANSFORM, this.updateZoomLevel);\n    }\n\n    public destroy() {\n      this.unbindEvents();\n      super.destroy();\n    }\n  }\n\n  register(ExtensionCategory.NODE, 'chart-node', ChartNode);\n  register(ExtensionCategory.BEHAVIOR, 'level-of-detail', LevelOfDetail);\n\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'chart-node',\n      style: {\n        labelPlacement: 'center',\n        lineWidth: 1,\n        ports: [{ placement: 'top' }, { placement: 'bottom' }],\n        radius: 2,\n        shadowBlur: 10,\n        shadowColor: '#e0e0e0',\n        shadowOffsetX: 3,\n        size: [150, 60],\n        stroke: '#C0C0C0',\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        router: {\n          type: 'orth',\n        },\n        stroke: '#C0C0C0',\n      },\n    },\n    layout: {\n      type: 'dagre',\n    },\n    autoFit: 'view',\n    behaviors: ['level-of-detail', 'zoom-canvas', 'drag-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/case-radial-dendrogram.ts",
    "content": "import data from '@@/dataset/flare.json';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const caseRadialDendrogram: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    node: {\n      style: {\n        size: 14,\n        labelText: (d) => d.id,\n        labelBackground: true,\n      },\n      state: {\n        active: {\n          fill: '#00C9C9',\n        },\n      },\n    },\n    edge: {\n      type: 'cubic-radial',\n      style: {\n        lineWidth: 2,\n      },\n      state: {\n        active: {\n          stroke: '#009999',\n        },\n      },\n    },\n    layout: {\n      type: 'dendrogram',\n      radial: true,\n      nodeSep: 30,\n      rankSep: 200,\n      preLayout: false,\n    },\n    behaviors: [\n      'drag-canvas',\n      'zoom-canvas',\n      'drag-element',\n      {\n        key: 'hover-activate',\n        type: 'hover-activate',\n        degree: 5,\n        direction: 'in',\n        inactiveState: 'inactive',\n      },\n    ],\n    transforms: ['place-radial-labels'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/case-unicorns-investors.ts",
    "content": "import type { Element, ElementDatum, IElementEvent, IPointerEvent, NodeData } from '@/src';\nimport { Graph } from '@/src';\n\n/**\n * Inspired by https://graphcommons.com/graphs/be8bc972-5b26-4f5c-837d-a34704f33a9e\n * Network Map of 🦄 Unicorns and Their 💰Investors\n * 1086 nodes, 1247 edges\n *\n * 10 VC firms in Silicon Valley funded 82% of all unicorns, 98% of all exited unicorns. Data from CB Insights, updated March 2020.\n * @param context - context\n * @returns - graph\n */\nexport const caseUnicornsInvestors: TestCase = async (context) => {\n  const data = await fetch('https://assets.antv.antgroup.com/g6/unicorns-investors.json').then((res) => res.json());\n\n  const size = (node: NodeData) => Math.max(...(node.style?.size as [number, number, number]));\n\n  const graph = new Graph({\n    ...context,\n    data,\n    autoFit: 'view',\n    node: {\n      style: {\n        label: true,\n        labelText: (d) => d.data?.name,\n        labelBackground: true,\n        icon: true,\n        iconText: (d) => (d.data?.type === 'Investor' ? '💰' : '🦄️'),\n        fill: (d) => (d.data?.type === 'Investor' ? '#6495ED' : '#FFA07A'),\n      },\n      state: {\n        inactive: {\n          fillOpacity: 0.3,\n          icon: false,\n          label: false,\n        },\n      },\n    },\n    edge: {\n      style: {\n        label: false,\n        labelText: (d) => d.data?.type,\n        labelBackground: true,\n      },\n      state: {\n        active: {\n          label: true,\n        },\n        inactive: {\n          strokeOpacity: 0,\n        },\n      },\n    },\n    layout: {\n      type: 'd3-force',\n      link: {\n        distance: (edge: any) => {\n          size(edge.source) + size(edge.target);\n        },\n      },\n      collide: { radius: (node: NodeData) => size(node) },\n      manyBody: { strength: (node: NodeData) => -4 * size(node) },\n      animation: false,\n      iterations: 20,\n    },\n    transforms: [\n      {\n        type: 'map-node-size',\n        scale: 'linear',\n        maxSize: 60,\n        minSize: 20,\n        mapLabelSize: [12, 16],\n      },\n    ],\n    behaviors: [\n      'drag-canvas',\n      'zoom-canvas',\n      function () {\n        return {\n          key: 'hover-activate',\n          type: 'hover-activate',\n          enable: (e: IPointerEvent<Element>) => e.targetType === 'node',\n          degree: 1,\n          inactiveState: 'inactive',\n          onHover: (e: IPointerEvent<Element>) => {\n            this.frontElement(e.target.id);\n            e.view.setCursor('pointer');\n          },\n          onHoverEnd: (e: IPointerEvent<Element>) => {\n            e.view.setCursor('default');\n          },\n        };\n      },\n      { type: 'fix-element-size', enable: true },\n      'auto-adapt-label',\n    ],\n    plugins: [\n      {\n        type: 'tooltip',\n        position: 'right',\n        enable: (e: IElementEvent) => e.targetType === 'node',\n        getContent: (e: IElementEvent, items: ElementDatum[]) => {\n          const { type, name } = items[0].data as { type: string; name: string };\n          const color = type === 'Investor' ? '#6495ED' : '#FFA07A';\n          return `<div>\n            <div style=\"font-weight: bold; font-size: 9px; color: ${color};\">${type}</div>\n            <div class=\"tooltip-name\">${name}</div>\n          </div>`;\n        },\n        style: {\n          '.tooltip': {\n            padding: '2px 8px',\n            'border-radius': '8px',\n          },\n        },\n      },\n    ],\n    animation: false,\n  });\n\n  performance.mark('render-start');\n\n  await graph.render();\n\n  performance.mark('render-end');\n\n  performance.measure('render', 'render-start', 'render-end');\n\n  console.log(performance.getEntriesByType('measure')[0].duration);\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/case-why-do-cats.ts",
    "content": "import { Renderer as CanvasRenderer } from '@antv/g-canvas';\nimport { Plugin as PluginRoughCanvasRenderer } from '@antv/g-plugin-rough-canvas-renderer';\nimport type { ComboData, GraphData, NodeData } from '@antv/g6';\nimport { BaseLayout, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { hierarchy, pack } from '@antv/vendor/d3-hierarchy';\n\nexport const caseWhyDoCats: TestCase = async (context) => {\n  const style = document.createElement('style');\n  style.innerHTML = `\n  @font-face {\n  font-family: 'handwriting';\n  src: url('https://mass-office.alipay.com/huamei_koqzbu/afts/file/sgUeRbI3d-IAAAAAAAAAABAADnV5AQBr/font.woff2')\n    format('woff2');\n}`;\n  document.head.appendChild(style);\n\n  function getColor(id: string) {\n    const colors = [\n      '#8dd3c7',\n      '#bebada',\n      '#fb8072',\n      '#80b1d3',\n      '#fdb462',\n      '#b3de69',\n      '#fccde5',\n      '#d9d9d9',\n      '#bc80bd',\n      '#ccebc5',\n      '#ffed6f',\n    ];\n    const index = parseInt(id);\n    return colors[index % colors.length];\n  }\n\n  type RowDatum = {\n    animal: string;\n    id: string;\n    id_num: string;\n    index_value: string;\n    leaf: string;\n    parentId: string;\n    remainder: string;\n    start_sentence: string;\n    sum_index_value: string;\n    text: string;\n  };\n\n  const rawData: RowDatum[] = await fetch('https://assets.antv.antgroup.com/g6/cat-hierarchy.json').then((res) =>\n    res.json(),\n  );\n\n  const topics = [\n    'cat.like',\n    'cat.hate',\n    'cat.love',\n    'cat.not.like',\n    'cat.afraid_of',\n    'cat.want.to',\n    'cat.scared.of',\n    'cat.not.want_to',\n  ];\n\n  const graphData = rawData.reduce(\n    (acc, row) => {\n      const { id } = row;\n      topics.forEach((topic) => {\n        if (id.startsWith(topic)) {\n          if (id === topic) {\n            acc.nodes.push({ ...row, depth: 1 });\n          } else {\n            acc.nodes.push({ ...row, depth: 2, actualParentId: topic });\n          }\n        }\n      });\n\n      return acc;\n    },\n    { nodes: [], edges: [], combos: [] } as Required<GraphData>,\n  );\n\n  class BubbleLayout extends BaseLayout {\n    id = 'bubble-layout';\n\n    public async execute(model: GraphData, options?: any): Promise<GraphData> {\n      const { nodes = [] } = model;\n\n      const { width = 0, height = 0 } = { ...this.options, ...options };\n\n      const root = hierarchy<NodeData | ComboData>({ id: 'root' }, (datum) => {\n        const { id } = datum;\n        if (id === 'root') return nodes.filter((node) => node.depth === 1);\n        else if (datum.depth === 2) return [];\n        else return nodes.filter((node) => node.actualParentId === id);\n      });\n\n      root.sum((d: any) => (+d.index_value || 0.01) ** 0.5 * 100);\n\n      pack<NodeData | ComboData>()\n        .size([width, height])\n        .padding((node) => {\n          return node.depth === 0 ? 20 : 2;\n        })(root);\n\n      const result: Required<GraphData> = { nodes: [], edges: [], combos: [] };\n\n      root.descendants().forEach((node) => {\n        const {\n          data: { id },\n          x,\n          y,\n          // @ts-expect-error r is exist\n          r,\n        } = node;\n\n        if (node.depth >= 1) result.nodes.push({ id, style: { x, y, size: r * 2 } });\n      });\n\n      return result;\n    }\n  }\n\n  register(ExtensionCategory.LAYOUT, 'bubble-layout', BubbleLayout);\n\n  const graph = new Graph({\n    ...context,\n    animation: false,\n    data: graphData,\n    renderer: (layer) => {\n      const renderer = new CanvasRenderer();\n      if (layer === 'main') {\n        renderer.registerPlugin(new PluginRoughCanvasRenderer());\n      }\n      return renderer;\n    },\n    node: {\n      style: (d) => {\n        const id_num = d.id_num as string;\n        const color = getColor(id_num);\n\n        if (d.depth === 1) {\n          return {\n            fill: 'none',\n            stroke: color,\n            labelFontFamily: 'handwriting',\n            labelFontSize: 20,\n            labelText: d.id.replace('cat.', '').replace(/\\.|_/g, ' '),\n            labelTextTransform: 'capitalize',\n            lineWidth: 1,\n            zIndex: -1,\n          };\n        }\n\n        const text = d.text as string;\n        const diameter = d.style!.size as number;\n\n        return {\n          fill: color,\n          fillOpacity: 0.7,\n          stroke: color,\n          fillStyle: 'cross-hatch',\n          hachureGap: 1.5,\n          iconFontFamily: 'handwriting',\n          iconFontSize: (diameter / text.length) * 2,\n          iconText: diameter > 20 ? d.text : '',\n          iconFontWeight: 'bold',\n          iconStroke: color,\n          iconLineWidth: 2,\n          lineWidth: (diameter || 20) ** 0.5 / 5,\n        };\n      },\n    },\n    layout: {\n      type: 'bubble-layout',\n    },\n    plugins: [\n      {\n        type: 'tooltip',\n        getContent: (event: any, items: NodeData[]) => {\n          return `<span style=\"text-transform: capitalize; font-family: handwriting; font-size: 20px;\">${items[0].id.replace(/\\.|_/g, ' ')}</span>`;\n        },\n      },\n    ],\n    behaviors: [{ type: 'drag-canvas', enable: true }, 'zoom-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/common-graph.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const commonGraph: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        fill: (d) => (d.id === '33' ? '#d4414c' : '#2f363d'),\n      },\n    },\n    layout: { type: 'd3-force' },\n    behaviors: ['zoom-canvas', 'drag-canvas'],\n    plugins: [],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/controller-viewport.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const controllerViewport: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 200, y: 200 } },\n        { id: 'node-2', style: { x: 300, y: 300 } },\n      ],\n      edges: [{ source: 'node-1', target: 'node-2' }],\n    },\n  });\n\n  await graph.render();\n\n  controllerViewport.form = (panel) => {\n    const animation = { duration: 500 };\n    const config = {\n      translateBy: () => graph.translateBy([10, 10], animation),\n      translateTo: () => graph.translateTo([0, 0], animation),\n      rotateBy: () => graph.rotateBy(45, animation),\n      rotateTo: () => graph.rotateTo(0, animation),\n      zoomBy: () => graph.zoomBy(1.1, animation),\n      zoomTo: () => graph.zoomTo(1, animation),\n    };\n    return [\n      panel.add(config, 'translateBy'),\n      panel.add(config, 'translateTo'),\n      panel.add(config, 'rotateBy'),\n      panel.add(config, 'rotateTo'),\n      panel.add(config, 'zoomBy'),\n      panel.add(config, 'zoomTo'),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/demo-autosize-element-label.ts",
    "content": "import type { FixShapeConfig } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nconst mockText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit';\n\nconst fixLabelConfig: FixShapeConfig = {\n  shape: (shapes) => shapes.find((shape) => shape.parentElement?.className === 'label' && shape.className === 'text')!,\n  fields: ['fontSize', 'lineHeight'],\n};\n\nexport const demoAutosizeElementLabel: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node1', combo: 'combo1', style: { x: 100, y: 100 } },\n        { id: 'node2', style: { x: 300, y: 300 } },\n      ],\n      edges: [{ id: 'edge1', source: 'node1', target: 'node2' }],\n      combos: [{ id: 'combo1', label: mockText }],\n    },\n    node: {\n      style: {\n        labelMaxLines: 3,\n        labelMaxWidth: '200%',\n        labelText: mockText,\n        labelWordWrap: true,\n      },\n    },\n    edge: {\n      style: {\n        labelMaxLines: 2,\n        labelMaxWidth: '60%',\n        labelText: mockText,\n        labelWordWrap: true,\n      },\n    },\n    combo: {\n      style: {\n        labelMaxLines: 1,\n        labelMaxWidth: '200%',\n        labelText: mockText,\n        labelWordWrap: true,\n      },\n    },\n    behaviors: [\n      {\n        type: 'fix-element-size',\n        key: 'fix-element-size',\n        enable: true,\n        state: undefined,\n        node: [fixLabelConfig],\n        edge: [fixLabelConfig],\n        combo: [fixLabelConfig],\n      },\n      'zoom-canvas',\n      'drag-canvas',\n      'drag-element',\n    ],\n    autoFit: 'center',\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/demo-found-flow.ts",
    "content": "import { Rect as GRect, Text as GText } from '@antv/g';\nimport { Badge, CommonEvent, ExtensionCategory, Graph, Label, Rect, register, treeToGraphData } from '@antv/g6';\nimport data from '../dataset/decision-tree.json';\n\nconst COLORS = {\n  B: '#1783FF',\n  R: '#F46649',\n  Y: '#DB9D0D',\n  G: '#60C42D',\n  DI: '#A7A7A7',\n};\nconst GREY_COLOR = '#CED4D9';\n\nclass TreeNode extends Rect {\n  get data() {\n    return this.context.model.getNodeLikeDatum(this.id);\n  }\n\n  get childrenData() {\n    return this.context.model.getChildrenData(this.id);\n  }\n\n  getLabelStyle(attributes: any) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2 + 8,\n      y: -height / 2 + 16,\n      text: (this.data.name || '') as string,\n      fontSize: 12,\n      opacity: 0.85,\n      fill: '#000',\n      cursor: 'pointer' as const,\n    };\n  }\n\n  getPriceStyle(attributes: any) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2 + 8,\n      y: height / 2 - 8,\n      text: (this.data.label || '') as string,\n      fontSize: 16,\n      fill: '#000',\n      opacity: 0.85,\n    };\n  }\n\n  drawPriceShape(attributes: any, container: any) {\n    const priceStyle = this.getPriceStyle(attributes);\n    this.upsert('price', GText, priceStyle, container);\n  }\n\n  getCurrencyStyle(attributes: any) {\n    const [, height] = this.getSize(attributes);\n    return {\n      x: this.shapeMap['price'].getLocalBounds().max[0] + 4,\n      y: height / 2 - 8,\n      text: (this.data.currency || '') as string,\n      fontSize: 12,\n      fill: '#000',\n      opacity: 0.75,\n    };\n  }\n\n  drawCurrencyShape(attributes: any, container: any) {\n    const currencyStyle = this.getCurrencyStyle(attributes);\n    this.upsert('currency', GText, currencyStyle, container);\n  }\n\n  getPercentStyle(attributes: any) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: width / 2 - 4,\n      y: height / 2 - 8,\n      text: `${((Number(this.data.variableValue) || 0) * 100).toFixed(2)}%`,\n      fontSize: 12,\n      textAlign: 'right' as const,\n      fill: COLORS[this.data.status as keyof typeof COLORS],\n    };\n  }\n\n  drawPercentShape(attributes: any, container: any) {\n    const percentStyle = this.getPercentStyle(attributes);\n    this.upsert('percent', GText, percentStyle, container);\n  }\n\n  getTriangleStyle(attributes: any) {\n    const percentMinX = this.shapeMap['percent'].getLocalBounds().min[0];\n    const [, height] = this.getSize(attributes);\n    return {\n      fill: COLORS[this.data.status as keyof typeof COLORS],\n      x: this.data.variableUp ? percentMinX - 18 : percentMinX,\n      y: height / 2 - 16,\n      fontFamily: 'iconfont',\n      fontSize: 16,\n      text: '\\ue62d',\n      transform: this.data.variableUp ? ('' as any) : ('rotate(180deg)' as any),\n    };\n  }\n\n  drawTriangleShape(attributes: any, container: any) {\n    const triangleStyle = this.getTriangleStyle(attributes);\n    this.upsert('triangle', Label, triangleStyle, container);\n  }\n\n  getVariableStyle(attributes: any) {\n    const [, height] = this.getSize(attributes);\n    return {\n      fill: '#000',\n      fontSize: 12,\n      opacity: 0.45,\n      text: (this.data.variableName || '') as string,\n      textAlign: 'right' as const,\n      x: this.shapeMap['triangle'].getLocalBounds().min[0] - 4,\n      y: height / 2 - 8,\n    };\n  }\n\n  drawVariableShape(attributes: any, container: any) {\n    const variableStyle = this.getVariableStyle(attributes);\n    this.upsert('variable', GText, variableStyle, container);\n  }\n\n  getCollapseStyle(attributes: any) {\n    if (this.childrenData.length === 0) return false;\n    const { collapsed } = attributes;\n    const [width] = this.getSize(attributes);\n    return {\n      backgroundFill: '#fff',\n      backgroundHeight: 16,\n      backgroundLineWidth: 1,\n      backgroundRadius: 0,\n      backgroundStroke: GREY_COLOR,\n      backgroundWidth: 16,\n      cursor: 'pointer' as const,\n      fill: GREY_COLOR,\n      fontSize: 16,\n      text: collapsed ? '+' : '-',\n      textAlign: 'center' as const,\n      textBaseline: 'middle' as const,\n      x: width / 2,\n      y: 0,\n    };\n  }\n\n  drawCollapseShape(attributes: any, container: any) {\n    const collapseStyle = this.getCollapseStyle(attributes);\n    const btn = this.upsert('collapse', Badge, collapseStyle, container);\n\n    if (btn && !Reflect.has(btn, '__bind__')) {\n      Reflect.set(btn, '__bind__', true);\n      btn.addEventListener(CommonEvent.CLICK, () => {\n        const { collapsed } = this.attributes;\n        const graph = this.context.graph;\n        if (collapsed) graph.expandElement(this.id);\n        else graph.collapseElement(this.id);\n      });\n    }\n  }\n\n  getProcessBarStyle(attributes: any) {\n    const { rate, status } = this.data;\n    const { radius } = attributes;\n    const color = COLORS[status as keyof typeof COLORS];\n    const percent = `${Number(rate) * 100}%`;\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2,\n      y: height / 2 - 4,\n      width: width,\n      height: 4,\n      radius: [0, 0, radius, radius],\n      fill: `linear-gradient(to right, ${color} ${percent}, ${GREY_COLOR} ${percent})`,\n    };\n  }\n\n  drawProcessBarShape(attributes: any, container: any) {\n    const processBarStyle = this.getProcessBarStyle(attributes);\n    this.upsert('process-bar', GRect, processBarStyle, container);\n  }\n\n  getKeyStyle(attributes: any) {\n    const keyStyle = super.getKeyStyle(attributes);\n    return {\n      ...keyStyle,\n      fill: '#fff',\n      lineWidth: 1,\n      stroke: GREY_COLOR,\n    };\n  }\n\n  render(attributes = this.parsedAttributes, container?: any) {\n    super.render(attributes, container);\n\n    this.drawPriceShape(attributes, container);\n    this.drawCurrencyShape(attributes, container);\n    this.drawPercentShape(attributes, container);\n    this.drawTriangleShape(attributes, container);\n    this.drawVariableShape(attributes, container);\n    this.drawProcessBarShape(attributes, container);\n    this.drawCollapseShape(attributes, container);\n  }\n}\n\nregister(ExtensionCategory.NODE, 'tree-node', TreeNode);\n\nexport const demoFoundFlow: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data: treeToGraphData(data, {\n      getNodeData: (datum: any, depth: number) => {\n        if (!datum.style) datum.style = {};\n        datum.style.collapsed = depth >= 2;\n        if (!datum.children) return datum;\n        const { children, ...restDatum } = datum;\n        return { ...restDatum, children: children.map((child: any) => child.id) };\n      },\n    }),\n    node: {\n      type: 'tree-node',\n      style: {\n        size: [202, 60],\n        ports: [{ placement: 'left' }, { placement: 'right' }],\n        radius: 4,\n      },\n    },\n    edge: {\n      type: 'cubic-horizontal',\n      style: {\n        stroke: GREY_COLOR,\n      },\n    },\n    layout: {\n      type: 'indented',\n      direction: 'LR',\n      dropCap: false,\n      indent: 300,\n      getHeight: () => 60,\n      preLayout: false,\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/demo-supply-chains.ts",
    "content": "import type { EdgeData } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nconst urls: Record<string, string> = {\n  container: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ldqFQLTPIwwAAAAAAAAAAAAADmJ7AQ/original',\n  warehouse: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ldqFQLTPIwwAAAAAAAAAAAAADmJ7AQ/original',\n  factory: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*k2sASKsW0EQAAAAAAAAAAAAADmJ7AQ/original',\n  store: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Z3HuQbWRrncAAAAAAAAAAAAADmJ7AQ/original',\n};\n\nconst data = {\n  nodes: [\n    {\n      id: 'assembly-line-1',\n      data: {\n        type: 'factory',\n        name: 'Assembly Line 1',\n      },\n      combo: 'factory',\n      style: { x: 50, y: 150 },\n    },\n    {\n      id: 'assembly-line-2',\n      data: {\n        type: 'factory',\n        name: 'Assembly Line 2',\n      },\n      combo: 'factory',\n      style: { x: 50, y: 300 },\n    },\n    {\n      id: 'warehouse-1-container-1',\n      combo: 'warehouse-1',\n      data: {\n        type: 'container',\n        name: 'Container 1',\n      },\n      style: { x: 275, y: 175 },\n    },\n    {\n      id: 'warehouse-1-container-2',\n      combo: 'warehouse-1',\n      data: {\n        type: 'container',\n        name: 'Container 2',\n      },\n      style: { x: 275, y: 115 },\n    },\n    {\n      id: 'warehouse-1-container-3',\n      combo: 'warehouse-1',\n      data: {\n        type: 'container',\n        name: 'Container 3',\n      },\n      style: { x: 275, y: 55 },\n    },\n    {\n      id: 'warehouse-2-container-1',\n      combo: 'warehouse-2',\n      data: {\n        type: 'container',\n        name: 'Container 1',\n      },\n      style: { x: 275, y: 255 },\n    },\n    {\n      id: 'warehouse-2-container-2',\n      combo: 'warehouse-2',\n      data: {\n        type: 'container',\n        name: 'Container 2',\n      },\n      style: { x: 275, y: 315 },\n    },\n    {\n      id: 'warehouse-2-container-3',\n      combo: 'warehouse-2',\n      data: {\n        type: 'container',\n        name: 'Container 3',\n      },\n      style: { x: 275, y: 370 },\n    },\n    {\n      id: 'warehouse-2-container-4',\n      combo: 'warehouse-2',\n      data: {\n        type: 'container',\n        name: 'Container 4',\n      },\n      style: { x: 275, y: 430 },\n    },\n    {\n      id: 'store',\n      data: {\n        type: 'store',\n        name: 'Store',\n      },\n      style: { x: 500, y: 225 },\n    },\n  ],\n  edges: [\n    {\n      id: 'g15',\n      source: 'assembly-line-1',\n      target: 'warehouse-1-container-1',\n      data: { transportation: 'airplane' },\n    },\n    {\n      id: 'g16',\n      source: 'assembly-line-1',\n      target: 'warehouse-1-container-2',\n      data: { transportation: 'truck' },\n    },\n    {\n      id: 'g17',\n      source: 'assembly-line-1',\n      target: 'warehouse-1-container-3',\n      data: { transportation: 'truck' },\n    },\n    {\n      id: 'g18',\n      source: 'assembly-line-2',\n      target: 'warehouse-2-container-1',\n      data: { transportation: 'train' },\n    },\n    {\n      id: 'g19',\n      source: 'assembly-line-2',\n      target: 'warehouse-2-container-2',\n      data: { transportation: 'train' },\n    },\n    {\n      id: 'g20',\n      source: 'assembly-line-2',\n      target: 'warehouse-2-container-3',\n      data: { transportation: 'truck' },\n    },\n    {\n      id: 'g21',\n      source: 'assembly-line-2',\n      target: 'warehouse-2-container-4',\n      data: { transportation: 'truck' },\n    },\n    {\n      id: 'g22',\n      source: 'warehouse-1-container-1',\n      target: 'store',\n      data: { transportation: 'truck' },\n    },\n    {\n      id: 'g23',\n      source: 'warehouse-1-container-2',\n      target: 'store',\n      data: { transportation: 'train' },\n    },\n    {\n      id: 'g24',\n      source: 'warehouse-1-container-3',\n      target: 'store',\n      data: { transportation: 'train' },\n    },\n    {\n      id: 'g25',\n      source: 'warehouse-2-container-1',\n      target: 'store',\n      data: { transportation: 'train' },\n    },\n    {\n      id: 'g26',\n      source: 'warehouse-2-container-2',\n      target: 'store',\n      data: { transportation: 'train' },\n    },\n    {\n      id: 'g27',\n      source: 'warehouse-2-container-3',\n      target: 'store',\n      data: { transportation: 'train' },\n    },\n    {\n      id: 'g28',\n      source: 'warehouse-2-container-4',\n      target: 'store',\n      data: { transportation: 'train' },\n    },\n  ],\n  combos: [\n    {\n      id: 'factory',\n      data: {\n        type: 'factory',\n        name: 'Factory',\n      },\n    },\n    {\n      id: 'warehouse-1',\n      combo: 'warehouse',\n      data: {\n        type: 'warehouse',\n        name: 'Warehouse 1',\n      },\n    },\n    {\n      id: 'warehouse-2',\n      combo: 'warehouse',\n      data: {\n        type: 'warehouse',\n        name: 'Warehouse 2',\n      },\n    },\n    {\n      id: 'warehouse',\n      data: {\n        type: 'warehouse',\n        name: 'Warehouses',\n      },\n    },\n  ],\n};\n\nexport const demoSupplyChains: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: (datum) => ({\n        labelText: datum.data!.name,\n        labelFill: '#fff',\n        labelBackground: true,\n        labelBackgroundFill: '#00C9C9',\n        labelBackgroundOpacity: 1,\n        labelBackgroundRadius: 10,\n        labelPadding: [-3, 5],\n        labelFontSize: 10,\n        iconSrc: urls[datum.data?.type as string],\n        iconWidth: 24,\n        iconHeight: 24,\n        size: 30,\n        fill: 'transparent',\n      }),\n    },\n    edge: {\n      style: (datum) => {\n        const index = ['airplane', 'truck', 'train'].indexOf(datum.data?.transportation as string);\n        const color = ['#5B8FF9', '#61DDAA', '#F6903D'][index];\n        return {\n          stroke: color,\n          labelText: datum.data!.transportation,\n          labelFill: '#fff',\n          labelBackground: true,\n          labelBackgroundFill: color,\n          labelBackgroundOpacity: 1,\n          labelBackgroundRadius: 10,\n          labelPadding: [-3, 5],\n          labelFontSize: 8,\n          lineDash: 0,\n          ...datum.style,\n        };\n      },\n    },\n    combo: {\n      type: 'rect',\n      style: (datum) => ({\n        collapsedMarker: true,\n        collapsedMarkerHeight: 32,\n        collapsedMarkerSrc: urls[datum.data?.type as string],\n        collapsedMarkerWidth: 32,\n        fill: datum.style?.collapsed ? ' transparent' : '#99add1',\n        fillOpacity: 0.05,\n        labelBackground: true,\n        labelBackgroundFill: '#00C9C9',\n        labelBackgroundOpacity: 1,\n        labelBackgroundRadius: 10,\n        labelFill: '#fff',\n        labelFontSize: 10,\n        labelPadding: [-3, 5],\n        labelText: datum.data!.name,\n        stroke: datum.style?.collapsed ? ' transparent' : '#99add1',\n        badge: !!datum.style?.collapsed,\n        badges: [\n          {\n            placement: 'top-right',\n            backgroundFill: '#FF7474',\n            padding: [0, 4],\n            text: '可展开',\n            fontSize: 6,\n            fill: '#fff',\n            offsetX: -10,\n          },\n        ],\n      }),\n      state: {\n        active: {\n          halo: false,\n          stroke: '#5AD8A6',\n          lineWidth: 2,\n        },\n      },\n    },\n    behaviors: ['collapse-expand', 'drag-element'],\n    transforms: [\n      {\n        key: 'process-parallel-edges',\n        type: 'process-parallel-edges',\n        distance: 20,\n        style: (edges: EdgeData[]) => ({\n          stroke: '#99add1',\n          lineWidth: 3,\n          lineDash: [2, 2],\n          labelText: edges.length.toString(),\n          labelBackgroundFill: '#99add1',\n        }),\n      },\n    ],\n  });\n\n  graph.render();\n\n  demoSupplyChains.form = (panel) => {\n    const config = {\n      mode: 'bundle',\n    };\n    return [\n      panel\n        .add(config, 'Parallel Edge Mode', ['bundle', 'merge'])\n        .name('node-1 type')\n        .onChange((value: string) => {\n          graph.updateTransform({\n            key: 'process-parallel-edges',\n            mode: value,\n          });\n          graph.render();\n        }),\n    ];\n  };\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-change-type.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementChangeType: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', type: 'rect', style: { x: 100, y: 100, fill: 'transparent', stroke: '#1783ff' } },\n        { id: 'node-2', style: { x: 200, y: 100 } },\n      ],\n      edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n    },\n  });\n\n  await graph.render();\n\n  elementChangeType.form = (panel) => {\n    const config = {\n      node1: 'rect',\n      node2: 'circle',\n    };\n    const options = { Circle: 'circle', Rect: 'rect', Diamond: 'diamond', Star: 'star' };\n\n    const changeType = (id: string, type: string) => {\n      graph.updateNodeData([{ id, type }]);\n      graph.draw();\n    };\n\n    return [\n      panel\n        .add(config, 'node1', options)\n        .name('node-1 type')\n        .onChange((value: string) => changeType('node-1', value)),\n      panel\n        .add(config, 'node2', options)\n        .name('node-2 type')\n        .onChange((value: string) => changeType('node-2', value)),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-combo.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementCombo: TestCase = async (context) => {\n  const data = {\n    nodes: [\n      { id: 'node-1', combo: 'combo-2', style: { x: 120, y: 100 } },\n      { id: 'node-2', combo: 'combo-1', style: { x: 300, y: 200 } },\n      { id: 'node-3', combo: 'combo-1', style: { x: 200, y: 300 } },\n    ],\n    edges: [\n      { id: 'edge-1', source: 'node-1', target: 'node-2' },\n      { id: 'edge-2', source: 'node-2', target: 'node-3' },\n    ],\n    combos: [\n      {\n        id: 'combo-1',\n        type: 'rect',\n        combo: 'combo-2',\n      },\n      {\n        id: 'combo-2',\n      },\n    ],\n  };\n\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    combo: {\n      style: {\n        labelText: (d) => d.id,\n        lineDash: 0,\n        collapsedLineDash: [5, 5],\n      },\n    },\n    behaviors: ['drag-element'],\n  });\n\n  await graph.render();\n\n  const COMBO_TYPE = ['circle', 'rect'];\n  const COLLAPSED_MARKER_TYPE = ['child-count', 'descendant-count', 'node-count', 'custom'];\n\n  elementCombo.form = (panel) => {\n    const config: Record<string, any> = {\n      combo2Type: 'circle',\n      collapsedMarker: true,\n      collapsedMarkerType: 'child-count',\n      collapseCombo1: () => {\n        graph.collapseElement('combo-1');\n      },\n      expandCombo1: () => {\n        graph.expandElement('combo-1');\n      },\n      collapseCombo2: () => {\n        graph.collapseElement('combo-2');\n      },\n      expandCombo2: () => {\n        graph.expandElement('combo-2');\n      },\n      addRemoveNode: async () => {\n        const node4 = graph.getNodeData('node-4');\n        if (node4) {\n          graph.removeNodeData(['node-4']);\n        } else {\n          graph.addNodeData([\n            {\n              id: 'node-4',\n              combo: 'combo-2',\n              style: { x: 100, y: 200, fill: 'pink' },\n            },\n          ]);\n        }\n        panels.at(-1)?.disable();\n        await graph.render();\n        panels.at(-1)?.enable();\n      },\n    };\n\n    const panels = [\n      panel.add(config, 'combo2Type', COMBO_TYPE).onChange((type: string) => {\n        config.combo2Type = type;\n        const combo2Data = graph.getComboData('combo-2');\n        graph.updateComboData([{ ...combo2Data, style: { ...combo2Data.style, type: config.combo2Type } }]);\n        graph.render();\n      }),\n      panel.add(config, 'collapsedMarker'),\n      panel.add(config, 'collapsedMarkerType', COLLAPSED_MARKER_TYPE),\n      panel.add(config, 'collapseCombo1'),\n      panel.add(config, 'expandCombo1'),\n      panel.add(config, 'collapseCombo2'),\n      panel.add(config, 'expandCombo2'),\n      panel.add(config, 'addRemoveNode'),\n    ];\n\n    return panels;\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-arrow.ts",
    "content": "import { idOf } from '@/src/utils/id';\nimport { Graph } from '@antv/g6';\n\nexport const elementEdgeArrow: TestCase = async (context) => {\n  const edgeIds = [\n    'default-arrow',\n    'triangle-arrow',\n    'simple-arrow',\n    'vee-arrow',\n    'circle-arrow',\n    'rect-arrow',\n    'diamond-arrow',\n    'triangleRect-arrow',\n  ];\n\n  const data = {\n    nodes: new Array(16).fill(0).map((_, i) => ({ id: `node${i + 1}` })),\n    edges: edgeIds.map((id, i) => ({\n      id,\n      source: `node${i * 2 + 1}`,\n      target: `node${i * 2 + 2}`,\n    })),\n  };\n\n  const graph = new Graph({\n    ...context,\n    data,\n    edge: {\n      type: 'line', // 👈🏻 Edge shape type.\n      style: {\n        labelText: (d) => d.id!,\n        labelBackground: true,\n        endArrow: true,\n        endArrowType: (d: any) => idOf(d).toString().split('-')[0] as any,\n      },\n    },\n    behaviors: ['drag-element'],\n    layout: {\n      type: 'grid',\n      cols: 2,\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-cubic-horizontal.ts",
    "content": "import data from '@@/dataset/element-edges.json';\nimport { Graph } from '@antv/g6';\n\nexport const elementEdgeCubicHorizontal: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        port: true,\n        ports: [{ placement: 'left' }, { placement: 'right' }],\n      },\n    },\n    edge: {\n      type: 'cubic-horizontal', // 👈🏻 Edge shape type.\n      style: {\n        labelText: (d) => d.id!,\n        labelBackground: true,\n        endArrow: true,\n      },\n    },\n    layout: {\n      type: 'antv-dagre',\n      begin: [50, 50],\n      rankdir: 'LR',\n      nodesep: 30,\n      ranksep: 150,\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-cubic-radial.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const elementEdgeCubicRadial: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    edge: {\n      type: 'cubic-radial',\n    },\n    layout: {\n      type: 'dendrogram',\n      radial: true,\n      nodeSep: 30,\n      rankSep: 200,\n      preLayout: true,\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-cubic-vertical.ts",
    "content": "import data from '@@/dataset/element-edges.json';\nimport { Graph } from '@antv/g6';\n\nexport const elementEdgeCubicVertical: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        port: true,\n        ports: [{ placement: 'top' }, { placement: 'bottom' }],\n      },\n    },\n    edge: {\n      type: 'cubic-vertical', // 👈🏻 Edge shape type.\n      style: {\n        labelText: (d) => d.id!,\n        labelBackground: true,\n        endArrow: true,\n      },\n    },\n    layout: {\n      type: 'antv-dagre',\n      begin: [50, 50],\n      rankdir: 'TB',\n      nodesep: 25,\n      ranksep: 150,\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-cubic.ts",
    "content": "import data from '@@/dataset/element-edges.json';\nimport { Graph } from '@antv/g6';\n\nexport const elementEdgeCubic: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    edge: {\n      type: 'cubic', // 👈🏻 Edge shape type.\n      style: {\n        labelText: (d) => d.id!,\n        labelBackground: true,\n        endArrow: true,\n      },\n    },\n    layout: {\n      type: 'radial',\n      unitRadius: 220,\n      linkDistance: 220,\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-custom-arrow.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementEdgeCustomArrow: TestCase = async (context) => {\n  const data = {\n    nodes: new Array(6).fill(0).map((_, i) => ({ id: `node${i + 1}` })),\n    edges: [\n      {\n        id: 'custom-arrow-1',\n        source: 'node1',\n        target: 'node2',\n        style: {\n          endArrowD: 'M-14,0 L-4,-4 L0,-14 L4,-4 L14,0 L4,4 L0,14 L-4,4 Z',\n          endArrowOffset: 14,\n        },\n      },\n      {\n        id: 'custom-arrow-2',\n        source: 'node3',\n        target: 'node4',\n        style: {\n          endArrowD: 'M -6,-5 L -6,5 L 6,10 L 6,-10 Z',\n          endArrowOffset: 20,\n        },\n      },\n      {\n        id: 'image-arrow',\n        source: 'node5',\n        target: 'node6',\n        style: {\n          endArrowSrc: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*N4ZMS7gHsUIAAAAAAAAAAABkARQnAQ',\n          endArrowSize: 28,\n          endArrowTransform: [['rotate', 90]],\n          endArrowX: -14,\n          endArrowY: -14,\n        },\n      },\n    ],\n  };\n\n  const graph = new Graph({\n    ...context,\n    data,\n    edge: {\n      type: 'line', // 👈🏻 Edge shape type.\n      style: {\n        stroke: '#F6BD16',\n        labelText: (d) => d.id!,\n        labelBackground: true,\n        endArrow: true,\n      },\n    },\n    layout: {\n      type: 'grid',\n      cols: 2,\n    },\n    behaviors: ['drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-line.ts",
    "content": "import data from '@@/dataset/element-edges.json';\nimport { Graph } from '@antv/g6';\n\nexport const elementEdgeLine: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    edge: {\n      type: 'line', // 👈🏻 Edge shape type.\n      style: {\n        labelText: (d) => d.id!,\n        labelBackground: true,\n        endArrow: true,\n        badge: true,\n        badgeText: '\\ue603',\n        badgeFontFamily: 'iconfont',\n        badgeBackgroundWidth: 12,\n        badgeBackgroundHeight: 12,\n      },\n    },\n    layout: {\n      type: 'radial',\n      unitRadius: 220,\n      linkDistance: 220,\n      preLayout: false,\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-loop-curve.ts",
    "content": "import { idOf } from '@/src/utils/id';\nimport { Graph } from '@antv/g6';\n\nexport const elementEdgeLoopCurve: TestCase = async (context) => {\n  const data = {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3-ports' }, { id: 'node4-ports' }],\n    edges: [\n      {\n        id: 'loop-1',\n        source: 'node1',\n        target: 'node1',\n        style: { placement: 'top' },\n      },\n      {\n        id: 'loop-2',\n        source: 'node1',\n        target: 'node1',\n        style: { placement: 'right' },\n      },\n      {\n        id: 'loop-3',\n        source: 'node1',\n        target: 'node1',\n        style: { placement: 'bottom' },\n      },\n      {\n        id: 'loop-4',\n        source: 'node1',\n        target: 'node1',\n        style: { placement: 'left' },\n      },\n      {\n        id: 'loop-5',\n        source: 'node2',\n        target: 'node2',\n        style: { placement: 'top-right' },\n      },\n      {\n        id: 'loop-6',\n        source: 'node2',\n        target: 'node2',\n        style: { placement: 'bottom-right' },\n      },\n      {\n        id: 'loop-7',\n        source: 'node2',\n        target: 'node2',\n        style: { placement: 'bottom-left' },\n      },\n      {\n        id: 'loop-8',\n        source: 'node2',\n        target: 'node2',\n        style: { placement: 'top-left' },\n      },\n      {\n        id: 'loop-9',\n        source: 'node3-ports',\n        target: 'node3-ports',\n        style: {\n          sourcePort: 'port-top',\n          targetPort: 'port-right',\n        },\n      },\n      {\n        id: 'loop-10',\n        source: 'node4-ports',\n        target: 'node4-ports',\n        style: {\n          sourcePort: 'port-right',\n          targetPort: 'port-right',\n        },\n      },\n    ],\n  };\n\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'rect',\n      style: {\n        size: [80, 30],\n        labelBackground: true,\n        port: (d) => idOf(d).toString().includes('ports'),\n        portR: 3,\n        ports: [\n          {\n            key: 'port-top',\n            placement: [0.7, 0],\n          },\n          {\n            key: 'port-right',\n            placement: 'right',\n          },\n        ],\n      },\n    },\n    edge: {\n      type: 'line',\n      style: {\n        endArrow: true,\n        loopPlacement: (d) => d.style!.placement,\n      },\n    },\n    layout: {\n      type: 'grid',\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-loop-polyline.ts",
    "content": "import { idOf } from '@/src/utils/id';\nimport { Graph } from '@antv/g6';\n\nexport const elementEdgeLoopPolyline: TestCase = async (context) => {\n  const data = {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3-ports' }, { id: 'node4-ports' }],\n    edges: [\n      {\n        id: 'loop-1',\n        source: 'node1',\n        target: 'node1',\n        style: { placement: 'top' },\n      },\n      {\n        id: 'loop-2',\n        source: 'node1',\n        target: 'node1',\n        style: { placement: 'right' },\n      },\n      {\n        id: 'loop-3',\n        source: 'node1',\n        target: 'node1',\n        style: { placement: 'bottom' },\n      },\n      {\n        id: 'loop-4',\n        source: 'node1',\n        target: 'node1',\n        style: { placement: 'left' },\n      },\n      {\n        id: 'loop-5',\n        source: 'node2',\n        target: 'node2',\n        style: { placement: 'top-right' },\n      },\n      {\n        id: 'loop-6',\n        source: 'node2',\n        target: 'node2',\n        style: { placement: 'bottom-right' },\n      },\n      {\n        id: 'loop-7',\n        source: 'node2',\n        target: 'node2',\n        style: { placement: 'bottom-left' },\n      },\n      {\n        id: 'loop-8',\n        source: 'node2',\n        target: 'node2',\n        style: { placement: 'top-left' },\n      },\n      {\n        id: 'loop-9',\n        source: 'node3-ports',\n        target: 'node3-ports',\n        style: {\n          sourcePort: 'port-top',\n          targetPort: 'port-right',\n        },\n      },\n      {\n        id: 'loop-10',\n        source: 'node4-ports',\n        target: 'node4-ports',\n        style: {\n          sourcePort: 'port-right',\n          targetPort: 'port-right',\n        },\n      },\n    ],\n  };\n\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'rect',\n      style: {\n        size: [80, 30],\n        port: (d) => idOf(d).toString().includes('ports'),\n        portR: 3,\n        ports: [\n          {\n            key: 'port-top',\n            placement: [0.7, 0],\n          },\n          {\n            key: 'port-right',\n            placement: 'right',\n          },\n        ],\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        endArrow: true,\n        loopPlacement: (d) => d.style!.placement,\n      },\n    },\n    layout: {\n      type: 'grid',\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-polyline-animation.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementEdgePolylineAnimation: TestCase = async (context) => {\n  const data = {\n    nodes: [\n      { id: 'node-1', style: { x: 200, y: 200 } },\n      { id: 'node-2', style: { x: 350, y: 120 } },\n    ],\n    edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n  };\n\n  const graph = new Graph({\n    ...context,\n    data,\n    edge: {\n      type: 'polyline',\n    },\n    behaviors: [{ type: 'drag-element' }],\n  });\n\n  await graph.render();\n\n  elementEdgePolylineAnimation.form = (panel) => {\n    const config = {\n      radius: 0,\n      enableRouter: false,\n      controlPoints: false,\n    };\n    const updateEdgeStyle = (id: string, attr: string, value: any) => {\n      graph.updateEdgeData((prev) => {\n        const edgeData = prev.find((edge: any) => edge.id === id)!;\n        return [\n          ...prev.filter((edge: any) => edge.id !== id),\n          {\n            ...edgeData,\n            style: {\n              ...edgeData?.style,\n              [attr]: value,\n            },\n          },\n        ];\n      });\n      graph.render();\n    };\n    return [\n      panel.add(config, 'radius', 0, 100, 1).onChange((val: number) => {\n        updateEdgeStyle('edge-1', 'radius', val);\n      }),\n      panel.add(config, 'enableRouter').onChange((val: boolean) => {\n        updateEdgeStyle('edge-1', 'router', val);\n      }),\n      panel.add(config, 'controlPoints').onChange((val: boolean) => {\n        updateEdgeStyle('edge-1', 'controlPoints', val ? [[300, 190]] : []);\n      }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-polyline-astar.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementEdgePolylineAstar: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 100, y: 100 } },\n        { id: 'node-2', style: { x: 100, y: 200 } },\n        { id: 'node-3', style: { x: 100, y: 150 } },\n      ],\n      edges: [\n        {\n          id: 'edge-1',\n          source: 'node-1',\n          target: 'node-2',\n          style: {\n            router: {\n              type: 'shortest-path',\n              offset: 0,\n              enableObstacleAvoidance: true,\n            },\n          },\n        },\n      ],\n    },\n    node: {\n      type: 'rect',\n      style: {\n        size: 25,\n        fill: '#f8f8f8',\n        stroke: '#8b9baf',\n        lineWidth: 1,\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        lineWidth: 1,\n      },\n    },\n    behaviors: ['drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-polyline-orth.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementEdgePolylineOrth: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: '0' },\n        { id: '1' },\n        { id: '2' },\n        { id: '3' },\n        { id: '4' },\n        { id: '5' },\n        { id: '6' },\n        { id: '7' },\n        { id: '8' },\n        { id: '9' },\n      ],\n      edges: [\n        { source: '0', target: '1' },\n        { source: '0', target: '2' },\n        { source: '1', target: '4' },\n        { source: '0', target: '3' },\n        { source: '3', target: '4' },\n        { source: '4', target: '5' },\n        { source: '4', target: '6' },\n        { source: '5', target: '7' },\n        { source: '5', target: '8' },\n        { source: '8', target: '9' },\n        { source: '2', target: '9' },\n        { source: '3', target: '9' },\n      ],\n    },\n    layout: {\n      type: 'antv-dagre',\n      nodesep: 50,\n      ranksep: 20,\n      controlPoints: true,\n    },\n    autoFit: 'view',\n    node: {\n      type: 'rect',\n      style: {\n        size: [60, 30],\n        radius: 8,\n        ports: [{ placement: 'top' }, { placement: 'bottom' }],\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        endArrow: true,\n        endArrowSize: 8,\n        lineWidth: 2,\n        radius: 10,\n        router: {\n          type: 'orth',\n        },\n      },\n    },\n    animation: false,\n    behaviors: ['drag-canvas', 'zoom-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-polyline.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementEdgePolyline: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-0', style: { x: 50, y: 40, labelText: 'Loop' } },\n        {\n          id: 'node-0-1',\n          style: {\n            x: 150,\n            y: 40,\n            labelText: 'Loop',\n            port: true,\n            ports: [\n              { key: 'top', placement: [0, 0.5], r: 2, fill: '#31d0c6' },\n              {\n                key: 'left',\n                placement: [0.5, 0],\n                r: 2,\n                fill: '#31d0c6',\n              },\n            ],\n          },\n        },\n        {\n          id: 'node-0-2',\n          style: {\n            x: 250,\n            y: 40,\n            labelText: 'Loop',\n            port: true,\n            ports: [{ key: 'top', placement: [0.5, 0], r: 2, fill: '#31d0c6' }],\n          },\n        },\n        {\n          id: 'node-1',\n          style: { x: 50, y: 120, labelText: '1' },\n        },\n        {\n          id: 'node-2',\n          style: { x: 150, y: 75, labelText: '2' },\n        },\n        {\n          id: 'node-3',\n          style: { x: 50, y: 220, labelText: '3' },\n        },\n        {\n          id: 'node-4',\n          style: { x: 150, y: 175, labelText: '4' },\n        },\n        {\n          id: 'control-point-1',\n          type: 'circle',\n          style: { x: 100, y: 175, size: 4 },\n        },\n        {\n          id: 'node-5',\n          style: { x: 50, y: 320, labelText: '5' },\n        },\n        {\n          id: 'node-6',\n          style: { x: 150, y: 275, labelText: '6' },\n        },\n        {\n          id: 'control-point-2',\n          type: 'circle',\n          style: { x: 100, y: 300, size: 4 },\n        },\n        {\n          id: 'node-7',\n          style: { x: 50, y: 420, labelText: '7', ports: [{ key: 'top', placement: [0.3, 0], r: 2, fill: '#31d0c6' }] },\n        },\n        {\n          id: 'node-8',\n          style: { x: 150, y: 375, labelText: '8' },\n        },\n        {\n          id: 'node-9',\n          style: { x: 250, y: 420, labelText: '9' },\n        },\n        {\n          id: 'node-10',\n          style: { x: 350, y: 375, labelText: '10', size: 50 },\n        },\n        {\n          id: 'control-point-3',\n          type: 'circle',\n          style: { x: 340, y: 390, size: 4 },\n        },\n      ],\n      edges: [\n        {\n          id: 'edge-1',\n          source: 'node-0',\n          target: 'node-0',\n          style: { loopPlacement: 'top' },\n        },\n        {\n          id: 'edge-2',\n          source: 'node-0',\n          target: 'node-0',\n          style: { loopPlacement: 'bottom-right' },\n        },\n        {\n          source: 'node-0-1',\n          target: 'node-0-1',\n          style: { sourcePort: 'top', targetPort: 'left', loopPlacement: 'bottom-left' },\n        },\n        {\n          source: 'node-0-2',\n          target: 'node-0-2',\n          style: { sourcePort: 'top' },\n        },\n        {\n          source: 'node-1',\n          target: 'node-2',\n          style: { router: { type: 'orth' } },\n        },\n        {\n          source: 'node-3',\n          target: 'node-4',\n          style: { router: false, controlPoints: [[100, 175]] },\n        },\n        {\n          source: 'node-5',\n          target: 'node-6',\n          style: { router: { type: 'orth' }, controlPoints: [[100, 300]] },\n        },\n        {\n          source: 'node-7',\n          target: 'node-8',\n          style: { router: { type: 'orth' } },\n        },\n        {\n          source: 'node-9',\n          target: 'node-10',\n          style: { router: { type: 'orth' }, controlPoints: [[340, 390]] },\n        },\n      ],\n    },\n    node: {\n      type: (d) => d.type || 'rect',\n      style: {\n        size: (d) => d.style?.size || [50, 20],\n        fill: '#f8f8f8',\n        stroke: '#8b9baf',\n        lineWidth: 1,\n        labelPlacement: 'center',\n        labelFill: '#8b9baf',\n        portLineWidth: 0,\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        stroke: '#1890FF',\n      },\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-port.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementEdgePort: TestCase = async (context) => {\n  const nodes: Record<string, any> = {\n    'node-2': {\n      ports: [\n        { key: 'left', placement: [0, 0.5], r: 4, stroke: '#31d0c6', fill: '#fff' },\n        { key: 'right', placement: [1, 0.5], r: 4, fill: '#31d0c6' },\n        { key: 'top', placement: [0.5, 0], r: 4, stroke: '#31d0c6', fill: '#fff' },\n        { key: 'bottom', placement: [0.5, 1], r: 4, stroke: '#31d0c6', fill: '#fff' },\n      ],\n    },\n    'node-3': {\n      ports: [\n        { key: 'left', placement: [0, 0.5], r: 4, stroke: '#31d0c6', fill: '#fff' },\n        { key: 'right', placement: [1, 0.5], r: 4, stroke: '#31d0c6', fill: '#fff' },\n        { key: 'top', placement: [0.5, 0], r: 4, stroke: '#31d0c6', fill: '#fff' },\n        { key: 'bottom', placement: [0.5, 1], r: 4, fill: '#31d0c6' },\n      ],\n    },\n    'node-4': {\n      ports: [\n        { key: 'left', placement: [0, 0.5], r: 4, stroke: '#31d0c6', fill: '#fff' },\n        { key: 'right', placement: [1, 0.5], r: 4, stroke: '#31d0c6', fill: '#fff' },\n      ],\n    },\n    'node-5': {\n      ports: [\n        { key: 'top', placement: [0.5, 0], r: 4, stroke: '#31d0c6', fill: '#fff' },\n        { key: 'bottom', placement: [0.5, 1], r: 4, stroke: '#31d0c6', fill: '#fff' },\n      ],\n    },\n    'node-6': {\n      ports: [\n        { key: 'left', placement: [0, 0.5], r: 4, stroke: '#31d0c6', fill: '#fff' },\n        { key: 'right', placement: [1, 0.5], r: 4, stroke: '#31d0c6', fill: '#fff' },\n      ],\n    },\n    'node-7': {\n      ports: [\n        { key: 'top', placement: [0.5, 0], r: 4, stroke: '#31d0c6', fill: '#fff' },\n        { key: 'bottom', placement: [0.5, 1], r: 4, fill: '#31d0c6' },\n      ],\n    },\n    'node-8': {\n      ports: [\n        { key: 'left', placement: [0, 0.5], r: 4, fill: '#31d0c6' },\n        { key: 'right', placement: [1, 0.5], r: 4, stroke: '#31d0c6', fill: '#fff' },\n      ],\n    },\n    'node-9': {\n      ports: [\n        { key: 'top', placement: [0.5, 0], r: 4, stroke: '#31d0c6', fill: '#fff' },\n        { key: 'bottom', placement: [0.5, 1], r: 4, stroke: '#31d0c6', fill: '#fff' },\n      ],\n    },\n    'node-11': {\n      ports: [\n        { key: 'bottom', placement: [0.5, 1], r: 4, stroke: '#31d0c6', fill: '#fff' },\n        { key: 'right', placement: [1, 0.5], r: 4, stroke: '#31d0c6', fill: '#fff' },\n      ],\n    },\n    'node-13': {\n      ports: [\n        { key: 'bottom', placement: [0.5, 1], r: 4, stroke: '#31d0c6', fill: '#fff' },\n        { key: 'right', placement: [1, 0.5], r: 4, fill: '#31d0c6' },\n      ],\n    },\n    'node-14': {\n      ports: [\n        { key: 'bottom', placement: [0.5, 1], r: 4, stroke: '#31d0c6', fill: '#fff' },\n        { key: 'right', placement: [1, 0.5], r: 4, stroke: '#31d0c6', fill: '#fff' },\n      ],\n    },\n    'node-16': {\n      ports: [\n        { key: 'bottom', placement: [0.5, 1], r: 4, fill: '#31d0c6' },\n        { key: 'right', placement: [1, 0.5], r: 4, stroke: '#31d0c6', fill: '#fff' },\n      ],\n    },\n    'node-18': {\n      ports: [\n        { key: 'bottom', placement: [0.5, 1], r: 4, fill: '#31d0c6' },\n        { key: 'right', placement: [1, 0.5], r: 4, stroke: '#31d0c6', fill: '#fff' },\n      ],\n    },\n  };\n\n  const edges: Record<string, any> = {\n    'edge-0': {\n      labelText: 'sourcePort❓ targetPort❓',\n    },\n    'edge-1': {\n      sourcePort: 'right',\n      targetPort: 'bottom',\n      labelText: 'sourcePort✅ targetPort✅',\n    },\n    'edge-2': {\n      labelText: 'sourcePort✖️ targetPort✖️',\n    },\n    'edge-3': {\n      targetPort: 'bottom',\n      labelText: 'sourcePort✖️ targetPort✅',\n    },\n    'edge-4': {\n      sourcePort: 'left',\n      labelText: 'sourcePort✅ targetPort✖️',\n    },\n    'edge-5': {\n      labelText: 'sourcePort❓ targetPort✖️',\n    },\n    'edge-6': {\n      targetPort: 'right',\n      labelText: 'sourcePort❓ targetPort✅',\n    },\n    'edge-7': {\n      labelText: 'sourcePort✖️ targetPort❓',\n    },\n    'edge-8': {\n      sourcePort: 'bottom',\n      labelText: 'sourcePort✅ targetPort❓',\n    },\n    'edge-9': {\n      sourcePort: 'bottom',\n      labelText: 'sourcePort✅ targetPort❓',\n    },\n  };\n\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: Array.from({ length: 20 }).map((_, i) => ({\n        id: `node-${i}`,\n        data: { index: i },\n        type: i === 19 ? 'star' : 'circle',\n        style: nodes[`node-${i}`] || {},\n      })),\n      edges: Array.from({ length: 10 }).map((_, i) => ({\n        id: `edge-${i}`,\n        source: `node-${i * 2}`,\n        target: `node-${i * 2 + 1}`,\n        type: i === 9 ? 'cubic' : 'line',\n        style: edges[`edge-${i}`] || {},\n      })),\n    },\n    node: {\n      style: {\n        x: (d) => [50, 200, 300, 450][(d.data!.index as number) % 4],\n        y: (d) => 50 + Math.floor((d.data!.index as number) / 4) * 100,\n        size: 50,\n        fill: '#f8f8f8',\n        stroke: '#8b9baf',\n        lineWidth: 1,\n      },\n    },\n    edge: {\n      style: {\n        stroke: '#1890FF',\n        lineWidth: 2,\n        labelFontSize: 12,\n        labelMaxLines: 2,\n        labelWordWrap: true,\n        labelWordWrapWidth: 100,\n        endArrow: true,\n      },\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-quadratic.ts",
    "content": "import data from '@@/dataset/element-edges.json';\nimport { Graph } from '@antv/g6';\n\nexport const elementEdgeQuadratic: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    edge: {\n      type: 'quadratic', // 👈🏻 Edge shape type.\n      style: {\n        labelText: (d) => d.id!,\n        labelBackground: true,\n        endArrow: true,\n      },\n    },\n    layout: {\n      type: 'radial',\n      unitRadius: 220,\n      linkDistance: 220,\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-edge-size.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementEdgeSize: TestCase = async (context) => {\n  const data = {\n    nodes: new Array(14).fill(0).map((_, i) => ({ id: `node${i + 1}` })),\n    edges: [1, 2, 4, 6, 8, 10, 12].map((lineWidth, i) => ({\n      id: `edge-${i}`,\n      source: `node${i * 2 + 1}`,\n      target: `node${i * 2 + 2}`,\n      style: { lineWidth },\n    })),\n  };\n\n  const graph = new Graph({\n    ...context,\n    data,\n    edge: {\n      type: 'line', // 👈🏻 Edge shape type.\n      style: { endArrow: true },\n    },\n    layout: {\n      type: 'grid',\n      cols: 2,\n    },\n    behaviors: ['drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-html-sub-graph.ts",
    "content": "import type { DisplayObject, Group } from '@antv/g';\nimport type { BaseComboStyleProps, GraphData, HTMLStyleProps, IElementEvent, NodeData } from '@antv/g6';\nimport { BaseCombo, ExtensionCategory, Graph, HTML, isCollapsed, register } from '@antv/g6';\nimport { isEqual } from '@antv/util';\n\nexport const elementHTMLSubGraph: TestCase = async (context) => {\n  interface CardNodeData {\n    type: 'card';\n    status: 'expanded' | 'collapsed';\n    data: { name: string; value: number }[];\n    children: CardNodeData[] | [GraphNodeData];\n  }\n  interface GraphNodeData {\n    type: 'graph';\n    data: GraphData;\n  }\n  type Data = CardNodeData | GraphNodeData;\n\n  const getSize = (d: NodeData) => {\n    const data = d.data as unknown as Data;\n    if (data.type === 'card') return data.status === 'expanded' ? [200, 100 * data.children.length] : [200, 100];\n    else return [200, 200];\n  };\n\n  class SubGraph extends HTML {\n    public connectedCallback(): void {\n      super.connectedCallback();\n      this.drawSubGraph();\n    }\n\n    public render(attributes?: Required<HTMLStyleProps>, container?: Group): void {\n      super.render(attributes, container);\n      this.drawSubGraph();\n    }\n\n    private get data() {\n      return this.context.graph.getElementData(this.id).data;\n    }\n\n    private graph?: Graph;\n\n    private previousData?: Record<string, unknown>;\n\n    private drawSubGraph() {\n      if (!this.isConnected) return;\n      const data = this.data;\n      if (isEqual(this.previousData, data)) return;\n      this.previousData = data;\n\n      this.drawGraphNode(data!.data as GraphData);\n    }\n\n    private drawGraphNode(data: GraphData) {\n      const [width, height] = this.getSize();\n      const container = this.getDomElement();\n      container.innerHTML = '';\n\n      const subGraph = new Graph({\n        container,\n        width,\n        height,\n        animation: false,\n        data: data,\n        node: {\n          style: {\n            labelText: (d) => d.id,\n            iconFontFamily: 'iconfont',\n            iconText: '\\ue6e5',\n          },\n        },\n        layout: {\n          type: 'force',\n          linkDistance: 50,\n        },\n        behaviors: ['zoom-canvas', { type: 'drag-canvas', enable: (event: MouseEvent) => event.shiftKey === true }],\n        autoFit: 'view',\n      });\n\n      subGraph.render();\n\n      this.graph = subGraph;\n    }\n\n    public destroy(): void {\n      this.graph?.destroy();\n      super.destroy();\n    }\n  }\n\n  class CardCombo extends BaseCombo {\n    protected getKeyStyle(attributes: Required<BaseComboStyleProps>) {\n      const keyStyle = super.getKeyStyle(attributes);\n      const [width, height] = this.getKeySize(attributes);\n      return {\n        ...keyStyle,\n        width,\n        height,\n        x: -width / 2,\n        y: -height / 2,\n      };\n    }\n\n    protected drawKeyShape(attributes: Required<BaseComboStyleProps>, container: Group): DisplayObject | undefined {\n      const { collapsed } = attributes;\n      const outer = this.upsert('key', 'rect', this.getKeyStyle(attributes), container);\n      if (!outer || !collapsed) {\n        this.removeCardShape();\n        return outer;\n      }\n\n      this.drawCardShape(attributes, container);\n\n      return outer;\n    }\n\n    protected drawCardShape(attributes: Required<BaseComboStyleProps>, container: Group) {\n      const [width, height] = this.getCollapsedKeySize(attributes);\n      const data = this.context.graph.getComboData(this.id).data as unknown as CardNodeData;\n\n      const baseX = -width / 2;\n      const baseY = -height / 2;\n\n      this.upsert(\n        'card-title',\n        'text',\n        {\n          x: baseX,\n          y: baseY,\n          text: '点分组: ' + this.id,\n          textAlign: 'left',\n          textBaseline: 'top',\n          fontSize: 16,\n          fontWeight: 'bold',\n          fill: '#4083f7',\n        },\n        container,\n      );\n\n      const gap = 10;\n      const sep = (width + gap) / data.data.length;\n      data.data.forEach(({ name, value }, index) => {\n        this.upsert(\n          `card-item-name-${index}`,\n          'text',\n          {\n            x: baseX + index * sep,\n            y: baseY + 40,\n            text: name,\n            textAlign: 'left',\n            textBaseline: 'top',\n            fontSize: 12,\n            fill: 'gray',\n          },\n          container,\n        );\n        this.upsert(\n          `card-item-value-${index}`,\n          'text',\n          {\n            x: baseX + index * sep,\n            y: baseY + 60,\n            text: value + '%',\n            textAlign: 'left',\n            textBaseline: 'top',\n            fontSize: 24,\n          },\n          container,\n        );\n      });\n    }\n\n    protected removeCardShape() {\n      Object.entries(this.shapeMap).forEach(([key, shape]) => {\n        if (key.startsWith('card-')) {\n          delete this.shapeMap[key];\n          shape.destroy();\n        }\n      });\n    }\n  }\n\n  register(ExtensionCategory.NODE, 'sub-graph', SubGraph);\n  register(ExtensionCategory.COMBO, 'card', CardCombo);\n\n  const graph = new Graph({\n    ...context,\n    animation: false,\n    zoom: 0.8,\n    data: {\n      nodes: [\n        {\n          id: 'node-1',\n          combo: 'combo-1-1',\n          style: { x: 120, y: 70 },\n          data: {\n            data: {\n              nodes: [\n                { id: 'node-1' },\n                { id: 'node-2' },\n                { id: 'node-3' },\n                { id: 'node-4' },\n                { id: 'node-5' },\n                { id: 'node-6' },\n                { id: 'node-7' },\n                { id: 'node-8' },\n              ],\n              edges: [\n                { source: 'node-1', target: 'node-2' },\n                { source: 'node-1', target: 'node-3' },\n                { source: 'node-1', target: 'node-4' },\n                { source: 'node-1', target: 'node-5' },\n                { source: 'node-1', target: 'node-6' },\n                { source: 'node-1', target: 'node-7' },\n                { source: 'node-1', target: 'node-8' },\n              ],\n            },\n          },\n        },\n        {\n          id: 'node-2',\n          combo: 'combo-1-2',\n          style: { x: 370, y: 70 },\n          data: {\n            data: {\n              nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }],\n              edges: [\n                { source: 'node-1', target: 'node-2' },\n                { source: 'node-1', target: 'node-3' },\n                { source: 'node-1', target: 'node-4' },\n              ],\n            },\n          },\n        },\n        {\n          id: 'node-3',\n          combo: 'combo-1-3-1',\n          style: { x: 120, y: 220 },\n          data: {\n            data: {\n              nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }],\n              edges: [\n                { source: 'node-1', target: 'node-2' },\n                { source: 'node-1', target: 'node-3' },\n                { source: 'node-1', target: 'node-4' },\n              ],\n            },\n          },\n        },\n        {\n          id: 'node-4',\n          combo: 'combo-1-3-2',\n          style: { x: 120, y: 370 },\n          data: {\n            data: {\n              nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }],\n              edges: [\n                { source: 'node-1', target: 'node-2' },\n                { source: 'node-1', target: 'node-3' },\n                { source: 'node-1', target: 'node-4' },\n              ],\n            },\n          },\n        },\n      ],\n      edges: [],\n      combos: [\n        {\n          id: 'combo-1',\n          data: {\n            data: [\n              { name: '指标名1', value: 33 },\n              { name: '指标名2', value: 44 },\n              { name: '指标名3', value: 55 },\n            ],\n          },\n        },\n        {\n          id: 'combo-1-1',\n          combo: 'combo-1',\n          style: { collapsed: true },\n          data: {\n            data: [\n              { name: '指标名1', value: 33 },\n              { name: '指标名2', value: 44 },\n              { name: '指标名3', value: 55 },\n            ],\n          },\n        },\n        {\n          id: 'combo-1-2',\n          combo: 'combo-1',\n          style: { collapsed: true },\n          data: {\n            data: [\n              { name: '指标名1', value: 33 },\n              { name: '指标名2', value: 44 },\n              { name: '指标名3', value: 55 },\n            ],\n          },\n        },\n        {\n          id: 'combo-1-3',\n          combo: 'combo-1',\n          style: { collapsed: true },\n          data: {\n            data: [\n              { name: '指标名1', value: 33 },\n              { name: '指标名2', value: 44 },\n              { name: '指标名3', value: 55 },\n            ],\n          },\n        },\n        {\n          id: 'combo-1-3-1',\n          combo: 'combo-1-3',\n          style: { collapsed: true },\n          data: {\n            data: [\n              { name: '指标名1', value: 33 },\n              { name: '指标名2', value: 44 },\n              { name: '指标名3', value: 55 },\n            ],\n          },\n        },\n        {\n          id: 'combo-1-3-2',\n          combo: 'combo-1-3',\n          style: { collapsed: true },\n          data: {\n            data: [\n              { name: '指标名1', value: 33 },\n              { name: '指标名2', value: 44 },\n              { name: '指标名3', value: 55 },\n            ],\n          },\n        },\n      ],\n    },\n    node: {\n      type: 'sub-graph',\n      style: {\n        dx: -100,\n        dy: -50,\n        size: getSize,\n      },\n    },\n    combo: {\n      type: 'card',\n      style: {\n        collapsedSize: [200, 100],\n        collapsedMarker: false,\n        radius: 10,\n      },\n    },\n    behaviors: [\n      { type: 'drag-element', enable: (event: MouseEvent) => event.shiftKey !== true },\n      'collapse-expand',\n      'zoom-canvas',\n      'drag-canvas',\n    ],\n    plugins: [\n      {\n        type: 'contextmenu',\n        getItems: (event: IElementEvent) => {\n          const { targetType, target } = event;\n          if (!['node', 'combo'].includes(targetType)) return [];\n          const id = target.id;\n\n          if (targetType === 'combo') {\n            const data = graph.getComboData(id);\n            if (isCollapsed(data)) {\n              return [{ name: '展开', value: 'expanded' }];\n            } else return [{ name: '收起', value: 'collapsed' }];\n          }\n          return [{ name: '收起', value: 'collapsed' }];\n        },\n        onClick: (value: CardNodeData['status'], target: HTMLElement, current: SubGraph) => {\n          const id = current.id;\n          const elementType = graph.getElementType(id);\n\n          if (elementType === 'node') {\n            const parent = graph.getParentData(id, 'combo');\n            if (parent) return graph.collapseElement(parent.id, false);\n          }\n\n          if (value === 'expanded') graph.expandElement(id, false);\n          else graph.collapseElement(id, false);\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-label-background.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementLabelBackground: TestCase = async (context) => {\n  const data = {\n    nodes: [\n      {\n        id: 'node1',\n        style: { x: 250, y: 100 },\n      },\n      {\n        id: 'node2',\n        style: { x: 150, y: 300 },\n      },\n      {\n        id: 'node3',\n        style: { x: 400, y: 300 },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge1',\n        source: 'node1',\n        target: 'node2',\n      },\n      {\n        id: 'edge2',\n        source: 'node1',\n        target: 'node3',\n      },\n      {\n        id: 'edge3',\n        source: 'node2',\n        target: 'node3',\n      },\n    ],\n  };\n\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n        labelPlacement: 'bottom',\n        labelFill: '#e66465',\n        labelFontSize: 12,\n        labelFontStyle: 'italic',\n        labelBackground: true,\n        labelBackgroundFill: '#eee',\n        labelBackgroundStroke: '#9ec9ff',\n        labelBackgroundRadius: 2,\n      },\n    },\n    edge: {\n      style: {\n        labelText: (d) => d.id!,\n        labelPlacement: 'center',\n        labelTextBaseline: 'top',\n        labelDy: 5,\n        labelFontSize: 12,\n        labelFontWeight: 'bold',\n        labelFill: '#1890ff',\n        labelBackgroundFill: '#eee',\n        labelBackgroundStroke: '#9ec9ff',\n        labelBackgroundRadius: 2,\n      },\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-label-oversized.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementLabelOversized: TestCase = async (context) => {\n  const data = {\n    nodes: [\n      {\n        id: 'node1',\n        data: {\n          x: 100,\n          y: 150,\n          label: `This label with padding is too  long to be displayed`,\n          size: 100,\n        },\n      },\n      {\n        id: 'node2',\n        data: {\n          x: 400,\n          y: 150,\n          label: 'This label with padding is too long to be displayed',\n          size: 150,\n        },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge1',\n        source: 'node1',\n        target: 'node2',\n        data: {\n          label: 'This label is too long to be displayed',\n        },\n      },\n    ],\n  };\n\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'rect',\n      style: {\n        x: (d) => d.data!.x as number,\n        y: (d) => d.data!.y as number,\n        size: (d) => d.data!.size as number,\n        labelPlacement: 'bottom',\n        labelText: (d) => d.data!.label as string,\n        labelMaxWidth: '90%',\n        labelBackground: true,\n        labelBackgroundFill: '#eee',\n        labelBackgroundFillOpacity: 0.5,\n        labelBackgroundRadius: 4,\n        labelPadding: [0, 10, 0, 10],\n        labelWordWrap: true,\n        labelMaxLines: 4,\n      },\n    },\n    edge: {\n      style: {\n        labelOffsetY: -4,\n        labelTextBaseline: 'bottom',\n        labelText: (d) => d.data!.label as string,\n        labelMaxWidth: '80%',\n        labelBackground: true,\n        labelBackgroundFill: 'red',\n        labelBackgroundFillOpacity: 0.5,\n        labelBackgroundRadius: 4,\n        labelWordWrap: true,\n        labelMaxLines: 4,\n      },\n    },\n    behaviors: ['drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-avatar.ts",
    "content": "import { Graph } from '@antv/g6';\n\nconst avatar = 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*_Do9Tq7MxFQAAAAAAAAAAAAADmJ7AQ/original';\nconst logo = 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*AzSISZeq81IAAAAAAAAAAAAADmJ7AQ/original';\n\nexport const elementNodeAvatar: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        {\n          id: 'node-1',\n          type: 'circle',\n          style: { iconSrc: avatar, iconWidth: 30, iconHeight: 30, iconRadius: 15, size: 40 },\n        },\n        {\n          id: 'node-2',\n          type: 'image',\n          style: { src: avatar, size: 40, radius: 20, iconSrc: logo, iconWidth: 40, iconHeight: 40, iconRadius: 20 },\n        },\n      ],\n    },\n    layout: {\n      type: 'grid',\n    },\n    behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-badges.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementNodeBadges: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        {\n          id: 'node-1',\n          style: {\n            x: 150,\n            y: 150,\n            size: 100,\n            badges: [\n              { text: 'left', placement: 'left' },\n              { text: 'right', placement: 'right' },\n              { text: 'top', placement: 'top' },\n              { text: 'bottom', placement: 'bottom' },\n              // { text: 'top-left', placement: 'top-left' },\n              // { text: 'top-right', placement: 'top-right' },\n              // { text: 'bottom-left', placement: 'bottom-left' },\n              // { text: 'bottom-right', placement: 'bottom-right' },\n            ],\n            badgeFontSize: 8,\n            badgePadding: [10, 10],\n          },\n        },\n      ],\n    },\n  });\n\n  await graph.render();\n\n  elementNodeBadges.form = (panel) => {\n    const config = {\n      add: () => {\n        graph.updateNodeData([\n          {\n            id: 'node-1',\n            style: {\n              badges: [\n                { text: 'left', placement: 'left' },\n                { text: 'right', placement: 'right' },\n                { text: 'top', placement: 'top' },\n                { text: 'bottom', placement: 'bottom' },\n                { text: 'top-left', placement: 'top-left' },\n                { text: 'top-right', placement: 'top-right' },\n                { text: 'bottom-left', placement: 'bottom-left' },\n                { text: 'bottom-right', placement: 'bottom-right' },\n              ],\n            },\n          },\n        ]);\n        graph.draw();\n      },\n      update: () => {\n        graph.updateNodeData([\n          {\n            id: 'node-1',\n            style: {\n              badges: [\n                { text: 'left', placement: 'left', backgroundFill: 'red' },\n                { text: 'right', placement: 'right' },\n                { text: 'top', placement: 'top' },\n                { text: 'bottom', placement: 'bottom' },\n                { text: 'top-left', placement: 'top-left' },\n                { text: 'top-right', placement: 'top-right' },\n                { text: 'bottom-left', placement: 'bottom-left' },\n                { text: 'bottom-right', placement: 'bottom-right' },\n              ],\n            },\n          },\n        ]);\n        graph.draw();\n      },\n      remove: () => {\n        graph.updateNodeData([\n          {\n            id: 'node-1',\n            style: {\n              badges: [\n                { text: 'left', placement: 'left' },\n                { text: 'right', placement: 'right' },\n                { text: 'top', placement: 'top' },\n                { text: 'bottom', placement: 'bottom' },\n              ],\n            },\n          },\n        ]);\n        graph.draw();\n      },\n    };\n    return [\n      panel.add(config, 'add').name('Add Badge'),\n      panel.add(config, 'update').name('Update Badge'),\n      panel.add(config, 'remove').name('Remove Badge'),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-circle.ts",
    "content": "import data from '@@/dataset/element-nodes.json';\nimport { Graph } from '@antv/g6';\n\nexport const elementNodeCircle: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'circle', // 👈🏻 Node shape type.\n      style: {\n        iconFontFamily: 'iconfont',\n        iconText: '\\ue602',\n        labelText: (d) => d.id!,\n        size: 40,\n      },\n    },\n    layout: {\n      type: 'grid',\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-diamond.ts",
    "content": "import data from '@@/dataset/element-nodes.json';\nimport { Graph } from '@antv/g6';\n\nexport const elementNodeDiamond: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'diamond', // 👈🏻 Node shape type.\n      style: {\n        size: 40,\n        labelText: (d) => d.id!,\n        iconFontFamily: 'iconfont',\n        iconText: '\\ue602',\n      },\n    },\n    layout: {\n      type: 'grid',\n    },\n  });\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-donut.ts",
    "content": "import { idOf } from '@/src/utils/id';\nimport { Graph } from '@antv/g6';\n\nexport const elementNodeDonut: TestCase = async (context) => {\n  const data = {\n    nodes: [\n      {\n        id: 'default',\n        style: {\n          innerR: '60%',\n          donuts: [\n            {\n              color: 'orange',\n              lineWidth: 2,\n            },\n          ],\n          fill: 'purple',\n        },\n      },\n      {\n        id: 'halo',\n        style: {\n          donuts: [{ color: 'red' }, { color: 'green' }],\n        },\n      },\n      {\n        id: 'badges',\n        style: {\n          donuts: [1, 2, 3],\n        },\n      },\n      {\n        id: 'ports',\n        style: {\n          donuts: [1, 1, 1],\n        },\n      },\n      {\n        id: 'active',\n        states: ['active'],\n        style: {\n          donuts: [\n            {\n              value: 20,\n            },\n            {\n              value: 1000,\n            },\n          ],\n        },\n      },\n      {\n        id: 'selected',\n        states: ['selected'],\n        style: {\n          donuts: [{ value: 1000 }, { value: 20 }],\n        },\n      },\n      {\n        id: 'highlight',\n        states: ['highlight'],\n        style: {\n          donutLineWidth: 1,\n          donutStroke: '#fff',\n          donuts: [1, 2, 3],\n        },\n      },\n      {\n        id: 'inactive',\n        states: ['inactive'],\n        style: {\n          innerR: 0,\n          donuts: [{ fill: 'red' }, { fill: 'green' }],\n        },\n      },\n      {\n        id: 'disabled',\n        states: ['disabled'],\n        style: {\n          innerR: '50%',\n          donuts: [{ color: 'green' }, { color: 'red' }],\n        },\n      },\n    ],\n  };\n\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'donut', // 👈🏻 Node shape type.\n      style: {\n        size: 40,\n        innerR: (d: any) => d.style.innerR ?? '50%',\n        labelText: (d) => d.id,\n        iconHeight: 20,\n        iconWidth: 20,\n        iconFontFamily: 'iconfont',\n        iconText: '\\ue602',\n        halo: (d) => idOf(d).toString().includes('halo'),\n        portR: 3,\n        ports: (d) =>\n          idOf(d).toString().includes('ports')\n            ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n            : [],\n        badges: (d) =>\n          idOf(d).toString().includes('badges')\n            ? [\n                { text: 'A', placement: 'right-top' },\n                { text: 'Important', placement: 'right' },\n                { text: 'Notice', placement: 'right-bottom' },\n              ]\n            : [],\n        badgeFontSize: 8,\n        badgePadding: [1, 4],\n      },\n    },\n    // TODO fixme when animation is enabled\n    animation: false,\n    layout: {\n      type: 'grid',\n    },\n    behaviors: ['drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-ellipse.ts",
    "content": "import data from '@@/dataset/element-nodes.json';\nimport { Graph } from '@antv/g6';\n\nexport const elementNodeEllipse: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'ellipse', // 👈🏻 Node shape type.\n      style: {\n        size: [45, 35],\n        labelText: (d) => d.id!,\n        iconHeight: 20,\n        iconWidth: 20,\n        iconFontFamily: 'iconfont',\n        iconText: '\\ue602',\n      },\n    },\n    layout: {\n      type: 'grid',\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-hexagon.ts",
    "content": "import data from '@@/dataset/element-nodes.json';\nimport { Graph } from '@antv/g6';\n\nexport const elementNodeHexagon: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'hexagon', // 👈🏻 Node shape type.\n      style: {\n        size: 40,\n        labelText: (d) => d.id!,\n        iconFontFamily: 'iconfont',\n        iconText: '\\ue602',\n      },\n    },\n    layout: {\n      type: 'grid',\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-html-2.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementNodeHTML2: TestCase = async (context) => {\n  const ICON_MAP = {\n    error: '&#10060;',\n    overload: '&#9889;',\n    running: '&#9989;',\n  };\n\n  const COLOR_MAP = {\n    error: '#f5222d',\n    overload: '#faad14',\n    running: '#52c41a',\n  } as const;\n\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', data: { location: 'East', status: 'error', ip: '192.168.1.2' } },\n        { id: 'node-2', data: { location: 'West', status: 'overload', ip: '192.168.1.3' } },\n        { id: 'node-3', data: { location: 'South', status: 'running', ip: '192.168.1.4' }, states: ['active'] },\n      ],\n    },\n    node: {\n      type: 'html',\n      style: {\n        size: [160, 60],\n        dx: -80,\n        dy: -30,\n        innerHTML: (d: any) => {\n          const {\n            data: { location, status, ip },\n          } = d;\n          const color = COLOR_MAP[status as keyof typeof COLOR_MAP];\n          return `\n  <div \n    style=\"\n      width:100%; \n      height: 100%; \n      background: ${color}bb; \n      border: 1px solid ${color};\n      color: #fff;\n      user-select: none;\n      display: flex; \n      padding: 10px;\n      \"\n  >\n    <div style=\"display: flex;flex-direction: column;flex: 1;\">\n      <div style=\"font-weight: bold;\">\n        ${location} Node\n      </div>\n      <div>\n        status: ${status} ${ICON_MAP[status as keyof typeof ICON_MAP]}\n      </div>\n    </div>\n    <div>\n      <span style=\"border: 1px solid white; padding: 2px;\">\n        ${ip}\n      </span>\n    </div>\n  </div>`;\n        },\n      },\n    },\n    layout: {\n      type: 'grid',\n    },\n    behaviors: ['drag-element', 'zoom-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-html.ts",
    "content": "import type { NodeData } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nexport const elementNodeHTML: TestCase = async (context) => {\n  const data = {\n    nodes: [\n      { id: 'html-1', style: { x: 100, y: 100, fill: 'orange' }, data: { content: 'HTML NODE 1' } },\n      { id: 'html-2', style: { x: 100, y: 200, fill: 'pink' }, data: { content: 'HTML NODE 2' } },\n    ],\n    edges: [{ source: 'html-1', target: 'html-2' }],\n  };\n\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'html', // 👈🏻 Node shape type.\n      style: {\n        size: [240, 80],\n        innerHTML: (d: NodeData) => `\n<div style=\"width: 100%; height: 100%; background: ${d.style!.fill}; display: flex; justify-content: center; align-items: center;\">\n  <span style=\"color: #fff; font-size: 20px;\">\n    ${d.data!.content}\n  </span>\n</div>`,\n      },\n    },\n    behaviors: ['drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-image.ts",
    "content": "import data from '@@/dataset/element-nodes.json';\nimport { Graph } from '@antv/g6';\n\nexport const elementNodeImage: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'image', // 👈🏻 Node shape type.\n      style: {\n        size: 40,\n        labelText: (d) => d.id!,\n        src: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*N4ZMS7gHsUIAAAAAAAAAAABkARQnAQ',\n        iconSrc: '',\n        haloStroke: '#227eff',\n      },\n      state: {\n        inactive: {\n          fillOpacity: 0.5,\n        },\n        disabled: {\n          fillOpacity: 0.2,\n        },\n      },\n    },\n    layout: {\n      type: 'grid',\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-rect.ts",
    "content": "import data from '@@/dataset/element-nodes.json';\nimport { Graph } from '@antv/g6';\n\nexport const elementNodeRect: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'rect', // 👈🏻 Node shape type.\n      style: {\n        radius: 4, // 👈🏻 Set the radius.\n        size: 40,\n        labelText: (d) => d.id!,\n        iconFontFamily: 'iconfont',\n        iconText: '\\ue602',\n      },\n    },\n    layout: {\n      type: 'grid',\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-star.ts",
    "content": "import data from '@@/dataset/element-nodes.json';\nimport { Graph } from '@antv/g6';\n\nexport const elementNodeStar: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'star', // 👈🏻 Node shape type.\n      style: {\n        size: 40,\n        labelText: (d) => d.id!,\n        iconFontFamily: 'iconfont',\n        iconText: '\\ue602',\n      },\n    },\n    layout: {\n      type: 'grid',\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-svg-icon.ts",
    "content": "import comment from '@@/assets/comment.svg';\nimport user from '@@/assets/user.svg';\nimport { Graph } from '@antv/g6';\n\nexport const elementNodeSVGIcon: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'user', data: { type: 'user' }, style: { x: 50, y: 50 } },\n        { id: 'comment', data: { type: 'comment' }, style: { x: 100, y: 50 } },\n      ],\n    },\n    node: {\n      style: {\n        size: 40,\n        fill: 'transparent',\n        iconSrc: (d) => (d?.data?.type === 'user' ? user : comment),\n        iconWidth: 40,\n        iconHeight: 40,\n      },\n    },\n    behaviors: ['drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-node-triangle.ts",
    "content": "import data from '@@/dataset/element-nodes.json';\nimport { Graph } from '@antv/g6';\n\nexport const elementNodeTriangle: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      type: 'triangle', // 👈🏻 Node shape type.\n      style: {\n        size: 40,\n        direction: (d: any) => (d.id === 'ports' ? 'left' : 'up'),\n        labelText: (d) => d.id!,\n        iconFontFamily: 'iconfont',\n        iconText: '\\ue602',\n        ports: (d) => (d.id === 'ports' ? [{ placement: 'left' }, { placement: 'top' }, { placement: 'bottom' }] : []),\n      },\n    },\n    layout: {\n      type: 'grid',\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-port.ts",
    "content": "import { idOf } from '@/src/utils/id';\nimport { Graph } from '@antv/g6';\n\nexport const elementPort: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 80, y: 200 } },\n        { id: 'node-2', style: { x: 250, y: 200 } },\n      ],\n      edges: [\n        {\n          id: 'edge-1',\n          source: 'node-1',\n          target: 'node-2',\n        },\n        {\n          id: 'edge-2',\n          source: 'node-1',\n          target: 'node-2',\n        },\n        { id: 'edge-3', source: 'node-1', target: 'node-2' },\n      ],\n    },\n    node: {\n      type: (d) => (d.id === 'node-1' ? 'circle' : 'rect'),\n      style: {\n        size: (d) => (d.id === 'node-1' ? 30 : [50, 150]),\n        port: true,\n        ports: (d) =>\n          d.id === 'node-2'\n            ? [\n                { key: 'port-1', placement: [0, 0.15] },\n                { key: 'port-2', placement: 'left' },\n                { key: 'port-3', placement: [0, 0.85] },\n              ]\n            : [],\n      },\n    },\n    edge: {\n      style: {\n        endArrow: true,\n        targetPort: (d) => `port-${idOf(d).toString().split('-')[1]}`,\n      },\n    },\n  });\n\n  await graph.render();\n\n  const config = {\n    showPort: false,\n    linkToCenter: false,\n  };\n\n  elementPort.form = (panel) => {\n    const updatePort = (attr: string, value: any) => {\n      graph.updateNodeData((prev) => {\n        const node2Data = prev.find((node: any) => node.id === 'node-2')!;\n        return [\n          ...prev.filter((node: any) => node.id !== 'node-2'),\n          {\n            ...node2Data,\n            style: {\n              ...node2Data!.style,\n              [attr]: value,\n            },\n          },\n        ];\n      });\n    };\n    return [\n      panel.add(config, 'showPort').onChange((showPort: boolean) => {\n        updatePort('portR', showPort ? 3 : 0);\n        graph.draw();\n      }),\n      panel.add(config, 'linkToCenter').onChange((linkToCenter: boolean) => {\n        updatePort('portLinkToCenter', linkToCenter);\n        graph.draw();\n      }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-position-combo.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementPositionCombo: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', combo: 'combo-1', style: { x: 100, y: 150 } },\n        { id: 'node-2', combo: 'combo-1', style: { x: 200, y: 150 } },\n        { id: 'node-3', combo: 'combo-2', style: { x: 400, y: 200 } },\n        { id: 'node-4', combo: 'combo-3', style: { x: 150, y: 300 } },\n      ],\n      combos: [{ id: 'combo-1', combo: 'combo-2' }, { id: 'combo-2' }, { id: 'combo-3', combo: 'combo-1' }],\n    },\n    node: {\n      style: {\n        size: 20,\n        labelWordWrapWidth: 200,\n        labelText: (d) => d.id,\n      },\n    },\n    combo: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    padding: 20,\n    autoFit: 'view',\n    behaviors: ['hover-activate'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-position.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementPosition: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 50, y: 50 } },\n        { id: 'node-2', style: { x: 200, y: 50 } },\n        { id: 'node-3', style: { x: 125, y: 150 } },\n      ],\n      edges: [\n        { id: 'edge-1', source: 'node-1', target: 'node-2' },\n        { id: 'edge-2', source: 'node-2', target: 'node-3' },\n        { id: 'edge-3', source: 'node-3', target: 'node-1' },\n      ],\n    },\n    node: {\n      style: {\n        size: 20,\n      },\n    },\n  });\n\n  await graph.render();\n\n  elementPosition.form = (panel) => {\n    const config = {\n      element: 'node-1',\n      x: 50,\n      y: 50,\n    };\n\n    const translate = () => {\n      graph.translateElementTo(\n        {\n          [config.element]: [config.x, config.y],\n        },\n        false,\n      );\n    };\n\n    const element = panel.add(config, 'element', ['node-1', 'node-2', 'node-3']).onChange((id: string) => {\n      const position = graph.getElementPosition(id);\n      x.setValue(position[0]);\n      y.setValue(position[1]);\n    });\n    const x = panel.add(config, 'x', 0, 300, 1).onChange(translate);\n    const y = panel.add(config, 'y', 0, 300, 1).onChange(translate);\n    return [element, x, y];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-state.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementState: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', states: ['active', 'selected'], style: { x: 50, y: 50 } },\n        { id: 'node-2', style: { x: 200, y: 50 } },\n        { id: 'node-3', states: ['active'], style: { x: 125, y: 150 } },\n      ],\n      edges: [\n        { id: 'edge-1', source: 'node-1', target: 'node-2', states: ['active'] },\n        { id: 'edge-2', source: 'node-2', target: 'node-3' },\n        { id: 'edge-3', source: 'node-3', target: 'node-1' },\n      ],\n    },\n    theme: 'light',\n    node: {\n      style: {\n        size: 20,\n      },\n      state: {\n        selected: {\n          fill: 'pink',\n        },\n      },\n      animation: {\n        update: [{ fields: ['lineWidth', 'fill'] }],\n      },\n    },\n    edge: {\n      style: {\n        lineWidth: 1,\n      },\n      state: {\n        active: {\n          lineWidth: 2,\n          stroke: 'pink',\n        },\n      },\n      animation: {\n        update: [\n          {\n            fields: ['lineWidth', 'stroke'],\n          },\n        ],\n      },\n    },\n  });\n\n  await graph.render();\n\n  elementState.form = (panel) => {\n    const config = {\n      element: 'node-1',\n      active: true,\n      selected: true,\n    };\n\n    const setState = () => {\n      const state: string[] = [];\n      if (config.active) state.push('active');\n      if (config.selected) state.push('selected');\n      graph.setElementState({ [config.element]: state });\n    };\n\n    const element = panel\n      .add(config, 'element', ['node-1', 'node-2', 'node-3', 'edge-1', 'edge-2', 'edge-3'])\n      .onChange((id: string) => {\n        const states = graph.getElementState(id);\n        selected.setValue(states.includes('selected'));\n        active.setValue(states.includes('active'));\n      });\n    const active = panel.add(config, 'active').onChange(setState);\n    const selected = panel.add(config, 'selected').onChange(setState);\n\n    return [element, active, selected];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-visibility-part.ts",
    "content": "import { Group, Rect } from '@antv/g';\nimport { BaseNodeStyleProps, Circle, ExtensionCategory, Graph, register, setVisibility } from '@antv/g6';\n\ninterface CustomCircleStyleProps extends BaseNodeStyleProps {\n  show: boolean;\n}\n\nclass CustomCircle extends Circle {\n  public renderPart(attributes: Required<CustomCircleStyleProps>, container: Group) {\n    const part = this.upsert('part', Rect, { width: 10, height: 10, stroke: 'red', lineWidth: 2 }, container)!;\n    this.upsert('part-rect', Rect, { x: 1, y: 1, width: 8, height: 8, fill: 'pink' }, part);\n\n    setVisibility(part, attributes.show ? 'visible' : 'hidden');\n  }\n\n  public render(attributes: Required<CustomCircleStyleProps>, container: Group): void {\n    super.render();\n    this.renderPart(attributes, container);\n  }\n}\n\nexport const elementVisibilityPart: TestCase = async (context) => {\n  register(ExtensionCategory.NODE, 'custom-circle', CustomCircle);\n\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [{ id: 'node-1', style: { x: 100, y: 100, show: true } }],\n    },\n    node: {\n      type: 'custom-circle',\n      style: {\n        size: 20,\n      },\n    },\n  });\n\n  await graph.draw();\n\n  elementVisibilityPart.form = (panel) => {\n    const config = {\n      node: true,\n      part: true,\n    };\n    return [\n      panel.add(config, 'node').onChange((show: boolean) => {\n        graph.updateNodeData([{ id: 'node-1', style: { visibility: show ? 'visible' : 'hidden' } }]);\n        graph.draw();\n      }),\n      panel.add(config, 'part').onChange((show: boolean) => {\n        graph.updateNodeData([{ id: 'node-1', style: { show } }]);\n        graph.draw();\n      }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-visibility.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementVisibility: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 50, y: 50 } },\n        { id: 'node-2', style: { x: 200, y: 50 } },\n        { id: 'node-3', style: { x: 125, y: 150 } },\n        { id: 'node-4', style: { x: 125, y: 200, visibility: 'hidden' } },\n      ],\n      edges: [\n        { id: 'edge-1', source: 'node-1', target: 'node-2' },\n        { id: 'edge-2', source: 'node-2', target: 'node-3' },\n        { id: 'edge-3', source: 'node-3', target: 'node-1' },\n      ],\n    },\n    theme: 'light',\n    node: { style: { size: 20, labelText: (d) => d.id!.toString().at(-1)! } },\n    edge: { style: { endArrow: true, labelText: (d) => d.id! } },\n  });\n\n  await graph.render();\n\n  elementVisibility.form = (panel) => {\n    const config = {\n      element: 'node-1',\n      visible: true,\n    };\n    const element = panel\n      .add(config, 'element', ['node-1', 'node-2', 'node-3', 'node-4', 'edge-1', 'edge-2', 'edge-3'])\n      .onChange((id: string) => {\n        visible.setValue(graph.getElementVisibility(id) !== 'hidden');\n      });\n    const visible = panel.add(config, 'visible').onChange((value: boolean) => {\n      value ? graph.showElement(config.element) : graph.hideElement(config.element);\n    });\n    return [element, visible];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/element-z-index.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const elementZIndex: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 50, y: 50 } },\n        { id: 'node-2', style: { x: 200, y: 50 } },\n        { id: 'node-3', style: { x: 125, y: 150 } },\n      ],\n      edges: [\n        { id: 'edge-1', source: 'node-1', target: 'node-2' },\n        { id: 'edge-2', source: 'node-2', target: 'node-3' },\n        { id: 'edge-3', source: 'node-3', target: 'node-1' },\n      ],\n      combos: [\n        { id: 'combo-1', style: { x: 50, y: 250 } },\n        { id: 'combo-2', combo: 'combo-1', style: { x: 50, y: 250 } },\n        { id: 'combo-3', combo: 'combo-2', style: { x: 150, y: 250 } },\n        { id: 'combo-4', style: { x: 350, y: 250 } },\n      ],\n    },\n    node: {\n      style: {\n        size: 40,\n        labelText: (d) => d.id,\n        labelWordWrapWidth: 200,\n      },\n      palette: 'tableau',\n    },\n    combo: {\n      style: {\n        labelText: (d) => d.id,\n        fillOpacity: 1,\n      },\n      palette: 'tableau',\n    },\n    behaviors: ['drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/graph-to-data-url.ts",
    "content": "import icon from '@@/assets/user.svg';\nimport { Graph } from '@antv/g6';\n\nexport const graphToDataURL: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    background: '#f4df4d',\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 50, y: 50, fill: 'purple', halo: true, labelText: 'node-1' } },\n        { id: 'node-2', style: { x: 100, y: 50, fill: 'pink', halo: true, labelText: 'node-2' } },\n        { id: 'node-3', style: { x: 150, y: 50, iconSrc: icon, iconWidth: 30, iconHeight: 30, labelText: 'node-2' } },\n      ],\n      edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', style: { stroke: 'orange', lineWidth: 2 } }],\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  });\n\n  graphToDataURL.form = (panel) => {\n    const config = {\n      toDataURL: () => {\n        graph.toDataURL({ mode: config.mode } as any).then((url) => {\n          navigator.clipboard.writeText(url);\n          alert('The data URL has been copied to the clipboard');\n        });\n      },\n      mode: 'viewport',\n      download: async () => {\n        const dataURL = await graph.toDataURL({ mode: config.mode } as any);\n        const [head, content] = dataURL.split(',');\n        const contentType = head.match(/:(.*?);/)![1];\n\n        const bstr = atob(content);\n        let length = bstr.length;\n        const u8arr = new Uint8Array(length);\n\n        while (length--) {\n          u8arr[length] = bstr.charCodeAt(length);\n        }\n\n        const blob = new Blob([u8arr], { type: contentType });\n\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement('a');\n        a.href = url;\n        a.download = 'graph.png';\n        a.click();\n      },\n    };\n    return [\n      panel.add(config, 'toDataURL'),\n      panel.add(config, 'mode', ['viewport', 'overall']),\n      panel.add(config, 'download').name('Download'),\n    ];\n  };\n\n  await graph.render();\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/image-node-halo-test.ts",
    "content": "import data from '@@/dataset/dagre-combo.json';\nimport { Graph } from '@antv/g6';\n\nexport const imageNodeHaloTest: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'center',\n    data,\n    node: {\n      type: (d) => (Number(d.id) % 2 === 0 ? 'image' : 'rect'),\n      style: {\n        src: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*N4ZMS7gHsUIAAAAAAAAAAABkARQnAQ',\n        size: [60, 30],\n        radius: 8,\n        labelText: (d) => d.id,\n        labelBackground: true,\n        ports: [{ placement: 'top' }, { placement: 'bottom' }],\n      },\n      state: {\n        selected: {\n          haloStroke: '#111',\n          haloLineWidth: 80,\n        },\n      },\n      palette: {\n        field: (d) => d.combo,\n      },\n    },\n    edge: {\n      type: 'cubic-vertical',\n      style: {\n        endArrow: true,\n      },\n    },\n    combo: {\n      type: 'rect',\n      style: {\n        radius: 8,\n        labelText: (d) => d.id,\n      },\n    },\n    layout: {\n      type: 'antv-dagre',\n      ranksep: 50,\n      nodesep: 5,\n      sortByCombo: true,\n    },\n    behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas', 'click-select'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/index.ts",
    "content": "export { animationElementEdgeCubic } from './animation-element-edge-cubic';\nexport { animationEdgeLine } from './animation-element-edge-line';\nexport { animationElementEdgeQuadratic } from './animation-element-edge-quadratic';\nexport { animationElementPosition } from './animation-element-position';\nexport { animationElementState } from './animation-element-state';\nexport { animationElementStateSwitch } from './animation-element-state-switch';\nexport { animationElementStylePosition } from './animation-element-style-position';\nexport { behaviorAutoAdaptLabel } from './behavior-auto-adapt-label';\nexport { behaviorBrushSelect } from './behavior-brush-select';\nexport { behaviorClickSelect } from './behavior-click-select';\nexport { behaviorCreateEdge } from './behavior-create-edge';\nexport { behaviorDragCanvas } from './behavior-drag-canvas';\nexport { behaviorDragNode } from './behavior-drag-element';\nexport { behaviorExpandCollapseCombo } from './behavior-expand-collapse-combo';\nexport { behaviorExpandCollapseNode } from './behavior-expand-collapse-node';\nexport { behaviorFixElementSize } from './behavior-fix-element-size';\nexport { behaviorFocusElement } from './behavior-focus-element';\nexport { behaviorHoverActivate } from './behavior-hover-activate';\nexport { behaviorLassoSelect } from './behavior-lasso-select';\nexport { behaviorOptimizeViewportTransform } from './behavior-optimize-viewport-transform';\nexport { behaviorScrollCanvas } from './behavior-scroll-canvas';\nexport { behaviorZoomCanvas } from './behavior-zoom-canvas';\nexport { bugDragRotatedCanvas } from './bug-drag-rotated-canvas';\nexport { bugDragRotatedElementForce } from './bug-drag-rotated-element-force';\nexport { bugProcessParallelEdgesComboFixed } from './bug-process-parallel-edges-combo-fixed';\nexport { bugTooltipResize } from './bug-tooltip-resize';\nexport { canvasCursor } from './canvas-cursor';\nexport { caseFishbone } from './case-fishbone';\nexport { caseFundFlow } from './case-fund-flow';\nexport { caseIndentedTree } from './case-indented-tree';\nexport { caseLanguageTree } from './case-language-tree';\nexport { caseMindmap } from './case-mindmap';\nexport { caseOrgChart } from './case-org-chart';\nexport { caseRadialDendrogram } from './case-radial-dendrogram';\nexport { caseUnicornsInvestors } from './case-unicorns-investors';\nexport { caseWhyDoCats } from './case-why-do-cats';\nexport { commonGraph } from './common-graph';\nexport { controllerViewport } from './controller-viewport';\nexport { demoAutosizeElementLabel } from './demo-autosize-element-label';\nexport { demoFoundFlow } from './demo-found-flow';\nexport { elementChangeType } from './element-change-type';\nexport { elementCombo } from './element-combo';\nexport { elementEdgeArrow } from './element-edge-arrow';\nexport { elementEdgeCubic } from './element-edge-cubic';\nexport { elementEdgeCubicHorizontal } from './element-edge-cubic-horizontal';\nexport { elementEdgeCubicRadial } from './element-edge-cubic-radial';\nexport { elementEdgeCubicVertical } from './element-edge-cubic-vertical';\nexport { elementEdgeCustomArrow } from './element-edge-custom-arrow';\nexport { elementEdgeLine } from './element-edge-line';\nexport { elementEdgeLoopCurve } from './element-edge-loop-curve';\nexport { elementEdgeLoopPolyline } from './element-edge-loop-polyline';\nexport { elementEdgePolyline } from './element-edge-polyline';\nexport { elementEdgePolylineAnimation } from './element-edge-polyline-animation';\nexport { elementEdgePolylineAstar } from './element-edge-polyline-astar';\nexport { elementEdgePolylineOrth } from './element-edge-polyline-orth';\nexport { elementEdgePort } from './element-edge-port';\nexport { elementEdgeQuadratic } from './element-edge-quadratic';\nexport { elementEdgeSize } from './element-edge-size';\nexport { elementHTMLSubGraph } from './element-html-sub-graph';\nexport { elementLabelBackground } from './element-label-background';\nexport { elementLabelOversized } from './element-label-oversized';\nexport { elementNodeAvatar } from './element-node-avatar';\nexport { elementNodeBadges } from './element-node-badges';\nexport { elementNodeCircle } from './element-node-circle';\nexport { elementNodeDiamond } from './element-node-diamond';\nexport { elementNodeDonut } from './element-node-donut';\nexport { elementNodeEllipse } from './element-node-ellipse';\nexport { elementNodeHexagon } from './element-node-hexagon';\nexport { elementNodeHTML } from './element-node-html';\nexport { elementNodeHTML2 } from './element-node-html-2';\nexport { elementNodeImage } from './element-node-image';\nexport { elementNodeRect } from './element-node-rect';\nexport { elementNodeStar } from './element-node-star';\nexport { elementNodeSVGIcon } from './element-node-svg-icon';\nexport { elementNodeTriangle } from './element-node-triangle';\nexport { elementPort } from './element-port';\nexport { elementPosition } from './element-position';\nexport { elementPositionCombo } from './element-position-combo';\nexport { elementState } from './element-state';\nexport { elementVisibility } from './element-visibility';\nexport { elementVisibilityPart } from './element-visibility-part';\nexport { elementZIndex } from './element-z-index';\nexport { graphToDataURL } from './graph-to-data-url';\nexport { imageNodeHaloTest } from './image-node-halo-test';\nexport { layoutAntVDagreFlow } from './layout-antv-dagre-flow';\nexport { layoutAntVDagreFlowCombo } from './layout-antv-dagre-flow-combo';\nexport { layoutCircularBasic } from './layout-circular-basic';\nexport { layoutCircularConfigurationTranslate } from './layout-circular-configuration-translate';\nexport { layoutCircularDegree } from './layout-circular-degree';\nexport { layoutCircularDivision } from './layout-circular-division';\nexport { layoutCircularSpiral } from './layout-circular-spiral';\nexport { layoutComboCombined } from './layout-combo-combined';\nexport { layoutCompactBoxBasic } from './layout-compact-box-basic';\nexport { layoutCompactBoxTopToBottom } from './layout-compact-box-left-align';\nexport { layoutCompactBoxLeftAlign } from './layout-compact-box-top-to-bottom';\nexport { layoutConcentric } from './layout-concentric';\nexport { layoutCustomDagre } from './layout-custom-dagre';\nexport { layoutCustomHorizontal } from './layout-custom-horizontal';\nexport { layoutCustomIterative } from './layout-custom-iterative';\nexport { layoutD3Force } from './layout-d3-force';\nexport { layoutDagre } from './layout-dagre';\nexport { layoutDendrogramBasic } from './layout-dendrogram-basic';\nexport { layoutDendrogramRadial } from './layout-dendrogram-radial';\nexport { layoutDendrogramTb } from './layout-dendrogram-tb';\nexport { layoutFishbone } from './layout-fishbone';\nexport { layoutForce } from './layout-force';\nexport { layoutForceCollision } from './layout-force-collision';\nexport { layoutForceLattice } from './layout-force-lattice';\nexport { layoutForceatlas2WASM } from './layout-forceatlas2-wasm';\nexport { layoutFruchtermanBasic } from './layout-fruchterman-basic';\nexport { layoutFruchtermanCluster } from './layout-fruchterman-cluster';\nexport { layoutFruchtermanFix } from './layout-fruchterman-fix';\nexport { layoutFruchtermanGPU } from './layout-fruchterman-gpu';\nexport { layoutFruchtermanWASM } from './layout-fruchterman-wasm';\nexport { layoutGrid } from './layout-grid';\nexport { layoutIndented } from './layout-indented';\nexport { layoutMDS } from './layout-mds';\nexport { layoutMindmapH } from './layout-mindmap-h';\nexport { layoutMindmapHCustomSide } from './layout-mindmap-h-custom-side';\nexport { layoutMindmapHLeft } from './layout-mindmap-h-left';\nexport { layoutMindmapHRight } from './layout-mindmap-h-right';\nexport { layoutPipelineMdsForce } from './layout-pipeline-mds-force';\nexport { layoutRadialBasic } from './layout-radial-basic';\nexport { layoutRadialConfigurationTranslate } from './layout-radial-configuration-translate';\nexport { layoutRadialPreventOverlap } from './layout-radial-prevent-overlap';\nexport { layoutRadialPreventOverlapUnstrict } from './layout-radial-prevent-overlap-unstrict';\nexport { layoutRadialSort } from './layout-radial-sort';\nexport { layoutSnake } from './layout-snake';\nexport { perf20000Elements } from './perf-20000-elements';\nexport { perfFCP } from './perf-fcp';\nexport { pluginBackground } from './plugin-background';\nexport { pluginBubbleSets } from './plugin-bubble-sets';\nexport { pluginCameraSetting } from './plugin-camera-setting';\nexport { pluginContextmenu } from './plugin-contextmenu';\nexport { pluginEdgeBundling } from './plugin-edge-bundling';\nexport { pluginEdgeFilterLens } from './plugin-edge-filter-lens';\nexport { pluginFisheye } from './plugin-fisheye';\nexport { pluginFullscreen } from './plugin-fullscreen';\nexport { pluginGridLine } from './plugin-grid-line';\nexport { pluginHistory } from './plugin-history';\nexport { pluginHull } from './plugin-hull';\nexport { pluginLegend } from './plugin-legend';\nexport { pluginMinimap } from './plugin-minimap';\nexport { pluginMiniMapEdgeArrow } from './plugin-minimap-edge-arrow';\nexport { pluginSnapline } from './plugin-snapline';\nexport { pluginTimebar } from './plugin-timebar';\nexport { pluginTitle } from './plugin-title';\nexport { pluginToolbarBuildIn } from './plugin-toolbar-build-in';\nexport { pluginToolbarIconfont } from './plugin-toolbar-iconfont';\nexport { pluginTooltip } from './plugin-tooltip';\nexport { pluginTooltipAsync } from './plugin-tooltip-async';\nexport { pluginTooltipDual } from './plugin-tooltip-dual';\nexport { pluginTooltipEnable } from './plugin-tooltip-enable';\nexport { pluginTooltipWithCustomNode } from './plugin-tooltip-with-custom-node';\nexport { pluginWatermark } from './plugin-watermark';\nexport { pluginWatermarkImage } from './plugin-watermark-image';\nexport { theme } from './theme';\nexport { transformMapNodeSize } from './transform-map-node-size';\nexport { transformPlaceRadialLabels } from './transform-place-radial-labels';\nexport { transformProcessParallelEdges } from './transform-process-parallel-edges';\nexport { viewportFit } from './viewport-fit';\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-antv-dagre-flow-combo.ts",
    "content": "import data from '@@/dataset/dagre-combo.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutAntVDagreFlowCombo: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data,\n    node: {\n      type: 'rect',\n      style: {\n        size: [60, 30],\n        radius: 8,\n        labelText: (d) => d.id,\n        labelPlacement: 'center',\n        ports: [{ placement: 'top' }, { placement: 'bottom' }],\n      },\n      palette: {\n        field: (d) => d.combo,\n      },\n    },\n    edge: {\n      type: 'cubic-vertical',\n      style: {\n        endArrow: true,\n      },\n    },\n    combo: {\n      type: 'rect',\n      style: {\n        radius: 8,\n        labelText: (d) => d.id,\n        lineDash: 0,\n        collapsedLineDash: [5, 5],\n      },\n    },\n    layout: {\n      type: 'antv-dagre',\n      ranksep: 50,\n      nodesep: 5,\n      sortByCombo: true,\n    },\n    behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas', 'collapse-expand'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-antv-dagre-flow.ts",
    "content": "import data from '@@/dataset/dagre.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutAntVDagreFlow: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data,\n    node: {\n      type: 'rect',\n      style: {\n        size: [60, 30],\n        radius: 8,\n        labelFill: '#fff',\n        labelPlacement: 'center',\n        labelText: (d) => d.id,\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        radius: 20,\n        endArrow: true,\n        lineWidth: 2,\n        stroke: '#C2C8D5',\n      },\n    },\n    layout: {\n      type: 'antv-dagre',\n      controlPoints: true,\n    },\n    behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-circular-basic.ts",
    "content": "import data from '@@/dataset/circular.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutCircularBasic: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data,\n    layout: {\n      type: 'circular',\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-circular-configuration-translate.ts",
    "content": "import data from '@@/dataset/circular.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutCircularConfigurationTranslate: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data,\n    edge: {\n      style: {\n        endArrow: true,\n        endArrowType: 'vee',\n      },\n    },\n    layout: {\n      type: 'circular',\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-circular-degree.ts",
    "content": "import data from '@@/dataset/circular.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutCircularDegree: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    edge: {\n      style: {\n        endArrow: true,\n        endArrowType: 'vee',\n      },\n    },\n    layout: {\n      type: 'circular',\n      ordering: 'degree',\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-circular-division.ts",
    "content": "import data from '@@/dataset/circular.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutCircularDivision: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    edge: {\n      style: {\n        endArrow: true,\n        endArrowType: 'vee',\n      },\n    },\n    layout: {\n      type: 'circular',\n      divisions: 5,\n      radius: 200,\n      startAngle: Math.PI / 4,\n      endAngle: Math.PI,\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-circular-spiral.ts",
    "content": "import data from '@@/dataset/circular.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutCircularSpiral: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    edge: {\n      style: {\n        endArrow: true,\n        endArrowType: 'vee',\n      },\n    },\n    layout: {\n      type: 'circular',\n      startRadius: 10,\n      endRadius: 300,\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-combo-combined.ts",
    "content": "import data from '@@/dataset/combo.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutComboCombined: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    layout: {\n      type: 'combo-combined',\n      comboPadding: 10,\n      nodeSpacing: 20,\n      comboSpacing: 80,\n    },\n    node: {\n      style: {\n        size: 20,\n        labelText: (d) => d.id,\n      },\n    },\n    edge: {\n      style: (model) => {\n        const { size, color } = model.data as { size: number; color: string };\n        return { stroke: color || '#99ADD1', lineWidth: size || 1 };\n      },\n    },\n    combo: {\n      style: {\n        labelText: (d) => d.id,\n        padding: 10,\n      },\n    },\n    behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n    autoFit: 'view',\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-compact-box-basic.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const layoutCompactBoxBasic: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n    node: {\n      style: {\n        labelText: (data) => data.id,\n        labelPlacement: 'right',\n        labelMaxWidth: 200,\n        ports: [{ placement: 'right' }, { placement: 'left' }],\n      },\n    },\n    edge: {\n      type: 'cubic-horizontal',\n    },\n    layout: {\n      type: 'compact-box',\n      direction: 'LR',\n      getHeight: function getHeight() {\n        return 32;\n      },\n      getWidth: function getWidth() {\n        return 32;\n      },\n      getVGap: function getVGap() {\n        return 10;\n      },\n      getHGap: function getHGap() {\n        return 100;\n      },\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-compact-box-left-align.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport type { NodeData } from '@antv/g6';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const layoutCompactBoxTopToBottom: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n    node: {\n      style: {\n        labelText: (data) => data.id,\n        labelPlacement: 'right',\n        labelMaxWidth: 200,\n        size: 12,\n        lineWidth: 1,\n        fill: '#fff',\n        ports: [{ placement: 'right' }, { placement: 'left' }],\n      },\n    },\n    edge: {\n      type: 'cubic-horizontal',\n    },\n    layout: {\n      type: 'compact-box',\n      direction: 'LR',\n      getId: function getId(d: NodeData) {\n        return d.id;\n      },\n      getHeight: function getHeight() {\n        return 16;\n      },\n      getVGap: function getVGap() {\n        return 10;\n      },\n      getHGap: function getHGap() {\n        return 100;\n      },\n      getWidth: function getWidth(d: NodeData) {\n        return d.id.toString().length + 20;\n      },\n    },\n    animation: false,\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-compact-box-top-to-bottom.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport type { NodeData } from '@antv/g6';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const layoutCompactBoxLeftAlign: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    node: {\n      style: {\n        labelText: (data) => data.id,\n        labelPlacement: 'right',\n        labelMaxWidth: 200,\n        transform: [['rotate', 90]],\n        size: 26,\n        fill: '#EFF4FF',\n        lineWidth: 1,\n        stroke: '#5F95FF',\n        ports: [{ placement: 'bottom' }, { placement: 'top' }],\n      },\n    },\n    edge: {\n      type: 'cubic-vertical',\n    },\n    layout: {\n      type: 'compact-box',\n      direction: 'TB',\n      getId: function getId(d: NodeData) {\n        return d.id;\n      },\n      getHeight: function getHeight() {\n        return 16;\n      },\n      getWidth: function getWidth() {\n        return 16;\n      },\n      getVGap: function getVGap() {\n        return 80;\n      },\n      getHGap: function getHGap() {\n        return 20;\n      },\n    },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n    animation: false,\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-concentric.ts",
    "content": "import data from '@@/dataset/gene.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutConcentric: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data,\n    layout: {\n      type: 'concentric',\n      maxLevelDiff: 0.5,\n      preventOverlap: true,\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n    animation: false,\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-custom-dagre.ts",
    "content": "import { layoutAdapter } from '@/src/utils/layout';\nimport type { BaseLayoutOptions, GraphData, NodeData } from '@antv/g6';\nimport { BaseLayout, DagreLayout, ExtensionCategory, Graph, register } from '@antv/g6';\n\ninterface CustomLayoutOptions extends BaseLayoutOptions {\n  gap?: number;\n  nodeSize: (node: NodeData) => number;\n  center: [number, number];\n}\n\nclass CustomLayout extends BaseLayout<CustomLayoutOptions> {\n  id = 'custom-layout';\n\n  async execute(data: GraphData): Promise<GraphData> {\n    const AdaptiveDagreLayout = layoutAdapter(DagreLayout, this.context);\n    const layout = new AdaptiveDagreLayout(this.context, this.options);\n    const model = await layout.execute(data);\n    return model;\n  }\n}\n\nexport const layoutCustomDagre: TestCase = async (context) => {\n  register(ExtensionCategory.LAYOUT, 'custom-layout', CustomLayout);\n\n  const data = {\n    nodes: [\n      { id: 'kspacey', data: { label: 'Kevin Spacey', width: 144, height: 100 } },\n      { id: 'swilliams', data: { label: 'Saul Williams', width: 160, height: 100 } },\n      { id: 'bpitt', data: { label: 'Brad Pitt', width: 108, height: 100 } },\n      { id: 'hford', data: { label: 'Harrison Ford', width: 168, height: 100 } },\n      { id: 'lwilson', data: { label: 'Luke Wilson', width: 144, height: 100 } },\n      { id: 'kbacon', data: { label: 'Kevin Bacon', width: 121, height: 100 } },\n    ],\n    edges: [\n      { id: 'kspacey->swilliams', source: 'kspacey', target: 'swilliams' },\n      { id: 'swilliams->kbacon', source: 'swilliams', target: 'kbacon' },\n      { id: 'bpitt->kbacon', source: 'bpitt', target: 'kbacon' },\n      { id: 'hford->lwilson', source: 'hford', target: 'lwilson' },\n      { id: 'lwilson->kbacon', source: 'lwilson', target: 'kbacon' },\n    ],\n  };\n\n  const graph = new Graph({\n    ...context,\n    autoFit: 'center',\n    data,\n    layout: {\n      type: 'custom-layout',\n    },\n    zoom: 0.8,\n    node: {\n      type: 'rect',\n      style: {\n        labelText: (d) => d.data!.label as string,\n        size: (d) => [d.data!.width, d.data!.height] as [number, number],\n      },\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-custom-horizontal.ts",
    "content": "import { BaseLayout, ExtensionCategory, Graph, idOf, register } from '@antv/g6';\n\nimport type { BaseLayoutOptions, GraphData, NodeData } from '@antv/g6';\n\ninterface CustomLayoutOptions extends BaseLayoutOptions {\n  gap?: number;\n  nodeSize: (node: NodeData) => number;\n  center: [number, number];\n}\n\nclass CustomLayout extends BaseLayout<CustomLayoutOptions> {\n  id = 'custom-layout';\n\n  async execute(data: GraphData): Promise<GraphData> {\n    const { nodes = [] } = data;\n    const { nodeSize, gap = 10 } = this.options;\n    return {\n      nodes: nodes.map((node, index) => {\n        const size = nodeSize(node);\n        return {\n          id: idOf(node),\n          style: {\n            x: index * (size + gap) + size / 2,\n            y: 100,\n          },\n        };\n      }),\n    };\n  }\n}\n\nexport const layoutCustomHorizontal: TestCase = async (context) => {\n  register(ExtensionCategory.LAYOUT, 'custom-layout', CustomLayout);\n\n  const graph = new Graph({\n    ...context,\n    autoFit: 'center',\n    data: {\n      nodes: Array.from({ length: 10 }).map((_, i) => ({ id: i })),\n    },\n    layout: {\n      type: 'custom-layout',\n      gap: 10,\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-custom-iterative.ts",
    "content": "import { BaseLayout, ExtensionCategory, Graph, register } from '@antv/g6';\n\nimport type { BaseLayoutOptions, GraphData } from '@antv/g6';\n\ninterface CustomLayoutOptions extends BaseLayoutOptions {\n  onTick: (data: GraphData) => void;\n}\n\nclass CustomIterativeLayout extends BaseLayout<CustomLayoutOptions> {\n  public id = 'custom-layout';\n\n  private tickCount = 0;\n\n  private data?: GraphData;\n\n  private timer?: number;\n\n  private resolve?: () => void;\n\n  private promise?: Promise<void>;\n\n  async execute(data: GraphData, options: CustomLayoutOptions): Promise<GraphData> {\n    const { onTick } = { ...this.options, ...options };\n\n    this.tickCount = 0;\n    this.data = data;\n\n    this.promise = new Promise((resolve) => {\n      this.resolve = resolve;\n    });\n\n    this.timer = window.setInterval(() => {\n      onTick(this.simulateTick());\n      if (this.tickCount === 10) this.stop();\n    }, 200);\n\n    await this.promise;\n\n    return this.simulateTick();\n  }\n\n  simulateTick = () => {\n    const x = this.tickCount++ % 2 === 0 ? 50 : 150;\n\n    return {\n      nodes: (this?.data?.nodes || []).map((node, index) => ({\n        id: node.id,\n        style: { x, y: 100 + index * 30 },\n      })),\n    };\n  };\n\n  tick = () => {\n    return this.simulateTick();\n  };\n\n  stop = () => {\n    clearInterval(this.timer);\n    this.resolve?.();\n  };\n}\n\nexport const layoutCustomIterative: TestCase = async (context) => {\n  register(ExtensionCategory.LAYOUT, 'custom-iterative', CustomIterativeLayout);\n\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: Array.from({ length: 10 }).map((_, i) => ({ id: i })),\n    },\n    layout: {\n      type: 'custom-iterative',\n      gap: 10,\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-d3-force.ts",
    "content": "import data from '@@/dataset/force.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutD3Force: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    padding: 20,\n    autoFit: 'view',\n    behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element', 'click-select'],\n    layout: {\n      type: 'd3-force',\n      nodeSize: (d: { style: { size: number } }) => (d.style.size as number) * 2,\n      collide: {\n        strength: 0.5,\n      },\n    },\n    node: {\n      style: {\n        labelText: (d) => d.id,\n        labelMaxWidth: '300%',\n      },\n      palette: {\n        type: 'group',\n        field: 'cluster',\n        color: [\n          '#1783FF',\n          '#00C9C9',\n          '#F08F56',\n          '#D580FF',\n          '#7863FF',\n          '#DB9D0D',\n          '#60C42D',\n          '#FF80CA',\n          '#2491B3',\n          '#17C76F',\n        ],\n      },\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-dagre.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const layoutDagre: TestCase = async (context) => {\n  const data = {\n    nodes: [\n      { id: 'kspacey', data: { label: 'Kevin Spacey', width: 144, height: 100 } },\n      { id: 'swilliams', data: { label: 'Saul Williams', width: 160, height: 100 } },\n      { id: 'bpitt', data: { label: 'Brad Pitt', width: 108, height: 100 } },\n      { id: 'hford', data: { label: 'Harrison Ford', width: 168, height: 100 } },\n      { id: 'lwilson', data: { label: 'Luke Wilson', width: 144, height: 100 } },\n      { id: 'kbacon', data: { label: 'Kevin Bacon', width: 121, height: 100 } },\n    ],\n    edges: [\n      { id: 'kspacey->swilliams', source: 'kspacey', target: 'swilliams' },\n      { id: 'swilliams->kbacon', source: 'swilliams', target: 'kbacon' },\n      { id: 'bpitt->kbacon', source: 'bpitt', target: 'kbacon' },\n      { id: 'hford->lwilson', source: 'hford', target: 'lwilson' },\n      { id: 'lwilson->kbacon', source: 'lwilson', target: 'kbacon' },\n    ],\n  };\n\n  const graph = new Graph({\n    ...context,\n    data,\n    layout: {\n      type: 'dagre',\n    },\n    zoom: 0.8,\n    node: {\n      type: 'rect',\n      style: {\n        labelText: (d) => d.data!.label as string,\n        size: (d) => [d.data!.width, d.data!.height] as [number, number],\n      },\n    },\n  });\n\n  await graph.render();\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-dendrogram-basic.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const layoutDendrogramBasic: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    node: {\n      style: {\n        labelText: (d) => d.id,\n        labelPlacement: (model) => (model.children?.length ? 'left' : 'right'),\n        ports: [{ placement: 'right' }, { placement: 'left' }],\n      },\n    },\n    edge: {\n      type: 'cubic-horizontal',\n    },\n    layout: {\n      type: 'dendrogram',\n      direction: 'LR',\n      nodeSep: 36,\n      rankSep: 250,\n      preLayout: true,\n    },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-dendrogram-radial.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const layoutDendrogramRadial: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    layout: {\n      type: 'dendrogram',\n      radial: true,\n      nodeSep: 30,\n      rankSep: 200,\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-dendrogram-tb.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const layoutDendrogramTb: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    node: {\n      style: (d) => {\n        const isLeafNode = !d.children?.length;\n        const style = {\n          labelText: d.id,\n          labelPlacement: 'right',\n          labelOffsetX: 2,\n          labelBackground: true,\n          ports: [{ placement: 'top' }, { placement: 'bottom' }],\n        };\n        if (isLeafNode) {\n          Object.assign(style, {\n            labelTransform: [\n              ['rotate', 90],\n              ['translate', 18],\n            ],\n            labelBaseline: 'center',\n            labelTextAlign: 'left',\n          });\n        }\n        return style;\n      },\n      animation: {\n        enter: false,\n      },\n    },\n    edge: {\n      type: 'cubic-vertical',\n    },\n    layout: {\n      type: 'dendrogram',\n      direction: 'TB',\n      nodeSep: 40,\n      rankSep: 100,\n      preLayout: true,\n    },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-fishbone.ts",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nconst data = {\n  id: 'Quality',\n  children: [\n    {\n      id: 'Machine',\n      children: [{ id: 'Mill' }, { id: 'Mixer' }, { id: 'Metal Lathe', children: [{ id: 'Milling' }] }],\n    },\n    { id: 'Method' },\n    {\n      id: 'Material',\n      children: [\n        {\n          id: 'Masonite',\n          children: [\n            { id: 'spearMint' },\n            { id: 'pepperMint', children: [{ id: 'test3' }] },\n            { id: 'test1', children: [{ id: 'test4' }] },\n          ],\n        },\n        {\n          id: 'Marscapone',\n          children: [{ id: 'Malty' }, { id: 'Minty' }],\n        },\n        { id: 'Meat', children: [{ id: 'Mutton' }] },\n      ],\n    },\n    {\n      id: 'Man Power',\n      children: [\n        { id: 'Manager' },\n        { id: \"Master's Student\" },\n        { id: 'Magician' },\n        { id: 'Miner' },\n        { id: 'Magister', children: [{ id: 'Malpractice' }] },\n        {\n          id: 'Massage Artist',\n          children: [{ id: 'Masseur' }, { id: 'Masseuse' }],\n        },\n      ],\n    },\n    {\n      id: 'Measurement',\n      children: [{ id: 'Malleability' }],\n    },\n    {\n      id: 'Milieu',\n      children: [{ id: 'Marine' }],\n    },\n  ],\n};\n\nexport const layoutFishbone: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    node: {\n      type: 'rect',\n      style: {\n        size: [32, 32],\n        // fill: () => randomColor(),\n        label: false,\n        labelFill: '#262626',\n        labelFontFamily: 'Gill Sans',\n        labelMaxLines: 2,\n        labelMaxWidth: '100%',\n        labelPlacement: 'center',\n        labelText: (d) => d.id,\n        labelWordWrap: true,\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        lineWidth: 3,\n      },\n    },\n    layout: {\n      type: 'fishbone',\n      vGap: 48,\n      hGap: 48,\n      direction: 'RL',\n    },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n    animation: false,\n  });\n\n  await graph.render();\n\n  layoutFishbone.form = (panel) => {\n    const config = {\n      type: 'fishbone',\n      direction: 'RL',\n    };\n\n    return [\n      panel\n        .add(config, 'direction', ['LR', 'RL'])\n        .name('Direction')\n        .onChange((value: 'LR' | 'RL') => {\n          graph.setLayout((prev) => ({ ...prev, direction: value }));\n          graph.render();\n        }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-force-collision.ts",
    "content": "// ref: https://observablehq.com/@d3/collision-detection\nimport { invokeLayoutMethod } from '@/src/utils/layout';\nimport type { IPointerEvent, RuntimeContext } from '@antv/g6';\nimport { BaseBehavior, ExtensionCategory, Graph, register } from '@antv/g6';\n\nexport const layoutForceCollision: TestCase = async (context) => {\n  const width = 500;\n\n  class CollisionElement extends BaseBehavior {\n    constructor(context: RuntimeContext) {\n      super(context, {});\n      this.onPointerMove = this.onPointerMove.bind(this);\n      this.bindEvents();\n    }\n\n    bindEvents() {\n      this.context.graph.on('pointermove', this.onPointerMove);\n    }\n\n    onPointerMove(event: IPointerEvent) {\n      const pos = this.context.graph.getCanvasByClient([event.client.x, event.client.y]);\n      const layoutInstance = this.context.layout\n        ?.getLayoutInstance()\n        .find((layout) => ['d3-force', 'd3-force-3d'].includes(layout?.id));\n\n      if (layoutInstance) {\n        invokeLayoutMethod(layoutInstance, 'setFixedPosition', '0', [...pos]);\n      }\n    }\n  }\n\n  register(ExtensionCategory.BEHAVIOR, 'collision-element', CollisionElement);\n\n  const graph = new Graph({\n    ...context,\n    data: getData(500),\n    layout: {\n      type: 'd3-force',\n      alphaTarget: 0.3,\n      velocityDecay: 0.1,\n      x: {\n        strength: 0.01,\n        x: width / 2,\n      },\n      y: {\n        strength: 0.01,\n        y: width / 2,\n      },\n      nodeSize: (d: { data: { r: number } }) => (d.data.r as number) * 2,\n      collide: {\n        iterations: 3,\n      },\n      manyBody: {\n        strength: (d: any, i: number) => (i ? 0 : (-width * 2) / 3),\n      },\n      link: false,\n    },\n    node: {\n      style: {\n        size: (d) => (d.id === '0' ? 0 : (d.data!.r as number) * 2),\n      },\n      palette: {\n        color: 'tableau',\n        type: 'group',\n        field: (d) => d.data!.group as string,\n      },\n    },\n    behaviors: ['collision-element'],\n  });\n\n  await graph.render();\n  return graph;\n};\n\nfunction getData(width: number, size = 200) {\n  const k = width / 200;\n  const r = randomUniform(k * 2, k * 8);\n  const n = 4;\n  return {\n    nodes: Array.from({ length: size }, (_, i) => ({ id: `${i}`, data: { r: r(), group: i && (i % n) + 1 } })),\n    edges: [],\n  };\n}\n\n// d3-random\nfunction randomUniform(min: number, max: number) {\n  min = min == null ? 0 : +min;\n  max = max == null ? 1 : +max;\n  if (arguments.length === 1) {\n    max = min;\n    min = 0;\n  } else max -= min;\n  return function () {\n    return Math.random() * max + min;\n  };\n}\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-force-lattice.ts",
    "content": "// ref: https://observablehq.com/@d3/force-directed-lattice\nimport { Graph } from '@antv/g6';\n\nexport const layoutForceLattice: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: getData(),\n    layout: {\n      type: 'd3-force',\n      manyBody: {\n        strength: -30,\n      },\n      link: {\n        strength: 1,\n        distance: 20,\n        iterations: 10,\n      },\n    },\n    node: {\n      style: {\n        size: 10,\n        fill: '#000',\n      },\n    },\n    edge: {\n      style: {\n        stroke: '#000',\n      },\n    },\n    behaviors: [{ type: 'drag-element-force' }, 'zoom-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n\nfunction getData(size = 10) {\n  const nodes = Array.from({ length: size * size }, (_, i) => ({ id: `${i}` }));\n  const edges = [];\n  for (let y = 0; y < size; ++y) {\n    for (let x = 0; x < size; ++x) {\n      if (y > 0) edges.push({ source: `${(y - 1) * size + x}`, target: `${y * size + x}` });\n      if (x > 0) edges.push({ source: `${y * size + (x - 1)}`, target: `${y * size + x}` });\n    }\n  }\n  return { nodes, edges };\n}\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-force.ts",
    "content": "import data from '@@/dataset/force.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutForce: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    padding: 20,\n    autoFit: 'view',\n    behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element', 'click-select'],\n    layout: {\n      type: 'force',\n    },\n    node: {\n      style: {\n        labelText: (d) => d.id,\n        labelMaxWidth: '300%',\n      },\n      palette: {\n        type: 'group',\n        field: 'cluster',\n        color: [\n          '#1783FF',\n          '#00C9C9',\n          '#F08F56',\n          '#D580FF',\n          '#7863FF',\n          '#DB9D0D',\n          '#60C42D',\n          '#FF80CA',\n          '#2491B3',\n          '#17C76F',\n        ],\n      },\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-forceatlas2-wasm.ts",
    "content": "import data from '@@/dataset/soccer.json';\nimport type { GraphOptions } from '@antv/g6';\nimport { Graph, register } from '@antv/g6';\n\nexport const layoutForceatlas2WASM: TestCase = async (context) => {\n  const { ForceAtlas2Layout, initThreads, supportsThreads } = await import('@antv/layout-wasm');\n  register('layout', 'forceatlas2-wasm', ForceAtlas2Layout as any);\n\n  const supported = await supportsThreads();\n  const threads = await initThreads(supported);\n\n  const options: GraphOptions = {\n    ...context,\n    data,\n    theme: 'light',\n    layout: {\n      type: 'forceatlas2-wasm',\n      threads,\n      dimensions: 2,\n      maxIteration: 100,\n      minMovement: 0.4,\n      distanceThresholdMode: 'mean',\n      kg: 5,\n      kr: 10,\n      ks: 0.1,\n    },\n    node: { style: { size: 20 } },\n  };\n\n  const graph = new Graph(options);\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-fruchterman-basic.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutFruchtermanBasic: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        labelPlacement: 'center',\n        labelText: (d) => d.id,\n      },\n    },\n    layout: {\n      type: 'fruchterman',\n      gravity: 5,\n      speed: 5,\n      nodeClusterBy: 'data.cluster',\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-fruchterman-cluster.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutFruchtermanCluster: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: { ...data, nodes: data.nodes.map((n) => ({ ...n, cluster: n.data.cluster })) },\n    node: {\n      style: {\n        labelPlacement: 'center',\n        labelText: (d) => d.id,\n      },\n      palette: {\n        field: 'cluster',\n      },\n    },\n    layout: {\n      type: 'fruchterman',\n      gravity: 5,\n      speed: 5,\n      clustering: true,\n      nodeClusterBy: 'cluster',\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-fruchterman-fix.ts",
    "content": "import data from '@@/dataset/relations.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutFruchtermanFix: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    layout: {\n      type: 'fruchterman',\n      speed: 10,\n      maxIteration: 500,\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n  });\n\n  graph.on('node:dragstart', function () {\n    graph.stopLayout();\n  });\n\n  graph.on('node:dragend', function () {\n    // FIXME: 不应该完全重新布局，而是以当前画布数据进行布局\n    graph.layout();\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-fruchterman-gpu.ts",
    "content": "import data from '@@/dataset/soccer.json';\nimport type { GraphOptions } from '@antv/g6';\nimport { Graph, register } from '@antv/g6';\n\nexport const layoutFruchtermanGPU: TestCase = async (context) => {\n  register('layout', 'fruchterman-gpu', (await import('@antv/layout-gpu')).FruchtermanLayout as any);\n\n  const options: GraphOptions = {\n    ...context,\n    data,\n    theme: 'light',\n    layout: {\n      type: 'fruchterman-gpu',\n      maxIteration: 1000,\n      minMovement: 0.4,\n      distanceThresholdMode: 'mean',\n      gravity: 1,\n      speed: 5,\n    },\n    node: { style: { size: 20 } },\n  };\n\n  const graph = new Graph(options);\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-fruchterman-wasm.ts",
    "content": "import data from '@@/dataset/soccer.json';\nimport type { GraphOptions } from '@antv/g6';\nimport { Graph, register } from '@antv/g6';\n\nexport const layoutFruchtermanWASM: TestCase = async (context) => {\n  const { FruchtermanLayout, initThreads, supportsThreads } = await import('@antv/layout-wasm');\n\n  register('layout', 'fruchterman-wasm', FruchtermanLayout as any);\n\n  const supported = await supportsThreads();\n  const threads = await initThreads(supported);\n\n  const options: GraphOptions = {\n    ...context,\n    data,\n    theme: 'light',\n    layout: {\n      type: 'fruchterman-wasm',\n      threads,\n      dimensions: 2,\n      maxIteration: 1000,\n      minMovement: 0.4,\n      distanceThresholdMode: 'mean',\n      gravity: 1,\n      speed: 5,\n    },\n    node: { style: { size: 20 } },\n  };\n\n  const graph = new Graph(options);\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-grid.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutGrid: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    layout: {\n      type: 'grid',\n      sortBy: (d) => d.data.cluster,\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element', 'click-select'],\n  });\n\n  await graph.render();\n\n  layoutGrid.form = (panel) => {\n    const config = {\n      sortBy: 'degree',\n    };\n    return [\n      panel\n        .add(config, 'sortBy', {\n          ID: 'id',\n          Degree: 'degree',\n          Cluster: (n1: any, n2: any) => Number(n2.data.cluster) - Number(n1.data.cluster),\n        })\n        .name('sortBy')\n        .onChange((value: string) => {\n          graph.setLayout({ type: 'grid', sortBy: value });\n          graph.layout();\n        }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-indented.ts",
    "content": "import tree from '@@/dataset/file-system.json';\nimport type { GraphOptions } from '@antv/g6';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const layoutIndented: TestCase = async (context) => {\n  const options: GraphOptions = {\n    ...context,\n    y: -200,\n    zoom: 0.5,\n    data: treeToGraphData(tree),\n    theme: 'light',\n    layout: {\n      type: 'indented',\n      isHorizontal: true,\n      direction: 'LR',\n      indent: 30,\n      getHeight: function getHeight() {\n        return 16;\n      },\n      getWidth: function getWidth() {\n        return 16;\n      },\n    },\n    node: { style: { size: 20 } },\n    edge: {\n      type: 'polyline',\n    },\n  };\n\n  const graph = new Graph(options);\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-mds.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutMDS: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    padding: 20,\n    autoFit: 'view',\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n        labelPlacement: 'center',\n      },\n    },\n    layout: {\n      type: 'mds',\n      linkDistance: 100,\n    },\n    behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas', 'click-select'],\n  });\n\n  await graph.render();\n\n  layoutMDS.form = (panel) => {\n    const config = {\n      linkDistance: 100,\n    };\n    return [\n      panel.add(config, 'linkDistance', 50, 120, 10).onChange((v: number) => {\n        graph.setLayout({ type: 'mds', linkDistance: v });\n        graph.layout();\n      }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-mindmap-h-custom-side.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport type { NodeData } from '@antv/g6';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const layoutMindmapHCustomSide: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: treeToGraphData(data),\n    autoFit: 'view',\n    node: {\n      style: function (this: Graph, model) {\n        const root = this.getNodeData().find((node) => node.depth === 0);\n        const rootX = Number(root?.style?.x ?? 0);\n        const x = Number(model.style?.x ?? 0) - rootX;\n        return {\n          labelText: model.id,\n          size: 26,\n          labelPlacement: x >= 0 ? 'right' : 'left',\n          labelMaxWidth: 200,\n          labelTextAlign: x >= 0 ? 'start' : 'end',\n          lineWidth: 1,\n          stroke: '#5F95FF',\n          fill: '#EFF4FF',\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        };\n      },\n    },\n    edge: {\n      type: 'cubic-horizontal',\n    },\n    layout: {\n      type: 'mindmap',\n      direction: 'H',\n      preLayout: false,\n      getHeight: () => {\n        return 16;\n      },\n      getWidth: () => {\n        return 16;\n      },\n      getVGap: () => {\n        return 10;\n      },\n      getHGap: () => {\n        return 50;\n      },\n      getSide: (d: NodeData) => {\n        if (d.id === 'Classification') {\n          return 'left';\n        }\n        return 'right';\n      },\n    },\n    behaviors: ['collapse-expand', 'drag-canvas', 'zoom-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-mindmap-h-left.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const layoutMindmapHLeft: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: treeToGraphData(data),\n    autoFit: 'view',\n    node: {\n      style: (model) => {\n        return {\n          labelText: model.id,\n          size: 26,\n          labelPlacement: 'left',\n          labelMaxWidth: 200,\n          labelTextAlign: 'end',\n          lineWidth: 1,\n          stroke: '#5F95FF',\n          fill: '#EFF4FF',\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        };\n      },\n    },\n    edge: {\n      type: 'cubic-horizontal',\n    },\n    layout: {\n      type: 'mindmap',\n      direction: 'H',\n      getHeight: () => {\n        return 16;\n      },\n      getWidth: () => {\n        return 16;\n      },\n      getVGap: () => {\n        return 10;\n      },\n      getHGap: () => {\n        return 100;\n      },\n      getSide: () => {\n        return 'left';\n      },\n    },\n    behaviors: ['collapse-expand', 'drag-canvas', 'zoom-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-mindmap-h-right.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const layoutMindmapHRight: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: treeToGraphData(data),\n    autoFit: 'view',\n    node: {\n      style: (model) => {\n        return {\n          labelText: model.id,\n          size: 26,\n          labelPlacement: 'right',\n          labelMaxWidth: 200,\n          lineWidth: 1,\n          stroke: '#5F95FF',\n          fill: '#EFF4FF',\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        };\n      },\n    },\n    edge: {\n      type: 'cubic-horizontal',\n    },\n    layout: {\n      type: 'mindmap',\n      direction: 'H',\n      getHeight: () => {\n        return 16;\n      },\n      getWidth: () => {\n        return 16;\n      },\n      getVGap: () => {\n        return 10;\n      },\n      getHGap: () => {\n        return 100;\n      },\n      getSide: () => {\n        return 'right';\n      },\n    },\n    behaviors: ['collapse-expand', 'drag-canvas', 'zoom-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-mindmap-h.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const layoutMindmapH: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: treeToGraphData(data),\n    autoFit: 'view',\n    node: {\n      style: function (this: Graph, model) {\n        const root = this.getNodeData().find((node) => node.depth === 0);\n        const rootX = Number(root?.style?.x ?? 0);\n        const x = Number(model.style?.x ?? 0) - rootX;\n        return {\n          labelText: model.id,\n          size: 26,\n          labelPlacement: x >= 0 ? 'right' : 'left',\n          labelMaxWidth: 200,\n          labelTextAlign: x >= 0 ? 'start' : 'end',\n          lineWidth: 1,\n          stroke: '#5F95FF',\n          fill: '#EFF4FF',\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        };\n      },\n    },\n    edge: {\n      type: 'cubic-horizontal',\n    },\n    layout: {\n      type: 'mindmap',\n      direction: 'H',\n      preLayout: false,\n      getHeight: () => {\n        return 16;\n      },\n      getWidth: () => {\n        return 16;\n      },\n      getVGap: () => {\n        return 10;\n      },\n      getHGap: () => {\n        return 50;\n      },\n    },\n    behaviors: ['collapse-expand', 'drag-canvas', 'zoom-canvas'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-pipeline-mds-force.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const layoutPipelineMdsForce: TestCase = async (context) => {\n  const data = {\n    nodes: [\n      { id: '2023022111330994' },\n      { id: '2023022131662846' },\n      { id: '2023022134006229' },\n      { id: '2023022134355387' },\n      { id: '2023022134649283' },\n      { id: '2023022135378377' },\n      { id: '2023022159807939' },\n      { id: '2023022171679817' },\n      { id: '2023022192941079' },\n      { id: '2023032121629632' },\n      { id: '2023032149712027' },\n      { id: '2023032171523093' },\n      { id: '2023032439702273' },\n      { id: '2023053113971286' },\n      { id: '2023062814858004' },\n      { id: '2023083116793312' },\n      { id: '2023083116798008' },\n      { id: '2023083116802328' },\n      { id: '2023083116802329' },\n      { id: '2023092717337264' },\n      { id: '2022042607099685' },\n      { id: '2023022115050705' },\n      { id: '2023022124015954' },\n      { id: '2023022160748942' },\n      { id: '2023022176798458' },\n      { id: '2023022183981042' },\n      { id: '2023032138615654' },\n      { id: '2023033152992057' },\n      { id: '2023062614749332' },\n      { id: '2023083016776599' },\n      { id: '2023083116802327' },\n      { id: '2023090716992598' },\n      { id: '2023112718364754' },\n      { id: '2023112918428536' },\n      { id: '10' },\n      { id: '11' },\n      { id: '13' },\n    ],\n    edges: [\n      { source: '2023032149712027', target: '2022042607099685', id: '2024030419919960' },\n      { source: '2023033152992057', target: '2023022124015954', id: '2024030419923397' },\n      { source: '2023092717337264', target: '2022042607099685', id: '2024022919878863' },\n      { source: '2023092717337264', target: '2023033152992057', id: '2024022919878864' },\n      { source: '2023092717337264', target: '2023022135378377', id: '2024022919878865' },\n      { source: '2023022159807939', target: '2023022183981042', id: '2023022171425708' },\n      { source: '2023022183981042', target: '2023022159807939', id: '2024022919872476' },\n      { source: '2023022171679817', target: '2023083016776599', id: '2024013019576232' },\n      { source: '2023083116802327', target: '2023032149712027', id: '2023102317587829' },\n      { source: '2023083116802327', target: '2023092717337264', id: '2023092717340047' },\n      { source: '2023083116802328', target: '2023092717337264', id: '2023092717340048' },\n      { source: '2023083116802329', target: '2023092717337264', id: '2023092717340049' },\n      { source: '2023092717337264', target: '2022042607099685', id: '2023092717337342' },\n      { source: '2023053113971286', target: '2023053113971286', id: '2023071415272433' },\n      { source: '2023062814858004', target: '2023062814858004', id: '2023062814858057' },\n      { source: '2023062614749332', target: '2023062614749332', id: '2023062614749380' },\n      { source: '2023022159807939', target: '2023053113971286', id: '2023053113971328' },\n      { source: '2023053113971286', target: '2023053113971286', id: '2023053113971329' },\n      { source: '2023032171523093', target: '2023032171523093', id: '2023032188641552' },\n      { source: '2023032138615654', target: '2023032138615654', id: '2023032163869608' },\n      { source: '2023032439702273', target: '2023032439702273', id: '2023032461304126' },\n      { source: '2023033152992057', target: '2023032149712027', id: '2023033129305007' },\n      { source: '2023033152992057', target: '2023022134006229', id: '2023040438213028' },\n      { source: '2023032121629632', target: '2023032121629632', id: '2023032184876493' },\n      { source: '2023022124015954', target: '2023032149712027', id: '2023041947056477' },\n      { source: '2023022134006229', target: '2023022192941079', id: '2023022160953640' },\n      { source: '2023033152992057', target: '2023022124015954', id: '2023041466404158' },\n      { source: '2023022192941079', target: '2023032149712027', id: '2023032412723482' },\n      { source: '2023022124015954', target: '2023022131662846', id: '2023041985653086' },\n      { source: '2023032149712027', target: '2023032149712027', id: '2023032177181441' },\n      { source: '2023022134006229', target: '2023022135378377', id: '2023022191267699' },\n      { source: '2023022134006229', target: '2023022115050705', id: '2023022113765807' },\n      { source: '2022042607099685', target: '2022042607099685', id: '2022042607099906' },\n      { source: '2023022192941079', target: '13', id: '1000' },\n      { source: '2023083116802329', target: '10', id: '1001' },\n      { source: '2023022124015954', target: '11', id: '1002' },\n      { source: '2023033152992057', target: '10', id: '1003' },\n    ],\n  };\n\n  const graph = new Graph({\n    ...context,\n    data,\n    layout: [\n      { type: 'mds', animation: false },\n      {\n        type: 'force',\n        animation: false,\n        preventOverlap: true,\n        nodeSize: 32,\n        maxSpeed: 500,\n        leafCluster: true,\n        clustering: false,\n        clusterNodeStrength: 35,\n        minMovement: 1.5,\n      },\n    ],\n    autoFit: 'view',\n  });\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-radial-basic.ts",
    "content": "import data from '@@/dataset/radial.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutRadialBasic: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n        labelPlacement: 'center',\n      },\n    },\n    layout: {\n      type: 'radial',\n      unitRadius: 50,\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-radial-configuration-translate.ts",
    "content": "import data from '@@/dataset/radial.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutRadialConfigurationTranslate: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n        labelPlacement: 'center',\n      },\n    },\n    edge: {\n      style: {\n        endArrow: true,\n        endArrowType: 'vee',\n      },\n    },\n    layout: {\n      type: 'radial',\n      unitRadius: 50,\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-radial-prevent-overlap-unstrict.ts",
    "content": "import data from '@@/dataset/radial.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutRadialPreventOverlapUnstrict: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n        labelPlacement: 'center',\n      },\n    },\n    edge: {\n      style: {\n        endArrow: true,\n        endArrowType: 'vee',\n      },\n    },\n    layout: {\n      type: 'radial',\n      unitRadius: 70,\n      preventOverlap: true,\n      strictRadial: false,\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-radial-prevent-overlap.ts",
    "content": "import data from '@@/dataset/radial.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutRadialPreventOverlap: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n        labelPlacement: 'center',\n      },\n    },\n    edge: {\n      style: {\n        endArrow: true,\n        endArrowType: 'vee',\n      },\n    },\n    layout: {\n      type: 'radial',\n      unitRadius: 50,\n      preventOverlap: true,\n      maxPreventOverlapIteration: 100,\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-radial-sort.ts",
    "content": "import data from '@@/dataset/radial.json';\nimport { Graph } from '@antv/g6';\n\nexport const layoutRadialSort: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n        labelPlacement: 'center',\n      },\n    },\n    edge: {\n      style: {\n        endArrow: true,\n        endArrowType: 'vee',\n      },\n    },\n    layout: {\n      type: 'radial',\n      unitRadius: 70,\n      maxIteration: 1000,\n      linkDistance: 10,\n      preventOverlap: true,\n      nodeSize: 30,\n      sortBy: (d) => d!.data.sortAttr2,\n      sortStrength: 50,\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/layout-snake.ts",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0', data: { label: '开始流程', time: '17:00:00' } },\n    { id: '1', data: { label: '流程1', time: '17:00:05' } },\n    { id: '2', data: { label: '流程2', time: '17:00:12' } },\n    { id: '3', data: { label: '流程3', time: '17:00:30' } },\n    { id: '4', data: { label: '流程4', time: '17:02:00' } },\n    { id: '5', data: { label: '流程5', time: '17:02:40' } },\n    { id: '6', data: { label: '流程6', time: '17:05:50' } },\n    { id: '7', data: { label: '流程7', time: '17:10:00' } },\n    { id: '8', data: { label: '流程8', time: '17:11:20' } },\n    { id: '9', data: { label: '流程9', time: '17:15:00' } },\n    { id: '10', data: { label: '流程10', time: '17:30:00' } },\n    { id: '11', data: { label: '流程11' } },\n    { id: '12', data: { label: '流程12' } },\n    { id: '13', data: { label: '流程13' } },\n    { id: '14', data: { label: '流程14' } },\n    { id: '15', data: { label: '流程结束' } },\n  ],\n  edges: [\n    { source: '0', target: '1', data: { done: true } },\n    { source: '1', target: '2', data: { done: true } },\n    { source: '2', target: '3', data: { done: true } },\n    { source: '3', target: '4', data: { done: true } },\n    { source: '4', target: '5', data: { done: true } },\n    { source: '5', target: '6', data: { done: true } },\n    { source: '6', target: '7', data: { done: true } },\n    { source: '7', target: '8', data: { done: true } },\n    { source: '8', target: '9', data: { done: true } },\n    { source: '9', target: '10', data: { done: true } },\n    { source: '10', target: '11', data: { done: false } },\n    { source: '11', target: '12', data: { done: false } },\n    { source: '12', target: '13', data: { done: false } },\n    { source: '13', target: '14', data: { done: false } },\n    { source: '14', target: '15', data: { done: false } },\n  ],\n};\n\nexport const layoutSnake: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        labelText: (d) => d.id,\n        labelPlacement: 'center',\n        labelFill: '#fff',\n      },\n    },\n    edge: {\n      style: {\n        endArrow: true,\n      },\n    },\n    layout: {\n      key: 'snake',\n      type: 'snake',\n    },\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/perf-20000-elements.ts",
    "content": "import type { GraphOptions } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nexport const perf20000Elements: TestCase = async (context) => {\n  const data = await fetch('https://assets.antv.antgroup.com/g6/20000.json').then((res) => res.json());\n\n  const graph = new Graph({\n    ...context,\n    animation: false,\n    data,\n    node: {\n      style: {\n        size: 8,\n        fill: 'gray',\n      },\n    },\n    theme: false,\n    behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n    autoFit: 'view',\n    plugins: [{ type: 'background', background: '#fff' }],\n  });\n\n  console.time('time');\n  await graph.render();\n  console.timeEnd('time');\n\n  perf20000Elements.form = (gui) => {\n    const themes: Record<string, GraphOptions> = {\n      '🌞 Light': {\n        theme: 'light',\n        node: {\n          palette: {\n            type: 'group',\n            field: 'cluster',\n          },\n        },\n        plugins: [{ type: 'background', background: '#fff' }],\n      },\n      '🌚 Dark': {\n        theme: 'dark',\n        node: {\n          palette: {\n            type: 'group',\n            field: 'cluster',\n          },\n        },\n        plugins: [{ type: 'background', background: '#000' }],\n      },\n      '🌎 Blue': {\n        theme: 'light',\n        node: {\n          palette: {\n            type: 'group',\n            field: 'cluster',\n            color: 'blues',\n            invert: true,\n          },\n        },\n        plugins: [{ type: 'background', background: '#f3faff' }],\n      },\n      '🌕 Yellow': {\n        background: '#fcf9f1',\n        theme: 'light',\n        node: {\n          palette: {\n            type: 'group',\n            field: 'cluster',\n            color: ['#ffe7ba', '#ffd591', '#ffc069', '#ffa940', '#fa8c16', '#d46b08', '#ad4e00', '#873800', '#612500'],\n          },\n        },\n        plugins: [{ type: 'background', background: '#fcf9f1' }],\n      },\n    };\n\n    return [\n      gui.add({ theme: '🌞 Light' }, 'theme', Object.keys(themes)).onChange((theme: string) => {\n        graph.setOptions(themes[theme]);\n        graph.draw();\n      }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/perf-fcp.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const perfFCP: TestCase = async (context) => {\n  const data = {\n    nodes: new Array(1000).fill(undefined).map((_, i) => ({ id: `${i}` })),\n  };\n\n  const graph = new Graph({\n    ...context,\n    animation: false,\n    data,\n    node: {\n      type: 'circle', // 👈🏻 Node shape type.\n      style: {\n        size: 40,\n        labelText: (d) => d.id!,\n        iconHeight: 20,\n        iconWidth: 20,\n        iconFontFamily: 'iconfont',\n        iconText: '\\ue602',\n      },\n    },\n    layout: {\n      type: 'grid',\n    },\n  });\n\n  const timeStart = performance.now();\n\n  await graph.render();\n\n  const timeElapsed = performance.now() - timeStart;\n  console.log('timeElapsed', timeElapsed);\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-background.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const pluginBackground: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoResize: true,\n    data,\n    layout: { type: 'd3-force' },\n    behaviors: ['drag-canvas', 'drag-element'],\n    plugins: [\n      {\n        type: 'background',\n        key: 'background',\n        backgroundImage:\n          'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*0Qq0ToQm1rEAAAAAAAAAAAAADmJ7AQ/original)',\n      },\n    ],\n  });\n\n  await graph.render();\n\n  pluginBackground.form = (panel) => {\n    const config = {\n      backgroundSize: 'cover',\n    };\n    return [\n      panel\n        .add(config, 'backgroundSize', {\n          Cover: 'cover',\n          Contain: 'contain',\n        })\n        .name('backgroundSize')\n        .onChange((backgroundSize: string) => {\n          graph.updatePlugin({\n            key: 'background',\n            backgroundSize,\n          });\n        }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-bubble-sets.ts",
    "content": "import type { BubbleSetsOptions } from '@/src/plugins';\nimport { idOf } from '@/src/utils/id';\nimport { BubbleSets, Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node0',\n      style: { size: 50, x: 220, y: 326 },\n    },\n    {\n      id: 'node1',\n      style: { size: 30, x: 426, y: 421 },\n    },\n    {\n      id: 'node2',\n      style: { size: 30, x: 329, y: 88 },\n    },\n    {\n      id: 'node3',\n      style: { size: 30, x: -16, y: 255 },\n    },\n    {\n      id: 'node4',\n      style: { size: 30, x: 79, y: 493 },\n    },\n    {\n      id: 'node5',\n      style: { size: 30, x: 235, y: 540 },\n    },\n    {\n      id: 'node6',\n      style: { size: 15, x: 428, y: 547 },\n    },\n    {\n      id: 'node7',\n      style: { size: 15, x: 546, y: 371 },\n    },\n    {\n      id: 'node8',\n      style: { size: 15, x: 333, y: -57 },\n    },\n    {\n      id: 'node9',\n      style: { size: 15, x: 202, y: -8 },\n    },\n    {\n      id: 'node10',\n      style: { size: 15, x: 473, y: 145 },\n    },\n    {\n      id: 'node11',\n      style: { size: 15, x: 458, y: 12 },\n    },\n    {\n      id: 'node12',\n      style: { size: 15, x: 353, y: 221 },\n    },\n    {\n      id: 'node13',\n      style: { size: 15, x: 201, y: 133 },\n    },\n    {\n      id: 'node14',\n      style: { size: 15, x: 94, y: 241 },\n    },\n    {\n      id: 'node15',\n      style: { size: 15, x: -67, y: 127 },\n    },\n    {\n      id: 'node16',\n      style: { size: 15, x: -91, y: 359 },\n    },\n  ],\n  edges: [\n    {\n      id: 'edge1',\n      source: 'node0',\n      target: 'node1',\n    },\n    {\n      id: 'edge2',\n      source: 'node0',\n      target: 'node2',\n    },\n    {\n      id: 'edge3',\n      source: 'node0',\n      target: 'node3',\n    },\n    {\n      id: 'edge4',\n      source: 'node0',\n      target: 'node4',\n    },\n    {\n      id: 'edge5',\n      source: 'node0',\n      target: 'node5',\n    },\n    {\n      id: 'edge6',\n      source: 'node1',\n      target: 'node6',\n    },\n    {\n      id: 'edge7',\n      source: 'node1',\n      target: 'node7',\n    },\n    {\n      id: 'edge8',\n      source: 'node2',\n      target: 'node8',\n    },\n    {\n      id: 'edge9',\n      source: 'node2',\n      target: 'node9',\n    },\n    {\n      id: 'edge10',\n      source: 'node2',\n      target: 'node10',\n    },\n    {\n      id: 'edge11',\n      source: 'node2',\n      target: 'node11',\n    },\n    {\n      id: 'edge12',\n      source: 'node2',\n      target: 'node12',\n    },\n    {\n      id: 'edge13',\n      source: 'node2',\n      target: 'node13',\n    },\n    {\n      id: 'edge14',\n      source: 'node3',\n      target: 'node14',\n    },\n    {\n      id: 'edge15',\n      source: 'node3',\n      target: 'node15',\n    },\n    {\n      id: 'edge16',\n      source: 'node3',\n      target: 'node16',\n    },\n  ],\n  combos: [],\n};\n\nexport const pluginBubbleSets: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    behaviors: ['drag-canvas', 'drag-element'],\n    plugins: [\n      {\n        key: 'bubble-sets',\n        type: 'bubble-sets',\n        members: ['node0', 'node1'],\n        labelText: 'Bubble',\n      },\n    ],\n    node: { style: { labelText: (d) => d.id } },\n    autoFit: 'view',\n  });\n\n  await graph.render();\n\n  pluginBubbleSets.form = (panel) => {\n    const bubblesets = graph.getPluginInstance<BubbleSets>('bubble-sets');\n\n    const config = {\n      member: 'node0',\n      // default options in bubblesets-js\n      // More info see: https://github.com/upsetjs/bubblesets-js/blob/main/src/BubbleSets.ts\n      maxRoutingIterations: 100,\n      maxMarchingIterations: 20,\n      pixelGroup: 4,\n      edgeR0: 10,\n      edgeR1: 20,\n      nodeR0: 15,\n      nodeR1: 50,\n      morphBuffer: 10,\n      threshold: 1,\n      memberInfluenceFactor: 1,\n      edgeInfluenceFactor: 1,\n      AvoidMemberInfluenceFactor: -0.8,\n      virtualEdges: true,\n    };\n\n    const members = [\n      ...graph.getNodeData().map(idOf),\n      ...graph.getEdgeData().map(idOf),\n      ...graph.getComboData().map(idOf),\n    ];\n\n    const panels = [\n      panel.add(config, 'member', members).name('Element'),\n      panel\n        .add(\n          {\n            AddMember: () => {\n              bubblesets.addMember(config.member);\n            },\n          },\n          'AddMember',\n        )\n        .name('Add Element as Member'),\n      panel\n        .add(\n          {\n            RemoveMember: () => {\n              bubblesets.removeMember(config.member);\n            },\n          },\n          'RemoveMember',\n        )\n        .name('Remove Element as Member'),\n      panel\n        .add(\n          {\n            AddAvoidMember: () => {\n              bubblesets.addAvoidMember(config.member);\n            },\n          },\n          'AddAvoidMember',\n        )\n        .name('Add Element as Non-Member'),\n\n      panel\n        .add(\n          {\n            RemoveMember: () => {\n              bubblesets.removeAvoidMember(config.member);\n            },\n          },\n          'RemoveMember',\n        )\n        .name('Remove Element as Non-Member'),\n    ];\n\n    const updateOptions = (options: BubbleSetsOptions) => {\n      graph.updatePlugin({\n        key: 'bubble-sets',\n        ...options,\n      });\n      graph.render();\n    };\n\n    Object.keys(config)\n      .slice(1, -1)\n      .forEach((key) => {\n        panels.push(\n          panel.add(config, key, 0, 100, 1).onChange((value: number) => {\n            updateOptions({ [key]: value } as BubbleSetsOptions);\n          }),\n        );\n      });\n\n    panels.push(\n      panel.add(config, 'virtualEdges').onChange((value: boolean) => {\n        updateOptions({ virtualEdges: value } as BubbleSetsOptions);\n      }),\n    );\n\n    return panels;\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-camera-setting.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { CameraSetting, ExtensionCategory, Graph, register } from '@antv/g6';\n\nexport const pluginCameraSetting: TestCase = async (context) => {\n  register(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting);\n\n  const graph = new Graph({\n    ...context,\n    data,\n    layout: { type: 'd3-force' },\n    plugins: [{ key: 'camera-setting', type: 'camera-setting' }],\n  });\n\n  pluginCameraSetting.form = (panel) => {\n    const config = {\n      cameraType: 'orbiting',\n      near: 0.1,\n      far: 1000,\n      fov: 45,\n      // aspect: 'auto',\n      projectionMode: 'orthographic',\n      distance: 500,\n      roll: 0,\n      elevation: 0,\n      azimuth: 0,\n    };\n\n    const handleChange = () => {\n      graph.updatePlugin({\n        key: 'camera-setting',\n        type: 'camera-setting',\n        ...config,\n      });\n    };\n\n    panel.onChange(handleChange);\n\n    return [\n      panel.add(config, 'cameraType', ['orbiting', 'exploring', 'tracking']).name('Camera Type'),\n      panel.add(config, 'near', 0.1, 10, 0.1).name('Near'),\n      panel.add(config, 'far', 100, 1000, 10).name('Far'),\n      panel.add(config, 'fov', 0, 180, 1).name('Fov'),\n      // panel.add(config, 'aspect', ['auto', 'custom']).name('Aspect'),\n      panel.add(config, 'projectionMode', ['orthographic', 'perspective']).name('Projection Mode'),\n      panel.add(config, 'distance', 100, 1000, 10).name('Distance'),\n      panel.add(config, 'roll', -180, 180, 1).name('Roll'),\n      panel.add(config, 'elevation', -90, 90, 1).name('Elevation'),\n      panel.add(config, 'azimuth', -180, 180, 1).name('Azimuth'),\n    ];\n  };\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-contextmenu.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const pluginContextmenu: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoResize: true,\n    data,\n    layout: { type: 'd3-force' },\n    behaviors: ['drag-canvas'],\n    plugins: [\n      {\n        key: 'contextmenu',\n        type: 'contextmenu',\n        trigger: 'contextmenu',\n        className: 'custom-class-name',\n        getItems: () => {\n          return [\n            { name: '展开一度关系', value: 'spread' },\n            { name: '查看详情', value: 'detail' },\n          ];\n        },\n        enable: (e: any) => e.targetType === 'node',\n      },\n    ],\n  });\n\n  await graph.render();\n\n  pluginContextmenu.form = (panel) => {\n    const config = {\n      trigger: 'contextmenu',\n    };\n    return [\n      panel\n        .add(config, 'trigger', {\n          Click: 'click',\n          Contextmenu: 'contextmenu',\n        })\n        .name('Trigger')\n        .onChange((trigger: string) => {\n          graph.setPlugins([\n            {\n              type: 'contextmenu',\n              trigger,\n              getContextmenuItems: () => {\n                return [\n                  { name: '展开一度关系', value: 'spread' },\n                  { name: '查看详情', value: 'detail' },\n                ];\n              },\n              enable: (e: any) => e.targetType === 'node',\n            },\n          ]);\n          graph.render();\n        }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-edge-bundling.ts",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: '0',\n      label: '0',\n    },\n    {\n      id: '1',\n      label: '1',\n    },\n    {\n      id: '2',\n      label: '2',\n    },\n    {\n      id: '3',\n      label: '3',\n    },\n    {\n      id: '4',\n      label: '4',\n    },\n    {\n      id: '5',\n      label: '5',\n    },\n    {\n      id: '6',\n      label: '6',\n    },\n    {\n      id: '7',\n      label: '7',\n    },\n    {\n      id: '8',\n      label: '8',\n    },\n    {\n      id: '9',\n      label: '9',\n    },\n    {\n      id: '10',\n      label: '10',\n    },\n    {\n      id: '11',\n      label: '11',\n    },\n    {\n      id: '12',\n      label: '12',\n    },\n    {\n      id: '13',\n      label: '13',\n    },\n    {\n      id: '14',\n      label: '14',\n    },\n    {\n      id: '15',\n      label: '15',\n    },\n    {\n      id: '16',\n      label: '16',\n    },\n    {\n      id: '17',\n      label: '17',\n    },\n    {\n      id: '18',\n      label: '18',\n    },\n    {\n      id: '19',\n      label: '19',\n    },\n    {\n      id: '20',\n      label: '20',\n    },\n    {\n      id: '21',\n      label: '21',\n    },\n    {\n      id: '22',\n      label: '22',\n    },\n    {\n      id: '23',\n      label: '23',\n    },\n    {\n      id: '24',\n      label: '24',\n    },\n    {\n      id: '25',\n      label: '25',\n    },\n    {\n      id: '26',\n      label: '26',\n    },\n    {\n      id: '27',\n      label: '27',\n    },\n    {\n      id: '28',\n      label: '28',\n    },\n    {\n      id: '29',\n      label: '29',\n    },\n    {\n      id: '30',\n      label: '30',\n    },\n    {\n      id: '31',\n      label: '31',\n    },\n    {\n      id: '32',\n      label: '32',\n    },\n    {\n      id: '33',\n      label: '33',\n    },\n  ],\n  edges: [\n    {\n      source: '0',\n      target: '1',\n    },\n    {\n      source: '0',\n      target: '2',\n    },\n    {\n      source: '0',\n      target: '3',\n    },\n    {\n      source: '0',\n      target: '4',\n    },\n    {\n      source: '0',\n      target: '5',\n    },\n    {\n      source: '0',\n      target: '7',\n    },\n    {\n      source: '0',\n      target: '8',\n    },\n    {\n      source: '0',\n      target: '9',\n    },\n    {\n      source: '0',\n      target: '10',\n    },\n    {\n      source: '0',\n      target: '11',\n    },\n    {\n      source: '0',\n      target: '13',\n    },\n    {\n      source: '0',\n      target: '14',\n    },\n    {\n      source: '0',\n      target: '15',\n    },\n    {\n      source: '0',\n      target: '16',\n    },\n    {\n      source: '2',\n      target: '3',\n    },\n    {\n      source: '4',\n      target: '5',\n    },\n    {\n      source: '4',\n      target: '6',\n    },\n    {\n      source: '5',\n      target: '6',\n    },\n    {\n      source: '7',\n      target: '13',\n    },\n    {\n      source: '8',\n      target: '14',\n    },\n    {\n      source: '9',\n      target: '10',\n    },\n    {\n      source: '10',\n      target: '22',\n    },\n    {\n      source: '10',\n      target: '14',\n    },\n    {\n      source: '10',\n      target: '12',\n    },\n    {\n      source: '10',\n      target: '24',\n    },\n    {\n      source: '10',\n      target: '21',\n    },\n    {\n      source: '10',\n      target: '20',\n    },\n    {\n      source: '11',\n      target: '24',\n    },\n    {\n      source: '11',\n      target: '22',\n    },\n    {\n      source: '11',\n      target: '14',\n    },\n    {\n      source: '12',\n      target: '13',\n    },\n    {\n      source: '16',\n      target: '17',\n    },\n    {\n      source: '16',\n      target: '18',\n    },\n    {\n      source: '16',\n      target: '21',\n    },\n    {\n      source: '16',\n      target: '22',\n    },\n    {\n      source: '17',\n      target: '18',\n    },\n    {\n      source: '17',\n      target: '20',\n    },\n    {\n      source: '18',\n      target: '19',\n    },\n    {\n      source: '19',\n      target: '20',\n    },\n    {\n      source: '19',\n      target: '33',\n    },\n    {\n      source: '19',\n      target: '22',\n    },\n    {\n      source: '19',\n      target: '23',\n    },\n    {\n      source: '20',\n      target: '21',\n    },\n    {\n      source: '21',\n      target: '22',\n    },\n    {\n      source: '22',\n      target: '24',\n    },\n    {\n      source: '22',\n      target: '25',\n    },\n    {\n      source: '22',\n      target: '26',\n    },\n    {\n      source: '22',\n      target: '23',\n    },\n    {\n      source: '22',\n      target: '28',\n    },\n    {\n      source: '22',\n      target: '30',\n    },\n    {\n      source: '22',\n      target: '31',\n    },\n    {\n      source: '22',\n      target: '32',\n    },\n    {\n      source: '22',\n      target: '33',\n    },\n    {\n      source: '23',\n      target: '28',\n    },\n    {\n      source: '23',\n      target: '27',\n    },\n    {\n      source: '23',\n      target: '29',\n    },\n    {\n      source: '23',\n      target: '30',\n    },\n    {\n      source: '23',\n      target: '31',\n    },\n    {\n      source: '23',\n      target: '33',\n    },\n    {\n      source: '32',\n      target: '33',\n    },\n  ],\n};\n\nexport const pluginEdgeBundling: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        size: 12,\n        labelText: (d) => d.id,\n        labelFontSize: 8,\n        labelFill: '#252525',\n        labelFontFamily: 'Futura',\n      },\n    },\n    layout: {\n      type: 'circular',\n      radius: 200,\n    },\n    plugins: ['edge-bundling'],\n    animation: false,\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-edge-filter-lens.ts",
    "content": "import data from '@@/dataset/relations.json';\nimport { Graph } from '@antv/g6';\n\nexport const pluginEdgeFilterLens: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: { size: 16 },\n      palette: {\n        field: (datum) => Math.floor(Number(datum.style?.y) / 60),\n      },\n    },\n    edge: {\n      style: {\n        label: false,\n        labelText: (d) => d.data!.value?.toString(),\n        stroke: '#ccc',\n      },\n    },\n    plugins: [\n      {\n        key: 'edge-filter-lens',\n        type: 'edge-filter-lens',\n      },\n    ],\n    autoFit: 'view',\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-fisheye.ts",
    "content": "import data from '@@/dataset/relations.json';\nimport type { NodeData } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nexport const pluginFisheye: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data,\n    node: {\n      style: {\n        size: (datum: NodeData) => datum.id.length * 2 + 10,\n        label: false,\n        labelText: (datum: NodeData) => datum.id,\n        labelBackground: true,\n        icon: false,\n        iconFontFamily: 'iconfont',\n        iconText: '\\ue6f6',\n        iconFill: '#fff',\n      },\n      palette: {\n        type: 'group',\n        field: (datum: NodeData) => datum.id,\n        color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n      },\n    },\n    edge: {\n      style: {\n        stroke: '#e2e2e2',\n      },\n    },\n    plugins: [{ key: 'fisheye', type: 'fisheye', nodeStyle: { label: true, icon: true } }],\n  });\n\n  await graph.render();\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-fullscreen.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Fullscreen, Graph } from '@antv/g6';\n\nexport const pluginFullscreen: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    layout: { type: 'd3-force' },\n    plugins: [\n      {\n        type: 'fullscreen',\n        key: 'fullscreen',\n      },\n    ],\n  });\n\n  graph.setPlugins((prev) => [\n    ...prev,\n    {\n      type: 'toolbar',\n      key: 'toolbar',\n      position: 'top-left',\n      onClick: (item: string) => {\n        const fullscreenPlugin = graph.getPluginInstance<Fullscreen>('fullscreen');\n        if (item === 'request-fullscreen') {\n          fullscreenPlugin.request();\n        }\n        if (item === 'exit-fullscreen') {\n          fullscreenPlugin.exit();\n        }\n      },\n      getItems: () => {\n        return [\n          { id: 'request-fullscreen', value: 'request-fullscreen' },\n          { id: 'exit-fullscreen', value: 'exit-fullscreen' },\n        ];\n      },\n    },\n  ]);\n\n  await graph.render();\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-grid-line.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const pluginGridLine: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoResize: true,\n    data,\n    layout: { type: 'd3-force' },\n    behaviors: ['drag-canvas', 'zoom-canvas'],\n    plugins: [{ type: 'grid-line', follow: false }],\n  });\n\n  await graph.render();\n\n  pluginGridLine.form = (panel) => {\n    const config = {\n      resize: () => {\n        const $container = document.getElementById('container')!;\n        Object.assign($container.style, { width: '600px', height: '600px' });\n        window.dispatchEvent(new Event('resize'));\n      },\n      follow: false,\n      size: 20,\n    };\n    return [\n      panel.add(config, 'resize').name('Emit Resize'),\n      panel\n        .add(config, 'follow')\n        .name('Follow The Graph')\n        .onChange((follow: boolean) => {\n          graph.setPlugins((plugins) =>\n            plugins.map((plugin) => {\n              if (typeof plugin === 'object' && plugin.type === 'grid-line') return { ...plugin, follow };\n              return plugin;\n            }),\n          );\n        }),\n      panel\n        .add(config, 'size', 10, 50, 5)\n        .name('Grid Size')\n        .onChange((size: number) => {\n          graph.setPlugins((plugins) =>\n            plugins.map((plugin) => {\n              if (typeof plugin === 'object' && plugin.type === 'grid-line') return { ...plugin, size };\n              return plugin;\n            }),\n          );\n        }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-history.ts",
    "content": "import type { History } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nexport const pluginHistory: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node-1', combo: 'combo-2', style: { x: 120, y: 100 } },\n        { id: 'node-2', combo: 'combo-1', style: { x: 300, y: 200 } },\n        { id: 'node-3', combo: 'combo-1', style: { x: 200, y: 300 } },\n      ],\n      edges: [\n        { id: 'edge-1', source: 'node-1', target: 'node-2' },\n        { id: 'edge-2', source: 'node-2', target: 'node-3' },\n      ],\n      combos: [\n        {\n          id: 'combo-1',\n          type: 'rect',\n          combo: 'combo-2',\n          style: { collapsed: true },\n        },\n        { id: 'combo-2' },\n      ],\n    },\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    combo: {\n      style: {\n        labelText: (d) => d.id,\n        lineDash: 0,\n        collapsedLineDash: [5, 5],\n      },\n    },\n    behaviors: ['drag-element', 'collapse-expand'],\n    plugins: [{ key: 'history', type: 'history' }],\n  });\n\n  await graph.render();\n\n  const history = graph.getPluginInstance<History>('history');\n\n  pluginHistory.form = (panel) => {\n    const config = {\n      element: 'node-1',\n      visible: true,\n      add: () => {\n        graph.addData({\n          nodes: [{ id: 'node-5', style: { x: 200, y: 100, fill: 'pink' } }],\n          edges: [{ source: 'node-1', target: 'node-5', style: { stroke: 'brown' } }],\n        });\n        graph.draw();\n      },\n      update: () => {\n        graph.updateData({\n          nodes: [{ id: 'node-1', style: { x: 150, y: 100, fill: 'red' } }],\n          edges: [{ id: 'edge-1', style: { stroke: 'green' } }],\n        });\n        graph.draw();\n      },\n      remove: () => {\n        graph.removeData({\n          nodes: ['node-1'],\n          edges: ['edge-1'],\n        });\n        graph.draw();\n      },\n      collapse: () => graph.collapseElement('combo-2'),\n      expand: () => graph.expandElement('combo-1'),\n      state: () => graph.setElementState('node-1', 'selected', true),\n      zIndex: () => graph.setElementZIndex('combo-2', 100),\n      undo: () => history.undo(),\n      redo: () => history.redo(),\n      clear: () => history.clear(),\n    };\n    const visible = panel\n      .add(config, 'visible')\n      .name('node-1 visibility')\n      .onChange((value: boolean) => {\n        value ? graph.showElement(config.element) : graph.hideElement(config.element);\n      });\n    return [\n      visible,\n      panel.add(config, 'add').name('add'),\n      panel.add(config, 'update').name('update'),\n      panel.add(config, 'remove').name('remove'),\n      panel.add(config, 'collapse').name('collapse combo2'),\n      panel.add(config, 'expand').name('expand combo1'),\n      panel.add(config, 'state').name('set node1 selected'),\n      panel.add(config, 'zIndex').name('front combo2'),\n      panel.add(config, 'undo'),\n      panel.add(config, 'redo'),\n      panel.add(config, 'clear'),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-hull.ts",
    "content": "import { HullOptions } from '@/src/plugins';\nimport type { CardinalPlacement } from '@/src/types';\nimport { Graph, Hull } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node0',\n      style: { size: 50, x: 220, y: 326 },\n    },\n    {\n      id: 'node1',\n      style: { size: 30, x: 426, y: 421 },\n    },\n    {\n      id: 'node2',\n      style: { size: 30, x: 329, y: 88 },\n    },\n    {\n      id: 'node3',\n      style: { size: 30, x: -16, y: 255 },\n    },\n    {\n      id: 'node4',\n      style: { size: 30, x: 79, y: 493 },\n    },\n    {\n      id: 'node5',\n      style: { size: 30, x: 235, y: 540 },\n    },\n    {\n      id: 'node6',\n      style: { size: 15, x: 428, y: 547 },\n    },\n    {\n      id: 'node7',\n      style: { size: 15, x: 546, y: 371 },\n    },\n    {\n      id: 'node8',\n      style: { size: 15, x: 333, y: -57 },\n    },\n    {\n      id: 'node9',\n      style: { size: 15, x: 202, y: -8 },\n    },\n    {\n      id: 'node10',\n      style: { size: 15, x: 473, y: 145 },\n    },\n    {\n      id: 'node11',\n      style: { size: 15, x: 458, y: 12 },\n    },\n    {\n      id: 'node12',\n      style: { size: 15, x: 353, y: 221 },\n    },\n    {\n      id: 'node13',\n      style: { size: 15, x: 201, y: 133 },\n    },\n    {\n      id: 'node14',\n      style: { size: 15, x: 94, y: 241 },\n    },\n    {\n      id: 'node15',\n      style: { size: 15, x: -67, y: 127 },\n    },\n    {\n      id: 'node16',\n      style: { size: 15, x: -91, y: 359 },\n    },\n  ],\n  edges: [\n    {\n      id: 'edge1',\n      source: 'node0',\n      target: 'node1',\n    },\n    {\n      id: 'edge2',\n      source: 'node0',\n      target: 'node2',\n    },\n    {\n      id: 'edge3',\n      source: 'node0',\n      target: 'node3',\n    },\n    {\n      id: 'edge4',\n      source: 'node0',\n      target: 'node4',\n    },\n    {\n      id: 'edge5',\n      source: 'node0',\n      target: 'node5',\n    },\n    {\n      id: 'edge6',\n      source: 'node1',\n      target: 'node6',\n    },\n    {\n      id: 'edge7',\n      source: 'node1',\n      target: 'node7',\n    },\n    {\n      id: 'edge8',\n      source: 'node2',\n      target: 'node8',\n    },\n    {\n      id: 'edge9',\n      source: 'node2',\n      target: 'node9',\n    },\n    {\n      id: 'edge10',\n      source: 'node2',\n      target: 'node10',\n    },\n    {\n      id: 'edge11',\n      source: 'node2',\n      target: 'node11',\n    },\n    {\n      id: 'edge12',\n      source: 'node2',\n      target: 'node12',\n    },\n    {\n      id: 'edge13',\n      source: 'node2',\n      target: 'node13',\n    },\n    {\n      id: 'edge14',\n      source: 'node3',\n      target: 'node14',\n    },\n    {\n      id: 'edge15',\n      source: 'node3',\n      target: 'node15',\n    },\n    {\n      id: 'edge16',\n      source: 'node3',\n      target: 'node16',\n    },\n  ],\n  combos: [],\n};\n\nexport const pluginHull: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    behaviors: ['drag-canvas', 'drag-element'],\n    plugins: [\n      {\n        key: 'hull',\n        type: 'hull',\n        members: ['node0', 'node1', 'node2'],\n        labelText: 'convex hull',\n        labelFontWeight: '700',\n        labelBackground: true,\n        labelBackgroundFill: 'pink',\n        lineWidth: 5,\n      },\n    ],\n    node: {\n      style: { labelText: (d) => d.id },\n    },\n    edge: {\n      style: {},\n    },\n    autoFit: 'view',\n  });\n\n  await graph.render();\n\n  const hull = graph.getPluginInstance<Hull>('hull');\n\n  const updateHullOptions = (optionsToUpdate: Partial<HullOptions>) => {\n    graph.updatePlugin({ key: 'hull', ...optionsToUpdate });\n    graph.render();\n  };\n\n  pluginHull.form = (panel) => {\n    const nodeIds = graph.getNodeData().map((node) => node.id);\n    const config = {\n      concavity: 100,\n      padding: 10,\n      corner: 'rounded',\n      labelPlacement: 'bottom',\n      labelCloseToPath: true,\n      labelAutoRotate: true,\n      node: 'node0',\n    };\n    return [\n      panel.add(config, 'concavity', 0, 100, 1).onChange((concavity: number) => {\n        updateHullOptions({ concavity });\n      }),\n      panel.add(config, 'padding', 0, 100, 1).onChange((padding: number) => {\n        updateHullOptions({ padding });\n      }),\n      panel\n        .add(config, 'corner', ['rounded', 'smooth', 'sharp'])\n        .name('Corner Type')\n        .onChange((corner: 'rounded' | 'smooth' | 'sharp') => {\n          updateHullOptions({ corner });\n        }),\n      panel\n        .add(config, 'labelPlacement', ['top', 'bottom', 'left', 'right'])\n        .name('Label Placement')\n        .onChange((labelPlacement: CardinalPlacement) => {\n          updateHullOptions({ labelPlacement });\n        }),\n      panel\n        .add(config, 'labelCloseToPath')\n        .name('Label Close To Path')\n        .onChange((labelCloseToPath: boolean) => {\n          updateHullOptions({ labelCloseToPath });\n        }),\n      panel\n        .add(config, 'labelAutoRotate')\n        .name('Label Auto Rotate')\n        .onChange((labelAutoRotate: boolean) => {\n          updateHullOptions({ labelAutoRotate });\n        }),\n      panel.add(config, 'node', nodeIds).name('Node'),\n      panel\n        .add(\n          {\n            AddMember: () => {\n              hull.addMember(config.node);\n            },\n          },\n          'AddMember',\n        )\n        .name('Add Member'),\n      panel\n        .add(\n          {\n            RemoveMember: () => {\n              hull.removeMember(config.node);\n            },\n          },\n          'RemoveMember',\n        )\n        .name('Remove Member'),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-legend.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const pluginLegend: TestCase = async (context) => {\n  const { nodes, edges } = data;\n  const findCluster = (id: string) => {\n    return nodes.find(({ id: node }) => node === id)?.data.cluster;\n  };\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes,\n      edges: edges.map(({ source, target }) => {\n        return {\n          source,\n          target,\n          id: `${source}-${target}`,\n          data: {\n            cluster: `${findCluster(source)}-${findCluster(target)}`,\n          },\n        };\n      }),\n    },\n    layout: { type: 'd3-force' },\n    behaviors: ['drag-canvas', 'drag-element', 'zoom-canvas'],\n    node: {\n      type: (item: any) => {\n        if (item.data.cluster === 'a') return 'diamond';\n        if (item.data.cluster === 'b') return 'rect';\n        if (item.data.cluster === 'c') return 'triangle';\n        return 'circle';\n      },\n      style: {\n        labelText: (d) => d.id,\n        lineWidth: 0,\n        fill: (item: any) => {\n          if (item.data.cluster === 'a') return 'red';\n          if (item.data.cluster === 'b') return 'blue';\n          if (item.data.cluster === 'c') return 'green';\n          return '#99add1';\n        },\n      },\n    },\n    plugins: [\n      {\n        key: 'legend',\n        type: 'legend',\n        titleText: 'Cluster Legend',\n        nodeField: 'cluster',\n        edgeField: 'cluster',\n        trigger: 'click',\n      },\n    ],\n  });\n\n  await graph.render();\n\n  pluginLegend.form = (panel) => {\n    const config = {\n      trigger: 'hover',\n    };\n    return [\n      panel\n        .add(config, 'trigger', ['hover', 'click'])\n        .name('Change Trigger Method')\n        .onChange((trigger: string) => {\n          graph.setPlugins((plugins) =>\n            plugins.map((plugin) => {\n              if (typeof plugin === 'object' && plugin.type === 'legend') return { ...plugin, trigger };\n              return plugin;\n            }),\n          );\n        }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-minimap-edge-arrow.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const pluginMiniMapEdgeArrow: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        {\n          id: '0',\n          name: 'a',\n        },\n        {\n          id: '1',\n          name: 'b',\n        },\n      ],\n      edges: [\n        {\n          id: '0-1',\n          source: '0',\n          target: '1',\n        },\n      ],\n    },\n    node: {\n      style: {\n        label: true,\n        labelText: (node) => {\n          return node.id;\n        },\n      },\n    },\n    edge: {\n      style: {\n        endArrow: true,\n        label: true,\n        labelText: (edge) => {\n          return `${edge.source} < ${edge.target}`;\n        },\n      },\n    },\n    layout: {\n      type: 'force',\n      linkDistance: 50,\n      clustering: true,\n      nodeClusterBy: 'cluster',\n      clusterNodeStrength: 70,\n    },\n    plugins: [\n      {\n        key: 'minimap',\n        type: 'minimap',\n        size: [240, 160],\n      },\n    ],\n    behaviors: ['drag-element'],\n  });\n\n  await graph.render();\n\n  pluginMiniMapEdgeArrow.form = (gui) => {\n    const config = {\n      hide: () => {\n        const edge = graph.getEdgeData('0-1');\n        graph.hideElement(edge.id!);\n        // graph.render();\n      },\n      show: () => {\n        const edge = graph.getEdgeData('0-1');\n        graph.showElement(edge.id!);\n        // graph.render();\n      },\n    };\n    return [gui.add(config, 'hide'), gui.add(config, 'show')];\n  };\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-minimap.ts",
    "content": "import { Renderer } from '@antv/g-svg';\nimport { Graph } from '@antv/g6';\n\nexport const pluginMinimap: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: { nodes: Array.from({ length: 20 }).map((_, i) => ({ id: `node${i}` })) },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'hover-activate'],\n    plugins: [\n      {\n        key: 'minimap',\n        type: 'minimap',\n        size: [240, 160],\n        renderer: new Renderer(),\n      },\n    ],\n    node: {\n      palette: 'spectral',\n    },\n    layout: { type: 'circular' },\n    autoFit: 'view',\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-snapline.ts",
    "content": "import { Graph, Node } from '@antv/g6';\n\nexport const pluginSnapline: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node1', style: { x: 100, y: 100 } },\n        { id: 'node2', style: { x: 300, y: 300 } },\n        { id: 'node3', style: { x: 120, y: 200 } },\n      ],\n    },\n    node: {\n      type: (datum) => (datum.id === 'node3' ? 'circle' : 'rect'),\n      style: {\n        size: (datum) => (datum.id === 'node3' ? 40 : [60, 30]),\n        fill: 'transparent',\n        lineWidth: 2,\n        labelText: (datum) => datum.id,\n      },\n    },\n    behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n    plugins: [\n      {\n        type: 'snapline',\n        key: 'snapline',\n        verticalLineStyle: { stroke: '#F08F56', lineWidth: 2 },\n        horizontalLineStyle: { stroke: '#17C76F', lineWidth: 2 },\n        autoSnap: false,\n      },\n    ],\n  });\n\n  await graph.render();\n\n  const config = {\n    filter: false,\n    offset: 20,\n    autoSnap: false,\n  };\n\n  pluginSnapline.form = (panel) => {\n    return [\n      panel\n        .add(config, 'filter')\n        .name('Add Filter(exclude circle)')\n        .onChange((filter: boolean) => {\n          graph.updatePlugin({\n            key: 'snapline',\n            filter: (node: Node) => (filter ? node.id !== 'node3' : true),\n          });\n        }),\n      panel\n        .add(config, 'offset', [0, 20, Infinity])\n        .name('Offset')\n        .onChange((offset: string) => {\n          graph.updatePlugin({\n            key: 'snapline',\n            offset,\n          });\n        }),\n      panel\n        .add(config, 'autoSnap')\n        .name('Auto Snap')\n        .onChange((autoSnap: boolean) => {\n          graph.updatePlugin({\n            key: 'snapline',\n            autoSnap,\n          });\n        }),\n    ];\n  };\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-timebar.ts",
    "content": "import type { GraphData, Timebar } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nconst formatId = (index: number) => `0${index}`.slice(-2);\nconst formatTime = (time: number) => {\n  const year = new Date(time).getFullYear();\n  const month = new Date(time).getMonth() + 1;\n  const date = new Date(time).getDate();\n  return `${year}-${month}-${date}`;\n};\n\nexport const pluginTimebar: TestCase = async (context) => {\n  const startTime = new Date('2023-08-01 00:00:00').getTime();\n  const diff = 3600 * 24 * 1000;\n  const timebarData = [10, 2, 3, 4, 15, 10, 6].map((value, index) => ({\n    time: new Date(startTime + index * diff),\n    value,\n  }));\n\n  const [rows, cols] = [7, 7];\n\n  const data: GraphData = {\n    nodes: new Array(rows * cols).fill(0).map((_, index) => ({\n      id: `${formatId(index)}`,\n      data: {\n        timestamp: startTime + (index % cols) * diff,\n        value: index % 20,\n        label: formatTime(startTime + (index % cols) * diff),\n      },\n    })),\n    edges: [],\n  };\n\n  for (let i = 0; i < rows * cols; i++) {\n    if (i % cols < cols - 1) {\n      data.edges!.push({\n        source: `${formatId(i)}`,\n        target: `${formatId(i + 1)}`,\n      });\n    }\n\n    if (i / rows < rows - 1) {\n      data.edges!.push({\n        source: `${formatId(i)}`,\n        target: `${formatId(i + rows)}`,\n      });\n    }\n  }\n\n  const graph = new Graph({\n    ...context,\n    data,\n    node: {\n      style: {\n        labelText: (d) => `${d.data!.label}`,\n      },\n    },\n    layout: {\n      type: 'grid',\n      sortBy: 'id',\n      cols,\n      rows,\n    },\n    autoFit: 'view',\n    padding: [10, 0, 90, 0],\n    behaviors: ['drag-element'],\n    plugins: [\n      {\n        type: 'timebar',\n        key: 'timebar',\n        data: timebarData,\n        mode: 'modify',\n        padding: 40,\n      },\n    ],\n  });\n\n  pluginTimebar.form = (panel) => {\n    const config = { position: 'bottom', mode: 'modify', timebarType: 'time' };\n    const timebar = graph.getPluginInstance<Timebar>('timebar');\n    const operation = {\n      play: () => timebar.play(),\n      pause: () => timebar.pause(),\n      reset: () => timebar.reset(),\n      backward: () => timebar.backward(),\n      forward: () => timebar.forward(),\n    };\n\n    const handleChange = () => {\n      graph.updatePlugin({\n        key: 'timebar',\n        ...config,\n      });\n    };\n\n    return [\n      panel.add(config, 'position', ['top', 'bottom']).onChange((position: 'top' | 'bottom') => {\n        graph.setOptions({\n          padding: position === 'top' ? [100, 0, 10, 0] : [10, 0, 65, 0],\n        });\n        graph.updatePlugin({\n          key: 'timebar',\n          position,\n        });\n        graph.fitView();\n      }),\n      panel.add(config, 'mode', ['modify', 'visibility']).onChange(handleChange),\n      panel.add(config, 'timebarType', ['time', 'chart']).onChange(() => {\n        graph.setOptions({\n          padding: config.position === 'top' ? [100, 0, 10, 0] : [10, 0, 100, 0],\n        });\n        graph.updatePlugin({\n          key: 'timebar',\n          ...config,\n          height: 100,\n        });\n        graph.fitView();\n      }),\n      ...Object.keys(operation).map((key) => panel.add(operation, key)),\n    ];\n  };\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-title.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const pluginTitle: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: { nodes: Array.from({ length: 12 }).map((_, i) => ({ id: `node${i}` })) },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n    plugins: [\n      {\n        key: 'title',\n        type: 'title',\n\n        align: 'center',\n        spacing: 4,\n        size: 60,\n\n        title: '这是一个标题这是一个标题',\n        titleFontSize: 28,\n        titleFontFamily: 'sans-serif',\n        titleFontWeight: 600,\n        titleFill: '#fff',\n        titleFillOpacity: 1,\n        titleStroke: '#000',\n        titleLineWidth: 2,\n        titleStrokeOpacity: 1,\n\n        subtitle: '这是一个副标',\n        subtitleFontSize: 16,\n        subtitleFontFamily: 'Arial',\n        subtitleFontWeight: 300,\n        subtitleFill: '#2989FF',\n        subtitleFillOpacity: 1,\n        subtitleStroke: '#000',\n        subtitleLineWidth: 1,\n        subtitleStrokeOpacity: 0.5,\n      },\n    ],\n    node: {\n      palette: 'spectral',\n      style: { labelText: '你好' },\n    },\n    layout: {\n      type: 'circular',\n    },\n    autoFit: 'view',\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-toolbar-build-in.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const pluginToolbarBuildIn: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoResize: true,\n    data,\n    layout: { type: 'd3-force' },\n    plugins: [\n      {\n        type: 'toolbar',\n        position: 'top-left',\n        onClick: (item: string, e: MouseEvent) => {\n          console.log('item clicked:', item, e);\n        },\n        getItems: () => {\n          return [\n            { id: 'zoom-in', value: 'zoom-in', title: 'Zoom in' },\n            { id: 'zoom-out', value: 'zoom-out', title: 'Zoom out' },\n            { id: 'redo', value: 'redo', title: 'Redo' },\n            { id: 'undo', value: 'undo', title: 'Undo' },\n            { id: 'edit', value: 'edit', title: 'Edit' },\n            { id: 'delete', value: 'delete', title: 'Delete' },\n            { id: 'auto-fit', value: 'auto-fit', title: 'Auto fit' },\n            { id: 'export', value: 'export', title: 'Export' },\n            { id: 'reset', value: 'reset', title: 'Reset' },\n          ];\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n\n  pluginToolbarBuildIn.form = (panel) => {\n    const config = {\n      position: 'top-left',\n    };\n    return [\n      panel\n        .add(config, 'position', {\n          'top-right': 'top-right',\n          'top-left': 'top-left',\n          'bottom-right': 'bottom-right',\n          'bottom-left': 'bottom-left',\n          'left-top': 'left-top',\n          'left-bottom': 'left-bottom',\n          'right-top': 'right-top',\n          'right-bottom': 'right-bottom',\n        })\n        .name('Position')\n        .onChange((position: string) => {\n          graph.setPlugins([\n            {\n              type: 'toolbar',\n              position,\n              getItems: () => {\n                return [\n                  { id: 'zoom-in', value: 'zoom-in' },\n                  { id: 'zoom-out', value: 'zoom-out' },\n                ];\n              },\n              enable: true,\n            },\n          ]);\n          graph.render();\n        }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-toolbar-iconfont.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph, iconfont } from '@antv/g6';\n\nexport const pluginToolbarIconfont: TestCase = async (context) => {\n  // Use iconfont for toolbar items.\n  const iconFont = document.createElement('script');\n  iconFont.src = iconfont.js;\n  document.head.appendChild(iconFont);\n\n  const graph = new Graph({\n    ...context,\n    autoResize: true,\n    data,\n    layout: { type: 'd3-force' },\n    plugins: [\n      {\n        type: 'toolbar',\n        position: 'right-top',\n        onClick: (item: string, e: MouseEvent) => {\n          console.log('item clicked:', item, e);\n        },\n        getItems: () => {\n          return [\n            { id: 'icon-xinjian', value: 'new' },\n            { id: 'icon-fenxiang', value: 'share' },\n            { id: 'icon-chexiao', value: 'undo' },\n          ];\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-tooltip-async.ts",
    "content": "import type { ElementDatum, IElementEvent } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nexport const pluginTooltipAsync: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [{ id: 'node1', style: { x: 150, y: 100 }, data: { desc: 'get content async test' } }],\n    },\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    plugins: [\n      {\n        key: 'tooltip',\n        type: 'tooltip',\n        trigger: 'click',\n        getContent: (evt: IElementEvent, items: ElementDatum[]) => {\n          return new Promise((resolve) => {\n            resolve(items[0].data?.desc || '');\n          });\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-tooltip-dual.ts",
    "content": "import type { IElementEvent, Tooltip } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nexport const pluginTooltipDual: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node1', style: { x: 100, y: 100 } },\n        { id: 'node2', style: { x: 200, y: 200 } },\n      ],\n      edges: [{ id: 'edge', source: 'node1', target: 'node2' }],\n    },\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    plugins: [\n      function () {\n        return {\n          key: 'tooltip-click',\n          type: 'tooltip',\n          trigger: 'click',\n          getContent: (evt: any, items: any[]) => {\n            return `<div>click ${items[0].id}</div>`;\n          },\n          onOpenChange: (open: boolean) => {\n            const tooltip = this.getPluginInstance('tooltip-hover') as Tooltip;\n            if (tooltip && open) tooltip.hide();\n          },\n        };\n      },\n      function () {\n        return {\n          key: 'tooltip-hover',\n          type: 'tooltip',\n          trigger: 'hover',\n          enable: (e: IElementEvent) => {\n            const tooltip = this.getPluginInstance('tooltip-click') as Tooltip;\n            // @ts-expect-error access private property\n            return e.target.id !== tooltip.currentTarget;\n          },\n          getContent: (evt: any, items: any[]) => {\n            return `<div>hover ${items[0].id}</div>`;\n          },\n          onOpenChange: (open: boolean) => {\n            const tooltip = this.getPluginInstance('tooltip-click') as Tooltip;\n            if (tooltip && open) {\n              tooltip.hide();\n            }\n          },\n        };\n      },\n    ],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-tooltip-enable.ts",
    "content": "import type { ElementDatum, IElementEvent } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nexport const pluginTooltipEnable: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: 'node1', style: { x: 150, y: 100 }, data: { type: 'test1', desc: 'This is a tooltip' } },\n        { id: 'node2', style: { x: 150, y: 200 }, data: { type: 'test1', desc: '' } },\n        { id: 'node3', style: { x: 150, y: 300 }, data: { type: 'test2', desc: 'This is a tooltip' } },\n      ],\n    },\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    plugins: [\n      {\n        key: 'tooltip',\n        type: 'tooltip',\n        trigger: 'click',\n        enable: (e: IElementEvent, items: ElementDatum[]) => {\n          return items[0].data?.type === 'test1';\n        },\n        getContent: (evt: IElementEvent, items: ElementDatum[]) => {\n          return items[0].data?.desc || '';\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-tooltip-with-custom-node.ts",
    "content": "import { Group } from '@antv/g';\nimport type { CircleStyleProps, ElementDatum, IElementEvent } from '@antv/g6';\nimport { Circle, ExtensionCategory, Graph, register } from '@antv/g6';\n\nclass CustomNode extends Circle {\n  drawOperatorBtns(attributes: Required<CircleStyleProps>, container: Group) {\n    this.upsert(\n      'custom-shape',\n      'text',\n      {\n        x: 0,\n        y: 0,\n        fontSize: 30,\n        text: '+',\n        fill: '#000',\n        cursor: 'pointer',\n      },\n      container,\n    );\n  }\n  render(attributes = this.parsedAttributes, container: Group) {\n    super.render(attributes, container);\n    this.drawOperatorBtns(attributes, container);\n  }\n}\n\nregister(ExtensionCategory.NODE, 'custom-node', CustomNode);\n\nexport const pluginTooltipWithCustomNode: TestCase = async (context) => {\n  const graph = new Graph({\n    container: 'container',\n    data: {\n      nodes: [\n        {\n          id: 'Jack',\n          data: {},\n          style: {\n            x: 200,\n            y: 200,\n          },\n        },\n      ],\n    },\n    node: {\n      type: 'custom-node',\n      style: {\n        size: 250,\n      },\n    },\n    plugins: [\n      {\n        type: 'tooltip',\n        trigger: 'hover',\n        enable: (e: IElementEvent, items: ElementDatum[]) => {\n          return e.originalTarget.className === 'custom-shape';\n        },\n      },\n    ],\n    behaviors: ['drag-element'],\n  });\n\n  graph.render();\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-tooltip.ts",
    "content": "import data from '@@/dataset/combo.json';\nimport type { IElementEvent } from '@antv/g6';\nimport { Graph } from '@antv/g6';\n\nexport const pluginTooltip: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    layout: {\n      type: 'combo-combined',\n      comboPadding: 2,\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    plugins: [\n      {\n        key: 'tooltip',\n        type: 'tooltip',\n        trigger: 'click',\n        getContent: (evt: any, items: any[]) => {\n          return `<div>${items[0].id || items[0].source + ' --> ' + items[0].target}</div>`;\n        },\n      },\n    ],\n    autoFit: 'view',\n  });\n\n  await graph.render();\n\n  pluginTooltip.form = (panel) => {\n    const config = {\n      trigger: 'click',\n      enable: 'all',\n    };\n    return [\n      panel\n        .add(config, 'trigger', ['hover', 'click'])\n        .name('Change Trigger Method')\n        .onChange((trigger: string) => {\n          graph.setPlugins((plugins) =>\n            plugins.map((plugin) => {\n              if (typeof plugin === 'object' && plugin.type === 'tooltip') return { ...plugin, trigger };\n              return plugin;\n            }),\n          );\n        }),\n      panel\n        .add(config, 'enable', ['all', 'node', 'edge', 'combo'])\n        .name('Change Enable Target')\n        .onChange((enable: string) => {\n          graph.setPlugins((plugins) =>\n            plugins.map((plugin) => {\n              if (typeof plugin === 'object' && plugin.type === 'tooltip') {\n                if (enable === 'all') return { ...plugin, enable: true };\n                else return { ...plugin, enable: (event: IElementEvent) => event.targetType === enable };\n              }\n              return plugin;\n            }),\n          );\n        }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-watermark-image.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const pluginWatermarkImage: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoResize: true,\n    data,\n    layout: { type: 'd3-force' },\n    plugins: [\n      {\n        type: 'watermark',\n        width: 100,\n        height: 100,\n        imageURL: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*7svFR6wkPMoAAAAAAAAAAAAADmJ7AQ/original',\n      },\n    ],\n  });\n\n  await graph.render();\n\n  pluginWatermarkImage.form = (panel) => {\n    const config = {\n      width: 200,\n      height: 100,\n      fontSize: 20,\n      textFill: 'red',\n    };\n    return [\n      panel\n        .add(config, 'width', 150, 400, 10)\n        .name('Width')\n        .onChange(() => {}),\n      panel\n        .add(config, 'height', 100, 200, 10)\n        .name('Width')\n        .onChange(() => {}),\n      panel\n        .add(config, 'fontSize', 10, 32, 1)\n        .name('FontSize')\n        .onChange(() => {}),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/plugin-watermark.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph, PluginOptions } from '@antv/g6';\n\nexport const pluginWatermark: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoResize: true,\n    data,\n    layout: { type: 'd3-force' },\n    plugins: [\n      {\n        type: 'watermark',\n        text: 'hello, \\na watermark.',\n        textFontSize: 12,\n      },\n    ],\n  });\n\n  await graph.render();\n\n  function updatePlugin(type: string, config: object) {\n    return (plugins: PluginOptions) => {\n      return plugins.map((plugin) => {\n        if (typeof plugin === 'object' && plugin.type === type) return { ...plugin, ...config };\n        return plugin;\n      });\n    };\n  }\n  pluginWatermark.form = (panel) => {\n    const config = {\n      width: 200,\n      height: 100,\n      textFontSize: 12,\n    };\n    return [\n      panel\n        .add(config, 'width', 150, 400, 10)\n        .name('Width')\n        .onChange((width: number) => {\n          graph.setPlugins(updatePlugin('watermark', { width }));\n        }),\n      panel\n        .add(config, 'height', 100, 200, 10)\n        .name('Height')\n        .onChange((height: number) => {\n          graph.setPlugins(updatePlugin('watermark', { height }));\n        }),\n      panel\n        .add(config, 'textFontSize', 10, 32, 1)\n        .name('TextFontSize')\n        .onChange((textFontSize: number) => {\n          graph.setPlugins(updatePlugin('watermark', { textFontSize }));\n        }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/theme.ts",
    "content": "import data from '@@/dataset/cluster.json';\nimport { Graph } from '@antv/g6';\n\nexport const theme: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data,\n    node: {\n      palette: {\n        field: 'cluster',\n      },\n    },\n    layout: {\n      type: 'radial',\n      unitRadius: 80,\n    },\n  });\n\n  await graph.render();\n\n  theme.form = (panel) => {\n    const config = {\n      theme: 'light',\n    };\n    const options = { Light: 'light', Dark: 'dark', Blue: 'blue' };\n\n    const themeOptions: { [key: string]: any } = {\n      light: {\n        background: '#fff',\n        theme: 'light',\n        node: {\n          palette: {\n            field: 'cluster',\n          },\n        },\n      },\n      dark: {\n        background: '#000',\n        theme: 'dark',\n        node: {\n          palette: {\n            field: 'cluster',\n          },\n        },\n      },\n      blue: {\n        background: '#f3faff',\n        theme: 'light',\n        node: {\n          palette: {\n            type: 'group',\n            field: 'cluster',\n            color: 'blues',\n            invert: true,\n          },\n        },\n      },\n      yellow: {\n        background: '#fcf9f1',\n        theme: 'light',\n        node: {\n          palette: {\n            type: 'group',\n            field: 'cluster',\n            color: ['#ffe7ba', '#ffd591', '#ffc069', '#ffa940', '#fa8c16', '#d46b08', '#ad4e00', '#873800', '#612500'],\n          },\n        },\n      },\n    };\n\n    const changeTheme = (theme: string) => {\n      graph.setOptions(themeOptions[theme]);\n      graph.render();\n    };\n\n    return [\n      panel.add(config, 'theme', options).onChange((value: string) => {\n        changeTheme(value);\n      }),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/transform-map-node-size.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const transformMapNodeSize: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-1', target: 'node-3' },\n        { source: 'node-1', target: 'node-4' },\n        { source: 'node-4', target: 'node-5' },\n      ],\n    },\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    layout: {\n      type: 'grid',\n    },\n    transforms: [\n      {\n        key: 'map-node-size',\n        type: 'map-node-size',\n        scale: 'log',\n      },\n    ],\n    animation: false,\n  });\n\n  await graph.render();\n\n  const config = { 'centrality.type': 'degree', mapLabelSize: false };\n\n  transformMapNodeSize.form = (panel) => [\n    panel\n      .add(config, 'centrality.type', ['degree', 'betweenness', 'closeness', 'eigenvector', 'pagerank'])\n      .name('Centrality Type')\n      .onChange((type: string) => {\n        graph.updateTransform({ key: 'map-node-size', centrality: { type } });\n        graph.draw();\n      }),\n    panel\n      .add(config, 'mapLabelSize')\n      .name('Sync To Label Size')\n      .onChange((mapLabelSize: boolean) => {\n        graph.updateTransform({ key: 'map-node-size', mapLabelSize });\n        graph.draw();\n      }),\n  ];\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/transform-place-radial-labels.ts",
    "content": "import data from '@@/dataset/algorithm-category.json';\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nexport const transformPlaceRadialLabels: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    node: {\n      style: {\n        labelText: (d) => d.id,\n      },\n    },\n    layout: {\n      type: 'dendrogram',\n      radial: true,\n      nodeSep: 30,\n      rankSep: 200,\n      preLayout: false,\n    },\n    transforms: ['place-radial-labels'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/transform-process-parallel-edges.ts",
    "content": "import data from '@@/dataset/parallel-edges.json';\nimport { Graph } from '@antv/g6';\n\nexport const transformProcessParallelEdges: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data,\n    node: { style: { labelText: (d) => d.id } },\n    behaviors: [\n      'drag-element',\n      {\n        type: 'hover-activate',\n        key: 'hover-activate',\n      },\n    ],\n    transforms: [\n      {\n        type: 'process-parallel-edges',\n        key: 'process-parallel-edges',\n        mode: 'merge',\n        style: {\n          lineDash: [2, 2],\n          lineWidth: 3,\n          stroke: '#99add1',\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n\n  let targetIndex = 3;\n\n  const config = { mode: 'merge' };\n\n  transformProcessParallelEdges.form = (panel) => [\n    panel\n      .add(config, 'mode', ['bundle', 'merge'])\n      .name('Mode')\n      .onChange((mode: string) => {\n        graph.updateTransform({ key: 'process-parallel-edges', mode });\n        graph.draw();\n      }),\n    panel\n      .add(\n        {\n          Add: () => {\n            graph.addEdgeData([\n              {\n                id: 'new-edge',\n                source: 'node1',\n                target: 'node4',\n                style: { stroke: '#FF9800', lineWidth: 2 },\n              },\n              {\n                id: 'new-loop',\n                source: 'node5',\n                target: 'node5',\n                style: { stroke: '#FF9800', lineWidth: 2 },\n              },\n            ]);\n            graph.draw();\n          },\n        },\n        'Add',\n      )\n      .name('Add Orange Edge'),\n    panel\n      .add(\n        {\n          Remove: () => {\n            graph.removeEdgeData(['edge1', 'loop1']);\n            graph.draw();\n          },\n        },\n        'Remove',\n      )\n      .name('Remove Purple Edge'),\n    panel\n      .add(\n        {\n          Update: () => {\n            const target = ['node2', 'node3', 'node4', 'node5', 'node6'][targetIndex % 5];\n            targetIndex++;\n            graph.updateEdgeData([{ id: 'new-edge', source: 'node1', target }]);\n            graph.draw();\n          },\n        },\n        'Update',\n      )\n      .name('Update Orange Edge'),\n  ];\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/demos/viewport-fit.ts",
    "content": "import { Graph } from '@antv/g6';\n\nexport const viewportFit: TestCase = async (context) => {\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes: [\n        { id: '1', style: { x: 200, y: 250 } },\n        { id: '2', style: { x: 300, y: 250 } },\n        { id: '3', style: { x: 250, y: 200 } },\n        { id: '4', style: { x: 250, y: 300 } },\n      ],\n    },\n    node: {\n      style: {\n        size: 50,\n        fill: (d) => (d.id === '1' ? '#d4414c' : '#2f363d'),\n      },\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas'],\n    plugins: [],\n  });\n\n  await graph.render();\n\n  viewportFit.form = (panel) => {\n    const config = {\n      x: 0,\n      y: 0,\n      zoom: 1,\n      fitView: () => graph.fitView(),\n      fitCenter: () => graph.fitCenter(),\n      focusElement: () => graph.focusElement('1'),\n    };\n    return [\n      panel.add(config, 'x', -100, 100, 1).onChange((x: number) => graph.translateTo([x, config.y], false)),\n      panel.add(config, 'y', -100, 100, 1).onChange((y: number) => graph.translateTo([config.x, y], false)),\n      panel.add(config, 'zoom', 0.01, 10, 0.1).onChange((zoom: number) => graph.zoomTo(zoom, false)),\n      panel.add(config, 'fitView'),\n      panel.add(config, 'fitCenter'),\n      panel.add(config, 'focusElement'),\n    ];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6/__tests__/index.html",
    "content": "<!doctype html>\n<html lang=\"en\" data-theme=\"light\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>G6: Preview</title>\n    <script type=\"module\">\n      import { iconfont } from '@/src';\n      const style = document.createElement('style');\n      style.innerHTML = `@import url('${iconfont.css}');`;\n      document.head.appendChild(style);\n    </script>\n    <style>\n      body {\n        margin: 0;\n        font-size: 14px;\n        color: var(--text-color);\n        background-color: var(--background-color);\n        background-image:\n          linear-gradient(var(--stroke-color) 1px, transparent 1px),\n          linear-gradient(90deg, var(--stroke-color) 1px, transparent 1px);\n        background-size: 25px 25px;\n        transition: all 0.5s;\n      }\n\n      #panel {\n        z-index: 1;\n        position: absolute;\n        right: 0;\n      }\n\n      #container {\n        width: 500px;\n        height: 500px;\n        border: 1px solid var(--stroke-color);\n      }\n\n      [data-theme='light'] {\n        --text-color: #000;\n        --background-color: #fff;\n        --background-color2: #ddd;\n        --stroke-color: #0001;\n      }\n\n      [data-theme='dark'] {\n        --text-color: #fff;\n        --background-color: #000;\n        --background-color2: #333;\n        --stroke-color: #333;\n      }\n    </style>\n  </head>\n\n  <body style=\"font-family: Arial, Helvetica, sans-serif\">\n    <div id=\"panel\"></div>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/g6/__tests__/main.ts",
    "content": "import { CanvasEvent } from '@antv/g';\nimport { Canvas, ComboEvent, CommonEvent, EdgeEvent, NodeEvent } from '@antv/g6';\nimport type { Controller } from 'lil-gui';\nimport GUI from 'lil-gui';\nimport Stats from 'stats.js';\nimport '../src/preset';\nimport * as demos from './demos';\nimport { createGraphCanvas } from './utils';\n\n// inject\nObject.assign(window, { NodeEvent, EdgeEvent, ComboEvent, CommonEvent });\n\ntype Options = {\n  Search: string;\n  Demo: string;\n  Renderer: string;\n  Theme: string;\n  Animation: boolean;\n  MultiLayers: boolean;\n  [keys: string]: any;\n};\n\nconst options: Options = {\n  Search: '',\n  Demo: Object.keys(demos)[0],\n  Renderer: 'canvas',\n  GridLine: true,\n  Theme: 'light',\n  Animation: true,\n  MultiLayers: true,\n  interval: 0,\n  Reload: () => {},\n  forms: [],\n};\n\nconst params = ['Type', 'Demo', 'Renderer', 'GridLine', 'Theme', 'Animation'] as const;\n\nsyncParamsFromSearch();\n\nconst panels = initPanel();\nconst stats = initStats();\n\nwindow.onload = render;\n\nfunction initPanel() {\n  const panel = new GUI({ container: document.getElementById('panel')!, autoPlace: true });\n  const Demo = panel.add(options, 'Demo', Object.keys(demos)).onChange(render);\n  const Search = panel.add(options, 'Search').onChange((keyword: string) => {\n    const keys = Object.keys(demos);\n    const filtered = keys.filter((key) => key.toLowerCase().includes(keyword.toLowerCase()));\n    Demo.options(filtered);\n  });\n  const Renderer = panel.add(options, 'Renderer', { Canvas: 'canvas', SVG: 'svg', WebGL: 'webgl' }).onChange(render);\n  const Theme = panel.add(options, 'Theme', { Light: 'light', Dark: 'dark' }).onChange(render);\n  const GridLine = panel.add(options, 'GridLine').onChange(() => {\n    syncParamsToSearch();\n    applyGridLine();\n  });\n  const Animation = panel.add(options, 'Animation').onChange(render);\n  const MultiLayers = panel.add(options, 'MultiLayers').onChange(render);\n  const reload = panel.add(options, 'Reload').onChange(render);\n\n  const goTo = (diff: number) => {\n    // @ts-expect-error private property\n    const keys = Demo._values;\n    const currentIndex = keys.indexOf(options.Demo);\n    const nextIndex = (currentIndex + diff + keys.length) % keys.length;\n    options.Demo = keys[nextIndex];\n    Demo.updateDisplay();\n    render();\n  };\n\n  globalThis.addEventListener('keydown', (e) => {\n    if (['ArrowRight', 'ArrowDown'].includes(e.key)) {\n      goTo(1);\n    } else if (['ArrowLeft', 'ArrowUp'].includes(e.key)) {\n      goTo(-1);\n    }\n  });\n\n  return { panel, Demo, Search, Renderer, GridLine, Theme, Animation, MultiLayers, reload };\n}\n\nfunction initStats() {\n  const container = document.getElementById('panel')!;\n  const stats = new Stats();\n  stats.showPanel(0);\n  const dom = stats.dom;\n  Object.assign(dom.style, { position: 'relative', top: 'unset', right: 'unset' });\n  container.appendChild(dom);\n  return stats;\n}\n\nlet canvas: Canvas | undefined;\nconst statsListener = () => stats.update();\n\nasync function render() {\n  syncParamsToSearch();\n  applyTheme();\n  destroyForm();\n  if (canvas) {\n    canvas.getLayer().removeEventListener(CanvasEvent.AFTER_RENDER, statsListener);\n  }\n\n  const $container = initContainer();\n\n  applyGridLine();\n\n  // render\n  const { Renderer, Demo, Animation, Theme, MultiLayers } = options;\n\n  const canvasOptions = { enableMultiLayer: MultiLayers };\n\n  canvas = createGraphCanvas($container, 500, 500, Renderer, canvasOptions);\n\n  canvas.getLayer().addEventListener(CanvasEvent.AFTER_RENDER, statsListener);\n\n  await canvas.ready;\n  const testCase = demos[Demo as keyof typeof demos];\n  if (!testCase) return;\n\n  performance.clearMarks();\n  performance.clearMeasures();\n  performance.mark('demo-start');\n\n  const graph = await testCase({\n    container: canvas,\n    animation: Animation,\n    theme: Theme,\n    canvas: canvasOptions,\n  });\n\n  performance.mark('demo-end');\n  performance.measure('demo', 'demo-start', 'demo-end');\n  console.log('Time:', performance.getEntriesByName('demo')[0].duration, 'ms');\n\n  Object.assign(window, { graph, __g_instances__: Object.values(graph.getCanvas().getLayers()) });\n\n  renderForm(panels.panel, testCase.form);\n}\n\nfunction renderForm(panel: GUI, form: TestCase['form']) {\n  if (form) options.forms.push(...form(panel));\n}\n\nfunction destroyForm() {\n  const { forms } = options;\n  forms.forEach((controller: Controller) => controller.destroy());\n  forms.length = 0;\n}\n\nfunction syncParamsFromSearch() {\n  const searchParams = new URLSearchParams(window.location.search);\n  params.forEach((key) => {\n    const value = searchParams.get(key);\n    if (!value) return;\n    if (key === 'Animation' || key === 'GridLine') options[key] = value === 'true';\n    else options[key] = value;\n  });\n}\n\nfunction syncParamsToSearch() {\n  const searchParams = new URLSearchParams(window.location.search);\n  Object.entries(options).forEach(([key, value]) => {\n    if (params.includes(key as (typeof params)[number])) searchParams.set(key, value.toString());\n  });\n  window.history.replaceState(null, '', `?${searchParams.toString()}`);\n}\n\nfunction initContainer() {\n  document.getElementById('container')?.remove();\n  const $container = document.createElement('div');\n  $container.id = 'container';\n  document.getElementById('app')?.appendChild($container);\n  return $container;\n}\n\nfunction applyTheme() {\n  document.documentElement.setAttribute('data-theme', options.Theme);\n}\n\nfunction applyGridLine() {\n  const show = options.GridLine;\n\n  const element = document.getElementById('container');\n  if (!element) return;\n  syncParamsToSearch();\n  if (show) {\n    document.body.style.backgroundSize = '25px 25px';\n    element.style.border = '1px solid #e8e8e8';\n  } else {\n    document.body.style.backgroundSize = '0';\n    element.style.border = 'none';\n  }\n}\n"
  },
  {
    "path": "packages/g6/__tests__/perf/data.perf.ts",
    "content": "import { Graph } from '@antv/g6';\nimport type { Test } from 'iperf';\n\nconst DataTestWrapper = (count: number): Test => {\n  return async ({ container, perf }) => {\n    const graph = new Graph({\n      container,\n      data: {\n        nodes: Array(count)\n          .fill(0)\n          .map((_, i) => ({ id: `${i}`, style: { x: 50, y: 50 } })),\n      },\n    });\n    await perf.evaluate('data diff', async () => {\n      // @ts-expect-error private method invoke\n      await graph.prepare();\n      // @ts-expect-error context is private property\n      const context = graph.context;\n      // @ts-expect-error private method invoke\n      context.element.computeChangesAndDrawData({});\n    });\n  };\n};\n\nexport const dataDiff1000: Test = DataTestWrapper(1000);\n\nexport const dataDiff5000: Test = DataTestWrapper(5000);\n\nexport const dataDiff10000: Test = DataTestWrapper(10000);\n\nexport const dataDiff50000: Test = DataTestWrapper(50000);\n\nexport const dataDiff100000: Test = DataTestWrapper(100000);\n"
  },
  {
    "path": "packages/g6/__tests__/perf/draw.perf.ts",
    "content": "import { Graph } from '@antv/g6';\nimport type { Test } from 'iperf';\n\nconst ElementTestWrapper = (count: number): Test => {\n  return async ({ container, perf }) => {\n    const graph = new Graph({\n      container,\n      animation: false, // be sure to close the animation\n      data: {\n        nodes: Array(count)\n          .fill(0)\n          .map((_, i) => ({ id: `${i}`, style: { x: 50, y: 50 } })),\n      },\n      layout: {\n        type: 'grid',\n      },\n    });\n\n    await perf.evaluate('element drawing', async () => {\n      await graph.draw();\n    });\n\n    await perf.evaluate('grid layout', async () => {\n      await graph.layout();\n    });\n  };\n};\n\nexport const elementDrawing100: Test = ElementTestWrapper(100);\n\nexport const elementDrawing500: Test = ElementTestWrapper(500);\n\nexport const elementDrawing1000: Test = ElementTestWrapper(1000);\n\nexport const elementDrawing5000: Test = ElementTestWrapper(5000);\n\nexport const elementDrawing10000: Test = ElementTestWrapper(10000);\n"
  },
  {
    "path": "packages/g6/__tests__/perf/massive-element.perf.ts",
    "content": "import { Graph } from '@antv/g6';\nimport type { Test } from 'iperf';\n\nexport const massiveElement60000: Test = async ({ container, perf }) => {\n  const data = await fetch('https://assets.antv.antgroup.com/g6/60000.json').then((res) => res.json());\n\n  const graph = new Graph({\n    container,\n    animation: false,\n    autoFit: 'view',\n    data,\n    node: {\n      style: {\n        size: 4,\n        batchKey: 'node',\n      },\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas'],\n  });\n\n  await perf.evaluate('massive element drawing', async () => {\n    await graph.draw();\n  });\n};\n\nmassiveElement60000.iteration = 3;\n"
  },
  {
    "path": "packages/g6/__tests__/perf/update-state.perf.ts",
    "content": "import { Graph } from '@antv/g6';\nimport type { Test } from 'iperf';\n\nexport const UpdateElementState: Test = async ({ container, perf }) => {\n  const nodes = Array(1000)\n    .fill(0)\n    .map((_, i) => ({ id: `${i}` }));\n  const edges = Array(999)\n    .fill(0)\n    .map((_, i) => ({ id: `edge-${i}`, source: `${i}`, target: `${i + 1}` }));\n\n  const graph = new Graph({\n    container,\n    animation: false,\n    data: { nodes, edges },\n    layout: { type: 'grid' },\n  });\n\n  await graph.render();\n\n  const selected = [...nodes, ...edges].map((element) => [element.id, 'selected']);\n\n  await perf.evaluate('update element state to selected', async () => {\n    await graph.setElementState(Object.fromEntries(selected));\n  });\n\n  const none = [...nodes, ...edges].map((element) => [element.id, []]);\n\n  await perf.evaluate('update element state to default', async () => {\n    await graph.setElementState(Object.fromEntries(none));\n  });\n\n  const position = nodes.map((node) => [node.id, [10, 10]]);\n\n  await perf.evaluate('update element position', async () => {\n    await graph.translateElementBy(Object.fromEntries(position), false);\n  });\n};\n"
  },
  {
    "path": "packages/g6/__tests__/perf-report/9821ed36_2024-08-22_20-39-12.json",
    "content": "{\n  \"version\": \"1.0\",\n  \"device\": {\n    \"os\": {\n      \"arch\": \"arm64\",\n      \"distro\": \"macOS\",\n      \"serial\": \"9821ed36011eee5abf6c71d6fc2c03fb4bf4655e674c56b7f50e2560cb6e924a\"\n    },\n    \"cpu\": {\n      \"manufacturer\": \"Apple\",\n      \"brand\": \"M1 Pro\",\n      \"speed\": 2.4,\n      \"cores\": 10\n    },\n    \"memory\": {\n      \"total\": 16384,\n      \"free\": 296.6875\n    },\n    \"gpu\": {\n      \"vendor\": \"Apple\",\n      \"model\": \"Apple M1 Pro\",\n      \"cores\": \"16\"\n    }\n  },\n  \"repo\": \"489f27b5c3ef49746516811e1012c88aadd61c4f\",\n  \"client\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/128.0.6613.18 Safari/537.36\",\n  \"reports\": {\n    \"dataDiff1000\": {\n      \"time\": [\n        {\n          \"min\": 1.9000000059604645,\n          \"max\": 16.19999998807907,\n          \"median\": 2.399999976158142,\n          \"avg\": 3,\n          \"variance\": 3.3875000156462196,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff10000\": {\n      \"time\": [\n        {\n          \"min\": 1.9000000059604645,\n          \"max\": 9.799999982118607,\n          \"median\": 4.399999976158142,\n          \"avg\": 4.237500000745058,\n          \"variance\": 4.7523437567986555,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff100000\": {\n      \"time\": [\n        {\n          \"min\": 11.300000011920929,\n          \"max\": 38,\n          \"median\": 12.400000005960464,\n          \"avg\": 15.299999997019768,\n          \"variance\": 31.937499957531692,\n          \"reliable\": true,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff5000\": {\n      \"time\": [\n        {\n          \"min\": 1.5999999940395355,\n          \"max\": 9.699999988079071,\n          \"median\": 3.2999999821186066,\n          \"avg\": 3.649999998509884,\n          \"variance\": 2.9674999792873864,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff50000\": {\n      \"time\": [\n        {\n          \"min\": 5.700000017881393,\n          \"max\": 16.099999994039536,\n          \"median\": 7.5999999940395355,\n          \"avg\": 8.825000006705523,\n          \"variance\": 11.751875018589198,\n          \"reliable\": true,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing100\": {\n      \"time\": [\n        {\n          \"min\": 9.400000005960464,\n          \"max\": 20,\n          \"median\": 10.300000011920929,\n          \"avg\": 10.68750000372529,\n          \"variance\": 1.1235937587358058,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 6.699999988079071,\n          \"max\": 9.899999976158142,\n          \"median\": 8.5,\n          \"avg\": 8.024999998509884,\n          \"variance\": 0.67437500230968,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing1000\": {\n      \"time\": [\n        {\n          \"min\": 41.69999998807907,\n          \"max\": 64.7000000178814,\n          \"median\": 51.5,\n          \"avg\": 49.899999998509884,\n          \"variance\": 43.767499907016756,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 37,\n          \"max\": 59.400000005960464,\n          \"median\": 42,\n          \"avg\": 42.11250000447035,\n          \"variance\": 6.611093760319055,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing10000\": {\n      \"time\": [\n        {\n          \"min\": 410.30000001192093,\n          \"max\": 529.0999999940395,\n          \"median\": 424.40000000596046,\n          \"avg\": 433.4375000037253,\n          \"variance\": 516.2098438784666,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 377.19999998807907,\n          \"max\": 420.09999999403954,\n          \"median\": 390.2000000178814,\n          \"avg\": 391.2000000067055,\n          \"variance\": 151.5974999138713,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing500\": {\n      \"time\": [\n        {\n          \"min\": 22.599999994039536,\n          \"max\": 42.599999994039536,\n          \"median\": 30.69999998807907,\n          \"avg\": 30.649999998509884,\n          \"variance\": 41.58999999597668,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 21.399999976158142,\n          \"max\": 29,\n          \"median\": 23.30000001192093,\n          \"avg\": 23.962499998509884,\n          \"variance\": 4.732343725003302,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing5000\": {\n      \"time\": [\n        {\n          \"min\": 201.69999998807907,\n          \"max\": 253.39999997615814,\n          \"median\": 208.09999999403954,\n          \"avg\": 216.7749999947846,\n          \"variance\": 218.39187494117766,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 188.10000002384186,\n          \"max\": 227.90000000596046,\n          \"median\": 197.09999999403954,\n          \"avg\": 196.08749999850988,\n          \"variance\": 24.878593807034196,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/g6/__tests__/perf-report/9821ed36_2024-08-29_11-11-17.json",
    "content": "{\n  \"version\": \"1.0\",\n  \"device\": {\n    \"os\": {\n      \"arch\": \"arm64\",\n      \"distro\": \"macOS\",\n      \"serial\": \"9821ed36011eee5abf6c71d6fc2c03fb4bf4655e674c56b7f50e2560cb6e924a\"\n    },\n    \"cpu\": {\n      \"manufacturer\": \"Apple\",\n      \"brand\": \"M1 Pro\",\n      \"speed\": 2.4,\n      \"cores\": 10\n    },\n    \"memory\": {\n      \"total\": 16384,\n      \"free\": 540.28125\n    },\n    \"gpu\": {\n      \"vendor\": \"Apple\",\n      \"model\": \"Apple M1 Pro\",\n      \"cores\": \"16\"\n    }\n  },\n  \"repo\": \"aa87ec67c38f03808c72d82caaa3d064b4ee9c01\",\n  \"client\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/128.0.6613.18 Safari/537.36\",\n  \"reports\": {\n    \"UpdateElementState\": {\n      \"time\": [\n        {\n          \"min\": 167.09999999403954,\n          \"max\": 202.29999999701977,\n          \"median\": 171.79999999701977,\n          \"avg\": 172.86250000447035,\n          \"variance\": 18.562343744896353,\n          \"reliable\": true,\n          \"key\": \"update element state to selected\"\n        },\n        {\n          \"min\": 102.20000000298023,\n          \"max\": 116.09999999403954,\n          \"median\": 104.30000001192093,\n          \"avg\": 105.06250000186265,\n          \"variance\": 3.0598437559511513,\n          \"reliable\": true,\n          \"key\": \"update element state to default\"\n        },\n        {\n          \"min\": 86,\n          \"max\": 105.8999999910593,\n          \"median\": 88.5,\n          \"avg\": 88.51249999925494,\n          \"variance\": 2.073593743089587,\n          \"reliable\": true,\n          \"key\": \"update element position\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff1000\": {\n      \"time\": [\n        {\n          \"min\": 2.7000000029802322,\n          \"max\": 11.300000011920929,\n          \"median\": 5.5,\n          \"avg\": 5.512499997392297,\n          \"variance\": 2.638593754386529,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff10000\": {\n      \"time\": [\n        {\n          \"min\": 2.8999999910593033,\n          \"max\": 12.099999994039536,\n          \"median\": 6,\n          \"avg\": 6.325000004842877,\n          \"variance\": 4.859374997671694,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff100000\": {\n      \"time\": [\n        {\n          \"min\": 11,\n          \"max\": 39.29999999701977,\n          \"median\": 12.700000002980232,\n          \"avg\": 16.387499999254942,\n          \"variance\": 60.1160937285237,\n          \"reliable\": true,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff5000\": {\n      \"time\": [\n        {\n          \"min\": 1.8999999910593033,\n          \"max\": 9.5,\n          \"median\": 8.099999994039536,\n          \"avg\": 6.374999998137355,\n          \"variance\": 6.324374991413206,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff50000\": {\n      \"time\": [\n        {\n          \"min\": 5.700000002980232,\n          \"max\": 17.899999991059303,\n          \"median\": 8.299999997019768,\n          \"avg\": 9.5,\n          \"variance\": 16.31750000014901,\n          \"reliable\": true,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing100\": {\n      \"time\": [\n        {\n          \"min\": 12.700000002980232,\n          \"max\": 24.600000008940697,\n          \"median\": 18.899999991059303,\n          \"avg\": 19.199999999254942,\n          \"variance\": 8.127499995231629,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 7.9000000059604645,\n          \"max\": 14.100000008940697,\n          \"median\": 11.900000005960464,\n          \"avg\": 11.237500000745058,\n          \"variance\": 3.8348437521792946,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing1000\": {\n      \"time\": [\n        {\n          \"min\": 48.099999994039536,\n          \"max\": 73.20000000298023,\n          \"median\": 61.70000000298023,\n          \"avg\": 61.374999998137355,\n          \"variance\": 13.14437501249835,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 39.20000000298023,\n          \"max\": 59.1000000089407,\n          \"median\": 43.29999999701977,\n          \"avg\": 44.212499998509884,\n          \"variance\": 14.278593749292193,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing10000\": {\n      \"time\": [\n        {\n          \"min\": 409.3999999910593,\n          \"max\": 536.2000000029802,\n          \"median\": 434.3999999910593,\n          \"avg\": 442.1124999951571,\n          \"variance\": 829.2260936078895,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 372.90000000596046,\n          \"max\": 414.20000000298023,\n          \"median\": 393.29999999701977,\n          \"avg\": 389.8499999977648,\n          \"variance\": 108.12999993681908,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing500\": {\n      \"time\": [\n        {\n          \"min\": 28.400000005960464,\n          \"max\": 51.099999994039536,\n          \"median\": 41.79999999701977,\n          \"avg\": 41.07499999925494,\n          \"variance\": 15.709374984912573,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 22.400000005960464,\n          \"max\": 29,\n          \"median\": 25.599999994039536,\n          \"avg\": 25.399999998509884,\n          \"variance\": 3.570000005811453,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing5000\": {\n      \"time\": [\n        {\n          \"min\": 213.79999999701977,\n          \"max\": 266.5,\n          \"median\": 223.09999999403954,\n          \"avg\": 230.39999999850988,\n          \"variance\": 165.17250003792344,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 185.79999999701977,\n          \"max\": 224.1000000089407,\n          \"median\": 195.70000000298023,\n          \"avg\": 193.41250000149012,\n          \"variance\": 35.76859376054257,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/g6/__tests__/perf-report/9821ed36_2024-08-29_13-24-51.json",
    "content": "{\n  \"version\": \"1.0\",\n  \"device\": {\n    \"os\": {\n      \"arch\": \"arm64\",\n      \"distro\": \"macOS\",\n      \"serial\": \"9821ed36011eee5abf6c71d6fc2c03fb4bf4655e674c56b7f50e2560cb6e924a\"\n    },\n    \"cpu\": {\n      \"manufacturer\": \"Apple\",\n      \"brand\": \"M1 Pro\",\n      \"speed\": 2.4,\n      \"cores\": 10\n    },\n    \"memory\": {\n      \"total\": 16384,\n      \"free\": 224.8125\n    },\n    \"gpu\": {\n      \"vendor\": \"Apple\",\n      \"model\": \"Apple M1 Pro\",\n      \"cores\": \"16\"\n    }\n  },\n  \"repo\": \"7fbbb77580d806932e7b777b34856243600dbf35\",\n  \"client\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/128.0.6613.18 Safari/537.36\",\n  \"reports\": {\n    \"UpdateElementState\": {\n      \"time\": [\n        {\n          \"min\": 115.70000000298023,\n          \"max\": 146.29999999701977,\n          \"median\": 118.5,\n          \"avg\": 119.1750000026077,\n          \"variance\": 11.989375011604281,\n          \"reliable\": true,\n          \"key\": \"update element state to selected\"\n        },\n        {\n          \"min\": 72.90000000596046,\n          \"max\": 81,\n          \"median\": 75.5,\n          \"avg\": 75.12500000186265,\n          \"variance\": 1.299374995883554,\n          \"reliable\": true,\n          \"key\": \"update element state to default\"\n        },\n        {\n          \"min\": 61.29999999701977,\n          \"max\": 73.59999999403954,\n          \"median\": 63.29999999701977,\n          \"avg\": 63.28749999962747,\n          \"variance\": 0.2935937537904829,\n          \"reliable\": true,\n          \"key\": \"update element position\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff1000\": {\n      \"time\": [\n        {\n          \"min\": 4.399999991059303,\n          \"max\": 13.900000005960464,\n          \"median\": 6.4000000059604645,\n          \"avg\": 6.262500002980232,\n          \"variance\": 1.0873437525331973,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff10000\": {\n      \"time\": [\n        {\n          \"min\": 2.8999999910593033,\n          \"max\": 13.700000002980232,\n          \"median\": 6.0999999940395355,\n          \"avg\": 6.199999999254942,\n          \"variance\": 3.767500012814999,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff100000\": {\n      \"time\": [\n        {\n          \"min\": 10.899999991059303,\n          \"max\": 41.70000000298023,\n          \"median\": 12.299999997019768,\n          \"avg\": 17.51249999552965,\n          \"variance\": 96.928593772389,\n          \"reliable\": true,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff5000\": {\n      \"time\": [\n        {\n          \"min\": 3.0999999940395355,\n          \"max\": 13,\n          \"median\": 3.6000000089406967,\n          \"avg\": 5.262499999254942,\n          \"variance\": 10.062343739960342,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff50000\": {\n      \"time\": [\n        {\n          \"min\": 5.4000000059604645,\n          \"max\": 15.400000005960464,\n          \"median\": 8.399999991059303,\n          \"avg\": 8.549999998882413,\n          \"variance\": 8.840000012554228,\n          \"reliable\": true,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing100\": {\n      \"time\": [\n        {\n          \"min\": 10.200000002980232,\n          \"max\": 30.099999994039536,\n          \"median\": 18.600000008940697,\n          \"avg\": 19.499999998137355,\n          \"variance\": 17.06999999091029,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 6.5999999940395355,\n          \"max\": 11.200000002980232,\n          \"median\": 8.400000005960464,\n          \"avg\": 8.550000002607703,\n          \"variance\": 0.5175000003352761,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing1000\": {\n      \"time\": [\n        {\n          \"min\": 55.3999999910593,\n          \"max\": 75.5,\n          \"median\": 65.29999999701977,\n          \"avg\": 65.09999999962747,\n          \"variance\": 7.902500012740493,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 31.700000002980232,\n          \"max\": 47,\n          \"median\": 36.20000000298023,\n          \"avg\": 36.34999999962747,\n          \"variance\": 5.345000000037253,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing10000\": {\n      \"time\": [\n        {\n          \"min\": 404.40000000596046,\n          \"max\": 515.7999999970198,\n          \"median\": 420.6000000089407,\n          \"avg\": 427.75000000186265,\n          \"variance\": 282.06500002410263,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 306.70000000298023,\n          \"max\": 341,\n          \"median\": 321.6000000089407,\n          \"avg\": 319.4125000014901,\n          \"variance\": 58.756093712486326,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing500\": {\n      \"time\": [\n        {\n          \"min\": 25,\n          \"max\": 59.5,\n          \"median\": 40.70000000298023,\n          \"avg\": 42.03749999962747,\n          \"variance\": 15.694843770219013,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 16.69999998807907,\n          \"max\": 24.299999997019768,\n          \"median\": 20.799999997019768,\n          \"avg\": 20.25,\n          \"variance\": 5.507500002086163,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing5000\": {\n      \"time\": [\n        {\n          \"min\": 203.70000000298023,\n          \"max\": 264.30000001192093,\n          \"median\": 224.79999999701977,\n          \"avg\": 225.87499999813735,\n          \"variance\": 117.29687504237518,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 154.29999999701977,\n          \"max\": 174.6000000089407,\n          \"median\": 164.19999998807907,\n          \"avg\": 161,\n          \"variance\": 32.70499999940395,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/g6/__tests__/perf-report/9821ed36_2024-09-03_10-33-27.json",
    "content": "{\n  \"version\": \"1.0\",\n  \"device\": {\n    \"os\": {\n      \"arch\": \"arm64\",\n      \"distro\": \"macOS\",\n      \"serial\": \"9821ed36011eee5abf6c71d6fc2c03fb4bf4655e674c56b7f50e2560cb6e924a\"\n    },\n    \"cpu\": {\n      \"manufacturer\": \"Apple\",\n      \"brand\": \"M1 Pro\",\n      \"speed\": 2.4,\n      \"cores\": 10\n    },\n    \"memory\": {\n      \"total\": 16384,\n      \"free\": 49.90625\n    },\n    \"gpu\": {\n      \"vendor\": \"Apple\",\n      \"model\": \"Apple M1 Pro\",\n      \"cores\": \"16\"\n    }\n  },\n  \"repo\": \"b7bf98794ad57ba0b9facab1f71118d1a20f04ae\",\n  \"client\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/128.0.6613.18 Safari/537.36\",\n  \"reports\": {\n    \"UpdateElementState\": {\n      \"time\": [\n        {\n          \"min\": 115.19999998807907,\n          \"max\": 149.60000002384186,\n          \"median\": 119.80000001192093,\n          \"avg\": 119.90000000596046,\n          \"variance\": 12.112500025331974,\n          \"reliable\": true,\n          \"key\": \"update element state to selected\"\n        },\n        {\n          \"min\": 71.5,\n          \"max\": 83,\n          \"median\": 77.09999996423721,\n          \"avg\": 75.46249997615814,\n          \"variance\": 5.592343688607215,\n          \"reliable\": true,\n          \"key\": \"update element state to default\"\n        },\n        {\n          \"min\": 61.39999997615814,\n          \"max\": 72.89999997615814,\n          \"median\": 63.5,\n          \"avg\": 63.474999994039536,\n          \"variance\": 1.0193749868869784,\n          \"reliable\": true,\n          \"key\": \"update element position\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff1000\": {\n      \"time\": [\n        {\n          \"min\": 1.199999988079071,\n          \"max\": 8,\n          \"median\": 2.299999952316284,\n          \"avg\": 2.7124999910593033,\n          \"variance\": 2.1160937546938663,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff10000\": {\n      \"time\": [\n        {\n          \"min\": 1.899999976158142,\n          \"max\": 10.200000047683716,\n          \"median\": 4.5,\n          \"avg\": 4.524999998509884,\n          \"variance\": 5.461875010505318,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff100000\": {\n      \"time\": [\n        {\n          \"min\": 11.199999988079071,\n          \"max\": 43.19999998807907,\n          \"median\": 12.599999964237213,\n          \"avg\": 17.174999997019768,\n          \"variance\": 83.42187500372529,\n          \"reliable\": true,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff5000\": {\n      \"time\": [\n        {\n          \"min\": 1.699999988079071,\n          \"max\": 9.5,\n          \"median\": 3.399999976158142,\n          \"avg\": 3.8124999925494194,\n          \"variance\": 3.2510937972739344,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff50000\": {\n      \"time\": [\n        {\n          \"min\": 5.5,\n          \"max\": 15.899999976158142,\n          \"median\": 8.200000047683716,\n          \"avg\": 9.025000005960464,\n          \"variance\": 11.214375038146972,\n          \"reliable\": true,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing100\": {\n      \"time\": [\n        {\n          \"min\": 9,\n          \"max\": 19.600000023841858,\n          \"median\": 9.400000035762787,\n          \"avg\": 10.299999997019768,\n          \"variance\": 1.777499954998494,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 5,\n          \"max\": 8,\n          \"median\": 5.700000047683716,\n          \"avg\": 5.899999991059303,\n          \"variance\": 0.6200000035762796,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing1000\": {\n      \"time\": [\n        {\n          \"min\": 40.09999996423721,\n          \"max\": 63,\n          \"median\": 50.80000001192093,\n          \"avg\": 50.024999998509884,\n          \"variance\": 39.936875096932056,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 30,\n          \"max\": 47.60000002384186,\n          \"median\": 36.10000002384186,\n          \"avg\": 35.80000000447035,\n          \"variance\": 5.50500000834465,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing10000\": {\n      \"time\": [\n        {\n          \"min\": 403.5,\n          \"max\": 511.10000002384186,\n          \"median\": 426.9000000357628,\n          \"avg\": 432.1625000014901,\n          \"variance\": 785.8023441968486,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 309.19999998807907,\n          \"max\": 339.5999999642372,\n          \"median\": 327.5999999642372,\n          \"avg\": 322.67500000447035,\n          \"variance\": 78.7543750076741,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing500\": {\n      \"time\": [\n        {\n          \"min\": 21.80000001192093,\n          \"max\": 39.5,\n          \"median\": 28.400000035762787,\n          \"avg\": 29.575000025331974,\n          \"variance\": 49.00437493242323,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 16.399999976158142,\n          \"max\": 25.80000001192093,\n          \"median\": 20.599999964237213,\n          \"avg\": 19.849999971687794,\n          \"variance\": 7.554999983757734,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing5000\": {\n      \"time\": [\n        {\n          \"min\": 198.69999998807907,\n          \"max\": 254,\n          \"median\": 207,\n          \"avg\": 214.125,\n          \"variance\": 191.3443749541044,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 154.19999998807907,\n          \"max\": 176.20000004768372,\n          \"median\": 162.80000001192093,\n          \"avg\": 161.2749999910593,\n          \"variance\": 21.26437514021993,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"massiveElement60000\": {\n      \"time\": [\n        {\n          \"min\": 9387.900000035763,\n          \"max\": 10796.300000011921,\n          \"median\": 9416.300000011921,\n          \"avg\": 9416.300000011921,\n          \"variance\": 0,\n          \"reliable\": true,\n          \"key\": \"massive element drawing\"\n        }\n      ],\n      \"status\": \"passed\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/g6/__tests__/perf-report/9821ed36_2024-09-03_11-28-42.json",
    "content": "{\n  \"version\": \"1.0\",\n  \"device\": {\n    \"os\": {\n      \"arch\": \"arm64\",\n      \"distro\": \"macOS\",\n      \"serial\": \"9821ed36011eee5abf6c71d6fc2c03fb4bf4655e674c56b7f50e2560cb6e924a\"\n    },\n    \"cpu\": {\n      \"manufacturer\": \"Apple\",\n      \"brand\": \"M1 Pro\",\n      \"speed\": 2.4,\n      \"cores\": 10\n    },\n    \"memory\": {\n      \"total\": 16384,\n      \"free\": 48.078125\n    },\n    \"gpu\": {\n      \"vendor\": \"Apple\",\n      \"model\": \"Apple M1 Pro\",\n      \"cores\": \"16\"\n    }\n  },\n  \"repo\": \"41a9f98828cf1025a113360f5d3acbd7a918dd63\",\n  \"client\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/128.0.6613.18 Safari/537.36\",\n  \"reports\": {\n    \"UpdateElementState\": {\n      \"time\": [\n        {\n          \"min\": 116.09999996423721,\n          \"max\": 148.30000001192093,\n          \"median\": 119.70000004768372,\n          \"avg\": 120.67500002682209,\n          \"variance\": 13.551875019520523,\n          \"reliable\": true,\n          \"key\": \"update element state to selected\"\n        },\n        {\n          \"min\": 74.30000001192093,\n          \"max\": 82.10000002384186,\n          \"median\": 75.90000003576279,\n          \"avg\": 75.63750000298023,\n          \"variance\": 0.9998437715321785,\n          \"reliable\": true,\n          \"key\": \"update element state to default\"\n        },\n        {\n          \"min\": 61.89999997615814,\n          \"max\": 73.79999995231628,\n          \"median\": 63.80000001192093,\n          \"avg\": 63.712500013411045,\n          \"variance\": 0.5585937445983297,\n          \"reliable\": true,\n          \"key\": \"update element position\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff1000\": {\n      \"time\": [\n        {\n          \"min\": 2.099999964237213,\n          \"max\": 8.600000023841858,\n          \"median\": 4.699999988079071,\n          \"avg\": 3.9375000074505806,\n          \"variance\": 0.999843751229346,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff10000\": {\n      \"time\": [\n        {\n          \"min\": 3.100000023841858,\n          \"max\": 12.399999976158142,\n          \"median\": 8,\n          \"avg\": 7.325000002980232,\n          \"variance\": 3.504375010281801,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff100000\": {\n      \"time\": [\n        {\n          \"min\": 11.099999964237213,\n          \"max\": 39.30000001192093,\n          \"median\": 13.600000023841858,\n          \"avg\": 17.825000002980232,\n          \"variance\": 63.99937488421798,\n          \"reliable\": true,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff5000\": {\n      \"time\": [\n        {\n          \"min\": 2,\n          \"max\": 15.599999964237213,\n          \"median\": 4.199999988079071,\n          \"avg\": 5.637500002980232,\n          \"variance\": 9.34484379000962,\n          \"reliable\": false,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"dataDiff50000\": {\n      \"time\": [\n        {\n          \"min\": 5.5,\n          \"max\": 18.5,\n          \"median\": 7.5,\n          \"avg\": 8.487500004470348,\n          \"variance\": 9.353593760840596,\n          \"reliable\": true,\n          \"key\": \"data diff\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing100\": {\n      \"time\": [\n        {\n          \"min\": 12.900000035762787,\n          \"max\": 21.80000001192093,\n          \"median\": 16.19999998807907,\n          \"avg\": 16.1875,\n          \"variance\": 2.983593802154065,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 5.900000035762787,\n          \"max\": 8.699999988079071,\n          \"median\": 7.899999976158142,\n          \"avg\": 7.824999995529652,\n          \"variance\": 0.49687498323619417,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing1000\": {\n      \"time\": [\n        {\n          \"min\": 53.60000002384186,\n          \"max\": 71.30000001192093,\n          \"median\": 66.89999997615814,\n          \"avg\": 66.61249999701977,\n          \"variance\": 7.411093775406481,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 33.10000002384186,\n          \"max\": 48.799999952316284,\n          \"median\": 36.90000003576279,\n          \"avg\": 35.974999994039536,\n          \"variance\": 2.8368749639391897,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing10000\": {\n      \"time\": [\n        {\n          \"min\": 414.19999998807907,\n          \"max\": 536.3000000119209,\n          \"median\": 424.5,\n          \"avg\": 431.0874999910593,\n          \"variance\": 312.4835943404585,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 306.19999998807907,\n          \"max\": 339.19999998807907,\n          \"median\": 321.5,\n          \"avg\": 319.17499999701977,\n          \"variance\": 83.65437486723066,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing500\": {\n      \"time\": [\n        {\n          \"min\": 35.80000001192093,\n          \"max\": 56.5,\n          \"median\": 42.69999998807907,\n          \"avg\": 43.962499998509884,\n          \"variance\": 21.732343734689056,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 17.100000023841858,\n          \"max\": 23.80000001192093,\n          \"median\": 20.899999976158142,\n          \"avg\": 20.850000008940697,\n          \"variance\": 0.7200000214576724,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"elementDrawing5000\": {\n      \"time\": [\n        {\n          \"min\": 204.19999998807907,\n          \"max\": 252.79999995231628,\n          \"median\": 225.5,\n          \"avg\": 225.11250000447035,\n          \"variance\": 51.89859370965511,\n          \"reliable\": true,\n          \"key\": \"element drawing\"\n        },\n        {\n          \"min\": 153.79999995231628,\n          \"max\": 171.89999997615814,\n          \"median\": 161.30000001192093,\n          \"avg\": 159.70000000298023,\n          \"variance\": 21.232500118315222,\n          \"reliable\": true,\n          \"key\": \"grid layout\"\n        }\n      ],\n      \"status\": \"passed\"\n    },\n    \"massiveElement60000\": {\n      \"time\": [\n        {\n          \"min\": 3130.5,\n          \"max\": 3468.2999999523163,\n          \"median\": 3167.199999988079,\n          \"avg\": 3167.199999988079,\n          \"variance\": 0,\n          \"reliable\": true,\n          \"key\": \"massive element drawing\"\n        }\n      ],\n      \"status\": \"passed\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/g6/__tests__/setup.ts",
    "content": "import 'jest-canvas-mock';\nimport './utils/to-be-close-to';\nimport './utils/use-snapshot-matchers';\n"
  },
  {
    "path": "packages/g6/__tests__/types.d.ts",
    "content": "import type { G6Spec, Graph } from '@/src';\nimport type { Controller, GUI } from 'lil-gui';\n\ndeclare global {\n  export interface TestCase {\n    (context: G6Spec): Promise<Graph>;\n    form?: (gui: GUI) => Controller[];\n  }\n\n  export type TestContext = G6Spec;\n\n  export module '*.svg' {\n    const content: string;\n    export default content;\n  }\n\n  export module '*.png' {\n    const content: string;\n    export default content;\n  }\n\n  export module '*.jpg' {\n    const content: string;\n    export default content;\n  }\n\n  export module '*.jpeg' {\n    const content: string;\n    export default content;\n  }\n}\n"
  },
  {
    "path": "packages/g6/__tests__/unit/animations/element-position.spec.ts",
    "content": "import { animationElementPosition } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('animation element position', () => {\n  it('animation element position', async () => {\n    const graph = await createDemoGraph(animationElementPosition, { animation: true });\n    await expect(graph).toMatchAnimation(__filename, [0, 200, 1000], () => {\n      graph.translateElementTo(\n        {\n          'node-1': [250, 100],\n          'node-2': [175, 200],\n          'node-3': [325, 200],\n          'node-4': [100, 300],\n          'node-5': [250, 300],\n          'node-6': [400, 300],\n        },\n        true,\n      );\n    });\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/animations/element-state-switch.spec.ts",
    "content": "import { animationElementStateSwitch } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('animation element state switch', () => {\n  it('animation element state switch', async () => {\n    const graph = await createDemoGraph(animationElementStateSwitch, { animation: true });\n    await expect(graph).toMatchAnimation(__filename, [0, 200, 1000], async () => {\n      graph.updateData({\n        nodes: [\n          { id: 'node-1', states: [] },\n          { id: 'node-2', states: ['active'] },\n          { id: 'node-3', states: ['selected'] },\n        ],\n        edges: [\n          { source: 'node-1', target: 'node-2', states: [] },\n          { source: 'node-2', target: 'node-3', states: ['active'] },\n        ],\n      });\n      await graph.draw();\n    });\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/animations/element-style-position.spec.ts",
    "content": "import { animationElementStylePosition } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('animation element style and position', () => {\n  it('animation element style and position', async () => {\n    const graph = await createDemoGraph(animationElementStylePosition, { animation: true });\n    await expect(graph).toMatchAnimation(__filename, [0, 200, 1000], () => {\n      graph.addNodeData([\n        { id: 'node-4', style: { x: 50, y: 200, fill: 'orange' } },\n        { id: 'node-5', style: { x: 75, y: 150, fill: 'purple' } },\n        { id: 'node-6', style: { x: 200, y: 100, fill: 'cyan' } },\n      ]);\n      graph.removeNodeData(['node-1']);\n      graph.updateNodeData([{ id: 'node-2', style: { x: 200, y: 200, stroke: 'green' } }]);\n      graph.draw();\n    });\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/auto-adapt-label.spec.ts",
    "content": "import { Point, type Graph } from '@/src';\nimport { behaviorAutoAdaptLabel } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior auto adapt label', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorAutoAdaptLabel, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('disable', async () => {\n    graph.updateBehavior({ key: 'auto-adapt-label', enable: false });\n    await expect(graph).toMatchSnapshot(__filename, 'disable');\n  });\n\n  it('update options', async () => {\n    graph.updateBehavior({ key: 'auto-adapt-label', enable: true, padding: 60 });\n    await expect(graph).toMatchSnapshot(__filename, 'padding-60');\n  });\n\n  it('update sorter', async () => {\n    graph.updateBehavior({ key: 'auto-adapt-label', padding: 0 });\n    const origin: Point = [200, 200, 0];\n    graph.zoomTo(3, false, origin);\n    await expect(graph).toMatchSnapshot(__filename, 'zoom-3');\n    graph.zoomTo(1, false, origin);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/behavior-create-edge-click.spec.ts",
    "content": "import type { EdgeData, Graph } from '@/src';\nimport { ComboEvent, CommonEvent, NodeEvent } from '@/src';\nimport { behaviorCreateEdge } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior create edge click', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorCreateEdge, { animation: false });\n  });\n\n  it('click create edge', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.setBehaviors([{ type: 'create-edge', trigger: 'click' }]);\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node1' }, targetType: 'node' });\n    graph.emit(CommonEvent.POINTER_MOVE, { client: { x: 100, y: 100 } });\n    await expect(graph).toMatchSnapshot(__filename, 'click-edge1-move');\n\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node2' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'click-edge1');\n\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node1' }, targetType: 'node' });\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node3' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'click-edge2');\n\n    graph.setBehaviors([{ type: 'create-edge', trigger: 'click', style: { stroke: 'red', lineWidth: 2 } }]);\n\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node2' }, targetType: 'node' });\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node3' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'click-edge3');\n\n    graph.emit(ComboEvent.CLICK, { target: { id: 'combo1' }, targetType: 'combo' });\n    graph.emit(ComboEvent.CLICK, { target: { id: 'combo2' }, targetType: 'combo' });\n    await expect(graph).toMatchSnapshot(__filename, 'click-edge4-combo');\n\n    graph.setBehaviors([\n      {\n        type: 'create-edge',\n        trigger: 'click',\n        style: { stroke: 'red', lineWidth: 2 },\n        onCreate: (edge: EdgeData) => {\n          const { source, target, ...rest } = edge;\n          return {\n            target: source,\n            source: target,\n            ...rest,\n          };\n        },\n      },\n    ]);\n\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node2' }, targetType: 'node' });\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node3' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'click-custom-edge4');\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/behavior-create-edge-drag.spec.ts",
    "content": "import type { EdgeData, Graph } from '@/src';\nimport { ComboEvent, CommonEvent, NodeEvent } from '@/src';\nimport { behaviorCreateEdge } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior create edge drag', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorCreateEdge, { animation: false });\n  });\n\n  it('drag create edge', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node1' }, targetType: 'node' });\n    graph.emit(CommonEvent.POINTER_MOVE, { client: { x: 100, y: 100 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'drag-edge1-move');\n\n    graph.emit(CommonEvent.POINTER_UP, { target: { id: 'node2' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-edge1');\n\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node1' }, targetType: 'node' });\n    graph.emit(CommonEvent.POINTER_UP, { target: { id: 'node3' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-edge2');\n\n    graph.setBehaviors([{ type: 'create-edge', trigger: 'drag', style: { stroke: 'red', lineWidth: 2 } }]);\n\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node2' }, targetType: 'node' });\n    graph.emit(CommonEvent.POINTER_UP, { target: { id: 'node3' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-edge3');\n\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'combo1' }, targetType: 'combo' });\n    graph.emit(CommonEvent.POINTER_UP, { target: { id: 'combo2' }, targetType: 'combo' });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-edge4-combo');\n\n    graph.setBehaviors([\n      {\n        type: 'create-edge',\n        trigger: 'drag',\n        style: { stroke: 'red', lineWidth: 2 },\n        onCreate: (edge: EdgeData) => {\n          const { source, target, ...rest } = edge;\n          return {\n            target: source,\n            source: target,\n            ...rest,\n          };\n        },\n      },\n    ]);\n\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node2' }, targetType: 'node' });\n    graph.emit(CommonEvent.POINTER_UP, { target: { id: 'node3' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-custom-edge4');\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/brush-select.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { CanvasEvent, CommonEvent } from '@/src';\nimport { behaviorBrushSelect } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior brush select', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorBrushSelect, { animation: false });\n  });\n\n  it('brush select', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_UP, { canvas: { x: 100, y: 100 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-select-clear');\n\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selecting-1');\n\n    graph.emit(CommonEvent.POINTER_UP, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selected-1');\n\n    graph.emit(CanvasEvent.CLICK);\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-clear-1');\n\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 300, y: 300 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selecting-2');\n\n    graph.emit(CommonEvent.POINTER_UP, { canvas: { x: 300, y: 300 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selected-2');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'brush-clear-2');\n\n    graph.setBehaviors([\n      { type: 'brush-select', trigger: 'drag', style: { fill: 'green', lineWidth: 2, stroke: 'blue' } },\n    ]);\n\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selecting-3');\n\n    graph.emit(CommonEvent.POINTER_UP, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selected-3');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'brush-clear-3');\n\n    graph.setBehaviors([{ type: 'brush-select', trigger: 'shift' }]);\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'Shift' });\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selecting-4');\n\n    graph.emit(CommonEvent.KEY_UP, { key: 'Shift' });\n    graph.emit(CommonEvent.POINTER_UP, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selected-4');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'brush-clear-4');\n\n    graph.setBehaviors([{ type: 'brush-select', state: 'active', trigger: 'shift', immediately: true }]);\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'Shift' });\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selecting-5');\n\n    graph.emit(CommonEvent.KEY_UP, { key: 'Shift' });\n    graph.emit(CommonEvent.POINTER_UP, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selected-5');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'brush-clear-5');\n\n    graph.setBehaviors([{ type: 'brush-select', mode: 'union', trigger: 'drag' }]);\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selecting-mode-union');\n\n    graph.emit(CommonEvent.POINTER_UP, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selected-mode-union');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'brush-clear-mode-union');\n\n    graph.setBehaviors([{ type: 'brush-select', mode: 'diff' }]);\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selecting-mode-diff');\n\n    graph.emit(CommonEvent.POINTER_UP, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selected-mode-diff');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'brush-clear-mode-diff');\n\n    graph.setBehaviors([{ type: 'brush-select', mode: 'intersect' }]);\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selecting-mode-intersect');\n\n    graph.emit(CommonEvent.POINTER_UP, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selected-mode-intersect');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'brush-clear-mode-intersect');\n\n    // zoom to test line width\n    graph.zoomTo(5);\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 250, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'brush-selecting-zoom');\n    graph.emit(CommonEvent.POINTER_UP, { canvas: { x: 250, y: 400 } });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/click-select-catch.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { CanvasEvent, NodeEvent } from '@/src';\nimport { behaviorClickSelect } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior click-select element', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorClickSelect, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('delete selected element', async () => {\n    graph.setBehaviors([\n      { type: 'click-select', state: 'active', unselectedState: 'inactive', multiple: true, trigger: [] },\n    ]);\n\n    graph.emit(NodeEvent.CLICK, { target: { id: '0' }, targetType: 'node' });\n    graph.emit(NodeEvent.CLICK, { target: { id: '1' }, targetType: 'node' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'selected');\n\n    graph.removeNodeData(['1']);\n    graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename, 'deleted');\n\n    graph.emit(CanvasEvent.CLICK, { target: {} });\n\n    await expect(graph).toMatchSnapshot(__filename, 'clear');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/click-select.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { CanvasEvent, CommonEvent, EdgeEvent, NodeEvent } from '@/src';\nimport { behaviorClickSelect } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior click-select element', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorClickSelect, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.emit(NodeEvent.CLICK, { target: { id: '0' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'after-select');\n\n    graph.emit(NodeEvent.CLICK, { target: { id: '0' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'after-deselect');\n  });\n\n  it('state and unselectedState', async () => {\n    graph.setBehaviors([{ type: 'click-select', state: 'active', unselectedState: 'inactive' }]);\n\n    graph.emit(NodeEvent.CLICK, { target: { id: '0' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'custom-state');\n    graph.emit(NodeEvent.CLICK, { target: { id: '0' }, targetType: 'node' });\n  });\n\n  it('state and neighborState', async () => {\n    graph.setBehaviors([\n      {\n        type: 'click-select',\n        state: 'selected',\n        neighborState: 'active',\n        unselectedState: 'inactive',\n        degree: 1,\n      },\n    ]);\n\n    graph.emit(NodeEvent.CLICK, { target: { id: '0' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'custom-neighborState');\n    graph.emit(NodeEvent.CLICK, { target: { id: '0' }, targetType: 'node' });\n  });\n\n  it('1 degree', async () => {\n    graph.setBehaviors([\n      {\n        type: 'click-select',\n        degree: 1,\n        state: 'selected',\n        neighborState: 'selected',\n        unselectedState: undefined,\n      },\n    ]);\n\n    graph.emit(NodeEvent.CLICK, { target: { id: '0' }, targetType: 'node' });\n    await expect(graph).toMatchSnapshot(__filename, 'node-1-degree');\n    graph.emit(CanvasEvent.CLICK, { target: {}, targetType: 'canvas' });\n\n    graph.emit(EdgeEvent.CLICK, { target: { id: '0-1' }, targetType: 'edge' });\n    await expect(graph).toMatchSnapshot(__filename, 'edge-1-degree');\n    graph.emit(CanvasEvent.CLICK, { target: {}, targetType: 'canvas' });\n  });\n\n  it('multiple', async () => {\n    graph.setBehaviors([{ type: 'click-select', multiple: true, degree: 0 }]);\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'shift' });\n    graph.emit(NodeEvent.CLICK, { target: { id: '0' }, targetType: 'node' });\n    graph.emit(NodeEvent.CLICK, { target: { id: '1' }, targetType: 'node' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'shift' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'multiple-shift');\n\n    graph.setBehaviors([{ type: 'click-select', multiple: true, trigger: ['meta'] }]);\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'meta' });\n    graph.emit(NodeEvent.CLICK, { target: { id: '0' }, targetType: 'node' });\n    graph.emit(NodeEvent.CLICK, { target: { id: '1' }, targetType: 'node' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'meta' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'multiple-meta');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/collapse-expand-combo.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { ComboEvent } from '@/src';\nimport { behaviorExpandCollapseCombo } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior combo expand collapse', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorExpandCollapseCombo, { animation: true });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('expand combo-1', async () => {\n    // @ts-expect-error access private property\n    const combo1 = graph.context.element?.getElement('combo-1');\n\n    await expect(graph).toMatchAnimation(\n      __filename,\n      [0, 500, 1000],\n      () => {\n        graph.emit(ComboEvent.DBLCLICK, { target: combo1, targetType: 'combo' });\n      },\n      'expand-combo-1',\n    );\n  });\n\n  it('collapse combo-2', async () => {\n    // @ts-expect-error access private property\n    const combo2 = graph.context.element?.getElement('combo-2');\n\n    await expect(graph).toMatchAnimation(\n      __filename,\n      [0, 500, 1000],\n      () => {\n        graph.emit(ComboEvent.DBLCLICK, { target: combo2, targetType: 'combo' });\n      },\n      'collapse-combo-2',\n    );\n  });\n\n  it('collapse combo-1, expand combo-2', async () => {\n    await graph.collapseElement('combo-1');\n\n    // @ts-expect-error access private property\n    const combo2 = graph.context.element?.getElement('combo-2');\n\n    await expect(graph).toMatchAnimation(\n      __filename,\n      [0, 500, 1000],\n      () => {\n        graph.emit(ComboEvent.DBLCLICK, { target: combo2, targetType: 'combo' });\n      },\n      'collapse-combo-1-expand-combo-2',\n    );\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/collapse-expand-node.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { NodeEvent } from '@/src';\nimport { behaviorExpandCollapseNode } from '@@/demos/behavior-expand-collapse-node';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior node expand collapse', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorExpandCollapseNode, { animation: true });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('collapse B', async () => {\n    // @ts-expect-error access private property\n    const B = graph.context.element!.getElement('B');\n\n    await expect(graph).toMatchAnimation(\n      __filename,\n      [0, 500, 1000],\n      () => {\n        graph.emit(NodeEvent.CLICK, { target: B, targetType: 'node' });\n      },\n      'collapse-B',\n    );\n  });\n\n  it('expand C', async () => {\n    // @ts-expect-error access private property\n    const C = graph.context.element!.getElement('C');\n\n    await expect(graph).toMatchAnimation(\n      __filename,\n      [0, 500, 1000],\n      () => {\n        graph.emit(NodeEvent.CLICK, { target: C, targetType: 'node' });\n      },\n      'expand-C',\n    );\n  });\n\n  it('expand B', async () => {\n    // @ts-expect-error access private property\n    const B = graph.context.element!.getElement('B');\n\n    await expect(graph).toMatchAnimation(\n      __filename,\n      [0, 500, 1000],\n      () => {\n        graph.emit(NodeEvent.CLICK, { target: B, targetType: 'node' });\n      },\n      'expand-B-again',\n    );\n  });\n\n  it('collapse A', async () => {\n    // @ts-expect-error access private property\n    const A = graph.context.element!.getElement('A');\n\n    await expect(graph).toMatchAnimation(\n      __filename,\n      [0, 500, 1000],\n      () => {\n        graph.emit(NodeEvent.CLICK, { target: A, targetType: 'node' });\n      },\n      'collapse-A',\n    );\n  });\n\n  it('expand A', async () => {\n    // @ts-expect-error access private property\n    const A = graph.context.element!.getElement('A');\n\n    await expect(graph).toMatchAnimation(\n      __filename,\n      [0, 500, 1000],\n      () => {\n        graph.emit(NodeEvent.CLICK, { target: A, targetType: 'node' });\n      },\n      'expand-A',\n    );\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/collapse-expand.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { ComboEvent } from '@/src';\nimport { behaviorExpandCollapseCombo } from '@@/demos';\nimport { createDemoGraph, sleep } from '@@/utils';\n\ndescribe('behavior combo expand collapse', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorExpandCollapseCombo, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('collapse', async () => {\n    // collapse combo-2\n    // @ts-expect-error private method\n    const combo2 = graph.context.element.getElement('combo-2');\n    graph.emit(ComboEvent.DBLCLICK, { target: combo2, targetType: 'combo' });\n    await expect(graph).toMatchSnapshot(__filename, 'collapse-combo-2');\n  });\n\n  it('expand', async () => {\n    // expand combo-2\n    // @ts-expect-error private method\n    const combo2 = graph.context.element.getElement('combo-2');\n    graph.emit(ComboEvent.DBLCLICK, { target: combo2, targetType: 'combo' });\n    // await async invoke\n    await sleep(100);\n    // expand combo-1\n    // @ts-expect-error private method\n    const combo1 = graph.context.element.getElement('combo-1');\n    graph.emit(ComboEvent.DBLCLICK, { target: combo1, targetType: 'combo' });\n    await expect(graph).toMatchSnapshot(__filename, 'expand-combo-1');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/drag-canvas.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { CommonEvent } from '@/src';\nimport { behaviorDragCanvas } from '@@/demos';\nimport { createDemoGraph, createGraph, dispatchCanvasEvent, sleep } from '@@/utils';\n\ndescribe('behavior drag canvas', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorDragCanvas, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', () => {\n    expect(graph.getBehaviors()).toEqual([\n      'drag-canvas',\n      {\n        type: 'drag-canvas',\n        key: 'drag-canvas',\n        trigger: {\n          up: ['ArrowUp'],\n          down: ['ArrowDown'],\n          right: ['ArrowRight'],\n          left: ['ArrowLeft'],\n        },\n      },\n    ]);\n  });\n\n  it('arrow up', () => {\n    const [x, y] = graph.getPosition();\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowUp' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowUp' });\n\n    expect(graph.getPosition()).toBeCloseTo([x, y - 10]);\n  });\n\n  it('arrow down', () => {\n    const [x, y] = graph.getPosition();\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowDown' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowDown' });\n    expect(graph.getPosition()).toBeCloseTo([x, y + 10]);\n  });\n\n  it('arrow left', () => {\n    const [x, y] = graph.getPosition();\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowLeft' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowLeft' });\n    expect(graph.getPosition()).toBeCloseTo([x - 10, y]);\n  });\n\n  it('arrow right', () => {\n    const [x, y] = graph.getPosition();\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowRight' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowRight' });\n    expect(graph.getPosition()).toBeCloseTo([x + 10, y]);\n  });\n\n  it('drag', () => {\n    const [x, y] = graph.getPosition();\n\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { movement: { x: 10, y: 10 }, targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    expect(graph.getPosition()).toBeCloseTo([x + 10, y + 10]);\n  });\n\n  it('drag for mobile', () => {\n    const [x, y] = graph.getPosition();\n\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { dx: 10, dy: 10, targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    expect(graph.getPosition()).toBeCloseTo([x + 10, y + 10]);\n\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { dx: -10, dy: -10, targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    expect(graph.getPosition()).toBeCloseTo([x, y]);\n  });\n\n  it('sensitivity', async () => {\n    graph.updateBehavior({ key: 'drag-canvas', sensitivity: 20 });\n\n    const [x, y] = graph.getPosition();\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowRight' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowRight' });\n    expect(graph.getPosition()).toBeCloseTo([x + 20, y]);\n\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('use shortcut to drag in the x-axis direction', () => {\n    graph.updateBehavior({ key: 'drag-canvas', direction: 'x' });\n\n    const [x, y] = graph.getPosition();\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowRight' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowRight' });\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowDown' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowDown' });\n\n    expect(graph.getPosition()).toBeCloseTo([x + 20, y]);\n  });\n\n  it('use shortcut to drag in the y-axis direction', () => {\n    graph.updateBehavior({ key: 'drag-canvas', direction: 'y' });\n\n    const [x, y] = graph.getPosition();\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowRight' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowRight' });\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowDown' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowDown' });\n\n    expect(graph.getPosition()).toBeCloseTo([x, y + 20]);\n    graph.updateBehavior({ key: 'drag-canvas', direction: 'both' });\n  });\n\n  it('onFinish with key', async () => {\n    const onFinish = jest.fn();\n    graph.updateBehavior({ key: 'drag-canvas', onFinish });\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowRight' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowRight' });\n\n    await sleep(500);\n    expect(onFinish).toHaveBeenCalledTimes(1);\n\n    onFinish.mockReset();\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowRight' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowRight' });\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowRight' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowRight' });\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowRight' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowRight' });\n\n    await sleep(500);\n    expect(onFinish).toHaveBeenCalledTimes(1);\n  });\n\n  it('onFinish with drag', async () => {\n    const onFinish = jest.fn();\n    graph.updateBehavior({ key: 'drag-canvas', trigger: 'drag', onFinish });\n\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { movement: { x: 10, y: 10 }, targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    expect(onFinish).toHaveBeenCalledTimes(1);\n  });\n\n  it('drag in the x-axis direction', () => {\n    graph.setBehaviors([{ type: 'drag-canvas', key: 'drag-canvas', trigger: 'drag', direction: 'x' }]);\n\n    const [x, y] = graph.getPosition();\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { movement: { x: 10, y: 10 }, targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    expect(graph.getPosition()).toBeCloseTo([x + 10, y]);\n  });\n\n  it('drag in the y-axis direction', () => {\n    graph.updateBehavior({ key: 'drag-canvas', trigger: 'drag', direction: 'y' });\n\n    const [x, y] = graph.getPosition();\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { movement: { x: 10, y: 10 }, targetType: 'canvas' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    expect(graph.getPosition()).toBeCloseTo([x, y + 10]);\n  });\n\n  it('trigger on element', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],\n      },\n      node: {\n        style: {\n          size: 20,\n        },\n      },\n      behaviors: [{ type: 'drag-canvas', enable: true }],\n    });\n\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename, 'drag-on-element-default');\n\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'node' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { movement: { x: -50, y: -50 }, targetType: 'node' });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    await expect(graph).toMatchSnapshot(__filename, 'drag-on-element');\n  });\n\n  it('range', () => {\n    graph.updateBehavior({ key: 'drag-canvas', trigger: 'drag', direction: 'both', range: 0.5 });\n\n    const emitDragEvent = (dx: number, dy: number, count: number) => {\n      for (let i = 0; i < count; i++) {\n        dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { targetType: 'canvas' });\n        dispatchCanvasEvent(graph, CommonEvent.DRAG, { movement: { x: dx, y: dy }, targetType: 'canvas' });\n        dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n      }\n    };\n\n    const [canvasWidth, canvasHeight] = graph.getCanvas().getSize();\n    emitDragEvent(10, 0, 60);\n    expect(graph.getPosition()[0]).toBeCloseTo(canvasWidth / 2);\n    emitDragEvent(-10, 0, 60);\n    expect(graph.getPosition()[0]).toBeCloseTo(-canvasWidth / 2);\n    emitDragEvent(0, -10, 60);\n    expect(graph.getPosition()[0]).toBeCloseTo(-canvasHeight / 2);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/drag-element-bug.spec.ts",
    "content": "import { Graph, NodeEvent } from '@/src';\nimport { positionOf } from '@/src/utils/position';\nimport { createGraphCanvas } from '@@/utils';\n\ndescribe('behavior drag element bug', () => {\n  it('drag on non-default zoom', async () => {\n    const graph = new Graph({\n      animation: false,\n      container: createGraphCanvas(document.getElementById('container')),\n      data: {\n        nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],\n      },\n      behaviors: ['drag-element'],\n    });\n\n    await graph.draw();\n\n    expect(graph.getZoom()).toBe(1);\n    expect(positionOf(graph.getNodeData('node-1'))).toEqual([100, 100, 0]);\n\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-1' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 20, dy: 20 });\n    graph.emit(NodeEvent.DRAG_END);\n    expect(positionOf(graph.getNodeData('node-1'))).toEqual([120, 120, 0]);\n\n    graph.zoomTo(2);\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-1' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 20, dy: 20 });\n    graph.emit(NodeEvent.DRAG_END);\n    expect(positionOf(graph.getNodeData('node-1'))).toEqual([130, 130, 0]);\n\n    graph.zoomTo(0.5);\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-1' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 20, dy: 20 });\n    graph.emit(NodeEvent.DRAG_END);\n    expect(positionOf(graph.getNodeData('node-1'))).toEqual([170, 170, 0]);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/drag-element-combo.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { CanvasEvent, ComboEvent, NodeEvent } from '@/src';\nimport { behaviorExpandCollapseCombo } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior drag combo', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorExpandCollapseCombo, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    graph.setBehaviors([{ type: 'drag-element', dropEffect: 'link' }]);\n    graph.expandElement('combo-1');\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('drag node out', async () => {\n    // drag node-2 to combo-2\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-2' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 80, dy: 60 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-node-2-before-drop-out');\n    graph.emit(ComboEvent.DROP, { target: { id: 'combo-2' } });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-node-2-after-drop-out');\n\n    // drag node-1 to canvas\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-1' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: -70, dy: -70 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-node-1-before-drop-out');\n    graph.emit(CanvasEvent.DROP, { target: {} });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-node-1-after-drop-out');\n  });\n\n  it('drag node into', async () => {\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-1' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 250, dy: 250 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-node-1-before-drop-into');\n    graph.emit(ComboEvent.DROP, { target: { id: 'combo-2' } });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-node-1-after-drop-into');\n  });\n\n  it('drag node move', async () => {\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-1' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 100, dy: 100 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-node-1-before-drop-move');\n    graph.emit(ComboEvent.DROP, { target: { id: 'combo-2' } });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-node-1-after-drop-move');\n  });\n\n  it('drag combo move', async () => {\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'combo-1' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: 100, dy: 100 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-1-before-drop-move');\n    graph.emit(ComboEvent.DROP, { target: { id: 'combo-2' } });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-1-after-drop-move');\n  });\n\n  it('drag combo out', async () => {\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'combo-1' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: -250, dy: -250 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-1-before-drop-out');\n    graph.emit(CanvasEvent.DROP, { target: {} });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-1-after-drop-out');\n  });\n\n  it('drag combo into', async () => {\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'combo-2' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: -200, dy: -200 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-2-before-drop-into');\n    graph.emit(ComboEvent.DROP, { target: { id: 'combo-1' } });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-2-after-drop-into');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/drag-element.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { ComboEvent, CommonEvent, NodeEvent } from '@/src';\nimport { behaviorDragNode } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior drag element', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorDragNode, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('pointer cursor', () => {\n    graph.emit(NodeEvent.POINTER_ENTER, {\n      target: { id: 'node-4' },\n      targetType: 'node',\n      type: CommonEvent.POINTER_ENTER,\n    });\n    expect(graph.getCanvas().getConfig().cursor).toBe('grab');\n\n    graph.emit(NodeEvent.POINTER_LEAVE, {\n      target: { id: 'node-4' },\n      targetType: 'node',\n      type: CommonEvent.POINTER_LEAVE,\n    });\n    expect(graph.getCanvas().getConfig().cursor).toBe('default');\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-4' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 20, dy: 20 });\n    expect(graph.getCanvas().getConfig().cursor).toBe('grabbing');\n    graph.emit(NodeEvent.DRAG_END);\n\n    await expect(graph).toMatchSnapshot(__filename, 'after-drag');\n  });\n\n  it('hide edges', async () => {\n    graph.setBehaviors([{ type: 'drag-element', hideEdge: 'both' }]);\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-4' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 20, dy: 20 });\n    await expect(graph).toMatchSnapshot(__filename, 'hideEdge-both');\n    graph.emit(NodeEvent.DRAG_END);\n\n    graph.setBehaviors([{ type: 'drag-element', hideEdge: 'in' }]);\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-3' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 0, dy: 20 });\n    await expect(graph).toMatchSnapshot(__filename, 'hideEdge-in');\n    graph.emit(NodeEvent.DRAG_END);\n\n    graph.setBehaviors([{ type: 'drag-element', hideEdge: 'out' }]);\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-3' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 0, dy: 20 });\n    await expect(graph).toMatchSnapshot(__filename, 'hideEdge-out');\n    graph.emit(NodeEvent.DRAG_END);\n  });\n\n  it('drag node shadow', async () => {\n    graph.setBehaviors([{ type: 'drag-element', shadow: true, shadowStroke: 'red', shadowStrokeOpacity: 1 }]);\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-4' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 20, dy: 20 });\n    await expect(graph).toMatchSnapshot(__filename, 'shadow');\n    graph.emit(NodeEvent.DRAG_END);\n    await expect(graph).toMatchSnapshot(__filename, 'shadow-after-drag');\n  });\n\n  it('drag combo', async () => {\n    graph.setBehaviors(['drag-element']);\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'combo-1' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: 20, dy: 20 });\n    graph.emit(ComboEvent.DRAG_END);\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo');\n  });\n\n  it('drag combo shadow', async () => {\n    graph.setBehaviors([{ type: 'drag-element', shadow: true, shadowStroke: 'red', shadowStrokeOpacity: 1 }]);\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'combo-1' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: 20, dy: 20 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-shadow');\n    graph.emit(ComboEvent.DRAG_END);\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-shadow-after-drag');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/fix-element-size.spec.ts",
    "content": "import { CanvasEvent, CommonEvent, EdgeEvent, NodeEvent, type Graph } from '@/src';\nimport { behaviorFixElementSize } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior fix element size', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorFixElementSize, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('fix entire element size', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    await expect(graph).toMatchSnapshot(__filename, 'entire-size-1');\n    graph.zoomTo(0.6);\n    await expect(graph).toMatchSnapshot(__filename, 'entire-size-0.6');\n    graph.zoomTo(1);\n\n    graph.emit(CanvasEvent.CLICK, { target: {} });\n  });\n\n  it('fix lineWidth of key', async () => {\n    graph.updateBehavior({\n      key: 'fix-element-size',\n      node: [{ shape: 'key', fields: ['lineWidth'] }],\n      edge: [{ shape: 'key', fields: ['lineWidth'] }],\n    });\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'shift' });\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node0' }, targetType: 'node' });\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node1' }, targetType: 'node' });\n    graph.emit(EdgeEvent.CLICK, { target: { id: 'node0-node1' }, targetType: 'edge' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'shift' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'lineWidth-1');\n    await graph.zoomTo(0.6);\n    await expect(graph).toMatchSnapshot(__filename, 'lineWidth-0.6');\n    await graph.zoomTo(1);\n\n    graph.emit(CanvasEvent.CLICK, { target: {} });\n  });\n\n  it('fix fontSize of label', async () => {\n    graph.updateBehavior({\n      key: 'fix-element-size',\n      node: [{ shape: 'label' }],\n      edge: [{ shape: 'label' }],\n    });\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'shift' });\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node0' }, targetType: 'node' });\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node1' }, targetType: 'node' });\n    graph.emit(EdgeEvent.CLICK, { target: { id: 'node0-node1' }, targetType: 'edge' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'shift' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'fontSize-1');\n    await graph.zoomTo(0.6);\n    await expect(graph).toMatchSnapshot(__filename, 'fontSize-0.6');\n    await graph.zoomTo(1);\n\n    graph.emit(CanvasEvent.CLICK, { target: {} });\n  });\n\n  it('fix both lineWidth and fontSize', async () => {\n    graph.updateBehavior({\n      key: 'fix-element-size',\n      node: [{ shape: 'key', fields: ['lineWidth'] }, { shape: 'label' }],\n      edge: [{ shape: 'key', fields: ['lineWidth'] }, { shape: 'label' }],\n    });\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'shift' });\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node0' }, targetType: 'node' });\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node1' }, targetType: 'node' });\n    graph.emit(EdgeEvent.CLICK, { target: { id: 'node0-node1' }, targetType: 'edge' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'shift' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'lineWidth-fontSize-1');\n    await graph.zoomTo(0.6);\n    await expect(graph).toMatchSnapshot(__filename, 'lineWidth-fontSize-0.6');\n    await graph.zoomTo(1);\n\n    graph.emit(CanvasEvent.CLICK, { target: {} });\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/focus-element.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { ComboEvent, NodeEvent } from '@/src';\nimport { behaviorFocusElement } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior focus element', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorFocusElement, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('focus node', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-1' }, targetType: 'node' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'focus-node-1');\n\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-2' }, targetType: 'node' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'focus-node-2');\n\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-3' }, targetType: 'node' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'focus-node-3');\n\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-4' }, targetType: 'node' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'focus-node-4');\n  });\n\n  it('focus combo', async () => {\n    graph.emit(ComboEvent.CLICK, { target: { id: 'combo-1' }, targetType: 'combo' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'focus-combo');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/hover-activate.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { CommonEvent, EdgeEvent, NodeEvent } from '@/src';\nimport { behaviorHoverActivate } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior hover-activate element', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorHoverActivate, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.emit(NodeEvent.POINTER_ENTER, { target: { id: '0' }, targetType: 'node', type: CommonEvent.POINTER_ENTER });\n\n    await expect(graph).toMatchSnapshot(__filename, 'after-hover');\n\n    graph.emit(NodeEvent.POINTER_LEAVE, { target: { id: '0' }, targetType: 'node', type: CommonEvent.POINTER_LEAVE });\n\n    await expect(graph).toMatchSnapshot(__filename, 'after-hover-out');\n  });\n\n  it('state and inactiveState', async () => {\n    graph.setBehaviors([{ type: 'hover-activate', state: 'active', inactiveState: 'inactive' }]);\n\n    graph.emit(NodeEvent.POINTER_ENTER, { target: { id: '0' }, targetType: 'node', type: CommonEvent.POINTER_ENTER });\n\n    await expect(graph).toMatchSnapshot(__filename, 'state');\n\n    graph.emit(NodeEvent.POINTER_LEAVE, { target: { id: '0' }, targetType: 'node', type: CommonEvent.POINTER_LEAVE });\n  });\n\n  it('1 degree', async () => {\n    graph.setBehaviors([{ type: 'hover-activate', state: 'active', inactiveState: 'inactive', degree: 1 }]);\n\n    graph.emit(NodeEvent.POINTER_ENTER, { target: { id: '0' }, targetType: 'node', type: CommonEvent.POINTER_ENTER });\n\n    await expect(graph).toMatchSnapshot(__filename, '1-degree-node');\n\n    graph.emit(NodeEvent.POINTER_LEAVE, { target: { id: '0' }, targetType: 'node', type: CommonEvent.POINTER_LEAVE });\n\n    graph.emit(EdgeEvent.POINTER_ENTER, { target: { id: '0-1' }, targetType: 'edge', type: CommonEvent.POINTER_ENTER });\n\n    await expect(graph).toMatchSnapshot(__filename, '1-degree-edge');\n\n    graph.emit(EdgeEvent.POINTER_LEAVE, { target: { id: '0-1' }, targetType: 'edge', type: CommonEvent.POINTER_LEAVE });\n  });\n\n  it('2 degree', async () => {\n    graph.setBehaviors([{ type: 'hover-activate', state: 'active', inactiveState: 'inactive', degree: 2 }]);\n\n    graph.emit(NodeEvent.POINTER_ENTER, { target: { id: '0' }, targetType: 'node', type: CommonEvent.POINTER_ENTER });\n\n    await expect(graph).toMatchSnapshot(__filename, '2-degree-node');\n\n    graph.emit(NodeEvent.POINTER_LEAVE, { target: { id: '0' }, targetType: 'node', type: CommonEvent.POINTER_LEAVE });\n\n    graph.emit(EdgeEvent.POINTER_ENTER, { target: { id: '0-1' }, targetType: 'edge', type: CommonEvent.POINTER_ENTER });\n\n    await expect(graph).toMatchSnapshot(__filename, '2-degree-edge');\n\n    graph.emit(EdgeEvent.POINTER_LEAVE, { target: { id: '0-1' }, targetType: 'edge', type: CommonEvent.POINTER_LEAVE });\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/lasso-select.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { CanvasEvent, CommonEvent } from '@/src';\nimport { behaviorLassoSelect } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior lasso select', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorLassoSelect, { animation: false });\n  });\n\n  it('lasso select', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_UP);\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-select-clear');\n\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 400 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 400, y: 400 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 400, y: 100 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selecting-1');\n\n    graph.emit(CommonEvent.POINTER_UP);\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selected-1');\n\n    graph.emit(CanvasEvent.CLICK);\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-1');\n\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 400 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 400, y: 400 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selecting-2');\n\n    graph.emit(CommonEvent.POINTER_UP);\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selected-2');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-2');\n\n    graph.updateBehavior({\n      key: 'lasso-select',\n      style: { fill: 'green', lineWidth: 2, stroke: 'blue' },\n    });\n\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 300 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 300, y: 300 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 300, y: 100 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selecting-3');\n\n    graph.emit(CommonEvent.POINTER_UP);\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selected-3');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-3');\n\n    graph.updateBehavior({ key: 'lasso-select', trigger: 'shift' });\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'Shift' });\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 200 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 200, y: 200 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 200, y: 100 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selecting-4');\n\n    graph.emit(CommonEvent.KEY_UP, { key: 'Shift' });\n    graph.emit(CommonEvent.POINTER_UP);\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selected-4');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-4');\n\n    graph.updateBehavior({ key: 'lasso-select', state: 'active', trigger: 'shift', immediately: true });\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'Shift' });\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 500 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 500, y: 500 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 500, y: 100 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selecting-5');\n\n    graph.emit(CommonEvent.KEY_UP, { key: 'Shift' });\n    graph.emit(CommonEvent.POINTER_UP);\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selected-5');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-5');\n\n    graph.updateBehavior({ key: 'lasso-select', mode: 'union', trigger: 'drag' });\n\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 500 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 500, y: 500 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 500, y: 100 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selecting-mode-union');\n\n    graph.emit(CommonEvent.POINTER_UP);\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selected-mode-union');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-mode-union');\n\n    graph.updateBehavior({ key: 'lasso-select', mode: 'diff' });\n\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 500 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 500, y: 500 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 500, y: 100 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selecting-mode-diff');\n\n    graph.emit(CommonEvent.POINTER_UP);\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selected-mode-diff');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-mode-diff');\n\n    graph.updateBehavior({ key: 'lasso-select', mode: 'intersect' });\n\n    graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 500 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 500, y: 500 } });\n    graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 500, y: 100 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selecting-mode-intersect');\n\n    graph.emit(CommonEvent.POINTER_UP);\n\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-selected-mode-intersect');\n\n    graph.emit(CanvasEvent.CLICK);\n    await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-mode-intersect');\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/optimize-viewport-transform.spec.ts",
    "content": "import type { ElementType, Graph } from '@/src';\nimport { GraphEvent } from '@/src';\nimport { behaviorOptimizeViewportTransform } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\nimport type { DisplayObject } from '@antv/g';\n\ndescribe('behavior optimize canvas', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorOptimizeViewportTransform, { animation: false });\n  });\n\n  it('viewport', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.emit(GraphEvent.BEFORE_TRANSFORM, {\n      type: GraphEvent.BEFORE_TRANSFORM,\n      data: {\n        mode: 'relative',\n        translate: [0, -3],\n      },\n    });\n    await expect(graph).toMatchSnapshot(__filename, 'viewport-change-key');\n    graph.emit(GraphEvent.AFTER_TRANSFORM, {\n      type: GraphEvent.AFTER_TRANSFORM,\n      data: {\n        mode: 'relative',\n        translate: [0, 3],\n      },\n    });\n    await expect(graph).toMatchSnapshot(__filename, 'after-viewport-change');\n  });\n\n  it(\"show node's key and icon shapes\", async () => {\n    graph.updateBehavior({\n      key: 'optimize-viewport-transform',\n      shapes: (type: ElementType, shape: DisplayObject) => type === 'node' && ['key', 'text'].includes(shape.className),\n    });\n    graph.emit(GraphEvent.BEFORE_TRANSFORM, {\n      type: GraphEvent.BEFORE_TRANSFORM,\n      data: {\n        mode: 'relative',\n        translate: [0, -3],\n      },\n    });\n    await expect(graph).toMatchSnapshot(__filename, 'viewport-change-key-text');\n    graph.emit(GraphEvent.AFTER_TRANSFORM, {\n      type: GraphEvent.AFTER_TRANSFORM,\n      data: {\n        mode: 'relative',\n        translate: [0, 3],\n      },\n    });\n    await expect(graph).toMatchSnapshot(__filename, 'after-viewport-change');\n  });\n\n  it(\"show node's key and edge's key\", async () => {\n    graph.updateBehavior({\n      key: 'optimize-viewport-transform',\n      shapes: {\n        node: ['key'],\n        edge: ['key'],\n      },\n    });\n    graph.emit(GraphEvent.BEFORE_TRANSFORM, {\n      type: GraphEvent.BEFORE_TRANSFORM,\n      data: {\n        mode: 'relative',\n        translate: [0, -3],\n      },\n    });\n    await expect(graph).toMatchSnapshot(__filename, 'viewport-change-keys');\n    graph.emit(GraphEvent.AFTER_TRANSFORM, {\n      type: GraphEvent.AFTER_TRANSFORM,\n      data: {\n        mode: 'relative',\n        translate: [0, 3],\n      },\n    });\n    await expect(graph).toMatchSnapshot(__filename, 'after-viewport-change');\n  });\n\n  it('destroy', () => {\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/scroll-canvas.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { CommonEvent } from '@/src';\nimport { ScrollCanvasOptions } from '@/src/behaviors';\nimport { behaviorScrollCanvas } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior scroll canvas', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorScrollCanvas, { animation: false });\n  });\n\n  function setBehavior(options?: Partial<ScrollCanvasOptions>) {\n    graph.setBehaviors((behaviors) =>\n      behaviors.map((behavior) => {\n        if (typeof behavior === 'object' && behavior.type === 'scroll-canvas') {\n          return { ...behavior, ...options };\n        }\n        return behavior;\n      }),\n    );\n  }\n\n  it('default status', () => {\n    expect(graph.getBehaviors()).toEqual([\n      {\n        key: 'scroll-canvas',\n        type: 'scroll-canvas',\n      },\n    ]);\n  });\n\n  function emitWheelEvent(options?: { deltaX: number; deltaY: number }) {\n    const dom = graph.getCanvas().getContextService().getDomElement();\n    dom?.dispatchEvent(new WheelEvent(CommonEvent.WHEEL, options));\n  }\n\n  it('scroll', async () => {\n    const [x, y] = graph.getPosition();\n    emitWheelEvent({ deltaX: -10, deltaY: -10 });\n    expect(graph.getPosition()).toBeCloseTo([x + 10, y + 10]);\n\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('direction', async () => {\n    setBehavior({ direction: 'x' });\n    let [x, y] = graph.getPosition();\n    emitWheelEvent({ deltaX: -10, deltaY: -10 });\n    expect(graph.getPosition()).toBeCloseTo([x + 10, y]);\n\n    setBehavior({ direction: 'y' });\n    [x, y] = graph.getPosition();\n    emitWheelEvent({ deltaX: -10, deltaY: -10 });\n    expect(graph.getPosition()).toBeCloseTo([x, y + 10]);\n\n    setBehavior({ direction: undefined });\n  });\n\n  it('sensitivity', () => {\n    const sensitivity = 5;\n    setBehavior({ sensitivity });\n    const [x, y] = graph.getPosition();\n    const deltaX = -10,\n      deltaY = -10;\n    emitWheelEvent({ deltaX, deltaY });\n    expect(graph.getPosition()).toBeCloseTo([x + Math.abs(deltaX * sensitivity), y + Math.abs(deltaY * sensitivity)]);\n  });\n\n  const shortcutScrollCanvasOptions: ScrollCanvasOptions = {\n    key: 'shortcut-scroll-canvas',\n    type: 'scroll-canvas',\n    trigger: {\n      up: ['ArrowUp'],\n      down: ['ArrowDown'],\n      right: ['ArrowRight'],\n      left: ['ArrowLeft'],\n    },\n  };\n\n  it('custom trigger', () => {\n    graph.setBehaviors((behavior) => [...behavior, shortcutScrollCanvasOptions]);\n\n    let [x, y] = graph.getPosition();\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowUp' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowUp' });\n    expect(graph.getPosition()).toBeCloseTo([x, y - 10]);\n\n    [x, y] = graph.getPosition();\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowDown' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowDown' });\n    expect(graph.getPosition()).toBeCloseTo([x, y + 10]);\n\n    [x, y] = graph.getPosition();\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowLeft' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowLeft' });\n    expect(graph.getPosition()).toBeCloseTo([x - 10, y]);\n\n    [x, y] = graph.getPosition();\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowRight' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'ArrowRight' });\n    expect(graph.getPosition()).toBeCloseTo([x + 10, y]);\n  });\n\n  it('range', () => {\n    graph.setBehaviors((behavior) => [...behavior, { ...shortcutScrollCanvasOptions, range: 0.5 }]);\n\n    const emitArrow = (key: 'ArrowRight' | 'ArrowLeft' | 'ArrowUp' | 'ArrowDown', count: number) => {\n      for (let i = 0; i < count; i++) {\n        graph.emit(CommonEvent.KEY_DOWN, { key });\n        graph.emit(CommonEvent.KEY_UP, { key });\n      }\n    };\n\n    const [canvasWidth, canvasHeight] = graph.getCanvas().getSize();\n    emitArrow('ArrowRight', 50);\n    expect(graph.getPosition()[0]).toBeCloseTo(canvasWidth / 2);\n    emitArrow('ArrowLeft', 50);\n    expect(graph.getPosition()[0]).toBeCloseTo(-canvasWidth / 2);\n    emitArrow('ArrowUp', 50);\n    expect(graph.getPosition()[1]).toBeCloseTo(-canvasHeight / 2);\n  });\n\n  it('destroy', () => {\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/behaviors/zoom-canvas.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { CommonEvent, ContainerEvent } from '@/src';\nimport type { ZoomCanvasOptions } from '@/src/behaviors/zoom-canvas';\nimport { behaviorZoomCanvas } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('behavior zoom canvas', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(behaviorZoomCanvas, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', () => {\n    expect(graph.getZoom()).toBe(1);\n    expect(graph.getBehaviors()).toEqual([{ type: 'zoom-canvas' }]);\n  });\n\n  it('zoom in', async () => {\n    graph.emit(CommonEvent.WHEEL, { deltaY: -10 });\n\n    expect(graph.getZoom()).toBe(1.1);\n\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('zoom out', () => {\n    const currentZoom = graph.getZoom();\n\n    graph.emit(CommonEvent.WHEEL, { deltaY: 5 });\n\n    expect(graph.getZoom()).toBe(currentZoom * 0.95);\n\n    graph.emit(CommonEvent.WHEEL, { deltaY: 5 });\n\n    expect(graph.getZoom()).toBeCloseTo(currentZoom * 0.95 ** 2);\n  });\n\n  it('mobile zoom', async () => {\n    const initZoom = graph.getZoom();\n    const canvas = graph.getCanvas();\n    const container = canvas.getContainer();\n    if (!container) return;\n\n    const initialBehaviors = graph.getBehaviors();\n    graph.setBehaviors([{ type: 'zoom-canvas' }, { type: 'zoom-canvas', trigger: ['pinch'] }]);\n\n    const pointerdownListener = jest.fn();\n    const pointermoveListener = jest.fn();\n\n    const pointerByTouch = [\n      {\n        client: {\n          x: 100,\n          y: 100,\n        },\n        pointerId: 1,\n        pointerType: 'touch',\n      },\n      {\n        client: {\n          x: 200,\n          y: 200,\n        },\n        pointerId: 2,\n        pointerType: 'touch',\n      },\n    ];\n\n    const dxForInitial = pointerByTouch[0].client.x - pointerByTouch[1].client.x;\n    const dyForInitial = pointerByTouch[0].client.y - pointerByTouch[1].client.y;\n    const initialDistance = Math.sqrt(dxForInitial * dxForInitial + dyForInitial * dyForInitial);\n\n    await expect(graph).toMatchSnapshot(__filename, 'mobile-initial');\n\n    graph.once('canvas:pointerdown', pointerdownListener);\n    canvas.document.emit(CommonEvent.POINTER_DOWN, { client: { x: 100, y: 100 } });\n    expect(pointerdownListener).toHaveBeenCalledTimes(1);\n\n    graph.once('canvas:pointermove', pointermoveListener);\n    canvas.document.emit(CommonEvent.POINTER_MOVE, { client: { x: 200, y: 200 } });\n    expect(pointermoveListener).toHaveBeenCalledTimes(1);\n\n    pointerByTouch[1] = {\n      client: {\n        x: 250,\n        y: 250,\n      },\n      pointerId: 2,\n      pointerType: 'touch',\n    };\n\n    const dxForMove = pointerByTouch[0].client.x - pointerByTouch[1].client.x;\n    const dyForMove = pointerByTouch[0].client.y - pointerByTouch[1].client.y;\n    const currentDistance = Math.sqrt(dxForMove * dxForMove + dyForMove * dyForMove);\n    const ratio = currentDistance / initialDistance;\n    const value = (ratio - 1) * 5;\n\n    await graph.zoomTo(initZoom * value, false, undefined);\n    expect(graph.getZoom()).not.toBe(initZoom);\n\n    await expect(graph).toMatchSnapshot(__filename, 'mobile-final');\n\n    await graph.zoomTo(initZoom, false, undefined);\n    expect(graph.getZoom()).toBe(initZoom);\n\n    graph.setBehaviors(initialBehaviors);\n    expect(graph.getBehaviors()).toEqual([{ type: 'zoom-canvas' }]);\n  });\n\n  const shortcutZoomCanvasOptions: ZoomCanvasOptions = {\n    key: 'shortcut-zoom-canvas',\n    type: 'zoom-canvas',\n    trigger: {\n      zoomIn: ['Control', '='],\n      zoomOut: ['Control', '-'],\n      reset: ['Control', '0'],\n    },\n  };\n\n  it('add second zoom canvas', () => {\n    graph.setBehaviors((behavior) => [...behavior, shortcutZoomCanvasOptions]);\n\n    expect(graph.getBehaviors()).toEqual([{ type: 'zoom-canvas' }, shortcutZoomCanvasOptions]);\n  });\n\n  it('zoom by shortcut', () => {\n    const currentZoom = graph.getZoom();\n\n    // zoom in\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    graph.emit(CommonEvent.KEY_DOWN, { key: '=' });\n\n    expect(graph.getZoom()).toBe(currentZoom * 1.1);\n\n    graph.emit(CommonEvent.KEY_UP, { key: 'Control' });\n    graph.emit(CommonEvent.KEY_UP, { key: '=' });\n\n    // reset\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    graph.emit(CommonEvent.KEY_DOWN, { key: '0' });\n\n    expect(graph.getZoom()).toBe(1);\n\n    graph.emit(CommonEvent.KEY_UP, { key: 'Control' });\n    graph.emit(CommonEvent.KEY_UP, { key: '0' });\n\n    // zoom out\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    graph.emit(CommonEvent.KEY_DOWN, { key: '-' });\n\n    expect(graph.getZoom()).toBe(0.9);\n\n    graph.emit(CommonEvent.KEY_UP, { key: 'Control' });\n    graph.emit(CommonEvent.KEY_UP, { key: '-' });\n  });\n\n  it('disable', () => {\n    graph.setBehaviors((behaviors) =>\n      behaviors.map((behavior) => {\n        if (typeof behavior === 'object') {\n          return { ...behavior, enable: false };\n        }\n        return behavior;\n      }),\n    );\n\n    const currentZoom = graph.getZoom();\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    graph.emit(CommonEvent.KEY_DOWN, { key: '=' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'Control' });\n    graph.emit(CommonEvent.KEY_UP, { key: '=' });\n    expect(graph.getZoom()).toBe(currentZoom);\n  });\n\n  it('remove behavior', () => {\n    graph.setBehaviors((behaviors) => behaviors.filter((_, index) => index === 1));\n    expect(graph.getBehaviors()).toEqual([{ ...shortcutZoomCanvasOptions, enable: false }]);\n  });\n\n  it('condition enable', () => {\n    graph.setBehaviors((behaviors) =>\n      behaviors.map((behavior) => {\n        if (typeof behavior === 'object') {\n          return {\n            ...behavior,\n            enable: (event: any) => event.targetType === 'canvas',\n          };\n        }\n        return behavior;\n      }),\n    );\n\n    const currentZoom = graph.getZoom();\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    graph.emit(CommonEvent.KEY_DOWN, { key: '=', targetType: 'node' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'Control' });\n    graph.emit(CommonEvent.KEY_UP, { key: '=' });\n\n    expect(graph.getZoom()).toBe(currentZoom);\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    graph.emit(CommonEvent.KEY_DOWN, { key: '=', targetType: 'canvas' });\n    graph.emit(CommonEvent.KEY_UP, { key: 'Control' });\n    graph.emit(CommonEvent.KEY_UP, { key: '=' });\n\n    expect(graph.getZoom()).toBe(currentZoom * 1.1);\n  });\n\n  it('preconditionKey', () => {\n    graph.setBehaviors([{ type: 'zoom-canvas', trigger: ['Control'] }]);\n\n    const currentZoom = graph.getZoom();\n\n    graph.emit(CommonEvent.WHEEL, { deltaY: -10 });\n    expect(graph.getZoom()).toBe(currentZoom);\n\n    graph.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    graph.emit(CommonEvent.WHEEL, { deltaY: -10 });\n    expect(graph.getZoom()).toBe(currentZoom * 1.1);\n  });\n\n  it('zoom to canvas center', async () => {\n    const center = graph.getCanvasCenter();\n\n    graph.setBehaviors([{ type: 'zoom-canvas', origin: center }]);\n\n    const currentZoom = graph.getZoom();\n    const targetZoom = currentZoom * 0.5;\n\n    graph.emit(CommonEvent.WHEEL, { deltaY: 50 });\n\n    expect(graph.getZoom()).toBe(targetZoom);\n\n    await expect(graph).toMatchSnapshot(__filename, 'zoom-to-canvas-center');\n  });\n\n  it('canvas event', () => {\n    const canvas = graph.getCanvas();\n\n    const pointermoveListener = jest.fn();\n    const clickListener = jest.fn();\n    const wheelListener = jest.fn();\n    const dblclickListener = jest.fn();\n    const contextmenuListener = jest.fn().mockImplementation((e) => e.preventDefault());\n\n    // pointerenter / pointerleave\n    graph.once('canvas:pointermove', pointermoveListener);\n    canvas.document.emit(CommonEvent.POINTER_MOVE, {});\n    expect(pointermoveListener).toHaveBeenCalledTimes(1);\n\n    // common event\n    graph.once('canvas:click', clickListener);\n    graph.once('canvas:wheel', wheelListener);\n    canvas.document.emit(CommonEvent.CLICK, {});\n    canvas.document.emit(CommonEvent.WHEEL, {});\n    expect(clickListener).toHaveBeenCalledTimes(1);\n    expect(wheelListener).toHaveBeenCalledTimes(1);\n\n    // double click\n    graph.once('canvas:dblclick', dblclickListener);\n    canvas.document.emit(CommonEvent.CLICK, { detail: 2 });\n    expect(dblclickListener).toHaveBeenCalledTimes(1);\n\n    // contextmenu\n    graph.once('canvas:contextmenu', contextmenuListener);\n    canvas.document.emit(CommonEvent.POINTER_DOWN, { button: 2 });\n    expect(contextmenuListener).toHaveBeenCalledTimes(1);\n  });\n\n  it('container event', () => {\n    const container = graph.getCanvas().getContainer();\n\n    const keydownListener = jest.fn();\n    graph.once(ContainerEvent.KEY_DOWN, keydownListener);\n    container?.dispatchEvent(new Event(ContainerEvent.KEY_DOWN));\n    expect(keydownListener).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/default.spec.ts",
    "content": "describe('default', () => {\n  it('expect', () => {\n    expect(1).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/change-type.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { elementChangeType } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element change type', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(elementChangeType, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('change type', async () => {\n    graph.updateNodeData([\n      { id: 'node-1', type: 'circle' },\n      { id: 'node-2', type: 'diamond' },\n    ]);\n\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename, 'change-type');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/combo.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { elementCombo } from '@@/demos';\nimport { createDemoGraph, createGraph } from '@@/utils';\n\ndescribe('combo', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(elementCombo, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('collapse circle combo', async () => {\n    const expandCombo = async () => {\n      await graph.expandElement('combo-2');\n    };\n    const collapseCombo = async () => {\n      graph.updateComboData([\n        {\n          id: 'combo-2',\n          style: {\n            collapsedMarker: false,\n          },\n        },\n      ]);\n      await graph.collapseElement('combo-2');\n    };\n    await collapseCombo();\n    await expect(graph).toMatchSnapshot(__filename, 'circle-collapse-center');\n    await expandCombo();\n  });\n\n  it('collapse rect combo', async () => {\n    const expandCombo = async () => {\n      await graph.expandElement('combo-1');\n    };\n    const collapseCombo = async () => {\n      graph.updateComboData([\n        {\n          id: 'combo-1',\n          type: 'rect',\n          style: {\n            collapsedMarker: false,\n          },\n        },\n      ]);\n      await graph.collapseElement('combo-1');\n    };\n\n    await collapseCombo();\n    await expect(graph).toMatchSnapshot(__filename, 'rect-collapse-center');\n    await expandCombo();\n  });\n\n  it('collapse combo with collapsed marker', async () => {\n    const expandCombo = async () => {\n      graph.updateComboData([\n        {\n          id: 'combo-2',\n          style: {\n            collapsed: false,\n          },\n        },\n      ]);\n      await graph.render();\n    };\n    const collapseCombo = async (type: any | ((children: any) => string)) => {\n      graph.updateComboData([\n        {\n          id: 'combo-2',\n          style: {\n            collapsed: true,\n            collapsedMarker: true,\n            collapsedMarkerType: type,\n          },\n        },\n      ]);\n      graph.render();\n    };\n    await collapseCombo('child-count');\n    await expect(graph).toMatchSnapshot(__filename, 'circle-marker-childCount');\n    await expandCombo();\n    await collapseCombo('descendant-count');\n    await expect(graph).toMatchSnapshot(__filename, 'circle-marker-descendantCount');\n    await expandCombo();\n    await collapseCombo('node-count');\n    await expect(graph).toMatchSnapshot(__filename, 'circle-marker-nodeCount');\n    await expandCombo();\n    await collapseCombo((children: any) => children.length.toString() + 'nodes');\n    await expect(graph).toMatchSnapshot(__filename, 'circle-marker-custom');\n  });\n});\n\ndescribe('combo drag zIndex', () => {\n  it('drag combo will bring related edges forward', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', combo: 'combo-2', style: { x: 120, y: 100 } },\n          { id: 'node-2', combo: 'combo-1', style: { x: 300, y: 200 } },\n          { id: 'node-3', combo: 'combo-1', style: { x: 200, y: 300 } },\n        ],\n        edges: [\n          { id: 'edge-1', source: 'node-1', target: 'node-2' },\n          { id: 'edge-2', source: 'node-2', target: 'node-3' },\n        ],\n        combos: [\n          {\n            id: 'combo-1',\n            type: 'rect',\n            combo: 'combo-2',\n          },\n          {\n            id: 'combo-2',\n          },\n        ],\n      },\n      edge: {\n        style: {\n          stroke: 'red',\n        },\n      },\n      combo: {\n        style: {\n          lineWidth: 1,\n          fillOpacity: 1,\n          stroke: 'black',\n        },\n      },\n    });\n\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename, 'combo-zIndex');\n\n    await graph.frontElement('combo-1');\n\n    await expect(graph).toMatchSnapshot(__filename, 'combo-zIndex');\n\n    await graph.frontElement('combo-2');\n\n    await expect(graph).toMatchSnapshot(__filename, 'combo-zIndex');\n  });\n});\n\ndescribe('combo with position', () => {\n  it('combo with position', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', combo: 'combo-1', style: { x: 50, y: 100 } },\n          { id: 'node-2', combo: 'combo-2' },\n          { id: 'node-3', combo: 'combo-3' },\n        ],\n        combos: [\n          { id: 'combo-1', style: { x: 0, y: 0, collapsed: true } },\n          { id: 'combo-2', style: { x: 100, y: 100, collapsed: true } },\n          { id: 'combo-3', style: { collapsed: true } },\n        ],\n      },\n      node: {\n        style: {\n          labelText: (d) => d.id,\n        },\n      },\n      combo: {\n        style: {\n          labelText: (d) => d.id,\n        },\n      },\n    });\n\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename, 'combo-with-position');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/arrow.spec.ts",
    "content": "import { elementEdgeArrow } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge arrow', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgeArrow);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/cubic-horizontal.spec.ts",
    "content": "import { elementEdgeCubicHorizontal } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge cubic horizontal', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgeCubicHorizontal);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/cubic-radial.spec.ts",
    "content": "import { elementEdgeCubicRadial } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge cubic radial', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgeCubicRadial);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/cubic-vertical.spec.ts",
    "content": "import { elementEdgeCubicVertical } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge cubic vertical', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgeCubicVertical);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/cubic.spec.ts",
    "content": "import { elementEdgeCubic } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge cubic', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgeCubic);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/custom-arrow.spec.ts",
    "content": "import { elementEdgeCustomArrow } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge custom arrow', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgeCustomArrow);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/line.spec.ts",
    "content": "import { elementEdgeLine } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge line', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgeLine);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/loop-curve.spec.ts",
    "content": "import { elementEdgeLoopCurve } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge loop curve', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgeLoopCurve);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/loop-polyline.spec.ts",
    "content": "import { elementEdgeLoopPolyline } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge loop polyline', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgeLoopPolyline);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/polyline-animation.spec.ts",
    "content": "import { type Graph } from '@/src';\nimport { elementEdgePolylineAnimation } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\nconst updateEdgeStyle = (graph: Graph, id: string, attr: string, value: any) => {\n  graph.updateEdgeData((prev) => {\n    const edgeData = prev.find((edge: any) => edge.id === id)!;\n    return [\n      ...prev.filter((edge: any) => edge.id !== id),\n      {\n        ...edgeData,\n        style: {\n          ...edgeData?.style,\n          [attr]: value,\n        },\n      },\n    ];\n  });\n  graph.render();\n};\n\ndescribe('element edge polyline animation', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(elementEdgePolylineAnimation, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('Control Points', async () => {\n    updateEdgeStyle(graph, 'edge-1', 'controlPoints', [[300, 190]]);\n\n    await expect(graph).toMatchSnapshot(__filename, 'controlPoints');\n  });\n\n  it('Radius', async () => {\n    updateEdgeStyle(graph, 'edge-1', 'radius', 20);\n\n    await expect(graph).toMatchSnapshot(__filename, 'radius');\n\n    updateEdgeStyle(graph, 'edge-1', 'radius', 0);\n  });\n\n  it('Router', async () => {\n    updateEdgeStyle(graph, 'edge-1', 'router', { type: 'orth' });\n\n    await expect(graph).toMatchSnapshot(__filename, 'edge-polyline-router-has-controlPoints');\n\n    updateEdgeStyle(graph, 'edge-1', 'controlPoints', []);\n\n    await expect(graph).toMatchSnapshot(__filename, 'edge-polyline-router-no-controlPoints');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/polyline-astar.spec.ts",
    "content": "import { elementEdgePolylineAstar } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge polyline astar', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgePolylineAstar);\n\n    graph.updateEdgeData([\n      {\n        id: 'edge-1',\n        style: {\n          router: {\n            type: 'shortest-path',\n            enableObstacleAvoidance: false,\n            startDirections: ['top', 'right', 'bottom', 'left'],\n            endDirections: ['top', 'right', 'bottom', 'left'],\n          },\n        },\n      },\n    ]);\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.updateEdgeData([\n      {\n        id: 'edge-1',\n        style: {\n          router: {\n            type: 'shortest-path',\n            enableObstacleAvoidance: false,\n            startDirections: ['left'],\n            endDirections: ['left'],\n          },\n        },\n      },\n    ]);\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename, 'left-left');\n\n    graph.updateEdgeData([\n      {\n        id: 'edge-1',\n        style: {\n          router: {\n            type: 'shortest-path',\n            offset: 0,\n            gridSize: 5,\n            enableObstacleAvoidance: true,\n            startDirections: ['top', 'right', 'bottom', 'left'],\n            endDirections: ['top', 'right', 'bottom', 'left'],\n          },\n        },\n      },\n    ]);\n    await graph.render();\n    await expect(graph).toMatchSnapshot(__filename, 'obstacle-move-node-1');\n\n    graph.updateNodeData([\n      {\n        id: 'node-2',\n        style: { x: 120, y: 200 },\n      },\n    ]);\n    await graph.render();\n    await expect(graph).toMatchSnapshot(__filename, 'obstacle-move-node-2');\n\n    graph.updateNodeData([\n      {\n        id: 'node-2',\n        style: { x: 150, y: 200 },\n      },\n    ]);\n    await graph.render();\n    await expect(graph).toMatchSnapshot(__filename, 'obstacle-move-node-3');\n\n    graph.updateNodeData([\n      {\n        id: 'node-2',\n        style: { x: 2000, y: 200 },\n      },\n    ]);\n    await graph.render();\n    await expect(graph).toMatchSnapshot(__filename, 'obstacle-move-node-4');\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/polyline-orth.spec.ts",
    "content": "import { elementEdgePolylineOrth } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge polyline orth', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgePolylineOrth);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.setNode({\n      type: 'rect',\n      style: {\n        size: [60, 30],\n        radius: 8,\n        ports: [{ placement: 'left' }, { placement: 'right' }],\n      },\n    });\n    graph.setLayout((prev) => ({ ...prev, rankdir: 'RL' }));\n    graph.render();\n    await expect(graph).toMatchSnapshot(__filename, 'dagre-RL');\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/polyline.spec.ts",
    "content": "import { elementEdgePolyline } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge polyline', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgePolyline);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/port.spec.ts",
    "content": "import { elementEdgePort } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge port', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgePort);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/quadratic.spec.ts",
    "content": "import { elementEdgeQuadratic } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge quadratic', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgeQuadratic);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/edges/size.spec.ts",
    "content": "import { elementEdgeSize } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element edge line size', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementEdgeSize);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/label-background.spec.ts",
    "content": "import { elementLabelBackground } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element label background', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementLabelBackground);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/label-oversized.spec.ts",
    "content": "import { elementLabelOversized } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element label oversized', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementLabelOversized);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/nodes/avatar.spec.ts",
    "content": "import { elementNodeAvatar } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element label oversized', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementNodeAvatar);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/nodes/circle.spec.ts",
    "content": "import { elementNodeCircle } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element label oversized', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementNodeCircle);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/nodes/diamond.spec.ts",
    "content": "import { elementNodeDiamond } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element label oversized', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementNodeDiamond);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/nodes/donut.spec.ts",
    "content": "import { elementNodeDonut } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element label oversized', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementNodeDonut);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/nodes/ellipse.spec.ts",
    "content": "import { elementNodeEllipse } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element label oversized', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementNodeEllipse);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/nodes/hexagon.spec.ts",
    "content": "import { elementNodeHexagon } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element label oversized', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementNodeHexagon);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/nodes/image.spec.ts",
    "content": "import { elementNodeImage } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element label oversized', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementNodeImage);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/nodes/rect.spec.ts",
    "content": "import { elementNodeRect } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element label oversized', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementNodeRect);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/nodes/star.spec.ts",
    "content": "import { elementNodeStar } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element label oversized', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementNodeStar);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/nodes/triangle.spec.ts",
    "content": "import { elementNodeTriangle } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element label oversized', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(elementNodeTriangle);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/override-methods.spec.ts",
    "content": "import { Circle, register } from '@/src';\nimport { createGraph } from '@@/utils';\n\ndescribe('element override methods', () => {\n  it('override onCreate, onUpdate, onDestroy', async () => {\n    const create = jest.fn();\n    const update = jest.fn();\n    const destroy = jest.fn();\n\n    register(\n      'node',\n      'custom-circle',\n      class CustomCircle extends Circle {\n        onCreate() {\n          create();\n        }\n        onUpdate() {\n          update();\n        }\n        onDestroy() {\n          destroy();\n        }\n      },\n    );\n\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1', type: 'custom-circle' }],\n      },\n    });\n\n    await graph.draw();\n\n    expect(create).toHaveBeenCalledTimes(1);\n\n    graph.translateElementBy('node-1', [10, 10]);\n    expect(update).toHaveBeenCalledTimes(1);\n\n    graph.removeNodeData(['node-1']);\n    await graph.draw();\n    expect(destroy).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/port.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { elementPort } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\nconst updatePort = (graph: Graph, attr: string, value: string | boolean | number) => {\n  graph.updateNodeData((prev) => {\n    const node2Data = prev.find((node: any) => node.id === 'node-2')!;\n    return [\n      ...prev.filter((node: any) => node.id !== 'node-2'),\n      {\n        ...node2Data,\n        style: {\n          ...node2Data!.style,\n          [attr]: value,\n        },\n      },\n    ];\n  });\n};\n\ndescribe('element port', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(elementPort, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename, 'port_hidden');\n  });\n\n  it('hide port', async () => {\n    updatePort(graph, 'portR', 3);\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename, 'port_show');\n  });\n\n  it('endArrow link to port center', async () => {\n    updatePort(graph, 'portR', 3);\n    updatePort(graph, 'portLinkToCenter', true);\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename, 'port_linkToCenter');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/position-combo.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { elementPositionCombo } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element position combo', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(elementPositionCombo, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/position.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { elementPosition } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element position', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(elementPosition, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('translateElementTo', async () => {\n    await graph.translateElementTo({\n      'node-1': [125, 100],\n      'node-2': [125, 100],\n      'node-3': [125, 100],\n    });\n    await expect(graph).toMatchSnapshot(__filename, 'translateElementTo');\n  });\n\n  it('translateElementBy', async () => {\n    await graph.translateElementBy({\n      'node-1': [-50, -50],\n      'node-2': [+50, -50],\n      'node-3': [0, +50],\n    });\n    await expect(graph).toMatchSnapshot(__filename, 'translateElementBy');\n  });\n\n  it('translateElementTo single api', async () => {\n    graph.translateElementTo('node-1', [50, 50]);\n    await expect(graph).toMatchSnapshot(__filename, 'translateElementTo-single');\n  });\n\n  it('translateElementBy single api', async () => {\n    graph.translateElementBy('node-1', [50, 50]);\n    await expect(graph).toMatchSnapshot(__filename, 'translateElementBy-single');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/shape.spec.ts",
    "content": "import type { BaseShapeStyleProps } from '@/src';\nimport { BaseShape } from '@/src';\nimport { Circle } from '@antv/g';\n\ndescribe('element shape', () => {\n  it('upsert hooks', () => {\n    interface ShapeStyleProps extends BaseShapeStyleProps {\n      shape: any;\n    }\n\n    const beforeCreate = jest.fn();\n    const afterCreate = jest.fn();\n    const beforeUpdate = jest.fn();\n    const afterUpdate = jest.fn();\n    const beforeDestroy = jest.fn();\n    const afterDestroy = jest.fn();\n\n    class Shape extends BaseShape<ShapeStyleProps> {\n      render() {\n        this.upsert('circle', Circle, this.attributes.shape, this, {\n          beforeCreate,\n          afterCreate,\n          beforeUpdate,\n          afterUpdate,\n          beforeDestroy,\n          afterDestroy,\n        });\n      }\n    }\n\n    const shape = new Shape({\n      style: {\n        shape: { r: 10 },\n      },\n    });\n\n    expect(beforeCreate).toHaveBeenCalledTimes(1);\n    expect(afterCreate).toHaveBeenCalledTimes(1);\n    expect(beforeUpdate).toHaveBeenCalledTimes(0);\n    expect(afterUpdate).toHaveBeenCalledTimes(0);\n    expect(beforeDestroy).toHaveBeenCalledTimes(0);\n    expect(afterDestroy).toHaveBeenCalledTimes(0);\n\n    shape.update({ shape: { r: 20 } });\n\n    expect(beforeCreate).toHaveBeenCalledTimes(1);\n    expect(afterCreate).toHaveBeenCalledTimes(1);\n    expect(beforeUpdate).toHaveBeenCalledTimes(1);\n    expect(afterUpdate).toHaveBeenCalledTimes(1);\n    expect(beforeDestroy).toHaveBeenCalledTimes(0);\n    expect(afterDestroy).toHaveBeenCalledTimes(0);\n\n    shape.update({ shape: false });\n\n    expect(beforeCreate).toHaveBeenCalledTimes(1);\n    expect(afterCreate).toHaveBeenCalledTimes(1);\n    expect(beforeUpdate).toHaveBeenCalledTimes(1);\n    expect(afterUpdate).toHaveBeenCalledTimes(1);\n    expect(beforeDestroy).toHaveBeenCalledTimes(1);\n    expect(afterDestroy).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/state.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { elementState } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element state', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(elementState, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('set state', async () => {\n    graph.setElementState({\n      'node-1': ['active'],\n      'node-2': ['selected'],\n      'edge-1': [],\n      'edge-2': ['active'],\n    });\n\n    await expect(graph).toMatchSnapshot(__filename, 'setState');\n  });\n\n  it('set state single api', async () => {\n    graph.setElementState('node-1', ['selected']);\n    await expect(graph).toMatchSnapshot(__filename, 'setState-single');\n\n    graph.setElementState('node-1', []);\n    await expect(graph).toMatchSnapshot(__filename, 'setState-single-default');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/visibility.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { elementVisibility } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element visibility', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(elementVisibility, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('hide', async () => {\n    await graph.hideElement(['node-1', 'node-2', 'node-3', 'edge-1', 'edge-2', 'edge-3']);\n\n    await expect(graph).toMatchSnapshot(__filename, 'hide');\n  });\n\n  it('show', async () => {\n    await graph.showElement(['node-1', 'node-2', 'edge-1']);\n\n    await expect(graph).toMatchSnapshot(__filename, 'show');\n  });\n\n  it('show and hide', async () => {\n    await graph.setElementVisibility({\n      'node-1': 'hidden',\n      'node-3': 'visible',\n      'edge-1': 'hidden',\n      'edge-2': 'visible',\n    });\n\n    await expect(graph).toMatchSnapshot(__filename, 'show-and-hide');\n  });\n\n  it('show and hide single api', async () => {\n    graph.setElementVisibility('node-1', 'visible');\n    await expect(graph).toMatchSnapshot(__filename, 'show-single');\n\n    graph.setElementVisibility('node-1', 'hidden');\n    await expect(graph).toMatchSnapshot(__filename, 'hide-single');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/elements/z-index.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { ComboEvent, NodeEvent } from '@/src';\nimport { elementZIndex } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('element zIndex', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(elementZIndex, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n\n  it('drag overlap', async () => {\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-1' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 140, dy: 0 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-overlap-node-1');\n    graph.emit(NodeEvent.DRAG_END);\n\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-2' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: -65, dy: 100 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-overlap-node-2');\n    graph.emit(NodeEvent.DRAG_END);\n\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node-3' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 75, dy: -100 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-overlap-node-3');\n    graph.emit(NodeEvent.DRAG_END);\n  });\n\n  it('combo overlap', async () => {\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'combo-1' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: 20, dy: 0 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-1');\n    graph.emit(ComboEvent.DRAG_END);\n\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'combo-2' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: 20, dy: 0 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-2');\n    graph.emit(ComboEvent.DRAG_END);\n\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'combo-3' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: 20, dy: 0 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-combo-3');\n    graph.emit(ComboEvent.DRAG_END);\n\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'combo-3' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: 20, dy: 0 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-overlap-combo-3');\n    graph.emit(ComboEvent.DRAG_END);\n\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'combo-4' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: -20, dy: 0 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-overlap-combo-4(1)');\n    graph.emit(ComboEvent.DRAG_END);\n\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'combo-4' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: -40, dy: 0 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-overlap-combo-4(2)');\n    graph.emit(ComboEvent.DRAG_END);\n\n    graph.emit(ComboEvent.DRAG_START, { target: { id: 'combo-4' }, targetType: 'combo' });\n    graph.emit(ComboEvent.DRAG, { dx: -40, dy: 0 });\n    await expect(graph).toMatchSnapshot(__filename, 'drag-overlap-combo-4(3)');\n    graph.emit(ComboEvent.DRAG_END);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/import.spec.ts",
    "content": "import * as G6 from '@/src';\n\ndescribe('import', () => {\n  it('default', () => {\n    expect(G6).not.toBe(undefined);\n    const entries = Object.entries(G6);\n    expect(entries.length).toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/circular.spec.ts",
    "content": "import {\n  layoutCircularBasic,\n  layoutCircularConfigurationTranslate,\n  layoutCircularDegree,\n  layoutCircularDivision,\n  layoutCircularSpiral,\n} from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('layout circular', () => {\n  it('layoutCircularBasic', async () => {\n    const graph = await createDemoGraph(layoutCircularBasic);\n    await expect(graph).toMatchSnapshot(__filename, 'basic');\n    graph.destroy();\n  });\n\n  it('layoutCircularConfigurationTranslate', async () => {\n    const graph = await createDemoGraph(layoutCircularConfigurationTranslate);\n    await expect(graph).toMatchSnapshot(__filename, 'configuration-translate');\n\n    graph.setLayout({\n      type: 'circular',\n      radius: 200,\n      startAngle: Math.PI / 4,\n      endAngle: Math.PI,\n      divisions: 5,\n      ordering: 'degree',\n    });\n    await graph.layout();\n\n    await expect(graph).toMatchSnapshot(__filename, 'configuration-translate-division');\n    graph.destroy();\n  });\n\n  it('layoutCircularDegree', async () => {\n    const graph = await createDemoGraph(layoutCircularDegree);\n    await expect(graph).toMatchSnapshot(__filename, 'degree');\n    graph.destroy();\n  });\n\n  it('layoutCircularDivision', async () => {\n    const graph = await createDemoGraph(layoutCircularDivision);\n    await expect(graph).toMatchSnapshot(__filename, 'division');\n    graph.destroy();\n  });\n\n  it('layoutCircularSpiral', async () => {\n    const graph = await createDemoGraph(layoutCircularSpiral);\n    await expect(graph).toMatchSnapshot(__filename, 'spiral');\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/combo-layout.spec.ts",
    "content": "import { layoutComboCombined } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\nimport { clear as clearMockRandom, mock as mockRandom } from 'jest-random-mock';\n\ndescribe('combo layout', () => {\n  beforeEach(() => {\n    mockRandom();\n  });\n\n  afterEach(() => {\n    clearMockRandom();\n  });\n\n  it('combined', async () => {\n    const graph = await createDemoGraph(layoutComboCombined);\n    await expect(graph).toMatchSnapshot(__filename, 'combined');\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/compact-box.spec.ts",
    "content": "import { layoutCompactBoxBasic, layoutCompactBoxLeftAlign, layoutCompactBoxTopToBottom } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('compact box', () => {\n  it('basic', async () => {\n    const graph = await createDemoGraph(layoutCompactBoxBasic);\n    await expect(graph).toMatchSnapshot(__filename, 'basic');\n    graph.destroy();\n  });\n\n  it('left align', async () => {\n    const graph = await createDemoGraph(layoutCompactBoxLeftAlign);\n    await expect(graph).toMatchSnapshot(__filename, 'left-align');\n    graph.destroy();\n  });\n\n  it('top to bottom', async () => {\n    const graph = await createDemoGraph(layoutCompactBoxTopToBottom);\n    await expect(graph).toMatchSnapshot(__filename, 'top-to-bottom');\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/concentric.spec.ts",
    "content": "import { layoutConcentric } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('layout concentric', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(layoutConcentric);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/custom-dagre.spec.ts",
    "content": "import { layoutCustomDagre } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('custom dagre', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(layoutCustomDagre);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/custom-layout-horizontal.spec.ts",
    "content": "import { layoutCustomHorizontal } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('custom layout horizontal', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(layoutCustomHorizontal);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/d3-force-collision.spec.ts",
    "content": "import { layoutForceCollision } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\nimport { clear as clearMockRandom, mock as mockRandom } from 'jest-random-mock';\n\ndescribe('layout d3 force collision', () => {\n  beforeAll(() => {\n    mockRandom();\n  });\n\n  afterAll(() => {\n    clearMockRandom();\n  });\n\n  it('render', async () => {\n    const graph = await createDemoGraph(layoutForceCollision);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/d3-force-lattice.spec.ts",
    "content": "import { layoutForceLattice } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('layout d3 force lattice', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(layoutForceLattice);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    // drag\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/d3-force.spec.ts",
    "content": "import { layoutD3Force } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('layout d3 force', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(layoutD3Force);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/dagre.spec.ts",
    "content": "import { layoutAntVDagreFlow, layoutAntVDagreFlowCombo, layoutDagre } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('antv dagre flow', () => {\n  it('flow', async () => {\n    const graph = await createDemoGraph(layoutAntVDagreFlow);\n    await expect(graph).toMatchSnapshot(__filename, 'antv-flow');\n    graph.destroy();\n  });\n\n  it('antv dagre flow combo', async () => {\n    const graph = await createDemoGraph(layoutAntVDagreFlowCombo);\n    await expect(graph).toMatchSnapshot(__filename, 'antv-flow-combo');\n    graph.destroy();\n  });\n\n  it('dagre.js', async () => {\n    const graph = await createDemoGraph(layoutDagre);\n    await expect(graph).toMatchSnapshot(__filename, 'dagre');\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/dendrogram.spec.ts",
    "content": "import { layoutDendrogramBasic, layoutDendrogramTb } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('dendrogram', () => {\n  it('basic', async () => {\n    const graph = await createDemoGraph(layoutDendrogramBasic);\n    await expect(graph).toMatchSnapshot(__filename, 'basic');\n    graph.destroy();\n  });\n\n  it('tb', async () => {\n    const graph = await createDemoGraph(layoutDendrogramTb);\n    await expect(graph).toMatchSnapshot(__filename, 'tb');\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/fishbone.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { layoutFishbone } from '@@/demos/layout-fishbone';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('fishbone', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(layoutFishbone);\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default', async () => {\n    await expect(graph).toMatchSnapshot(__filename, 'direction-RL');\n  });\n\n  it('direction RL', async () => {\n    graph.setLayout((prev) => ({ ...prev, type: 'fishbone', direction: 'LR' }));\n    await graph.render();\n    await expect(graph).toMatchSnapshot(__filename, 'direction-LR');\n  });\n\n  it('vGap and hGap', async () => {\n    graph.setLayout((prev) => ({ ...prev, type: 'fishbone', vGap: 32, hGap: 32 }));\n    await graph.render();\n    await expect(graph).toMatchSnapshot(__filename, 'vGap-32-hGap-32');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/fruchterman.spec.ts",
    "content": "import { layoutFruchtermanBasic, layoutFruchtermanCluster } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\nimport { clear as clearMockRandom, mock as mockRandom } from 'jest-random-mock';\n\ndescribe('layout fruchterman', () => {\n  beforeEach(() => {\n    mockRandom();\n  });\n\n  afterEach(() => {\n    clearMockRandom();\n  });\n\n  it('basic', async () => {\n    const graph = await createDemoGraph(layoutFruchtermanBasic);\n    await expect(graph).toMatchSnapshot(__filename, 'basic');\n\n    graph.destroy();\n  });\n\n  it('cluster', async () => {\n    const graph = await createDemoGraph(layoutFruchtermanCluster);\n    await expect(graph).toMatchSnapshot(__filename, 'cluster');\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/grid.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { layoutGrid } from '@@/demos/layout-grid';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('grid', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(layoutGrid);\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('sortBy default', async () => {\n    await expect(graph).toMatchSnapshot(__filename, 'sortby-default');\n  });\n\n  it('sortBy id', async () => {\n    graph.setLayout({ type: 'grid', sortBy: 'id' });\n    await graph.layout();\n    await expect(graph).toMatchSnapshot(__filename, 'sortby-id');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/indented.spec.ts",
    "content": "import { layoutIndented } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('layout d3 force', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(layoutIndented);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/mds.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { layoutMDS } from '@@/demos/layout-mds';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('mds', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(layoutMDS);\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('mds linkDistance = 100', async () => {\n    await expect(graph).toMatchSnapshot(__filename, 'ld100');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/mindmap.spec.ts",
    "content": "import { layoutMindmapH, layoutMindmapHCustomSide, layoutMindmapHLeft, layoutMindmapHRight } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('mindmap', () => {\n  it('h custom side', async () => {\n    const graph = await createDemoGraph(layoutMindmapHCustomSide);\n    await expect(graph).toMatchSnapshot(__filename, 'h-custom-side');\n    graph.destroy();\n  });\n\n  it('h left', async () => {\n    const graph = await createDemoGraph(layoutMindmapHLeft);\n    await expect(graph).toMatchSnapshot(__filename, 'h-left');\n    graph.destroy();\n  });\n\n  it('h', async () => {\n    const graph = await createDemoGraph(layoutMindmapH);\n    await expect(graph).toMatchSnapshot(__filename, 'h');\n    graph.destroy();\n  });\n\n  it('h right', async () => {\n    const graph = await createDemoGraph(layoutMindmapHRight);\n    await expect(graph).toMatchSnapshot(__filename, 'h-right');\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/pipeline.spec.ts",
    "content": "import { GraphEvent } from '@/src';\nimport { layoutPipelineMdsForce } from '@@/demos';\nimport { createDemoGraph, createGraph } from '@@/utils';\n\ndescribe('pipeline', () => {\n  it('event', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: new Array(10).fill(null).map((_, i) => ({ id: `${i}` })),\n      },\n      layout: [\n        {\n          type: 'force',\n        },\n        {\n          type: 'd3-force',\n        },\n        {\n          type: 'grid',\n        },\n      ],\n    });\n\n    const before = jest.fn();\n    const after = jest.fn();\n\n    graph.on(GraphEvent.BEFORE_STAGE_LAYOUT, (e) => {\n      before(e);\n    });\n\n    graph.on(GraphEvent.AFTER_STAGE_LAYOUT, (e) => {\n      after(e);\n    });\n\n    await graph.render();\n\n    expect(before).toHaveBeenCalledTimes(3);\n    expect(after).toHaveBeenCalledTimes(3);\n\n    expect(before.mock.calls[0][0].data.options.type).toBe('force');\n    expect(before.mock.calls[1][0].data.options.type).toBe('d3-force');\n    expect(before.mock.calls[2][0].data.options.type).toBe('grid');\n\n    expect(after.mock.calls[0][0].data.options.type).toBe('force');\n    expect(after.mock.calls[1][0].data.options.type).toBe('d3-force');\n    expect(after.mock.calls[2][0].data.options.type).toBe('grid');\n  });\n\n  it('layout-pipeline-mds-force', async () => {\n    const graph = await createDemoGraph(layoutPipelineMdsForce);\n    await expect(graph).toMatchSnapshot(__filename, 'layout-pipeline-mds-force');\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/radial-layout.spec.ts",
    "content": "import {\n  layoutRadialBasic,\n  layoutRadialConfigurationTranslate,\n  layoutRadialPreventOverlap,\n  layoutRadialPreventOverlapUnstrict,\n  layoutRadialSort,\n} from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('radial layout', () => {\n  it('basic', async () => {\n    const graph = await createDemoGraph(layoutRadialBasic);\n    await expect(graph).toMatchSnapshot(__filename, 'basic');\n    graph.destroy();\n  });\n\n  it('configuration translate', async () => {\n    const graph = await createDemoGraph(layoutRadialConfigurationTranslate);\n    await expect(graph).toMatchSnapshot(__filename, 'configuration-translate');\n    graph.destroy();\n  });\n\n  it('prevent overlap', async () => {\n    const graph = await createDemoGraph(layoutRadialPreventOverlap);\n    await expect(graph).toMatchSnapshot(__filename, 'prevent-overlap');\n    graph.destroy();\n  });\n\n  it('prevent overlap unstrict', async () => {\n    const graph = await createDemoGraph(layoutRadialPreventOverlapUnstrict);\n    await expect(graph).toMatchSnapshot(__filename, 'prevent-overlap-unstrict');\n    graph.destroy();\n  });\n\n  it('sort', async () => {\n    const graph = await createDemoGraph(layoutRadialSort);\n    await expect(graph).toMatchSnapshot(__filename, 'sort');\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/layouts/snake.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { layoutSnake } from '@@/demos/layout-snake';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('snake', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(layoutSnake);\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default', async () => {\n    await expect(graph).toMatchSnapshot(__filename, 'default');\n  });\n\n  it('padding', async () => {\n    graph.setLayout((prev) => ({ ...prev, padding: 20 }));\n    await graph.layout();\n    await expect(graph).toMatchSnapshot(__filename, 'padding-20');\n  });\n\n  it('set cols as 1', async () => {\n    graph.setLayout((prev) => ({ ...prev, padding: 0, cols: 1 }));\n    await graph.layout();\n    await expect(graph).toMatchSnapshot(__filename, 'cols-1');\n  });\n\n  it('set cols as 20', async () => {\n    graph.setLayout((prev) => ({ ...prev, padding: 0, cols: 20 }));\n    await graph.layout();\n    await expect(graph).toMatchSnapshot(__filename, 'cols-20');\n  });\n\n  it('colSep and rowSep', async () => {\n    graph.setLayout((prev) => ({ ...prev, cols: 6, colGap: 50, rowGap: 50 }));\n    await graph.layout();\n    await expect(graph).toMatchSnapshot(__filename, 'gap-50');\n  });\n\n  it('anti-clockwise', async () => {\n    graph.setLayout((prev) => ({ ...prev, clockwise: false }));\n    await graph.layout();\n    await expect(graph).toMatchSnapshot(__filename, 'anti-clockwise');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/background.spec.ts",
    "content": "import { pluginBackground } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('plugin background', () => {\n  it('background', async () => {\n    const graph = await createDemoGraph(pluginBackground);\n    const container = graph.getCanvas().getContainer()!;\n\n    const el = container.querySelector('.g6-background') as HTMLDivElement;\n\n    expect(graph.getPlugins()).toEqual([\n      {\n        type: 'background',\n        key: 'background',\n        backgroundImage:\n          'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*0Qq0ToQm1rEAAAAAAAAAAAAADmJ7AQ/original)',\n      },\n    ]);\n    expect(el.style.backgroundImage).toContain(\n      'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*0Qq0ToQm1rEAAAAAAAAAAAAADmJ7AQ/original)',\n    );\n\n    await graph.destroy();\n    expect(container.querySelector('.g6-background')).toBeFalsy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/bubble-sets.spec.ts",
    "content": "import type { BubbleSets, Graph, ID } from '@/src';\nimport { NodeEvent } from '@/src';\nimport { pluginBubbleSets } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('plugin bubble-sets', () => {\n  let graph: Graph;\n  let bubbleSets: BubbleSets;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(pluginBubbleSets, { animation: false });\n    bubbleSets = graph.getPluginInstance<BubbleSets>('bubble-sets');\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  const reset = () => {\n    const members: ID[] = ['node0', 'node1', 'node8', 'node9', 'node11'];\n    const avoidMembers: ID[] = [];\n    bubbleSets.updateMember(members);\n    bubbleSets.updateAvoidMember(avoidMembers);\n  };\n\n  it('default', async () => {\n    reset();\n    await expect(graph).toMatchSnapshot(__filename, 'default');\n  });\n\n  it('add/remove/update member', async () => {\n    bubbleSets.addMember(['node12', 'edge1']);\n    await expect(graph).toMatchSnapshot(__filename, 'member-add');\n    bubbleSets.removeMember(['node12', 'edge1']);\n    await expect(graph).toMatchSnapshot(__filename, 'member-remove');\n    bubbleSets.updateMember(['node3', 'node4', 'node5', 'node6', 'node7']);\n    await expect(graph).toMatchSnapshot(__filename, 'member-update');\n    expect(bubbleSets.getMember()).toEqual(['node3', 'node4', 'node5', 'node6', 'node7']);\n    reset();\n  });\n\n  it('add/remove/update non-member', async () => {\n    bubbleSets.addAvoidMember('node13');\n    await expect(graph).toMatchSnapshot(__filename, 'non-member-add');\n    bubbleSets.removeAvoidMember('node13');\n    await expect(graph).toMatchSnapshot(__filename, 'non-member-remove');\n    bubbleSets.updateAvoidMember(['node13', 'node2']);\n    await expect(graph).toMatchSnapshot(__filename, 'non-member-update');\n    expect(bubbleSets.getAvoidMember()).toEqual(['node13', 'node2']);\n    reset();\n  });\n\n  it('update options', async () => {\n    graph.updatePlugin({ key: 'bubble-sets', fill: 'pink' });\n    graph.render();\n    await expect(graph).toMatchSnapshot(__filename, 'options-update');\n    reset();\n  });\n\n  it('update element', async () => {\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node11' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 50, dy: 50 });\n    graph.emit(NodeEvent.DRAG_END);\n\n    await expect(graph).toMatchSnapshot(__filename, 'element-position-update');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/camera-setting.spec.ts",
    "content": "import { pluginCameraSetting } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\nimport { CameraProjectionMode } from '@antv/g';\n\ndescribe('plugin camera-setting', () => {\n  it('camera-setting orthographic', async () => {\n    const graph = await createDemoGraph(pluginCameraSetting);\n    const camera = graph.getCanvas().getCamera();\n    expect(camera.getProjectionMode()).toBe(CameraProjectionMode.ORTHOGRAPHIC);\n    expect([...camera.getPosition()]).toBeCloseTo([250, 250, 500]);\n    // 视点位置 / Focal point\n    expect([...camera.getFocalPoint()]).toBeCloseTo([250, 250, 0]);\n    expect(camera.getDistance()).toBe(500);\n    expect(camera.getNear()).toBe(0.1);\n    expect(camera.getFar()).toBe(1000);\n    expect(camera.getZoom()).toBe(1);\n    graph.destroy();\n  });\n\n  it('camera-setting perspective', async () => {\n    const graph = await createDemoGraph(pluginCameraSetting);\n    graph.updatePlugin({\n      key: 'camera-setting',\n      type: 'camera-setting',\n      projectionMode: 'perspective',\n      near: 0.01,\n      far: 50000,\n      fov: 75,\n      aspect: 'auto',\n      distance: 1000,\n    });\n\n    const camera = graph.getCanvas().getCamera();\n    expect(camera.getProjectionMode()).toBe(CameraProjectionMode.PERSPECTIVE);\n    expect([...camera.getPosition()]).toBeCloseTo([250, 250, 1000]);\n    // 视点位置 / Focal point\n    expect([...camera.getFocalPoint()]).toBeCloseTo([250, 250, 0]);\n    expect(camera.getDistance()).toBe(1000);\n    expect(camera.getNear()).toBe(0.01);\n    expect(camera.getFar()).toBe(50000);\n    expect(camera.getZoom()).toBe(1);\n    graph.destroy();\n  });\n\n  it.only('camera-setting perspective orbiting azimuth', async () => {\n    const graph = await createDemoGraph(pluginCameraSetting);\n    graph.updatePlugin({\n      key: 'camera-setting',\n      type: 'camera-setting',\n      cameraType: 'orbiting',\n      projectionMode: 'perspective',\n      roll: 0,\n      elevation: 45,\n      azimuth: 0,\n    });\n\n    const camera = graph.getCanvas().getCamera();\n    expect(camera.getProjectionMode()).toBe(CameraProjectionMode.PERSPECTIVE);\n\n    // 相机以视点为中心，沿 y 轴顺时针旋转 45 度\n    // The camera rotates 45 degrees clockwise along the positive y-axis with the focal point as the center\n    const delta = 500 / Math.sqrt(2);\n    expect([...camera.getPosition()]).toBeCloseTo([250, 250 + delta, delta]);\n    // 视点位置 / Focal point\n    expect([...camera.getFocalPoint()]).toBeCloseTo([250, 250, 0]);\n\n    graph.updatePlugin({\n      key: 'camera-setting',\n      type: 'camera-setting',\n      roll: 0,\n      elevation: 0,\n      azimuth: 45,\n    });\n    // 相机以视点为中心，沿 z 轴逆时针旋转 45 度\n    // The camera rotates 45 degrees counterclockwise along the positive z-axis with the focal point as the center\n    expect([...camera.getPosition()]).toBeCloseTo([250 - delta, 250, delta]);\n\n    graph.updatePlugin({\n      key: 'camera-setting',\n      type: 'camera-setting',\n      roll: 180,\n      elevation: 45,\n      azimuth: 0,\n    });\n    expect([...camera.getPosition()]).toBeCloseTo([250, 250 + delta, delta]);\n\n    expect(camera.getDistance()).toBeCloseTo(500);\n    expect(camera.getNear()).toBe(0.1);\n    expect(camera.getFar()).toBe(1000);\n    expect(camera.getZoom()).toBe(1);\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/contextmenu.spec.ts",
    "content": "import type { Contextmenu } from '@/src';\nimport { NodeEvent } from '@/src';\nimport { pluginContextmenu } from '@@/demos';\nimport { createDemoGraph, sleep } from '@@/utils';\n\ndescribe('plugin contextmenu', () => {\n  it('contextmenu', async () => {\n    const graph = await createDemoGraph(pluginContextmenu);\n    const onClick = jest.fn();\n    graph.updatePlugin({ key: 'contextmenu', onClick });\n\n    const container = graph.getCanvas().getContainer()!;\n\n    const el = container.querySelector('.g6-contextmenu') as HTMLDivElement;\n\n    const $dom = container.querySelector('.g6-contextmenu');\n    expect($dom).toBeTruthy();\n    expect($dom?.classList.contains('custom-class-name')).toBeTruthy();\n\n    expect(el.querySelector('.g6-contextmenu-li')).toBeFalsy();\n\n    const emit = () => {\n      graph.emit(NodeEvent.CONTEXT_MENU, {\n        target: { id: '1' },\n        targetType: 'node',\n        client: {\n          x: 100,\n          y: 100,\n        },\n      });\n    };\n\n    emit();\n\n    await sleep(100);\n    expect(el.querySelector('.g6-contextmenu-ul')).toBeTruthy();\n    expect(el.querySelectorAll('.g6-contextmenu-li').length).toBe(2);\n\n    const instance = graph.getPluginInstance<Contextmenu>('contextmenu');\n\n    // @ts-expect-error private method\n    instance.onMenuItemClick({ target: el.querySelector('.g6-contextmenu-li') });\n    expect(onClick).toHaveBeenCalledTimes(1);\n    expect(container.querySelector<HTMLDivElement>('.g6-contextmenu')!.style.display).toBe('none');\n\n    emit();\n\n    await sleep(100);\n    expect(container.querySelector<HTMLDivElement>('.g6-contextmenu')!.style.display).toBe('block');\n    document.body.click();\n    expect(container.querySelector<HTMLDivElement>('.g6-contextmenu')!.style.display).toBe('none');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/edge-bundling.spec.ts",
    "content": "import { createDemoGraph } from '@@/utils';\nimport type { Graph } from '@antv/g6';\nimport { pluginEdgeBundling } from '../../demos';\n\ndescribe('plugin edge bundling', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(pluginEdgeBundling, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default edge bundling', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/edge-filter-lens.spec.ts",
    "content": "import { pluginEdgeFilterLens } from '@@/demos';\nimport { CommonEvent, Graph } from '@antv/g6';\nimport { createDemoGraph, dispatchCanvasEvent } from '../../utils';\n\ndescribe('edge-filter-lens', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(pluginEdgeFilterLens, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('move lens by pointermove', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    dispatchCanvasEvent(graph, CommonEvent.POINTER_MOVE, { canvas: { x: 200, y: 100 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'move-lens-pointermove');\n  });\n\n  it('move lens by click', async () => {\n    graph.updatePlugin({ key: 'edge-filter-lens', trigger: 'click' });\n\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 180, y: 100 } });\n    await expect(graph).toMatchSnapshot(__filename, 'move-lens-click-1');\n\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 200, y: 100 } });\n    await expect(graph).toMatchSnapshot(__filename, 'move-lens-click-2');\n  });\n\n  it('move lens by drag', async () => {\n    graph.updatePlugin({ key: 'edge-filter-lens', trigger: 'drag' });\n\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 180, y: 100 } });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { canvas: { x: 200, y: 100 } });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { canvas: { x: 220, y: 100 } });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    await expect(graph).toMatchSnapshot(__filename, 'move-lens-drag');\n  });\n\n  it('scale lens by wheel', async () => {\n    function emitWheelEvent(options?: { deltaX: number; deltaY: number; clientX: number; clientY: number }) {\n      const dom = graph.getCanvas().getContextService().getDomElement();\n      dom?.dispatchEvent(new WheelEvent(CommonEvent.WHEEL, options));\n    }\n\n    emitWheelEvent({ deltaX: 1, deltaY: 2, clientX: 200, clientY: 100 });\n    emitWheelEvent({ deltaX: 1, deltaY: 2, clientX: 200, clientY: 100 });\n\n    await expect(graph).toMatchSnapshot(__filename, 'scale-larger');\n\n    emitWheelEvent({ deltaX: -1, deltaY: -2, clientX: 200, clientY: 100 });\n    emitWheelEvent({ deltaX: -1, deltaY: -2, clientX: 200, clientY: 100 });\n    emitWheelEvent({ deltaX: -1, deltaY: -2, clientX: 200, clientY: 100 });\n    emitWheelEvent({ deltaX: -1, deltaY: -2, clientX: 200, clientY: 100 });\n\n    await expect(graph).toMatchSnapshot(__filename, 'scale-smaller');\n  });\n\n  it('show edge when only its source/target node in lens', async () => {\n    graph.updatePlugin({ key: 'edge-filter-lens', trigger: 'click', nodeType: 'source' });\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 200, y: 200 } });\n    await expect(graph).toMatchSnapshot(__filename, 'node-type-source');\n\n    graph.updatePlugin({ key: 'edge-filter-lens', trigger: 'click', nodeType: 'target' });\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 200, y: 200 } });\n    await expect(graph).toMatchSnapshot(__filename, 'node-type-target');\n\n    graph.updatePlugin({ key: 'edge-filter-lens', trigger: 'click', nodeType: 'either' });\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 200, y: 200 } });\n    await expect(graph).toMatchSnapshot(__filename, 'node-type-either');\n  });\n\n  it('lens style', async () => {\n    graph.updatePlugin({ key: 'edge-filter-lens', edgeStyle: () => ({ stroke: '#f00' }) });\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 200, y: 200 } });\n    await expect(graph).toMatchSnapshot(__filename, 'lens-style');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/fisheye.spec.ts",
    "content": "import { pluginFisheye } from '@@/demos';\nimport { CommonEvent, Graph } from '@antv/g6';\nimport { createDemoGraph, dispatchCanvasEvent } from '../../utils';\nimport { emitWheelEvent } from '../../utils/dom';\n\ndescribe('plugin-fisheye', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(pluginFisheye, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('move lens by pointermove', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    dispatchCanvasEvent(graph, CommonEvent.POINTER_MOVE, { canvas: { x: 420, y: 150 } });\n\n    await expect(graph).toMatchSnapshot(__filename, 'move-lens-pointermove');\n  });\n\n  it('move lens by drag', async () => {\n    graph.updatePlugin({ key: 'fisheye', trigger: 'drag' });\n\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 420, y: 150 } });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { canvas: { x: 420, y: 150 } });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG, { canvas: { x: 400, y: 180 } });\n    dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n\n    await expect(graph).toMatchSnapshot(__filename, 'move-lens-drag');\n  });\n\n  it('move lens by click', async () => {\n    graph.updatePlugin({ key: 'fisheye', trigger: 'click' });\n\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 180, y: 100 } });\n    await expect(graph).toMatchSnapshot(__filename, 'move-lens-click-1');\n\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 200, y: 100 } });\n    await expect(graph).toMatchSnapshot(__filename, 'move-lens-click-2');\n  });\n\n  it('scale lens R/D by wheel', async () => {\n    graph.updatePlugin({ key: 'fisheye', scaleRBy: 'wheel', scaleDBy: 'unset' });\n\n    const emitWheelUpEvent = (count: number) => {\n      for (let i = 0; i < count; i++) {\n        emitWheelEvent(graph, { deltaX: 1, deltaY: 2, clientX: 420, clientY: 150 });\n      }\n    };\n    const emitWheelDownEvent = (count: number) => {\n      for (let i = 0; i < count; i++) {\n        emitWheelEvent(graph, { deltaX: -1, deltaY: -2, clientX: 420, clientY: 150 });\n      }\n    };\n\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 420, y: 150 } });\n\n    emitWheelUpEvent(5);\n    await expect(graph).toMatchSnapshot(__filename, 'scale-R-wheel-larger');\n    emitWheelDownEvent(10);\n    await expect(graph).toMatchSnapshot(__filename, 'scale-R-wheel-smaller');\n    emitWheelUpEvent(5);\n\n    graph.updatePlugin({ key: 'fisheye', scaleRBy: 'unset', scaleDBy: 'wheel' });\n\n    emitWheelUpEvent(5);\n    await expect(graph).toMatchSnapshot(__filename, 'scale-D-wheel-larger');\n    emitWheelDownEvent(10);\n    await expect(graph).toMatchSnapshot(__filename, 'scale-D-wheel-smaller');\n    emitWheelUpEvent(5);\n  });\n\n  it('scale lens R/D by drag', async () => {\n    graph.updatePlugin({ key: 'fisheye', scaleRBy: 'drag', scaleDBy: 'unset' });\n\n    const emitPositionDragEvent = (count: number) => {\n      dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { canvas: { x: 420, y: 150 } });\n      for (let i = 0; i < count; i++) {\n        dispatchCanvasEvent(graph, CommonEvent.DRAG, { dx: 1, dy: -2 });\n      }\n      dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n    };\n\n    const emitNegativeDragEvent = (count: number) => {\n      dispatchCanvasEvent(graph, CommonEvent.DRAG_START, { canvas: { x: 420, y: 150 } });\n      for (let i = 0; i < count; i++) {\n        dispatchCanvasEvent(graph, CommonEvent.DRAG, { dx: -1, dy: 2 });\n      }\n      dispatchCanvasEvent(graph, CommonEvent.DRAG_END);\n    };\n\n    emitPositionDragEvent(5);\n    await expect(graph).toMatchSnapshot(__filename, 'scale-R-drag-larger');\n    emitNegativeDragEvent(10);\n    await expect(graph).toMatchSnapshot(__filename, 'scale-R-drag-smaller');\n    emitPositionDragEvent(5);\n\n    graph.updatePlugin({ key: 'fisheye', scaleRBy: 'unset', scaleDBy: 'drag' });\n\n    emitPositionDragEvent(5);\n    await expect(graph).toMatchSnapshot(__filename, 'scale-D-drag-larger');\n    emitNegativeDragEvent(10);\n    await expect(graph).toMatchSnapshot(__filename, 'scale-D-drag-smaller');\n    emitPositionDragEvent(5);\n  });\n\n  it('show D percent', async () => {\n    graph.updatePlugin({ key: 'fisheye', showDPercent: false });\n\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 420, y: 150 } });\n    await expect(graph).toMatchSnapshot(__filename, 'hide-D-percent');\n  });\n\n  it('lens style', async () => {\n    graph.updatePlugin({\n      key: 'fisheye',\n      showDPercent: true,\n      style: { fill: '#f00', lineDash: [5, 5], stroke: '#666' },\n    });\n\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 420, y: 150 } });\n    await expect(graph).toMatchSnapshot(__filename, 'lens-style');\n  });\n\n  it('node style in lens', async () => {\n    graph.updatePlugin({ key: 'fisheye', style: { lineDash: 0 }, nodeStyle: { halo: true } });\n\n    dispatchCanvasEvent(graph, CommonEvent.CLICK, { canvas: { x: 420, y: 150 } });\n    await expect(graph).toMatchSnapshot(__filename, 'node-style');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/grid-line.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { pluginGridLine } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('plugin grid line', () => {\n  let graph: Graph;\n  let gridLineElement: HTMLCollectionOf<HTMLElement>;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(pluginGridLine, { animation: false });\n    gridLineElement = graph\n      .getCanvas()\n      .getContainer()!\n      .getElementsByClassName('g6-grid-line')! as HTMLCollectionOf<HTMLElement>;\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default status', () => {\n    expect(graph.getPlugins()).toEqual([{ type: 'grid-line', follow: false }]);\n    expect(gridLineElement.length).toBe(1);\n    expect(gridLineElement[0].style.backgroundSize).toBe('20px 20px');\n  });\n\n  it('update grid line', () => {\n    graph.setPlugins((plugins) =>\n      plugins.map((plugin) => {\n        if (typeof plugin === 'object') {\n          return {\n            ...plugin,\n            follow: true,\n            size: 30,\n          };\n        }\n        return plugin;\n      }),\n    );\n\n    expect(graph.getPlugins()).toEqual([{ type: 'grid-line', follow: true, size: 30 }]);\n    expect(gridLineElement[0].style.backgroundSize).toBe('30px 30px');\n  });\n\n  it('drag', () => {\n    graph.emit('aftertransform', { data: { translate: [10, 10] } });\n    expect(gridLineElement[0].style.backgroundPosition).toBe('10px 10px');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/history/plugin-history.spec.ts",
    "content": "import type { History } from '@/src';\nimport { ComboEvent, Graph, NodeEvent } from '@/src';\nimport { pluginHistory } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('history plugin', () => {\n  let graph: Graph;\n  let history: History;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(pluginHistory, { animation: false });\n    history = graph.getPluginInstance<History>('history');\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('addData', async () => {\n    graph.addData({\n      nodes: [{ id: 'node-5', style: { x: 200, y: 100, fill: 'pink' } }],\n      edges: [{ source: 'node-1', target: 'node-5', style: { stroke: 'brown' } }],\n    });\n    graph.draw();\n    await expect(graph).toMatchSnapshot(__filename, 'addData');\n    history.undo();\n    await expect(graph).toMatchSnapshot(__filename, 'addData-undo');\n    history.redo();\n    await expect(graph).toMatchSnapshot(__filename, 'addData-redo');\n    history.undo();\n  });\n\n  it('updateData', async () => {\n    graph.updateData({\n      nodes: [{ id: 'node-1', style: { x: 150, y: 100, fill: 'red' } }],\n      edges: [{ id: 'edge-1', style: { stroke: 'green' } }],\n    });\n    graph.draw();\n    await expect(graph).toMatchSnapshot(__filename, 'updateData');\n    history.undo();\n    await expect(graph).toMatchSnapshot(__filename, 'updateData-undo');\n    history.redo();\n    await expect(graph).toMatchSnapshot(__filename, 'updateData-redo');\n    history.undo();\n  });\n\n  it('removeData', async () => {\n    graph.removeData({\n      nodes: ['node-1'],\n      edges: ['edge-1'],\n    });\n    graph.draw();\n    await expect(graph).toMatchSnapshot(__filename, 'deleteData');\n    history.undo();\n    await expect(graph).toMatchSnapshot(__filename, 'deleteData-undo');\n    history.redo();\n    await expect(graph).toMatchSnapshot(__filename, 'deleteData-redo');\n    history.undo();\n  });\n\n  it('collapse/expand', async () => {\n    graph.collapseElement('combo-2');\n    await expect(graph).toMatchSnapshot(__filename, 'collapse');\n    history.undo();\n    await expect(graph).toMatchSnapshot(__filename, 'collapse-undo');\n    history.redo();\n    await expect(graph).toMatchSnapshot(__filename, 'collapse-redo');\n    history.undo();\n\n    graph.expandElement('combo-1');\n    await expect(graph).toMatchSnapshot(__filename, 'expand');\n    history.undo();\n    await expect(graph).toMatchSnapshot(__filename, 'expand-undo');\n    history.redo();\n    await expect(graph).toMatchSnapshot(__filename, 'expand-redo');\n    history.undo();\n  });\n\n  it('setElementState', async () => {\n    graph.setElementState('node-1', 'selected', true);\n    await expect(graph).toMatchSnapshot(__filename, 'setElementsState');\n    history.undo();\n    await expect(graph).toMatchSnapshot(__filename, 'setElementsState-undo');\n    history.redo();\n    await expect(graph).toMatchSnapshot(__filename, 'setElementsState-redo');\n    history.undo();\n  });\n\n  it('setElementVisibility', async () => {\n    graph.setElementVisibility('node-1', 'hidden');\n    await expect(graph).toMatchSnapshot(__filename, 'hideElement');\n    history.undo();\n    await expect(graph).toMatchSnapshot(__filename, 'hideElement-undo');\n    history.redo();\n    await expect(graph).toMatchSnapshot(__filename, 'hideElement-redo');\n    history.undo();\n  });\n\n  it('setElementZIndex', async () => {\n    graph.setElementZIndex('combo-2', 100);\n    graph.setElementZIndex('node-1', 101);\n    await expect(graph).toMatchSnapshot(__filename, 'setElementZIndex');\n    history.undo();\n    await expect(graph).toMatchSnapshot(__filename, 'setElementZIndex-undo');\n    history.redo();\n    await expect(graph).toMatchSnapshot(__filename, 'setElementZIndex-redo');\n    history.undo();\n  });\n\n  it('create-edge', async () => {\n    graph.setBehaviors((prev) => [\n      ...prev,\n      { type: 'create-edge', trigger: 'click', style: { stroke: 'red', lineWidth: 2 } },\n    ]);\n    graph.emit(NodeEvent.CLICK, { target: { id: 'node-1' }, targetType: 'node' });\n    graph.emit(ComboEvent.CLICK, { target: { id: 'combo-2' }, targetType: 'combo' });\n    await expect(graph).toMatchSnapshot(__filename, 'create-edge');\n    history.undo();\n    await expect(graph).toMatchSnapshot(__filename, 'create-edge-undo');\n    history.redo();\n    await expect(graph).toMatchSnapshot(__filename, 'create-edge-redo');\n    history.undo();\n  });\n\n  it('beforeAddCommand', async () => {\n    const undoStackLen = history.undoStack.length;\n\n    graph.updatePlugin({ key: 'history', beforeAddCommand: () => false });\n    graph.setElementVisibility('node-1', 'hidden');\n    await graph.draw();\n    expect(history.undoStack.length).toEqual(undoStackLen);\n\n    graph.updatePlugin({ key: 'history', beforeAddCommand: () => true });\n    graph.setElementVisibility('node-1', 'visible');\n    await graph.draw();\n    expect(history.undoStack.length).toEqual(undoStackLen + 1);\n  });\n\n  it('canUndo/canRedo/clear', async () => {\n    expect(history.canUndo()).toBeTruthy();\n    expect(history.canRedo()).toBeTruthy();\n    history.clear();\n    expect(history.undoStack.length).toEqual(0);\n    expect(history.canUndo()).toBeFalsy();\n    expect(history.canRedo()).toBeFalsy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/history/utils.spec.ts",
    "content": "import { alignFields, parseCommand } from '@/src/plugins/history/util';\nimport type { DataChange } from '@/src/types';\n\ndescribe('history utils', () => {\n  it('alignFields', () => {\n    const data = {\n      id: 'combo-2',\n      data: {},\n      style: {\n        zIndex: 0,\n        collapsed: true,\n        visibility: 'hidden',\n        states: ['active'],\n      },\n    };\n    const data2 = {\n      id: 'combo-2',\n      data: {},\n      style: {\n        zIndex: 0,\n      },\n    };\n    alignFields(data, data2);\n    expect(data2).toEqual({\n      id: 'combo-2',\n      data: {},\n      style: {\n        zIndex: 0,\n        collapsed: false,\n        visibility: 'visible',\n        states: [],\n      },\n    });\n  });\n\n  it('parseCommand', () => {\n    const command = [\n      {\n        value: {\n          id: 'combo-2',\n          data: {},\n          style: {\n            x: 188.11,\n            y: 193.5,\n            zIndex: 0,\n            collapsed: true,\n          },\n        },\n        original: {\n          id: 'combo-2',\n          data: {},\n          style: {\n            x: 188.11,\n            y: 193.5,\n            zIndex: 0,\n            collapsed: false,\n          },\n        },\n        type: 'ComboUpdated',\n      },\n    ] as DataChange[];\n    expect(parseCommand(command).current.update.combos![0]).toEqual({\n      data: {},\n      id: 'combo-2',\n      style: { collapsed: true, x: 188.11, y: 193.5, zIndex: 0 },\n    });\n    expect(parseCommand(command).original.update.combos![0]).toEqual({\n      data: {},\n      id: 'combo-2',\n      style: { collapsed: false, x: 188.11, y: 193.5, zIndex: 0 },\n    });\n    const command2 = [\n      {\n        type: 'NodeAdded',\n        value: {\n          id: 'node-1',\n          data: {},\n          style: {\n            x: 100,\n            y: 100,\n          },\n        },\n      },\n    ] as DataChange[];\n    expect(parseCommand(command2).current.add.nodes![0]).toEqual({\n      data: {},\n      id: 'node-1',\n      style: { x: 100, y: 100 },\n    });\n    expect(parseCommand(command2).original.remove.nodes![0]).toEqual({\n      data: {},\n      id: 'node-1',\n      style: { x: 100, y: 100 },\n    });\n    const command3 = [\n      {\n        type: 'EdgeRemoved',\n        value: {\n          id: 'edge-1',\n          data: {},\n          source: 'node-1',\n          target: 'node-2',\n        },\n      },\n    ] as DataChange[];\n    expect(parseCommand(command3).current.remove.edges![0]).toEqual({\n      data: {},\n      id: 'edge-1',\n      source: 'node-1',\n      target: 'node-2',\n    });\n    expect(parseCommand(command3).original.add.edges![0]).toEqual({\n      data: {},\n      id: 'edge-1',\n      source: 'node-1',\n      target: 'node-2',\n    });\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/hull/plugin-hull.spec.ts",
    "content": "import type { Graph, Hull } from '@/src';\nimport { NodeEvent } from '@/src';\nimport type { HullOptions } from '@/src/plugins';\nimport { pluginHull } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('plugin hull', () => {\n  let graph: Graph;\n  let hull: Hull;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(pluginHull, { animation: false });\n    hull = graph.getPluginInstance<Hull>('hull');\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('init', async () => {\n    await expect(graph).toMatchSnapshot(__filename, 'default');\n  });\n\n  const updateHullOptions = (optionsToUpdate: Partial<HullOptions>) => {\n    graph.updatePlugin({ key: 'hull', ...optionsToUpdate });\n    graph.render();\n  };\n\n  it('update corner', async () => {\n    updateHullOptions({ corner: 'sharp' });\n    await expect(graph).toMatchSnapshot(__filename, 'corner__sharp');\n    updateHullOptions({ corner: 'smooth' });\n    await expect(graph).toMatchSnapshot(__filename, 'corner__smooth');\n    updateHullOptions({ corner: 'rounded' });\n    await expect(graph).toMatchSnapshot(__filename, 'corner__rounded');\n  });\n\n  it('update padding', async () => {\n    updateHullOptions({ padding: 20 });\n    await expect(graph).toMatchSnapshot(__filename, 'padding__20');\n    updateHullOptions({ padding: 0 });\n    await expect(graph).toMatchSnapshot(__filename, 'padding__0');\n  });\n\n  it('update labelPlacement', async () => {\n    updateHullOptions({ labelPlacement: 'top' });\n    await expect(graph).toMatchSnapshot(__filename, 'labelPlacement__top');\n    updateHullOptions({ labelPlacement: 'left' });\n    await expect(graph).toMatchSnapshot(__filename, 'labelPlacement__left');\n    updateHullOptions({ labelPlacement: 'right' });\n    await expect(graph).toMatchSnapshot(__filename, 'labelPlacement__right');\n    updateHullOptions({ labelPlacement: 'bottom' });\n    await expect(graph).toMatchSnapshot(__filename, 'labelPlacement__bottom');\n  });\n\n  it('update labelCloseToPath', async () => {\n    updateHullOptions({ labelCloseToPath: false });\n    await expect(graph).toMatchSnapshot(__filename, 'labelCloseToHull__false');\n    updateHullOptions({ labelCloseToPath: true });\n    await expect(graph).toMatchSnapshot(__filename, 'labelCloseToHull__true');\n  });\n\n  it('update labelAutoRotate', async () => {\n    updateHullOptions({ labelAutoRotate: false });\n    await expect(graph).toMatchSnapshot(__filename, 'labelAutoRotate__false');\n    updateHullOptions({ labelAutoRotate: true });\n    await expect(graph).toMatchSnapshot(__filename, 'labelAutoRotate__true');\n  });\n\n  it('addMember', async () => {\n    hull.addMember('node3');\n    await expect(graph).toMatchSnapshot(__filename, 'addMember__node3');\n  });\n\n  it('removeMember', async () => {\n    hull.removeMember('node1');\n    await expect(graph).toMatchSnapshot(__filename, 'removeMember__node1');\n  });\n\n  it('updateMember', async () => {\n    hull.updateMember(['node5', 'node6']);\n    await expect(graph).toMatchSnapshot(__filename, 'updateMember');\n    expect(hull.getMember()).toEqual(['node5', 'node6']);\n  });\n\n  it('update element position', async () => {\n    graph.emit(NodeEvent.DRAG_START, { target: { id: 'node5' }, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { dx: 50, dy: -50 });\n    graph.emit(NodeEvent.DRAG_END);\n    await expect(graph).toMatchSnapshot(__filename, 'updateMember__position');\n  });\n\n  it('empty members', async () => {\n    hull.updateMember([]);\n    await expect(graph).toMatchSnapshot(__filename, 'emptyMembers');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/hull/util.spec.ts",
    "content": "import { computeHullPath } from '@/src/plugins/hull/util';\n\ndescribe('util', () => {\n  it('computeHullPath', () => {\n    expect(computeHullPath([[10, 10]], 0, 'rounded')).toEqual([\n      ['M', 10, 10],\n      ['A', 0, 0, 0, 0, 0, 10, 10],\n      ['A', 0, 0, 0, 0, 0, 10, 10],\n    ]);\n    expect(computeHullPath([[10, 10]], 0, 'sharp')).toEqual([\n      ['M', 10, 10],\n      ['L', 10, 10],\n      ['L', 10, 10],\n      ['L', 10, 10],\n      ['Z'],\n    ]);\n    expect(computeHullPath([[10, 10]], 0, 'smooth')).toEqual([\n      ['M', 10, 10],\n      ['A', 0, 0, 0, 0, 0, 10, 10],\n      ['A', 0, 0, 0, 0, 0, 10, 10],\n    ]);\n    expect(\n      computeHullPath(\n        [\n          [10, 10],\n          [20, 20],\n        ],\n        0,\n        'rounded',\n      ),\n    ).toEqual([\n      ['M', 10, 10],\n      ['L', 20, 20],\n      ['A', 0, 0, 0, 0, 0, 20, 20],\n      ['L', 10, 10],\n      ['A', 0, 0, 0, 0, 0, 10, 10],\n    ]);\n  });\n  expect(\n    computeHullPath(\n      [\n        [10, 10],\n        [20, 20],\n      ],\n      0,\n      'sharp',\n    ),\n  ).toEqual([['M', 10, 10], ['L', 20, 20], ['L', 20, 20], ['L', 10, 10], ['Z']]);\n  expect(\n    computeHullPath(\n      [\n        [10, 10],\n        [20, 20],\n      ],\n      0,\n      'smooth',\n    ),\n  ).toEqual([\n    ['M', 10, 10],\n    ['L', 20, 20],\n    ['A', 0, 0, 0, 0, 0, 20, 20],\n    ['L', 10, 10],\n    ['A', 0, 0, 0, 0, 0, 10, 10],\n  ]);\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/legend.spec.ts",
    "content": "import { Legend } from '@/src/plugins/legend';\nimport { pluginLegend } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\nconst mockEvent: any = {\n  __data__: {\n    id: 'node__0',\n    index: 0,\n    style: {\n      layout: 'flex',\n      labelText: 'a',\n      markerLineWidth: 3,\n      marker: 'diamond',\n      markerStroke: '#000000',\n      markerFill: 'red',\n      spacing: 4,\n      markerSize: 16,\n      labelFontSize: 16,\n      markerOpacity: 1,\n      labelOpacity: 1,\n    },\n  },\n};\n\ndescribe('plugin legend', () => {\n  it('normal', async () => {\n    const graph = await createDemoGraph(pluginLegend);\n    await expect(graph).toMatchSnapshot(__filename, 'normal');\n    graph.destroy();\n  });\n\n  it('click', async () => {\n    const graph = await createDemoGraph(pluginLegend);\n\n    const legend = graph.getPluginInstance<Legend>('legend');\n\n    legend.click(mockEvent);\n    await expect(graph).toMatchSnapshot(__filename, 'click');\n    legend.click(mockEvent);\n    await expect(graph).toMatchSnapshot(__filename, 'click-again');\n    graph.destroy();\n  });\n\n  it('update trigger to hover', async () => {\n    const graph = await createDemoGraph(pluginLegend);\n    graph.setPlugins((plugins) =>\n      plugins.map((plugin) => {\n        if (typeof plugin === 'object') {\n          return {\n            ...plugin,\n            trigger: 'hover',\n            position: 'top',\n          };\n        }\n        return plugin;\n      }),\n    );\n\n    const legend = graph.getPluginInstance<Legend>('legend');\n\n    legend.mouseenter(mockEvent);\n    await expect(graph).toMatchSnapshot(__filename, 'mouseenter');\n    legend.mouseleave(mockEvent);\n    await expect(graph).toMatchSnapshot(__filename, 'mouseleave');\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/snapline.spec.ts",
    "content": "import type { Graph, Node } from '@/src';\nimport { NodeEvent } from '@/src';\nimport { pluginSnapline } from '@@/demos';\nimport { createDemoGraph } from '../../utils';\n\ndescribe('plugin snapline', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(pluginSnapline);\n  });\n\n  it('snapline', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    // @ts-expect-error access private property\n    const node = graph.context.element?.getElement('node3');\n\n    let i = 0;\n\n    const moveNodeAndCreateSnapshot = async (x: number, y: number, prefix: string, reset = false) => {\n      graph.updateNodeData([{ id: 'node3', style: { x, y } }]);\n      graph.render();\n      graph.emit(NodeEvent.DRAG_START, { target: node, targetType: 'node' });\n      graph.emit(NodeEvent.DRAG, { target: node, dx: 0, dy: 0 });\n      if (reset) i = 0;\n      await expect(graph).toMatchSnapshot(__filename, `drag-node3-${prefix}-${i}`);\n      graph.emit(NodeEvent.DRAG_END, { target: node });\n      i++;\n    };\n\n    await moveNodeAndCreateSnapshot(50, 300, 'vertical');\n    await moveNodeAndCreateSnapshot(90, 300, 'vertical');\n    await moveNodeAndCreateSnapshot(100, 300, 'vertical');\n    await moveNodeAndCreateSnapshot(110, 300, 'vertical');\n    await moveNodeAndCreateSnapshot(150, 300, 'vertical');\n    await moveNodeAndCreateSnapshot(200, 300, 'vertical');\n    await moveNodeAndCreateSnapshot(250, 300, 'vertical');\n    await moveNodeAndCreateSnapshot(290, 300, 'vertical');\n    await moveNodeAndCreateSnapshot(300, 300, 'vertical');\n    await moveNodeAndCreateSnapshot(310, 300, 'vertical');\n    await moveNodeAndCreateSnapshot(350, 300, 'vertical');\n    await moveNodeAndCreateSnapshot(400, 300, 'vertical');\n\n    await moveNodeAndCreateSnapshot(200, 65, 'horizontal', true);\n    await moveNodeAndCreateSnapshot(200, 95, 'horizontal');\n    await moveNodeAndCreateSnapshot(200, 100, 'horizontal');\n    await moveNodeAndCreateSnapshot(200, 105, 'horizontal');\n    await moveNodeAndCreateSnapshot(200, 135, 'horizontal');\n    await moveNodeAndCreateSnapshot(200, 150, 'horizontal');\n    await moveNodeAndCreateSnapshot(200, 265, 'horizontal');\n    await moveNodeAndCreateSnapshot(200, 295, 'horizontal');\n    await moveNodeAndCreateSnapshot(200, 300, 'horizontal');\n    await moveNodeAndCreateSnapshot(200, 305, 'horizontal');\n    await moveNodeAndCreateSnapshot(200, 335, 'horizontal');\n\n    graph.updatePlugin({ key: 'snapline', offset: Infinity });\n    graph.updateNodeData([{ id: 'node3', style: { x: 100, y: 300 } }]);\n    graph.render();\n    graph.emit(NodeEvent.DRAG_START, { target: node, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { target: node, dx: 0, dy: 0 });\n    await expect(graph).toMatchSnapshot(__filename, `offset-infinity`);\n    graph.emit(NodeEvent.DRAG_END, { target: node });\n\n    graph.updatePlugin({ key: 'snapline', filter: (node: Node) => node.id !== 'node2' });\n    graph.render();\n    graph.emit(NodeEvent.DRAG_START, { target: node, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { target: node, dx: 0, dy: 0 });\n    await expect(graph).toMatchSnapshot(__filename, `filter-node2`);\n    graph.emit(NodeEvent.DRAG_END, { target: node });\n\n    graph.updatePlugin({ key: 'snapline', filter: () => true, autoSnap: true });\n    graph.updateNodeData([{ id: 'node3', style: { x: 96, y: 304 } }]);\n    graph.render();\n    graph.emit(NodeEvent.DRAG_START, { target: node, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { target: node, dx: 0, dy: 0 });\n    await expect(graph).toMatchSnapshot(__filename, `auto-snap`);\n    graph.emit(NodeEvent.DRAG_END, { target: node });\n\n    // zoom to test lineWidth\n    graph.zoomTo(5, false, [300, 300]);\n    graph.updatePlugin({ key: 'snapline', autoSnap: false });\n    graph.updateNodeData([{ id: 'node3', style: { x: 260, y: 300 } }]);\n    graph.render();\n    graph.emit(NodeEvent.DRAG_START, { target: node, targetType: 'node' });\n    graph.emit(NodeEvent.DRAG, { target: node, dx: 0, dy: 0 });\n    await expect(graph).toMatchSnapshot(__filename, 'zoom-5');\n    graph.emit(NodeEvent.DRAG_END, { target: node });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/timebar.spec.ts",
    "content": "import type { Timebar } from '@/src';\nimport { pluginTimebar } from '@@/demos';\nimport { createDemoGraph, sleep } from '@@/utils';\n\ndescribe('plugin timebar', () => {\n  it('time type, modify', async () => {\n    const graph = await createDemoGraph(pluginTimebar);\n\n    await expect(graph).toMatchSnapshot(__filename);\n    const timebar = graph.getPluginInstance<Timebar>('timebar');\n\n    timebar.play();\n    await sleep(1000);\n    await expect(graph).toMatchSnapshot(__filename, 'play-1-time-modify');\n\n    await sleep(1000);\n    await expect(graph).toMatchSnapshot(__filename, 'play-2-time-modify');\n\n    timebar.pause();\n\n    timebar.backward();\n    await expect(graph).toMatchSnapshot(__filename, 'backward-1-time-modify');\n\n    timebar.forward();\n    await expect(graph).toMatchSnapshot(__filename, 'forward-1-time-modify');\n\n    timebar.forward();\n    await expect(graph).toMatchSnapshot(__filename, 'forward-2-time-modify');\n\n    timebar.reset();\n    await expect(graph).toMatchSnapshot(__filename, 'reset-modify');\n\n    graph.destroy();\n  });\n\n  it('time type, visibility', async () => {\n    const graph = await createDemoGraph(pluginTimebar);\n    const timebar = graph.getPluginInstance<Timebar>('timebar');\n\n    timebar.update({\n      mode: 'visibility',\n    });\n\n    timebar.forward();\n    await expect(graph).toMatchSnapshot(__filename, 'forward-1-time-visibility');\n\n    timebar.forward();\n    timebar.forward();\n    timebar.backward();\n    await expect(graph).toMatchSnapshot(__filename, 'backward-1-time-visibility');\n\n    timebar.reset();\n    await expect(graph).toMatchSnapshot(__filename, 'reset-visibility');\n\n    graph.destroy();\n  });\n\n  it('chart type', async () => {\n    // In current, cannot capture the timebar snapshot\n  });\n\n  it('event callback', async () => {\n    const onChange = jest.fn();\n    const onRest = jest.fn();\n    const onPlay = jest.fn();\n    const onPause = jest.fn();\n    const onBackward = jest.fn();\n    const onForward = jest.fn();\n\n    const graph = await createDemoGraph(pluginTimebar);\n    const timebar = graph.getPluginInstance<Timebar>('timebar');\n\n    timebar.update({\n      onChange,\n      onRest,\n      onPlay,\n      onPause,\n      onBackward,\n      onForward,\n    });\n\n    // api calls do not trigger event callbacks\n    // timebar.play();\n    // expect(onPlay).toHaveBeenCalledTimes(1);\n    // timebar.pause();\n    // expect(onPause).toHaveBeenCalledTimes(1);\n    // timebar.forward();\n    // expect(onForward).toHaveBeenCalledTimes(1);\n    // timebar.backward();\n    // expect(onBackward).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/title.spec.ts",
    "content": "import type { Graph, Label, Title } from '@/src';\nimport { pluginTitle } from '@@/demos';\nimport { createDemoGraph } from '../../utils';\n\ndescribe('plugin title', () => {\n  let graph: Graph;\n  let titlePlugin: Title;\n\n  const executeTest = () => {\n    expect(typeof titlePlugin).toBe('object');\n\n    const [width] = graph.getSize();\n\n    // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n    // @ts-ignore\n    const [titleLabel, subtitleLabel] = titlePlugin.canvas.getRoot().childNodes;\n\n    const titleAttr = (titleLabel as Label).attributes;\n    const titleX = titleAttr.x;\n    expect(titleX).toBe(width / 2);\n\n    const subtitleAttr = (subtitleLabel as Label).attributes;\n    const subtitleX = subtitleAttr.x;\n    expect(subtitleX).toBe(width / 2);\n\n    expect(titleAttr.text).toBe('这是一个标题这是一个标题');\n    expect(subtitleAttr.text).toBe('这是一个副标');\n\n    expect(titleAttr.fontSize).toBe(28);\n    expect(subtitleAttr.fill).toBe('#2989FF');\n  };\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(pluginTitle);\n    titlePlugin = graph.getPluginInstance('title');\n  });\n\n  it('title', executeTest);\n\n  afterAll(() => {\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/toolbar/plugin-toolbar.spec.ts",
    "content": "import { pluginToolbarBuildIn } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\nimport { get } from '@antv/util';\n\ndescribe('plugin toolbar', () => {\n  it('toolbar', async () => {\n    const graph = await createDemoGraph(pluginToolbarBuildIn);\n    const container = graph.getCanvas().getContainer()!;\n\n    const el = container.querySelector('.g6-toolbar') as HTMLDivElement;\n\n    expect(graph.getPlugins().length).toBe(1);\n    expect(get(graph.getPlugins(), [0, 'position'])).toBe('top-left');\n\n    expect(el.querySelectorAll('.g6-toolbar-item').length).toBe(9);\n\n    graph.destroy();\n    expect(container.querySelector('.g6-toolbar-item')).toBeFalsy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/toolbar/util.spec.ts",
    "content": "import { parsePositionToStyle } from '@/src/plugins/toolbar/util';\n\ndescribe('util', () => {\n  it('parsePositionToStyle', () => {\n    expect(parsePositionToStyle('top-left')).toEqual({\n      top: '8px',\n      right: 'unset',\n      bottom: 'unset',\n      left: '8px',\n      flexDirection: 'row',\n    });\n    expect(parsePositionToStyle('top-right')).toEqual({\n      top: '8px',\n      right: '8px',\n      bottom: 'unset',\n      left: 'unset',\n      flexDirection: 'row',\n    });\n    expect(parsePositionToStyle('bottom-left')).toEqual({\n      top: 'unset',\n      right: 'unset',\n      bottom: '8px',\n      left: '8px',\n      flexDirection: 'row',\n    });\n    expect(parsePositionToStyle('bottom-right')).toEqual({\n      top: 'unset',\n      right: '8px',\n      bottom: '8px',\n      left: 'unset',\n      flexDirection: 'row',\n    });\n\n    expect(parsePositionToStyle('left-top')).toEqual({\n      top: '8px',\n      right: 'unset',\n      bottom: 'unset',\n      left: '8px',\n      flexDirection: 'column',\n    });\n    expect(parsePositionToStyle('right-top')).toEqual({\n      top: '8px',\n      right: '8px',\n      bottom: 'unset',\n      left: 'unset',\n      flexDirection: 'column',\n    });\n    expect(parsePositionToStyle('left-bottom')).toEqual({\n      top: 'unset',\n      right: 'unset',\n      bottom: '8px',\n      left: '8px',\n      flexDirection: 'column',\n    });\n    expect(parsePositionToStyle('right-bottom')).toEqual({\n      top: 'unset',\n      right: '8px',\n      bottom: '8px',\n      left: 'unset',\n      flexDirection: 'column',\n    });\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/tooltip.spec.ts",
    "content": "import type { Tooltip } from '@/src';\nimport { pluginTooltipAsync, pluginTooltipEnable } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('plugin tooltip', () => {\n  it('enable', async () => {\n    const graph = await createDemoGraph(pluginTooltipEnable);\n    const container = graph.getCanvas().getContainer()!;\n    const el = container.querySelector('.tooltip') as HTMLDivElement;\n    const plugin = graph.getPluginInstance<Tooltip>('tooltip');\n\n    await plugin.showById('node3');\n    expect(el.style.visibility).toBe('hidden');\n\n    await plugin.showById('node1');\n    expect(el.style.visibility).toBe('visible');\n\n    graph.destroy();\n  });\n\n  it('get content null', async () => {\n    const graph = await createDemoGraph(pluginTooltipEnable);\n    const container = graph.getCanvas().getContainer()!;\n    const el = container.querySelector('.tooltip') as HTMLDivElement;\n    const plugin = graph.getPluginInstance<Tooltip>('tooltip');\n\n    await plugin.showById('node2');\n    expect(el.style.visibility).toBe('hidden');\n\n    graph.destroy();\n  });\n\n  it('get content async', async () => {\n    const graph = await createDemoGraph(pluginTooltipAsync);\n    const container = graph.getCanvas().getContainer()!;\n    const el = container.querySelector('.tooltip') as HTMLDivElement;\n    const plugin = graph.getPluginInstance<Tooltip>('tooltip');\n\n    await plugin.showById('node1');\n    expect(el.style.visibility).toBe('visible');\n    expect(el.innerHTML).toBe('get content async test');\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/utils/dom.spec.ts",
    "content": "import { createPluginContainer, insertDOM } from '@/src/plugins/utils/dom';\n\ndescribe('plugin dom utils', () => {\n  it('createPluginContainer', () => {\n    const el = createPluginContainer('test');\n    expect(el.getAttribute('class')).toBe('g6-test');\n    expect(el.style.position).toBe('unset');\n    expect(el.style.display).toBe('block');\n    expect(el.style.inset).toBe('0px');\n    expect(el.style.height).toBe('100%');\n    expect(el.style.width).toBe('100%');\n    expect(el.style.overflow).toBe('hidden');\n    expect(el.style.pointerEvents).toBe('none');\n  });\n\n  it('createPluginContainer cover=false', () => {\n    const el = createPluginContainer('test', false);\n    expect(el.getAttribute('class')).toBe('g6-test');\n    expect(el.style.position).toBe('absolute');\n    expect(el.style.display).toBe('block');\n    expect(el.style.height).not.toBe('100%');\n    expect(el.style.width).not.toBe('100%');\n    expect(el.style.overflow).not.toBe('hidden');\n    expect(el.style.pointerEvents).not.toBe('none');\n  });\n\n  it('createPluginContainer with style', () => {\n    const el = createPluginContainer('test', false, { color: 'red' });\n    expect(el.getAttribute('class')).toBe('g6-test');\n    expect(el.style.color).toBe('red');\n  });\n\n  it('insertDOM', () => {\n    insertDOM('g6-test', 'div', { color: 'red' }, 'test', document.body);\n\n    let el = document.getElementById('g6-test')!;\n    expect(el).toBeTruthy();\n    expect(el.style.color).toBe('red');\n    expect(el.innerHTML).toBe('test');\n\n    insertDOM('g6-test', 'div', { color: 'red' }, 'new html', document.body);\n\n    el = document.getElementById('g6-test')!;\n    expect(el.innerHTML).toBe('new html');\n\n    el = insertDOM('g6-test');\n    expect(el.tagName.toLowerCase()).toBe('div');\n    expect(el.innerHTML).toBe('');\n    expect(el.parentNode).toBe(document.body);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/plugins/watermark.spec.ts",
    "content": "import { pluginWatermark, pluginWatermarkImage } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('plugin watermark', () => {\n  it('watermark text', async () => {\n    const graph = await createDemoGraph(pluginWatermark);\n    const container = graph.getCanvas().getContainer()!;\n\n    const el = container.querySelector('.g6-watermark') as HTMLDivElement;\n\n    expect(graph.getPlugins()).toEqual([{ type: 'watermark', text: 'hello, \\na watermark.', textFontSize: 12 }]);\n    expect(el.style.backgroundImage).toContain('data:image/png;base64');\n\n    await graph.destroy();\n    expect(container.querySelector('.g6-watermark')).toBeFalsy();\n  });\n\n  it('watermark image', async () => {\n    const graph = await createDemoGraph(pluginWatermarkImage);\n    const container = graph.getCanvas().getContainer()!;\n\n    expect(graph.getPlugins()).toEqual([\n      {\n        type: 'watermark',\n        width: 100,\n        height: 100,\n        imageURL: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*7svFR6wkPMoAAAAAAAAAAAAADmJ7AQ/original',\n      },\n    ]);\n    await graph.destroy();\n    expect(container.querySelector('.g6-watermark')).toBeFalsy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/registry.spec.ts",
    "content": "import {\n  Circle,\n  CircleCombo,\n  Cubic,\n  CubicHorizontal,\n  CubicRadial,\n  CubicVertical,\n  Diamond,\n  Donut,\n  Ellipse,\n  ExtensionCategory,\n  HTML,\n  Hexagon,\n  Image,\n  Line,\n  Polyline,\n  Quadratic,\n  Rect,\n  RectCombo,\n  Star,\n  Triangle,\n  getExtension,\n  getExtensions,\n  register,\n} from '@/src';\nimport { dark, light } from '@/src/themes';\nimport { Circle as GCircle } from '@antv/g';\nimport { pick } from '@antv/util';\n\ndescribe('registry', () => {\n  it('registerBuiltInPlugins', () => {\n    expect(getExtensions(ExtensionCategory.NODE)).toEqual({\n      circle: Circle,\n      ellipse: Ellipse,\n      image: Image,\n      rect: Rect,\n      star: Star,\n      triangle: Triangle,\n      diamond: Diamond,\n      donut: Donut,\n      hexagon: Hexagon,\n      html: HTML,\n    });\n    expect(getExtensions(ExtensionCategory.EDGE)).toEqual({\n      cubic: Cubic,\n      line: Line,\n      polyline: Polyline,\n      quadratic: Quadratic,\n      'cubic-horizontal': CubicHorizontal,\n      'cubic-vertical': CubicVertical,\n      'cubic-radial': CubicRadial,\n    });\n    expect(getExtensions(ExtensionCategory.COMBO)).toEqual({\n      circle: CircleCombo,\n      rect: RectCombo,\n    });\n    expect(getExtensions(ExtensionCategory.THEME)).toEqual({\n      dark,\n      light,\n    });\n  });\n\n  it('register, getPlugin, getPlugins', () => {\n    class CircleNode {}\n    class RectNode {}\n    class Edge {}\n    register(ExtensionCategory.NODE, 'circle-node', CircleNode as any);\n    register(ExtensionCategory.NODE, 'rect-node', RectNode as any);\n    register(ExtensionCategory.EDGE, 'line-edge', Edge as any);\n    expect(getExtension(ExtensionCategory.NODE, 'circle-node')).toEqual(CircleNode);\n    expect(getExtension(ExtensionCategory.NODE, 'rect-node')).toEqual(RectNode);\n    expect(getExtension(ExtensionCategory.NODE, 'diamond-node')).toEqual(undefined);\n    expect(getExtension(ExtensionCategory.EDGE, 'line-edge')).toEqual(Edge);\n\n    const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();\n\n    register(ExtensionCategory.NODE, 'circle-node', CircleNode as any);\n    expect(consoleErrorSpy).toHaveBeenCalledTimes(0);\n\n    consoleErrorSpy.mockRestore();\n\n    expect(pick(getExtensions(ExtensionCategory.NODE), ['circle-node', 'rect-node'])).toEqual({\n      'circle-node': CircleNode,\n      'rect-node': RectNode,\n    });\n  });\n\n  it('override', () => {\n    class CircleNode {}\n    class RectNode {}\n    register(ExtensionCategory.NODE, 'circle-node', CircleNode as any);\n    register(ExtensionCategory.NODE, 'circle-node', RectNode as any);\n    expect(getExtension(ExtensionCategory.NODE, 'circle-node')).toEqual(RectNode);\n  });\n\n  it('register shape', () => {\n    const shapes = getExtensions(ExtensionCategory.SHAPE);\n    expect(Object.keys(shapes)).toEqual([\n      'circle',\n      'ellipse',\n      'group',\n      'html',\n      'image',\n      'line',\n      'path',\n      'polygon',\n      'polyline',\n      'rect',\n      'text',\n      'label',\n      'badge',\n    ]);\n\n    register(ExtensionCategory.SHAPE, 'circle-shape', GCircle);\n\n    expect(getExtension(ExtensionCategory.SHAPE, 'circle-shape')).toEqual(GCircle);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/canvas.spec.ts",
    "content": "import { createGraph, createGraphCanvas } from '@@/utils';\n\ndescribe('Canvas', () => {\n  const svg = createGraphCanvas(null, 500, 500, 'svg');\n  beforeAll(async () => {\n    await svg.ready;\n  });\n\n  it('context', () => {\n    expect(svg.context).toBeDefined();\n  });\n\n  it('getDevice', async () => {\n    // getDevice only exists on webgl\n    // webgl canvas cannot init in test env\n    // expect(webgl.getDevice()).toBeDefined();\n  });\n\n  it('coordinate transform', () => {\n    // TODO g canvas client 坐标转换疑似异常\n    expect(svg.getClientByCanvas([0, 0])).toBeCloseTo([0, 0, 0]);\n    expect(svg.getCanvasByViewport([0, 0])).toBeCloseTo([0, 0, 0]);\n\n    expect(svg.getViewportByClient([0, 0])).toBeCloseTo([0, 0, 0]);\n    expect(svg.getViewportByCanvas([0, 0])).toBeCloseTo([0, 0, 0]);\n\n    const camera = svg.getCamera();\n    camera.pan(100, 100);\n    expect([...camera.getPosition()]).toBeCloseTo([350, 350, 500]);\n    expect([...camera.getFocalPoint()]).toBeCloseTo([250, 250, 0]);\n    expect(svg.getCanvasByViewport([0, 0])).toBeCloseTo([100, 100, 0]);\n    expect(svg.getViewportByCanvas([0, 0])).toBeCloseTo([-100, -100, 0]);\n\n    // camera pan 采用相对移动\n    camera.pan(-200, -200);\n    // focal point wont change\n    // expect([...camera.getFocalPoint()]).toBeCloseTo([250, 250, 0]);\n    expect([...camera.getPosition()]).toBeCloseTo([150, 150, 500]);\n    expect(svg.getCanvasByViewport([0, 0])).toBeCloseTo([-100, -100, 0]);\n    expect(svg.getViewportByCanvas([0, 0])).toBeCloseTo([100, 100, 0]);\n\n    // move to origin\n    camera.pan(100, 100);\n\n    camera.pan(-100, -100);\n    expect(svg.getCanvasByViewport([0, 0])).toBeCloseTo([-100, -100, 0]);\n\n    camera.pan(100, 100);\n  });\n\n  it('coordinate transform with landmark', async () => {\n    const camera = svg.getCamera();\n\n    const [px, py, pz] = camera.getPosition();\n    const [fx, fy, fz] = camera.getFocalPoint();\n\n    // expect([fx, fy, fz]).toEqual([250, 250, 0]);\n    expect([px, py, pz]).toEqual([250, 250, 500]);\n\n    const landmark1 = camera.createLandmark('landmark1', {\n      // 视点坐标 / viewport coordinates\n      focalPoint: [fx + 100, fy + 100, fz],\n      // 相机坐标 / camera coordinates\n      position: [px + 100, py + 100, pz],\n    });\n\n    await new Promise<void>((resolve) => {\n      camera.gotoLandmark(landmark1, { onfinish: resolve });\n    });\n\n    expect(svg.getCanvasByViewport([0, 0])).toBeCloseTo([100, 100, 0]);\n    expect(svg.getViewportByCanvas([0, 0])).toBeCloseTo([-100, -100, 0]);\n\n    const landmark2 = camera.createLandmark('landmark2', {\n      // 视点坐标 / viewport coordinates\n      focalPoint: [fx - 100, fy - 100, fz],\n      // 相机坐标 / camera coordinates\n      position: [px - 100, py - 100, pz],\n    });\n\n    await new Promise<void>((resolve) => {\n      camera.gotoLandmark(landmark2, { onfinish: resolve });\n    });\n\n    expect(svg.getCanvasByViewport([0, 0])).toBeCloseTo([-100, -100, 0]);\n    expect(svg.getViewportByCanvas([0, 0])).toBeCloseTo([100, 100, 0]);\n\n    expect([...camera.getFocalPoint()]).toBeCloseTo([150, 150, 0]);\n    expect([...camera.getPosition()]).toBeCloseTo([150, 150, 500]);\n  });\n\n  it('cursor', async () => {\n    const graph = createGraph({\n      cursor: 'progress',\n    });\n\n    await graph.draw();\n\n    expect(graph.getCanvas().getConfig().cursor).toEqual('progress');\n  });\n\n  it('layers', () => {\n    const singleLayerCanvas = createGraphCanvas(document.getElementById('container'), 500, 500, 'svg', {\n      enableMultiLayer: false,\n    });\n    expect(Object.keys(singleLayerCanvas.getLayers())).toEqual(['main']);\n\n    const multiLayerCanvas = createGraphCanvas(document.getElementById('container'), 500, 500, 'svg');\n    expect(Object.keys(multiLayerCanvas.getLayers())).toEqual(['background', 'main', 'label', 'transient']);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/data.spec.ts",
    "content": "import { treeToGraphData } from '@/src';\nimport { DataController } from '@/src/runtime/data';\nimport { reduceDataChanges } from '@/src/utils/change';\nimport { idOf } from '@/src/utils/id';\nimport tree from '@@/dataset/algorithm-category.json';\nimport { clone } from '@antv/util';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', data: { value: 1 }, style: { fill: 'red' } },\n    { id: 'node-2', data: { value: 2 }, combo: 'combo-1', style: { fill: 'green' } },\n    { id: 'node-3', data: { value: 3 }, combo: 'combo-1', style: { fill: 'blue' } },\n  ],\n  edges: [\n    { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 }, style: {} },\n    { id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 }, style: {} },\n  ],\n  combos: [{ id: 'combo-1', data: {}, style: {} }],\n};\n\ndescribe('DataController', () => {\n  it('init', () => {\n    const controller = new DataController();\n\n    expect(controller).toBeDefined();\n    expect(controller.model).toBeDefined();\n  });\n\n  it('empty data', () => {\n    const controller = new DataController();\n\n    expect(controller.getData()).toEqual({ nodes: [], edges: [], combos: [] });\n\n    controller.addComboData([{ id: 'combo-1' }]);\n\n    expect(controller.getComboData(['combo-1'])).toEqual([{ id: 'combo-1', data: {}, style: { zIndex: 0 } }]);\n  });\n\n  it('setData', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    controller.setData({\n      nodes: [{ id: 'node-4', data: { value: 4 }, style: { fill: 'yellow', zIndex: 0 } }],\n    });\n\n    expect(controller.getData()).toEqual({\n      nodes: [{ id: 'node-4', data: { value: 4 }, style: { fill: 'yellow', zIndex: 0 } }],\n      edges: [],\n      combos: [],\n    });\n  });\n\n  it('addData', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    expect(controller.getData()).toEqual({\n      nodes: [\n        { id: 'node-1', data: { value: 1 }, style: { fill: 'red', zIndex: 0 } },\n        { id: 'node-2', data: { value: 2 }, combo: 'combo-1', style: { fill: 'green', zIndex: 1 } },\n        { id: 'node-3', data: { value: 3 }, combo: 'combo-1', style: { fill: 'blue', zIndex: 1 } },\n      ],\n      edges: [\n        { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 }, style: { zIndex: 0 } },\n        { id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 }, style: { zIndex: 0 } },\n      ],\n      combos: [{ id: 'combo-1', data: {}, style: { zIndex: 0 } }],\n    });\n\n    controller.addData({\n      nodes: [{ id: 'node-4', data: { value: 4 }, style: { fill: 'yellow', zIndex: 0 } }],\n    });\n\n    expect(controller.hasNode('node-4')).toBe(true);\n\n    controller.addComboData([{ id: 'combo-2' }]);\n\n    expect(controller.hasEdge('combo-2')).toBe(false);\n    expect(controller.hasEdge('combo-2')).toBe(false);\n    expect(controller.hasCombo('combo-2')).toBe(true);\n\n    controller.addNodeData([]);\n\n    controller.addNodeData([{ id: 'node-5', data: { value: 5 }, combo: 'combo-2', style: { fill: 'black' } }]);\n\n    controller.addEdgeData([{ id: 'edge-3', source: 'node-1', target: 'node-5', data: { weight: 3 } }]);\n\n    expect(controller.getData()).toEqual({\n      nodes: [\n        { id: 'node-1', data: { value: 1 }, style: { fill: 'red', zIndex: 0 } },\n        { id: 'node-2', data: { value: 2 }, combo: 'combo-1', style: { fill: 'green', zIndex: 1 } },\n        { id: 'node-3', data: { value: 3 }, combo: 'combo-1', style: { fill: 'blue', zIndex: 1 } },\n        { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow', zIndex: 0 } },\n        { id: 'node-5', data: { value: 5 }, combo: 'combo-2', style: { fill: 'black', zIndex: 1 } },\n      ],\n      edges: [\n        { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 }, style: { zIndex: 0 } },\n        { id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 }, style: { zIndex: 0 } },\n        { id: 'edge-3', source: 'node-1', target: 'node-5', data: { weight: 3 }, style: { zIndex: 0 } },\n      ],\n      combos: [\n        { id: 'combo-1', data: {}, style: { zIndex: 0 } },\n        { id: 'combo-2', data: {}, style: { zIndex: 0 } },\n      ],\n    });\n  });\n\n  it('getData', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    expect(controller.getNodeData(['node-2']).map(idOf)).toEqual(data.nodes.slice(1, 2).map(idOf));\n\n    expect(controller.getEdgeData(['edge-1']).map(idOf)).toEqual(data.edges.slice(0, 1).map(idOf));\n\n    expect(controller.getComboData(['combo-1']).map(idOf)).toEqual(data.combos.slice(0, 1).map(idOf));\n\n    expect(controller.getChildrenData('combo-1').map(idOf)).toEqual(['node-2', 'node-3']);\n\n    expect(controller.getAncestorsData('node-2', 'combo').map(idOf)).toEqual(['combo-1']);\n  });\n\n  it('updateData', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    controller.updateData({\n      nodes: [{ id: 'node-1', data: { value: 2 }, style: { fill: 'pink', lineWidth: 2 } }],\n      edges: [{ id: 'edge-1', data: { weight: 678 } }],\n    });\n\n    controller.addComboData([{ id: 'combo-2' }]);\n\n    controller.updateNodeData([{ id: 'node-1', data: { value: 666 }, combo: 'combo-2' }]);\n\n    controller.updateNodeData([{ id: 'node-2', combo: 'combo-2' }]);\n\n    controller.updateEdgeData([{ id: 'edge-2', data: { weight: 666 } }]);\n\n    controller.updateComboData([{ id: 'combo-1', data: { value: 100 }, style: { stroke: 'blue' } }]);\n\n    expect(controller.getData()).toEqual({\n      nodes: [\n        { id: 'node-1', data: { value: 666 }, combo: 'combo-2', style: { fill: 'pink', lineWidth: 2, zIndex: 1 } },\n        { id: 'node-2', data: { value: 2 }, combo: 'combo-2', style: { fill: 'green', zIndex: 1 } },\n        { id: 'node-3', data: { value: 3 }, combo: 'combo-1', style: { fill: 'blue', zIndex: 1 } },\n      ],\n      edges: [\n        { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 678 }, style: { zIndex: 0 } },\n        { id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 666 }, style: { zIndex: 0 } },\n      ],\n      combos: [\n        { id: 'combo-1', data: { value: 100 }, style: { stroke: 'blue', zIndex: 0 } },\n        { id: 'combo-2', data: {}, style: { zIndex: 0 } },\n      ],\n    });\n  });\n\n  it('updateNodeData', () => {\n    const controller = new DataController();\n    controller.addNodeData([{ id: 'node-1', data: { value: 1 }, style: { fill: 'red' } }]);\n    // no changes\n    controller.updateNodeData([{ id: 'node-1', data: { value: 1 }, style: { fill: 'red' } }]);\n\n    expect(controller.getNodeData()).toEqual([{ id: 'node-1', data: { value: 1 }, style: { fill: 'red', zIndex: 0 } }]);\n  });\n\n  it('updateEdgeData', () => {\n    const controller = new DataController();\n    controller.addData({\n      nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n      edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 } }],\n    });\n    // no changes\n    controller.updateEdgeData([{ id: 'edge-1', data: { weight: 1 } }]);\n\n    expect(controller.getEdgeData()).toEqual([\n      { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 }, style: { zIndex: -1 } },\n    ]);\n\n    // update source\n    controller.updateEdgeData([{ id: 'edge-1', source: 'node-2' }]);\n\n    expect(controller.getEdgeData()).toEqual([\n      { id: 'edge-1', source: 'node-2', target: 'node-2', data: { weight: 1 }, style: { zIndex: -1 } },\n    ]);\n\n    // update target\n    controller.updateEdgeData([{ id: 'edge-1', target: 'node-1' }]);\n    expect(controller.getEdgeData()).toEqual([\n      { id: 'edge-1', source: 'node-2', target: 'node-1', data: { weight: 1 }, style: { zIndex: -1 } },\n    ]);\n  });\n\n  it('updateComboData', () => {\n    const data = {\n      nodes: [\n        { id: 'node-1', combo: 'combo-1', style: { x: 50, y: 50 } },\n        { id: 'node-2', combo: 'combo-1', style: { x: 100, y: 100 } },\n      ],\n      combos: [\n        {\n          id: 'combo-1',\n          style: { x: 0, y: 0 },\n        },\n        { id: 'combo-2' },\n      ],\n    };\n\n    const controller = new DataController();\n\n    controller.addData(data);\n\n    controller.updateComboData([{ id: 'combo-1', style: { x: 100, y: 100, z: 0 } }]);\n\n    // no changes\n    controller.updateComboData([{ id: 'combo-1', style: { x: 100, y: 100, z: 0 } }]);\n\n    expect(controller.getData()).toEqual({\n      nodes: [\n        { id: 'node-1', data: {}, combo: 'combo-1', style: { x: 50, y: 50, zIndex: 1 } },\n        { id: 'node-2', data: {}, combo: 'combo-1', style: { x: 100, y: 100, zIndex: 1 } },\n      ],\n      edges: [],\n      combos: [\n        { id: 'combo-1', data: {}, style: { x: 100, y: 100, z: 0, zIndex: 0 } },\n        { id: 'combo-2', data: {}, style: { zIndex: 0 } },\n      ],\n    });\n\n    controller.updateComboData([{ id: 'combo-1', style: { fill: 'pink' } }]);\n\n    expect(controller.getData()).toEqual({\n      nodes: [\n        { id: 'node-1', data: {}, combo: 'combo-1', style: { x: 50, y: 50, zIndex: 1 } },\n        { id: 'node-2', data: {}, combo: 'combo-1', style: { x: 100, y: 100, zIndex: 1 } },\n      ],\n      edges: [],\n      combos: [\n        { id: 'combo-1', data: {}, style: { x: 100, y: 100, z: 0, fill: 'pink', zIndex: 0 } },\n        { id: 'combo-2', data: {}, style: { zIndex: 0 } },\n      ],\n    });\n\n    controller.updateComboData([{ id: 'combo-2' }]);\n  });\n\n  it('translateComboBy', () => {\n    const controller = new DataController();\n\n    controller.addData({\n      nodes: [{ id: 'node-1', combo: 'combo-1' }],\n      combos: [{ id: 'combo-1' }],\n    });\n\n    controller.translateComboBy('combo-1', [100, 100]);\n\n    expect(controller.getData()).toEqual({\n      nodes: [{ id: 'node-1', data: {}, combo: 'combo-1', style: { x: 100, y: 100, z: 0, zIndex: 1 } }],\n      combos: [{ id: 'combo-1', data: {}, style: { x: 100, y: 100, z: 0, zIndex: 0 } }],\n      edges: [],\n    });\n  });\n\n  it('translateComboBy with children', () => {\n    const controller = new DataController();\n\n    controller.addData({\n      nodes: [\n        { id: 'node-1' },\n        { id: 'node-2', combo: 'combo-1', style: { y: 50, fill: 'green' } },\n        { id: 'node-3', combo: 'combo-1', style: { x: 100, y: 100, fill: 'blue' } },\n      ],\n      combos: [{ id: 'combo-1' }],\n    });\n\n    controller.translateComboBy('combo-1', [66, 67]);\n\n    expect(controller.getNodeData()).toEqual([\n      { id: 'node-1', data: {}, style: { zIndex: 0 } },\n      { id: 'node-2', combo: 'combo-1', data: {}, style: { x: 66, y: 117, z: 0, fill: 'green', zIndex: 1 } },\n      { id: 'node-3', combo: 'combo-1', data: {}, style: { x: 166, y: 167, z: 0, fill: 'blue', zIndex: 1 } },\n    ]);\n  });\n\n  it('translateComboBy without children', () => {\n    const controller = new DataController();\n\n    controller.addData({\n      combos: [{ id: 'combo-1' }],\n    });\n\n    controller.translateComboBy('combo-1', [100, 100]);\n\n    expect(controller.getData()).toEqual({\n      nodes: [],\n      edges: [],\n      combos: [{ id: 'combo-1', data: {}, style: { x: 100, y: 100, z: 0, zIndex: 0 } }],\n    });\n  });\n\n  it('translateComboTo', () => {\n    const controller = new DataController();\n\n    controller.addData({\n      nodes: [\n        { id: 'node-1', combo: 'combo-1' },\n        { id: 'node-2', combo: 'combo-1', style: { x: 10, y: 10 } },\n      ],\n      combos: [{ id: 'combo-1' }],\n    });\n\n    controller.translateComboTo('combo-1', [100, 100]);\n\n    expect(controller.getData()).toEqual({\n      nodes: [\n        { id: 'node-1', data: {}, combo: 'combo-1', style: { x: 100, y: 100, z: 0, zIndex: 1 } },\n        { id: 'node-2', data: {}, combo: 'combo-1', style: { x: 110, y: 110, z: 0, zIndex: 1 } },\n      ],\n      combos: [{ id: 'combo-1', data: {}, style: { x: 100, y: 100, z: 0, zIndex: 0 } }],\n      edges: [],\n    });\n  });\n\n  it('removeData', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    controller.removeData({\n      nodes: ['node-1'],\n      edges: ['edge-1'],\n      // combos: ['combo-1'],\n    });\n\n    expect(controller.getData()).toEqual({\n      nodes: [\n        { id: 'node-2', data: { value: 2 }, combo: 'combo-1', style: { fill: 'green', zIndex: 1 } },\n        { id: 'node-3', data: { value: 3 }, combo: 'combo-1', style: { fill: 'blue', zIndex: 1 } },\n      ],\n      edges: [{ id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 }, style: { zIndex: 0 } }],\n      combos: [{ id: 'combo-1', data: {}, style: { zIndex: 0 } }],\n    });\n\n    controller.removeComboData(['combo-1']);\n\n    expect(controller.getData()).toEqual({\n      nodes: [\n        { id: 'node-2', data: { value: 2 }, combo: undefined, style: { fill: 'green', zIndex: 1 } },\n        { id: 'node-3', data: { value: 3 }, combo: undefined, style: { fill: 'blue', zIndex: 1 } },\n      ],\n      edges: [{ id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 }, style: { zIndex: 0 } }],\n      combos: [],\n    });\n\n    controller.removeEdgeData([]);\n\n    controller.removeEdgeData(['edge-2']);\n\n    expect(controller.getData()).toEqual({\n      nodes: [\n        { id: 'node-2', data: { value: 2 }, combo: undefined, style: { fill: 'green', zIndex: 1 } },\n        { id: 'node-3', data: { value: 3 }, combo: undefined, style: { fill: 'blue', zIndex: 1 } },\n      ],\n      edges: [],\n      combos: [],\n    });\n\n    controller.removeNodeData();\n\n    controller.removeNodeData(['node-2']);\n\n    expect(controller.getData()).toEqual({\n      nodes: [{ id: 'node-3', data: { value: 3 }, combo: undefined, style: { fill: 'blue', zIndex: 1 } }],\n      edges: [],\n      combos: [],\n    });\n  });\n\n  it('removeComboData', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    expect(controller.getParentData('node-2', 'combo')?.id).toEqual(data.combos[0].id);\n\n    controller.removeComboData(['combo-1']);\n\n    expect(controller.getData()).toEqual({\n      nodes: [\n        { id: 'node-1', data: { value: 1 }, style: { fill: 'red', zIndex: 0 } },\n        { id: 'node-2', data: { value: 2 }, combo: undefined, style: { fill: 'green', zIndex: 1 } },\n        { id: 'node-3', data: { value: 3 }, combo: undefined, style: { fill: 'blue', zIndex: 1 } },\n      ],\n      edges: [\n        { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 }, style: { zIndex: 0 } },\n        { id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 }, style: { zIndex: 0 } },\n      ],\n      combos: [],\n    });\n\n    expect(controller.getParentData('node-2', 'combo')).toEqual(undefined);\n  });\n\n  it('removeComboData with children', () => {\n    const data = {\n      nodes: [\n        { id: 'node-1', data: {}, combo: 'combo-1' },\n        { id: 'node-2', data: {}, combo: 'combo-1' },\n        { id: 'node-3', data: {}, combo: 'combo-1' },\n      ],\n      combos: [\n        { id: 'combo-1', data: {}, combo: 'combo-2' },\n        { id: 'combo-2', data: {}, style: {} },\n      ],\n    };\n\n    const controller = new DataController();\n\n    controller.addData(data);\n\n    expect(controller.getComboData(['combo-1'])[0].style?.zIndex).toEqual(1);\n    expect(controller.getComboData(['combo-2'])[0].style?.zIndex).toEqual(0);\n    expect(controller.getParentData('node-1', 'combo')?.id).toEqual('combo-1');\n    expect(controller.getParentData('combo-1', 'combo')?.id).toEqual('combo-2');\n\n    // combo-1 删除后，node-1、node-2、node-3 会被移动到 combo-2\n    // after combo-1 is removed, node-1, node-2, node-3 will be moved to combo-2\n    // 迁移后 combo-2 的 zIndex 不会被更新\n    // after migration, the zIndex of combo-2 will not be updated\n    controller.removeComboData(['combo-1']);\n    expect(controller.getComboData(['combo-2'])[0].style?.zIndex).toEqual(0);\n\n    expect(controller.getParentData('combo-2', 'combo')).toEqual(undefined);\n    expect(controller.getParentData('node-1', 'combo')?.id).toEqual('combo-2');\n\n    expect(controller.getData()).toEqual({\n      nodes: [\n        { id: 'node-1', data: {}, style: { zIndex: 2 }, combo: 'combo-2' },\n        { id: 'node-2', data: {}, style: { zIndex: 2 }, combo: 'combo-2' },\n        { id: 'node-3', data: {}, style: { zIndex: 2 }, combo: 'combo-2' },\n      ],\n      edges: [],\n      combos: [{ id: 'combo-2', data: {}, style: { zIndex: 0 } }],\n    });\n  });\n\n  it('changes', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    const changes1 = controller.getChanges();\n\n    expect(changes1).toEqual([\n      { value: { id: 'combo-1', data: {}, style: {} }, type: 'ComboAdded' },\n      { value: { id: 'node-1', data: { value: 1 }, style: { fill: 'red' } }, type: 'NodeAdded' },\n      { value: { id: 'node-2', data: { value: 2 }, combo: 'combo-1', style: { fill: 'green' } }, type: 'NodeAdded' },\n      { value: { id: 'node-3', data: { value: 3 }, combo: 'combo-1', style: { fill: 'blue' } }, type: 'NodeAdded' },\n      // 新增子元素后更新 combo / update combo after add child\n      {\n        value: { id: 'combo-1', data: {}, style: {} },\n        original: { id: 'combo-1', data: {}, style: {} },\n        type: 'ComboUpdated',\n      },\n      {\n        value: { id: 'combo-1', data: {}, style: {} },\n        original: { id: 'combo-1', data: {}, style: {} },\n        type: 'ComboUpdated',\n      },\n      {\n        value: { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 }, style: {} },\n        type: 'EdgeAdded',\n      },\n      {\n        value: { id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 }, style: {} },\n        type: 'EdgeAdded',\n      },\n      // 更新 zIndex / update zIndex\n      {\n        value: { id: 'combo-1', data: {}, style: { zIndex: 0 } },\n        original: { id: 'combo-1', data: {}, style: {} },\n        type: 'ComboUpdated',\n      },\n      {\n        value: { id: 'node-1', data: { value: 1 }, style: { fill: 'red', zIndex: 0 } },\n        original: { id: 'node-1', data: { value: 1 }, style: { fill: 'red' } },\n        type: 'NodeUpdated',\n      },\n      {\n        value: { id: 'node-2', combo: 'combo-1', data: { value: 2 }, style: { fill: 'green', zIndex: 1 } },\n        original: { id: 'node-2', combo: 'combo-1', data: { value: 2 }, style: { fill: 'green' } },\n        type: 'NodeUpdated',\n      },\n      {\n        value: { id: 'node-3', combo: 'combo-1', data: { value: 3 }, style: { fill: 'blue', zIndex: 1 } },\n        original: { id: 'node-3', combo: 'combo-1', data: { value: 3 }, style: { fill: 'blue' } },\n        type: 'NodeUpdated',\n      },\n      {\n        value: { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 }, style: { zIndex: 0 } },\n        original: { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 }, style: {} },\n        type: 'EdgeUpdated',\n      },\n      {\n        value: { id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 }, style: { zIndex: 0 } },\n        original: { id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 }, style: {} },\n        type: 'EdgeUpdated',\n      },\n    ]);\n\n    controller.clearChanges();\n\n    controller.setData({\n      nodes: [\n        { id: 'node-3', data: { value: 3 }, combo: 'combo-2', style: { fill: 'pink' } },\n        { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow' } },\n      ],\n      combos: [{ id: 'combo-2' }],\n    });\n\n    const changes2 = controller.getChanges();\n\n    expect(changes2).toEqual([\n      { value: { id: 'combo-2' }, type: 'ComboAdded' },\n      { value: { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow' } }, type: 'NodeAdded' },\n      {\n        value: { id: 'combo-2', data: {}, style: { zIndex: 0 } },\n        original: { id: 'combo-2', data: {}, style: {} },\n        type: 'ComboUpdated',\n      },\n      {\n        value: { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow', zIndex: 0 } },\n        original: { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow' } },\n        type: 'NodeUpdated',\n      },\n      {\n        value: { id: 'node-3', combo: 'combo-2', data: { value: 3 }, style: { fill: 'pink', zIndex: 1 } },\n        original: { id: 'node-3', combo: 'combo-1', data: { value: 3 }, style: { fill: 'blue', zIndex: 1 } },\n        type: 'NodeUpdated',\n      },\n      // 移动节点后更新 combo / update combo after move node\n      {\n        value: { id: 'combo-2', data: {}, style: { zIndex: 0 } },\n        original: { id: 'combo-2', data: {}, style: { zIndex: 0 } },\n        type: 'ComboUpdated',\n      },\n      {\n        value: { id: 'node-3', combo: 'combo-2', data: { value: 3 }, style: { fill: 'pink', zIndex: 1 } },\n        original: { id: 'node-3', combo: 'combo-2', data: { value: 3 }, style: { fill: 'pink', zIndex: 1 } },\n        type: 'NodeUpdated',\n      },\n      {\n        value: { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 }, style: { zIndex: 0 } },\n        type: 'EdgeRemoved',\n      },\n      {\n        value: { id: 'edge-2', source: 'node-2', target: 'node-3', data: { weight: 2 }, style: { zIndex: 0 } },\n        type: 'EdgeRemoved',\n      },\n      { value: { id: 'node-1', data: { value: 1 }, style: { fill: 'red', zIndex: 0 } }, type: 'NodeRemoved' },\n      {\n        value: { id: 'node-2', combo: 'combo-1', data: { value: 2 }, style: { fill: 'green', zIndex: 1 } },\n        type: 'NodeRemoved',\n      },\n      // 移除节点后更新 combo / update combo after remove node\n      {\n        value: { id: 'combo-1', data: {}, style: { zIndex: 0 } },\n        original: { id: 'combo-1', data: {}, style: { zIndex: 0 } },\n        type: 'ComboUpdated',\n      },\n      { value: { id: 'combo-1', data: {}, style: { zIndex: 0 } }, type: 'ComboRemoved' },\n    ]);\n\n    expect(reduceDataChanges([...changes1, ...changes2])).toEqual([\n      {\n        type: 'NodeAdded',\n        value: { id: 'node-3', combo: 'combo-2', data: { value: 3 }, style: { fill: 'pink', zIndex: 1 } },\n      },\n      { type: 'ComboAdded', value: { id: 'combo-2', data: {}, style: { zIndex: 0 } } },\n      { type: 'NodeAdded', value: { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow', zIndex: 0 } } },\n    ]);\n  });\n\n  it('changes add', () => {\n    const controller = new DataController();\n\n    controller.addData({\n      nodes: [\n        { id: 'node-3', data: { value: 3 }, combo: 'combo-2', style: { fill: 'pink' } },\n        { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow' } },\n      ],\n      combos: [{ id: 'combo-2' }],\n    });\n\n    const changes = controller.getChanges();\n\n    expect(reduceDataChanges(changes)).toEqual([\n      { value: { id: 'combo-2', data: {}, style: { zIndex: 0 } }, type: 'ComboAdded' },\n      {\n        type: 'NodeAdded',\n        value: { id: 'node-3', data: { value: 3 }, combo: 'combo-2', style: { fill: 'pink', zIndex: 1 } },\n      },\n      { value: { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow', zIndex: 0 } }, type: 'NodeAdded' },\n    ]);\n  });\n\n  it('silence', () => {\n    const controller = new DataController();\n\n    controller.silence(() => {\n      controller.addData(clone(data));\n\n      controller.setData({\n        nodes: [\n          { id: 'node-3', data: { value: 3 }, combo: 'combo-2', style: { fill: 'pink' } },\n          { id: 'node-4', data: { value: 4 }, style: { fill: 'yellow' } },\n        ],\n        combos: [{ id: 'combo-2' }],\n      });\n    });\n\n    const changes = controller.getChanges();\n\n    expect(changes.length).toBe(0);\n  });\n\n  it('getElementData', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    expect(controller.getElementDataById('node-1').id).toEqual(data.nodes[0].id);\n\n    expect(controller.getElementDataById('edge-1').id).toEqual(data.edges[0].id);\n\n    expect(controller.getElementDataById('combo-1').id).toEqual(data.combos[0].id);\n\n    expect(() => {\n      controller.getElementDataById('undefined');\n    }).toThrow();\n  });\n\n  it('getNodeLikeData', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    expect(controller.getNodeLikeData(['node-1'])[0].id).toEqual(data.nodes[0].id);\n\n    expect(controller.getNodeLikeData(['edge-1'])[0]).toEqual(undefined);\n\n    expect(controller.getNodeLikeData(['combo-1'])[0].id).toEqual(data.combos[0].id);\n\n    expect(controller.getNodeLikeData().map(idOf)).toEqual([...data.combos, ...data.nodes].map(idOf));\n  });\n\n  it('getRootsData', () => {\n    const controller = new DataController();\n\n    controller.addData(treeToGraphData(tree));\n\n    expect(controller.getRootsData('tree').map(idOf)).toEqual(['Modeling Methods']);\n  });\n\n  it('getAncestorsData getParentData getChildrenData', () => {\n    const controller = new DataController();\n\n    controller.addData(treeToGraphData(tree));\n\n    expect(controller.getAncestorsData('Logistic regression', 'tree').map(idOf)).toEqual([\n      'Classification',\n      'Modeling Methods',\n    ]);\n\n    expect(controller.getParentData('Classification', 'tree')?.id).toBe(tree.id);\n\n    expect(controller.getChildrenData('Modeling Methods').map((child) => child.id)).toEqual(\n      tree.children.map((child) => child.id),\n    );\n  });\n\n  it('hasNode', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    expect(controller.hasNode('node-1')).toBe(true);\n    expect(controller.hasNode('node-4')).toBe(false);\n  });\n\n  it('hasEdge', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    expect(controller.hasEdge('edge-1')).toBe(true);\n    expect(controller.hasEdge('edge-4')).toBe(false);\n  });\n\n  it('hasCombo', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    expect(controller.hasCombo('combo-1')).toBe(true);\n    expect(controller.hasCombo('combo-4')).toBe(false);\n  });\n\n  it('getComboChildrenData', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    expect(controller.getChildrenData('combo-1').map(idOf)).toEqual(data.nodes.slice(1).map(idOf));\n\n    controller.updateNodeData([{ id: 'node-1', combo: 'combo-1' }]);\n\n    expect(controller.getChildrenData('combo-1')?.sort((a, b) => (a.id < b.id ? -1 : 1))).toEqual([\n      { id: 'node-1', data: { value: 1 }, combo: 'combo-1', style: { fill: 'red', zIndex: 1 } },\n      { id: 'node-2', data: { value: 2 }, combo: 'combo-1', style: { fill: 'green', zIndex: 1 } },\n      { id: 'node-3', data: { value: 3 }, combo: 'combo-1', style: { fill: 'blue', zIndex: 1 } },\n    ]);\n\n    controller.addComboData([{ id: 'combo-2' }]);\n\n    controller.updateNodeData([\n      { id: 'node-2', combo: 'combo-2' },\n      { id: 'node-3', combo: 'combo-2' },\n    ]);\n\n    expect(controller.getChildrenData('combo-1')).toEqual([\n      { id: 'node-1', data: { value: 1 }, combo: 'combo-1', style: { fill: 'red', zIndex: 1 } },\n    ]);\n\n    expect(controller.getChildrenData('combo-2')).toEqual([\n      { id: 'node-2', data: { value: 2 }, combo: 'combo-2', style: { fill: 'green', zIndex: 1 } },\n      { id: 'node-3', data: { value: 3 }, combo: 'combo-2', style: { fill: 'blue', zIndex: 1 } },\n    ]);\n  });\n\n  it('getParentComboData', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    expect(controller.getParentData('node-1', 'combo')).toEqual(undefined);\n\n    controller.updateNodeData([{ id: 'node-1', combo: 'combo-1' }]);\n\n    expect(controller.getParentData('node-1', 'combo')?.id).toEqual(data.combos[0].id);\n\n    expect(controller.getParentData('combo-3', 'combo')).toEqual(undefined);\n  });\n\n  it('getRelatedEdgesData', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    expect(controller.getRelatedEdgesData('node-1')).toEqual([\n      { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 }, style: { zIndex: 0 } },\n    ]);\n\n    controller.addEdgeData([{ id: 'edge-3', source: 'node-1', target: 'node-3', data: { weight: 3 } }]);\n\n    expect(controller.getRelatedEdgesData('node-1')).toEqual([\n      { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 }, style: { zIndex: 0 } },\n      { id: 'edge-3', source: 'node-1', target: 'node-3', data: { weight: 3 }, style: { zIndex: 0 } },\n    ]);\n\n    expect(controller.getRelatedEdgesData('node-1', 'in')).toEqual([]);\n\n    expect(controller.getRelatedEdgesData('node-1', 'out')).toEqual([\n      { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 1 }, style: { zIndex: 0 } },\n      { id: 'edge-3', source: 'node-1', target: 'node-3', data: { weight: 3 }, style: { zIndex: 0 } },\n    ]);\n  });\n\n  it('getNeighborNodesData', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    expect(controller.getNeighborNodesData('node-1')).toEqual([\n      { id: 'node-2', data: { value: 2 }, combo: 'combo-1', style: { fill: 'green', zIndex: 1 } },\n    ]);\n\n    controller.addEdgeData([{ id: 'edge-3', source: 'node-1', target: 'node-3', data: { weight: 3 } }]);\n\n    expect(controller.getNeighborNodesData('node-1')).toEqual([\n      { id: 'node-2', data: { value: 2 }, combo: 'combo-1', style: { fill: 'green', zIndex: 1 } },\n      { id: 'node-3', data: { value: 3 }, combo: 'combo-1', style: { fill: 'blue', zIndex: 1 } },\n    ]);\n  });\n\n  it('getElementType', () => {\n    const controller = new DataController();\n\n    controller.addData(clone(data));\n\n    expect(controller.getElementType('node-1')).toEqual('node');\n\n    expect(controller.getElementType('edge-1')).toEqual('edge');\n\n    expect(controller.getElementType('combo-1')).toEqual('combo');\n\n    expect(() => {\n      controller.getElementType('undefined');\n    }).toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/element/event.spec.ts",
    "content": "import { IPointerEvent, NodeEvent } from '@/src';\nimport { createGraph } from '@@/utils';\nimport type { DisplayObject } from '@antv/g';\nimport { CustomEvent } from '@antv/g';\n\ndescribe('element event', () => {\n  it('element event target', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n      },\n    });\n\n    const click = jest.fn();\n\n    graph.on<IPointerEvent>(NodeEvent.CLICK, click);\n\n    await graph.draw();\n\n    // @ts-expect-error context is private\n    const node1 = graph.context.element!.getElement('node-1')!;\n    (node1.children[0] as DisplayObject).dispatchEvent(new CustomEvent('click', {}));\n\n    expect(click).toHaveBeenCalledTimes(1);\n    expect(click.mock.calls[0][0].target).toBe(node1);\n    expect(click.mock.calls[0][0].targetType).toBe('node');\n    expect(click.mock.calls[0][0].originalTarget).toBe(node1.children[0]);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/element/state.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('element states', () => {\n  it('element state', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n      },\n    });\n\n    await graph.draw();\n\n    expect(graph.getElementState('node-1')).toEqual([]);\n\n    graph.setElementState('node-1', 'selected');\n    expect(graph.getElementState('node-1')).toEqual(['selected']);\n\n    graph.setElementState('node-1', ['selected', 'hovered']);\n    expect(graph.getElementState('node-1')).toEqual(['selected', 'hovered']);\n\n    graph.setElementState('node-1', '');\n    expect(graph.getElementState('node-1')).toEqual([]);\n\n    graph.setElementState('node-1', 'selected');\n\n    graph.setElementState('node-1', []);\n    expect(graph.getElementState('node-1')).toEqual([]);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/element/visibility.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { createGraph } from '@@/utils';\n\ndescribe('element visibility', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', style: { x: 50, y: 50 } },\n          { id: 'node-2', style: { x: 200, y: 50 } },\n          { id: 'node-3', style: { x: 125, y: 150, opacity: 0.5 } },\n        ],\n        edges: [\n          { source: 'node-1', target: 'node-2' },\n          { source: 'node-2', target: 'node-3', style: { opacity: 0.5 } },\n          { source: 'node-3', target: 'node-1' },\n        ],\n      },\n      theme: 'light',\n      node: {\n        style: {\n          size: 20,\n        },\n      },\n    });\n\n    await graph.render();\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('hide', async () => {\n    graph.hideElement(['node-3', 'node-2-node-3', 'node-3-node-1']);\n\n    expect(graph.getElementVisibility('node-3')).toBe('hidden');\n    expect(graph.getElementVisibility('node-2-node-3')).toBe('hidden');\n    expect(graph.getElementVisibility('node-3-node-1')).toBe('hidden');\n\n    await expect(graph).toMatchSnapshot(__filename, 'hidden');\n  });\n\n  it('show', async () => {\n    graph.showElement(['node-3', 'node-2-node-3', 'node-3-node-1']);\n\n    expect(graph.getElementVisibility('node-3')).toBe('visible');\n    expect(graph.getElementVisibility('node-2-node-3')).toBe('visible');\n    expect(graph.getElementVisibility('node-3-node-1')).toBe('visible');\n\n    await expect(graph).toMatchSnapshot(__filename, 'visible');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/element/z-index.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { createGraph } from '@@/utils';\n\ndescribe('element z-index', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', style: { x: 150, y: 150, fill: 'red' } },\n          { id: 'node-2', style: { x: 175, y: 175, fill: 'green' } },\n          { id: 'node-3', style: { x: 200, y: 200, fill: 'blue' } },\n        ],\n      },\n      theme: 'light',\n      node: {\n        style: {\n          size: 50,\n        },\n      },\n    });\n\n    await graph.render();\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('front', async () => {\n    graph.frontElement('node-2');\n\n    await expect(graph).toMatchSnapshot(__filename, 'front');\n  });\n\n  it('to', async () => {\n    graph.setElementZIndex({ 'node-2': -1 });\n\n    await expect(graph).toMatchSnapshot(__filename);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/element.spec.ts",
    "content": "import { Graph } from '@/src';\nimport * as BUILT_IN_PALETTES from '@/src/palettes';\nimport { light as LIGHT_THEME } from '@/src/themes';\nimport { idOf } from '@/src/utils/id';\nimport { createGraph } from '@@/utils';\nimport { omit } from '@antv/util';\n\ndescribe('ElementController', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', style: { x: 100, y: 100, fill: 'red', stroke: 'pink', lineWidth: 1 }, data: { value: 100 } },\n          { id: 'node-2', style: { x: 150, y: 100 }, data: { value: 150 } },\n          { id: 'node-3', combo: 'combo-1', states: ['selected'], style: { x: 125, y: 150 }, data: { value: 150 } },\n        ],\n        edges: [\n          { id: 'edge-1', source: 'node-1', target: 'node-2', data: { weight: 250 } },\n          {\n            id: 'edge-2',\n            source: 'node-2',\n            target: 'node-3',\n            states: ['active', 'selected'],\n            style: { lineWidth: 5 },\n            data: { weight: 300 },\n          },\n        ],\n        combos: [{ id: 'combo-1' }],\n      },\n      node: {\n        style: {\n          fill: (datum) => ((datum?.data?.value as number) > 100 ? 'red' : 'blue'),\n        },\n        state: {\n          selected: {\n            fill: (datum) => ((datum?.data?.value as number) > 100 ? 'purple' : 'cyan'),\n          },\n        },\n        palette: 'spectral',\n      },\n      edge: {\n        style: {},\n        state: {\n          selected: {\n            stroke: 'red',\n          },\n          active: {\n            stroke: 'pink',\n            lineWidth: 4,\n          },\n        },\n        palette: { type: 'group', color: 'oranges', field: (d) => idOf(d).toString(), invert: true },\n      },\n      combo: {\n        style: {},\n        state: {},\n        palette: 'blues',\n      },\n    });\n\n    await graph.render();\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('static', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    // @ts-expect-error context is private.\n    const elementController = graph.context.element!;\n\n    const options = graph.getOptions();\n\n    const node1 = options.data!.nodes![0];\n    const node2 = options.data!.nodes![1];\n    const node3 = options.data!.nodes![2];\n    const edge1 = options.data!.edges![0];\n    const edge2 = options.data!.edges![1];\n    const combo1 = options.data!.combos![0];\n\n    const edge1Id = idOf(edge1);\n    const edge2Id = idOf(edge2);\n\n    // ref light theme\n    expect(elementController.getThemeStyle('node')).toEqual(LIGHT_THEME.node!.style);\n    expect(elementController.getThemeStateStyle('node', [])).toEqual({});\n\n    expect(elementController.getThemeStateStyle('node', ['selected'])).toEqual({\n      ...LIGHT_THEME.node!.state!.selected,\n    });\n    expect(elementController.getThemeStateStyle('node', ['selected', 'active'])).toEqual({\n      ...LIGHT_THEME.node!.state!.selected,\n      ...LIGHT_THEME.node!.state!.active,\n    });\n\n    expect(elementController.getDefaultStyle('node-1')).toEqual({ fill: 'blue' });\n    expect(elementController.getDefaultStyle('node-2')).toEqual({ fill: 'red' });\n    expect(elementController.getDefaultStyle('node-3')).toEqual({ fill: 'red' });\n    expect(elementController.getDefaultStyle(edge1Id)).toEqual({});\n    expect(elementController.getDefaultStyle('combo-1')).toEqual({});\n\n    expect(elementController.getStateStyle('node-1')).toEqual({});\n    expect(elementController.getStateStyle('node-2')).toEqual({});\n    expect(elementController.getStateStyle('node-3')).toEqual({ fill: 'purple' });\n    expect(elementController.getStateStyle(idOf(options.data!.edges![1]))).toEqual({\n      stroke: 'red',\n      lineWidth: 4,\n    });\n    expect(elementController.getStateStyle('combo-1')).toEqual({});\n\n    expect(elementController.getElementComputedStyle('node', node1)).toEqual({\n      ...LIGHT_THEME.node?.style,\n      fill: 'blue',\n      stroke: 'pink',\n      lineWidth: 1,\n      // from palette\n      x: 100,\n      y: 100,\n    });\n\n    expect(elementController.getElementComputedStyle('node', node2)).toEqual({\n      ...LIGHT_THEME.node?.style,\n      fill: 'red',\n      // from palette\n      x: 150,\n      y: 100,\n    });\n\n    expect(elementController.getElementComputedStyle('node', node3)).toEqual({\n      ...LIGHT_THEME.node?.style,\n      ...LIGHT_THEME.node?.state?.selected,\n      // from state\n      fill: 'purple',\n      // from palette\n      x: 125,\n      y: 150,\n    });\n\n    expect(omit(elementController.getElementComputedStyle('edge', edge1), ['sourceNode', 'targetNode'])).toEqual({\n      ...LIGHT_THEME.edge?.style,\n      stroke: BUILT_IN_PALETTES.oranges.at(-1),\n    });\n\n    expect(omit(elementController.getElementComputedStyle('edge', edge2), ['sourceNode', 'targetNode'])).toEqual({\n      ...LIGHT_THEME.edge?.style,\n      ...LIGHT_THEME.edge?.state?.active,\n      ...LIGHT_THEME.edge?.state?.selected,\n      lineWidth: 4,\n      stroke: 'red',\n    });\n\n    const comboStyle = elementController.getElementComputedStyle('combo', combo1);\n\n    expect(comboStyle.childrenNode[0]).toEqual('node-3');\n\n    expect(omit(comboStyle, ['childrenNode', 'childrenData'])).toEqual({\n      ...LIGHT_THEME.combo?.style,\n      fill: BUILT_IN_PALETTES.blues[0],\n    });\n  });\n\n  it('runtime', async () => {\n    graph.setData({\n      nodes: [{ id: 'node-1' }, { id: 'node-2', combo: 'combo-1' }, { id: 'node-3', combo: 'combo-1' }],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-2', target: 'node-3' },\n      ],\n      combos: [{ id: 'combo-1' }],\n    });\n\n    await graph.render();\n\n    // @ts-expect-error context is private.\n    const elementController = graph.context.element!;\n\n    expect(elementController.getNodes().length).toBe(3);\n    expect(elementController.getEdges().length).toBe(2);\n    expect(elementController.getCombos().length).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/graph/add-children-data.spec.ts",
    "content": "import { layoutCompactBoxBasic } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('add children data', () => {\n  it('default', async () => {\n    const graph = await createDemoGraph(layoutCompactBoxBasic, { animation: false });\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.addChildrenData('Rules', [{ id: 'node-1' }, { id: 'node-2' }]);\n    await graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename, 'add-children-data');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/graph/auto-resize.spec.ts",
    "content": "import { Graph } from '@/src';\nimport { createGraphCanvas, sleep } from '@@/utils';\n\ndescribe('Graph autoResize', () => {\n  it('autoResize trigger by window.resize', async () => {\n    const $container = document.createElement('div');\n    document.body.appendChild($container);\n    const canvas = createGraphCanvas($container, 500, 500);\n\n    const graph = new Graph({\n      container: canvas,\n      width: 500,\n      height: 500,\n      autoResize: true,\n      data: {\n        nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n      },\n      theme: 'light',\n      node: {},\n      layout: {\n        type: 'grid',\n      },\n    });\n\n    await graph.render();\n\n    expect(graph.getSize()).toEqual([500, 500]);\n\n    $container.style.display = 'block';\n    $container.style.width = '400px';\n    $container.style.height = '400px';\n    window.dispatchEvent(new Event('resize'));\n\n    // auto resize debounce is 300ms.\n    await sleep(500);\n\n    expect(graph.getSize()).toEqual([400, 400]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/graph/event.spec.ts",
    "content": "import { GraphEvent } from '@/src';\nimport { createGraph } from '@@/utils';\nimport { Renderer as CanvasRenderer } from '@antv/g-canvas';\n\ndescribe('event', () => {\n  it('canvas ready', async () => {\n    const graph = createGraph({\n      container: document.createElement('div'),\n    });\n\n    const ready = jest.fn();\n    graph.on(GraphEvent.BEFORE_CANVAS_INIT, ready);\n    graph.on(GraphEvent.AFTER_CANVAS_INIT, ready);\n\n    await graph.draw();\n\n    expect(ready).toHaveBeenCalledTimes(2);\n\n    graph.destroy();\n  });\n\n  it('graph lifecycle event', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n      },\n      layout: {\n        type: 'grid',\n      },\n    });\n\n    const sequence: string[] = [];\n\n    const addSequence = (type: string) => () => {\n      sequence.push(type);\n    };\n\n    const beforeDraw = jest.fn(addSequence('beforeDraw'));\n    const afterDraw = jest.fn(addSequence('afterDraw'));\n    const beforeRender = jest.fn(addSequence('beforeRender'));\n    const afterRender = jest.fn(addSequence('afterRender'));\n    const beforeLayout = jest.fn(addSequence('beforeLayout'));\n    const afterLayout = jest.fn(addSequence('afterLayout'));\n\n    graph.on(GraphEvent.BEFORE_DRAW, beforeDraw);\n    graph.on(GraphEvent.AFTER_DRAW, afterDraw);\n    graph.on(GraphEvent.BEFORE_RENDER, beforeRender);\n    graph.on(GraphEvent.AFTER_RENDER, afterRender);\n    graph.on(GraphEvent.BEFORE_LAYOUT, beforeLayout);\n    graph.on(GraphEvent.AFTER_LAYOUT, afterLayout);\n\n    await graph.render();\n\n    expect(beforeDraw).toHaveBeenCalledTimes(1);\n    expect(afterDraw).toHaveBeenCalledTimes(1);\n    expect(beforeRender).toHaveBeenCalledTimes(1);\n    expect(afterRender).toHaveBeenCalledTimes(1);\n    expect(beforeLayout).toHaveBeenCalledTimes(1);\n    expect(afterLayout).toHaveBeenCalledTimes(1);\n\n    expect(sequence).toEqual(['beforeRender', 'beforeLayout', 'afterLayout', 'beforeDraw', 'afterDraw', 'afterRender']);\n\n    graph.destroy();\n  });\n\n  it('element lifecycle event', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n      },\n    });\n\n    const create = jest.fn();\n    const update = jest.fn();\n    const destroy = jest.fn();\n\n    graph.on(GraphEvent.AFTER_ELEMENT_CREATE, create);\n    graph.on(GraphEvent.AFTER_ELEMENT_UPDATE, update);\n    graph.on(GraphEvent.AFTER_ELEMENT_DESTROY, destroy);\n\n    await graph.draw();\n\n    expect(create).toHaveBeenCalledTimes(3);\n    expect(update).toHaveBeenCalledTimes(0);\n    expect(destroy).toHaveBeenCalledTimes(0);\n\n    expect(create.mock.calls[0][0].elementType).toEqual('node');\n    expect(create.mock.calls[0][0].data.id).toEqual('node-1');\n    expect(create.mock.calls[1][0].elementType).toEqual('node');\n    expect(create.mock.calls[1][0].data.id).toEqual('node-2');\n    expect(create.mock.calls[2][0].elementType).toEqual('edge');\n    expect(create.mock.calls[2][0].data.id).toEqual('edge-1');\n\n    create.mockClear();\n\n    graph.addNodeData([{ id: 'node-3' }]);\n    graph.updateData({\n      nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],\n      edges: [{ id: 'edge-1', source: 'node-1', target: 'node-3' }],\n    });\n    graph.removeNodeData(['node-2']);\n\n    await graph.draw();\n\n    expect(create).toHaveBeenCalledTimes(1);\n    expect(update).toHaveBeenCalledTimes(2);\n    expect(destroy).toHaveBeenCalledTimes(1);\n\n    expect(create.mock.calls[0][0].data.id).toEqual('node-3');\n    expect(update.mock.calls[0][0].data.id).toEqual('node-1');\n    expect(update.mock.calls[1][0].data.id).toEqual('edge-1');\n    expect(destroy.mock.calls[0][0].data.id).toEqual('node-2');\n\n    graph.destroy();\n  });\n\n  it('element state', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n      },\n    });\n\n    await graph.draw();\n\n    const create = jest.fn();\n    const update = jest.fn();\n    const destroy = jest.fn();\n\n    graph.on(GraphEvent.AFTER_ELEMENT_CREATE, create);\n    graph.on(GraphEvent.AFTER_ELEMENT_UPDATE, update);\n    graph.on(GraphEvent.AFTER_ELEMENT_DESTROY, destroy);\n\n    // state change\n    graph.updateNodeData([{ id: 'node-1', states: ['active'] }]);\n    await graph.draw();\n\n    expect(update).toHaveBeenCalledTimes(2);\n    expect(update.mock.calls[0][0].data.id).toEqual('node-1');\n    expect(update.mock.calls[0][0].data.states).toEqual(['active']);\n\n    // 同时会更新相邻的边 / It will also update the adjacent edge\n    expect(update.mock.calls[1][0].data.id).toEqual('edge-1');\n\n    update.mockClear();\n    graph.setElementState('node-1', []);\n    expect(update).toHaveBeenCalledTimes(2);\n    expect(update.mock.calls[0][0].data.id).toEqual('node-1');\n    expect(update.mock.calls[0][0].data.states).toEqual([]);\n    expect(update.mock.calls[1][0].data.id).toEqual('edge-1');\n\n    graph.destroy();\n  });\n\n  it('renderer change event', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n      },\n    });\n\n    const beforeRendererChange = jest.fn();\n    const afterRendererChange = jest.fn();\n\n    graph.on(GraphEvent.BEFORE_RENDERER_CHANGE, beforeRendererChange);\n    graph.on(GraphEvent.AFTER_RENDERER_CHANGE, afterRendererChange);\n\n    await graph.render();\n\n    expect(beforeRendererChange).toHaveBeenCalledTimes(0);\n    expect(afterRendererChange).toHaveBeenCalledTimes(0);\n\n    const renderer = () => new CanvasRenderer();\n\n    graph.setOptions({\n      renderer,\n    });\n\n    expect(beforeRendererChange).toHaveBeenCalledTimes(1);\n    expect(afterRendererChange).toHaveBeenCalledTimes(1);\n  });\n\n  it('draw event', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n      },\n    });\n\n    const beforeDraw = jest.fn();\n    const afterDraw = jest.fn();\n\n    graph.on(GraphEvent.BEFORE_DRAW, (event: any) => {\n      beforeDraw(event.data.render);\n    });\n    graph.on(GraphEvent.AFTER_DRAW, (event: any) => {\n      afterDraw(event.data.render);\n    });\n\n    await graph.render();\n\n    expect(beforeDraw).toHaveBeenCalledTimes(1);\n    expect(beforeDraw.mock.calls[0][0]).toBe(true);\n    expect(afterDraw).toHaveBeenCalledTimes(1);\n    expect(afterDraw.mock.calls[0][0]).toBe(true);\n\n    beforeDraw.mockClear();\n    afterDraw.mockClear();\n\n    graph.addNodeData([{ id: 'node-3' }]);\n\n    await graph.draw();\n\n    expect(beforeDraw).toHaveBeenCalledTimes(1);\n    expect(beforeDraw.mock.calls[0][0]).toBe(false);\n    expect(afterDraw).toHaveBeenCalledTimes(1);\n    expect(afterDraw.mock.calls[0][0]).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/graph/get-plugin-instantce.spec.ts",
    "content": "import { BasePlugin, ExtensionCategory, register } from '@/src';\nimport { createGraph } from '@@/utils';\n\ndescribe('getPluginInstance', () => {\n  it('getPluginInstance', async () => {\n    const fn = jest.fn();\n\n    class CustomPlugin extends BasePlugin<any> {\n      // plugin api\n      api() {\n        fn();\n      }\n    }\n\n    register(ExtensionCategory.PLUGIN, 'custom', CustomPlugin);\n    const graph = createGraph({\n      plugins: [\n        {\n          key: 'custom-plugin',\n          type: 'custom',\n        },\n      ],\n    });\n\n    await graph.draw();\n\n    const plugin = graph.getPluginInstance<CustomPlugin>('custom-plugin');\n    expect(plugin instanceof CustomPlugin).toBe(true);\n    plugin.api();\n    expect(fn).toHaveBeenCalled();\n\n    const undefinedPlugin = graph.getPluginInstance<CustomPlugin>('undefined-plugin');\n    expect(undefinedPlugin).toBe(undefined);\n  });\n\n  it('getPluginInstance by type', async () => {\n    const fn = jest.fn();\n\n    class CustomPlugin extends BasePlugin<any> {\n      api() {\n        fn();\n      }\n    }\n\n    register(ExtensionCategory.PLUGIN, 'custom-2', CustomPlugin);\n    const graph = createGraph({\n      plugins: ['custom-2', 'custom-2'],\n    });\n\n    await graph.draw();\n\n    const plugin = graph.getPluginInstance<CustomPlugin>('custom-2');\n    expect(plugin instanceof CustomPlugin).toBe(true);\n    plugin.api();\n    expect(fn).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/graph/graph.spec.ts",
    "content": "import { Graph, idOf } from '@/src';\nimport data from '@@/dataset/cluster.json';\nimport { commonGraph } from '@@/demos/common-graph';\nimport { createDemoGraph, createGraph } from '@@/utils';\n\ndescribe('Graph', () => {\n  let graph: Graph;\n  beforeAll(async () => {\n    graph = await createDemoGraph(commonGraph, { animation: false });\n  });\n\n  it('getOptions/setOptions', () => {\n    graph.setOptions({ zoomRange: [-10, 10] });\n    expect(graph.getOptions().zoomRange).toEqual([-10, 10]);\n  });\n\n  it('setZoomRange/getZoomRange', () => {\n    graph.setZoomRange([-5, 5]);\n    expect(graph.getZoomRange()).toEqual([-5, 5]);\n  });\n\n  it('setNode/getNode', () => {\n    const options = graph.getOptions();\n    graph.setNode(Object.assign({}, options.node, { state: { selected: { fill: 'pink' } } }));\n    expect(graph.getOptions().node!.state!.selected).toEqual({\n      fill: 'pink',\n    });\n  });\n\n  it('setEdge/getEdge', () => {\n    const options = graph.getOptions();\n    graph.setEdge(Object.assign({}, options.edge, { state: { selected: { stroke: 'pink' } } }));\n    expect(graph.getOptions().edge!.state!.selected).toEqual({\n      stroke: 'pink',\n    });\n  });\n\n  it('setCombo/getCombo', () => {\n    const options = graph.getOptions();\n    graph.setCombo(Object.assign({}, options.combo, { state: { selected: { fill: 'pink' } } }));\n    expect(graph.getOptions().combo!.state!.selected).toEqual({\n      fill: 'pink',\n    });\n  });\n\n  it('hasNode', () => {\n    expect(graph.hasNode('0')).toBe(true);\n    expect(graph.hasNode('1')).toBe(true);\n\n    expect(graph.hasNode('non-existent-node')).toBe(false);\n    expect(graph.hasNode('node-999')).toBe(false);\n\n    expect(graph.hasNode('')).toBe(false);\n    expect(graph.hasNode(null as any)).toBe(false);\n    expect(graph.hasNode(undefined as any)).toBe(false);\n  });\n\n  it('hasEdge', () => {\n    expect(graph.hasEdge('0-1')).toBe(true);\n\n    expect(graph.hasEdge('non-existent-edge')).toBe(false);\n    expect(graph.hasEdge('edge-999')).toBe(false);\n\n    expect(graph.hasEdge('')).toBe(false);\n    expect(graph.hasEdge(null as any)).toBe(false);\n    expect(graph.hasEdge(undefined as any)).toBe(false);\n  });\n\n  it('hasCombo', () => {\n    graph.addComboData([\n      { id: 'combo-test-1', style: {} },\n      { id: 'combo-test-2', style: {} },\n    ]);\n\n    expect(graph.hasCombo('combo-test-1')).toBe(true);\n    expect(graph.hasCombo('combo-test-2')).toBe(true);\n\n    expect(graph.hasCombo('non-existent-combo')).toBe(false);\n    expect(graph.hasCombo('combo-999')).toBe(false);\n\n    expect(graph.hasCombo('')).toBe(false);\n    expect(graph.hasCombo(null as any)).toBe(false);\n    expect(graph.hasCombo(undefined as any)).toBe(false);\n\n    graph.removeComboData(['combo-test-1', 'combo-test-2']);\n  });\n\n  it('setSize/getSize', () => {\n    expect(graph.getSize()).toEqual([500, 500]);\n    expect(createGraph({}).getSize()).toEqual([0, 0]);\n\n    const g = createGraph({ width: 100, height: 50 });\n    expect(g.getSize()).toEqual([100, 50]);\n    g.setSize(400, 100);\n    expect(g.getSize()).toEqual([400, 100]);\n  });\n\n  it('setTheme', () => {\n    graph.setTheme('light');\n    expect(graph.getTheme()).toEqual('light');\n  });\n\n  it('getTheme', () => {\n    expect(graph.getTheme()).toEqual('light');\n  });\n\n  it('getLayout', () => {\n    expect(graph.getLayout()).toEqual({ type: 'd3-force' });\n  });\n\n  it('getBehaviors/setBehaviors/updateBehavior', () => {\n    expect(graph.getBehaviors()).toEqual(['zoom-canvas', 'drag-canvas']);\n    graph.setBehaviors(['drag-canvas']);\n    expect(graph.getBehaviors()).toEqual(['drag-canvas']);\n    graph.setBehaviors([{ key: 'behavior-1', type: 'zoom-canvas', enable: false }]);\n    expect(graph.getBehaviors()).toEqual([{ key: 'behavior-1', type: 'zoom-canvas', enable: false }]);\n    graph.updateBehavior({ key: 'behavior-1', enable: true });\n    expect(graph.getBehaviors()).toEqual([{ key: 'behavior-1', type: 'zoom-canvas', enable: true }]);\n\n    expect(createGraph({}).getBehaviors()).toEqual([]);\n  });\n\n  it('getPlugins/setPlugins/updatePlugin', () => {\n    expect(graph.getPlugins()).toEqual([]);\n    graph.setPlugins([{ type: 'test' }]);\n    expect(graph.getPlugins()).toEqual([{ type: 'test' }]);\n    graph.setPlugins([{ key: 'plugin-1', type: 'test' }]);\n    expect(graph.getPlugins()).toEqual([{ key: 'plugin-1', type: 'test' }]);\n    graph.updatePlugin({ key: 'plugin-1', enable: false });\n    expect(graph.getPlugins()).toEqual([{ key: 'plugin-1', type: 'test', enable: false }]);\n    graph.setPlugins([]);\n\n    const g = createGraph({});\n    expect(g.getPlugins()).toEqual([]);\n    g.setPlugins([\n      { type: 'test', key: 'test' },\n      { type: 'test2', key: 'test2' },\n    ]);\n    g.updatePlugin({ key: 'test', enable: false });\n    expect(g.getPlugins()).toEqual([\n      { type: 'test', key: 'test', enable: false },\n      { type: 'test2', key: 'test2' },\n    ]);\n  });\n\n  it('getTransforms/setTransforms/updateTransform', () => {\n    expect(graph.getTransforms()).toEqual([]);\n    graph.setTransforms([{ type: 'flow', key: 'flow-1' }]);\n    expect(graph.getTransforms()).toEqual([{ type: 'flow', key: 'flow-1' }]);\n    graph.updateTransform({ key: 'flow-1', props1: 'a' });\n    expect(graph.getTransforms()).toEqual([{ type: 'flow', key: 'flow-1', props1: 'a' }]);\n    graph.setTransforms([\n      { type: 'flow', key: 'flow-1' },\n      { type: 'flow', key: 'flow-2' },\n    ]);\n    graph.updateTransform({ key: 'flow-2', props1: 'b' });\n    expect(graph.getTransforms()).toEqual([\n      { type: 'flow', key: 'flow-1' },\n      { type: 'flow', key: 'flow-2', props1: 'b' },\n    ]);\n    graph.setTransforms([]);\n  });\n\n  it('updateData/getData/setData', () => {\n    // 调整之后，getData 获取的为当前 graph 最新的数据，而不是初始化时的数据\n    // After adjustment, the data obtained by getData is the latest data of the graph, not the data when it is initialized\n    const currData = graph.getData();\n    expect(currData.nodes?.map(idOf)).toEqual(data.nodes.map(idOf));\n    expect(currData.edges?.map(idOf)).toEqual(data.edges.map(idOf));\n\n    graph.setData({\n      nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n      edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n    });\n    expect(graph.getData()).toEqual({\n      nodes: [\n        { id: 'node-1', data: {}, style: { zIndex: 0 } },\n        { id: 'node-2', data: {}, style: { zIndex: 0 } },\n      ],\n      edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', data: {}, style: { zIndex: -1 } }],\n      combos: [],\n    });\n\n    graph.updateData({ edges: [{ id: 'edge-1', style: { lineWidth: 5 } }] });\n    expect(graph.getData()).toEqual({\n      nodes: [\n        { id: 'node-1', data: {}, style: { zIndex: 0 } },\n        { id: 'node-2', data: {}, style: { zIndex: 0 } },\n      ],\n      edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', data: {}, style: { lineWidth: 5, zIndex: -1 } }],\n      combos: [],\n    });\n  });\n\n  it('addData/updateData/setData callback', () => {\n    const g = createGraph({\n      data: {\n        nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n      },\n    });\n    g.updateData((data) => {\n      expect(data).toEqual({\n        nodes: [\n          { id: 'node-1', data: {}, style: { zIndex: 0 } },\n          { id: 'node-2', data: {}, style: { zIndex: 0 } },\n        ],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', data: {}, style: { zIndex: -1 } }],\n        combos: [],\n      });\n      return { nodes: [{ id: 'node-1', data: { value: 1 } }] };\n    });\n    expect(g.getNodeData('node-1')).toEqual({ id: 'node-1', data: { value: 1 }, style: { zIndex: 0 } });\n    g.setData((data) => {\n      expect(data).toEqual({\n        nodes: [\n          { id: 'node-1', data: { value: 1 }, style: { zIndex: 0 } },\n          { id: 'node-2', data: {}, style: { zIndex: 0 } },\n        ],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', data: {}, style: { zIndex: -1 } }],\n        combos: [],\n      });\n      return { nodes: [], edges: [] };\n    });\n    g.addData((data) => {\n      expect(data).toEqual({\n        nodes: [],\n        edges: [],\n        combos: [],\n      });\n      return { nodes: [{ id: 'node-1' }] };\n    });\n    expect(g.getNodeData('node-1')).toEqual({ id: 'node-1', data: {}, style: { zIndex: 0 } });\n  });\n\n  it('getElementData', () => {\n    expect(graph.getElementData('node-1').id).toEqual('node-1');\n    expect(graph.getElementData(['node-1']).map(idOf)).toEqual(['node-1']);\n  });\n\n  it('getXxxData/addXxxData/updateXxxData/removeXxxData', () => {\n    expect(graph.getNodeData('node-1').id).toEqual('node-1');\n    expect(graph.getNodeData(['node-1']).map(idOf)).toEqual(['node-1']);\n    expect(graph.getNodeData().map(idOf)).toEqual(['node-1', 'node-2']);\n    expect(graph.getEdgeData('edge-1').id).toEqual('edge-1');\n    expect(graph.getEdgeData(['edge-1']).map(idOf)).toEqual(['edge-1']);\n    expect(graph.getEdgeData().map(idOf)).toEqual(['edge-1']);\n    expect(graph.getComboData()).toEqual([]);\n\n    graph.addComboData([{ id: 'combo-2', style: {} }]);\n    graph.addComboData([{ id: 'combo-1', combo: 'combo-2', style: {} }]);\n    graph.addNodeData([\n      { id: 'node-3', combo: 'combo-1' },\n      { id: 'node-4', combo: 'combo-1' },\n    ]);\n    graph.addEdgeData([{ id: 'edge-2', source: 'node-3', target: 'node-4' }]);\n    expect(graph.getNodeData().map(idOf)).toEqual(['node-1', 'node-2', 'node-3', 'node-4']);\n    expect(graph.getEdgeData().map(idOf)).toEqual(['edge-1', 'edge-2']);\n    expect(graph.getComboData('combo-1').id).toEqual('combo-1');\n    expect(graph.getComboData(['combo-1']).map(idOf)).toEqual(['combo-1']);\n    expect(graph.getComboData()).toEqual([\n      { id: 'combo-2', data: {}, style: { zIndex: 0 } },\n      { id: 'combo-1', combo: 'combo-2', data: {}, style: { zIndex: 1 } },\n    ]);\n    expect(graph.getChildrenData('combo-1').map(idOf)).toEqual(['node-3', 'node-4']);\n    expect(graph.getDescendantsData('combo-2').map(idOf)).toEqual(['combo-1', 'node-3', 'node-4']);\n    graph.removeComboData(['combo-2']);\n    graph.updateNodeData([{ id: 'node-3', style: { x: 100, y: 100 } }]);\n    graph.updateEdgeData([{ id: 'edge-2', style: { lineWidth: 10 } }]);\n    graph.updateComboData([{ id: 'combo-1', style: { stroke: 'red' } }]);\n    expect(graph.getNodeData()).toEqual([\n      { id: 'node-1', data: {}, style: { zIndex: 0 } },\n      { id: 'node-2', data: {}, style: { zIndex: 0 } },\n      { id: 'node-3', data: {}, combo: 'combo-1', style: { x: 100, y: 100, zIndex: 2 } },\n      { id: 'node-4', data: {}, combo: 'combo-1', style: { zIndex: 2 } },\n    ]);\n    expect(graph.getEdgeData().map(idOf)).toEqual(['edge-1', 'edge-2']);\n    expect(graph.getComboData()).toEqual([{ id: 'combo-1', data: {}, style: { stroke: 'red', zIndex: 1 } }]);\n    graph.removeComboData(['combo-1']);\n    graph.removeNodeData(['node-3', 'node-4']);\n    expect(graph.getNodeData().map(idOf)).toEqual(['node-1', 'node-2']);\n    expect(graph.getEdgeData().map(idOf)).toEqual(['edge-1']);\n    expect(graph.getComboData()).toEqual([]);\n  });\n\n  it('getXxxData/addXxxData/updateXxxData/removeXxxData callback', () => {\n    const g = createGraph({\n      data: {\n        nodes: [{ id: 'node-1' }],\n      },\n    });\n    g.addNodeData((data) => {\n      expect(data).toEqual([{ id: 'node-1', data: {}, style: { zIndex: 0 } }]);\n      return [{ id: 'node-2' }];\n    });\n    expect(g.getNodeData().map(idOf)).toEqual(['node-1', 'node-2']);\n    g.updateNodeData((data) => {\n      expect(data).toEqual([\n        { id: 'node-1', data: {}, style: { zIndex: 0 } },\n        { id: 'node-2', data: {}, style: { zIndex: 0 } },\n      ]);\n      return [{ id: 'node-2', style: { x: 100 } }];\n    });\n    expect(g.getNodeData()).toEqual([\n      { id: 'node-1', data: {}, style: { zIndex: 0 } },\n      { id: 'node-2', data: {}, style: { x: 100, zIndex: 0 } },\n    ]);\n\n    g.addEdgeData((data) => {\n      expect(data).toEqual([]);\n      return [{ id: 'edge-1', source: 'node-1', target: 'node-2' }];\n    });\n    expect(g.getEdgeData().map(idOf)).toEqual(['edge-1']);\n    g.updateEdgeData((data) => {\n      expect(data).toEqual([{ id: 'edge-1', source: 'node-1', target: 'node-2', data: {}, style: { zIndex: -1 } }]);\n      return [{ id: 'edge-1', style: { lineWidth: 5, zIndex: 1 } }];\n    });\n    expect(g.getEdgeData()).toEqual([\n      { id: 'edge-1', source: 'node-1', target: 'node-2', data: {}, style: { lineWidth: 5, zIndex: 1 } },\n    ]);\n\n    g.addComboData((data) => {\n      expect(data).toEqual([]);\n      return [{ id: 'combo-1' }];\n    });\n    expect(g.getComboData().map(idOf)).toEqual(['combo-1']);\n    g.updateComboData((data) => {\n      expect(data).toEqual([{ id: 'combo-1', data: {}, style: { zIndex: 0 } }]);\n      return [{ id: 'combo-1', style: { stroke: 'red' } }];\n    });\n    expect(g.getComboData()).toEqual([{ id: 'combo-1', data: {}, style: { stroke: 'red', zIndex: 0 } }]);\n\n    g.removeEdgeData((data) => {\n      expect(data.length).toBe(1);\n      return ['edge-1'];\n    });\n    expect(g.getEdgeData()).toEqual([]);\n\n    g.removeNodeData((data) => {\n      expect(data.length).toBe(2);\n      return ['node-1'];\n    });\n    expect(g.getNodeData().map(idOf)).toEqual(['node-2']);\n\n    g.removeComboData((data) => {\n      expect(data.length).toBe(1);\n      return ['combo-1'];\n    });\n    expect(g.getComboData()).toEqual([]);\n  });\n\n  it('draw', async () => {\n    await expect(graph).toMatchSnapshot(__filename, 'before-draw');\n    await graph.draw();\n    await expect(graph).toMatchSnapshot(__filename, 'after-draw');\n    expect(graph.getElementRenderStyle('node-1')).toBeDefined();\n  });\n\n  it('getElementType', () => {\n    expect(graph.getElementType('node-1')).toEqual('node');\n    expect(graph.getElementType('edge-1')).toEqual('edge');\n  });\n\n  it('getRelatedEdgesData', () => {\n    expect(graph.getRelatedEdgesData('node-1').map(idOf)).toEqual(['edge-1']);\n\n    expect(graph.getRelatedEdgesData('node-1', 'in')).toEqual([]);\n\n    expect(graph.getRelatedEdgesData('node-1', 'out').map(idOf)).toEqual(['edge-1']);\n  });\n\n  it('getNeighborNodesData', () => {\n    expect(graph.getNeighborNodesData('node-1')).toEqual([{ id: 'node-2', data: {}, style: { zIndex: 0 } }]);\n  });\n\n  it('getParentData', () => {\n    expect(graph.getParentData('node-1', 'combo')).toBeUndefined();\n  });\n\n  it('getAncestors', () => {\n    const tree = createGraph({\n      data: {\n        nodes: [{ id: 'node-1', children: ['node-2'] }, { id: 'node-2', children: ['node-3'] }, { id: 'node-3' }],\n      },\n    });\n    expect(tree.getAncestorsData('node-3', 'tree').map(idOf)).toEqual(['node-2', 'node-1']);\n    expect(tree.getAncestorsData('node-2', 'tree').map(idOf)).toEqual(['node-1']);\n    expect(tree.getAncestorsData('node-1', 'tree')).toEqual([]);\n\n    const combo = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-1', combo: 'combo-1' },\n          { id: 'node-2', combo: 'combo-1' },\n        ],\n        combos: [{ id: 'combo-1' }, { id: 'combo-2', combo: 'combo-1' }],\n      },\n    });\n    expect(combo.getAncestorsData('node-1', 'combo').map(idOf)).toEqual(['combo-1']);\n    expect(combo.getAncestorsData('node-2', 'combo').map(idOf)).toEqual(['combo-1']);\n    expect(combo.getAncestorsData('combo-1', 'combo')).toEqual([]);\n    expect(combo.getAncestorsData('combo-2', 'combo').map(idOf)).toEqual(['combo-1']);\n  });\n\n  it('getElementRenderBounds', () => {\n    const renderBounds = graph.getElementRenderBounds('node-1');\n    // the default size of the node is 32\n    expect(renderBounds.min).toEqual([-16, -16, 0]);\n    expect(renderBounds.max).toEqual([16, 16, 0]);\n  });\n\n  it('setElementState/getElementState/getElementDataByState', async () => {\n    await graph.setElementState('node-2', 'selected');\n    expect(graph.getElementState('node-2')).toEqual(['selected']);\n    await graph.setElementState('node-2', []);\n    expect(graph.getElementState('node-2')).toEqual([]);\n\n    await graph.setElementState({ 'node-1': 'selected' });\n    expect(graph.getElementState('node-1')).toEqual(['selected']);\n    expect(graph.getElementState('node-2')).toEqual([]);\n    expect(graph.getElementDataByState('node', 'selected')).toEqual([\n      { id: 'node-1', data: {}, style: { zIndex: 0 }, states: ['selected'] },\n    ]);\n  });\n\n  it('setElementZIndex/getElementZIndex', async () => {\n    await graph.setElementZIndex('node-1', 2);\n    expect(graph.getElementZIndex('node-1')).toBe(2);\n    await graph.setElementZIndex({ 'node-1': 0 });\n    expect(graph.getElementZIndex('node-1')).toBe(0);\n\n    const baseNodeZIndex = 0;\n    await graph.frontElement('node-1');\n    expect(graph.getElementZIndex('node-1')).toBe(baseNodeZIndex + 1);\n    expect(graph.getElementZIndex('node-2')).toBe(baseNodeZIndex);\n  });\n\n  it('setElementVisibility/getElementVisibility', async () => {\n    await graph.hideElement('node-1');\n    expect(graph.getElementVisibility('node-1')).toBe('hidden');\n    await graph.showElement('node-1');\n    expect(graph.getElementVisibility('node-1')).toBe('visible');\n\n    await graph.setElementVisibility({ 'node-1': 'hidden' });\n    expect(graph.getElementVisibility('node-1')).toBe('hidden');\n    expect(graph.getElementVisibility('node-2')).toBe('visible');\n    await graph.setElementVisibility({ 'node-1': 'visible' });\n    expect(graph.getElementVisibility('node-1')).toBe('visible');\n  });\n\n  it('layout', async () => {\n    await expect(graph).toMatchSnapshot(__filename, 'before-layout');\n    await graph.layout();\n    await expect(graph).toMatchSnapshot(__filename, 'after-layout');\n    expect(graph.getElementPosition('node-1')).toBeDefined();\n  });\n\n  it('translateBy/translateTo', async () => {\n    const [px, py] = graph.getPosition();\n    await graph.translateBy([100, 100]);\n    expect(graph.getPosition()).toBeCloseTo([px + 100, py + 100]);\n    await expect(graph).toMatchSnapshot(__filename, 'after-translate');\n    await graph.translateTo([0, 0]);\n    expect(graph.getPosition()).toBeCloseTo([px, py]);\n  });\n\n  it('zoomTo/zoomBy', async () => {\n    const zoom = graph.getZoom();\n    expect(zoom).toBeCloseTo(1);\n    await graph.zoomTo(2);\n    expect(graph.getZoom()).toBeCloseTo(2);\n    await expect(graph).toMatchSnapshot(__filename, 'after-zoom-2');\n    graph.zoomBy(0.5);\n    expect(graph.getZoom()).toBeCloseTo(1);\n  });\n\n  it('rotateTo/rotateBy', async () => {\n    const rotate = graph.getRotation();\n    expect(rotate).toBeCloseTo(0);\n    graph.rotateTo(90);\n    expect(graph.getRotation()).toBeCloseTo(90);\n    await expect(graph).toMatchSnapshot(__filename, 'after-rotate-90');\n    graph.rotateBy(-90);\n    expect(graph.getRotation()).toBeCloseTo(0);\n  });\n\n  it('translateElementTo/translateElementBy', async () => {\n    const [px, py] = graph.getElementPosition('node-1');\n    graph.translateElementBy({ 'node-1': [100, 100] }, false);\n    await expect(graph).toMatchSnapshot(__filename, 'after-translate-node-1');\n    expect(graph.getElementPosition('node-1')).toBeCloseTo([px + 100, py + 100, 0]);\n    graph.translateElementTo({ 'node-1': [px, py] }, false);\n    expect(graph.getElementPosition('node-1')).toBeCloseTo([px, py, 0]);\n  });\n\n  it('getCanvasByViewport', () => {\n    expect(graph.getCanvasByViewport([250, 250])).toBeCloseTo([250, 250, 0]);\n  });\n\n  it('getViewportByCanvas', () => {\n    expect(graph.getViewportByCanvas([250, 250])).toBeCloseTo([250, 250, 0]);\n  });\n\n  it('getClientByCanvas', () => {\n    expect(graph.getClientByCanvas([250, 250])).toBeCloseTo([250, 250, 0]);\n  });\n\n  it('getCanvasByClient', () => {\n    expect(graph.getCanvasByClient([250, 250])).toBeCloseTo([250, 250, 0]);\n  });\n\n  it('getViewportCenter', () => {\n    expect(graph.getViewportCenter()).toBeCloseTo([250, 250, 0]);\n  });\n\n  it('toDataURL', async () => {\n    expect(await graph.toDataURL()).toBeDefined();\n    expect(await graph.toDataURL({ mode: 'overall' })).toBeDefined();\n    expect((await graph.toDataURL({ type: 'image/jpeg' })).startsWith('data:image/jpeg')).toBe(true);\n    expect((await graph.toDataURL({ type: 'image/png' })).startsWith('data:image/png')).toBe(true);\n  });\n\n  it('resize', () => {\n    graph.resize(600, 600);\n    expect(graph.getSize()).toEqual([600, 600]);\n  });\n\n  it('clear/removeData', async () => {\n    graph.removeEdgeData(['edge-1']);\n    expect(graph.getEdgeData()).toEqual([]);\n    graph.removeData({ nodes: ['node-1'] });\n    expect(graph.getNodeData().map(idOf)).toEqual(['node-2']);\n    await graph.clear();\n    expect(graph.getData()).toEqual({ nodes: [], edges: [], combos: [] });\n  });\n\n  it('destroy', () => {\n    graph.destroy();\n    // @ts-expect-error context is private.\n    expect(graph.context).toEqual({});\n    expect(graph.destroyed).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/graph/this.spec.ts",
    "content": "import { createGraph } from '@@/utils';\n\ndescribe('this pointer', () => {\n  it('element mapper', async () => {\n    const graph = createGraph({\n      data: {\n        nodes: [\n          { id: 'node-0', combo: 'combo-0', style: { x: 100, y: 100 }, states: ['selected'] },\n          { id: 'node-1', combo: 'combo-0', style: { x: 150, y: 100 } },\n          { id: 'node-2', style: { x: 250, y: 100 } },\n        ],\n        edges: [{ source: 'node-1', target: 'node-2', states: ['activate'] }],\n        combos: [{ id: 'combo-0', states: ['disabled'] }],\n      },\n    });\n\n    await graph.render();\n\n    const test = jest.fn((instance) => {\n      expect(instance).toBe(graph);\n    });\n\n    const node = {\n      type: function () {\n        test(this); // 3 times\n        return 'circle';\n      },\n      style: {\n        fill: function () {\n          test(this); // 3 times\n          return 'red';\n        },\n      },\n      state: {\n        selected: {\n          fill: function () {\n            test(this); // 1 time\n            return 'yellow';\n          },\n        },\n      },\n    };\n    const edge = {\n      type: function () {\n        test(this); // 1 time\n        return 'line';\n      },\n      style: {\n        stroke: function () {\n          test(this); // 1 time\n          return 'blue';\n        },\n      },\n      state: {\n        activate: {\n          stroke: function () {\n            test(this); // 1 time\n            return 'orange';\n          },\n        },\n      },\n    };\n    const combo = {\n      type: function () {\n        test(this); // 1 time\n        return 'circle';\n      },\n      style: {\n        fill: function () {\n          test(this); // 1 time\n          return 'green';\n        },\n      },\n      state: {\n        disabled: {\n          fill: function () {\n            test(this); // 1 time\n            return 'purple';\n          },\n        },\n      },\n    };\n\n    graph.setNode(node);\n    graph.setEdge(edge);\n    graph.setCombo(combo);\n\n    await graph.render();\n\n    graph.setNode({\n      style: function () {\n        test(this); // 3 times\n        return {};\n      },\n      state: {\n        selected: function () {\n          test(this); // 1 time\n          return {};\n        },\n      },\n    });\n\n    graph.setEdge({\n      state: {\n        activate: {\n          fill: function () {\n            test(this); // 1 time\n            return 'pink';\n          },\n        },\n      },\n    });\n\n    graph.setCombo({});\n\n    await graph.render();\n\n    expect(test).toHaveBeenCalledTimes(18);\n  });\n\n  it('behavior, plugin, transform', async () => {\n    const test = jest.fn((instance) => {\n      expect(instance).toBe(graph);\n    });\n\n    const graph = createGraph({\n      behaviors: [\n        'click-select',\n        function () {\n          test(this);\n          return {\n            type: 'drag-element',\n          };\n        },\n      ],\n      plugins: [\n        'history',\n        function () {\n          test(this);\n          return {\n            type: 'tooltip',\n          };\n        },\n      ],\n      transforms: [\n        function () {\n          test(this);\n          return {\n            type: 'parallel-edges',\n          };\n        },\n      ],\n    });\n\n    await graph.render();\n\n    expect(test).toHaveBeenCalledTimes(3);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/layout.spec.ts",
    "content": "import { layoutCircularBasic } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('layout options', () => {\n  it('layoutEmptyOptions', async () => {\n    const graph = await createDemoGraph(layoutCircularBasic);\n    await expect(graph).toMatchSnapshot(__filename, 'empty');\n    graph.destroy();\n  });\n\n  it('layoutExtraOptions', async () => {\n    const graph = await createDemoGraph(layoutCircularBasic);\n    await graph.layout({\n      type: 'circular',\n      radius: 1000,\n    });\n    await expect(graph).toMatchSnapshot(__filename, 'extra');\n    graph.destroy();\n  });\n\n  it('layoutOtherTypeOptionsAndRecover', async () => {\n    const graph = await createDemoGraph(layoutCircularBasic);\n    await graph.layout({\n      type: 'concentric',\n    });\n    await expect(graph).toMatchSnapshot(__filename, 'other-type');\n    await graph.layout();\n    await expect(graph).toMatchSnapshot(__filename, 'recover');\n    graph.destroy();\n  });\n\n  it('layoutArrayOptions', async () => {\n    const graph = await createDemoGraph(layoutCircularBasic);\n    await graph.layout(\n      Array.from({ length: 4 }, () => ({\n        type: 'circular',\n      })),\n    );\n    await expect(graph).toMatchSnapshot(__filename, 'layout-array');\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/runtime/viewport.spec.ts",
    "content": "import { Graph } from '@/src';\nimport { controllerViewport } from '@@/demos/controller-viewport';\nimport { viewportFit } from '@@/demos/viewport-fit';\nimport { createDemoGraph } from '@@/utils';\nimport { AABB } from '@antv/g';\n\ndescribe('ViewportController', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(controllerViewport);\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('viewport center', () => {\n    expect(graph.getViewportCenter()).toBeCloseTo([250, 250, 0]);\n  });\n\n  it('canvas size', () => {\n    // @ts-expect-error context is private.\n    expect(graph.context.viewport.getCanvasSize()).toEqual([500, 500]);\n  });\n\n  it('viewport zoom', async () => {\n    expect(graph.getZoom()).toBe(1);\n\n    await graph.zoomBy(0.5);\n    expect(graph.getZoom()).toBe(0.5);\n\n    await expect(graph).toMatchSnapshot(__filename, 'zoom-0.5');\n\n    await graph.zoomBy(4, { duration: 100 });\n    expect(graph.getZoom()).toBe(2);\n\n    await expect(graph).toMatchSnapshot(__filename, 'zoom-2');\n\n    await graph.zoomTo(1);\n    expect(graph.getZoom()).toBe(1);\n\n    graph.setZoomRange([0.1, 10]);\n    expect(graph.getZoomRange()).toEqual([0.1, 10]);\n  });\n\n  it('viewport translate', async () => {\n    await graph.translateBy([100, 100]);\n\n    expect(graph.getPosition()).toBeCloseTo([100, 100]);\n\n    await graph.translateTo([200, 200]);\n\n    expect(graph.getPosition()).toBeCloseTo([200, 200]);\n\n    await expect(graph).toMatchSnapshot(__filename, 'translate');\n\n    await graph.translateTo([0, 0], { duration: 100 });\n  });\n\n  it('viewport rotate', async () => {\n    await graph.rotateBy(45);\n    expect(graph.getRotation()).toBe(45);\n\n    await graph.rotateBy(90);\n    expect(graph.getRotation()).toBe(45 + 90);\n    await expect(graph).toMatchSnapshot(__filename, 'rotate-135');\n\n    await graph.rotateTo(90, { duration: 100 });\n    expect(graph.getRotation()).toBe(90);\n\n    await expect(graph).toMatchSnapshot(__filename, 'rotate-90');\n\n    await graph.rotateTo(0);\n  });\n\n  it('coordinate transform', async () => {\n    expect(graph.getPosition()).toBeCloseTo([0, 0]);\n    expect(graph.getClientByCanvas([0, 0])).toBeCloseTo([0, 0, 0]);\n\n    expect(graph.getCanvasCenter()).toBeCloseTo([250, 250, 0]);\n    expect(graph.getViewportCenter()).toBeCloseTo([250, 250, 0]);\n    expect(graph.getCanvasByViewport([0, 0])).toBeCloseTo([0, 0, 0]);\n    expect(graph.getViewportByCanvas([0, 0])).toBeCloseTo([0, 0, 0]);\n\n    // without animation\n    await graph.translateTo([100, 100]);\n\n    expect(graph.getPosition()).toBeCloseTo([100, 100]);\n    expect(graph.getCanvasCenter()).toBeCloseTo([250, 250, 0]);\n    expect(graph.getViewportCenter()).toBeCloseTo([250 - 100, 250 - 100, 0]);\n    expect(graph.getCanvasByViewport([0, 0])).toBeCloseTo([-100, -100, 0]);\n    expect(graph.getViewportByCanvas([0, 0])).toBeCloseTo([100, 100, 0]);\n  });\n\n  it('getViewportSize', async () => {\n    await graph.zoomTo(0.5);\n    const bbox = new AABB();\n    bbox.setMinMax([0, 0, 0], [100, 100, 0]);\n\n    // @ts-expect-error\n    expect(graph.context.viewport.getBBoxInViewport(bbox).halfExtents).toBeCloseTo([25, 25, 0]);\n\n    await graph.zoomTo(1);\n    // @ts-expect-error\n    expect(graph.context.viewport.getBBoxInViewport(bbox).halfExtents).toBeCloseTo([50, 50, 0]);\n\n    await graph.zoomTo(2);\n    // @ts-expect-error\n    expect(graph.context.viewport.getBBoxInViewport(bbox).halfExtents).toBeCloseTo([100, 100, 0]);\n  });\n\n  it('isInViewport', async () => {\n    await graph.translateTo([100, 100]);\n    // @ts-expect-error\n    expect(graph.context.viewport?.isInViewport([0, 0])).toBe(false);\n    // @ts-expect-error\n    expect(graph.context.viewport?.isInViewport([100, 100])).toBe(true);\n    const bbox = new AABB();\n    bbox.setMinMax([0, 0, 0], [100, 100, 0]);\n    // @ts-expect-error\n    expect(graph.context.viewport?.isInViewport(bbox)).toBe(true);\n  });\n});\n\ndescribe('Viewport Fit without Animation', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(viewportFit);\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default', async () => {\n    await expect(graph).toMatchSnapshot(__filename, 'before-fit');\n  });\n\n  it('focusElement', async () => {\n    await graph.focusElement('1');\n    await expect(graph).toMatchSnapshot(__filename, 'focusElement');\n  });\n\n  it('fitCenter', async () => {\n    await graph.fitCenter();\n    await expect(graph).toMatchSnapshot(__filename, 'fitCenter');\n  });\n\n  it('fitView', async () => {\n    await graph.fitView();\n    await expect(graph).toMatchSnapshot(__filename, 'fitView');\n  });\n\n  it('re focusElement', async () => {\n    await graph.focusElement('1');\n    await expect(graph).toMatchSnapshot(__filename, 're-focusElement');\n  });\n\n  it('re fitCenter', async () => {\n    await graph.fitCenter();\n    await expect(graph).toMatchSnapshot(__filename, 're-fitCenter');\n  });\n});\n\ndescribe('Viewport Fit with Animation', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(viewportFit, { animation: true });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default', async () => {\n    await expect(graph).toMatchSnapshot(__filename, 'before-fit-animation');\n  });\n\n  it('focusElement', async () => {\n    await graph.focusElement('1');\n    await expect(graph).toMatchSnapshot(__filename, 'focusElement-animation');\n  });\n\n  it('fitCenter', async () => {\n    await graph.fitCenter();\n    await expect(graph).toMatchSnapshot(__filename, 'fitCenter-animation');\n  });\n\n  it('fitView', async () => {\n    await graph.fitView();\n    await expect(graph).toMatchSnapshot(__filename, 'fitView-animation');\n  });\n\n  it('re focusElement', async () => {\n    await graph.focusElement('1');\n    await expect(graph).toMatchSnapshot(__filename, 're-focusElement-animation');\n  });\n\n  it('re fitCenter', async () => {\n    await graph.fitCenter();\n    await expect(graph).toMatchSnapshot(__filename, 're-fitCenter-animation');\n  });\n});\n\ndescribe('Viewport Fit with AutoFit and Padding without Animation', () => {\n  let graph: Graph;\n  beforeAll(async () => {\n    graph = await createDemoGraph(viewportFit, {\n      padding: [100, 0, 0, 100],\n      autoFit: 'view',\n    });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default', async () => {\n    await expect(graph).toMatchSnapshot(__filename, 'auto-fit-with-padding');\n  });\n});\n\ndescribe('Viewport Fit with AutoFit and Padding with Animation', () => {\n  let graph: Graph;\n  beforeAll(async () => {\n    graph = await createDemoGraph(viewportFit, {\n      padding: [100, 0, 0, 100],\n      autoFit: 'view',\n      animation: true,\n    });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default', async () => {\n    await expect(graph).toMatchSnapshot(__filename, 'auto-fit-with-padding-animation');\n  });\n});\n\ndescribe('Viewport Fit with lineWidth', () => {\n  let graph: Graph;\n  beforeAll(async () => {\n    graph = await createDemoGraph(viewportFit, { padding: 10 });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('default', async () => {\n    graph.setNode({\n      ...graph.getOptions().node,\n      style: {\n        size: 50,\n        lineWidth: 5,\n        stroke: 'pink',\n        fill: (d: any) => (d.id === '1' ? '#d4414c' : '#2f363d'),\n      },\n    });\n    await graph.draw();\n    await graph.fitView();\n    await expect(graph).toMatchSnapshot(__filename, 'with-lineWidth');\n  });\n});\n\ndescribe('Fit View with no data in graph', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(async (context) => {\n      const graph = new Graph(context);\n      await graph.render();\n      return graph;\n    });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('fitView with no contents in the graph is ignored', () => {\n    // @ts-expect-error context is private.\n    const zoomBeforeFitView = graph.context.viewport?.getZoom();\n    graph.fitView();\n    // @ts-expect-error context is private.\n    expect(graph.context.viewport?.getZoom()).toBe(zoomBeforeFitView);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/spec/behavior.spec.ts",
    "content": "import type { BehaviorOptions } from '@/src';\n\ndescribe('spec behavior', () => {\n  it('behavior', () => {\n    const behavior: BehaviorOptions = ['drag-canvas', 'zoom-canvas', { type: 'unset' }, { type: 'any', anyProps: 1 }];\n\n    expect(behavior).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/spec/canvas.spec.ts",
    "content": "import type { CanvasOptions } from '@/src';\nimport { Renderer } from '@antv/g-canvas';\n\ndescribe('spec canvas', () => {\n  it('canvas', () => {\n    const canvas: CanvasOptions = {\n      width: 100,\n      height: 100,\n      renderer: () => new Renderer(),\n      autoResize: true,\n    };\n\n    expect(canvas).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/spec/data.spec.ts",
    "content": "import type { GraphData } from '@/src';\n\ndescribe('spec data', () => {\n  it('empty data', () => {\n    const data: GraphData = {\n      nodes: [],\n      edges: [],\n    };\n\n    expect(data).toBeTruthy();\n  });\n\n  it('data', () => {\n    const data: GraphData = {\n      nodes: [\n        {\n          id: 'node1',\n          data: {\n            value: 1,\n          },\n          combo: 'combo-1',\n          style: {\n            collapsed: true,\n            fill: 'red',\n          },\n        },\n      ],\n    };\n\n    expect(data).toBeTruthy();\n  });\n\n  it('data with combo', () => {\n    const data: GraphData = {\n      nodes: [\n        {\n          id: 'node1',\n          data: {\n            value: 1,\n          },\n          combo: 'combo-1',\n          collapsed: true,\n          style: {\n            fill: 'red',\n          },\n        },\n      ],\n      combos: [\n        {\n          id: 'combo-1',\n          data: {\n            value: 1,\n          },\n          collapsed: true,\n          style: {\n            fill: 'red',\n          },\n        },\n      ],\n    };\n\n    expect(data).toBeTruthy();\n  });\n\n  it('normal data', () => {\n    const data: GraphData = {\n      nodes: [\n        { id: 'node-1' },\n        { id: 'node-2', data: { value: 1, field: 'A' } },\n        { id: 'node-3', data: { value: 2 }, style: { x: 1, fill: 'red', y: 1, opacity: 0.1 } },\n      ],\n      edges: [\n        {\n          id: 'edge-1',\n          source: 'node-1',\n          target: 'node-2',\n          data: { value: 1, field: 'A' },\n          style: { stroke: 'red' },\n        },\n        { id: 'edge-2', source: 'node-1', target: 'node-3' },\n      ],\n      combos: [\n        { id: 'combo-1' },\n        { id: 'combo-2', data: { value: 1, field: 'A' } },\n        { id: 'combo-3', data: { value: 2 }, style: { x: 1, fill: 'red' } },\n      ],\n    };\n\n    expect(data).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/spec/element/combo.spec.ts",
    "content": "import type { ComboOptions } from '@/src';\n\ndescribe('spec element combo', () => {\n  it('combo 1', () => {\n    const combo: ComboOptions = {\n      style: {\n        comboStyle: (model: any) => model.style?.comboStyle || 'white',\n      },\n      state: {\n        state1: {\n          comboStyle: 'red',\n        },\n      },\n      animation: {\n        enter: 'fade',\n        show: 'fade',\n      },\n    };\n\n    expect(combo).toBeTruthy();\n  });\n\n  it('combo 2', () => {\n    const combo: ComboOptions = {\n      style: {\n        opacity: (data) => data.style?.opacity || 1,\n      },\n      state: {\n        activate: {\n          opacity: 1,\n        },\n      },\n      animation: {\n        enter: [\n          {\n            fields: ['lineWidth'],\n            shape: 'keyShape',\n            duration: 1000,\n          },\n        ],\n      },\n    };\n\n    expect(combo).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/spec/element/edge.spec.ts",
    "content": "import type { EdgeOptions } from '@/src';\n\ndescribe('spec element edge', () => {\n  it('edge 1', () => {\n    const edge: EdgeOptions = {\n      style: {\n        edgeStyle: (model: any) => model.style?.edgeStyle || 'white',\n      },\n      state: {\n        state1: {\n          edgeStyle: 'red',\n        },\n      },\n      animation: {\n        enter: 'fade',\n        show: 'fade',\n      },\n      palette: {\n        type: 'group',\n        color: 'my-palette',\n        invert: true,\n      },\n    };\n\n    expect(edge).toBeTruthy();\n  });\n\n  it('edge 2', () => {\n    const edge: EdgeOptions = {\n      style: {\n        opacity: (data) => data.style?.opacity || 1,\n      },\n      state: {\n        activate: {\n          opacity: 1,\n        },\n      },\n      animation: {\n        enter: [\n          {\n            fields: ['lineWidth'],\n            shape: 'keyShape',\n            duration: 1000,\n          },\n        ],\n      },\n      palette: {\n        type: 'group',\n        color: 'my-palette',\n        invert: true,\n      },\n    };\n\n    expect(edge).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/spec/element/node.spec.ts",
    "content": "import type { NodeOptions } from '@/src';\n\ndescribe('spec element node', () => {\n  it('node 1', () => {\n    const node: NodeOptions = {\n      animation: {\n        enter: [\n          {\n            shape: 'keyShape',\n            fields: ['opacity'],\n            duration: 1000,\n          },\n        ],\n      },\n      style: {\n        x: 1,\n        y: 1,\n      },\n      palette: 'spectral',\n      state: {\n        selected: {\n          any: 1,\n          x: 1,\n        },\n      },\n    };\n\n    expect(node).toBeTruthy();\n  });\n\n  it('node 2', () => {\n    const node: NodeOptions = {\n      style: {\n        nodeStyle: (model: any) => model.style?.nodeStyle || 'white',\n      },\n      state: {\n        state1: {\n          nodeStyle: (data: any) => data.style?.nodeStyle || 'white',\n        },\n      },\n      animation: {\n        enter: 'fade',\n        show: 'fade',\n      },\n      palette: 'spectral',\n    };\n\n    expect(node).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/spec/index.spec.ts",
    "content": "import type { GraphOptions } from '@/src';\nimport { Renderer } from '@antv/g-canvas';\n\ndescribe('spec', () => {\n  it('spec', () => {\n    const options: GraphOptions = {\n      width: 800,\n      height: 600,\n      renderer: () => new Renderer(),\n      devicePixelRatio: 2,\n      autoResize: true,\n      autoFit: 'view',\n      padding: [10, 10],\n      zoom: 1.2,\n      zoomRange: [0.5, 2],\n      data: {\n        nodes: [\n          {\n            id: 'node-1',\n            data: {\n              value: 1,\n            },\n            style: {\n              nodeStyle: 'red',\n            },\n          },\n        ],\n        edges: [],\n        combos: [],\n      },\n      node: {\n        style: {\n          nodeStyle: 'blue',\n        },\n        state: {\n          state1: {\n            nodeStyle: 'green',\n          },\n        },\n        animation: {\n          enter: 'fade',\n        },\n        palette: {\n          type: 'group',\n          field: 'field',\n          color: 'brBG',\n        },\n      },\n      edge: {\n        animation: {\n          enter: [{ fields: ['opacity'], duration: 500, shape: 'keyShape' }],\n        },\n      },\n      theme: 'light',\n      behaviors: ['drag-canvas', 'my-behavior', { type: 'drag-element' }],\n      plugins: ['my-plugin', { type: 'another-plugin', text: 'text', value: 1 }],\n    };\n\n    expect(options).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/spec/layout.spec.ts",
    "content": "import type { LayoutOptions } from '@/src';\n\ndescribe('spec layout', () => {\n  it('layout 1', () => {\n    const layout: LayoutOptions = {\n      type: 'force',\n      linkDistance: 50,\n      maxSpeed: 100,\n      animated: true,\n      clustering: true,\n      nodeClusterBy: 'cluster',\n      clusterNodeStrength: 70,\n    };\n\n    expect(layout).toBeTruthy();\n  });\n\n  it('layout 2', () => {\n    const layout: LayoutOptions = {\n      type: 'antv-dagre',\n      nodesep: 100,\n      ranksep: 70,\n      controlPoints: true,\n    };\n\n    expect(layout).toBeTruthy();\n  });\n\n  it('custom layout', () => {\n    const layout: LayoutOptions = {\n      type: 'any',\n      value: 1,\n    };\n\n    expect(layout).toBeTruthy();\n  });\n\n  it('register', () => {\n    const builtInLayout: LayoutOptions = {\n      type: 'concentric',\n      clockwise: true,\n      height: 100,\n    };\n    expect(builtInLayout).toBeTruthy();\n\n    type RegisterLayout = LayoutOptions;\n\n    const registerLayout1: RegisterLayout = {\n      type: 'layout1',\n      param: 1,\n    };\n    expect(registerLayout1).toBeTruthy();\n\n    const registerLayout2: RegisterLayout = {\n      type: 'layout2',\n      args: true,\n    };\n    expect(registerLayout2).toBeTruthy();\n\n    const pipeLayout: LayoutOptions = [\n      {\n        type: 'force',\n        nodeFilter: (node) => (node.data as { value: number }).value > 1,\n      },\n    ];\n    expect(pipeLayout).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/spec/optimize.spec.ts",
    "content": "describe('spec optimize', () => {\n  it('optimize 1', () => {\n    expect(1).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/spec/plugin.spec.ts",
    "content": "import type { PluginOptions } from '@/src';\n\ndescribe('spec plugin', () => {\n  it('plugin', () => {\n    const plugin: PluginOptions = ['minimap', { type: 'unset', key: '1' }];\n\n    expect(plugin).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/spec/theme.spec.ts",
    "content": "import type { Graph, ThemeOptions } from '@/src';\nimport { theme } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('spec theme', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(theme, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('theme', async () => {\n    const theme: ThemeOptions = 'light';\n\n    expect(theme).toBeTruthy();\n  });\n\n  it('palette', async () => {\n    graph.setOptions({\n      node: {\n        palette: {\n          type: 'group',\n          field: 'cluster',\n          color: 'spectral',\n        },\n      },\n    });\n    graph.render();\n\n    await expect(graph).toMatchSnapshot(__filename, 'theme_node_palette_spectral');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/spec/viewport.spec.ts",
    "content": "import type { ViewportOptions } from '@/src';\n\ndescribe('spec viewport', () => {\n  it('viewport 1', () => {\n    const viewport: ViewportOptions = {\n      autoFit: 'view',\n      padding: 0,\n      zoom: 1,\n      zoomRange: [0.5, 2],\n    };\n\n    expect(viewport).toBeTruthy();\n  });\n\n  it('viewport 2', () => {\n    const viewport: ViewportOptions = {\n      autoFit: {\n        type: 'center',\n        animation: {\n          duration: 1000,\n        },\n      },\n      padding: [10, 10],\n      zoom: 0.5,\n    };\n\n    expect(viewport).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/themes/base.spec.ts",
    "content": "import { create } from '@/src/themes/base';\n\ndescribe('base', () => {\n  it('create', () => {\n    expect(\n      create({\n        bgColor: '#ffffff',\n        comboColor: '#99ADD1',\n        comboColorDisabled: '#f0f0f0',\n        comboStroke: '#99add1',\n        comboStrokeDisabled: '#d9d9d9',\n        edgeColor: '#99add1',\n        edgeColorDisabled: '#d9d9d9',\n        edgeColorInactive: '#1B324F',\n        nodeColor: '#1783ff',\n        nodeColorDisabled: '#1B324F',\n        nodeStroke: '#000000',\n        textColor: '#000000',\n      }),\n    ).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/transforms/base-transform.spec.ts",
    "content": "import { BaseTransform } from '@/src/transforms/base-transform';\n\ndescribe('BaseTransform', () => {\n  it('beforeDraw', () => {\n    class Transform extends BaseTransform {}\n\n    const baseTransform = new Transform({} as any, {});\n    expect(baseTransform.beforeDraw({} as any, {})).toEqual({});\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/transforms/transform-map-node-size.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { transformMapNodeSize } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\nconst nodeSizeMap = (graph: Graph) =>\n  Object.fromEntries(graph.getNodeData().map((node) => [node.id, node.style?.size]));\n\ndescribe('transform map node size', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(transformMapNodeSize, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('centrality', async () => {\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.updateTransform({\n      key: 'map-node-size',\n      centrality: { type: 'degree' },\n      minSize: 10,\n      maxSize: 40,\n      scale: 'linear',\n    });\n    await graph.draw();\n\n    expect(nodeSizeMap(graph)).toEqual({\n      'node-1': [40, 40, 40],\n      'node-2': [10, 10, 10],\n      'node-3': [10, 10, 10],\n      'node-4': [25, 25, 25],\n      'node-5': [10, 10, 10],\n    });\n\n    graph.updateTransform({\n      key: 'map-node-size',\n      centrality: { type: 'betweenness' },\n    });\n    await graph.draw();\n\n    expect(nodeSizeMap(graph)).toEqual({\n      'node-1': [40, 40, 40],\n      'node-2': [10, 10, 10],\n      'node-3': [10, 10, 10],\n      'node-4': [28, 28, 28],\n      'node-5': [10, 10, 10],\n    });\n\n    graph.updateTransform({\n      key: 'map-node-size',\n      centrality: { type: 'pagerank' },\n    });\n    await graph.draw();\n\n    expect(nodeSizeMap(graph)['node-1']).toEqual([10, 10, 10]);\n    expect(nodeSizeMap(graph)['node-5']).toEqual([40, 40, 40]);\n\n    graph.updateTransform({\n      key: 'map-node-size',\n      centrality: { type: 'eigenvector' },\n    });\n    await graph.draw();\n\n    expect(nodeSizeMap(graph)).toEqual({\n      'node-1': [40, 40, 40],\n      'node-2': [10, 10, 10],\n      'node-3': [10, 10, 10],\n      'node-4': [25, 25, 25],\n      'node-5': [10, 10, 10],\n    });\n\n    graph.updateTransform({\n      key: 'map-node-size',\n      centrality: { type: 'eigenvector', directed: true },\n    });\n    await graph.draw();\n\n    expect(nodeSizeMap(graph)).toEqual({\n      'node-1': [40, 40, 40],\n      'node-2': [10, 10, 10],\n      'node-3': [10, 10, 10],\n      'node-4': [20, 20, 20],\n      'node-5': [10, 10, 10],\n    });\n\n    graph.updateTransform({\n      key: 'map-node-size',\n      centrality: { type: 'closeness' },\n      minSize: 10,\n      maxSize: 50,\n    });\n    await graph.draw();\n\n    expect(nodeSizeMap(graph)).toEqual({\n      'node-1': [50, 50, 50],\n      'node-2': [16.25, 16.25, 16.25],\n      'node-3': [16.25, 16.25, 16.25],\n      'node-4': [35, 35, 35],\n      'node-5': [10, 10, 10],\n    });\n  });\n\n  it('multiple scale types', async () => {\n    graph.updateTransform({\n      key: 'map-node-size',\n      centrality: { type: 'degree' },\n      minSize: 10,\n      maxSize: 40,\n      scale: 'pow',\n    });\n    await graph.draw();\n\n    expect(nodeSizeMap(graph)).toEqual({\n      'node-1': [40, 40, 40],\n      'node-2': [10, 10, 10],\n      'node-3': [10, 10, 10],\n      'node-4': [17.5, 17.5, 17.5],\n      'node-5': [10, 10, 10],\n    });\n\n    graph.updateTransform({\n      key: 'map-node-size',\n      centrality: { type: 'degree' },\n      minSize: 10,\n      maxSize: 40,\n      scale: 'sqrt',\n    });\n    await graph.draw();\n\n    expect(nodeSizeMap(graph)).toEqual({\n      'node-1': [40, 40, 40],\n      'node-2': [10, 10, 10],\n      'node-3': [10, 10, 10],\n      'node-4': [10 + 30 * Math.sqrt(0.5), 10 + 30 * Math.sqrt(0.5), 10 + 30 * Math.sqrt(0.5)],\n      'node-5': [10, 10, 10],\n    });\n\n    graph.updateTransform({\n      key: 'map-node-size',\n      centrality: { type: 'degree' },\n      minSize: 10,\n      maxSize: 40,\n      scale: 'none',\n    });\n    await graph.draw();\n\n    expect(nodeSizeMap(graph)).toEqual({\n      'node-1': [10, 10, 10],\n      'node-2': [10, 10, 10],\n      'node-3': [10, 10, 10],\n      'node-4': [10, 10, 10],\n      'node-5': [10, 10, 10],\n    });\n\n    graph.updateTransform({\n      key: 'map-node-size',\n      centrality: { type: 'degree' },\n      minSize: 10,\n      maxSize: 40,\n      scale: (value: number, domain: [number, number], range: [number, number]) => {\n        const [d0, d1] = domain;\n        const [r0, r1] = range;\n        return r0 + ((value - d0) / (d1 - d0)) * (r1 - r0);\n      },\n    });\n    await graph.draw();\n\n    expect(nodeSizeMap(graph)).toEqual({\n      'node-1': [40, 40, 40],\n      'node-2': [10, 10, 10],\n      'node-3': [10, 10, 10],\n      'node-4': [25, 25, 25],\n      'node-5': [10, 10, 10],\n    });\n  });\n\n  it('sync to label size', async () => {\n    graph.updateTransform({\n      key: 'map-node-size',\n      centrality: { type: 'degree' },\n      maxSize: 80,\n      minSize: 20,\n      scale: 'log',\n      mapLabelSize: true,\n    });\n    await graph.draw();\n\n    await expect(graph).toMatchSnapshot(__filename, 'label-size');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/transforms/transform-position-radial-labels.spec.ts",
    "content": "import { transformPlaceRadialLabels } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('transform position radial labels', () => {\n  it('render', async () => {\n    const graph = await createDemoGraph(transformPlaceRadialLabels);\n    await expect(graph).toMatchSnapshot(__filename);\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/transforms/transform-process-parallel-edges.spec.ts",
    "content": "import type { Graph } from '@/src';\nimport { getParallelEdges, groupByEndpoints, isParallelEdges } from '@/src/transforms/process-parallel-edges';\nimport { transformProcessParallelEdges } from '@@/demos';\nimport { createDemoGraph } from '@@/utils';\n\ndescribe('transform-process-parallel-edges', () => {\n  let graph: Graph;\n\n  beforeAll(async () => {\n    graph = await createDemoGraph(transformProcessParallelEdges, { animation: false });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('mode', async () => {\n    await expect(graph).toMatchSnapshot(__filename, 'merge-mode');\n    graph.updateTransform({ key: 'process-parallel-edges', mode: 'bundle' });\n    graph.draw();\n    await expect(graph).toMatchSnapshot(__filename, 'bundle-mode');\n\n    await expect(graph).toMatchSnapshot(__filename, 'bundle-add-orange-edge__before');\n    graph.addEdgeData([\n      {\n        id: 'new-edge',\n        source: 'node1',\n        target: 'node4',\n        style: { stroke: '#FF9800', lineWidth: 2 },\n      },\n      {\n        id: 'new-loop',\n        source: 'node5',\n        target: 'node5',\n        style: { stroke: '#FF9800', lineWidth: 2 },\n      },\n    ]);\n    graph.draw();\n    await expect(graph).toMatchSnapshot(__filename, 'bundle-add-orange-edge__after');\n\n    await expect(graph).toMatchSnapshot(__filename, 'bundle-update-orange-edge__before');\n    graph.updateEdgeData([{ id: 'new-edge', source: 'node1', target: 'node6' }]);\n    graph.draw();\n    await expect(graph).toMatchSnapshot(__filename, 'bundle-update-orange-edge__after');\n\n    await expect(graph).toMatchSnapshot(__filename, 'bundle-remove-purple-edge__before');\n    graph.removeEdgeData(['edge1', 'loop1']);\n    graph.draw();\n    await expect(graph).toMatchSnapshot(__filename, 'bundle-remove-purple-edge__after');\n  });\n\n  it('isParallelEdges', () => {\n    expect(\n      isParallelEdges(\n        { source: 'node1', target: 'node2', style: { sourceNode: 'node1', targetNode: 'node2' } },\n        { source: 'node2', target: 'node1', style: { sourceNode: 'node2', targetNode: 'node1' } },\n      ),\n    ).toBe(true);\n    expect(\n      isParallelEdges(\n        { source: 'node1', target: 'node2', style: { sourceNode: 'node1', targetNode: 'node2' } },\n        { source: 'node1', target: 'node2', style: { sourceNode: 'node1', targetNode: 'node2' } },\n      ),\n    ).toBe(true);\n    expect(\n      isParallelEdges(\n        { source: 'node1', target: 'node2', style: { sourceNode: 'node1', targetNode: 'node2' } },\n        { source: 'node2', target: 'node3', style: { sourceNode: 'node2', targetNode: 'node3' } },\n      ),\n    ).toBe(false);\n  });\n\n  it('getParallelEdges', () => {\n    expect(\n      getParallelEdges({ source: 'node1', target: 'node2', style: { sourceNode: 'node1', targetNode: 'node2' } }, [\n        { source: 'node2', target: 'node1', style: { sourceNode: 'node2', targetNode: 'node1' } },\n      ]),\n    ).toEqual([{ source: 'node2', target: 'node1', style: { sourceNode: 'node2', targetNode: 'node1' } }]);\n    expect(\n      getParallelEdges({ source: 'node1', target: 'node2', style: { sourceNode: 'node1', targetNode: 'node2' } }, [\n        { source: 'node1', target: 'node2', style: { sourceNode: 'node1', targetNode: 'node2' } },\n        { source: 'node2', target: 'node3', style: { sourceNode: 'node2', targetNode: 'node3' } },\n      ]),\n    ).toEqual([]);\n  });\n\n  it('groupByEndpoints', () => {\n    expect(groupByEndpoints(new Map())).toEqual({ edgeMap: new Map(), reverses: {} });\n    expect(groupByEndpoints(new Map([['edge1', { source: 'node1', target: 'node2' }]])).edgeMap).toEqual(\n      new Map([['node1-node2', [{ source: 'node1', target: 'node2' }]]]),\n    );\n    const res = groupByEndpoints(\n      new Map([\n        ['edge1', { source: 'node1', target: 'node2' }],\n        ['edge2', { source: 'node2', target: 'node1' }],\n      ]),\n    );\n    expect(res.edgeMap).toEqual(\n      new Map([\n        [\n          'node1-node2',\n          [\n            { source: 'node1', target: 'node2' },\n            { source: 'node2', target: 'node1' },\n          ],\n        ],\n      ]),\n    );\n    expect(res.reverses).toEqual({ 'node2|node1|1': true });\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/anchor.spec.ts",
    "content": "import { parseAnchor } from '@/src/utils/anchor';\nimport { getXYByAnchor } from '@/src/utils/position';\nimport { AABB } from '@antv/g';\n\ndescribe('anchor', () => {\n  it('parseAnchor', () => {\n    expect(parseAnchor([0.5, 0.5])).toEqual([0.5, 0.5]);\n    expect(parseAnchor('0.5 0.5')).toEqual([0.5, 0.5]);\n    expect(parseAnchor('1.8 1.8')).toEqual([0.5, 0.5]);\n  });\n\n  it('getXYByAnchor', () => {\n    const bbox = new AABB();\n    bbox.setMinMax([0, 0, 0], [100, 100, 0]);\n    expect(getXYByAnchor(bbox, [0.5, 0.5])).toEqual([50, 50]);\n    expect(getXYByAnchor(bbox, '0.5 0.5')).toEqual([50, 50]);\n    expect(getXYByAnchor(bbox, [0.25, 0.25])).toEqual([25, 25]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/animation.spec.ts",
    "content": "import type { GraphOptions } from '@/src';\nimport { AnimationEffectTiming } from '@/src';\nimport { STDAnimation } from '@/src/animations/types';\nimport { DEFAULT_ANIMATION_OPTIONS, DEFAULT_ELEMENTS_ANIMATION_OPTIONS } from '@/src/constants';\nimport { register } from '@/src/registry/register';\nimport {\n  createAnimationsProxy,\n  getAnimationOptions,\n  getElementAnimationOptions,\n  inferDefaultValue,\n  preprocessKeyframes,\n} from '@/src/utils/animation';\nimport type { IAnimation } from '@antv/g';\n\ndescribe('animation', () => {\n  it('createAnimationsProxy', () => {\n    const sourcePause = jest.fn();\n    const targetPause = jest.fn();\n    const source = {\n      currentTime: 0,\n      pause: () => sourcePause(),\n    } as IAnimation;\n    const targets = [\n      { currentTime: 0, pause: () => targetPause() },\n      { currentTime: 0, pause: () => targetPause() },\n    ] as IAnimation[];\n\n    const proxy = createAnimationsProxy(source, targets);\n\n    expect(proxy.currentTime).toBe(0);\n    proxy.currentTime = 100;\n\n    expect(source.currentTime).toBe(100);\n    expect(targets[0].currentTime).toBe(100);\n    expect(targets[1].currentTime).toBe(100);\n\n    proxy.pause();\n\n    expect(sourcePause).toHaveBeenCalledTimes(1);\n    expect(targetPause).toHaveBeenCalledTimes(2);\n\n    expect(createAnimationsProxy([])).toBe(null);\n\n    const proxy2 = createAnimationsProxy([source, ...targets])!;\n    proxy2.currentTime = 200;\n    expect(proxy2.currentTime).toBe(200);\n    expect(source.currentTime).toBe(200);\n    expect(targets[0].currentTime).toBe(200);\n    expect(targets[1].currentTime).toBe(200);\n\n    proxy2.pause();\n    expect(sourcePause).toHaveBeenCalledTimes(2);\n    expect(targetPause).toHaveBeenCalledTimes(4);\n  });\n\n  it('preprocessKeyframes', () => {\n    expect(\n      preprocessKeyframes([\n        { fill: 'red', opacity: 0, stroke: 1, lineWidth: 0, lineDash: undefined, startPoint: [0, 0, 0] },\n        { fill: 'blue', opacity: 1, lineWidth: 0, lineDash: undefined, startPoint: [0, 0, 0] },\n      ]),\n    ).toEqual([\n      { fill: 'red', opacity: 0 },\n      { fill: 'blue', opacity: 1 },\n    ]);\n  });\n\n  it('inferDefaultValue', () => {\n    expect(inferDefaultValue('x')).toBe(0);\n    expect(inferDefaultValue('y')).toBe(0);\n    expect(inferDefaultValue('z')).toBe(0);\n    expect(inferDefaultValue('opacity')).toBe(1);\n    expect(inferDefaultValue('stroke')).toBe(undefined);\n    expect(inferDefaultValue('visibility')).toBe('visible');\n    expect(inferDefaultValue('collapsed')).toBe(false);\n    expect(inferDefaultValue('states')).toEqual([]);\n  });\n\n  it('getAnimation', () => {\n    expect(getAnimationOptions({}, false)).toBe(false);\n    expect(getAnimationOptions({ animation: false }, true)).toBe(false);\n    expect(getAnimationOptions({}, true)).toEqual(DEFAULT_ANIMATION_OPTIONS);\n\n    expect(getAnimationOptions({ animation: { duration: 1000 } }, true)).toEqual({\n      ...DEFAULT_ANIMATION_OPTIONS,\n      duration: 1000,\n    });\n\n    expect(getAnimationOptions({ animation: { duration: 1000 } }, { duration: 500, easing: 'linear' })).toEqual({\n      ...DEFAULT_ANIMATION_OPTIONS,\n      duration: 500,\n      easing: 'linear',\n    });\n  });\n\n  it('getElementAnimationOptions', () => {\n    // global, element, local => result\n    // total 2 * 3 * 3 = 18 cases\n    const animations: [\n      GraphOptions['animation'],\n      undefined | false | AnimationEffectTiming,\n      undefined | false | AnimationEffectTiming,\n      false | STDAnimation,\n    ][] = [\n      [false, false, false, []],\n      [false, false, undefined, []],\n      [false, undefined, false, []],\n      [false, undefined, undefined, []],\n      [undefined, false, false, []],\n      [undefined, false, undefined, []],\n      [undefined, undefined, false, []],\n      [undefined, undefined, undefined, []],\n      [{ duration: 1000 }, undefined, undefined, []],\n      [false, undefined, undefined, []],\n      [undefined, { duration: 1000 }, undefined, [{ ...DEFAULT_ELEMENTS_ANIMATION_OPTIONS, fields: [] }]],\n      [\n        { duration: 1000 },\n        { duration: 500 },\n        undefined,\n        [{ ...DEFAULT_ELEMENTS_ANIMATION_OPTIONS, duration: 500, fields: [] }],\n      ],\n      [\n        { duration: 1000 },\n        { duration: 500 },\n        { duration: 200 },\n        [{ ...DEFAULT_ELEMENTS_ANIMATION_OPTIONS, duration: 200, fields: [] }],\n      ],\n      [{ duration: 1000 }, { duration: 500 }, false, []],\n      [{ duration: 1000 }, false, { duration: 200 }, []],\n      [false, { duration: 500 }, { duration: 200 }, []],\n      [false, { duration: 500 }, false, []],\n      [{ duration: 1000 }, false, false, []],\n      [true, undefined, undefined, []],\n      [true, { duration: 500 }, undefined, [{ ...DEFAULT_ELEMENTS_ANIMATION_OPTIONS, duration: 500, fields: [] }]],\n      [\n        true,\n        { duration: 500 },\n        { duration: 200 },\n        [{ ...DEFAULT_ELEMENTS_ANIMATION_OPTIONS, duration: 200, fields: [] }],\n      ],\n      [true, false, { duration: 200 }, []],\n    ];\n\n    const stage = 'update' as const;\n    const elementType = 'node' as const;\n    for (const [global, element, local, result] of animations) {\n      expect(\n        getElementAnimationOptions(\n          {\n            animation: global,\n            [elementType]: {\n              animation: {\n                ...(element === false\n                  ? { [stage]: false }\n                  : element === undefined\n                    ? {}\n                    : { [stage]: [{ ...element, fields: [] }] }),\n              },\n            },\n          },\n          elementType,\n          stage,\n          local,\n        ),\n      ).toEqual(result);\n    }\n  });\n\n  it('getElementAnimationOptions in theme', () => {\n    const stage = 'update' as const;\n\n    register('theme', 'test-element-animation', {\n      node: { animation: { [stage]: false } },\n      edge: { animation: false },\n      combo: {\n        animation: { [stage]: [{ fields: ['d', 'stroke'], shape: 'key', duration: 2000 }] },\n      },\n    });\n\n    expect(getElementAnimationOptions({ theme: 'test-element-animation' }, 'node', stage)).toEqual([]);\n    expect(getElementAnimationOptions({ theme: 'test-element-animation' }, 'edge', stage)).toEqual([]);\n    expect(getElementAnimationOptions({ theme: 'test-element-animation' }, 'combo', stage)).toEqual([\n      { ...DEFAULT_ELEMENTS_ANIMATION_OPTIONS, fields: ['d', 'stroke'], shape: 'key', duration: 2000 },\n    ]);\n  });\n\n  it('getElementAnimationOptions mixin', () => {\n    const stage = 'update' as const;\n\n    register('theme', 'test-element-animation-mixin', {\n      node: { animation: { [stage]: false } },\n      edge: { animation: false },\n      combo: {\n        animation: { [stage]: [{ fields: ['d', 'stroke'], shape: 'key', duration: 2000 }] },\n      },\n    });\n\n    const options = {\n      theme: 'test-element-animation-mixin',\n      node: {\n        animation: {\n          enter: [{ fields: ['x', 'y'], duration: 1000 }],\n        },\n      },\n    };\n\n    expect(getElementAnimationOptions(options, 'node', stage)).toEqual([]);\n    expect(getElementAnimationOptions(options, 'node', 'enter')).toEqual([\n      { ...DEFAULT_ELEMENTS_ANIMATION_OPTIONS, fields: ['x', 'y'], duration: 1000 },\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/array.spec.ts",
    "content": "import { deduplicate } from '@/src/utils/array';\n\ndescribe('array', () => {\n  it('deduplicate', () => {\n    expect(deduplicate([1, 2, 3, 4, 5, 1, 2, 3, 4, 5])).toEqual([1, 2, 3, 4, 5]);\n\n    expect(\n      deduplicate(\n        [{ id: 'node-1', data: { value: 1 } }, { id: 'node-2' }, { id: 'node-1', data: { value: 2 } }],\n        (item) => item.id,\n      ),\n    ).toEqual(expect.arrayContaining([{ id: 'node-1', data: { value: 1 } }, { id: 'node-2' }]));\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/bbox.spec.ts",
    "content": "import {\n  getBBoxHeight,\n  getBBoxSize,\n  getBBoxWidth,\n  getCombinedBBox,\n  getExpandedBBox,\n  getIncircleRadius,\n  getNearestBoundaryPoint,\n  getNearestBoundarySide,\n  getNodeBBox,\n  getPointBBox,\n  getTriangleCenter,\n  isBBoxInside,\n  isPointInBBox,\n  isPointOnBBoxBoundary,\n  isPointOutsideBBox,\n} from '@/src/utils/bbox';\nimport { AABB } from '@antv/g';\n\ndescribe('bbox', () => {\n  const bbox = new AABB();\n  bbox.setMinMax([0, 0, 0], [1, 1, 1]);\n\n  it('getBBoxWidth', () => {\n    expect(getBBoxWidth(bbox)).toBe(1);\n  });\n\n  it('getBBoxHeight', () => {\n    expect(getBBoxHeight(bbox)).toBe(1);\n  });\n\n  it('getBBoxSize', () => {\n    expect(getBBoxSize(bbox)).toEqual([1, 1]);\n  });\n\n  it('getNodeBBox', () => {\n    const bbox = new AABB();\n    bbox.setMinMax([10, 10, 0], [10, 10, 0]);\n    expect(getNodeBBox([10, 10, 0])).toEqual(bbox);\n  });\n\n  it('getPointBBox', () => {\n    const pointBBox = new AABB();\n    pointBBox.setMinMax([10, 10, 0], [10, 10, 0]);\n    expect(getPointBBox([10, 10, 0])).toEqual(pointBBox);\n  });\n\n  it('getExpandedBBox', () => {\n    const expandedBBox = new AABB();\n    expandedBBox.setMinMax([-10, -10, 0], [11, 11, 1]);\n    expect(getExpandedBBox(bbox, 10)).toEqual(expandedBBox);\n    expect(getExpandedBBox(bbox, [10, 10, 10, 10])).toEqual(expandedBBox);\n  });\n\n  it('getCombinedBBox', () => {\n    const bbox1 = new AABB();\n    bbox1.setMinMax([0, 0, 0], [1, 1, 1]);\n    const bbox2 = new AABB();\n    bbox2.setMinMax([2, 2, 2], [3, 3, 3]);\n    const bbox3 = new AABB();\n    bbox3.setMinMax([0, 0, 0], [3, 3, 3]);\n    expect(getCombinedBBox([bbox1, bbox2])).toEqual(bbox3);\n    expect(getCombinedBBox([])).toEqual(new AABB());\n  });\n\n  it('isBBoxInside', () => {\n    const bbox1 = new AABB();\n    bbox1.setMinMax([0, 0, 0], [1, 1, 1]);\n    const bbox2 = new AABB();\n    bbox2.setMinMax([0.5, 0.5, 0], [1.5, 1.5, 1]);\n    const bbox3 = new AABB();\n    bbox3.setMinMax([0, 0, 0], [2, 2, 2]);\n    expect(isBBoxInside(bbox1, bbox2)).toBe(false);\n    expect(isBBoxInside(bbox1, bbox3)).toBe(true);\n  });\n\n  it('isPointInBBox', () => {\n    expect(isPointInBBox([0.5, 0.5, 0], bbox)).toBe(true);\n    expect(isPointInBBox([0, 0, 0], bbox)).toBe(true);\n    expect(isPointInBBox([1, 1, 1], bbox)).toBe(true);\n    expect(isPointInBBox([2, 2, 2], bbox)).toBe(false);\n  });\n\n  it('isPointOutsideBBox', () => {\n    expect(isPointOutsideBBox([2, 2, 2], bbox)).toBe(true);\n    expect(isPointOutsideBBox([0.5, 0.5, 0], bbox)).toBe(false);\n  });\n\n  it('isPointOnBBoxBoundary', () => {\n    expect(isPointOnBBoxBoundary([0, 0.5, 0], bbox)).toEqual(true);\n    expect(isPointOnBBoxBoundary([0, 2, 0], bbox)).toEqual(false);\n    expect(isPointOnBBoxBoundary([0, 2, 0], bbox, true)).toEqual(true);\n  });\n\n  it('getNearestBoundarySide', () => {\n    expect(getNearestBoundarySide([0.2, 0.5, 0], bbox)).toBe('left');\n    expect(getNearestBoundarySide([0.5, 0.2, 0], bbox)).toBe('top');\n    expect(getNearestBoundarySide([0.8, 0.5, 0], bbox)).toBe('right');\n    expect(getNearestBoundarySide([0.5, 0.8, 0], bbox)).toBe('bottom');\n  });\n\n  it('getNearestBoundaryPoint', () => {\n    expect(getNearestBoundaryPoint([0.2, 0.5, 0], bbox)).toEqual([0, 0.5, 0]);\n    expect(getNearestBoundaryPoint([0.5, 0.2, 0], bbox)).toEqual([0.5, 0, 0]);\n    expect(getNearestBoundaryPoint([0.8, 0.5, 0], bbox)).toEqual([1, 0.5, 0]);\n    expect(getNearestBoundaryPoint([0.5, 0.8, 0], bbox)).toEqual([0.5, 1, 0]);\n    expect(getNearestBoundaryPoint([1.8, 0.5, 0], bbox)).toEqual([1, 0.5, 0]);\n    expect(getNearestBoundaryPoint([0.5, 1.8, 0], bbox)).toEqual([0.5, 1, 0]);\n  });\n\n  it('getTriangleCenter', () => {\n    expect(getTriangleCenter(bbox, 'left')).toEqual([2 / 3, 0.5]);\n    expect(getTriangleCenter(bbox, 'right')).toEqual([0.33333333333333337, 0.5]);\n    expect(getTriangleCenter(bbox, 'up')).toEqual([0.5, 2 / 3]);\n    expect(getTriangleCenter(bbox, 'down')).toEqual([0.5, 0.33333333333333337]);\n  });\n\n  it('getIncircleRadius', () => {\n    expect(getIncircleRadius(bbox, 'left')).toEqual(0.3090169943749474);\n    expect(getIncircleRadius(bbox, 'right')).toEqual(0.3090169943749474);\n    expect(getIncircleRadius(bbox, 'up')).toEqual(0.3090169943749474);\n    expect(getIncircleRadius(bbox, 'down')).toEqual(0.3090169943749474);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/cache.spec.ts",
    "content": "import { cacheStyle, getCachedStyle, hasCachedStyle, setCacheStyle } from '@/src/utils/cache';\nimport { Circle } from '@antv/g';\n\ndescribe('cache', () => {\n  it('cacheStyle and getCachedStyle and setCacheStyle', () => {\n    const circle = new Circle({\n      style: {\n        r: 10,\n        fill: 'red',\n        stroke: 'blue',\n      },\n    });\n\n    expect(hasCachedStyle(circle, 'fill')).toBe(false);\n\n    cacheStyle(circle, ['fill', 'stroke']);\n\n    circle.style.fill = 'green';\n\n    expect(hasCachedStyle(circle, 'fill')).toBe(true);\n\n    expect(getCachedStyle(circle, 'fill')).toBe('red');\n\n    setCacheStyle(circle, 'fill', 'yellow');\n\n    expect(getCachedStyle(circle, 'fill')).toBe('yellow');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/change.spec.ts",
    "content": "import { reduceDataChanges } from '@/src/utils/change';\n\ndescribe('change', () => {\n  it('reduceDataChanges', () => {\n    expect(\n      reduceDataChanges([\n        { type: 'NodeAdded', value: { id: 'node-0' } },\n        { type: 'NodeAdded', value: { id: 'node-1' } },\n        { type: 'NodeAdded', value: { id: 'node-2' } },\n        { type: 'EdgeAdded', value: { source: 'node-1', target: 'edge-2' } },\n        {\n          type: 'NodeUpdated',\n          value: { id: 'node-3', style: { fill: 'pink' } },\n          original: { id: 'node-3', style: { fill: 'red' } },\n        },\n        {\n          type: 'NodeUpdated',\n          value: { id: 'node-3', style: { fill: 'purple', lineWidth: 2 } },\n          original: { id: 'node-3', style: { fill: 'pink' } },\n        },\n        { type: 'NodeUpdated', value: { id: 'node-0', data: { value: 10 } }, original: { id: 'node-0' } },\n        { type: 'NodeRemoved', value: { id: 'node-0' } },\n      ]),\n    ).toEqual([\n      { type: 'NodeAdded', value: { id: 'node-1' } },\n      { type: 'NodeAdded', value: { id: 'node-2' } },\n      { type: 'EdgeAdded', value: { source: 'node-1', target: 'edge-2' } },\n      {\n        type: 'NodeUpdated',\n        value: { id: 'node-3', style: { fill: 'purple', lineWidth: 2 } },\n        original: { id: 'node-3', style: { fill: 'red' } },\n      },\n    ]);\n  });\n\n  it('reduceDataChanges with Updated and Removed', () => {\n    expect(\n      reduceDataChanges([\n        {\n          type: 'NodeUpdated',\n          value: { id: 'node-3', style: { fill: 'pink' } },\n          original: { id: 'node-3', style: { fill: 'red' } },\n        },\n        { type: 'NodeRemoved', value: { id: 'node-3' } },\n      ]),\n    ).toEqual([{ type: 'NodeRemoved', value: { id: 'node-3' } }]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/collapsibility.spec.ts",
    "content": "import { isCollapsed } from '@/src/utils/collapsibility';\n\ndescribe('collapsibility', () => {\n  it('isCollapsed', () => {\n    expect(isCollapsed({ id: 'xxx' })).toBe(false);\n    expect(isCollapsed({ id: 'xxx', style: { collapsed: true } })).toBe(true);\n    expect(isCollapsed({ id: 'xxx', style: { collapsed: false } })).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/contextmenu.spec.ts",
    "content": "import { getContentFromItems } from '@/src/plugins/contextmenu/util';\n\ndescribe('contextmenu', () => {\n  it('getContentFromItems', () => {\n    expect(\n      getContentFromItems([\n        { name: 'expand', value: 'expand' },\n        { name: 'collapse', value: 'collapse' },\n      ]),\n    ).toEqual(`\n    <ul class=\"g6-contextmenu-ul\">\n      <li  class=\"g6-contextmenu-li\" value=\"expand\">expand</li><li  class=\"g6-contextmenu-li\" value=\"collapse\">collapse</li>\n    </ul>\n  `);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/data.spec.ts",
    "content": "import type { EdgeData, NodeData } from '@/src';\nimport { cloneElementData, isElementDataEqual, isEmptyData, mergeElementsData } from '@/src/utils/data';\n\ndescribe('data', () => {\n  it('mergeElementsData', () => {\n    const originalData: NodeData = {\n      id: 'node-1',\n      data: {\n        value1: 100,\n        value2: { value3: 300, value4: 400 },\n      },\n      style: {\n        fill: 'red',\n        badges: [\n          { text: 'badge1', stroke: 'red' },\n          { text: 'badge2', stroke: 'blue' },\n        ],\n      },\n    };\n\n    const modifiedData: NodeData = {\n      id: 'node-1',\n      data: {\n        value1: 200,\n        value2: { value3: 300 },\n      },\n      style: {\n        badges: [\n          { text: 'badge2', stroke: 'blue' },\n          { text: 'badge3', stroke: 'green' },\n        ],\n        stroke: 'pink',\n      },\n    };\n\n    expect(mergeElementsData(originalData, modifiedData)).toEqual({\n      id: 'node-1',\n      data: {\n        value1: 200,\n        value2: { value3: 300 },\n      },\n      style: {\n        fill: 'red',\n        badges: [\n          { text: 'badge2', stroke: 'blue' },\n          { text: 'badge3', stroke: 'green' },\n        ],\n        stroke: 'pink',\n      },\n    });\n  });\n\n  it('mergeElementsData edge', () => {\n    const originalData: EdgeData = {\n      id: 'edge-1',\n      source: 'node-1',\n      target: 'node-2',\n    };\n\n    const modifiedData: EdgeData = {\n      id: 'edge-1',\n      source: 'node-2',\n      target: 'node-1',\n      data: {\n        weight: 10,\n      },\n    };\n\n    expect(mergeElementsData(originalData, modifiedData)).toEqual({\n      id: 'edge-1',\n      source: 'node-2',\n      target: 'node-1',\n      data: {\n        weight: 10,\n      },\n    });\n  });\n\n  it('cloneElementData', () => {\n    const data = { id: 'node-1', data: { value1: { a: 1 }, value2: 2 }, style: { fill: 'pink', startPoint: [0, 100] } };\n    const clonedData = cloneElementData(data);\n    expect(clonedData).toEqual(data);\n\n    data.data.value1.a = 2;\n    data.style.startPoint[0] = 100;\n    expect(clonedData).toEqual(data);\n\n    data.data.value2 = 3;\n    data.style.startPoint = [100, 100];\n    expect(clonedData).not.toEqual(data);\n  });\n\n  it('isEmptyData', () => {\n    expect(isEmptyData({})).toBe(true);\n    expect(isEmptyData({ nodes: [] })).toBe(true);\n    expect(isEmptyData({ nodes: [], edges: [] })).toBe(true);\n    expect(isEmptyData({ nodes: [], edges: [], combos: [] })).toBe(true);\n    expect(isEmptyData({ nodes: [{ id: 'node-1' }] })).toBe(false);\n    expect(isEmptyData({ edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }] })).toBe(false);\n    expect(isEmptyData({ combos: [{ id: 'combo-1' }] })).toBe(false);\n  });\n\n  it('isElementDataEqual', () => {\n    expect(isElementDataEqual({ id: 'node-1' }, { id: 'node-1' })).toBe(true);\n    expect(isElementDataEqual({ id: 'node-1' }, { id: 'node-2' })).toBe(false);\n\n    // children\n    expect(isElementDataEqual({ id: 'node-1', children: ['a', 'b'] }, { id: 'node-1', children: ['a', 'b'] })).toBe(\n      true,\n    );\n    expect(isElementDataEqual({ id: 'node-1', children: ['a', 'b'] }, { id: 'node-1', children: ['a', 'c'] })).toBe(\n      false,\n    );\n    expect(\n      isElementDataEqual({ id: 'node-1', children: ['a', 'b'] }, { id: 'node-1', children: ['a', 'b', 'c'] }),\n    ).toBe(false);\n    expect(isElementDataEqual({ id: 'node-1' }, { id: 'node-1', data: {} })).toBe(true);\n    expect(isElementDataEqual({ id: 'node-1', data: { value: 1 } }, { id: 'node-1', data: { value: 1 } })).toBe(true);\n\n    // states\n    expect(isElementDataEqual({ id: 'node-1' }, { id: 'node-1', states: [] })).toBe(true);\n    expect(isElementDataEqual({ id: 'node-1', states: [] }, { id: 'node-1', states: [] })).toBe(true);\n    expect(isElementDataEqual({ id: 'node-1', states: ['selected'] }, { id: 'node-1', states: ['selected'] })).toBe(\n      true,\n    );\n    expect(\n      isElementDataEqual({ id: 'node-1', states: ['selected'] }, { id: 'node-1', states: ['selected', 'hover'] }),\n    ).toBe(false);\n\n    // too deep\n    const obj = { a: 1 };\n    expect(\n      isElementDataEqual({ id: 'node-1', data: { value: { ...obj } } }, { id: 'node-1', data: { value: { ...obj } } }),\n    ).toBe(false);\n    expect(isElementDataEqual({ id: 'node-1', data: { value: obj } }, { id: 'node-1', data: { value: obj } })).toBe(\n      true,\n    );\n\n    // style\n    expect(isElementDataEqual({ id: 'node-1' }, { id: 'node-1', style: {} })).toBe(true);\n    expect(isElementDataEqual({ id: 'node-1', style: { fill: 'red' } }, { id: 'node-1', style: { fill: 'red' } })).toBe(\n      true,\n    );\n    expect(\n      isElementDataEqual({ id: 'node-1', style: { fill: 'red' } }, { id: 'node-1', style: { fill: 'blue' } }),\n    ).toBe(false);\n    expect(\n      isElementDataEqual(\n        { id: 'node-1', style: { fill: 'red' } },\n        { id: 'node-1', style: { fill: 'red', stroke: 'red' } },\n      ),\n    ).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/diff.spec.ts",
    "content": "import { arrayDiff } from '@/src/utils/diff';\n\ndescribe('diff', () => {\n  const key = (d: { id: string }) => d.id;\n\n  it('array diff simple', () => {\n    const original = [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }];\n    const modified = [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-4' }];\n\n    const { enter, update, exit, keep } = arrayDiff(original, modified, key);\n\n    expect(enter).toEqual([{ id: 'node-4' }]);\n    expect(update).toEqual([]);\n    expect(exit).toEqual([{ id: 'node-3' }]);\n    expect(keep).toEqual([{ id: 'node-1' }, { id: 'node-2' }]);\n  });\n\n  it('array diff', () => {\n    const original = [\n      { id: 'node-1' },\n      { id: 'node-2', data: { value: 1 } },\n      { id: 'node-3', data: { value: 2 }, style: { fill: 'red' } },\n    ];\n\n    const modified = [\n      { id: 'node-1' },\n      { id: 'node-2', data: { value: 2 } },\n      { id: 'node-4', data: { value: 3 }, style: { fill: 'red' } },\n    ];\n\n    const { enter, update, exit, keep } = arrayDiff(original, modified, key);\n\n    expect(enter).toEqual([{ id: 'node-4', data: { value: 3 }, style: { fill: 'red' } }]);\n    expect(update).toEqual([{ id: 'node-2', data: { value: 2 } }]);\n    expect(exit).toEqual([{ id: 'node-3', data: { value: 2 }, style: { fill: 'red' } }]);\n    expect(keep).toEqual([{ id: 'node-1' }]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/dom.spec.ts",
    "content": "import { sizeOf } from '@/src/utils/dom';\n\ndescribe('dom', () => {\n  it('should return the size of the graph container', () => {\n    // Create a mock container element\n    const container = document.createElement('div');\n    container.style.width = '500px';\n    container.style.height = '300px';\n\n    // Call the sizeOf function\n    const result = sizeOf(container);\n\n    // Assert the result\n    expect(result).toEqual([500, 300]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/edge.spec.ts",
    "content": "import type { ID } from '@/src';\nimport { Rect } from '@/src/elements';\nimport { Badge, Label } from '@/src/elements/shapes';\nimport {\n  findActualConnectNodeData,\n  getBadgePositionStyle,\n  getCubicPath,\n  getCurveControlPoint,\n  getLabelPositionStyle,\n  getPolylineLoopControlPoints,\n  getPolylinePath,\n  getQuadraticPath,\n  getRadians,\n  getSubgraphRelatedEdges,\n  parseCurveOffset,\n  parseCurvePosition,\n} from '@/src/utils/edge';\nimport { AABB, Line } from '@antv/g';\n\ndescribe('edge', () => {\n  describe('getLabelPositionStyle', () => {\n    it('should return correctly label position style', () => {\n      // horizontal line\n      const line = new Line({\n        style: {\n          x1: 0,\n          y1: 100,\n          x2: 100,\n          y2: 100,\n        },\n      });\n\n      //  with rotation angle below Math.PI\n      const line1 = new Line({\n        style: {\n          x1: 0,\n          y1: 100,\n          x2: 100,\n          y2: 200,\n        },\n      });\n\n      // with rotation angle over Math.PI\n      const line2 = new Line({\n        style: {\n          x1: 0,\n          y1: 200,\n          x2: 100,\n          y2: 100,\n        },\n      });\n\n      const labelPlacement = getLabelPositionStyle(line, 'center', false, 0, 0);\n      expect(labelPlacement.textAlign).toEqual('center');\n      expect(labelPlacement.transform).toEqual([['translate', 50, 100]]);\n\n      const labelPosition2 = getLabelPositionStyle(line, 'center', true, 5, 5);\n      expect(labelPosition2.textAlign).toEqual('center');\n      expect(labelPosition2.transform).toEqual([['translate', 55, 105]]);\n\n      const labelPosition3 = getLabelPositionStyle(line, 'start', true, 5, 5);\n      expect(labelPosition3.textAlign).toEqual('left');\n      expect(labelPosition3.transform).toEqual([['translate', 5, 105]]);\n\n      const labelPosition4 = getLabelPositionStyle(line, 'end', true, 5, 5);\n      expect(labelPosition4.textAlign).toEqual('right');\n      expect(labelPosition4.transform).toEqual([['translate', 104, 105]]);\n\n      const labelPosition5 = getLabelPositionStyle(line, 0.5, true, 5, 5);\n      expect(labelPosition5.textAlign).toEqual('center');\n      expect(labelPosition5.transform).toEqual([['translate', 55, 105]]);\n\n      //  with rotation angle below Math.PI\n      const labelPosition6 = getLabelPositionStyle(line1, 'center', true, 5, 5);\n      expect(labelPosition6.textAlign).toEqual('center');\n      expect(labelPosition6.transform).toEqual([\n        [\n          'translate',\n          50 + 5 * Math.cos(Math.PI / 4) - 5 * Math.sin(Math.PI / 4),\n          150 + 5 * Math.sin(Math.PI / 4) + 5 * Math.cos(Math.PI / 4),\n        ],\n        ['rotate', 45],\n      ]);\n\n      const labelPosition7 = getLabelPositionStyle(line1, 'start', true, 5, 5);\n      expect(labelPosition7.textAlign).toEqual('left');\n\n      const labelPosition8 = getLabelPositionStyle(line1, 'end', true, 5, 5);\n      expect(labelPosition8.textAlign).toEqual('right');\n\n      // with rotation angle over Math.PI\n      const labelPosition9 = getLabelPositionStyle(line2, 'center', true, 5, 5);\n      expect(labelPosition9.textAlign).toEqual('center');\n      expect(labelPosition9.transform).toEqual([\n        [\n          'translate',\n          50 + 5 * Math.cos(-Math.PI / 4) - 5 * Math.sin(-Math.PI / 4),\n          150 + 5 * Math.sin(-Math.PI / 4) + 5 * Math.cos(-Math.PI / 4),\n        ],\n        ['rotate', -45],\n      ]);\n    });\n  });\n\n  it('getBadgePositionStyle', () => {\n    const shapeMap = {\n      key: new Line({ style: { x1: 0, y1: 0, x2: 100, y2: 0 } }),\n      label: new Label({ style: { text: 'label', background: true } }),\n      badge: new Badge({ style: { text: 'badge', background: true } }),\n    };\n    expect(getBadgePositionStyle(shapeMap, 'prefix', 'center', 10, 0)).toEqual({\n      textAlign: 'center',\n      transform: [['translate', 50, 0]],\n    });\n  });\n\n  it('getCurveControlPoint', () => {\n    expect(getCurveControlPoint([0, 0], [100, 0], 0.5, 20)).toEqual([50, -20]);\n    expect(getCurveControlPoint([0, 0], [100, 0], 0.5, -20)).toEqual([50, 20]);\n  });\n\n  it('parseCurveOffset', () => {\n    expect(parseCurveOffset(20)).toEqual([20, -20]);\n    expect(parseCurveOffset([20, 30])).toEqual([20, 30]);\n  });\n\n  it('parseCurvePosition', () => {\n    expect(parseCurvePosition(0.2)).toEqual([0.2, 0.8]);\n    expect(parseCurvePosition([0.2, 0.8])).toEqual([0.2, 0.8]);\n  });\n\n  it('getQuadraticPath', () => {\n    expect(getQuadraticPath([0, 10], [10, 10], [100, 100])).toEqual([\n      ['M', 0, 10],\n      ['Q', 100, 100, 10, 10],\n    ]);\n  });\n\n  it('getCubicPath', () => {\n    expect(\n      getCubicPath(\n        [0, 10],\n        [100, 100],\n        [\n          [20, 20],\n          [50, 50],\n        ],\n      ),\n    ).toEqual([\n      ['M', 0, 10],\n      ['C', 20, 20, 50, 50, 100, 100],\n    ]);\n  });\n\n  it('getPolylinePath', () => {\n    expect(\n      getPolylinePath(\n        [\n          [0, 10],\n          [20, 20],\n          [50, 50],\n          [100, 100],\n        ],\n        0,\n      ),\n    ).toEqual([\n      ['M', 0, 10],\n      ['L', 20, 20],\n      ['L', 50, 50],\n      ['L', 100, 100],\n    ]);\n    expect(\n      getPolylinePath(\n        [\n          [0, 10],\n          [20, 20],\n          [50, 50],\n          [100, 100],\n        ],\n        0,\n        true,\n      ),\n    ).toEqual([['M', 0, 10], ['L', 20, 20], ['L', 50, 50], ['L', 100, 100], ['Z']]);\n    expect(\n      getPolylinePath(\n        [\n          [0, 10],\n          [20, 20],\n          [50, 50],\n          [100, 100],\n        ],\n        10,\n      )[1][1],\n    ).toBeCloseTo(13.33);\n  });\n\n  it('getRadians', () => {\n    const bbox = new AABB();\n    bbox.setMinMax([0, 0, 0], [100, 100, 0]);\n    const EIGHTH_PI = Math.PI / 8;\n    expect(getRadians(bbox).bottom[0]).toBeCloseTo(EIGHTH_PI * 3);\n    expect(getRadians(bbox).top[0]).toBeCloseTo(-EIGHTH_PI * 5);\n  });\n\n  it('getPolylineLoopControlPoints', () => {\n    const node = new Rect({ style: { x: 100, y: 100, size: 100 } });\n    expect(getPolylineLoopControlPoints(node, [150, 100], [150, 100], 10)).toEqual([\n      [160, 100],\n      [160, 110],\n      [150, 110],\n    ]);\n    expect(getPolylineLoopControlPoints(node, [100, 150], [100, 150], 10)).toEqual([\n      [100, 160],\n      [110, 160],\n      [110, 150],\n    ]);\n    expect(getPolylineLoopControlPoints(node, [50, 100], [50, 100], 10)).toEqual([\n      [40, 100],\n      [40, 110],\n      [50, 110],\n    ]);\n    expect(getPolylineLoopControlPoints(node, [100, 50], [100, 50], 10)).toEqual([\n      [100, 40],\n      [110, 40],\n      [110, 50],\n    ]);\n    expect(getPolylineLoopControlPoints(node, [150, 150], [100, 150], 10)).toEqual([\n      [160, 150],\n      [160, 160],\n      [100, 160],\n    ]);\n    expect(getPolylineLoopControlPoints(node, [150, 150], [150, 100], 10)).toEqual([\n      [160, 150],\n      [160, 100],\n    ]);\n    expect(getPolylineLoopControlPoints(node, [120, 50], [140, 50], 10)).toEqual([\n      [120, 40],\n      [140, 40],\n    ]);\n    expect(getPolylineLoopControlPoints(node, [150, 120], [150, 140], 10)).toEqual([\n      [160, 120],\n      [160, 140],\n    ]);\n    expect(getPolylineLoopControlPoints(node, [50, 120], [50, 140], 10)).toEqual([\n      [40, 120],\n      [40, 140],\n    ]);\n  });\n\n  it('getSubgraphRelatedEdges', () => {\n    /**\n     *    1 - 2\n     *   /     \\\n     *  3 - - - 4\n     *   \\  |   /\n     *      5\n     */\n    const data = {\n      nodes: [\n        { id: 'node-1', combo: 'combo-1' },\n        { id: 'node-2', combo: 'combo-1' },\n        { id: 'node-3' },\n        { id: 'node-4' },\n        { id: 'node-5' },\n      ],\n      edges: [\n        { id: 'node-1-node-2', source: 'node-1', target: 'node-2' },\n        { id: 'node-1-node-3', source: 'node-1', target: 'node-3' },\n        { id: 'node-2-node-4', source: 'node-2', target: 'node-4' },\n        { id: 'node-3-node-5', source: 'node-3', target: 'node-5' },\n        { id: 'node-4-node-5', source: 'node-4', target: 'node-5' },\n        { id: 'combo-1-node-5', source: 'combo-1', target: 'node-5' },\n      ],\n      combos: [{ id: 'combo-1' }],\n    };\n\n    const getRelatedEdges = (id: ID) => data.edges.filter((edge) => edge.id.includes(id as string));\n\n    expect(getSubgraphRelatedEdges(['node-1', 'node-2', 'combo-1'], getRelatedEdges)).toEqual({\n      edges: [\n        { id: 'node-1-node-2', source: 'node-1', target: 'node-2' },\n        { id: 'node-1-node-3', source: 'node-1', target: 'node-3' },\n        { id: 'node-2-node-4', source: 'node-2', target: 'node-4' },\n        { id: 'combo-1-node-5', source: 'combo-1', target: 'node-5' },\n      ],\n      external: [\n        { id: 'node-1-node-3', source: 'node-1', target: 'node-3' },\n        { id: 'node-2-node-4', source: 'node-2', target: 'node-4' },\n        { id: 'combo-1-node-5', source: 'combo-1', target: 'node-5' },\n      ],\n      internal: [{ id: 'node-1-node-2', source: 'node-1', target: 'node-2' }],\n    });\n\n    expect(getSubgraphRelatedEdges(['node-3', 'node-5'], getRelatedEdges)).toEqual({\n      edges: [\n        { id: 'node-1-node-3', source: 'node-1', target: 'node-3' },\n        { id: 'node-3-node-5', source: 'node-3', target: 'node-5' },\n        { id: 'node-4-node-5', source: 'node-4', target: 'node-5' },\n        { id: 'combo-1-node-5', source: 'combo-1', target: 'node-5' },\n      ],\n      external: [\n        { id: 'node-1-node-3', source: 'node-1', target: 'node-3' },\n        { id: 'node-4-node-5', source: 'node-4', target: 'node-5' },\n        { id: 'combo-1-node-5', source: 'combo-1', target: 'node-5' },\n      ],\n      internal: [{ id: 'node-3-node-5', source: 'node-3', target: 'node-5' }],\n    });\n  });\n\n  it('findActualConnectNodeData', () => {\n    expect(findActualConnectNodeData({ id: 'node-1' }, () => undefined).id).toBe('node-1');\n    expect(\n      findActualConnectNodeData({ id: 'node-1' }, (id) => {\n        if (id === 'node-1') return { id: 'node-2' };\n        return undefined;\n      }).id,\n    ).toBe('node-1');\n    expect(\n      findActualConnectNodeData({ id: 'node-1' }, (id) => {\n        if (id === 'node-1') return { id: 'node-2', style: { collapsed: true } };\n        if (id === 'node-2') return { id: 'node-3' };\n        return undefined;\n      }).id,\n    ).toBe('node-2');\n    expect(\n      findActualConnectNodeData({ id: 'node-1' }, (id) => {\n        if (id === 'node-1') return { id: 'node-2' };\n        if (id === 'node-2') return { id: 'node-3', style: { collapsed: true } };\n        return undefined;\n      }).id,\n    ).toBe('node-3');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/element.spec.ts",
    "content": "import { Polyline } from '@/src/elements/edges';\nimport { Circle } from '@/src/elements/nodes';\nimport { ID, PortStyleProps } from '@/src/types';\nimport {\n  findPorts,\n  getAllPorts,\n  getBoundingPoints,\n  getHexagonPoints,\n  getPortConnectionPoint,\n  getPortXYByPlacement,\n  getStarPoints,\n  getStarPorts,\n  getTextStyleByPlacement,\n  getTrianglePoints,\n  getTrianglePorts,\n  isEdge,\n  isElement,\n  isNode,\n  isSameNode,\n  isSimplePort,\n  isVisible,\n  updateStyle,\n} from '@/src/utils/element';\nimport { getXYByPlacement } from '@/src/utils/position';\nimport { AABB, DisplayObject, Line, Rect } from '@antv/g';\n\ndescribe('element', () => {\n  const bbox = new AABB();\n  bbox.setMinMax([100, 100, 0], [200, 200, 0]);\n\n  const node1 = new Circle({\n    id: 'node-1',\n  });\n\n  const node2 = new Circle({\n    id: 'node-2',\n  });\n\n  const context: any = {\n    element: {\n      getElement(id: ID) {\n        if (id === 'node-1') return node1;\n        else return node2;\n      },\n    },\n  };\n\n  const edge = new Polyline({ style: { sourceNode: 'node-1', targetNode: 'node-2' }, context });\n\n  it('isNode', () => {\n    const rect = new Rect({ style: { width: 10, height: 10 } });\n    expect(isNode(rect)).toBe(false);\n    expect(isElement(rect)).toBe(false);\n    const node = new Circle({});\n    expect(isNode(node)).toBe(true);\n    expect(isElement(node)).toBe(true);\n  });\n\n  it('isEdge', () => {\n    const line = new Line({ style: { x1: 0, y1: 0, x2: 10, y2: 10 } });\n    expect(isEdge(line)).toBe(false);\n    expect(isElement(line)).toBe(false);\n    expect(isEdge(edge)).toBe(true);\n    expect(isElement(edge)).toBe(true);\n  });\n\n  it('isSameNode', () => {\n    expect(isSameNode(node1, undefined!)).toBeFalsy();\n    expect(isSameNode(node1, node2)).toBeFalsy();\n    expect(isSameNode(node1, node1)).toBeTruthy();\n  });\n\n  it('getXYByPlacement', () => {\n    expect(getXYByPlacement(bbox, 'left')).toEqual([100, 150]);\n    expect(getXYByPlacement(bbox, 'right')).toEqual([200, 150]);\n    expect(getXYByPlacement(bbox, 'top')).toEqual([150, 100]);\n    expect(getXYByPlacement(bbox, 'bottom')).toEqual([150, 200]);\n\n    expect(getXYByPlacement(bbox, 'left-top')).toEqual([100, 100]);\n    expect(getXYByPlacement(bbox, 'right-bottom')).toEqual([200, 200]);\n\n    expect(getXYByPlacement(bbox, 'center')).toEqual([150, 150]);\n\n    expect(getXYByPlacement(bbox)).toEqual([150, 150]);\n  });\n\n  it('getPortXYByPlacement', () => {\n    expect(getPortXYByPlacement(bbox, 'left')).toEqual([100, 150]);\n    expect(getPortXYByPlacement(bbox, 'right')).toEqual([200, 150]);\n    expect(getPortXYByPlacement(bbox, 'top')).toEqual([150, 100]);\n    expect(getPortXYByPlacement(bbox, 'bottom')).toEqual([150, 200]);\n\n    expect(getPortXYByPlacement(bbox)).toEqual([150, 150]);\n\n    expect(getPortXYByPlacement(bbox, [0.5, 1])).toEqual([150, 200]);\n    expect(getPortXYByPlacement(bbox, [0, 0.5])).toEqual([100, 150]);\n  });\n\n  it('getAllPorts', () => {\n    const node = new Circle({\n      style: {\n        x: 0,\n        y: 0,\n        size: 100,\n        port: true,\n        ports: [\n          { key: 'left', placement: [0, 0.5], r: 4 },\n          { key: 'right', placement: [1, 0.5] },\n        ],\n      },\n    });\n    expect(Object.values(getAllPorts(node)).length).toBe(2);\n    expect(getAllPorts(node)['right']).toEqual([50, 0]);\n  });\n\n  it('isSimplePort', () => {\n    expect(\n      isSimplePort({\n        placement: 'left',\n      }),\n    ).toBeTruthy();\n    expect(\n      isSimplePort({\n        placement: 'left',\n        r: 0,\n      }),\n    ).toBeTruthy();\n    expect(\n      isSimplePort({\n        placement: 'left',\n        r: 4,\n      }),\n    ).toBeFalsy();\n  });\n\n  it('findPorts', () => {\n    const sourceNode = new Circle({\n      id: 'source',\n      style: {\n        port: true,\n        ports: [{ key: 'left', placement: [0, 0.5], r: 4 }],\n      },\n    });\n    const targetNode = new Circle({\n      id: 'target',\n      style: {\n        port: true,\n        ports: [{ key: 'top', placement: [0.5, 0], r: 4 }],\n      },\n    });\n    const sourcePortKey = 'left';\n    const targetPortKey = 'top';\n    const [sourcePort, targetPort] = findPorts(sourceNode, targetNode, sourcePortKey, targetPortKey);\n    expect((sourcePort as DisplayObject<PortStyleProps>).className).toEqual('port-left');\n    expect((targetPort as DisplayObject<PortStyleProps>).className).toEqual('port-top');\n  });\n\n  it('getPortConnectionPoint', () => {\n    const node = new Circle({\n      id: 'source',\n      style: {\n        x: 100,\n        y: 100,\n        port: true,\n        ports: [{ key: 'left', placement: [0, 0.5], r: 4 }],\n        portLinkToCenter: true,\n      },\n    });\n    expect(getPortConnectionPoint(node.getPorts()['left'], [0, 0])).toEqual([84, 100, 0]);\n  });\n\n  it('getTextStyleByPlacement', () => {\n    expect(getTextStyleByPlacement(bbox, 'left')).toEqual({\n      transform: [['translate', 100, 150]],\n      textAlign: 'right',\n      textBaseline: 'middle',\n    });\n    expect(getTextStyleByPlacement(bbox, 'right')).toEqual({\n      transform: [['translate', 200, 150]],\n      textAlign: 'left',\n      textBaseline: 'middle',\n    });\n    expect(getTextStyleByPlacement(bbox, 'top')).toEqual({\n      transform: [['translate', 150, 100]],\n      textAlign: 'center',\n      textBaseline: 'bottom',\n    });\n    expect(getTextStyleByPlacement(bbox, 'bottom')).toEqual({\n      transform: [['translate', 150, 200]],\n      textAlign: 'center',\n      textBaseline: 'top',\n    });\n\n    expect(getTextStyleByPlacement(bbox, 'left-top')).toEqual({\n      transform: [['translate', 100, 100]],\n      textAlign: 'right',\n      textBaseline: 'bottom',\n    });\n    expect(getTextStyleByPlacement(bbox, 'right-bottom')).toEqual({\n      transform: [['translate', 200, 200]],\n      textAlign: 'left',\n      textBaseline: 'top',\n    });\n\n    expect(getTextStyleByPlacement(bbox, 'center')).toEqual({\n      transform: [['translate', 150, 150]],\n      textAlign: 'center',\n      textBaseline: 'middle',\n    });\n\n    expect(getTextStyleByPlacement(bbox)).toEqual({\n      transform: [['translate', 150, 200]],\n      textAlign: 'center',\n      textBaseline: 'top',\n    });\n  });\n\n  it('getStarPoints', () => {\n    expect(getStarPoints(32, 16).length).toBe(10);\n  });\n\n  it('getStarPorts', () => {\n    expect(getStarPorts(32, 16).top).toEqual([0, -32]);\n  });\n\n  it('getTrianglePoints', () => {\n    expect(getTrianglePoints(40, 40, 'up').length).toBe(3);\n    expect(getTrianglePoints(40, 40, 'up')).toEqual([\n      [-20, 20],\n      [20, 20],\n      [0, -20],\n    ]);\n  });\n\n  it('getTrianglePorts', () => {\n    const ports = getTrianglePorts(32, 16, 'up');\n    expect(ports.default).toEqual([0, -8]);\n    expect(ports.left).toEqual([-16, 8]);\n    expect(ports.right).toEqual([16, 8]);\n    expect(ports.top).toEqual([0, -8]);\n    expect(ports.bottom).toBeFalsy();\n    expect(getTrianglePorts(32, 16, 'down')).toEqual({\n      default: [0, 8],\n      left: [-16, -8],\n      right: [16, -8],\n      bottom: [0, 8],\n    });\n    expect(getTrianglePorts(32, 16, 'left')).toEqual({\n      default: [-16, 0],\n      top: [16, -8],\n      bottom: [16, 8],\n      left: [-16, 0],\n    });\n    expect(getTrianglePorts(32, 16, 'right')).toEqual({\n      default: [16, 0],\n      top: [-16, -8],\n      bottom: [-16, 8],\n      right: [16, 0],\n    });\n  });\n\n  it('getBoundingPoints', () => {\n    expect(getBoundingPoints(100, 100).length).toBe(4);\n    expect(getBoundingPoints(100, 100)).toEqual([\n      [50, -50],\n      [50, 50],\n      [-50, 50],\n      [-50, -50],\n    ]);\n  });\n\n  it('isVisible', () => {\n    expect(isVisible(new Rect({ style: { width: 50, height: 50 } }))).toBe(true);\n    expect(isVisible(new Rect({ style: { width: 50, height: 50, visibility: 'hidden' } }))).toBe(false);\n    expect(isVisible(new Rect({ style: { width: 50, height: 50, visibility: 'unset' } }))).toBe(true);\n    expect(isVisible(new Rect({ style: { width: 50, height: 50, visibility: 'visible' } }))).toBe(true);\n    expect(isVisible(new Rect({ style: { width: 50, height: 50, visibility: 'inherit' } }))).toBe(true);\n    expect(isVisible(new Rect({ style: { width: 50, height: 50, visibility: 'initial' } }))).toBe(true);\n  });\n\n  it('update', () => {\n    const rect = new Rect({ style: { width: 50, height: 50 } });\n    updateStyle(rect, { width: 100, height: 100 });\n    expect(rect.style.width).toBe(100);\n    expect(rect.style.height).toBe(100);\n\n    const circle = new Circle({ style: { size: 50 } });\n    updateStyle(circle, { size: 100 });\n    expect(circle.style.size).toBe(100);\n  });\n\n  it('getHexagonPoints', () => {\n    expect(getHexagonPoints(32).length).toBe(6);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/event.spec.ts",
    "content": "import type { ID } from '@/src';\nimport { Polyline } from '@/src/elements/edges';\nimport { Circle } from '@/src/elements/nodes';\nimport { eventTargetOf } from '@/src/utils/event';\nimport { Document, Rect } from '@antv/g';\n\ndescribe('event', () => {\n  const node1 = new Circle({\n    id: 'node-1',\n  });\n\n  const node2 = new Circle({ id: 'node-2' });\n\n  const context: any = {\n    element: {\n      getElement(id: ID) {\n        if (id === 'node-1') return node1;\n        else return node2;\n      },\n    },\n  };\n\n  const edge = new Polyline({ style: { sourceNode: 'node-1', targetNode: 'node-2' }, context });\n\n  it('eventTargetOf', () => {\n    expect(eventTargetOf(node1)?.type).toEqual('node');\n    expect(eventTargetOf(edge)?.type).toEqual('edge');\n    expect(eventTargetOf(new Rect({ style: { width: 50, height: 50 } }))).toBeFalsy();\n    expect(eventTargetOf(new Document())?.type).toBe('canvas');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/extension.spec.ts",
    "content": "import { BehaviorOptions, PluginOptions } from '@/src';\nimport { parseExtensions } from '@/src/utils/extension';\n\ndescribe('extension', () => {\n  it('parseBehaviors', () => {\n    expect(parseExtensions({} as any, 'behavior', [])).toEqual([]);\n\n    const options: BehaviorOptions = [\n      'drag-element',\n      { type: 'drag-canvas' },\n      { type: 'shortcut', key: 'shortcut-zoom-in' },\n      { type: 'shortcut', key: 'shortcut-zoom-out' },\n      'scroll-canvas',\n      'scroll-canvas',\n    ];\n\n    expect(parseExtensions({} as any, 'behavior', options)).toEqual([\n      { type: 'drag-element', key: 'behavior-drag-element-0' },\n      { type: 'drag-canvas', key: 'behavior-drag-canvas-0' },\n      { type: 'shortcut', key: 'shortcut-zoom-in' },\n      { type: 'shortcut', key: 'shortcut-zoom-out' },\n      { type: 'scroll-canvas', key: 'behavior-scroll-canvas-0' },\n      { type: 'scroll-canvas', key: 'behavior-scroll-canvas-1' },\n    ]);\n  });\n\n  it('parsePlugins', () => {\n    expect(parseExtensions({} as any, 'plugin', [])).toEqual([]);\n\n    const options: PluginOptions = [\n      'minimap',\n      { key: 'my-tooltip', type: 'tooltip' },\n      { type: 'tooltip' },\n      {\n        type: 'contextmenu',\n        key: 'my-contextmenu',\n        trigger: 'contextmenu',\n      },\n      'minimap',\n    ];\n\n    expect(parseExtensions({} as any, 'plugin', options)).toEqual([\n      { type: 'minimap', key: 'plugin-minimap-0' },\n      { type: 'tooltip', key: 'my-tooltip' },\n      { type: 'tooltip', key: 'plugin-tooltip-0' },\n      {\n        type: 'contextmenu',\n        key: 'my-contextmenu',\n        trigger: 'contextmenu',\n      },\n      { type: 'minimap', key: 'plugin-minimap-1' },\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/graphlib.spec.ts",
    "content": "import type { NodeData } from '@/src';\nimport { toG6Data, toGraphlibData } from '@/src/utils/graphlib';\n\ndescribe('graphlib', () => {\n  it('toGraphlibData', () => {\n    expect(\n      [\n        { id: 'node-1' },\n        { id: 'node-2', data: { value: 1 } },\n        { id: 'node-3', data: { value: 2 }, style: { opacity: 0.5 } },\n      ].map(toGraphlibData),\n    ).toEqual([\n      { id: 'node-1', data: { id: 'node-1', data: {}, style: {} } },\n      { id: 'node-2', data: { id: 'node-2', data: { value: 1 }, style: {} } },\n      { id: 'node-3', data: { id: 'node-3', data: { value: 2 }, style: { opacity: 0.5 } } },\n    ]);\n\n    expect(\n      [\n        { id: 'edge-1', source: 'node-1', target: 'node-2' },\n        { id: 'edge-2', source: 'node-2', target: 'node-3', data: { value: 2 }, style: { fill: 'blue' } },\n      ].map(toGraphlibData),\n    ).toEqual([\n      {\n        id: 'edge-1',\n        source: 'node-1',\n        target: 'node-2',\n        data: { id: 'edge-1', source: 'node-1', target: 'node-2', data: {}, style: {} },\n      },\n      {\n        id: 'edge-2',\n        source: 'node-2',\n        target: 'node-3',\n        data: { id: 'edge-2', source: 'node-2', target: 'node-3', data: { value: 2 }, style: { fill: 'blue' } },\n      },\n    ]);\n  });\n\n  it('data isolation', () => {\n    const raw: NodeData = {\n      id: 'node-3',\n      data: { basic: 2, array: [1, 2, 3], object: { a: 1 } },\n      style: { x: 100, y: 100, opacity: 0.5, size: [100, 100] },\n    };\n    const graphlibData = toGraphlibData(raw);\n\n    expect(graphlibData.data).toEqual(raw);\n\n    Object.assign(graphlibData.data.data!, { basic: 3, array: [4, 5, 6], object: { b: 2 } });\n\n    expect(raw.data).toEqual({ basic: 2, array: [1, 2, 3], object: { a: 1 } });\n    expect(graphlibData.data.data).toEqual({ basic: 3, array: [4, 5, 6], object: { b: 2 } });\n\n    graphlibData.data.style!.x = 200;\n    graphlibData.data.style!.size = [200, 200];\n\n    expect(raw.style).toEqual({ x: 100, y: 100, opacity: 0.5, size: [100, 100] });\n  });\n\n  it('toG6Data', () => {\n    expect(\n      [\n        { id: 'node-1', data: { id: 'node-1' } },\n        { id: 'node-2', data: { id: 'node-2', data: { value: 1 } } },\n        { id: 'node-3', data: { id: 'node-3', data: { value: 2 }, style: { opacity: 0.5 } } },\n      ].map(toG6Data),\n    ).toEqual([\n      { id: 'node-1' },\n      { id: 'node-2', data: { value: 1 } },\n      { id: 'node-3', data: { value: 2 }, style: { opacity: 0.5 } },\n    ]);\n\n    expect(\n      [\n        {\n          id: 'edge-1',\n          source: 'node-1',\n          target: 'node-2',\n          data: { id: 'edge-1', source: 'node-1', target: 'node-2' },\n        },\n        {\n          id: 'edge-2',\n          source: 'node-2',\n          target: 'node-3',\n          data: { id: 'edge-2', source: 'node-2', target: 'node-3', data: { value: 2 }, style: { fill: 'blue' } },\n        },\n      ].map(toG6Data),\n    ).toEqual([\n      { id: 'edge-1', source: 'node-1', target: 'node-2' },\n      { id: 'edge-2', source: 'node-2', target: 'node-3', data: { value: 2 }, style: { fill: 'blue' } },\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/id.spec.ts",
    "content": "import { idOf, idsOf, parentIdOf } from '@/src/utils/id';\n\ndescribe('id', () => {\n  it('idOf', () => {\n    expect(idOf({ id: '1' })).toBe('1');\n    expect(idOf({ source: 'node-1', target: 'edge-1' })).toBe(`node-1-edge-1`);\n    expect(() => idOf({})).toThrow();\n  });\n\n  it('parentIdOf', () => {\n    expect(parentIdOf({ combo: '1' })).toBe('1');\n    expect(parentIdOf({})).toBeUndefined();\n  });\n\n  it('idsOf', () => {\n    expect(idsOf({ nodes: [{ id: '1' }, { id: '2' }], edges: [{ source: '1', target: '2' }] }, true)).toEqual([\n      '1',\n      '2',\n      '1-2',\n    ]);\n    expect(idsOf({}, true)).toEqual([]);\n    expect(idsOf({ nodes: [{ id: '1' }, { id: '2' }], edges: [{ source: '1', target: '2' }] }, false)).toEqual({\n      nodes: ['1', '2'],\n      edges: ['1-2'],\n      combos: [],\n    });\n    expect(idsOf({}, false)).toEqual({ nodes: [], edges: [], combos: [] });\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/is.spec.ts",
    "content": "import { isEdgeData, isPoint, isVector2, isVector3 } from '@/src/utils/is';\n\ndescribe('is', () => {\n  it('isEdgeData', () => {\n    expect(isEdgeData({})).toBe(false);\n    expect(isEdgeData({ source: 'node-1', target: 'edge-1' })).toBe(true);\n  });\n\n  it('isVector2', () => {\n    expect(isVector2([1, 2])).toBe(true);\n    expect(isVector2([1, 2, 3])).toBe(false);\n  });\n\n  it('isVector3', () => {\n    expect(isVector3([1, 2, 3])).toBe(true);\n    expect(isVector3([1, 2])).toBe(false);\n  });\n\n  it('isPoint', () => {\n    expect(isPoint([1, 2])).toBe(true);\n    expect(isPoint([1, 2, 3])).toBe(true);\n    expect(isPoint(new Float32Array([1, 2, 3]))).toBe(true);\n    expect(isPoint([1, '2'])).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/layout.spec.ts",
    "content": "import type { GraphData } from '@/src';\nimport { AntVGraphData } from '@/src/layouts/types';\nimport {\n  getLayoutProperty,\n  invokeLayoutMethod,\n  isComboLayout,\n  isPositionSpecified,\n  isTreeLayout,\n  layoutAdapter,\n  layoutMapping2GraphData,\n} from '@/src/utils/layout';\nimport dagreData from '@@/dataset/dagre.json';\n\nclass MockLayout {\n  public id = 'mock';\n\n  private nodes: any[] = [];\n\n  private edges: any[] = [];\n\n  public async execute(model: any, options: any): Promise<void> {\n    this.nodes = (model.nodes ?? []).map((datum: any, index: number) => {\n      const node = typeof options?.node === 'function' ? options.node(datum) : datum;\n      return { ...node, x: index * 10, y: index * 20, z: node.z ?? 0 };\n    });\n\n    this.edges = (model.edges ?? []).map((datum: any, index: number) => {\n      const edge = typeof options?.edge === 'function' ? options.edge(datum) : datum;\n      return {\n        ...edge,\n        points: [\n          { x: index, y: index },\n          { x: index + 1, y: index + 1 },\n        ],\n      };\n    });\n\n    if (typeof options?.onTick === 'function') {\n      for (let i = 0; i < 3; i++) options.onTick(this);\n    }\n  }\n\n  public forEachNode(callback: (node: any) => void) {\n    this.nodes.forEach(callback);\n  }\n\n  public forEachEdge(callback: (edge: any) => void) {\n    this.edges.forEach(callback);\n  }\n}\n\ndescribe('layout', () => {\n  it('isComboLayout', () => {\n    expect(isComboLayout({ type: 'force' })).toBe(false);\n    expect(isComboLayout({ type: 'comboCombined' })).toBe(true);\n    expect(isComboLayout({ type: 'antv-dagre', sortByCombo: true })).toBe(true);\n    expect(isComboLayout({ type: 'antv-dagre' })).toBe(false);\n  });\n\n  it('isTreeLayout', () => {\n    expect(isTreeLayout({ type: 'force' })).toBe(false);\n    expect(isTreeLayout({ type: 'compact-box' })).toBe(true);\n    expect(isTreeLayout({ type: 'mindmap' })).toBe(true);\n  });\n\n  it('isPositionSpecified', () => {\n    expect(isPositionSpecified({})).toBe(false);\n    expect(isPositionSpecified({ x: 100 })).toBe(false);\n    expect(isPositionSpecified({ y: 100 })).toBe(false);\n    expect(isPositionSpecified({ x: 100, y: 100 })).toBe(true);\n    expect(isPositionSpecified({ x: 100, y: 100, z: 100 })).toBe(true);\n    expect(isPositionSpecified({ x: 0, y: 0, z: 0 })).toBe(true);\n  });\n\n  it('layoutMapping2GraphData', () => {\n    const layoutMapping: AntVGraphData = {\n      nodes: [\n        { id: 'node-1', data: { x: 0, y: 0 } },\n        { id: 'node-2', data: { x: 100, y: 100 } },\n        { id: 'combo-1', data: { x: 50, y: 50, _isCombo: true } },\n      ],\n      edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', data: {} }],\n    };\n\n    const graphData: GraphData = layoutMapping2GraphData(layoutMapping);\n\n    expect(graphData).toEqual({\n      nodes: [\n        { id: 'node-1', style: { x: 0, y: 0, z: 0 } },\n        { id: 'node-2', style: { x: 100, y: 100, z: 0 } },\n      ],\n      edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', style: {} }],\n      combos: [{ id: 'combo-1', style: { x: 50, y: 50, z: 0 } }],\n    });\n  });\n\n  it('layoutMapping2GraphData with controlPoints', () => {\n    const layoutMapping: AntVGraphData = {\n      nodes: [\n        { id: 'node-1', data: { x: 0, y: 0 } },\n        { id: 'node-2', data: { x: 100, y: 100 } },\n      ],\n      edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', data: { controlPoints: [{ x: 50, y: 50 }] } }],\n    };\n\n    const graphData: GraphData = layoutMapping2GraphData(layoutMapping);\n\n    expect(graphData).toEqual({\n      nodes: [\n        { id: 'node-1', style: { x: 0, y: 0, z: 0 } },\n        { id: 'node-2', style: { x: 100, y: 100, z: 0 } },\n      ],\n      edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', style: { controlPoints: [[50, 50, 0]] } }],\n      combos: [],\n    });\n  });\n\n  it('layoutAdapter', async () => {\n    const data: GraphData = {\n      nodes: [{ id: 'node-1', style: { x: 1, y: 2, z: 3 } }],\n      edges: [{ id: 'edge-1', source: 'node-1', target: 'combo-1' }],\n      combos: [{ id: 'combo-1' }],\n    };\n\n    const context = {\n      model: {\n        isCombo: (id: string) => id === 'combo-1',\n        model: {\n          hasTreeStructure: () => true,\n          getParent: () => null,\n        },\n      },\n    } as any;\n    const AdaptiveMockLayout = layoutAdapter(MockLayout as any, context);\n\n    const layout = new AdaptiveMockLayout(context);\n\n    const result = await layout.execute(data);\n    expect(result).toEqual({\n      nodes: [{ id: 'node-1', style: { x: 0, y: 0, z: 3 } }],\n      edges: [\n        {\n          id: 'edge-1',\n          source: 'node-1',\n          target: 'combo-1',\n          style: {\n            controlPoints: [\n              { x: 0, y: 0 },\n              { x: 1, y: 1 },\n            ],\n          },\n        },\n      ],\n      combos: [{ id: 'combo-1', style: { x: 10, y: 20, z: 0 } }],\n    });\n  });\n\n  it('layoutAdapter with onTick', async () => {\n    const data: GraphData = {\n      nodes: [{ id: 'node-1' }],\n      edges: [],\n      combos: [],\n    };\n\n    const context = {\n      model: {\n        isCombo: () => false,\n        model: {\n          hasTreeStructure: () => true,\n          getParent: () => null,\n        },\n      },\n    } as any;\n    const AdaptiveMockLayout = layoutAdapter(MockLayout as any, context);\n\n    const onTick = jest.fn();\n\n    const layout = new AdaptiveMockLayout(context, {\n      onTick,\n    });\n\n    await layout.execute(data);\n\n    expect(onTick).toHaveBeenCalled();\n    expect(onTick).toHaveBeenCalledTimes(3);\n  });\n\n  it('invoke and get', async () => {\n    const context = {\n      model: {\n        isCombo: () => false,\n        model: {\n          hasTreeStructure: () => true,\n          getParent: () => null,\n        },\n      },\n    } as any;\n    const AdaptiveMockLayout = layoutAdapter(MockLayout as any, context);\n\n    const layout = new AdaptiveMockLayout(context);\n\n    expect(invokeLayoutMethod(layout, 'execute', dagreData)).toBeInstanceOf(Promise);\n    expect(invokeLayoutMethod(layout, 'null')).toBe(null);\n\n    expect(typeof getLayoutProperty(layout, 'options')).toBe('object');\n    expect(getLayoutProperty(layout, 'id')).toBe('mock');\n    expect(getLayoutProperty(layout, 'null')).toBe(null);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/line.spec.ts",
    "content": "import { getLinesIntersection, isLinesParallel } from '@/src/utils/line';\n\ndescribe('line', () => {\n  it('isLinesParallel', () => {\n    expect(\n      isLinesParallel(\n        [\n          [100, 100],\n          [100, 50],\n        ],\n        [\n          [100, 150],\n          [100, 200],\n        ],\n      ),\n    ).toEqual(true);\n    expect(\n      isLinesParallel(\n        [\n          [100, 100],\n          [100, 50],\n        ],\n        [\n          [100, 150],\n          [150, 200],\n        ],\n      ),\n    ).toEqual(false);\n  });\n\n  it('getLinesIntersection', () => {\n    expect(\n      getLinesIntersection(\n        [\n          [100, 0],\n          [100, 200],\n        ],\n        [\n          [0, 100],\n          [200, 100],\n        ],\n      ),\n    ).toEqual([100, 100]);\n    expect(\n      getLinesIntersection(\n        [\n          [100, 0],\n          [100, 200],\n        ],\n        [\n          [0, 100],\n          [50, 300],\n        ],\n      ),\n    ).toEqual(undefined);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/math.spec.ts",
    "content": "import { isBetween } from '@/src/utils/math';\n\ndescribe('math', () => {\n  it('isBetween', () => {\n    expect(isBetween(1, 0, 2)).toBe(true);\n    expect(isBetween(1, 2, 3)).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/padding.spec.ts",
    "content": "import { getHorizontalPadding, getVerticalPadding, parsePadding } from '@/src/utils/padding';\n\ndescribe('padding', () => {\n  it('parsePadding', () => {\n    expect(parsePadding()).toEqual([0, 0, 0, 0]);\n\n    expect(parsePadding([])).toEqual([0, 0, 0, 0]);\n\n    expect(parsePadding(10)).toEqual([10, 10, 10, 10]);\n\n    expect(parsePadding([10, 20])).toEqual([10, 20, 10, 20]);\n\n    expect(parsePadding([10, 20, 30])).toEqual([10, 20, 30, 20]);\n\n    expect(parsePadding([10, 20, 30, 40])).toEqual([10, 20, 30, 40]);\n  });\n\n  it('getVerticalPadding', () => {\n    expect(getVerticalPadding()).toEqual(0);\n\n    expect(getVerticalPadding([])).toEqual(0);\n\n    expect(getVerticalPadding(10)).toEqual(20);\n\n    expect(getVerticalPadding([10, 20])).toEqual(20);\n\n    expect(getVerticalPadding([10, 20, 30])).toEqual(40);\n\n    expect(getVerticalPadding([10, 20, 30, 40])).toEqual(40);\n  });\n\n  it('getHorizontalPadding', () => {\n    expect(getHorizontalPadding()).toEqual(0);\n\n    expect(getHorizontalPadding([])).toEqual(0);\n\n    expect(getHorizontalPadding(10)).toEqual(20);\n\n    expect(getHorizontalPadding([10, 20])).toEqual(40);\n\n    expect(getHorizontalPadding([10, 20, 30])).toEqual(40);\n\n    expect(getHorizontalPadding([10, 20, 30, 40])).toEqual(60);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/palette.spec.ts",
    "content": "import { register } from '@/src';\nimport { idOf } from '@/src/utils/id';\nimport { assignColorByPalette, getPaletteColors, parsePalette } from '@/src/utils/palette';\n\ndescribe('palette', () => {\n  it('parsePalette', () => {\n    expect(parsePalette('category3')?.color).toEqual('category3');\n    expect(parsePalette(['red', 'green', 'blue'])?.color).toEqual(['red', 'green', 'blue']);\n    expect(parsePalette({ type: 'value', color: 'custom-blues', field: 'value' })).toEqual({\n      type: 'value',\n      color: 'custom-blues',\n      field: 'value',\n    });\n  });\n\n  it('assignColorByPalette unset', () => {\n    const data = [\n      { id: 'node-1', data: { value: 100, category: 'A' } },\n      { id: 'node-2', data: { value: 200, category: 'B' } },\n      { id: 'node-3', data: { value: 300, category: 'C' } },\n    ];\n\n    expect(assignColorByPalette(data)).toEqual({});\n  });\n\n  it('assignColorByPalette discrete', () => {\n    register('palette', 'category3', ['#1f77b4', '#ff7f0e', '#2ca02c']);\n\n    const data3 = [\n      { id: 'node-1', data: { value: 100, category: 'A' } },\n      { id: 'node-2', data: { value: 200, category: 'B' } },\n      { id: 'node-3', data: { value: 300, category: 'C' } },\n    ];\n\n    const data4 = [...data3, { id: 'node-4', data: { value: 400, category: 'D' } }];\n\n    const data5 = [...data4, { id: 'node-5', data: { value: 500, category: 'A' } }];\n\n    expect(\n      assignColorByPalette(data3, {\n        type: 'group',\n        color: 'category3',\n        field: 'category',\n      }),\n    ).toEqual({\n      'node-1': '#1f77b4',\n      'node-2': '#ff7f0e',\n      'node-3': '#2ca02c',\n    });\n\n    // invert\n    expect(\n      assignColorByPalette(data3, {\n        type: 'group',\n        color: 'category3',\n        field: 'category',\n        invert: true,\n      }),\n    ).toEqual({\n      'node-1': '#2ca02c',\n      'node-2': '#ff7f0e',\n      'node-3': '#1f77b4',\n    });\n\n    expect(\n      assignColorByPalette(data4, {\n        type: 'group',\n        color: 'category3',\n        field: 'category',\n      }),\n    ).toEqual({\n      'node-1': '#1f77b4',\n      'node-2': '#ff7f0e',\n      'node-3': '#2ca02c',\n      'node-4': '#1f77b4',\n    });\n\n    expect(\n      assignColorByPalette(data5, {\n        type: 'group',\n        color: 'category3',\n        field: 'category',\n      }),\n    ).toEqual({\n      'node-1': '#1f77b4',\n      'node-2': '#ff7f0e',\n      'node-3': '#2ca02c',\n      'node-4': '#1f77b4',\n      'node-5': '#1f77b4',\n    });\n\n    expect(\n      assignColorByPalette(data5, {\n        type: 'group',\n        field: (d) => idOf(d).toString(),\n        color: 'category3',\n      }),\n    ).toEqual({\n      'node-1': '#1f77b4',\n      'node-2': '#ff7f0e',\n      'node-3': '#2ca02c',\n      'node-4': '#1f77b4',\n      'node-5': '#ff7f0e',\n    });\n\n    expect(\n      assignColorByPalette(data5, {\n        type: 'group',\n        field: (d) => idOf(d).toString(),\n        color: 'spectral',\n      }),\n    ).toEqual({\n      'node-1': 'rgb(158, 1, 66)',\n      'node-2': 'rgb(213, 62, 79)',\n      'node-3': 'rgb(244, 109, 67)',\n      'node-4': 'rgb(253, 174, 97)',\n      'node-5': 'rgb(254, 224, 139)',\n    });\n  });\n\n  it('assignColorByPalette continuous', () => {\n    register('palette', 'custom-blues', (value) => `rgb(0, 0, ${(value * 255).toFixed(0)})`);\n\n    const createData = (length: number) => {\n      return Array.from({ length }, (_, index) => ({ id: `node-${index + 1}`, data: { value: index * 100 + 100 } }));\n    };\n\n    const data3 = createData(3);\n\n    expect(\n      assignColorByPalette(data3, {\n        type: 'value',\n        color: 'custom-blues',\n        field: 'value',\n      }),\n    ).toEqual({\n      'node-1': 'rgb(0, 0, 0)',\n      'node-2': 'rgb(0, 0, 128)',\n      'node-3': 'rgb(0, 0, 255)',\n    });\n\n    // invert\n    expect(\n      assignColorByPalette(data3, {\n        type: 'value',\n        color: 'custom-blues',\n        field: 'value',\n        invert: true,\n      }),\n    ).toEqual({\n      'node-1': 'rgb(0, 0, 255)',\n      'node-2': 'rgb(0, 0, 128)',\n      'node-3': 'rgb(0, 0, 0)',\n    });\n\n    const data11 = createData(11);\n    expect(\n      assignColorByPalette(data11, {\n        type: 'value',\n        color: 'custom-blues',\n        field: 'value',\n      }),\n    ).toEqual({\n      'node-1': 'rgb(0, 0, 0)',\n      'node-2': 'rgb(0, 0, 26)',\n      'node-3': 'rgb(0, 0, 51)',\n      'node-4': 'rgb(0, 0, 77)',\n      'node-5': 'rgb(0, 0, 102)',\n      'node-6': 'rgb(0, 0, 128)',\n      'node-7': 'rgb(0, 0, 153)',\n      'node-8': 'rgb(0, 0, 179)',\n      'node-9': 'rgb(0, 0, 204)',\n      'node-10': 'rgb(0, 0, 230)',\n      'node-11': 'rgb(0, 0, 255)',\n    });\n  });\n\n  it('getPaletteColors', () => {\n    expect(getPaletteColors('spectral')![0]).toBe('rgb(158, 1, 66)');\n    expect(getPaletteColors(['#f00', '#0f0', '#00f'])).toEqual(['#f00', '#0f0', '#00f']);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/path.spec.ts",
    "content": "import { getClosedSpline, parsePath, pathToPoints } from '@/src/utils/path';\nimport type { PathArray } from '@antv/util';\n\ndescribe('path', () => {\n  const EMPTY_PATH: PathArray = [\n    ['M', 0, 0],\n    ['L', 0, 0],\n  ];\n\n  it('parsePath', () => {\n    expect(parsePath('M 0 0 L 0 0')).toEqual([\n      ['M', 0, 0],\n      ['L', 0, 0],\n    ]);\n    expect(parsePath('M 0 0 L 0 0 Z')).toEqual([['M', 0, 0], ['L', 0, 0], ['Z']]);\n    // Arc\n    expect(parsePath('M 0 0 A 1 1 0 0 0 0 0')).toEqual([\n      ['M', 0, 0],\n      ['A', 1, 1, 0, 0, 0, 0, 0],\n    ]);\n    // Q\n    expect(parsePath('M 0 0 Q 1 1 0 0')).toEqual([\n      ['M', 0, 0],\n      ['Q', 1, 1, 0, 0],\n    ]);\n  });\n\n  it('pathToPoints', () => {\n    expect(pathToPoints('M 0 0 L 0 0')).toEqual([\n      [0, 0, 0],\n      [0, 0, 0],\n    ]);\n    expect(pathToPoints('M 0 0 L 0 0 Z')).toEqual([\n      [0, 0, 0],\n      [0, 0, 0],\n      [0, 0, 0],\n    ]);\n    expect(pathToPoints('M 0 0 A 1 1 0 0 0 0 0')).toEqual([\n      [0, 0, 0],\n      [0, 0, 0],\n    ]);\n    expect(pathToPoints('M 0 0 A 1 1 0 0 0 1 1')).toEqual([\n      [0, 0, 0],\n      [1, 1, 0],\n    ]);\n    expect(pathToPoints('M 0 0 Q 1 1 0 0')).toEqual([\n      [0, 0, 0],\n      [1, 1, 0],\n      [0, 0, 0],\n    ]);\n  });\n\n  it('getClosedSpline', () => {\n    expect(getClosedSpline([])).toEqual(EMPTY_PATH);\n    expect(\n      getClosedSpline([\n        [0, 0],\n        [6, 6],\n      ]),\n    ).toEqual([\n      ['M', 6, 6],\n      ['C', 6, 6, 0, 0, 0, 0],\n      ['C', 0, 0, 6, 6, 6, 6],\n      ['C', 6, 6, 0, 0, 0, 0],\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/placement.spec.ts",
    "content": "import { parsePlacement } from '@/src/utils/placement';\n\ndescribe('placement', () => {\n  it('parsePlacement', () => {\n    expect(parsePlacement('top')).toEqual([0.5, 0]);\n    expect(parsePlacement('bottom')).toEqual([0.5, 1]);\n    expect(parsePlacement('left')).toEqual([0, 0.5]);\n    expect(parsePlacement('right')).toEqual([1, 0.5]);\n    expect(parsePlacement('left-top')).toEqual([0, 0]);\n    expect(parsePlacement('left-bottom')).toEqual([0, 1]);\n    expect(parsePlacement('right-top')).toEqual([1, 0]);\n    expect(parsePlacement('right-bottom')).toEqual([1, 1]);\n    expect(parsePlacement('top-left')).toEqual([0, 0]);\n    expect(parsePlacement('top-right')).toEqual([1, 0]);\n    expect(parsePlacement('bottom-left')).toEqual([0, 1]);\n    expect(parsePlacement('bottom-right')).toEqual([1, 1]);\n    expect(parsePlacement('center')).toEqual([0.5, 0.5]);\n    expect(parsePlacement([0.5, 0.5])).toEqual([0.5, 0.5]);\n    expect(parsePlacement([0.5, 0])).toEqual([0.5, 0]);\n    expect(parsePlacement([0.5, 0])).toEqual([0.5, 0]);\n    expect(parsePlacement([0.5, 1.2])).toEqual([0.5, 0.5]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/point.spec.ts",
    "content": "import { getDiamondPoints } from '@/src/utils/element';\nimport {\n  centerOf,\n  deduplicate,\n  findNearestLine,\n  findNearestPointOnLine,\n  findNearestPoints,\n  getDistanceToLine,\n  getEllipseIntersectPoint,\n  getPolygonIntersectPoint,\n  getRectIntersectPoint,\n  getSymmetricPoint,\n  isCollinear,\n  isHorizontal,\n  isOrthogonal,\n  isPointInPolygon,\n  isVertical,\n  moveTo,\n  parsePoint,\n  round,\n  sortByClockwise,\n  sortByX,\n  toPointObject,\n} from '@/src/utils/point';\nimport { Circle, Rect } from '@antv/g';\n\ndescribe('Point Functions', () => {\n  it('parsePoint', () => {\n    expect(parsePoint({ x: 100, y: 100 })).toEqual([100, 100, 0]);\n  });\n\n  it('toPointObject', () => {\n    expect(toPointObject([100, 100])).toEqual({ x: 100, y: 100, z: 0 });\n  });\n\n  it('sortByX', () => {\n    expect(\n      sortByX([\n        [150, 150],\n        [50, 50],\n        [100, 100],\n      ]),\n    ).toEqual([\n      [50, 50],\n      [100, 100],\n      [150, 150],\n    ]);\n  });\n\n  it('deduplicate', () => {\n    expect(\n      deduplicate([\n        [100, 100],\n        [100, 100],\n        [100, 100],\n      ]),\n    ).toEqual([[100, 100]]);\n  });\n\n  it('sortByX', () => {\n    expect(\n      sortByX([\n        [100, 100],\n        [50, 50],\n        [150, 150],\n      ]),\n    ).toEqual([\n      [50, 50],\n      [100, 100],\n      [150, 150],\n    ]);\n  });\n\n  it('round', () => {\n    expect(round([100.123, 100.123], 2)).toEqual([100.12, 100.12]);\n  });\n\n  it('moveTo', () => {\n    expect(moveTo([100, 100], [50, 100], 10)).toEqual([90, 100]);\n    expect(moveTo([50, 100], [100, 100], 10)).toEqual([60, 100]);\n  });\n\n  it('isHorizontal', () => {\n    expect(isHorizontal([100, 100], [50, 100])).toEqual(true);\n    expect(isHorizontal([100, 100], [50, 150])).toEqual(false);\n  });\n\n  it('isVertical', () => {\n    expect(isVertical([100, 100], [100, 50])).toEqual(true);\n    expect(isVertical([100, 100], [50, 150])).toEqual(false);\n  });\n\n  it('isOrthogonal', () => {\n    expect(isOrthogonal([100, 100], [100, 50])).toEqual(true);\n    expect(isOrthogonal([100, 100], [50, 100])).toEqual(true);\n  });\n\n  it('isCollinear', () => {\n    expect(isCollinear([100, 100], [100, 50], [100, 150])).toEqual(true);\n    expect(isCollinear([100, 100], [50, 100], [150, 100])).toEqual(true);\n    expect(isCollinear([100, 100], [50, 50], [150, 100])).toEqual(false);\n  });\n\n  it('getSymmetricPoint', () => {\n    expect(getSymmetricPoint([50, 50], [100, 100])).toEqual([150, 150]);\n    expect(getSymmetricPoint([-50, -50], [0, 0])).toEqual([50, 50]);\n  });\n\n  it('getRectIntersectPoint', () => {\n    const rect = new Rect({\n      style: {\n        x: 100,\n        y: 100,\n        width: 2,\n        height: 2,\n      },\n    });\n    expect(getRectIntersectPoint([110, 110], rect.getBounds())).toEqual([102, 102]);\n    expect(getRectIntersectPoint([110, 110], rect.getBounds(), true)).toEqual([100, 100]);\n  });\n\n  it('getEllipseIntersectPoint', () => {\n    const circle = new Circle({\n      style: {\n        cx: 100,\n        cy: 100,\n        r: 1,\n      },\n    });\n    expect(getEllipseIntersectPoint([110, 100], circle.getBounds())).toEqual([101, 100]);\n    expect(getEllipseIntersectPoint([110, 100], circle.getBounds(), true)).toEqual([99, 100]);\n\n    const circle2 = new Circle({\n      style: {\n        r: 20,\n      },\n    });\n    expect(getEllipseIntersectPoint([0, 0], circle2.getBounds())).toEqual([20, 0]);\n\n    const circle3 = new Circle({\n      style: {\n        cx: 100,\n        cy: 100,\n        r: 20,\n      },\n    });\n    expect(getEllipseIntersectPoint([100, 100], circle3.getBounds())).toEqual([120, 100]);\n  });\n\n  it('getDiamondIntersectPoint', () => {\n    expect(getPolygonIntersectPoint([100, 100], [0, 0], getDiamondPoints(100, 100)).point).toEqual([25, 25]);\n    expect(getDiamondPoints(0, 0)).toEqual([\n      [0, -0],\n      [0, 0],\n      [0, 0],\n      [-0, 0],\n    ]);\n    const height = 10;\n    const width = 10;\n    expect(getDiamondPoints(width, height)).toEqual([\n      [0, -height / 2],\n      [width / 2, 0],\n      [0, height / 2],\n      [-width / 2, 0],\n    ]);\n  });\n\n  it('findNearestPoints', () => {\n    expect(\n      findNearestPoints(\n        [\n          [0, 0],\n          [100, 110],\n        ],\n        [\n          [1, 1],\n          [100, 100],\n        ],\n      ),\n    ).toEqual([\n      [0, 0],\n      [1, 1],\n    ]);\n  });\n\n  it('isPointInPolygon', () => {\n    expect(\n      isPointInPolygon(\n        [10, 10],\n        [\n          [0, 0],\n          [100, 0],\n          [100, 100],\n          [0, 100],\n        ],\n      ),\n    ).toEqual(true);\n    expect(\n      isPointInPolygon(\n        [20, 20],\n        [\n          [0, 0],\n          [10, 0],\n          [10, 10],\n          [0, 10],\n        ],\n      ),\n    ).toEqual(false);\n    expect(\n      isPointInPolygon(\n        [20, 30],\n        [\n          [0, 0],\n          [20, 0],\n          [20, 20],\n          [0, 20],\n        ],\n      ),\n    ).toEqual(false);\n  });\n\n  it('findNearestLine', () => {\n    expect(\n      findNearestLine(\n        [10, 10],\n        [\n          [\n            [20, 0],\n            [20, 20],\n          ],\n          [\n            [30, 0],\n            [30, 30],\n          ],\n        ],\n      ),\n    ).toEqual([\n      [20, 0],\n      [20, 20],\n    ]);\n  });\n\n  it('getDistanceToLine', () => {\n    expect(\n      getDistanceToLine(\n        [10, 10],\n        [\n          [20, 0],\n          [20, 20],\n        ],\n      ),\n    ).toEqual(10);\n  });\n\n  it('findNearestPointOnLine', () => {\n    expect(\n      findNearestPointOnLine(\n        [10, 10],\n        [\n          [20, 0],\n          [20, 20],\n        ],\n      ),\n    ).toEqual([20, 10]);\n    expect(\n      findNearestPointOnLine(\n        [10, 10],\n        [\n          [20, 20],\n          [20, 20],\n        ],\n      ),\n    ).toEqual([20, 20]);\n    expect(\n      findNearestPointOnLine(\n        [10, 10],\n        [\n          [20, 0],\n          [20, 5],\n        ],\n      ),\n    ).toEqual([20, 5]);\n  });\n\n  it('centerOf', () => {\n    expect(\n      centerOf([\n        [0, 0],\n        [100, 100],\n      ]),\n    ).toEqual([50, 50]);\n  });\n\n  it('sortByClockwise', () => {\n    expect(\n      sortByClockwise([\n        [100, 100],\n        [50, 50],\n        [150, 150],\n      ]),\n    ).toEqual([\n      [150, 150],\n      [100, 100],\n      [50, 50],\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/polygon.spec.ts",
    "content": "import { getPolygonTextStyleByPlacement } from '@/src/utils/polygon';\nimport type { TransformArray } from '@antv/g';\nimport { AABB, Path } from '@antv/g';\nimport type { PathArray } from '@antv/util';\n\ndescribe('polygon', () => {\n  const EMPTY_PATH: PathArray = [\n    ['M', 0, 0],\n    ['L', 0, 0],\n  ];\n\n  it('getPolygonTextStyleByPlacement', () => {\n    const bounds = new AABB();\n    bounds.setMinMax([0, 0, 0], [100, 100, 0]);\n    // different placement\n    expect(getPolygonTextStyleByPlacement(bounds, 'top', 0, 0, false, EMPTY_PATH, false)).toEqual({\n      textAlign: 'center',\n      textBaseline: 'bottom',\n      transform: [['translate', 50, 0]],\n    });\n    expect(getPolygonTextStyleByPlacement(bounds, 'left', 0, 0, false, EMPTY_PATH, false)).toEqual({\n      textAlign: 'right',\n      textBaseline: 'middle',\n      transform: [['translate', 0, 50]],\n    });\n    expect(getPolygonTextStyleByPlacement(bounds, 'right', 0, 0, false, EMPTY_PATH, false)).toEqual({\n      textAlign: 'left',\n      textBaseline: 'middle',\n      transform: [['translate', 100, 50]],\n    });\n    expect(getPolygonTextStyleByPlacement(bounds, 'bottom', 0, 0, false, EMPTY_PATH, false)).toEqual({\n      textAlign: 'center',\n      textBaseline: 'top',\n      transform: [['translate', 50, 100]],\n    });\n\n    // with offset\n    expect(getPolygonTextStyleByPlacement(bounds, 'top', 10, 10, false, EMPTY_PATH, false)).toEqual({\n      textAlign: 'center',\n      textBaseline: 'bottom',\n      transform: [['translate', 60, 10]],\n    });\n\n    // closeToHull and autoRotate\n    const circle: PathArray = [\n      ['M', 0, 0],\n      ['A', 100, 100, 0, 0, 0, 100, 0],\n    ];\n    expect(getPolygonTextStyleByPlacement(bounds, 'top', 0, 0, true, circle, true)).toEqual({\n      textAlign: 'center',\n      textBaseline: 'bottom',\n      transform: [['translate', 50, 0]],\n    });\n\n    const d: PathArray = [\n      ['M', 0, 0],\n      ['L', 100, 20],\n      ['L', 80, 100],\n      ['L', 0, 60],\n      ['L', 0, 0],\n    ];\n    const shape = new Path({ style: { d } });\n    const labelStyle1 = getPolygonTextStyleByPlacement(shape.getRenderBounds(), 'top', 0, 0, true, d, true);\n    expect(labelStyle1.textAlign).toBe('center');\n    expect(labelStyle1.textBaseline).toBe('bottom');\n    expect((labelStyle1.transform as TransformArray).some((t) => t[0] === 'rotate')).toBe(true);\n\n    const labelStyle2 = getPolygonTextStyleByPlacement(shape.getRenderBounds(), 'right', 0, 0, true, d, true);\n    expect(labelStyle2.textAlign).toBe('center');\n    expect(labelStyle2.textBaseline).toBe('top');\n    expect((labelStyle2.transform as TransformArray).some((t) => t[0] === 'rotate')).toBe(true);\n\n    const labelStyle3 = getPolygonTextStyleByPlacement(shape.getRenderBounds(), 'bottom', 0, 0, true, d, true);\n    expect(labelStyle3.textAlign).toBe('center');\n    expect(labelStyle3.textBaseline).toBe('top');\n    expect((labelStyle3.transform as TransformArray).some((t) => t[0] === 'rotate')).toBe(true);\n\n    const labelStyle4 = getPolygonTextStyleByPlacement(shape.getRenderBounds(), 'left', 0, 0, true, d, true);\n    expect(labelStyle4.textAlign).toBe('center');\n    expect(labelStyle4.textBaseline).toBe('top');\n    expect((labelStyle4.transform as TransformArray).some((t) => t[0] === 'rotate')).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/position.spec.ts",
    "content": "import {\n  getXYByAnchor,\n  getXYByPlacement,\n  getXYByRelativePlacement,\n  hasPosition,\n  positionOf,\n} from '@/src/utils/position';\nimport { AABB } from '@antv/g';\n\ndescribe('position', () => {\n  const bbox = new AABB();\n  bbox.setMinMax([0, 0, 0], [100, 100, 0]);\n  it('getXYByRelativePlacement', () => {\n    expect(getXYByRelativePlacement(bbox, [0.5, 0.5])).toEqual([50, 50]);\n    expect(getXYByRelativePlacement(bbox, [0, 0])).toEqual([0, 0]);\n    expect(getXYByRelativePlacement(bbox, [1, 1])).toEqual([100, 100]);\n    expect(getXYByRelativePlacement(bbox, [0.2, 0.2])).toEqual([20, 20]);\n  });\n\n  it('getXYByPlacement', () => {\n    expect(getXYByPlacement(bbox, 'center')).toEqual([50, 50]);\n    expect(getXYByPlacement(bbox, 'left')).toEqual([0, 50]);\n    expect(getXYByPlacement(bbox, 'right')).toEqual([100, 50]);\n    expect(getXYByPlacement(bbox, 'top')).toEqual([50, 0]);\n    expect(getXYByPlacement(bbox, 'bottom')).toEqual([50, 100]);\n  });\n\n  it('getXYByAnchor', () => {\n    expect(getXYByAnchor(bbox, '0.5 0.5')).toEqual([50, 50]);\n    expect(getXYByAnchor(bbox, '0 0')).toEqual([0, 0]);\n    expect(getXYByAnchor(bbox, '1 1')).toEqual([100, 100]);\n    expect(getXYByAnchor(bbox, '0.2 0.2')).toEqual([20, 20]);\n    expect(getXYByAnchor(bbox, [0.5, 0.5])).toEqual([50, 50]);\n    expect(getXYByAnchor(bbox, [0, 0])).toEqual([0, 0]);\n    expect(getXYByAnchor(bbox, [1, 1])).toEqual([100, 100]);\n    expect(getXYByAnchor(bbox, [0.2, 0.2])).toEqual([20, 20]);\n  });\n\n  it('positionOf', () => {\n    expect(positionOf({ id: 'node' })).toEqual([0, 0, 0]);\n    expect(positionOf({ id: 'node', style: { x: 10 } })).toEqual([10, 0, 0]);\n    expect(positionOf({ id: 'node', style: { x: 10, y: 20 } })).toEqual([10, 20, 0]);\n    expect(positionOf({ id: 'node', style: { x: 10, y: 20, z: 30 } })).toEqual([10, 20, 30]);\n  });\n\n  it('hasPosition', () => {\n    expect(hasPosition({ id: 'node' })).toBeFalsy();\n    expect(hasPosition({ id: 'node', style: { x: 10 } })).toBeTruthy();\n    expect(hasPosition({ id: 'node', style: { y: 20 } })).toBeTruthy();\n    expect(hasPosition({ id: 'node', style: { z: 30 } })).toBeTruthy();\n    expect(hasPosition({ id: 'node', style: { x: 10, y: 20, z: 30 } })).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/prefix.spec.ts",
    "content": "import {\n  addPrefix,\n  omitStyleProps,\n  removePrefix,\n  replacePrefix,\n  startsWith,\n  subObject,\n  subStyleProps,\n} from '@/src/utils/prefix';\n\ndescribe('prefix', () => {\n  it('addPrefix', () => {\n    expect(addPrefix('abc', 'prefix')).toBe('prefixAbc');\n    expect(addPrefix('Abc', 'prefix')).toBe('prefixAbc');\n    expect(addPrefix('', 'prefix')).toBe('prefix');\n  });\n\n  it('removePrefix', () => {\n    expect(removePrefix('prefixAbc', 'prefix')).toBe('abc');\n    expect(removePrefix('prefixAbc', 'prefix', false)).toBe('Abc');\n    expect(removePrefix('Abc', 'prefix')).toBe('Abc');\n    expect(removePrefix('', 'prefix')).toBe('');\n    expect(removePrefix('prefix', '')).toBe('prefix');\n    expect(removePrefix('prefixAbc', '')).toBe('prefixAbc');\n  });\n\n  it('startsWith', () => {\n    expect(startsWith('prefixAbc', 'prefix')).toBe(true);\n    expect(startsWith('prefixAbc', 'prefix')).toBe(true);\n    expect(startsWith('Abc', 'prefix')).toBe(false);\n    expect(startsWith('', 'prefix')).toBe(false);\n    expect(startsWith('prefix', 'prefix')).toBe(false);\n  });\n\n  it('subStyleProps', () => {\n    expect(subStyleProps({ prefixAbc: 1, prefixDef: 2, Abc: 3 }, 'prefix')).toEqual({ abc: 1, def: 2 });\n    expect(subStyleProps({ Abc: 1, Def: 2 }, 'prefix')).toEqual({});\n    expect(subStyleProps({}, 'prefix')).toEqual({});\n    expect(subStyleProps({ prefixAbc: 1, className: 3, class: 2 }, 'prefix')).toEqual({ abc: 1 });\n  });\n\n  it('subObject', () => {\n    expect(subObject({ ab: 1, ac: 2, bc: 3 }, 'a')).toEqual({ b: 1, c: 2 });\n    expect(subObject({ ab: 1, ac: 2, bc: 3 }, 'b')).toEqual({ c: 3 });\n  });\n\n  it('omitStyleProps', () => {\n    expect(omitStyleProps({ prefixAbc: 1, prefixDef: 2, Abc: 3 }, 'prefix')).toEqual({ Abc: 3 });\n    expect(omitStyleProps({ Abc: 1, Def: 2 }, 'prefix')).toEqual({ Abc: 1, Def: 2 });\n    expect(omitStyleProps({}, 'prefix')).toEqual({});\n    expect(omitStyleProps({ prefixAbc: 1, prefixDef: 2, labelAbc: 3 }, ['prefix', 'label'])).toEqual({});\n  });\n\n  it('replacePrefix', () => {\n    expect(replacePrefix({ prefixAbc: 1, prefixDef: 2, Abc: 3 }, 'prefix', 'newPrefix')).toEqual({\n      newPrefixAbc: 1,\n      newPrefixDef: 2,\n      Abc: 3,\n    });\n    expect(replacePrefix({ Abc: 1, Def: 2 }, 'prefix', 'newPrefix')).toEqual({ Abc: 1, Def: 2 });\n    expect(replacePrefix({}, 'prefix', 'newPrefix')).toEqual({});\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/print.spec.ts",
    "content": "import { version } from '@/src';\nimport { print } from '@/src/utils/print';\n\ndescribe('print', () => {\n  it('print', () => {\n    const spy = jest.spyOn(console, 'debug').mockImplementation();\n    print.debug('debug message');\n    expect(spy).toHaveBeenCalledWith(`[G6 v${version}] debug message`);\n    spy.mockRestore();\n\n    const spy2 = jest.spyOn(console, 'info').mockImplementation();\n    print.info('info message');\n    expect(spy2).toHaveBeenCalledWith(`[G6 v${version}] info message`);\n    spy2.mockRestore();\n\n    const spy3 = jest.spyOn(console, 'warn').mockImplementation();\n    print.warn('warn message');\n    expect(spy3).toHaveBeenCalledWith(`[G6 v${version}] warn message`);\n    spy3.mockRestore();\n\n    const spy4 = jest.spyOn(console, 'error').mockImplementation();\n    print.error('error message');\n    expect(spy4).toHaveBeenCalledWith(`[G6 v${version}] error message`);\n    spy4.mockRestore();\n  });\n\n  it('print mute', () => {\n    print.mute = true;\n\n    const spy = jest.spyOn(console, 'debug').mockImplementation();\n    print.debug('debug message');\n    expect(spy).not.toHaveBeenCalled();\n    spy.mockRestore();\n\n    const spy2 = jest.spyOn(console, 'info').mockImplementation();\n    print.info('info message');\n    expect(spy2).not.toHaveBeenCalled();\n    spy2.mockRestore();\n\n    const spy3 = jest.spyOn(console, 'warn').mockImplementation();\n    print.warn('warn message');\n    expect(spy3).not.toHaveBeenCalled();\n    spy3.mockRestore();\n\n    const spy4 = jest.spyOn(console, 'error').mockImplementation();\n    print.error('error message');\n    expect(spy4).not.toHaveBeenCalled();\n    spy4.mockRestore();\n\n    print.mute = false;\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/random.spec.ts",
    "content": "import { createDeterministicRandom } from '@@/utils';\n\ndescribe('createDeterministicRandom', () => {\n  it('should generate a random number between 0 and 1', () => {\n    const r1 = createDeterministicRandom();\n    const r2 = createDeterministicRandom();\n\n    expect(new Array(100).fill(0).map(r1)).toEqual(new Array(100).fill(0).map(r2));\n    expect(r1()).toBeGreaterThanOrEqual(0);\n    expect(r1()).toBeLessThan(1);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/relation.spec.ts",
    "content": "import { Graph } from '@/src';\nimport { getElementNthDegreeIds, getNodeNthDegreeIds } from '@/src/utils/relation';\n\ndescribe('relation', () => {\n  let graph: Graph;\n\n  beforeAll(() => {\n    graph = new Graph({\n      data: {\n        nodes: [{ id: '1', combo: 'combo1' }, { id: '2' }, { id: '3' }, { id: '4' }, { id: '5' }, { id: '6' }],\n        edges: [\n          { source: '1', target: '2' },\n          { source: '1', target: '3' },\n          { source: '2', target: '4' },\n          { source: '3', target: '5' },\n          { source: '5', target: '6' },\n          { source: 'combo1', target: '6' },\n        ],\n        combos: [{ id: 'combo1' }],\n      },\n    });\n  });\n\n  afterAll(() => {\n    graph.destroy();\n  });\n\n  it('getElementNthDegreeIds', () => {\n    expect(getElementNthDegreeIds(graph, 'node', '1', 0)).toEqual(['1']);\n    expect(getElementNthDegreeIds(graph, 'node', '1', 1)).toEqual(['1', '1-2', '1-3', '2', '3']);\n    expect(getElementNthDegreeIds(graph, 'edge', '1-2', 0)).toEqual(['1-2']);\n    expect(getElementNthDegreeIds(graph, 'edge', '1-2', 1)).toEqual(['1', '2', '1-2']);\n    expect(getElementNthDegreeIds(graph, 'edge', '1-2', 2)).toEqual(['1', '1-2', '1-3', '2', '3', '2-4', '4']);\n    expect(getElementNthDegreeIds(graph, 'combo', 'combo1', 1)).toEqual(['combo1', 'combo1-6', '6']);\n    expect(getElementNthDegreeIds(graph, 'node', '1', 1, 'in')).toEqual(['1']);\n    expect(getElementNthDegreeIds(graph, 'node', '1', 1, 'out')).toEqual(['1', '1-2', '1-3', '2', '3']);\n  });\n\n  it('getNodeNthDegreeIds', () => {\n    expect(getNodeNthDegreeIds(graph, '1', 0)).toEqual(['1']);\n    expect(getNodeNthDegreeIds(graph, '1', 1)).toEqual(['1', '1-2', '1-3', '2', '3']);\n    expect(getNodeNthDegreeIds(graph, '1', 2)).toEqual(['1', '1-2', '1-3', '2', '2-4', '3', '3-5', '4', '5']);\n    expect(getNodeNthDegreeIds(graph, '1', 1, 'in')).toEqual(['1']);\n    expect(getNodeNthDegreeIds(graph, '1', 1, 'out')).toEqual(['1', '1-2', '1-3', '2', '3']);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/router.spec.ts",
    "content": "import { Rect } from '@/src';\nimport type { Point } from '@/src/types';\nimport {\n  freeJoin,\n  getBBoxSize,\n  getDirection,\n  insideNode,\n  nodeToNode,\n  nodeToPoint,\n  orth,\n  pointToNode,\n  pointToPoint,\n} from '@/src/utils/router/orth';\nimport { estimateCost, getNearestPoint } from '@/src/utils/router/shortest-path';\nimport { manhattanDistance } from '@/src/utils/vector';\nimport { AABB } from '@antv/g';\n\ndescribe('router', () => {\n  describe('orth', () => {\n    const bbox = new AABB();\n    bbox.setMinMax([0, 0, 0], [1, 2, 0]);\n\n    it('orth', () => {\n      const sourceNode = new Rect({ style: { size: 15, x: 5, y: 5 } });\n      const sourcePoint: Point = [5, 5];\n      const targetNode = new Rect({ style: { size: 15, x: 25, y: 25 } });\n      const targetPoint: Point = [25, 25];\n      const controlPoints: Point[] = [[5, 10]];\n      expect(orth(sourcePoint, targetPoint, sourceNode, targetNode, controlPoints, { padding: 10 })).toEqual([\n        [-12.51, 5],\n        [-12.51, 22.51],\n        [5, 22.51],\n        [5, 10],\n        [5, 7.5],\n        [25, 7.5],\n      ]);\n\n      const sourceNode2 = new Rect({ style: { size: 25, x: 100, y: 100 } });\n      const targetNode2 = new Rect({ style: { size: 25, x: 150, y: 150 } });\n      const controlPoints2: Point[] = [[160, 100]];\n      expect(orth([100, 100], [150, 150], sourceNode2, targetNode2, controlPoints2, { padding: 10 })).toEqual([\n        [160, 100],\n        [172.5, 100],\n        [172.5, 150],\n      ]);\n      expect(orth([100, 100], [150, 150], sourceNode2, targetNode2, [], { padding: 10 })).toEqual([[100, 150]]);\n\n      const sourceNode3 = new Rect({ style: { size: 10, x: 5, y: 0 } });\n      const targetNode3 = new Rect({ style: { size: 10, x: 20, y: 25 } });\n      expect(orth([5, 5], [20, 20], sourceNode3, targetNode3, [], { padding: 0 })).toEqual([\n        [5, 5],\n        [5, 20],\n        [20, 20],\n      ]);\n    });\n\n    it('getDirection', () => {\n      expect(getDirection([0, 0], [0, 1])).toEqual('S');\n      expect(getDirection([0, 0], [1, 0])).toEqual('E');\n      expect(getDirection([0, 0], [0, -1])).toEqual('N');\n      expect(getDirection([0, 0], [-1, 0])).toEqual('W');\n      expect(getDirection([0, 0], [1, 1])).toEqual(null);\n    });\n\n    it('getBBoxSize', () => {\n      expect(getBBoxSize(bbox, 'N')).toEqual(2);\n      expect(getBBoxSize(bbox, 'S')).toEqual(2);\n      expect(getBBoxSize(bbox, 'E')).toEqual(1);\n      expect(getBBoxSize(bbox, 'W')).toEqual(1);\n    });\n\n    it('pointToPoint', () => {\n      expect(pointToPoint([0, 0], [1, 1], 'S').points).toEqual([[0, 1]]);\n      expect(pointToPoint([0, 0], [1, 1], 'S').direction).toEqual('E');\n      expect(pointToPoint([0, 0], [1, 1], 'E').points).toEqual([[1, 0]]);\n      expect(pointToPoint([0, 0], [1, 1], 'E').direction).toEqual('S');\n    });\n\n    it('nodeToPoint', () => {\n      const sourceBBox = new AABB();\n      sourceBBox.setMinMax([0, 0, 0], [10, 10, 0]);\n      expect(nodeToPoint([5, 5], [20, 20], sourceBBox).points).toEqual([[5, 20]]);\n      expect(nodeToPoint([5, 5], [20, 20], sourceBBox).direction).toEqual('E');\n      expect(nodeToPoint([10, 5], [20, 20], sourceBBox).points).toEqual([[20, 5]]);\n      expect(nodeToPoint([10, 5], [20, 20], sourceBBox).direction).toEqual('S');\n    });\n\n    it('pointToNode', () => {\n      const targetBBox = new AABB();\n      targetBBox.setMinMax([10, 10, 0], [25, 25, 0]);\n      expect(pointToNode([10, 5], [20, 25], targetBBox, 'N').points).toEqual([[20, 5]]);\n    });\n\n    it('nodeToNode', () => {\n      const sourcePoint: Point = [5, 5, 0];\n      const targetPoint: Point = [7, 7, 0];\n      const sourceBBox = new AABB();\n      sourceBBox.setMinMax([0, 0, 0], [10, 10, 0]);\n      const targetBBox = new AABB();\n      targetBBox.setMinMax([2, 2, 0], [12, 12, 0]);\n      expect(nodeToNode(sourcePoint, targetPoint, sourceBBox, targetBBox).points).toEqual([\n        [6, 5],\n        [6, 2],\n      ]);\n    });\n\n    it('insideNode', () => {\n      const sourcePoint: Point = [5, 5];\n      const targetPoint: Point = [7, 7];\n      const sourceBBox = new AABB();\n      sourceBBox.setMinMax([0, 0, 0], [10, 10, 0]);\n      const targetBBox = new AABB();\n      targetBBox.setMinMax([0, 0, 0], [12, 12, 0]);\n      expect(insideNode(sourcePoint, targetPoint, sourceBBox, targetBBox).points).toEqual([\n        [-0.01, 5],\n        [-0.01, 7],\n      ]);\n      expect(insideNode(sourcePoint, targetPoint, sourceBBox, targetBBox, 'S').points).toEqual([\n        [5, 12.01],\n        [7, 12.01],\n      ]);\n    });\n\n    it('freeJoin', () => {\n      const sourceBBox = new AABB();\n      sourceBBox.setMinMax([0, 0, 0], [10, 10, 0]);\n      const sourcePoint: Point = [5, 10];\n      const targetPoint: Point = [20, 5];\n      expect(freeJoin(sourcePoint, targetPoint, sourceBBox)).toEqual([20, 10]);\n    });\n  });\n\n  describe('shortestPath', () => {\n    it('estimateCost', () => {\n      expect(\n        estimateCost(\n          [0, 0],\n          [\n            [1, 0],\n            [0, 1],\n            [1, 1],\n          ],\n          manhattanDistance,\n        ),\n      ).toEqual(1);\n    });\n\n    it('getNearestPoint', () => {\n      expect(\n        getNearestPoint(\n          [\n            [0, 0],\n            [1, 0],\n            [0, 1],\n            [1, 1],\n          ],\n          [2, 0],\n          manhattanDistance,\n        ),\n      ).toEqual([1, 0]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/scale.spec.ts",
    "content": "import { linear, log, pow, sqrt } from '@/src/utils/scale';\n\ndescribe('scale', () => {\n  it('linear', () => {\n    expect(linear(0.2, [0, 1], [0, 0])).toEqual(0);\n    expect(linear(0, [0, 1], [0, 100])).toEqual(0);\n    expect(linear(0.5, [0, 1], [0, 100])).toEqual(50);\n    expect(linear(1, [0, 1], [0, 100])).toEqual(100);\n  });\n\n  it('log', () => {\n    expect(log(0, [0, 1], [0, 100])).toEqual(0);\n    expect(log(0.5, [0, 1], [0, 100])).toEqual((Math.log(1.5) / Math.log(2)) * 100);\n    expect(log(1, [0, 1], [0, 100])).toEqual(100);\n  });\n\n  it('pow', () => {\n    expect(pow(0, [0, 1], [0, 100], 2)).toEqual(0);\n    expect(pow(0.5, [0, 1], [0, 100])).toEqual(25);\n    expect(pow(0.5, [0, 1], [0, 100], 2)).toEqual(25);\n    expect(pow(1, [0, 1], [0, 100], 2)).toEqual(100);\n  });\n\n  it('sqrt', () => {\n    expect(sqrt(0, [0, 1], [0, 100])).toEqual(0);\n    expect(sqrt(0.25, [0, 1], [0, 100])).toEqual(50);\n    expect(sqrt(1, [0, 1], [0, 100])).toEqual(100);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/shape.spec.ts",
    "content": "import { getAncestorShapes, getDescendantShapes } from '@/src/utils/shape';\nimport { Circle, Group, Line, Rect } from '@antv/g';\n\ndescribe('shape', () => {\n  it('getDescendantShapes', () => {\n    const group = new Group();\n    const rect = group.appendChild(new Rect());\n    const circleGroup = group.appendChild(new Group());\n    const circle = circleGroup.appendChild(new Circle());\n    const line = circleGroup.appendChild(new Line());\n\n    expect(getDescendantShapes(group)).toEqual([rect, circleGroup, circle, line]);\n\n    expect(getDescendantShapes(circleGroup)).toEqual([circle, line]);\n\n    expect(getDescendantShapes(circle)).toEqual([]);\n  });\n\n  it('getDescendantShapes with marker', () => {\n    const marker = new Rect({\n      style: {\n        width: 10,\n        height: 10,\n      },\n    });\n\n    const line = new Line({\n      style: {\n        x1: 0,\n        y1: 0,\n        x2: 100,\n        y2: 100,\n        markerEnd: marker,\n      },\n    });\n\n    expect(getDescendantShapes(line)[0]).not.toBe(marker);\n    expect(getDescendantShapes(line)[0]).toBe(line.parsedStyle.markerEnd);\n  });\n\n  it('getAncestorShapes', () => {\n    const group = new Group();\n    const rect = group.appendChild(new Rect());\n    const circleGroup = group.appendChild(new Group());\n    const circle = circleGroup.appendChild(new Circle());\n\n    expect(getAncestorShapes(group)).toEqual([]);\n    expect(getAncestorShapes(rect)).toEqual([group]);\n    expect(getAncestorShapes(circle)).toEqual([circleGroup, group]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/shortcut.spec.ts",
    "content": "import { CommonEvent } from '@/src';\nimport { Shortcut } from '@/src/utils/shortcut';\nimport EventEmitter from '@antv/event-emitter';\n\ndescribe('shortcut', () => {\n  const emitter = new EventEmitter();\n\n  const shortcut = new Shortcut(emitter);\n\n  afterAll(() => {\n    shortcut.destroy();\n    emitter.off();\n  });\n\n  it('bind and unbind', () => {\n    const controlEqual = jest.fn();\n    const controlMinus = jest.fn();\n    shortcut.bind(['Control', '='], controlEqual);\n    shortcut.bind(['Control', '-'], controlMinus);\n\n    emitter.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    emitter.emit(CommonEvent.KEY_DOWN, { key: '=' });\n    emitter.emit(CommonEvent.KEY_UP, { key: 'Control' });\n    emitter.emit(CommonEvent.KEY_UP, { key: '=' });\n\n    expect(controlEqual).toHaveBeenCalledTimes(1);\n    expect(controlMinus).toHaveBeenCalledTimes(0);\n\n    emitter.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    emitter.emit(CommonEvent.KEY_DOWN, { key: '-' });\n    emitter.emit(CommonEvent.KEY_UP, { key: 'Control' });\n    emitter.emit(CommonEvent.KEY_UP, { key: '-' });\n\n    expect(controlEqual).toHaveBeenCalledTimes(1);\n    expect(controlMinus).toHaveBeenCalledTimes(1);\n\n    emitter.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    emitter.emit(CommonEvent.KEY_DOWN, { key: '=' });\n    emitter.emit(CommonEvent.KEY_UP, { key: '=' });\n    emitter.emit(CommonEvent.KEY_DOWN, { key: '-' });\n    emitter.emit(CommonEvent.KEY_UP, { key: '-' });\n    emitter.emit(CommonEvent.KEY_UP, { key: 'Control' });\n\n    expect(controlEqual).toHaveBeenCalledTimes(2);\n    expect(controlMinus).toHaveBeenCalledTimes(2);\n\n    // Test modifier key works at window level too\n    window.dispatchEvent(new KeyboardEvent(CommonEvent.KEY_DOWN, { key: 'Control' }));\n    emitter.emit(CommonEvent.KEY_DOWN, { key: '=' });\n    emitter.emit(CommonEvent.KEY_UP, { key: '=' });\n    emitter.emit(CommonEvent.KEY_DOWN, { key: '-' });\n    emitter.emit(CommonEvent.KEY_UP, { key: '-' });\n    window.dispatchEvent(new KeyboardEvent(CommonEvent.KEY_UP, { key: 'Control' }));\n\n    expect(controlEqual).toHaveBeenCalledTimes(3);\n    expect(controlMinus).toHaveBeenCalledTimes(3);\n\n    shortcut.unbind(['Control', '='], controlEqual);\n    shortcut.unbind(['Control', '-']);\n\n    emitter.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    emitter.emit(CommonEvent.KEY_DOWN, { key: '=' });\n    emitter.emit(CommonEvent.KEY_UP, { key: '=' });\n    emitter.emit(CommonEvent.KEY_DOWN, { key: '-' });\n    emitter.emit(CommonEvent.KEY_UP, { key: '-' });\n    emitter.emit(CommonEvent.KEY_UP, { key: 'Control' });\n\n    expect(controlEqual).toHaveBeenCalledTimes(3);\n    expect(controlMinus).toHaveBeenCalledTimes(3);\n  });\n\n  it('wheel', () => {\n    const wheel = jest.fn();\n    shortcut.bind(['Control', 'wheel'], wheel);\n\n    emitter.emit(CommonEvent.WHEEL, { deltaX: 0, deltaY: 10 });\n    expect(wheel).toHaveBeenCalledTimes(0);\n\n    emitter.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    emitter.emit(CommonEvent.WHEEL, { deltaX: 0, deltaY: 10 });\n    expect(wheel).toHaveBeenCalledTimes(1);\n    expect(wheel.mock.calls[0][0].deltaY).toBe(10);\n    emitter.emit(CommonEvent.KEY_UP, { key: 'Control' });\n  });\n\n  it('drag', () => {\n    const drag = jest.fn();\n    shortcut.bind(['drag'], drag);\n\n    emitter.emit(CommonEvent.DRAG, { deltaX: 10, deltaY: 0 });\n    expect(drag).toHaveBeenCalledTimes(1);\n    expect(drag.mock.calls[0][0].deltaX).toBe(10);\n    expect(drag.mock.calls[0][0].deltaY).toBe(0);\n\n    drag.mockClear();\n\n    // shift drag\n    emitter.emit(CommonEvent.KEY_DOWN, { key: 'Shift' });\n    emitter.emit(CommonEvent.DRAG, { deltaX: 10, deltaY: 0 });\n    emitter.emit(CommonEvent.KEY_UP, { key: 'Shift' });\n    expect(drag).toHaveBeenCalledTimes(0);\n\n    shortcut.unbindAll();\n    shortcut.bind(['Shift', 'drag'], drag);\n\n    emitter.emit(CommonEvent.KEY_DOWN, { key: 'Shift' });\n    emitter.emit(CommonEvent.DRAG, { deltaX: 10, deltaY: 0 });\n    emitter.emit(CommonEvent.KEY_UP, { key: 'Shift' });\n    expect(drag).toHaveBeenCalledTimes(1);\n    expect(drag.mock.calls[0][0].deltaX).toBe(10);\n    expect(drag.mock.calls[0][0].deltaY).toBe(0);\n  });\n\n  it('focus', () => {\n    const wheel = jest.fn();\n    shortcut.bind(['Control', 'wheel'], wheel);\n\n    emitter.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    emitter.emit(CommonEvent.WHEEL, { deltaX: 0, deltaY: 10 });\n    expect(wheel).toHaveBeenCalledTimes(1);\n\n    emitter.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    window.dispatchEvent(new FocusEvent('focus'));\n\n    // @ts-expect-error private property\n    expect(shortcut.recordKey.size).toBe(0);\n\n    emitter.emit(CommonEvent.KEY_DOWN, { key: 'Control' });\n    emitter.emit(CommonEvent.WHEEL, { deltaX: 0, deltaY: 10 });\n    expect(wheel).toHaveBeenCalledTimes(2);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/size.spec.ts",
    "content": "import { parseSize } from '@/src/utils/size';\n\ndescribe('size', () => {\n  it('parseSize', () => {\n    expect(parseSize()).toEqual([0, 0, 0]);\n    expect(parseSize(10)).toEqual([10, 10, 10]);\n    expect(parseSize([10, 20])).toEqual([10, 20, 10]);\n    expect(parseSize([10, 20, 30])).toEqual([10, 20, 30]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/state.spec.ts",
    "content": "import { statesOf } from '@/src/utils/state';\n\ndescribe('state', () => {\n  it('statesOf', () => {\n    expect(\n      statesOf({\n        id: 'node-1',\n      }),\n    ).toEqual([]);\n\n    expect(\n      statesOf({\n        id: 'node-1',\n        states: ['selected'],\n      }),\n    ).toEqual(['selected']);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/style.spec.ts",
    "content": "import { computeElementCallbackStyle, getSubShapeStyle, mergeOptions } from '@/src/utils/style';\n\ndescribe('style', () => {\n  it('computeElementCallbackStyle', () => {\n    const datum = {\n      id: 'node-1',\n      data: {\n        value: 100,\n      },\n      type: 'A',\n      style: {\n        fill: 'pink',\n        lineWidth: 5,\n      },\n    };\n\n    const style = {\n      stroke: 'blue',\n      size: (data: any) => data.data.value / 2,\n      fill: (data: any) => (data.data.type === 'B' ? 'green' : 'red'),\n    };\n\n    const computedStyle = computeElementCallbackStyle(style, { datum, graph: {} as any });\n\n    expect(computedStyle).toEqual({\n      stroke: 'blue',\n      size: 50,\n      fill: 'red',\n    });\n\n    const style1 = (data: any) => {\n      return {\n        fill: data.data.type === 'B' ? 'green' : 'red',\n      };\n    };\n\n    expect(computeElementCallbackStyle(style1, { datum, graph: {} as any })).toEqual({\n      fill: 'red',\n    });\n  });\n\n  it('mergeOptions', () => {\n    expect(\n      mergeOptions(\n        { style: { a: 1, b: [1, 2], c: { d: 1 } }, id: '1' },\n        { style: { a: 2, b: [2, 3], c: { f: 1 } }, id: '2' },\n      ),\n    ).toEqual({ style: { a: 2, b: [2, 3], c: { f: 1 } }, id: '2' });\n  });\n\n  it('getSubShapeStyle', () => {\n    const style = {\n      x: 100,\n      y: 100,\n      class: 'node',\n      transform: [['translate', 100, 100]],\n      zIndex: 100,\n      fill: 'pink',\n    };\n    expect(getSubShapeStyle(style)).toEqual({ fill: 'pink' });\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/symbol.spec.ts",
    "content": "import { circle, diamond, rect, simple, triangle, triangleRect, vee } from '@/src/utils/symbol';\n\ndescribe('Symbol Functions', () => {\n  describe('circle', () => {\n    it('should return the correct path for a circle', () => {\n      const path = circle(10, 10);\n      expect(path).toEqual([['M', -5, 0], ['A', 5, 5, 0, 1, 0, 5, 0], ['A', 5, 5, 0, 1, 0, -5, 0], ['Z']]);\n    });\n  });\n\n  describe('triangle', () => {\n    it('should return the correct path for a triangle', () => {\n      const path = triangle(10, 10);\n      expect(path).toEqual([['M', -5, 0], ['L', 5, -5], ['L', 5, 5], ['Z']]);\n    });\n  });\n\n  describe('diamond', () => {\n    it('should return the correct path for a diamond', () => {\n      const path = diamond(10, 10);\n      expect(path).toEqual([['M', -5, 0], ['L', 0, -5], ['L', 5, 0], ['L', 0, 5], ['Z']]);\n    });\n  });\n\n  describe('vee', () => {\n    it('should return the correct path for a vee', () => {\n      const path = vee(10, 10);\n      expect(path).toEqual([['M', -5, 0], ['L', 5, -5], ['L', 3, 0], ['L', 5, 5], ['Z']]);\n    });\n  });\n\n  describe('rect', () => {\n    it('should return the correct path for a rectangle', () => {\n      const path = rect(10, 10);\n      expect(path).toEqual([['M', -5, -5], ['L', 5, -5], ['L', 5, 5], ['L', -5, 5], ['Z']]);\n    });\n  });\n\n  describe('triangleRect', () => {\n    it('should return the correct path for a triangleRect', () => {\n      const path = triangleRect(10, 10);\n      expect(path).toEqual([\n        ['M', -5, 0],\n        ['L', 0, -5],\n        ['L', 0, 5],\n        ['Z'],\n        ['M', 3.571428571428571, -5],\n        ['L', 5, -5],\n        ['L', 5, 5],\n        ['L', 3.571428571428571, 5],\n        ['Z'],\n      ]);\n    });\n  });\n\n  describe('simple', () => {\n    it('should return the correct path for a simple shape', () => {\n      const path = simple(10, 10);\n      expect(path).toEqual([\n        ['M', 5, -5],\n        ['L', -5, 0],\n        ['L', 5, 0],\n        ['L', -5, 0],\n        ['L', 5, 5],\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/text.spec.ts",
    "content": "import { getWordWrapWidthWithBase } from '@/src/utils/text';\n\ndescribe('text', () => {\n  it('getWordWrapWidthWithBase', () => {\n    expect(getWordWrapWidthWithBase(10, 100)).toBe(100);\n    expect(getWordWrapWidthWithBase(10, '100%')).toBe(10);\n    expect(getWordWrapWidthWithBase(10, '100!')).toBe(20);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/theme.spec.ts",
    "content": "import { register } from '@/src/registry/register';\nimport { themeOf } from '@/src/utils/theme';\n\ndescribe('theme', () => {\n  it('themeOf', () => {\n    expect(themeOf({})).toEqual({});\n    expect(themeOf({ theme: 'null' })).toEqual({});\n\n    const theme = { node: {} };\n    register('theme', 'light', theme);\n\n    expect(themeOf({ theme: 'light' })).toBe(theme);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/traverse.spec.ts",
    "content": "import type { HierarchyStructure } from '@/src/utils/traverse';\nimport { bfs, dfs } from '@/src/utils/traverse';\n\nconst tree: HierarchyStructure<{ value: number }> = {\n  value: 1,\n  children: [\n    { value: 2, children: [{ value: 3, children: [{ value: 4 }] }] },\n    { value: 5, children: [{ value: 6 }] },\n  ],\n};\n\ndescribe('traverse', () => {\n  it('dfs TB', () => {\n    const result: number[] = [];\n\n    dfs(\n      tree,\n      (node) => {\n        result.push(node.value);\n      },\n      (node) => node.children,\n      'TB',\n    );\n\n    expect(result).toEqual([1, 2, 3, 4, 5, 6]);\n  });\n\n  it('dfs BT', () => {\n    const result: number[] = [];\n\n    dfs(\n      tree,\n      (node) => {\n        result.push(node.value);\n      },\n      (node) => node.children,\n      'BT',\n    );\n\n    expect(result).toEqual([4, 3, 2, 6, 5, 1]);\n  });\n\n  it('validate depth TB', () => {\n    const result: Record<number, number> = {};\n\n    dfs(\n      tree,\n      (node, depth) => {\n        result[node.value] = depth;\n      },\n      (node) => node.children,\n      'TB',\n    );\n\n    expect(result).toEqual({ 1: 0, 2: 1, 3: 2, 4: 3, 5: 1, 6: 2 });\n  });\n\n  it('validate depth BT', () => {\n    const result: Record<number, number> = {};\n\n    dfs(\n      tree,\n      (node, depth) => {\n        result[node.value] = depth;\n      },\n      (node) => node.children,\n      'BT',\n    );\n\n    expect(result).toEqual({ 1: 0, 2: 1, 3: 2, 4: 3, 5: 1, 6: 2 });\n  });\n\n  it('bfs', () => {\n    const result: Record<number, number> = {};\n\n    bfs(\n      tree,\n      (node, depth) => {\n        result[node.value] = depth;\n      },\n      (node) => node.children,\n    );\n\n    expect(result).toEqual({ 1: 0, 2: 1, 5: 1, 3: 2, 6: 2, 4: 3 });\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/tree.spec.ts",
    "content": "import { treeToGraphData } from '@/src/utils/tree';\n\ndescribe('tree', () => {\n  it('treeToGraphData', () => {\n    expect(\n      treeToGraphData({\n        id: 'root',\n        children: [{ id: 'child' }],\n      }),\n    ).toEqual({\n      nodes: [\n        {\n          id: 'root',\n          depth: 0,\n          children: ['child'],\n        },\n        { id: 'child', depth: 1 },\n      ],\n      edges: [{ source: 'root', target: 'child' }],\n    });\n\n    expect(\n      treeToGraphData({\n        id: 'root',\n        style: { fill: 'red' },\n        data: { value: 10 },\n        children: [{ id: 'child', style: { fill: 'green' }, data: { value: 1 } }],\n      }),\n    ).toEqual({\n      nodes: [\n        {\n          id: 'root',\n          children: ['child'],\n          depth: 0,\n          style: { fill: 'red' },\n          data: { value: 10 },\n        },\n        { id: 'child', depth: 1, style: { fill: 'green' }, data: { value: 1 } },\n      ],\n      edges: [{ source: 'root', target: 'child' }],\n    });\n\n    expect(\n      treeToGraphData(\n        {\n          id: 'root',\n          style: { fill: 'red' },\n          data: { value: 10 },\n          children: [{ id: 'child', style: { fill: 'green' }, data: { value: 1 } }],\n        },\n        {\n          getEdgeData: (source, target) => ({\n            source: source.id,\n            target: target.id,\n            data: { weight: source.data.value + target.data.value },\n          }),\n        },\n      ),\n    ).toEqual({\n      nodes: [\n        {\n          id: 'root',\n          children: ['child'],\n          depth: 0,\n          style: { fill: 'red' },\n          data: { value: 10 },\n        },\n        { id: 'child', depth: 1, style: { fill: 'green' }, data: { value: 1 } },\n      ],\n      edges: [{ source: 'root', target: 'child', data: { weight: 11 } }],\n    });\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/vector.spec.ts",
    "content": "import {\n  add,\n  angle,\n  cross,\n  distance,\n  divide,\n  dot,\n  exactEquals,\n  manhattanDistance,\n  mod,\n  multiply,\n  normalize,\n  perpendicular,\n  rad,\n  rotate,\n  scale,\n  subtract,\n  toVector2,\n  toVector3,\n} from '@/src/utils/vector';\n\ndescribe('Vector Functions', () => {\n  it('add', () => {\n    expect(add([0, 1], [2, 3])).toEqual([2, 4]);\n    expect(add([0, 1, 3], [2, 3, 4])).toEqual([2, 4, 7]);\n    expect(add([0, 1], [2, 3, 0])).toEqual([2, 4]);\n  });\n\n  it('subtract', () => {\n    expect(subtract([0, 1], [2, 3])).toEqual([-2, -2]);\n    expect(subtract([0, 1, 3], [2, 3, 4])).toEqual([-2, -2, -1]);\n    expect(subtract([0, 1], [2, 3, 0])).toEqual([-2, -2]);\n  });\n\n  it('multiply', () => {\n    expect(multiply([0, 1], [2, 3])).toEqual([0, 3]);\n    expect(multiply([0, 1, 3], [2, 3, 4])).toEqual([0, 3, 12]);\n    expect(multiply([0, 1], [2, 3, 0])).toEqual([0, 3]);\n    expect(multiply([0, 1], 2)).toEqual([0, 2]);\n    expect(multiply([0, 1, 3], 2)).toEqual([0, 2, 6]);\n  });\n\n  it('divide', () => {\n    expect(divide([0, 1], [2, 3])).toEqual([0, 1 / 3]);\n    expect(divide([0, 1, 3], [2, 3, 4])).toEqual([0, 1 / 3, 3 / 4]);\n    expect(divide([0, 1], 2)).toEqual([0, 0.5]);\n    expect(divide([0, 1, 3], 2)).toEqual([0, 0.5, 1.5]);\n  });\n\n  it('dot', () => {\n    expect(dot([0, 1], [2, 3])).toEqual(3);\n    expect(dot([0, 1, 0], [2, 3])).toEqual(3);\n    expect(dot([0, 1, 3], [2, 3, 4])).toEqual(15);\n  });\n\n  it('cross', () => {\n    expect(cross([0, 1], [-1, 0])).toEqual([0, -0, 1]);\n    expect(cross([0, 1], [2, 3])).toEqual([0, 0, -2]);\n    expect(cross([0, 1, 3], [2, 3, 4])).toEqual([-5, 6, -2]);\n  });\n\n  it('scale', () => {\n    expect(scale([0, 1], 2)).toEqual([0, 2]);\n    expect(scale([0, 1, 3], 2)).toEqual([0, 2, 6]);\n  });\n\n  it('distance', () => {\n    expect(distance([0, 0], [3, 4])).toEqual(5);\n    expect(distance([0, 0, 0], [3, 4])).toEqual(5);\n    expect(distance([0, 0, 0], [3, 4, 0])).toEqual(5);\n  });\n\n  it('manhattanDistance', () => {\n    expect(manhattanDistance([0, 0], [3, 4])).toEqual(7);\n    expect(manhattanDistance([0, 0, 0], [3, 4])).toEqual(7);\n    expect(manhattanDistance([0, 0, 0], [3, 4, 0])).toEqual(7);\n  });\n\n  it('normalize', () => {\n    expect(normalize([3, 4])).toEqual([0.6, 0.8]);\n    expect(normalize([3, 4, 0])).toEqual([0.6, 0.8, 0]);\n  });\n\n  it('angle', () => {\n    expect(angle([1, 0], [0, 1])).toEqual(Math.PI / 2);\n    expect(angle([1, 0, 0], [0, 1])).toEqual(Math.PI / 2);\n    expect(angle([1, 0], [-1, 0], true)).toEqual(Math.PI);\n    expect(angle([1, 0], [0, -1], true)).toEqual((Math.PI * 3) / 2);\n  });\n\n  it('exactEquals', () => {\n    expect(exactEquals([1, 2], [1, 2])).toEqual(true);\n    expect(exactEquals([1, 2], [1, 3])).toEqual(false);\n    expect(exactEquals([1, 2, 3], [1, 2, 3])).toEqual(true);\n    expect(exactEquals([1, 2, 3], [1, 2, 4])).toEqual(false);\n  });\n\n  it('perpendicular', () => {\n    expect(perpendicular([1, 1])).toEqual([-1, 1]);\n    expect(perpendicular([1, 1], false)).toEqual([1, -1]);\n  });\n\n  it('mode', () => {\n    expect(mod([1, 2], 2)).toEqual([1, 0]);\n    expect(mod([1, 2, 3], 2)).toEqual([1, 0, 1]);\n  });\n\n  it('toVector2', () => {\n    expect(toVector2([1, 2, 3])).toEqual([1, 2]);\n    expect(toVector2([1, 2])).toEqual([1, 2]);\n  });\n\n  it('toVector3', () => {\n    expect(toVector3([1, 2, 3])).toEqual([1, 2, 3]);\n    expect(toVector3([1, 2])).toEqual([1, 2, 0]);\n  });\n\n  it('rad', () => {\n    expect(rad([1, 0])).toEqual(0);\n    expect(rad([0, 1])).toEqual(Math.PI / 2);\n  });\n\n  it('rotate', () => {\n    expect(rotate([10, 10], 30)).toBeCloseTo([3.66, 13.66]);\n    expect(rotate([10, 20], 90)).toBeCloseTo([-20, 10]);\n    expect(rotate([10, 20], 180)).toBeCloseTo([-10, -20]);\n    expect(rotate([10, 20], 270)).toBeCloseTo([20, -10]);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/visibility.spec.ts",
    "content": "import { BaseShape } from '@/src';\nimport { setVisibility } from '@/src/utils/visibility';\nimport { Circle } from '@antv/g';\n\nclass Shape extends BaseShape<{ visibility: 'visible' | 'hidden' }> {\n  render() {\n    this.upsert('visibleShape', Circle, { r: 10 }, this);\n    this.upsert('hiddenShape', Circle, { r: 10, visibility: 'hidden' }, this);\n  }\n}\n\ndescribe('visibility', () => {\n  it('shape visibility', () => {\n    const shape = new Shape({});\n    const vShape = shape.getShape('visibleShape');\n    const hShape = shape.getShape('hiddenShape');\n\n    expect(shape.style.visibility).toBe(undefined);\n    expect(vShape.style.visibility).toBe(undefined);\n    expect(hShape.style.visibility).toBe('hidden');\n\n    setVisibility(shape, 'hidden');\n    expect(shape.style.visibility).toBe('hidden');\n    expect(vShape.style.visibility).toBe('hidden');\n    expect(hShape.style.visibility).toBe('hidden');\n\n    setVisibility(shape, 'visible');\n    expect(shape.style.visibility).toBe('visible');\n    expect(vShape.style.visibility).toBe('visible');\n    expect(hShape.style.visibility).toBe('visible');\n  });\n\n  it('default is hidden', () => {\n    const shape = new Shape({ style: { visibility: 'hidden' } });\n    const vShape = shape.getShape('visibleShape');\n    const hShape = shape.getShape('hiddenShape');\n\n    expect(shape.style.visibility).toBe('hidden');\n    expect(vShape.style.visibility).toBe('hidden');\n    expect(hShape.style.visibility).toBe('hidden');\n\n    setVisibility(shape, 'visible');\n    expect(shape.style.visibility).toBe('visible');\n    expect(vShape.style.visibility).toBe('visible');\n    expect(hShape.style.visibility).toBe('visible');\n  });\n\n  it('setVisibility', () => {\n    /**\n     * d: default\n     * v: visible\n     * h: hidden\n     *           d\n     *       /   |   \\\n     *    d      v     h\n     *   /|\\    /|\\   /|\\\n     *  d v h  d v h d b h\n     */\n\n    const root = new Circle({ id: 'root', style: { r: 10 } });\n    const l1 = root.appendChild(new Circle({ id: 'l1', style: { r: 10 } }));\n    const l2 = root.appendChild(new Circle({ id: 'l2', style: { r: 10, visibility: 'visible' } }));\n    const l3 = root.appendChild(new Circle({ id: 'l3', style: { r: 10, visibility: 'hidden' } }));\n    const l1_1 = l1.appendChild(new Circle({ id: 'l1_1', style: { r: 10 } }));\n    const l1_2 = l1.appendChild(new Circle({ id: 'l1_2', style: { r: 10, visibility: 'visible' } }));\n    const l1_3 = l1.appendChild(new Circle({ id: 'l1_3', style: { r: 10, visibility: 'hidden' } }));\n    const l2_1 = l2.appendChild(new Circle({ id: 'l2_1', style: { r: 10 } }));\n    const l2_2 = l2.appendChild(new Circle({ id: 'l2_2', style: { r: 10, visibility: 'visible' } }));\n    const l2_3 = l2.appendChild(new Circle({ id: 'l2_3', style: { r: 10, visibility: 'hidden' } }));\n    const l3_1 = l3.appendChild(new Circle({ id: 'l3_1', style: { r: 10 } }));\n    const l3_2 = l3.appendChild(new Circle({ id: 'l3_2', style: { r: 10, visibility: 'visible' } }));\n    const l3_3 = l3.appendChild(new Circle({ id: 'l3_3', style: { r: 10, visibility: 'hidden' } }));\n\n    const assertDefault = () => {\n      expect(root.style.visibility).toBe(undefined);\n      expect(l1.style.visibility).toBe(undefined);\n      expect(l2.style.visibility).toBe('visible');\n      expect(l3.style.visibility).toBe('hidden');\n      expect(l1_1.style.visibility).toBe(undefined);\n      expect(l1_2.style.visibility).toBe('visible');\n      expect(l1_3.style.visibility).toBe('hidden');\n      expect(l2_1.style.visibility).toBe(undefined);\n      expect(l2_2.style.visibility).toBe('visible');\n      expect(l2_3.style.visibility).toBe('hidden');\n      expect(l3_1.style.visibility).toBe(undefined);\n      expect(l3_2.style.visibility).toBe('visible');\n      expect(l3_3.style.visibility).toBe('hidden');\n    };\n\n    const assertHidden = () => {\n      expect(root.style.visibility).toBe('hidden');\n      expect(l1.style.visibility).toBe('hidden');\n      expect(l2.style.visibility).toBe('hidden');\n      expect(l3.style.visibility).toBe('hidden');\n      expect(l1_1.style.visibility).toBe('hidden');\n      expect(l1_2.style.visibility).toBe('hidden');\n      expect(l1_3.style.visibility).toBe('hidden');\n      expect(l2_1.style.visibility).toBe('hidden');\n      expect(l2_2.style.visibility).toBe('hidden');\n      expect(l2_3.style.visibility).toBe('hidden');\n      expect(l3_1.style.visibility).toBe('hidden');\n      expect(l3_2.style.visibility).toBe('hidden');\n      expect(l3_3.style.visibility).toBe('hidden');\n    };\n\n    const assertVisible = () => {\n      expect(root.style.visibility).toBe('visible');\n      expect(l1.style.visibility).toBe('visible');\n      expect(l2.style.visibility).toBe('visible');\n      expect(l3.style.visibility).toBe('visible');\n      expect(l1_1.style.visibility).toBe('visible');\n      expect(l1_2.style.visibility).toBe('visible');\n      expect(l1_3.style.visibility).toBe('visible');\n      expect(l2_1.style.visibility).toBe('visible');\n      expect(l2_2.style.visibility).toBe('visible');\n      expect(l2_3.style.visibility).toBe('visible');\n      expect(l3_1.style.visibility).toBe('visible');\n      expect(l3_2.style.visibility).toBe('visible');\n      expect(l3_3.style.visibility).toBe('visible');\n    };\n\n    assertDefault();\n\n    setVisibility(root, 'hidden');\n\n    assertHidden();\n\n    setVisibility(root, 'visible');\n\n    assertVisible();\n\n    setVisibility(root, 'hidden');\n\n    assertHidden();\n  });\n\n  it('setVisibility 2', () => {\n    const root = new Circle({ id: 'root', style: { r: 10 } });\n    const level1 = root.appendChild(new Circle({ id: 'level1', style: { r: 10 } }));\n    const level2 = level1.appendChild(new Circle({ id: 'level2', style: { r: 10 } }));\n\n    expect(root.style.visibility).toBe(undefined);\n    expect(level1.style.visibility).toBe(undefined);\n    expect(level2.style.visibility).toBe(undefined);\n\n    setVisibility(level1, 'hidden');\n    expect(root.style.visibility).toBe(undefined);\n    expect(level1.style.visibility).toBe('hidden');\n    expect(level2.style.visibility).toBe('hidden');\n\n    setVisibility(level1, 'visible');\n    expect(root.style.visibility).toBe(undefined);\n    expect(level1.style.visibility).toBe('visible');\n    expect(level2.style.visibility).toBe('visible');\n\n    setVisibility(root, 'hidden');\n    expect(root.style.visibility).toBe('hidden');\n    expect(level1.style.visibility).toBe('hidden');\n    expect(level2.style.visibility).toBe('hidden');\n\n    setVisibility(root, 'visible');\n    expect(root.style.visibility).toBe('visible');\n    expect(level1.style.visibility).toBe('visible');\n    expect(level2.style.visibility).toBe('visible');\n\n    setVisibility(level1, 'hidden');\n    expect(root.style.visibility).toBe('visible');\n    expect(level1.style.visibility).toBe('hidden');\n    expect(level2.style.visibility).toBe('hidden');\n\n    setVisibility(root, 'hidden');\n    expect(root.style.visibility).toBe('hidden');\n    expect(level1.style.visibility).toBe('hidden');\n    expect(level2.style.visibility).toBe('hidden');\n\n    setVisibility(root, 'visible');\n    expect(root.style.visibility).toBe('visible');\n    expect(level1.style.visibility).toBe('visible');\n    expect(level2.style.visibility).toBe('visible');\n\n    setVisibility(level1, 'visible');\n    expect(root.style.visibility).toBe('visible');\n    expect(level1.style.visibility).toBe('visible');\n    expect(level2.style.visibility).toBe('visible');\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/utils/z-index.spec.ts",
    "content": "import { getZIndexOf } from '@/src/utils/z-index';\n\ndescribe('z-index', () => {\n  it('getZIndexOf', () => {\n    expect(getZIndexOf({ id: 'node-1' })).toBe(0);\n    expect(getZIndexOf({ id: 'node-1', style: {} })).toBe(0);\n    expect(getZIndexOf({ id: 'node-1', style: { zIndex: 1 } })).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/unit/version.spec.ts",
    "content": "import pkg from '@/package.json';\nimport { version } from '@/src';\n\ndescribe('version', () => {\n  it('version', () => {\n    expect(version).toBe(pkg.version);\n  });\n});\n"
  },
  {
    "path": "packages/g6/__tests__/utils/canvas.ts",
    "content": "import type { Graph } from '@/src';\nimport { CustomEvent } from '@antv/g';\n\nexport function dispatchCanvasEvent(graph: Graph, type: string, data?: any) {\n  // @ts-expect-error private method\n  const canvas = graph.context.canvas.document;\n  canvas.dispatchEvent(new CustomEvent(type, data));\n}\n"
  },
  {
    "path": "packages/g6/__tests__/utils/create.ts",
    "content": "import { resetEntityCounter } from '@antv/g';\nimport { Renderer as CanvasRenderer } from '@antv/g-canvas';\nimport { Renderer as SVGRenderer } from '@antv/g-svg';\nimport { Renderer as WebGLRenderer } from '@antv/g-webgl';\nimport type { CanvasConfig, GraphOptions, Node, Point } from '@antv/g6';\nimport { Canvas, Circle, Graph } from '@antv/g6';\nimport { OffscreenCanvasContext } from './offscreen-canvas-context';\n\nfunction getRenderer(renderer: string) {\n  switch (renderer) {\n    case 'svg':\n      return new SVGRenderer();\n    case 'webgl':\n      return new WebGLRenderer();\n    case 'canvas':\n      return new CanvasRenderer();\n    default:\n      return new SVGRenderer();\n  }\n}\n\n/**\n * Create graph canvas with config.\n * @param dom - dom\n * @param width - width\n * @param height - height\n * @param renderer - render\n * @param options - options\n * @returns instance\n */\nexport function createGraphCanvas(\n  dom?: null | HTMLElement,\n  width: number = 500,\n  height: number = 500,\n  renderer: string = 'svg',\n  options?: Partial<CanvasConfig>,\n) {\n  const container = dom || document.createElement('div');\n\n  resetEntityCounter();\n\n  const extraOptions: Record<string, unknown> = {\n    ...options,\n  };\n\n  if (globalThis.process) {\n    const offscreenNodeCanvas = {\n      getContext: () => context,\n    } as unknown as HTMLCanvasElement;\n    const context = new OffscreenCanvasContext(offscreenNodeCanvas);\n    // 下列参数仅在 node 环境下需要传入 / These parameters only need to be passed in the node environment\n    Object.assign(extraOptions, {\n      document: container.ownerDocument,\n      offscreenCanvas: offscreenNodeCanvas,\n    });\n  }\n\n  const offscreenNodeCanvas = {\n    getContext: () => context,\n  } as unknown as HTMLCanvasElement;\n  const context = new OffscreenCanvasContext(offscreenNodeCanvas);\n\n  return new Canvas({\n    container,\n    width,\n    height,\n    renderer: () => getRenderer(renderer),\n    ...extraOptions,\n  });\n}\n\n/**\n * <zh/> 一个会连接到圆心的节点\n *\n * <en/> A node that will connect to the center\n */\nclass CenterConnectCircle extends Circle {\n  public getIntersectPoint(): Point {\n    const bounds = this.getShape('key').getBounds();\n    return bounds.center;\n  }\n}\n\nexport function createEdgeNode(point: Point): Node {\n  return new CenterConnectCircle({\n    style: {\n      x: point[0],\n      y: point[1],\n    },\n  });\n}\n\nexport async function createDemoGraph(demo: TestCase, context?: Partial<TestContext>): Promise<Graph> {\n  const container = createGraphCanvas(document.getElementById('container'));\n  return demo({ animation: false, container, theme: 'light', ...context });\n}\n\nexport function createGraph(options: GraphOptions) {\n  const container = createGraphCanvas(document.getElementById('container'));\n  return new Graph({\n    container,\n    animation: false,\n    theme: 'light',\n    ...options,\n  });\n}\n"
  },
  {
    "path": "packages/g6/__tests__/utils/dir.ts",
    "content": "import path from 'path';\n\n/**\n * <zh/> 获取快照目录\n *\n * <en/> Get snapshot directory\n * @param dir - __filename\n * @param detail - <zh/> 快照详情 | <en/> snapshot detail\n * @returns <zh/> 快照目录 | <en/> snapshot directory\n */\nexport function getSnapshotDir(dir: string, detail: string = 'default'): [string, string] {\n  const root = process.cwd();\n\n  const relativeDir = dir.replace(root, '');\n\n  const relativeDirSlices = relativeDir.split(path.sep);\n\n  const testsPartIdx = relativeDirSlices.findIndex((slice) => slice === '__tests__');\n  if (testsPartIdx !== -1) relativeDirSlices[testsPartIdx] = '';\n  const unitPartIdx = relativeDirSlices.findIndex((slice) => slice === 'unit');\n  if (unitPartIdx !== -1) relativeDirSlices[unitPartIdx] = '';\n\n  const outputDir = path.join(root, '__tests__', 'snapshots', ...relativeDirSlices).replace('.spec.ts', '');\n  return [outputDir, detail];\n}\n"
  },
  {
    "path": "packages/g6/__tests__/utils/dom.ts",
    "content": "import type { Graph } from '@/src';\nimport { CommonEvent } from '@/src';\n\nexport function emitWheelEvent(\n  graph: Graph,\n  options?: { deltaX: number; deltaY: number; clientX: number; clientY: number },\n) {\n  const dom = graph.getCanvas().getContextService().getDomElement();\n  dom?.dispatchEvent(new WheelEvent(CommonEvent.WHEEL, options));\n}\n"
  },
  {
    "path": "packages/g6/__tests__/utils/index.ts",
    "content": "export { dispatchCanvasEvent } from './canvas';\nexport { createDemoGraph, createEdgeNode, createGraph, createGraphCanvas } from './create';\nexport { createDeterministicRandom } from './random';\nexport { sleep } from './sleep';\n"
  },
  {
    "path": "packages/g6/__tests__/utils/offscreen-canvas-context.ts",
    "content": "// Computed as round(measureText(text).width * 10) at 10px system-ui. For\n// characters that are not represented in this map, we’d ideally want to use a\n// weighted average of what we expect to see. But since we don’t really know\n// what that is, using “e” seems reasonable.\nconst defaultWidthMap: Record<string, number> = {\n  a: 56,\n  b: 63,\n  c: 57,\n  d: 63,\n  e: 58,\n  f: 37,\n  g: 62,\n  h: 60,\n  i: 26,\n  j: 26,\n  k: 55,\n  l: 26,\n  m: 88,\n  n: 60,\n  o: 60,\n  p: 62,\n  q: 62,\n  r: 39,\n  s: 54,\n  t: 38,\n  u: 60,\n  v: 55,\n  w: 79,\n  x: 54,\n  y: 55,\n  z: 55,\n  A: 69,\n  B: 67,\n  C: 73,\n  D: 74,\n  E: 61,\n  F: 58,\n  G: 76,\n  H: 75,\n  I: 28,\n  J: 55,\n  K: 67,\n  L: 58,\n  M: 89,\n  N: 75,\n  O: 78,\n  P: 65,\n  Q: 78,\n  R: 67,\n  S: 65,\n  T: 65,\n  U: 75,\n  V: 69,\n  W: 98,\n  X: 69,\n  Y: 67,\n  Z: 67,\n  0: 64,\n  1: 48,\n  2: 62,\n  3: 64,\n  4: 66,\n  5: 63,\n  6: 65,\n  7: 58,\n  8: 65,\n  9: 65,\n  ' ': 29,\n  '!': 32,\n  '\"': 49,\n  \"'\": 31,\n  '(': 39,\n  ')': 39,\n  ',': 31,\n  '-': 48,\n  '.': 31,\n  '/': 32,\n  ':': 31,\n  ';': 31,\n  '?': 52,\n  '‘': 31,\n  '’': 31,\n  '“': 47,\n  '”': 47,\n  '…': 82,\n};\n\nexport function measureText(text: string, fontSize: number) {\n  let sum = 0;\n  for (let i = 0; i < text.length; i++) {\n    sum += ((defaultWidthMap[text[i]] ?? 100) * fontSize) / 100;\n  }\n  return sum;\n}\n\nexport class OffscreenCanvasContext {\n  private fontSize!: number;\n\n  constructor(public canvas: HTMLCanvasElement) {}\n\n  set font(font: string) {\n    // `${fontStyle} ${fontVariant} ${fontWeight} ${fontSizeString}\n    const [, , , fontSizeString] = font.split(' ');\n    const fontSize = parseFloat(fontSizeString.replace('px', ''));\n    this.fontSize = fontSize;\n  }\n\n  fillRect() {}\n  fillText() {}\n  getImageData(sx: number, sy: number, sw: number, sh: number) {\n    return {\n      // ignore ascent and descent\n      data: new Uint8ClampedArray(sw * sh * 4).fill(0),\n    };\n  }\n\n  measureText(text: string) {\n    return {\n      width: measureText(text, this.fontSize),\n      actualBoundingBoxAscent: 0,\n      actualBoundingBoxDescent: 0,\n      actualBoundingBoxLeft: 0,\n      actualBoundingBoxRight: 0,\n      fontBoundingBoxAscent: 0,\n      fontBoundingBoxDescent: 0,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/g6/__tests__/utils/random.ts",
    "content": "/**\n * Get random number(0 - 1) generator, with deterministic random number.\n * @returns Random number\n */\nexport function createDeterministicRandom() {\n  let i = 0;\n  return () => {\n    i++;\n    return (Math.E * i) % 1;\n  };\n}\n"
  },
  {
    "path": "packages/g6/__tests__/utils/sleep.ts",
    "content": "export function sleep(n: number) {\n  return new Promise((resolve) => {\n    setTimeout(resolve, n);\n  });\n}\n"
  },
  {
    "path": "packages/g6/__tests__/utils/svg-transformer.js",
    "content": "module.exports = {\n  process() {\n    return {\n      code: `module.exports = {};`,\n    };\n  },\n};\n"
  },
  {
    "path": "packages/g6/__tests__/utils/to-be-close-to.ts",
    "content": "type Digital = number | number[];\n\nfunction closeTo(received: number, expected: number, numDigits: number = 2) {\n  return Math.abs(received - expected) < 10 ** -numDigits / 2;\n}\n\nexport function toBeCloseTo(received: Digital, expected: Digital, numDigits?: number) {\n  const isSourceArray = Array.isArray(received);\n  const isTargetArray = Array.isArray(expected);\n\n  if (isSourceArray !== isTargetArray) return false;\n\n  if (isSourceArray && isTargetArray) {\n    return received.length === expected.length && received.every((n, i) => closeTo(n, expected[i], numDigits));\n  }\n\n  if (!isSourceArray && !isTargetArray) {\n    return closeTo(received, expected, numDigits);\n  }\n\n  return false;\n}\n\ndeclare global {\n  // eslint-disable-next-line @typescript-eslint/no-namespace\n  namespace jest {\n    interface Matchers<R> {\n      toBeCloseTo(expected: Digital, numDigits?: number): R;\n    }\n  }\n}\n\nexpect.extend({\n  toBeCloseTo: (received: Digital, expected: Digital, numDigits?: number) => {\n    const pass = toBeCloseTo(received, expected, numDigits);\n    return {\n      message: () => `expected: \\x1b[32m${received}\\n\\x1b[31mreceived: ${expected}\\x1b[0m`,\n      pass,\n    };\n  },\n});\n"
  },
  {
    "path": "packages/g6/__tests__/utils/to-match-svg-snapshot.ts",
    "content": "import type { Graph, IAnimateEvent } from '@/src';\nimport type { CanvasLayer } from '@/src/types';\nimport type { Canvas, IAnimation } from '@antv/g';\nimport chalk from 'chalk';\nimport { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { optimize } from 'svgo';\nimport { serializeToString } from 'xmlserializer';\nimport { getSnapshotDir } from './dir';\nimport { sleep } from './sleep';\n\nconst format = (svg: SVGElement) => {\n  return optimize(serializeToString(svg as any), {\n    js2svg: {\n      pretty: true,\n      indent: 2,\n    },\n    plugins: [\n      'cleanupIds',\n      'cleanupAttrs',\n      'sortAttrs',\n      'sortDefsChildren',\n      'removeUselessDefs',\n      {\n        name: 'convertPathData',\n        params: {\n          floatPrecision: 4,\n          forceAbsolutePath: true,\n\n          applyTransforms: false,\n          applyTransformsStroked: false,\n          straightCurves: false,\n          convertToQ: false,\n          lineShorthands: false,\n          convertToZ: false,\n          curveSmoothShorthands: false,\n          smartArcRounding: false,\n          removeUseless: false,\n          collapseRepeated: false,\n          utilizeAbsolute: false,\n          negativeExtraSpace: false,\n        },\n      },\n      {\n        name: 'convertTransform',\n        params: {\n          floatPrecision: 4,\n\n          convertToShorts: false,\n          matrixToTransform: false,\n          shortTranslate: false,\n          shortScale: false,\n          shortRotate: false,\n          removeUseless: false,\n          collapseIntoOne: false,\n        },\n      },\n      {\n        name: 'cleanupNumericValues',\n        params: {\n          floatPrecision: 4,\n        },\n      },\n    ],\n  }).data;\n};\n\nexport type ToMatchSVGSnapshotOptions = {\n  fileFormat?: string;\n};\n\n// @see https://jestjs.io/docs/26.x/expect#expectextendmatchers\nexport async function toMatchSVGSnapshot(\n  gCanvas: Record<CanvasLayer, Canvas>,\n  dir: string,\n  name: string,\n  options: ToMatchSVGSnapshotOptions = {},\n): Promise<{ message: () => string; pass: boolean }> {\n  await sleep(300);\n\n  const { fileFormat = 'svg' } = options;\n  const namePath = join(dir, name);\n  const actualPath = join(dir, `${name}-actual.${fileFormat}`);\n  const expectedPath = join(dir, `${name}.${fileFormat}`);\n\n  let actual: string = '';\n\n  // Clone <svg>\n  const svg = (gCanvas.main.getContextService().getDomElement() as unknown as SVGElement).cloneNode(true) as SVGElement;\n  const gRoot = svg.querySelector('#g-root');\n\n  // remove css style\n  svg.style.gridArea = '';\n\n  Object.entries(gCanvas).forEach(([key, gCanvas]) => {\n    if (key === 'main') return;\n    const dom = (gCanvas.getContextService().getDomElement() as unknown as SVGElement).cloneNode(true) as SVGElement;\n    // @ts-expect-error dom is SVGElement\n    gRoot?.append(...(dom.querySelector('#g-root')?.childNodes || []));\n  });\n\n  actual += svg ? format(svg) : '';\n\n  try {\n    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n    if (!existsSync(expectedPath)) {\n      if (process.env.CI === 'true') {\n        throw new Error(`Please generate golden image for ${namePath}`);\n      }\n      console.warn(`! generate ${namePath}`);\n      writeFileSync(expectedPath, actual);\n      return {\n        message: () => `generate ${namePath}`,\n        pass: true,\n      };\n    } else {\n      const expected = readFileSync(expectedPath, {\n        encoding: 'utf8',\n        flag: 'r',\n      });\n      if (actual === expected) {\n        if (existsSync(actualPath)) unlinkSync(actualPath);\n        return {\n          message: () => `match ${namePath}`,\n          pass: true,\n        };\n      }\n\n      // Perverse actual file.\n      if (actual) writeFileSync(actualPath, actual);\n\n      const formatPath = (p: string) => p.split('/g6/')[1];\n      return {\n        message: () =>\n          `mismatch: \\n expected: ${chalk.green(formatPath(expectedPath))}\\n received: ${chalk.red(formatPath(actualPath))}`,\n        pass: false,\n      };\n    }\n  } catch (e) {\n    return {\n      message: () => `${e}`,\n      pass: false,\n    };\n  }\n}\n\nexport async function toMatchSnapshot(\n  graph: Graph,\n  dir: string,\n  detail?: string,\n  options: ToMatchSVGSnapshotOptions = {},\n) {\n  return await toMatchSVGSnapshot(graph.getCanvas().getLayers(), ...getSnapshotDir(dir, detail), options);\n}\n\nexport async function toMatchAnimation(\n  graph: Graph,\n  dir: string,\n  frames: number[],\n  operation: () => void | Promise<void>,\n  detail = 'default',\n  options: ToMatchSVGSnapshotOptions = {},\n) {\n  const animationPromise = new Promise<IAnimation>((resolve) => {\n    graph.once<IAnimateEvent>('beforeanimate', (e) => {\n      resolve(e.animation!);\n    });\n  });\n\n  await operation();\n\n  const animation = await animationPromise;\n\n  animation.pause();\n\n  for (const frame of frames) {\n    animation.currentTime = frame;\n\n    await sleep(32);\n    const result = await toMatchSVGSnapshot(\n      graph.getCanvas().getLayers(),\n      ...getSnapshotDir(dir, `${detail}-${frame}`),\n      options,\n    );\n\n    if (!result.pass) {\n      return result;\n    }\n  }\n\n  return {\n    message: () => `match ${detail}`,\n    pass: true,\n  };\n}\n"
  },
  {
    "path": "packages/g6/__tests__/utils/use-snapshot-matchers.ts",
    "content": "import {\n  ToMatchSVGSnapshotOptions,\n  toMatchAnimation,\n  toMatchSVGSnapshot,\n  toMatchSnapshot,\n} from './to-match-svg-snapshot';\n\ndeclare global {\n  // eslint-disable-next-line @typescript-eslint/no-namespace\n  namespace jest {\n    interface Matchers<R> {\n      toMatchSVGSnapshot(dir: string, name: string, options?: ToMatchSVGSnapshotOptions): Promise<R>;\n      toMatchSnapshot(dir: string, detail?: string, options?: ToMatchSVGSnapshotOptions): Promise<R>;\n      toMatchAnimation(\n        dir: string,\n        frames: number[],\n        operation: () => void | Promise<void>,\n        detail?: string,\n        options?: ToMatchSVGSnapshotOptions,\n      ): Promise<R>;\n    }\n  }\n}\n\nexpect.extend({\n  toMatchSVGSnapshot,\n  toMatchSnapshot,\n  toMatchAnimation,\n});\n"
  },
  {
    "path": "packages/g6/jest.config.js",
    "content": "// Installing third-party modules by tnpm or cnpm will name modules with underscore as prefix.\n// In this case _{module} is also necessary.\nconst esm = ['internmap', 'd3-*', 'lodash-es', 'chalk'].map((d) => `_${d}|${d}`).join('|');\n\nmodule.exports = {\n  testTimeout: 30 * 1000,\n  testEnvironment: 'jsdom',\n  setupFilesAfterEnv: ['./__tests__/setup.ts'],\n  transform: {\n    '^.+\\\\.[tj]s$': [\n      '@swc/jest',\n      {\n        jsc: {\n          parser: {\n            syntax: 'typescript',\n            decorators: true,\n          },\n        },\n      },\n    ],\n    '^.+\\\\.svg$': ['<rootDir>/__tests__/utils/svg-transformer.js'],\n  },\n  collectCoverageFrom: ['src/**/*.ts'],\n  coveragePathIgnorePatterns: [\n    '<rootDir>/src/elements/nodes/html.ts',\n    '<rootDir>/src/plugins/minimap',\n    '<rootDir>/src/plugins/hull/(?!index\\\\.ts$).*',\n  ],\n  moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],\n  collectCoverage: true,\n  testRegex: '(/__tests__/.*\\\\.(test|spec))\\\\.(ts|tsx|js)$',\n  transformIgnorePatterns: [`<rootDir>/node_modules/.pnpm/(?!(${esm}))`],\n  testPathIgnorePatterns: ['/(lib|esm)/__tests__/'],\n  moduleNameMapper: {\n    '^@@/(.*)$': '<rootDir>/__tests__/$1',\n    '^@/(.*)$': '<rootDir>/$1',\n    '@antv/g6': '<rootDir>/src',\n  },\n};\n"
  },
  {
    "path": "packages/g6/package.json",
    "content": "{\n  \"name\": \"@antv/g6\",\n  \"version\": \"5.1.0\",\n  \"description\": \"A Graph Visualization Framework in JavaScript\",\n  \"keywords\": [\n    \"antv\",\n    \"g6\",\n    \"graph\",\n    \"graph analysis\",\n    \"graph editor\",\n    \"graph visualization\",\n    \"relational data\"\n  ],\n  \"homepage\": \"https://g6.antv.antgroup.com/\",\n  \"bugs\": {\n    \"url\": \"https://github.com/antvis/g6/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/antvis/g6\"\n  },\n  \"license\": \"MIT\",\n  \"author\": \"https://github.com/orgs/antvis/people\",\n  \"main\": \"lib/index.js\",\n  \"module\": \"esm/index.js\",\n  \"types\": \"lib/index.d.ts\",\n  \"files\": [\n    \"src\",\n    \"esm\",\n    \"lib\",\n    \"dist\",\n    \"README\"\n  ],\n  \"scripts\": {\n    \"build\": \"run-p build:*\",\n    \"build:cjs\": \"rimraf ./lib && tsc --module commonjs --outDir lib -p tsconfig.build.json\",\n    \"build:dev:watch\": \"npm run build:esm -- --watch\",\n    \"build:esm\": \"rimraf ./esm && tsc --module ESNext --outDir esm -p tsconfig.build.json\",\n    \"build:umd\": \"rimraf ./dist && rollup -c && npm run size\",\n    \"bundle-vis\": \"cross-env BUNDLE_VIS=1 npm run build:umd\",\n    \"ci\": \"run-s lint type-check build test\",\n    \"coverage\": \"jest --coverage\",\n    \"coverage:open\": \"open coverage/lcov-report/index.html\",\n    \"dev\": \"vite\",\n    \"fix\": \"eslint ./src ./__tests__ --fix && prettier ./src __tests__ --write \",\n    \"jest\": \"node --expose-gc --max-old-space-size=1024 --unhandled-rejections=strict ../../node_modules/jest/bin/jest --coverage --logHeapUsage --detectOpenHandles\",\n    \"lint\": \"eslint ./src __tests__ --quiet && prettier ./src __tests__ --check\",\n    \"prepublishOnly\": \"run-s ci readme\",\n    \"readme\": \"cp ../../README.* ./\",\n    \"size\": \"limit-size\",\n    \"start\": \"rimraf ./lib && tsc --module commonjs --outDir lib --watch\",\n    \"tag\": \"node ./scripts/tag.mjs\",\n    \"test\": \"jest\",\n    \"test:integration\": \"npm run jest __tests__/integration\",\n    \"test:unit\": \"npm run jest __tests__/unit\",\n    \"type-check\": \"tsc --noEmit\",\n    \"version\": \"node ./scripts/version.mjs\"\n  },\n  \"dependencies\": {\n    \"@antv/algorithm\": \"^0.1.26\",\n    \"@antv/component\": \"^2.1.7\",\n    \"@antv/event-emitter\": \"^0.1.3\",\n    \"@antv/g\": \"^6.1.28\",\n    \"@antv/g-canvas\": \"^2.0.48\",\n    \"@antv/g-plugin-dragndrop\": \"^2.0.38\",\n    \"@antv/graphlib\": \"^2.0.4\",\n    \"@antv/hierarchy\": \"^0.7.1\",\n    \"@antv/layout\": \"^2.0.0\",\n    \"@antv/util\": \"^3.3.11\",\n    \"bubblesets-js\": \"^2.3.4\"\n  },\n  \"devDependencies\": {\n    \"@antv/g-svg\": \"^2.0.42\",\n    \"@antv/g-webgl\": \"^2.0.52\",\n    \"@antv/layout-gpu\": \"^1.1.7\",\n    \"@antv/layout-wasm\": \"^1.4.2\",\n    \"@antv/vendor\": \"^1.0.11\",\n    \"@types/hull.js\": \"^1.0.4\",\n    \"@types/xmlserializer\": \"^0.6.6\",\n    \"cross-env\": \"^7.0.3\",\n    \"jest-canvas-mock\": \"^2.5.2\",\n    \"jest-random-mock\": \"^1.0.0\",\n    \"xmlserializer\": \"^0.6.1\"\n  },\n  \"publishConfig\": {\n    \"registry\": \"https://registry.npmjs.org/\"\n  },\n  \"limit-size\": [\n    {\n      \"gzip\": true,\n      \"limit\": \"400 Kb\",\n      \"path\": \"dist/g6.min.js\"\n    },\n    {\n      \"limit\": \"1.5 Mb\",\n      \"path\": \"dist/g6.min.js\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/g6/perf.config.js",
    "content": "import { defineConfig } from 'iperf';\nimport path from 'path';\n\nexport default defineConfig({\n  perf: {\n    report: {\n      dir: './__tests__/perf-report',\n    },\n  },\n  resolve: {\n    alias: {\n      '@antv/g6': path.resolve(__dirname, './src'),\n    },\n  },\n});\n"
  },
  {
    "path": "packages/g6/rollup.config.mjs",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport terser from '@rollup/plugin-terser';\nimport typescript from '@rollup/plugin-typescript';\nimport nodePolyfills from 'rollup-plugin-polyfill-node';\nimport { visualizer } from 'rollup-plugin-visualizer';\n\nconst isBundleVis = !!process.env.BUNDLE_VIS;\n\nexport default [\n  {\n    input: 'src/index.ts',\n    output: {\n      file: 'dist/g6.min.js',\n      name: 'G6',\n      format: 'umd',\n      sourcemap: false,\n    },\n    plugins: [\n      nodePolyfills(),\n      resolve(),\n      commonjs(),\n      typescript({\n        tsconfig: 'tsconfig.build.json',\n      }),\n      terser(),\n      ...(isBundleVis ? [visualizer()] : []),\n    ],\n  },\n];\n"
  },
  {
    "path": "packages/g6/scripts/tag.mjs",
    "content": "import chalk from 'chalk';\nimport { execFileSync } from 'child_process';\nimport { readFileSync } from 'fs';\nimport open from 'open';\nimport readline from 'readline';\n\nconst pkg = JSON.parse(readFileSync('./package.json', 'utf-8'));\nconst version = pkg.version;\nconst repository = pkg.repository.url;\n\nconsole.log(chalk.yellow('The tag will be created with the version: '), chalk.bold(chalk.green(version)));\n\nconst rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n\nrl.question('Do you want to continue? (y/n): ', (answer) => {\n  if (answer === 'y') {\n    execFileSync('git', ['tag', version]);\n    execFileSync('git', ['push', 'origin', version]);\n    open(`${repository}/releases/new`);\n  }\n  rl.close();\n});\n"
  },
  {
    "path": "packages/g6/scripts/version.mjs",
    "content": "import { readFileSync, writeFileSync } from 'fs';\n\nconst pkg = readFileSync('./package.json', 'utf-8');\nconst version = JSON.parse(pkg).version;\nwriteFileSync('./src/version.ts', `export const version = '${version}';\\n`, 'utf-8');\n"
  },
  {
    "path": "packages/g6/src/animations/executor.ts",
    "content": "import type { DisplayObject, IAnimation } from '@antv/g';\nimport { upperFirst } from '@antv/util';\nimport { createAnimationsProxy, inferDefaultValue, preprocessKeyframes } from '../utils/animation';\nimport { replaceTranslateInTransform } from '../utils/transform';\nimport type { AnimationExecutor } from './types';\n\n/**\n * <zh/> 动画语法执行器\n *\n * <en/> Animation syntax executor\n * @param element - <zh/> 要执行动画的图形 | <en/> shape to execute animation\n * @param keyframes - <zh/> 动画关键帧 | <en/> animation keyframes\n * @param options - <zh/> 动画语法 | <en/> animation syntax\n * @returns <zh/> 动画实例 | <en/> animation instance\n */\nexport const executor: AnimationExecutor = (element, keyframes, options) => {\n  if (!options.length) return null;\n\n  const [originalStyle, modifiedStyle] = keyframes;\n  /**\n   * <zh/> 获取图形关键帧样式\n   *\n   * <en/> Get the keyframe style of the shape\n   * @param shapeID - <zh/> 图形 ID | <en/> shape ID\n   * @returns <zh/> 图形关键帧样式 | <en/> keyframe style of the shape\n   */\n  const getKeyframeStyle = (\n    shapeID?: string,\n  ): { shape: DisplayObject; fromStyle: Record<string, any>; toStyle: Record<string, any> } | null => {\n    if (shapeID) {\n      const shape = element.getShape(shapeID);\n      if (!shape) return null;\n\n      const name = `get${upperFirst(shapeID)}Style` as keyof typeof element;\n\n      const styler: (attrs?: Record<string, unknown>) => Record<string, unknown> =\n        element?.[name]?.bind(element) || ((attrs) => attrs);\n\n      const fromStyle = styler?.(originalStyle) || {};\n      const toStyle = styler?.(modifiedStyle) || {};\n\n      return { shape, fromStyle, toStyle };\n    } else {\n      const shape = element;\n      return { shape, fromStyle: originalStyle, toStyle: modifiedStyle };\n    }\n  };\n\n  let mainResult: IAnimation;\n\n  const subResults = options\n    .map(({ fields, shape: shapeID, states: enabledStates, ...effectTiming }) => {\n      const keyframeStyle = getKeyframeStyle(shapeID);\n      if (!keyframeStyle) return null;\n\n      const { shape, fromStyle, toStyle } = keyframeStyle;\n\n      const keyframes: Keyframe[] = [{}, {}];\n\n      fields.forEach((attr) => {\n        Object.assign(keyframes[0], { [attr]: fromStyle[attr] ?? inferDefaultValue(attr) });\n        Object.assign(keyframes[1], { [attr]: toStyle[attr] ?? inferDefaultValue(attr) });\n      });\n\n      // x/y -> translate\n      if (keyframes.some((keyframe) => Object.keys(keyframe).some((attr) => ['x', 'y', 'z'].includes(attr)))) {\n        const { x = 0, y = 0, z, transform = '' } = shape.attributes || {};\n        keyframes.forEach((keyframe) => {\n          // @ts-expect-error ignore type error\n          keyframe.transform = replaceTranslateInTransform(\n            (keyframe.x as number) ?? (x as number),\n            (keyframe.y as number) ?? (y as number),\n            (keyframe.z as number) ?? (z as number),\n            transform,\n          );\n        });\n      }\n\n      const result = shape.animate(preprocessKeyframes(keyframes), effectTiming);\n\n      if (shapeID === undefined) mainResult = result!;\n\n      return result;\n    })\n    .filter(Boolean) as IAnimation[];\n\n  const result = mainResult! || subResults?.[0];\n\n  if (!result) return null;\n\n  return createAnimationsProxy(\n    result,\n    subResults.filter((result) => result !== result),\n  );\n};\n"
  },
  {
    "path": "packages/g6/src/animations/index.ts",
    "content": "/**\n * <zh/> 内置的动画元素。\n * <en/> Built-in animations.\n */\nexport { executor } from './executor';\n\nexport const Fade = [{ fields: ['opacity'] }];\n\nexport const Translate = [{ fields: ['x', 'y'] }];\n\nexport const NodeCollapse = [{ fields: ['x', 'y'] }];\n\nexport const NodeExpand = NodeCollapse;\n\nexport const PathIn = [{ fields: ['sourceNode', 'targetNode'] }];\n\nexport const PathOut = PathIn;\n\nexport const ComboCollapse = [{ fields: ['childrenNode', 'x', 'y'] }];\n\nexport const ComboExpand = ComboCollapse;\n\nexport const ComboCollapseExpand = [{ fields: ['childrenNode', 'x', 'y'] }];\n"
  },
  {
    "path": "packages/g6/src/animations/types.ts",
    "content": "import type { IAnimation } from '@antv/g';\nimport type { AnimationStage } from '../spec/element/animation';\nimport type { Element, ElementType, State } from '../types';\n\nexport type STDAnimation = AnimationOptions[];\n\n/**\n * <zh/> 元素动画选项\n *\n * <en/> Element animation options\n */\nexport interface AnimationOptions extends AnimationEffectTiming {\n  /**\n   * <zh/> 执行动画的字段（样式）名\n   *\n   * <en/> Field (style) name of the animation\n   */\n  fields: string[];\n  /**\n   * <zh/> 执行动画的图形，默认为当前元素\n   *\n   * <en/> Shape of the animation, default is the current element\n   */\n  shape?: string;\n  /**\n   * <zh/> 参与动画的状态\n   *\n   * <en/> States involved in the animation\n   */\n  states?: State[];\n}\n\nexport interface AnimationContext {\n  /**\n   * <zh/> 执行动画的元素\n   *\n   * <en/> Element to execute animation\n   */\n  element: Element;\n  /**\n   * <zh/> 元素类型\n   *\n   * <en/> Element type\n   */\n  elementType: ElementType;\n  /**\n   * <zh/> 动画阶段\n   *\n   * <en/> Animation stage\n   */\n  stage: AnimationStage;\n  /**\n   * <zh/> 动画的源样式\n   *\n   * <en/> Source style of animation\n   * @remarks\n   * <zh/> 用于在动画执行前将 shape 的样式设置为源样式，例如 move-to 动画，需要将 shape 的 x, y 设置为源样式\n   *\n   * <en/> Used to set the style of shape to the source style before the animation is executed. For example, the move-to animation needs to set the x and y of shape to the source style\n   */\n  originalStyle: Record<string, unknown>;\n  /**\n   * <zh/> 额外的动画终态样式\n   *\n   * <en/> Additional animation final state style\n   * @remarks\n   * <zh/> 例如元素销毁前，需要将元素的终态透明度设置为 0\n   *\n   * <en/> For example, before the element is destroyed, the final state opacity of the element needs to be set to 0\n   */\n  modifiedStyle?: Record<string, unknown>;\n  /**\n   * <zh/> 变更样式\n   *\n   * <en/> Updated style\n   */\n  updatedStyle?: Record<string, unknown>;\n}\n\n/**\n * <zh/> 动画效果时序\n *\n * <en/> Animation effect timing\n */\nexport interface AnimationEffectTiming {\n  /**\n   * <zh/> 动画延迟时间\n   *\n   * <en/> Animation delay time\n   */\n  delay?: number;\n  /**\n   * <zh/> 动画方向\n   *\n   * <en/> Animation direction\n   */\n  direction?: PlaybackDirection;\n  /**\n   * <zh/> 动画持续时间\n   *\n   * <en/> Animation duration\n   */\n  duration?: number;\n  /**\n   * <zh/> 动画缓动函数\n   *\n   * <en/> Animation easing function\n   */\n  easing?: string;\n  /**\n   * <zh/> 动画结束后的填充模式\n   *\n   * <en/> Fill mode after the animation ends\n   */\n  fill?: FillMode;\n  /**\n   * <zh/> 动画迭代次数\n   *\n   * <en/> Number of iterations of the animation\n   */\n  iterations?: number;\n}\n\nexport type AnimationExecutor = (\n  element: Element,\n  keyframes: [Record<string, unknown>, Record<string, unknown>],\n  options: STDAnimation,\n) => IAnimation | null;\n"
  },
  {
    "path": "packages/g6/src/behaviors/auto-adapt-label.ts",
    "content": "import { AABB } from '@antv/g';\nimport { groupBy, isFunction, throttle } from '@antv/util';\nimport { GraphEvent } from '../constants';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { ComboData, EdgeData, NodeData } from '../spec';\nimport type { Element, ElementDatum, ID, IEvent, Node, NodeCentralityOptions, Padding } from '../types';\nimport { getExpandedBBox } from '../utils/bbox';\nimport { getNodeCentralities } from '../utils/centrality';\nimport { arrayDiff } from '../utils/diff';\nimport { setVisibility } from '../utils/visibility';\nimport type { BaseBehaviorOptions } from './base-behavior';\nimport { BaseBehavior } from './base-behavior';\n\n/**\n * <zh/> 标签自适应显示配置项\n *\n * <en/> Auto Adapt Label Options\n */\nexport interface AutoAdaptLabelOptions extends BaseBehaviorOptions {\n  /**\n   * <zh/> 是否启用\n   *\n   * <en/> Whether to enable\n   * @defaultValue `true`\n   */\n  enable?: boolean | ((event: IEvent) => boolean);\n  /**\n   * <zh/> 根据元素的重要性从高到低排序，重要性越高的元素其标签显示优先级越高。一般情况下 combo > node > edge\n   *\n   * <en/> Sort elements by their importance in descending order; elements with higher importance have higher label display priority; usually combo > node > edge\n   */\n  sort?: (elementA: ElementDatum, elementB: ElementDatum) => -1 | 0 | 1;\n  /**\n   * <zh/> 根据节点的重要性从高到低排序，重要性越高的节点其标签显示优先级越高。内置几种中心性算法，也可以自定义排序函数。需要注意，如果设置了 `sort`，则 `sortNode` 不会生效\n   *\n   * <en/> Sort nodes by importance in descending order; nodes with higher importance have higher label display priority. Several centrality algorithms are built in, and custom sorting functions can also be defined. It should be noted that if `sort` is set, `sortNode` will not take effect\n   * @defaultValue { type: 'degree' }\n   */\n  sortNode?: NodeCentralityOptions | ((nodeA: NodeData, nodeB: NodeData) => -1 | 0 | 1);\n  /**\n   * <zh/> 根据边的重要性从高到低排序，重要性越高的边其标签显示优先级越高。默认按照数据先后进行排序。需要注意，如果设置了 `sort`，则 `sortEdge` 不会生效\n   *\n   * <en/> Sort edges by importance in descending order; edges with higher importance have higher label display priority. By default, they are sorted according to the data. It should be noted that if `sort` is set, `sortEdge` will not take effect\n   */\n  sortEdge?: (edgeA: EdgeData, edgeB: EdgeData) => -1 | 0 | 1;\n  /**\n   * <zh/> 根据群组的重要性从高到低排序，重要性越高的群组其标签显示优先级越高。默认按照数据先后进行排序。需要注意，如果设置了 `sort`，则 `sortCombo` 不会生效\n   *\n   * <en/> Sort combos by importance in descending order; combos with higher importance have higher label display priority. By default, they are sorted according to the data. It should be noted that if `sort` is set, `sortCombo` will not take effect\n   */\n  sortCombo?: (comboA: ComboData, comboB: ComboData) => -1 | 0 | 1;\n  /**\n   * <zh/> 设置标签的内边距，用于判断标签是否重叠，以避免标签显示过于密集\n   *\n   * <en/> Set the padding of the label to determine whether the label overlaps to avoid the label being displayed too densely\n   * @defaultValue 0\n   */\n  padding?: Padding;\n  /**\n   * <zh/> 节流时间\n   *\n   * <en/> Throttle time\n   * @defaultValue 32\n   */\n  throttle?: number;\n}\n\n/**\n * <zh/> 标签自适应显示\n *\n * <en/> Auto Adapt Label\n * @remarks\n * <zh/> 标签自适应显示是一种动态标签管理策略，旨在根据当前可视范围的空间分配、节点重要性等因素，智能调整哪些标签应显示或隐藏。通过对可视区域的实时分析，确保用户在不同的交互场景下获得最相关最清晰的信息展示，同时避免视觉过载和信息冗余。\n *\n * <en/ >Label Adaptive Display is a dynamic label management strategy designed to intelligently adjust which labels should be shown or hidden based on factors such as the spatial allocation of the current viewport and node importance. By analyzing the visible area in real-time, it ensures that users receive the most relevant and clear information display in various interactive scenarios, while avoiding visual overload and information redundancy.\n */\nexport class AutoAdaptLabel extends BaseBehavior<AutoAdaptLabelOptions> {\n  static defaultOptions: Partial<AutoAdaptLabelOptions> = {\n    enable: true,\n    throttle: 100,\n    padding: 0,\n    sortNode: { type: 'degree' },\n  };\n\n  constructor(context: RuntimeContext, options: AutoAdaptLabelOptions) {\n    super(context, Object.assign({}, AutoAdaptLabel.defaultOptions, options));\n    this.bindEvents();\n  }\n\n  public update(options: Partial<AutoAdaptLabelOptions>): void {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n    this.onToggleVisibility({} as IEvent);\n  }\n\n  /**\n   * <zh/> 检查当前包围盒是否有足够的空间进行展示；如果与已经展示的包围盒有重叠，则不会展示\n   *\n   * <en/> Check whether the current bounding box has enough space to display; if it overlaps with the displayed bounding box, it will not be displayed\n   * @param bbox - bbox\n   * @param bboxes - occupied bboxes which are already shown\n   * @returns whether the bbox is overlapping with the bboxes or outside the viewpointBounds\n   */\n  private isOverlapping = (bbox: AABB, bboxes: AABB[]) => {\n    return bboxes.some((b) => bbox.intersects(b));\n  };\n\n  private occupiedBounds: AABB[] = [];\n\n  private detectLabelCollision = (elements: Element[]): { show: Element[]; hide: Element[] } => {\n    const viewport = this.context.viewport!;\n    const res: { show: Element[]; hide: Element[] } = { show: [], hide: [] };\n    this.occupiedBounds = [];\n\n    elements.forEach((element) => {\n      const labelBounds = element.getShape('label').getRenderBounds();\n      if (viewport.isInViewport(labelBounds, true) && !this.isOverlapping(labelBounds, this.occupiedBounds)) {\n        res.show.push(element);\n        this.occupiedBounds.push(getExpandedBBox(labelBounds, this.options.padding));\n      } else {\n        res.hide.push(element);\n      }\n    });\n    return res;\n  };\n\n  private getLabelElements(): Element[] {\n    // @ts-expect-error access private property\n    const { elementMap } = this.context.element;\n    const elements: Element[] = [];\n\n    for (const key in elementMap) {\n      const element = elementMap[key];\n      if (element.isVisible() && element.getShape('label')) {\n        elements.push(element);\n      }\n    }\n\n    return elements;\n  }\n\n  private getLabelElementsInView(): Element[] {\n    const viewport = this.context.viewport!;\n    return this.getLabelElements().filter((node) => viewport.isInViewport(node.getShape('key').getRenderBounds()));\n  }\n\n  private hideLabelIfExceedViewport = (prevElementsInView: Element[], currentElementsInView: Element[]) => {\n    const { exit } = arrayDiff<Element>(prevElementsInView, currentElementsInView, (d) => d.id);\n    exit?.forEach(this.hideLabel);\n  };\n\n  private nodeCentralities: Map<ID, number> = new Map();\n\n  private sortNodesByCentrality = (nodes: Node[], centrality: NodeCentralityOptions) => {\n    const { model } = this.context;\n    const graphData = model.getData();\n    const getRelatedEdgesData = model.getRelatedEdgesData.bind(model);\n\n    const nodesWithCentrality = nodes.map((node) => {\n      if (!this.nodeCentralities.has(node.id)) {\n        this.nodeCentralities = getNodeCentralities(graphData, getRelatedEdgesData, centrality);\n      }\n      return { node, centrality: this.nodeCentralities.get(node.id)! };\n    });\n    return nodesWithCentrality.sort((a, b) => b.centrality - a.centrality).map((item) => item.node);\n  };\n\n  protected sortLabelElementsInView = (labelElements: Element[]): Element[] => {\n    const { sort, sortNode, sortCombo, sortEdge } = this.options;\n    const { model } = this.context;\n\n    if (isFunction(sort))\n      return labelElements.sort((a, b) => sort(model.getElementDataById(a.id), model.getElementDataById(b.id)));\n\n    const { node: nodes = [], edge: edges = [], combo: combos = [] } = groupBy(labelElements, (el) => (el as any).type);\n\n    const sortedCombos = isFunction(sortCombo)\n      ? combos.sort((a, b) => sortCombo(...(model.getComboData([a.id, b.id]) as [ComboData, ComboData])))\n      : combos;\n\n    const sortedNodes = isFunction(sortNode)\n      ? nodes.sort((a, b) => sortNode(...(model.getNodeData([a.id, b.id]) as [NodeData, NodeData])))\n      : this.sortNodesByCentrality(nodes as Node[], sortNode);\n\n    const sortedEdges = isFunction(sortEdge)\n      ? edges.sort((a, b) => sortEdge(...(model.getEdgeData([a.id, b.id]) as [EdgeData, EdgeData])))\n      : edges;\n\n    return [...sortedCombos, ...sortedNodes, ...sortedEdges];\n  };\n\n  private labelElementsInView: Element[] = [];\n\n  private isFirstRender = true;\n\n  protected onToggleVisibility = (event: IEvent) => {\n    // @ts-expect-error missing type\n    if (event.data?.stage === 'zIndex') return;\n\n    if (!this.validate(event)) {\n      if (this.hiddenElements.size > 0) {\n        this.hiddenElements.forEach(this.showLabel);\n        this.hiddenElements.clear();\n      }\n      return;\n    }\n\n    const labelElementsInView = this.isFirstRender ? this.getLabelElements() : this.getLabelElementsInView();\n    this.hideLabelIfExceedViewport(this.labelElementsInView, labelElementsInView);\n    this.labelElementsInView = labelElementsInView;\n\n    // 根据元素的重要性从高到低排序，重要性越高的元素其标签显示优先级越高；通常 combo > node > edge\n    // Sort elements by their importance in descending order; elements with higher importance have higher label display priority; usually combo > node > edge\n    const sortedElements = this.sortLabelElementsInView(this.labelElementsInView);\n    const { show, hide } = this.detectLabelCollision(sortedElements);\n\n    for (let i = show.length - 1; i >= 0; i--) {\n      this.showLabel(show[i]);\n    }\n    hide.forEach(this.hideLabel);\n  };\n\n  private hiddenElements: Map<ID, Element> = new Map();\n\n  private hideLabel = (element: Element) => {\n    const label = element.getShape('label');\n    if (label) setVisibility(label, 'hidden');\n    this.hiddenElements.set(element.id, element);\n  };\n\n  private showLabel = (element: Element) => {\n    const label = element.getShape('label');\n    if (label) setVisibility(label, 'visible');\n    element.toFront();\n    this.hiddenElements.delete(element.id);\n  };\n\n  protected onTransform = throttle(this.onToggleVisibility, this.options.throttle, { leading: true }) as () => void;\n\n  private enableToggle = true;\n\n  private toggle = (event: IEvent) => {\n    if (!this.enableToggle) return;\n    this.onToggleVisibility(event);\n  };\n\n  private onBeforeRender = () => {\n    this.enableToggle = false;\n  };\n\n  private onAfterRender = (event: IEvent) => {\n    this.onToggleVisibility(event);\n    this.enableToggle = true;\n  };\n\n  private bindEvents() {\n    const { graph } = this.context;\n    graph.on(GraphEvent.BEFORE_RENDER, this.onBeforeRender);\n    graph.on(GraphEvent.AFTER_RENDER, this.onAfterRender);\n    graph.on(GraphEvent.AFTER_DRAW, this.toggle);\n    graph.on(GraphEvent.AFTER_LAYOUT, this.toggle);\n    graph.on(GraphEvent.AFTER_TRANSFORM, this.onTransform);\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n    graph.off(GraphEvent.BEFORE_RENDER, this.onBeforeRender);\n    graph.off(GraphEvent.AFTER_RENDER, this.onAfterRender);\n    graph.off(GraphEvent.AFTER_DRAW, this.toggle);\n    graph.off(GraphEvent.AFTER_LAYOUT, this.toggle);\n    graph.off(GraphEvent.AFTER_TRANSFORM, this.onTransform);\n  }\n\n  private validate(event: IEvent): boolean {\n    if (this.destroyed) return false;\n    const { enable } = this.options;\n    if (isFunction(enable)) return enable(event);\n    return !!enable;\n  }\n\n  public destroy(): void {\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/behaviors/base-behavior.ts",
    "content": "import { BaseExtension } from '../registry/extension';\nimport type { CustomBehaviorOption } from '../spec/behavior';\n\n/**\n * <zh/> 交互通用配置项\n *\n * <en/> Base options for behaviors\n */\nexport interface BaseBehaviorOptions extends CustomBehaviorOption {}\n\n/**\n * <zh/> 交互的基类\n *\n * <en/> Base class for behaviors\n */\nexport abstract class BaseBehavior<T extends BaseBehaviorOptions = BaseBehaviorOptions> extends BaseExtension<T> {}\n"
  },
  {
    "path": "packages/g6/src/behaviors/brush-select.ts",
    "content": "import type { RectStyleProps } from '@antv/g';\nimport { Rect } from '@antv/g';\nimport { deepMix, isFunction } from '@antv/util';\nimport { CanvasEvent, CommonEvent } from '../constants';\nimport type { Graph } from '../runtime/graph';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { ElementDatum, ElementType, ID, IPointerEvent, Point, State } from '../types';\nimport { idOf } from '../utils/id';\nimport { getBoundingPoints, isPointInPolygon } from '../utils/point';\nimport type { ShortcutKey } from '../utils/shortcut';\nimport { Shortcut } from '../utils/shortcut';\nimport type { BaseBehaviorOptions } from './base-behavior';\nimport { BaseBehavior } from './base-behavior';\n\n/**\n * <zh/> 框选配置项\n *\n * <en/> Brush select options\n */\nexport interface BrushSelectOptions extends BaseBehaviorOptions {\n  /**\n   * <zh/> 是否启用动画\n   *\n   * <en/> Whether to enable animation.\n   * @defaultValue false\n   */\n  animation?: boolean;\n  /**\n   * <zh/> 是否启用框选功能\n   *\n   * <en/> Whether to enable Brush select element function.\n   * @defaultValue true\n   */\n  enable?: boolean | ((event: IPointerEvent) => boolean);\n  /**\n   * <zh/> 可框选的元素类型\n   *\n   * <en/> Enable Elements type.\n   * @defaultValue ['node', 'combo', 'edge']\n   */\n  enableElements?: ElementType[];\n  /**\n   * <zh/> 按下该快捷键配合鼠标点击进行框选\n   *\n   * <en/> Press this shortcut key to apply brush select with mouse click.\n   * @remarks\n   * <zh/> 注意，`trigger` 设置为 `['drag']` 时会导致 `drag-canvas` 行为失效。两者不可同时配置。\n   *\n   * <en/> Note that setting `trigger` to `['drag']` will cause the `drag-canvas` behavior to fail. The two cannot be configured at the same time.\n   * @defaultValue ['shift']\n   */\n  trigger?: ShortcutKey;\n  /**\n   * <zh/> 被选中时切换到该状态\n   *\n   * <en/> The state to switch to when selected.\n   * @defaultValue 'selected'\n   */\n  state?: State;\n  /**\n   * <zh/> 框选的选择模式\n   * - `'union'`：保持已选元素的当前状态，并添加指定的 state 状态。\n   * - `'intersect'`：如果已选元素已有指定的 state 状态，则保留；否则清除该状态。\n   * - `'diff'`：对已选元素的指定 state 状态进行取反操作。\n   * - `'default'`：清除已选元素的当前状态，并添加指定的 state 状态。\n   *\n   * <en/> Brush select mode\n   * - `'union'`: Keep the current state of the selected elements and add the specified state.\n   * - `'intersect'`: If the selected elements already have the specified state, keep it; otherwise, clearBrush it.\n   * - `'diff'`: Perform a negation operation on the specified state of the selected elements.\n   * - `'default'`: Clear the current state of the selected elements and add the specified state.\n   * @defaultValue 'default'\n   */\n  mode?: 'union' | 'intersect' | 'diff' | 'default';\n  /**\n   * <zh/> 是否及时框选, 仅在框选模式为 `default` 时生效\n   *\n   * <en/> Whether to brush select immediately, only valid when the brush select mode is `default`\n   * @defaultValue false\n   */\n  immediately?: boolean;\n  /**\n   * <zh/> 框选 框样式\n   *\n   * <en/> Timely screening.\n   */\n  style?: RectStyleProps;\n  /**\n   * <zh/> 框选元素状态回调。\n   *\n   * <en/> Callback when brush select elements.\n   * @param states - 选中的元素状态\n   */\n  onSelect?: (states: Record<ID, State | State[]>) => void;\n}\n/**\n * <zh/> 框选一组元素\n *\n * <en/> Brush select elements\n */\nexport class BrushSelect extends BaseBehavior<BrushSelectOptions> {\n  static defaultOptions: Partial<BrushSelectOptions> = {\n    animation: false,\n    enable: true,\n    enableElements: ['node', 'combo', 'edge'],\n    immediately: false,\n    mode: 'default',\n    state: 'selected',\n    trigger: ['shift'],\n    style: {\n      width: 0,\n      height: 0,\n      lineWidth: 1,\n      fill: '#1677FF',\n      stroke: '#1677FF',\n      fillOpacity: 0.1,\n      zIndex: 2,\n      pointerEvents: 'none',\n    },\n  };\n\n  private startPoint?: Point;\n  private endPoint?: Point;\n  private rectShape?: Rect;\n  private shortcut?: Shortcut;\n\n  constructor(context: RuntimeContext, options: BrushSelectOptions) {\n    super(context, deepMix({}, BrushSelect.defaultOptions, options));\n    this.shortcut = new Shortcut(context.graph);\n\n    this.onPointerDown = this.onPointerDown.bind(this);\n    this.onPointerMove = this.onPointerMove.bind(this);\n    this.onPointerUp = this.onPointerUp.bind(this);\n    this.clearStates = this.clearStates.bind(this);\n\n    this.bindEvents();\n  }\n\n  /**\n   * Triggered when the pointer is pressed\n   * @param event - Pointer event\n   * @internal\n   */\n  protected onPointerDown(event: IPointerEvent) {\n    if (!this.validate(event) || !this.isKeydown() || this.startPoint) return;\n    const { canvas, graph } = this.context;\n    const style = { ...this.options.style };\n\n    // 根据缩放比例调整 lineWidth\n    // Adjust lineWidth according to the zoom ratio\n    if (this.options.style.lineWidth) {\n      style.lineWidth = +this.options.style.lineWidth / graph.getZoom();\n    }\n\n    this.rectShape = new Rect({ id: 'g6-brush-select', style });\n    canvas.appendChild(this.rectShape);\n\n    this.startPoint = [event.canvas.x, event.canvas.y];\n  }\n\n  /**\n   * Triggered when the pointer is moved\n   * @param event - Pointer event\n   * @internal\n   */\n  protected onPointerMove(event: IPointerEvent) {\n    if (!this.startPoint) return;\n    const { immediately, mode } = this.options;\n\n    this.endPoint = getCursorPoint(event, this.context.graph);\n\n    this.rectShape?.attr({\n      x: Math.min(this.endPoint[0], this.startPoint[0]),\n      y: Math.min(this.endPoint[1], this.startPoint[1]),\n      width: Math.abs(this.endPoint[0] - this.startPoint[0]),\n      height: Math.abs(this.endPoint[1] - this.startPoint[1]),\n    });\n\n    if (immediately && mode === 'default') this.updateElementsStates(getBoundingPoints(this.startPoint, this.endPoint));\n  }\n\n  /**\n   * Triggered when the pointer is released\n   * @param event - Pointer event\n   * @internal\n   */\n  protected onPointerUp(event: IPointerEvent) {\n    if (!this.startPoint) return;\n    if (!this.endPoint) {\n      this.clearBrush();\n      return;\n    }\n\n    this.endPoint = getCursorPoint(event, this.context.graph);\n    this.updateElementsStates(getBoundingPoints(this.startPoint, this.endPoint));\n\n    this.clearBrush();\n  }\n\n  /**\n   * <zh/> 清除状态\n   *\n   * <en/> Clear state\n   * @internal\n   */\n  protected clearStates() {\n    if (this.endPoint) return;\n\n    this.clearElementsStates();\n  }\n\n  /**\n   * <zh/> 清除画布上所有元素的状态\n   *\n   * <en/> Clear the state of all elements on the canvas\n   * @internal\n   */\n  protected clearElementsStates() {\n    const { graph } = this.context;\n    const states = Object.values(graph.getData()).reduce((acc, data) => {\n      return Object.assign(\n        {},\n        acc,\n        data.reduce((acc: Record<ID, State[]>, datum: ElementDatum) => {\n          const restStates = (datum.states || [])?.filter((state) => state !== this.options.state);\n          acc[idOf(datum)] = restStates;\n          return acc;\n        }, {}),\n      );\n    }, {});\n\n    graph.setElementState(states, this.options.animation);\n  }\n\n  /**\n   * <zh/> 更新选中的元素状态\n   *\n   * <en/> Update the state of the selected elements\n   * @param points - <zh/> 框选区域的顶点 | <en/> The vertex of the selection area\n   * @internal\n   */\n  protected updateElementsStates(points: Point[]) {\n    const { graph } = this.context;\n    const { enableElements, state, mode, onSelect } = this.options;\n\n    const selectedIds = this.selector(graph, points, enableElements);\n\n    const states: Record<ID, State | State[]> = {};\n\n    switch (mode) {\n      case 'union':\n        selectedIds.forEach((id) => {\n          states[id] = [...graph.getElementState(id), state];\n        });\n        break;\n      case 'diff':\n        selectedIds.forEach((id) => {\n          const prevStates = graph.getElementState(id);\n          states[id] = prevStates.includes(state) ? prevStates.filter((s) => s !== state) : [...prevStates, state];\n        });\n        break;\n      case 'intersect':\n        selectedIds.forEach((id) => {\n          const prevStates = graph.getElementState(id);\n          states[id] = prevStates.includes(state) ? [state] : [];\n        });\n        break;\n      case 'default':\n      default:\n        selectedIds.forEach((id) => {\n          states[id] = [state];\n        });\n        break;\n    }\n\n    if (isFunction(onSelect)) onSelect(states);\n\n    graph.setElementState(states, this.options.animation);\n  }\n\n  /**\n   * <zh/> 查找画布上在指定区域内显示的元素。当节点的包围盒中心在矩形内时，节点被选中；当边的两端节点在矩形内时，边被选中；当 combo 的包围盒中心在矩形内时，combo 被选中。\n   *\n   * <en/> Find the elements displayed in the specified area on the canvas. A node is selected if the center of its bbox is inside the rect; An edge is selected if both end nodes are inside the rect ;A combo is selected if the center of its bbox is inside the rect.\n   * @param graph - <zh/> 图实例 | <en/> Graph instance\n   * @param points - <zh/> 框选区域的顶点 | <en/> The vertex of the selection area\n   * @param itemTypes - <zh/> 元素类型 | <en/> Element type\n   * @returns <zh/> 选中的元素 ID 数组 | <en/> Selected element ID array\n   * @internal\n   */\n  protected selector(graph: Graph, points: Point[], itemTypes: ElementType[]): ID[] {\n    if (!itemTypes || itemTypes.length === 0) return [];\n\n    const elements: ID[] = [];\n\n    const graphData = graph.getData();\n    itemTypes.forEach((itemType) => {\n      graphData[`${itemType}s`].forEach((datum) => {\n        const id = idOf(datum);\n        if (graph.getElementVisibility(id) !== 'hidden' && isPointInPolygon(graph.getElementPosition(id), points)) {\n          elements.push(id);\n        }\n      });\n    });\n\n    // 如果边的两端节点都在框选范围内，则边也被选中 | If source node and target node are within the selection range, that edge is also selected\n    if (itemTypes.includes('edge')) {\n      const edges = graphData.edges;\n      edges?.forEach((edge) => {\n        const { source, target } = edge;\n        if (elements.includes(source) && elements.includes(target)) {\n          elements.push(idOf(edge));\n        }\n      });\n    }\n\n    return elements;\n  }\n\n  private clearBrush() {\n    this.rectShape?.remove();\n    this.rectShape = undefined;\n    this.startPoint = undefined;\n    this.endPoint = undefined;\n  }\n\n  /**\n   * <zh/> 当前按键是否和 trigger 配置一致\n   *\n   * <en/> Is the current key consistent with the trigger configuration\n   * @returns <zh/> 是否一致 | <en/> Is consistent\n   * @internal\n   */\n  protected isKeydown(): boolean {\n    const { trigger } = this.options;\n    const keys = (Array.isArray(trigger) ? trigger : [trigger]) as string[];\n    return this.shortcut!.match(keys.filter((key) => key !== 'drag'));\n  }\n\n  /**\n   * <zh/> 验证是否启用框选\n   *\n   * <en/> Verify whether brush select is enabled\n   * @param event - <zh/> 事件 | <en/> Event\n   * @returns <zh/> 是否启用 | <en/> Whether to enable\n   * @internal\n   */\n  protected validate(event: IPointerEvent) {\n    if (this.destroyed) return false;\n    const { enable } = this.options;\n    if (isFunction(enable)) return enable(event);\n    return !!enable;\n  }\n\n  private bindEvents() {\n    const { graph } = this.context;\n\n    graph.on(CommonEvent.POINTER_DOWN, this.onPointerDown);\n    graph.on(CommonEvent.POINTER_MOVE, this.onPointerMove);\n    graph.on(CommonEvent.POINTER_UP, this.onPointerUp);\n    graph.on(CanvasEvent.CLICK, this.clearStates);\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n\n    graph.off(CommonEvent.POINTER_DOWN, this.onPointerDown);\n    graph.off(CommonEvent.POINTER_MOVE, this.onPointerMove);\n    graph.off(CommonEvent.POINTER_UP, this.onPointerUp);\n    graph.off(CanvasEvent.CLICK, this.clearStates);\n  }\n\n  /**\n   * <zh/> 更新配置项\n   *\n   * <en/> Update configuration\n   * @param options - <zh/> 配置项 | <en/> Options\n   * @internal\n   */\n  public update(options: Partial<BrushSelectOptions>) {\n    this.unbindEvents();\n    this.options = deepMix(this.options, options);\n    this.bindEvents();\n  }\n\n  /**\n   * <zh/> 销毁\n   *\n   * <en/> Destroy\n   * @internal\n   */\n  public destroy() {\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n\nexport const getCursorPoint = (event: IPointerEvent, graph: Graph): Point => {\n  // Fixed #7182: 判断 html 类型节点，并把 html 节点的浏览器坐标转换为 canvas 坐标。\n  // 没有直接判断的方式，nativeEvent.target 非 canvas 则表示 html 节点触发的。\n  // Fixed #7182: Handles brush selection on HTML nodes by converting client coordinates to canvas coordinates.\n  // An HTML node is identified if the event's targetType is 'node' but the nativeEvent.target is not the canvas element.\n  if (\n    (event.targetType === 'node' || event.targetType === 'combo') &&\n    !(event.nativeEvent.target instanceof HTMLCanvasElement)\n  ) {\n    const [x, y] = graph.getCanvasByClient([event.client.x, event.client.y]);\n    return [x, y];\n  }\n  return [event.canvas.x, event.canvas.y];\n};\n"
  },
  {
    "path": "packages/g6/src/behaviors/click-select.ts",
    "content": "import { isFunction } from '@antv/util';\nimport { CanvasEvent, CommonEvent } from '../constants';\nimport { ELEMENT_TYPES } from '../constants/element';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { Element, ElementType, ID, IPointerEvent, State } from '../types';\nimport { idOf } from '../utils/id';\nimport { getElementNthDegreeIds } from '../utils/relation';\nimport type { ShortcutKey } from '../utils/shortcut';\nimport { Shortcut } from '../utils/shortcut';\nimport { statesOf } from '../utils/state';\nimport type { BaseBehaviorOptions } from './base-behavior';\nimport { BaseBehavior } from './base-behavior';\n\n/**\n * <zh/> 点击元素交互配置项\n *\n * <en/> Click element behavior options\n */\nexport interface ClickSelectOptions extends BaseBehaviorOptions {\n  /**\n   * <zh/> 是否启用动画\n   *\n   * <en/> Whether to enable animation\n   * @defaultValue true\n   */\n  animation?: boolean;\n  /**\n   * <zh/> 是否启用点击元素的功能\n   *\n   * <en/> Whether to enable the function of clicking the element\n   * @defaultValue true\n   * @remarks\n   * <zh/> 可以通过函数的方式动态控制是否启用，例如只有节点被选中时才启用。\n   *\n   * <en/> Whether to enable can be dynamically controlled by functions, such as only when nodes are selected.\n   *\n   * ```json\n   * { enable: event => event.targetType === 'node'}\n   * ```\n   */\n  enable?: boolean | ((event: IPointerEvent) => boolean);\n  /**\n   * <zh/> 是否允许多选\n   *\n   * <en/> Whether to allow multiple selection\n   * @defaultValue false\n   */\n  multiple?: boolean;\n  /**\n   * <zh/> 按下该快捷键配合鼠标点击进行多选\n   *\n   * <en/> Press this shortcut key to apply multiple selection with mouse click\n   * @defaultValue ['shift']\n   */\n  trigger?: ShortcutKey;\n  /**\n   * <zh/> 当元素被选中时应用的状态\n   *\n   * <en/> The state to be applied when an element is selected\n   * @defaultValue 'selected'\n   */\n  state?: State;\n  /**\n   * <zh/> 当有元素选中时，其相邻 n 度关系的元素应用的状态。n 的值由属性 degree 控制，例如 degree 为 1 时表示直接相邻的元素\n   *\n   * <en/> The state to be applied to the neighboring elements within n degrees when an element is selected. The value of n is controlled by the degree property, for instance, a degree of 1 indicates direct neighbors\n   * @defaultValue 'selected'\n   */\n  neighborState?: State;\n  /**\n   * <zh/> 当有元素被选中时，除了选中元素及其受影响的邻居元素外，其他所有元素应用的状态。\n   *\n   * <en/> The state to be applied to all unselected elements when some elements are selected, excluding the selected element and its affected neighbors\n   */\n  unselectedState?: State;\n  /**\n   * <zh/> 选中元素的度，即决定了影响范围\n   *\n   * <en/> The degree to determine the scope of influence\n   * @defaultValue 0\n   * @remarks\n   * <zh/> 对于节点来说，`0` 表示只选中当前节点，`1` 表示选中当前节点及其直接相邻的节点和边，以此类推。\n   *\n   * <zh/> 对于边来说，`0` 表示只选中当前边，`1` 表示选中当前边及其直接相邻的节点，以此类推。\n   *\n   * <en/> For nodes, `0` means only the current node is selected, `1` means the current node and its directly adjacent nodes and edges are selected, etc.\n   *\n   * <en/> For edges, `0 `means only the current edge is selected,`1` means the current edge and its directly adjacent nodes are selected, etc.\n   */\n  degree?: number | ((event: IPointerEvent) => number);\n  /**\n   * <zh/> 点击元素时的回调\n   *\n   * <en/> Callback when the element is clicked\n   * @param event - <zh/> 点击事件 | <en/> click event\n   */\n  onClick?: (event: IPointerEvent) => void;\n}\n\n/**\n * <zh/> 点击元素\n *\n * <en/> Click Element\n * @remarks\n * <zh/> 当鼠标点击元素时，可以激活元素的状态，例如选中节点或边。当 degree 设置为 `1` 时，点击节点会高亮当前节点及其直接相邻的节点和边。\n *\n * <en/> When the mouse clicks on an element, you can activate the state of the element, such as selecting nodes or edges. When the degree is 1, clicking on a node will highlight the current node and its directly adjacent nodes and edges.\n */\nexport class ClickSelect extends BaseBehavior<ClickSelectOptions> {\n  private shortcut: Shortcut;\n\n  static defaultOptions: Partial<ClickSelectOptions> = {\n    animation: true,\n    enable: true,\n    multiple: false,\n    trigger: ['shift'],\n    state: 'selected',\n    neighborState: 'selected',\n    unselectedState: undefined,\n    degree: 0,\n  };\n\n  constructor(context: RuntimeContext, options: ClickSelectOptions) {\n    super(context, Object.assign({}, ClickSelect.defaultOptions, options));\n    this.shortcut = new Shortcut(context.graph);\n    this.bindEvents();\n  }\n\n  private bindEvents() {\n    const { graph } = this.context;\n    this.unbindEvents();\n    ELEMENT_TYPES.forEach((type) => {\n      graph.on(`${type}:${CommonEvent.CLICK}`, this.onClickSelect);\n    });\n    graph.on(CanvasEvent.CLICK, this.onClickCanvas);\n  }\n\n  private onClickSelect = async (event: IPointerEvent<Element>) => {\n    if (!this.validate(event)) return;\n    await this.updateState(event);\n    this.options.onClick?.(event);\n  };\n\n  private onClickCanvas = async (event: IPointerEvent) => {\n    if (!this.validate(event)) return;\n    await this.clearState();\n    this.options.onClick?.(event);\n  };\n\n  private get isMultipleSelect() {\n    const { multiple, trigger } = this.options;\n    return multiple && this.shortcut.match(trigger);\n  }\n\n  protected getNeighborIds(event: IPointerEvent<Element>) {\n    const { target, targetType } = event;\n    const { graph } = this.context;\n    const { degree } = this.options;\n    return getElementNthDegreeIds(\n      graph,\n      targetType as ElementType,\n      target.id,\n      typeof degree === 'function' ? degree(event) : degree,\n    ).filter((id) => id !== target.id);\n  }\n\n  private async updateState(event: IPointerEvent<Element>) {\n    const { state: selectState, unselectedState, neighborState, animation } = this.options;\n    if (!selectState && !neighborState && !unselectedState) return;\n\n    const { target } = event;\n    const { graph } = this.context;\n\n    const datum = graph.getElementData(target.id);\n\n    const type = statesOf(datum).includes(selectState) ? 'unselect' : 'select';\n\n    const states: Record<ID, State[]> = {};\n\n    const isMultipleSelect = this.isMultipleSelect;\n\n    const click = [target.id];\n    const neighbor = this.getNeighborIds(event);\n\n    if (!isMultipleSelect) {\n      if (type === 'select') {\n        Object.assign(states, this.getClearStates(!!unselectedState));\n        const addState = (list: ID[], state: State) => {\n          list.forEach((id) => {\n            if (!states[id]) states[id] = graph.getElementState(id);\n            states[id].push(state);\n          });\n        };\n        addState(click, selectState);\n        addState(neighbor, neighborState);\n        if (unselectedState) {\n          Object.keys(states).forEach((id) => {\n            if (!click.includes(id) && !neighbor.includes(id)) states[id].push(unselectedState);\n          });\n        }\n      } else Object.assign(states, this.getClearStates());\n    } else {\n      Object.assign(states, this.getDataStates());\n\n      if (type === 'select') {\n        const addState = (list: ID[], state: State) => {\n          list.forEach((id) => {\n            const dataStatesSet = new Set(graph.getElementState(id));\n            dataStatesSet.add(state);\n            dataStatesSet.delete(unselectedState);\n            states[id] = Array.from(dataStatesSet);\n          });\n        };\n\n        addState(click, selectState);\n        addState(neighbor, neighborState);\n        if (unselectedState) {\n          Object.keys(states).forEach((id) => {\n            const _states = states[id];\n            if (\n              !_states.includes(selectState) &&\n              !_states.includes(neighborState) &&\n              !_states.includes(unselectedState)\n            ) {\n              states[id].push(unselectedState);\n            }\n          });\n        }\n      } else {\n        const targetState = states[target.id];\n        states[target.id] = targetState.filter((s) => s !== selectState && s !== neighborState);\n        if (!targetState.includes(unselectedState)) states[target.id].push(unselectedState);\n        neighbor.forEach((id) => {\n          states[id] = states[id].filter((s) => s !== neighborState);\n          if (!states[id].includes(selectState)) states[id].push(unselectedState);\n        });\n      }\n    }\n\n    await graph.setElementState(states, animation);\n  }\n\n  private getDataStates() {\n    const { graph } = this.context;\n    const { nodes, edges, combos } = graph.getData();\n\n    const states: Record<ID, State[]> = {};\n    [...nodes, ...edges, ...combos].forEach((data) => {\n      states[idOf(data)] = statesOf(data);\n    });\n\n    return states;\n  }\n\n  /**\n   * <zh/> 获取需要清除的状态\n   *\n   * <en/> Get the states that need to be cleared\n   * @param complete - <zh/> 是否返回所有状态 | <en/> Whether to return all states\n   * @returns - <zh/> 需要清除的状态 | <en/> States that need to be cleared\n   */\n  private getClearStates(complete = false) {\n    const { graph } = this.context;\n    const { state, unselectedState, neighborState } = this.options;\n    const statesToClear = new Set([state, unselectedState, neighborState]);\n    const { nodes, edges, combos } = graph.getData();\n\n    const states: Record<ID, State[]> = {};\n    [...nodes, ...edges, ...combos].forEach((data) => {\n      const datumStates = statesOf(data);\n      const newStates = datumStates.filter((s) => !statesToClear.has(s));\n      if (complete) states[idOf(data)] = newStates;\n      else if (newStates.length !== datumStates.length) states[idOf(data)] = newStates;\n    });\n\n    return states;\n  }\n\n  private async clearState() {\n    const { graph } = this.context;\n    await graph.setElementState(this.getClearStates(), this.options.animation);\n  }\n\n  private validate(event: IPointerEvent) {\n    if (this.destroyed) return false;\n    const { enable } = this.options;\n    if (isFunction(enable)) return enable(event);\n    return !!enable;\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n\n    ELEMENT_TYPES.forEach((type) => {\n      graph.off(`${type}:${CommonEvent.CLICK}`, this.onClickSelect);\n    });\n    graph.off(CanvasEvent.CLICK, this.onClickCanvas);\n  }\n\n  public destroy() {\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/behaviors/collapse-expand.ts",
    "content": "import { isFunction } from '@antv/util';\nimport { CommonEvent } from '../constants';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { ID, IPointerEvent, NodeLikeData } from '../types';\nimport { isCollapsed } from '../utils/collapsibility';\nimport { isElement } from '../utils/element';\nimport type { BaseBehaviorOptions } from './base-behavior';\nimport { BaseBehavior } from './base-behavior';\n\n/**\n * <zh/> 展开/收起元素交互配置项\n *\n * <en/> Collapse/Expand combo behavior options\n */\nexport interface CollapseExpandOptions extends BaseBehaviorOptions {\n  /**\n   * <zh/> 是否启用动画\n   *\n   * <en/> Whether to enable animation\n   * @defaultValue true\n   */\n  animation?: boolean;\n  /**\n   * <zh/> 是否启用展开/收起功能\n   *\n   * <en/> Whether to enable the expand/collapse function\n   * @defaultValue true\n   */\n  enable?: boolean | ((event: IPointerEvent) => boolean);\n  /**\n   * <zh/> 触发方式\n   *\n   * <en/> Trigger method\n   * @defaultValue 'dblclick'\n   */\n  trigger?: CommonEvent.CLICK | CommonEvent.DBLCLICK;\n  /**\n   * <zh/> 完成收起时的回调\n   *\n   * <en/> Callback when collapse is completed\n   */\n  onCollapse?: (id: ID) => void;\n  /**\n   * <zh/> 完成展开时的回调\n   *\n   * <en/> Callback when expand is completed\n   */\n  onExpand?: (id: ID) => void;\n  /**\n   * <zh/> 是否对准目标元素，避免视图偏移\n   *\n   * <en/> Whether to focus on the target element to avoid view offset\n   */\n  align?: boolean;\n}\n\n/**\n * <zh/> 展开/收起元素交互\n *\n * <en/> Collapse/Expand Element behavior\n * @remarks\n * <zh/> 通过操作展开/收起元素。\n *\n * <en/> Expand/collapse elements by operation.\n */\nexport class CollapseExpand extends BaseBehavior<CollapseExpandOptions> {\n  static defaultOptions: Partial<CollapseExpandOptions> = {\n    enable: true,\n    animation: true,\n    trigger: CommonEvent.DBLCLICK,\n    align: true,\n  };\n\n  constructor(context: RuntimeContext, options: CollapseExpandOptions) {\n    super(context, Object.assign({}, CollapseExpand.defaultOptions, options));\n\n    this.bindEvents();\n  }\n\n  public update(options: Partial<CollapseExpandOptions>) {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  private bindEvents() {\n    const { graph } = this.context;\n    const { trigger } = this.options;\n    graph.on(`node:${trigger}`, this.onCollapseExpand);\n    graph.on(`combo:${trigger}`, this.onCollapseExpand);\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n    const { trigger } = this.options;\n    graph.off(`node:${trigger}`, this.onCollapseExpand);\n    graph.off(`combo:${trigger}`, this.onCollapseExpand);\n  }\n\n  private onCollapseExpand = async (event: IPointerEvent) => {\n    if (!this.validate(event)) return;\n    const { target } = event;\n    if (!isElement(target)) return;\n\n    const id = target.id;\n    const { model, graph } = this.context;\n    const data = model.getElementDataById(id) as NodeLikeData;\n    if (!data) return false;\n\n    const { onCollapse, onExpand, animation, align } = this.options;\n    if (isCollapsed(data)) {\n      await graph.expandElement(id, { animation, align });\n      onExpand?.(id);\n    } else {\n      await graph.collapseElement(id, { animation, align });\n      onCollapse?.(id);\n    }\n  };\n\n  private validate(event: IPointerEvent): boolean {\n    if (this.destroyed) return false;\n    const { enable } = this.options;\n    if (isFunction(enable)) return enable(event);\n    return !!enable;\n  }\n\n  public destroy(): void {\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/behaviors/create-edge.ts",
    "content": "import { isFunction, uniqueId } from '@antv/util';\nimport { CanvasEvent, ComboEvent, CommonEvent, EdgeEvent, NodeEvent } from '../constants';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { EdgeData } from '../spec';\nimport type { EdgeStyle } from '../spec/element/edge';\nimport type { ID, IElementEvent, IPointerEvent, NodeLikeData } from '../types';\nimport { OVERRIDE_KEY } from '../utils/data';\nimport type { BaseBehaviorOptions } from './base-behavior';\nimport { BaseBehavior } from './base-behavior';\n\nconst ASSIST_EDGE_ID = 'g6-create-edge-assist-edge-id';\nconst ASSIST_NODE_ID = 'g6-create-edge-assist-node-id';\n\n/**\n * <zh/> 创建边交互配置项\n *\n * <en/> Create edge behavior options\n */\nexport interface CreateEdgeOptions extends BaseBehaviorOptions {\n  /**\n   * <zh/> 是否启用创建边的功能\n   *\n   * <en/> Whether to enable the function of creating edges\n   * @defaultValue true\n   */\n  enable?: boolean | ((event: IPointerEvent) => boolean);\n  /**\n   * <zh/> 新建边的样式配置\n   *\n   * <en/> Style configs of the new edge\n   */\n  style?: EdgeStyle;\n  /**\n   * <zh/> 交互配置 点击 或 拖拽\n   *\n   * <en/> trigger click or drag.\n   * @defaultValue 'drag'\n   */\n  trigger?: 'click' | 'drag';\n  /**\n   * <zh/> 成功创建边回调\n   *\n   * <en/> Callback when create is completed.\n   */\n  onFinish?: (edge: EdgeData) => void;\n  /**\n   * <zh/> 创建边回调，返回边数据。如果返回 undefined，则不创建该边。\n   *\n   * <en/> Callback when create edge, return EdgeData. If returns undefined, the edge will not be created.\n   */\n  onCreate?: (edge: EdgeData) => EdgeData | undefined;\n}\n\n/**\n * <zh/> 创建边交互\n *\n * <en/> Create edge behavior\n * @remarks\n * <zh/> 通过拖拽或点击节点创建边，支持自定义边样式。\n *\n * <en/> Create edges by dragging or clicking nodes, and support custom edge styles.\n */\nexport class CreateEdge extends BaseBehavior<CreateEdgeOptions> {\n  static defaultOptions: Partial<CreateEdgeOptions> = {\n    animation: true,\n    enable: true,\n    style: {},\n    trigger: 'drag',\n    onCreate: (data) => data,\n    onFinish: () => {},\n  };\n\n  public source?: ID;\n\n  constructor(context: RuntimeContext, options: CreateEdgeOptions) {\n    super(context, Object.assign({}, CreateEdge.defaultOptions, options));\n    this.bindEvents();\n  }\n\n  /**\n   * Update options\n   * @param options - The options to update\n   * @internal\n   */\n  public update(options: Partial<CreateEdgeOptions>): void {\n    super.update(options);\n    this.bindEvents();\n  }\n\n  private bindEvents() {\n    const { graph } = this.context;\n    const { trigger } = this.options;\n    this.unbindEvents();\n\n    if (trigger === 'click') {\n      graph.on(NodeEvent.CLICK, this.handleCreateEdge);\n      graph.on(ComboEvent.CLICK, this.handleCreateEdge);\n      graph.on(CanvasEvent.CLICK, this.cancelEdge);\n      graph.on(EdgeEvent.CLICK, this.cancelEdge);\n    } else {\n      graph.on(NodeEvent.DRAG_START, this.handleCreateEdge);\n      graph.on(ComboEvent.DRAG_START, this.handleCreateEdge);\n      graph.on(CommonEvent.POINTER_UP, this.drop);\n    }\n\n    graph.on(CommonEvent.POINTER_MOVE, this.updateAssistEdge);\n  }\n\n  private drop = async (event: IElementEvent) => {\n    const { targetType } = event;\n    if (['combo', 'node'].includes(targetType) && this.source) {\n      await this.handleCreateEdge(event);\n    } else {\n      await this.cancelEdge();\n    }\n  };\n\n  private handleCreateEdge = async (event: IElementEvent) => {\n    if (!this.validate(event)) return;\n    const { graph, canvas, batch, element } = this.context;\n    const { style } = this.options;\n\n    if (this.source) {\n      this.createEdge(event);\n      await this.cancelEdge();\n      return;\n    }\n\n    batch!.startBatch();\n    canvas.setCursor('crosshair');\n    this.source = this.getSelectedNodeIDs([event.target.id])[0];\n    const sourceNode = graph.getElementData(this.source) as NodeLikeData;\n\n    graph.addNodeData([\n      {\n        id: ASSIST_NODE_ID,\n        type: 'circle',\n        [OVERRIDE_KEY]: false,\n        style: {\n          size: 1,\n          visibility: 'hidden',\n          ports: [{ key: 'port-1', placement: [0.5, 0.5] }],\n          x: sourceNode.style?.x,\n          y: sourceNode.style?.y,\n        },\n      },\n    ]);\n\n    graph.addEdgeData([\n      {\n        id: ASSIST_EDGE_ID,\n        source: this.source,\n        target: ASSIST_NODE_ID,\n        style: {\n          pointerEvents: 'none',\n          ...style,\n        },\n      },\n    ]);\n    await element!.draw({ animation: false })?.finished;\n  };\n\n  private updateAssistEdge = async (event: IPointerEvent) => {\n    if (!this.source) return;\n    const { model, element } = this.context;\n    model.translateNodeTo(ASSIST_NODE_ID, [event.client.x, event.client.y]);\n    await element!.draw({ animation: false, silence: true })?.finished;\n  };\n\n  private createEdge = (event: IElementEvent) => {\n    const { graph } = this.context;\n    const { style, onFinish, onCreate } = this.options;\n    const targetId = event.target?.id;\n    if (targetId === undefined || this.source === undefined) return;\n\n    const target = this.getSelectedNodeIDs([event.target.id])?.[0];\n    const id = `${this.source}-${target}-${uniqueId()}`;\n\n    const edgeData = onCreate({ id, source: this.source, target, style });\n    if (edgeData) {\n      graph.addEdgeData([edgeData]);\n      onFinish(edgeData);\n    }\n  };\n\n  private cancelEdge = async () => {\n    if (!this.source) return;\n    const { graph, element, batch } = this.context;\n\n    graph.removeNodeData([ASSIST_NODE_ID]);\n\n    this.source = undefined;\n\n    await element!.draw({ animation: false })?.finished;\n    batch!.endBatch();\n  };\n\n  private getSelectedNodeIDs(currTarget: ID[]) {\n    return Array.from(\n      new Set(\n        this.context.graph\n          .getElementDataByState('node', this.options.state)\n          .map((node) => node.id)\n          .concat(currTarget),\n      ),\n    );\n  }\n\n  private validate(event: IPointerEvent) {\n    if (this.destroyed) return false;\n    const { enable } = this.options;\n    if (isFunction(enable)) return enable(event);\n    return !!enable;\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n\n    graph.off(NodeEvent.CLICK, this.handleCreateEdge);\n    graph.off(ComboEvent.CLICK, this.handleCreateEdge);\n    graph.off(CanvasEvent.CLICK, this.cancelEdge);\n    graph.off(EdgeEvent.CLICK, this.cancelEdge);\n    graph.off(NodeEvent.DRAG_START, this.handleCreateEdge);\n    graph.off(ComboEvent.DRAG_START, this.handleCreateEdge);\n    graph.off(CommonEvent.POINTER_UP, this.drop);\n    graph.off(CommonEvent.POINTER_MOVE, this.updateAssistEdge);\n  }\n\n  public destroy() {\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/behaviors/drag-canvas.ts",
    "content": "import type { Cursor } from '@antv/g';\nimport { debounce, isObject } from '@antv/util';\nimport { CommonEvent } from '../constants';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { IDragEvent, IKeyboardEvent, IPointerEvent, Vector2, ViewportAnimationEffectTiming } from '../types';\nimport { getExpandedBBox, getPointBBox, isPointInBBox } from '../utils/bbox';\nimport { parsePadding } from '../utils/padding';\nimport { PinchHandler } from '../utils/pinch';\nimport type { ShortcutKey } from '../utils/shortcut';\nimport { Shortcut } from '../utils/shortcut';\nimport { multiply, rotate, subtract } from '../utils/vector';\nimport type { BaseBehaviorOptions } from './base-behavior';\nimport { BaseBehavior } from './base-behavior';\n\n/**\n * <zh/> 拖拽画布交互配置项\n *\n * <en/> Drag canvas behavior options\n */\nexport interface DragCanvasOptions extends BaseBehaviorOptions {\n  /**\n   * <zh/> 是否启用拖拽动画，仅在使用按键移动时有效\n   *\n   * <en/> Whether to enable the animation of dragging, only valid when using key movement\n   */\n  animation?: ViewportAnimationEffectTiming;\n  /**\n   * <zh/> 是否启用拖拽画布的功能\n   *\n   * <en/> Whether to enable the function of dragging the canvas\n   * @defaultValue true\n   */\n  enable?: boolean | ((event: IPointerEvent | IKeyboardEvent) => boolean);\n  /**\n   * <zh/> 允许拖拽方向\n   * - `'x'`: 只允许水平拖拽\n   * - `'y'`: 只允许垂直拖拽\n   * - `'both'`: 不受限制，允许水平和垂直拖拽\n   *\n   * <en/> Allowed drag direction\n   * - `'x'`: Only allow horizontal drag\n   * - `'y'`: Only allow vertical drag\n   * - `'both'`: Allow horizontal and vertical drag\n   * @defaultValue `'both'`\n   */\n  direction?: 'x' | 'y' | 'both';\n  /**\n   * <zh/> 可拖拽的视口范围，默认最多可拖拽一屏。可以分别设置上、右、下、左四个方向的范围，每个方向的范围在 [0, Infinity] 之间\n   *\n   * <en/> The draggable viewport range allows you to drag up to one screen by default. You can set the range for each direction (top, right, bottom, left) individually, with each direction's range between [0, Infinity]\n   * @defaultValue Infinity\n   */\n  range?: number | number[];\n  /**\n   * <zh/> 触发拖拽的方式，默认使用指针按下拖拽\n   *\n   * <en/> The way to trigger dragging, default to dragging with the pointer pressed\n   */\n  trigger?: {\n    up: ShortcutKey;\n    down: ShortcutKey;\n    left: ShortcutKey;\n    right: ShortcutKey;\n  };\n  /**\n   * <zh/> 触发一次按键移动的距离\n   *\n   * <en/> The distance of a single key movement\n   * @defaultValue 10\n   */\n  sensitivity?: number;\n  /**\n   * <zh/> 完成拖拽时的回调\n   *\n   * <en/> Callback when dragging is completed\n   */\n  onFinish?: () => void;\n}\n\n/**\n * <zh/> 拖拽画布交互\n *\n * <en/> Drag canvas behavior\n */\nexport class DragCanvas extends BaseBehavior<DragCanvasOptions> {\n  static defaultOptions: Partial<DragCanvasOptions> = {\n    enable: (event) => {\n      if ('targetType' in event) return event.targetType === 'canvas';\n      return true;\n    },\n    sensitivity: 10,\n    direction: 'both',\n    range: Infinity,\n  };\n\n  private shortcut: Shortcut;\n\n  private defaultCursor: Cursor;\n\n  constructor(context: RuntimeContext, options: DragCanvasOptions) {\n    super(context, Object.assign({}, DragCanvas.defaultOptions, options));\n\n    this.shortcut = new Shortcut(context.graph);\n\n    this.bindEvents();\n    this.defaultCursor = this.context.canvas.getConfig().cursor || 'default';\n  }\n\n  /**\n   * <zh/> 更新配置\n   *\n   * <en/> Update options\n   * @param options - <zh/> 配置项 | <en/> Options\n   * @internal\n   */\n  public update(options: Partial<DragCanvasOptions>): void {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  private bindEvents() {\n    const { trigger } = this.options;\n\n    if (isObject(trigger)) {\n      const { up = [], down = [], left = [], right = [] } = trigger;\n      this.shortcut.bind(up, (event) => this.onTranslate([0, 1], event));\n      this.shortcut.bind(down, (event) => this.onTranslate([0, -1], event));\n      this.shortcut.bind(left, (event) => this.onTranslate([1, 0], event));\n      this.shortcut.bind(right, (event) => this.onTranslate([-1, 0], event));\n    } else {\n      const { graph } = this.context;\n      graph.on(CommonEvent.DRAG_START, this.onDragStart);\n      graph.on(CommonEvent.DRAG, this.onDrag);\n      graph.on(CommonEvent.DRAG_END, this.onDragEnd);\n    }\n  }\n\n  private isDragging = false;\n\n  private onDragStart = (event: IDragEvent) => {\n    if (!this.validate(event)) return;\n    this.isDragging = true;\n    this.context.canvas.setCursor('grabbing');\n  };\n\n  private onDrag = (event: IDragEvent) => {\n    if (!this.isDragging || PinchHandler.isPinching) return;\n    const x = event.movement?.x ?? event.dx;\n    const y = event.movement?.y ?? event.dy;\n    if ((x | y) !== 0) {\n      this.translate([x, y], false);\n    }\n  };\n\n  private onDragEnd = () => {\n    this.isDragging = false;\n    this.context.canvas.setCursor(this.defaultCursor);\n    this.options.onFinish?.();\n  };\n\n  private invokeOnFinish = debounce(() => {\n    this.options.onFinish?.();\n  }, 300);\n\n  private async onTranslate(value: Vector2, event: IPointerEvent | IKeyboardEvent) {\n    if (!this.validate(event)) return;\n    const { sensitivity } = this.options;\n    const delta = sensitivity * -1;\n    await this.translate(multiply(value, delta) as Vector2, this.options.animation);\n\n    this.invokeOnFinish();\n  }\n\n  /**\n   * <zh/> 平移画布\n   *\n   * <en/> Translate canvas\n   * @param offset - <zh/> 平移距离 | <en/> Translation distance\n   * @param animation - <zh/> 动画配置 | <en/> Animation configuration\n   * @internal\n   */\n  protected async translate(offset: Vector2, animation?: ViewportAnimationEffectTiming) {\n    offset = this.clampByDirection(offset);\n    offset = this.clampByRange(offset);\n    offset = this.clampByRotation(offset);\n\n    await this.context.graph.translateBy(offset, animation);\n  }\n\n  private clampByRotation([dx, dy]: Vector2): Vector2 {\n    const rotation = this.context.graph.getRotation();\n    return rotate([dx, dy], rotation);\n  }\n\n  private clampByDirection([dx, dy]: Vector2): Vector2 {\n    const { direction } = this.options;\n    if (direction === 'x') {\n      dy = 0;\n    } else if (direction === 'y') {\n      dx = 0;\n    }\n    return [dx, dy];\n  }\n\n  private clampByRange([dx, dy]: Vector2): Vector2 {\n    const { viewport, canvas } = this.context;\n\n    const [canvasWidth, canvasHeight] = canvas.getSize();\n    const [top, right, bottom, left] = parsePadding(this.options.range);\n    const range = [canvasHeight * top, canvasWidth * right, canvasHeight * bottom, canvasWidth * left];\n    const draggableArea = getExpandedBBox(getPointBBox(viewport!.getCanvasCenter()), range);\n\n    const nextViewportCenter = subtract(viewport!.getViewportCenter(), [dx, dy, 0]);\n    if (!isPointInBBox(nextViewportCenter, draggableArea)) {\n      const {\n        min: [minX, minY],\n        max: [maxX, maxY],\n      } = draggableArea;\n\n      if ((nextViewportCenter[0] < minX && dx > 0) || (nextViewportCenter[0] > maxX && dx < 0)) {\n        dx = 0;\n      }\n      if ((nextViewportCenter[1] < minY && dy > 0) || (nextViewportCenter[1] > maxY && dy < 0)) {\n        dy = 0;\n      }\n    }\n    return [dx, dy];\n  }\n\n  private validate(event: IPointerEvent | IKeyboardEvent) {\n    if (this.destroyed) return false;\n    const { enable } = this.options;\n    if (typeof enable === 'function') return enable(event);\n    return !!enable;\n  }\n\n  private unbindEvents() {\n    this.shortcut.unbindAll();\n    const { graph } = this.context;\n    graph.off(CommonEvent.DRAG_START, this.onDragStart);\n    graph.off(CommonEvent.DRAG, this.onDrag);\n    graph.off(CommonEvent.DRAG_END, this.onDragEnd);\n  }\n\n  public destroy(): void {\n    this.shortcut.destroy();\n    this.unbindEvents();\n    this.context.canvas.setCursor(this.defaultCursor);\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/behaviors/drag-element-force.ts",
    "content": "import type { ID, IElementDragEvent, Point } from '../types';\nimport { idOf } from '../utils/id';\nimport { getLayoutProperty, invokeLayoutMethod } from '../utils/layout';\nimport { print } from '../utils/print';\nimport { add } from '../utils/vector';\nimport type { DragElementOptions } from './drag-element';\nimport { DragElement } from './drag-element';\n\n/**\n * <zh/> 调用力导布局拖拽元素交互配置项\n *\n * <en/> Call d3-force layout to drag element behavior options\n */\nexport interface DragElementForceOptions extends Omit<DragElementOptions, 'animation' | 'dropEffect' | 'shadow'> {\n  /**\n   * <zh/> 在拖拽结束后，节点是否保持固定位置\n   * - `true`: 在拖拽结束后，节点的位置将保持固定，不受布局算法的影响\n   * - `false`: 在拖拽结束后，节点的位置将继续受到布局算法的影响\n   *\n   * <en/> Whether the node remains in a fixed position after dragging ends\n   * - `true`: After dragging ends, the node's position will remain fixed and will not be affected by the layout algorithm\n   * - `false`: After dragging ends, the node's position will continue to be affected by the layout algorithm\n   */\n  fixed?: boolean;\n}\n\n/**\n * <zh/> 调用力导布局拖拽元素的交互\n *\n * <en/> Call d3-force layout to drag element behavior\n * @remarks\n * <zh/> 只能在使用 d3-force 布局时使用该交互，在拖拽过程中会实时重新计算布局。\n *\n * <en/> This behavior can only be used with d3-force layout. The layout will be recalculated in real time during dragging.\n */\nexport class DragElementForce extends DragElement {\n  private get forceLayoutInstance() {\n    return this.context.layout!.getLayoutInstance().find((layout) => ['d3-force', 'd3-force-3d'].includes(layout?.id));\n  }\n\n  /**\n   * Whether the behavior is enabled\n   * @param event - The event object\n   * @returns Is the behavior enabled\n   * @internal\n   */\n  protected validate(event: IElementDragEvent): boolean {\n    if (!this.context.layout) return false;\n\n    // 未使用力导布局 / The force layout is not used\n    if (!this.forceLayoutInstance) {\n      print.warn('DragElementForce only works with d3-force or d3-force-3d layout');\n      return false;\n    }\n\n    return super.validate(event);\n  }\n\n  /**\n   * Move selected elements by offset\n   * @param ids - The selected element IDs\n   * @param offset - The offset to move\n   * @internal\n   */\n  protected async moveElement(ids: ID[], offset: Point) {\n    const layout = this.forceLayoutInstance;\n    this.context.graph.getNodeData(ids).forEach((element, index) => {\n      const { x = 0, y = 0 } = element.style || {};\n      if (layout)\n        invokeLayoutMethod(layout, 'setFixedPosition', ids[index], [...add([+x, +y], this.clampByRotation(offset))]);\n    });\n  }\n\n  /**\n   * Triggered when the drag starts\n   * @param event - The event object\n   * @internal\n   */\n  protected onDragStart(event: IElementDragEvent) {\n    this.enable = this.validate(event);\n    if (!this.enable) return;\n\n    this.target = this.getSelectedNodeIDs([event.target.id]);\n    this.hideEdge();\n    this.context.graph.frontElement(this.target);\n\n    const layout = this.forceLayoutInstance;\n    if (layout) getLayoutProperty(layout, 'simulation').alphaTarget(0.3).restart();\n\n    this.context.graph.getNodeData(this.target).forEach((element) => {\n      const { x = 0, y = 0 } = element.style || {};\n      if (layout) invokeLayoutMethod(layout, 'setFixedPosition', idOf(element), [+x, +y]);\n    });\n  }\n\n  /**\n   * Triggered when dragging\n   * @param event - The event object\n   * @internal\n   */\n  protected onDrag(event: IElementDragEvent) {\n    if (!this.enable) return;\n\n    const delta = this.getDelta(event);\n    this.moveElement(this.target, delta);\n  }\n\n  /**\n   * Triggered when the drag ends\n   * @internal\n   */\n  protected onDragEnd() {\n    const layout = this.forceLayoutInstance;\n    if (layout) getLayoutProperty(layout, 'simulation').alphaTarget(0);\n\n    if (this.options.fixed) return;\n\n    this.context.graph.getNodeData(this.target).forEach((element) => {\n      if (layout) invokeLayoutMethod(layout, 'setFixedPosition', idOf(element), [null, null, null]);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/behaviors/drag-element.ts",
    "content": "import type { BaseStyleProps, Cursor } from '@antv/g';\nimport { Rect } from '@antv/g';\nimport { isFunction } from '@antv/util';\nimport { COMBO_KEY, CanvasEvent, ComboEvent, CommonEvent } from '../constants';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { EdgeDirection, ID, IElementDragEvent, IPointerEvent, Point, Prefix, State, Vector2 } from '../types';\nimport { getBBoxSize, getCombinedBBox } from '../utils/bbox';\nimport { isToBeDestroyed } from '../utils/element';\nimport { idOf } from '../utils/id';\nimport { subStyleProps } from '../utils/prefix';\nimport { Shortcut, ShortcutKey } from '../utils/shortcut';\nimport { divide, rotate, subtract } from '../utils/vector';\nimport type { BaseBehaviorOptions } from './base-behavior';\nimport { BaseBehavior } from './base-behavior';\n\n/**\n * <zh/> 拖拽元素交互配置项\n *\n * <en/> Drag element behavior options\n */\nexport interface DragElementOptions extends BaseBehaviorOptions, Prefix<'shadow', BaseStyleProps> {\n  /**\n   * <zh/> 是否启用拖拽动画\n   *\n   * <en/> Whether to enable drag animation\n   * @defaultValue true\n   */\n  animation?: boolean;\n  /**\n   * <zh/> 是否启用拖拽节点的功能，默认可以拖拽 node 和 combo\n   *\n   * <en/> Whether to enable the function of dragging the node，default can drag node and combo\n   * @defaultValue ['node', 'combo'].includes(event.targetType)\n   */\n  enable?: boolean | ((event: IElementDragEvent) => boolean);\n  /**\n   * <zh/> 触发拖拽的方式\n   * 支持按下组合键才能触发拖拽元素\n   *\n   * <en/> The way to trigger drag element\n   * Support triggering by pressing a combination of keys\n   */\n  trigger?: ShortcutKey;\n  /**\n   * <zh/> 拖拽操作效果\n   * - `'link'`: 将拖拽元素置入为目标元素的子元素\n   * - `'move'`: 移动元素并更新父元素尺寸\n   * - `'none'`: 仅更新拖拽目标位置，不做任何额外操作\n   *\n   * <en/> Drag operation effect\n   * - `'link'`: Place the drag element as a child element of the target element\n   * - `'move'`: Move the element and update the parent element size\n   * - `'none'`: Only update the drag target position, no additional operations\n   * @remarks\n   * <zh/> combo 元素可作为元素容器置入 node 或 combo 元素\n   *\n   * <en/> The combo element can be placed as an element container into the node or combo element\n   * @defaultValue 'move'\n   */\n  dropEffect?: 'link' | 'move' | 'none';\n  /**\n   * <zh/> 节点选中的状态，启用多选时会基于该状态查找选中的节点\n   *\n   * <en/> The state name of the selected node, when multi-selection is enabled, the selected nodes will be found based on this state\n   * @defaultValue 'selected'\n   */\n  state?: State;\n  /**\n   * <zh/> 拖拽时隐藏的边\n   * - `'none'`: 不隐藏\n   * - `'out'`: 隐藏以节点为源节点的边\n   * - `'in'`: 隐藏以节点为目标节点的边\n   * - `'both'`: 隐藏与节点相关的所有边\n   * - `'all'`: 隐藏图中所有边\n   *\n   * <en/> Edges hidden during dragging\n   * - `'none'`: do not hide\n   * - `'out'`: hide the edges with the node as the source node\n   * - `'in'`: hide the edges with the node as the target node\n   * - `'both'`: hide all edges related to the node\n   * - `'all'`: hide all edges in the graph\n   * @remarks\n   * <zh/> 使用幽灵节点时不会隐藏边\n   *\n   * <en/> Edges will not be hidden when using the drag shadow\n   * @defaultValue 'none'\n   */\n  hideEdge?: 'none' | 'all' | EdgeDirection;\n  /**\n   * <zh/> 是否启用幽灵节点，即用一个图形代替节点跟随鼠标移动\n   *\n   * <en/> Whether to enable the drag shadow, that is, use a shape to replace the node to follow the mouse movement\n   */\n  shadow?: boolean;\n  /**\n   * <zh/> 完成拖拽时的回调\n   *\n   * <en/> Callback when dragging is completed\n   */\n  onFinish?: (ids: ID[]) => void;\n  /**\n   * <zh/> 指针样式\n   *\n   * <en/> Cursor style\n   */\n  cursor?: {\n    /**\n     * <zh/> 默认指针样式\n     *\n     * <en/> Default cursor style\n     */\n    default?: Cursor;\n    /**\n     * <zh/> 可抓取指针样式\n     *\n     * <en/> Cursor style that can be grabbed\n     */\n    grab: Cursor;\n    /**\n     * <zh/> 抓取中指针样式\n     *\n     * <en/> Cursor style when grabbing\n     */\n    grabbing: Cursor;\n  };\n}\n\n/**\n * <zh/> 拖拽元素交互\n *\n * <en/> Drag element behavior\n */\nexport class DragElement extends BaseBehavior<DragElementOptions> {\n  static defaultOptions: Partial<DragElementOptions> = {\n    animation: true,\n    enable: (event) => ['node', 'combo'].includes(event.targetType),\n    trigger: [],\n    dropEffect: 'move',\n    state: 'selected',\n    hideEdge: 'none',\n    shadow: false,\n    shadowZIndex: 100,\n    shadowFill: '#F3F9FF',\n    shadowFillOpacity: 0.5,\n    shadowStroke: '#1890FF',\n    shadowStrokeOpacity: 0.9,\n    shadowLineDash: [5, 5],\n    cursor: {\n      default: 'default',\n      grab: 'grab',\n      grabbing: 'grabbing',\n    },\n  };\n\n  protected enable: boolean = false;\n\n  private enableElements = ['node', 'combo'];\n\n  protected target: ID[] = [];\n\n  private shadow?: Rect;\n\n  private shadowOrigin: Point = [0, 0];\n\n  private hiddenEdges: ID[] = [];\n\n  private isDragging: boolean = false;\n\n  private shortcut: Shortcut;\n\n  constructor(context: RuntimeContext, options: DragElementOptions) {\n    super(context, Object.assign({}, DragElement.defaultOptions, options));\n    this.shortcut = new Shortcut(context.graph);\n\n    this.onDragStart = this.onDragStart.bind(this);\n    this.onDrag = this.onDrag.bind(this);\n    this.onDragEnd = this.onDragEnd.bind(this);\n    this.onDrop = this.onDrop.bind(this);\n\n    this.bindEvents();\n  }\n  /**\n   * <zh/> 更新元素拖拽配置\n   *\n   * <en/> Update the element dragging configuration\n   * @param options - <zh/> 配置项 | <en/> options\n   * @internal\n   */\n  public update(options: Partial<DragElementOptions>): void {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  private bindEvents() {\n    const { graph, canvas } = this.context;\n\n    // @ts-expect-error internal property\n    const $canvas: HTMLCanvasElement = canvas.getLayer().getContextService().$canvas;\n    if ($canvas) {\n      $canvas.addEventListener('blur', this.onDragEnd);\n      $canvas.addEventListener('contextmenu', this.onDragEnd);\n    }\n\n    this.enableElements.forEach((type) => {\n      graph.on(`${type}:${CommonEvent.DRAG_START}`, this.onDragStart);\n      graph.on(`${type}:${CommonEvent.DRAG}`, this.onDrag);\n      graph.on(`${type}:${CommonEvent.DRAG_END}`, this.onDragEnd);\n      graph.on(`${type}:${CommonEvent.POINTER_ENTER}`, this.setCursor);\n      graph.on(`${type}:${CommonEvent.POINTER_LEAVE}`, this.setCursor);\n    });\n\n    if (['link'].includes(this.options.dropEffect)) {\n      graph.on(ComboEvent.DROP, this.onDrop);\n      graph.on(CanvasEvent.DROP, this.onDrop);\n    }\n  }\n\n  /**\n   * <zh/> 获取当前选中的节点 id 集合\n   *\n   * <en/> Get the id collection of the currently selected node\n   * @param currTarget - <zh/> 当前拖拽目标元素 id 集合 | <en/> The id collection of the current drag target element\n   * @returns <zh/> 当前选中的节点 id 集合 | <en/> The id collection of the currently selected node\n   * @internal\n   */\n  protected getSelectedNodeIDs(currTarget: ID[]) {\n    return Array.from(\n      new Set(\n        this.context.graph\n          .getElementDataByState('node', this.options.state)\n          .map((node) => node.id)\n          .concat(currTarget),\n      ),\n    );\n  }\n\n  /**\n   * Get the delta of the drag\n   * @param event - drag event object\n   * @returns delta\n   * @internal\n   */\n  protected getDelta(event: IElementDragEvent) {\n    const zoom = this.context.graph.getZoom();\n    return divide([event.dx, event.dy], zoom);\n  }\n\n  /**\n   * <zh/> 拖拽开始时的回调\n   *\n   * <en/> Callback when dragging starts\n   * @param event - <zh/> 拖拽事件对象 | <en/> drag event object\n   * @internal\n   */\n  protected onDragStart(event: IElementDragEvent) {\n    this.enable = this.validate(event);\n    if (!this.enable) return;\n\n    const { batch, canvas, graph } = this.context;\n    canvas.setCursor(this.options!.cursor?.grabbing || 'grabbing');\n    this.isDragging = true;\n    batch!.startBatch();\n\n    // 如果当前节点是选中状态，则查询出画布中所有选中的节点，否则只拖拽当前节点\n    // If the current node is selected, query all selected nodes in the canvas, otherwise only drag the current node\n    const id = event.target.id;\n    const states = graph.getElementState(id);\n    if (states.includes(this.options.state)) this.target = this.getSelectedNodeIDs([id]);\n    else this.target = [id];\n\n    this.hideEdge();\n    this.context.graph.frontElement(this.target);\n    if (this.options.shadow) this.createShadow(this.target);\n  }\n\n  /**\n   * <zh/> 拖拽过程中的回调\n   *\n   * <en/> Callback when dragging\n   * @param event - <zh/> 拖拽事件对象 | <en/> drag event object\n   * @internal\n   */\n  protected onDrag(event: IElementDragEvent) {\n    if (!this.enable) return;\n    const delta = this.getDelta(event);\n\n    if (this.options.shadow) this.moveShadow(delta);\n    else this.moveElement(this.target, delta);\n  }\n\n  /**\n   * <zh/> 元素拖拽结束的回调\n   *\n   * <en/> Callback when dragging ends\n   * @internal\n   */\n  protected onDragEnd() {\n    if (!this.enable) return; // It can be called multiple times\n    this.enable = false;\n    if (this.options.shadow) {\n      if (!this.shadow) return;\n      this.shadow.style.visibility = 'hidden';\n      const { x = 0, y = 0 } = this.shadow.attributes;\n      const [dx, dy] = subtract([+x, +y], this.shadowOrigin);\n      this.moveElement(this.target, [dx, dy]);\n    }\n    this.showEdges();\n    this.options.onFinish?.(this.target);\n    const { batch, canvas } = this.context;\n    batch!.endBatch();\n    canvas.setCursor(this.options!.cursor?.grab || 'grab');\n    this.isDragging = false;\n    this.target = [];\n  }\n\n  /**\n   * <zh/> 拖拽放下的回调\n   *\n   * <en/> Callback when dragging is released\n   * @param event - <zh/> 拖拽事件对象 | <en/> drag event object\n   */\n  private onDrop = async (event: IElementDragEvent) => {\n    if (this.options.dropEffect !== 'link') return;\n    const { model, element } = this.context;\n    const modifiedParentId = event.target.id;\n    this.target.forEach((id) => {\n      const originalParent = model.getParentData(id, COMBO_KEY);\n      // 如果是在原父 combo 内部拖拽，需要刷新 combo 数据\n      // If it is a drag and drop within the original parent combo, you need to refresh the combo data\n      if (originalParent && idOf(originalParent) === modifiedParentId) {\n        model.refreshComboData(modifiedParentId);\n      }\n      model.setParent(id, modifiedParentId, COMBO_KEY);\n    });\n    await element?.draw({ animation: true })?.finished;\n  };\n\n  private setCursor = (event: IPointerEvent) => {\n    if (this.isDragging) return;\n    const { type } = event;\n    const { canvas } = this.context;\n    const { cursor } = this.options;\n\n    if (type === CommonEvent.POINTER_ENTER) canvas.setCursor(cursor?.grab || 'grab');\n    else canvas.setCursor(cursor?.default || 'default');\n  };\n\n  /**\n   * <zh/> 当前按键是否和 trigger 配置一致\n   *\n   * <en/> Is the current key consistent with the trigger configuration\n   * @returns <zh/> 是否一致 | <en/> Is consistent\n   * @internal\n   */\n  protected isKeydown(): boolean {\n    const { trigger } = this.options;\n    if (!trigger?.length) return true;\n    return this.shortcut.match(trigger);\n  }\n\n  /**\n   * <zh/> 验证元素是否允许拖拽\n   *\n   * <en/> Verify if the element is allowed to be dragged\n   * @param event - <zh/> 拖拽事件对象 | <en/> drag event object\n   * @returns <zh/> 是否允许拖拽 | <en/> Whether to allow dragging\n   * @internal\n   */\n  protected validate(event: IElementDragEvent) {\n    if (\n      this.destroyed ||\n      isToBeDestroyed(event.target) ||\n      // @ts-expect-error private property\n      // 避免动画冲突，在combo/node折叠展开过程中不触发\n      this.context.graph.isCollapsingExpanding ||\n      !this.isKeydown()\n    )\n      return false;\n    const { enable } = this.options;\n    if (isFunction(enable)) return enable(event);\n    return !!enable;\n  }\n\n  protected clampByRotation([dx, dy]: Point): Vector2 {\n    const rotation = this.context.graph.getRotation();\n    return rotate([dx, dy], rotation);\n  }\n\n  /**\n   * <zh/> 移动元素\n   *\n   * <en/> Move the element\n   * @param ids - <zh/> 元素 id 集合 | <en/> element id collection\n   * @param offset <zh/> 偏移量 | <en/> offset\n   * @internal\n   */\n  protected async moveElement(ids: ID[], offset: Point) {\n    const { graph, model } = this.context;\n    const { dropEffect } = this.options;\n\n    if (dropEffect === 'move') ids.forEach((id) => model.refreshComboData(id));\n    graph.translateElementBy(Object.fromEntries(ids.map((id) => [id, this.clampByRotation(offset)])), false);\n  }\n\n  private moveShadow(offset: Point) {\n    if (!this.shadow) return;\n    const { x = 0, y = 0 } = this.shadow.attributes;\n    const [dx, dy] = offset;\n    this.shadow.attr({ x: +x + dx, y: +y + dy });\n  }\n\n  private createShadow(target: ID[]) {\n    const shadowStyle = subStyleProps(this.options, 'shadow');\n\n    const bbox = getCombinedBBox(target.map((id) => this.context.element!.getElement(id)!.getBounds()));\n    const [x, y] = bbox.min;\n    this.shadowOrigin = [x, y];\n    const [width, height] = getBBoxSize(bbox);\n    const positionStyle = { width, height, x, y };\n\n    if (this.shadow) {\n      this.shadow.attr({\n        ...shadowStyle,\n        ...positionStyle,\n        visibility: 'visible',\n      });\n    } else {\n      this.shadow = new Rect({\n        style: {\n          // @ts-ignore $layer is not in the type definition\n          $layer: 'transient',\n          ...shadowStyle,\n          ...positionStyle,\n          pointerEvents: 'none',\n        },\n      });\n      this.context.canvas.appendChild(this.shadow);\n    }\n  }\n\n  private showEdges() {\n    if (this.options.shadow || this.hiddenEdges.length === 0) return;\n    this.context.graph.showElement(this.hiddenEdges);\n    this.hiddenEdges = [];\n  }\n\n  /**\n   * Hide the edge\n   * @internal\n   */\n  protected hideEdge() {\n    const { hideEdge, shadow } = this.options;\n    if (hideEdge === 'none' || shadow) return;\n    const { graph } = this.context;\n    if (hideEdge === 'all') this.hiddenEdges = graph.getEdgeData().map(idOf);\n    else {\n      this.hiddenEdges = Array.from(\n        new Set(this.target.map((id) => graph.getRelatedEdgesData(id, hideEdge).map(idOf)).flat()),\n      );\n    }\n    graph.hideElement(this.hiddenEdges);\n  }\n\n  private unbindEvents() {\n    const { graph, canvas } = this.context;\n\n    // @ts-expect-error internal property\n    const $canvas: HTMLCanvasElement = canvas.getLayer().getContextService().$canvas;\n    if ($canvas) {\n      $canvas.removeEventListener('blur', this.onDragEnd);\n      $canvas.removeEventListener('contextmenu', this.onDragEnd);\n    }\n\n    this.enableElements.forEach((type) => {\n      graph.off(`${type}:${CommonEvent.DRAG_START}`, this.onDragStart);\n      graph.off(`${type}:${CommonEvent.DRAG}`, this.onDrag);\n      graph.off(`${type}:${CommonEvent.DRAG_END}`, this.onDragEnd);\n      graph.off(`${type}:${CommonEvent.POINTER_ENTER}`, this.setCursor);\n      graph.off(`${type}:${CommonEvent.POINTER_LEAVE}`, this.setCursor);\n    });\n\n    graph.off(`combo:${CommonEvent.DROP}`, this.onDrop);\n    graph.off(`canvas:${CommonEvent.DROP}`, this.onDrop);\n  }\n\n  public destroy() {\n    this.unbindEvents();\n    this.shadow?.destroy();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/behaviors/fix-element-size.ts",
    "content": "import type { DisplayObject } from '@antv/g';\nimport { isEmpty, isFunction, isNumber } from '@antv/util';\nimport { GraphEvent } from '../constants';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { ComboData, EdgeData, NodeData } from '../spec';\nimport type {\n  Combo,\n  Edge,\n  Element,\n  ID,\n  IGraphLifeCycleEvent,\n  IViewportEvent,\n  Node,\n  NodeLikeData,\n  State,\n} from '../types';\nimport { idOf } from '../utils/id';\nimport { getDescendantShapes } from '../utils/shape';\nimport type { BaseBehaviorOptions } from './base-behavior';\nimport { BaseBehavior } from './base-behavior';\n\nexport type FixShapeConfig = {\n  /**\n   * <zh/> 指定要固定大小的图形，可以是图形的类名字，或者是一个函数，该函数接收构成元素的所有图形并返回目标图形\n   *\n   * <en/> Specify the shape to be fixed in size. This can be a class name string of the shape, or a function that takes all shapes composing the element and returns the target shape\n   */\n  shape: string | ((shapes: DisplayObject[]) => DisplayObject);\n  /**\n   * <zh/> 指定要固定大小的图形属性字段。如果未指定，则默认固定整个图形的大小\n   *\n   * <en/> Specify the fields of the shape to be fixed in size. If not specified, the entire shape's size will be fixed by default\n   */\n  fields?: string[];\n};\n\n/**\n * <zh/> 固定元素大小交互配置项\n *\n * <en/> Fix element size behavior options\n */\nexport interface FixElementSizeOptions extends BaseBehaviorOptions {\n  /**\n   * <zh/> 是否启用固定元素大小交互。默认在缩小画布时启用\n   *\n   * <en/> Whether to enable the fix element size behavior. Enabled by default when zooming out\n   * @remarks\n   * <zh/> 默认在缩小画布时启用，设置 `enable: (event) => event.data.scale < 1`；如果希望在放大画布时启用，设置 `enable: (event) => event.data.scale > 1`；如果希望在放大缩小画布时都启用，设置 `enable: true`\n   *\n   * <en/> Enabled by default when zooming out, set `enable: (event) => event.data.scale < 1`; If you want to enable it when zooming in, set `enable: (event) => event.data.scale > 1`; If you want to enable it when zooming in and out, set `enable: true`\n   * @defaultValue (event) => Boolean(event.data.scale < 1)\n   */\n  enable?: boolean | ((event: IViewportEvent) => boolean);\n  /**\n   * <zh/> 指定要固定大小的元素状态\n   *\n   * <en/> Specify the state of elements to be fixed in size\n   * @defaultValue `'selected'`\n   */\n  state?: State;\n  /**\n   * <zh/> 节点过滤器，用于过滤哪些节点在缩放过程中保持固定大小\n   *\n   * <en/> Node filter for filtering which nodes remain fixed in size during zooming\n   * @defaultValue () => true\n   */\n  nodeFilter?: (datum: NodeData) => boolean;\n  /**\n   * <zh/> 边过滤器，用于过滤哪些边在缩放过程中保持固定大小\n   *\n   * <en/> Edge filter for filtering which edges remain fixed in size during zooming\n   * @defaultValue () => true\n   */\n  edgeFilter?: (datum: EdgeData) => boolean;\n  /**\n   * <zh/> Combo 过滤器，用于过滤哪些 Combo 在缩放过程中保持固定大小\n   *\n   * <en/> Combo filter for filtering which combos remain fixed in size during zooming\n   * @defaultValue () => true\n   */\n  comboFilter?: (datum: ComboData) => boolean;\n  /**\n   * <zh/> 节点配置项，用于定义哪些属性在视觉上保持固定大小。若未指定（即为 undefined），则整个节点将被固定\n   *\n   * <en/> Node configuration for defining which node attributes should remain fixed in size visually. If not specified (i.e., undefined), the entire node will be fixed in size.\n   * @example\n   * <zh/> 如果在缩放过程中希望固定节点主图形的 lineWidth，可以这样配置：\n   *\n   * <en/> If you want to fix the lineWidth of the key shape of the node during zooming, you can configure it like this:\n   * ```ts\n   * { node: [{ shape: 'key', fields: ['lineWidth'] }] }\n   *```\n   * <zh/> 如果在缩放过程中想保持元素标签大小不变，可以这样配置：\n   *\n   * <en/> If you want to keep the label size of the element unchanged during zooming, you can configure it like this:\n   * ```ts\n   *  { shape: 'label' }\n   * ```\n   */\n  node?: FixShapeConfig | FixShapeConfig[];\n  /**\n   * <zh/> 边配置项，用于定义哪些属性在视觉上保持固定大小。默认固定 lineWidth、labelFontSize 属性\n   *\n   * <en/> Edge configuration for defining which edge attributes should remain fixed in size visually. By default, the lineWidth and labelFontSize attributes are fixed\n   * @defaultValue [{ shape: 'key', fields: ['lineWidth'] }, { shape: 'halo', fields: ['lineWidth'] }, { shape: 'label' }]\n   */\n  edge?: FixShapeConfig | FixShapeConfig[];\n  /**\n   * <zh/> Combo 配置项，用于定义哪些属性在视觉上保持固定大小。默认整个 Combo 将被固定\n   *\n   * <en/> Combo configuration for defining which combo attributes should remain fixed in size visually. By default, the entire combo will be fixed\n   */\n  combo?: FixShapeConfig | FixShapeConfig[];\n  /**\n   * <zh/> 元素重绘时是否还原样式\n   *\n   * <en/> Whether to reset styles when elements are redrawn\n   * @defaultValue false\n   */\n  reset?: boolean;\n}\n\n/**\n * <zh/> 缩放画布过程中固定元素大小\n *\n * <en/> Fix element size while zooming\n */\nexport class FixElementSize extends BaseBehavior<FixElementSizeOptions> {\n  static defaultOptions: Partial<FixElementSizeOptions> = {\n    enable: (event: IViewportEvent) => event.data.scale! < 1,\n    nodeFilter: () => true,\n    edgeFilter: () => true,\n    comboFilter: () => true,\n    edge: [{ shape: 'key', fields: ['lineWidth'] }, { shape: 'halo', fields: ['lineWidth'] }, { shape: 'label' }],\n    reset: false,\n  };\n\n  constructor(context: RuntimeContext, options: FixElementSizeOptions) {\n    super(context, Object.assign({}, FixElementSize.defaultOptions, options));\n    this.bindEvents();\n  }\n\n  private isZoomEvent = (event: IViewportEvent) => Boolean(event.data && 'scale' in event.data);\n\n  private relatedEdgeToUpdate: Set<ID> = new Set();\n\n  private zoom = this.context.graph.getZoom();\n\n  private fixElementSize = async (event: IViewportEvent) => {\n    if (!this.validate(event)) return;\n\n    const { graph } = this.context;\n    const { state, nodeFilter, edgeFilter, comboFilter } = this.options;\n\n    const nodeData = (state ? graph.getElementDataByState('node', state) : graph.getNodeData()).filter(nodeFilter);\n    const edgeData = (state ? graph.getElementDataByState('edge', state) : graph.getEdgeData()).filter(edgeFilter);\n    const comboData = (state ? graph.getElementDataByState('combo', state) : graph.getComboData()).filter(comboFilter);\n\n    // 设置阈值防止过大或过小时抖动 | Set the threshold to prevent jitter when too large or too small\n    const currentScale = this.isZoomEvent(event)\n      ? (this.zoom = Math.max(0.01, Math.min(event.data.scale!, 10)))\n      : this.zoom;\n\n    const nodeLikeData = [...nodeData, ...comboData];\n    if (nodeLikeData.length > 0) {\n      nodeLikeData.forEach((datum) => this.fixNodeLike(datum, currentScale));\n    }\n\n    this.updateRelatedEdges();\n\n    if (edgeData.length > 0) {\n      edgeData.forEach((datum) => this.fixEdge(datum, currentScale));\n    }\n  };\n\n  private cachedStyles: Map<ID, { shape: DisplayObject; [field: string]: any }[]> = new Map();\n\n  private getOriginalFieldValue = (id: ID, shape: DisplayObject, field: string) => {\n    const shapesStyle = this.cachedStyles.get(id) || [];\n    const shapeStyle = shapesStyle.find((style) => style.shape === shape)?.style || {};\n    if (!(field in shapeStyle)) {\n      shapeStyle[field] = shape.attributes[field];\n      this.cachedStyles.set(id, [\n        ...shapesStyle.filter((style) => style.shape !== shape),\n        { shape, style: shapeStyle },\n      ]);\n    }\n    return shapeStyle[field];\n  };\n\n  private scaleEntireElement = (id: ID, el: DisplayObject, currentScale: number) => {\n    el.setLocalScale(1 / currentScale);\n    const shapesStyle = this.cachedStyles.get(id) || [];\n    shapesStyle.push({ shape: el });\n    this.cachedStyles.set(id, shapesStyle);\n  };\n\n  private scaleSpecificShapes = (el: Element, currentScale: number, config: FixShapeConfig | FixShapeConfig[]) => {\n    const descendantShapes = getDescendantShapes(el);\n    const configs = Array.isArray(config) ? config : [config];\n\n    configs.forEach((config: FixShapeConfig) => {\n      const { shape: shapeFilter, fields } = config;\n      const shape = typeof shapeFilter === 'function' ? shapeFilter(descendantShapes) : el.getShape(shapeFilter);\n\n      if (!shape) return;\n      if (!fields) {\n        this.scaleEntireElement(el.id, shape, currentScale);\n        return;\n      }\n\n      fields.forEach((field) => {\n        const oriFieldValue = this.getOriginalFieldValue(el.id, shape, field);\n        if (!isNumber(oriFieldValue)) return;\n        shape.style[field] = oriFieldValue / currentScale;\n      });\n    });\n  };\n\n  private skipIfExceedViewport = (el: Element) => {\n    const { viewport } = this.context;\n    return !viewport?.isInViewport(el.getRenderBounds(), false, 30);\n  };\n\n  private fixNodeLike = (datum: NodeLikeData, currentScale: number) => {\n    const id = idOf(datum);\n    const { element, model } = this.context;\n    const el = element!.getElement(id) as Node | Combo;\n\n    if (!el || this.skipIfExceedViewport(el)) return;\n\n    const edges = model.getRelatedEdgesData(id);\n    edges.forEach((edge) => this.relatedEdgeToUpdate.add(idOf(edge)));\n\n    const config = this.options[(el as any).type];\n    if (!config) {\n      this.scaleEntireElement(id, el, currentScale);\n      return;\n    }\n    this.scaleSpecificShapes(el, currentScale, config);\n  };\n\n  private fixEdge = (datum: EdgeData, currentScale: number) => {\n    const id = idOf(datum);\n    const el = this.context.element!.getElement(id) as Edge;\n\n    if (!el || this.skipIfExceedViewport(el)) return;\n\n    const config = this.options.edge;\n    if (!config) {\n      el.style.transformOrigin = 'center';\n      this.scaleEntireElement(id, el, currentScale);\n      return;\n    }\n    this.scaleSpecificShapes(el, currentScale, config);\n  };\n\n  private updateRelatedEdges = () => {\n    const { element } = this.context;\n    if (this.relatedEdgeToUpdate.size > 0) {\n      this.relatedEdgeToUpdate.forEach((id) => {\n        const edge = element!.getElement(id) as Edge;\n        edge?.update({});\n      });\n    }\n    this.relatedEdgeToUpdate.clear();\n  };\n\n  private restoreCachedStyles() {\n    if (this.cachedStyles.size > 0) {\n      this.cachedStyles.forEach((shapesStyle) => {\n        shapesStyle.forEach(({ shape, style }) => {\n          if (isEmpty(style)) {\n            shape.setLocalScale(1);\n          } else {\n            if (this.options.state) return;\n            Object.entries(style).forEach(([field, value]) => (shape.style[field] = value));\n          }\n        });\n      });\n      const { graph, element } = this.context;\n      const nodeIds = Object.keys(Object.fromEntries(this.cachedStyles)).filter(\n        (id) => id && graph.getElementType(id) === 'node',\n      );\n      if (nodeIds.length > 0) {\n        const edgeIds = new Set<ID>();\n        nodeIds.forEach((id) => {\n          graph.getRelatedEdgesData(id).forEach((edge) => edgeIds.add(idOf(edge)));\n        });\n        edgeIds.forEach((id) => {\n          const edge = element?.getElement(id) as Edge;\n          edge?.update({});\n        });\n      }\n    }\n    // this.cachedStyles.clear();\n  }\n\n  private resetTransform = async (event: IGraphLifeCycleEvent) => {\n    // 首屏渲染时跳过 | Skip when rendering the first screen\n    if (event.data?.firstRender) return;\n\n    if (this.options.reset) {\n      this.restoreCachedStyles();\n    } else {\n      this.fixElementSize({ data: { scale: this.zoom } } as IViewportEvent);\n    }\n  };\n\n  private bindEvents() {\n    const { graph } = this.context;\n    graph.on(GraphEvent.AFTER_DRAW, this.resetTransform);\n    graph.on(GraphEvent.AFTER_TRANSFORM, this.fixElementSize);\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n    graph.off(GraphEvent.AFTER_DRAW, this.resetTransform);\n    graph.off(GraphEvent.AFTER_TRANSFORM, this.fixElementSize);\n  }\n\n  private validate(event: IViewportEvent) {\n    if (this.destroyed) return false;\n    const { enable } = this.options;\n    if (isFunction(enable)) return enable(event);\n    return !!enable;\n  }\n\n  public destroy(): void {\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/behaviors/focus-element.ts",
    "content": "import { isFunction } from '@antv/util';\nimport { CommonEvent } from '../constants';\nimport { ELEMENT_TYPES } from '../constants/element';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { IElementEvent, ViewportAnimationEffectTiming } from '../types';\nimport { Shortcut, ShortcutKey } from '../utils/shortcut';\nimport type { BaseBehaviorOptions } from './base-behavior';\nimport { BaseBehavior } from './base-behavior';\n\n/**\n * <zh/> 聚焦元素交互配置项\n *\n * <en/> Focus element behavior options\n */\nexport interface FocusElementOptions extends BaseBehaviorOptions {\n  /**\n   * <zh/> 是否启用动画以及动画配置\n   *\n   * <en/> Whether to enable animation\n   */\n  animation?: ViewportAnimationEffectTiming;\n  /**\n   * <zh/> 是否启用聚焦功能\n   *\n   * <en/> Whether to enable the function of focusing on the element\n   * @defaultValue true\n   */\n  enable?: boolean | ((event: IElementEvent) => boolean);\n  /**\n   * <zh/> 触发聚焦的组合键\n   * 支持按下组合键的同时点击元素才能触发聚焦\n   *\n   * <en/> The shortcut key to trigger focus\n   * Focus can only be triggered when the element is clicked while the key combination is pressed.\n   */\n  trigger?: ShortcutKey;\n}\n\n/**\n * <zh/> 聚焦元素交互行为\n *\n * <en/> Focus element behavior\n * @remarks\n * <zh/> 点击元素时，将元素聚焦到视图中心。\n *\n * <en/> When an element is clicked, the element is focused to the center of the view.\n */\nexport class FocusElement extends BaseBehavior<FocusElementOptions> {\n  static defaultOptions: Partial<FocusElementOptions> = {\n    animation: {\n      easing: 'ease-in',\n      duration: 500,\n    },\n    enable: true,\n    trigger: [],\n  };\n\n  private shortcut: Shortcut;\n\n  constructor(context: RuntimeContext, options: FocusElementOptions) {\n    super(context, Object.assign({}, FocusElement.defaultOptions, options));\n    this.shortcut = new Shortcut(context.graph);\n    this.bindEvents();\n  }\n\n  private bindEvents() {\n    const { graph } = this.context;\n    this.unbindEvents();\n\n    ELEMENT_TYPES.forEach((type) => {\n      graph.on(`${type}:${CommonEvent.CLICK}`, this.focus);\n    });\n  }\n\n  private focus = async (event: IElementEvent) => {\n    if (!this.validate(event)) return;\n    const { graph } = this.context;\n\n    await graph.focusElement(event.target.id, this.options.animation);\n  };\n\n  private validate(event: IElementEvent) {\n    if (this.destroyed || !this.isKeydown()) return false;\n    const { enable } = this.options;\n    if (isFunction(enable)) return enable(event);\n    return !!enable;\n  }\n\n  /**\n   * <zh/> 当前按键是否和 trigger 配置一致\n   *\n   * <en/> Is the current key consistent with the trigger configuration\n   * @returns <zh/> 是否一致 | <en/> Is consistent\n   * @internal\n   */\n  private isKeydown(): boolean {\n    const { trigger } = this.options;\n    if (!trigger?.length) return true;\n    return this.shortcut.match(trigger);\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n\n    ELEMENT_TYPES.forEach((type) => {\n      graph.off(`${type}:${CommonEvent.CLICK}`, this.focus);\n    });\n  }\n\n  public destroy() {\n    this.unbindEvents();\n    this.shortcut.destroy();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/behaviors/hover-activate.ts",
    "content": "import { isFunction } from '@antv/util';\nimport { CommonEvent } from '../constants';\nimport { ELEMENT_TYPES } from '../constants/element';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { EdgeDirection, Element, ElementType, ID, IDragEvent, IPointerEvent, State } from '../types';\nimport { isToBeDestroyed } from '../utils/element';\nimport { idsOf } from '../utils/id';\nimport { getElementNthDegreeIds } from '../utils/relation';\nimport type { BaseBehaviorOptions } from './base-behavior';\nimport { BaseBehavior } from './base-behavior';\n\n/**\n * <zh/> 悬浮元素交互配置项\n *\n * <en/> Hover element behavior options\n */\nexport interface HoverActivateOptions extends BaseBehaviorOptions {\n  /**\n   * <zh/> 是否启用动画\n   *\n   * <en/> Whether to enable animation\n   * @defaultValue true\n   */\n  animation?: boolean;\n  /**\n   * <zh/> 是否启用悬浮元素的功能\n   *\n   * <en/> Whether to enable hover element function\n   * @defaultValue true\n   */\n  enable?: boolean | ((event: IPointerEvent) => boolean);\n  /**\n   * <zh/> 激活元素的n度关系\n   * - 默认为 `0`，表示只激活当前节点\n   * - `1` 表示激活当前节点及其直接相邻的节点和边，以此类推\n   *\n   * <en/> N-degree relationship of the hovered element\n   * - default to `0`, which means only the current node is activated\n   * - `1` means the current node and its directly adjacent nodes and edges are activated, etc\n   * @defaultValue 0\n   */\n  degree?: number | ((event: IPointerEvent) => number);\n  /**\n   * <zh/> 指定边的方向\n   * - `'both'`: 表示激活当前节点的所有关系\n   * - `'in'`: 表示激活当前节点的入边和入节点\n   * - `'out'`: 表示激活当前节点的出边和出节点\n   *\n   * <en/> Specify the direction of the edge\n   * - `'both'`: Activate all relationships of the current node\n   * - `'in'`: Activate the incoming edges and nodes of the current node\n   * - `'out'`: Activate the outgoing edges and nodes of the current node\n   * @defaultValue 'both'\n   */\n  direction?: EdgeDirection;\n  /**\n   * <zh/> 激活元素的状态，默认为 `active`\n   *\n   * <en/> Active element state, default to`active`\n   * @defaultValue 'active'\n   */\n  state?: State;\n  /**\n   * <zh/> 非激活元素的状态，默认为不改变\n   *\n   * <en/> Inactive element state, default to no change\n   */\n  inactiveState?: State;\n  /**\n   * <zh/> 当元素被悬停时的回调\n   *\n   * <en/> Callback when the element is hovered\n   */\n  onHover?: (event: IPointerEvent) => void;\n  /**\n   * <zh/> 当悬停结束时的回调\n   *\n   * <en/> Callback when the hover ends\n   */\n  onHoverEnd?: (event: IPointerEvent) => void;\n}\n\n/**\n * <zh/> 悬浮元素交互\n *\n * <en/> Hover element behavior\n * @remarks\n * <zh/> 当鼠标悬停在元素上时，可以激活元素的状态，例如高亮节点或边。\n *\n * <en/> When the mouse hovers over an element, you can activate the state of the element, such as highlighting nodes or edges.\n */\nexport class HoverActivate extends BaseBehavior<HoverActivateOptions> {\n  static defaultOptions: Partial<HoverActivateOptions> = {\n    animation: false,\n    enable: true,\n    degree: 0,\n    direction: 'both',\n    state: 'active',\n    inactiveState: undefined,\n  };\n\n  private isFrozen = false;\n\n  constructor(context: RuntimeContext, options: HoverActivateOptions) {\n    super(context, Object.assign({}, HoverActivate.defaultOptions, options));\n    this.bindEvents();\n  }\n\n  private toggleFrozen = (e: IDragEvent) => {\n    this.isFrozen = e.type === 'dragstart';\n  };\n\n  private bindEvents() {\n    const { graph } = this.context;\n    this.unbindEvents();\n\n    ELEMENT_TYPES.forEach((type) => {\n      graph.on(`${type}:${CommonEvent.POINTER_ENTER}`, this.hoverElement);\n      graph.on(`${type}:${CommonEvent.POINTER_LEAVE}`, this.hoverElement);\n    });\n\n    const canvas = this.context.canvas.document;\n    canvas.addEventListener(`${CommonEvent.DRAG_START}`, this.toggleFrozen);\n    canvas.addEventListener(`${CommonEvent.DRAG_END}`, this.toggleFrozen);\n  }\n\n  private hoverElement = (event: IPointerEvent<Element>) => {\n    if (!this.validate(event)) return;\n    const isEnter = event.type === CommonEvent.POINTER_ENTER;\n    this.updateElementsState(event, isEnter);\n\n    const { onHover, onHoverEnd } = this.options;\n    if (isEnter) onHover?.(event);\n    else onHoverEnd?.(event);\n  };\n\n  protected getActiveIds(event: IPointerEvent<Element>) {\n    const { graph } = this.context;\n    const { degree, direction } = this.options;\n    const elementId = event.target.id;\n\n    return degree\n      ? getElementNthDegreeIds(\n          graph,\n          event.targetType as ElementType,\n          elementId,\n          typeof degree === 'function' ? degree(event) : degree,\n          direction,\n        )\n      : [elementId];\n  }\n\n  private updateElementsState = (event: IPointerEvent<Element>, add: boolean) => {\n    if (!this.options.state && !this.options.inactiveState) return;\n\n    const { graph } = this.context;\n    const { state, animation, inactiveState } = this.options;\n\n    const activeIds = this.getActiveIds(event);\n    const states: Record<ID, State[]> = {};\n\n    if (state) {\n      Object.assign(states, this.getElementsState(activeIds, state, add));\n    }\n\n    if (inactiveState) {\n      const inactiveIds = idsOf(graph.getData(), true).filter((id) => !activeIds.includes(id));\n      Object.assign(states, this.getElementsState(inactiveIds, inactiveState, add));\n    }\n    graph.setElementState(states, animation);\n  };\n\n  private getElementsState = (ids: ID[], state: State, add: boolean) => {\n    const { graph } = this.context;\n    const states: Record<ID, State[]> = {};\n    ids.forEach((id) => {\n      const currentState = graph.getElementState(id);\n      if (add) {\n        states[id] = currentState.includes(state) ? currentState : [...currentState, state];\n      } else {\n        states[id] = currentState.filter((s) => s !== state);\n      }\n    });\n    return states;\n  };\n\n  private validate(event: IPointerEvent<Element>) {\n    if (\n      this.destroyed ||\n      this.isFrozen ||\n      isToBeDestroyed(event.target) ||\n      // @ts-expect-error private property\n      // 避免动画冲突，在combo/node折叠展开过程中不触发\n      this.context.graph.isCollapsingExpanding\n    )\n      return false;\n    const { enable } = this.options;\n    if (isFunction(enable)) return enable(event);\n    return !!enable;\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n\n    ELEMENT_TYPES.forEach((type) => {\n      graph.off(`${type}:${CommonEvent.POINTER_ENTER}`, this.hoverElement);\n      graph.off(`${type}:${CommonEvent.POINTER_LEAVE}`, this.hoverElement);\n    });\n\n    const canvas = this.context.canvas.document;\n    canvas.removeEventListener(`${CommonEvent.DRAG_START}`, this.toggleFrozen);\n    canvas.removeEventListener(`${CommonEvent.DRAG_END}`, this.toggleFrozen);\n  }\n\n  public destroy() {\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/behaviors/index.ts",
    "content": "export { AutoAdaptLabel } from './auto-adapt-label';\nexport { BaseBehavior } from './base-behavior';\nexport { BrushSelect } from './brush-select';\nexport { ClickSelect } from './click-select';\nexport { CollapseExpand } from './collapse-expand';\nexport { CreateEdge } from './create-edge';\nexport { DragCanvas } from './drag-canvas';\nexport { DragElement } from './drag-element';\nexport { DragElementForce } from './drag-element-force';\nexport { FixElementSize } from './fix-element-size';\nexport { FocusElement } from './focus-element';\nexport { HoverActivate } from './hover-activate';\nexport { LassoSelect } from './lasso-select';\nexport { OptimizeViewportTransform } from './optimize-viewport-transform';\nexport { ScrollCanvas } from './scroll-canvas';\nexport { ZoomCanvas } from './zoom-canvas';\n\nexport type { AutoAdaptLabelOptions } from './auto-adapt-label';\nexport type { BaseBehaviorOptions } from './base-behavior';\nexport type { BrushSelectOptions } from './brush-select';\nexport type { ClickSelectOptions } from './click-select';\nexport type { CollapseExpandOptions } from './collapse-expand';\nexport type { CreateEdgeOptions } from './create-edge';\nexport type { DragCanvasOptions } from './drag-canvas';\nexport type { DragElementOptions } from './drag-element';\nexport type { DragElementForceOptions } from './drag-element-force';\nexport type { FixElementSizeOptions } from './fix-element-size';\nexport type { FocusElementOptions } from './focus-element';\nexport type { HoverActivateOptions } from './hover-activate';\nexport type { LassoSelectOptions } from './lasso-select';\nexport type { OptimizeViewportTransformOptions } from './optimize-viewport-transform';\nexport type { ScrollCanvasOptions } from './scroll-canvas';\nexport type { ZoomCanvasOptions } from './zoom-canvas';\n"
  },
  {
    "path": "packages/g6/src/behaviors/lasso-select.ts",
    "content": "import { Path } from '@antv/g';\nimport type { IPointerEvent, Point } from '../types';\nimport { pointsToPath } from '../utils/path';\nimport type { BrushSelectOptions } from './brush-select';\nimport { BrushSelect, getCursorPoint } from './brush-select';\n\n/**\n * <zh/> 套索选择交互配置项\n *\n * <en/> Lasso select behavior options\n */\nexport interface LassoSelectOptions extends BrushSelectOptions {}\n\n/**\n * <zh/> 套索选择交互\n *\n * <en/> Lasso select behavior\n * @remarks\n * <zh/> 用不规则多边形框选一组元素。\n *\n * <en/> Select a group of elements with an irregular polygon.\n */\nexport class LassoSelect extends BrushSelect {\n  private points?: Point[];\n  private pathShape?: Path;\n\n  /**\n   * Triggered when the mouse is pressed\n   * @param event - mouse event\n   * @internal\n   */\n  protected onPointerDown(event: IPointerEvent) {\n    if (!super.validate(event) || !super.isKeydown() || this.points) return;\n    const { canvas, graph } = this.context;\n\n    this.pathShape = new Path({\n      id: 'g6-lasso-select',\n      style: this.options.style,\n    });\n\n    canvas.appendChild(this.pathShape);\n\n    this.points = [getCursorPoint(event, graph)];\n  }\n\n  /**\n   * Triggered when the mouse is moved\n   * @param event - mouse event\n   * @internal\n   */\n  protected onPointerMove(event: IPointerEvent) {\n    if (!this.points) return;\n    const { immediately, mode } = this.options;\n\n    this.points.push(getCursorPoint(event, this.context.graph));\n\n    this.pathShape?.setAttribute('d', pointsToPath(this.points));\n\n    if (immediately && mode === 'default' && this.points.length > 2) super.updateElementsStates(this.points);\n  }\n\n  /**\n   * Triggered when the mouse is released\n   * @internal\n   */\n  protected onPointerUp() {\n    if (!this.points) return;\n    if (this.points.length < 2) {\n      this.clearLasso();\n      return;\n    }\n    super.updateElementsStates(this.points);\n\n    this.clearLasso();\n  }\n\n  private clearLasso() {\n    this.pathShape?.remove();\n    this.pathShape = undefined;\n    this.points = undefined;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/behaviors/optimize-viewport-transform.ts",
    "content": "import type { BaseStyleProps, DisplayObject } from '@antv/g';\nimport { debounce, isFunction } from '@antv/util';\nimport { GraphEvent } from '../constants';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { ElementType } from '../types';\nimport type { IViewportEvent } from '../types/event';\nimport { setVisibility } from '../utils/visibility';\nimport type { BaseBehaviorOptions } from './base-behavior';\nimport { BaseBehavior } from './base-behavior';\n\n/**\n * <zh/> 画布优化交互配置项\n *\n * <en/> Canvas optimization behavior options\n */\nexport interface OptimizeViewportTransformOptions extends BaseBehaviorOptions {\n  /**\n   * <zh/> 是否启用画布优化功能\n   *\n   * <en/> Whether to enable canvas optimization function\n   * @defaultValue true\n   */\n  enable?: boolean | ((event: IViewportEvent) => boolean);\n  /**\n   * <zh/> 指定始终显示的图形元素。应用此交互后，在画布操作过程中，只有通过该属性指定的图形元素会保持可见，其余图形元素将被隐藏，从而提升渲染性能。默认情况下，节点始终可见，而其他图形元素在操作画布过程中会自动隐藏\n   *\n   * <en/> Specify the shapes that are always visible. After applying this interaction, only the shapes specified by this property will remain visible during the canvas operation, and the rest of the shapes will be hidden to improve rendering performance. By default, nodes are always visible, while other shapes are automatically hidden during canvas operations\n   */\n  shapes?:\n    | { node?: string[]; edge?: string[]; combo?: string[] }\n    | ((type: ElementType, shape: DisplayObject) => boolean);\n  /**\n   * <zh/> 设置防抖时间\n   *\n   * <en/> Set debounce time\n   * @defaultValue 200\n   */\n  debounce?: number;\n}\n\n/**\n * <zh/> 操作画布过程中隐藏元素\n *\n * <en/> Hide elements during canvas operations (dragging, zooming, scrolling)\n */\nexport class OptimizeViewportTransform extends BaseBehavior<OptimizeViewportTransformOptions> {\n  static defaultOptions: Partial<OptimizeViewportTransformOptions> = {\n    enable: true,\n    debounce: 200,\n    shapes: (type: ElementType) => type === 'node',\n  };\n\n  private hiddenShapes: DisplayObject[] = [];\n\n  private isVisible: boolean = true;\n\n  constructor(context: RuntimeContext, options: OptimizeViewportTransformOptions) {\n    super(context, Object.assign({}, OptimizeViewportTransform.defaultOptions, options));\n    this.bindEvents();\n  }\n\n  private setElementsVisibility = (\n    elements: DisplayObject[],\n    visibility: BaseStyleProps['visibility'],\n    filter?: (shape: DisplayObject) => boolean,\n  ) => {\n    elements.filter(Boolean).forEach((element) => {\n      if (visibility === 'hidden' && !element.isVisible()) {\n        this.hiddenShapes.push(element);\n      } else if (visibility === 'visible' && this.hiddenShapes.includes(element)) {\n        this.hiddenShapes.splice(this.hiddenShapes.indexOf(element), 1);\n      } else {\n        setVisibility(element, visibility, filter);\n      }\n    });\n  };\n\n  private filterShapes = (type: ElementType, filter: OptimizeViewportTransformOptions['shapes']) => {\n    if (isFunction(filter)) return (shape: DisplayObject) => !filter(type, shape);\n    const includesClassnames = filter?.[type];\n    return (shape: DisplayObject) => {\n      if (!shape.className) return true;\n      return !includesClassnames?.includes(shape.className);\n    };\n  };\n\n  private hideShapes = (event: IViewportEvent) => {\n    if (!this.validate(event) || !this.isVisible) return;\n\n    const { element } = this.context;\n    const { shapes = {} } = this.options;\n    this.setElementsVisibility(element!.getNodes(), 'hidden', this.filterShapes('node', shapes));\n    this.setElementsVisibility(element!.getEdges(), 'hidden', this.filterShapes('edge', shapes));\n    this.setElementsVisibility(element!.getCombos(), 'hidden', this.filterShapes('combo', shapes));\n    this.isVisible = false;\n  };\n\n  private showShapes = debounce((event: IViewportEvent) => {\n    if (!this.validate(event) || this.isVisible) return;\n\n    const { element } = this.context;\n    this.setElementsVisibility(element!.getNodes(), 'visible');\n    this.setElementsVisibility(element!.getEdges(), 'visible');\n    this.setElementsVisibility(element!.getCombos(), 'visible');\n    this.isVisible = true;\n  }, this.options.debounce);\n\n  private bindEvents() {\n    const { graph } = this.context;\n\n    graph.on(GraphEvent.BEFORE_TRANSFORM, this.hideShapes);\n    graph.on(GraphEvent.AFTER_TRANSFORM, this.showShapes);\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n\n    graph.off(GraphEvent.BEFORE_TRANSFORM, this.hideShapes);\n    graph.off(GraphEvent.AFTER_TRANSFORM, this.showShapes);\n  }\n\n  private validate(event: IViewportEvent) {\n    if (this.destroyed) return false;\n\n    const { enable } = this.options;\n    if (isFunction(enable)) return enable(event);\n    return !!enable;\n  }\n\n  public update(options: Partial<OptimizeViewportTransformOptions>) {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  public destroy() {\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/behaviors/scroll-canvas.ts",
    "content": "import { isFunction, isObject } from '@antv/util';\nimport { CommonEvent } from '../constants';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { IKeyboardEvent, Point } from '../types';\nimport { getExpandedBBox, getPointBBox, isPointInBBox } from '../utils/bbox';\nimport { parsePadding } from '../utils/padding';\nimport { Shortcut, ShortcutKey } from '../utils/shortcut';\nimport { multiply, subtract } from '../utils/vector';\nimport type { BaseBehaviorOptions } from './base-behavior';\nimport { BaseBehavior } from './base-behavior';\n\n/**\n * <zh/> 滚动画布交互配置项\n *\n * <en/> Scroll canvas behavior options\n */\nexport interface ScrollCanvasOptions extends BaseBehaviorOptions {\n  /**\n   * <zh/> 是否启用滚动画布的功能\n   *\n   * <en/> Whether to enable the function of scrolling the canvas\n   * @defaultValue true\n   */\n  enable?: boolean | ((event: WheelEvent | IKeyboardEvent) => boolean);\n  /**\n   * <zh/> 触发滚动的方式，默认使用指针滚动\n   *\n   * <en/> The way to trigger scrolling, default to scrolling with the pointer pressed\n   */\n  trigger?: {\n    up: ShortcutKey;\n    down: ShortcutKey;\n    left: ShortcutKey;\n    right: ShortcutKey;\n  };\n  /**\n   * <zh/> 允许的滚动方向\n   * - 默认情况下没有限制\n   * - `'x'` : 只允许水平滚动\n   * - `'y'` : 只允许垂直滚动\n   *\n   * <en/> The allowed rolling direction\n   * - by default, there is no restriction\n   * - `'x'`: only allow horizontal scrolling\n   * - `'y'`: only allow vertical scrolling\n   */\n  direction?: 'x' | 'y';\n  /**\n   * <zh/> 可滚动的视口范围，默认最多可滚动一屏。可以分别设置上、右、下、左四个方向的范围，每个方向的范围在 [0, Infinity] 之间\n   *\n   * <en/> The scrollable viewport range allows you to scroll up to one screen by default. You can set the range for each direction (top, right, bottom, left) individually, with each direction's range between [0, Infinity]\n   * @defaultValue 1\n   */\n  range?: number | number[];\n  /**\n   * <zh/> 滚动灵敏度\n   *\n   * <en/> Scroll sensitivity\n   * @defaultValue 1\n   */\n  sensitivity?: number;\n  /**\n   * <zh/> 完成滚动时的回调\n   *\n   * <en/> Callback when scrolling is completed\n   */\n  onFinish?: () => void;\n  /**\n   * <zh/> 是否阻止默认事件\n   *\n   * <en/> Whether to prevent the default event\n   * @defaultValue true\n   */\n  preventDefault?: boolean;\n}\n\n/**\n * <zh/> 滚动画布交互\n *\n * <en/> Scroll canvas behavior\n */\nexport class ScrollCanvas extends BaseBehavior<ScrollCanvasOptions> {\n  static defaultOptions: Partial<ScrollCanvasOptions> = {\n    enable: true,\n    sensitivity: 1,\n    preventDefault: true,\n    range: Infinity,\n  };\n\n  private shortcut: Shortcut;\n\n  constructor(context: RuntimeContext, options: ScrollCanvasOptions) {\n    super(context, Object.assign({}, ScrollCanvas.defaultOptions, options));\n\n    this.shortcut = new Shortcut(context.graph);\n\n    this.bindEvents();\n  }\n\n  /**\n   * <zh/> 更新配置\n   *\n   * <en/> Update options\n   * @param options - <zh/> 配置项 | <en/> Options\n   * @internal\n   */\n  public update(options: Partial<ScrollCanvasOptions>): void {\n    super.update(options);\n    this.bindEvents();\n  }\n\n  private bindEvents() {\n    const { trigger } = this.options;\n    this.shortcut.unbindAll();\n    if (isObject(trigger)) {\n      this.graphDom?.removeEventListener(CommonEvent.WHEEL, this.onWheel);\n      const { up = [], down = [], left = [], right = [] } = trigger;\n\n      this.shortcut.bind(up, (event) => this.scroll([0, -10], event));\n      this.shortcut.bind(down, (event) => this.scroll([0, 10], event));\n      this.shortcut.bind(left, (event) => this.scroll([-10, 0], event));\n      this.shortcut.bind(right, (event) => this.scroll([10, 0], event));\n    } else {\n      /**\n       * 这里必需在原生canvas上绑定wheel事件，参考：\n       * https://g.antv.antgroup.com/api/event/faq#%E5%9C%A8-chrome-%E4%B8%AD%E7%A6%81%E6%AD%A2%E9%A1%B5%E9%9D%A2%E9%BB%98%E8%AE%A4%E6%BB%9A%E5%8A%A8%E8%A1%8C%E4%B8%BA\n       */\n      this.graphDom?.addEventListener(CommonEvent.WHEEL, this.onWheel, { passive: false });\n    }\n  }\n\n  get graphDom() {\n    return this.context.graph.getCanvas().getContextService().getDomElement();\n  }\n\n  private onWheel = async (event: WheelEvent) => {\n    if (this.options.preventDefault) event.preventDefault();\n    const diffX = event.deltaX;\n    const diffY = event.deltaY;\n\n    await this.scroll([-diffX, -diffY], event);\n  };\n\n  private formatDisplacement(d: Point) {\n    const { sensitivity } = this.options;\n\n    d = multiply(d, sensitivity);\n    d = this.clampByDirection(d);\n    d = this.clampByRange(d);\n\n    return d;\n  }\n\n  private clampByDirection([dx, dy]: Point) {\n    const { direction } = this.options;\n    if (direction === 'x') {\n      dy = 0;\n    } else if (direction === 'y') {\n      dx = 0;\n    }\n    return [dx, dy] as Point;\n  }\n\n  private clampByRange([dx, dy]: Point) {\n    const { viewport, canvas } = this.context;\n\n    const [canvasWidth, canvasHeight] = canvas.getSize();\n    const [top, right, bottom, left] = parsePadding(this.options.range);\n    const range = [canvasHeight * top, canvasWidth * right, canvasHeight * bottom, canvasWidth * left];\n    const scrollableArea = getExpandedBBox(getPointBBox(viewport!.getCanvasCenter()), range);\n\n    const nextViewportCenter = subtract(viewport!.getViewportCenter(), [dx, dy, 0]);\n    if (!isPointInBBox(nextViewportCenter, scrollableArea)) {\n      const {\n        min: [minX, minY],\n        max: [maxX, maxY],\n      } = scrollableArea;\n\n      if ((nextViewportCenter[0] < minX && dx > 0) || (nextViewportCenter[0] > maxX && dx < 0)) {\n        dx = 0;\n      }\n      if ((nextViewportCenter[1] < minY && dy > 0) || (nextViewportCenter[1] > maxY && dy < 0)) {\n        dy = 0;\n      }\n    }\n    return [dx, dy] as Point;\n  }\n\n  private async scroll(value: Point, event: WheelEvent | IKeyboardEvent) {\n    if (!this.validate(event)) return;\n    const { onFinish } = this.options;\n    const graph = this.context.graph;\n    const formattedValue = this.formatDisplacement(value);\n    await graph.translateBy(formattedValue, false);\n    onFinish?.();\n  }\n\n  private validate(event: WheelEvent | IKeyboardEvent) {\n    if (this.destroyed) return false;\n\n    const { enable } = this.options;\n    if (isFunction(enable)) return enable(event);\n    return !!enable;\n  }\n  /**\n   * <zh/> 销毁画布滚动\n   *\n   * <en/> Destroy the canvas scrolling\n   */\n  public destroy(): void {\n    this.shortcut.destroy();\n    this.graphDom?.removeEventListener(CommonEvent.WHEEL, this.onWheel);\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/behaviors/types.ts",
    "content": "import type { BaseBehavior } from './base-behavior';\n\nexport type Behavior = BaseBehavior<any>;\n"
  },
  {
    "path": "packages/g6/src/behaviors/zoom-canvas.ts",
    "content": "import { clamp, isFunction } from '@antv/util';\nimport { CommonEvent } from '../constants';\nimport type { RuntimeContext } from '../runtime/types';\nimport type {\n  IKeyboardEvent,\n  IPointerEvent,\n  IWheelEvent,\n  Point,\n  PointObject,\n  ViewportAnimationEffectTiming,\n} from '../types';\nimport { parsePoint } from '../utils/point';\nimport type { ShortcutKey } from '../utils/shortcut';\nimport { Shortcut } from '../utils/shortcut';\nimport type { BaseBehaviorOptions } from './base-behavior';\nimport { BaseBehavior } from './base-behavior';\n\n/**\n * <zh/> 缩放画布交互配置项\n *\n * <en/> Zoom canvas behavior options\n */\nexport interface ZoomCanvasOptions extends BaseBehaviorOptions {\n  /**\n   * <zh/> 是否启用缩放动画\n   *\n   * <en/> Whether to enable the animation of zooming\n   * @defaultValue '{ duration: 200 }'\n   */\n  animation?: ViewportAnimationEffectTiming;\n  /**\n   * <zh/> 是否启用缩放画布的功能\n   *\n   * <en/> Whether to enable the function of zooming the canvas\n   * @defaultValue true\n   */\n  enable?: boolean | ((event: IWheelEvent | IKeyboardEvent | IPointerEvent) => boolean);\n  /**\n   * <zh/> 缩放中心点(视口坐标)\n   * - 默认情况下为鼠标位置中心\n   *\n   * <en/> zoom center(viewport coordinates)\n   * - by default , the center is the mouse position center\n   */\n  origin?: Point;\n  /**\n   * <zh/> 触发缩放的方式\n   * - ShortcutKey：组合快捷键，**默认使用滚轮缩放**，['Control'] 表示按住 Control 键滚动鼠标滚轮时触发缩放\n   * - CombinationKey：缩放快捷键，例如 { zoomIn: ['Control', '+'], zoomOut: ['Control', '-'], reset: ['Control', '0'] }\n   *\n   * <en/> The way to trigger zoom\n   * - ShortcutKey: Combination shortcut key, **default to zoom with the mouse wheel**, ['Control'] means zooming when holding down the Control key and scrolling the mouse wheel\n   * - CombinationKey: Zoom shortcut key, such as { zoomIn: ['Control', '+'], zoomOut: ['Control', '-'], reset: ['Control', '0'] }\n   */\n  trigger?:\n    | ShortcutKey\n    | {\n        zoomIn: ShortcutKey;\n        zoomOut: ShortcutKey;\n        reset: ShortcutKey;\n      };\n  /**\n   * <zh/> 缩放灵敏度\n   *\n   * <en/> Zoom sensitivity\n   * @defaultValue 1\n   */\n  sensitivity?: number;\n  /**\n   * <zh/> 完成缩放时的回调\n   *\n   * <en/> Callback when zooming is completed\n   */\n  onFinish?: () => void;\n  /**\n   * <zh/> 是否阻止默认事件\n   *\n   * <en/> Whether to prevent the default event\n   * @defaultValue true\n   */\n  preventDefault?: boolean;\n}\n\n/**\n * <zh/> 缩放画布交互\n *\n * <en/> Zoom canvas behavior\n */\nexport class ZoomCanvas extends BaseBehavior<ZoomCanvasOptions> {\n  static defaultOptions: Partial<ZoomCanvasOptions> = {\n    animation: { duration: 200 },\n    enable: true,\n    sensitivity: 1,\n    trigger: [],\n    preventDefault: true,\n  };\n\n  private shortcut: Shortcut;\n\n  constructor(context: RuntimeContext, options: ZoomCanvasOptions) {\n    super(context, Object.assign({}, ZoomCanvas.defaultOptions, options));\n\n    this.shortcut = new Shortcut(context.graph);\n\n    this.bindEvents();\n  }\n\n  /**\n   * <zh/> 更新配置\n   *\n   * <en/> Update options\n   * @param options - <zh/> 配置项 | <en/> Options\n   * @internal\n   */\n  public update(options: Partial<ZoomCanvasOptions>): void {\n    super.update(options);\n    this.bindEvents();\n  }\n\n  private bindEvents() {\n    const { trigger } = this.options;\n    this.shortcut.unbindAll();\n\n    if (Array.isArray(trigger)) {\n      if (trigger.includes(CommonEvent.PINCH)) {\n        this.shortcut.bind([CommonEvent.PINCH], (event) => {\n          this.zoom(event.scale, event, false);\n        });\n      } else {\n        const container = this.context.canvas.getContainer();\n        container?.addEventListener(CommonEvent.WHEEL, this.preventDefault);\n        this.shortcut.bind([...trigger, CommonEvent.WHEEL], (event) => {\n          const { deltaX, deltaY } = event;\n          this.zoom(-(deltaY ?? deltaX), event, false);\n        });\n      }\n    }\n\n    if (typeof trigger === 'object') {\n      const {\n        zoomIn = [],\n        zoomOut = [],\n        reset = [],\n      } = trigger as {\n        zoomIn: ShortcutKey;\n        zoomOut: ShortcutKey;\n        reset: ShortcutKey;\n      };\n      this.shortcut.bind(zoomIn, (event) => this.zoom(10, event, this.options.animation));\n      this.shortcut.bind(zoomOut, (event) => this.zoom(-10, event, this.options.animation));\n      this.shortcut.bind(reset, this.onReset);\n    }\n  }\n\n  /**\n   * <zh/> 缩放画布\n   *\n   * <en/> Zoom canvas\n   * @param value - <zh/> 缩放值， > 0 放大， < 0 缩小 | <en/> Zoom value, > 0 zoom in, < 0 zoom out\n   * @param event - <zh/> 事件对象 | <en/> Event object\n   * @param animation - <zh/> 缩放动画配置 | <en/> Zoom animation configuration\n   */\n  protected zoom = async (\n    value: number,\n    event: IWheelEvent | IKeyboardEvent | IPointerEvent,\n    animation: ZoomCanvasOptions['animation'],\n  ) => {\n    if (!this.validate(event)) return;\n    const { graph } = this.context;\n\n    let origin: Point | undefined = this.options.origin;\n    if (!origin && 'viewport' in event) {\n      origin = parsePoint(event.viewport as PointObject);\n    }\n\n    const { sensitivity, onFinish } = this.options;\n    const ratio = 1 + (clamp(value, -50, 50) * sensitivity) / 100;\n    const zoom = graph.getZoom();\n    await graph.zoomTo(zoom * ratio, animation, origin);\n\n    onFinish?.();\n  };\n\n  protected onReset = async () => {\n    await this.context.graph.zoomTo(1, this.options.animation);\n  };\n\n  /**\n   * <zh/> 验证是否可以缩放\n   *\n   * <en/> Verify whether it can be zoomed\n   * @param event - <zh/> 事件对象 | <en/> Event object\n   * @returns <zh/> 是否可以缩放 | <en/> Whether it can be zoomed\n   * @internal\n   */\n  protected validate(event: IWheelEvent | IKeyboardEvent | IPointerEvent) {\n    if (this.destroyed) return false;\n    const { enable } = this.options;\n    if (isFunction(enable)) return enable(event);\n    return !!enable;\n  }\n\n  private preventDefault = (event: Event) => {\n    if (this.options.preventDefault) event.preventDefault();\n  };\n\n  /**\n   * <zh/> 销毁缩放画布\n   *\n   * <en/> Destroy zoom canvas\n   */\n  public destroy() {\n    this.shortcut.destroy();\n    this.context.canvas.getContainer()?.removeEventListener(CommonEvent.WHEEL, this.preventDefault);\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/constants/animation.ts",
    "content": "import type { AnimationEffectTiming } from '../animations/types';\n\nexport const DEFAULT_ANIMATION_OPTIONS: AnimationEffectTiming = {\n  duration: 500,\n};\n\nexport const DEFAULT_ELEMENTS_ANIMATION_OPTIONS: AnimationEffectTiming = {\n  duration: 1000,\n  easing: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',\n  iterations: 1,\n  fill: 'both',\n};\n"
  },
  {
    "path": "packages/g6/src/constants/change.ts",
    "content": "export const ChangeEvent = {\n  /**\n   * <zh/> 数据变更\n   *\n   * <en/> Data changes\n   */\n  CHANGE: 'change',\n};\n\nexport const enum ChangeType {\n  /**\n   * <zh/> 节点添加\n   *\n   * <en/> Node added\n   */\n  'NodeAdded' = 'NodeAdded',\n  /**\n   * <zh/> 节点更新\n   *\n   * <en/> Node updated\n   */\n  'NodeUpdated' = 'NodeUpdated',\n  /**\n   * <zh/> 节点删除\n   *\n   * <en/> Node removed\n   */\n  'NodeRemoved' = 'NodeRemoved',\n  /**\n   * <zh/> 边添加\n   *\n   * <en/> Edge added\n   */\n  'EdgeAdded' = 'EdgeAdded',\n  /**\n   * <zh/> 边更新\n   *\n   * <en/> Edge updated\n   */\n  'EdgeUpdated' = 'EdgeUpdated',\n  /**\n   * <zh/> 边删除\n   *\n   * <en/> Edge removed\n   */\n  'EdgeRemoved' = 'EdgeRemoved',\n  /**\n   * <zh/> Combo添加\n   *\n   * <en/> Combo added\n   */\n  'ComboAdded' = 'ComboAdded',\n  /**\n   * <zh/> Combo更新\n   *\n   * <en/> Combo updated\n   */\n  'ComboUpdated' = 'ComboUpdated',\n  /**\n   * <zh/> Combo删除\n   *\n   * <en/> Combo removed\n   */\n  'ComboRemoved' = 'ComboRemoved',\n}\n"
  },
  {
    "path": "packages/g6/src/constants/element.ts",
    "content": "/**\n * <zh/> 根据不同的 node，自动计算 icon 的大小之后，乘以一下缩放系数，防止贴的太紧密\n *\n * <en/> According to the different nodes, the size of the icon is automatically calculated, and then multiplied by the following scaling factor to prevent it from being too close\n */\nexport const ICON_SIZE_RATIO = 0.8;\n\nexport const ELEMENT_TYPES = ['node', 'edge', 'combo'] as const;\n"
  },
  {
    "path": "packages/g6/src/constants/events/animation.ts",
    "content": "export enum AnimationType {\n  DRAW = 'draw',\n  COLLAPSE = 'collapse',\n  EXPAND = 'expand',\n  TRANSFORM = 'transform',\n}\n"
  },
  {
    "path": "packages/g6/src/constants/events/canvas.ts",
    "content": "/**\n * <zh/> 画布事件\n *\n * <en/> Canvas event\n */\nexport enum CanvasEvent {\n  /**\n   * <zh/> 点击时触发\n   *\n   * <en/> Triggered when click\n   */\n  CLICK = 'canvas:click',\n  /**\n   * <zh/> 双击时触发\n   *\n   * <en/> Triggered when double click\n   */\n  DBLCLICK = 'canvas:dblclick',\n  /**\n   * <zh/> 指针移入时触发\n   *\n   * <en/> Triggered when the pointer enters\n   */\n  POINTER_OVER = 'canvas:pointerover',\n  /**\n   * <zh/> 指针移出时触发\n   *\n   * <en/> Triggered when the pointer leaves\n   */\n  POINTER_LEAVE = 'canvas:pointerleave',\n  /**\n   * <zh/> 指针移入时或移入子元素时触发（不会冒泡）\n   *\n   * <en/> Triggered when the pointer enters or enters a child element (does not bubble)\n   */\n  POINTER_ENTER = 'canvas:pointerenter',\n  /**\n   * <zh/> 指针移动时触发\n   *\n   * <en/> Triggered when the pointer moves\n   */\n  POINTER_MOVE = 'canvas:pointermove',\n  /**\n   * <zh/> 指针移出时触发\n   *\n   * <en/> Triggered when the pointer leaves\n   */\n  POINTER_OUT = 'canvas:pointerout',\n  /**\n   * <zh/> 指针按下时触发\n   *\n   * <en/> Triggered when the pointer is pressed\n   */\n  POINTER_DOWN = 'canvas:pointerdown',\n  /**\n   * <zh/> 指针抬起时触发\n   *\n   * <en/> Triggered when the pointer is lifted\n   */\n  POINTER_UP = 'canvas:pointerup',\n  /**\n   * <zh/> 打开上下文菜单时触发\n   *\n   * <en/> Triggered when the context menu is opened\n   */\n  CONTEXT_MENU = 'canvas:contextmenu',\n  /**\n   * <zh/> 开始拖拽时触发\n   *\n   * <en/> Triggered when dragging starts\n   */\n  DRAG_START = 'canvas:dragstart',\n  /**\n   * <zh/> 拖拽过程中触发\n   *\n   * <en/> Triggered when dragging\n   */\n  DRAG = 'canvas:drag',\n  /**\n   * <zh/> 拖拽结束时触发\n   *\n   * <en/> Triggered when dragging ends\n   */\n  DRAG_END = 'canvas:dragend',\n  /**\n   * <zh/> 拖拽进入时触发\n   *\n   * <en/> Triggered when dragging enters\n   */\n  DRAG_ENTER = 'canvas:dragenter',\n  /**\n   * <zh/> 拖拽经过时触发\n   *\n   * <en/> Triggered when dragging passes\n   */\n  DRAG_OVER = 'canvas:dragover',\n  /**\n   * <zh/> 拖拽离开时触发\n   *\n   * <en/> Triggered when dragging leaves\n   */\n  DRAG_LEAVE = 'canvas:dragleave',\n  /**\n   * <zh/> 拖拽放下时触发\n   *\n   * <en/> Triggered when dragging is dropped\n   */\n  DROP = 'canvas:drop',\n  /**\n   * <zh/> 滚动时触发\n   *\n   * <en/> Triggered when scrolling\n   */\n  WHEEL = 'canvas:wheel',\n}\n"
  },
  {
    "path": "packages/g6/src/constants/events/combo.ts",
    "content": "/**\n * <zh/> 组合事件\n *\n * <en/> Combo event\n */\nexport enum ComboEvent {\n  /**\n   * <zh/> 点击时触发\n   *\n   * <en/> Triggered when click\n   */\n  CLICK = 'combo:click',\n  /**\n   * <zh/> 双击时触发\n   *\n   * <en/> Triggered when double click\n   */\n  DBLCLICK = 'combo:dblclick',\n  /**\n   * <zh/> 指针移入时触发\n   *\n   * <en/> Triggered when the pointer enters\n   */\n  POINTER_OVER = 'combo:pointerover',\n  /**\n   * <zh/> 指针移出时触发\n   *\n   * <en/> Triggered when the pointer leaves\n   */\n  POINTER_LEAVE = 'combo:pointerleave',\n  /**\n   * <zh/> 指针移入时或移入子元素时触发（不会冒泡）\n   *\n   * <en/> Triggered when the pointer enters or enters a child element (does not bubble)\n   */\n  POINTER_ENTER = 'combo:pointerenter',\n  /**\n   * <zh/> 指针移动时触发\n   *\n   * <en/> Triggered when the pointer moves\n   */\n  POINTER_MOVE = 'combo:pointermove',\n  /**\n   * <zh/> 指针移出时触发\n   *\n   * <en/> Triggered when the pointer leaves\n   */\n  POINTER_OUT = 'combo:pointerout',\n  /**\n   * <zh/> 指针按下时触发\n   *\n   * <en/> Triggered when the pointer is pressed\n   */\n  POINTER_DOWN = 'combo:pointerdown',\n  /**\n   * <zh/> 指针抬起时触发\n   *\n   * <en/> Triggered when the pointer is lifted\n   */\n  POINTER_UP = 'combo:pointerup',\n  /**\n   * <zh/> 打开上下文菜单时触发\n   *\n   * <en/> Triggered when the context menu is opened\n   */\n  CONTEXT_MENU = 'combo:contextmenu',\n  /**\n   * <zh/> 开始拖拽时触发\n   *\n   * <en/> Triggered when dragging starts\n   */\n  DRAG_START = 'combo:dragstart',\n  /**\n   * <zh/> 拖拽过程中触发\n   *\n   * <en/> Triggered when dragging\n   */\n  DRAG = 'combo:drag',\n  /**\n   * <zh/> 拖拽结束时触发\n   *\n   * <en/> Triggered when dragging ends\n   */\n  DRAG_END = 'combo:dragend',\n  /**\n   * <zh/> 拖拽进入时触发\n   *\n   * <en/> Triggered when dragging enters\n   */\n  DRAG_ENTER = 'combo:dragenter',\n  /**\n   * <zh/> 拖拽经过时触发\n   *\n   * <en/> Triggered when dragging passes\n   */\n  DRAG_OVER = 'combo:dragover',\n  /**\n   * <zh/> 拖拽离开时触发\n   *\n   * <en/> Triggered when dragging leaves\n   */\n  DRAG_LEAVE = 'combo:dragleave',\n  /**\n   * <zh/> 拖拽放下时触发\n   *\n   * <en/> Triggered when dragging is dropped\n   */\n  DROP = 'combo:drop',\n}\n"
  },
  {
    "path": "packages/g6/src/constants/events/common.ts",
    "content": "export enum CommonEvent {\n  /**\n   * <zh/> 点击时触发\n   *\n   * <en/> Triggered when click\n   */\n  CLICK = 'click',\n  /**\n   * <zh/> 双击时触发\n   *\n   * <en/> Triggered when double click\n   */\n  DBLCLICK = 'dblclick',\n  /**\n   * <zh/> 指针移入时触发\n   *\n   * <en/> Triggered when the pointer enters\n   */\n  POINTER_OVER = 'pointerover',\n  /**\n   * <zh/> 指针移出时触发\n   *\n   * <en/> Triggered when the pointer leaves\n   */\n  POINTER_LEAVE = 'pointerleave',\n  /**\n   * <zh/> 指针移入时或移入子元素时触发（不会冒泡）\n   *\n   * <en/> Triggered when the pointer enters or enters a child element (does not bubble)\n   */\n  POINTER_ENTER = 'pointerenter',\n  /**\n   * <zh/> 指针移动时触发\n   *\n   * <en/> Triggered when the pointer moves\n   */\n  POINTER_MOVE = 'pointermove',\n  /**\n   * <zh/> 指针移出时触发\n   *\n   * <en/> Triggered when the pointer leaves\n   */\n  POINTER_OUT = 'pointerout',\n  /**\n   * <zh/> 指针按下时触发\n   *\n   * <en/> Triggered when the pointer is pressed\n   */\n  POINTER_DOWN = 'pointerdown',\n  /**\n   * <zh/> 指针抬起时触发\n   *\n   * <en/> Triggered when the pointer is lifted\n   */\n  POINTER_UP = 'pointerup',\n  /**\n   * <zh/> 打开上下文菜单时触发\n   *\n   * <en/> Triggered when the context menu is opened\n   */\n  CONTEXT_MENU = 'contextmenu',\n  /**\n   * <zh/> 开始拖拽时触发\n   *\n   * <en/> Triggered when dragging starts\n   */\n  DRAG_START = 'dragstart',\n  /**\n   * <zh/> 拖拽过程中触发\n   *\n   * <en/> Triggered when dragging\n   */\n  DRAG = 'drag',\n  /**\n   * <zh/> 拖拽结束时触发\n   *\n   * <en/> Triggered when dragging ends\n   */\n  DRAG_END = 'dragend',\n  /**\n   * <zh/> 拖拽进入时触发\n   *\n   * <en/> Triggered when dragging enters\n   */\n  DRAG_ENTER = 'dragenter',\n  /**\n   * <zh/> 拖拽经过时触发\n   *\n   * <en/> Triggered when dragging passes\n   */\n  DRAG_OVER = 'dragover',\n  /**\n   * <zh/> 拖拽离开时触发\n   *\n   * <en/> Triggered when dragging leaves\n   */\n  DRAG_LEAVE = 'dragleave',\n  /**\n   * <zh/> 拖拽放下时触发\n   *\n   * <en/> Triggered when dragging is dropped\n   */\n  DROP = 'drop',\n  /**\n   * <zh/> 按下键盘时触发\n   *\n   * <en/> Triggered when the keyboard is pressed\n   */\n  KEY_DOWN = 'keydown',\n  /**\n   * <zh/> 抬起键盘时触发\n   *\n   * <en/> Triggered when the keyboard is lifted\n   */\n  KEY_UP = 'keyup',\n  /**\n   * <zh/> 滚动时触发\n   *\n   * <en/> Triggered when scrolling\n   */\n  WHEEL = 'wheel',\n  /**\n   * <zh/> 双指捏拢或张开时触发\n   *\n   * <en/> Triggered when pinch in and pinch out\n   */\n  PINCH = 'pinch',\n}\n"
  },
  {
    "path": "packages/g6/src/constants/events/container.ts",
    "content": "export enum ContainerEvent {\n  /**\n   * <zh/> 按下键盘时触发\n   *\n   * <en/> Triggered when the keyboard is pressed\n   */\n  KEY_DOWN = 'keydown',\n  /**\n   * <zh/> 抬起键盘时触发\n   *\n   * <en/> Triggered when the keyboard is lifted\n   */\n  KEY_UP = 'keyup',\n}\n"
  },
  {
    "path": "packages/g6/src/constants/events/edge.ts",
    "content": "/**\n * <zh/> 边事件\n *\n * <en/> Edge event\n */\nexport enum EdgeEvent {\n  /**\n   * <zh/> 点击时触发\n   *\n   * <en/> Triggered when click\n   */\n  CLICK = 'edge:click',\n  /**\n   * <zh/> 双击时触发\n   *\n   * <en/> Triggered when double click\n   */\n  DBLCLICK = 'edge:dblclick',\n  /**\n   * <zh/> 指针移入时触发\n   *\n   * <en/> Triggered when the pointer enters\n   */\n  POINTER_OVER = 'edge:pointerover',\n  /**\n   * <zh/> 指针移出时触发\n   *\n   * <en/> Triggered when the pointer leaves\n   */\n  POINTER_LEAVE = 'edge:pointerleave',\n  /**\n   * <zh/> 指针移入时或移入子元素时触发（不会冒泡）\n   *\n   * <en/> Triggered when the pointer enters or enters a child element (does not bubble)\n   */\n  POINTER_ENTER = 'edge:pointerenter',\n  /**\n   * <zh/> 指针移动时触发\n   *\n   * <en/> Triggered when the pointer moves\n   */\n  POINTER_MOVE = 'edge:pointermove',\n  /**\n   * <zh/> 指针移出时触发\n   *\n   * <en/> Triggered when the pointer leaves\n   */\n  POINTER_OUT = 'edge:pointerout',\n  /**\n   * <zh/> 指针按下时触发\n   *\n   * <en/> Triggered when the pointer is pressed\n   */\n  POINTER_DOWN = 'edge:pointerdown',\n  /**\n   * <zh/> 指针抬起时触发\n   *\n   * <en/> Triggered when the pointer is lifted\n   */\n  POINTER_UP = 'edge:pointerup',\n  /**\n   * <zh/> 打开上下文菜单时触发\n   *\n   * <en/> Triggered when the context menu is opened\n   */\n  CONTEXT_MENU = 'edge:contextmenu',\n  /**\n   * <zh/> 拖拽进入时触发\n   *\n   * <en/> Triggered when dragging enters\n   */\n  DRAG_ENTER = 'edge:dragenter',\n  /**\n   * <zh/> 拖拽经过时触发\n   *\n   * <en/> Triggered when dragging passes\n   */\n  DRAG_OVER = 'edge:dragover',\n  /**\n   * <zh/> 拖拽离开时触发\n   *\n   * <en/> Triggered when dragging leaves\n   */\n  DRAG_LEAVE = 'edge:dragleave',\n  /**\n   * <zh/> 拖拽放下时触发\n   *\n   * <en/> Triggered when dragging is dropped\n   */\n  DROP = 'edge:drop',\n}\n"
  },
  {
    "path": "packages/g6/src/constants/events/graph.ts",
    "content": "export enum GraphEvent {\n  /**\n   * <zh/> 画布初始化之前\n   *\n   * <en/> Before the canvas is initialized\n   */\n  BEFORE_CANVAS_INIT = 'beforecanvasinit',\n  /**\n   * <zh/> 画布初始化之后\n   *\n   * <en/> After the canvas is initialized\n   */\n  AFTER_CANVAS_INIT = 'aftercanvasinit',\n  /**\n   * <zh/> 视口尺寸变更之前\n   *\n   * <en/> Before the viewport size changes\n   */\n  BEFORE_SIZE_CHANGE = 'beforesizechange',\n  /**\n   * <zh/> 视口尺寸变更之后\n   *\n   * <en/> After the viewport size changes\n   */\n  AFTER_SIZE_CHANGE = 'aftersizechange',\n  /**\n   * <zh/> 元素创建之前\n   *\n   * <en/> Before creating element\n   */\n  BEFORE_ELEMENT_CREATE = 'beforeelementcreate',\n  /**\n   * <zh/> 元素创建之后\n   *\n   * <en/> After creating element\n   */\n  AFTER_ELEMENT_CREATE = 'afterelementcreate',\n  /**\n   * <zh/> 元素更新之前\n   *\n   * <en/> Before updating element\n   */\n  BEFORE_ELEMENT_UPDATE = 'beforeelementupdate',\n  /**\n   * <zh/> 元素更新之后\n   *\n   * <en/> After updating element\n   */\n  AFTER_ELEMENT_UPDATE = 'afterelementupdate',\n  /**\n   * <zh/> 元素销毁之前\n   *\n   * <en/> Before destroying element\n   */\n  BEFORE_ELEMENT_DESTROY = 'beforeelementdestroy',\n  /**\n   * <zh/> 元素销毁之后\n   *\n   * <en/> After destroying element\n   */\n  AFTER_ELEMENT_DESTROY = 'afterelementdestroy',\n  /**\n   * <zh/> 元素平移之前\n   *\n   * <en/> Before element translation\n   */\n  BEFORE_ELEMENT_TRANSLATE = 'beforeelementtranslate',\n  /**\n   * <zh/> 元素平移之后\n   *\n   * <en/> After element translation\n   */\n  AFTER_ELEMENT_TRANSLATE = 'afterelementtranslate',\n  /**\n   * <zh/> 绘制开始之前\n   *\n   * <en/> Before drawing\n   */\n  BEFORE_DRAW = 'beforedraw',\n  /**\n   * <zh/> 绘制结束之后\n   *\n   * <en/> After drawing\n   */\n  AFTER_DRAW = 'afterdraw',\n  /**\n   * <zh/> 渲染开始之前\n   *\n   * <en/> Before rendering\n   */\n  BEFORE_RENDER = 'beforerender',\n  /**\n   * <zh/> 渲染完成之后\n   *\n   * <en/> After rendering\n   */\n  AFTER_RENDER = 'afterrender',\n  /**\n   * <zh/> 动画开始之前\n   *\n   * <en/> Before animation\n   */\n  BEFORE_ANIMATE = 'beforeanimate',\n  /**\n   * <zh/> 动画结束之后\n   *\n   * <en/> After animation\n   */\n  AFTER_ANIMATE = 'afteranimate',\n  /**\n   * <zh/> 布局开始之前\n   *\n   * <en/> Before layout\n   */\n  BEFORE_LAYOUT = 'beforelayout',\n  /**\n   * <zh/> 布局结束之后\n   *\n   * <en/> After layout\n   */\n  AFTER_LAYOUT = 'afterlayout',\n  /**\n   * <zh/> 布局过程之前，用于流水线布局过程获取当前执行的布局\n   *\n   * <en/> Before the layout process, used to get the current layout being executed in the pipeline layout process\n   */\n  BEFORE_STAGE_LAYOUT = 'beforestagelayout',\n  /**\n   * <zh/> 布局过程之后，用于流水线布局过程获取当前执行的布局\n   *\n   * <en/> After the layout process, used to get the current layout being executed in the pipeline layout process\n   */\n  AFTER_STAGE_LAYOUT = 'afterstagelayout',\n  /**\n   * <zh/> 可视区域变化之前\n   *\n   * <en/> Before the visible area changes\n   */\n  BEFORE_TRANSFORM = 'beforetransform',\n  /**\n   * <zh/> 可视区域变化之后\n   *\n   * <en/> After the visible area changes\n   */\n  AFTER_TRANSFORM = 'aftertransform',\n  /**\n   * <zh/> 批处理开始\n   *\n   * <en/> Batch processing starts\n   */\n  BATCH_START = 'batchstart',\n  /**\n   * <zh/> 批处理结束\n   *\n   * <en/> Batch processing ends\n   */\n  BATCH_END = 'batchend',\n  /**\n   * <zh/> 销毁开始之前\n   *\n   * <en/> Before destruction\n   */\n  BEFORE_DESTROY = 'beforedestroy',\n  /**\n   * <zh/> 销毁结束之后\n   *\n   * <en/> After destruction\n   */\n  AFTER_DESTROY = 'afterdestroy',\n  /**\n   * <zh/> 渲染器变更之前\n   *\n   * <en/> Before the renderer changes\n   */\n  BEFORE_RENDERER_CHANGE = 'beforerendererchange',\n  /**\n   * <zh/> 渲染器变更之后\n   *\n   * <en/> After the renderer changes\n   */\n  AFTER_RENDERER_CHANGE = 'afterrendererchange',\n}\n"
  },
  {
    "path": "packages/g6/src/constants/events/history.ts",
    "content": "export enum HistoryEvent {\n  /**\n   * <zh/> 当命令被撤销时\n   *\n   * <en/> When the command is undone\n   */\n  UNDO = 'undo',\n  /**\n   * <zh/> 当命令被重做时\n   *\n   * <en/> When the command is redone\n   */\n  REDO = 'redo',\n  /**\n   * <zh/> 当命令被取消时\n   *\n   * <en/> When the command is canceled\n   */\n  CANCEL = 'cancel',\n  /**\n   *  <zh/> 当命令被添加到队列时\n   *\n   *  <en/> When the command is added\n   */\n  ADD = 'add',\n  /**\n   * <zh/> 当历史队列被清空时\n   *\n   *  <en/> When the command queue is cleared\n   */\n  CLEAR = 'clear',\n  /**\n   * <zh/> 当历史队列发生变化时\n   *\n   * <en/> When the command queue changes\n   */\n  CHANGE = 'change',\n}\n"
  },
  {
    "path": "packages/g6/src/constants/events/index.ts",
    "content": "export { AnimationType } from './animation';\nexport { CanvasEvent } from './canvas';\nexport { ComboEvent } from './combo';\nexport { CommonEvent } from './common';\nexport { ContainerEvent } from './container';\nexport { EdgeEvent } from './edge';\nexport { GraphEvent } from './graph';\nexport { HistoryEvent } from './history';\nexport { NodeEvent } from './node';\n"
  },
  {
    "path": "packages/g6/src/constants/events/node.ts",
    "content": "/**\n * <zh/> 节点事件\n *\n * <en/> Node event\n */\nexport enum NodeEvent {\n  /**\n   * <zh/> 点击时触发\n   *\n   * <en/> Triggered when click\n   */\n  CLICK = 'node:click',\n  /**\n   * <zh/> 双击时触发\n   *\n   * <en/> Triggered when double click\n   */\n  DBLCLICK = 'node:dblclick',\n  /**\n   * <zh/> 指针移入时触发\n   *\n   * <en/> Triggered when the pointer enters\n   */\n  POINTER_OVER = 'node:pointerover',\n  /**\n   * <zh/> 指针移出时触发\n   *\n   * <en/> Triggered when the pointer leaves\n   */\n  POINTER_LEAVE = 'node:pointerleave',\n  /**\n   * <zh/> 指针移入时或移入子元素时触发（不会冒泡）\n   *\n   * <en/> Triggered when the pointer enters or enters a child element (does not bubble)\n   */\n  POINTER_ENTER = 'node:pointerenter',\n  /**\n   * <zh/> 指针移动时触发\n   *\n   * <en/> Triggered when the pointer moves\n   */\n  POINTER_MOVE = 'node:pointermove',\n  /**\n   * <zh/> 指针移出时触发\n   *\n   * <en/> Triggered when the pointer leaves\n   */\n  POINTER_OUT = 'node:pointerout',\n  /**\n   * <zh/> 指针按下时触发\n   *\n   * <en/> Triggered when the pointer is pressed\n   */\n  POINTER_DOWN = 'node:pointerdown',\n  /**\n   * <zh/> 指针抬起时触发\n   *\n   * <en/> Triggered when the pointer is lifted\n   */\n  POINTER_UP = 'node:pointerup',\n  /**\n   * <zh/> 打开上下文菜单时触发\n   *\n   * <en/> Triggered when the context menu is opened\n   */\n  CONTEXT_MENU = 'node:contextmenu',\n  /**\n   * <zh/> 开始拖拽时触发\n   *\n   * <en/> Triggered when dragging starts\n   */\n  DRAG_START = 'node:dragstart',\n  /**\n   * <zh/> 拖拽过程中触发\n   *\n   * <en/> Triggered when dragging\n   */\n  DRAG = 'node:drag',\n  /**\n   * <zh/> 拖拽结束时触发\n   *\n   * <en/> Triggered when dragging ends\n   */\n  DRAG_END = 'node:dragend',\n  /**\n   * <zh/> 拖拽进入时触发\n   *\n   * <en/> Triggered when dragging enters\n   */\n  DRAG_ENTER = 'node:dragenter',\n  /**\n   * <zh/> 拖拽经过时触发\n   *\n   * <en/> Triggered when dragging passes\n   */\n  DRAG_OVER = 'node:dragover',\n  /**\n   * <zh/> 拖拽离开时触发\n   *\n   * <en/> Triggered when dragging leaves\n   */\n  DRAG_LEAVE = 'node:dragleave',\n  /**\n   * <zh/> 拖拽放下时触发\n   *\n   * <en/> Triggered when dragging is dropped\n   */\n  DROP = 'node:drop',\n}\n"
  },
  {
    "path": "packages/g6/src/constants/graphlib.ts",
    "content": "export const COMBO_KEY = 'combo';\n\nexport const TREE_KEY = 'tree';\n"
  },
  {
    "path": "packages/g6/src/constants/index.ts",
    "content": "export * from './animation';\nexport * from './change';\nexport * from './events';\nexport * from './graphlib';\nexport * from './registry';\n"
  },
  {
    "path": "packages/g6/src/constants/registry.ts",
    "content": "export enum ExtensionCategory {\n  /**\n   * <zh/> 节点元素\n   *\n   * <en/> Node element\n   */\n  NODE = 'node',\n  /**\n   * <zh/> 边元素\n   *\n   * <en/> Edge element\n   */\n  EDGE = 'edge',\n  /**\n   * <zh/> 组合元素\n   *\n   * <en/> Combination element\n   */\n  COMBO = 'combo',\n  /**\n   * <zh/> 主题\n   *\n   * <en/> Theme\n   */\n  THEME = 'theme',\n  /**\n   * <zh/> 色板\n   *\n   * <en/> Palette\n   */\n  PALETTE = 'palette',\n  /**\n   * <zh/> 布局\n   *\n   * <en/> Layout\n   */\n  LAYOUT = 'layout',\n  /**\n   * <zh/> 交互\n   *\n   * <en/> Behavior\n   */\n  BEHAVIOR = 'behavior',\n  /**\n   * <zh/> 插件\n   *\n   * <en/> Plugin\n   */\n  PLUGIN = 'plugin',\n  /**\n   * <zh/> 动画\n   *\n   * <en/> Animation\n   */\n  ANIMATION = 'animation',\n  /**\n   * <zh/> 数据转换\n   *\n   * <en/> Data transform\n   */\n  TRANSFORM = 'transform',\n  /**\n   * <zh/> 图形\n   *\n   * <en/> Shape\n   */\n  SHAPE = 'shape',\n}\n"
  },
  {
    "path": "packages/g6/src/elements/base-element.ts",
    "content": "import type { IAnimation } from '@antv/g';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { Keyframe } from '../types';\nimport type { BaseShapeStyleProps } from './shapes';\nimport { BaseShape } from './shapes';\n\nexport abstract class BaseElement<T extends BaseShapeStyleProps> extends BaseShape<T> {\n  protected get context(): RuntimeContext {\n    // @ts-expect-error skip type-check\n    return this.config.context;\n  }\n\n  protected get parsedAttributes() {\n    return this.attributes as Required<T>;\n  }\n\n  /**\n   * <zh/> 动画帧执行函数\n   *\n   * <en/> Animation frame execution function\n   */\n  protected onframe() {}\n\n  public animate(keyframes: Keyframe[], options?: number | KeyframeAnimationOptions | undefined): IAnimation | null {\n    const animation = super.animate(keyframes, options);\n\n    if (animation) {\n      animation.onframe = () => this.onframe();\n      animation.finished.then(() => this.onframe());\n    }\n\n    return animation;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/combos/base-combo.ts",
    "content": "import { AABB, BaseStyleProps, DisplayObject, DisplayObjectConfig, Group } from '@antv/g';\nimport { isFunction } from '@antv/util';\nimport type {\n  CollapsedMarkerStyleProps,\n  Combo,\n  ID,\n  NodeLikeData,\n  Padding,\n  Point,\n  Prefix,\n  STDSize,\n  Size,\n} from '../../types';\nimport { getBBoxHeight, getBBoxWidth, getCombinedBBox, getExpandedBBox } from '../../utils/bbox';\nimport { idOf } from '../../utils/id';\nimport { parsePadding } from '../../utils/padding';\nimport { getXYByPlacement, hasPosition, positionOf } from '../../utils/position';\nimport { subStyleProps } from '../../utils/prefix';\nimport { parseSize } from '../../utils/size';\nimport { mergeOptions } from '../../utils/style';\nimport { add, divide } from '../../utils/vector';\nimport type { BaseNodeStyleProps } from '../nodes';\nimport { BaseNode } from '../nodes';\nimport { Icon, IconStyleProps } from '../shapes';\nimport { connectImage, dispatchPositionChange } from '../shapes/image';\n\n/**\n * <zh/> 组合通用样式配置项\n *\n * <en/> Common style props for combo\n */\nexport interface BaseComboStyleProps\n  extends\n    BaseNodeStyleProps,\n    Prefix<'collapsed', BaseStyleProps>,\n    Prefix<'collapsedMarker', CollapsedMarkerStyleProps> {\n  /**\n   * <zh/> 组合展开后的默认大小\n   *\n   * <en/> The default size of combo when expanded\n   */\n  size?: Size;\n  /**\n   * <zh/> 组合收起后的默认大小\n   *\n   * <en/> The default size of combo when collapsed\n   */\n  collapsedSize?: Size;\n  /**\n   * <zh/> 组合的子元素，可以是节点或者组合\n   *\n   * <en/> The children of combo, which can be nodes or combos\n   */\n  childrenNode?: ID[];\n  /**\n   * <zh/> 组合的子元素数据\n   *\n   * <en/> The data of the children of combo\n   * @remarks\n   * <zh/> 如果组合是收起状态，children 可能为空，通过 childrenData 能够获取完整的子元素数据\n   *\n   * <en/> If the combo is collapsed, children may be empty, and the complete child element data can be obtained through childrenData\n   */\n  childrenData?: NodeLikeData[];\n  /**\n   * <zh/> 组合的内边距，只在展开状态下生效\n   *\n   * <en/> The padding of combo, only effective when expanded\n   */\n  padding?: Padding;\n  /**\n   * <zh/> 组合收起时是否显示标记\n   *\n   * <en/> Whether to show the marker when the combo is collapsed\n   */\n  collapsedMarker?: boolean;\n}\n\n/**\n * <zh/> 组合元素的基类\n *\n * <en/> Base class of combo\n * @remarks\n * <zh/> 自定义组合时，推荐使用这个类作为基类。这样，用户只需要专注于实现 keyShape 的绘制逻辑\n *\n * <en/> When customizing a combo, it is recommended to use this class as the base class. In this way, users only need to focus on the logic of drawing keyShape\n */\nexport abstract class BaseCombo<S extends BaseComboStyleProps = BaseComboStyleProps>\n  extends BaseNode<S>\n  implements Combo\n{\n  public type = 'combo';\n\n  static defaultStyleProps: Partial<BaseComboStyleProps> = {\n    childrenNode: [],\n    droppable: true,\n    draggable: true,\n    collapsed: false,\n    collapsedSize: 32,\n    collapsedMarker: true,\n    collapsedMarkerZIndex: 1,\n    collapsedMarkerFontSize: 12,\n    collapsedMarkerTextAlign: 'center',\n    collapsedMarkerTextBaseline: 'middle',\n    collapsedMarkerType: 'child-count',\n  };\n  constructor(options: DisplayObjectConfig<BaseComboStyleProps>) {\n    super(mergeOptions({ style: BaseCombo.defaultStyleProps }, options));\n    this.updateComboPosition(this.parsedAttributes);\n  }\n\n  /**\n   * Draw the key shape of combo\n   */\n  protected abstract drawKeyShape(attributes: Required<S>, container: Group): DisplayObject | undefined;\n\n  protected getKeySize(attributes: Required<S>): STDSize {\n    const { collapsed, childrenNode = [] } = attributes;\n    if (childrenNode.length === 0) return this.getEmptyKeySize(attributes);\n    return collapsed ? this.getCollapsedKeySize(attributes) : this.getExpandedKeySize(attributes);\n  }\n\n  protected getEmptyKeySize(attributes: Required<S>): STDSize {\n    const { padding, collapsedSize } = attributes;\n    const [top, right, bottom, left] = parsePadding(padding);\n    return add(parseSize(collapsedSize), [left + right, top + bottom, 0]) as STDSize;\n  }\n\n  protected getCollapsedKeySize(attributes: Required<S>): STDSize {\n    return parseSize(attributes.collapsedSize);\n  }\n\n  protected getExpandedKeySize(attributes: Required<S>): STDSize {\n    const contentBBox = this.getContentBBox(attributes);\n    return [getBBoxWidth(contentBBox), getBBoxHeight(contentBBox), 0];\n  }\n\n  protected getContentBBox(attributes: Required<S>): AABB {\n    const { childrenNode = [], padding } = attributes;\n    const children = childrenNode.map((id) => this.context!.element!.getElement(id)).filter(Boolean);\n    if (children.length === 0) {\n      const bbox = new AABB();\n      const { x = 0, y = 0, size } = attributes;\n      const [width, height] = parseSize(size);\n      bbox.setMinMax([x - width / 2, y - height / 2, 0], [x + width / 2, y + height / 2, 0]);\n      return bbox;\n    }\n\n    const childrenBBox = getCombinedBBox(children.map((child) => child!.getBounds()));\n\n    if (!padding) return childrenBBox;\n\n    return getExpandedBBox(childrenBBox, padding);\n  }\n\n  protected drawCollapsedMarkerShape(attributes: Required<S>, container: Group): void {\n    const style = this.getCollapsedMarkerStyle(attributes);\n    this.upsert('collapsed-marker', Icon, style, container);\n    connectImage(this);\n  }\n\n  protected getCollapsedMarkerStyle(attributes: Required<S>): IconStyleProps | false {\n    if (!attributes.collapsed || !attributes.collapsedMarker) return false;\n\n    const { type, ...collapsedMarkerStyle } = subStyleProps<CollapsedMarkerStyleProps>(\n      this.getGraphicStyle(attributes),\n      'collapsedMarker',\n    );\n    const keyShape = this.getShape('key');\n    const [x, y] = getXYByPlacement(keyShape.getLocalBounds(), 'center');\n\n    const style = { ...collapsedMarkerStyle, x, y };\n\n    if (type) {\n      const text = this.getCollapsedMarkerText(type, attributes);\n      Object.assign(style, { text });\n    }\n\n    return style;\n  }\n\n  protected getCollapsedMarkerText(type: CollapsedMarkerStyleProps['type'], attributes: Required<S>): string {\n    const { childrenData = [] } = attributes;\n    const { model } = this.context;\n\n    if (type === 'descendant-count') return model.getDescendantsData(this.id).length.toString();\n    if (type === 'child-count') return childrenData.length.toString();\n    if (type === 'node-count')\n      return model\n        .getDescendantsData(this.id)\n        .filter((datum) => model.getElementType(idOf(datum)) === 'node')\n        .length.toString();\n    if (isFunction(type)) return type(childrenData);\n    return '';\n  }\n\n  public getComboPosition(attributes: Required<S>): Point {\n    const { x = 0, y = 0, collapsed, childrenData = [] } = attributes;\n\n    if (childrenData.length === 0) return [+x, +y, 0];\n\n    if (collapsed) {\n      const { model } = this.context;\n      const descendants = model.getDescendantsData(this.id).filter((datum) => !model.isCombo(idOf(datum)));\n\n      if (descendants.length > 0 && descendants.some(hasPosition)) {\n        // combo 被收起，返回平均中心位置 / combo is collapsed, return the average center position\n        const totalPosition = descendants.reduce((acc, datum) => add(acc, positionOf(datum)), [0, 0, 0] as Point);\n        return divide(totalPosition, descendants.length);\n      }\n      // empty combo\n      return [+x, +y, 0];\n    }\n\n    return this.getContentBBox(attributes).center;\n  }\n\n  protected getComboStyle(attributes: Required<S>) {\n    const [x, y] = this.getComboPosition(attributes);\n    // x/y will be used to calculate position later.\n    return { x, y, transform: [['translate', x, y]] };\n  }\n\n  protected updateComboPosition(attributes: Required<S>) {\n    const comboStyle = this.getComboStyle(attributes);\n    Object.assign(this.style, comboStyle);\n    // Sync combo position to model\n    const { x, y } = comboStyle;\n    this.context.model.syncNodeLikeDatum({ id: this.id, style: { x, y } });\n    dispatchPositionChange(this);\n  }\n\n  public render(attributes: Required<S>, container: Group = this) {\n    super.render(attributes, container);\n\n    // collapsed marker\n    this.drawCollapsedMarkerShape(attributes, container);\n  }\n\n  public update(attr: Partial<S> = {}): void {\n    super.update(attr);\n    this.updateComboPosition(this.parsedAttributes);\n  }\n\n  protected onframe() {\n    super.onframe();\n    // 收起状态下，通过动画来更新位置\n    // Update position through animation in collapsed state\n    if (!this.attributes.collapsed) this.updateComboPosition(this.parsedAttributes);\n    this.drawKeyShape(this.parsedAttributes, this);\n  }\n\n  public animate(keyframes: Keyframe[], options?: number | KeyframeAnimationOptions) {\n    const animation = super.animate(\n      this.attributes.collapsed\n        ? keyframes\n        : // 如果当前 combo 是展开状态，则动画不受 x, y, z, transform 影响，仅由子元素决定位置\n          // If the current combo is in the expanded state, the animation is not affected by x, y, z, transform, and the position is determined only by the child elements\n          keyframes.map(({ x, y, z, transform, ...keyframe }: any) => keyframe),\n      options,\n    );\n\n    if (!animation) return animation;\n\n    return new Proxy(animation, {\n      set: (target, propKey, value) => {\n        if (propKey === 'currentTime') Promise.resolve().then(() => this.onframe());\n        return Reflect.set(target, propKey, value);\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/combos/circle.ts",
    "content": "import type { DisplayObjectConfig, CircleStyleProps as GCircleStyleProps } from '@antv/g';\nimport { Circle as GCircle, Group } from '@antv/g';\nimport type { Point, STDSize } from '../../types';\nimport { getBBoxSize } from '../../utils/bbox';\nimport { getEllipseIntersectPoint } from '../../utils/point';\nimport { subStyleProps } from '../../utils/prefix';\nimport { parseSize } from '../../utils/size';\nimport type { BaseComboStyleProps } from './base-combo';\nimport { BaseCombo } from './base-combo';\n\n/**\n * <zh/> 圆形组合样式配置项\n *\n * <en/> Circle combo style props\n */\nexport interface CircleComboStyleProps extends BaseComboStyleProps {}\n\n/**\n * <zh/> 圆形组合\n *\n * <en/> Circle combo\n */\nexport class CircleCombo extends BaseCombo<CircleComboStyleProps> {\n  constructor(options: DisplayObjectConfig<CircleComboStyleProps>) {\n    super(options);\n  }\n\n  protected drawKeyShape(attributes: Required<CircleComboStyleProps>, container: Group): GCircle | undefined {\n    return this.upsert('key', GCircle, this.getKeyStyle(attributes), container);\n  }\n\n  protected getKeyStyle(attributes: Required<CircleComboStyleProps>): GCircleStyleProps {\n    const { collapsed } = attributes;\n    const keyStyle = super.getKeyStyle(attributes);\n\n    const [width] = this.getKeySize(attributes);\n    return {\n      ...keyStyle,\n      ...(collapsed && subStyleProps(keyStyle, 'collapsed')),\n      r: width / 2,\n    };\n  }\n\n  protected getCollapsedKeySize(attributes: Required<CircleComboStyleProps>): STDSize {\n    const [collapsedWidth, collapsedHeight] = parseSize(attributes.collapsedSize);\n    const collapsedR = Math.max(collapsedWidth, collapsedHeight) / 2;\n    return [collapsedR * 2, collapsedR * 2, 0];\n  }\n\n  protected getExpandedKeySize(attributes: Required<CircleComboStyleProps>): STDSize {\n    const contentBBox = this.getContentBBox(attributes);\n    const [width, height] = getBBoxSize(contentBBox);\n    const expandedR = Math.sqrt(width ** 2 + height ** 2) / 2;\n    return [expandedR * 2, expandedR * 2, 0];\n  }\n\n  public getIntersectPoint(point: Point, useExtendedLine = false): Point {\n    const keyShapeBounds = this.getShape('key').getBounds();\n    return getEllipseIntersectPoint(point, keyShapeBounds, useExtendedLine);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/combos/index.ts",
    "content": "export { BaseCombo } from './base-combo';\nexport { CircleCombo } from './circle';\nexport { RectCombo } from './rect';\n\nexport type { BaseComboStyleProps } from './base-combo';\nexport type { CircleComboStyleProps } from './circle';\nexport type { RectComboStyleProps } from './rect';\n"
  },
  {
    "path": "packages/g6/src/elements/combos/rect.ts",
    "content": "import type { DisplayObjectConfig, RectStyleProps as GRectStyleProps } from '@antv/g';\nimport { Rect as GRect, Group } from '@antv/g';\nimport { subStyleProps } from '../../utils/prefix';\nimport type { BaseComboStyleProps } from './base-combo';\nimport { BaseCombo } from './base-combo';\n\n/**\n * <zh/> 矩形组合样式配置项\n *\n * <en/> Rect combo style props\n */\nexport interface RectComboStyleProps extends BaseComboStyleProps {}\n\n/**\n * <zh/> 矩形组合\n *\n * <en/> Rect combo\n */\nexport class RectCombo extends BaseCombo<RectComboStyleProps> {\n  constructor(options: DisplayObjectConfig<RectComboStyleProps>) {\n    super(options);\n  }\n\n  protected drawKeyShape(attributes: Required<RectComboStyleProps>, container: Group): GRect | undefined {\n    return this.upsert('key', GRect, this.getKeyStyle(attributes), container);\n  }\n\n  protected getKeyStyle(attributes: Required<RectComboStyleProps>): GRectStyleProps {\n    const keyStyle = super.getKeyStyle(attributes);\n\n    const [width, height] = this.getKeySize(attributes);\n    return {\n      ...keyStyle,\n      ...(attributes.collapsed && subStyleProps(keyStyle, 'collapsed')),\n      width,\n      height,\n      x: -width / 2,\n      y: -height / 2,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/edges/base-edge.ts",
    "content": "import type { DisplayObject, DisplayObjectConfig, Group, LineStyleProps, PathStyleProps } from '@antv/g';\nimport { Image, Path } from '@antv/g';\nimport type { PathArray } from '@antv/util';\nimport { isFunction, pick } from '@antv/util';\nimport type {\n  Edge,\n  EdgeArrowStyleProps,\n  EdgeBadgeStyleProps,\n  EdgeKey,\n  EdgeLabelStyleProps,\n  ID,\n  Keyframe,\n  LoopStyleProps,\n  Node,\n  Point,\n  Prefix,\n} from '../../types';\nimport { getBBoxHeight, getBBoxWidth, getNodeBBox } from '../../utils/bbox';\nimport { getArrowSize, getBadgePositionStyle, getCubicLoopPath, getLabelPositionStyle } from '../../utils/edge';\nimport { findPorts, getConnectionPoint, getPortPosition, isSameNode } from '../../utils/element';\nimport { subStyleProps } from '../../utils/prefix';\nimport { parseSize } from '../../utils/size';\nimport { mergeOptions } from '../../utils/style';\nimport * as Symbol from '../../utils/symbol';\nimport { getWordWrapWidthByEnds } from '../../utils/text';\nimport { BaseElement } from '../base-element';\nimport type { BadgeStyleProps, BaseShapeStyleProps, LabelStyleProps } from '../shapes';\nimport { Badge, Label } from '../shapes';\n\n/**\n * <zh/> 边的通用样式属性\n *\n * <en/> Base style properties of the edge\n */\nexport interface BaseEdgeStyleProps\n  extends\n    BaseShapeStyleProps,\n    Prefix<'label', EdgeLabelStyleProps>,\n    Prefix<'halo', PathStyleProps>,\n    Prefix<'badge', EdgeBadgeStyleProps>,\n    Prefix<'startArrow', EdgeArrowStyleProps>,\n    Prefix<'endArrow', EdgeArrowStyleProps>,\n    Prefix<'loop', LoopStyleProps> {\n  /**\n   * <zh/> 是否显示边的标签\n   *\n   * <en/> Whether to display the label of the edge\n   * @defaultValue true\n   */\n  label?: boolean;\n  /**\n   * <zh/> 是否启用自环边\n   *\n   * <en/> Whether to enable self-loop edge\n   * @defaultValue true\n   */\n  loop?: boolean;\n  /**\n   * <zh/> 是否显示边的光晕\n   *\n   * <en/> Whether to display the halo of the edge\n   * @defaultValue false\n   */\n  halo?: boolean;\n  /**\n   * <zh/> 是否显示边的徽标\n   *\n   * <en/> Whether to display the badge of the edge\n   * @defaultValue true\n   */\n  badge?: boolean;\n  /**\n   * <zh/> 是否显示边的起始箭头\n   *\n   * <en/> Whether to display the start arrow of the edge\n   * @defaultValue false\n   */\n  startArrow?: boolean;\n  /**\n   * <zh/> 是否显示边的结束箭头\n   *\n   * <en/> Whether to display the end arrow of the edge\n   * @defaultValue false\n   */\n  endArrow?: boolean;\n  /**\n   * <zh/> 起始箭头的偏移量\n   *\n   * <en/> Offset of the start arrow\n   */\n  startArrowOffset?: number;\n  /**\n   * <zh/> 结束箭头的偏移量\n   *\n   * <en/> Offset of the end arrow\n   */\n  endArrowOffset?: number;\n  /**\n   * <zh/> 边的起点 ID\n   *\n   * <en/> The ID of the source node\n   * @remarks\n   * <zh/> 该属性指向物理意义上的起点，由 G6 内部维护，用户无需过多关注。通常情况下，`sourceNode` 与上一级的 `source` 属性一致。但在某些情况下，`sourceNode` 可能会被 G6 内部转换，例如在 Combo 收起时内部节点上的边会自动连接到父 Combo，此时 `sourceNode` 会变更为父 Combo 的 ID。\n   *\n   * <en/> This property concerning the physical origin, maintained internally by G6. In general, `sourceNode` corresponds to the `source` attribute of the parent level. However, in certain cases, such as when a Combo is collapsed and internal nodes are destroyed, corresponding edges will automatically connect to the parent Combo. At this point, `sourceNode` will be changed to the ID of the parent Combo\n   */\n  sourceNode: ID;\n  /**\n   * <zh/> 边的终点 shape\n   *\n   * <en/> The source shape. Represents the start of the edge\n   */\n  targetNode: ID;\n  /**\n   * <zh/> 边起始连接的 port\n   *\n   * <en/> The Port of the source node\n   */\n  sourcePort?: string;\n  /**\n   * <zh/> 边终点连接的 port\n   *\n   * <en/> The Port of the target node\n   */\n  targetPort?: string;\n  /**\n   * <zh/> 在 “起点” 处添加一个标记图形，其中 “起始点” 为边与起始节点的交点\n   *\n   * <en/> Add a marker at the \"start point\", where the \"start point\" is the intersection of the edge and the source node\n   */\n  markerStart?: DisplayObject | null;\n  /**\n   * <zh/> 调整 “起点” 处标记图形的位置，正偏移量向内，负偏移量向外\n   *\n   * <en/> Adjust the position of the marker at the \"start point\", positive offset inward, negative offset outward\n   * @defaultValue 0\n   */\n  markerStartOffset?: number;\n  /**\n   * <zh/> 在 “终点” 处添加一个标记图形，其中 “终点” 为边与终止节点的交点\n   *\n   * <en/> Add a marker at the \"end point\", where the \"end point\" is the intersection of the edge and the target node\n   */\n  markerEnd?: DisplayObject | null;\n  /**\n   * <zh/> 调整 “终点” 处标记图形的位置，正偏移量向内，负偏移量向外\n   *\n   * <en/> Adjust the position of the marker at the \"end point\", positive offset inward, negative offset outward\n   * @defaultValue 0\n   */\n  markerEndOffset?: number;\n  /**\n   * <zh/> 在路径除了 “起点” 和 “终点” 之外的每一个顶点上放置标记图形。在内部实现中，由于我们会把路径中部分命令转换成 C 命令，因此这些顶点实际是三阶贝塞尔曲线的控制点\n   *\n   * <en/> Place a marker on each vertex of the path except for the \"start point\" and \"end point\". In the internal implementation, because we will convert some commands in the path to C commands, these controlPoints are actually the control points of the cubic Bezier curve\n   */\n  markerMid?: DisplayObject | null;\n  /**\n   * <zh/> 3D 场景中生效，始终朝向屏幕，因此线宽不受透视投影影像\n   *\n   * <en/> Effective in 3D scenes, always facing the screen, so the line width is not affected by the perspective projection image\n   * @defaultValue true\n   */\n  isBillboard?: boolean;\n}\n\ntype ParsedBaseEdgeStyleProps = Required<BaseEdgeStyleProps>;\n\n/**\n * <zh/> 边元素基类\n *\n * <en/> Base class of the edge\n */\nexport abstract class BaseEdge extends BaseElement<BaseEdgeStyleProps> implements Edge {\n  public type = 'edge';\n\n  static defaultStyleProps: Partial<BaseEdgeStyleProps> = {\n    badge: true,\n    badgeOffsetX: 0,\n    badgeOffsetY: 0,\n    badgePlacement: 'suffix',\n    isBillboard: true,\n    label: true,\n    labelAutoRotate: true,\n    labelIsBillboard: true,\n    labelMaxWidth: '80%',\n    labelOffsetX: 4,\n    labelOffsetY: 0,\n    labelPlacement: 'center',\n    labelTextBaseline: 'middle',\n    labelWordWrap: false,\n    halo: false,\n    haloDroppable: false,\n    haloLineDash: 0,\n    haloLineWidth: 12,\n    haloPointerEvents: 'none',\n    haloStrokeOpacity: 0.25,\n    haloZIndex: -1,\n    loop: true,\n    startArrow: false,\n    startArrowLineDash: 0,\n    startArrowLineJoin: 'round',\n    startArrowLineWidth: 1,\n    startArrowTransformOrigin: 'center',\n    startArrowType: 'vee',\n    endArrow: false,\n    endArrowLineDash: 0,\n    endArrowLineJoin: 'round',\n    endArrowLineWidth: 1,\n    endArrowTransformOrigin: 'center',\n    endArrowType: 'vee',\n    loopPlacement: 'top',\n    loopClockwise: true,\n  };\n\n  constructor(options: DisplayObjectConfig<BaseEdgeStyleProps>) {\n    super(mergeOptions({ style: BaseEdge.defaultStyleProps }, options));\n  }\n\n  protected get sourceNode() {\n    const { sourceNode: source } = this.parsedAttributes;\n    return this.context.element!.getElement<Node>(source)!;\n  }\n\n  protected get targetNode() {\n    const { targetNode: target } = this.parsedAttributes;\n    return this.context.element!.getElement<Node>(target)!;\n  }\n\n  protected getKeyStyle(attributes: ParsedBaseEdgeStyleProps): PathStyleProps {\n    const { loop, ...style } = this.getGraphicStyle(attributes);\n    const { sourceNode, targetNode } = this;\n\n    const d = loop && isSameNode(sourceNode, targetNode) ? this.getLoopPath(attributes) : this.getKeyPath(attributes);\n\n    const keyStyle: PathStyleProps = { d };\n\n    Path.PARSED_STYLE_LIST.forEach((key) => {\n      // @ts-expect-error skip type error\n      if (key in style) keyStyle[key] = style[key];\n    });\n\n    return keyStyle;\n  }\n\n  protected abstract getKeyPath(attributes: ParsedBaseEdgeStyleProps): PathArray;\n\n  protected getLoopPath(attributes: ParsedBaseEdgeStyleProps): PathArray {\n    const { sourcePort, targetPort } = attributes;\n    const node = this.sourceNode;\n\n    const bbox = getNodeBBox(node);\n    const defaultDist = Math.max(getBBoxWidth(bbox), getBBoxHeight(bbox));\n\n    const {\n      placement,\n      clockwise,\n      dist = defaultDist,\n    } = subStyleProps<Required<LoopStyleProps>>(this.getGraphicStyle(attributes), 'loop');\n\n    return getCubicLoopPath(node, placement, clockwise, dist, sourcePort, targetPort);\n  }\n\n  protected getEndpoints(\n    attributes: ParsedBaseEdgeStyleProps,\n    optimize = true,\n    controlPoints: Point[] | (() => Point[]) = [],\n  ): [Point, Point] {\n    const { sourcePort: sourcePortKey, targetPort: targetPortKey } = attributes;\n    const { sourceNode, targetNode } = this;\n\n    const [sourcePort, targetPort] = findPorts(sourceNode, targetNode, sourcePortKey, targetPortKey);\n\n    if (!optimize) {\n      const sourcePoint = sourcePort ? getPortPosition(sourcePort) : sourceNode.getCenter();\n      const targetPoint = targetPort ? getPortPosition(targetPort) : targetNode.getCenter();\n      return [sourcePoint, targetPoint];\n    }\n\n    const _controlPoints = typeof controlPoints === 'function' ? controlPoints() : controlPoints;\n    const sourcePoint = getConnectionPoint(sourcePort || sourceNode, _controlPoints[0] || targetPort || targetNode);\n    const targetPoint = getConnectionPoint(\n      targetPort || targetNode,\n      _controlPoints[_controlPoints.length - 1] || sourcePort || sourceNode,\n    );\n\n    return [sourcePoint, targetPoint];\n  }\n\n  protected getHaloStyle(attributes: ParsedBaseEdgeStyleProps): false | PathStyleProps {\n    if (attributes.halo === false) return false;\n\n    const keyStyle = this.getKeyStyle(attributes);\n    const haloStyle = subStyleProps<LineStyleProps>(this.getGraphicStyle(attributes), 'halo');\n\n    return { ...keyStyle, ...haloStyle };\n  }\n\n  protected getLabelStyle(attributes: ParsedBaseEdgeStyleProps): false | LabelStyleProps {\n    if (attributes.label === false || !attributes.labelText) return false;\n\n    const labelStyle = subStyleProps<Required<EdgeLabelStyleProps>>(this.getGraphicStyle(attributes), 'label');\n    const { placement, offsetX, offsetY, autoRotate, maxWidth, ...restStyle } = labelStyle;\n    const labelPositionStyle = getLabelPositionStyle(\n      this.shapeMap.key as EdgeKey,\n      placement,\n      autoRotate,\n      offsetX,\n      offsetY,\n    );\n\n    const bbox = this.shapeMap.key.getLocalBounds();\n    const wordWrapWidth = getWordWrapWidthByEnds([bbox.min, bbox.max], maxWidth);\n\n    return Object.assign({ wordWrapWidth }, labelPositionStyle, restStyle);\n  }\n\n  protected getBadgeStyle(attributes: ParsedBaseEdgeStyleProps): false | BadgeStyleProps {\n    if (attributes.badge === false || !attributes.badgeText) return false;\n\n    const { offsetX, offsetY, placement, ...badgeStyle } = subStyleProps<Required<EdgeBadgeStyleProps>>(\n      attributes,\n      'badge',\n    );\n\n    return Object.assign(\n      badgeStyle,\n      getBadgePositionStyle(this.shapeMap, placement, attributes.labelPlacement, offsetX, offsetY),\n    );\n  }\n\n  protected drawArrow(attributes: ParsedBaseEdgeStyleProps, type: 'start' | 'end') {\n    const isStart = type === 'start';\n    const arrowType = type === 'start' ? 'startArrow' : 'endArrow';\n    const enable = attributes[arrowType];\n\n    const keyShape = this.shapeMap.key as Path;\n\n    if (enable) {\n      const arrowStyle = this.getArrowStyle(attributes, isStart);\n\n      const [marker, markerOffset, arrowOffset] = isStart\n        ? (['markerStart', 'markerStartOffset', 'startArrowOffset'] as const)\n        : (['markerEnd', 'markerEndOffset', 'endArrowOffset'] as const);\n\n      const arrow = keyShape.parsedStyle[marker];\n      // update\n      if (arrow) arrow.attr(arrowStyle);\n      // create\n      else {\n        const Ctor = arrowStyle.src ? Image : Path;\n        const arrowShape = new Ctor({ style: arrowStyle });\n        keyShape.style[marker] = arrowShape;\n      }\n      keyShape.style[markerOffset] = attributes[arrowOffset] || arrowStyle.width / 2 + +arrowStyle.lineWidth;\n    } else {\n      // destroy\n      const marker = isStart ? 'markerStart' : 'markerEnd';\n      keyShape.style[marker]?.destroy();\n      keyShape.style[marker] = null;\n    }\n  }\n\n  private getArrowStyle(attributes: ParsedBaseEdgeStyleProps, isStart: boolean) {\n    const keyStyle = this.getShape('key')!.attributes;\n    const arrowType = isStart ? 'startArrow' : 'endArrow';\n    const { size, type, ...arrowStyle } = subStyleProps<Required<EdgeArrowStyleProps>>(\n      this.getGraphicStyle(attributes),\n      arrowType,\n    );\n    const [width, height] = parseSize(getArrowSize(keyStyle.lineWidth, size));\n    const arrowFn = isFunction(type) ? type : Symbol[type] || Symbol.triangle;\n    const d = arrowFn(width, height);\n\n    return Object.assign(\n      pick(keyStyle, ['stroke', 'strokeOpacity', 'fillOpacity']),\n      { width, height },\n      { ...(d && { d, fill: type === 'simple' ? '' : keyStyle.stroke }) },\n      arrowStyle,\n    );\n  }\n\n  protected drawLabelShape(attributes: ParsedBaseEdgeStyleProps, container: Group) {\n    const style = this.getLabelStyle(attributes);\n    this.upsert('label', Label, style, container);\n  }\n\n  protected drawHaloShape(attributes: ParsedBaseEdgeStyleProps, container: Group) {\n    const style = this.getHaloStyle(attributes);\n    this.upsert('halo', Path, style, container);\n  }\n\n  protected drawBadgeShape(attributes: ParsedBaseEdgeStyleProps, container: Group) {\n    const style = this.getBadgeStyle(attributes);\n    this.upsert('badge', Badge, style, container);\n  }\n\n  protected drawSourceArrow(attributes: ParsedBaseEdgeStyleProps) {\n    this.drawArrow(attributes, 'start');\n  }\n\n  protected drawTargetArrow(attributes: ParsedBaseEdgeStyleProps) {\n    this.drawArrow(attributes, 'end');\n  }\n\n  protected drawKeyShape(attributes: ParsedBaseEdgeStyleProps, container: Group): Path | undefined {\n    const style = this.getKeyStyle(attributes);\n    return this.upsert('key', Path, style, container);\n  }\n\n  public render(attributes = this.parsedAttributes, container: Group = this): void {\n    // 1. key shape\n    this.drawKeyShape(attributes, container);\n    if (!this.getShape('key')) return;\n\n    // 2. arrows\n    this.drawSourceArrow(attributes);\n    this.drawTargetArrow(attributes);\n\n    // 3. label\n    this.drawLabelShape(attributes, container);\n\n    // 4. halo\n    this.drawHaloShape(attributes, container);\n\n    // 5. badges\n    this.drawBadgeShape(attributes, container);\n  }\n\n  protected onframe() {\n    this.drawKeyShape(this.parsedAttributes, this);\n    this.drawSourceArrow(this.parsedAttributes);\n    this.drawTargetArrow(this.parsedAttributes);\n    this.drawHaloShape(this.parsedAttributes, this);\n    this.drawLabelShape(this.parsedAttributes, this);\n    this.drawBadgeShape(this.parsedAttributes, this);\n  }\n\n  public animate(keyframes: Keyframe[], options?: number | KeyframeAnimationOptions) {\n    const animation = super.animate(keyframes, options);\n\n    if (!animation) return animation;\n\n    // 设置 currentTime 时触发更新\n    // Trigger update when setting currentTime\n    return new Proxy(animation, {\n      set: (target, propKey, value) => {\n        // 需要推迟 onframe 调用时机，等待节点位置更新完成\n        // Need to delay the timing of the onframe call, wait for the node position update to complete\n        if (propKey === 'currentTime') Promise.resolve().then(() => this.onframe());\n        return Reflect.set(target, propKey, value);\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/edges/cubic-horizontal.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { Point } from '../../types';\nimport { mergeOptions } from '../../utils/style';\nimport type { BaseEdgeStyleProps } from './base-edge';\nimport { Cubic } from './cubic';\n\n/**\n * <zh/> 水平方向的三次贝塞尔曲线样式配置项\n *\n * <en/> Cubic Bezier curve in horizontal direction style properties\n */\nexport interface CubicHorizontalStyleProps extends BaseEdgeStyleProps {\n  /**\n   * <zh/> 控制点在两端点连线上的相对位置，范围为`0-1`\n   *\n   * <en/> The relative position of the control point on the line, ranging from `0-1`\n   * @defaultValue [0.5, 0.5]\n   */\n  curvePosition?: number | [number, number];\n  /**\n   * <zh/> 控制点距离两端点连线的距离，可理解为控制边的弯曲程度\n   *\n   * <en/> The distance of the control point from the line\n   * @defaultValue [0, 0]\n   */\n  curveOffset?: number | [number, number];\n}\n\n/**\n * <zh/> 水平方向的三次贝塞尔曲线\n *\n * <en/> Cubic Bezier curve in horizontal direction\n * @remarks\n * <zh/> 特别注意，计算控制点时主要考虑 x 轴上的距离，忽略 y 轴的变化\n *\n * <en/> Please note that when calculating the control points, the distance on the x-axis is mainly considered, and the change on the y-axis is ignored\n */\nexport class CubicHorizontal extends Cubic {\n  static defaultStyleProps: Partial<CubicHorizontalStyleProps> = {\n    curvePosition: [0.5, 0.5],\n    curveOffset: [0, 0],\n  };\n\n  constructor(options: DisplayObjectConfig<CubicHorizontalStyleProps>) {\n    super(mergeOptions({ style: CubicHorizontal.defaultStyleProps }, options));\n  }\n\n  protected getControlPoints(\n    sourcePoint: Point,\n    targetPoint: Point,\n    curvePosition: [number, number],\n    curveOffset: [number, number],\n  ): [Point, Point] {\n    const xDist = targetPoint[0] - sourcePoint[0];\n    return [\n      [sourcePoint[0] + xDist * curvePosition[0] + curveOffset[0], sourcePoint[1]],\n      [targetPoint[0] - xDist * curvePosition[1] + curveOffset[1], targetPoint[1]],\n    ];\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/edges/cubic-radial.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { NodeData } from '../../spec';\nimport type { Point } from '../../types';\nimport { positionOf } from '../../utils/position';\nimport { mergeOptions } from '../../utils/style';\nimport { distance, rad, subtract } from '../../utils/vector';\nimport type { CubicStyleProps } from './cubic';\nimport { Cubic } from './cubic';\n\n/**\n * <zh/> 径向贝塞尔曲线样式配置项\n *\n * <en/> Radial cubic style props\n */\nexport interface CubicRadialStyleProps extends CubicStyleProps {}\n\n/**\n * <zh/> 径向贝塞尔曲线\n *\n * <en/> Radial cubic edge\n */\nexport class CubicRadial extends Cubic {\n  static defaultStyleProps: Partial<CubicStyleProps> = {\n    curvePosition: 0.5,\n    curveOffset: 20,\n  };\n\n  constructor(options: DisplayObjectConfig<CubicStyleProps>) {\n    super(mergeOptions({ style: CubicRadial.defaultStyleProps }, options));\n  }\n\n  private get ref(): NodeData {\n    return this.context.model.getRootsData()[0];\n  }\n\n  protected getEndpoints(attributes: Required<CubicStyleProps>): [Point, Point] {\n    if (this.sourceNode.id === this.ref.id) {\n      return super.getEndpoints(attributes);\n    }\n\n    const refPoint = positionOf(this.ref);\n    const sourcePoint = this.sourceNode.getIntersectPoint(refPoint, true);\n    const targetPoint = this.targetNode.getIntersectPoint(refPoint);\n\n    return [sourcePoint, targetPoint];\n  }\n\n  private toRadialCoordinate(p: Point) {\n    const refPoint = positionOf(this.ref);\n    const r = distance(p, refPoint);\n    const radian = rad(subtract(p, refPoint));\n    return [r, radian];\n  }\n\n  protected getControlPoints(\n    sourcePoint: Point,\n    targetPoint: Point,\n    curvePosition: [number, number],\n    curveOffset: [number, number],\n  ): [Point, Point] {\n    const [r1, rad1] = this.toRadialCoordinate(sourcePoint);\n    const [r2] = this.toRadialCoordinate(targetPoint);\n\n    const rDist = r2 - r1;\n\n    return [\n      [\n        sourcePoint[0] + (rDist * curvePosition[0] + curveOffset[0]) * Math.cos(rad1),\n        sourcePoint[1] + (rDist * curvePosition[0] + curveOffset[0]) * Math.sin(rad1),\n      ],\n      [\n        targetPoint[0] - (rDist * curvePosition[1] - curveOffset[0]) * Math.cos(rad1),\n        targetPoint[1] - (rDist * curvePosition[1] - curveOffset[0]) * Math.sin(rad1),\n      ],\n    ];\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/edges/cubic-vertical.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { Point } from '../../types';\nimport { mergeOptions } from '../../utils/style';\nimport type { BaseEdgeStyleProps } from './base-edge';\nimport { Cubic } from './cubic';\n\n/**\n * <zh/> 垂直方向的三次贝塞尔曲线样式配置项\n *\n * <en/> Cubic Bezier curve style properties in vertical direction\n */\nexport interface CubicVerticalStyleProps extends BaseEdgeStyleProps {\n  /**\n   * <zh/> 控制点在两端点连线上的相对位置，范围为`0-1`\n   *\n   * <en/> The relative position of the control point on the line, ranging from `0-1`\n   * @defaultValue [0.5, 0.5]\n   */\n  curvePosition?: number | [number, number];\n  /**\n   * <zh/> 控制点距离两端点连线的距离，可理解为控制边的弯曲程度\n   *\n   * <en/> The distance of the control point from the line\n   * @defaultValue [0, 0]\n   */\n  curveOffset?: number | [number, number];\n}\n\n/**\n * <zh/> 垂直方向的三次贝塞尔曲线\n *\n * <en/> Cubic Bezier curve in vertical direction\n * @remarks\n * <zh/> 特别注意，计算控制点时主要考虑 y 轴上的距离，忽略 x 轴的变化\n *\n * <en/> Please note that when calculating the control points, the distance on the y-axis is mainly considered, and the change on the x-axis is ignored\n */\nexport class CubicVertical extends Cubic {\n  static defaultStyleProps: Partial<CubicVerticalStyleProps> = {\n    curvePosition: [0.5, 0.5],\n    curveOffset: [0, 0],\n  };\n\n  constructor(options: DisplayObjectConfig<CubicVerticalStyleProps>) {\n    super(mergeOptions({ style: CubicVertical.defaultStyleProps }, options));\n  }\n\n  protected getControlPoints(\n    sourcePoint: Point,\n    targetPoint: Point,\n    curvePosition: [number, number],\n    curveOffset: [number, number],\n  ): [Point, Point] {\n    const yDist = targetPoint[1] - sourcePoint[1];\n    return [\n      [sourcePoint[0], sourcePoint[1] + yDist * curvePosition[0] + curveOffset[0]],\n      [targetPoint[0], targetPoint[1] - yDist * curvePosition[1] + curveOffset[1]],\n    ];\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/edges/cubic.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { PathArray } from '@antv/util';\nimport type { Point } from '../../types';\nimport { getCubicPath, getCurveControlPoint, parseCurveOffset, parseCurvePosition } from '../../utils/edge';\nimport { mergeOptions } from '../../utils/style';\nimport type { BaseEdgeStyleProps } from './base-edge';\nimport { BaseEdge } from './base-edge';\n\n/**\n * <zh/> 三次贝塞尔曲线样式配置项\n *\n * <en/> Cubic Bezier curve style properties\n */\nexport interface CubicStyleProps extends BaseEdgeStyleProps {\n  /**\n   * <zh/> 控制点数组，用于定义曲线的形状。如果不指定，将会通过 `curveOffset` 和 `curvePosition` 来计算控制点\n   *\n   * <en/> Control points. Used to define the shape of the curve. If not specified, it will be calculated using `curveOffset` and `curvePosition`.\n   */\n  controlPoints?: [Point, Point];\n  /**\n   * <zh/> 控制点在两端点连线上的相对位置，范围为`0-1`\n   *\n   * <en/> The relative position of the control point on the line, ranging from `0-1`\n   * @defaultValue 0.5\n   */\n  curvePosition?: number | [number, number];\n  /**\n   * <zh/> 控制点距离两端点连线的距离，可理解为控制边的弯曲程度\n   *\n   * <en/> The distance of the control point from the line\n   * @defaultValue 20\n   */\n  curveOffset?: number | [number, number];\n}\n\ntype ParsedCubicStyleProps = Required<CubicStyleProps>;\n\n/**\n * <zh/> 三次贝塞尔曲线\n *\n * <en/> Cubic Bezier curve\n */\nexport class Cubic extends BaseEdge {\n  static defaultStyleProps: Partial<CubicStyleProps> = {\n    curvePosition: 0.5,\n    curveOffset: 20,\n  };\n\n  constructor(options: DisplayObjectConfig<CubicStyleProps>) {\n    super(mergeOptions({ style: Cubic.defaultStyleProps }, options));\n  }\n\n  /**\n   * @inheritdoc\n   */\n  protected getKeyPath(attributes: ParsedCubicStyleProps): PathArray {\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes);\n    const { controlPoints, curvePosition, curveOffset } = attributes;\n\n    const actualControlPoints = this.getControlPoints(\n      sourcePoint,\n      targetPoint,\n      parseCurvePosition(curvePosition),\n      parseCurveOffset(curveOffset),\n      controlPoints,\n    );\n\n    return getCubicPath(sourcePoint, targetPoint, actualControlPoints);\n  }\n\n  protected getControlPoints(\n    sourcePoint: Point,\n    targetPoint: Point,\n    curvePosition: [number, number],\n    curveOffset: [number, number],\n    controlPoints?: [Point, Point],\n  ): [Point, Point] {\n    return controlPoints?.length === 2\n      ? controlPoints\n      : [\n          getCurveControlPoint(sourcePoint, targetPoint, curvePosition[0], curveOffset[0]),\n          getCurveControlPoint(sourcePoint, targetPoint, curvePosition[1], curveOffset[1]),\n        ];\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/edges/index.ts",
    "content": "export { BaseEdge } from './base-edge';\nexport { Cubic } from './cubic';\nexport { CubicHorizontal } from './cubic-horizontal';\nexport { CubicRadial } from './cubic-radial';\nexport { CubicVertical } from './cubic-vertical';\nexport { Line } from './line';\nexport { Polyline } from './polyline';\nexport { Quadratic } from './quadratic';\n\nexport type { BaseEdgeStyleProps } from './base-edge';\nexport type { CubicStyleProps } from './cubic';\nexport type { CubicHorizontalStyleProps } from './cubic-horizontal';\nexport type { CubicRadialStyleProps } from './cubic-radial';\nexport type { CubicVerticalStyleProps } from './cubic-vertical';\nexport type { LineStyleProps } from './line';\nexport type { PolylineStyleProps } from './polyline';\nexport type { QuadraticStyleProps } from './quadratic';\n"
  },
  {
    "path": "packages/g6/src/elements/edges/line.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { PathArray } from '@antv/util';\nimport { mergeOptions } from '../../utils/style';\nimport type { BaseEdgeStyleProps } from './base-edge';\nimport { BaseEdge } from './base-edge';\n\n/**\n * <zh/> 直线样式配置项\n *\n * <en/> Line style properties\n */\nexport interface LineStyleProps extends BaseEdgeStyleProps {}\n\ntype ParsedLineStyleProps = Required<LineStyleProps>;\n\n/**\n * <zh/> 直线\n *\n * <en/> Line\n */\nexport class Line extends BaseEdge {\n  static defaultStyleProps: Partial<LineStyleProps> = {};\n\n  constructor(options: DisplayObjectConfig<LineStyleProps>) {\n    super(mergeOptions({ style: Line.defaultStyleProps }, options));\n  }\n\n  protected getKeyPath(attributes: ParsedLineStyleProps): PathArray {\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes);\n    return [\n      ['M', sourcePoint[0], sourcePoint[1]],\n      ['L', targetPoint[0], targetPoint[1]],\n    ];\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/edges/polyline.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { PathArray } from '@antv/util';\nimport type { LoopStyleProps, Point, PolylineRouter } from '../../types';\nimport { getBBoxHeight, getBBoxWidth, getNodeBBox } from '../../utils/bbox';\nimport { getPolylineLoopPath, getPolylinePath } from '../../utils/edge';\nimport { subStyleProps } from '../../utils/prefix';\nimport { orth } from '../../utils/router/orth';\nimport { aStarSearch } from '../../utils/router/shortest-path';\nimport { mergeOptions } from '../../utils/style';\nimport type { BaseEdgeStyleProps } from './base-edge';\nimport { BaseEdge } from './base-edge';\n\n/**\n * <zh/> 折线样式配置项\n *\n * <en/> Polyline style properties\n */\nexport interface PolylineStyleProps extends BaseEdgeStyleProps {\n  /**\n   * <zh/> 圆角半径\n   *\n   * <en/> The radius of the rounded corner\n   * @defaultValue 0\n   */\n  radius?: number;\n  /**\n   * <zh/> 控制点数组\n   *\n   * <en/> Control point array\n   */\n  controlPoints?: Point[];\n  /**\n   * <zh/> 是否启用路由，默认开启且 controlPoints 会自动计入\n   *\n   * <en/> Whether to enable routing, it is enabled by default and controlPoints will be automatically included\n   * @defaultValue false\n   */\n  router?: PolylineRouter;\n}\n\ntype ParsedPolylineStyleProps = Required<PolylineStyleProps>;\n\n/**\n * <zh/> 折线\n *\n * <en/> Polyline\n */\nexport class Polyline extends BaseEdge {\n  static defaultStyleProps: Partial<PolylineStyleProps> = {\n    radius: 0,\n    controlPoints: [],\n    router: false,\n  };\n\n  constructor(options: DisplayObjectConfig<PolylineStyleProps>) {\n    super(mergeOptions({ style: Polyline.defaultStyleProps }, options));\n  }\n\n  protected getControlPoints(attributes: ParsedPolylineStyleProps): Point[] {\n    const { router } = attributes;\n    const { sourceNode, targetNode } = this;\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes, false);\n\n    let controlPoints: Point[] = [];\n\n    if (!router) {\n      controlPoints = attributes.controlPoints;\n    } else {\n      if (router.type === 'shortest-path') {\n        const nodes = this.context.element!.getNodes();\n        controlPoints = aStarSearch(sourceNode, targetNode, nodes, router);\n\n        if (!controlPoints.length) {\n          controlPoints = orth(sourcePoint, targetPoint, sourceNode, targetNode, attributes.controlPoints, {\n            padding: router.offset,\n          });\n        }\n      } else if (router.type === 'orth') {\n        controlPoints = orth(sourcePoint, targetPoint, sourceNode, targetNode, attributes.controlPoints, router);\n      }\n    }\n\n    return controlPoints;\n  }\n\n  protected getPoints(attributes: ParsedPolylineStyleProps): Point[] {\n    const controlPoints = this.getControlPoints(attributes);\n\n    const [newSourcePoint, newTargetPoint] = this.getEndpoints(attributes, true, controlPoints);\n    return [newSourcePoint, ...controlPoints, newTargetPoint];\n  }\n\n  protected getKeyPath(attributes: ParsedPolylineStyleProps): PathArray {\n    const points = this.getPoints(attributes);\n\n    return getPolylinePath(points, attributes.radius);\n  }\n\n  protected getLoopPath(attributes: ParsedPolylineStyleProps): PathArray {\n    const { sourcePort: sourcePortKey, targetPort: targetPortKey, radius } = attributes;\n    const node = this.sourceNode;\n\n    const bbox = getNodeBBox(node);\n    // 默认转折点距离为 bbox 的最大宽高的 1/4\n    // Default distance of the turning point is 1/4 of the maximum width and height of the bbox\n    const defaultDist = Math.max(getBBoxWidth(bbox), getBBoxHeight(bbox)) / 4;\n\n    const {\n      placement,\n      clockwise,\n      dist = defaultDist,\n    } = subStyleProps<Required<LoopStyleProps>>(this.getGraphicStyle(attributes), 'loop');\n\n    return getPolylineLoopPath(node, radius, placement, clockwise, dist, sourcePortKey, targetPortKey);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/edges/quadratic.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { PathArray } from '@antv/util';\nimport type { Point } from '../../types';\nimport { getCurveControlPoint, getQuadraticPath } from '../../utils/edge';\nimport { mergeOptions } from '../../utils/style';\nimport type { BaseEdgeStyleProps } from './base-edge';\nimport { BaseEdge } from './base-edge';\n\n/**\n * <zh/> 二次贝塞尔曲线样式配置项\n *\n * <en/> Quadratic Bezier curve style properties\n */\nexport interface QuadraticStyleProps extends BaseEdgeStyleProps {\n  /**\n   * <zh/> 控制点，用于定义曲线的形状。如果不指定，将会通过`curveOffset`和`curvePosition`来计算控制点\n   *\n   * <en/> Control point. Used to define the shape of the curve. If not specified, it will be calculated using `curveOffset` and `curvePosition`.\n   */\n  controlPoint?: Point;\n  /**\n   * <zh/> 控制点在两端点连线上的相对位置，范围为`0-1`\n   *\n   * <en/> The relative position of the control point on the line, ranging from `0-1`\n   * @defaultValue 0.5\n   */\n  curvePosition?: number;\n  /**\n   * <zh/> 控制点距离两端点连线的距离，可理解为控制边的弯曲程度\n   *\n   * <en/> The distance of the control point from the line\n   * @defaultValue 30\n   */\n  curveOffset?: number;\n}\n\ntype ParsedQuadraticStyleProps = Required<QuadraticStyleProps>;\n\n/**\n * <zh/> 二次贝塞尔曲线\n *\n * <en/> Quadratic Bezier curve\n */\nexport class Quadratic extends BaseEdge {\n  static defaultStyleProps: Partial<QuadraticStyleProps> = {\n    curvePosition: 0.5,\n    curveOffset: 30,\n  };\n\n  constructor(options: DisplayObjectConfig<QuadraticStyleProps>) {\n    super(mergeOptions({ style: Quadratic.defaultStyleProps }, options));\n  }\n\n  protected getKeyPath(attributes: ParsedQuadraticStyleProps): PathArray {\n    const { curvePosition, curveOffset } = attributes;\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes);\n    const controlPoint =\n      attributes.controlPoint || getCurveControlPoint(sourcePoint, targetPoint, curvePosition, curveOffset);\n    return getQuadraticPath(sourcePoint, targetPoint, controlPoint);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/effect.ts",
    "content": "import type { Element } from '../types';\n\nconst EFFECT_WEAKMAP = new WeakMap<Element, Record<string, any>>();\n\n/**\n * <zh/> 判定给定样式是否与上一次的样式相同\n *\n * <en/> Determine whether the given style are the same as the previous ones\n * @param target - <zh/> 目标元素 | <en/> Target element\n * @param key - <zh/> 缓存 key | <en/> Cache key\n * @param style - <zh/> 样式属性 | <en/> Style attribute\n * @returns <zh/> 是否执行函数 | <en/> Whether to execute the function\n * @deprecated <zh/> 该方法已废弃，并不能显著提升性能 | <en/> This method is deprecated and does not significantly improve performance\n */\nexport function effect<T extends false | Record<string, any>>(target: Element, key: string, style: T): boolean {\n  // return true;\n  if (!EFFECT_WEAKMAP.has(target)) EFFECT_WEAKMAP.set(target, {});\n  const cache = EFFECT_WEAKMAP.get(target)!;\n  if (!cache[key]) {\n    cache[key] = style;\n    return true;\n  }\n  const original = cache[key];\n  if (isStyleEqual(original, style)) return false;\n  cache[key] = style;\n  return true;\n}\n\n/**\n * <zh/> 比较两个样式属性是否相等\n *\n * <en/> Compare whether two style attributes are equal\n * @param a - <zh/> 样式属性 a | <en/> Style attribute a\n * @param b - <zh/> 样式属性 b | <en/> Style attribute b\n * @param depth - <zh/> 比较深度 | <en/> Comparison depth\n * @returns <zh/> 是否相等 | <en/> Whether they are equal\n * @remarks\n * <zh/> 进行第二层浅比较用于比较 badges、ports 等复合图形属性\n *\n * <en/> Perform a second-level shallow comparison to compare complex shape attributes such as badges and ports\n */\nconst isStyleEqual = (a: false | Record<string, unknown>, b: false | Record<string, unknown>, depth = 2): boolean => {\n  if (typeof a !== 'object' || typeof b !== 'object') return a === b;\n\n  const keys1 = Object.keys(a);\n  const keys2 = Object.keys(b);\n\n  if (keys1.length !== keys2.length) return false;\n\n  for (const key of keys1) {\n    const val1 = a[key];\n    const val2 = b[key];\n    if (depth > 1 && typeof val1 === 'object' && typeof val2 === 'object') {\n      if (!isStyleEqual(val1 as Record<string, unknown>, val2 as Record<string, unknown>, depth - 1)) return false;\n    } else if (val1 !== val2) return false;\n  }\n\n  return true;\n};\n"
  },
  {
    "path": "packages/g6/src/elements/index.ts",
    "content": "export * from './combos';\nexport * from './edges';\nexport * from './nodes';\n"
  },
  {
    "path": "packages/g6/src/elements/nodes/base-node.ts",
    "content": "import type { BaseStyleProps, CircleStyleProps, DisplayObject, DisplayObjectConfig, Group } from '@antv/g';\nimport { Circle as GCircle } from '@antv/g';\nimport type { CategoricalPalette } from '../../palettes/types';\nimport type { RuntimeContext } from '../../runtime/types';\nimport type { NodeData } from '../../spec';\nimport type {\n  ID,\n  Node,\n  NodeBadgeStyleProps,\n  NodeLabelStyleProps,\n  NodePortStyleProps,\n  Point,\n  Port,\n  PortPlacement,\n  PortStyleProps,\n  Prefix,\n  Size,\n} from '../../types';\nimport { getPortXYByPlacement, getTextStyleByPlacement, isSimplePort } from '../../utils/element';\nimport { inferIconStyle } from '../../utils/node';\nimport { getPaletteColors } from '../../utils/palette';\nimport { getRectIntersectPoint } from '../../utils/point';\nimport { omitStyleProps, subObject, subStyleProps } from '../../utils/prefix';\nimport { parseSize } from '../../utils/size';\nimport { mergeOptions } from '../../utils/style';\nimport { getWordWrapWidthByBox } from '../../utils/text';\nimport { setVisibility } from '../../utils/visibility';\nimport { BaseElement } from '../base-element';\nimport type { BadgeStyleProps, BaseShapeStyleProps, IconStyleProps, LabelStyleProps } from '../shapes';\nimport { Badge, Icon, Label } from '../shapes';\nimport { connectImage, dispatchPositionChange } from '../shapes/image';\n\n/**\n * <zh/> 节点通用样式配置项\n *\n * <en/> Base node style props\n */\nexport interface BaseNodeStyleProps\n  extends\n    BaseShapeStyleProps,\n    Prefix<'label', NodeLabelStyleProps>,\n    Prefix<'halo', BaseStyleProps>,\n    Prefix<'icon', IconStyleProps>,\n    Prefix<'badge', BadgeStyleProps>,\n    Prefix<'port', PortStyleProps> {\n  /**\n   * <zh/> x 坐标\n   *\n   * <en/> The x-coordinate of node\n   */\n  x?: number;\n  /**\n   * <zh/> y 坐标\n   *\n   * <en/> The y-coordinate of node\n   */\n  y?: number;\n  /**\n   * <zh/> z 坐标\n   *\n   * <en/> The z-coordinate of node\n   */\n  z?: number;\n  /**\n   * <zh/> 节点大小，快捷设置节点宽高\n   * - 若值为数字，则表示节点的宽度、高度以及深度相同为指定值\n   * - 若值为数组，则按数组元素依次表示节点的宽度、高度以及深度\n   *\n   * <en/> The size of node, which is a shortcut to set the width and height of node\n   * - If the value is a number, it means the width, height, and depth of the node are the same as the specified value\n   * - If the value is an array, it means the width, height, and depth of the node are represented by the array elements in turn\n   */\n  size?: Size;\n  /**\n   * <zh/> 当前节点/组合是否展开\n   *\n   * <en/> Whether the current node/combo is expanded\n   */\n  collapsed?: boolean;\n  /**\n   * <zh/> 子节点实例\n   *\n   * <en/> The instance of the child node\n   * @remarks\n   * <zh/> 仅在树图中生效\n   *\n   * <en/> Only valid in the tree graph\n   * @ignore\n   */\n  childrenNode?: ID[];\n  /**\n   * <zh/> 子节点数据\n   *\n   * <en/> The data of the child node\n   * @remarks\n   * <zh/> 仅在树图中生效。如果当前节点为收起状态，children 可能为空，通过 childrenData 能够获取完整的子元素数据\n   *\n   * <en/> Only valid in the tree graph. If the current node is collapsed, children may be empty, and the complete child element data can be obtained through childrenData\n   * @ignore\n   */\n  childrenData?: NodeData[];\n  /**\n   * <zh/> 是否显示节点标签\n   *\n   * <en/> Whether to show the node label\n   * @defaultValue true\n   */\n  label?: boolean;\n  /**\n   * <zh/> 是否显示节点光晕\n   *\n   * <en/> Whether to show the node halo\n   * @defaultValue false\n   */\n  halo?: boolean;\n  /**\n   * <zh/> 是否显示节点图标\n   *\n   * <en/> Whether to show the node icon\n   * @defaultValue true\n   */\n  icon?: boolean;\n  /**\n   * <zh/> 是否显示节点徽标\n   *\n   * <en/> Whether to show the node badge\n   * @defaultValue true\n   */\n  badge?: boolean;\n  /**\n   * <zh/> 是否显示连接桩\n   *\n   * <en/> Whether to show the node port\n   * @defaultValue true\n   */\n  port?: boolean;\n  /**\n   * <zh/> 连接桩配置项，支持配置多个连接桩\n   *\n   * <en/> Port configuration, supports configuring multiple ports\n   * @example\n   * ```json\n   * {\n   *    port: true,\n   *    ports: [\n   *      { key: 'top', placement: [0.5, 0], r: 4, stroke: '#31d0c6', fill: '#fff' },\n   *      { key: 'bottom', placement: [0.5, 1], r: 4, stroke: '#31d0c6', fill: '#fff' },\n   *    ],\n   * }\n   * ```\n   */\n  ports?: NodePortStyleProps[];\n  /**\n   * <zh/> 徽标\n   *\n   * <en/> Badge\n   * @example\n   * ```json\n   * {\n   *    badge: true,\n   *    badges: [\n   *      { text: 'A', placement: 'right-top'},\n   *      { text: 'Important', placement: 'right' },\n   *      { text: 'Notice', placement: 'right-bottom' },\n   *    ],\n   *    badgePalette: ['#7E92B5', '#F4664A', '#FFBE3A'],\n   * }\n   * ```\n   */\n  badges?: NodeBadgeStyleProps[];\n  /**\n   * <zh/> 徽标的背景色板\n   *\n   * <en/> Badge background color palette\n   */\n  badgePalette?: CategoricalPalette;\n}\n\n/**\n * <zh/> 节点元素的基类\n *\n * <en/> Base node class\n * @remarks\n * <zh/> 自定义节点时，建议将此类作为基类。这样，你只需要关注如何实现 keyShape 的绘制逻辑\n *\n * <zh/> 设计文档：https://www.yuque.com/antv/g6/gl1iof1xpzg6ed98\n *\n * <en/> When customizing a node, it is recommended to use this class as the base class. This way, you can directly focus on how to implement the drawing logic of keyShape\n *\n * <en/> Design document: https://www.yuque.com/antv/g6/gl1iof1xpzg6ed98\n */\nexport abstract class BaseNode<S extends BaseNodeStyleProps = BaseNodeStyleProps>\n  extends BaseElement<S>\n  implements Node\n{\n  public type = 'node';\n\n  static defaultStyleProps: Partial<BaseNodeStyleProps> = {\n    x: 0,\n    y: 0,\n    size: 32,\n    droppable: true,\n    draggable: true,\n    port: true,\n    ports: [],\n    portZIndex: 2,\n    portLinkToCenter: false,\n    badge: true,\n    badges: [],\n    badgeZIndex: 3,\n    halo: false,\n    haloDroppable: false,\n    haloLineDash: 0,\n    haloLineWidth: 12,\n    haloStrokeOpacity: 0.25,\n    haloPointerEvents: 'none',\n    haloZIndex: -1,\n    icon: true,\n    iconZIndex: 1,\n    label: true,\n    labelIsBillboard: true,\n    labelMaxWidth: '200%',\n    labelPlacement: 'bottom',\n    labelWordWrap: false,\n    labelZIndex: 0,\n  };\n\n  constructor(options: DisplayObjectConfig<S>) {\n    super(mergeOptions({ style: BaseNode.defaultStyleProps }, options));\n  }\n\n  protected getSize(attributes = this.attributes) {\n    const { size } = attributes;\n    return parseSize(size);\n  }\n\n  protected getKeyStyle(attributes: Required<S>) {\n    const style = this.getGraphicStyle(attributes);\n\n    return Object.assign(omitStyleProps(style, ['label', 'halo', 'icon', 'badge', 'port'])) as any;\n  }\n\n  protected getLabelStyle(attributes: Required<S>): false | LabelStyleProps {\n    if (attributes.label === false || !attributes.labelText) return false;\n\n    const { placement, maxWidth, offsetX, offsetY, ...labelStyle } = subStyleProps<Required<NodeLabelStyleProps>>(\n      this.getGraphicStyle(attributes),\n      'label',\n    );\n    const keyBounds = this.getShape('key').getLocalBounds();\n\n    return Object.assign(\n      getTextStyleByPlacement(keyBounds, placement, offsetX, offsetY),\n      { wordWrapWidth: getWordWrapWidthByBox(keyBounds, maxWidth) },\n      labelStyle,\n    );\n  }\n\n  protected getHaloStyle(attributes: Required<S>) {\n    if (attributes.halo === false) return false;\n\n    const { fill, ...keyStyle } = this.getKeyStyle(attributes);\n    const haloStyle = subStyleProps(this.getGraphicStyle(attributes), 'halo');\n\n    return { ...keyStyle, stroke: fill, ...haloStyle } as any;\n  }\n\n  protected getIconStyle(attributes: Required<S>): false | IconStyleProps {\n    if (attributes.icon === false || (!attributes.iconText && !attributes.iconSrc)) return false;\n\n    const iconStyle = subStyleProps(this.getGraphicStyle(attributes), 'icon');\n\n    return Object.assign(inferIconStyle(attributes.size!, iconStyle), iconStyle);\n  }\n\n  protected getBadgesStyle(attributes: Required<S>): Record<string, NodeBadgeStyleProps | false> {\n    const badges = subObject(this.shapeMap, 'badge-');\n    const badgesShapeStyle: Record<string, NodeBadgeStyleProps | false> = {};\n\n    Object.keys(badges).forEach((key) => {\n      badgesShapeStyle[key] = false;\n    });\n    if (attributes.badge === false || !attributes.badges?.length) return badgesShapeStyle;\n\n    const { badges: badgeOptions = [], badgePalette, opacity = 1, ...restAttributes } = attributes;\n    const colors = getPaletteColors(badgePalette);\n    const badgeStyle = subStyleProps<BadgeStyleProps>(this.getGraphicStyle(restAttributes), 'badge');\n\n    badgeOptions.forEach((option, i) => {\n      badgesShapeStyle[i] = {\n        backgroundFill: colors ? colors[i % colors?.length] : undefined,\n        opacity,\n        ...badgeStyle,\n        ...this.getBadgeStyle(option),\n      };\n    });\n\n    return badgesShapeStyle;\n  }\n\n  protected getBadgeStyle(style: NodeBadgeStyleProps): NodeBadgeStyleProps {\n    const keyShape = this.getShape('key');\n    const { placement = 'top', offsetX, offsetY, ...restStyle } = style;\n    const textStyle = getTextStyleByPlacement(keyShape.getLocalBounds(), placement, offsetX, offsetY, true);\n    return { ...textStyle, ...restStyle };\n  }\n\n  protected getPortsStyle(attributes: Required<S>): Record<string, PortStyleProps | false> {\n    const ports = this.getPorts();\n    const portsShapeStyle: Record<string, PortStyleProps | false> = {};\n\n    Object.keys(ports).forEach((key) => {\n      portsShapeStyle[key] = false;\n    });\n\n    if (attributes.port === false || !attributes.ports?.length) return portsShapeStyle;\n\n    const portStyle = subStyleProps<PortStyleProps>(this.getGraphicStyle(attributes), 'port');\n    const { ports: portOptions = [] } = attributes;\n    portOptions.forEach((option, index) => {\n      const key = option.key || index;\n      const mergedStyle = { ...portStyle, ...option };\n      if (isSimplePort(mergedStyle)) {\n        portsShapeStyle[key] = false;\n      } else {\n        const [x, y] = this.getPortXY(attributes, option);\n        portsShapeStyle[key] = { transform: [['translate', x, y]], ...mergedStyle };\n      }\n    });\n    return portsShapeStyle;\n  }\n\n  protected getPortXY(attributes: Required<S>, style: NodePortStyleProps): Point {\n    const { placement = 'left' } = style;\n    const keyShape = this.getShape('key');\n    return getPortXYByPlacement(getBoundsInOffscreen(this.context, keyShape), placement as PortPlacement);\n  }\n\n  /**\n   * Get the ports for the node.\n   * @returns Ports shape map.\n   */\n  public getPorts(): Record<string, Port> {\n    return subObject(this.shapeMap, 'port-');\n  }\n\n  /**\n   * Get the center point of the node.\n   * @returns The center point of the node.\n   */\n  public getCenter(): Point {\n    return this.getShape('key').getBounds().center;\n  }\n\n  /**\n   * Get the point on the outer contour of the node that is the intersection with a line starting in the center the ending in the point `p`.\n   * @param point - The point to intersect with the node.\n   * @param useExtendedLine - Whether to use the extended line.\n   * @returns The intersection point.\n   */\n  public getIntersectPoint(point: Point, useExtendedLine = false): Point {\n    const keyShapeBounds = this.getShape('key').getBounds();\n    return getRectIntersectPoint(point, keyShapeBounds, useExtendedLine);\n  }\n\n  protected drawHaloShape(attributes: Required<S>, container: Group): void {\n    const style = this.getHaloStyle(attributes);\n    const keyShape = this.getShape('key');\n    this.upsert('halo', keyShape.constructor as new (...args: unknown[]) => DisplayObject, style, container);\n  }\n\n  protected drawIconShape(attributes: Required<S>, container: Group): void {\n    const style = this.getIconStyle(attributes);\n    this.upsert('icon', Icon, style, container);\n    connectImage(this);\n  }\n\n  protected drawBadgeShapes(attributes: Required<S>, container: Group): void {\n    const badgesStyle = this.getBadgesStyle(attributes);\n    Object.keys(badgesStyle).forEach((key) => {\n      const style = badgesStyle[key];\n      this.upsert(`badge-${key}`, Badge, style, container);\n    });\n  }\n\n  protected drawPortShapes(attributes: Required<S>, container: Group): void {\n    const portsStyle = this.getPortsStyle(attributes);\n\n    Object.keys(portsStyle).forEach((key) => {\n      const style = portsStyle[key] as CircleStyleProps;\n      const shapeKey = `port-${key}`;\n      this.upsert(shapeKey, GCircle, style, container);\n    });\n  }\n\n  protected drawLabelShape(attributes: Required<S>, container: Group): void {\n    const style = this.getLabelStyle(attributes);\n    this.upsert('label', Label, style, container);\n  }\n\n  protected abstract drawKeyShape(attributes: Required<S>, container: Group): DisplayObject | undefined;\n\n  // 用于装饰抽象方法 / Used to decorate abstract methods\n  private _drawKeyShape(attributes: Required<S>, container: Group) {\n    return this.drawKeyShape(attributes, container);\n  }\n\n  public render(attributes = this.parsedAttributes, container: Group = this) {\n    // 1. key shape\n    this._drawKeyShape(attributes, container);\n    if (!this.getShape('key')) return;\n\n    // 2. halo, use shape same with keyShape\n    this.drawHaloShape(attributes, container);\n\n    // 3. icon\n    this.drawIconShape(attributes, container);\n\n    // 4. badges\n    this.drawBadgeShapes(attributes, container);\n\n    // 5. label\n    this.drawLabelShape(attributes, container);\n\n    // 6. ports\n    this.drawPortShapes(attributes, container);\n  }\n\n  public update(attr?: Partial<S>): void {\n    super.update(attr);\n    if (attr && ('x' in attr || 'y' in attr || 'z' in attr)) {\n      dispatchPositionChange(this);\n    }\n  }\n\n  protected onframe() {\n    this.drawBadgeShapes(this.parsedAttributes, this);\n    this.drawLabelShape(this.parsedAttributes, this);\n  }\n}\n\n/**\n * <zh/> 在离屏画布中获取图形包围盒\n *\n * <en/> Get the bounding box of the shape in the off-screen canvas\n * @param context - <zh/> 运行时上下文 <en/> Runtime context\n * @param shape - <zh/> 图形实例 <en/> Graphic instance\n * @returns <zh/> 图形包围盒 <en/> Graphic bounding box\n */\nfunction getBoundsInOffscreen(context: RuntimeContext, shape: DisplayObject) {\n  if (!context) return shape.getLocalBounds();\n\n  // 将主图形靠背至全局空间，避免受到父级 transform 的影响\n  // 合理的操作应该是拷贝至离屏画布，但目前 G 有点问题\n  // Move the main shape to the global space to avoid being affected by the parent transform\n  // The reasonable operation should be moved to the off-screen canvas, but there is a problem with G at present\n  const canvas = context.canvas.getLayer();\n  const substitute = shape.cloneNode();\n  setVisibility(substitute, 'hidden');\n  canvas.appendChild(substitute);\n  const bounds = substitute.getLocalBounds();\n  substitute.destroy();\n\n  return bounds;\n}\n"
  },
  {
    "path": "packages/g6/src/elements/nodes/circle.ts",
    "content": "import type { DisplayObjectConfig, CircleStyleProps as GCircleStyleProps, Group } from '@antv/g';\nimport { Circle as GCircle } from '@antv/g';\nimport { ICON_SIZE_RATIO } from '../../constants/element';\nimport type { Point } from '../../types';\nimport { getEllipseIntersectPoint } from '../../utils/point';\nimport { mergeOptions } from '../../utils/style';\nimport type { IconStyleProps } from '../shapes';\nimport type { BaseNodeStyleProps } from './base-node';\nimport { BaseNode } from './base-node';\n\n/**\n * <zh/> 圆形节点样式配置项\n *\n * <en/> Circle node style props\n */\nexport interface CircleStyleProps extends BaseNodeStyleProps {}\n\n/**\n * <zh/> 圆形节点\n *\n * <en/> Circle node\n */\nexport class Circle extends BaseNode {\n  static defaultStyleProps: Partial<CircleStyleProps> = {\n    size: 32,\n  };\n\n  constructor(options: DisplayObjectConfig<CircleStyleProps>) {\n    super(mergeOptions({ style: Circle.defaultStyleProps }, options));\n  }\n\n  protected drawKeyShape(attributes: Required<CircleStyleProps>, container: Group) {\n    return this.upsert('key', GCircle, this.getKeyStyle(attributes), container);\n  }\n\n  protected getKeyStyle(attributes: Required<CircleStyleProps>): GCircleStyleProps {\n    const keyStyle = super.getKeyStyle(attributes);\n    return { ...keyStyle, r: Math.min(...this.getSize(attributes)) / 2 };\n  }\n\n  protected getIconStyle(attributes: Required<CircleStyleProps>): false | IconStyleProps {\n    const style = super.getIconStyle(attributes);\n    const { r } = this.getShape<GCircle>('key').attributes;\n    const size = (r as number) * 2 * ICON_SIZE_RATIO;\n    return style ? ({ width: size, height: size, ...style } as IconStyleProps) : false;\n  }\n\n  public getIntersectPoint(point: Point, useExtendedLine = false): Point {\n    const keyShapeBounds = this.getShape('key').getBounds();\n    return getEllipseIntersectPoint(point, keyShapeBounds, useExtendedLine);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/nodes/diamond.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { Point } from '../../types';\nimport { getDiamondPoints } from '../../utils/element';\nimport type { PolygonStyleProps } from '../shapes';\nimport { Polygon } from '../shapes/polygon';\n\n/**\n * <zh/> 菱形节点样式配置项\n *\n * <en/> Diamond node style props\n */\nexport interface DiamondStyleProps extends PolygonStyleProps {}\n\n/**\n * <zh/> 菱形节点\n *\n * <en/> Diamond node\n */\nexport class Diamond extends Polygon {\n  constructor(options: DisplayObjectConfig<DiamondStyleProps>) {\n    super(options);\n  }\n\n  protected getPoints(attributes: Required<DiamondStyleProps>): Point[] {\n    const [width, height] = this.getSize(attributes);\n    return getDiamondPoints(width, height);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/nodes/donut.ts",
    "content": "import { Path } from '@antv/g';\nimport { isNumber, isString } from '@antv/util';\nimport { getPaletteColors } from '../../utils/palette';\nimport { subStyleProps } from '../../utils/prefix';\nimport { parseSize } from '../../utils/size';\nimport { mergeOptions } from '../../utils/style';\nimport { Circle } from './circle';\n\nimport type { BaseStyleProps, DisplayObjectConfig, Group } from '@antv/g';\nimport type { PathArray } from '@antv/util';\nimport type { CategoricalPalette } from '../../palettes/types';\nimport type { DonutRound, Prefix } from '../../types';\nimport type { CircleStyleProps } from './circle';\n\n/**\n * <zh/> 甜甜圈节点样式配置项\n *\n * <en/> Donut node style props\n */\nexport interface DonutStyleProps extends CircleStyleProps, Prefix<'donut', BaseStyleProps> {\n  /**\n   * <zh/> 内环半径，使用百分比或者像素值\n   *\n   * <en/> Inner ring radius, using percentage or pixel value.\n   * @defaultValue '50%'\n   */\n  innerR?: string | number;\n  /**\n   * <zh/> 圆环数据\n   *\n   * <en/> Donut data.\n   */\n  donuts?: number[] | DonutRound[];\n  /**\n   * <zh/> 颜色或者色板名\n   *\n   * <en/> Color or palette.\n   * @defaultValue 'tableau'\n   */\n  donutPalette?: string | CategoricalPalette;\n}\n\n/**\n * <zh/> 甜甜圈节点\n *\n * <en/> Donut node\n */\nexport class Donut extends Circle {\n  static defaultStyleProps: Partial<DonutStyleProps> = {\n    innerR: '50%',\n    donuts: [],\n    donutPalette: 'tableau',\n  };\n\n  constructor(options: DisplayObjectConfig<DonutStyleProps>) {\n    super(mergeOptions({ style: Donut.defaultStyleProps }, options));\n  }\n\n  private parseOuterR() {\n    const { size } = this.parsedAttributes as Required<DonutStyleProps>;\n    return Math.min(...parseSize(size)) / 2;\n  }\n\n  private parseInnerR() {\n    const { innerR } = this.parsedAttributes as Required<DonutStyleProps>;\n    return isString(innerR) ? (parseInt(innerR) / 100) * this.parseOuterR() : innerR;\n  }\n\n  protected drawDonutShape(attributes: Required<DonutStyleProps>, container: Group): void {\n    const { donuts } = attributes;\n    if (!donuts?.length) return;\n\n    const parsedDonuts = donuts.map((round) => (isNumber(round) ? { value: round } : round) as DonutRound);\n\n    const style = subStyleProps<BaseStyleProps>(this.getGraphicStyle(attributes), 'donut');\n\n    const colors = getPaletteColors(attributes.donutPalette);\n    if (!colors) return;\n\n    const sum = parsedDonuts.reduce((acc, cur) => acc + (cur.value ?? 0), 0);\n    const outerR = this.parseOuterR();\n    const innerR = this.parseInnerR();\n\n    let start = 0;\n    parsedDonuts.forEach((round, index) => {\n      const { value = 0, color = colors[index % colors.length], ...roundStyle } = round;\n      const angle = (sum === 0 ? 1 / parsedDonuts.length : value / sum) * 360;\n\n      this.upsert(\n        `round${index}`,\n        Path,\n        {\n          ...style,\n          d: arc(outerR, innerR, start, start + angle),\n          fill: color,\n          ...roundStyle,\n        },\n        container,\n      );\n\n      start += angle;\n    });\n  }\n\n  public render(attributes: Required<DonutStyleProps>, container: Group = this) {\n    super.render(attributes, container);\n    this.drawDonutShape(attributes, container);\n  }\n}\n\nconst point = (x: number, y: number, r: number, angel: number) => [x + Math.sin(angel) * r, y - Math.cos(angel) * r];\n\nconst full = (x: number, y: number, R: number, r: number): PathArray => {\n  if (r <= 0 || R <= r) {\n    return [['M', x - R, y], ['A', R, R, 0, 1, 1, x + R, y], ['A', R, R, 0, 1, 1, x - R, y], ['Z']];\n  }\n  return [\n    ['M', x - R, y],\n    ['A', R, R, 0, 1, 1, x + R, y],\n    ['A', R, R, 0, 1, 1, x - R, y],\n    ['Z'],\n    ['M', x + r, y],\n    ['A', r, r, 0, 1, 0, x - r, y],\n    ['A', r, r, 0, 1, 0, x + r, y],\n    ['Z'],\n  ];\n};\n\nconst part = (x: number, y: number, R: number, r: number, start: number, end: number): PathArray => {\n  const [s, e] = [(start / 360) * 2 * Math.PI, (end / 360) * 2 * Math.PI];\n  const P = [point(x, y, r, s), point(x, y, R, s), point(x, y, R, e), point(x, y, r, e)];\n  const flag = e - s > Math.PI ? 1 : 0;\n  return [\n    ['M', P[0][0], P[0][1]],\n    ['L', P[1][0], P[1][1]],\n    ['A', R, R, 0, flag, 1, P[2][0], P[2][1]],\n    ['L', P[3][0], P[3][1]],\n    ['A', r, r, 0, flag, 0, P[0][0], P[0][1]],\n    ['Z'],\n  ];\n};\n\nconst arc = (R = 0, r = 0, start: number, end: number): PathArray => {\n  const [x, y] = [0, 0];\n  if (Math.abs(start - end) % 360 < 0.000001) return full(x, y, R, r);\n  return part(x, y, R, r, start, end);\n};\n"
  },
  {
    "path": "packages/g6/src/elements/nodes/ellipse.ts",
    "content": "import type { DisplayObjectConfig, EllipseStyleProps as GEllipseStyleProps, Group } from '@antv/g';\nimport { Ellipse as GEllipse } from '@antv/g';\nimport { ICON_SIZE_RATIO } from '../../constants/element';\nimport type { Point } from '../../types';\nimport { getEllipseIntersectPoint } from '../../utils/point';\nimport { mergeOptions } from '../../utils/style';\nimport type { IconStyleProps } from '../shapes';\nimport type { BaseNodeStyleProps } from './base-node';\nimport { BaseNode } from './base-node';\n\n/**\n * <zh/> 椭圆节点样式配置项\n *\n * <en/> Ellipse node style props\n */\nexport interface EllipseStyleProps extends BaseNodeStyleProps {}\n\n/**\n * <zh/> 椭圆节点\n *\n * <en/> Ellipse node\n */\nexport class Ellipse extends BaseNode {\n  static defaultStyleProps: Partial<EllipseStyleProps> = {\n    size: [45, 35],\n  };\n\n  constructor(options: DisplayObjectConfig<EllipseStyleProps>) {\n    super(mergeOptions({ style: Ellipse.defaultStyleProps }, options));\n  }\n\n  protected drawKeyShape(attributes: Required<EllipseStyleProps>, container: Group) {\n    return this.upsert('key', GEllipse, this.getKeyStyle(attributes), container);\n  }\n\n  protected getKeyStyle(attributes: Required<EllipseStyleProps>): GEllipseStyleProps {\n    const keyStyle = super.getKeyStyle(attributes);\n    const [majorAxis, minorAxis] = this.getSize(attributes);\n    return {\n      ...keyStyle,\n      rx: majorAxis / 2,\n      ry: minorAxis / 2,\n    };\n  }\n\n  protected getIconStyle(attributes: Required<EllipseStyleProps>): false | IconStyleProps {\n    const style = super.getIconStyle(attributes);\n    const { rx, ry } = this.getShape<GEllipse>('key').attributes;\n    const size = Math.min(+rx, +ry) * 2 * ICON_SIZE_RATIO;\n\n    return style ? ({ width: size, height: size, ...style } as IconStyleProps) : false;\n  }\n\n  public getIntersectPoint(point: Point, useExtendedLine = false): Point {\n    const keyShapeBounds = this.getShape('key').getBounds();\n    return getEllipseIntersectPoint(point, keyShapeBounds, useExtendedLine);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/nodes/hexagon.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport { ICON_SIZE_RATIO } from '../../constants/element';\nimport type { Point } from '../../types';\nimport { getHexagonPoints } from '../../utils/element';\nimport type { IconStyleProps, PolygonStyleProps } from '../shapes';\nimport { Polygon } from '../shapes/polygon';\n\n/**\n * <zh/> 六边形节点样式配置项\n *\n * <en/> Hexagon node style props\n */\nexport interface HexagonStyleProps extends PolygonStyleProps {\n  /**\n   * <zh/> 外半径，默认为宽高的最小值的一半\n   *\n   * <en/> outer radius, default is half of the minimum value of width and height\n   */\n  outerR?: number;\n}\n\n/**\n * <zh/> 六边形节点\n *\n * <en/> Hexagon node\n */\nexport class Hexagon extends Polygon<HexagonStyleProps> {\n  constructor(options: DisplayObjectConfig<HexagonStyleProps>) {\n    super(options);\n  }\n\n  private getOuterR(attributes: Required<HexagonStyleProps>): number {\n    return attributes.outerR || Math.min(...this.getSize(attributes)) / 2;\n  }\n\n  protected getPoints(attributes: Required<HexagonStyleProps>): Point[] {\n    return getHexagonPoints(this.getOuterR(attributes));\n  }\n\n  protected getIconStyle(attributes: Required<HexagonStyleProps>): false | IconStyleProps {\n    const style = super.getIconStyle(attributes);\n    const size = this.getOuterR(attributes) * ICON_SIZE_RATIO;\n    return style ? ({ width: size, height: size, ...style } as IconStyleProps) : false;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/nodes/html.ts",
    "content": "import {\n  DisplayObjectConfig,\n  FederatedMouseEvent,\n  FederatedPointerEvent,\n  HTML as GHTML,\n  HTMLStyleProps as GHTMLStyleProps,\n  Group,\n  ICanvas,\n  IEventTarget,\n  Rect,\n} from '@antv/g';\nimport { Renderer } from '@antv/g-canvas';\nimport { isNil, isUndefined, pick, set } from '@antv/util';\nimport { CommonEvent } from '../../constants';\nimport type { BaseNodeStyleProps } from './base-node';\nimport { BaseNode } from './base-node';\n\n/**\n * <zh/> HTML 节点样式配置项\n *\n * <en/> HTML node style props\n */\nexport interface HTMLStyleProps extends BaseNodeStyleProps {\n  /**\n   * <zh/> HTML 内容，可以为字符串或者 `HTMLElement`\n   *\n   * <en/> HTML content, can be a string or `HTMLElement`\n   */\n  innerHTML: string | HTMLElement;\n  /**\n   * <zh/> 横行偏移量。HTML 容器默认以左上角为原点，通过 dx 来进行横向偏移\n   *\n   * <en/> Horizontal offset. The HTML container defaults to the upper left corner as the origin, and the horizontal offset is performed through dx\n   * @defaultValue 0\n   */\n  dx?: number;\n  /**\n   * <zh/> 纵向偏移量。HTML 容器默认以左上角为原点，通过 dy 来进行纵向偏移\n   *\n   * <en/> Vertical offset. The HTML container defaults to the upper left corner as the origin, and the vertical offset is performed through dy\n   * @defaultValue 0\n   */\n  dy?: number;\n}\n\n/**\n * <zh/> HTML 节点\n *\n * <en/> HTML node\n * @see https://github.com/antvis/G/blob/next/packages/g/src/plugins/EventPlugin.ts\n */\nexport class HTML extends BaseNode<HTMLStyleProps> {\n  static defaultStyleProps: Partial<HTMLStyleProps> = {\n    size: [160, 80],\n    halo: false,\n    icon: false,\n    label: false,\n    pointerEvents: 'auto',\n  };\n\n  constructor(options: DisplayObjectConfig<HTMLStyleProps>) {\n    super({ ...options, style: Object.assign({}, HTML.defaultStyleProps, options.style) });\n  }\n\n  private rootPointerEvent = new FederatedPointerEvent(null);\n\n  private get eventService() {\n    return this.context.canvas.context.eventService;\n  }\n\n  private get events() {\n    return [\n      CommonEvent.CLICK,\n      CommonEvent.POINTER_DOWN,\n      CommonEvent.POINTER_MOVE,\n      CommonEvent.POINTER_UP,\n      CommonEvent.POINTER_OVER,\n      CommonEvent.POINTER_LEAVE,\n    ];\n  }\n\n  protected getDomElement() {\n    return this.getShape<GHTML>('key').getDomElement();\n  }\n\n  /**\n   * @override\n   */\n  public render(attributes: Required<HTMLStyleProps> = this.parsedAttributes, container: Group = this): void {\n    this.drawKeyShape(attributes, container);\n\n    this.drawPortShapes(attributes, container);\n  }\n\n  protected getKeyStyle(attributes: Required<HTMLStyleProps>): GHTMLStyleProps {\n    const {\n      dx = 0,\n      dy = 0,\n      ...style\n    } = pick(attributes, ['dx', 'dy', 'innerHTML', 'pointerEvents', 'cursor']) as unknown as HTMLStyleProps;\n    const [width, height] = this.getSize(attributes);\n    return { x: dx, y: dy, ...style, width, height };\n  }\n\n  protected drawKeyShape(attributes: Required<HTMLStyleProps>, container: Group) {\n    const style = this.getKeyStyle(attributes);\n    const { x, y, width = 0, height = 0 } = style;\n    const bounds = this.upsert('key-container', Rect, { x, y, width, height, opacity: 0 }, container)!;\n    return this.upsert('key', GHTML, style, bounds);\n  }\n\n  public connectedCallback() {\n    // only enable in canvas renderer\n    const renderer = this.context.canvas.getRenderer('main');\n    const isCanvasRenderer = renderer instanceof Renderer;\n    if (!isCanvasRenderer) return;\n\n    const element = this.getDomElement();\n    this.events.forEach((eventName) => {\n      // @ts-expect-error assert event is PointerEvent\n      element.addEventListener(eventName, this.forwardEvents);\n    });\n  }\n\n  public attributeChangedCallback(name: any, oldValue: any, newValue: any) {\n    if (name === 'zIndex' && oldValue !== newValue) {\n      this.getDomElement().style.zIndex = newValue;\n    }\n  }\n\n  public destroy() {\n    const element = this.getDomElement();\n    this.events.forEach((eventName) => {\n      // @ts-expect-error assert event is PointerEvent\n      element.removeEventListener(eventName, this.forwardEvents);\n    });\n    super.destroy();\n  }\n\n  private forwardEvents = (nativeEvent: PointerEvent) => {\n    const canvas = this.context.canvas;\n    const iCanvas = canvas.context.renderingContext.root!.ownerDocument!.defaultView!;\n\n    const normalizedEvents = this.normalizeToPointerEvent(nativeEvent, iCanvas);\n\n    normalizedEvents.forEach((normalizedEvent) => {\n      const event = this.bootstrapEvent(this.rootPointerEvent, normalizedEvent, iCanvas, nativeEvent);\n      // 当点击到 html 元素时，避免触发 pointerupoutside 事件\n      // When clicking on the html element, avoid triggering the pointerupoutside event\n      set(canvas.context.eventService, 'mappingTable.pointerupoutside', []);\n      canvas.context.eventService.mapEvent(event);\n    });\n  };\n\n  private normalizeToPointerEvent(event: PointerEvent, canvas: ICanvas): PointerEvent[] {\n    const normalizedEvents = [];\n    if (canvas.isTouchEvent(event)) {\n      for (let i = 0; i < event.changedTouches.length; i++) {\n        const touch = event.changedTouches[i] as any;\n\n        // use changedTouches instead of touches since touchend has no touches\n        // @see https://stackoverflow.com/a/10079076\n        if (isUndefined(touch.button)) touch.button = 0;\n        if (isUndefined(touch.buttons)) touch.buttons = 1;\n        if (isUndefined(touch.isPrimary)) {\n          touch.isPrimary = event.touches.length === 1 && event.type === 'touchstart';\n        }\n        if (isUndefined(touch.width)) touch.width = touch.radiusX || 1;\n        if (isUndefined(touch.height)) touch.height = touch.radiusY || 1;\n        if (isUndefined(touch.tiltX)) touch.tiltX = 0;\n        if (isUndefined(touch.tiltY)) touch.tiltY = 0;\n        if (isUndefined(touch.pointerType)) touch.pointerType = 'touch';\n        // @see https://developer.mozilla.org/zh-CN/docs/Web/API/Touch/identifier\n        if (isUndefined(touch.pointerId)) touch.pointerId = touch.identifier || 0;\n        if (isUndefined(touch.pressure)) touch.pressure = touch.force || 0.5;\n        if (isUndefined(touch.twist)) touch.twist = 0;\n        if (isUndefined(touch.tangentialPressure)) touch.tangentialPressure = 0;\n        touch.isNormalized = true;\n        touch.type = event.type;\n\n        normalizedEvents.push(touch);\n      }\n    } else if (canvas.isMouseEvent(event)) {\n      const tempEvent = event as any;\n      if (isUndefined(tempEvent.isPrimary)) tempEvent.isPrimary = true;\n      if (isUndefined(tempEvent.width)) tempEvent.width = 1;\n      if (isUndefined(tempEvent.height)) tempEvent.height = 1;\n      if (isUndefined(tempEvent.tiltX)) tempEvent.tiltX = 0;\n      if (isUndefined(tempEvent.tiltY)) tempEvent.tiltY = 0;\n      if (isUndefined(tempEvent.pointerType)) tempEvent.pointerType = 'mouse';\n      if (isUndefined(tempEvent.pointerId)) tempEvent.pointerId = 1;\n      if (isUndefined(tempEvent.pressure)) tempEvent.pressure = 0.5;\n      if (isUndefined(tempEvent.twist)) tempEvent.twist = 0;\n      if (isUndefined(tempEvent.tangentialPressure)) tempEvent.tangentialPressure = 0;\n      tempEvent.isNormalized = true;\n\n      normalizedEvents.push(tempEvent);\n    } else {\n      normalizedEvents.push(event);\n    }\n\n    return normalizedEvents as PointerEvent[];\n  }\n\n  private transferMouseData(event: FederatedMouseEvent, nativeEvent: MouseEvent): void {\n    event.isTrusted = nativeEvent.isTrusted;\n    event.srcElement = nativeEvent.srcElement as IEventTarget;\n    event.timeStamp = performance.now();\n    event.type = nativeEvent.type;\n\n    event.altKey = nativeEvent.altKey;\n    event.metaKey = nativeEvent.metaKey;\n    event.shiftKey = nativeEvent.shiftKey;\n    event.ctrlKey = nativeEvent.ctrlKey;\n    event.button = nativeEvent.button;\n    event.buttons = nativeEvent.buttons;\n    event.client.x = nativeEvent.clientX;\n    event.client.y = nativeEvent.clientY;\n    event.movement.x = nativeEvent.movementX;\n    event.movement.y = nativeEvent.movementY;\n    event.page.x = nativeEvent.pageX;\n    event.page.y = nativeEvent.pageY;\n    event.screen.x = nativeEvent.screenX;\n    event.screen.y = nativeEvent.screenY;\n    event.relatedTarget = null;\n  }\n\n  private bootstrapEvent(\n    event: FederatedPointerEvent,\n    normalizedEvent: PointerEvent,\n    view: ICanvas,\n    nativeEvent: PointerEvent | MouseEvent | TouchEvent,\n  ): FederatedPointerEvent {\n    event.view = view;\n    // @ts-ignore\n    event.originalEvent = null;\n    event.nativeEvent = nativeEvent;\n\n    event.pointerId = normalizedEvent.pointerId;\n    event.width = normalizedEvent.width;\n    event.height = normalizedEvent.height;\n    event.isPrimary = normalizedEvent.isPrimary;\n    event.pointerType = normalizedEvent.pointerType;\n    event.pressure = normalizedEvent.pressure;\n    event.tangentialPressure = normalizedEvent.tangentialPressure;\n    event.tiltX = normalizedEvent.tiltX;\n    event.tiltY = normalizedEvent.tiltY;\n    event.twist = normalizedEvent.twist;\n    this.transferMouseData(event, normalizedEvent);\n\n    const { x, y } = this.getViewportXY(normalizedEvent);\n    event.viewport.x = x;\n    event.viewport.y = y;\n    const [canvasX, canvasY] = this.context.canvas.getCanvasByViewport([x, y]);\n    event.canvas.x = canvasX;\n    event.canvas.y = canvasY;\n    event.global.copyFrom(event.canvas);\n    event.offset.copyFrom(event.canvas);\n\n    event.isTrusted = nativeEvent.isTrusted;\n    if (event.type === 'pointerleave') {\n      event.type = 'pointerout';\n    }\n    return event;\n  }\n\n  private getViewportXY(nativeEvent: PointerEvent | WheelEvent) {\n    let x: number;\n    let y: number;\n    const { offsetX, offsetY, clientX, clientY } = nativeEvent;\n    if (!isNil(offsetX) && !isNil(offsetY)) {\n      x = offsetX;\n      y = offsetY;\n    } else {\n      const point = this.eventService.client2Viewport({ x: clientX, y: clientY });\n      x = point.x;\n      y = point.y;\n    }\n    return { x, y };\n  }\n\n  protected onframe(): void {\n    super.onframe();\n    // sync opacity\n    const { opacity } = this.attributes;\n    this.getDomElement().style.opacity = `${opacity}`;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/nodes/image.ts",
    "content": "import type { DisplayObjectConfig, RectStyleProps as GRectStyleProps, Group } from '@antv/g';\nimport { ImageStyleProps as GImageStyleProps, Rect as GRect } from '@antv/g';\nimport { ICON_SIZE_RATIO } from '../../constants/element';\nimport { subStyleProps } from '../../utils/prefix';\nimport { mergeOptions } from '../../utils/style';\nimport { add, toVector2 } from '../../utils/vector';\nimport type { IconStyleProps } from '../shapes';\nimport { Image as ImageShape } from '../shapes';\nimport { connectImage, dispatchPositionChange } from '../shapes/image';\nimport type { BaseNodeStyleProps } from './base-node';\nimport { BaseNode } from './base-node';\n\n/**\n * <zh/> 图片节点样式配置项\n *\n * <en/> Image node style props\n */\nexport interface ImageStyleProps extends BaseNodeStyleProps {\n  /**\n   * <zh/> 图片来源，即图片地址字符串\n   *\n   * <en/> Image source, i.e. image address string\n   */\n  img?: string | HTMLImageElement;\n  /**\n   * <zh/> 该属性为 img 的别名\n   *\n   * <en/> This property is an alias for img\n   */\n  src?: string | HTMLImageElement;\n}\n\n/**\n * <zh/> 图片节点\n *\n * <en/> Image node\n */\nexport class Image extends BaseNode<ImageStyleProps> {\n  static defaultStyleProps: Partial<ImageStyleProps> = {\n    size: 32,\n  };\n\n  constructor(options: DisplayObjectConfig<ImageStyleProps>) {\n    super(mergeOptions({ style: Image.defaultStyleProps }, options));\n  }\n\n  protected getKeyStyle(attributes: Required<ImageStyleProps>): GImageStyleProps {\n    const [width, height] = this.getSize(attributes);\n    const { fillOpacity, opacity = fillOpacity, ...keyStyle } = super.getKeyStyle(attributes);\n\n    return {\n      opacity,\n      ...keyStyle,\n      width,\n      height,\n      x: -width / 2,\n      y: -height / 2,\n    };\n  }\n\n  public getBounds() {\n    return this.getShape('key').getBounds();\n  }\n\n  protected getHaloStyle(attributes: Required<ImageStyleProps>): false | GRectStyleProps {\n    if (attributes.halo === false) return false;\n    const { fill: keyStyleFill, stroke: keyStyleStroke, ...keyStyle } = this.getShape<GRect>('key').attributes;\n    const haloStyle = subStyleProps(this.getGraphicStyle(attributes), 'halo');\n    const haloLineWidth = Number(haloStyle.lineWidth);\n    const [width, height] = add(toVector2(this.getSize(attributes)), [haloLineWidth, haloLineWidth]);\n    const { lineWidth } = haloStyle;\n    const recalculate = {\n      fill: 'transparent',\n      lineWidth: lineWidth / 2,\n      width: width - lineWidth / 2,\n      height: height - lineWidth / 2,\n      x: -(width - lineWidth / 2) / 2,\n      y: -(height - lineWidth / 2) / 2,\n    };\n    return { ...haloStyle, ...recalculate };\n  }\n\n  protected getIconStyle(attributes: Required<ImageStyleProps>): false | IconStyleProps {\n    const style = super.getIconStyle(attributes);\n    const [width, height] = this.getSize(attributes);\n\n    return style\n      ? ({\n          width: width * ICON_SIZE_RATIO,\n          height: height * ICON_SIZE_RATIO,\n          ...style,\n        } as IconStyleProps)\n      : false;\n  }\n\n  protected drawKeyShape(attributes: Required<ImageStyleProps>, container: Group): ImageShape | undefined {\n    const image = this.upsert('key', ImageShape, this.getKeyStyle(attributes), container);\n    connectImage(this);\n    return image;\n  }\n\n  protected drawHaloShape(attributes: Required<ImageStyleProps>, container: Group): void {\n    this.upsert('halo', GRect, this.getHaloStyle(attributes), container);\n  }\n\n  public update(attr?: Partial<ImageStyleProps>): void {\n    super.update(attr);\n    if (attr && ('x' in attr || 'y' in attr || 'z' in attr)) {\n      dispatchPositionChange(this);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/nodes/index.ts",
    "content": "export { BaseNode } from './base-node';\nexport { Circle } from './circle';\nexport { Diamond } from './diamond';\nexport { Donut } from './donut';\nexport { Ellipse } from './ellipse';\nexport { Hexagon } from './hexagon';\nexport { HTML } from './html';\nexport { Image } from './image';\nexport { Rect } from './rect';\nexport { Star } from './star';\nexport { Triangle } from './triangle';\n\nexport type { BaseNodeStyleProps } from './base-node';\nexport type { CircleStyleProps } from './circle';\nexport type { DiamondStyleProps } from './diamond';\nexport type { DonutStyleProps } from './donut';\nexport type { EllipseStyleProps } from './ellipse';\nexport type { HexagonStyleProps } from './hexagon';\nexport type { HTMLStyleProps } from './html';\nexport type { ImageStyleProps } from './image';\nexport type { RectStyleProps } from './rect';\nexport type { StarStyleProps } from './star';\nexport type { TriangleStyleProps } from './triangle';\n"
  },
  {
    "path": "packages/g6/src/elements/nodes/rect.ts",
    "content": "import type { DisplayObjectConfig, RectStyleProps as GRectStyleProps, Group } from '@antv/g';\nimport { Rect as GRect } from '@antv/g';\nimport { ICON_SIZE_RATIO } from '../../constants/element';\nimport type { IconStyleProps } from '../shapes';\nimport type { BaseNodeStyleProps } from './base-node';\nimport { BaseNode } from './base-node';\n\n/**\n * <zh/> 矩形节点样式配置项\n *\n * <en/> Rect node style props\n */\nexport interface RectStyleProps extends BaseNodeStyleProps {}\ntype ParsedRectStyleProps = Required<RectStyleProps>;\n\n/**\n * <zh/> 矩形节点\n *\n * <en/> Rect node\n */\nexport class Rect extends BaseNode<RectStyleProps> {\n  constructor(options: DisplayObjectConfig<RectStyleProps>) {\n    super(options);\n  }\n\n  protected getKeyStyle(attributes: ParsedRectStyleProps): GRectStyleProps {\n    const [width, height] = this.getSize(attributes);\n    return {\n      ...super.getKeyStyle(attributes),\n      width,\n      height,\n      x: -width / 2,\n      y: -height / 2,\n    };\n  }\n\n  protected getIconStyle(attributes: ParsedRectStyleProps): false | IconStyleProps {\n    const style = super.getIconStyle(attributes);\n    const { width, height } = this.getShape<GRect>('key').attributes;\n\n    return style\n      ? ({\n          width: (width as number) * ICON_SIZE_RATIO,\n          height: (height as number) * ICON_SIZE_RATIO,\n          ...style,\n        } as IconStyleProps)\n      : false;\n  }\n\n  protected drawKeyShape(attributes: ParsedRectStyleProps, container: Group): GRect | undefined {\n    return this.upsert('key', GRect, this.getKeyStyle(attributes), container);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/nodes/star.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport { ICON_SIZE_RATIO } from '../../constants/element';\nimport type { NodePortStyleProps, Point, StarPortPlacement } from '../../types';\nimport { getPortXYByPlacement, getStarPoints, getStarPorts } from '../../utils/element';\nimport type { IconStyleProps, PolygonStyleProps } from '../shapes';\nimport { Polygon } from '../shapes/polygon';\n\n/**\n * <zh/> 五角星节点样式配置项\n *\n * <en/> Star node style props\n */\nexport interface StarStyleProps extends PolygonStyleProps {\n  /**\n   * <zh/> 内半径，默认为外半径的 3/8\n   *\n   * <en/> Inner radius, default is 3/8 of the outer radius\n   */\n  innerR?: number;\n}\n\n/**\n * <zh/> 五角星节点\n *\n * <en/> Star node\n */\nexport class Star extends Polygon<StarStyleProps> {\n  constructor(options: DisplayObjectConfig<StarStyleProps>) {\n    super(options);\n  }\n\n  private getInnerR(attributes: Required<StarStyleProps>): number {\n    return attributes.innerR || (this.getOuterR(attributes) * 3) / 8;\n  }\n\n  private getOuterR(attributes: Required<StarStyleProps>): number {\n    return Math.min(...this.getSize(attributes)) / 2;\n  }\n\n  protected getPoints(attributes: Required<StarStyleProps>): Point[] {\n    return getStarPoints(this.getOuterR(attributes), this.getInnerR(attributes));\n  }\n\n  protected getIconStyle(attributes: Required<StarStyleProps>): false | IconStyleProps {\n    const style = super.getIconStyle(attributes);\n    const size = this.getInnerR(attributes) * 2 * ICON_SIZE_RATIO;\n    return style ? ({ width: size, height: size, ...style } as IconStyleProps) : false;\n  }\n\n  protected getPortXY(attributes: Required<StarStyleProps>, style: NodePortStyleProps): Point {\n    const { placement = 'top' } = style;\n    const bbox = this.getShape('key').getLocalBounds();\n    const ports = getStarPorts(this.getOuterR(attributes), this.getInnerR(attributes));\n    return getPortXYByPlacement(bbox, placement as StarPortPlacement, ports, false);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/nodes/triangle.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport { isEmpty } from '@antv/util';\nimport { ICON_SIZE_RATIO } from '../../constants/element';\nimport type { NodePortStyleProps, Point, TriangleDirection, TrianglePortPlacement } from '../../types';\nimport { getIncircleRadius, getTriangleCenter } from '../../utils/bbox';\nimport { getPortXYByPlacement, getTrianglePoints, getTrianglePorts } from '../../utils/element';\nimport { subStyleProps } from '../../utils/prefix';\nimport { mergeOptions } from '../../utils/style';\nimport type { PolygonStyleProps } from '../shapes';\nimport { IconStyleProps } from '../shapes';\nimport { Polygon } from '../shapes/polygon';\n\n/**\n * <zh/> 三角形节点样式配置项\n *\n * <en/> Triangle node style props\n */\nexport interface TriangleStyleProps extends PolygonStyleProps {\n  /**\n   * <zh/> 三角形的方向\n   *\n   * <en/> The direction of the triangle\n   * @defaultValue 'up'\n   */\n  direction?: TriangleDirection;\n}\n\n/**\n * <zh/> 三角形节点\n *\n * <en/> Triangle node\n */\nexport class Triangle extends Polygon<TriangleStyleProps> {\n  static defaultStyleProps: Partial<TriangleStyleProps> = {\n    size: 40,\n    direction: 'up',\n  };\n\n  constructor(options: DisplayObjectConfig<TriangleStyleProps>) {\n    super(mergeOptions({ style: Triangle.defaultStyleProps }, options));\n  }\n\n  protected getPoints(attributes: Required<TriangleStyleProps>): Point[] {\n    const { direction } = attributes;\n    const [width, height] = this.getSize(attributes);\n    return getTrianglePoints(width, height, direction);\n  }\n\n  protected getPortXY(attributes: Required<TriangleStyleProps>, style: NodePortStyleProps): Point {\n    const { direction } = attributes;\n    const { placement = 'top' } = style;\n    const bbox = this.getShape('key').getLocalBounds();\n    const [width, height] = this.getSize(attributes);\n    const ports = getTrianglePorts(width, height, direction);\n    return getPortXYByPlacement(bbox, placement as TrianglePortPlacement, ports, false);\n  }\n\n  // icon 处于内切三角形的重心\n  // icon is at the centroid of the triangle\n  protected getIconStyle(attributes: Required<TriangleStyleProps>): false | IconStyleProps {\n    const { icon, iconText, iconSrc, direction } = attributes;\n\n    if (icon === false || isEmpty(iconText || iconSrc)) return false;\n\n    const iconStyle = subStyleProps<IconStyleProps>(this.getGraphicStyle(attributes), 'icon');\n    const bbox = this.getShape('key').getLocalBounds();\n    const [x, y] = getTriangleCenter(bbox, direction);\n    const size = getIncircleRadius(bbox, direction) * 2 * ICON_SIZE_RATIO;\n\n    return {\n      x,\n      y,\n      width: size,\n      height: size,\n      ...iconStyle,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/shapes/badge.ts",
    "content": "import type { DisplayObjectConfig, Group } from '@antv/g';\nimport { mergeOptions } from '../../utils/style';\nimport { BaseShape } from './base-shape';\nimport type { LabelStyleProps } from './label';\nimport { Label } from './label';\n\n/**\n * <zh/> 徽标样式\n *\n * <en/> Badge style\n */\nexport interface BadgeStyleProps extends LabelStyleProps {}\n\n/**\n * <zh/> 徽标\n *\n * <en/> Badge\n * @remarks\n * <zh/> 徽标是一种特殊的标签，通常用于展示数量或状态信息。\n *\n * <en/> Badge is a special label, usually used to display quantity or status information.\n */\nexport class Badge extends BaseShape<BadgeStyleProps> {\n  static defaultStyleProps: Partial<BadgeStyleProps> = {\n    padding: [2, 4, 2, 4],\n    fontSize: 10,\n    wordWrap: false,\n    backgroundRadius: '50%',\n    backgroundOpacity: 1,\n  };\n\n  constructor(options: DisplayObjectConfig<BadgeStyleProps>) {\n    super(mergeOptions({ style: Badge.defaultStyleProps }, options));\n  }\n\n  protected getBadgeStyle(attributes: Required<BadgeStyleProps>) {\n    return this.getGraphicStyle(attributes);\n  }\n\n  public render(attributes: Required<BadgeStyleProps> = this.parsedAttributes, container: Group = this) {\n    this.upsert('label', Label, this.getBadgeStyle(attributes), container);\n  }\n\n  public getGeometryBounds() {\n    const labelShape = this.getShape<BaseShape<LabelStyleProps>>('label');\n    const shape = labelShape.getShape('background') || labelShape.getShape('text');\n    return shape.getGeometryBounds();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/shapes/base-shape.ts",
    "content": "import type { BaseStyleProps, DisplayObject, DisplayObjectConfig, Group, IAnimation } from '@antv/g';\nimport { CustomElement } from '@antv/g';\nimport { isEmpty, isFunction, upperFirst } from '@antv/util';\nimport { ExtensionCategory } from '../../constants';\nimport type { Keyframe } from '../../types';\nimport { createAnimationsProxy, preprocessKeyframes } from '../../utils/animation';\nimport { setAttributes, updateStyle } from '../../utils/element';\nimport { subObject } from '../../utils/prefix';\nimport { format } from '../../utils/print';\nimport { getSubShapeStyle } from '../../utils/style';\nimport { replaceTranslateInTransform } from '../../utils/transform';\nimport { setVisibility } from '../../utils/visibility';\nimport { getExtension } from './../../registry/get';\n\nexport interface BaseShapeStyleProps extends BaseStyleProps {}\n\n/**\n * <zh/> 图形基类\n *\n * <en/> Base class for shapes\n */\nexport abstract class BaseShape<StyleProps extends BaseShapeStyleProps> extends CustomElement<StyleProps> {\n  constructor(options: DisplayObjectConfig<StyleProps>) {\n    applyTransform(options.style);\n    super(options);\n    this.render(this.attributes as Required<StyleProps>, this);\n    this.setVisibility();\n    this.bindEvents();\n  }\n\n  /**\n   * <zh/> 解析后的属性\n   *\n   * <en/> parsed attributes\n   * @returns <zh/> 解析后的属性 | <en/> parsed attributes\n   * @internal\n   */\n  protected get parsedAttributes() {\n    return this.attributes as Required<StyleProps>;\n  }\n\n  /**\n   * <zh/> 图形实例映射表\n   *\n   * <en/> shape instance map\n   * @internal\n   */\n  protected shapeMap: Record<string, DisplayObject> = {};\n\n  /**\n   * <zh/> 动画实例映射表\n   *\n   * <en/> animation instance map\n   * @internal\n   */\n  protected animateMap: Record<string, IAnimation> = {};\n\n  /**\n   * <zh/> 创建、更新或删除图形\n   *\n   * <en/> create, update or remove shape\n   * @param className - <zh/> 图形名称 | <en/> shape name\n   * @param Ctor - <zh/> 图形类型 | <en/> shape type\n   * @param style - <zh/> 图形样式。若要删除图形，传入 false | <en/> shape style. Pass false to remove the shape\n   * @param container - <zh/> 容器 | <en/> container\n   * @param hooks - <zh/> 钩子函数 | <en/> hooks\n   * @returns <zh/> 图形实例 | <en/> shape instance\n   */\n  protected upsert<T extends DisplayObject>(\n    className: string,\n    Ctor: string | { new (...args: any[]): T },\n    style: T['attributes'] | false,\n    container: DisplayObject,\n    hooks?: UpsertHooks,\n  ): T | undefined {\n    const target = this.shapeMap[className] as T | undefined;\n    // remove\n    // 如果 style 为 false，则删除图形 / remove shape if style is false\n    if (style === false) {\n      if (target) {\n        hooks?.beforeDestroy?.(target);\n        container.removeChild(target);\n        delete this.shapeMap[className];\n        hooks?.afterDestroy?.(target);\n      }\n      return;\n    }\n\n    const _Ctor = typeof Ctor === 'string' ? getExtension(ExtensionCategory.SHAPE, Ctor) : Ctor;\n\n    if (!_Ctor) {\n      throw new Error(format(`Shape ${Ctor} not found`));\n    }\n\n    // create\n    if (!target || target.destroyed || !(target instanceof _Ctor)) {\n      if (target) {\n        hooks?.beforeDestroy?.(target);\n        target?.destroy();\n        hooks?.afterDestroy?.(target);\n      }\n\n      hooks?.beforeCreate?.();\n      const instance = new _Ctor({ className, style });\n      container.appendChild(instance);\n      this.shapeMap[className] = instance;\n      hooks?.afterCreate?.(instance);\n      return instance as T;\n    }\n\n    // update\n    hooks?.beforeUpdate?.(target);\n    updateStyle(target, style);\n    hooks?.afterUpdate?.(target);\n\n    return target;\n  }\n\n  public update(attr: Partial<StyleProps> = {}): void {\n    const attributes = Object.assign({}, this.attributes, attr) as Required<StyleProps>;\n    applyTransform(attributes);\n    setAttributes(this, attributes);\n    this.render(attributes, this);\n    this.setVisibility();\n  }\n\n  /**\n   * <zh/> 在初始化时会被自动调用\n   *\n   * <en/> will be called automatically when initializing\n   * @param attributes\n   * @param container\n   */\n  public abstract render(attributes: Required<StyleProps>, container: Group): void;\n\n  public bindEvents() {}\n\n  /**\n   * <zh/> 从给定的属性对象中提取图形样式属性。删除特定的属性，如位置、变换和类名\n   *\n   * <en/> Extracts the shape styles from a given attribute object.\n   * Removes specific styles like position, transformation, and class name.\n   * @param style - <zh/> 属性对象 | <en/> attribute object\n   * @returns <zh/> 仅包含样式属性的对象 | <en/> An object containing only the style properties.\n   */\n  public getGraphicStyle<T extends Record<string, any>>(\n    style: T,\n  ): Omit<T, 'x' | 'y' | 'z' | 'transform' | 'transformOrigin' | 'className' | 'class' | 'zIndex' | 'visibility'> {\n    return getSubShapeStyle(style);\n  }\n\n  /**\n   * Get the prefix pairs for composite shapes used to handle animation\n   * @returns tuples array where each tuple contains a key corresponding to a method `get${key}Style` and its shape prefix\n   * @internal\n   */\n  protected get compositeShapes(): [string, string][] {\n    return [\n      ['badges', 'badge-'],\n      ['ports', 'port-'],\n    ];\n  }\n\n  public animate(keyframes: Keyframe[], options?: number | KeyframeAnimationOptions): IAnimation | null {\n    if (keyframes.length === 0) return null;\n    const animationMap: IAnimation[] = [];\n\n    // 如果 keyframes 中存在 x/y/z ，替换为 transform\n    // if x/y/z exists in keyframes, replace them with transform\n    if (keyframes[0].x !== undefined || keyframes[0].y !== undefined || keyframes[0].z !== undefined) {\n      const { x: _x = 0, y: _y = 0, z: _z = 0 } = this.attributes as Record<string, any>;\n      keyframes.forEach((keyframe) => {\n        const { x = _x, y = _y, z = _z } = keyframe;\n        Object.assign(keyframe, { transform: z ? [['translate3d', x, y, z]] : [['translate', x, y]] });\n      });\n    }\n\n    const result = super.animate(keyframes, options);\n    if (result) {\n      releaseAnimation(this, result);\n      animationMap.push(result);\n    }\n\n    if (Array.isArray(keyframes) && keyframes.length > 0) {\n      // 如果 keyframes 中仅存在 skippedAttrs 中的属性，则仅更新父元素属性（跳过子图形）\n      // if only skippedAttrs exist in keyframes, only update parent element attributes (skip child shapes)\n      const skippedAttrs = ['transform', 'transformOrigin', 'x', 'y', 'z', 'zIndex'];\n      if (Object.keys(keyframes[0]).some((attr) => !skippedAttrs.includes(attr))) {\n        Object.entries(this.shapeMap).forEach(([key, shape]) => {\n          // 如果存在方法名为 `get${key}Style` 的方法，则使用该方法获取样式，并自动为该图形实例创建动画\n          // if there is a method named `get${key}Style`, use this method to get style and automatically create animation for the shape instance\n          const methodName = `get${upperFirst(key)}Style` as keyof this;\n          const method = this[methodName];\n\n          if (isFunction(method)) {\n            const subKeyframes: Keyframe[] = keyframes.map((style) =>\n              method.call(this, { ...this.attributes, ...style }),\n            );\n            const result = shape.animate(preprocessKeyframes(subKeyframes), options);\n            if (result) {\n              releaseAnimation(shape, result);\n              animationMap.push(result);\n            }\n          }\n        });\n\n        const handleCompositeShapeAnimation = (shapeSet: Record<string, DisplayObject>, name: string) => {\n          if (!isEmpty(shapeSet)) {\n            const methodName = `get${upperFirst(name)}Style` as keyof this;\n            const method = this[methodName];\n            if (isFunction(method)) {\n              const itemsKeyframes = keyframes.map((style) => method.call(this, { ...this.attributes, ...style }));\n              Object.entries(itemsKeyframes[0]).map(([key]) => {\n                const subKeyframes = itemsKeyframes.map((styles) => styles[key]);\n                const shape = shapeSet[key];\n                if (shape) {\n                  const result = shape.animate(preprocessKeyframes(subKeyframes), options);\n                  if (result) {\n                    releaseAnimation(shape, result);\n                    animationMap.push(result);\n                  }\n                }\n              });\n            }\n          }\n        };\n\n        this.compositeShapes.forEach(([key, prefix]) => {\n          const shapeSet = subObject(this.shapeMap, prefix);\n          handleCompositeShapeAnimation(shapeSet, key);\n        });\n      }\n    }\n\n    return createAnimationsProxy(animationMap);\n  }\n\n  public getShape<T extends DisplayObject>(name: string): T {\n    return this.shapeMap[name] as T;\n  }\n\n  private setVisibility() {\n    const { visibility } = this.attributes;\n    setVisibility(this, visibility);\n  }\n\n  public destroy(): void {\n    this.shapeMap = {};\n    this.animateMap = {};\n    super.destroy();\n  }\n}\n\n/**\n * <zh/> 释放动画\n *\n * <en/> Release animation\n * @param target - <zh/> 目标对象 | <en/> target object\n * @param animation - <zh/> 动画实例 | <en/> animation instance\n * @description see: https://github.com/antvis/G/issues/1731\n */\nfunction releaseAnimation(target: DisplayObject, animation: IAnimation) {\n  animation?.finished.then(() => {\n    // @ts-expect-error private property\n    const index = target.activeAnimations.findIndex((_) => _ === animation);\n    // @ts-expect-error private property\n    if (index > -1) target.activeAnimations.splice(index, 1);\n  });\n}\n\n/**\n * <zh/> 图形 upsert 方法生命周期钩子\n *\n * <en/> Shape upsert method lifecycle hooks\n */\nexport interface UpsertHooks {\n  /**\n   * <zh/> 图形创建前\n   *\n   * <en/> Before creating the shape\n   */\n  beforeCreate?: () => void;\n  /**\n   * <zh/> 图形创建后\n   *\n   * <en/> After creating the shape\n   * @param instance - <zh/> 图形实例 | <en/> shape instance\n   */\n  afterCreate?: (instance: DisplayObject) => void;\n  /**\n   * <zh/> 图形更新前\n   *\n   * <en/> Before updating the shape\n   * @param instance - <zh/> 图形实例 | <en/> shape instance\n   */\n  beforeUpdate?: (instance: DisplayObject) => void;\n  /**\n   * <zh/> 图形更新后\n   *\n   * <en/> After updating the shape\n   * @param instance - <zh/> 图形实例 | <en/> shape instance\n   */\n  afterUpdate?: (instance: DisplayObject) => void;\n  /**\n   * <zh/> 图形销毁前\n   *\n   * <en/> Before destroying the shape\n   * @param instance - <zh/> 图形实例 | <en/> shape instance\n   */\n  beforeDestroy?: (instance: DisplayObject) => void;\n  /**\n   * <zh/> 图形销毁后\n   *\n   * <en/> After destroying the shape\n   * @param instance - <zh/> 图形实例 | <en/> shape instance\n   */\n  afterDestroy?: (instance: DisplayObject) => void;\n}\n\n/**\n * <zh/> 应用 transform\n *\n * <en/> Apply transform\n * @param style - <zh/> 样式 | <en/> style\n * @returns <zh/> 样式 | <en/> style\n */\nfunction applyTransform(style?: BaseShapeStyleProps) {\n  if (!style) return {};\n  if ('x' in style || 'y' in style || 'z' in style) {\n    const { x = 0, y = 0, z, transform } = style as any;\n    const newTransform = replaceTranslateInTransform(x, y, z, transform);\n    if (newTransform) style.transform = newTransform;\n  }\n  return style;\n}\n"
  },
  {
    "path": "packages/g6/src/elements/shapes/contour.ts",
    "content": "import type { DisplayObjectConfig, Group, PathStyleProps } from '@antv/g';\nimport { Path } from '@antv/g';\nimport type { CardinalPlacement, Prefix } from '../../types';\nimport { getPolygonTextStyleByPlacement } from '../../utils/polygon';\nimport { subStyleProps } from '../../utils/prefix';\nimport { mergeOptions } from '../../utils/style';\nimport { getWordWrapWidthByBox } from '../../utils/text';\nimport type { LabelStyleProps } from '../shapes';\nimport { BaseShape } from './base-shape';\nimport { Label } from './label';\n\nexport interface ContourLabelStyleProps extends LabelStyleProps {\n  /**\n   * <zh/> 标签位置\n   *\n   * <en/> Label position\n   * @defaultValue 'bottom'\n   */\n  placement?: CardinalPlacement | 'center';\n  /**\n   * <zh/> 标签是否贴合轮廓\n   *\n   * <en/> Whether the label is close to the contour\n   * @defaultValue true\n   */\n  closeToPath?: boolean;\n  /**\n   * <zh/> 标签是否跟随轮廓旋转，仅在 closeToPath 为 true 时生效\n   *\n   * <en/> Whether the label rotates with the contour. Only effective when closeToPath is true\n   * @defaultValue true\n   */\n  autoRotate?: boolean;\n  /**\n   * <zh/> x 轴偏移量\n   *\n   * <en/> Label x-axis offset\n   * @defaultValue 0\n   */\n  offsetX?: number;\n  /**\n   * <zh/> y 轴偏移量\n   *\n   * <en/> Label y-axis offset\n   * @defaultValue 0\n   */\n  offsetY?: number;\n  /**\n   * <zh/> 文本的最大宽度，超出会自动省略\n   *\n   * <en/> The maximum width of the text, which will be automatically ellipsis if exceeded\n   */\n  maxWidth?: number;\n}\n\nexport interface ContourStyleProps extends PathStyleProps, Prefix<'label', ContourLabelStyleProps> {\n  /**\n   * <zh/> 是否显示标签\n   *\n   * <en/> Whether to display the label\n   * @defaultValue true\n   */\n  label?: boolean;\n}\ntype ParsedContourStyleProps = Required<ContourStyleProps>;\ntype ContourOptions = DisplayObjectConfig<ContourStyleProps>;\n\nexport class Contour extends BaseShape<ContourStyleProps> {\n  static defaultStyleProps: Partial<ContourStyleProps> = {\n    label: true,\n    labelPlacement: 'bottom',\n    labelCloseToPath: true,\n    labelAutoRotate: true,\n    labelOffsetX: 0,\n    labelOffsetY: 0,\n  };\n\n  constructor(options: ContourOptions) {\n    super(mergeOptions({ style: Contour.defaultStyleProps }, options));\n  }\n\n  protected getLabelStyle(attributes: ParsedContourStyleProps): LabelStyleProps | false {\n    if (!attributes.label || !attributes.d || attributes.d.length === 0) return false;\n    const { maxWidth, offsetX, offsetY, autoRotate, placement, closeToPath, ...labelStyle } = subStyleProps<\n      Required<ContourLabelStyleProps>\n    >(this.getGraphicStyle(attributes), 'label');\n\n    const key = this.shapeMap.key;\n    const keyBounds = key?.getRenderBounds();\n\n    return Object.assign(\n      getPolygonTextStyleByPlacement(keyBounds, placement, offsetX, offsetY, closeToPath, attributes.d, autoRotate),\n      { wordWrapWidth: getWordWrapWidthByBox(keyBounds, maxWidth) },\n      labelStyle,\n    );\n  }\n\n  protected getKeyStyle(attributes: ParsedContourStyleProps): PathStyleProps {\n    return this.getGraphicStyle(attributes);\n  }\n\n  public render(attributes: ParsedContourStyleProps, container: Group): void {\n    this.upsert('key', Path, this.getKeyStyle(attributes), container);\n    this.upsert('label', Label, this.getLabelStyle(attributes), container);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/shapes/icon.ts",
    "content": "import { DisplayObjectConfig, Text as GText, Group, TextStyleProps } from '@antv/g';\nimport type { BaseShapeStyleProps } from './base-shape';\nimport { BaseShape } from './base-shape';\nimport type { ImageStyleProps } from './image';\nimport { Image as GImage } from './image';\n\n/**\n * <zh/> 图标样式\n *\n * <en/> Icon style\n */\nexport interface IconStyleProps extends BaseShapeStyleProps, Partial<TextStyleProps>, Omit<ImageStyleProps, 'z'> {}\n\n/**\n * <zh/> 图标\n *\n * <en/> Icon\n * @remarks\n * <zh/> 图标是一种特殊的图形，可以是图片或者文字。传入 src 属性时，会渲染图片；传入 text 属性时，会渲染文字。\n *\n * <en/> Icon is a special shape, which can be an image or text. When the src attribute is passed in, an image will be rendered; when the text attribute is passed in, text will be rendered.\n */\nexport class Icon extends BaseShape<IconStyleProps> {\n  constructor(options: DisplayObjectConfig<IconStyleProps>) {\n    super(options);\n  }\n\n  private isImage() {\n    const { src } = this.attributes;\n    return !!src;\n  }\n\n  protected getIconStyle(attributes: IconStyleProps = this.attributes): IconStyleProps {\n    const { width = 0, height = 0 } = attributes;\n    const style = this.getGraphicStyle(attributes);\n    if (this.isImage()) {\n      return {\n        x: -width / 2,\n        y: -height / 2,\n        ...style,\n      };\n    }\n    return {\n      textBaseline: 'middle',\n      textAlign: 'center',\n      ...style,\n    };\n  }\n\n  public render(attributes = this.attributes, container: Group = this): void {\n    this.upsert('icon', (this.isImage() ? GImage : GText) as any, this.getIconStyle(attributes), container);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/shapes/image.ts",
    "content": "import type { DisplayObject, DisplayObjectConfig, ImageStyleProps as GImageStyleProps } from '@antv/g';\nimport { ElementEvent, Image as GImage, Rect as GRect } from '@antv/g';\nimport { getAncestorShapes } from '../../utils/shape';\n\nexport interface ImageStyleProps extends GImageStyleProps {\n  /**\n   * <zh/> 圆角半径\n   *\n   * <en/> Radius of the rounded corner\n   */\n  radius?: number | number[];\n}\n\nexport class Image extends GImage {\n  constructor(options: DisplayObjectConfig<ImageStyleProps>) {\n    super(options);\n    current = this;\n    this.isMutationObserved = true;\n    this.addEventListener(ElementEvent.MOUNTED, this.onMounted);\n    this.addEventListener(ElementEvent.ATTR_MODIFIED, this.onAttrModified);\n  }\n\n  private onMounted = () => {\n    this.handleRadius();\n  };\n\n  private onAttrModified = () => {\n    this.handleRadius();\n  };\n\n  public handleRadius() {\n    const { radius, clipPath, width = 0, height = 0 } = this.attributes as ImageStyleProps;\n\n    if (radius && width && height) {\n      const [x, y] = this.getBounds().min;\n      const clipPathStyle = { x, y, radius, width, height };\n\n      if (clipPath) {\n        Object.assign(this.parsedStyle.clipPath!.style, clipPathStyle);\n      } else {\n        const rect = new GRect({ style: clipPathStyle });\n        this.style.clipPath = rect;\n      }\n    } else {\n      if (clipPath) this.style.clipPath = null;\n    }\n  }\n}\n\nconst ImagesWeakMap = new WeakMap<DisplayObject, Image[]>();\n\nlet current: Image | null = null;\n\n/**\n * <zh/> 由于 g clipPath 不支持相对位置，因此当作用的元素发生位置变化时，需要通知 Image 更新 clipPath。\n *\n * 通过 connectImage 创建图形与图片的关联，并结合 dispatchPositionChange 方法触发更新\n *\n * ⚠️ 这是一种临时的、黑盒的解决方案，如果后续 g 支持相对位置，会移除该方法。\n *\n * <en/> Since g clipPath does not support relative positions, when the position of the affected element changes, the Image needs to be notified to update the clipPath.\n *\n * Use connectImage to create an association between the shape and the image, and combine it with the dispatchPositionChange method to trigger the update.\n *\n * ⚠️ This is a temporary, black-box solution, and if g supports relative positions in the future, this method will be removed.\n * @param target - <zh/> 目标元素 <en/> Target element\n */\nexport const connectImage = (target: DisplayObject) => {\n  if (current && getAncestorShapes(current).includes(target)) {\n    const images = ImagesWeakMap.get(target);\n    if (images) {\n      if (!images.includes(current)) images.push(current);\n    } else ImagesWeakMap.set(target, [current]);\n  }\n};\n\n/**\n * <zh/> 触发关联的图片更新位置\n *\n * <en/> Trigger the associated image to update its position\n * @param target - <zh/> 目标元素 <en/> Target element\n */\nexport const dispatchPositionChange = (target: DisplayObject) => {\n  const image = ImagesWeakMap.get(target);\n  if (image) {\n    image.forEach((i) => i.handleRadius());\n  }\n};\n"
  },
  {
    "path": "packages/g6/src/elements/shapes/index.ts",
    "content": "/**\n * <zh/> 图形组件，用于构建复合元素\n *\n * <en/> Shape components, used to build composite elements\n */\n\nexport { Badge } from './badge';\nexport { BaseShape } from './base-shape';\nexport { Contour } from './contour';\nexport { Icon } from './icon';\nexport { Image } from './image';\nexport { Label } from './label';\n\nexport type { BadgeStyleProps } from './badge';\nexport type { BaseShapeStyleProps } from './base-shape';\nexport type { ContourStyleProps } from './contour';\nexport type { IconStyleProps } from './icon';\nexport type { ImageStyleProps } from './image';\nexport type { LabelStyleProps } from './label';\nexport type { PolygonStyleProps } from './polygon';\n"
  },
  {
    "path": "packages/g6/src/elements/shapes/label.ts",
    "content": "import { DisplayObjectConfig, Group, Rect, RectStyleProps, Text, TextStyleProps } from '@antv/g';\nimport type { Padding } from '../../types/padding';\nimport type { Prefix } from '../../types/prefix';\nimport { parsePadding } from '../../utils/padding';\nimport { omitStyleProps, startsWith, subStyleProps } from '../../utils/prefix';\nimport { mergeOptions } from '../../utils/style';\nimport { BaseShape } from './base-shape';\n\n/**\n * <zh/> 标签样式\n *\n * <en/> Label style\n */\nexport interface LabelStyleProps extends TextStyleProps, Prefix<'background', RectStyleProps> {\n  /**\n   * <zh/> 是否显示背景\n   *\n   * <en/> Whether to show background\n   */\n  background?: boolean;\n  /**\n   * <zh/> 标签内边距\n   *\n   * <en/> Label padding\n   * @defaultValue 0\n   */\n  padding?: Padding;\n}\n\n/**\n * <zh/> 标签\n *\n * <en/> Label\n * @remarks\n * <zh/> 标签是一种具有背景的文本图形。\n *\n * <en/> Label is a text shape with background.\n */\nexport class Label extends BaseShape<LabelStyleProps> {\n  static defaultStyleProps: Partial<LabelStyleProps> = {\n    padding: 0,\n    fontSize: 12,\n    fontFamily: 'system-ui, sans-serif',\n    wordWrap: true,\n    maxLines: 1,\n    wordWrapWidth: 128,\n    textOverflow: '...',\n    textBaseline: 'middle',\n    backgroundOpacity: 0.75,\n    backgroundZIndex: -1,\n    backgroundLineWidth: 0,\n  };\n\n  constructor(options: DisplayObjectConfig<LabelStyleProps>) {\n    super(mergeOptions({ style: Label.defaultStyleProps }, options));\n  }\n\n  protected isTextStyle(key: string) {\n    return startsWith(key, 'label');\n  }\n\n  protected isBackgroundStyle(key: string) {\n    return startsWith(key, 'background');\n  }\n\n  protected getTextStyle(attributes: Required<LabelStyleProps>) {\n    const { padding, ...style } = this.getGraphicStyle(attributes);\n    return omitStyleProps<TextStyleProps>(style, 'background');\n  }\n\n  protected getBackgroundStyle(attributes: Required<LabelStyleProps>) {\n    if (attributes.background === false) return false;\n\n    const style = this.getGraphicStyle(attributes);\n    const { wordWrap, wordWrapWidth, padding } = style;\n    const backgroundStyle = subStyleProps<RectStyleProps>(style, 'background');\n\n    const {\n      min: [minX, minY],\n      center: [centerX, centerY],\n      halfExtents: [halfWidth, halfHeight],\n    } = this.shapeMap.text.getGeometryBounds();\n\n    const [top, right, bottom, left] = parsePadding(padding);\n    const totalWidth = halfWidth * 2 + left + right;\n\n    const { width, height } = backgroundStyle;\n    if (width && height) {\n      Object.assign(backgroundStyle, { x: centerX - Number(width) / 2, y: centerY - Number(height) / 2 });\n    } else {\n      Object.assign(backgroundStyle, {\n        x: minX - left,\n        y: minY - top,\n        width: wordWrap ? Math.min(totalWidth, wordWrapWidth + left + right) : totalWidth,\n        height: halfHeight * 2 + top + bottom,\n      });\n    }\n\n    // parse percentage radius\n    const { radius } = backgroundStyle;\n    // if radius look like '10%', convert it to number\n    if (typeof radius === 'string' && radius.endsWith('%')) {\n      const percentage = Number(radius.replace('%', '')) / 100;\n      backgroundStyle.radius = Math.min(+backgroundStyle.width, +backgroundStyle.height) * percentage;\n    }\n\n    return backgroundStyle;\n  }\n\n  public render(attributes: Required<LabelStyleProps> = this.parsedAttributes, container: Group = this): void {\n    this.upsert('text', Text, this.getTextStyle(attributes), container);\n    this.upsert('background', Rect, this.getBackgroundStyle(attributes), container);\n  }\n\n  public getGeometryBounds() {\n    const shape = this.getShape('background') || this.getShape('text');\n    return shape.getGeometryBounds();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/elements/shapes/polygon.ts",
    "content": "import type { DisplayObjectConfig, PolygonStyleProps as GPolygonStyleProps, Group } from '@antv/g';\nimport { Polygon as GPolygon } from '@antv/g';\nimport type { Point } from '../../types';\nimport { getPolygonIntersectPoint } from '../../utils/point';\nimport type { BaseNodeStyleProps } from '../nodes/base-node';\nimport { BaseNode } from '../nodes/base-node';\n\n/**\n * <zh/> 多边形节点样式配置项\n *\n * <en/> Polygon node style props\n */\nexport interface PolygonStyleProps extends BaseNodeStyleProps {\n  /**\n   * <zh/> 多边形的顶点坐标\n   *\n   * <en/> The vertex coordinates of the polygon\n   * @internal\n   */\n  points?: ([number, number] | [number, number, number])[];\n}\n\n/**\n * Abstract class for polygon nodes,i.e triangle, diamond, hexagon, etc.\n */\nexport abstract class Polygon<T extends PolygonStyleProps = PolygonStyleProps> extends BaseNode<T> {\n  constructor(options: DisplayObjectConfig<T>) {\n    super(options);\n  }\n\n  public get parsedAttributes() {\n    return this.attributes as unknown as Required<T>;\n  }\n\n  protected drawKeyShape(attributes: Required<T>, container: Group) {\n    return this.upsert('key', GPolygon, this.getKeyStyle(attributes), container);\n  }\n\n  protected getKeyStyle(attributes: Required<T>): GPolygonStyleProps {\n    const keyStyle = super.getKeyStyle(attributes);\n    return { ...keyStyle, points: this.getPoints(attributes) };\n  }\n\n  protected abstract getPoints(attributes: Required<T>): Point[];\n\n  public getIntersectPoint(point: Point, useExtendedLine = false): Point {\n    const { points } = this.getShape<GPolygon>('key').attributes;\n    const center: Point = [+(this.attributes?.x || 0), +(this.attributes?.y || 0)];\n    return getPolygonIntersectPoint(point, center, points!, true, useExtendedLine).point;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/exports.ts",
    "content": "export {\n  AutoAdaptLabel,\n  BaseBehavior,\n  BrushSelect,\n  ClickSelect,\n  CollapseExpand,\n  CreateEdge,\n  DragCanvas,\n  DragElement,\n  DragElementForce,\n  FixElementSize,\n  FocusElement,\n  HoverActivate,\n  LassoSelect,\n  OptimizeViewportTransform,\n  ScrollCanvas,\n  ZoomCanvas,\n} from './behaviors';\nexport {\n  CanvasEvent,\n  ComboEvent,\n  CommonEvent,\n  ContainerEvent,\n  EdgeEvent,\n  ExtensionCategory,\n  GraphEvent,\n  HistoryEvent,\n  NodeEvent,\n} from './constants';\nexport { BaseCombo, CircleCombo, RectCombo } from './elements/combos';\nexport {\n  BaseEdge,\n  Cubic,\n  CubicHorizontal,\n  CubicRadial,\n  CubicVertical,\n  Line,\n  Polyline,\n  Quadratic,\n} from './elements/edges';\nexport { effect } from './elements/effect';\nexport {\n  BaseNode,\n  Circle,\n  Diamond,\n  Donut,\n  Ellipse,\n  HTML,\n  Hexagon,\n  Image,\n  Rect,\n  Star,\n  Triangle,\n} from './elements/nodes';\nexport { Badge, BaseShape, Icon, Label } from './elements/shapes';\nexport {\n  AntVDagreLayout,\n  BaseLayout,\n  CircularLayout,\n  ComboCombinedLayout,\n  compactBox as CompactBoxLayout,\n  ConcentricLayout,\n  D3ForceLayout,\n  DagreLayout,\n  dendrogram as DendrogramLayout,\n  FishboneLayout,\n  ForceAtlas2Layout,\n  ForceLayout,\n  FruchtermanLayout,\n  GridLayout,\n  indented as IndentedLayout,\n  MDSLayout,\n  mindmap as MindmapLayout,\n  RadialLayout,\n  RandomLayout,\n  SnakeLayout,\n} from './layouts';\nexport {\n  Background,\n  BasePlugin,\n  BubbleSets,\n  CameraSetting,\n  Contextmenu,\n  EdgeBundling,\n  EdgeFilterLens,\n  Fisheye,\n  Fullscreen,\n  GridLine,\n  History,\n  Hull,\n  Legend,\n  Minimap,\n  Snapline,\n  Timebar,\n  Title,\n  Toolbar,\n  Tooltip,\n  Watermark,\n} from './plugins';\nexport { getExtension, getExtensions } from './registry/get';\nexport { register } from './registry/register';\nexport { Canvas } from './runtime/canvas';\nexport { Graph } from './runtime/graph';\nexport { BaseTransform, MapNodeSize, PlaceRadialLabels, ProcessParallelEdges } from './transforms';\nexport { isCollapsed } from './utils/collapsibility';\nexport { idOf } from './utils/id';\nexport { invokeLayoutMethod } from './utils/layout';\nexport { positionOf } from './utils/position';\nexport { omitStyleProps, subStyleProps } from './utils/prefix';\nexport { Shortcut } from './utils/shortcut';\nexport { parseSize } from './utils/size';\nexport { treeToGraphData } from './utils/tree';\nexport { setVisibility } from './utils/visibility';\n\nexport type { BaseStyleProps } from '@antv/g';\nexport type {\n  AntVDagreLayoutOptions,\n  CircularLayoutOptions,\n  ComboCombinedLayoutOptions,\n  ConcentricLayoutOptions,\n  D3Force3DLayoutOptions,\n  D3ForceLayoutOptions,\n  DagreLayoutOptions,\n  ForceAtlas2LayoutOptions,\n  ForceLayoutOptions,\n  FruchtermanLayoutOptions,\n  GridLayoutOptions,\n  MDSLayoutOptions,\n  RadialLayoutOptions,\n  RandomLayoutOptions,\n} from '@antv/layout';\nexport type { PathArray } from '@antv/util';\nexport type { AnimationContext, AnimationEffectTiming, AnimationExecutor, AnimationOptions } from './animations/types';\nexport type {\n  AutoAdaptLabelOptions,\n  BaseBehaviorOptions,\n  BrushSelectOptions,\n  ClickSelectOptions,\n  CollapseExpandOptions,\n  CreateEdgeOptions,\n  DragCanvasOptions,\n  DragElementForceOptions,\n  DragElementOptions,\n  FixElementSizeOptions,\n  FocusElementOptions,\n  HoverActivateOptions,\n  LassoSelectOptions,\n  OptimizeViewportTransformOptions,\n  ScrollCanvasOptions,\n  ZoomCanvasOptions,\n} from './behaviors';\nexport type { FixShapeConfig } from './behaviors/fix-element-size';\nexport type { BaseComboStyleProps, CircleComboStyleProps, RectComboStyleProps } from './elements/combos';\nexport type {\n  BaseEdgeStyleProps,\n  CubicHorizontalStyleProps,\n  CubicRadialStyleProps,\n  CubicStyleProps,\n  CubicVerticalStyleProps,\n  LineStyleProps,\n  PolylineStyleProps,\n  QuadraticStyleProps,\n} from './elements/edges';\nexport type {\n  BaseNodeStyleProps,\n  CircleStyleProps,\n  DiamondStyleProps,\n  DonutStyleProps,\n  EllipseStyleProps,\n  HTMLStyleProps,\n  HexagonStyleProps,\n  ImageStyleProps,\n  RectStyleProps,\n  StarStyleProps,\n  TriangleStyleProps,\n} from './elements/nodes';\nexport type {\n  BadgeStyleProps,\n  BaseShapeStyleProps,\n  IconStyleProps,\n  LabelStyleProps,\n  PolygonStyleProps,\n} from './elements/shapes';\nexport type { UpsertHooks } from './elements/shapes/base-shape';\nexport type { ContourLabelStyleProps, ContourStyleProps } from './elements/shapes/contour';\nexport type { FishboneLayoutOptions, SnakeLayoutOptions } from './layouts';\nexport type { BaseLayoutOptions, WebWorkerLayoutOptions } from './layouts/types';\nexport type { CategoricalPalette } from './palettes/types';\nexport type {\n  BackgroundOptions,\n  BasePluginOptions,\n  BubbleSetsOptions,\n  CameraSettingOptions,\n  ContextmenuOptions,\n  EdgeBundlingOptions,\n  EdgeFilterLensOptions,\n  FisheyeOptions,\n  FullscreenOptions,\n  GridLineOptions,\n  HistoryOptions,\n  HullOptions,\n  LegendOptions,\n  MinimapOptions,\n  SnaplineOptions,\n  TimebarOptions,\n  ToolbarOptions,\n  TooltipOptions,\n  WatermarkOptions,\n} from './plugins';\nexport type { CanvasConfig, DataURLOptions } from './runtime/canvas';\nexport type { CollapseExpandNodeOptions } from './runtime/element';\nexport type { RuntimeContext } from './runtime/types';\nexport type {\n  BehaviorOptions,\n  CanvasOptions,\n  ComboData,\n  ComboOptions,\n  EdgeData,\n  EdgeOptions,\n  GraphData,\n  GraphOptions,\n  NodeData,\n  NodeOptions,\n  PluginOptions,\n  ThemeOptions,\n  ViewportOptions,\n} from './spec';\nexport type { CustomBehaviorOption } from './spec/behavior';\nexport type { AnimationStage } from './spec/element/animation';\nexport type { LayoutOptions, STDLayoutOptions, SingleLayoutOptions } from './spec/layout';\nexport type { CustomPluginOption } from './spec/plugin';\nexport type {\n  BaseTransformOptions,\n  MapNodeSizeOptions,\n  PlaceRadialLabelsOptions,\n  ProcessParallelEdgesOptions,\n} from './transforms';\nexport type { DrawData } from './transforms/types';\nexport type {\n  CardinalPlacement,\n  CollapsedMarkerStyleProps,\n  Combo,\n  CornerPlacement,\n  DirectionalPlacement,\n  Edge,\n  EdgeArrowStyleProps,\n  EdgeDirection,\n  EdgeLabelStyleProps,\n  Element,\n  ElementDatum,\n  ElementHooks,\n  ElementMethods,\n  ElementType,\n  FitViewOptions,\n  HierarchyKey,\n  IAnimateEvent,\n  ID,\n  IDragEvent,\n  IElementDragEvent,\n  IElementEvent,\n  IElementLifeCycleEvent,\n  IEvent,\n  IGraphLifeCycleEvent,\n  IKeyboardEvent,\n  IPointerEvent,\n  IViewportEvent,\n  IWheelEvent,\n  LoopPlacement,\n  LoopStyleProps,\n  Node,\n  NodeBadgeStyleProps,\n  NodeCentralityOptions,\n  NodeLabelStyleProps,\n  NodeLikeData,\n  NodePortStyleProps,\n  Padding,\n  Placement,\n  Point,\n  PortStyleProps,\n  Prefix,\n  PrefixKey,\n  RelativePlacement,\n  Size,\n  State,\n  TransformOptions,\n  TreeData,\n  TriangleDirection,\n  Vector2,\n  Vector3,\n  ViewportAnimationEffectTiming,\n} from './types';\nexport type { Command, CommandData } from './types/history';\nexport type { ShortcutKey } from './utils/shortcut';\n"
  },
  {
    "path": "packages/g6/src/global.d.ts",
    "content": "import '@antv/g';\nimport type { RuntimeContext } from './runtime/types';\n\ndeclare module '@antv/g' {\n  interface BaseStyleProps {\n    /**\n     * <zh/> 图形所在的图层，默认为 'main'。\n     *\n     * <en/> The layer where the shape is located, default is 'main'.\n     */\n    $layer?: string;\n  }\n\n  interface DisplayObjectConfig {\n    context?: RuntimeContext;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/index.ts",
    "content": "import './preset';\n\nexport * from './exports';\n\nexport { version } from './version';\nexport const iconfont = {\n  css: '//at.alicdn.com/t/a/font_470089_8hnbbf8n4u8.css',\n  js: '//at.alicdn.com/t/a/font_470089_8hnbbf8n4u8.js',\n};\n"
  },
  {
    "path": "packages/g6/src/layouts/base-layout.ts",
    "content": "import type { RuntimeContext } from '../runtime/types';\nimport type { GraphData } from '../spec';\nimport type { BaseLayoutOptions } from './types';\n\n/**\n * <zh/> 布局的基类\n *\n * <en/> Base class for layout\n */\nexport abstract class BaseLayout<O extends BaseLayoutOptions = any> {\n  public abstract id: string;\n\n  public options: O;\n\n  protected context: RuntimeContext;\n\n  constructor(context: RuntimeContext, options?: O) {\n    this.context = context;\n    this.options = options || ({} as O);\n  }\n\n  public stop?: () => void;\n\n  public tick?: (iterations?: number) => GraphData;\n\n  public abstract execute(model: GraphData, options?: O): Promise<GraphData>;\n}\n"
  },
  {
    "path": "packages/g6/src/layouts/fishbone.ts",
    "content": "import { isEmpty, memoize } from '@antv/util';\nimport type { BaseLayoutOptions } from '../layouts/types';\nimport type { EdgeData, GraphData, NodeData } from '../spec';\nimport type { ElementDatum, ID, Point, Size, STDSize } from '../types';\nimport { idOf } from '../utils/id';\nimport { parseSize } from '../utils/size';\nimport { BaseLayout } from './base-layout';\n\nexport interface FishboneLayoutOptions extends BaseLayoutOptions {\n  /**\n   * <zh/> 节点大小\n   *\n   * <en/> Node size\n   */\n  nodeSize?: Size | ((node: NodeData) => Size);\n  /**\n   * <zh/> 排布方向\n   * - `'RL'` 从右到左，鱼头在右\n   * - `'LR'` 从左到右，鱼头在左\n   *\n   * <en/> Layout direction\n   * - `'RL'` From right to left, the fish head is on the right\n   * - `'LR'` From left to right, the fish head is on the left\n   * @defaultValue `'LR'`\n   */\n  direction?: 'RL' | 'LR';\n  /**\n   * <zh/> 获取水平间距\n   *\n   * <en/> Get horizontal spacing\n   */\n  hGap?: number;\n  /**\n   * <zh/> 获取垂直间距\n   *\n   * <en/> Get vertical spacing\n   */\n  vGap?: number;\n  /**\n   * <zh/> 获取鱼骨间距\n   *\n   * <en/> Get rib separation\n   * @defaultValue () => 60\n   */\n  getRibSep?: (node: NodeData) => number;\n  /**\n   * <zh/> 布局宽度\n   *\n   * <en/> Layout width\n   */\n  width?: number;\n  /**\n   * <zh/> 布局高度\n   *\n   * <en/> Layout height\n   */\n  height?: number;\n}\n\ntype NodeResult = { id: ID; x: number; y: number };\ntype EdgeResult = { id: ID; controlPoints: Point[]; relatedNodeId: ID };\ntype LayoutResult = { nodes: NodeResult[]; edges: EdgeResult[] };\n\n/**\n * <zh/> 鱼骨图布局\n *\n * <en/> Fishbone layout\n * @remarks\n * <zh/> [鱼骨图布局](https://en.wikipedia.org/wiki/Ishikawa_diagram)是一种专门用于表示层次结构数据的图形布局方式。它通过模拟鱼骨的形状，将数据节点按照层次结构排列，使得数据的层次关系更加清晰直观。鱼骨图布局特别适用于需要展示因果关系、层次结构或分类信息的数据集。\n *\n * <en/> [Fishbone layout](https://en.wikipedia.org/wiki/Ishikawa_diagram) is a graphical layout method specifically designed to represent hierarchical data. By simulating the shape of a fishbone, it arranges data nodes according to their hierarchical structure, making the hierarchical relationships of the data clearer and more intuitive. The fishbone diagram layout is particularly suitable for datasets that need to display causal relationships, hierarchical structures, or classification information.\n */\nexport class FishboneLayout extends BaseLayout {\n  id = 'fishbone';\n\n  static defaultOptions: Partial<FishboneLayoutOptions> = {\n    direction: 'RL',\n    getRibSep: () => 60,\n  };\n\n  private getRoot() {\n    const roots = this.context.model.getRootsData();\n    if (isEmpty(roots) || roots.length > 2) return;\n\n    return roots[0];\n  }\n\n  private formatSize(nodeSize: Size | ((node: NodeData) => Size)): (node: NodeData) => STDSize {\n    const nodeSizeFunc = typeof nodeSize === 'function' ? nodeSize : () => nodeSize;\n    return (node: NodeData) => parseSize(nodeSizeFunc(node));\n  }\n\n  private doLayout(root: NodeData, options: Required<FishboneLayoutOptions>): LayoutResult {\n    const { hGap, getRibSep, vGap, nodeSize, height } = options;\n\n    const { model } = this.context;\n\n    const getSize = this.formatSize(nodeSize);\n    let ribX = getSize(root)[0] + getRibSep(root);\n\n    const getHorizontalOffset = (node: NodeData, result = 0): number => {\n      result += hGap * ((node.children || []).length + 1);\n\n      node.children?.forEach((childId) => {\n        const child = model.getNodeLikeDatum(childId) as NodeData;\n        child.children?.forEach((grandChildId) => {\n          const grandChild = model.getNodeLikeDatum(grandChildId) as NodeData;\n          result = getHorizontalOffset(grandChild, result);\n        });\n      });\n\n      return result;\n    };\n\n    const getAuxiliaryPoint = (node: NodeData): number => {\n      if (node.depth === 1) return ribX;\n\n      const parent = model.getParentData(node.id, 'tree') as NodeData;\n\n      if (isAtEvenDepth(node)) {\n        const ancestor = model.getParentData(parent.id, 'tree') as NodeData;\n        const deltaY = calculateY(node) - calculateY(ancestor);\n        return getAuxiliaryPoint(parent) + (deltaY * hGap) / vGap;\n      } else {\n        const nodeIndex = (parent.children || []).indexOf(node.id);\n        const followingSiblingsIncludeSelf = model.getNodeData((parent.children || []).slice(nodeIndex));\n        return (\n          calculateX(parent) -\n          followingSiblingsIncludeSelf.reduce((acc, sibling) => acc + getHorizontalOffset(sibling), 0) -\n          getSize(parent)[0] / 2\n        );\n      }\n    };\n\n    const calculateX = memoize(\n      (node: NodeData): number => {\n        if (isRoot(node)) return getSize(node)[0] / 2;\n\n        const parent = model.getParentData(node.id, 'tree') as NodeData;\n\n        if (isAtEvenDepth(node)) {\n          return getAuxiliaryPoint(node) + getHorizontalOffset(node) + getSize(node)[0] / 2;\n        } else {\n          const deltaY = calculateY(node) - calculateY(parent);\n          const ratio = hGap / vGap;\n          return getAuxiliaryPoint(node) + deltaY * ratio;\n        }\n      },\n      (node) => node.id,\n    );\n\n    const getParentY = (nodeId: ID): number => calculateY(model.getParentData(nodeId, 'tree')!);\n\n    const calculateY = memoize(\n      (node: NodeData): number => {\n        if (isRoot(node)) return height / 2;\n\n        if (!isAtEvenDepth(node)) {\n          // If the node has no children, calculate Y based on the parent\n          if (isEmpty(node.children)) return getParentY(node.id) + vGap;\n\n          // If the last child has no children, calculate Y based on the last child\n          const lastChild = model.getNodeLikeDatum(node.children!.slice(-1)[0]);\n          if (isEmpty(lastChild.children)) return calculateY(lastChild) + vGap;\n\n          // If the last child has children, calculate Y based on the last descendant of the last child\n          const lastDescendant = model.getDescendantsData(node.id).slice(-1)[0];\n          return (isAtEvenDepth(lastDescendant) ? getParentY(lastDescendant.id) : calculateY(lastDescendant)) + vGap;\n        } else {\n          // depth > 0 && isAtEvenDepth(node)\n          const parent = model.getParentData(node.id, 'tree') as NodeData;\n          const nodeIndex = parent.children!.indexOf(node.id);\n          // If the node is the first sibling, return Y based on parent\n          if (nodeIndex === 0) return getParentY(parent.id) + vGap;\n\n          // If the previous sibling has no children, calculate Y based on the previous sibling\n          const prevSibling = model.getNodeLikeDatum(parent.children![nodeIndex - 1]);\n          if (isEmpty(prevSibling.children)) return calculateY(prevSibling) + vGap;\n\n          // If the previous sibling has children, calculate Y based on the last descendant of the previous sibling\n          const descendants = model.getDescendantsData(prevSibling.id);\n          return (\n            Math.max(\n              ...descendants.map((descendant) =>\n                isAtEvenDepth(descendant) ? getParentY(descendant.id) : calculateY(descendant),\n              ),\n            ) + vGap\n          );\n        }\n      },\n      (node) => node.id,\n    );\n\n    let tmpRibX = 0;\n    const result: LayoutResult = { nodes: [], edges: [] };\n\n    const layout = (node: NodeData) => {\n      node.children?.forEach((childId) => layout(model.getNodeLikeDatum(childId)));\n\n      const y = calculateY(node);\n      const x = calculateX(node);\n      result.nodes.push({ id: node.id, x, y });\n\n      if (isRoot(node)) return;\n\n      const edge = model.getRelatedEdgesData(node.id, 'in')[0];\n      const controlPoint = [getAuxiliaryPoint(node), isAtEvenDepth(node) ? y : getParentY(node.id)] as Point;\n      result.edges.push({ id: idOf(edge), controlPoints: [controlPoint], relatedNodeId: node.id });\n\n      tmpRibX = Math.max(tmpRibX, x + getRibSep(node));\n      if (node.depth === 1) ribX = tmpRibX;\n    };\n\n    layout(root);\n\n    return result;\n  }\n\n  private placeAlterative(result: LayoutResult, root: NodeData) {\n    const oddIndexedRibs = (root.children || []).filter((_, index) => index % 2 !== 0);\n    if (oddIndexedRibs.length === 0) return result;\n\n    const { model } = this.context;\n    const rootY = result.nodes.find((node) => node.id === root.id)!.y;\n\n    const shouldFlip = (nodeId: ID) => {\n      const ancestors = model.getAncestorsData(nodeId, 'tree');\n      if (isEmpty(ancestors)) return false;\n      const ribId = ancestors.length === 1 ? nodeId : ancestors[ancestors.length - 2].id;\n      return oddIndexedRibs.includes(ribId);\n    };\n\n    result.nodes.forEach((node) => {\n      if (shouldFlip(node.id)) node.y = 2 * rootY - node.y;\n    });\n    result.edges.forEach((edge) => {\n      if (shouldFlip(edge.relatedNodeId)) {\n        edge.controlPoints = edge.controlPoints.map((point) => [point[0], 2 * rootY - point[1]]);\n      }\n    });\n  }\n\n  private rightToLeft(result: LayoutResult, options: Required<FishboneLayoutOptions>) {\n    result.nodes.forEach((node) => (node.x = options.width! - node.x));\n    result.edges.forEach((edge) => {\n      edge.controlPoints = edge.controlPoints.map((point) => [options.width! - point[0], point[1]]);\n    });\n    return result;\n  }\n\n  async execute(data: GraphData, propOptions: FishboneLayoutOptions): Promise<GraphData> {\n    const options = { ...FishboneLayout.defaultOptions, ...this.options, ...propOptions };\n    const { direction, nodeSize } = options;\n\n    const root = this.getRoot();\n    if (!root) return data;\n\n    const getSize = this.formatSize(nodeSize);\n    options.vGap ||= Math.max(...(data.nodes || []).map((node) => getSize(node)[1]));\n    options.hGap ||= Math.max(...(data.nodes || []).map((node) => getSize(node)[0]));\n\n    let result = this.doLayout(root, options);\n\n    this.placeAlterative(result, root);\n\n    if (direction === 'RL') {\n      result = this.rightToLeft(result, options);\n    }\n\n    const { model } = this.context;\n    const nodes: NodeData[] = [];\n    const edges: EdgeData[] = [];\n\n    result.nodes.forEach((node) => {\n      const { id, x, y } = node;\n      const nodeData = model.getNodeLikeDatum(id);\n      nodes.push(assignElementStyle(nodeData, { x, y }) as NodeData);\n    });\n\n    result.edges.forEach((edge) => {\n      const { id, controlPoints } = edge;\n      const edgeData = model.getEdgeDatum(id);\n      edges.push(assignElementStyle(edgeData, { controlPoints }) as EdgeData);\n    });\n\n    return { nodes, edges };\n  }\n}\n\nconst assignElementStyle = (element: ElementDatum, style: Record<string, unknown>) => {\n  return { ...element, style: { ...(element.style || {}), ...style } };\n};\n\nconst isRoot = (node: NodeData) => node.depth === 0;\n\nconst isAtEvenDepth = (node: NodeData) => (node.depth ||= 0) % 2 === 0;\n"
  },
  {
    "path": "packages/g6/src/layouts/index.ts",
    "content": "export { compactBox, dendrogram, indented, mindmap } from '@antv/hierarchy';\nexport {\n  AntVDagreLayout,\n  CircularLayout,\n  ComboCombinedLayout,\n  ConcentricLayout,\n  D3ForceLayout,\n  DagreLayout,\n  ForceAtlas2Layout,\n  ForceLayout,\n  FruchtermanLayout,\n  GridLayout,\n  MDSLayout,\n  RadialLayout,\n  RandomLayout,\n} from '@antv/layout';\nexport { BaseLayout } from './base-layout';\nexport { FishboneLayout } from './fishbone';\nexport { SnakeLayout } from './snake';\n\nexport type { FishboneLayoutOptions } from './fishbone';\nexport type { SnakeLayoutOptions } from './snake';\n"
  },
  {
    "path": "packages/g6/src/layouts/snake.ts",
    "content": "import type { GraphData, NodeData } from '../spec';\nimport type { ID, Padding, Size } from '../types';\nimport { parsePadding } from '../utils/padding';\nimport { parseSize } from '../utils/size';\nimport { BaseLayout } from './base-layout';\nimport type { BaseLayoutOptions } from './types';\n\nexport interface SnakeLayoutOptions extends BaseLayoutOptions {\n  /**\n   * <zh/> 节点尺寸\n   *\n   * <en/> Node size\n   */\n  nodeSize?: Size | ((node: NodeData) => Size);\n  /**\n   * <zh/> 内边距，即布局区域与画布边界的距离\n   *\n   * <en/> Padding, the distance between the layout area and the canvas boundary\n   * @defaultValue 0\n   */\n  padding?: Padding;\n  /**\n   * <zh/> 节点排序方法。默认按照在图中的路径顺序进行展示\n   *\n   * <en/> Node sorting method\n   */\n  sortBy?: (nodeA: NodeData, nodeB: NodeData) => -1 | 0 | 1;\n  /**\n   * <zh/> 节点列数\n   *\n   * <en/> Number of node columns\n   * @defaultValue 5\n   */\n  cols?: number;\n  /**\n   * <zh/> 节点行之间的间隙大小。默认将根据画布高度和节点总行数自动计算\n   *\n   * <en/> The size of the gap between a node's rows\n   */\n  rowGap?: number;\n  /**\n   * <zh/> 节点列之间的间隙大小。默认将根据画布宽度和节点总列数自动计算\n   *\n   * <en/> The size of the gap between a node's columns\n   */\n  colGap?: number;\n  /**\n   * <zh/> 节点排布方向是否顺时针\n   *\n   * <en/> Whether the node arrangement direction is clockwise\n   * @defaultValue true\n   * @remarks\n   * <zh/> 在顺时针排布时，节点从左上角开始，第一行从左到右排列，第二行从右到左排列，依次类推，形成 S 型路径。在逆时针排布时，节点从右上角开始，第一行从右到左排列，第二行从左到右排列，依次类推，形成反向 S 型路径。\n   *\n   * <en/> When arranged clockwise, the nodes start from the upper left corner, the first row is arranged from left to right, the second row is arranged from right to left, and so on, forming an S-shaped path. When arranged counterclockwise, the nodes start from the upper right corner, the first row is arranged from right to left, the second row is arranged from left to right, and so on, forming a reverse S-shaped path.\n   */\n  clockwise?: boolean;\n}\n\n/**\n * <zh/> 蛇形布局\n *\n * <en/> Snake layout\n * @remarks\n * <zh/> 蛇形布局（Snake Layout）是一种特殊的图形布局方式，能够在较小的空间内更有效地展示长链结构。需要注意的是，其图数据需要确保节点按照从源节点到汇节点的顺序进行线性排列，形成一条明确的路径。\n *\n * <zh/> 节点按 S 字型排列，第一个节点位于第一行的起始位置，接下来的节点在第一行向右排列，直到行末尾。到达行末尾后，下一行的节点从右向左反向排列。这个过程重复进行，直到所有节点排列完毕。\n *\n * <en/> The Snake layout is a special way of graph layout that can more effectively display long chain structures in a smaller space. Note that the graph data needs to ensure that the nodes are linearly arranged in the order from the source node to the sink node to form a clear path.\n *\n * <en/> The nodes are arranged in an S-shaped pattern, with the first node at the beginning of the first row, and the following nodes arranged to the right until the end of the row. After reaching the end of the row, the nodes in the next row are arranged in reverse from right to left. This process is repeated until all nodes are arranged.\n */\nexport class SnakeLayout extends BaseLayout {\n  public id = 'snake';\n\n  static defaultOptions: Partial<SnakeLayoutOptions> = {\n    padding: 0,\n    cols: 5,\n    clockwise: true,\n  };\n\n  private formatSize(nodes: NodeData[], size?: Size | ((node: NodeData) => Size)): [number, number] {\n    const sizeFn = typeof size === 'function' ? size : ((() => size) as (node: NodeData) => Size);\n    return nodes.reduce(\n      (acc, node) => {\n        const [w, h] = parseSize(sizeFn(node)) || [0, 0];\n        return [Math.max(acc[0], w), Math.max(acc[1], h)];\n      },\n      [0, 0],\n    );\n  }\n\n  /**\n   * Validates the graph data to ensure it meets the requirements for linear arrangement.\n   * @param data - Graph data\n   * @returns false if the graph is not connected, has more than one source or sink node, or contains cycles.\n   */\n  private validate(data: GraphData): boolean {\n    const { nodes = [], edges = [] } = data;\n    const inDegree: { [key: ID]: number } = {};\n    const outDegree: { [key: ID]: number } = {};\n    const adjList: { [key: ID]: ID[] } = {};\n\n    nodes.forEach((node) => {\n      inDegree[node.id] = 0;\n      outDegree[node.id] = 0;\n      adjList[node.id] = [];\n    });\n\n    edges.forEach((edge) => {\n      inDegree[edge.target]++;\n      outDegree[edge.source]++;\n      adjList[edge.source].push(edge.target);\n    });\n\n    // 检查图是否连通\n    // Check if the graph is connected\n    const visited: Set<ID> = new Set();\n    const dfs = (nodeId: ID) => {\n      if (visited.has(nodeId)) return;\n      visited.add(nodeId);\n      adjList[nodeId].forEach(dfs);\n    };\n    dfs(nodes[0].id);\n    if (visited.size !== nodes.length) return false;\n\n    // 检查是否有且仅有一个源节点和一个汇节点\n    // Check if there is exactly one source node and one sink node\n    const sourceNodes = nodes.filter((node) => inDegree[node.id] === 0);\n    const sinkNodes = nodes.filter((node) => outDegree[node.id] === 0);\n    if (sourceNodes.length !== 1 || sinkNodes.length !== 1) return false;\n\n    // 检查中间节点是否只有一个前驱和一个后继\n    // Check if the middle nodes have only one predecessor and one successor\n    const middleNodes = nodes.filter((node) => inDegree[node.id] === 1 && outDegree[node.id] === 1);\n    if (middleNodes.length !== nodes.length - 2) return false;\n\n    return true;\n  }\n\n  async execute(model: GraphData, options?: SnakeLayoutOptions): Promise<GraphData> {\n    if (!this.validate(model)) return model;\n\n    const {\n      nodeSize: propNodeSize,\n      padding: propPadding,\n      sortBy,\n      cols,\n      colGap: propColSep,\n      rowGap: propRowSep,\n      clockwise,\n      width,\n      height,\n    } = Object.assign({}, SnakeLayout.defaultOptions, this.options, options) as Required<SnakeLayoutOptions>;\n\n    const [top, right, bottom, left] = parsePadding(propPadding);\n    const nodeSize = this.formatSize(model.nodes || [], propNodeSize);\n\n    const rows = Math.ceil((model.nodes || []).length / cols);\n    let colSep = propColSep ? propColSep : (width - left - right - cols * nodeSize[0]) / (cols - 1);\n    let rowSep = propRowSep ? propRowSep : (height - top - bottom - rows * nodeSize[1]) / (rows - 1);\n    if (rowSep === Infinity || rowSep < 0) rowSep = 0;\n    if (colSep === Infinity || colSep < 0) colSep = 0;\n\n    const sortedNodes = sortBy ? model.nodes?.sort(sortBy) : topologicalSort(model);\n\n    const nodes = (sortedNodes || []).map((node, index) => {\n      const rowIndex = Math.floor(index / cols);\n      const colIndex = index % cols;\n\n      const actualColIndex = clockwise\n        ? rowIndex % 2 === 0\n          ? colIndex\n          : cols - 1 - colIndex\n        : rowIndex % 2 === 0\n          ? cols - 1 - colIndex\n          : colIndex;\n\n      const x = left + actualColIndex * (nodeSize[0] + colSep) + nodeSize[0] / 2;\n      const y = top + rowIndex * (nodeSize[1] + rowSep) + nodeSize[1] / 2;\n\n      return {\n        id: node.id,\n        style: { x, y },\n      };\n    });\n\n    return { nodes };\n  }\n}\n\n/**\n * Topological sorting. The nodes are sorted according to the order of the paths(from the start node to the end node).\n * @param data - Graph data\n * @returns Sorted nodes\n */\nfunction topologicalSort(data: GraphData): NodeData[] {\n  const { nodes = [], edges = [] } = data;\n  const inDegree: { [key: ID]: number } = {};\n  const adjList: { [key: ID]: ID[] } = {};\n\n  nodes.forEach((node) => {\n    inDegree[node.id] = 0;\n    adjList[node.id] = [];\n  });\n\n  edges.forEach((edge) => {\n    inDegree[edge.target]++;\n    adjList[edge.source].push(edge.target);\n  });\n\n  const queue: ID[] = [];\n  const sortedNodes: NodeData[] = [];\n\n  nodes.forEach((node) => {\n    if (inDegree[node.id] === 0) {\n      queue.push(node.id);\n    }\n  });\n\n  while (queue.length > 0) {\n    const nodeId = queue.shift()!;\n    const node = nodes.find((n) => n.id === nodeId)!;\n    sortedNodes.push(node);\n\n    adjList[nodeId].forEach((neighbor) => {\n      inDegree[neighbor]--;\n      if (inDegree[neighbor] === 0) {\n        queue.push(neighbor);\n      }\n    });\n  }\n\n  return sortedNodes;\n}\n"
  },
  {
    "path": "packages/g6/src/layouts/types.ts",
    "content": "import type { Graph as IGraph } from '@antv/graphlib';\nimport type {\n  AntVDagreLayoutOptions,\n  LayoutWithIterations as AntVIterativeLayout,\n  Layout as AntVNonIterativeLayout,\n  CircularLayoutOptions,\n  ConcentricLayoutOptions,\n  D3Force3DLayoutOptions,\n  D3ForceLayoutOptions,\n  DagreLayoutOptions,\n  ForceAtlas2LayoutOptions,\n  ForceLayoutOptions,\n  FruchtermanLayoutOptions,\n  GraphData,\n  GridLayoutOptions,\n  MDSLayoutOptions,\n  RadialLayoutOptions,\n  RandomLayoutOptions,\n} from '@antv/layout';\nimport type { ComboData, EdgeData, NodeData } from '../spec/data';\nimport type { BaseLayout } from './base-layout';\nimport type { FishboneLayoutOptions } from './fishbone';\nimport type { SnakeLayoutOptions } from './snake';\n\nexport type BuiltInLayoutOptions =\n  | AntVDagreLayout\n  | CircularLayout\n  | ConcentricLayout\n  | D3ForceLayout\n  | D3Force3DLayout\n  | DagreLayout\n  | ForceAtlas2\n  | ForceLayout\n  | FruchtermanLayout\n  | GridLayout\n  | MDSLayout\n  | RadialLayout\n  | RandomLayout\n  | SnakeLayout\n  | FishboneLayout;\n\nexport interface BaseLayoutOptions extends AnimationOptions, WebWorkerLayoutOptions {\n  /**\n   * <zh/> 布局类型\n   *\n   * <en/> Layout type\n   */\n  type: string;\n  /**\n   * <zh/> 参与该布局的节点\n   *\n   * <en/> Nodes involved in the layout\n   * @param node - <zh/> 节点数据 | <en/> node data\n   * @returns <zh/> 是否参与布局 | <en/> Whether to participate in the layout\n   */\n  nodeFilter?: (node: NodeData) => boolean;\n  /**\n   * <zh/> 参与该布局的combo元素\n   *\n   * <en/> Combos involved in the layout\n   * @param node - <zh/> combo数据 | <en/> combo data\n   * @returns <zh/> 是否参与布局 | <en/> Whether to participate in the layout\n   */\n  comboFilter?: (combo: ComboData) => boolean;\n  /**\n   * <zh/> 使用前布局，在初始化元素前计算布局\n   *\n   * <en/> Use pre-layout to calculate the layout before initializing the elements\n   * @remarks\n   * <zh/> 不适用于流水线布局\n   *\n   * <en/> Not applicable to pipeline layout\n   */\n  preLayout?: boolean;\n  /**\n   * <zh/> 不可见节点是否参与布局\n   *\n   * <en/> Whether invisible nodes participate in the layout\n   * @remarks\n   * <zh/> 当 preLayout 为 true 时生效\n   *\n   * <en/> Takes effect when preLayout is true\n   */\n  isLayoutInvisibleNodes?: boolean;\n  /**\n   * <zh/> 布局区域宽度，默认为画布宽度\n   *\n   * <en/> Width of the layout area, default is the canvas width\n   */\n  width?: number;\n  /**\n   * <zh/> 布局区域高度，默认为画布高度\n   *\n   * <en/> Height of the layout area, default is the canvas height\n   */\n  height?: number;\n  [key: string]: unknown;\n}\n\ninterface CircularLayout extends BaseLayoutOptions, CircularLayoutOptions {\n  type: 'circular';\n}\n\ninterface RandomLayout extends BaseLayoutOptions, RandomLayoutOptions {\n  type: 'random';\n}\n\ninterface GridLayout extends BaseLayoutOptions, GridLayoutOptions {\n  type: 'grid';\n}\n\ninterface MDSLayout extends BaseLayoutOptions, MDSLayoutOptions {\n  type: 'mds';\n}\n\ninterface ConcentricLayout extends BaseLayoutOptions, ConcentricLayoutOptions {\n  type: 'concentric';\n}\n\ninterface RadialLayout extends BaseLayoutOptions, RadialLayoutOptions {\n  type: 'radial';\n}\n\ninterface FruchtermanLayout extends BaseLayoutOptions, FruchtermanLayoutOptions {\n  type: 'fruchterman' | 'fruchterman-gpu';\n}\n\ninterface D3ForceLayout extends BaseLayoutOptions, D3ForceLayoutOptions {\n  type: 'd3-force';\n}\n\ninterface D3Force3DLayout extends BaseLayoutOptions, D3Force3DLayoutOptions {\n  type: 'd3-force3d';\n}\n\ninterface ForceLayout extends BaseLayoutOptions, ForceLayoutOptions {\n  type: 'force' | 'gforce';\n}\n\ninterface ForceAtlas2 extends BaseLayoutOptions, ForceAtlas2LayoutOptions {\n  type: 'force-atlas2';\n}\n\ninterface AntVDagreLayout extends BaseLayoutOptions, AntVDagreLayoutOptions {\n  type: 'antv-dagre';\n}\n\ninterface DagreLayout extends BaseLayoutOptions, DagreLayoutOptions {\n  type: 'dagre';\n}\n\ninterface SnakeLayout extends BaseLayoutOptions, SnakeLayoutOptions {\n  type: 'snake';\n}\n\ninterface FishboneLayout extends BaseLayoutOptions, FishboneLayoutOptions {\n  type: 'fishbone';\n}\n\ninterface AnimationOptions {\n  /**\n   * <zh/> 启用布局动画，对于迭代布局，会在两次迭代之间进行动画过渡\n   *\n   * <en/> Enable layout animation, for iterative layout, animation transition will be performed between two iterations\n   */\n  animation?: boolean;\n}\n\nexport interface WebWorkerLayoutOptions {\n  /**\n   * <zh/> 是否在 WebWorker 中运行布局\n   *\n   * <en/> Whether to run the layout in WebWorker\n   */\n  enableWorker?: boolean;\n  /**\n   * <zh/> 迭代布局的迭代次数\n   *\n   * <en/> Iterations for iterable layouts\n   */\n  iterations?: number;\n}\n\nexport type AntVLayout = AntVNonIterativeLayout<any> | AntVIterativeLayout<any>;\n\nexport type Layout = BaseLayout | AntVLayout;\n\nexport type AntVGraphData = GraphData;\n\n/** Legacy AntV Layout 1.x */\nexport type LegacyGraph = IGraph<NodeData, EdgeData>;\nexport type LegacyAntVLayout<T = any> = {\n  id: string;\n  options: T;\n  assign(graph: LegacyGraph, options?: T): Promise<void>;\n  execute(graph: LegacyGraph, options?: T): Promise<AntVGraphData>;\n  tick(iterations?: number): AntVGraphData;\n  stop(): void;\n};\n"
  },
  {
    "path": "packages/g6/src/palettes/index.ts",
    "content": "/**\n * <zh/> 内置色板\n *\n * <en/> Built-in palettes\n */\nexport const spectral = [\n  'rgb(158, 1, 66)',\n  'rgb(213, 62, 79)',\n  'rgb(244, 109, 67)',\n  'rgb(253, 174, 97)',\n  'rgb(254, 224, 139)',\n  'rgb(255, 255, 191)',\n  'rgb(230, 245, 152)',\n  'rgb(171, 221, 164)',\n  'rgb(102, 194, 165)',\n  'rgb(50, 136, 189)',\n  'rgb(94, 79, 162)',\n];\n\nexport const tableau = [\n  'rgb(78, 121, 167)',\n  'rgb(242, 142, 44)',\n  'rgb(225, 87, 89)',\n  'rgb(118, 183, 178)',\n  'rgb(89, 161, 79)',\n  'rgb(237, 201, 73)',\n  'rgb(175, 122, 161)',\n  'rgb(255, 157, 167)',\n  'rgb(156, 117, 95)',\n  'rgb(186, 176, 171)',\n];\n\nexport const oranges = [\n  'rgb(255, 245, 235)',\n  'rgb(254, 230, 206)',\n  'rgb(253, 208, 162)',\n  'rgb(253, 174, 107)',\n  'rgb(253, 141, 60)',\n  'rgb(241, 105, 19)',\n  'rgb(217, 72, 1)',\n  'rgb(166, 54, 3)',\n  'rgb(127, 39, 4)',\n];\n\nexport const greens = [\n  'rgb(247, 252, 245)',\n  'rgb(229, 245, 224)',\n  'rgb(199, 233, 192)',\n  'rgb(161, 217, 155)',\n  'rgb(116, 196, 118)',\n  'rgb(65, 171, 93)',\n  'rgb(35, 139, 69)',\n  'rgb(0, 109, 44)',\n  'rgb(0, 68, 27)',\n];\n\nexport const blues = [\n  'rgb(247, 251, 255)',\n  'rgb(222, 235, 247)',\n  'rgb(198, 219, 239)',\n  'rgb(158, 202, 225)',\n  'rgb(107, 174, 214)',\n  'rgb(66, 146, 198)',\n  'rgb(33, 113, 181)',\n  'rgb(8, 81, 156)',\n  'rgb(8, 48, 107)',\n];\n"
  },
  {
    "path": "packages/g6/src/palettes/types.ts",
    "content": "export type Palette = string | BuiltInPalette | CategoricalPalette | ContinuousPalette;\n\nexport type STDPalette = CategoricalPalette | ContinuousPalette;\n\nexport type BuiltInPalette = 'spectral' | 'oranges' | 'greens' | 'blues';\n\nexport type CategoricalPalette = string[];\n\nexport type ContinuousPalette = (ratio: number) => string;\n"
  },
  {
    "path": "packages/g6/src/plugins/background/index.ts",
    "content": "import { omit } from '@antv/util';\nimport type { RuntimeContext } from '../../runtime/types';\nimport type { BasePluginOptions } from '../base-plugin';\nimport { BasePlugin } from '../base-plugin';\nimport { createPluginContainer } from '../utils/dom';\n\n/**\n * <zh/> 背景配置项\n *\n * <en/> Background options\n */\nexport interface BackgroundOptions extends BasePluginOptions, CSSStyleDeclaration {}\n\n/**\n * <zh/> 背景图\n *\n * <en/> Background image\n * @remarks\n * <zh/> 支持为图画布设置一个背景图片，让画布更有层次感、叙事性。\n *\n * <en/> Support setting a background image for the canvas to make the canvas more hierarchical and narrative.\n */\nexport class Background extends BasePlugin<BackgroundOptions> {\n  static defaultOptions: Partial<BackgroundOptions> = {\n    transition: 'background 0.5s',\n    backgroundSize: 'cover',\n    zIndex: '-1', // aviod to cover the other plugin's dom, eg: grid-line.\n  };\n\n  private $element: HTMLElement = createPluginContainer('background');\n\n  constructor(context: RuntimeContext, options: BackgroundOptions) {\n    super(context, Object.assign({}, Background.defaultOptions, options));\n\n    const $container = this.context.canvas.getContainer();\n    $container!.prepend(this.$element);\n\n    this.update(options);\n  }\n\n  /**\n   * <zh/> 更新背景图配置\n   *\n   * <en/> Update the background image configuration\n   * @param options - <zh/> 配置项 | <en/> Options\n   * @internal\n   */\n  public async update(options: Partial<BackgroundOptions>) {\n    super.update(options);\n\n    // Set the background style.\n    Object.assign(this.$element.style, omit(this.options, ['key', 'type']));\n  }\n\n  /**\n   * <zh/> 销毁背景图\n   *\n   * <en/> Destroy the background image\n   * @internal\n   */\n  public destroy(): void {\n    super.destroy();\n    // Remove the background dom.\n    this.$element.remove();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/base-plugin.ts",
    "content": "import { BaseExtension } from '../registry/extension';\nimport type { CustomPluginOption } from '../spec/plugin';\n\nexport interface BasePluginOptions extends CustomPluginOption {}\n\n/**\n * <zh/> 插件的基类\n *\n * <en/> Base class for plugins\n */\nexport abstract class BasePlugin<T extends BasePluginOptions> extends BaseExtension<T> {}\n"
  },
  {
    "path": "packages/g6/src/plugins/bubble-sets.ts",
    "content": "import type { PathArray } from '@antv/util';\nimport { deepMix, isEqual, isFunction } from '@antv/util';\nimport type { IBubbleSetOptions, ILine, IRectangle } from 'bubblesets-js';\nimport { BubbleSets as BubbleSetsJS, Line, Rectangle, defaultOptions } from 'bubblesets-js';\nimport { GraphEvent } from '../constants';\nimport type { ContourStyleProps } from '../elements/shapes';\nimport { Contour } from '../elements/shapes';\nimport type { Graph } from '../runtime/graph';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { ID } from '../types';\nimport { getBBoxHeight, getBBoxWidth } from '../utils/bbox';\nimport { arrayDiff } from '../utils/diff';\nimport type { ElementLifeCycleEvent } from '../utils/event';\nimport { idOf } from '../utils/id';\nimport { getClosedSpline } from '../utils/path';\nimport { parsePoint } from '../utils/point';\nimport type { BasePluginOptions } from './base-plugin';\nimport { BasePlugin } from './base-plugin';\n\n/**\n * <zh/> 气泡集配置项\n *\n * <en/> BubbleSets options\n */\nexport interface BubbleSetsOptions extends BasePluginOptions, IBubbleSetOptions, ContourStyleProps {\n  /**\n   * <zh/> 成员元素，包括节点和边\n   *\n   * <en/> Member elements, including nodes and edges\n   */\n  members: ID[];\n  /**\n   * <zh/> 需要避开的元素，在绘制轮廓时不会包含这些元素。目前支持设置节点\n   *\n   * <en/> Elements to avoid, these elements will not be included when drawing the contour, currently only nodes are supported\n   */\n  avoidMembers?: ID[];\n}\n\n/**\n * <zh/> 气泡集\n *\n * <en/> BubbleSets\n * @remarks\n * <zh/> BubbleSets 最初由 Christopher Collins 在 2009 年的论文 \"Bubble Sets: Revealing Set Relations with Isocontours over Existing Visualizations\" 中提出。\n *\n * <zh/> 实现原理是通过创建一种类似于气泡的形状来表示集合。每个集合都被表示为一个独特的 \"气泡\"，集合中的元素被包含在这个气泡内部。如果两个集合有交集，那么这两个气泡会有重叠的部分，这个重叠的部分就表示这两个集合的交集。\n *\n * <en/> BubbleSets was originally proposed by Christopher Collins in the 2009 paper \"Bubble Sets: Revealing Set Relations with Isocontours over Existing Visualizations\".\n *\n * <en/> The principle is to represent sets by creating a shape similar to a bubble. Each set is represented by a unique \"bubble\", and the elements in the set are contained within this bubble. If two sets have an intersection, then the two bubbles will have an overlapping part, which represents the intersection of the two sets.\n */\nexport class BubbleSets extends BasePlugin<BubbleSetsOptions> {\n  private shape?: Contour;\n\n  private bubbleSets!: BubbleSetsJS;\n\n  private path: PathArray | null = null;\n\n  private members: Map<ID, IRectangle | ILine> = new Map();\n\n  private avoidMembers: Map<ID, IRectangle | ILine> = new Map();\n\n  private bubbleSetOptions: IBubbleSetOptions = {};\n\n  static defaultOptions: Partial<BubbleSetsOptions> = {\n    members: [],\n    avoidMembers: [],\n    /** shape style */\n    fill: 'lightblue',\n    fillOpacity: 0.2,\n    stroke: 'blue',\n    strokeOpacity: 0.2,\n    /** bubbleSetJS config */\n    ...defaultOptions,\n  };\n\n  constructor(context: RuntimeContext, options: BubbleSetsOptions) {\n    super(context, deepMix({}, BubbleSets.defaultOptions, options));\n\n    this.bindEvents();\n\n    this.bubbleSets = new BubbleSetsJS(this.options);\n  }\n\n  private bindEvents() {\n    this.context.graph.on(GraphEvent.AFTER_RENDER, this.drawBubbleSets);\n    this.context.graph.on(GraphEvent.AFTER_ELEMENT_UPDATE, this.updateBubbleSetsPath);\n  }\n\n  private init() {\n    this.bubbleSets = new BubbleSetsJS(this.options);\n    this.members.clear();\n    this.avoidMembers.clear();\n    this.path = null;\n  }\n\n  private parseOptions() {\n    const { type, key, members, avoidMembers, ...rest } = this.options;\n    const res = Object.keys(rest).reduce(\n      (acc: { style: ContourStyleProps; bubbleSetOptions: IBubbleSetOptions }, key: string) => {\n        if (key in defaultOptions) {\n          acc.bubbleSetOptions[key as keyof IBubbleSetOptions] = rest[key];\n        } else {\n          acc.style[key as keyof ContourStyleProps] = rest[key];\n        }\n        return acc;\n      },\n      { style: {}, bubbleSetOptions: {} },\n    );\n    return { type, key, members, avoidMembers, ...res };\n  }\n\n  private drawBubbleSets = () => {\n    const { style, bubbleSetOptions } = this.parseOptions();\n    if (!isEqual(this.bubbleSetOptions, bubbleSetOptions)) this.init();\n    this.bubbleSetOptions = { ...bubbleSetOptions };\n\n    const finalStyle = { ...style, d: this.getPath() };\n    if (!this.shape) {\n      this.shape = new Contour({ style: finalStyle });\n      this.context.canvas.appendChild(this.shape);\n    } else {\n      this.shape.update(finalStyle);\n    }\n  };\n\n  private updateBubbleSetsPath = (event: ElementLifeCycleEvent) => {\n    if (!this.shape) return;\n    const id = idOf(event.data);\n    if (![...this.options.members, ...this.options.avoidMembers].includes(id)) return;\n    this.shape.update({ ...this.parseOptions().style, d: this.getPath(id) });\n  };\n\n  private getPath = (forceUpdateId?: ID): PathArray => {\n    const { graph } = this.context;\n\n    const currMembers = this.options.members;\n    const prevMembers = [...this.members.keys()];\n    const currAvoidMembers = this.options.avoidMembers;\n    const prevAvoidMembers = [...this.avoidMembers.keys()];\n\n    if (currMembers.length === 0 && currAvoidMembers.length === 0) {\n      this.members.clear();\n      this.avoidMembers.clear();\n      this.path = [] as unknown as PathArray;\n      return this.path;\n    }\n\n    if (\n      !forceUpdateId &&\n      this.path &&\n      isEqual(currMembers, prevMembers) &&\n      isEqual(currAvoidMembers, prevAvoidMembers)\n    ) {\n      return this.path;\n    }\n\n    const { enter: membersToEnter = [], exit: membersToExit = [] } = arrayDiff(prevMembers, currMembers, (d) => d);\n    const { enter: avoidMembersToEnter = [], exit: avoidMembersToExit = [] } = arrayDiff(\n      prevAvoidMembers,\n      currAvoidMembers,\n      (d) => d,\n    );\n\n    if (forceUpdateId) {\n      const isMemberNow = currMembers.includes(forceUpdateId);\n      const isAvoidNow = currAvoidMembers.includes(forceUpdateId);\n\n      if (isMemberNow) {\n        membersToExit.push(forceUpdateId);\n        membersToEnter.push(forceUpdateId);\n      }\n      if (isAvoidNow) {\n        avoidMembersToExit.push(forceUpdateId);\n        avoidMembersToEnter.push(forceUpdateId);\n      }\n    }\n\n    const updateBubbleSets = (ids: ID[], isEntering: boolean, isMember: boolean) => {\n      ids.forEach((id) => {\n        const members = isMember ? this.members : this.avoidMembers;\n        const pushMember = isMember ? 'pushMember' : 'pushNonMember';\n        const removeMember = isMember ? 'removeMember' : 'removeNonMember';\n        if (isEntering) {\n          let area: IRectangle | ILine;\n          if (graph.getElementType(id) === 'edge') {\n            [area] = convertToLine(graph, id);\n            this.bubbleSets.pushEdge(area);\n          } else {\n            [area] = convertToRectangle(graph, id);\n            this.bubbleSets[pushMember](area);\n          }\n          members.set(id, area);\n        } else {\n          const area = members.get(id);\n          if (area) {\n            if (graph.getElementType(id) === 'edge') {\n              this.bubbleSets.removeEdge(area as ILine);\n            } else {\n              this.bubbleSets[removeMember](area as IRectangle);\n            }\n            members.delete(id);\n          }\n        }\n      });\n    };\n\n    updateBubbleSets(membersToExit, false, true);\n    updateBubbleSets(membersToEnter, true, true);\n    updateBubbleSets(avoidMembersToExit, false, false);\n    updateBubbleSets(avoidMembersToEnter, true, false);\n\n    const pointPath = this.bubbleSets.compute();\n    const cleanPath = pointPath.sample(8).simplify(0).bSplines().simplify(0);\n    this.path = getClosedSpline(cleanPath.points.map(parsePoint));\n    return this.path;\n  };\n\n  /**\n   * <zh/> 添加成员元素\n   *\n   * <en/> Add member elements\n   * @param members - <zh/> 单个或多个 | <en/> single or multiple\n   */\n  public addMember(members: ID | ID[]) {\n    const membersToAdd = Array.isArray(members) ? members : [members];\n    if (membersToAdd.some((member) => this.options.avoidMembers.includes(member))) {\n      this.options.avoidMembers = this.options.avoidMembers.filter((id) => !membersToAdd.includes(id));\n    }\n    this.options.members = [...new Set([...this.options.members, ...membersToAdd])];\n    this.drawBubbleSets();\n  }\n  /**\n   * <zh/> 移除成员元素\n   *\n   * <en/> Remove member elements\n   * @param members - <zh/> 单个或多个 | <en/> single or multiple\n   */\n  public removeMember(members: ID | ID[]) {\n    const membersToRemove = Array.isArray(members) ? members : [members];\n    this.options.members = this.options.members.filter((id) => !membersToRemove.includes(id));\n    this.drawBubbleSets();\n  }\n  /**\n   * <zh/> 更新成员元素\n   *\n   * <en/> Update member elements\n   * @param members - <zh/> 值或者回调函数 | <en/> value or callback function\n   */\n  public updateMember(members: ID[] | ((prev: ID[]) => ID[])) {\n    this.options.members = isFunction(members) ? members(this.options.members) : members;\n    this.drawBubbleSets();\n  }\n  /**\n   * <zh/> 获取成员元素\n   *\n   * <en/> Get member elements\n   * @returns <zh/> 成员元素数组 | <en/> member elements array\n   */\n  public getMember() {\n    return this.options.members;\n  }\n\n  /**\n   * <zh/> 添加需要避开的元素\n   *\n   * <en/> Add elements to avoid\n   * @param avoidMembers - <zh/> 单个或多个 | <en/> single or multiple\n   */\n  public addAvoidMember(avoidMembers: ID | ID[]) {\n    const avoidMembersToAdd = Array.isArray(avoidMembers) ? avoidMembers : [avoidMembers];\n    if (avoidMembersToAdd.some((avoidMember) => this.options.members.includes(avoidMember))) {\n      this.options.members = this.options.members.filter((id) => !avoidMembersToAdd.includes(id));\n    }\n    this.options.avoidMembers = [...new Set([...this.options.avoidMembers, ...avoidMembersToAdd])];\n    this.drawBubbleSets();\n  }\n  /**\n   * <zh/> 移除需要避开的元素\n   *\n   * <en/> Remove elements to avoid\n   * @param avoidMembers - <zh/> 单个或多个 | <en/> single or multiple\n   */\n  public removeAvoidMember(avoidMembers: ID | ID[]) {\n    const avoidMembersToRemove = Array.isArray(avoidMembers) ? avoidMembers : [avoidMembers];\n    if (this.options.avoidMembers.some((member) => avoidMembersToRemove.includes(member))) {\n      this.options.avoidMembers = this.options.avoidMembers.filter((id) => !avoidMembersToRemove.includes(id));\n      this.drawBubbleSets();\n    }\n  }\n  /**\n   * <zh/> 更新需要避开的元素\n   *\n   * <en/> Update elements to avoid\n   * @param avoidMembers - <zh/> 单个或多个 | <en/> single or multiple\n   */\n  public updateAvoidMember(avoidMembers: ID | ID[]) {\n    this.options.avoidMembers = Array.isArray(avoidMembers) ? avoidMembers : [avoidMembers];\n    this.drawBubbleSets();\n  }\n  /**\n   * <zh/> 获取需要避开的元素\n   *\n   * <en/> Get elements to avoid\n   * @returns avoidMembers <zh/> 成员元素数组 | <en/> member elements array\n   */\n  public getAvoidMember() {\n    return this.options.avoidMembers;\n  }\n  /**\n   * <zh/> 销毁\n   *\n   * <en/> Destroy\n   * @internal\n   */\n  public destroy(): void {\n    this.context.graph.off(GraphEvent.AFTER_RENDER, this.drawBubbleSets);\n    this.context.graph.off(GraphEvent.AFTER_ELEMENT_UPDATE, this.updateBubbleSetsPath);\n\n    if (this.shape) {\n      this.shape.destroy();\n      this.shape = undefined;\n    }\n\n    super.destroy();\n  }\n}\n\n/**\n * <zh/> 将节点转换为 BubbleSetJS 支持的矩形\n *\n * <en/> Convert nodes to rectangles supported by BubbleSetJS\n * @param graph - <zh/> 图实例 | <en/> graph instance\n * @param ids - <zh/> 元素 ID 数组 | <en/> element ID array\n * @returns <zh/> 矩形数组 | <en/> rectangle array\n */\nconst convertToRectangle = (graph: Graph, ids: ID | ID[]): IRectangle[] => {\n  const idArr = Array.isArray(ids) ? ids : [ids];\n  return idArr.map((id) => {\n    const bbox = graph.getElementRenderBounds(id);\n    return new Rectangle(bbox.min[0], bbox.min[1], getBBoxWidth(bbox), getBBoxHeight(bbox));\n  });\n};\n\n/**\n * <zh/> 将边转换为 BubbleSetJS 支持的线\n *\n * <en/> Convert edges to lines supported by BubbleSetJS\n * @param graph - <zh/> 图实例 | <en/> graph instance\n * @param ids - <zh/> 元素 ID 数组 | <en/> element ID array\n * @returns <zh/> 线数组 | <en/> line array\n */\nconst convertToLine = (graph: Graph, ids: ID | ID[]): ILine[] => {\n  const idArr = Array.isArray(ids) ? ids : [ids];\n  return idArr.map((id) => {\n    const data = graph.getEdgeData(id);\n    const source = graph.getElementPosition(data.source);\n    const target = graph.getElementPosition(data.target);\n    return Line.from({ x1: source[0], y1: source[1], x2: target[0], y2: target[1] });\n  });\n};\n"
  },
  {
    "path": "packages/g6/src/plugins/camera-setting.ts",
    "content": "import { GraphEvent } from '../constants';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { BasePluginOptions } from './base-plugin';\nimport { BasePlugin } from './base-plugin';\n\nexport interface CameraSettingOptions extends BasePluginOptions {\n  /**\n   * <zh/> 投影模式，透视投影仅在 3D 场景下有效\n   * - `'perspective'` : 透视投影\n   * - `'orthographic'` : 正交投影\n   *\n   * <en/> Projection mode, perspective projection is only valid in 3D scenes\n   * - `'perspective'` : perspective projection\n   * - `'orthographic'` : Orthogonal projection\n   */\n  projectionMode?: 'perspective' | 'orthographic';\n  /**\n   * <zh/> 相机类型\n   * - `'orbiting'`: 固定视点，改变相机位置\n   * - `'exploring'`: 类似 orbiting，但允许相机在北极和南极之间旋转\n   * - `'tracking'`: 固定相机位置，改变视点\n   *\n   * <en/> Camera type\n   * - `'orbiting'`: Fixed viewpoint, change camera position\n   * - `'exploring'`: Similar to orbiting, but allows the camera to rotate between the North Pole and the South Pole\n   * - `'tracking'`: Fixed camera position, change viewpoint\n   */\n  cameraType?: 'orbiting' | 'exploring' | 'tracking';\n  /**\n   * <zh/> 近平面位置\n   *\n   * <en/> The position of the near plane\n   */\n  near?: number;\n  /**\n   * <zh/> 远平面位置\n   *\n   * <en/> The position of the far plane\n   */\n  far?: number;\n  /**\n   * <zh/> 相机视角，仅在透视相机下有效\n   *\n   * <en/> Camera field of view, only valid in perspective camera\n   */\n  fov?: number;\n  /**\n   * <zh/> 相机视口宽高比，仅在透视相机下有效\n   * - number : 具体的宽高比\n   * - `'auto'` : 自动设置为画布的宽高比\n   *\n   * <en/> Camera viewport aspect ratio, only valid in perspective camera.\n   * - number : Specific aspect ratio\n   * - `'auto'` : Automatically set to the aspect ratio of the canvas\n   */\n  aspect?: number | 'auto';\n  /**\n   * <zh/> 相机距离目标的距离\n   *\n   * <en/> The distance from the camera to the target\n   * @defaultValue 500\n   */\n  distance?: number;\n  /**\n   * <zh/> 最小视距\n   *\n   * <en/> Minimum distance\n   */\n  minDistance?: number;\n  /**\n   * <zh/> 最大视距\n   *\n   * <en/> Maximum distance\n   */\n  maxDistance?: number;\n  /**\n   * <zh/> 滚转角\n   *\n   * <en/> Roll\n   */\n  roll?: number;\n  /**\n   * <zh/> 仰角\n   *\n   * <en/> Elevation\n   */\n  elevation?: number;\n  /**\n   * <zh/> 方位角\n   *\n   * <en/> Azimuth\n   */\n  azimuth?: number;\n}\n\n/**\n * <zh/> 配置相机参数\n *\n * <en/> Configure camera parameters\n */\nexport class CameraSetting extends BasePlugin<CameraSettingOptions> {\n  constructor(context: RuntimeContext, options: CameraSettingOptions) {\n    super(context, options);\n    this.bindEvents();\n  }\n  /**\n   * <zh/> 更新相机参数\n   *\n   * <en/> Update camera parameters\n   * @param options - <zh/> 相机配置项 | <en/> Camera configuration options\n   * @internal\n   */\n  public update(options: Partial<CameraSettingOptions>): void {\n    this.setOptions(options);\n    super.update(options);\n  }\n\n  private bindEvents() {\n    this.context.graph.once(GraphEvent.BEFORE_DRAW, () => this.setOptions(this.options));\n  }\n\n  private setOptions = (options: Partial<CameraSettingOptions>) => {\n    const caller = {\n      cameraType: 'setType',\n      near: 'setNear',\n      far: 'setFar',\n      fov: 'setFov',\n      aspect: 'setAspect',\n      // 确保 projectionMode 在 near/far/fov/aspect 之后设置\n      // Ensure that projectionMode is set after near/far/fov/aspect\n      projectionMode: 'setProjectionMode',\n      distance: 'setDistance',\n      minDistance: 'setMinDistance',\n      maxDistance: 'setMaxDistance',\n      roll: 'setRoll',\n      elevation: 'setElevation',\n      azimuth: 'setAzimuth',\n    } as const;\n\n    const valueMapper = (key: string, value: string) => {\n      switch (key) {\n        case 'projectionMode':\n          return value === 'perspective' ? 1 : 0;\n        case 'cameraType':\n          return { orbiting: 0, exploring: 1, tracking: 2 }[value]!;\n        case 'aspect':\n          if (typeof value === 'number') return value;\n          return this.getCanvasAspect();\n        default:\n          return value;\n      }\n    };\n\n    Object.entries(caller).forEach(([key, method]) => {\n      const value = options[key];\n      if (value !== undefined) {\n        const actualValue = valueMapper(key, value);\n        // @ts-expect-error incorrect ts type check\n        this.context.canvas.getCamera()[method](actualValue);\n      }\n    });\n  };\n\n  private getCanvasAspect() {\n    const [width, height] = this.context.viewport!.getCanvasSize();\n    return width / height;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/contextmenu/index.ts",
    "content": "import type { RuntimeContext } from '../../runtime/types';\nimport type { Element } from '../../types';\nimport type { IElementEvent } from '../../types/event';\nimport type { BasePluginOptions } from '../base-plugin';\nimport { BasePlugin } from '../base-plugin';\nimport { createPluginContainer, insertDOM } from '../utils/dom';\nimport type { Item } from './util';\nimport { CONTEXTMENU_CSS, getContentFromItems } from './util';\n/**\n * <zh/> 上下文菜单配置项\n *\n * <en/> Contextmenu options\n */\nexport interface ContextmenuOptions extends BasePluginOptions {\n  /**\n   * <zh/> 给菜单的 DOM 追加的类名\n   *\n   * <en/> The class name appended to the menu DOM for custom styles\n   * @defaultValue 'g6-contextmenu'\n   */\n  className?: string;\n  /**\n   * <zh/> 如何触发右键菜单\n   * - `'click'` : 点击触发\n   * - `'contextmenu'` : 右键触发\n   *\n   * <en/> How to trigger the context menu\n   * - `'click'` : Click trigger\n   * - `'contextmenu'` : Right-click trigger\n   * @defaultValue 'contextmenu'\n   */\n  trigger?: 'click' | 'contextmenu';\n  /**\n   * <zh/> 菜单显式 X、Y 方向的偏移量\n   *\n   * <en/> The offset X, y direction of the menu\n   * @defaultValue [4, 4]\n   */\n  offset?: [number, number];\n  /**\n   * <zh/> 当菜单被点击后，触发的回调方法\n   *\n   * <en/> The callback method triggered when the menu is clicked\n   */\n  onClick?: (value: string, target: HTMLElement, current: Element) => void;\n  /**\n   * <zh/> 返回菜单的项目列表，支持 `Promise` 类型的返回值。是 `getContent` 的快捷配置\n   *\n   * <en/> Return the list of menu items, support the `Promise` type return value. It is a shortcut configuration of `getContent`\n   */\n  getItems?: (event: IElementEvent) => Item[] | Promise<Item[]>;\n  /**\n   * <zh/> 返回菜单的内容，支持 `Promise` 类型的返回值，也可以使用 `getItems` 进行快捷配置\n   *\n   * <en/> Return the content of menu, support the `Promise` type return value, you can also use `getItems` for shortcut configuration\n   */\n  getContent?: (event: IElementEvent) => HTMLElement | string | Promise<HTMLElement | string>;\n  /**\n   * <zh/> 当 `getContent` 返回一个 `Promise` 时，使用的菜单内容\n   *\n   * <en/> The menu content when loading is used when getContent returns a Promise\n   */\n  loadingContent?: HTMLElement | string;\n  /**\n   * <zh/> 是否可用，通过参数判断是否支持右键菜单，默认是全部可用\n   *\n   * <en/> Whether the plugin is available, determine whether the right-click menu is supported through parameters, The default is all available\n   * @defaultValue true\n   */\n  enable?: boolean | ((event: IElementEvent) => boolean);\n}\n\n/**\n * <zh/> 上下文菜单\n *\n * <en/> Contextmenu\n * @remarks\n * <zh/> 上下文菜单，也被称为右键菜单，是当用户在某个特定区域上点击后出现的一个菜单。支持在点击前后，触发自定义事件。\n *\n * <en/> Contextmenu, also known as the right-click menu , is a menu that appears when a user clicks on a specific area. Supports triggering custom events before and after clicking.\n */\nexport class Contextmenu extends BasePlugin<ContextmenuOptions> {\n  static defaultOptions: Partial<ContextmenuOptions> = {\n    trigger: 'contextmenu',\n    offset: [4, 4],\n    loadingContent: '<div class=\"g6-contextmenu-loading\">Loading...</div>',\n    getContent: () => 'It is a empty context menu.',\n    enable: () => true,\n  };\n\n  private $element!: HTMLElement;\n\n  private targetElement: Element | null = null;\n\n  constructor(context: RuntimeContext, options: ContextmenuOptions) {\n    super(context, Object.assign({}, Contextmenu.defaultOptions, options));\n\n    this.initElement();\n    this.update(options);\n  }\n\n  private initElement() {\n    this.$element = createPluginContainer('contextmenu', false, { zIndex: '99' });\n    const { className } = this.options;\n    if (className) this.$element.classList.add(className);\n\n    const $container = this.context.canvas.getContainer();\n    $container!.appendChild(this.$element);\n\n    insertDOM('g6-contextmenu-css', 'style', {}, CONTEXTMENU_CSS, document.head);\n  }\n\n  /**\n   * <zh/> 显示上下文菜单\n   *\n   * <en/> Show the contextmenu\n   * @param event - <zh/> 元素指针事件 | <en/> Element pointer event\n   * @internal\n   */\n  public async show(event: IElementEvent) {\n    const { enable, offset } = this.options;\n\n    if ((typeof enable === 'function' && !enable(event)) || !enable) {\n      this.hide();\n      return;\n    }\n\n    const content = await this.getDOMContent(event);\n\n    if (content instanceof HTMLElement) {\n      this.$element.innerHTML = '';\n      this.$element.appendChild(content);\n    } else {\n      this.$element.innerHTML = content;\n    }\n\n    // NOTICE: 为什么事件中的 client 是相对浏览器，而不是画布容器？\n    const clientRect = this.context.graph.getCanvas().getContainer()!.getBoundingClientRect();\n\n    this.$element.style.left = `${event.client.x - clientRect.left + offset[0]}px`;\n    this.$element.style.top = `${event.client.y - clientRect.top + offset[1]}px`;\n    this.$element.style.display = 'block';\n\n    this.targetElement = event.target;\n  }\n\n  /**\n   * <zh/> 隐藏上下文菜单\n   *\n   * <en/> Hide the contextmenu\n   */\n  public hide() {\n    this.$element.style.display = 'none';\n    this.targetElement = null;\n  }\n\n  /**\n   * <zh/> 更新上下文菜单的配置项\n   *\n   * <en/> Update the contextmenu options\n   * @param options - <zh/> 配置项 | <en/> Options\n   * @internal\n   */\n  public update(options: Partial<ContextmenuOptions>) {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  /**\n   * <zh/> 销毁上下文菜单\n   *\n   * <en/> Destroy the contextmenu\n   * @internal\n   */\n  public destroy(): void {\n    this.unbindEvents();\n    super.destroy();\n    this.$element.remove();\n  }\n\n  private async getDOMContent(event: IElementEvent) {\n    const { getContent, getItems } = this.options;\n\n    if (getItems) {\n      return getContentFromItems(await getItems(event));\n    }\n    return await getContent(event);\n  }\n\n  private bindEvents() {\n    const { graph } = this.context;\n    const { trigger } = this.options;\n\n    graph.on(`canvas:${trigger}`, this.onTriggerEvent);\n    graph.on(`node:${trigger}`, this.onTriggerEvent);\n    graph.on(`edge:${trigger}`, this.onTriggerEvent);\n    graph.on(`combo:${trigger}`, this.onTriggerEvent);\n\n    document.addEventListener('click', this.onMenuItemClick);\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n    const { trigger } = this.options;\n\n    graph.off(`canvas:${trigger}`, this.onTriggerEvent);\n    graph.off(`node:${trigger}`, this.onTriggerEvent);\n    graph.off(`edge:${trigger}`, this.onTriggerEvent);\n    graph.off(`combo:${trigger}`, this.onTriggerEvent);\n\n    document.removeEventListener('click', this.onMenuItemClick);\n  }\n\n  private onTriggerEvent = (event: IElementEvent) => {\n    // `contextmenu` 事件默认会触发浏览器的右键菜单，需要阻止默认事件\n    // `click` 事件不需要阻止默认事件\n    event.preventDefault?.();\n    this.show(event);\n  };\n\n  private onMenuItemClick = (event: MouseEvent) => {\n    const { onClick, trigger } = this.options;\n    if (event.target instanceof HTMLElement) {\n      if (event.target.className.includes('g6-contextmenu-li')) {\n        const value = event.target.getAttribute('value') as string;\n        onClick?.(value, event.target, this.targetElement!);\n        this.hide();\n      }\n    }\n\n    if (trigger !== 'click') this.hide();\n  };\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/contextmenu/util.ts",
    "content": "/**\n * <zh/> 右键菜单显示项目。\n * <en/> The item of the right-click menu.\n */\nexport type Item = {\n  /**\n   * <zh/> 菜单项显示的名字。\n   * <en/> The name of the menu item.\n   */\n  name: string;\n  /**\n   * <zh/> 菜单项对应的值。\n   * <en/> The value corresponding to the menu item.\n   */\n  value: string;\n};\n\n/**\n * Get the content of the right-click menu.\n * @param items - context menu items\n * @returns HTML string\n */\nexport function getContentFromItems(items: Item[]) {\n  return `\n    <ul class=\"g6-contextmenu-ul\">\n      ${items.map((item) => `<li  class=\"g6-contextmenu-li\" value=\"${item.value}\">${item.name}</li>`).join('')}\n    </ul>\n  `;\n}\n\n/**\n * Style of the right-click menu, same with `tooltip`.\n */\nexport const CONTEXTMENU_CSS = `\n  .g6-contextmenu {\n    font-size: 12px;\n    background-color: rgba(255, 255, 255, 0.96);\n    border-radius: 4px;\n    overflow: hidden;\n    box-shadow: rgba(0, 0, 0, 0.12) 0px 6px 12px 0px;\n    transition: visibility 0.2s cubic-bezier(0.23, 1, 0.32, 1) 0s, left 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s, top 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s;\n  }\n\n  .g6-contextmenu-ul {\n    max-width: 256px;\n    min-width: 96px;\n    list-style: none;\n    padding: 0;\n    margin: 0;\n  }\n\n  .g6-contextmenu-li {\n    padding: 8px 12px;\n    cursor: pointer;\n    user-select: none;\n  }\n\n  .g6-contextmenu-li:hover {\n    background-color: #f5f5f5;\n    cursor: pointer;\n  }\n`;\n"
  },
  {
    "path": "packages/g6/src/plugins/edge-bundling/index.ts",
    "content": "import { isEmpty } from '@antv/util';\nimport { GraphEvent } from '../../constants';\nimport type { RuntimeContext } from '../../runtime/types';\nimport type { EdgeData } from '../../spec';\nimport type { ID, Point } from '../../types';\nimport { getPolylinePath } from '../../utils/edge';\nimport { idOf } from '../../utils/id';\nimport { positionOf } from '../../utils/position';\nimport { add, distance, divide, dot, multiply, subtract, toVector2 } from '../../utils/vector';\nimport type { BasePluginOptions } from '../base-plugin';\nimport { BasePlugin } from '../base-plugin';\n\n/**\n * <zh/> 边绑定插件的配置项\n *\n * <en/> Edge bundling options\n */\nexport interface EdgeBundlingOptions extends BasePluginOptions {\n  /**\n   * <zh/> 边的强度\n   *\n   * <en/> The strength of the edge\n   * @defaultValue 0.1\n   */\n  K?: number;\n  /**\n   * <zh/> 初始步长。在后续的周期中，步长将双倍递增\n   *\n   * <en/> An initial step size. In subsequent cycles, the step size will double incrementally\n   * @defaultValue 0.1\n   */\n  lambda?: number;\n  /**\n   * <zh/> 模拟周期数\n   *\n   * <en/> The number of simulation cycles\n   * @defaultValue 6\n   */\n  cycles?: number;\n  /**\n   * <zh/> 初始切割点数。在后续的周期中，切割点数将根据 `divRate` 逐步递增\n   *\n   * <en/> An initial number of subdivision points for each edge. In subsequent cycles, the number of subdivision points will increase gradually according to `divRate`\n   * @defaultValue 1\n   */\n  divisions?: number;\n  /**\n   * <zh/> 切割点数增长率\n   *\n   * <en/> The rate at which the number of subdivision points increases\n   * @defaultValue 2\n   */\n  divRate?: number;\n  /**\n   * <zh/> 指定在第一个周期中执行的迭代次数。在后续的周期中，迭代次数将根据 `iterRate` 逐步递减\n   *\n   * <en/> The number of iteration steps during the first cycle. In subsequent cycles, the number of iterations will decrease gradually according to `iterRate`\n   * @defaultValue 90\n   */\n  iterations?: number;\n  /**\n   * <zh/> 迭代次数递减率\n   *\n   * <en/> The rate at which the number of iterations decreases\n   * @defaultValue 2 / 3\n   */\n  iterRate?: number;\n  /**\n   * <zh/> 边兼容性阈值，决定了哪些边应该被绑定在一起\n   *\n   * <en/> Edge compatibility threshold, which determines which edges should be bundled together\n   * @defaultValue 0.6\n   */\n  bundleThreshold?: number;\n}\n\n/**\n * <zh/> 边绑定\n *\n * <en/> Edge bundling\n * @remarks\n * <zh/> 边绑定（Edge Bundling）是一种图可视化技术，用于减少复杂网络图中的视觉混乱，并揭示图中的高级别模式和结构。其思想是将相邻的边捆绑在一起。\n *\n * <zh/> G6 中提供的边绑定插件是基于 FEDB（Force-Directed Edge Bundling for Graph Visualization）一文的实现：将边建模为可以相互吸引的柔性弹簧，通过自组织的方式进行捆绑。\n *\n * <en/> Edge bundling is a graph visualization technique used to reduce visual clutter in complex network graphs and reveal high-level patterns and structures in the graph. The idea is to bundle adjacent edges together.\n *\n * <en/> The edge bundling plugin provided in G6 is based on the implementation of the paper FEDB (Force-Directed Edge Bundling for Graph Visualization): modeling edges as flexible springs that can attract each other and bundling them in a self-organizing way.\n */\nexport class EdgeBundling extends BasePlugin<EdgeBundlingOptions> {\n  static defaultOptions: Partial<EdgeBundlingOptions> = {\n    K: 0.1,\n    lambda: 0.1,\n    divisions: 1,\n    divRate: 2,\n    cycles: 6,\n    iterations: 90,\n    iterRate: 2 / 3,\n    bundleThreshold: 0.6,\n  };\n\n  constructor(context: RuntimeContext, options?: EdgeBundlingOptions) {\n    super(context, Object.assign({}, EdgeBundling.defaultOptions, options));\n    this.bindEvents();\n  }\n\n  private edgeBundles: Record<ID, EdgeData[]> = {};\n\n  private edgePoints: Record<ID, Point[]> = {};\n\n  private get nodeMap(): Record<ID, Point> {\n    const nodes = this.context.model.getNodeData();\n    return Object.fromEntries(nodes.map((node) => [idOf(node), toVector2(positionOf(node))]));\n  }\n\n  private divideEdges(divisions: number) {\n    const edges = this.context.model.getEdgeData();\n\n    edges.forEach((edge) => {\n      const edgeId = idOf(edge);\n      this.edgePoints[edgeId] ||= [];\n\n      const source = this.nodeMap[edge.source];\n      const target = this.nodeMap[edge.target];\n\n      if (divisions === 1) {\n        this.edgePoints[edgeId].push(source);\n        this.edgePoints[edgeId].push(divide(add(source, target), 2));\n        this.edgePoints[edgeId].push(target);\n      } else {\n        const edgeLength =\n          this.edgePoints[edgeId].length === 0\n            ? // edge is a straight line\n              distance(source, target)\n            : // edge is a polyline\n              getEdgeLength(this.edgePoints[edgeId]);\n\n        const divisionLength = edgeLength / (divisions + 1);\n        let currentDivisionLength = divisionLength;\n\n        const newEdgePoints: Point[] = [source];\n\n        for (let i = 1; i < this.edgePoints[edgeId].length; i++) {\n          const prevEp = this.edgePoints[edgeId][i - 1];\n          const ep = this.edgePoints[edgeId][i];\n          let oriDivisionLength = distance(ep, prevEp);\n\n          while (oriDivisionLength > currentDivisionLength) {\n            const ratio = currentDivisionLength / oriDivisionLength;\n            const edgePoint = add(prevEp, multiply(subtract(ep, prevEp), ratio));\n            newEdgePoints.push(edgePoint);\n\n            oriDivisionLength -= currentDivisionLength;\n            currentDivisionLength = divisionLength;\n          }\n\n          currentDivisionLength -= oriDivisionLength;\n        }\n\n        newEdgePoints.push(target);\n        this.edgePoints[edgeId] = newEdgePoints;\n      }\n    });\n  }\n\n  private getVectorPosition(edge: EdgeData): VectorPosition {\n    const source = this.nodeMap[edge.source];\n    const target = this.nodeMap[edge.target];\n    const [vx, vy] = subtract(target, source);\n    const length = distance(source, target);\n    return { source, target, vx, vy, length };\n  }\n\n  private measureEdgeCompatibility(edge1: EdgeData, edge2: EdgeData) {\n    const vector1 = this.getVectorPosition(edge1);\n    const vector2 = this.getVectorPosition(edge2);\n\n    const ac = getAngleCompatibility(vector1, vector2);\n    const sc = getScaleCompatibility(vector1, vector2);\n    const pc = getPositionCompatibility(vector1, vector2);\n    const vc = getVisibilityCompatibility(vector1, vector2);\n\n    return ac * sc * pc * vc;\n  }\n\n  private getEdgeBundles() {\n    const edgeBundles: Record<ID, EdgeData[]> = {};\n    const bundleThreshold = this.options.bundleThreshold;\n    const edges = this.context.model.getEdgeData();\n\n    edges.forEach((edge1, i) => {\n      edges.forEach((edge2, j) => {\n        if (j <= i) return;\n\n        const compatibility = this.measureEdgeCompatibility(edge1, edge2);\n        if (compatibility >= bundleThreshold) {\n          edgeBundles[idOf(edge1)] ||= [];\n          edgeBundles[idOf(edge1)].push(edge2);\n          edgeBundles[idOf(edge2)] ||= [];\n          edgeBundles[idOf(edge2)].push(edge1);\n        }\n      });\n    });\n    return edgeBundles;\n  }\n\n  private getSpringForce(divisions: { pre: Point; cur: Point; next: Point }, kp: number): Point {\n    const { pre, cur, next } = divisions;\n    return multiply(subtract(add(pre, next), multiply(cur, 2)), kp);\n  }\n\n  private getElectrostaticForce(pidx: number, edge: EdgeData): Point {\n    if (isEmpty(this.edgeBundles)) {\n      this.edgeBundles = this.getEdgeBundles();\n    }\n\n    const edgeBundle = this.edgeBundles[idOf(edge)];\n    let resForce: Point = [0, 0];\n\n    edgeBundle?.forEach((eb) => {\n      const p1 = this.edgePoints[idOf(eb)][pidx];\n      const p2 = this.edgePoints[idOf(edge)][pidx];\n      const force = subtract(p1, p2);\n      const length = distance(p1, p2);\n      resForce = add(resForce, multiply(force, 1 / length));\n    });\n\n    return resForce;\n  }\n\n  private getEdgeForces(edge: EdgeData, divisions: number, lambda: number): Point[] {\n    const source = this.nodeMap[edge.source];\n    const target = this.nodeMap[edge.target];\n    const kp = this.options.K / (distance(source, target) * (divisions + 1));\n    const edgePointForces: Point[] = [[0, 0]];\n    const edgeId = idOf(edge);\n\n    for (let i = 1; i < divisions; i++) {\n      const spring = this.getSpringForce(\n        {\n          pre: this.edgePoints[edgeId][i - 1],\n          cur: this.edgePoints[edgeId][i],\n          next: this.edgePoints[edgeId][i + 1] || [0, 0],\n        },\n        kp,\n      );\n      const electrostatic = this.getElectrostaticForce(i, edge);\n      edgePointForces.push(multiply(add(spring, electrostatic), lambda));\n    }\n\n    edgePointForces.push([0, 0]);\n\n    return edgePointForces;\n  }\n\n  protected onBundle = () => {\n    const { model, element } = this.context;\n    const edges = model.getEdgeData();\n    this.divideEdges(this.options.divisions);\n\n    const { cycles, iterRate, divRate } = this.options;\n    let { lambda, divisions, iterations } = this.options;\n    for (let i = 0; i < cycles; i++) {\n      for (let j = 0; j < iterations; j++) {\n        const forces: Record<ID, Point[]> = {};\n        edges.forEach((edge) => {\n          if (edge.source === edge.target) return;\n          const edgeId = idOf(edge);\n          forces[edgeId] = this.getEdgeForces(edge, divisions, lambda);\n\n          for (let p = 0; p < divisions + 1; p++) {\n            this.edgePoints[edgeId] ||= [];\n            this.edgePoints[edgeId][p] = add(this.edgePoints[edgeId][p], forces[edgeId][p]);\n          }\n        });\n      }\n\n      // parameters for next cycle\n      lambda /= 2;\n      divisions *= divRate;\n      iterations *= iterRate;\n      this.divideEdges(divisions);\n    }\n\n    edges.forEach((edge) => {\n      const edgeId = idOf(edge);\n      const edgeEl = element!.getElement(edgeId);\n      edgeEl?.update({ d: getPolylinePath(this.edgePoints[edgeId]) });\n    });\n  };\n\n  private bindEvents() {\n    const { graph } = this.context;\n\n    graph.on(GraphEvent.AFTER_RENDER, this.onBundle);\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n\n    graph.off(GraphEvent.AFTER_RENDER, this.onBundle);\n  }\n\n  public destroy(): void {\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n\ninterface VectorPosition {\n  source: Point;\n  target: Point;\n  vx: number;\n  vy: number;\n  length: number;\n}\n\n// The larger the angle between edges P and Q, the smaller Ca(P,Q).\n// Ca(P,Q) is 0 if P and Q are orthogonal and 1 if P and Q are parallel.\nconst getAngleCompatibility = (p: VectorPosition, q: VectorPosition): number => {\n  return Math.abs(dot([p.vx, p.vy], [q.vx, q.vy]) / (p.length * q.length));\n};\n\n// Cs(P,Q) is 1 if P and Q have equal length and approaches 0 if the ratio between the longest and the shortest edge approaches ∞.\nconst getScaleCompatibility = (p: VectorPosition, q: VectorPosition): number => {\n  const aLength = (p.length + q.length) / 2;\n  return 2 / (aLength / Math.min(p.length, q.length) + Math.max(p.length, q.length) / aLength);\n};\n\n// Cp(P,Q) is 1 if Pm and Qm coincide and approaches 0 if ||Pm −Qm|| approaches ∞.\nconst getPositionCompatibility = (p: VectorPosition, q: VectorPosition): number => {\n  const aLength = (p.length + q.length) / 2;\n\n  const pMid = divide(add(p.source, p.target), 2);\n  const qMid = divide(add(q.source, q.target), 2);\n\n  return aLength / (aLength + distance(pMid, qMid));\n};\n\nconst projectPointToEdge = (p: Point, e: VectorPosition): Point => {\n  if (e.source[0] === e.target[0]) return [e.source[0], p[1]];\n  if (e.source[1] === e.target[1]) return [p[0], e.source[1]];\n  const k = (e.source[1] - e.target[1]) / (e.source[0] - e.target[0]);\n  const x = (k * k * e.source[0] + k * (p[1] - e.source[1]) + p[0]) / (k * k + 1);\n  const y = k * (x - e.source[0]) + e.source[1];\n  return [x, y];\n};\n\nconst getEdgeVisibility = (p: VectorPosition, q: VectorPosition): number => {\n  const is = projectPointToEdge(q.source, p);\n  const it = projectPointToEdge(q.target, p);\n  const iMid = divide(add(is, it), 2);\n  const pMid = divide(add(p.source, p.target), 2);\n  if (distance(is, it) === 0) return 0;\n  return Math.max(0, 1 - (2 * distance(pMid, iMid)) / distance(is, it));\n};\n\nconst getVisibilityCompatibility = (p: VectorPosition, q: VectorPosition): number => {\n  return Math.min(getEdgeVisibility(p, q), getEdgeVisibility(q, p));\n};\n\n/**\n * Calculate the length of a polyline\n * @param points - The points of the polyline\n * @returns The length of the polyline\n */\nconst getEdgeLength = (points: Point[]): number => {\n  let length = 0;\n  for (let i = 1; i < points.length; i++) {\n    length += distance(points[i], points[i - 1]);\n  }\n  return length;\n};\n"
  },
  {
    "path": "packages/g6/src/plugins/edge-filter-lens/index.ts",
    "content": "import { CommonEvent } from '../../constants';\nimport { Circle, type CircleStyleProps } from '../../elements';\nimport type { RuntimeContext } from '../../runtime/types';\nimport type { EdgeData, GraphData, NodeData } from '../../spec';\nimport type { EdgeStyle } from '../../spec/element/edge';\nimport type { NodeStyle } from '../../spec/element/node';\nimport type {\n  Element,\n  ElementDatum,\n  ElementType,\n  ID,\n  IDragEvent,\n  IPointerEvent,\n  Point,\n  PointObject,\n} from '../../types';\nimport { idOf } from '../../utils/id';\nimport { parsePoint, toPointObject } from '../../utils/point';\nimport { positionOf } from '../../utils/position';\nimport { distance } from '../../utils/vector';\nimport type { BasePluginOptions } from '../base-plugin';\nimport { BasePlugin } from '../base-plugin';\n\n/**\n * <zh/> 边过滤镜插件配置项\n *\n * <en/> Edge filter lens plugin options\n */\nexport interface EdgeFilterLensOptions extends BasePluginOptions {\n  /**\n   * <zh/> 移动透镜的方式\n   * - `'pointermove'`：始终跟随鼠标移动\n   * - `'click'`：鼠标点击时透镜移动\n   * - `'drag'`：拖拽透镜\n   *\n   * <en/> The way to move the lens\n   * - `'pointermove'`: always follow the mouse movement\n   * - `'click'`: move the lens when the mouse clicks\n   * - `'drag'`: drag the lens\n   * @defaultValue 'pointermove'\n   */\n  trigger?: 'pointermove' | 'click' | 'drag';\n  /**\n   * <zh/> 透镜的半径\n   *\n   * <en/> The radius of the lens\n   * @defaultValue 60\n   */\n  r?: number;\n  /**\n   * <zh/> 透镜的最大半径。只有在 `scaleRBy` 为 `wheel` 时生效\n   *\n   * <en/> The maximum radius of the lens. Only valid when `scaleRBy` is `wheel`\n   * @defaultValue canvas 宽高最小值的一半\n   */\n  maxR?: number;\n  /**\n   * <zh/> 透镜的最小半径。只有在 `scaleRBy` 为 `wheel` 时生效\n   *\n   * <en/> The minimum radius of the lens. Only valid when `scaleRBy` is `wheel`\n   * @defaultValue 0\n   */\n  minR?: number;\n  /**\n   * <zh/> 缩放透镜半径的方式\n   * - `'wheel'`：通过滚轮缩放透镜的半径\n   *\n   * <en/> The way to scale the radius of the lens\n   * - `'wheel'`: scale the radius of the lens by the wheel\n   * @defaultValue `'wheel'`\n   */\n  scaleRBy?: 'wheel';\n  /**\n   * <zh/> 边显示的条件\n   * - `'both'`：只有起始节点和目标节点都在透镜中时，边才会显示\n   * - `'source'`：只有起始节点在透镜中时，边才会显示\n   * - `'target'`：只有目标节点在透镜中时，边才会显示\n   * - `'either'`：只要起始节点或目标节点有一个在透镜中时，边就会显示\n   *\n   * <en/> The condition for displaying the edge\n   * - `'both'`: The edge is displayed only when both the source node and the target node are in the lens\n   * - `'source'`: The edge is displayed only when the source node is in the lens\n   * - `'target'`: The edge is displayed only when the target node is in the lens\n   * - `'either'`: The edge is displayed when either the source node or the target node is in the lens\n   * @defaultValue 'both'\n   */\n  nodeType?: 'both' | 'source' | 'target' | 'either';\n  /**\n   * <zh/> 过滤出始终不在透镜中显示的元素\n   *\n   * <en/> Filter elements that are never displayed in the lens\n   * @param id - <zh/> 元素的 id | <en/> The id of the element\n   * @param elementType - <zh/> 元素的类型 | <en/> The type of the element\n   * @returns <zh/> 是否显示 | <en/> Whether to display\n   */\n  filter?: (id: ID, elementType: ElementType) => boolean;\n  /**\n   * <zh/> 透镜的样式\n   *\n   * <en/> The style of the lens\n   */\n  style?: Partial<CircleStyleProps>;\n  /**\n   * <zh/> 在透镜中节点的样式\n   *\n   * <en/> The style of the nodes displayed in the lens\n   */\n  nodeStyle?: NodeStyle | ((datum: NodeData) => NodeStyle);\n  /**\n   * <zh/> 在透镜中边的样式\n   *\n   * <en/> The style of the edges displayed in the lens\n   */\n  edgeStyle?: EdgeStyle | ((datum: EdgeData) => EdgeStyle);\n  /**\n   * <zh/> 是否阻止默认事件\n   *\n   * <en/> Whether to prevent the default event\n   * @defaultValue true\n   */\n  preventDefault?: boolean;\n}\n\nconst defaultLensStyle: Exclude<CircleStyleProps, 'r'> = {\n  fill: '#fff',\n  fillOpacity: 1,\n  lineWidth: 1,\n  stroke: '#000',\n  strokeOpacity: 0.8,\n  zIndex: -Infinity,\n};\n\nconst DELTA = 0.05;\n\n/**\n * <zh/> 边过滤镜插件\n *\n * <en/> Edge filter lens plugin\n * @remarks\n * <zh/> 边过滤镜可以将关注的边保留在过滤镜范围内，其他边将在该范围内不显示。\n *\n * <en/> EdgeFilterLens can keep the focused edges within the lens range, while other edges will not be displayed within that range.\n */\nexport class EdgeFilterLens extends BasePlugin<EdgeFilterLensOptions> {\n  static defaultOptions: Partial<EdgeFilterLensOptions> = {\n    trigger: 'pointermove',\n    r: 60,\n    nodeType: 'both',\n    filter: () => true,\n    style: { lineWidth: 2 },\n    nodeStyle: { label: false },\n    edgeStyle: { label: true },\n    scaleRBy: 'wheel',\n    preventDefault: true,\n  };\n\n  constructor(context: RuntimeContext, options: EdgeFilterLensOptions) {\n    super(context, Object.assign({}, EdgeFilterLens.defaultOptions, options));\n    this.bindEvents();\n  }\n\n  private lens!: Circle;\n\n  private shapes = new Map<ID, Element>();\n\n  private r = this.options.r;\n\n  private get canvas() {\n    return this.context.canvas.getLayer('transient');\n  }\n\n  private get isLensOn() {\n    return this.lens && !this.lens.destroyed;\n  }\n\n  protected onEdgeFilter = (event: IPointerEvent) => {\n    if (this.options.trigger === 'drag' && this.isLensOn) return;\n\n    const origin = parsePoint(event.canvas as PointObject);\n    this.renderLens(origin);\n    this.renderFocusElements();\n  };\n\n  private renderLens = (origin: Point) => {\n    const style = Object.assign({}, defaultLensStyle, this.options.style);\n\n    if (!this.isLensOn) {\n      this.lens = new Circle({ style });\n      this.canvas.appendChild(this.lens);\n    }\n\n    Object.assign(style, toPointObject(origin), { size: this.r * 2 });\n\n    this.lens.update(style);\n  };\n\n  private getFilterData = (): Required<GraphData> => {\n    const { filter } = this.options;\n    const { model } = this.context;\n    const data = model.getData();\n\n    if (!filter) return data;\n\n    const { nodes, edges, combos } = data;\n\n    return {\n      nodes: nodes.filter((node) => filter(idOf(node), 'node')),\n      edges: edges.filter((edge) => filter(idOf(edge), 'edge')),\n      combos: combos.filter((combo) => filter(idOf(combo), 'combo')),\n    };\n  };\n\n  private getFocusElements = (origin: Point) => {\n    const { nodes, edges } = this.getFilterData();\n\n    const focusNodes = nodes.filter((datum) => distance(positionOf(datum), origin) < this.r);\n    const focusNodeIds = focusNodes.map((node) => idOf(node));\n\n    const focusEdges = edges.filter((datum) => {\n      const { source, target } = datum;\n      const isSourceFocus = focusNodeIds.includes(source);\n      const isTargetFocus = focusNodeIds.includes(target);\n\n      switch (this.options.nodeType) {\n        case 'both':\n          return isSourceFocus && isTargetFocus;\n        case 'either':\n          return isSourceFocus !== isTargetFocus;\n        case 'source':\n          return isSourceFocus && !isTargetFocus;\n        case 'target':\n          return !isSourceFocus && isTargetFocus;\n        default:\n          return false;\n      }\n    });\n\n    return { nodes: focusNodes, edges: focusEdges };\n  };\n\n  private renderFocusElements = () => {\n    const { element, graph } = this.context;\n    if (!this.isLensOn) return;\n\n    const origin = this.lens.getCenter();\n    const { nodes, edges } = this.getFocusElements(origin);\n\n    const ids = new Set<ID>();\n\n    const iterate = (datum: ElementDatum) => {\n      const id = idOf(datum);\n      ids.add(id);\n\n      const shape = element!.getElement(id);\n      if (!shape) return;\n\n      const cloneShape = this.shapes.get(id) || shape.cloneNode();\n\n      cloneShape.setPosition(shape.getPosition());\n      cloneShape.id = shape.id;\n\n      if (!this.shapes.has(id)) {\n        this.canvas.appendChild(cloneShape);\n        this.shapes.set(id, cloneShape);\n      } else {\n        Object.entries(shape.attributes).forEach(([key, value]) => {\n          if (cloneShape.style[key] !== value) cloneShape.style[key] = value;\n        });\n      }\n\n      const elementType = graph.getElementType(id) as Exclude<ElementType, 'combo'>;\n      const style = this.getElementStyle(elementType, datum);\n\n      // @ts-ignore\n      cloneShape.update(style);\n    };\n\n    nodes.forEach(iterate);\n    edges.forEach(iterate);\n\n    this.shapes.forEach((shape, id) => {\n      if (!ids.has(id)) {\n        shape.destroy();\n        this.shapes.delete(id);\n      }\n    });\n  };\n\n  private getElementStyle(elementType: ElementType, datum: ElementDatum) {\n    const styler = elementType === 'node' ? this.options.nodeStyle : this.options.edgeStyle;\n    if (typeof styler === 'function') return styler(datum as any);\n    return styler;\n  }\n\n  private scaleRByWheel = (event: WheelEvent) => {\n    if (this.options.preventDefault) event.preventDefault();\n    const { clientX, clientY, deltaX, deltaY } = event;\n    const { graph, canvas } = this.context;\n    const scaleOrigin = graph.getCanvasByClient([clientX, clientY]);\n    const origin = this.lens?.getCenter();\n\n    if (!this.isLensOn || distance(scaleOrigin, origin) > this.r) {\n      return;\n    }\n\n    const { maxR, minR } = this.options;\n    const ratio = deltaX + deltaY > 0 ? 1 / (1 - DELTA) : 1 - DELTA;\n    const canvasR = Math.min(...canvas.getSize()) / 2;\n    this.r = Math.max(minR || 0, Math.min(maxR || canvasR, this.r * ratio));\n\n    this.renderLens(origin);\n    this.renderFocusElements();\n  };\n\n  get graphDom() {\n    return this.context.graph.getCanvas().getContextService().getDomElement();\n  }\n\n  private isLensDragging = false;\n\n  private onDragStart = (event: IDragEvent) => {\n    const dragOrigin = parsePoint(event.canvas as PointObject);\n    const origin = this.lens?.getCenter();\n\n    if (!this.isLensOn || distance(dragOrigin, origin) > this.r) return;\n\n    this.isLensDragging = true;\n  };\n\n  private onDrag = (event: IDragEvent) => {\n    if (!this.isLensDragging) return;\n\n    const dragOrigin = parsePoint(event.canvas as PointObject);\n    this.renderLens(dragOrigin);\n    this.renderFocusElements();\n  };\n\n  private onDragEnd = () => {\n    this.isLensDragging = false;\n  };\n\n  private bindEvents() {\n    const { graph } = this.context;\n    const { trigger, scaleRBy } = this.options;\n\n    const canvas = graph.getCanvas().getLayer();\n\n    if (['click', 'drag'].includes(trigger)) {\n      canvas.addEventListener(CommonEvent.CLICK, this.onEdgeFilter);\n    }\n\n    if (trigger === 'pointermove') {\n      canvas.addEventListener(CommonEvent.POINTER_MOVE, this.onEdgeFilter);\n    } else if (trigger === 'drag') {\n      canvas.addEventListener(CommonEvent.DRAG_START, this.onDragStart);\n      canvas.addEventListener(CommonEvent.DRAG, this.onDrag);\n      canvas.addEventListener(CommonEvent.DRAG_END, this.onDragEnd);\n    }\n\n    if (scaleRBy === 'wheel') {\n      this.graphDom?.addEventListener(CommonEvent.WHEEL, this.scaleRByWheel, { passive: false });\n    }\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n    const { trigger, scaleRBy } = this.options;\n    const canvas = graph.getCanvas().getLayer();\n\n    if (['click', 'drag'].includes(trigger)) {\n      canvas.removeEventListener(CommonEvent.CLICK, this.onEdgeFilter);\n    }\n\n    if (trigger === 'pointermove') {\n      canvas.removeEventListener(CommonEvent.POINTER_MOVE, this.onEdgeFilter);\n    } else if (trigger === 'drag') {\n      canvas.removeEventListener(CommonEvent.DRAG_START, this.onDragStart);\n      canvas.removeEventListener(CommonEvent.DRAG, this.onDrag);\n      canvas.removeEventListener(CommonEvent.DRAG_END, this.onDragEnd);\n    }\n\n    if (scaleRBy === 'wheel') {\n      this.graphDom?.removeEventListener(CommonEvent.WHEEL, this.scaleRByWheel);\n    }\n  }\n\n  public update(options: Partial<EdgeFilterLensOptions>) {\n    this.unbindEvents();\n    super.update(options);\n    this.r = options.r ?? this.r;\n    this.bindEvents();\n  }\n\n  public destroy() {\n    this.unbindEvents();\n    if (this.isLensOn) {\n      this.lens.destroy();\n    }\n    this.shapes.forEach((shape, id) => {\n      shape.destroy();\n      this.shapes.delete(id);\n    });\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/fisheye/index.ts",
    "content": "import { pick } from '@antv/util';\nimport { CommonEvent } from '../../constants';\nimport type { CircleStyleProps } from '../../elements';\nimport { Circle } from '../../elements';\nimport type { RuntimeContext } from '../../runtime/types';\nimport type { NodeData } from '../../spec';\nimport type { NodeStyle } from '../../spec/element/node';\nimport type { ID, IDragEvent, IPointerEvent, Node, Point, PointObject } from '../../types';\nimport { arrayDiff } from '../../utils/diff';\nimport { idOf } from '../../utils/id';\nimport { parsePoint, toPointObject } from '../../utils/point';\nimport { positionOf } from '../../utils/position';\nimport { distance } from '../../utils/vector';\nimport type { BasePluginOptions } from '../base-plugin';\nimport { BasePlugin } from '../base-plugin';\n\n/**\n * <zh/> 鱼眼放大镜插件配置项\n *\n * <en/> Fisheye Plugin Options\n */\nexport interface FisheyeOptions extends BasePluginOptions {\n  /**\n   * <zh/> 移动鱼眼放大镜的方式\n   * - `'pointermove'`：始终跟随鼠标移动\n   * - `'click'`：鼠标点击时移动\n   * - `'drag'`：拖拽移动\n   *\n   * <en/> The way to move the fisheye lens\n   * - `'pointermove'`: always follow the mouse movement\n   * - `'click'`: move when the mouse is clicked\n   * - `'drag'`: move by dragging\n   * @defaultValue `'pointermove'`\n   */\n  trigger?: 'pointermove' | 'drag' | 'click';\n  /**\n   * <zh/> 鱼眼放大镜半径\n   *\n   * <en/> The radius of the fisheye lens\n   * @remarks\n   * <img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*unAvQqAb_NMAAAAAAAAAAAAADmJ7AQ/original\" width=\"200\" />\n   * @defaultValue 120\n   */\n  r?: number;\n  /**\n   * <zh/> 鱼眼放大镜可调整的最大半径，配合 `scaleRBy` 使用\n   *\n   * <en/> The maximum radius that the fisheye lens can be adjusted, used with `scaleRBy`\n   * @defaultValue 画布宽高的最小值的一半\n   */\n  maxR?: number;\n  /**\n   * <zh/> 鱼眼放大镜可调整的最小半径，配合 `scaleRBy` 使用\n   *\n   * <en/> The minimum radius that the fisheye lens can be adjusted, used with `scaleRBy`\n   * @defaultValue 0\n   */\n  minR?: number;\n  /**\n   * <zh/> 调整鱼眼放大镜范围半径的方式\n   * - `'wheel'`：滚轮调整\n   * - `'drag'`：拖拽调整\n   *\n   * <en/> The way to adjust the range radius of the fisheye lens\n   * - `'wheel'`: adjust by wheel\n   * - `'drag'`: adjust by drag\n   * @remarks\n   * <zh/> 如果 `trigger`、`scaleRBy` 和 `scaleDBy` 同时设置为 `'drag'`，优先级顺序为 `trigger` > `scaleRBy` > `scaleDBy`，只会为优先级最高的配置项绑定拖拽事件。同理，如果 `scaleRBy` 和 `scaleDBy` 同时设置为 `'wheel'`，只会为 `scaleRBy` 绑定滚轮事件\n   *\n   * <en/> If `trigger`, `scaleRBy`, and `scaleDBy` are set to `'drag'` at the same time, the priority order is `trigger` > `scaleRBy` > `scaleDBy`, and only the configuration item with the highest priority will be bound to the drag event. Similarly, if `scaleRBy` and `scaleDBy` are set to `'wheel'` at the same time, only `scaleRBy` will be bound to the wheel event\n   */\n  scaleRBy?: 'wheel' | 'drag';\n  /**\n   * <zh/> 畸变因子\n   *\n   * <en/> Distortion factor\n   * @remarks\n   * <img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*4ITFR7GOl8UAAAAAAAAAAAAADmJ7AQ/original\" width=\"200\" />\n   * @defaultValue 1.5\n   */\n  d?: number;\n  /**\n   * <zh/> 鱼眼放大镜可调整的最大畸变因子，配合 `scaleDBy` 使用\n   *\n   * <en/> The maximum distortion factor that the fisheye lens can be adjusted, used with `scaleDBy`\n   * @defaultValue 5\n   */\n  maxD?: number;\n  /**\n   * <zh/> 鱼眼放大镜可调整的最小畸变因子，配合 `scaleDBy` 使用\n   *\n   * <en/> The minimum distortion factor that the fisheye lens can be adjusted, used with `scaleDBy`\n   * @defaultValue 0\n   */\n  minD?: number;\n  /**\n   * <zh/> 调整鱼眼放大镜畸变因子的方式\n   * - `'wheel'`：滚轮调整\n   * - `'drag'`：拖拽调整\n   *\n   * <en/> The way to adjust the distortion factor of the fisheye lens\n   * - `'wheel'`: adjust by wheel\n   * - `'drag'`: adjust by drag\n   */\n  scaleDBy?: 'wheel' | 'drag';\n  /**\n   * <zh/> 是否在鱼眼放大镜中显示畸变因子数值\n   *\n   * <en/> Whether to display the value of the distortion factor in the fisheye lens\n   * @defaultValue true\n   */\n  showDPercent?: boolean;\n  /**\n   * <zh/> 鱼眼放大镜样式\n   *\n   * <en/> Fisheye Lens Style\n   */\n  style?: Partial<CircleStyleProps>;\n  /**\n   * <zh/> 在鱼眼放大镜中的节点样式\n   *\n   * <en/> Node style in the fisheye lens\n   */\n  nodeStyle?: NodeStyle | ((datum: NodeData) => NodeStyle);\n  /**\n   * <zh/> 是否阻止默认事件\n   *\n   * <en/> Whether to prevent the default event\n   * @defaultValue true\n   */\n  preventDefault?: boolean;\n}\n\nconst defaultLensStyle: Exclude<CircleStyleProps, 'r'> = {\n  fill: '#ccc',\n  fillOpacity: 0.1,\n  lineWidth: 2,\n  stroke: '#000',\n  strokeOpacity: 0.8,\n  labelFontSize: 12,\n};\n\nconst R_DELTA = 0.05;\nconst D_DELTA = 0.1;\n\n/**\n * <zh/> 鱼眼放大镜\n *\n * <en/> Fisheye Distortion\n * @remarks\n * <zh/> Fisheye 鱼眼放大镜是为 focus+context 的探索场景设计的，它能够保证在放大关注区域的同时，保证上下文以及上下文与关注中心的关系不丢失。\n *\n * <en/> Fisheye is designed for focus+context exploration, it keeps the context and the relationships between context and the focus while magnifying the focus area.\n */\nexport class Fisheye extends BasePlugin<FisheyeOptions> {\n  static defaultOptions: Partial<FisheyeOptions> = {\n    trigger: 'pointermove',\n    r: 120,\n    d: 1.5,\n    maxD: 5,\n    minD: 0,\n    showDPercent: true,\n    style: {},\n    nodeStyle: { label: true },\n    preventDefault: true,\n  };\n\n  constructor(context: RuntimeContext, options: FisheyeOptions) {\n    super(context, Object.assign({}, Fisheye.defaultOptions, options));\n    this.bindEvents();\n  }\n\n  private lens!: Circle;\n\n  private r = this.options.r;\n  private d = this.options.d;\n\n  private get canvas() {\n    return this.context.canvas.getLayer('transient');\n  }\n\n  private get isLensOn() {\n    return this.lens && !this.lens.destroyed;\n  }\n\n  protected onCreateFisheye = (event: IPointerEvent) => {\n    if (this.options.trigger === 'drag' && this.isLensOn) return;\n\n    const origin = parsePoint(event.canvas as PointObject);\n    this.onMagnify(origin);\n  };\n\n  protected onMagnify = (origin: Point) => {\n    if (origin.some(isNaN)) return;\n\n    this.renderLens(origin);\n    this.renderFocusElements();\n  };\n\n  private renderLens = (origin: Point) => {\n    const style = Object.assign({}, defaultLensStyle, this.options.style);\n\n    if (!this.isLensOn) {\n      this.lens = new Circle({ style });\n      this.canvas.appendChild(this.lens);\n    }\n\n    Object.assign(style, toPointObject(origin), {\n      size: this.r * 2,\n      label: this.options.showDPercent,\n      labelText: this.getDPercent(),\n    });\n\n    this.lens.update(style);\n  };\n\n  private getDPercent = () => {\n    const { minD, maxD } = this.options as Required<FisheyeOptions>;\n    const percent = Math.round(((this.d - minD) / (maxD - minD)) * 100);\n    return `${percent}%`;\n  };\n\n  private prevMagnifiedStyleMap = new Map<ID, NodeStyle>();\n  private prevOriginStyleMap = new Map<ID, NodeStyle>();\n\n  private renderFocusElements = () => {\n    if (!this.isLensOn) return;\n\n    const { graph } = this.context;\n    const origin = this.lens.getCenter();\n\n    const molecularParam = (this.d + 1) * this.r;\n\n    const magnifiedStyleMap = new Map<ID, NodeStyle>();\n    const originStyleMap = new Map<ID, NodeStyle>();\n\n    const nodeData = graph.getNodeData();\n    nodeData.forEach((datum) => {\n      const position = positionOf(datum);\n      const distanceToOrigin = distance(position, origin);\n      if (distanceToOrigin > this.r) return;\n\n      const magnifiedDistance = (molecularParam * distanceToOrigin) / (this.d * distanceToOrigin + this.r);\n      const [nodeX, nodeY] = position;\n      const [originX, originY] = origin;\n      const cos = (nodeX - originX) / distanceToOrigin;\n      const sin = (nodeY - originY) / distanceToOrigin;\n\n      const newPoint: Point = [originX + magnifiedDistance * cos, originY + magnifiedDistance * sin];\n      const nodeId = idOf(datum);\n\n      const style = this.getNodeStyle(datum);\n      const originStyle = pick(graph.getElementRenderStyle(nodeId), Object.keys(style));\n\n      magnifiedStyleMap.set(nodeId, { ...toPointObject(newPoint), ...style });\n      originStyleMap.set(nodeId, { ...toPointObject(position), ...originStyle });\n    });\n\n    this.updateStyle(magnifiedStyleMap, originStyleMap);\n  };\n\n  private getNodeStyle = (datum: NodeData) => {\n    const { nodeStyle } = this.options;\n    return typeof nodeStyle === 'function' ? nodeStyle(datum) : nodeStyle;\n  };\n\n  private updateStyle = (magnifiedStyleMap: Map<ID, NodeStyle>, originStyleMap: Map<ID, NodeStyle>) => {\n    const { graph, element } = this.context;\n\n    const { enter, exit, keep } = arrayDiff<ID>(\n      Array.from(this.prevMagnifiedStyleMap.keys()),\n      Array.from(magnifiedStyleMap.keys()),\n      (d) => d,\n    );\n\n    const relatedEdges = new Set<ID>();\n\n    const update = (nodeId: ID, style: NodeStyle) => {\n      const node = element!.getElement(nodeId) as Node;\n      node?.update(style);\n\n      graph.getRelatedEdgesData(nodeId).forEach((datum) => {\n        relatedEdges.add(idOf(datum));\n      });\n    };\n\n    [...enter, ...keep].forEach((nodeId) => {\n      update(nodeId, magnifiedStyleMap.get(nodeId)!);\n    });\n\n    exit.forEach((nodeId) => {\n      update(nodeId, this.prevOriginStyleMap.get(nodeId)!);\n      this.prevOriginStyleMap.delete(nodeId);\n    });\n\n    relatedEdges.forEach((edgeId) => {\n      const edge = element!.getElement(edgeId);\n      edge?.update({});\n    });\n\n    this.prevMagnifiedStyleMap = magnifiedStyleMap;\n    originStyleMap.forEach((style, nodeId) => {\n      if (!this.prevOriginStyleMap.has(nodeId)) {\n        this.prevOriginStyleMap.set(nodeId, style);\n      }\n    });\n  };\n\n  private isWheelValid = (event: WheelEvent) => {\n    if (this.options.preventDefault) event.preventDefault();\n\n    if (!this.isLensOn) return false;\n\n    const { clientX, clientY } = event;\n    const scaleOrigin = this.context.graph.getCanvasByClient([clientX, clientY]);\n    const origin = this.lens.getCenter();\n    if (distance(scaleOrigin, origin) > this.r) return false;\n\n    return true;\n  };\n\n  private scaleR = (positive: boolean) => {\n    const { maxR, minR } = this.options;\n    const ratio = positive ? 1 / (1 - R_DELTA) : 1 - R_DELTA;\n    const canvasR = Math.min(...this.context.canvas.getSize()) / 2;\n    this.r = Math.max(minR || 0, Math.min(maxR || canvasR, this.r * ratio));\n  };\n\n  private scaleD = (positive: boolean) => {\n    const { maxD, minD } = this.options as Required<FisheyeOptions>;\n    const newD = positive ? this.d + D_DELTA : this.d - D_DELTA;\n    this.d = Math.max(minD, Math.min(maxD, newD));\n  };\n\n  private scaleRByWheel = (event: WheelEvent) => {\n    if (!this.isWheelValid(event)) return;\n\n    const { deltaX, deltaY } = event;\n    this.scaleR(deltaX + deltaY > 0);\n\n    const origin = this.lens.getCenter();\n    this.onMagnify(origin);\n  };\n\n  private scaleDByWheel = (event: WheelEvent) => {\n    if (!this.isWheelValid(event)) return;\n\n    const { deltaX, deltaY } = event;\n    this.scaleD(deltaX + deltaY > 0);\n\n    const origin = this.lens.getCenter();\n    this.onMagnify(origin);\n  };\n\n  private isDragValid = (event: IDragEvent) => {\n    if (this.options.preventDefault) event.preventDefault();\n\n    if (!this.isLensOn) return false;\n\n    const dragOrigin = parsePoint(event.canvas as PointObject);\n    const origin = this.lens.getCenter();\n    if (distance(dragOrigin, origin) > this.r) return false;\n\n    return true;\n  };\n\n  private isLensDragging = false;\n\n  private onDragStart = (event: IDragEvent) => {\n    if (!this.isDragValid(event)) return;\n\n    this.isLensDragging = true;\n  };\n\n  private onDrag = (event: IDragEvent) => {\n    if (!this.isLensDragging) return;\n\n    const dragOrigin = parsePoint(event.canvas as PointObject);\n    this.onMagnify(dragOrigin);\n  };\n\n  private onDragEnd = () => {\n    this.isLensDragging = false;\n  };\n\n  private scaleRByDrag = (event: IDragEvent) => {\n    if (!this.isLensDragging) return;\n\n    const { dx, dy } = event;\n    this.scaleR(dx - dy > 0);\n\n    const origin = this.lens.getCenter();\n    this.onMagnify(origin);\n  };\n\n  private scaleDByDrag = (event: IDragEvent) => {\n    if (!this.isLensDragging) return;\n\n    const { dx, dy } = event;\n    this.scaleD(dx - dy > 0);\n\n    const origin = this.lens.getCenter();\n    this.onMagnify(origin);\n  };\n\n  get graphDom() {\n    return this.context.graph.getCanvas().getContextService().getDomElement();\n  }\n\n  private bindEvents() {\n    const { graph } = this.context;\n    const { trigger, scaleRBy, scaleDBy } = this.options;\n\n    const canvas = graph.getCanvas().getLayer();\n\n    if (['click', 'drag'].includes(trigger)) {\n      canvas.addEventListener(CommonEvent.CLICK, this.onCreateFisheye);\n    }\n\n    if (trigger === 'pointermove') {\n      canvas.addEventListener(CommonEvent.POINTER_MOVE, this.onCreateFisheye);\n    }\n\n    if (trigger === 'drag' || scaleRBy === 'drag' || scaleDBy === 'drag') {\n      canvas.addEventListener(CommonEvent.DRAG_START, this.onDragStart);\n      canvas.addEventListener(CommonEvent.DRAG_END, this.onDragEnd);\n\n      const dragFunc = trigger === 'drag' ? this.onDrag : scaleRBy === 'drag' ? this.scaleRByDrag : this.scaleDByDrag;\n      canvas.addEventListener(CommonEvent.DRAG, dragFunc);\n    }\n\n    if (scaleRBy === 'wheel' || scaleDBy === 'wheel') {\n      const wheelFunc = scaleRBy === 'wheel' ? this.scaleRByWheel : this.scaleDByWheel;\n      this.graphDom?.addEventListener(CommonEvent.WHEEL, wheelFunc, { passive: false });\n    }\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n    const { trigger, scaleRBy, scaleDBy } = this.options;\n\n    const canvas = graph.getCanvas().getLayer();\n\n    if (['click', 'drag'].includes(trigger)) {\n      canvas.removeEventListener(CommonEvent.CLICK, this.onCreateFisheye);\n    }\n\n    if (trigger === 'pointermove') {\n      canvas.removeEventListener(CommonEvent.POINTER_MOVE, this.onCreateFisheye);\n    }\n\n    if (trigger === 'drag' || scaleRBy === 'drag' || scaleDBy === 'drag') {\n      canvas.removeEventListener(CommonEvent.DRAG_START, this.onDragStart);\n      canvas.removeEventListener(CommonEvent.DRAG_END, this.onDragEnd);\n\n      const dragFunc = trigger === 'drag' ? this.onDrag : scaleRBy === 'drag' ? this.scaleRByDrag : this.scaleDByDrag;\n      canvas.removeEventListener(CommonEvent.DRAG, dragFunc);\n    }\n\n    if (scaleRBy === 'wheel' || scaleDBy === 'wheel') {\n      const wheelFunc = scaleRBy === 'wheel' ? this.scaleRByWheel : this.scaleDByWheel;\n      this.graphDom?.removeEventListener(CommonEvent.WHEEL, wheelFunc);\n    }\n  }\n\n  public update(options: Partial<FisheyeOptions>) {\n    this.unbindEvents();\n    super.update(options);\n    this.r = options.r ?? this.r;\n    this.d = options.d ?? this.d;\n    this.bindEvents();\n  }\n\n  public destroy() {\n    this.unbindEvents();\n    if (this.isLensOn) {\n      this.lens?.destroy();\n    }\n    this.prevMagnifiedStyleMap.clear();\n    this.prevOriginStyleMap.clear();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/fullscreen/index.ts",
    "content": "import type { RuntimeContext } from '../../runtime/types';\nimport { print } from '../../utils/print';\nimport type { ShortcutKey } from '../../utils/shortcut';\nimport { Shortcut } from '../../utils/shortcut';\nimport type { BasePluginOptions } from '../base-plugin';\nimport { BasePlugin } from '../base-plugin';\n\n/**\n * <zh/> 全屏配置项\n *\n * <en/> Full screen options\n */\nexport interface FullscreenOptions extends BasePluginOptions {\n  /**\n   * <zh/> 触发全屏的方式\n   * - `request` : 请求全屏\n   * - `exit` : 退出全屏\n   *\n   * <en/> The way to trigger full screen\n   * - `request`: request full screen\n   * - `exit`: exit full screen\n   */\n  trigger?: {\n    request?: ShortcutKey;\n    exit?: ShortcutKey;\n  };\n  /**\n   * <zh/> 是否自适应画布尺寸，全屏后画布尺寸会自动适应屏幕尺寸\n   *\n   * <en/> Whether to adapt the canvas size\n   * @defaultValue true\n   */\n  autoFit?: boolean;\n  /**\n   * <zh/> 进入全屏后的回调\n   *\n   * <en/> Callback after entering full screen\n   */\n  onEnter?: () => void;\n  /**\n   * <zh/> 退出全屏后的回调\n   *\n   * <en/> Callback after exiting full screen\n   */\n  onExit?: () => void;\n}\n\n/**\n * <zh/> 全屏\n *\n * <en/> Full screen\n */\nexport class Fullscreen extends BasePlugin<FullscreenOptions> {\n  static defaultOptions: Partial<FullscreenOptions> = {\n    trigger: {},\n    autoFit: true,\n  };\n\n  private shortcut: Shortcut;\n\n  private style: HTMLStyleElement;\n\n  private $el = this.context.canvas.getContainer()!;\n\n  private graphSize: [number, number] = [0, 0];\n\n  constructor(context: RuntimeContext, options: FullscreenOptions) {\n    super(context, Object.assign({}, Fullscreen.defaultOptions, options));\n\n    this.shortcut = new Shortcut(context.graph);\n\n    this.bindEvents();\n\n    this.style = document.createElement('style');\n    document.head.appendChild(this.style);\n    this.style.innerHTML = `\n      :not(:root):fullscreen::backdrop {\n        background: transparent;\n      }\n    `;\n  }\n\n  private bindEvents() {\n    this.unbindEvents();\n    this.shortcut.unbindAll();\n\n    const { request = [], exit = [] } = this.options.trigger;\n    this.shortcut.bind(request, this.request);\n    this.shortcut.bind(exit, this.exit);\n\n    const events = ['webkitfullscreenchange', 'mozfullscreenchange', 'fullscreenchange', 'MSFullscreenChange'];\n    events.forEach((eventName) => {\n      document.addEventListener(eventName, this.onFullscreenChange, false);\n    });\n  }\n\n  private unbindEvents() {\n    this.shortcut.unbindAll();\n    const events = ['webkitfullscreenchange', 'mozfullscreenchange', 'fullscreenchange', 'MSFullscreenChange'];\n    events.forEach((eventName) => {\n      document.removeEventListener(eventName, this.onFullscreenChange, false);\n    });\n  }\n\n  private setGraphSize(fullScreen = true) {\n    let width, height;\n    if (fullScreen) {\n      width = globalThis.screen?.width || 0;\n      height = globalThis.screen?.height || 0;\n      this.graphSize = this.context.graph.getSize();\n    } else {\n      [width, height] = this.graphSize;\n    }\n    this.context.graph.setSize(width, height);\n    this.context.graph.render();\n  }\n\n  private onFullscreenChange = () => {\n    const isFull = !!document.fullscreenElement;\n    if (this.options.autoFit) this.setGraphSize(isFull);\n    if (isFull) {\n      this.options.onEnter?.();\n    } else {\n      this.options.onExit?.();\n    }\n  };\n\n  /**\n   * <zh/> 请求全屏\n   *\n   * <en/> Request full screen\n   */\n  public request() {\n    if (document.fullscreenElement || !isFullscreenEnabled()) return;\n    this.$el.requestFullscreen().catch((err: Error) => {\n      print.warn(`Error attempting to enable full-screen: ${err.message} (${err.name})`);\n    });\n  }\n\n  /**\n   * <zh/> 退出全屏\n   *\n   * <en/> Exit full screen\n   */\n  public exit() {\n    if (!document.fullscreenElement) return;\n    document.exitFullscreen();\n  }\n\n  /**\n   * <zh/> 更新配置\n   *\n   * <en/> Update options\n   * @param options - <zh/> 配置项 | <en/> Options\n   * @internal\n   */\n  public update(options: Partial<FullscreenOptions>): void {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  public destroy(): void {\n    this.exit();\n    this.style.remove();\n    super.destroy();\n  }\n}\n\n/**\n * <zh/> 判断是否支持全屏\n *\n * <en/> Determine whether full screen is enabled\n * @returns <zh/> 是否支持全屏 | <en/> Whether full screen is enabled\n */\nfunction isFullscreenEnabled() {\n  return (\n    document.fullscreenEnabled ||\n    // <zh/> 使用 Reflect 语法规避 ts 检查 | <en/> use Reflect to avoid ts checking\n    Reflect.get(document, 'webkitFullscreenEnabled') ||\n    Reflect.get(document, 'mozFullscreenEnabled') ||\n    Reflect.get(document, 'msFullscreenEnabled')\n  );\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/grid-line.ts",
    "content": "import { isBoolean } from '@antv/util';\nimport { GraphEvent } from '../constants';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { IViewportEvent, Point } from '../types';\nimport { ViewportEvent } from '../utils/event';\nimport { add, mod, multiply } from '../utils/vector';\nimport { BasePlugin, BasePluginOptions } from './base-plugin';\nimport { createPluginContainer } from './utils/dom';\n\n/**\n * <zh/> 网格线配置项\n *\n * <en/> Grid line options\n */\nexport interface GridLineOptions extends BasePluginOptions {\n  /**\n   * <zh/> 网格线颜色\n   *\n   * <en/> Grid line color\n   * @defaultValue '#0001'\n   */\n  stroke?: string;\n  /**\n   * <zh/> 网格线宽\n   *\n   * <en/> Grid line width\n   * @defaultValue 1\n   */\n  lineWidth?: number | string;\n  /**\n   * <zh/> 单个网格的大小\n   *\n   * <en/> The size of a single grid\n   * @defaultValue 20\n   */\n  size?: number;\n  /**\n   * <zh/> 是否显示边框\n   *\n   * <en/> Whether to show the border\n   * @defaultValue true\n   */\n  border?: boolean;\n  /**\n   * <zh/> 边框线宽\n   *\n   * <en/> Border line width\n   * @defaultValue 1\n   */\n  borderLineWidth?: number;\n  /**\n   * <zh/> 边框颜色\n   *\n   * <en/> Border color\n   * @defaultValue '#0001'\n   * @remarks\n   * <zh/> 完整属性定义参考 [CSS border-color](https://developer.mozilla.org/zh-CN/docs/Web/CSS/border-color)\n   *\n   * <en/> Refer to [CSS border-color](https://developer.mozilla.org/en-US/docs/Web/CSS/border-color) for the complete property definition\n   */\n  borderStroke?: string;\n  /**\n   * <zh/> 边框样式\n   *\n   * <en/> Border style\n   * @defaultValue 'solid'\n   * @remarks\n   * <zh/> 完整属性定义参考 [CSS border-style](https://developer.mozilla.org/zh-CN/docs/Web/CSS/border-style)\n   *\n   * <en/> Refer to [CSS border-style](https://developer.mozilla.org/en-US/docs/Web/CSS/border-style) for the complete property definition\n   */\n  borderStyle?: string;\n  /**\n   * <zh/> 是否跟随图移动\n   *\n   * <en/> Whether to follow with the graph\n   * @defaultValue false\n   */\n  follow?:\n    | boolean\n    | {\n        /**\n         * <zh/> 是否跟随图平移\n         *\n         * <en/> Whether to follow the graph translation\n         */\n        translate?: boolean;\n        /**\n         * <zh/> 是否跟随图缩放\n         *\n         * <en/> Whether to follow the graph zoom\n         */\n        zoom?: boolean;\n      };\n}\n\n/**\n * <zh/> 网格线\n *\n * <en/> Grid line\n * @remarks\n * <zh/> 网格线插件，多用于辅助绘图\n *\n * <en/> Grid line plugin, often used to auxiliary drawing\n */\nexport class GridLine extends BasePlugin<GridLineOptions> {\n  static defaultOptions: Partial<GridLineOptions> = {\n    border: true,\n    borderLineWidth: 1,\n    borderStroke: '#eee',\n    borderStyle: 'solid',\n    lineWidth: 1,\n    size: 20,\n    stroke: '#eee',\n  };\n\n  private $element: HTMLElement = createPluginContainer('grid-line', true);\n  private offset: Point = [0, 0];\n  private currentScale: number = 1;\n  private baseSize: number;\n\n  constructor(context: RuntimeContext, options: GridLineOptions) {\n    super(context, Object.assign({}, GridLine.defaultOptions, options));\n\n    const $container = this.context.canvas.getContainer()!;\n    $container.prepend(this.$element);\n\n    this.baseSize = this.options.size;\n\n    this.updateStyle();\n    this.bindEvents();\n  }\n\n  /**\n   * <zh/> 更新网格线配置\n   *\n   * <en/> Update the configuration of the grid line\n   * @param options - <zh/> 配置项 | <en/> options\n   * @internal\n   */\n  public update(options: Partial<GridLineOptions>) {\n    super.update(options);\n\n    if (options.size !== undefined) {\n      this.baseSize = options.size;\n    }\n\n    this.updateStyle();\n  }\n\n  private bindEvents() {\n    const { graph } = this.context;\n    graph.on(GraphEvent.AFTER_TRANSFORM, this.onTransform);\n  }\n\n  private updateStyle() {\n    const { stroke, lineWidth, border, borderLineWidth, borderStroke, borderStyle } = this.options;\n\n    const scaledSize = this.baseSize * this.currentScale;\n\n    Object.assign(this.$element.style, {\n      border: border ? `${borderLineWidth}px ${borderStyle} ${borderStroke}` : 'none',\n      backgroundImage: `linear-gradient(${stroke} ${lineWidth}px, transparent ${lineWidth}px), linear-gradient(90deg, ${stroke} ${lineWidth}px, transparent ${lineWidth}px)`,\n      backgroundSize: `${scaledSize}px ${scaledSize}px`,\n      backgroundRepeat: 'repeat',\n    });\n  }\n\n  private updateOffset(delta: Point) {\n    const scaledSize = this.baseSize * this.currentScale;\n    this.offset = mod(add(this.offset, delta), scaledSize);\n    this.$element.style.backgroundPosition = `${this.offset[0]}px ${this.offset[1]}px`;\n  }\n\n  private followZoom = (event: IViewportEvent) => {\n    const {\n      data: { scale, origin },\n    } = event;\n\n    if (!scale || (origin === undefined && this.context.viewport === undefined)) return;\n\n    const prevScale = this.currentScale;\n    this.currentScale = scale;\n\n    const deltaScale = scale / prevScale;\n    const positionOffset = multiply(origin || this.context.graph.getCanvasCenter(), 1 - deltaScale);\n    const scaledSize = this.baseSize * scale;\n\n    const scaledOffset = multiply(this.offset, deltaScale);\n    const modulatedOffset = mod(scaledOffset, scaledSize);\n    const newOffset = add(modulatedOffset, positionOffset);\n\n    this.$element.style.backgroundSize = `${scaledSize}px ${scaledSize}px`;\n    this.$element.style.backgroundPosition = `${newOffset[0]}px ${newOffset[1]}px`;\n\n    this.offset = mod(newOffset, scaledSize);\n  };\n\n  private followTranslate = (event: IViewportEvent) => {\n    if (!this.options.follow) return;\n    const {\n      data: { translate },\n    } = event;\n    if (translate) this.updateOffset(translate);\n  };\n\n  private parseFollow(follow: GridLineOptions['follow']): { translate: boolean; zoom: boolean } {\n    return isBoolean(follow)\n      ? { translate: follow, zoom: follow }\n      : { translate: follow?.translate ?? false, zoom: follow?.zoom ?? false };\n  }\n\n  private onTransform = (event: ViewportEvent) => {\n    const follow = this.parseFollow(this.options.follow);\n\n    if (follow.zoom) this.followZoom(event);\n    if (follow.translate) this.followTranslate(event);\n  };\n\n  /**\n   * <zh/> 销毁网格线\n   *\n   * <en/> Destroy the grid line\n   * @internal\n   */\n  public destroy(): void {\n    this.context.graph.off(GraphEvent.AFTER_TRANSFORM, this.onTransform);\n    this.$element.remove();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/history/index.ts",
    "content": "import EventEmitter from '@antv/event-emitter';\nimport { GraphEvent } from '../../constants';\nimport { HistoryEvent } from '../../constants/events/history';\nimport type { RuntimeContext } from '../../runtime/types';\nimport { DataChange, Loosen } from '../../types';\nimport type { Command } from '../../types/history';\nimport type { GraphLifeCycleEvent } from '../../utils/event';\nimport { idsOf } from '../../utils/id';\nimport type { BasePluginOptions } from '../base-plugin';\nimport { BasePlugin } from '../base-plugin';\nimport { parseCommand } from './util';\n\n/**\n * <zh/> 历史记录配置项\n *\n * <en/> History options\n */\nexport interface HistoryOptions extends BasePluginOptions {\n  /**\n   * <zh/>  最多记录该数据长度的历史记录\n   *\n   * <en/> The maximum number of history records\n   * @defaultValue 0(不做限制)\n   */\n  stackSize?: number;\n  /**\n   * <zh/> 当一个命令被添加到 Undo/Redo 队列前被调用，如果该方法返回 false，那么这个命令将不会被添加到队列中。revert 为 true 时表示撤销操作，为 false 时表示重做操作\n   *\n   * <en/> Called before a command is added to the Undo/Redo queue. If this method returns false, the command will not be added to the queue. revert is true for undo operations and false for redo operations\n   */\n  beforeAddCommand?: (cmd: Command, revert: boolean) => boolean | void;\n  /**\n   * <zh/> 当一个命令被添加到 Undo/Redo 队列后被调用。revert 为 true 时表示撤销操作，为 false 时表示重做操作\n   *\n   * <en/> Called after a command is added to the Undo/Redo queue. revert is true for undo operations and false for redo operations\n   */\n  afterAddCommand?: (cmd: Command, revert: boolean) => void;\n  /**\n   * <zh/> 执行命令时的回调函数\n   *\n   * <en/> Callback function when executing a command\n   */\n  executeCommand?: (cmd: Command) => void;\n}\n\n/**\n * <zh/> 历史记录\n *\n * <en/> History\n * @remarks\n * <zh/> 历史记录用于记录图的数据变化，支持撤销和重做等操作。\n *\n * <en/> History is used to record data changes in the graph and supports operations such as undo and redo.\n */\nexport class History extends BasePlugin<HistoryOptions> {\n  static defaultOptions: Partial<HistoryOptions> = {\n    stackSize: 0,\n  };\n  private emitter: EventEmitter;\n  private batchChanges: DataChange[][] | null = null;\n  private batchAnimation = false;\n  public undoStack: Command[] = [];\n  public redoStack: Command[] = [];\n  private freezed = false;\n\n  constructor(context: RuntimeContext, options: HistoryOptions) {\n    super(context, Object.assign({}, History.defaultOptions, options));\n\n    this.emitter = new EventEmitter();\n\n    const { graph } = this.context;\n    graph.on(GraphEvent.AFTER_DRAW, this.addCommand);\n    graph.on(GraphEvent.BATCH_START, this.initBatchCommand);\n    graph.on(GraphEvent.BATCH_END, this.addCommand);\n  }\n\n  /**\n   * <zh/> 是否可以执行撤销操作\n   *\n   * <en/> Whether undo can be done\n   * @returns <zh/> 是否可以执行撤销操作 | <en/> Whether undo can be done\n   */\n  public canUndo() {\n    return this.undoStack.length > 0;\n  }\n\n  /**\n   * <zh/> 是否可以执行重做操作\n   *\n   * <en/> Whether redo can be done\n   * @returns <zh/> 是否可以执行重做操作 | <en/> Whether redo can be done\n   */\n  public canRedo() {\n    return this.redoStack.length > 0;\n  }\n\n  /**\n   * <zh/> 执行撤销\n   *\n   * <en/> Execute undo\n   * @returns <zh/> 返回当前实例 | <en/> Return the current instance\n   */\n  public undo() {\n    const cmd = this.undoStack.pop();\n    if (cmd) {\n      this.executeCommand(cmd);\n\n      const before = this.options.beforeAddCommand?.(cmd, false);\n      if (before === false) return;\n\n      this.redoStack.push(cmd);\n      this.options.afterAddCommand?.(cmd, false);\n      this.notify(HistoryEvent.UNDO, cmd);\n    }\n    return this;\n  }\n\n  /**\n   * <zh/> 执行重做\n   *\n   * <en/> Execute redo\n   * @returns <zh/> 返回当前实例 | <en/> Return the current instance\n   */\n  public redo() {\n    const cmd = this.redoStack.pop();\n    if (cmd) {\n      this.executeCommand(cmd, false);\n      this.undoStackPush(cmd);\n      this.notify(HistoryEvent.REDO, cmd);\n    }\n    return this;\n  }\n\n  /**\n   * <zh/> 执行撤销且不计入历史记录\n   *\n   * <en/> Execute undo and do not record in history\n   * @returns <zh/> 返回当前实例 | <en/> Return the current instance\n   */\n  public undoAndCancel() {\n    const cmd = this.undoStack.pop();\n    if (cmd) {\n      this.executeCommand(cmd, false);\n      this.redoStack = [];\n      this.notify(HistoryEvent.CANCEL, cmd);\n    }\n    return this;\n  }\n\n  private executeCommand = (cmd: Command, revert = true) => {\n    this.freezed = true;\n\n    this.options.executeCommand?.(cmd);\n\n    const values = revert ? cmd.original : cmd.current;\n    this.context.graph.addData(values.add);\n    this.context.graph.updateData(values.update);\n    this.context.graph.removeData(idsOf(values.remove, false));\n    this.context.element?.draw({ silence: true, animation: cmd.animation });\n\n    this.freezed = false;\n  };\n\n  private addCommand = (event: GraphLifeCycleEvent) => {\n    if (this.freezed) return;\n\n    if (event.type === GraphEvent.AFTER_DRAW) {\n      const { dataChanges = [], animation = true } = (event as GraphLifeCycleEvent).data;\n\n      if (this.context.batch?.isBatching) {\n        if (!this.batchChanges) return;\n        this.batchChanges.push(dataChanges);\n        this.batchAnimation &&= animation;\n        return;\n      }\n      this.batchChanges = [dataChanges];\n      this.batchAnimation = animation;\n    }\n\n    this.undoStackPush(parseCommand(this.batchChanges!.flat(), this.batchAnimation, this.context));\n    this.notify(HistoryEvent.ADD, this.undoStack[this.undoStack.length - 1]);\n  };\n\n  private initBatchCommand = (event: GraphLifeCycleEvent) => {\n    const { initiate } = event.data;\n    this.batchAnimation = false;\n    if (initiate) {\n      this.batchChanges = [];\n    } else {\n      const cmd = this.undoStack.pop();\n      if (!cmd) this.batchChanges = null;\n    }\n  };\n\n  private undoStackPush(cmd: Command): void {\n    const { stackSize } = this.options;\n\n    if (stackSize !== 0 && this.undoStack.length >= stackSize) {\n      this.undoStack.shift();\n    }\n\n    const before = this.options.beforeAddCommand?.(cmd, true);\n    if (before === false) return;\n\n    this.undoStack.push(cmd);\n    this.options.afterAddCommand?.(cmd, true);\n  }\n\n  /**\n   * <zh/> 清空历史记录\n   *\n   * <en/> Clear history\n   */\n  public clear(): void {\n    this.undoStack = [];\n    this.redoStack = [];\n    this.batchChanges = null;\n    this.batchAnimation = false;\n    this.notify(HistoryEvent.CLEAR, null);\n  }\n\n  private notify(event: Loosen<HistoryEvent>, cmd: Command | null) {\n    this.emitter.emit(event, { cmd });\n    this.emitter.emit(HistoryEvent.CHANGE, { cmd });\n  }\n\n  /**\n   * <zh/> 监听历史记录事件\n   *\n   * <en/> Listen to history events\n   * @param event  - <zh/> 事件名称 | <en/> Event name\n   * @param handler - <zh/> 事件处理函数 | <en/> Event handler\n   */\n  public on(event: Loosen<HistoryEvent>, handler: (e: { cmd?: Command | null }) => void): void {\n    this.emitter.on(event, handler);\n  }\n\n  /**\n   * <zh/> 销毁\n   *\n   * <en/> Destroy\n   * @internal\n   */\n  public destroy(): void {\n    const { graph } = this.context;\n    graph.off(GraphEvent.AFTER_DRAW, this.addCommand);\n    graph.off(GraphEvent.BATCH_START, this.initBatchCommand);\n    graph.off(GraphEvent.BATCH_END, this.addCommand);\n\n    this.emitter.off();\n\n    super.destroy();\n    this.undoStack = [];\n    this.redoStack = [];\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/history/util.ts",
    "content": "import { isObject } from '@antv/util';\nimport type { RuntimeContext } from '../../runtime/types';\nimport type { DataChange, DataChanges, ElementDatum } from '../../types';\nimport type { Command } from '../../types/history';\nimport { inferDefaultValue } from '../../utils/animation';\nimport { groupByChangeType, reduceDataChanges } from '../../utils/change';\nimport { idOf } from '../../utils/id';\n\n/**\n * <zh/> 对齐两个对象的字段。若目标对象缺少字段，则会添加默认值。\n *\n * <en/> Align the fields of two objects. If the target object lacks fields, default values will be added.\n * @param refObject - <zh/> 参考对象 ｜ <en/> Reference object\n * @param targetObject - <zh/> 目标对象 ｜ <en/> Target object\n */\nexport function alignFields(refObject: Record<string, any>, targetObject: Record<string, any>) {\n  for (const key in refObject) {\n    if (isObject(refObject[key]) && !Array.isArray(refObject[key]) && refObject[key] !== null) {\n      if (!targetObject[key]) targetObject[key] = {};\n      alignFields(refObject[key], targetObject[key]);\n    } else if (targetObject[key] === undefined) {\n      targetObject[key] = inferDefaultValue(key);\n    }\n  }\n}\n\n/**\n * <zh/> 解析数据变更为历史记录命令\n *\n * <en/> Parse data changes into history commands\n * @param changes - <zh/> 数据变更 ｜ <en/> Data changes\n * @param animation - <zh/> 是否开启动画 ｜ <en/> Whether to enable animation\n * @param context - <zh/> 运行时上下文 ｜ <en/> Runtime context\n * @returns <zh/> 历史记录命令 ｜ <en/> History command\n */\nexport function parseCommand(changes: DataChange[], animation = false, context?: RuntimeContext): Command {\n  const cmd = {\n    animation,\n    current: { add: {}, update: {}, remove: {} },\n    original: { add: {}, update: {}, remove: {} },\n  } as Command;\n\n  const { add, update, remove } = groupByChangeType(reduceDataChanges(changes));\n\n  (['nodes', 'edges', 'combos'] as const).forEach((category) => {\n    if (update[category]) {\n      update[category].forEach((item: DataChanges['update'][typeof category][number]) => {\n        const newValue = { ...item.value };\n        let newOriginal = { ...item.original };\n        if (context) {\n          // 特殊处理：获取元素原始 color\n          const itemType = context.graph.getElementType(idOf(item.original));\n          const colorKey = itemType === 'edge' ? 'stroke' : 'fill';\n          const style = context.element!.getElementComputedStyle(itemType, item.original);\n          newOriginal = {\n            ...item.original,\n            style: { [colorKey]: style[colorKey], ...item.original.style },\n          } as ElementDatum;\n        }\n        alignFields(newValue, newOriginal);\n        cmd.current.update[category] ||= [];\n        (cmd.current.update[category] as ElementDatum[]).push(newValue);\n        cmd.original.update[category] ||= [];\n        (cmd.original.update[category] as ElementDatum[]).push(newOriginal);\n      });\n    }\n\n    if (add[category]) {\n      add[category].forEach((item: DataChanges['add'][typeof category][number]) => {\n        const newValue = { ...item.value };\n        cmd.current.add[category] ||= [];\n        (cmd.current.add[category] as ElementDatum[]).push(newValue);\n        cmd.original.remove[category] ||= [];\n        (cmd.original.remove[category] as ElementDatum[]).push(newValue);\n      });\n    }\n\n    if (remove[category]) {\n      remove[category].forEach((item: DataChanges['remove'][typeof category][number]) => {\n        const newValue = { ...item.value };\n        cmd.current.remove[category] ||= [];\n        (cmd.current.remove[category] as ElementDatum[]).push(newValue);\n        cmd.original.add[category] ||= [];\n        (cmd.original.add[category] as ElementDatum[]).push(newValue);\n      });\n    }\n  });\n\n  return cmd;\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/hull/hull/format.ts",
    "content": "import type { Point } from '../../../types';\n\nexport type PointObject = Record<string, number>;\nexport type BBox = [number, number, number, number];\nexport type FormatTuple = [string, string];\n\nexport const formatUtil = {\n  toXy<T extends PointObject>(pointset: T[] | Point[], format?: FormatTuple): T[] | Point[] {\n    if (!format) return [...pointset] as Point[];\n\n    const xProperty = format[0].slice(1);\n    const yProperty = format[1].slice(1);\n\n    return (pointset as T[]).map((pt) => [pt[xProperty], pt[yProperty]]) as Point[];\n  },\n\n  fromXy(coordinates: Point[], format?: FormatTuple): Point[] | PointObject[] {\n    if (!format) return [...coordinates];\n\n    const xProperty = format[0].slice(1);\n    const yProperty = format[1].slice(1);\n\n    return coordinates.map(([x, y]) => ({\n      [xProperty]: x,\n      [yProperty]: y,\n    }));\n  },\n};\nexport type PointConverter = typeof formatUtil;\n"
  },
  {
    "path": "packages/g6/src/plugins/hull/hull/grid_handle.ts",
    "content": "import type { Point } from '../../../types';\nimport type { BBox } from './format';\n\nexport class Grid {\n  private _cells: Point[][][] = [];\n  private _cellSize: number;\n  private _reverseCellSize: number;\n  constructor(points: Point[], cellSize: number) {\n    this._cellSize = cellSize;\n    this._reverseCellSize = 1 / cellSize;\n    for (const point of points) {\n      const x = this.coordToCellNum(point[0]);\n      const y = this.coordToCellNum(point[1]);\n\n      if (!this._cells[x]) {\n        this._cells[x] = [];\n      }\n\n      if (!this._cells[x][y]) {\n        this._cells[x][y] = [];\n      }\n\n      this._cells[x][y].push(point);\n    }\n  }\n  cellPoints(x: number, y: number): Point[] {\n    return this._cells[x]?.[y] || [];\n  }\n  rangePoints(bbox: BBox): Point[] {\n    const tlCellX = this.coordToCellNum(bbox[0]);\n    const tlCellY = this.coordToCellNum(bbox[1]);\n    const brCellX = this.coordToCellNum(bbox[2]);\n    const brCellY = this.coordToCellNum(bbox[3]);\n    const points: Point[] = [];\n    for (let x = tlCellX; x <= brCellX; x++) {\n      for (let y = tlCellY; y <= brCellY; y++) {\n        const cell = this.cellPoints(x, y);\n        for (const point of cell) {\n          points.push(point);\n        }\n      }\n    }\n    return points;\n  }\n  removePoint(point: Point): Point[] {\n    const cellX = this.coordToCellNum(point[0]);\n    const cellY = this.coordToCellNum(point[1]);\n    const cell = this._cells[cellX][cellY];\n    const index = cell.findIndex(([px, py]) => px === point[0] && py === point[1]);\n    if (index > -1) {\n      cell.splice(index, 1);\n    }\n    return cell;\n  }\n  private trunc(val: number): number {\n    return Math.trunc(val);\n  }\n  coordToCellNum(x: number): number {\n    return this.trunc(x * this._reverseCellSize);\n  }\n  extendBbox(bbox: BBox, scaleFactor: number): BBox {\n    return [\n      bbox[0] - scaleFactor * this._cellSize,\n      bbox[1] - scaleFactor * this._cellSize,\n      bbox[2] + scaleFactor * this._cellSize,\n      bbox[3] + scaleFactor * this._cellSize,\n    ];\n  }\n}\n\nexport function grid(points: Point[], cellSize: number): Grid {\n  return new Grid(points, cellSize);\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/hull/hull/index.ts",
    "content": "import type { Point } from '../../../types';\nimport type { BBox, FormatTuple } from './format';\nimport { formatUtil } from './format';\nimport type { Grid } from './grid_handle';\nimport { grid } from './grid_handle';\nimport { monotoneConvexHull2D as convexHull } from './monotone-convex-hull-2d';\nimport { segmentsIntersect as intersect } from './robust-segment-intersect';\n\nfunction _filterDuplicates(pointset: Point[]) {\n  const unique = [pointset[0]];\n  let lastPoint = pointset[0];\n  for (let i = 1; i < pointset.length; i++) {\n    const currentPoint = pointset[i];\n    if (lastPoint[0] !== currentPoint[0] || lastPoint[1] !== currentPoint[1]) {\n      unique.push(currentPoint);\n    }\n    lastPoint = currentPoint;\n  }\n  return unique;\n}\n\nfunction _sortByX(pointset: Point[]) {\n  return pointset.sort(function (a, b) {\n    return a[0] - b[0] || a[1] - b[1];\n  });\n}\n\nfunction _sqLength(a: Point, b: Point) {\n  return Math.pow(b[0] - a[0], 2) + Math.pow(b[1] - a[1], 2);\n}\n\nfunction _cos(o: Point, a: Point, b: Point) {\n  const aShifted = [a[0] - o[0], a[1] - o[1]],\n    bShifted = [b[0] - o[0], b[1] - o[1]],\n    sqALen = _sqLength(o, a),\n    sqBLen = _sqLength(o, b),\n    dot = aShifted[0] * bShifted[0] + aShifted[1] * bShifted[1];\n\n  return dot / Math.sqrt(sqALen * sqBLen);\n}\n\nfunction _intersect(segment: [Point, Point], pointset: Point[]) {\n  for (let i = 0; i < pointset.length - 1; i++) {\n    const seg = [pointset[i], pointset[i + 1]];\n    if (\n      (segment[0][0] === seg[0][0] && segment[0][1] === seg[0][1]) ||\n      (segment[0][0] === seg[1][0] && segment[0][1] === seg[1][1])\n    ) {\n      continue;\n    }\n    if (intersect(segment[0], segment[1], seg[0], seg[1])) {\n      return true;\n    }\n  }\n  return false;\n}\n\nfunction _occupiedArea(pointset: Point[]) {\n  let minX = Infinity;\n  let minY = Infinity;\n  let maxX = -Infinity;\n  let maxY = -Infinity;\n\n  for (let i = pointset.length - 1; i >= 0; i--) {\n    if (pointset[i][0] < minX) {\n      minX = pointset[i][0];\n    }\n    if (pointset[i][1] < minY) {\n      minY = pointset[i][1];\n    }\n    if (pointset[i][0] > maxX) {\n      maxX = pointset[i][0];\n    }\n    if (pointset[i][1] > maxY) {\n      maxY = pointset[i][1];\n    }\n  }\n\n  return [\n    maxX - minX, // width\n    maxY - minY, // height\n  ];\n}\n\nfunction _bBoxAround(edge: [Point, Point]): BBox {\n  return [\n    Math.min(edge[0][0], edge[1][0]), // left\n    Math.min(edge[0][1], edge[1][1]), // top\n    Math.max(edge[0][0], edge[1][0]), // right\n    Math.max(edge[0][1], edge[1][1]), // bottom\n  ];\n}\n\nfunction _midPoint(edge: [Point, Point], innerPoints: Point[], convex: Point[]) {\n  let point = null,\n    angle1Cos = MAX_CONCAVE_ANGLE_COS,\n    angle2Cos = MAX_CONCAVE_ANGLE_COS,\n    a1Cos,\n    a2Cos;\n\n  for (let i = 0; i < innerPoints.length; i++) {\n    a1Cos = _cos(edge[0], edge[1], innerPoints[i]);\n    a2Cos = _cos(edge[1], edge[0], innerPoints[i]);\n\n    if (\n      a1Cos > angle1Cos &&\n      a2Cos > angle2Cos &&\n      !_intersect([edge[0], innerPoints[i]], convex) &&\n      !_intersect([edge[1], innerPoints[i]], convex)\n    ) {\n      angle1Cos = a1Cos;\n      angle2Cos = a2Cos;\n      point = innerPoints[i];\n    }\n  }\n\n  return point;\n}\n\nfunction _concave(\n  convex: Point[],\n  maxSqEdgeLen: number,\n  maxSearchArea: [number, number],\n  grid: Grid,\n  edgeSkipList: Set<string>,\n) {\n  let midPointInserted = false;\n\n  for (let i = 0; i < convex.length - 1; i++) {\n    const edge: [Point, Point] = [convex[i], convex[i + 1]];\n    // generate a key in the format X0,Y0,X1,Y1\n    const keyInSkipList = edge[0][0] + ',' + edge[0][1] + ',' + edge[1][0] + ',' + edge[1][1];\n\n    if (_sqLength(edge[0], edge[1]) < maxSqEdgeLen || edgeSkipList.has(keyInSkipList)) {\n      continue;\n    }\n\n    let scaleFactor = 0;\n    let bBoxAround = _bBoxAround(edge);\n    let bBoxWidth;\n    let bBoxHeight;\n    let midPoint;\n    do {\n      bBoxAround = grid.extendBbox(bBoxAround, scaleFactor);\n      bBoxWidth = bBoxAround[2] - bBoxAround[0];\n      bBoxHeight = bBoxAround[3] - bBoxAround[1];\n\n      midPoint = _midPoint(edge, grid.rangePoints(bBoxAround), convex);\n      scaleFactor++;\n    } while (midPoint === null && (maxSearchArea[0] > bBoxWidth || maxSearchArea[1] > bBoxHeight));\n\n    if (bBoxWidth >= maxSearchArea[0] && bBoxHeight >= maxSearchArea[1]) {\n      edgeSkipList.add(keyInSkipList);\n    }\n\n    if (midPoint !== null) {\n      convex.splice(i + 1, 0, midPoint);\n      grid.removePoint(midPoint);\n      midPointInserted = true;\n    }\n  }\n\n  if (midPointInserted) {\n    return _concave(convex, maxSqEdgeLen, maxSearchArea, grid, edgeSkipList);\n  }\n\n  return convex;\n}\n\nexport function hull(pointset: Point[], concavity: number, format?: FormatTuple): Point[] {\n  const maxEdgeLen = concavity || 20;\n\n  const points = _filterDuplicates(_sortByX(formatUtil.toXy(pointset, format) as Point[]));\n\n  if (points.length < 4) {\n    const concave = points.concat([points[0]]);\n    return (format ? formatUtil.fromXy(concave, format) : concave) as Point[];\n  }\n\n  const occupiedArea = _occupiedArea(points);\n  const maxSearchArea: [number, number] = [\n    occupiedArea[0] * MAX_SEARCH_BBOX_SIZE_PERCENT,\n    occupiedArea[1] * MAX_SEARCH_BBOX_SIZE_PERCENT,\n  ];\n\n  const convex = convexHull(points)\n    .reverse()\n    .map((idx: number) => points[idx]); // ccw -> cw, indices -> points\n  convex.push(convex[0]);\n\n  const innerPoints = points.filter(function (pt) {\n    return convex.indexOf(pt) < 0;\n  });\n\n  const cellSize = Math.ceil(1 / (points.length / (occupiedArea[0] * occupiedArea[1])));\n\n  const concave = _concave(convex, Math.pow(maxEdgeLen, 2), maxSearchArea, grid(innerPoints, cellSize), new Set());\n\n  return (format ? formatUtil.fromXy(concave, format) : concave) as Point[];\n}\n\nconst MAX_CONCAVE_ANGLE_COS = Math.cos(90 / (180 / Math.PI)); // angle = 90 deg\nconst MAX_SEARCH_BBOX_SIZE_PERCENT = 0.6;\n"
  },
  {
    "path": "packages/g6/src/plugins/hull/hull/monotone-convex-hull-2d.ts",
    "content": "'use strict';\n\nimport type { Point } from '../../../types';\nimport robustOrientation from './robust-orientation';\n\nconst orient = robustOrientation[3];\n\nexport function monotoneConvexHull2D(points: Point[]): number[] {\n  const n = points.length;\n\n  // Handle special cases when the input contains fewer than 3 points\n  if (n < 3) {\n    const result = new Array(n);\n    for (let i = 0; i < n; ++i) {\n      result[i] = i;\n    }\n\n    // Special case for exactly 2 identical points\n    if (n === 2 && points[0][0] === points[1][0] && points[0][1] === points[1][1]) {\n      return [0];\n    }\n\n    return result;\n  }\n\n  // Sort point indices along the x-axis (breaking ties with y-axis)\n  const sorted = new Array(n);\n  for (let i = 0; i < n; ++i) {\n    sorted[i] = i;\n  }\n  sorted.sort((a, b) => {\n    const d = points[a][0] - points[b][0];\n    if (d) {\n      return d;\n    }\n    return points[a][1] - points[b][1];\n  });\n\n  // Construct the upper and lower hulls\n  const lower: number[] = [sorted[0], sorted[1]];\n  const upper: number[] = [sorted[0], sorted[1]];\n\n  for (let i = 2; i < n; ++i) {\n    const idx = sorted[i];\n    const p = points[idx];\n\n    // Insert into the lower hull\n    let m = lower.length;\n    while (m > 1 && orient(points[lower[m - 2]], points[lower[m - 1]], p) <= 0) {\n      m -= 1;\n      lower.pop();\n    }\n    lower.push(idx);\n\n    // Insert into the upper hull\n    m = upper.length;\n    while (m > 1 && orient(points[upper[m - 2]], points[upper[m - 1]], p) >= 0) {\n      m -= 1;\n      upper.pop();\n    }\n    upper.push(idx);\n  }\n\n  // Merge the lower and upper hulls into the final result\n  const result = new Array(upper.length + lower.length - 2);\n  let ptr = 0;\n  for (let i = 0, nl = lower.length; i < nl; ++i) {\n    result[ptr++] = lower[i];\n  }\n  for (let j = upper.length - 2; j > 0; --j) {\n    result[ptr++] = upper[j];\n  }\n\n  // Return the final convex hull\n  return result;\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/hull/hull/robust-orientation.ts",
    "content": "'use strict';\n\nimport { scaleLinearExpansion as robustScale } from './robust-scale';\nimport { robustSubtract } from './robust-subtract';\nimport { linearExpansionSum as robustSum } from './robust-sum';\nimport { twoProduct } from './two-product';\n\ntype PointND = number[];\n\nconst NUM_EXPAND = 5;\nconst EPSILON = 1.1102230246251565e-16;\nconst ERR_BOUND_3 = (3.0 + 16.0 * EPSILON) * EPSILON;\nconst ERR_BOUND_4 = (7.0 + 56.0 * EPSILON) * EPSILON;\n\nfunction orientation_3(\n  sum: typeof robustSum,\n  prod: typeof twoProduct,\n  scale: typeof robustScale,\n  sub: typeof robustSubtract,\n): (m0: PointND, m1: PointND, m2: PointND) => number {\n  return function orientation3Exact(m0, m1, m2) {\n    const p = sum(sum(prod(m1[1], m2[0]), prod(-m2[1], m1[0])), sum(prod(m0[1], m1[0]), prod(-m1[1], m0[0])));\n    const n = sum(prod(m0[1], m2[0]), prod(-m2[1], m0[0]));\n    const d = sub(p, n);\n    return d[d.length - 1];\n  };\n}\n\nfunction orientation_4(\n  sum: typeof robustSum,\n  prod: typeof twoProduct,\n  scale: typeof robustScale,\n  sub: typeof robustSubtract,\n): (m0: PointND, m1: PointND, m2: PointND, m3: PointND) => number {\n  return function orientation4Exact(m0, m1, m2, m3) {\n    const p = sum(\n      sum(\n        scale(sum(prod(m2[1], m3[0]), prod(-m3[1], m2[0])), m1[2]),\n        sum(\n          scale(sum(prod(m1[1], m3[0]), prod(-m3[1], m1[0])), -m2[2]),\n          scale(sum(prod(m1[1], m2[0]), prod(-m2[1], m1[0])), m3[2]),\n        ),\n      ),\n      sum(\n        scale(sum(prod(m1[1], m3[0]), prod(-m3[1], m1[0])), m0[2]),\n        sum(\n          scale(sum(prod(m0[1], m3[0]), prod(-m3[1], m0[0])), -m1[2]),\n          scale(sum(prod(m0[1], m1[0]), prod(-m1[1], m0[0])), m3[2]),\n        ),\n      ),\n    );\n    const n = sum(\n      sum(\n        scale(sum(prod(m2[1], m3[0]), prod(-m3[1], m2[0])), m0[2]),\n        sum(\n          scale(sum(prod(m0[1], m3[0]), prod(-m3[1], m0[0])), -m2[2]),\n          scale(sum(prod(m0[1], m2[0]), prod(-m2[1], m0[0])), m3[2]),\n        ),\n      ),\n      sum(\n        scale(sum(prod(m1[1], m2[0]), prod(-m2[1], m1[0])), m0[2]),\n        sum(\n          scale(sum(prod(m0[1], m2[0]), prod(-m2[1], m0[0])), -m1[2]),\n          scale(sum(prod(m0[1], m1[0]), prod(-m1[1], m0[0])), m2[2]),\n        ),\n      ),\n    );\n    const d = sub(p, n);\n    return d[d.length - 1];\n  };\n}\n\nfunction orientation_5(\n  sum: typeof robustSum,\n  prod: typeof twoProduct,\n  scale: typeof robustScale,\n  sub: typeof robustSubtract,\n): (...args: PointND[]) => number {\n  return function orientation5Exact(m0, m1, m2, m3, m4) {\n    const p = sum(\n      sum(\n        sum(\n          scale(\n            sum(\n              scale(sum(prod(m3[1], m4[0]), prod(-m4[1], m3[0])), m2[2]),\n              sum(\n                scale(sum(prod(m2[1], m4[0]), prod(-m4[1], m2[0])), -m3[2]),\n                scale(sum(prod(m2[1], m3[0]), prod(-m3[1], m2[0])), m4[2]),\n              ),\n            ),\n            m1[3],\n          ),\n          sum(\n            scale(\n              sum(\n                scale(sum(prod(m3[1], m4[0]), prod(-m4[1], m3[0])), m1[2]),\n                sum(\n                  scale(sum(prod(m1[1], m4[0]), prod(-m4[1], m1[0])), -m3[2]),\n                  scale(sum(prod(m1[1], m3[0]), prod(-m3[1], m1[0])), m4[2]),\n                ),\n              ),\n              -m2[3],\n            ),\n            scale(\n              sum(\n                scale(sum(prod(m2[1], m4[0]), prod(-m4[1], m2[0])), m1[2]),\n                sum(\n                  scale(sum(prod(m1[1], m4[0]), prod(-m4[1], m1[0])), -m2[2]),\n                  scale(sum(prod(m1[1], m2[0]), prod(-m2[1], m1[0])), m4[2]),\n                ),\n              ),\n              m3[3],\n            ),\n          ),\n        ),\n        sum(\n          scale(\n            sum(\n              scale(sum(prod(m2[1], m3[0]), prod(-m3[1], m2[0])), m1[2]),\n              sum(\n                scale(sum(prod(m1[1], m3[0]), prod(-m3[1], m1[0])), -m2[2]),\n                scale(sum(prod(m1[1], m2[0]), prod(-m2[1], m1[0])), m3[2]),\n              ),\n            ),\n            -m4[3],\n          ),\n          sum(\n            scale(\n              sum(\n                scale(sum(prod(m3[1], m4[0]), prod(-m4[1], m3[0])), m1[2]),\n                sum(\n                  scale(sum(prod(m1[1], m4[0]), prod(-m4[1], m1[0])), -m3[2]),\n                  scale(sum(prod(m1[1], m3[0]), prod(-m3[1], m1[0])), m4[2]),\n                ),\n              ),\n              m0[3],\n            ),\n            scale(\n              sum(\n                scale(sum(prod(m3[1], m4[0]), prod(-m4[1], m3[0])), m0[2]),\n                sum(\n                  scale(sum(prod(m0[1], m4[0]), prod(-m4[1], m0[0])), -m3[2]),\n                  scale(sum(prod(m0[1], m3[0]), prod(-m3[1], m0[0])), m4[2]),\n                ),\n              ),\n              -m1[3],\n            ),\n          ),\n        ),\n      ),\n      sum(\n        sum(\n          scale(\n            sum(\n              scale(sum(prod(m1[1], m4[0]), prod(-m4[1], m1[0])), m0[2]),\n              sum(\n                scale(sum(prod(m0[1], m4[0]), prod(-m4[1], m0[0])), -m1[2]),\n                scale(sum(prod(m0[1], m1[0]), prod(-m1[1], m0[0])), m4[2]),\n              ),\n            ),\n            m3[3],\n          ),\n          sum(\n            scale(\n              sum(\n                scale(sum(prod(m1[1], m3[0]), prod(-m3[1], m1[0])), m0[2]),\n                sum(\n                  scale(sum(prod(m0[1], m3[0]), prod(-m3[1], m0[0])), -m1[2]),\n                  scale(sum(prod(m0[1], m1[0]), prod(-m1[1], m0[0])), m3[2]),\n                ),\n              ),\n              -m4[3],\n            ),\n            scale(\n              sum(\n                scale(sum(prod(m2[1], m3[0]), prod(-m3[1], m2[0])), m1[2]),\n                sum(\n                  scale(sum(prod(m1[1], m3[0]), prod(-m3[1], m1[0])), -m2[2]),\n                  scale(sum(prod(m1[1], m2[0]), prod(-m2[1], m1[0])), m3[2]),\n                ),\n              ),\n              m0[3],\n            ),\n          ),\n        ),\n        sum(\n          scale(\n            sum(\n              scale(sum(prod(m2[1], m3[0]), prod(-m3[1], m2[0])), m0[2]),\n              sum(\n                scale(sum(prod(m0[1], m3[0]), prod(-m3[1], m0[0])), -m2[2]),\n                scale(sum(prod(m0[1], m2[0]), prod(-m2[1], m0[0])), m3[2]),\n              ),\n            ),\n            -m1[3],\n          ),\n          sum(\n            scale(\n              sum(\n                scale(sum(prod(m1[1], m3[0]), prod(-m3[1], m1[0])), m0[2]),\n                sum(\n                  scale(sum(prod(m0[1], m3[0]), prod(-m3[1], m0[0])), -m1[2]),\n                  scale(sum(prod(m0[1], m1[0]), prod(-m1[1], m0[0])), m3[2]),\n                ),\n              ),\n              m2[3],\n            ),\n            scale(\n              sum(\n                scale(sum(prod(m1[1], m2[0]), prod(-m2[1], m1[0])), m0[2]),\n                sum(\n                  scale(sum(prod(m0[1], m2[0]), prod(-m2[1], m0[0])), -m1[2]),\n                  scale(sum(prod(m0[1], m1[0]), prod(-m1[1], m0[0])), m2[2]),\n                ),\n              ),\n              -m3[3],\n            ),\n          ),\n        ),\n      ),\n    );\n    const n = sum(\n      sum(\n        sum(\n          scale(\n            sum(\n              scale(sum(prod(m3[1], m4[0]), prod(-m4[1], m3[0])), m2[2]),\n              sum(\n                scale(sum(prod(m2[1], m4[0]), prod(-m4[1], m2[0])), -m3[2]),\n                scale(sum(prod(m2[1], m3[0]), prod(-m3[1], m2[0])), m4[2]),\n              ),\n            ),\n            m0[3],\n          ),\n          scale(\n            sum(\n              scale(sum(prod(m3[1], m4[0]), prod(-m4[1], m3[0])), m0[2]),\n              sum(\n                scale(sum(prod(m0[1], m4[0]), prod(-m4[1], m0[0])), -m3[2]),\n                scale(sum(prod(m0[1], m3[0]), prod(-m3[1], m0[0])), m4[2]),\n              ),\n            ),\n            -m2[3],\n          ),\n        ),\n        sum(\n          scale(\n            sum(\n              scale(sum(prod(m2[1], m4[0]), prod(-m4[1], m2[0])), m0[2]),\n              sum(\n                scale(sum(prod(m0[1], m4[0]), prod(-m4[1], m0[0])), -m2[2]),\n                scale(sum(prod(m0[1], m2[0]), prod(-m2[1], m0[0])), m4[2]),\n              ),\n            ),\n            m3[3],\n          ),\n          scale(\n            sum(\n              scale(sum(prod(m2[1], m3[0]), prod(-m3[1], m2[0])), m0[2]),\n              sum(\n                scale(sum(prod(m0[1], m3[0]), prod(-m3[1], m0[0])), -m2[2]),\n                scale(sum(prod(m0[1], m2[0]), prod(-m2[1], m0[0])), m3[2]),\n              ),\n            ),\n            -m4[3],\n          ),\n        ),\n      ),\n      sum(\n        sum(\n          scale(\n            sum(\n              scale(sum(prod(m2[1], m4[0]), prod(-m4[1], m2[0])), m1[2]),\n              sum(\n                scale(sum(prod(m1[1], m4[0]), prod(-m4[1], m1[0])), -m2[2]),\n                scale(sum(prod(m1[1], m2[0]), prod(-m2[1], m1[0])), m4[2]),\n              ),\n            ),\n            m0[3],\n          ),\n          scale(\n            sum(\n              scale(sum(prod(m2[1], m4[0]), prod(-m4[1], m2[0])), m0[2]),\n              sum(\n                scale(sum(prod(m0[1], m4[0]), prod(-m4[1], m0[0])), -m2[2]),\n                scale(sum(prod(m0[1], m2[0]), prod(-m2[1], m0[0])), m4[2]),\n              ),\n            ),\n            -m1[3],\n          ),\n        ),\n        sum(\n          scale(\n            sum(\n              scale(sum(prod(m1[1], m4[0]), prod(-m4[1], m1[0])), m0[2]),\n              sum(\n                scale(sum(prod(m0[1], m4[0]), prod(-m4[1], m0[0])), -m1[2]),\n                scale(sum(prod(m0[1], m1[0]), prod(-m1[1], m0[0])), m4[2]),\n              ),\n            ),\n            m2[3],\n          ),\n          scale(\n            sum(\n              scale(sum(prod(m1[1], m2[0]), prod(-m2[1], m1[0])), m0[2]),\n              sum(\n                scale(sum(prod(m0[1], m2[0]), prod(-m2[1], m0[0])), -m1[2]),\n                scale(sum(prod(m0[1], m1[0]), prod(-m1[1], m0[0])), m2[2]),\n              ),\n            ),\n            -m4[3],\n          ),\n        ),\n      ),\n    );\n    const d = sub(p, n);\n    return d[d.length - 1];\n  };\n}\n\nfunction orientation(n: number) {\n  const fn = n === 3 ? orientation_3 : n === 4 ? orientation_4 : orientation_5;\n  return fn(robustSum, twoProduct, robustScale, robustSubtract);\n}\n\nconst orientation3Exact = orientation(3);\nconst orientation4Exact = orientation(4);\n\nconst CACHED: Array<Function> = [\n  function orientation0() {\n    return 0;\n  },\n  function orientation1() {\n    return 0;\n  },\n  function orientation2(a: PointND, b: PointND) {\n    return b[0] - a[0];\n  },\n  function orientation3(a: PointND, b: PointND, c: PointND) {\n    const l = (a[1] - c[1]) * (b[0] - c[0]);\n    const r = (a[0] - c[0]) * (b[1] - c[1]);\n    const det = l - r;\n    let s: number;\n    if (l > 0) {\n      if (r <= 0) {\n        return det;\n      } else {\n        s = l + r;\n      }\n    } else if (l < 0) {\n      if (r >= 0) {\n        return det;\n      } else {\n        s = -(l + r);\n      }\n    } else {\n      return det;\n    }\n    const tol = ERR_BOUND_3 * s;\n    if (det >= tol || det <= -tol) {\n      return det;\n    }\n    return orientation3Exact(a, b, c);\n  },\n  function orientation4(a: PointND, b: PointND, c: PointND, d: PointND) {\n    const adx = a[0] - d[0];\n    const bdx = b[0] - d[0];\n    const cdx = c[0] - d[0];\n    const ady = a[1] - d[1];\n    const bdy = b[1] - d[1];\n    const cdy = c[1] - d[1];\n    const adz = a[2] - d[2];\n    const bdz = b[2] - d[2];\n    const cdz = c[2] - d[2];\n    const bdx_cdy = bdx * cdy;\n    const cdx_bdy = cdx * bdy;\n    const cdx_ady = cdx * ady;\n    const adx_cdy = adx * cdy;\n    const adx_bdy = adx * bdy;\n    const bdx_ady = bdx * ady;\n    const det = adz * (bdx_cdy - cdx_bdy) + bdz * (cdx_ady - adx_cdy) + cdz * (adx_bdy - bdx_ady);\n    const permanent =\n      (Math.abs(bdx_cdy) + Math.abs(cdx_bdy)) * Math.abs(adz) +\n      (Math.abs(cdx_ady) + Math.abs(adx_cdy)) * Math.abs(bdz) +\n      (Math.abs(adx_bdy) + Math.abs(bdx_ady)) * Math.abs(cdz);\n    const tol = ERR_BOUND_4 * permanent;\n    if (det > tol || -det > tol) {\n      return det;\n    }\n    return orientation4Exact(a, b, c, d);\n  },\n];\n\nfunction slowOrient(args: Array<PointND | PointND>): number {\n  let proc = CACHED[args.length];\n  if (!proc) {\n    proc = CACHED[args.length] = orientation(args.length);\n  }\n  return proc.apply(undefined, ...args);\n}\n\nfunction proc(\n  slow: typeof slowOrient,\n  o0: Function,\n  o1: Function,\n  o2: Function,\n  o3: Function,\n  o4: Function,\n  o5: Function,\n): (...args: Array<PointND | PointND>) => number {\n  return function getOrientation(...args: Array<PointND | PointND>) {\n    switch (args.length) {\n      case 0:\n      case 1:\n        return 0;\n      case 2:\n        return o2(args[0], args[1]);\n      case 3:\n        return o3(args[0], args[1], args[2]);\n      case 4:\n        return o4(args[0], args[1], args[2], args[3]);\n      case 5:\n        return o5(args[0], args[1], args[2], args[3], args[4]);\n    }\n\n    return slow(args);\n  };\n}\n\nfunction generateOrientationProc(): any {\n  while (CACHED.length <= NUM_EXPAND) {\n    CACHED.push(orientation(CACHED.length));\n  }\n  // @ts-ignore\n  const orientationProc = proc(undefined, slowOrient, ...CACHED);\n  for (let i = 0; i <= NUM_EXPAND; ++i) {\n    // @ts-ignore\n    orientationProc[i] = CACHED[i];\n  }\n  return orientationProc;\n}\n\nexport default generateOrientationProc();\n"
  },
  {
    "path": "packages/g6/src/plugins/hull/hull/robust-scale.ts",
    "content": "'use strict';\n\nimport { twoProduct } from './two-product';\nimport { fastTwoSum as twoSum } from './two-sum';\n\nexport function scaleLinearExpansion(e: number[], scale: number): number[] {\n  const n = e.length;\n\n  if (n === 1) {\n    const ts = twoProduct(e[0], scale);\n    if (ts[0]) {\n      return ts;\n    }\n    return [ts[1]];\n  }\n\n  const g = new Array(2 * n);\n  const q: [number, number] = [0.1, 0.1];\n  const t: [number, number] = [0.1, 0.1];\n  let count = 0;\n\n  twoProduct(e[0], scale, q);\n  if (q[0]) {\n    g[count++] = q[0];\n  }\n\n  for (let i = 1; i < n; ++i) {\n    twoProduct(e[i], scale, t);\n    const pq = q[1];\n    twoSum(pq, t[0], q);\n    if (q[0]) {\n      g[count++] = q[0];\n    }\n    const a = t[1];\n    const b = q[1];\n    const x = a + b;\n    const bv = x - a;\n    const y = b - bv;\n    q[1] = x;\n    if (y) {\n      g[count++] = y;\n    }\n  }\n\n  if (q[1]) {\n    g[count++] = q[1];\n  }\n  if (count === 0) {\n    g[count++] = 0.0;\n  }\n  g.length = count;\n  return g;\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/hull/hull/robust-segment-intersect.ts",
    "content": "import type { Point } from '../../../types';\nimport orient from './robust-orientation';\n\nfunction checkCollinear(a0: Point, a1: Point, b0: Point, b1: Point): boolean {\n  for (let d = 0; d < 2; ++d) {\n    const x0 = a0[d];\n    const y0 = a1[d];\n    const [l0, h0] = [Math.min(x0, y0), Math.max(x0, y0)];\n\n    const x1 = b0[d];\n    const y1 = b1[d];\n    const [l1, h1] = [Math.min(x1, y1), Math.max(x1, y1)];\n    if (h1 < l0 || h0 < l1) return false;\n  }\n  return true;\n}\n\nexport function segmentsIntersect(a0: Point, a1: Point, b0: Point, b1: Point): boolean {\n  const x0 = orient(a0, b0, b1);\n  const y0 = orient(a1, b0, b1);\n  if ((x0 > 0 && y0 > 0) || (x0 < 0 && y0 < 0)) return false;\n  const x1 = orient(b0, a0, a1);\n  const y1 = orient(b1, a0, a1);\n  if ((x1 > 0 && y1 > 0) || (x1 < 0 && y1 < 0)) return false;\n  if (x0 === 0 && y0 === 0 && x1 === 0 && y1 === 0) {\n    return checkCollinear(a0, a1, b0, b1);\n  }\n  return true;\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/hull/hull/robust-subtract.ts",
    "content": "'use strict';\n\nfunction scalarScalar(a: number, b: number): number[] {\n  const x = a + b;\n  const bv = x - a;\n  const av = x - bv;\n  const br = b - bv;\n  const ar = a - av;\n  const y = ar + br;\n\n  if (y) {\n    return [y, x];\n  }\n  return [x];\n}\n\nexport function robustSubtract(e: number[], f: number[]): number[] {\n  const ne = e.length | 0;\n  const nf = f.length | 0;\n\n  // Base case: scalar subtraction\n  if (ne === 1 && nf === 1) {\n    return scalarScalar(e[0], -f[0]);\n  }\n\n  const n = ne + nf;\n  const g = new Array(n);\n  let count = 0;\n\n  let ep_tr = 0;\n  let fp_tr = 0;\n\n  const abs = Math.abs;\n  let ei = e[ep_tr];\n  let ea = abs(ei);\n  let fi = -f[fp_tr];\n  let fa = abs(fi);\n\n  let a: number, b: number;\n\n  // Initial comparison to determine `a` and `b`\n  if (ea < fa) {\n    b = ei;\n    ep_tr += 1;\n    if (ep_tr < ne) {\n      ei = e[ep_tr];\n      ea = abs(ei);\n    }\n  } else {\n    b = fi;\n    fp_tr += 1;\n    if (fp_tr < nf) {\n      fi = -f[fp_tr];\n      fa = abs(fi);\n    }\n  }\n\n  if ((ep_tr < ne && ea < fa) || fp_tr >= nf) {\n    a = ei;\n    ep_tr += 1;\n    if (ep_tr < ne) {\n      ei = e[ep_tr];\n      ea = abs(ei);\n    }\n  } else {\n    a = fi;\n    fp_tr += 1;\n    if (fp_tr < nf) {\n      fi = -f[fp_tr];\n      fa = abs(fi);\n    }\n  }\n\n  let x = a + b;\n  let bv = x - a;\n  let y = b - bv;\n\n  let q0 = y;\n  let q1 = x;\n\n  let _x: number, _bv: number, _av: number, _br: number, _ar: number;\n\n  // Main loop for combining expansions\n  while (ep_tr < ne && fp_tr < nf) {\n    if (ea < fa) {\n      a = ei;\n      ep_tr += 1;\n      if (ep_tr < ne) {\n        ei = e[ep_tr];\n        ea = abs(ei);\n      }\n    } else {\n      a = fi;\n      fp_tr += 1;\n      if (fp_tr < nf) {\n        fi = -f[fp_tr];\n        fa = abs(fi);\n      }\n    }\n\n    b = q0;\n    x = a + b;\n    bv = x - a;\n    y = b - bv;\n\n    if (y) {\n      g[count++] = y;\n    }\n\n    _x = q1 + x;\n    _bv = _x - q1;\n    _av = _x - _bv;\n    _br = x - _bv;\n    _ar = q1 - _av;\n\n    q0 = _ar + _br;\n    q1 = _x;\n  }\n\n  // Handle remaining elements in `e`\n  while (ep_tr < ne) {\n    a = ei;\n    b = q0;\n    x = a + b;\n    bv = x - a;\n    y = b - bv;\n\n    if (y) {\n      g[count++] = y;\n    }\n\n    _x = q1 + x;\n    _bv = _x - q1;\n    _av = _x - _bv;\n    _br = x - _bv;\n    _ar = q1 - _av;\n\n    q0 = _ar + _br;\n    q1 = _x;\n\n    ep_tr += 1;\n    if (ep_tr < ne) {\n      ei = e[ep_tr];\n    }\n  }\n\n  // Handle remaining elements in `f`\n  while (fp_tr < nf) {\n    a = fi;\n    b = q0;\n    x = a + b;\n    bv = x - a;\n    y = b - bv;\n\n    if (y) {\n      g[count++] = y;\n    }\n\n    _x = q1 + x;\n    _bv = _x - q1;\n    _av = _x - _bv;\n    _br = x - _bv;\n    _ar = q1 - _av;\n\n    q0 = _ar + _br;\n    q1 = _x;\n\n    fp_tr += 1;\n    if (fp_tr < nf) {\n      fi = -f[fp_tr];\n    }\n  }\n\n  // Finalize the result\n  if (q0) {\n    g[count++] = q0;\n  }\n  if (q1) {\n    g[count++] = q1;\n  }\n  if (!count) {\n    g[count++] = 0.0;\n  }\n\n  g.length = count;\n  return g;\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/hull/hull/robust-sum.ts",
    "content": "'use strict';\n\nfunction scalarScalar(a: number, b: number): number[] {\n  const x = a + b;\n  const bv = x - a;\n  const av = x - bv;\n  const br = b - bv;\n  const ar = a - av;\n  const y = ar + br;\n\n  if (y) {\n    return [y, x];\n  }\n  return [x];\n}\n\nexport function linearExpansionSum(e: number[], f: number[]): number[] {\n  const ne = e.length | 0;\n  const nf = f.length | 0;\n\n  if (ne === 1 && nf === 1) {\n    return scalarScalar(e[0], f[0]);\n  }\n\n  const n = ne + nf;\n  const g = new Array(n);\n  let count = 0;\n\n  let ep_tr = 0;\n  let fp_tr = 0;\n\n  const abs = Math.abs;\n  let ei = e[ep_tr];\n  let ea = abs(ei);\n  let fi = f[fp_tr];\n  let fa = abs(fi);\n  let a, b;\n\n  if (ea < fa) {\n    b = ei;\n    ep_tr += 1;\n    if (ep_tr < ne) {\n      ei = e[ep_tr];\n      ea = abs(ei);\n    }\n  } else {\n    b = fi;\n    fp_tr += 1;\n    if (fp_tr < nf) {\n      fi = f[fp_tr];\n      fa = abs(fi);\n    }\n  }\n\n  if ((ep_tr < ne && ea < fa) || fp_tr >= nf) {\n    a = ei;\n    ep_tr += 1;\n    if (ep_tr < ne) {\n      ei = e[ep_tr];\n      ea = abs(ei);\n    }\n  } else {\n    a = fi;\n    fp_tr += 1;\n    if (fp_tr < nf) {\n      fi = f[fp_tr];\n      fa = abs(fi);\n    }\n  }\n\n  let x = a + b;\n  let bv = x - a;\n  let y = b - bv;\n  let q0 = y;\n  let q1 = x;\n\n  let _x, _bv, _av, _br, _ar;\n\n  while (ep_tr < ne && fp_tr < nf) {\n    if (ea < fa) {\n      a = ei;\n      ep_tr += 1;\n      if (ep_tr < ne) {\n        ei = e[ep_tr];\n        ea = abs(ei);\n      }\n    } else {\n      a = fi;\n      fp_tr += 1;\n      if (fp_tr < nf) {\n        fi = f[fp_tr];\n        fa = abs(fi);\n      }\n    }\n\n    b = q0;\n    x = a + b;\n    bv = x - a;\n    y = b - bv;\n\n    if (y) {\n      g[count++] = y;\n    }\n\n    _x = q1 + x;\n    _bv = _x - q1;\n    _av = _x - _bv;\n    _br = x - _bv;\n    _ar = q1 - _av;\n    q0 = _ar + _br;\n    q1 = _x;\n  }\n\n  while (ep_tr < ne) {\n    a = ei;\n    b = q0;\n    x = a + b;\n    bv = x - a;\n    y = b - bv;\n\n    if (y) {\n      g[count++] = y;\n    }\n\n    _x = q1 + x;\n    _bv = _x - q1;\n    _av = _x - _bv;\n    _br = x - _bv;\n    _ar = q1 - _av;\n    q0 = _ar + _br;\n    q1 = _x;\n\n    ep_tr += 1;\n    if (ep_tr < ne) {\n      ei = e[ep_tr];\n    }\n  }\n\n  while (fp_tr < nf) {\n    a = fi;\n    b = q0;\n    x = a + b;\n    bv = x - a;\n    y = b - bv;\n\n    if (y) {\n      g[count++] = y;\n    }\n\n    _x = q1 + x;\n    _bv = _x - q1;\n    _av = _x - _bv;\n    _br = x - _bv;\n    _ar = q1 - _av;\n    q0 = _ar + _br;\n    q1 = _x;\n\n    fp_tr += 1;\n    if (fp_tr < nf) {\n      fi = f[fp_tr];\n    }\n  }\n\n  if (q0) {\n    g[count++] = q0;\n  }\n  if (q1) {\n    g[count++] = q1;\n  }\n  if (!count) {\n    g[count++] = 0.0;\n  }\n  g.length = count;\n  return g;\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/hull/hull/two-product.ts",
    "content": "'use strict';\n\nconst SPLITTER: number = +(Math.pow(2, 27) + 1.0);\n\nexport function twoProduct(a: number, b: number, result?: [number, number]): [number, number] {\n  const x: number = a * b;\n\n  const c: number = SPLITTER * a;\n  const a_big: number = c - a;\n  const ahi: number = c - a_big;\n  const alo: number = a - ahi;\n\n  const d: number = SPLITTER * b;\n  const b_big: number = d - b;\n  const bhi: number = d - b_big;\n  const blo: number = b - bhi;\n\n  const err1: number = x - ahi * bhi;\n  const err2: number = err1 - alo * bhi;\n  const err3: number = err2 - ahi * blo;\n\n  const y: number = alo * blo - err3;\n\n  if (result) {\n    result[0] = y;\n    result[1] = x;\n    return result;\n  }\n\n  return [y, x];\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/hull/hull/two-sum.ts",
    "content": "'use strict';\n\nexport function fastTwoSum(a: number, b: number, result?: [number, number]): [number, number] {\n  const x = a + b;\n  const bv = x - a;\n  const av = x - bv;\n  const br = b - bv;\n  const ar = a - av;\n\n  if (result) {\n    result[0] = ar + br;\n    result[1] = x;\n    return result;\n  }\n\n  return [ar + br, x];\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/hull/index.ts",
    "content": "import { PathArray, isEqual, isFunction } from '@antv/util';\nimport { GraphEvent } from '../../constants';\nimport type { ContourStyleProps } from '../../elements/shapes';\nimport { Contour } from '../../elements/shapes';\nimport type { RuntimeContext } from '../../runtime/types';\nimport type { ID, Point } from '../../types';\nimport type { ElementLifeCycleEvent } from '../../utils/event';\nimport { idOf } from '../../utils/id';\nimport { positionOf } from '../../utils/position';\nimport type { BasePluginOptions } from '../base-plugin';\nimport { BasePlugin } from '../base-plugin';\nimport { hull } from './hull';\nimport { computeHullPath } from './util';\n\n/**\n * <zh/> Hull 配置项\n *\n * <en/> Hull options\n */\nexport interface HullOptions extends BasePluginOptions, ContourStyleProps {\n  /**\n   * <zh/> Hull 内的元素\n   *\n   * <en/> Elements in Hull\n   */\n  members?: ID[];\n  /**\n   * <zh/> 凹度，数值越大凹度越小；默认为 Infinity 代表为 Convex Hull\n   *\n   * <en/> Concavity. Default is Infinity, which means Convex Hull\n   * @defaultValue Infinity\n   */\n  concavity?: number;\n  /**\n   * <zh/> 内边距\n   *\n   * <en/> Padding\n   * @defaultValue 10\n   */\n  padding?: number;\n  /**\n   * <zh/> 拐角类型\n   *\n   * <en/> Corner type\n   * @defaultValue 'rounded'\n   */\n  corner?: 'rounded' | 'smooth' | 'sharp';\n}\n\n/**\n * <zh/> 轮廓\n *\n * <en/> Hull\n * @remarks\n * <zh/> 轮廓包围（Hull）用于处理和表示一组点的凸多边形包围盒。轮廓包围分为两种形态：凸包和凹包。\n *\n * <zh/> 凸包（Convex Hull）：这是一个凸多边形，它包含所有的点，并且没有任何凹陷。你可以将其想象为一组点的最小包围盒，没有任何点在这个多边形的外部。\n *\n * <zh/> 凹包（Concave Hull）：这是一个凹多边形，它同样包含所有的点，但是可能会有凹陷。凹包的凹陷程度由 concavity 参数控制。concavity 越大，凹陷越小。当 concavity 为 Infinity 时，凹包就变成了凸包。\n *\n * <en/> Hull is used to process and represent the convex polygon bounding box of a set of points. Hull has two forms: convex hull and concave hull.\n *\n * <en/>  Convex Hull: This is a convex polygon that contains all points and has no concave. You can think of it as the smallest bounding box of a set of points, with no points outside the polygon.\n *\n *  <en/>  Concave Hull: This is a concave polygon that also contains all points, but may have concave. The concavity of the concave hull is controlled by the concavity parameter. The larger the concavity, the smaller the concave. When concavity is Infinity, the concave hull becomes a convex hull.\n */\nexport class Hull extends BasePlugin<HullOptions> {\n  private shape!: Contour;\n  /**\n   * <zh/> 在 Hull 上的元素\n   *\n   * <en/> Element Ids on Hull\n   */\n  private hullMemberIds: ID[] = [];\n  /**\n   * <zh/> Hull 绘制路径\n   *\n   * <en/> Hull path\n   */\n  private path!: PathArray;\n\n  private optionsCache!: HullOptions;\n\n  static defaultOptions: Partial<HullOptions> = {\n    members: [],\n    padding: 10,\n    corner: 'rounded',\n    concavity: Infinity,\n    /** hull style */\n    fill: 'lightblue',\n    fillOpacity: 0.2,\n    labelOpacity: 1,\n    stroke: 'blue',\n    strokeOpacity: 0.2,\n  };\n\n  constructor(context: RuntimeContext, options: HullOptions) {\n    super(context, Object.assign({}, Hull.defaultOptions, options));\n\n    this.bindEvents();\n  }\n\n  private bindEvents() {\n    this.context.graph.on(GraphEvent.AFTER_RENDER, this.drawHull);\n    this.context.graph.on(GraphEvent.AFTER_ELEMENT_UPDATE, this.updateHullPath);\n  }\n\n  private unbindEvents() {\n    this.context.graph.off(GraphEvent.AFTER_RENDER, this.drawHull);\n    this.context.graph.off(GraphEvent.AFTER_ELEMENT_UPDATE, this.updateHullPath);\n  }\n\n  private getHullStyle(forceUpdate?: boolean): ContourStyleProps {\n    const { members, padding, corner, ...style } = this.options;\n    return { ...style, d: this.getHullPath(forceUpdate) };\n  }\n\n  private drawHull = () => {\n    if (!this.shape) {\n      this.shape = new Contour({ style: this.getHullStyle() });\n      this.context.canvas.appendChild(this.shape);\n    } else {\n      const forceUpdate = !isEqual(this.optionsCache, this.options);\n      this.shape.update(this.getHullStyle(forceUpdate));\n    }\n    this.optionsCache = { ...this.options };\n  };\n\n  private updateHullPath = (event: ElementLifeCycleEvent) => {\n    if (!this.shape) return;\n    if (!this.options.members.includes(idOf(event.data))) return;\n    this.shape.update({ d: this.getHullPath(true) });\n  };\n\n  private getHullPath = (forceUpdate = false): string | PathArray => {\n    const { graph } = this.context;\n    const members = this.getMember();\n    if (members.length === 0) return '';\n\n    const data = members.map((id) => graph.getNodeData(id));\n    const vertices = hull(data.map(positionOf), this.options.concavity).slice(1).reverse() as Point[];\n    const hullMemberIds = vertices.flatMap((point) => data.filter((m) => isEqual(positionOf(m), point)).map(idOf));\n    if (isEqual(hullMemberIds, this.hullMemberIds) && !forceUpdate) return this.path;\n    this.hullMemberIds = hullMemberIds;\n    this.path = computeHullPath(vertices, this.getPadding(), this.options.corner);\n    return this.path;\n  };\n\n  private getPadding() {\n    const { graph } = this.context;\n\n    const memberPadding = this.hullMemberIds.reduce((acc: number, id: ID) => {\n      const { halfExtents } = graph.getElementRenderBounds(id);\n      const size = Math.max(halfExtents[0], halfExtents[1]);\n      return Math.max(acc, size);\n    }, 0);\n\n    return memberPadding + this.options.padding;\n  }\n\n  /**\n   * <zh/> 添加 Hull 成员\n   *\n   * <en/> Add Hull member\n   * @param members - <zh/> 元素 Ids | <en/> Element Ids\n   */\n  public addMember(members: ID | ID[]) {\n    const membersToAdd = Array.isArray(members) ? members : [members];\n    this.options.members = [...new Set([...this.options.members, ...membersToAdd])];\n    this.shape.update({ d: this.getHullPath() });\n  }\n\n  /**\n   * <zh/> 移除 Hull 成员\n   *\n   * <en/> Remove Hull member\n   * @param members - <zh/> 元素 Ids | <en/> Element Ids\n   */\n  public removeMember(members: ID | ID[]) {\n    const membersToRemove = Array.isArray(members) ? members : [members];\n    this.options.members = this.options.members.filter((id) => !membersToRemove.includes(id));\n    if (membersToRemove.some((id) => this.hullMemberIds.includes(id))) {\n      this.shape.update({ d: this.getHullPath() });\n    }\n  }\n\n  /**\n   * <zh/> 更新 Hull 成员\n   *\n   * <en/> Update Hull member\n   * @param members - <zh/> 元素 Ids | <en/> Element Ids\n   */\n  public updateMember(members: ID[] | ((prev: ID[]) => ID[])) {\n    this.options.members = isFunction(members) ? members(this.options.members) : members;\n    this.shape.update(this.getHullStyle(true));\n  }\n\n  /**\n   * <zh/> 获取 Hull 成员\n   *\n   * <en/> Get Hull member\n   * @returns <zh/> 元素 Ids | <en/> Element Ids\n   */\n  public getMember() {\n    return this.options.members;\n  }\n\n  /**\n   * <zh/> 销毁 Hull\n   *\n   * <en/> Destroy Hull\n   * @internal\n   */\n  public destroy(): void {\n    this.unbindEvents();\n    this.shape.destroy();\n    this.hullMemberIds = [];\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/hull/util.ts",
    "content": "import { type PathArray } from '@antv/util';\nimport type { Point, Vector2 } from '../../types';\nimport { getLinesIntersection } from '../../utils/line';\nimport { getClosedSpline } from '../../utils/path';\nimport { isCollinear, sortByClockwise } from '../../utils/point';\nimport { add, angle, normalize, perpendicular, scale, subtract, toVector2, toVector3 } from '../../utils/vector';\nimport type { HullOptions } from '../hull';\n\n/**\n * <zh/> 计算 Hull 路径\n *\n * <en/> Compute Hull Path\n * @param points - <zh/> 顶点列表 | <en/> Vertices of Hull\n * @param padding - <zh/> 内边距 | <en/> padding\n * @param corner - <zh/> 拐角类型，目前支持 'sharp'、'rounded' 和 'smooth' | <en/> Corner type, currently supports 'sharp', 'rounded' and 'smooth'\n * @returns <zh/> Hull 路径 | <en/> Hull Path\n */\nexport function computeHullPath(points: Point[], padding: number, corner: 'rounded' | 'smooth' | 'sharp'): PathArray {\n  if (points.length === 1) return genSinglePointHullPath(points[0], padding, corner);\n  if (points.length === 2) return genTwoPointsHullPath(points, padding, corner);\n  if (points.length === 3) {\n    const [p1, p2, p3] = sortByClockwise(points);\n    if (isCollinear(p1, p2, p3)) return genTwoPointsHullPath([p1, p3], padding, corner);\n  }\n\n  switch (corner) {\n    case 'smooth':\n      return genMultiPointsSmoothHull(points, padding);\n    case 'sharp':\n      return genMultiPointsSharpHull(points, padding);\n    case 'rounded':\n    default:\n      return genMultiPointsRoundedHull(points, padding);\n  }\n}\n\n/**\n * <zh/> 生成单点 Hull 路径\n *\n * <en/> Generate Hull Path for a single point\n * @param point - <zh/> 单点 | <en/> Single point\n * @param padding - <zh/> 内边距 | <en/> Padding\n * @param corner - <zh/> 拐角类型 | <en/> Corner type\n * @returns <zh/> 单点 Hull 路径 | <en/> Single point Hull Path\n */\nconst genSinglePointHullPath = (point: Point, padding: number, corner: HullOptions['corner']): PathArray => {\n  if (corner === 'sharp')\n    return [\n      ['M', point[0] - padding, point[1] - padding],\n      ['L', point[0] + padding, point[1] - padding],\n      ['L', point[0] + padding, point[1] + padding],\n      ['L', point[0] - padding, point[1] + padding],\n      ['Z'],\n    ];\n  const arcData: [number, number, number, number, number] = [padding, padding, 0, 0, 0];\n  return [\n    ['M', point[0], point[1] - padding],\n    ['A', ...arcData, point[0], point[1] + padding],\n    ['A', ...arcData, point[0], point[1] - padding],\n  ];\n};\n\n/**\n * <zh/> 生成两点 Hull 路径\n *\n * <en/> Generate Hull Path for two points\n * @param points - <zh/> 两点 | <en/> Two points\n * @param padding - <zh/> 内边距 | <en/> Padding\n * @param corner - <zh/> 拐角类型 | <en/> Corner type\n * @returns <zh/> 两点 Hull 路径 | <en/> Two points Hull Path\n */\nconst genTwoPointsHullPath = (points: Point[], padding: number, corner: HullOptions['corner']): PathArray => {\n  const arcData: [number, number, number, number, number] = [padding, padding, 0, 0, 0];\n\n  const point1 =\n    corner === 'sharp' ? add(points[0], scale(normalize(subtract(points[0], points[1])), padding)) : points[0];\n  const point2 =\n    corner === 'sharp' ? add(points[1], scale(normalize(subtract(points[1], points[0])), padding)) : points[1];\n\n  const offsetVector = scale(normalize(perpendicular(subtract(point1, point2) as Vector2, false)), padding);\n  const invOffsetVector = scale(offsetVector, -1);\n\n  const prev = add(point1, offsetVector);\n  const current = add(point2, offsetVector);\n  const p2 = add(point2, invOffsetVector);\n  const p3 = add(point1, invOffsetVector);\n\n  if (corner === 'sharp') {\n    return [['M', prev[0], prev[1]], ['L', current[0], current[1]], ['L', p2[0], p2[1]], ['L', p3[0], p3[1]], ['Z']];\n  }\n\n  return [\n    ['M', prev[0], prev[1]],\n    ['L', current[0], current[1]],\n    ['A', ...arcData, p2[0], p2[1]],\n    ['L', p3[0], p3[1]],\n    ['A', ...arcData, prev[0], prev[1]],\n  ];\n};\n\n/**\n * <zh/> 生成多点 Hull 路径且拐角为圆角\n *\n * <en/> Generate Hull Path for multiple points with rounded corners\n * @param points - <zh/> 形成 Hull 的点集 | <en/> Points that form the Hull\n * @param padding - <zh/> 内边距 | <en/> Padding\n * @returns <zh/> 圆角外壳路径 | <en/> Rounded hull path\n */\nconst genMultiPointsRoundedHull = (points: Point[], padding: number): PathArray => {\n  const segments = sortByClockwise(points).map((current, i) => {\n    const prev2Index = (i - 2 + points.length) % points.length;\n    const prevIndex = (i - 1 + points.length) % points.length;\n    const nextIndex = (i + 1) % points.length;\n    const prev2 = points[prev2Index];\n    const prev = points[prevIndex];\n    const next = points[nextIndex];\n\n    const v0 = subtract(prev2, prev) as Vector2;\n    const v1 = subtract(prev, current) as Vector2;\n    const v2 = subtract(current, next) as Vector2;\n\n    // 判断是否为凹角 ｜ Determine if it is a concave angle\n    const isConcave = (v1: Vector2, v2: Vector2): boolean => {\n      return angle(v1, v2, true) < Math.PI;\n    };\n    const concavePrev = isConcave(v0, v1);\n    const concaveNext = isConcave(v1, v2);\n\n    const offsetVector = (v: Vector2) => scale(normalize(perpendicular(v, false)), padding);\n    const offset = offsetVector(v1);\n    return [\n      {\n        p: toVector2(concavePrev ? add(prev, offsetVector(v0)) : add(prev, offset)),\n        concave: concavePrev && prev,\n      },\n      {\n        p: toVector2(concaveNext ? add(current, offsetVector(v2)) : add(current, offset)),\n        concave: concaveNext && current,\n      },\n    ];\n  });\n\n  const arcData = [padding, padding, 0, 0, 0];\n  const startIndex = segments.findIndex(\n    (segment, i) =>\n      !segments[(i - 1 + segments.length) % segments.length][0].concave &&\n      !segments[(i - 1 + segments.length) % segments.length][1].concave &&\n      !segment[0].concave &&\n      !segment[0].concave &&\n      !segment[1].concave,\n  );\n  const sortedSegments = segments.slice(startIndex).concat(segments.slice(0, startIndex));\n  let concavePoints: Point[] = [];\n  return sortedSegments.flatMap((segment, i) => {\n    const pathFragment = [];\n    const lastSegment = sortedSegments[segments.length - 1];\n    if (i === 0) pathFragment.push(['M', ...lastSegment[1].p]);\n    if (!segment[0].concave) {\n      pathFragment.push(['A', ...arcData, ...segment[0].p]);\n    } else {\n      concavePoints.push(segment[0].p, segment[1].p);\n    }\n    if (!segment[1].concave) {\n      pathFragment.push(['L', ...segment[1].p]);\n    } else {\n      concavePoints.unshift(segment[1].p);\n    }\n    if (concavePoints.length === 3) {\n      pathFragment.pop();\n      pathFragment.push(['C', ...concavePoints.flat()]);\n      concavePoints = [];\n    }\n    return pathFragment;\n  }) as PathArray;\n};\n\n/**\n * <zh/> 生成多点 Hull 路径且拐角为平滑\n *\n * <en/> Generate Hull Path for multiple points with smooth corners\n * @param points - <zh/> 形成 Hull 的点集 | <en/> Points that form the Hull\n * @param padding - <zh/> 内边距 | <en/> Padding\n * @returns <zh/> 平滑外壳路径 | <en/> Smooth hull path\n */\nconst genMultiPointsSmoothHull = (points: Point[], padding: number): PathArray => {\n  const hullPoints = sortByClockwise(points).map((p, i) => {\n    const pNext = points[(i + 1) % points.length];\n    return { p, v: normalize(subtract(pNext, p)) };\n  });\n\n  // Compute the expanded hull points, and the nearest prior control point for each.\n  hullPoints.forEach((hp, i) => {\n    const priorIndex = i > 0 ? i - 1 : points.length - 1;\n    const prevV = hullPoints[priorIndex].v as Vector2;\n    const extensionVec = normalize(add(prevV, scale(hp.v, angle(prevV, hp.v, true) < Math.PI ? 1 : -1)));\n    hp.p = add(hp.p, scale(extensionVec, padding));\n  });\n\n  return getClosedSpline(hullPoints.map((obj) => obj.p));\n};\n\n/**\n * <zh/> 生成多点 Hull 路径且拐角为尖锐\n *\n * <en/> Generate Hull Path for multiple points with sharp corners\n * @param points - <zh/> 形成 Hull 的点集 | <en/> Points that form the Hull\n * @param padding - <zh/> 内边距 | <en/> Padding\n * @returns <zh/> 锐角外壳路径 | <en/> Sharp hull path\n */\nconst genMultiPointsSharpHull = (points: Point[], padding: number): PathArray => {\n  const segments = points.map((current, i) => {\n    const prev = points[i === 0 ? points.length - 1 : i - 1];\n    const offset = toVector3(scale(normalize(perpendicular(subtract(prev, current) as Vector2, false)), padding));\n    return [add(prev, offset), add(current, offset)];\n  });\n\n  const arr = segments.flat();\n\n  const vertices = arr\n    .map((_, i) => {\n      if (i % 2 === 0) return null;\n      const l1 = [arr[(i - 1) % arr.length], arr[i % arr.length]] as [Point, Point];\n      const l2 = [arr[(i + 1) % arr.length], arr[(i + 2) % arr.length]] as [Point, Point];\n      return getLinesIntersection(l1, l2, true);\n    })\n    .filter(Boolean) as Point[];\n  return vertices.map((point, i) => [i === 0 ? 'M' : 'L', point[0], point[1]]).concat([['Z']]) as PathArray;\n};\n"
  },
  {
    "path": "packages/g6/src/plugins/index.ts",
    "content": "export { Background } from './background';\nexport { BasePlugin } from './base-plugin';\nexport { BubbleSets } from './bubble-sets';\nexport { CameraSetting } from './camera-setting';\nexport { Contextmenu } from './contextmenu';\nexport { EdgeBundling } from './edge-bundling';\nexport { EdgeFilterLens } from './edge-filter-lens';\nexport { Fisheye } from './fisheye';\nexport { Fullscreen } from './fullscreen';\nexport { GridLine } from './grid-line';\nexport { History } from './history';\nexport { Hull } from './hull';\nexport { Legend } from './legend';\nexport { Minimap } from './minimap';\nexport { Snapline } from './snapline';\nexport { Timebar } from './timebar';\nexport { Title } from './title';\nexport { Toolbar } from './toolbar';\nexport { Tooltip } from './tooltip';\nexport { Watermark } from './watermark';\n\nexport type { BackgroundOptions } from './background';\nexport type { BasePluginOptions } from './base-plugin';\nexport type { BubbleSetsOptions } from './bubble-sets';\nexport type { CameraSettingOptions } from './camera-setting';\nexport type { ContextmenuOptions } from './contextmenu';\nexport type { EdgeBundlingOptions } from './edge-bundling';\nexport type { EdgeFilterLensOptions } from './edge-filter-lens';\nexport type { FisheyeOptions } from './fisheye';\nexport type { FullscreenOptions } from './fullscreen';\nexport type { GridLineOptions } from './grid-line';\nexport type { HistoryOptions } from './history';\nexport type { HullOptions } from './hull';\nexport type { LegendOptions } from './legend';\nexport type { MinimapOptions } from './minimap';\nexport type { SnaplineOptions } from './snapline';\nexport type { TimebarOptions } from './timebar';\nexport type { SubTitleStyle, TitleOptions, TitleStyle } from './title';\nexport type { ToolbarOptions } from './toolbar';\nexport type { TooltipOptions } from './tooltip';\nexport type { WatermarkOptions } from './watermark';\n"
  },
  {
    "path": "packages/g6/src/plugins/legend.ts",
    "content": "import { Category, Selection } from '@antv/component';\nimport { CategoryStyleProps } from '@antv/component/lib/ui/legend/types';\nimport { Canvas } from '@antv/g';\nimport { get, isFunction } from '@antv/util';\nimport { GraphEvent } from '../constants';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { ElementDatum, ElementType, ID, State } from '../types';\nimport type { CardinalPlacement } from '../types/placement';\nimport type { BasePluginOptions } from './base-plugin';\nimport { BasePlugin } from './base-plugin';\nimport { createPluginCanvas } from './utils/canvas';\n\ninterface Datum extends Record<string, any> {\n  id?: string;\n  label?: string;\n  color?: string;\n  marker?: string;\n  elementType?: ElementType;\n}\n\n/**\n * <zh/> 图例配置项\n *\n * <en/> Legend options\n */\nexport interface LegendOptions extends BasePluginOptions, Omit<CategoryStyleProps, 'data'> {\n  /**\n   * <zh/> 图例触发行为\n   * - `'hover'`：鼠标移入图例项时触发\n   * - `'click'`：鼠标点击图例项时触发\n   *\n   * <en/> Legend trigger behavior\n   * - `'hover'`：mouseover the legend item\n   * - `'click'`：click the legend item\n   * @defaultValue 'hover'\n   */\n  trigger?: 'hover' | 'click';\n  /**\n   * <zh/> 图例在画布中的相对位置，默认为 'bottom'，代表在画布正下方\n   *\n   * <en/> Relative position of the legend in the canvas, defaults to 'bottom', representing the bottom of the canvas\n   * @defaultValue 'bottom'\n   */\n  position?: CardinalPlacement;\n  /**\n   * <zh/> 图例挂载的容器，无则挂载到 Graph 所在容器\n   *\n   * <en/> The container where the legend is mounted, if not, it will be mounted to the container where the Graph is located\n   */\n  container?: HTMLElement | string;\n  /**\n   * <zh/> 图例画布类名，传入外置容器时不生效\n   *\n   * <en/> The class name of the legend canvas, which does not take effect when an external container is passed in\n   */\n  className?: string;\n  /**\n   * <zh/> 图例的容器样式，传入外置容器时不生效\n   *\n   * <en/> The style of the legend container, which does not take effect when an external container is passed in\n   */\n  containerStyle?: Partial<CSSStyleDeclaration>;\n  /**\n   * <zh/> 节点分类标识\n   *\n   * <en/> Node Classification Identifier\n   */\n  nodeField?: string | ((item: ElementDatum) => string);\n  /**\n   * <zh/> 边分类标识\n   *\n   * <en/> Edge Classification Identifier\n   */\n  edgeField?: string | ((item: ElementDatum) => string);\n  /**\n   * <zh/> 组合分类标识\n   *\n   * <en/> Combo Classification Identifier\n   */\n  comboField?: string | ((item: ElementDatum) => string);\n}\n\n/**\n * <zh/> 图例\n *\n * <en/> Legend\n * @remarks\n * <zh/> 图例插件用于展示图中元素的分类信息，支持节点、边、组合的分类信息展示。\n *\n * <en/> The legend plugin is used to display the classification information of elements in the graph, and supports the display of classification information of nodes, edges, and combos.\n */\nexport class Legend extends BasePlugin<LegendOptions> {\n  static defaultOptions: Partial<LegendOptions> = {\n    position: 'bottom',\n    trigger: 'hover',\n    orientation: 'horizontal',\n    layout: 'flex',\n    itemSpacing: 4,\n    rowPadding: 10,\n    colPadding: 10,\n    itemMarkerSize: 16,\n    itemLabelFontSize: 16,\n    width: 240,\n    height: 160,\n  };\n  private typePrefix = '__data__';\n  private draw = false;\n  private fieldMap = {\n    node: new Map<string, ID[]>(),\n    edge: new Map<string, ID[]>(),\n    combo: new Map<string, ID[]>(),\n  };\n  private selectedItems: string[] = [];\n  private category?: Category;\n  private container?: HTMLElement;\n  private canvas?: Canvas;\n\n  constructor(context: RuntimeContext, options: LegendOptions) {\n    super(context, Object.assign({}, Legend.defaultOptions, options));\n    this.bindEvents();\n  }\n\n  /**\n   * <zh/> 更新图例配置\n   *\n   * <en/> Update the legend configuration\n   * @param options - <zh/> 图例配置项 | <en/> Legend options\n   * @internal\n   */\n  public update(options: Partial<LegendOptions>) {\n    super.update(options);\n    this.clear();\n    this.createElement();\n  }\n\n  private clear() {\n    this.canvas?.destroy();\n    this.container?.remove();\n    this.canvas = undefined;\n    this.container = undefined;\n\n    this.draw = false;\n  }\n\n  private bindEvents = () => {\n    const { graph } = this.context;\n    graph.on(GraphEvent.AFTER_DRAW, this.createElement);\n  };\n\n  private changeState = (el: Selection, state: State | State[]) => {\n    const { graph } = this.context;\n    const { typePrefix } = this;\n    const composeId = get(el, [typePrefix, 'id']);\n    const category = get(el, [typePrefix, 'style', 'labelText']);\n    const [type] = composeId.split('__');\n    const ids = this.fieldMap[type as keyof typeof this.fieldMap].get(category) || [];\n\n    graph.setElementState(Object.fromEntries(ids?.map((id) => [id, state])));\n  };\n\n  /**\n   * <zh/> 图例元素点击事件\n   *\n   * <en/> Legend element click event\n   * @param event - <zh/> 点击的元素 | <en/> The element that is clicked\n   */\n  public click = (event: Selection) => {\n    if (this.options.trigger === 'hover') return;\n    const composeId = get(event, [this.typePrefix, 'id']);\n    if (!this.selectedItems.includes(composeId)) {\n      this.selectedItems.push(composeId);\n      this.changeState(event, 'selected');\n    } else {\n      this.selectedItems = this.selectedItems.filter((item) => item !== composeId);\n      this.changeState(event, []);\n    }\n  };\n\n  /**\n   * <zh/> 图例元素移出事件\n   *\n   * <en/> Legend element mouseleave event\n   * @param event - <zh/> 移出的元素 | <en/> The element that is moved out\n   */\n  public mouseleave = (event: Selection) => {\n    if (this.options.trigger === 'click') return;\n    this.selectedItems = [];\n    this.changeState(event, []);\n  };\n\n  /**\n   * <zh/> 图例元素移入事件\n   *\n   * <en/> Legend element mouseenter event\n   * @param event - <zh/> 移入的元素 | <en/> The element that is moved in\n   */\n  public mouseenter = (event: Selection) => {\n    if (this.options.trigger === 'click') return;\n    const composeId = get(event, [this.typePrefix, 'id']);\n    if (!this.selectedItems.includes(composeId)) {\n      this.selectedItems.push(composeId);\n      this.changeState(event, 'active');\n    } else {\n      this.selectedItems = this.selectedItems.filter((item) => item !== composeId);\n    }\n  };\n\n  /**\n   * <zh/> 刷新图例元素状态\n   *\n   * <en/> Refresh the status of the legend element\n   */\n  public updateElement() {\n    if (!this.category) return;\n\n    this.category.update({\n      itemMarkerOpacity: ({ id }) => {\n        if (!this.selectedItems.length || this.selectedItems.includes(id)) return 1;\n        return 0.5;\n      },\n      itemLabelOpacity: ({ id }) => {\n        if (!this.selectedItems.length || this.selectedItems.includes(id)) return 1;\n        return 0.5;\n      },\n    });\n  }\n\n  private setFieldMap = (field: string, id: ID, type: ElementType) => {\n    if (!field) return;\n    const map = this.fieldMap[type];\n    if (!map) return;\n    if (!map.has(field)) {\n      map.set(field, [id]);\n    } else {\n      const ids = map.get(field);\n      if (ids) {\n        ids.push(id);\n        map.set(field, ids);\n      }\n    }\n  };\n\n  private getEvents = () => {\n    return {\n      mouseenter: this.mouseenter,\n      mouseleave: this.mouseleave,\n      click: this.click,\n    };\n  };\n\n  protected getMarkerData = (field: string | ((item: ElementDatum) => string), elementType: ElementType) => {\n    if (!field) return [];\n    const { model, element } = this.context;\n    const { nodes, edges, combos } = model.getData();\n    const items: { [key: string]: Datum } = {};\n\n    const getField = (item: ElementDatum) => {\n      if (isFunction(field)) return field(item);\n      return field;\n    };\n\n    const defaultType = {\n      node: 'circle',\n      edge: 'line',\n      combo: 'rect',\n    };\n\n    // 用于将 G6 element 转换为 components 支持的类型\n    // Used to convert G6 element to types supported by components\n    const markerMapping: { [key: string]: string } = {\n      circle: 'circle',\n      ellipse: 'circle', // 待 components 支持 ellipse\n      image: 'bowtie',\n      rect: 'square',\n      star: 'cross',\n      triangle: 'triangle',\n      diamond: 'diamond',\n      cubic: 'dot',\n      line: 'hyphen',\n      polyline: 'hyphen',\n      quadratic: 'hv',\n      'cubic-horizontal': 'hyphen',\n      'cubic-vertical': 'line',\n    };\n\n    const getElementStyle = (type: ElementType, datum: ElementDatum) => {\n      const style = element?.getElementComputedStyle(type, datum);\n      return style;\n    };\n\n    const getElementModel = (data: ElementDatum[], type: ElementType) => {\n      data.forEach((item) => {\n        const { id } = item;\n        const value = get(item, ['data', getField(item)]);\n        const marker = element?.getElementType(type, item) || 'circle';\n        const style = getElementStyle(type, item);\n        const color = (type === 'edge' ? style?.stroke : style?.fill) || '#1783ff';\n\n        if (id && value && value.replace(/\\s+/g, '')) {\n          this.setFieldMap(value, id, type);\n          if (!items[value]) {\n            items[value] = {\n              id: `${type}__${id}`,\n              label: value,\n              marker: markerMapping[marker] || defaultType[type],\n              elementType: type,\n              lineWidth: 1,\n              stroke: color,\n              fill: color,\n            };\n          }\n        }\n      });\n    };\n\n    switch (elementType) {\n      case 'node':\n        getElementModel(nodes, 'node');\n        break;\n      case 'edge':\n        getElementModel(edges, 'edge');\n        break;\n      case 'combo':\n        getElementModel(combos, 'combo');\n        break;\n      default:\n        return [];\n    }\n\n    return Object.values(items);\n  };\n\n  private upsertCanvas() {\n    if (this.canvas) return this.canvas;\n\n    const graphCanvas = this.context.canvas;\n    const [canvasWidth, canvasHeight] = graphCanvas.getSize();\n\n    const { width = canvasWidth, height = canvasHeight, position, container, containerStyle, className } = this.options;\n    const [$container, canvas] = createPluginCanvas({\n      width,\n      height,\n      graphCanvas,\n      container,\n      containerStyle,\n      placement: position,\n      className: 'legend',\n    });\n\n    this.container = $container;\n    if (className) $container.classList.add(className);\n    this.canvas = canvas;\n\n    return this.canvas;\n  }\n\n  private createElement = () => {\n    if (this.draw) {\n      this.updateElement();\n      return;\n    }\n    const {\n      width,\n      height,\n      nodeField,\n      edgeField,\n      comboField,\n      trigger,\n      position,\n      container,\n      containerStyle,\n      className,\n      ...rest\n    } = this.options;\n    const nodeItems = this.getMarkerData(nodeField, 'node');\n    const edgeItems = this.getMarkerData(edgeField, 'edge');\n    const comboItems = this.getMarkerData(comboField, 'combo');\n    const items = [...nodeItems, ...comboItems, ...edgeItems];\n\n    const categoryStyle = Object.assign(\n      {\n        width,\n        height,\n        data: items,\n        itemMarkerLineWidth: ({ lineWidth }: Datum) => lineWidth,\n        itemMarker: ({ marker }: Datum) => marker,\n        itemMarkerStroke: ({ stroke }: Datum) => stroke,\n        itemMarkerFill: ({ fill }: Datum) => fill,\n        gridCol: nodeItems.length,\n      },\n      rest,\n      this.getEvents(),\n    );\n\n    const category = new Category({\n      className: 'legend',\n      style: categoryStyle,\n    });\n    this.category = category;\n\n    const canvas = this.upsertCanvas();\n    canvas.appendChild(category);\n\n    this.draw = true;\n  };\n\n  /**\n   * <zh/>销毁图例\n   *\n   * <en/> Destroy the legend\n   * @internal\n   */\n  public destroy(): void {\n    this.clear();\n    this.context.graph.off(GraphEvent.AFTER_DRAW, this.createElement);\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/minimap/index.ts",
    "content": "import { Canvas, DisplayObject, IRenderer, Landmark } from '@antv/g';\nimport { debounce, throttle } from '@antv/util';\nimport { GraphEvent } from '../../constants';\nimport type { RuntimeContext } from '../../runtime/types';\nimport { GraphData } from '../../spec';\nimport type { ElementDatum, ElementType, IGraphLifeCycleEvent, Padding, Placement, Vector3 } from '../../types';\nimport { isVisible } from '../../utils/element';\nimport { idOf } from '../../utils/id';\nimport { parsePadding } from '../../utils/padding';\nimport { toPointObject } from '../../utils/point';\nimport type { BasePluginOptions } from '../base-plugin';\nimport { BasePlugin } from '../base-plugin';\nimport { createPluginCanvas } from '../utils/canvas';\n\n/**\n * <zh/> 缩略图插件配置项\n *\n * <en/> Minimap plugin options\n */\nexport interface MinimapOptions extends BasePluginOptions {\n  /**\n   * <zh/> 宽度和高度\n   *\n   * <en/> Width and height\n   * @defaultValue [240, 160]\n   */\n  size?: [number, number];\n  /**\n   * <zh/> 内边距\n   *\n   * <en/> Padding\n   * @defaultValue 10\n   */\n  padding?: Padding;\n  /**\n   * <zh/> 缩略图相对于画布的位置\n   *\n   * <en/> The position of the minimap relative to the canvas\n   * @defaultValue 'right-bottom'\n   */\n  position?: Placement;\n  /**\n   * <zh/> 过滤器，用于过滤不必显示的元素\n   *\n   * <en/> Filter, used to filter elements that do not need to be displayed\n   * @param id - <zh/> 元素的 id | <en/> The id of the element\n   * @param elementType - <zh/> 元素的类型 | <en/> The type of the element\n   * @returns <zh/> 是否显示 | <en/> Whether to display\n   */\n  filter?: (id: string, elementType: ElementType) => boolean;\n  /**\n   * <zh/> 元素缩略图形的生成方法\n   *\n   * <en/> The method of generating the thumbnail of the element\n   * @defaultValue 'key'\n   * @remarks\n   * <zh/>\n   * - 'key' 使用元素的主图形作为缩略图形\n   * - 'icon' 使用元素中心的 icon 作为缩略图形\n   * - 更多图形名称可查阅 https://g6.antv.antgroup.com/manual/element/node/base-node#style\n   * - 也可以传入一个函数，接收元素的 [id, 类型, 元素节点]，返回一个自定义样式的图形\n   *\n   * <en/>\n   * - 'key' uses the key shape of the element as the thumbnail shape\n   * - 'icon' uses the icon shape of the element as the thumbnail shape\n   * - more shape name see https://g6.antv.antgroup.com/manual/element/node/base-node#style\n   * - You can also pass in a function that receives the [id, type of the element, element] and returns a custom shape\n   */\n  shape?: string | 'key' | 'icon' | ((id: string, elementType: ElementType, element: DisplayObject) => DisplayObject);\n  /**\n   * <zh/> 缩略图画布类名，传入外置容器时不生效\n   *\n   * <en/> The class name of the minimap canvas, which does not take effect when an external container is passed in\n   */\n  className?: string;\n  /**\n   * <zh/> 缩略图挂载的容器，无则挂载到 Graph 所在容器\n   *\n   * <en/> The container where the minimap is mounted, if not, it will be mounted to the container where the Graph is located\n   */\n  container?: HTMLElement | string;\n  /**\n   * <zh/> 缩略图的容器样式，传入外置容器时不生效\n   *\n   * <en/> The style of the minimap container, which does not take effect when an external container is passed in\n   */\n  containerStyle?: Partial<CSSStyleDeclaration>;\n  /**\n   * <zh/> 遮罩的样式\n   *\n   * <en/> The style of the mask\n   */\n  maskStyle?: Partial<CSSStyleDeclaration>;\n  /**\n   * <zh/> 渲染器，默认使用 Canvas 渲染器\n   *\n   * <en/> Renderer, default to use Canvas renderer\n   */\n  renderer?: IRenderer;\n  /**\n   * <zh/> 延迟更新时间(毫秒)，用于性能优化\n   *\n   * <en/> Delay update time(ms), used for performance optimization\n   * @defaultValue 128\n   */\n  delay?: number;\n}\n\n/**\n * <zh/> 缩略图插件\n *\n * <en/> Minimap plugin\n */\nexport class Minimap extends BasePlugin<MinimapOptions> {\n  static defaultOptions: Partial<MinimapOptions> = {\n    size: [240, 160],\n    shape: 'key',\n    padding: 10,\n    position: 'right-bottom',\n    maskStyle: {\n      border: '1px solid #ddd',\n      background: 'rgba(0, 0, 0, 0.1)',\n    },\n    containerStyle: {\n      border: '1px solid #ddd',\n      background: '#fff',\n    },\n    delay: 128,\n  };\n\n  private canvas!: Canvas;\n\n  constructor(context: RuntimeContext, options: MinimapOptions) {\n    super(context, Object.assign({}, Minimap.defaultOptions, options));\n    this.setOnRender();\n    this.bindEvents();\n  }\n\n  public update(options: Partial<MinimapOptions>): void {\n    this.unbindEvents();\n    super.update(options);\n    if ('delay' in options) this.setOnRender();\n    this.bindEvents();\n  }\n\n  private setOnRender() {\n    this.onRender = debounce(() => {\n      this.renderMinimap();\n      this.renderMask();\n    }, this.options.delay);\n  }\n\n  private bindEvents() {\n    const { graph } = this.context;\n    graph.on(GraphEvent.AFTER_DRAW, this.onDraw);\n    graph.on(GraphEvent.AFTER_RENDER, this.onRender);\n    graph.on(GraphEvent.AFTER_ANIMATE, this.onRender);\n    graph.on(GraphEvent.AFTER_TRANSFORM, this.onTransform);\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n    graph.off(GraphEvent.AFTER_DRAW, this.onDraw);\n    graph.off(GraphEvent.AFTER_RENDER, this.onRender);\n    graph.off(GraphEvent.AFTER_ANIMATE, this.onRender);\n    graph.off(GraphEvent.AFTER_TRANSFORM, this.onTransform);\n  }\n\n  private onDraw = (event: IGraphLifeCycleEvent) => {\n    if (event?.data?.render) return;\n    this.onRender();\n  };\n\n  private onRender!: () => void;\n\n  /**\n   * <zh/> 创建或更新缩略图\n   *\n   * <en/> Create or update the minimap\n   */\n  private renderMinimap() {\n    const data = this.getElements();\n    const canvas = this.initCanvas();\n    this.setShapes(canvas, data);\n  }\n\n  private getElements(): Required<GraphData> {\n    const { filter } = this.options;\n    const { model, element } = this.context;\n    const originData = model.getData();\n    //过滤那些不存在于elementMap中的数据\n    const data = {\n      nodes: originData.nodes.filter((node) => element?.getElement(idOf(node))),\n      edges: originData.edges.filter((edge) => {\n        const edgeElement = element?.getElement(idOf(edge));\n        // 边数据存在且可见时才保留\n        return edgeElement && isVisible(edgeElement);\n      }),\n      combos: originData.combos.filter((combo) => element?.getElement(idOf(combo))),\n    };\n\n    if (!filter) return data;\n\n    const { nodes, edges, combos } = data;\n\n    return {\n      nodes: nodes.filter((node) => filter(idOf(node), 'node')),\n      edges: edges.filter((edge) => filter(idOf(edge), 'edge')),\n      combos: combos.filter((combo) => filter(idOf(combo), 'combo')),\n    };\n  }\n\n  private setShapes(canvas: Canvas, data: Required<GraphData>) {\n    const { nodes, edges, combos } = data;\n\n    const { shape } = this.options;\n    const { element } = this.context;\n\n    const iterate = (datum: ElementDatum, elType: ElementType) => {\n      const id = idOf(datum);\n      const target = element?.getElement(id);\n      if (!target) return;\n\n      const keyShape = target.getShape('key');\n      let cloneShape: DisplayObject;\n\n      if (typeof shape === 'string') {\n        const shapeName = shape;\n        const miniShape = target.getShape(shapeName);\n        cloneShape = miniShape.cloneNode();\n      } else {\n        const miniShape = shape(id, elType, target);\n        if (miniShape === target) {\n          cloneShape = miniShape.cloneNode(true);\n        } else {\n          cloneShape = miniShape;\n        }\n      }\n\n      /**\n       * 这里使用的是 keyShape 的位置\n       * 对于整个元素的位置而言，使用 keyShape 位置会比较准确\n       * 也比较合理\n       */\n      cloneShape.setPosition(keyShape.getPosition());\n      // keep zIndex / id\n      if (target.style.zIndex) cloneShape.style.zIndex = target.style.zIndex;\n      cloneShape.id = target.id;\n\n      canvas.appendChild(cloneShape);\n    };\n\n    canvas.removeChildren();\n\n    // 注意执行顺序 / Note the execution order\n    edges.forEach((datum) => iterate(datum, 'edge'));\n    combos.forEach((datum) => iterate(datum, 'combo'));\n    nodes.forEach((datum) => iterate(datum, 'node'));\n  }\n\n  private container!: HTMLElement;\n\n  private initCanvas() {\n    const {\n      renderer,\n      size: [width, height],\n    } = this.options;\n\n    if (this.canvas) {\n      const { width: w, height: h } = this.canvas.getConfig();\n      if (width !== w || height !== h) this.canvas.resize(width, height);\n      if (renderer) this.canvas.setRenderer(renderer);\n    } else {\n      const { className, position, container, containerStyle } = this.options;\n\n      const [$container, canvas] = createPluginCanvas({\n        renderer,\n        width,\n        height,\n        placement: position,\n        className: 'minimap',\n        container,\n        containerStyle,\n        graphCanvas: this.context.canvas,\n      });\n\n      if (className) $container.classList.add(className);\n\n      this.container = $container;\n      this.canvas = canvas;\n    }\n\n    this.setCamera();\n\n    return this.canvas;\n  }\n\n  private landmarkMap = new Map<string, Landmark>();\n\n  private createLandmark(position: Vector3, focalPoint: Vector3, zoom: number) {\n    const key = `${position.join(',')}-${focalPoint.join(',')}-${zoom}`;\n\n    if (this.landmarkMap.has(key)) return this.landmarkMap.get(key)!;\n\n    const camera = this.canvas.getCamera();\n    const landmark = camera.createLandmark(key, {\n      position,\n      focalPoint,\n      zoom,\n    });\n    this.landmarkMap.set(key, landmark);\n    return landmark;\n  }\n\n  private setCamera() {\n    const { canvas } = this.context;\n\n    const camera = this.canvas?.getCamera();\n    if (!camera) return;\n\n    const {\n      size: [minimapWidth, minimapHeight],\n      padding,\n    } = this.options;\n    const [top, right, bottom, left] = parsePadding(padding);\n    const { min: boundsMin, max: boundsMax, center } = canvas.getBounds('elements');\n    const boundsWidth = boundsMax[0] - boundsMin[0];\n    const boundsHeight = boundsMax[1] - boundsMin[1];\n\n    const availableWidth = minimapWidth - left - right;\n    const availableHeight = minimapHeight - top - bottom;\n\n    const scaleX = availableWidth / boundsWidth;\n    const scaleY = availableHeight / boundsHeight;\n    const scale = Math.min(scaleX, scaleY);\n\n    const landmark = this.createLandmark(center, center, scale);\n    camera.gotoLandmark(landmark, 0);\n  }\n\n  private mask: HTMLElement | null = null;\n\n  private get maskBBox(): [number, number, number, number] {\n    const { canvas: graphCanvas } = this.context;\n    const canvasSize = graphCanvas.getSize();\n    const canvasMin = graphCanvas.getCanvasByViewport([0, 0]);\n    const canvasMax = graphCanvas.getCanvasByViewport(canvasSize);\n\n    const maskMin = this.canvas.canvas2Viewport(toPointObject(canvasMin));\n    const maskMax = this.canvas.canvas2Viewport(toPointObject(canvasMax));\n\n    const width = maskMax.x - maskMin.x;\n    const height = maskMax.y - maskMin.y;\n\n    return [maskMin.x, maskMin.y, width, height];\n  }\n\n  /**\n   * <zh/> 计算遮罩包围盒\n   *\n   * <en/> Calculate the bounding box of the mask\n   * @returns <zh/> 遮罩包围盒 | <en/> Mask bounding box\n   */\n  private calculateMaskBBox(): [number, number, number, number] {\n    const {\n      size: [minimapWidth, minimapHeight],\n    } = this.options;\n\n    let [x, y, width, height] = this.maskBBox;\n\n    // clamp x, y, width, height\n    if (x < 0) {\n      width = upper(width + x, minimapWidth);\n      x = 0;\n    }\n    if (y < 0) {\n      height = upper(height + y, minimapHeight);\n      y = 0;\n    }\n    if (x + width > minimapWidth) width = lower(minimapWidth - x, 0);\n    if (y + height > minimapHeight) height = lower(minimapHeight - y, 0);\n\n    return [upper(x, minimapWidth), upper(y, minimapHeight), lower(width, 0), lower(height, 0)];\n  }\n\n  /**\n   * <zh/> 创建或更新遮罩\n   *\n   * <en/> Create or update the mask\n   */\n  private renderMask() {\n    const { maskStyle } = this.options;\n\n    if (!this.mask) {\n      this.mask = document.createElement('div');\n      this.mask.addEventListener('pointerdown', this.onMaskDragStart);\n      this.mask.draggable = true;\n      this.mask.addEventListener('dragstart', (event) => event.preventDefault && event.preventDefault());\n    }\n\n    this.container.appendChild(this.mask);\n\n    Object.assign(this.mask.style, {\n      ...maskStyle,\n      cursor: 'move',\n      position: 'absolute',\n      pointerEvents: 'auto',\n    });\n\n    this.updateMask();\n  }\n\n  private isMaskDragging = false;\n\n  private onMaskDragStart = (event: PointerEvent) => {\n    if (!this.mask) return;\n    this.isMaskDragging = true;\n    this.mask.setPointerCapture(event.pointerId);\n    this.mask.addEventListener('pointermove', this.onMaskDrag);\n    this.mask.addEventListener('pointerup', this.onMaskDragEnd);\n    this.mask.addEventListener('pointercancel', this.onMaskDragEnd);\n  };\n\n  private onMaskDrag = (event: PointerEvent) => {\n    if (!this.mask || !this.isMaskDragging) return;\n    const {\n      size: [minimapWidth, minimapHeight],\n    } = this.options;\n    const { movementX, movementY } = event;\n\n    const { left, top, width: w, height: h } = this.mask.style;\n    const [, , fullWidth, fullHeight] = this.maskBBox;\n\n    let x = parseInt(left) + movementX;\n    let y = parseInt(top) + movementY;\n    let width = parseInt(w);\n    let height = parseInt(h);\n\n    // 确保 mask 在 minimap 内部\n    // Ensure that the mask is inside the minimap\n    if (x < 0) x = 0;\n    if (y < 0) y = 0;\n    if (x + width > minimapWidth) x = lower(minimapWidth - width, 0);\n    if (y + height > minimapHeight) y = lower(minimapHeight - height, 0);\n\n    // 当拖拽画布导致 mask 缩小时，拖拽 mask 时，能够恢复到实际大小\n    // When dragging the canvas causes the mask to shrink, dragging the mask will restore it to its actual size\n    if (width < fullWidth) {\n      if (movementX > 0) {\n        x = lower(x - movementX, 0);\n        width = upper(width + movementX, minimapWidth);\n      } else if (movementX < 0) width = upper(width - movementX, minimapWidth);\n    }\n    if (height < fullHeight) {\n      if (movementY > 0) {\n        y = lower(y - movementY, 0);\n        height = upper(height + movementY, minimapHeight);\n      } else if (movementY < 0) height = upper(height - movementY, minimapHeight);\n    }\n\n    Object.assign(this.mask.style, {\n      left: x + 'px',\n      top: y + 'px',\n      width: width + 'px',\n      height: height + 'px',\n    });\n\n    // 基于 movement 进行相对移动\n    // Move relative to movement\n    const deltaX = parseInt(left) - x;\n    const deltaY = parseInt(top) - y;\n    if (deltaX === 0 && deltaY === 0) return;\n\n    const zoom1 = this.context.canvas.getCamera().getZoom();\n    const zoom2 = this.canvas.getCamera().getZoom();\n    const ratio = zoom1 / zoom2;\n\n    this.context.graph.translateBy([deltaX * ratio, deltaY * ratio], false);\n  };\n\n  private onMaskDragEnd = (event: PointerEvent) => {\n    if (!this.mask) return;\n    this.isMaskDragging = false;\n    this.mask.releasePointerCapture(event.pointerId);\n    this.mask.removeEventListener('pointermove', this.onMaskDrag);\n    this.mask.removeEventListener('pointerup', this.onMaskDragEnd);\n    this.mask.removeEventListener('pointercancel', this.onMaskDragEnd);\n  };\n\n  private onTransform = throttle(\n    () => {\n      if (this.isMaskDragging) return;\n      this.updateMask();\n      this.setCamera();\n    },\n    32,\n    { leading: true },\n  ) as () => void;\n\n  private updateMask() {\n    if (!this.mask) return;\n    const [x, y, width, height] = this.calculateMaskBBox();\n\n    Object.assign(this.mask.style, {\n      top: y + 'px',\n      left: x + 'px',\n      width: width + 'px',\n      height: height + 'px',\n    });\n  }\n\n  public destroy(): void {\n    this.unbindEvents();\n    this.canvas?.destroy();\n    this.mask?.remove();\n    this.container?.remove();\n    super.destroy();\n  }\n}\n\nconst upper = (value: number, max: number) => Math.min(value, max);\n\nconst lower = (value: number, min: number) => Math.max(value, min);\n"
  },
  {
    "path": "packages/g6/src/plugins/snapline/index.ts",
    "content": "import { AABB, BaseStyleProps, DisplayObject, Line, LineStyleProps } from '@antv/g';\nimport { isEqual } from '@antv/util';\nimport { NodeEvent } from '../../constants';\nimport type { RuntimeContext } from '../../runtime/types';\nimport type { ID, IDragEvent, Node } from '../../types';\nimport { isVisible } from '../../utils/element';\nimport { divide } from '../../utils/vector';\nimport type { BasePluginOptions } from '../base-plugin';\nimport { BasePlugin } from '../base-plugin';\n\n/**\n * <zh/> 对齐线插件配置项\n *\n * <en/> Snapline plugin options\n */\nexport interface SnaplineOptions extends BasePluginOptions {\n  /**\n   * <zh/> 对齐精度，即移动节点时与目标位置的距离小于 tolerance 时触发显示对齐线\n   *\n   * <en/> The alignment accuracy, that is, when the distance between the moved node and the target position is less than tolerance, the alignment line is displayed\n   * @defaultValue 5\n   */\n  tolerance?: number;\n  /**\n   * <zh/> 对齐线头尾的延伸距离。取值范围：[0, Infinity]\n   *\n   * <en/> The extension distance of the snapline. The value range is [0, Infinity]\n   * @defaultValue 20\n   */\n  offset?: number;\n  /**\n   * <zh/> 是否启用自动吸附\n   *\n   * <en/> Whether to enable automatic adsorption\n   * @defaultValue true\n   */\n  autoSnap?: boolean;\n  /**\n   * <zh/> 指定元素上的哪个图形作为参照图形\n   *\n   * <en/> Specifies which shape on the element to use as the reference shape\n   * @defaultValue `'key'`\n   * @remarks\n   * <zh/>\n   * - 'key' 使用元素的主图形作为参照图形\n   * - 也可以传入一个函数，接收元素对象，返回一个图形\n   *\n   * <en/>\n   * - `'key'` uses the key shape of the element as the reference shape\n   * - You can also pass in a function that receives the element and returns a shape\n   */\n  shape?: string | ((node: Node) => DisplayObject);\n  /**\n   * <zh/> 垂直对齐线样式\n   *\n   * <en/> Vertical snapline style\n   * @defaultValue `{ stroke: '#1783FF' }`\n   */\n  verticalLineStyle?: BaseStyleProps;\n  /**\n   * <zh/> 水平对齐线样式\n   *\n   * <en/> Horizontal snapline style\n   * @defaultValue `{ stroke: '#1783FF' }`\n   */\n  horizontalLineStyle?: BaseStyleProps;\n  /**\n   * <zh/> 过滤器，用于过滤不需要作为参考的节点\n   *\n   * <en/> Filter, used to filter nodes that do not need to be used as references\n   * @defaultValue `() => true`\n   */\n  filter?: (node: Node) => boolean;\n}\n\nconst defaultLineStyle: LineStyleProps = { x1: 0, y1: 0, x2: 0, y2: 0, visibility: 'hidden' };\n\ntype Metadata = {\n  verticalX: number | null;\n  verticalMinY: number | null;\n  verticalMaxY: number | null;\n  horizontalY: number | null;\n  horizontalMinX: number | null;\n  horizontalMaxX: number | null;\n};\n\n/**\n * <zh/> 对齐线插件\n *\n * <en/> Snapline plugin\n */\nexport class Snapline extends BasePlugin<SnaplineOptions> {\n  static defaultOptions: Partial<SnaplineOptions> = {\n    tolerance: 5,\n    offset: 20,\n    autoSnap: true,\n    shape: 'key',\n    verticalLineStyle: { stroke: '#1783FF' },\n    horizontalLineStyle: { stroke: '#1783FF' },\n    filter: () => true,\n  };\n\n  private horizontalLine!: Line;\n  private verticalLine!: Line;\n\n  constructor(context: RuntimeContext, options: SnaplineOptions) {\n    super(context, Object.assign({}, Snapline.defaultOptions, options));\n    this.bindEvents();\n  }\n\n  private initSnapline = () => {\n    const canvas = this.context.canvas.getLayer('transient');\n\n    if (!this.horizontalLine) {\n      this.horizontalLine = canvas.appendChild(\n        new Line({ style: { ...defaultLineStyle, ...this.options.horizontalLineStyle } }),\n      );\n    }\n\n    if (!this.verticalLine) {\n      this.verticalLine = canvas.appendChild(\n        new Line({ style: { ...defaultLineStyle, ...this.options.verticalLineStyle } }),\n      );\n    }\n  };\n\n  private getNodes(): Node[] {\n    const { filter } = this.options;\n    const allNodes = this.context.element?.getNodes() || [];\n\n    // 不考虑超出画布视口范围、不可见的节点\n    // Nodes that are out of the canvas viewport range, invisible are not considered\n    const nodes = allNodes.filter((node) => {\n      return isVisible(node) && this.context.viewport?.isInViewport(node.getRenderBounds());\n    });\n\n    if (!filter) return nodes;\n\n    return nodes.filter((node) => filter(node));\n  }\n\n  private hideSnapline() {\n    this.horizontalLine.style.visibility = 'hidden';\n    this.verticalLine.style.visibility = 'hidden';\n  }\n\n  private getLineWidth(direction: 'horizontal' | 'vertical') {\n    const { lineWidth } = this.options[`${direction}LineStyle`] as LineStyleProps;\n    return +(lineWidth || defaultLineStyle.lineWidth || 1) / this.context.graph.getZoom();\n  }\n\n  private updateSnapline(metadata: Metadata) {\n    const { verticalX, verticalMinY, verticalMaxY, horizontalY, horizontalMinX, horizontalMaxX } = metadata;\n    const [canvasWidth, canvasHeight] = this.context.canvas.getSize();\n    const { offset } = this.options;\n\n    if (horizontalY !== null) {\n      Object.assign(this.horizontalLine.style, {\n        x1: offset === Infinity ? 0 : horizontalMinX! - offset,\n        y1: horizontalY,\n        x2: offset === Infinity ? canvasWidth : horizontalMaxX! + offset,\n        y2: horizontalY,\n        visibility: 'visible',\n        lineWidth: this.getLineWidth('horizontal'),\n      });\n    } else {\n      this.horizontalLine.style.visibility = 'hidden';\n    }\n\n    if (verticalX !== null) {\n      Object.assign(this.verticalLine.style, {\n        x1: verticalX,\n        y1: offset === Infinity ? 0 : verticalMinY! - offset,\n        x2: verticalX,\n        y2: offset === Infinity ? canvasHeight : verticalMaxY! + offset,\n        visibility: 'visible',\n        lineWidth: this.getLineWidth('vertical'),\n      });\n    } else {\n      this.verticalLine.style.visibility = 'hidden';\n    }\n  }\n\n  private isHorizontalSticking = false;\n  private isVerticalSticking = false;\n  private enableStick = true;\n\n  private autoSnapToLine = async (nodeId: ID, bbox: AABB, metadata: Metadata) => {\n    const { verticalX, horizontalY } = metadata;\n    const { tolerance } = this.options;\n    const {\n      min: [nodeMinX, nodeMinY],\n      max: [nodeMaxX, nodeMaxY],\n      center: [nodeCenterX, nodeCenterY],\n    } = bbox;\n\n    let dx = 0;\n    let dy = 0;\n    if (verticalX !== null) {\n      if (distance(nodeMaxX, verticalX) < tolerance) dx = verticalX - nodeMaxX;\n      if (distance(nodeMinX, verticalX) < tolerance) dx = verticalX - nodeMinX;\n      if (distance(nodeCenterX, verticalX) < tolerance) dx = verticalX - nodeCenterX;\n\n      if (dx !== 0) this.isVerticalSticking = true;\n    }\n    if (horizontalY !== null) {\n      if (distance(nodeMaxY, horizontalY) < tolerance) dy = horizontalY - nodeMaxY;\n      if (distance(nodeMinY, horizontalY) < tolerance) dy = horizontalY - nodeMinY;\n      if (distance(nodeCenterY, horizontalY) < tolerance) dy = horizontalY - nodeCenterY;\n\n      if (dy !== 0) this.isHorizontalSticking = true;\n    }\n    if (dx !== 0 || dy !== 0) {\n      // Stick to the line\n      await this.context.graph.translateElementBy({ [nodeId]: [dx, dy] }, false);\n    }\n  };\n\n  /**\n   * Get the delta of the drag\n   * @param event - drag event object\n   * @returns delta\n   * @internal\n   */\n  protected getDelta(event: IDragEvent<Node>) {\n    const zoom = this.context.graph.getZoom();\n    return divide([event.dx, event.dy], zoom);\n  }\n\n  private enableSnap = (event: IDragEvent<Node>) => {\n    const { target } = event;\n\n    const threshold = 0.5;\n\n    if (this.isHorizontalSticking || this.isVerticalSticking) {\n      const [dx, dy] = this.getDelta(event);\n      if (\n        this.isHorizontalSticking &&\n        this.isVerticalSticking &&\n        Math.abs(dx) <= threshold &&\n        Math.abs(dy) <= threshold\n      ) {\n        this.context.graph.translateElementBy({ [target.id]: [-dx, -dy] }, false);\n        return false;\n      } else if (this.isHorizontalSticking && Math.abs(dy) <= threshold) {\n        this.context.graph.translateElementBy({ [target.id]: [0, -dy] }, false);\n        return false;\n      } else if (this.isVerticalSticking && Math.abs(dx) <= threshold) {\n        this.context.graph.translateElementBy({ [target.id]: [-dx, 0] }, false);\n        return false;\n      } else {\n        this.isHorizontalSticking = false;\n        this.isVerticalSticking = false;\n        this.enableStick = false;\n        setTimeout(() => {\n          this.enableStick = true;\n        }, 200);\n      }\n    }\n\n    return this.enableStick;\n  };\n\n  private calcSnaplineMetadata = (target: Node, nodeBBox: AABB): Metadata => {\n    const { tolerance, shape } = this.options;\n\n    const {\n      min: [nodeMinX, nodeMinY],\n      max: [nodeMaxX, nodeMaxY],\n      center: [nodeCenterX, nodeCenterY],\n    } = nodeBBox;\n\n    let verticalX: number | null = null;\n    let verticalMinY: number | null = null;\n    let verticalMaxY: number | null = null;\n    let horizontalY: number | null = null;\n    let horizontalMinX: number | null = null;\n    let horizontalMaxX: number | null = null;\n\n    this.getNodes().some((snapNode: Node) => {\n      if (isEqual(target.id, snapNode.id)) return false;\n\n      const snapBBox = getShape(snapNode, shape).getRenderBounds();\n      const {\n        min: [snapMinX, snapMinY],\n        max: [snapMaxX, snapMaxY],\n        center: [snapCenterX, snapCenterY],\n      } = snapBBox;\n\n      if (verticalX === null) {\n        if (distance(snapCenterX, nodeCenterX) < tolerance) {\n          verticalX = snapCenterX;\n        } else if (distance(snapMinX, nodeMinX) < tolerance) {\n          verticalX = snapMinX;\n        } else if (distance(snapMinX, nodeMaxX) < tolerance) {\n          verticalX = snapMinX;\n        } else if (distance(snapMaxX, nodeMaxX) < tolerance) {\n          verticalX = snapMaxX;\n        } else if (distance(snapMaxX, nodeMinX) < tolerance) {\n          verticalX = snapMaxX;\n        }\n\n        if (verticalX !== null) {\n          verticalMinY = Math.min(snapMinY, nodeMinY);\n          verticalMaxY = Math.max(snapMaxY, nodeMaxY);\n        }\n      }\n\n      if (horizontalY === null) {\n        if (distance(snapCenterY, nodeCenterY) < tolerance) {\n          horizontalY = snapCenterY;\n        } else if (distance(snapMinY, nodeMinY) < tolerance) {\n          horizontalY = snapMinY;\n        } else if (distance(snapMinY, nodeMaxY) < tolerance) {\n          horizontalY = snapMinY;\n        } else if (distance(snapMaxY, nodeMaxY) < tolerance) {\n          horizontalY = snapMaxY;\n        } else if (distance(snapMaxY, nodeMinY) < tolerance) {\n          horizontalY = snapMaxY;\n        }\n\n        if (horizontalY !== null) {\n          horizontalMinX = Math.min(snapMinX, nodeMinX);\n          horizontalMaxX = Math.max(snapMaxX, nodeMaxX);\n        }\n      }\n\n      return verticalX !== null && horizontalY !== null;\n    });\n    return { verticalX, verticalMinY, verticalMaxY, horizontalY, horizontalMinX, horizontalMaxX };\n  };\n\n  protected onDragStart = () => {\n    this.initSnapline();\n  };\n\n  protected onDrag = async (event: IDragEvent<Node>) => {\n    const { target } = event;\n\n    if (this.options.autoSnap) {\n      const enable = this.enableSnap(event);\n      if (!enable) return;\n    }\n\n    const nodeBBox = getShape(target, this.options.shape).getRenderBounds();\n    const metadata = this.calcSnaplineMetadata(target, nodeBBox);\n\n    this.hideSnapline();\n\n    if (metadata.verticalX !== null || metadata.horizontalY !== null) {\n      this.updateSnapline(metadata);\n    }\n\n    if (this.options.autoSnap) {\n      await this.autoSnapToLine(target.id, nodeBBox, metadata);\n    }\n  };\n\n  protected onDragEnd = () => {\n    this.hideSnapline();\n  };\n\n  private async bindEvents() {\n    const { graph } = this.context;\n    graph.on(NodeEvent.DRAG_START, this.onDragStart);\n    graph.on(NodeEvent.DRAG, this.onDrag);\n    graph.on(NodeEvent.DRAG_END, this.onDragEnd);\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n    graph.off(NodeEvent.DRAG_START, this.onDragStart);\n    graph.off(NodeEvent.DRAG, this.onDrag);\n    graph.off(NodeEvent.DRAG_END, this.onDragEnd);\n  }\n\n  private destroyElements() {\n    this.horizontalLine?.destroy();\n    this.verticalLine?.destroy();\n  }\n\n  public destroy() {\n    this.destroyElements();\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n\nconst distance = (a: number, b: number) => Math.abs(a - b);\n\nconst getShape = (node: Node, shapeFilter: string | ((node: Node) => DisplayObject)) => {\n  return typeof shapeFilter === 'function' ? shapeFilter(node) : node.getShape(shapeFilter);\n};\n"
  },
  {
    "path": "packages/g6/src/plugins/timebar.ts",
    "content": "import { Timebar as TimebarComponent } from '@antv/component';\nimport { Canvas } from '@antv/g';\nimport { isArray, isDate, isNumber } from '@antv/util';\nimport { idOf } from '../utils/id';\nimport { parsePadding } from '../utils/padding';\nimport { BasePlugin } from './base-plugin';\n\nimport type { TimebarStyleProps as TimebarComponentStyleProps } from '@antv/component';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { GraphData } from '../spec';\nimport type { ElementDatum, ElementType, ID, Padding } from '../types';\nimport type { BasePluginOptions } from './base-plugin';\nimport { createPluginCanvas } from './utils/canvas';\n\nconst prospectiveTimeKeys = ['timestamp', 'time', 'date', 'datetime'];\n\n/**\n * <zh/> Timebar 时间条的配置项。\n * <en/> The options of the Timebar.\n */\nexport interface TimebarOptions extends BasePluginOptions {\n  /**\n   * <zh/> 给工具栏的 DOM 追加的类名，便于自定义样式\n   *\n   * <en/> The class name appended to the menu DOM for custom styles\n   * @defaultValue 'g6-timebar'\n   */\n  className?: string;\n  /**\n   * <zh/> X 位置\n   *\n   * <en/> X position\n   * @remarks\n   * <zh/> 设置后 `position` 会失效\n   *\n   * <en/> `position` will be invalidated after setting `x`\n   */\n  x?: number;\n  /**\n   * <zh/> Y 位置\n   *\n   * <en/> Y position\n   * @remarks\n   * <zh/> 设置后 `position` 会失效\n   *\n   * <en/> `position` will be invalidated after setting `y`\n   */\n  y?: number;\n  /**\n   * <zh/> 时间条宽度\n   *\n   * <en/> Timebar width\n   * @defaultValue 450\n   */\n  width?: number;\n  /**\n   * <zh/> 时间条高度\n   *\n   * <en/> Timebar height\n   * @defaultValue 60\n   */\n  height?: number;\n  /**\n   * <zh/> Timebar 的位置\n   *\n   * <en/> Timebar location\n   * @defaultValue 'bottom'\n   */\n  position?: 'bottom' | 'top';\n  /**\n   * <zh/> 边距\n   *\n   * <en/> Padding\n   */\n  padding?: Padding;\n  /**\n   * <zh/> 获取元素时间\n   *\n   * <en/> Get element time\n   */\n  getTime?: (datum: ElementDatum) => number;\n  /**\n   * <zh/> 时间数据\n   *\n   * <en/> Time data\n   * @remarks\n   * <zh/> `timebarType` 为 `'chart'` 时，需要额外传入 `value` 字段作为图表数据\n   *\n   * <en/> When `timebarType` is `'chart'`, you need to pass in the `value` field as chart data\n   */\n  data: number[] | { time: number; value: number }[];\n  /**\n   * <zh/> Timebar 展示类型\n   * - `'time'`: 显示为时间轴\n   * - `'chart'`: 显示为趋势图\n   *\n   * <en/> Timebar Displays the type\n   * - `'time'`: Display as a timeline\n   * - `'chart'`: Display as a trend chart\n   * @defaultValue 'time'\n   */\n  timebarType?: 'time' | 'chart';\n  /**\n   * <zh/> 筛选类型\n   *\n   * <en/> Filter element types\n   */\n  elementTypes?: ElementType[];\n  /**\n   * <zh/> 筛选模式\n   *  - `'modify'`: 通过修改图数据进行筛选\n   *  - `'visibility'`: 通过修改元素可见性进行筛选\n   *\n   * <en/> Filter mode\n   *  - `'modify'`: Filter by modifying the graph data.\n   *  - `'visibility'`: Filter by modifying element visibility\n   * @defaultValue 'modify'\n   */\n  mode?: 'modify' | 'visibility';\n  /**\n   * <zh/> 当前时间值\n   *\n   * <en/> Current time value\n   */\n  values?: number | [number, number] | Date | [Date, Date];\n  /**\n   * <zh/> 图表模式下自定义时间格式化\n   *\n   * <en/> Custom time formatting in chart mode\n   */\n  labelFormatter?: (time: number | Date) => string;\n  /**\n   * <zh/> 是否循环播放\n   *\n   * <en/> Whether to loop\n   * @defaultValue false\n   */\n  loop?: boolean;\n  /**\n   * <zh/> 时间区间变化时执行的回调\n   *\n   * <en/> Callback executed when the time interval changes\n   */\n  onChange?: (values: number | [number, number]) => void;\n  /**\n   * <zh/> 重置时执行的回调\n   *\n   * <en/> Callback executed when reset\n   */\n  onReset?: () => void;\n  /**\n   * <zh/> 播放速度变化时执行的回调\n   *\n   * <en/> Callback executed when the playback speed changes\n   */\n  onSpeedChange?: (speed: number) => void;\n  /**\n   * <zh/> 开始播放时执行的回调\n   *\n   * <en/> Callback executed when playback starts\n   */\n  onPlay?: () => void;\n  /**\n   * <zh/> 暂停时执行的回调\n   *\n   * <en/> Callback executed when paused\n   */\n  onPause?: () => void;\n  /**\n   * <zh/> 后退时执行的回调\n   *\n   * <en/> Callback executed when backward\n   */\n  onBackward?: () => void;\n  /**\n   * <zh/> 前进时执行的回调\n   *\n   * <en/> Callback executed when forward\n   */\n  onForward?: () => void;\n}\n\n/**\n * <zh/> 时间组件\n *\n * <en/> Timebar\n */\nexport class Timebar extends BasePlugin<TimebarOptions> {\n  static defaultOptions: Partial<TimebarOptions> = {\n    position: 'bottom',\n    enable: true,\n    timebarType: 'time',\n    className: 'g6-timebar',\n    width: 450,\n    height: 60,\n    zIndex: 3,\n    elementTypes: ['node'],\n    padding: 10,\n    mode: 'modify',\n    getTime: (datum) => inferTime(datum, prospectiveTimeKeys, undefined),\n    loop: false,\n  };\n\n  private timebar?: TimebarComponent;\n\n  private canvas?: Canvas;\n\n  private container?: HTMLElement;\n\n  private originalData?: GraphData;\n\n  private get padding() {\n    return parsePadding(this.options.padding);\n  }\n\n  constructor(context: RuntimeContext, options: TimebarOptions) {\n    super(context, Object.assign({}, Timebar.defaultOptions, options));\n    this.backup();\n    this.upsertTimebar();\n  }\n\n  /**\n   * <zh/> 播放\n   *\n   * <en/> Play\n   */\n  public play() {\n    this.timebar?.play();\n  }\n\n  /**\n   * <zh/> 暂停\n   *\n   * <en/> Pause\n   */\n  public pause() {\n    this.timebar?.pause();\n  }\n\n  /**\n   * <zh/> 前进\n   *\n   * <en/> Forward\n   */\n  public forward() {\n    this.timebar?.forward();\n  }\n\n  /**\n   * <zh/> 后退\n   *\n   * <en/> Backward\n   */\n  public backward() {\n    this.timebar?.backward();\n  }\n\n  /**\n   * <zh/> 重置\n   *\n   * <en/> Reset\n   */\n  public reset() {\n    this.timebar?.reset();\n  }\n\n  /**\n   * <zh/> 更新时间条配置项\n   *\n   * <en/> Update timebar configuration options\n   * @param options - <zh/> 配置项 | <en/> Options\n   * @internal\n   */\n  public update(options: Partial<TimebarOptions>) {\n    super.update(options);\n    this.backup();\n\n    this.upsertTimebar();\n  }\n\n  /**\n   * <zh/> 备份数据\n   *\n   * <en/> Backup data\n   */\n  private backup() {\n    this.originalData = shallowCopy(this.context.graph.getData());\n  }\n\n  private upsertTimebar() {\n    const { canvas } = this.context;\n    const { onChange, timebarType, data, x, y, width, height, mode, ...restOptions } = this.options;\n    const canvasSize = canvas.getSize();\n    const [top] = this.padding;\n\n    this.upsertCanvas().ready.then(() => {\n      const style: TimebarComponentStyleProps = {\n        x: canvasSize[0] / 2 - width / 2,\n        y: top,\n        onChange: (value) => {\n          const range = (isArray(value) ? value : [value, value]).map((time) =>\n            isDate(time) ? time.getTime() : time,\n          ) as [number, number];\n\n          if (this.options.mode === 'modify') this.filterElements(range);\n          else this.hiddenElements(range);\n\n          onChange?.(range);\n        },\n        ...restOptions,\n        data: data.map((datum) => (isNumber(datum) ? { time: datum, value: 0 } : datum)),\n        width,\n        height,\n        type: timebarType,\n      };\n\n      if (!this.timebar) {\n        this.timebar = new TimebarComponent({ style });\n        this.canvas?.appendChild(this.timebar);\n      } else {\n        this.timebar.update(style);\n      }\n    });\n  }\n\n  private upsertCanvas() {\n    if (this.canvas) return this.canvas;\n\n    const { className, height, position } = this.options;\n    const graphCanvas = this.context.canvas;\n    const [width] = graphCanvas.getSize();\n    const [top, , bottom] = this.padding;\n\n    const [$container, canvas] = createPluginCanvas({\n      width,\n      height: height + top + bottom,\n      graphCanvas,\n      className: 'timebar',\n      placement: position,\n    });\n\n    this.container = $container;\n    if (className) $container.classList.add(className);\n    this.canvas = canvas;\n\n    return this.canvas;\n  }\n\n  private async filterElements(range: number | [number, number]) {\n    if (!this.originalData) return;\n    const { elementTypes, getTime } = this.options;\n    const { graph, element } = this.context;\n\n    const newData = shallowCopy(this.originalData);\n\n    elementTypes.forEach((type) => {\n      const key = `${type}s` as const;\n      newData[key] = (this.originalData![key] || []).filter((datum) => {\n        const time = getTime(datum);\n        if (match(time, range)) return true;\n        return false;\n      }) as any;\n    });\n\n    const nodeLikeIds = [...newData.nodes, ...newData.combos].map((datum) => idOf(datum));\n    newData.edges = newData.edges!.filter((edge) => {\n      const source = edge.source;\n      const target = edge.target;\n      return nodeLikeIds.includes(source) && nodeLikeIds.includes(target);\n    });\n\n    graph.setData(newData);\n    await element!.draw({ animation: false, silence: true })?.finished;\n  }\n\n  private hiddenElements(range: number | [number, number]) {\n    const { graph } = this.context;\n    const { elementTypes, getTime } = this.options;\n    const hideElementId: ID[] = [];\n    const showElementId: ID[] = [];\n\n    elementTypes.forEach((elementType) => {\n      const key = `${elementType}s` as const;\n      const elementData = this.originalData?.[key] || [];\n\n      elementData.forEach((elementDatum) => {\n        const id = idOf(elementDatum);\n        const time = getTime(elementDatum);\n        if (match(time, range)) showElementId.push(id);\n        else hideElementId.push(id);\n      });\n    });\n\n    graph.hideElement(hideElementId, false);\n    graph.showElement(showElementId, false);\n  }\n\n  /**\n   * <zh/> 销毁时间条\n   *\n   * <en/> Destroy the timebar\n   * @internal\n   */\n  public destroy(): void {\n    const { graph } = this.context;\n    this.originalData && graph.setData({ ...this.originalData });\n    this.timebar?.destroy();\n    this.canvas?.destroy();\n    this.container?.remove();\n    this.originalData = undefined;\n    this.container = undefined;\n    this.timebar = undefined;\n    this.canvas = undefined;\n\n    super.destroy();\n  }\n}\n\nconst shallowCopy = (data: GraphData) => {\n  const { nodes = [], edges = [], combos = [] } = data;\n  return {\n    nodes: [...nodes],\n    edges: [...edges],\n    combos: [...combos],\n  };\n};\n\nconst match = (time: number, range: number | [number, number]) => {\n  if (isNumber(range)) return time === range;\n  const [start, end] = range;\n  return time >= start && time <= end;\n};\n\nconst inferTime = (datum: ElementDatum, optionsKeys: string[], defaultValue?: any): number => {\n  for (let i = 0; i < optionsKeys.length; i++) {\n    const key = optionsKeys[i];\n    const val = datum.data?.[key];\n    if (val) return val as number;\n  }\n  return defaultValue;\n};\n"
  },
  {
    "path": "packages/g6/src/plugins/title/index.ts",
    "content": "import { Canvas } from '@antv/g';\nimport { GraphEvent } from '../../constants';\nimport type { LabelStyleProps } from '../../elements/shapes/label';\nimport { Label } from '../../elements/shapes/label';\nimport type { RuntimeContext } from '../../runtime/types';\nimport type { Prefix } from '../../types';\nimport { parsePadding } from '../../utils/padding';\nimport { subStyleProps } from '../../utils/prefix';\nimport type { BasePluginOptions } from '../base-plugin';\nimport { BasePlugin } from '../base-plugin';\nimport { createPluginCanvas } from '../utils/canvas';\n\nconst commonStyle: Partial<LabelStyleProps> = {\n  fill: '#1D2129',\n  wordWrap: true, // 自动换行\n  maxLines: 1, // 最大行数\n  textOverflow: 'ellipsis', // 溢出隐藏省略号\n  textBaseline: 'top',\n\n  /**\n   * textAlign 需要和 x 结合使用\n   * 举例: 前提条件: 画布 width = 600\n   * - textAlign: 'start' | 'left\n   *    需要设 x = 0\n   * - textAlign: 'end' | 'right'\n   *    需要设 x = 600 (即画布的宽度)\n   * - textAlign: 'center'\n   *    需要设 x = 300 (即画布的宽度 / 2)\n   */\n  textAlign: 'start',\n  x: 0,\n};\nconst defaultTitleStyle: Partial<LabelStyleProps> = {\n  ...commonStyle,\n  fillOpacity: 0.9,\n  fontSize: 16,\n  fontWeight: 'bold',\n};\nconst defaultSubTitleStyle: Partial<LabelStyleProps> = {\n  ...commonStyle,\n  fillOpacity: 0.65,\n  fontSize: 12,\n  fontWeight: 'normal',\n};\nconst defaultOptions: Partial<TitleOptions> = {\n  align: 'left',\n  spacing: 8,\n  size: 44,\n  padding: [16, 24, 0, 24],\n};\n\nconst titleKey = 'title';\nconst subtitleKey = 'subtitle';\n\n/**\n * <zh/> 标题的样式\n *\n * <en/> Title styles\n */\nexport type TitleStyle = Prefix<typeof titleKey, Omit<LabelStyleProps, 'x' | 'y' | 'text'>>;\n/**\n * <zh/> 副标题的样式\n *\n * <en/> Subtitle styles\n */\nexport type SubTitleStyle = Prefix<typeof subtitleKey, Omit<LabelStyleProps, 'x' | 'y' | 'text'>>;\n\n/**\n * <zh/> 标题插件配置项\n *\n * <en/> Title plugin options\n */\nexport interface TitleOptions extends BasePluginOptions, TitleStyle, SubTitleStyle {\n  /**\n   * <zh/> 整个标题的高度\n   *\n   * <en/> whole title height\n   * @defaultValue 44\n   */\n  size?: number;\n  /**\n   * <zh/> 整个标题位于图的位置\n   *\n   * <en/> The entire title is located at the position of the graph\n   * @defaultValue 'left'\n   */\n  align?: 'left' | 'center' | 'right';\n  /**\n   * <zh/> 主标题、副标题之间的上下间距\n   *\n   * <en/> The y spacing between the title and subtitle\n   * @defaultValue 8\n   */\n  spacing?: number;\n  /**\n   * <zh/> 标题内边距\n   *\n   * <en/> whole title padding\n   * @defaultValue [16, 24, 0, 24]\n   */\n  padding?: number | number[];\n  /**\n   * <zh/> 标题内容\n   *\n   * <en/> title text\n   */\n  [titleKey]: string;\n  /**\n   * <zh/> 副标题内容\n   *\n   * <en/> subtitle text\n   */\n  [subtitleKey]?: string | null;\n  /**\n   * <zh/> 标题画布类名，传入外置容器时不生效\n   *\n   * <en/> The class name of the title canvas, which does not take effect when an external container is passed in\n   */\n  className?: string;\n}\n\nexport class Title extends BasePlugin<TitleOptions> {\n  private canvas!: Canvas;\n  private container!: HTMLElement;\n\n  private get padding() {\n    return parsePadding(this.options.padding);\n  }\n\n  constructor(context: RuntimeContext, options: TitleOptions) {\n    const combineOption = Object.assign({}, defaultOptions, options);\n    super(context, combineOption);\n\n    this.bindEvents();\n  }\n\n  private onRender = () => {\n    const canvas = this.updateCanvas();\n    this.renderTitle(canvas);\n  };\n\n  private bindEvents() {\n    const { graph } = this.context;\n    graph.on(GraphEvent.AFTER_RENDER, this.onRender);\n    graph.on(GraphEvent.AFTER_ANIMATE, this.onRender);\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n    graph.off(GraphEvent.AFTER_RENDER, this.onRender);\n    graph.off(GraphEvent.AFTER_ANIMATE, this.onRender);\n  }\n\n  public destroy(): void {\n    this.unbindEvents();\n    this.canvas?.destroy();\n    this.container?.remove();\n    super.destroy();\n  }\n\n  private updateCanvas() {\n    const { size, className, align } = this.options;\n    const [width] = this.context.canvas.getSize();\n    const [pt = 0, , pb = 0] = this.padding;\n    const height = size + pt + pb;\n\n    if (this.canvas) {\n      const { width: w, height: h } = this.canvas.getConfig();\n      if (width !== w || height !== h) this.canvas.resize(width, height);\n    } else {\n      const positions = {\n        left: 'left-top',\n        center: 'top',\n        right: 'right-top',\n      } as const;\n\n      const [$container, canvas] = createPluginCanvas({\n        width,\n        height,\n        placement: positions[align] || positions.left,\n        className: 'title-canvas',\n        graphCanvas: this.context.canvas,\n      });\n\n      if (className) $container.classList.add(className);\n\n      this.container = $container;\n      this.canvas = canvas;\n    }\n\n    return this.canvas;\n  }\n\n  private renderTitle(canvas: Canvas) {\n    const titles = new TitleComponent({\n      options: this.options,\n      ctx: this.context,\n    });\n\n    canvas.removeChildren();\n    titles.getTitle().forEach((label) => {\n      if (label) canvas.appendChild(label);\n    });\n  }\n}\n\nclass TitleComponent {\n  private options: TitleOptions;\n\n  private context: RuntimeContext;\n  private get padding() {\n    return parsePadding(this.options.padding);\n  }\n\n  constructor(props: { ctx: RuntimeContext; options: TitleOptions }) {\n    const { options, ctx } = props;\n\n    this.options = options;\n\n    this.context = ctx;\n  }\n\n  public getTitle() {\n    const {\n      [titleKey]: propsTitle,\n      [subtitleKey]: propsSubtitle,\n      spacing = 44,\n      padding,\n      align,\n      ...style\n    } = this.options;\n\n    const titleText = propsTitle;\n    const subTitleText = propsSubtitle;\n\n    const titleStyle = subStyleProps(style, titleKey) as LabelStyleProps;\n    const subtitleStyle = subStyleProps(style, subtitleKey) as LabelStyleProps;\n\n    const [topGraphWidth] = this.context.graph.getSize();\n    const [pt = 0, pr = 0, , pl = 0] = this.padding;\n    const canvasWidth = topGraphWidth;\n    const textWidth = canvasWidth - pl - pr;\n\n    let subTitle: Label | null = null;\n\n    let alignX = pl;\n    let textAlign = 'left' as 'left' | 'center' | 'right';\n\n    switch (align) {\n      case 'left':\n        alignX = pl;\n        textAlign = 'left';\n        break;\n      case 'center':\n        alignX = canvasWidth / 2;\n        textAlign = 'center';\n        break;\n\n      case 'right':\n        alignX = canvasWidth - pr;\n        textAlign = 'right';\n        break;\n      default:\n        alignX = pl;\n        textAlign = 'left';\n    }\n\n    const title = new Label({\n      className: titleKey,\n      style: {\n        ...defaultTitleStyle,\n        wordWrapWidth: textWidth - 5,\n        x: alignX,\n        y: pt,\n        textAlign,\n        ...titleStyle,\n        text: titleText,\n      },\n    });\n\n    const titleBBox = title.getBBox();\n\n    if (subTitleText) {\n      subTitle = new Label({\n        className: 'subTitle',\n        style: {\n          ...defaultSubTitleStyle,\n          wordWrapWidth: textWidth - 5,\n          x: alignX,\n          y: titleBBox.height + spacing + pt,\n          textAlign,\n          ...subtitleStyle,\n          text: subTitleText,\n        },\n      });\n    }\n\n    return [title, subTitle];\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/toolbar/index.ts",
    "content": "import type { RuntimeContext } from '../../runtime/types';\nimport type { CornerPlacement } from '../../types';\nimport type { BasePluginOptions } from '../base-plugin';\nimport { BasePlugin } from '../base-plugin';\nimport { createPluginContainer, insertDOM } from '../utils/dom';\nimport type { ToolbarItem } from './util';\nimport { BUILD_IN_SVG_ICON, TOOLBAR_CSS, parsePositionToStyle } from './util';\n\n/**\n * <zh/> Toolbar 工具栏的配置项\n *\n * <en/> The options of the Toolbar toolbar\n */\nexport interface ToolbarOptions extends BasePluginOptions {\n  /**\n   * <zh/> 给工具栏的 DOM 追加的 classname，便于自定义样式。默认是包含 `g6-toolbar`\n   *\n   * <en/> The classname appended to the menu DOM for custom styles. The default is `g6-toolbar`\n   */\n  className?: string;\n  /**\n   * <zh/> Toolbar 的位置，相对于画布，会影响 DOM 的 style 样式\n   *\n   * <en/> The position of the Toolbar relative to the canvas, which will affect the style of the DOM\n   * @defaultValue 'top-left'\n   */\n  position?: CornerPlacement;\n  /**\n   * <zh/> 工具栏显式的 style 样式，可以用来设置它相对于画布的位置、背景容器样式等\n   *\n   * <en/> The style style of the Toolbar, which can be used to set its position relative to the canvas\n   */\n  style?: Partial<CSSStyleDeclaration>;\n  /**\n   * <zh/> 当工具栏被点击后，触发的回调方法\n   *\n   * <en/> The callback method triggered when the toolbar item is clicked\n   */\n  onClick?: (value: string, target: Element) => void;\n  /**\n   * <zh/> 返回工具栏的项目列表，支持 `Promise` 类型的返回值\n   *\n   * <en/> Return the list of toolbar items, support return a `Promise` as items\n   */\n  getItems: () => ToolbarItem[] | Promise<ToolbarItem[]>;\n}\n\n/**\n * <zh/> 工具栏，支持配置工具栏项目，以及点击之后的回调方法\n *\n * <en/> Toolbar, support configuration of toolbar items, and callback methods after clicking\n */\nexport class Toolbar extends BasePlugin<ToolbarOptions> {\n  static defaultOptions: Partial<ToolbarOptions> = {\n    position: 'top-left',\n  };\n\n  private $element: HTMLElement = createPluginContainer('toolbar', false);\n\n  constructor(context: RuntimeContext, options: ToolbarOptions) {\n    super(context, Object.assign({}, Toolbar.defaultOptions, options));\n\n    const $container = this.context.canvas.getContainer();\n    this.$element.style.display = 'flex';\n    $container!.appendChild(this.$element);\n\n    // 设置样式\n    insertDOM('g6-toolbar-css', 'style', {}, TOOLBAR_CSS, document.head);\n    insertDOM('g6-toolbar-svgicon', 'div', { display: 'none' }, BUILD_IN_SVG_ICON);\n\n    this.$element.addEventListener('click', this.onToolbarItemClick);\n\n    this.update(options);\n  }\n\n  /**\n   * <zh/> 更新工具栏的配置项\n   *\n   * <en/> Update the configuration of the toolbar\n   * @param options - <zh/> 工具栏的配置项 | <en/> The options of the toolbar\n   * @internal\n   */\n  public async update(options: Partial<ToolbarOptions>) {\n    super.update(options);\n\n    const { className, position, style } = this.options;\n\n    this.$element.className = `g6-toolbar ${className || ''}`;\n\n    // 设置容器的样式，主要是位置，背景之类的\n    Object.assign(this.$element.style, style, parsePositionToStyle(position));\n\n    this.$element.innerHTML = await this.getDOMContent();\n  }\n\n  /**\n   * <zh/> 销毁工具栏\n   *\n   * <en/> Destroy the toolbar\n   * @internal\n   */\n  public destroy(): void {\n    this.$element.removeEventListener('click', this.onToolbarItemClick);\n    this.$element.remove();\n\n    super.destroy();\n  }\n\n  private async getDOMContent() {\n    const items = await this.options.getItems();\n    return items\n      .map(\n        (item) => `\n          <div class=\"g6-toolbar-item\" value=\"${item.value}\" title=\"${item.title ?? ''}\">\n            <svg aria-hidden=\"true\" focusable=\"false\">\n              <use xlink:href=\"#${item.id}\"></use>\n            </svg>\n          </div>`,\n      )\n      .join('');\n  }\n\n  private onToolbarItemClick = (e: MouseEvent) => {\n    const { onClick } = this.options;\n    if (e.target instanceof Element) {\n      if (e.target.className.includes('g6-toolbar-item')) {\n        const v = e.target.getAttribute('value') as string;\n        onClick?.(v, e.target);\n      }\n    }\n  };\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/toolbar/util.ts",
    "content": "import type { CornerPlacement } from '../../types';\n\n/**\n * <zh/> 工具栏显示项目。\n *\n * <en/> The item of the toolbar.\n */\nexport interface ToolbarItem {\n  /**\n   * <zh/> 可以使用 id 来配置内置的工具栏项，可以是 'zoom-in'、'zoom-out'、'auto-fit'、'reset' 等值，也可以配合三方的 iconfont 使用，原理是通过 id 来匹配内置的 svg symbol。See: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/symbol。\n   *\n   * <en/> You can use id to configure the built-in toolbar items, which can be values such as 'zoom-in', 'zoom-out', 'auto-fit', 'reset', etc. One of the two configurations with `marker`. The principle is to match the built-in svg symbol through id. See: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/symbol.\n   */\n  readonly id: 'zoom-in' | 'zoom-out' | 'redo' | 'undo' | 'edit' | 'delete' | 'auto-fit' | 'export' | 'reset' | string;\n  /**\n   * <zh/> 工具栏项对应的值，在 onClick 中作为回调参数。\n   *\n   * <en/> The value corresponding to the toolbar item, used as a callback parameter in `onClick`.\n   */\n  readonly value: string;\n  /**\n   * <en/> The title of the toolbar item. The title will be displayed as a tooltip when the mouse hovers over the item.\n   */\n  readonly title?: string;\n}\n\n/**\n * <zh/> 解析 toolbar 的 position 为位置样式。\n *\n * <en/> Parse the position of the toolbar into position style.\n * @param position - position\n * @returns style\n */\nexport function parsePositionToStyle(position: CornerPlacement): Partial<CSSStyleDeclaration> {\n  const style: Partial<CSSStyleDeclaration> = {\n    top: 'unset',\n    right: 'unset',\n    bottom: 'unset',\n    left: 'unset',\n  };\n\n  const directions = position.split('-') as ('top' | 'right' | 'bottom' | 'left')[];\n  directions.forEach((d) => {\n    style[d] = '8px';\n  });\n\n  style.flexDirection = position.startsWith('top') || position.startsWith('bottom') ? 'row' : 'column';\n\n  return style;\n}\n\n/**\n * 内置默认的 toolbar 的 CSS 样式。\n */\nexport const TOOLBAR_CSS = `\n  .g6-toolbar {\n    position: absolute;\n    z-index: 100;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    justify-content: center;\n    border-radius: 4px;\n    box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.1);\n    opacity: 0.65;\n  }\n  .g6-toolbar .g6-toolbar-item {\n    display: inline-block;\n    width: 16px;\n    height: 16px;\n    padding: 4px;\n    cursor: pointer;\n    box-sizing: content-box;\n  }\n\n  .g6-toolbar .g6-toolbar-item:hover {\n    background-color: #f0f0f0;\n  }\n\n  .g6-toolbar .g6-toolbar-item svg {\n    display: inline-block;\n    width: 100%;\n    height: 100%;\n    pointer-events: none;\n  }\n`;\n\nexport const BUILD_IN_SVG_ICON = `\n  <svg>\n    <symbol id=\"zoom-in\" viewBox=\"64 64 896 896\">\n      <path d=\"M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z\"></path>\n    </symbol>\n    <symbol id=\"zoom-out\" viewBox=\"64 64 896 896\">\n      <path d=\"M637 443H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h312c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zm284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z\"></path>\n    </symbol>\n    <symbol id=\"edit\" viewBox=\"64 64 896 896\">\n      <path d=\"M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z\"></path>\n    </symbol>\n    <symbol id=\"delete\" viewBox=\"64 64 896 896\">\n      <path d=\"M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z\"></path>\n    </symbol>\n    <symbol id=\"redo\" viewBox=\"64 64 896 896\">\n      <path d=\"M758.2 839.1C851.8 765.9 912 651.9 912 523.9 912 303 733.5 124.3 512.6 124 291.4 123.7 112 302.8 112 523.9c0 125.2 57.5 236.9 147.6 310.2 3.5 2.8 8.6 2.2 11.4-1.3l39.4-50.5c2.7-3.4 2.1-8.3-1.2-11.1-8.1-6.6-15.9-13.7-23.4-21.2a318.64 318.64 0 01-68.6-101.7C200.4 609 192 567.1 192 523.9s8.4-85.1 25.1-124.5c16.1-38.1 39.2-72.3 68.6-101.7 29.4-29.4 63.6-52.5 101.7-68.6C426.9 212.4 468.8 204 512 204s85.1 8.4 124.5 25.1c38.1 16.1 72.3 39.2 101.7 68.6 29.4 29.4 52.5 63.6 68.6 101.7 16.7 39.4 25.1 81.3 25.1 124.5s-8.4 85.1-25.1 124.5a318.64 318.64 0 01-68.6 101.7c-9.3 9.3-19.1 18-29.3 26L668.2 724a8 8 0 00-14.1 3l-39.6 162.2c-1.2 5 2.6 9.9 7.7 9.9l167 .8c6.7 0 10.5-7.7 6.3-12.9l-37.3-47.9z\"></path>\n    </symbol>\n    <symbol id=\"undo\" viewBox=\"64 64 896 896\">\n      <path d=\"M511.4 124C290.5 124.3 112 303 112 523.9c0 128 60.2 242 153.8 315.2l-37.5 48c-4.1 5.3-.3 13 6.3 12.9l167-.8c5.2 0 9-4.9 7.7-9.9L369.8 727a8 8 0 00-14.1-3L315 776.1c-10.2-8-20-16.7-29.3-26a318.64 318.64 0 01-68.6-101.7C200.4 609 192 567.1 192 523.9s8.4-85.1 25.1-124.5c16.1-38.1 39.2-72.3 68.6-101.7 29.4-29.4 63.6-52.5 101.7-68.6C426.9 212.4 468.8 204 512 204s85.1 8.4 124.5 25.1c38.1 16.1 72.3 39.2 101.7 68.6 29.4 29.4 52.5 63.6 68.6 101.7 16.7 39.4 25.1 81.3 25.1 124.5s-8.4 85.1-25.1 124.5a318.64 318.64 0 01-68.6 101.7c-7.5 7.5-15.3 14.5-23.4 21.2a7.93 7.93 0 00-1.2 11.1l39.4 50.5c2.8 3.5 7.9 4.1 11.4 1.3C854.5 760.8 912 649.1 912 523.9c0-221.1-179.4-400.2-400.6-399.9z\"></path>\n    </symbol>\n    <symbol id=\"export\" viewBox=\"64 64 896 896\">\n      <path d=\"M880 912H144c-17.7 0-32-14.3-32-32V144c0-17.7 14.3-32 32-32h360c4.4 0 8 3.6 8 8v56c0 4.4-3.6 8-8 8H184v656h656V520c0-4.4 3.6-8 8-8h56c4.4 0 8 3.6 8 8v360c0 17.7-14.3 32-32 32zM770.87 199.13l-52.2-52.2a8.01 8.01 0 014.7-13.6l179.4-21c5.1-.6 9.5 3.7 8.9 8.9l-21 179.4c-.8 6.6-8.9 9.4-13.6 4.7l-52.4-52.4-256.2 256.2a8.03 8.03 0 01-11.3 0l-42.4-42.4a8.03 8.03 0 010-11.3l256.1-256.3z\"></path>\n    </symbol>\n    <symbol id=\"auto-fit\" viewBox=\"64 64 896 896\">\n      <path d=\"M952 474H829.8C812.5 327.6 696.4 211.5 550 194.2V72c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v122.2C327.6 211.5 211.5 327.6 194.2 474H72c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h122.2C211.5 696.4 327.6 812.5 474 829.8V952c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V829.8C696.4 812.5 812.5 696.4 829.8 550H952c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zM512 756c-134.8 0-244-109.2-244-244s109.2-244 244-244 244 109.2 244 244-109.2 244-244 244z\"></path>\n      <path d=\"M512 392c-32.1 0-62.1 12.4-84.8 35.2-22.7 22.7-35.2 52.7-35.2 84.8s12.5 62.1 35.2 84.8C449.9 619.4 480 632 512 632s62.1-12.5 84.8-35.2C619.4 574.1 632 544 632 512s-12.5-62.1-35.2-84.8A118.57 118.57 0 00512 392z\"></path>\n    </symbol>\n    <symbol id=\"reset\" viewBox=\"64 64 896 896\">\n      <path d=\"M909.1 209.3l-56.4 44.1C775.8 155.1 656.2 92 521.9 92 290 92 102.3 279.5 102 511.5 101.7 743.7 289.8 932 521.9 932c181.3 0 335.8-115 394.6-276.1 1.5-4.2-.7-8.9-4.9-10.3l-56.7-19.5a8 8 0 00-10.1 4.8c-1.8 5-3.8 10-5.9 14.9-17.3 41-42.1 77.8-73.7 109.4A344.77 344.77 0 01655.9 829c-42.3 17.9-87.4 27-133.8 27-46.5 0-91.5-9.1-133.8-27A341.5 341.5 0 01279 755.2a342.16 342.16 0 01-73.7-109.4c-17.9-42.4-27-87.4-27-133.9s9.1-91.5 27-133.9c17.3-41 42.1-77.8 73.7-109.4 31.6-31.6 68.4-56.4 109.3-73.8 42.3-17.9 87.4-27 133.8-27 46.5 0 91.5 9.1 133.8 27a341.5 341.5 0 01109.3 73.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 003 14.1l175.6 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c-.1-6.6-7.8-10.3-13-6.2z\"></path>\n    </symbol>\n    <symbol id=\"exit-fullscreen\" viewBox=\"0 0 1024 1024\">\n      <path d=\"M418.13333333 361.43786666c0 0.2048-0.13653333 0.4096-0.13653334 0.68266667C417.99679999 362.32533333 418.13333333 362.53013333 418.13333333 362.73493333 418.13333333 371.54133333 414.44693333 379.392 408.78079999 385.39946666 408.43946666 385.7408 408.30293333 386.21866666 408.02986666 386.49173333c-1.09226667 1.09226667-2.59413333 1.77493333-3.82293333 2.73066667C398.40426666 393.65973333 391.64586666 396.8 383.93173333 396.8 383.72693333 396.8 383.59039999 396.73173333 383.38559999 396.73173333S382.97599999 396.8 382.77119999 396.8L112.29866666 396.8C92.50133333 396.8 76.79999999 381.50826666 76.79999999 362.66666666 76.66346666 343.89333333 92.63786666 328.53333333 112.16213333 328.53333333l189.44 0L87.44959999 114.51733333C73.59146666 100.59093333 73.25013333 78.5408 86.63039999 65.29706666c13.17546667-13.44853333 35.36213333-12.97066667 49.152 0.88746667l214.08426667 214.08426667L349.86666666 90.89706666C349.79839999 71.23626666 365.22666666 55.46666666 383.99999999 55.46666666 402.77333333 55.33013333 418.13333333 71.30453333 418.13333333 90.8288L418.13333333 361.43786666zM928.90453333 328.53333333l-189.44 0 214.15253333-214.08426667c13.85813333-13.9264 14.19946667-35.90826667 0.88746667-49.22026666-13.17546667-13.44853333-35.36213333-12.97066667-49.152 0.88746666l-214.08426667 214.08426667L691.26826666 90.89706666C691.26826666 71.23626666 675.83999999 55.46666666 657.06666666 55.46666666 638.29333333 55.33013333 622.93333333 71.30453333 622.93333333 90.8288l0 270.60906666c0 0.2048 0.13653333 0.4096 0.13653333 0.68266667C623.06986666 362.32533333 622.93333333 362.53013333 622.93333333 362.73493333 622.93333333 371.54133333 626.61973333 379.392 632.28586666 385.39946666c0.34133333 0.34133333 0.47786667 0.8192 0.8192 1.09226667 1.09226667 1.09226667 2.59413333 1.77493333 3.8912 2.73066667C642.66239999 393.65973333 649.42079999 396.8 657.13493333 396.8c0.2048 0 0.34133333-0.06826667 0.54613333-0.06826667S658.09066666 396.8 658.29546666 396.8l270.5408 0C948.56533333 396.8 964.26666666 381.50826666 964.26666666 362.66666666 964.40319999 343.89333333 948.42879999 328.53333333 928.90453333 328.53333333zM418.13333333 635.73333333c0-8.8064-3.6864-16.5888-9.35253334-22.66453333C408.43946666 612.72746666 408.30293333 612.2496 408.02986666 611.90826666 406.86933333 610.88426666 405.43573333 610.2016 404.20693333 609.24586666 398.47253333 604.80853333 391.64586666 601.6 383.93173333 601.6 383.72693333 601.6 383.59039999 601.73653333 383.38559999 601.73653333S382.97599999 601.6 382.77119999 601.6L112.29866666 601.6C92.50133333 601.6 76.79999999 616.96 76.79999999 635.73333333 76.66346666 654.50666666 92.63786666 669.86666666 112.16213333 669.86666666l189.44 0-214.15253334 214.15253334c-13.85813333 13.85813333-14.19946667 35.84-0.88746666 49.22026666 13.17546667 13.44853333 35.36213333 12.9024 49.152-0.95573333l214.08426666-214.08426667 0 189.37173334c0 19.59253333 15.42826667 35.49866667 34.2016 35.36213333C402.77333333 943.2064 418.13333333 927.232 418.13333333 907.5712L418.13333333 637.09866666c0-0.27306667-0.13653333-0.47786667-0.13653334-0.68266666C417.99679999 636.14293333 418.13333333 635.93813333 418.13333333 635.73333333zM739.46453333 669.86666666l189.44 0c19.456 0 35.49866667-15.36 35.36213333-34.13333333C964.26666666 616.96 948.56533333 601.6 928.76799999 601.6L658.29546666 601.6C658.09066666 601.6 657.88586666 601.73653333 657.68106666 601.73653333S657.33973333 601.6 657.13493333 601.6C649.42079999 601.6 642.59413333 604.80853333 636.85973333 609.24586666 635.63093333 610.2016 634.19733333 610.88426666 633.03679999 611.90826666 632.76373333 612.2496 632.62719999 612.72746666 632.28586666 613.0688 626.61973333 619.14453333 622.93333333 626.92693333 622.93333333 635.73333333c0 0.2048 0.13653333 0.4096 0.13653333 0.68266667C623.06986666 636.6208 622.93333333 636.8256 622.93333333 637.09866666l0 270.5408C622.93333333 927.232 638.29333333 943.2064 657.06666666 942.93333333c18.77333333 0.13653333 34.2016-15.70133333 34.2016-35.36213333l0-189.37173334 214.08426667 214.08426667c13.78986667 13.85813333 35.90826667 14.40426667 49.152 0.95573333 13.312-13.312 12.97066667-35.36213333-0.88746667-49.22026666L739.46453333 669.86666666z\"  ></path></symbol>\n    <symbol id=\"request-fullscreen\" viewBox=\"0 0 1024 1024\">\n      <path d=\"M69.818182 87.598545v273.128728a34.909091 34.909091 0 0 0 69.818182 0V163.653818l221.928727 222.021818a33.512727 33.512727 0 0 0 47.383273-47.383272L186.926545 116.363636h197.073455a34.909091 34.909091 0 0 0 0-69.818181H110.871273C85.364364 46.545455 69.818182 59.671273 69.818182 87.598545zM938.542545 46.545455H665.413818a34.909091 34.909091 0 0 0 0 69.818181h197.073455L640.465455 338.292364a33.512727 33.512727 0 0 0 47.383272 47.383272l221.928728-222.021818v197.073455a34.909091 34.909091 0 0 0 69.818181 0V87.598545c0-27.927273-15.453091-41.053091-40.96-41.05309z m-827.671272 907.636363h273.128727a34.909091 34.909091 0 0 0 0-69.818182H186.926545l222.021819-221.928727a33.512727 33.512727 0 0 0-47.383273-47.383273L139.636364 837.073455V640a34.909091 34.909091 0 0 0-69.818182 0v273.128727c0 27.927273 15.546182 41.053091 41.053091 41.053091z m868.724363-41.053091V640a34.909091 34.909091 0 0 0-69.818181 0v197.073455L687.941818 615.051636a33.512727 33.512727 0 0 0-47.383273 47.383273L862.487273 884.363636H665.413818a34.909091 34.909091 0 0 0 0 69.818182h273.128727c25.6 0 41.053091-13.125818 41.053091-41.053091z\"  ></path></symbol>\n  </svg>\n`;\n"
  },
  {
    "path": "packages/g6/src/plugins/tooltip.ts",
    "content": "import type { TooltipStyleProps } from '@antv/component';\nimport { Tooltip as TooltipComponent } from '@antv/component';\nimport { get } from '@antv/util';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { ElementDatum, ElementType, ID, IElementEvent } from '../types';\nimport { isToBeDestroyed } from '../utils/element';\nimport type { BasePluginOptions } from './base-plugin';\nimport { BasePlugin } from './base-plugin';\n\n/**\n * <zh/> 提示框插件配置项\n *\n * <en/> Tooltip plugin options\n */\nexport interface TooltipOptions\n  extends\n    BasePluginOptions,\n    Pick<TooltipStyleProps, 'position' | 'offset' | 'enterable' | 'style' | 'container' | 'title'> {\n  /**\n   *  <zh/> 触发行为，可选 hover | click\n   * - `'hover'`：鼠标移入元素时触发\n   * - `'click'`：鼠标点击元素时触发\n   *\n   *  <en/> Trigger behavior, optional hover | click\n   * - `'hover'`：mouse hover element\n   * - `'click'`：mouse click element\n   * @defaultValue 'hover\n   */\n  trigger?: 'hover' | 'click';\n  /**\n   *  <zh/> 自定义内容\n   *\n   *  <en/> Function for getting tooltip content\n   */\n  getContent?: (event: IElementEvent, items: ElementDatum[]) => Promise<HTMLElement | string>;\n  /**\n   *  <zh/> 是否启用\n   *\n   *  <en/> Is enable\n   *  @defaultValue true\n   */\n  enable?: boolean | ((event: IElementEvent, items: ElementDatum[]) => boolean);\n  /**\n   * <zh/> 显示隐藏的回调\n   *\n   * <en/> Callback executed when visibility of the tooltip card is changed\n   */\n  onOpenChange: (open: boolean) => void;\n}\n\n/**\n * <zh/> 提示框插件\n *\n * <en/> Tooltip plugin\n */\nexport class Tooltip extends BasePlugin<TooltipOptions> {\n  static defaultOptions: Partial<TooltipOptions> = {\n    trigger: 'hover',\n    position: 'top-right',\n    enterable: false,\n    enable: true,\n    offset: [10, 10],\n    style: {\n      '.tooltip': {\n        visibility: 'hidden',\n      },\n    },\n  };\n  private currentTarget: string | null = null;\n  private tooltipElement: TooltipComponent | null = null;\n  private container: HTMLElement | null = null;\n\n  constructor(context: RuntimeContext, options: TooltipOptions) {\n    super(context, Object.assign({}, Tooltip.defaultOptions, options));\n    this.render();\n    this.bindEvents();\n  }\n\n  /**\n   * <zh/> 获取事件及处理事件的方法\n   *\n   * <en/> Get event and handle event methods\n   * @returns <zh/> 事件及处理事件的方法 | <en/> Event and handling event methods\n   */\n  private getEvents(): { [key: string]: (event: IElementEvent) => void } {\n    if (this.options.trigger === 'click') {\n      return {\n        'node:click': this.onClick,\n        'edge:click': this.onClick,\n        'combo:click': this.onClick,\n        'canvas:click': this.onPointerLeave,\n        contextmenu: this.onPointerLeave,\n        drag: this.onPointerLeave,\n      };\n    }\n\n    return {\n      'node:pointerover': this.onPointerOver,\n      'node:pointermove': this.onPointerMove,\n      'canvas:pointermove': this.onCanvasMove,\n      'edge:pointerover': this.onPointerOver,\n      'edge:pointermove': this.onPointerMove,\n      'combo:pointerover': this.onPointerOver,\n      'combo:pointermove': this.onPointerMove,\n      contextmenu: this.onPointerLeave,\n      'node:drag': this.onPointerLeave,\n    };\n  }\n  /**\n   * <zh/> 更新tooltip配置\n   *\n   * <en/> Update the tooltip configuration\n   * @param options - <zh/> 配置项 | <en/> options\n   * @internal\n   */\n  public update(options: Partial<TooltipOptions>) {\n    this.unbindEvents();\n    super.update(options);\n    if (this.tooltipElement) {\n      this.container?.removeChild(this.tooltipElement.HTMLTooltipElement);\n    }\n    this.tooltipElement = this.initTooltip();\n    this.bindEvents();\n  }\n\n  private render() {\n    const { canvas } = this.context;\n    const $container = canvas.getContainer();\n    if (!$container) return;\n    this.container = $container;\n    this.tooltipElement = this.initTooltip();\n  }\n\n  private unbindEvents() {\n    const { graph } = this.context;\n    /** The previous event binding needs to be removed when updating the trigger. */\n    const events = this.getEvents();\n    Object.keys(events).forEach((eventName) => {\n      graph.off(eventName, events[eventName]);\n    });\n  }\n\n  private bindEvents() {\n    const { graph } = this.context;\n    const events = this.getEvents();\n    Object.keys(events).forEach((eventName) => {\n      graph.on(eventName, events[eventName]);\n    });\n  }\n\n  private isEnable = (event: IElementEvent, items: ElementDatum[]) => {\n    const { enable } = this.options;\n    if (typeof enable === 'function') {\n      return enable(event, items);\n    }\n    return enable;\n  };\n\n  /**\n   * <zh/> 点击事件\n   *\n   * <en/> Click event\n   * @param event - <zh/> 元素 | <en/> element\n   */\n  public onClick = (event: IElementEvent) => {\n    const {\n      target: { id },\n    } = event;\n    // click the same item twice, tooltip will be hidden\n    if (this.currentTarget === id) {\n      this.hide(event);\n    } else {\n      this.show(event);\n    }\n  };\n\n  /**\n   * <zh/> 在目标元素(node/edge/combo)上移动\n   *\n   * <en/> Move on target element (node/edge/combo)\n   * @param event - <zh/> 目标元素 | <en/> target element\n   */\n  public onPointerMove = (event: IElementEvent) => {\n    const { target } = event;\n    if (!this.currentTarget || target.id === this.currentTarget) {\n      return;\n    }\n    this.show(event);\n  };\n  /**\n   * <zh/> 点击画布/触发拖拽/出现上下文菜单隐藏tooltip\n   *\n   * <en/> Hide tooltip when clicking canvas/triggering drag/appearing context menu\n   * @param event - <zh/> 目标元素 | <en/> target element\n   */\n  public onPointerLeave = (event: IElementEvent) => {\n    this.hide(event);\n  };\n  /**\n   * <zh/> 移动画布\n   *\n   * <en/> Move canvas\n   * @param event - <zh/> 目标元素 | <en/> target element\n   */\n  public onCanvasMove = (event: IElementEvent) => {\n    this.hide(event);\n  };\n\n  private onPointerOver = (event: IElementEvent) => {\n    this.show(event);\n  };\n\n  /**\n   * <zh/> 显示目标元素的提示框\n   *\n   * <en/> Show tooltip of target element\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   */\n  public showById = async (id: ID) => {\n    const event = {\n      target: { id },\n    } as IElementEvent;\n    await this.show(event);\n  };\n\n  private getElementData = (id: ID, targetType: ElementType) => {\n    const { model } = this.context;\n    switch (targetType) {\n      case 'node':\n        return model.getNodeData([id]);\n      case 'edge':\n        return model.getEdgeData([id]);\n      case 'combo':\n        return model.getComboData([id]);\n      default:\n        return [];\n    }\n  };\n\n  /**\n   * <zh/> 在目标元素上显示tooltip\n   *\n   * <en/> Show tooltip on target element\n   * @param event - <zh/> 目标元素 | <en/> target element\n   * @internal\n   */\n  public show = async (event: IElementEvent) => {\n    const {\n      client,\n      target: { id },\n    } = event;\n    if (isToBeDestroyed(event.target)) return;\n\n    const targetType = this.context.graph.getElementType(id);\n    const { getContent, title } = this.options;\n    const items: ElementDatum[] = this.getElementData(id, targetType as ElementType);\n\n    if (!this.tooltipElement) return;\n    // if shown, when is not enable, hide\n    if (!this.isEnable(event, items)) {\n      this.hide(event);\n      return;\n    }\n\n    let tooltipContent: { [key: string]: unknown } = {};\n    if (getContent) {\n      tooltipContent.content = await getContent(event, items);\n      if (!tooltipContent.content) return;\n    } else {\n      const style = this.context.graph.getElementRenderStyle(id);\n      const color = targetType === 'node' ? style.fill : style.stroke;\n      tooltipContent = {\n        title: title || targetType,\n        data: items.map((item) => {\n          return {\n            name: 'ID',\n            value: item.id || `${item.source} -> ${item.target}`,\n            color,\n          };\n        }),\n      };\n    }\n\n    this.currentTarget = id;\n\n    let x;\n    let y;\n    if (client) {\n      x = client.x;\n      y = client.y;\n    } else {\n      const style = get(items, '0.style', { x: 0, y: 0 });\n      x = style.x;\n      y = style.y;\n    }\n\n    this.options.onOpenChange?.(true);\n    this.tooltipElement.update({\n      ...this.tooltipStyleProps,\n      x,\n      y,\n      style: {\n        '.tooltip': {\n          visibility: 'visible',\n        },\n      },\n      ...tooltipContent,\n    });\n  };\n  /**\n   * <zh/> 隐藏tooltip\n   *\n   * <en/> Hidden tooltip\n   * @param event - <zh/> 目标元素,不传则为外部调用 | <en/> Target element, not passed in as external call\n   */\n  public hide = (event?: IElementEvent) => {\n    // if e is undefined, hide the tooltip， external call\n    if (!event) {\n      this.options.onOpenChange?.(false);\n      this.tooltipElement?.hide();\n      this.currentTarget = null;\n      return;\n    }\n    if (!this.tooltipElement) return;\n    // No target node: tooltip has been hidden. No need for duplicated call.\n    if (!this.currentTarget) return;\n    const {\n      client: { x, y },\n    } = event;\n    this.options.onOpenChange?.(false);\n    this.tooltipElement.hide(x, y);\n    this.currentTarget = null;\n  };\n\n  private get tooltipStyleProps() {\n    const { canvas } = this.context;\n    const { center } = canvas.getBounds();\n    const $container = canvas.getContainer() as HTMLElement;\n    const { top, left } = $container.getBoundingClientRect();\n    const { style, position, enterable, container = { x: -left, y: -top }, title, offset } = this.options;\n    const [x, y] = center;\n    const [width, height] = canvas.getSize();\n\n    return {\n      x,\n      y,\n      container,\n      title,\n      bounding: { x: 0, y: 0, width, height },\n      position,\n      enterable,\n      offset,\n      style,\n    };\n  }\n\n  private initTooltip = () => {\n    const tooltipElement = new TooltipComponent({\n      className: 'tooltip',\n      style: this.tooltipStyleProps,\n    });\n    this.container?.appendChild(tooltipElement.HTMLTooltipElement);\n    return tooltipElement;\n  };\n\n  /**\n   * <zh/> 销毁tooltip\n   *\n   * <en/> Destroy tooltip\n   * @internal\n   */\n  public destroy(): void {\n    this.unbindEvents();\n    if (this.tooltipElement) {\n      this.container?.removeChild(this.tooltipElement.HTMLTooltipElement);\n    }\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/types.ts",
    "content": "import type { BasePlugin } from './base-plugin';\n\nexport type Plugin = BasePlugin<any>;\n"
  },
  {
    "path": "packages/g6/src/plugins/utils/canvas.ts",
    "content": "import type { IRenderer } from '@antv/g';\nimport { Canvas as GCanvas } from '@antv/g';\nimport { Renderer } from '@antv/g-canvas';\nimport { Canvas } from '../../runtime/canvas';\nimport type { Placement } from '../../types';\nimport { parsePlacement } from '../../utils/placement';\nimport { createPluginContainer } from './dom';\n\ninterface Options {\n  /** <zh/> 插件宽度 | <en/> Plugin width */\n  width: number;\n  /** <zh/> 插件高度 | <en/> Plugin height */\n  height: number;\n  /** <zh/> 渲染器 | <en/> Render */\n  renderer?: IRenderer;\n  /** <zh/> 插件放置位置 | <en/> Plugin placement */\n  placement: Placement;\n  /** <zh/> 插件类名 | <en/> Plugin class name */\n  className: string;\n  /** <zh/> 指定插件放置容器 | <en/> Specify the plugin placement container */\n  container?: string | HTMLElement;\n  /** <zh/> 容器样式 | <en/> Container style */\n  containerStyle?: Partial<CSSStyleDeclaration>;\n  /** <zh/> G6 画布 | <en/> G6 canvas */\n  graphCanvas: Canvas;\n}\n\n/**\n * <zh/> 创建插件画布\n *\n * <en/> Create a plugin canvas\n * @param options - <zh/> 配置项 | <en/> options\n * @returns <zh/> [容器, 画布] | <en/> [container, canvas]\n */\nexport function createPluginCanvas(options: Options): [HTMLElement, GCanvas] {\n  const { width, height, renderer } = options;\n  const $container = getContainer(options);\n\n  const canvas = new GCanvas({\n    width,\n    height,\n    container: $container,\n    renderer: renderer || new Renderer(),\n  });\n\n  return [$container, canvas];\n}\n\n/**\n * <zh/> 获取容器\n *\n * <en/> Get container\n * @param options - <zh/> 配置项 | <en/> options\n * @returns <zh/> 容器 | <en/> container\n */\nfunction getContainer(options: Options) {\n  const { container, className, graphCanvas } = options;\n  if (container) {\n    return typeof container === 'string' ? document.getElementById(container)! : container;\n  }\n\n  const $container = createPluginContainer(className, false);\n\n  const { width, height, containerStyle } = options;\n  const [x, y] = computePosition(options);\n\n  Object.assign($container.style, {\n    position: 'absolute',\n    left: x + 'px',\n    top: y + 'px',\n    width: width + 'px',\n    height: height + 'px',\n    ...containerStyle,\n  });\n\n  graphCanvas.getContainer()?.appendChild($container);\n  return $container;\n}\n\n/**\n * <zh/> 计算容器位置\n *\n * <en/> Compute the position of the container\n * @param options - <zh/> 配置项 | <en/> options\n * @returns <zh/> 位置 | <en/> position\n */\nfunction computePosition(options: Options) {\n  const { width, height, placement, graphCanvas } = options;\n  const [W, H] = graphCanvas.getSize();\n  const [xRatio, yRatio] = parsePlacement(placement);\n  return [xRatio * (W - width), yRatio * (H - height)];\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/utils/dom.ts",
    "content": "/**\n * <zh/> 创建插件容器\n *\n * <en/> Create a plugin container\n * @param type - <zh/> 插件类型 | <en/> plugin type\n * @param cover - <zh/> 容器是否覆盖整个画布 | <en/> Whether the container covers the entire canvas\n * @param style - <zh/> 额外样式 | <en/> Additional style\n * @returns <zh/> 插件容器 | <en/> plugin container\n */\nexport function createPluginContainer(type: string, cover = true, style?: Partial<CSSStyleDeclaration>): HTMLElement {\n  const container = document.createElement('div');\n\n  container.setAttribute('class', `g6-${type}`);\n\n  Object.assign(container.style, {\n    position: 'absolute',\n    display: 'block',\n  });\n\n  if (cover) {\n    Object.assign(container.style, {\n      position: 'unset',\n      gridArea: '1 / 1 / 2 / 2',\n      inset: '0px',\n      height: '100%',\n      width: '100%',\n      overflow: 'hidden',\n      pointerEvents: 'none',\n    });\n  }\n\n  if (style) Object.assign(container.style, style);\n\n  return container;\n}\n\n/**\n * <zh/> 创建 DOM 元素，如果存在则删除，再创建一个新的\n *\n * <en/> Create a DOM element, if exists, remove it and create a new one.\n * @param id - <zh/> id | <en/> id\n * @param tag - <zh/> 标签 | <en/> tag\n * @param style - <zh/> 样式 | <en/> style\n * @param innerHTML - <zh/> 内容 | <en/> innerHTML\n * @param container - <zh/> 容器 | <en/> container\n * @returns <zh/> 创建的 DOM 元素 | <en/> created DOM element\n */\nexport function insertDOM(\n  id: string,\n  tag = 'div',\n  style: Partial<CSSStyleDeclaration> = {},\n  innerHTML = '',\n  container: HTMLElement = document.body,\n) {\n  const dom = document.getElementById(id);\n  if (dom) dom.remove();\n\n  const div = document.createElement(tag);\n  div.innerHTML = innerHTML;\n  div.id = id;\n\n  Object.assign(div.style, style);\n\n  container.appendChild(div);\n\n  return div;\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/watermark/index.ts",
    "content": "import type { RuntimeContext } from '../../runtime/types';\nimport type { BasePluginOptions } from '../base-plugin';\nimport { BasePlugin } from '../base-plugin';\nimport { createPluginContainer } from '../utils/dom';\nimport { getImageWatermark, getTextWatermark } from './util';\n\n/**\n * <zh/> 水印配置项\n *\n * <en/> Watermark options\n */\nexport interface WatermarkOptions extends BasePluginOptions {\n  /**\n   * <zh/> 水印的宽度（单个）\n   *\n   * <en/> The width of watermark(single)\n   * @defaultValue 200\n   */\n  width?: number;\n  /**\n   * <zh/> 水印的高度（单个）\n   *\n   * <en/> The height of watermark(single)\n   * @defaultValue 100\n   */\n  height?: number;\n  /**\n   * <zh/> 水印的透明度\n   *\n   * <en/> The opacity of watermark\n   * @defaultValue 0.2\n   */\n  opacity?: number;\n  /**\n   * <zh/> 水印的旋转角度\n   *\n   * <en/> The rotate angle of watermark\n   * @defaultValue Math.PI / 12\n   */\n  rotate?: number;\n  /**\n   * <zh/> 图片地址，如果有值，则使用，否则使用文本\n   *\n   * <en/> The image url, if it has a value, it will be used, otherwise it will use the text\n   */\n  imageURL?: string;\n  /**\n   * <zh/> 水印文本\n   *\n   * <en/> The text of watermark\n   */\n  text?: string;\n  /**\n   * <zh/> 文本水印的文字颜色\n   *\n   * <en/> The color of text watermark\n   * @defaultValue '#000'\n   */\n  textFill?: string;\n  /**\n   * <zh/> 文本水印的文本大小\n   *\n   * <en/> The font size of text watermark\n   * @defaultValue 16\n   */\n  textFontSize?: number;\n  /**\n   * <zh/> 文本水印的文本字体\n   *\n   * <en/> The font of text watermark\n   */\n  textFontFamily?: string;\n  /**\n   * <zh/> 文本水印的文本字体粗细\n   *\n   * <en/> The font weight of text watermark\n   */\n  textFontWeight?: string;\n  /**\n   * <zh/> 文本水印的文本字体变体\n   *\n   * <en/> The font variant of text watermark\n   */\n  textFontVariant?: string;\n  /**\n   * <zh/> 文本水印的文本对齐方式\n   *\n   * <en/> The text align of text watermark\n   * @defaultValue 'center'\n   */\n  textAlign?: 'center' | 'end' | 'left' | 'right' | 'start';\n  /**\n   * <zh/> 文本水印的文本对齐基线\n   *\n   * <en/> The text baseline of text watermark\n   * @defaultValue 'middle'\n   */\n  textBaseline?: 'alphabetic' | 'bottom' | 'hanging' | 'ideographic' | 'middle' | 'top';\n  /**\n   * <zh/> 水印的背景定位行为\n   *\n   * <en/> The background attachment of watermark\n   */\n  backgroundAttachment?: string;\n  /**\n   * <zh/> 水印的背景混合\n   *\n   * <en/> The background blend of watermark\n   */\n  backgroundBlendMode?: string;\n  /**\n   * <zh/> 水印的背景裁剪\n   *\n   * <en/> The background clip of watermark\n   */\n  backgroundClip?: string;\n  /**\n   * <zh/> 水印的背景颜色\n   *\n   * <en/> The background color of watermark\n   */\n  backgroundColor?: string;\n  /**\n   * <zh/> 水印的背景图片\n   *\n   * <en/> The background image of watermark\n   */\n  backgroundImage?: string;\n  /**\n   * <zh/> 水印的背景原点\n   *\n   * <en/> The background origin of watermark\n   */\n  backgroundOrigin?: string;\n  /**\n   * <zh/> 水印的背景位置\n   *\n   * <en/> The background position of watermark\n   */\n  backgroundPosition?: string;\n  /**\n   * <zh/> 水印的背景位置-x\n   *\n   * <en/> The background position-x of watermark\n   */\n  backgroundPositionX?: string;\n  /**\n   * <zh/> 水印的背景位置-y\n   *\n   * <en/> The background position-y of watermark\n   */\n  backgroundPositionY?: string;\n  /**\n   * <zh/> 水印的背景重复\n   *\n   * <en/> The background repeat of watermark\n   * @defaultValue 'repeat'\n   */\n  backgroundRepeat?: string;\n  /**\n   * <zh/> 水印的背景大小\n   *\n   * <en/> The background size of watermark\n   */\n  backgroundSize?: string;\n}\n\n/**\n * <zh/> 水印\n *\n * <en/> Watermark\n * @remarks\n * <zh/> 支持使用文本和图片作为水印，实现原理是在 Graph 容器的 div 上加上 `background-image` 属性，然后就可以通过 css 来控制水印的位置和样式。对于文本，会使用隐藏 canvas 转成图片的方式来实现\n *\n * <en/> Support using text and image as watermark, the principle is to add the `background-image` property to the div of the Graph container, and then you can control the position and style of the watermark through css. For text, it will be converted to an image using a hidden canvas\n */\nexport class Watermark extends BasePlugin<WatermarkOptions> {\n  static defaultOptions: Partial<WatermarkOptions> = {\n    width: 200,\n    height: 100,\n    opacity: 0.2,\n    rotate: Math.PI / 12,\n    text: '',\n    textFill: '#000',\n    textFontSize: 16,\n    textAlign: 'center',\n    textBaseline: 'middle',\n    backgroundRepeat: 'repeat',\n  };\n\n  private $element: HTMLElement = createPluginContainer('watermark');\n\n  constructor(context: RuntimeContext, options: WatermarkOptions) {\n    super(context, Object.assign({}, Watermark.defaultOptions, options));\n\n    const $container = this.context.canvas.getContainer();\n    $container!.appendChild(this.$element);\n\n    this.update(options);\n  }\n\n  /**\n   * <zh/> 更新水印配置\n   *\n   * <en/> Update the watermark configuration\n   * @param options - <zh/> 配置项 | <en/> Options\n   * @internal\n   */\n  public async update(options: Partial<WatermarkOptions>) {\n    super.update(options);\n\n    const { width, height, text, imageURL, ...rest } = this.options;\n\n    // Set the background style.\n    Object.keys(rest).forEach((key) => {\n      if (key.startsWith('background')) {\n        // @ts-expect-error ignore\n        this.$element.style[key] = options[key];\n      }\n    });\n\n    // Set the background image\n    const base64 = imageURL\n      ? await getImageWatermark(width, height, imageURL, rest)\n      : await getTextWatermark(width, height, text, rest);\n    this.$element.style.backgroundImage = `url(${base64})`;\n  }\n\n  /**\n   * <zh/> 销毁水印\n   *\n   * <en/> Destroy the watermark\n   * @internal\n   */\n  public destroy(): void {\n    super.destroy();\n    // Remove the background dom.\n    this.$element.remove();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/plugins/watermark/util.ts",
    "content": "// Only use one instance.\nlet canvas: HTMLCanvasElement;\n\n/**\n * Create a canvas instance.\n * @param width - width\n * @param height - height\n * @returns a new Canvas\n */\nfunction createCanvas(width: number, height: number): HTMLCanvasElement {\n  if (!canvas) {\n    canvas = document.createElement('canvas');\n  }\n  canvas.width = width;\n  canvas.height = height;\n  const ctx = canvas.getContext('2d');\n  ctx!.clearRect(0, 0, width, height);\n  return canvas;\n}\n\n/**\n * 从文本获取水印的 base64\n * @param width - width\n * @param height - height\n * @param text - 样式\n * @param style - 样式\n * @returns 水印的 base64\n */\nexport async function getTextWatermark(width: number, height: number, text: string, style: any) {\n  const canvas = createCanvas(width, height);\n  const ctx = canvas.getContext('2d')!;\n\n  const {\n    rotate,\n    opacity,\n    textFill,\n    textFontSize,\n    textFontFamily,\n    textFontVariant,\n    textFontWeight,\n    textAlign,\n    textBaseline,\n  } = style;\n\n  // Set the style.\n  // Default is align center and middle.\n  ctx.textAlign = textAlign;\n  ctx.textBaseline = textBaseline;\n  ctx.translate(width / 2, height / 2);\n\n  ctx.font = `${textFontSize}px ${textFontFamily} ${textFontVariant} ${textFontWeight}`;\n\n  rotate && ctx.rotate(rotate);\n  opacity && (ctx.globalAlpha = opacity);\n  if (textFill) {\n    ctx.fillStyle = textFill;\n    // Draw the text.\n    ctx.fillText(`${text}`, 0, 0);\n  }\n\n  // Return the base64.\n  return canvas.toDataURL();\n}\n\n/**\n * Get the image base64 of the watermark.\n * @param width - width\n * @param height - height\n * @param imageURL - image URL\n * @param style - 样式\n * @returns 水印的 base64\n */\nexport async function getImageWatermark(width: number, height: number, imageURL: string, style: any) {\n  const canvas = createCanvas(width, height);\n  const ctx = canvas.getContext('2d')!;\n  const { rotate, opacity } = style;\n\n  rotate && ctx.rotate(rotate);\n  opacity && (ctx.globalAlpha = opacity);\n\n  const img = new Image();\n  img.crossOrigin = 'anonymous';\n  img.src = imageURL;\n\n  return new Promise((resolve) => {\n    img.onload = function () {\n      const sepX = width > img.width ? (width - img.width) / 2 : 0;\n      const sepY = height > img.height ? (height - img.height) / 2 : 0;\n      ctx.drawImage(img, 0, 0, img.width, img.height, sepX, sepY, width - sepX * 2, height - sepY * 2);\n      resolve(canvas.toDataURL());\n    };\n  });\n}\n"
  },
  {
    "path": "packages/g6/src/preset.ts",
    "content": "import { registerBuiltInExtensions } from './registry/build-in';\n\nregisterBuiltInExtensions();\n"
  },
  {
    "path": "packages/g6/src/registry/build-in.ts",
    "content": "import {\n  Circle as GCircle,\n  Ellipse as GEllipse,\n  Group as GGroup,\n  HTML as GHTML,\n  Line as GLine,\n  Path as GPath,\n  Polygon as GPolygon,\n  Polyline as GPolyline,\n  Rect as GRect,\n  Text as GText,\n} from '@antv/g';\nimport { ComboCollapse, ComboExpand, Fade, NodeCollapse, NodeExpand, PathIn, PathOut, Translate } from '../animations';\nimport {\n  AutoAdaptLabel,\n  BrushSelect,\n  ClickSelect,\n  CollapseExpand,\n  CreateEdge,\n  DragCanvas,\n  DragElement,\n  DragElementForce,\n  FixElementSize,\n  FocusElement,\n  HoverActivate,\n  LassoSelect,\n  OptimizeViewportTransform,\n  ScrollCanvas,\n  ZoomCanvas,\n} from '../behaviors';\nimport {\n  Circle,\n  CircleCombo,\n  Cubic,\n  CubicHorizontal,\n  CubicRadial,\n  CubicVertical,\n  Diamond,\n  Donut,\n  Ellipse,\n  HTML,\n  Hexagon,\n  Image,\n  Line,\n  Polyline,\n  Quadratic,\n  Rect,\n  RectCombo,\n  Star,\n  Triangle,\n} from '../elements';\nimport { Badge as BadgeShape, Image as ImageShape, Label as LabelShape } from '../elements/shapes';\nimport {\n  AntVDagreLayout,\n  CircularLayout,\n  ComboCombinedLayout,\n  ConcentricLayout,\n  D3ForceLayout,\n  DagreLayout,\n  FishboneLayout,\n  ForceAtlas2Layout,\n  ForceLayout,\n  FruchtermanLayout,\n  GridLayout,\n  MDSLayout,\n  RadialLayout,\n  RandomLayout,\n  SnakeLayout,\n  compactBox,\n  dendrogram,\n  indented,\n  mindmap,\n} from '../layouts';\nimport { blues, greens, oranges, spectral, tableau } from '../palettes';\nimport {\n  Background,\n  BubbleSets,\n  Contextmenu,\n  EdgeBundling,\n  EdgeFilterLens,\n  Fisheye,\n  Fullscreen,\n  GridLine,\n  History,\n  Hull,\n  Legend,\n  Minimap,\n  Snapline,\n  Timebar,\n  Title,\n  Toolbar,\n  Tooltip,\n  Watermark,\n} from '../plugins';\nimport { dark, light } from '../themes';\nimport {\n  ArrangeDrawOrder,\n  CollapseExpandCombo,\n  CollapseExpandNode,\n  GetEdgeActualEnds,\n  MapNodeSize,\n  PlaceRadialLabels,\n  ProcessParallelEdges,\n  UpdateRelatedEdge,\n} from '../transforms';\nimport type { ExtensionRegistry } from './types';\n\n/**\n * <zh/> 内置插件统一在这里注册。\n * <en/> Built-in extensions are registered here.\n */\nconst BUILT_IN_EXTENSIONS: ExtensionRegistry = {\n  animation: {\n    'combo-collapse': ComboCollapse,\n    'combo-expand': ComboExpand,\n    'node-collapse': NodeCollapse,\n    'node-expand': NodeExpand,\n    'path-in': PathIn,\n    'path-out': PathOut,\n    fade: Fade,\n    translate: Translate,\n  },\n  behavior: {\n    'brush-select': BrushSelect,\n    'click-select': ClickSelect,\n    'collapse-expand': CollapseExpand,\n    'create-edge': CreateEdge,\n    'drag-canvas': DragCanvas,\n    'drag-element-force': DragElementForce,\n    'drag-element': DragElement,\n    'fix-element-size': FixElementSize,\n    'focus-element': FocusElement,\n    'hover-activate': HoverActivate,\n    'lasso-select': LassoSelect,\n    'auto-adapt-label': AutoAdaptLabel,\n    'optimize-viewport-transform': OptimizeViewportTransform,\n    'scroll-canvas': ScrollCanvas,\n    'zoom-canvas': ZoomCanvas,\n  },\n  combo: {\n    circle: CircleCombo,\n    rect: RectCombo,\n  },\n  edge: {\n    cubic: Cubic,\n    line: Line,\n    polyline: Polyline,\n    quadratic: Quadratic,\n    'cubic-horizontal': CubicHorizontal,\n    'cubic-radial': CubicRadial,\n    'cubic-vertical': CubicVertical,\n  },\n  layout: {\n    'antv-dagre': AntVDagreLayout,\n    'combo-combined': ComboCombinedLayout,\n    'compact-box': compactBox as any,\n    'd3-force': D3ForceLayout,\n    'force-atlas2': ForceAtlas2Layout,\n    circular: CircularLayout,\n    concentric: ConcentricLayout,\n    dagre: DagreLayout,\n    dendrogram: dendrogram as any,\n    fishbone: FishboneLayout,\n    force: ForceLayout,\n    fruchterman: FruchtermanLayout,\n    grid: GridLayout,\n    indented: indented as any,\n    mds: MDSLayout,\n    mindmap: mindmap as any,\n    radial: RadialLayout,\n    random: RandomLayout,\n    snake: SnakeLayout,\n  },\n  node: {\n    circle: Circle,\n    diamond: Diamond,\n    ellipse: Ellipse,\n    hexagon: Hexagon,\n    html: HTML,\n    image: Image,\n    rect: Rect,\n    star: Star,\n    donut: Donut,\n    triangle: Triangle,\n  },\n  palette: {\n    spectral,\n    tableau,\n    oranges,\n    greens,\n    blues,\n  },\n  theme: {\n    dark,\n    light,\n  },\n  plugin: {\n    'bubble-sets': BubbleSets,\n    'edge-bundling': EdgeBundling,\n    'edge-filter-lens': EdgeFilterLens,\n    'grid-line': GridLine,\n    background: Background,\n    contextmenu: Contextmenu,\n    fisheye: Fisheye,\n    fullscreen: Fullscreen,\n    history: History,\n    hull: Hull,\n    legend: Legend,\n    minimap: Minimap,\n    snapline: Snapline,\n    timebar: Timebar,\n    title: Title,\n    toolbar: Toolbar,\n    tooltip: Tooltip,\n    watermark: Watermark,\n  },\n  transform: {\n    'arrange-draw-order': ArrangeDrawOrder,\n    'collapse-expand-combo': CollapseExpandCombo,\n    'collapse-expand-node': CollapseExpandNode,\n    'get-edge-actual-ends': GetEdgeActualEnds,\n    'map-node-size': MapNodeSize,\n    'place-radial-labels': PlaceRadialLabels,\n    'process-parallel-edges': ProcessParallelEdges,\n    'update-related-edges': UpdateRelatedEdge,\n  },\n  shape: {\n    circle: GCircle,\n    ellipse: GEllipse,\n    group: GGroup,\n    html: GHTML,\n    image: ImageShape,\n    line: GLine,\n    path: GPath,\n    polygon: GPolygon,\n    polyline: GPolyline,\n    rect: GRect,\n    text: GText,\n    label: LabelShape,\n    badge: BadgeShape,\n  },\n};\n\nimport type { ExtensionCategory } from '../constants';\nimport { register } from './register';\n\n/**\n * <zh/> 注册内置扩展\n *\n * <en/> Register built-in extensions\n */\nexport function registerBuiltInExtensions() {\n  Object.entries(BUILT_IN_EXTENSIONS).forEach(([category, extensions]) => {\n    Object.entries(extensions).forEach(([type, extension]) => {\n      register(category as ExtensionCategory, type, extension as any);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/g6/src/registry/extension/index.ts",
    "content": "import type EventEmitter from '@antv/event-emitter';\nimport type { Graph } from '../../runtime/graph';\nimport type { RuntimeContext } from '../../runtime/types';\nimport type { IEvent } from '../../types';\nimport { arrayDiff } from '../../utils/diff';\nimport { parseExtensions } from '../../utils/extension';\nimport { print } from '../../utils/print';\nimport { getExtension } from '../get';\nimport type { STDExtensionOption } from './types';\n\nexport abstract class ExtensionController<E extends BaseExtension<any>> {\n  protected context: RuntimeContext;\n\n  protected extensions: STDExtensionOption[] = [];\n\n  protected extensionMap: Record<string, E> = {};\n\n  public abstract category: 'plugin' | 'behavior' | 'transform';\n\n  constructor(context: RuntimeContext) {\n    this.context = context;\n  }\n\n  public setExtensions(\n    extensions: (\n      | string\n      | { type: string; [keys: string]: unknown }\n      | ((this: Graph) => { type: string; [keys: string]: unknown })\n    )[],\n  ) {\n    const stdExtensions = parseExtensions(this.context.graph, this.category, extensions) as STDExtensionOption[];\n    const { enter, update, exit, keep } = arrayDiff(this.extensions, stdExtensions, (extension) => extension.key);\n\n    this.createExtensions(enter);\n    this.updateExtensions([...update, ...keep]);\n    this.destroyExtensions(exit);\n\n    this.extensions = stdExtensions;\n  }\n\n  protected createExtension(extension: STDExtensionOption) {\n    const { category } = this;\n\n    const { key, type } = extension;\n    const Ctor = getExtension(category, type);\n    if (!Ctor) return print.warn(`The extension ${type} of ${category} is not registered.`);\n\n    const instance = new Ctor(this.context, extension);\n    instance.initialized = true;\n    this.extensionMap[key] = instance as E;\n  }\n\n  protected createExtensions(extensions: STDExtensionOption[]) {\n    extensions.forEach((extension) => this.createExtension(extension));\n  }\n\n  protected updateExtension(extension: STDExtensionOption) {\n    const { key } = extension;\n    const instance = this.extensionMap[key];\n    if (instance) {\n      instance.update(extension);\n    }\n  }\n\n  protected updateExtensions(extensions: STDExtensionOption[]) {\n    extensions.forEach((extension) => this.updateExtension(extension));\n  }\n\n  protected destroyExtension(key: string) {\n    const instance = this.extensionMap[key];\n\n    if (!instance) return;\n    if (instance.initialized && !instance.destroyed) {\n      instance.destroy();\n    }\n\n    delete this.extensionMap[key];\n  }\n\n  protected destroyExtensions(extensions: STDExtensionOption[]) {\n    extensions.forEach(({ key }) => this.destroyExtension(key));\n  }\n\n  public destroy() {\n    this.destroyExtensions(this.extensions);\n    // @ts-expect-error force delete\n    this.context = {};\n    this.extensions = [];\n    this.extensionMap = {};\n  }\n}\n\n/**\n * <zh/> 模块实例基类\n *\n * <en/> Base class for extension instance\n */\nexport class BaseExtension<T extends { type: string; key?: string; [key: string]: unknown }> {\n  protected context: RuntimeContext;\n\n  protected options: Required<T>;\n\n  protected events: [EventEmitter | HTMLElement, string, (event: IEvent) => void][] = [];\n\n  public initialized = false;\n\n  public destroyed = false;\n\n  constructor(context: RuntimeContext, options: Partial<T>) {\n    this.context = context;\n    this.options = options as Required<T>;\n  }\n\n  public update(options: Partial<T>) {\n    this.options = Object.assign(this.options, options);\n  }\n\n  public destroy() {\n    // @ts-expect-error force delete\n    this.context = {};\n    // @ts-expect-error force delete\n    this.options = {};\n\n    this.destroyed = true;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/registry/extension/types.ts",
    "content": "/**\n * <zh/> 标准扩展配置项\n *\n * <en/> Standard extension options\n */\nexport interface STDExtensionOption {\n  /**\n   * <zh/> 扩展类型\n   *\n   * <en/> Extension type\n   */\n  type: string;\n  /**\n   * <zh/> 扩展 key，即唯一标识\n   *\n   * <en/> Extension key, that is, the unique identifier\n   */\n  key: string;\n  [key: string]: unknown;\n}\n"
  },
  {
    "path": "packages/g6/src/registry/get.ts",
    "content": "import { ExtensionCategory } from '../constants';\nimport type { Loosen } from '../types';\nimport { EXTENSION_REGISTRY } from './store';\nimport type { ExtensionRegistry } from './types';\n\n/**\n * <zh/> 根据类别和类型获取扩展\n *\n * <en/> Get the extension by category and type\n * @param category - <zh/> 扩展类别 | <en/> Extension category\n * @param type - <zh/> 扩展类型 | <en/> Extension type\n * @returns <zh/> 注册的扩展 | <en/> Registered extension\n * @internal\n */\nexport function getExtension<T extends ExtensionCategory>(\n  category: Loosen<T>,\n  type: string,\n): ExtensionRegistry[T][string] | undefined {\n  const extension = EXTENSION_REGISTRY[category]?.[type];\n\n  if (extension) {\n    return extension as ExtensionRegistry[T][string];\n  }\n  return undefined;\n}\n\n/**\n * <zh/> 根据类别获取扩展\n *\n * <en/> Get the extension by category and type\n * @param category - <zh/> 扩展类别 | <en/> Extension category\n * @returns <zh/> 注册的扩展 | <en/> Registered extension\n * @internal\n */\nexport function getExtensions<T extends Loosen<ExtensionCategory>>(category: T): ExtensionRegistry[T] {\n  return EXTENSION_REGISTRY[category];\n}\n"
  },
  {
    "path": "packages/g6/src/registry/register.ts",
    "content": "import type { ExtensionCategory } from '../constants';\nimport type { Loosen } from '../types';\nimport { print } from '../utils/print';\nimport { EXTENSION_REGISTRY } from './store';\nimport type { ExtensionRegistry } from './types';\n\n/**\n * <zh/> 注册一个新的扩展。\n *\n * <en/> Registers a new extension.\n * @param category\n * <zh/> 扩展要注册的分类，目前支持注册的扩展分类有：{@link ExtensionCategory}\n *\n * <en/> The category under which the extension is to be registered, see {@link ExtensionCategory}\n * @param type\n * <zh/> 要注册的扩展的类型，将作为使用扩展时的标识\n *\n * <en/> Extension type that used as an identifier when mounting the extension on a graph\n * @param Ctor\n * <zh/> 要注册的扩展类，在使用时创建实例\n *\n * <en/> Whether to override the registered extension\n * @remarks\n * <zh/> 内置扩展在项目导入时会自动注册。对于非内置扩展，可以通过 `register` 方法手动注册。扩展只需要注册一次，即可在项目的任何位置使用。\n *\n * <en/> Built-in extensions are automatically registered when the project is imported. For non-built-in extensions, you can manually register them using the `register` method. Extensions only need to be registered once and can be used anywhere in the project.\n * @example\n * ```ts\n * import { register, BaseNode } from '@antv/g6';\n *\n * class CircleNode extends BaseNode {}\n *\n * register('node', 'circle-node', CircleNode);\n * ```\n * @public\n */\nexport function register<T extends ExtensionCategory>(\n  category: Loosen<T>,\n  type: string,\n  Ctor: ExtensionRegistry[T][string],\n) {\n  const ext = EXTENSION_REGISTRY[category][type];\n  if (ext) {\n    print.warn(`The extension ${type} of ${category} has been registered before, and will be overridden.`);\n  }\n\n  Object.assign(EXTENSION_REGISTRY[category]!, { [type]: Ctor });\n}\n"
  },
  {
    "path": "packages/g6/src/registry/store.ts",
    "content": "import type { ExtensionRegistry } from './types';\n\n/**\n * <zh/> 扩展注册表\n *\n * <en/> Extension registry\n */\nexport const EXTENSION_REGISTRY: ExtensionRegistry = {\n  animation: {},\n  behavior: {},\n  combo: {},\n  edge: {},\n  layout: {},\n  node: {},\n  palette: {},\n  theme: {},\n  plugin: {},\n  transform: {},\n  shape: {},\n};\n"
  },
  {
    "path": "packages/g6/src/registry/types.ts",
    "content": "import type { DisplayObject } from '@antv/g';\nimport type { STDAnimation } from '../animations/types';\nimport type { Behavior } from '../behaviors/types';\nimport type { Layout } from '../layouts/types';\nimport type { STDPalette } from '../palettes/types';\nimport type { Plugin } from '../plugins/types';\nimport type { Theme } from '../themes/types';\nimport type { Transform } from '../transforms/types';\nimport type { Combo, Edge, Node } from '../types';\n\n/**\n * <zh/> 扩展注册表\n *\n * <en/> Extension registry\n */\nexport interface ExtensionRegistry {\n  node: Record<string, { new (...args: any[]): Node }>;\n  edge: Record<string, { new (...args: any[]): Edge }>;\n  combo: Record<string, { new (...args: any[]): Combo }>;\n  theme: Record<string, Theme>; // theme is a object options\n  palette: Record<string, STDPalette>;\n  layout: Record<string, { new (...args: any[]): Layout }>;\n  behavior: Record<string, { new (...args: any[]): Behavior }>;\n  plugin: Record<string, { new (...args: any[]): Plugin }>;\n  animation: Record<string, STDAnimation>; // animation spec\n  transform: Record<string, { new (...args: any[]): Transform }>;\n  shape: Record<string, { new (...args: any[]): DisplayObject }>;\n}\n"
  },
  {
    "path": "packages/g6/src/runtime/animation.ts",
    "content": "import type { IAnimation } from '@antv/g';\nimport { executor } from '../animations/executor';\nimport type { AnimationContext, AnimationEffectTiming } from '../animations/types';\nimport type { ID, Point } from '../types';\nimport { createAnimationsProxy, getElementAnimationOptions, inferDefaultValue } from '../utils/animation';\nimport { getCachedStyle } from '../utils/cache';\nimport type { RuntimeContext } from './types';\n\nexport class Animation {\n  private context: RuntimeContext;\n\n  constructor(context: RuntimeContext) {\n    this.context = context;\n  }\n\n  private tasks: [AnimationContext, AnimationCallbacks | undefined][] = [];\n\n  private animations: Set<IAnimation> = new Set();\n\n  private getTasks() {\n    const tasks = [...this.tasks];\n    this.tasks = [];\n    return tasks;\n  }\n\n  public add(context: AnimationContext, callbacks?: AnimationCallbacks) {\n    this.tasks.push([context, callbacks]);\n  }\n\n  public animate(\n    localAnimation?: AnimationEffectTiming | boolean,\n    callbacks?: AnimationCallbacks,\n    extendOptions?: ExtendOptions,\n  ) {\n    callbacks?.before?.();\n\n    const animations = this.getTasks()\n      .map(([context, cb]) => {\n        const { element, elementType, stage } = context;\n        const options = getElementAnimationOptions(this.context.options, elementType, stage, localAnimation);\n        cb?.before?.();\n        const animation = options.length ? executor(element, this.inferStyle(context, extendOptions), options) : null;\n\n        if (animation) {\n          cb?.beforeAnimate?.(animation);\n          animation.finished.then(() => {\n            cb?.afterAnimate?.(animation);\n            cb?.after?.();\n            this.animations.delete(animation);\n          });\n        } else cb?.after?.();\n\n        return animation;\n      })\n      .filter(Boolean) as IAnimation[];\n\n    animations.forEach((animation) => this.animations.add(animation));\n\n    const animation = createAnimationsProxy(animations);\n\n    if (animation) {\n      callbacks?.beforeAnimate?.(animation);\n\n      animation.finished.then(() => {\n        callbacks?.afterAnimate?.(animation);\n        callbacks?.after?.();\n        this.release();\n      });\n    } else callbacks?.after?.();\n\n    return animation;\n  }\n\n  /**\n   * <zh/> 推断额外的动画样式\n   *\n   * <en/> Infer additional animation styles\n   * @param context - <zh/> 动画上下文 | <en/> Animation context\n   * @param options - <zh/> 扩展选项 | <en/> Extend options\n   * @returns <zh/> 始态样式与终态样式 | <en/> Initial style and final style\n   */\n  public inferStyle(\n    context: AnimationContext,\n    options?: ExtendOptions,\n  ): [Record<string, unknown>, Record<string, unknown>] {\n    const { element, elementType, stage, originalStyle, updatedStyle = {} } = context;\n\n    if (!context.modifiedStyle) context.modifiedStyle = { ...originalStyle, ...updatedStyle };\n    const { modifiedStyle } = context;\n\n    const fromStyle: Record<string, unknown> = {};\n    const toStyle: Record<string, unknown> = {};\n\n    if (stage === 'enter') {\n      Object.assign(fromStyle, { opacity: 0 });\n    } else if (stage === 'exit') {\n      Object.assign(toStyle, { opacity: 0 });\n    } else if (stage === 'show') {\n      Object.assign(fromStyle, { opacity: 0 });\n      Object.assign(toStyle, { opacity: getCachedStyle(element, 'opacity') ?? inferDefaultValue('opacity') });\n    } else if (stage === 'hide') {\n      Object.assign(fromStyle, { opacity: getCachedStyle(element, 'opacity') ?? inferDefaultValue('opacity') });\n      Object.assign(toStyle, { opacity: 0 });\n    } else if (stage === 'collapse') {\n      const { collapse } = options || {};\n      const { target, descendants, position } = collapse!;\n      if (elementType === 'node') {\n        // 为即将被删除的元素设置目标位置\n        // Set the target position for the element to be deleted\n        if (descendants.includes(element.id)) {\n          const [x, y, z] = position;\n          Object.assign(toStyle, { x, y, z });\n        }\n      } else if (elementType === 'combo') {\n        if (element.id === target || descendants.includes(element.id)) {\n          const [x, y] = position;\n          Object.assign(toStyle, { x, y, childrenNode: originalStyle.childrenNode });\n        }\n      } else if (elementType === 'edge') {\n        Object.assign(toStyle, { sourceNode: modifiedStyle.sourceNode, targetNode: modifiedStyle.targetNode });\n      }\n    } else if (stage === 'expand') {\n      const { expand } = options || {};\n      const { target, descendants, position } = expand!;\n      if (elementType === 'node') {\n        // 设置展开节点的起点位置\n        // Set the starting position of the expanded node\n        if (element.id === target || descendants.includes(element.id)) {\n          const [x, y, z] = position;\n          Object.assign(fromStyle, { x, y, z });\n        }\n      } else if (elementType === 'combo') {\n        // 设置展开后的组合子元素\n        // Set the child elements of the expanded combo\n        if (element.id === target || descendants.includes(element.id)) {\n          const [x, y, z] = position;\n          Object.assign(fromStyle, { x, y, z, childrenNode: modifiedStyle.childrenNode });\n        }\n      } else if (elementType === 'edge') {\n        // 设置展开后的边的起点和终点\n        // Set the starting point and end point of the edge after expansion\n        Object.assign(fromStyle, { sourceNode: modifiedStyle.sourceNode, targetNode: modifiedStyle.targetNode });\n      }\n    }\n\n    return [\n      Object.keys(fromStyle).length > 0 ? Object.assign({}, originalStyle, fromStyle) : originalStyle,\n      Object.keys(toStyle).length > 0 ? Object.assign({}, modifiedStyle, toStyle) : modifiedStyle,\n    ];\n  }\n\n  public stop() {\n    this.animations.forEach((animation) => animation.cancel());\n  }\n\n  public clear() {\n    this.tasks = [];\n  }\n\n  /**\n   * <zh/> 释放存量动画对象\n   *\n   * <en/> Release stock animation objects\n   * @description see: https://github.com/antvis/G/issues/1731\n   */\n  private release() {\n    const { canvas } = this.context;\n\n    // @ts-expect-error private property\n    const animationsWithPromises = canvas.document?.timeline?.animationsWithPromises;\n    if (animationsWithPromises) {\n      // @ts-expect-error private property\n      canvas.document.timeline.animationsWithPromises = animationsWithPromises.filter(\n        (animation: IAnimation) => animation.playState !== 'finished',\n      );\n    }\n  }\n\n  public destroy() {\n    this.stop();\n    this.animations.clear();\n    this.tasks = [];\n  }\n}\n\ninterface AnimationCallbacks {\n  before?: () => void;\n  beforeAnimate?: (animation: IAnimation) => void;\n  afterAnimate?: (animation: IAnimation) => void;\n  after?: () => void;\n}\n\ninterface ExtendOptions {\n  /**\n   * <zh/> stage 为 collapse 时，指定当前展开/收起的目标元素及其后代元素\n   *\n   * <en/> When the stage is collapse, specify the target element and its descendants to expand/collapse\n   */\n  collapse?: {\n    target: ID;\n    descendants: ID[];\n    position: Point;\n  };\n\n  /**\n   * <zh/> stage 为 expand 时，指定当前展开/收起的目标元素及其后代元素\n   *\n   * <en/> When the stage is expand, specify the target element and its descendants to expand/collapse\n   */\n  expand?: {\n    target: ID;\n    descendants: ID[];\n    position: Point;\n  };\n}\n"
  },
  {
    "path": "packages/g6/src/runtime/batch.ts",
    "content": "import { GraphEvent } from '../constants';\nimport type { BaseEvent } from '../utils/event';\nimport { GraphLifeCycleEvent } from '../utils/event';\nimport type { RuntimeContext } from './types';\n\nexport class BatchController {\n  private context: RuntimeContext;\n\n  private batchCount: number = 0;\n\n  constructor(context: RuntimeContext) {\n    this.context = context;\n  }\n\n  private emit(event: BaseEvent) {\n    const { graph } = this.context;\n    graph.emit(event.type, event);\n  }\n\n  public startBatch(initiate = true) {\n    this.batchCount++;\n    if (this.batchCount === 1) this.emit(new GraphLifeCycleEvent(GraphEvent.BATCH_START, { initiate }));\n  }\n\n  public endBatch() {\n    this.batchCount--;\n    if (this.batchCount === 0) this.emit(new GraphLifeCycleEvent(GraphEvent.BATCH_END));\n  }\n\n  public get isBatching() {\n    return this.batchCount > 0;\n  }\n\n  public destroy() {\n    // @ts-ignore\n    this.context = null;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/runtime/behavior.ts",
    "content": "import type { DisplayObject, FederatedPointerEvent, FederatedWheelEvent } from '@antv/g';\nimport type { BaseBehavior } from '../behaviors/base-behavior';\nimport { CommonEvent, ContainerEvent } from '../constants';\nimport { ExtensionController } from '../registry/extension';\nimport type { BehaviorOptions, CustomBehaviorOption } from '../spec/behavior';\nimport type { Target } from '../types';\nimport { isToBeDestroyed } from '../utils/element';\nimport { eventTargetOf } from '../utils/event';\nimport type { RuntimeContext } from './types';\n\nexport class BehaviorController extends ExtensionController<BaseBehavior<CustomBehaviorOption>> {\n  /**\n   * <zh/> 当前事件的目标\n   *\n   *  <en/> The current event target\n   */\n  private currentTarget: Target | null = null;\n\n  private currentTargetType: string | null = null;\n\n  public category = 'behavior' as const;\n\n  constructor(context: RuntimeContext) {\n    super(context);\n    this.forwardEvents();\n    this.setBehaviors(this.context.options.behaviors || []);\n  }\n\n  public setBehaviors(behaviors: BehaviorOptions) {\n    this.setExtensions(behaviors);\n  }\n\n  private forwardEvents() {\n    const container = this.context.canvas.getContainer();\n    if (container) {\n      [ContainerEvent.KEY_DOWN, ContainerEvent.KEY_UP].forEach((name) => {\n        container.addEventListener(name, this.forwardContainerEvents);\n      });\n    }\n\n    const canvas = this.context.canvas.document;\n    if (canvas) {\n      [\n        CommonEvent.CLICK,\n        CommonEvent.DBLCLICK,\n        CommonEvent.POINTER_OVER,\n        CommonEvent.POINTER_LEAVE,\n        CommonEvent.POINTER_ENTER,\n        CommonEvent.POINTER_MOVE,\n        CommonEvent.POINTER_OUT,\n        CommonEvent.POINTER_DOWN,\n        CommonEvent.POINTER_UP,\n        CommonEvent.CONTEXT_MENU,\n        CommonEvent.DRAG_START,\n        CommonEvent.DRAG,\n        CommonEvent.DRAG_END,\n        CommonEvent.DRAG_ENTER,\n        CommonEvent.DRAG_OVER,\n        CommonEvent.DRAG_LEAVE,\n        CommonEvent.DROP,\n        CommonEvent.WHEEL,\n      ].forEach((name) => {\n        canvas.addEventListener(name, this.forwardCanvasEvents);\n      });\n    }\n  }\n\n  private forwardCanvasEvents = (event: FederatedPointerEvent | FederatedWheelEvent) => {\n    const { target: originalTarget } = event;\n    const target = eventTargetOf(originalTarget as DisplayObject);\n    if (!target) return;\n    const { graph, canvas } = this.context;\n    const { type: targetType, element: targetElement } = target;\n    // 即将销毁或已销毁的元素不再触发事件\n    // Elements that are about to be destroyed or have been destroyed no longer trigger events\n    if ('destroyed' in targetElement && (isToBeDestroyed(targetElement) || targetElement.destroyed)) return;\n\n    const { type, detail, button } = event;\n    const stdEvent = { ...event, target: targetElement, targetType, originalTarget };\n\n    if (type === CommonEvent.POINTER_MOVE) {\n      if (this.currentTarget !== targetElement) {\n        if (this.currentTarget) {\n          graph.emit(`${this.currentTargetType}:${CommonEvent.POINTER_LEAVE}`, {\n            ...stdEvent,\n            type: CommonEvent.POINTER_LEAVE,\n            target: this.currentTarget,\n            targetType: this.currentTargetType,\n          });\n        }\n        if (targetElement) {\n          Object.assign(stdEvent, { type: CommonEvent.POINTER_ENTER });\n          graph.emit(`${targetType}:${CommonEvent.POINTER_ENTER}`, stdEvent);\n        }\n      }\n      this.currentTarget = targetElement;\n      this.currentTargetType = targetType;\n    }\n\n    // 非右键点击事件 / Click event except right click\n    if (!(type === CommonEvent.CLICK && button === 2)) {\n      graph.emit(`${targetType}:${type}`, stdEvent);\n      graph.emit(type, stdEvent);\n    }\n\n    // 双击事件 / Double click event\n    if (type === CommonEvent.CLICK && detail === 2) {\n      Object.assign(stdEvent, { type: CommonEvent.DBLCLICK });\n      graph.emit(`${targetType}:${CommonEvent.DBLCLICK}`, stdEvent);\n      graph.emit(CommonEvent.DBLCLICK, stdEvent);\n    }\n\n    // 右键菜单 / Contextmenu\n    if (type === CommonEvent.POINTER_DOWN && button === 2) {\n      Object.assign(stdEvent, {\n        type: CommonEvent.CONTEXT_MENU,\n        preventDefault: () => {\n          canvas.getContainer()?.addEventListener(CommonEvent.CONTEXT_MENU, (e) => e.preventDefault(), {\n            once: true,\n          });\n        },\n      });\n      graph.emit(`${targetType}:${CommonEvent.CONTEXT_MENU}`, stdEvent);\n      graph.emit(CommonEvent.CONTEXT_MENU, stdEvent);\n    }\n  };\n\n  private forwardContainerEvents = (event: FocusEvent | KeyboardEvent) => {\n    this.context.graph.emit(event.type, event);\n  };\n\n  public destroy(): void {\n    const container = this.context.canvas.getContainer();\n    if (container) {\n      [ContainerEvent.KEY_DOWN, ContainerEvent.KEY_UP].forEach((name) => {\n        container.removeEventListener(name, this.forwardContainerEvents);\n      });\n    }\n    this.context.canvas.document.removeAllEventListeners();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/runtime/canvas.ts",
    "content": "import type { Cursor, DisplayObject, CanvasConfig as GCanvasConfig, IChildNode } from '@antv/g';\nimport { CanvasEvent, Canvas as GCanvas } from '@antv/g';\nimport { Renderer as CanvasRenderer } from '@antv/g-canvas';\nimport { Plugin as DragNDropPlugin } from '@antv/g-plugin-dragndrop';\nimport { createDOM } from '@antv/util';\nimport type { CanvasOptions } from '../spec/canvas';\nimport type { CanvasLayer, Point } from '../types';\nimport { getBBoxSize, getCombinedBBox } from '../utils/bbox';\nimport { parsePoint, toPointObject } from '../utils/point';\n\nexport interface CanvasConfig extends Pick<\n  GCanvasConfig,\n  'container' | 'devicePixelRatio' | 'width' | 'height' | 'cursor' | 'background'\n> {\n  /**\n   * <zh/> 渲染器\n   *\n   * <en/> renderer\n   */\n  renderer?: CanvasOptions['renderer'];\n  /**\n   * <zh/> 是否启用多图层\n   *\n   * <en/> Whether to enable multiple layers\n   * @defaultValue true\n   * @remarks\n   * <zh/> 非动态参数，仅在初始化时生效\n   *\n   * <en/> Non-dynamic parameters, only take effect during initialization\n   */\n  enableMultiLayer?: boolean;\n}\n\nexport interface DataURLOptions {\n  /**\n   * <zh/> 导出模式\n   *  - viewport: 导出视口内容\n   *  - overall: 导出整个画布\n   *\n   * <en/> export mode\n   *  - viewport: export the content of the viewport\n   *  - overall: export the entire canvas\n   */\n  mode?: 'viewport' | 'overall';\n  /**\n   * <zh/> 图片类型\n   *\n   * <en/> image type\n   * @defaultValue 'image/png'\n   */\n  type: 'image/png' | 'image/jpeg' | 'image/webp' | 'image/bmp';\n  /**\n   * <zh/> 图片质量, 仅对 image/jpeg 和 image/webp 有效，取值范围 0 ~ 1\n   *\n   * <en/> image quality, only valid for image/jpeg and image/webp, range 0 ~ 1\n   */\n  encoderOptions: number;\n}\n\nconst SINGLE_LAYER_NAME: CanvasLayer[] = ['main'];\nconst MULTI_LAYER_NAME: CanvasLayer[] = ['background', 'main', 'label', 'transient'];\n\n/**\n * <zh/> 获取主画布图层\n *\n * <en/> Get the main canvas layer\n * @param layers - <zh/> 画布图层 | <en/> Canvas layer\n * @returns <zh/> 主画布图层 | <en/> Main canvas layer\n */\nfunction getMainLayerOf(layers: Record<CanvasLayer, GCanvas>) {\n  return layers.main;\n}\n\nexport class Canvas {\n  private extends: {\n    config: CanvasConfig;\n    renderer: CanvasOptions['renderer'];\n    renderers: Record<CanvasLayer, CanvasRenderer>;\n    layers: Record<CanvasLayer, GCanvas>;\n  };\n\n  private config: CanvasConfig = {\n    enableMultiLayer: true,\n  };\n\n  public getConfig() {\n    return this.config;\n  }\n\n  public getLayer(layer: CanvasLayer = 'main') {\n    return this.extends.layers[layer] || getMainLayerOf(this.getLayers());\n  }\n\n  /**\n   * <zh/> 获取所有图层\n   *\n   * <en/> Get all layers\n   * @returns <zh/> 图层 <en/> Layer\n   */\n  public getLayers() {\n    return this.extends.layers;\n  }\n\n  /**\n   * <zh/> 获取渲染器\n   *\n   * <en/> Get renderer\n   * @param layer - <zh/> 图层 <en/> Layer\n   * @returns <zh/> 渲染器 <en/> Renderer\n   */\n  public getRenderer(layer: CanvasLayer) {\n    return this.extends.renderers[layer];\n  }\n\n  /**\n   * <zh/> 获取相机\n   *\n   * <en/> Get camera\n   * @param layer - <zh/> 图层 <en/> Layer\n   * @returns <zh/> 相机 <en/> Camera\n   */\n  public getCamera(layer: CanvasLayer = 'main') {\n    return this.getLayer(layer).getCamera();\n  }\n\n  public getRoot(layer: CanvasLayer = 'main') {\n    return this.getLayer(layer).getRoot();\n  }\n\n  public getContextService(layer: CanvasLayer = 'main') {\n    return this.getLayer(layer).getContextService();\n  }\n\n  public setCursor(cursor: Cursor): void {\n    this.config.cursor = cursor;\n    this.getLayer().setCursor(cursor);\n  }\n\n  public get document() {\n    return this.getLayer().document;\n  }\n\n  public get context() {\n    return this.getLayer().context;\n  }\n\n  constructor(config: CanvasConfig) {\n    Object.assign(this.config, config);\n\n    const { renderer, background, cursor, enableMultiLayer, ...restConfig } = this.config;\n    const layersName = enableMultiLayer ? MULTI_LAYER_NAME : SINGLE_LAYER_NAME;\n    const renderers = createRenderers(renderer, layersName);\n    const layers = Object.fromEntries(\n      layersName.map((layer) => {\n        const canvas = new GCanvas({\n          ...restConfig,\n          supportsMutipleCanvasesInOneContainer: enableMultiLayer,\n          renderer: renderers[layer],\n          background: enableMultiLayer ? (layer === 'background' ? background : undefined) : background,\n        });\n\n        return [layer, canvas];\n      }),\n    ) as Record<CanvasLayer, GCanvas>;\n\n    configCanvasDom(layers);\n\n    this.extends = {\n      config: this.config,\n      renderer,\n      renderers,\n      layers,\n    };\n  }\n\n  public get ready() {\n    return Promise.all(Object.entries(this.getLayers()).map(([, canvas]) => canvas.ready));\n  }\n\n  public resize(width: number, height: number) {\n    Object.assign(this.extends.config, { width, height });\n    Object.values(this.getLayers()).forEach((canvas) => {\n      const camera = canvas.getCamera();\n      const position = camera.getPosition();\n      const focalPoint = camera.getFocalPoint();\n\n      canvas.resize(width, height);\n\n      camera.setPosition(position);\n      camera.setFocalPoint(focalPoint);\n    });\n  }\n\n  /**\n   * <zh/> 获取画布边界\n   *\n   * <en/> Get canvas boundary\n   * @param group\n   * <zh/> 元素分组\n   * - undefined: 获取整个画布边界\n   * - 'elements': 仅获取元素边界\n   * - 'plugins': 仅获取插件边界\n   *\n   * <en/> Element group\n   * - undefined: Get the entire canvas boundary\n   * - 'elements': Get only the element boundary\n   * - 'plugins': Get only the plugin boundary\n   * @returns <zh/> 边界 <en/> Boundary\n   */\n  public getBounds(group?: 'elements' | 'plugins') {\n    return getCombinedBBox(\n      Object.values(this.getLayers())\n        .map((canvas) => {\n          const g = group\n            ? (canvas\n                .getRoot()\n                .childNodes.find((node) => (node as DisplayObject).classList.includes(group)) as DisplayObject)\n            : canvas.getRoot();\n          return g;\n        })\n        .filter((el) => el?.childNodes.length > 0)\n        .map((el) => el.getBounds()),\n    );\n  }\n\n  public getContainer() {\n    const container = this.extends.config.container!;\n    return typeof container === 'string' ? document.getElementById(container!) : container;\n  }\n\n  public getSize(): [number, number] {\n    return [this.extends.config.width || 0, this.extends.config.height || 0];\n  }\n\n  public appendChild<T extends IChildNode>(child: T, index?: number): T {\n    const layer = ((child as unknown as DisplayObject).style?.$layer || 'main') as CanvasLayer;\n    return this.getLayer(layer).appendChild(child, index);\n  }\n\n  public setRenderer(renderer: CanvasOptions['renderer']) {\n    if (renderer === this.extends.renderer) return;\n    const renderers = createRenderers(renderer, this.config.enableMultiLayer ? MULTI_LAYER_NAME : SINGLE_LAYER_NAME);\n    this.extends.renderers = renderers;\n    Object.entries(renderers).forEach(([layer, instance]) => this.getLayer(layer as CanvasLayer).setRenderer(instance));\n    configCanvasDom(this.getLayers());\n  }\n\n  public getCanvasByViewport(point: Point): Point {\n    return parsePoint(this.getLayer().viewport2Canvas(toPointObject(point)));\n  }\n\n  public getViewportByCanvas(point: Point): Point {\n    return parsePoint(this.getLayer().canvas2Viewport(toPointObject(point)));\n  }\n\n  public getViewportByClient(point: Point): Point {\n    return parsePoint(this.getLayer().client2Viewport(toPointObject(point)));\n  }\n\n  public getClientByViewport(point: Point): Point {\n    return parsePoint(this.getLayer().viewport2Client(toPointObject(point)));\n  }\n\n  public getClientByCanvas(point: Point): Point {\n    return this.getClientByViewport(this.getViewportByCanvas(point));\n  }\n\n  public getCanvasByClient(point: Point): Point {\n    const main = this.getLayer();\n    const viewportPoint = main.client2Viewport(toPointObject(point));\n    return parsePoint(main.viewport2Canvas(viewportPoint));\n  }\n\n  public async toDataURL(options: Partial<DataURLOptions> = {}) {\n    const devicePixelRatio = globalThis.devicePixelRatio || 1;\n    const { mode = 'viewport', ...restOptions } = options;\n    let [startX, startY, width, height] = [0, 0, 0, 0];\n\n    if (mode === 'viewport') {\n      [width, height] = this.getSize();\n    } else if (mode === 'overall') {\n      const bounds = this.getBounds();\n      const size = getBBoxSize(bounds);\n      [startX, startY] = bounds.min;\n      [width, height] = size;\n    }\n\n    const container: HTMLElement = createDOM('<div id=\"virtual-image\"></div>');\n\n    const offscreenCanvas = new GCanvas({\n      width,\n      height,\n      renderer: new CanvasRenderer(),\n      devicePixelRatio,\n      container,\n      background: this.extends.config.background,\n    });\n\n    await offscreenCanvas.ready;\n\n    offscreenCanvas.appendChild(this.getLayer('background').getRoot().cloneNode(true));\n    offscreenCanvas.appendChild(this.getRoot().cloneNode(true));\n\n    // Handle label canvas\n    const label = this.getLayer('label').getRoot().cloneNode(true);\n    const originCanvasPosition = offscreenCanvas.viewport2Canvas({ x: 0, y: 0 });\n    const currentCanvasPosition = this.getCanvasByViewport([0, 0]);\n    label.translate([\n      currentCanvasPosition[0] - originCanvasPosition.x,\n      currentCanvasPosition[1] - originCanvasPosition.y,\n    ]);\n    label.scale(1 / this.getCamera().getZoom());\n    offscreenCanvas.appendChild(label);\n\n    offscreenCanvas.appendChild(this.getLayer('transient').getRoot().cloneNode(true));\n\n    const camera = this.getCamera();\n    const offscreenCamera = offscreenCanvas.getCamera();\n\n    if (mode === 'viewport') {\n      offscreenCamera.setZoom(camera.getZoom());\n      offscreenCamera.setPosition(camera.getPosition());\n      offscreenCamera.setFocalPoint(camera.getFocalPoint());\n    } else if (mode === 'overall') {\n      const [x, y, z] = offscreenCamera.getPosition();\n      const [fx, fy, fz] = offscreenCamera.getFocalPoint();\n      offscreenCamera.setPosition([x + startX, y + startY, z]);\n      offscreenCamera.setFocalPoint([fx + startX, fy + startY, fz]);\n    }\n\n    const contextService = offscreenCanvas.getContextService();\n\n    return new Promise<string>((resolve) => {\n      offscreenCanvas.addEventListener(CanvasEvent.RERENDER, async () => {\n        // 等待图片渲染完成 / Wait for the image to render\n        await new Promise((r) => setTimeout(r, 300));\n        const url = await contextService.toDataURL(restOptions);\n        resolve(url);\n      });\n    });\n  }\n\n  public destroy() {\n    Object.values(this.getLayers()).forEach((canvas) => {\n      const camera = canvas.getCamera();\n      camera.cancelLandmarkAnimation();\n      canvas.destroy();\n    });\n  }\n}\n\n/**\n * <zh/> 创建渲染器\n *\n * <en/> Create renderers\n * @param renderer - <zh/> 渲染器创建器 <en/> Renderer creator\n * @param layersName - <zh/> 图层名称 <en/> Layer name\n * @returns <zh/> 渲染器 <en/> Renderer\n */\nfunction createRenderers(renderer: CanvasConfig['renderer'], layersName: CanvasLayer[]) {\n  return Object.fromEntries(\n    layersName.map((layer) => {\n      const instance = renderer?.(layer) || new CanvasRenderer();\n\n      if (instance instanceof CanvasRenderer) {\n        instance.setConfig({ enableDirtyRectangleRendering: false });\n      }\n\n      if (layer === 'main') {\n        instance.registerPlugin(\n          new DragNDropPlugin({\n            isDocumentDraggable: true,\n            isDocumentDroppable: true,\n            dragstartDistanceThreshold: 10,\n            dragstartTimeThreshold: 100,\n          }),\n        );\n      } else {\n        instance.unregisterPlugin(instance.getPlugin('dom-interaction'));\n      }\n\n      return [layer, instance];\n    }),\n  ) as Record<CanvasLayer, CanvasRenderer>;\n}\n\n/**\n * <zh/> 配置画布 DOM\n *\n * <en/> Configure canvas DOM\n * @param layers - <zh/> 画布 <en/> Canvas\n */\nfunction configCanvasDom(layers: Record<CanvasLayer, GCanvas>) {\n  Object.entries(layers).forEach(([layer, canvas]) => {\n    const domElement = canvas.getContextService().getDomElement() as unknown as HTMLElement;\n\n    // 浏览器环境下，设置画布样式\n    // Set canvas style in browser environment\n    if (domElement?.style) {\n      domElement.style.gridArea = '1 / 1 / 2 / 2';\n      domElement.style.outline = 'none';\n      domElement.tabIndex = 1;\n\n      if (layer !== 'main') domElement.style.pointerEvents = 'none';\n    }\n\n    if (domElement?.parentElement) {\n      domElement.parentElement.style.display = 'grid';\n      // 给父元素设置独立的层叠上下文，避免外部元素影响内部的层叠逻辑\n      domElement.parentElement.style.isolation = 'isolate';\n    }\n  });\n}\n"
  },
  {
    "path": "packages/g6/src/runtime/data.ts",
    "content": "import { Graph as GraphLib } from '@antv/graphlib';\nimport { isNil, isNumber, uniq } from '@antv/util';\nimport { COMBO_KEY, ChangeType, TREE_KEY } from '../constants';\nimport type { ComboData, EdgeData, GraphData, NodeData } from '../spec';\nimport type {\n  DataAdded,\n  DataChange,\n  DataID,\n  DataRemoved,\n  DataUpdated,\n  ElementDatum,\n  HierarchyKey,\n  ID,\n  NodeLikeData,\n  PartialEdgeData,\n  PartialGraphData,\n  PartialNodeLikeData,\n  Point,\n  State,\n} from '../types';\nimport type { EdgeDirection } from '../types/edge';\nimport type { ElementType } from '../types/element';\nimport { isCollapsed } from '../utils/collapsibility';\nimport { cloneElementData, isElementDataEqual, mergeElementsData } from '../utils/data';\nimport { arrayDiff } from '../utils/diff';\nimport { toG6Data, toGraphlibData } from '../utils/graphlib';\nimport { idOf, parentIdOf } from '../utils/id';\nimport { positionOf } from '../utils/position';\nimport { format, print } from '../utils/print';\nimport { dfs } from '../utils/traverse';\nimport { add } from '../utils/vector';\n\nexport class DataController {\n  public model: GraphLib<NodeLikeData, EdgeData>;\n\n  /**\n   * <zh/> 最近一次删除的 combo 的 id\n   *\n   * <en/> The ids of the last deleted combos\n   * @remarks\n   * <zh/> 当删除 combo 后，会将其 id 从 comboIds 中移除，此时根据 Graphlib 的 changes 事件获取到的 NodeRemoved 无法区分是 combo 还是 node。\n   * 因此需要记录最近一次删除的 combo 的 id，并用于 isCombo 的判断\n   *\n   * <en/> When the combo is deleted, its id will be removed from comboIds. At this time, the NodeRemoved obtained according to the changes event of Graphlib cannot distinguish whether it is a combo or a node.\n   * Therefore, it is necessary to record the id of the last deleted combo and use it to judge isCombo\n   */\n  protected latestRemovedComboIds = new Set<ID>();\n\n  protected comboIds = new Set<ID>();\n\n  /**\n   * <zh/> 获取详细数据变更\n   *\n   * <en/> Get detailed data changes\n   */\n  private changes: DataChange[] = [];\n\n  /**\n   * <zh/> 批处理计数器\n   *\n   * <en/> Batch processing counter\n   */\n  private batchCount = 0;\n\n  /**\n   * <zh/> 是否处于无痕模式\n   *\n   * <en/> Whether it is in traceless mode\n   */\n  private isTraceless = false;\n\n  constructor() {\n    this.model = new GraphLib();\n  }\n\n  private pushChange(change: DataChange) {\n    if (this.isTraceless) return;\n    const { type } = change;\n\n    if (type === ChangeType.NodeUpdated || type === ChangeType.EdgeUpdated || type === ChangeType.ComboUpdated) {\n      const { value, original } = change;\n      this.changes.push({ value: cloneElementData(value), original: cloneElementData(original), type } as DataUpdated);\n    } else {\n      this.changes.push({ value: cloneElementData(change.value), type } as DataAdded | DataRemoved);\n    }\n  }\n\n  public getChanges(): DataChange[] {\n    return this.changes;\n  }\n\n  public clearChanges() {\n    this.changes = [];\n  }\n\n  public batch(callback: () => void) {\n    this.batchCount++;\n    this.model.batch(callback);\n    this.batchCount--;\n  }\n\n  protected isBatching() {\n    return this.batchCount > 0;\n  }\n\n  /**\n   * <zh/> 执行操作而不会留下记录\n   *\n   * <en/> Perform operations without leaving records\n   * @param callback - <zh/> 回调函数 | <en/> callback function\n   * @remarks\n   * <zh/> 通常用于运行时调整元素并同步数据，避免触发数据变更导致重绘\n   *\n   * <en/> Usually used to adjust elements at runtime and synchronize data to avoid triggering data changes and causing redraws\n   */\n  public silence(callback: () => void) {\n    this.isTraceless = true;\n    callback();\n    this.isTraceless = false;\n  }\n\n  public isCombo(id: ID) {\n    return this.comboIds.has(id) || this.latestRemovedComboIds.has(id);\n  }\n\n  public getData() {\n    return {\n      nodes: this.getNodeData(),\n      edges: this.getEdgeData(),\n      combos: this.getComboData(),\n    };\n  }\n\n  public getNodeData(ids?: ID[]) {\n    return this.model.getAllNodes().reduce((acc, node) => {\n      const data = toG6Data(node);\n      if (this.isCombo(idOf(data))) return acc;\n      if (ids === undefined) acc.push(data);\n      else ids.includes(idOf(data)) && acc.push(data);\n      return acc;\n    }, [] as NodeData[]);\n  }\n\n  public getEdgeDatum(id: ID) {\n    return toG6Data(this.model.getEdge(id));\n  }\n\n  public getEdgeData(ids?: ID[]) {\n    return this.model.getAllEdges().reduce((acc, edge) => {\n      const data = toG6Data(edge);\n      if (ids === undefined) acc.push(data);\n      else ids.includes(idOf(data)) && acc.push(data);\n      return acc;\n    }, [] as EdgeData[]);\n  }\n\n  public getComboData(ids?: ID[]) {\n    return this.model.getAllNodes().reduce((acc, combo) => {\n      const data = toG6Data(combo);\n      if (!this.isCombo(idOf(data))) return acc;\n\n      if (ids === undefined) acc.push(data as ComboData);\n      else ids.includes(idOf(data)) && acc.push(data as ComboData);\n      return acc;\n    }, [] as ComboData[]);\n  }\n\n  public getRootsData(hierarchyKey: HierarchyKey = TREE_KEY) {\n    return this.model.getRoots(hierarchyKey).map(toG6Data);\n  }\n\n  public getAncestorsData(id: ID, hierarchyKey: HierarchyKey): NodeLikeData[] {\n    const { model } = this;\n    if (!model.hasNode(id) || !model.hasTreeStructure(hierarchyKey)) return [];\n    return model.getAncestors(id, hierarchyKey).map(toG6Data);\n  }\n\n  public getDescendantsData(id: ID): NodeLikeData[] {\n    const root = this.getElementDataById(id) as NodeLikeData;\n    const data: NodeLikeData[] = [];\n    dfs(\n      root,\n      (node) => {\n        if (node !== root) data.push(node);\n      },\n      (node) => this.getChildrenData(idOf(node)),\n      'TB',\n    );\n    return data;\n  }\n\n  public getParentData(id: ID, hierarchyKey: HierarchyKey): NodeLikeData | undefined {\n    const { model } = this;\n    if (!hierarchyKey) {\n      print.warn('The hierarchy structure key is not specified');\n      return undefined;\n    }\n    if (!model.hasNode(id) || !model.hasTreeStructure(hierarchyKey)) return undefined;\n    const parent = model.getParent(id, hierarchyKey);\n    return parent ? toG6Data(parent) : undefined;\n  }\n\n  public getChildrenData(id: ID): NodeLikeData[] {\n    const structureKey = this.getElementType(id) === 'node' ? TREE_KEY : COMBO_KEY;\n    const { model } = this;\n    if (!model.hasNode(id) || !model.hasTreeStructure(structureKey)) return [];\n    return model.getChildren(id, structureKey).map(toG6Data);\n  }\n\n  /**\n   * <zh/> 获取指定类型元素的数据\n   *\n   * <en/> Get the data of the specified type of element\n   * @param elementType - <zh/> 元素类型 | <en/> element type\n   * @returns <zh/> 元素数据 | <en/> element data\n   */\n  public getElementsDataByType(elementType: ElementType) {\n    if (elementType === 'node') return this.getNodeData();\n    if (elementType === 'edge') return this.getEdgeData();\n    if (elementType === 'combo') return this.getComboData();\n    return [];\n  }\n\n  /**\n   * <zh/> 根据 ID 获取元素的数据，不用关心元素的类型\n   *\n   * <en/> Get the data of the element by ID, no need to care about the type of the element\n   * @param id - <zh/> 元素 ID 数组 | <en/> element ID array\n   * @returns <zh/> 元素数据 | <en/> data of the element\n   */\n  public getElementDataById(id: ID): ElementDatum {\n    const type = this.getElementType(id);\n    if (type === 'edge') return this.getEdgeDatum(id);\n    return this.getNodeLikeDatum(id);\n  }\n\n  /**\n   * <zh/> 获取节点的数据\n   *\n   * <en/> Get node data\n   * @param id - <zh/> 节点 ID | <en/> node ID\n   * @returns <zh/> 节点数据 | <en/> node data\n   */\n  public getNodeLikeDatum(id: ID) {\n    const data = this.model.getNode(id);\n    return toG6Data(data);\n  }\n\n  /**\n   * <zh/> 获取所有节点和 combo 的数据\n   *\n   * <en/> Get all node and combo data\n   * @param ids - <zh/> 节点和 combo ID 数组 | <en/> node and combo ID array\n   * @returns <zh/> 节点和 combo 的数据 | <en/> node and combo data\n   */\n  public getNodeLikeData(ids?: ID[]) {\n    return this.model.getAllNodes().reduce((acc, node) => {\n      const data = toG6Data(node);\n      if (ids) ids.includes(idOf(data)) && acc.push(data);\n      else acc.push(data);\n      return acc;\n    }, [] as NodeLikeData[]);\n  }\n\n  public getElementDataByState(elementType: ElementType, state: string) {\n    const elementData = this.getElementsDataByType(elementType);\n    return elementData.filter((datum) => datum.states?.includes(state));\n  }\n\n  public getElementState(id: ID): State[] {\n    return this.getElementDataById(id)?.states || [];\n  }\n\n  public hasNode(id: ID) {\n    return this.model.hasNode(id) && !this.isCombo(id);\n  }\n\n  public hasEdge(id: ID) {\n    return this.model.hasEdge(id);\n  }\n\n  public hasCombo(id: ID) {\n    return this.model.hasNode(id) && this.isCombo(id);\n  }\n\n  public getRelatedEdgesData(id: ID, direction: EdgeDirection = 'both') {\n    return this.model.getRelatedEdges(id, direction).map(toG6Data) as EdgeData[];\n  }\n\n  public getNeighborNodesData(id: ID) {\n    return this.model.getNeighbors(id).map(toG6Data);\n  }\n\n  public setData(data: GraphData) {\n    const { nodes: modifiedNodes = [], edges: modifiedEdges = [], combos: modifiedCombos = [] } = data;\n    const { nodes: originalNodes, edges: originalEdges, combos: originalCombos } = this.getData();\n\n    const nodeDiff = arrayDiff(originalNodes, modifiedNodes, (node) => idOf(node), isElementDataEqual);\n    const edgeDiff = arrayDiff(originalEdges, modifiedEdges, (edge) => idOf(edge), isElementDataEqual);\n    const comboDiff = arrayDiff(originalCombos, modifiedCombos, (combo) => idOf(combo), isElementDataEqual);\n\n    this.batch(() => {\n      const dataToAdd = {\n        nodes: nodeDiff.enter,\n        edges: edgeDiff.enter,\n        combos: comboDiff.enter,\n      };\n      this.addData(dataToAdd);\n      this.computeZIndex(dataToAdd, 'add', true);\n\n      const dataToUpdate = {\n        nodes: nodeDiff.update,\n        edges: edgeDiff.update,\n        combos: comboDiff.update,\n      };\n      this.updateData(dataToUpdate);\n      this.computeZIndex(dataToUpdate, 'update', true);\n\n      const dataToRemove = {\n        nodes: nodeDiff.exit.map(idOf),\n        edges: edgeDiff.exit.map(idOf),\n        combos: comboDiff.exit.map(idOf),\n      };\n      this.removeData(dataToRemove);\n    });\n  }\n\n  public addData(data: GraphData) {\n    const { nodes, edges, combos } = data;\n    this.batch(() => {\n      // add combo first\n      this.addComboData(combos);\n      this.addNodeData(nodes);\n      this.addEdgeData(edges);\n    });\n    this.computeZIndex(data, 'add');\n  }\n\n  public addNodeData(nodes: NodeData[] = []) {\n    if (!nodes.length) return;\n    this.model.addNodes(\n      nodes.map((node) => {\n        this.pushChange({ value: node, type: ChangeType.NodeAdded });\n        return toGraphlibData(node);\n      }),\n    );\n    this.updateNodeLikeHierarchy(nodes);\n\n    this.computeZIndex({ nodes }, 'add');\n  }\n\n  public addEdgeData(edges: EdgeData[] = []) {\n    if (!edges.length) return;\n    this.model.addEdges(\n      edges.map((edge) => {\n        this.pushChange({ value: edge, type: ChangeType.EdgeAdded });\n        return toGraphlibData(edge);\n      }),\n    );\n\n    this.computeZIndex({ edges }, 'add');\n  }\n\n  public addComboData(combos: ComboData[] = []) {\n    if (!combos.length) return;\n    const { model } = this;\n\n    if (!model.hasTreeStructure(COMBO_KEY)) {\n      model.attachTreeStructure(COMBO_KEY);\n    }\n\n    model.addNodes(\n      combos.map((combo) => {\n        this.comboIds.add(idOf(combo));\n        this.pushChange({ value: combo, type: ChangeType.ComboAdded });\n        return toGraphlibData(combo);\n      }),\n    );\n\n    this.updateNodeLikeHierarchy(combos);\n\n    this.computeZIndex({ combos }, 'add');\n  }\n\n  public addChildrenData(parentId: ID, childrenData: NodeData[]) {\n    const parentData = this.getNodeLikeDatum(parentId) as NodeData;\n    const childrenId = childrenData.map(idOf);\n    this.addNodeData(childrenData);\n    this.updateNodeData([{ id: parentId, children: [...(parentData.children || []), ...childrenId] }]);\n    this.addEdgeData(childrenId.map((childId) => ({ source: parentId, target: childId })));\n  }\n\n  /**\n   * <zh/> 计算 zIndex\n   *\n   * <en/> Calculate zIndex\n   * @param data - <zh/> 新增的数据 | <en/> newly added data\n   * @param type - <zh/> 操作类型 | <en/> operation type\n   * @param force - <zh/> 忽略批处理 | <en/> ignore batch processing\n   * @remarks\n   * <zh/> 调用该函数的情况：\n   * - 新增元素\n   * - 更新节点/组合的 combo\n   * - 更新节点的 children\n   *\n   * <en/> The situation of calling this function:\n   * - Add element\n   * - Update the combo of the node/combo\n   * - Update the children of the node\n   */\n  protected computeZIndex(data: PartialGraphData, type: 'add' | 'update', force = false) {\n    if (!force && this.isBatching()) return;\n    this.batch(() => {\n      const { nodes = [], edges = [], combos = [] } = data;\n\n      combos.forEach((combo) => {\n        const id = idOf(combo);\n        if (type === 'add' && isNumber(combo.style?.zIndex)) return;\n        if (type === 'update' && !('combo' in combo)) return;\n\n        const parent = this.getParentData(id, COMBO_KEY);\n        const zIndex = parent ? (parent.style?.zIndex ?? 0) + 1 : 0;\n\n        this.preventUpdateNodeLikeHierarchy(() => {\n          this.updateComboData([{ id, style: { zIndex } }]);\n        });\n      });\n\n      nodes.forEach((node) => {\n        const id = idOf(node);\n        if (type === 'add' && isNumber(node.style?.zIndex)) return;\n        if (type === 'update' && !('combo' in node) && !('children' in node)) return;\n\n        let zIndex = 0;\n\n        const comboParent = this.getParentData(id, COMBO_KEY);\n        if (comboParent) {\n          zIndex = (comboParent.style?.zIndex || 0) + 1;\n        } else {\n          const nodeParent = this.getParentData(id, TREE_KEY);\n          if (nodeParent) zIndex = nodeParent?.style?.zIndex || 0;\n        }\n\n        this.preventUpdateNodeLikeHierarchy(() => {\n          this.updateNodeData([{ id, style: { zIndex } }]);\n        });\n      });\n\n      edges.forEach((edge) => {\n        if (isNumber(edge.style?.zIndex)) return;\n\n        let { id, source, target } = edge;\n        if (!id) id = idOf(edge);\n        else {\n          const datum = this.getEdgeDatum(id);\n          source = datum.source;\n          target = datum.target;\n        }\n\n        if (!source || !target) return;\n\n        const sourceZIndex = this.getNodeLikeDatum(source)?.style?.zIndex || 0;\n        const targetZIndex = this.getNodeLikeDatum(target)?.style?.zIndex || 0;\n\n        this.updateEdgeData([{ id: idOf(edge), style: { zIndex: Math.max(sourceZIndex, targetZIndex) - 1 } }]);\n      });\n    });\n  }\n\n  /**\n   * <zh/> 计算元素置顶后的 zIndex\n   *\n   * <en/> Calculate the zIndex after the element is placed on top\n   * @param id - <zh/> 元素 ID | <en/> ID of the element\n   * @returns <zh/> zIndex | <en/> zIndex\n   */\n  public getFrontZIndex(id: ID) {\n    const elementType = this.getElementType(id);\n    const elementData = this.getElementDataById(id);\n    const data = this.getData();\n\n    // 排除当前元素 / Exclude the current element\n    Object.assign(data, {\n      [`${elementType}s`]: data[`${elementType}s`].filter((element) => idOf(element) !== id),\n    });\n\n    if (elementType === 'combo') {\n      // 如果 combo 展开，则排除 combo 的子节点/combo 及内部边\n      // If the combo is expanded, exclude the child nodes/combos of the combo and the internal edges\n      if (!isCollapsed(elementData as ComboData)) {\n        const ancestorIds = new Set(this.getAncestorsData(id, COMBO_KEY).map(idOf));\n        data.nodes = data.nodes.filter((element) => !ancestorIds.has(idOf(element)));\n        data.combos = data.combos.filter((element) => !ancestorIds.has(idOf(element)));\n        data.edges = data.edges.filter(({ source, target }) => !ancestorIds.has(source) && !ancestorIds.has(target));\n      }\n    }\n\n    return Math.max(\n      elementData.style?.zIndex || 0,\n      0,\n      ...Object.values(data)\n        .flat()\n        .map((datum) => (datum?.style?.zIndex || 0) + 1),\n    );\n  }\n\n  protected updateNodeLikeHierarchy(data: NodeLikeData[]) {\n    if (!this.enableUpdateNodeLikeHierarchy) return;\n    const { model } = this;\n\n    data.forEach((datum) => {\n      const id = idOf(datum);\n      const parent = parentIdOf(datum);\n\n      if (parent !== undefined) {\n        if (!model.hasTreeStructure(COMBO_KEY)) model.attachTreeStructure(COMBO_KEY);\n\n        // 解除原父节点的子节点关系，更新原父节点及其祖先的数据\n        // Remove the child relationship of the original parent node, update the data of the original parent node and its ancestors\n        if (parent === null) {\n          this.refreshComboData(id);\n        }\n\n        this.setParent(id, parentIdOf(datum), COMBO_KEY);\n      }\n\n      const children = (datum as NodeData).children || [];\n      if (children.length) {\n        if (!model.hasTreeStructure(TREE_KEY)) model.attachTreeStructure(TREE_KEY);\n        const _children = children.filter((child) => model.hasNode(child));\n        _children.forEach((child) => this.setParent(child, id, TREE_KEY));\n        if (_children.length !== children.length) {\n          // 从数据中移除不存在的子节点\n          // Remove non-existent child nodes from the data\n          this.updateNodeData([{ id, children: _children }]);\n        }\n      }\n    });\n  }\n\n  private enableUpdateNodeLikeHierarchy = true;\n\n  /**\n   * <zh/> 执行变更时不要更新节点层次结构\n   *\n   * <en/> Do not update the node hierarchy when executing changes\n   * @param callback - <zh/> 变更函数 | <en/> change function\n   */\n  public preventUpdateNodeLikeHierarchy(callback: () => void) {\n    this.enableUpdateNodeLikeHierarchy = false;\n    callback();\n    this.enableUpdateNodeLikeHierarchy = true;\n  }\n\n  public updateData(data: PartialGraphData) {\n    const { nodes, edges, combos } = data;\n    this.batch(() => {\n      this.updateNodeData(nodes);\n      this.updateComboData(combos);\n      this.updateEdgeData(edges);\n    });\n    this.computeZIndex(data, 'update');\n  }\n\n  public updateNodeData(nodes: PartialNodeLikeData<NodeData>[] = []) {\n    if (!nodes.length) return;\n    const { model } = this;\n    this.batch(() => {\n      const modifiedNodes: NodeData[] = [];\n      nodes.forEach((modifiedNode) => {\n        const id = idOf(modifiedNode);\n        const originalNode = toG6Data(model.getNode(id));\n        if (isElementDataEqual(originalNode, modifiedNode)) return;\n\n        const value = mergeElementsData(originalNode, modifiedNode);\n        this.pushChange({ value, original: originalNode, type: ChangeType.NodeUpdated });\n        model.mergeNodeData(id, value);\n        modifiedNodes.push(value);\n      });\n\n      this.updateNodeLikeHierarchy(modifiedNodes);\n    });\n\n    this.computeZIndex({ nodes }, 'update');\n  }\n\n  /**\n   * <zh/> 将所有数据提交到变更记录中以进行重绘\n   *\n   * <en/> Submit all data to the change record for redrawing\n   */\n  public refreshData() {\n    const { nodes, edges, combos } = this.getData();\n    nodes.forEach((node) => {\n      this.pushChange({ value: node, original: node, type: ChangeType.NodeUpdated });\n    });\n    edges.forEach((edge) => {\n      this.pushChange({ value: edge, original: edge, type: ChangeType.EdgeUpdated });\n    });\n    combos.forEach((combo) => {\n      this.pushChange({ value: combo, original: combo, type: ChangeType.ComboUpdated });\n    });\n  }\n\n  public syncNodeLikeDatum(datum: PartialNodeLikeData<NodeData>) {\n    const { model } = this;\n\n    const id = idOf(datum);\n    if (!model.hasNode(id)) return;\n    const original = toG6Data(model.getNode(id));\n    const value = mergeElementsData(original, datum);\n    model.mergeNodeData(id, value);\n  }\n\n  public syncEdgeDatum(datum: PartialEdgeData<EdgeData>) {\n    const { model } = this;\n\n    const id = idOf(datum);\n    if (!model.hasEdge(id)) return;\n    const original = toG6Data(model.getEdge(id));\n    const value = mergeElementsData(original, datum);\n    model.mergeEdgeData(id, value);\n  }\n\n  public updateEdgeData(edges: PartialEdgeData<EdgeData>[] = []) {\n    if (!edges.length) return;\n    const { model } = this;\n    this.batch(() => {\n      edges.forEach((modifiedEdge) => {\n        const id = idOf(modifiedEdge);\n        const originalEdge = toG6Data(model.getEdge(id));\n        if (isElementDataEqual(originalEdge, modifiedEdge)) return;\n\n        if (modifiedEdge.source && originalEdge.source !== modifiedEdge.source) {\n          model.updateEdgeSource(id, modifiedEdge.source);\n        }\n        if (modifiedEdge.target && originalEdge.target !== modifiedEdge.target) {\n          model.updateEdgeTarget(id, modifiedEdge.target);\n        }\n        const updatedData = mergeElementsData(originalEdge, modifiedEdge);\n        this.pushChange({ value: updatedData, original: originalEdge, type: ChangeType.EdgeUpdated });\n        model.mergeEdgeData(id, updatedData);\n      });\n    });\n\n    this.computeZIndex({ edges }, 'update');\n  }\n\n  public updateComboData(combos: PartialNodeLikeData<ComboData>[] = []) {\n    if (!combos.length) return;\n    const { model } = this;\n    model.batch(() => {\n      const modifiedCombos: ComboData[] = [];\n      combos.forEach((modifiedCombo) => {\n        const id = idOf(modifiedCombo);\n        const originalCombo = toG6Data(model.getNode(id)) as ComboData;\n        if (isElementDataEqual(originalCombo, modifiedCombo)) return;\n\n        const value = mergeElementsData(originalCombo, modifiedCombo);\n        this.pushChange({ value, original: originalCombo, type: ChangeType.ComboUpdated });\n        model.mergeNodeData(id, value);\n        modifiedCombos.push(value);\n      });\n\n      this.updateNodeLikeHierarchy(modifiedCombos);\n    });\n\n    this.computeZIndex({ combos }, 'update');\n  }\n\n  /**\n   * <zh/> 设置节点的父节点\n   *\n   * <en/> Set the parent node of the node\n   * @param id - <zh/> 节点 ID | <en/> node ID\n   * @param parent - <zh/> 父节点 ID | <en/> parent node ID\n   * @param hierarchyKey - <zh/> 层次结构类型 | <en/> hierarchy type\n   * @param update - <zh/> 添加新/旧父节点数据更新记录 | <en/> add new/old parent node data update record\n   */\n  public setParent(id: ID, parent: ID | undefined | null, hierarchyKey: HierarchyKey, update: boolean = true) {\n    if (id === parent) return;\n    const elementData = this.getNodeLikeDatum(id);\n    const originalParentId = parentIdOf(elementData);\n\n    if (originalParentId !== parent && hierarchyKey === COMBO_KEY) {\n      const modifiedDatum = { id, combo: parent };\n      if (this.isCombo(id)) this.syncNodeLikeDatum(modifiedDatum);\n      else this.syncNodeLikeDatum(modifiedDatum);\n    }\n\n    this.model.setParent(id, parent, hierarchyKey);\n\n    if (update && hierarchyKey === COMBO_KEY) {\n      uniq([originalParentId, parent]).forEach((pId) => {\n        if (pId !== undefined) this.refreshComboData(pId);\n      });\n    }\n  }\n\n  /**\n   * <zh/> 刷新 combo 数据\n   *\n   * <en/> Refresh combo data\n   * @param id - <zh/> combo ID | <en/> combo ID\n   * @remarks\n   * <zh/> 不会更改数据，但会触发数据变更事件\n   *\n   * <en/> Will not change the data, but will trigger data change events\n   */\n  public refreshComboData(id: ID) {\n    const combo = this.getComboData([id])[0];\n    const ancestors = this.getAncestorsData(id, COMBO_KEY) as ComboData[];\n\n    if (combo) this.pushChange({ value: combo, original: combo, type: ChangeType.ComboUpdated });\n\n    ancestors.forEach((value) => {\n      this.pushChange({ value: value, original: value, type: ChangeType.ComboUpdated });\n    });\n  }\n\n  public getElementPosition(id: ID): Point {\n    const datum = this.getElementDataById(id) as NodeLikeData;\n    return positionOf(datum);\n  }\n\n  public translateNodeLikeBy(id: ID, offset: Point) {\n    if (this.isCombo(id)) this.translateComboBy(id, offset);\n    else this.translateNodeBy(id, offset);\n  }\n\n  public translateNodeLikeTo(id: ID, position: Point) {\n    if (this.isCombo(id)) this.translateComboTo(id, position);\n    else this.translateNodeTo(id, position);\n  }\n\n  public translateNodeBy(id: ID, offset: Point) {\n    const curr = this.getElementPosition(id);\n    const position = add(curr, [...offset, 0].slice(0, 3) as Point);\n    this.translateNodeTo(id, position);\n  }\n\n  public translateNodeTo(id: ID, position: Point) {\n    const [x = 0, y = 0, z = 0] = position;\n    this.preventUpdateNodeLikeHierarchy(() => {\n      this.updateNodeData([{ id, style: { x, y, z } }]);\n    });\n  }\n\n  public translateComboBy(id: ID, offset: Point) {\n    const [dx = 0, dy = 0, dz = 0] = offset;\n    if ([dx, dy, dz].some(isNaN) || [dx, dy, dz].every((o) => o === 0)) return;\n    const combo = this.getComboData([id])[0];\n    if (!combo) return;\n    const seenNodeLikeIds = new Set<ID>();\n    dfs<NodeLikeData>(\n      combo,\n      (succeed) => {\n        const succeedID = idOf(succeed);\n        if (seenNodeLikeIds.has(succeedID)) return;\n        seenNodeLikeIds.add(succeedID);\n        const [x, y, z] = positionOf(succeed);\n        const value = mergeElementsData(succeed, {\n          style: { x: x + dx, y: y + dy, z: z + dz },\n        });\n        this.pushChange({\n          value,\n          // @ts-ignore\n          original: succeed,\n          type: this.isCombo(succeedID) ? ChangeType.ComboUpdated : ChangeType.NodeUpdated,\n        });\n\n        this.model.mergeNodeData(succeedID, value);\n      },\n      (node) => this.getChildrenData(idOf(node)),\n      'BT',\n    );\n  }\n\n  public translateComboTo(id: ID, position: Point) {\n    if (position.some(isNaN)) return;\n    const [tx = 0, ty = 0, tz = 0] = position;\n    const combo = this.getComboData([id])?.[0];\n    if (!combo) return;\n\n    const [comboX, comboY, comboZ] = positionOf(combo);\n    const dx = tx - comboX;\n    const dy = ty - comboY;\n    const dz = tz - comboZ;\n\n    dfs<NodeLikeData>(\n      combo,\n      (succeed) => {\n        const succeedId = idOf(succeed);\n        const [x, y, z] = positionOf(succeed);\n        const value = mergeElementsData(succeed, {\n          style: { x: x + dx, y: y + dy, z: z + dz },\n        });\n        this.pushChange({\n          value,\n          // @ts-ignore\n          original: succeed,\n          type: this.isCombo(succeedId) ? ChangeType.ComboUpdated : ChangeType.NodeUpdated,\n        });\n        this.model.mergeNodeData(succeedId, value);\n      },\n      (node) => this.getChildrenData(idOf(node)),\n      'BT',\n    );\n  }\n\n  public removeData(data: DataID) {\n    const { nodes, edges, combos } = data;\n    this.batch(() => {\n      // remove edges first\n      this.removeEdgeData(edges);\n      this.removeNodeData(nodes);\n      this.removeComboData(combos);\n\n      this.latestRemovedComboIds = new Set(combos);\n    });\n  }\n\n  public removeNodeData(ids: ID[] = []) {\n    if (!ids.length) return;\n    this.batch(() => {\n      ids.forEach((id) => {\n        // 移除关联边、子节点\n        // remove related edges and child nodes\n        this.removeEdgeData(this.getRelatedEdgesData(id).map(idOf));\n        // TODO 树图情况下移除子节点\n\n        this.pushChange({ value: this.getNodeData([id])[0], type: ChangeType.NodeRemoved });\n        this.removeNodeLikeHierarchy(id);\n      });\n      this.model.removeNodes(ids);\n    });\n  }\n\n  public removeEdgeData(ids: ID[] = []) {\n    if (!ids.length) return;\n    ids.forEach((id) => this.pushChange({ value: this.getEdgeData([id])[0], type: ChangeType.EdgeRemoved }));\n    this.model.removeEdges(ids);\n  }\n\n  public removeComboData(ids: ID[] = []) {\n    if (!ids.length) return;\n    this.batch(() => {\n      ids.forEach((id) => {\n        this.pushChange({ value: this.getComboData([id])[0], type: ChangeType.ComboRemoved });\n        this.removeNodeLikeHierarchy(id);\n        this.comboIds.delete(id);\n      });\n      this.model.removeNodes(ids);\n    });\n  }\n\n  /**\n   * <zh/> 移除节点层次结构，将其子节点移动到父节点的 children 列表中\n   *\n   * <en/> Remove the node hierarchy and move its child nodes to the parent node's children list\n   * @param id - <zh/> 待处理的节点 | <en/> node to be processed\n   */\n  protected removeNodeLikeHierarchy(id: ID) {\n    if (this.model.hasTreeStructure(COMBO_KEY)) {\n      const grandParent = parentIdOf(this.getNodeLikeDatum(id));\n\n      // 从父节点的 children 列表中移除\n      // remove from its parent's children list\n      // 调用 graphlib.setParent，不需要更新数据\n      this.setParent(id, undefined, COMBO_KEY, false);\n      // 将子节点移动到父节点的 children 列表中\n      // move the children to the grandparent's children list\n\n      this.model.getChildren(id, COMBO_KEY).forEach((child) => {\n        const childData = toG6Data(child);\n        const childId = idOf(childData);\n        this.setParent(idOf(childData), grandParent, COMBO_KEY, false);\n        const value = mergeElementsData(childData, {\n          id: idOf(childData),\n          combo: grandParent,\n        });\n        this.pushChange({\n          value,\n          original: childData,\n          type: this.isCombo(childId) ? ChangeType.ComboUpdated : ChangeType.NodeUpdated,\n        });\n        this.model.mergeNodeData(idOf(childData), value);\n      });\n\n      if (!isNil(grandParent)) this.refreshComboData(grandParent);\n    }\n  }\n\n  /**\n   * <zh/> 获取元素的类型\n   *\n   * <en/> Get the type of the element\n   * @param id - <zh/> 元素 ID | <en/> ID of the element\n   * @returns <zh/> 元素类型 | <en/> type of the element\n   */\n  public getElementType(id: ID): ElementType {\n    if (this.model.hasNode(id)) {\n      if (this.isCombo(id)) return 'combo';\n      return 'node';\n    }\n\n    if (this.model.hasEdge(id)) return 'edge';\n\n    throw new Error(format(`Unknown element type of id: ${id}`));\n  }\n\n  public destroy() {\n    const { model } = this;\n    const nodes = model.getAllNodes();\n    const edges = model.getAllEdges();\n\n    model.removeEdges(edges.map((edge) => edge.id));\n    model.removeNodes(nodes.map((node) => node.id));\n\n    // @ts-expect-error force delete\n    this.context = {};\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/runtime/element.ts",
    "content": "/* eslint-disable jsdoc/require-returns */\n/* eslint-disable jsdoc/require-param */\nimport type { BaseStyleProps } from '@antv/g';\nimport { Group } from '@antv/g';\nimport { groupBy } from '@antv/util';\nimport { AnimationType, COMBO_KEY, ChangeType, GraphEvent } from '../constants';\nimport { ELEMENT_TYPES } from '../constants/element';\nimport { getExtension } from '../registry/get';\nimport type { ComboData, EdgeData, GraphData, LayoutOptions, NodeData } from '../spec';\nimport type { AnimationStage } from '../spec/element/animation';\nimport type { DrawData, ProcedureData } from '../transforms/types';\nimport type {\n  Combo,\n  DataChange,\n  Edge,\n  Element,\n  ElementData,\n  ElementDatum,\n  ElementType,\n  ID,\n  Node,\n  NodeLikeData,\n  State,\n  StyleIterationContext,\n} from '../types';\nimport { cacheStyle, hasCachedStyle } from '../utils/cache';\nimport { reduceDataChanges } from '../utils/change';\nimport { isCollapsed } from '../utils/collapsibility';\nimport { isOverridable } from '../utils/data';\nimport { markToBeDestroyed, updateStyle } from '../utils/element';\nimport type { BaseEvent } from '../utils/event';\nimport { AnimateEvent, ElementLifeCycleEvent, GraphLifeCycleEvent, emit } from '../utils/event';\nimport { idOf } from '../utils/id';\nimport { assignColorByPalette, parsePalette } from '../utils/palette';\nimport { positionOf } from '../utils/position';\nimport { print } from '../utils/print';\nimport { computeElementCallbackStyle } from '../utils/style';\nimport { themeOf } from '../utils/theme';\nimport { subtract } from '../utils/vector';\nimport { setVisibility } from '../utils/visibility';\nimport type { RuntimeContext } from './types';\n\nexport class ElementController {\n  private context: RuntimeContext;\n\n  private container!: Group;\n\n  private elementMap: Record<ID, Element> = {};\n\n  private shapeTypeMap: Record<ID, string> = {};\n\n  constructor(context: RuntimeContext) {\n    this.context = context;\n  }\n\n  public init() {\n    this.initContainer();\n  }\n\n  private initContainer() {\n    if (!this.container || this.container.destroyed) {\n      const { canvas } = this.context;\n      this.container = canvas.appendChild(new Group({ className: 'elements' }));\n    }\n  }\n\n  private emit(event: BaseEvent, context: DrawContext) {\n    if (context.silence) return;\n    emit(this.context.graph, event);\n  }\n\n  private forEachElementData(callback: (elementType: ElementType, elementData: ElementData) => void) {\n    ELEMENT_TYPES.forEach((elementType) => {\n      const elementData = this.context.model.getElementsDataByType(elementType);\n      callback(elementType, elementData);\n    });\n  }\n\n  public getElementType(elementType: ElementType, datum: ElementDatum) {\n    const { options, graph } = this.context;\n    const userDefinedType = isOverridable(datum) ? options[elementType]?.type || datum.type : datum.type;\n\n    if (!userDefinedType) {\n      if (elementType === 'edge') return 'line';\n      // node / combo\n      else return 'circle';\n    }\n    if (typeof userDefinedType === 'string') return userDefinedType;\n    // @ts-expect-error skip type check\n    return userDefinedType.call(graph, datum);\n  }\n\n  private getTheme(elementType: ElementType) {\n    return themeOf(this.context.options)[elementType] || {};\n  }\n\n  public getThemeStyle(elementType: ElementType) {\n    return this.getTheme(elementType).style || {};\n  }\n\n  public getThemeStateStyle(elementType: ElementType, states: State[]) {\n    const { state = {} } = this.getTheme(elementType);\n    return Object.assign({}, ...states.map((name) => state[name] || {}));\n  }\n\n  private paletteStyle: Record<ID, string> = {};\n\n  private computePaletteStyle() {\n    const { options } = this.context;\n\n    this.paletteStyle = {};\n\n    this.forEachElementData((elementType, elementData) => {\n      const palette = Object.assign(\n        {},\n        parsePalette(this.getTheme(elementType)?.palette),\n        parsePalette(options[elementType]?.palette),\n      );\n      if (palette?.field) {\n        Object.assign(this.paletteStyle, assignColorByPalette(elementData, palette));\n      }\n    });\n  }\n\n  public getPaletteStyle(elementType: ElementType, id: ID): BaseStyleProps {\n    const color = this.paletteStyle[id];\n    if (!color) return {};\n\n    if (elementType === 'edge') return { stroke: color };\n    return { fill: color };\n  }\n\n  private defaultStyle: Record<ID, Record<string, unknown>> = {};\n\n  /**\n   * <zh/> 计算单个元素的默认样式\n   *\n   * <en/> compute default style of single element\n   */\n  private computeElementDefaultStyle(elementType: ElementType, context: StyleIterationContext) {\n    const { options } = this.context;\n    const defaultStyle = options[elementType]?.style || {};\n    if ('transform' in defaultStyle && Array.isArray(defaultStyle.transform)) {\n      defaultStyle.transform = [...defaultStyle.transform];\n    }\n    this.defaultStyle[idOf(context.datum)] = computeElementCallbackStyle(defaultStyle as any, context);\n  }\n\n  private computeElementsDefaultStyle(ids?: ID[]) {\n    const { graph } = this.context;\n    this.forEachElementData((elementType, elementData) => {\n      const length = elementData.length;\n      for (let i = 0; i < length; i++) {\n        const datum = elementData[i];\n        if (ids === undefined || ids.includes(idOf(datum))) {\n          this.computeElementDefaultStyle(elementType, { datum, graph });\n        }\n      }\n    });\n  }\n\n  public getDefaultStyle(id: ID) {\n    return this.defaultStyle[id] || {};\n  }\n\n  private getElementState(id: ID) {\n    try {\n      const { model } = this.context;\n      return model.getElementState(id);\n    } catch {\n      return [];\n    }\n  }\n\n  private stateStyle: Record<ID, Record<string, unknown>> = {};\n\n  /**\n   * <zh/> 获取单个元素的单个状态的样式\n   *\n   * <en/> get single state style of single element\n   */\n  private getElementStateStyle(elementType: ElementType, state: State, context: StyleIterationContext) {\n    const { options } = this.context;\n    const stateStyle = options[elementType]?.state?.[state] || {};\n    return computeElementCallbackStyle(stateStyle as any, context);\n  }\n\n  /**\n   * <zh/> 计算单个元素的合并状态样式\n   *\n   * <en/> compute merged state style of single element\n   */\n  private computeElementStatesStyle(elementType: ElementType, states: State[], context: StyleIterationContext) {\n    this.stateStyle[idOf(context.datum)] = Object.assign(\n      {},\n      ...states.map((state) => this.getElementStateStyle(elementType, state, context)),\n    );\n  }\n\n  /**\n   * <zh/> 计算全部元素的状态样式\n   *\n   * <en/> compute state style of all elements\n   * @param ids - <zh/> 计算指定元素的状态样式 | <en/> compute state style of specified elements\n   */\n  private computeElementsStatesStyle(ids?: ID[]) {\n    const { graph } = this.context;\n    this.forEachElementData((elementType, elementData) => {\n      const length = elementData.length;\n      for (let i = 0; i < length; i++) {\n        const datum = elementData[i];\n        if (ids === undefined || ids.includes(idOf(datum))) {\n          const states = this.getElementState(idOf(datum));\n          this.computeElementStatesStyle(elementType, states, { datum, graph });\n        }\n      }\n    });\n  }\n\n  public getStateStyle(id: ID) {\n    return this.stateStyle[id] || {};\n  }\n\n  private computeStyle(stage?: string, ids?: ID[]) {\n    const skip = ['translate', 'zIndex'];\n    if (stage && skip.includes(stage)) return;\n\n    this.computePaletteStyle();\n    this.computeElementsDefaultStyle(ids);\n    this.computeElementsStatesStyle(ids);\n  }\n\n  public getElement<T extends Element>(id: ID): T | undefined {\n    return this.elementMap[id] as T;\n  }\n\n  public getNodes() {\n    return this.context.model.getNodeData().map(({ id }) => this.elementMap[id]) as Node[];\n  }\n\n  public getEdges() {\n    return this.context.model.getEdgeData().map((edge) => this.elementMap[idOf(edge)]) as Edge[];\n  }\n\n  public getCombos() {\n    return this.context.model.getComboData().map(({ id }) => this.elementMap[id]) as Combo[];\n  }\n\n  public getElementComputedStyle(elementType: ElementType, datum: ElementDatum) {\n    const id = idOf(datum);\n    // 优先级(从低到高) Priority (from low to high):\n    const themeStyle = this.getThemeStyle(elementType);\n    const paletteStyle = this.getPaletteStyle(elementType, id);\n    const dataStyle = datum.style || {};\n    const defaultStyle = this.getDefaultStyle(id);\n    const themeStateStyle = this.getThemeStateStyle(elementType, this.getElementState(id));\n    const stateStyle = this.getStateStyle(id);\n\n    const style = isOverridable(datum)\n      ? Object.assign({}, themeStyle, paletteStyle, dataStyle, defaultStyle, themeStateStyle, stateStyle)\n      : Object.assign({}, dataStyle);\n\n    if (elementType === 'combo') {\n      const childrenData = this.context.model.getChildrenData(id);\n      const isCollapsed = !!style.collapsed;\n      const childrenNode = isCollapsed ? [] : childrenData.map(idOf).filter((id) => this.getElement(id));\n      Object.assign(style, { childrenNode, childrenData });\n    }\n    return style;\n  }\n\n  private getDrawData(context: DrawContext): DrawPayload | null {\n    this.init();\n\n    const data = this.computeChangesAndDrawData(context);\n    if (!data) return null;\n\n    const { type = 'draw', stage = type } = context;\n    this.markDestroyElement(data.drawData);\n    // 计算样式 / Calculate style\n    this.computeStyle(stage);\n\n    return { type, stage, data };\n  }\n\n  /**\n   * <zh/> 开始绘制流程\n   *\n   * <en/> start render process\n   */\n  public draw(context: DrawContext = { animation: true }) {\n    const drawData = this.getDrawData(context);\n    if (!drawData) return;\n\n    const {\n      data: {\n        drawData: { add, update, remove },\n      },\n    } = drawData;\n\n    this.destroyElements(remove, context);\n    this.createElements(add, context);\n    this.updateElements(update, context);\n\n    return this.setAnimationTask(context, drawData);\n  }\n\n  public async preLayoutDraw(context: DrawContext = { animation: true }) {\n    const preResult = this.getDrawData(context);\n    if (!preResult) return;\n\n    const {\n      data: { drawData },\n    } = preResult;\n\n    await this.context.layout?.preLayout?.(drawData);\n\n    const { add, update, remove } = drawData;\n    this.destroyElements(remove, context);\n    this.createElements(add, context);\n    this.updateElements(update, context);\n\n    return this.setAnimationTask(context, preResult);\n  }\n\n  private setAnimationTask(context: DrawContext, data: DrawPayload) {\n    const { animation, silence } = context;\n    const {\n      data: { dataChanges, drawData },\n      stage,\n      type,\n    } = data;\n\n    return this.context.animation!.animate(\n      animation,\n      silence\n        ? {}\n        : {\n            before: () =>\n              this.emit(\n                new GraphLifeCycleEvent(GraphEvent.BEFORE_DRAW, {\n                  dataChanges,\n                  animation,\n                  stage,\n                  render: type === 'render',\n                }),\n                context,\n              ),\n            beforeAnimate: (animation) =>\n              this.emit(new AnimateEvent(GraphEvent.BEFORE_ANIMATE, AnimationType.DRAW, animation, drawData), context),\n            afterAnimate: (animation) =>\n              this.emit(new AnimateEvent(GraphEvent.AFTER_ANIMATE, AnimationType.DRAW, animation, drawData), context),\n            after: () =>\n              this.emit(\n                new GraphLifeCycleEvent(GraphEvent.AFTER_DRAW, {\n                  dataChanges,\n                  animation,\n                  stage,\n                  render: type === 'render',\n                  firstRender: this.context.graph.rendered === false,\n                }),\n                context,\n              ),\n          },\n    );\n  }\n\n  private computeChangesAndDrawData(context: DrawContext) {\n    const { model } = this.context;\n    const dataChanges = model.getChanges();\n    const tasks = reduceDataChanges(dataChanges);\n    if (tasks.length === 0) return null;\n\n    const {\n      NodeAdded = [],\n      NodeUpdated = [],\n      NodeRemoved = [],\n      EdgeAdded = [],\n      EdgeUpdated = [],\n      EdgeRemoved = [],\n      ComboAdded = [],\n      ComboUpdated = [],\n      ComboRemoved = [],\n    } = groupBy(tasks, (change) => change.type) as unknown as Record<`${ChangeType}`, DataChange[]>;\n\n    const moveToAddedIfUnrendered = (updated: DataChange[], added: DataChange[]) => {\n      const keptUpdates: DataChange[] = [];\n      updated.forEach((change) => {\n        const id = idOf(change.value);\n        if (!this.getElement(id)) {\n          added.push(change);\n        } else {\n          keptUpdates.push(change);\n        }\n      });\n      return keptUpdates;\n    };\n\n    const finalNodeUpdated = moveToAddedIfUnrendered(NodeUpdated, NodeAdded);\n    const finalEdgeUpdated = moveToAddedIfUnrendered(EdgeUpdated, EdgeAdded);\n    const finalComboUpdated = moveToAddedIfUnrendered(ComboUpdated, ComboAdded);\n\n    const dataOf = <T extends DataChange['value']>(data: DataChange[]) =>\n      new Map(\n        data.map((datum) => {\n          const data = datum.value;\n          return [idOf(data), data] as [ID, T];\n        }),\n      );\n\n    const input: DrawData = {\n      add: {\n        nodes: dataOf<NodeData>(NodeAdded),\n        edges: dataOf<EdgeData>(EdgeAdded),\n        combos: dataOf<ComboData>(ComboAdded),\n      },\n      update: {\n        nodes: dataOf<NodeData>(finalNodeUpdated),\n        edges: dataOf<EdgeData>(finalEdgeUpdated),\n        combos: dataOf<ComboData>(finalComboUpdated),\n      },\n      remove: {\n        nodes: dataOf<NodeData>(NodeRemoved),\n        edges: dataOf<EdgeData>(EdgeRemoved),\n        combos: dataOf<ComboData>(ComboRemoved),\n      },\n    };\n    const drawData = this.transformData(input, context);\n\n    // 清空变更 / Clear changes\n    model.clearChanges();\n\n    return { dataChanges, drawData };\n  }\n\n  private transformData(input: DrawData, context: DrawContext): DrawData {\n    const transforms = this.context.transform.getTransformInstance();\n\n    return Object.values(transforms).reduce((data, transform) => transform.beforeDraw(data, context), input);\n  }\n\n  private createElement(elementType: ElementType, datum: ElementDatum, context: DrawContext) {\n    const id = idOf(datum);\n    const currentElement = this.getElement(id);\n    if (currentElement) return;\n    const type = this.getElementType(elementType, datum);\n    const style = this.getElementComputedStyle(elementType, datum);\n\n    // get shape constructor\n    const Ctor = getExtension(elementType, type);\n    if (!Ctor) return print.warn(`The element ${type} of ${elementType} is not registered.`);\n\n    this.emit(new ElementLifeCycleEvent(GraphEvent.BEFORE_ELEMENT_CREATE, elementType, datum), context);\n\n    const element = this.container.appendChild(\n      new Ctor({\n        id,\n        context: this.context,\n        style,\n      }),\n    ) as Element;\n\n    this.shapeTypeMap[id] = type;\n    this.elementMap[id] = element;\n\n    const { stage = 'enter' } = context;\n\n    this.context.animation?.add(\n      {\n        element,\n        elementType,\n        stage,\n        originalStyle: { ...element.attributes },\n        updatedStyle: style,\n      },\n      {\n        after: () => {\n          this.emit(new ElementLifeCycleEvent(GraphEvent.AFTER_ELEMENT_CREATE, elementType, datum), context);\n          element.onCreate?.();\n        },\n      },\n    );\n  }\n\n  private createElements(data: ProcedureData, context: DrawContext) {\n    const { nodes, edges, combos } = data;\n    const iteration: [ElementType, Map<ID, ElementDatum>][] = [\n      ['node', nodes],\n      ['combo', combos],\n      ['edge', edges],\n    ];\n\n    iteration.forEach(([elementType, elementData]) => {\n      elementData.forEach((datum) => this.createElement(elementType, datum, context));\n    });\n  }\n\n  private getUpdateStageStyle(elementType: ElementType, datum: ElementDatum, context: DrawContext) {\n    const { stage = 'update' } = context;\n\n    // 优化 translate 阶段，直接返回 x, y, z，避免计算样式\n    // Optimize the translate stage, return x, y, z directly to avoid calculating style\n    if (stage === 'translate') {\n      if (elementType === 'node' || elementType === 'combo') {\n        const { style: { x = 0, y = 0, z = 0 } = {} } = datum as NodeLikeData;\n        return { x, y, z };\n      } else return {};\n    }\n\n    return this.getElementComputedStyle(elementType, datum);\n  }\n\n  private updateElement(elementType: ElementType, datum: ElementDatum, context: DrawContext) {\n    const id = idOf(datum);\n    const { stage = 'update' } = context;\n\n    const element = this.getElement(id);\n    if (!element) return () => null;\n\n    this.emit(new ElementLifeCycleEvent(GraphEvent.BEFORE_ELEMENT_UPDATE, elementType, datum), context);\n\n    const type = this.getElementType(elementType, datum);\n    const style = this.getUpdateStageStyle(elementType, datum, context);\n\n    // 如果类型不同，需要先销毁原有元素，再创建新元素\n    // If the type is different, you need to destroy the original element first, and then create a new element\n    if (this.shapeTypeMap[id] !== type) {\n      element.destroy();\n      delete this.shapeTypeMap[id];\n      delete this.elementMap[id];\n\n      this.createElement(elementType, datum, { animation: false, silence: true });\n    }\n\n    const exactStage = stage !== 'visibility' ? stage : style.visibility === 'hidden' ? 'hide' : 'show';\n\n    // 避免立即将 visibility 设置为 hidden，导致元素不可见，而是在 after 阶段再设置\n    // Avoid setting visibility to hidden immediately, causing the element to be invisible, but set it in the after phase\n    if (exactStage === 'hide') delete style['visibility'];\n\n    this.context.animation?.add(\n      {\n        element,\n        elementType,\n        stage: exactStage,\n        originalStyle: { ...element.attributes },\n        updatedStyle: style,\n      },\n      {\n        before: () => {\n          // 通过 elementMap[id] 访问最新的 element，防止 type 不同导致的 element 丢失\n          // Access the latest element through elementMap[id] to prevent the loss of element caused by different types\n          const element = this.elementMap[id];\n          if (stage !== 'collapse') updateStyle(element, style);\n\n          if (stage === 'visibility') {\n            // 缓存原始透明度 / Cache original opacity\n            // 会在 animation controller 中访问该缓存值 / The cached value will be accessed in the animation controller\n            if (!hasCachedStyle(element, 'opacity')) cacheStyle(element, 'opacity');\n            this.visibilityCache.set(element, exactStage === 'show' ? 'visible' : 'hidden');\n            if (exactStage === 'show') setVisibility(element, 'visible');\n          }\n        },\n        after: () => {\n          const element = this.elementMap[id];\n          if (stage === 'collapse') updateStyle(element, style);\n          if (exactStage === 'hide') setVisibility(element, this.visibilityCache.get(element));\n          this.emit(new ElementLifeCycleEvent(GraphEvent.AFTER_ELEMENT_UPDATE, elementType, datum), context);\n          element.onUpdate?.();\n        },\n      },\n    );\n  }\n\n  private updateElements(data: ProcedureData, context: DrawContext) {\n    const { nodes, edges, combos } = data;\n    const iteration: [ElementType, Map<ID, ElementDatum>][] = [\n      ['node', nodes],\n      ['combo', combos],\n      ['edge', edges],\n    ];\n\n    iteration.forEach(([elementType, elementData]) => {\n      elementData.forEach((datum) => this.updateElement(elementType, datum, context));\n    });\n  }\n\n  private visibilityCache = new WeakMap<Element, BaseStyleProps['visibility']>();\n\n  /**\n   * <zh/> 标记销毁元素\n   *\n   * <en/> mark destroy element\n   * @param data - <zh/> 绘制数据 | <en/> draw data\n   */\n  private markDestroyElement(data: DrawData) {\n    Object.values(data.remove).forEach((elementData) => {\n      elementData.forEach((datum) => {\n        const id = idOf(datum);\n        const element = this.getElement(id);\n        if (element) markToBeDestroyed(element);\n      });\n    });\n  }\n\n  private destroyElement(elementType: ElementType, datum: ElementDatum, context: DrawContext) {\n    const { stage = 'exit' } = context;\n    const id = idOf(datum);\n    const element = this.elementMap[id];\n    if (!element) return () => null;\n\n    this.emit(new ElementLifeCycleEvent(GraphEvent.BEFORE_ELEMENT_DESTROY, elementType, datum), context);\n\n    this.context.animation?.add(\n      {\n        element,\n        elementType,\n        stage,\n        originalStyle: { ...element.attributes },\n        updatedStyle: {},\n      },\n      {\n        after: () => {\n          this.clearElement(id);\n          element.destroy();\n          element.onDestroy?.();\n          this.emit(new ElementLifeCycleEvent(GraphEvent.AFTER_ELEMENT_DESTROY, elementType, datum), context);\n        },\n      },\n    );\n  }\n\n  private destroyElements(data: ProcedureData, context: DrawContext) {\n    const { nodes, edges, combos } = data;\n    const iteration: [ElementType, Map<ID, ElementDatum>][] = [\n      ['combo', combos],\n      ['edge', edges],\n      ['node', nodes],\n    ];\n\n    iteration.forEach(([elementType, elementData]) => {\n      elementData.forEach((datum) => this.destroyElement(elementType, datum, context));\n    });\n\n    // TODO 重新计算色板样式，如果是分组色板，则不需要重新计算\n  }\n\n  private clearElement(id: ID) {\n    delete this.paletteStyle[id];\n    delete this.defaultStyle[id];\n    delete this.stateStyle[id];\n    delete this.elementMap[id];\n    delete this.shapeTypeMap[id];\n  }\n\n  /**\n   * <zh/> 将布局结果对齐到元素，避免视图偏移。会修改布局结果\n   *\n   * <en/> Align the layout result to the element to avoid view offset. Will modify the layout result\n   * @param layoutResult - <zh/> 布局结果 | <en/> layout result\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   */\n  private alignLayoutResultToElement(layoutResult: GraphData, id: ID) {\n    const target = layoutResult.nodes?.find((node) => idOf(node) === id);\n\n    if (target) {\n      const originalPosition = positionOf(this.context.model.getNodeLikeDatum(id));\n      const modifiedPosition = positionOf(target);\n      const delta = subtract(originalPosition, modifiedPosition);\n      layoutResult.nodes?.forEach((node) => {\n        if (node.style?.x) node.style.x += delta[0];\n        if (node.style?.y) node.style.y += delta[1];\n        if (node.style?.z) node.style.z += delta[2] || 0;\n      });\n    }\n  }\n\n  /**\n   * <zh/> 同步布局结果\n   *\n   * <en/> Sync layout result\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @param align - <zh/> 是否对齐 | <en/> whether to align\n   */\n  private async syncLayoutResult(id: ID, align?: boolean) {\n    const { layout, model } = this.context;\n    if (!layout) return;\n\n    const layoutOptions = this.context.options.layout;\n    const forcePreLayout = (opts: LayoutOptions): LayoutOptions => {\n      if (Array.isArray(opts)) {\n        return opts.map((o) => ({ ...o, preLayout: true }));\n      }\n      return { ...opts, preLayout: true };\n    };\n    const layoutResult = await layout.simulate(layoutOptions ? forcePreLayout(layoutOptions) : undefined);\n    if (align) this.alignLayoutResultToElement(layoutResult, id);\n    model.updateData(layoutResult);\n  }\n\n  /**\n   * <zh/> 收起节点\n   *\n   * <en/> collapse node\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @param options - <zh/> 选项 | <en/> options\n   */\n  public async collapseNode(id: ID, options: CollapseExpandNodeOptions): Promise<void> {\n    const { animation, align } = options;\n    await this.syncLayoutResult(id, align);\n\n    // 重新计算数据 / Recalculate data\n    const data = this.computeChangesAndDrawData({ stage: 'collapse', animation });\n    if (!data) return;\n    const { drawData } = data;\n    const { add, remove, update } = drawData;\n    this.markDestroyElement(drawData);\n    const context = { animation, stage: 'collapse', data: drawData } as const;\n\n    this.destroyElements(remove, context);\n    this.createElements(add, context);\n    this.updateElements(update, context);\n\n    await this.context.animation!.animate(\n      animation,\n      {\n        beforeAnimate: (animation) =>\n          this.emit(new AnimateEvent(GraphEvent.BEFORE_ANIMATE, AnimationType.COLLAPSE, animation, drawData), context),\n        afterAnimate: (animation) =>\n          this.emit(new AnimateEvent(GraphEvent.AFTER_ANIMATE, AnimationType.COLLAPSE, animation, drawData), context),\n      },\n      {\n        collapse: {\n          target: id,\n          descendants: Array.from(remove.nodes).map(([, node]) => idOf(node)),\n          position: positionOf(update.nodes.get(id)!),\n        },\n      },\n    )?.finished;\n  }\n\n  /**\n   * <zh/> 展开节点\n   *\n   * <en/> expand node\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @param animation - <zh/> 是否使用动画，默认为 true | <en/> Whether to use animation, default is true\n   */\n  public async expandNode(id: ID, options: CollapseExpandNodeOptions): Promise<void> {\n    const { model } = this.context;\n    const { animation, align } = options;\n    const position = positionOf(model.getNodeData([id])[0]);\n\n    await this.syncLayoutResult(id, align);\n\n    // 重新计算数据 / Recalculate data\n    const data = this.computeChangesAndDrawData({ stage: 'expand', animation });\n    this.createElements(data!.drawData.add, { animation: false, stage: 'expand', target: id });\n    // 重置动画 / Reset animation\n    this.context.animation!.clear();\n    this.computeStyle('expand');\n    if (!data) return;\n    const { drawData } = data;\n    const { update, add } = drawData;\n\n    const context = { animation, stage: 'expand', data: drawData } as const;\n\n    // 将新增节点/边添加到更新列表 / Add new nodes/edges to the update list\n    add.edges.forEach((edge) => update.edges.set(idOf(edge), edge));\n    add.nodes.forEach((node) => update.nodes.set(idOf(node), node));\n\n    this.updateElements(update, context);\n\n    await this.context.animation!.animate(\n      animation,\n      {\n        beforeAnimate: (animation) =>\n          this.emit(new AnimateEvent(GraphEvent.BEFORE_ANIMATE, AnimationType.EXPAND, animation, drawData), context),\n        afterAnimate: (animation) =>\n          this.emit(new AnimateEvent(GraphEvent.AFTER_ANIMATE, AnimationType.EXPAND, animation, drawData), context),\n      },\n      {\n        expand: {\n          target: id,\n          descendants: Array.from(add.nodes).map(([, node]) => idOf(node)),\n          position,\n        },\n      },\n    )?.finished;\n  }\n\n  public async collapseCombo(id: ID, animation: boolean): Promise<void> {\n    const { model, element } = this.context;\n    if (model.getAncestorsData(id, COMBO_KEY).some((datum) => isCollapsed(datum))) return;\n\n    const combo = element!.getElement<Combo>(id)!;\n\n    const position = combo.getComboPosition({\n      ...combo.attributes,\n      collapsed: true,\n    });\n\n    const data = this.computeChangesAndDrawData({ stage: 'collapse', animation });\n    if (!data) return;\n\n    const { dataChanges, drawData } = data;\n    this.markDestroyElement(drawData);\n    const { update, remove } = drawData;\n    const context = { animation, stage: 'collapse', data: drawData } as const;\n\n    this.destroyElements(remove, context);\n    this.updateElements(update, context);\n\n    const idsOf = (data: Map<ID, ElementDatum>) => Array.from(data).map(([, node]) => idOf(node));\n\n    await this.context.animation!.animate(\n      animation,\n      {\n        before: () => this.emit(new GraphLifeCycleEvent(GraphEvent.BEFORE_DRAW, { dataChanges, animation }), context),\n        beforeAnimate: (animation) =>\n          this.emit(new AnimateEvent(GraphEvent.BEFORE_ANIMATE, AnimationType.COLLAPSE, animation, drawData), context),\n        afterAnimate: (animation) =>\n          this.emit(new AnimateEvent(GraphEvent.AFTER_ANIMATE, AnimationType.COLLAPSE, animation, drawData), context),\n        after: () => this.emit(new GraphLifeCycleEvent(GraphEvent.AFTER_DRAW, { dataChanges, animation }), context),\n      },\n      {\n        collapse: {\n          target: id,\n          descendants: [...idsOf(remove.nodes), ...idsOf(remove.combos)],\n          position,\n        },\n      },\n    )?.finished;\n  }\n\n  public async expandCombo(id: ID, animation: boolean): Promise<void> {\n    const { model } = this.context;\n    const position = positionOf(model.getComboData([id])[0]);\n\n    // 重新计算数据 / Recalculate data\n    this.computeStyle('expand');\n    const data = this.computeChangesAndDrawData({ stage: 'expand', animation });\n    if (!data) return;\n\n    const { dataChanges, drawData } = data;\n    const { add, update } = drawData;\n    const context = { animation, stage: 'expand', data: drawData, target: id } as const;\n\n    this.createElements(add, context);\n    this.updateElements(update, context);\n\n    const idsOf = (data: Map<ID, ElementDatum>) => Array.from(data).map(([, node]) => idOf(node));\n\n    await this.context.animation!.animate(\n      animation,\n      {\n        before: () => this.emit(new GraphLifeCycleEvent(GraphEvent.BEFORE_DRAW, { dataChanges, animation }), context),\n        beforeAnimate: (animation) =>\n          this.emit(new AnimateEvent(GraphEvent.BEFORE_ANIMATE, AnimationType.EXPAND, animation, drawData), context),\n        afterAnimate: (animation) =>\n          this.emit(new AnimateEvent(GraphEvent.AFTER_ANIMATE, AnimationType.EXPAND, animation, drawData), context),\n        after: () => this.emit(new GraphLifeCycleEvent(GraphEvent.AFTER_DRAW, { dataChanges, animation }), context),\n      },\n      {\n        expand: {\n          target: id,\n          descendants: [...idsOf(add.nodes), ...idsOf(add.combos)],\n          position,\n        },\n      },\n    )?.finished;\n  }\n\n  /**\n   * <zh/> 清空所有元素\n   *\n   * <en/> clear all elements\n   */\n  public clear() {\n    this.container.destroy();\n    this.initContainer();\n    this.elementMap = {};\n    this.shapeTypeMap = {};\n    this.defaultStyle = {};\n    this.stateStyle = {};\n    this.paletteStyle = {};\n  }\n\n  public destroy() {\n    this.clear();\n    this.container.destroy();\n    // @ts-expect-error force delete\n    this.context = {};\n  }\n}\n\nexport interface DrawContext {\n  /** <zh/> 是否使用动画，默认为 true | <en/> Whether to use animation, default is true */\n  animation?: boolean;\n  /** <zh/> 当前绘制阶段 | <en/> Current draw stage */\n  stage?: AnimationStage;\n  /** <zh/> 是否不抛出事件 | <en/> Whether not to dispatch events */\n  silence?: boolean;\n  /** <zh/> 收起/展开的对象 ID | <en/> ID of the object to collapse/expand */\n  collapseExpandTarget?: ID;\n  /** <zh/> 绘制类型 | <en/> Draw type */\n  type?: 'render' | 'draw';\n  /** <zh/> 展开阶段的目标元素 id | <en/> ID of the target element in the expand stage */\n  target?: ID;\n}\n\ninterface DrawPayload {\n  data: {\n    dataChanges: DataChange[];\n    drawData: DrawData;\n  };\n  stage: AnimationStage;\n  type: 'render' | 'draw';\n}\n\n/**\n * <zh/> 展开/收起节点选项\n *\n * <en/> Expand / collapse node options\n */\nexport interface CollapseExpandNodeOptions {\n  /**\n   * <zh/> 是否使用动画\n   *\n   * <en/> Whether to use animation\n   */\n  animation?: boolean;\n  /**\n   * <zh/> 保证展开/收起的节点位置不变\n   *\n   * <en/> Ensure that the position of the expanded/collapsed node remains unchanged\n   */\n  align?: boolean;\n}\n"
  },
  {
    "path": "packages/g6/src/runtime/graph.ts",
    "content": "import EventEmitter from '@antv/event-emitter';\nimport type { AABB, BaseStyleProps } from '@antv/g';\nimport { debounce, isEqual, isFunction, isNumber, isObject, isString, omit } from '@antv/util';\nimport { COMBO_KEY, GraphEvent } from '../constants';\nimport type { Plugin } from '../plugins/types';\nimport type {\n  BehaviorOptions,\n  ComboData,\n  ComboOptions,\n  EdgeData,\n  EdgeOptions,\n  GraphData,\n  GraphOptions,\n  LayoutOptions,\n  NodeData,\n  NodeOptions,\n  PluginOptions,\n  ThemeOptions,\n  TransformOptions,\n} from '../spec';\nimport type { UpdateBehaviorOption } from '../spec/behavior';\nimport type { UpdatePluginOption } from '../spec/plugin';\nimport type { UpdateTransformOption } from '../spec/transform';\nimport type {\n  DataID,\n  EdgeDirection,\n  ElementDatum,\n  ElementType,\n  FitViewOptions,\n  HierarchyKey,\n  ID,\n  IEvent,\n  NodeLikeData,\n  PartialEdgeData,\n  PartialGraphData,\n  PartialNodeLikeData,\n  Point,\n  State,\n  Vector2,\n  ViewportAnimationEffectTiming,\n} from '../types';\nimport { isCollapsed } from '../utils/collapsibility';\nimport { sizeOf } from '../utils/dom';\nimport { getSubgraphRelatedEdges } from '../utils/edge';\nimport { GraphLifeCycleEvent, emit } from '../utils/event';\nimport { idOf } from '../utils/id';\nimport { isPreLayout } from '../utils/layout';\nimport { format } from '../utils/print';\nimport { subtract } from '../utils/vector';\nimport { getZIndexOf } from '../utils/z-index';\nimport { Animation } from './animation';\nimport { BatchController } from './batch';\nimport { BehaviorController } from './behavior';\nimport type { DataURLOptions } from './canvas';\nimport { Canvas } from './canvas';\nimport { DataController } from './data';\nimport type { CollapseExpandNodeOptions } from './element';\nimport { ElementController } from './element';\nimport { LayoutController } from './layout';\nimport { inferOptions } from './options';\nimport { PluginController } from './plugin';\nimport { TransformController } from './transform';\nimport { RuntimeContext } from './types';\nimport { ViewportController } from './viewport';\n\nexport class Graph extends EventEmitter {\n  private options: GraphOptions = {};\n\n  /**\n   * @internal\n   */\n  static defaultOptions: GraphOptions = {\n    autoResize: false,\n    theme: 'light',\n    rotation: 0,\n    zoom: 1,\n    zoomRange: [0.01, 10],\n  };\n\n  /**\n   * <zh/> 当前图实例是否已经渲染\n   *\n   * <en/> Whether the current graph instance has been rendered\n   */\n  public rendered = false;\n\n  /**\n   * <zh/> 当前图实例是否已经被销毁\n   *\n   * <en/> Whether the current graph instance has been destroyed\n   */\n  public destroyed = false;\n\n  // @ts-expect-error will be initialized in createRuntime\n  private context: RuntimeContext = {\n    model: new DataController(),\n  };\n\n  constructor(options: GraphOptions) {\n    super();\n    this._setOptions(Object.assign({}, Graph.defaultOptions, options), true);\n    this.context.graph = this;\n\n    // Listening resize to autoResize.\n    this.options.autoResize && globalThis.addEventListener?.('resize', this.onResize);\n  }\n\n  /**\n   * <zh/> 获取配置项\n   *\n   * <en/> Get options\n   * @returns <zh/> 配置项 | <en/> options\n   * @apiCategory option\n   */\n  public getOptions(): GraphOptions {\n    return this.options;\n  }\n\n  /**\n   * <zh/> 设置配置项\n   *\n   * <en/> Set options\n   * @param options - <zh/> 配置项 | <en/> options\n   * @remarks\n   * <zh/> 要更新 devicePixelRatio、container 属性请销毁后重新创建实例\n   *\n   * <en/> To update devicePixelRatio and container properties, please destroy and recreate the instance\n   * @apiCategory option\n   */\n  public setOptions(options: GraphOptions): void {\n    this._setOptions(options, false);\n  }\n\n  private _setOptions(options: GraphOptions, isInit: boolean) {\n    this.updateCanvas(options);\n    Object.assign(this.options, inferOptions(options));\n\n    if (isInit) {\n      const { data } = options;\n      if (data) this.addData(data);\n      return;\n    }\n    const { behaviors, combo, data, edge, layout, node, plugins, theme, transforms } = options;\n    if (behaviors) this.setBehaviors(behaviors);\n    if (data) this.setData(data);\n    if (node) this.setNode(node);\n    if (edge) this.setEdge(edge);\n    if (combo) this.setCombo(combo);\n    if (layout) this.setLayout(layout);\n    if (theme) this.setTheme(theme);\n    if (plugins) this.setPlugins(plugins);\n    if (transforms) this.setTransforms(transforms);\n  }\n\n  /**\n   * <zh/> 获取当前画布容器的尺寸\n   *\n   * <en/> Get the size of the current canvas container\n   * @returns <zh/> 画布尺寸 | <en/> canvas size\n   * @apiCategory canvas\n   */\n  public getSize(): [number, number] {\n    if (this.context.canvas) return this.context.canvas.getSize();\n    return [this.options.width || 0, this.options.height || 0];\n  }\n\n  /**\n   * <zh/> 设置当前画布容器的尺寸\n   *\n   * <en/> Set the size of the current canvas container\n   * @param width - <zh/> 画布宽度 | <en/> canvas width\n   * @param height - <zh/> 画布高度 | <en/> canvas height\n   * @apiCategory canvas\n   */\n  public setSize(width: number, height: number): void {\n    if (width) this.options.width = width;\n    if (height) this.options.height = height;\n\n    this.resize(width, height);\n  }\n\n  /**\n   * <zh/> 设置当前图的缩放区间\n   *\n   * <en/> Get the zoom range of the current graph\n   * @param zoomRange - <zh/> 缩放区间 | <en/> zoom range\n   * @apiCategory viewport\n   */\n  public setZoomRange(zoomRange: GraphOptions['zoomRange']): void {\n    this.options.zoomRange = zoomRange;\n  }\n\n  /**\n   * <zh/> 获取当前图的缩放区间\n   *\n   * <en/> Get the zoom range of the current graph\n   * @returns <zh/> 缩放区间 | <en/> zoom range\n   * @apiCategory viewport\n   */\n  public getZoomRange(): GraphOptions['zoomRange'] {\n    return this.options.zoomRange;\n  }\n\n  /**\n   * <zh/> 设置节点样式映射\n   *\n   * <en/> Set node mapper\n   * @param node - <zh/> 节点配置 | <en/> node options\n   * @remarks\n   * <zh/> 即 `options.node` 的值\n   *\n   * <en/> The value of `options.node`\n   * @apiCategory element\n   */\n  public setNode(node: NodeOptions): void {\n    this.options.node = node;\n    this.context.model.refreshData();\n  }\n\n  /**\n   * <zh/> 设置边样式映射\n   *\n   * <en/> Set edge mapper\n   * @param edge - <zh/> 边配置 | <en/> edge options\n   * @remarks\n   * <zh/> 即 `options.edge` 的值\n   *\n   * <en/> The value of `options.edge`\n   * @apiCategory element\n   */\n  public setEdge(edge: EdgeOptions): void {\n    this.options.edge = edge;\n    this.context.model.refreshData();\n  }\n\n  /**\n   * <zh/> 设置组合样式映射\n   *\n   * <en/> Set combo mapper\n   * @param combo - <zh/> 组合配置 | <en/> combo options\n   * @remarks\n   * <zh/> 即 `options.combo` 的值\n   *\n   * <en/> The value of `options.combo`\n   * @apiCategory element\n   */\n  public setCombo(combo: ComboOptions): void {\n    this.options.combo = combo;\n    this.context.model.refreshData();\n  }\n\n  /**\n   * <zh/> 获取主题\n   *\n   * <en/> Get theme\n   * @returns <zh/> 当前主题 | <en/> current theme\n   * @apiCategory theme\n   */\n  public getTheme(): ThemeOptions {\n    return this.options.theme!;\n  }\n\n  /**\n   * <zh/> 设置主题\n   *\n   * <en/> Set theme\n   * @param theme - <zh/> 主题名 | <en/> theme name\n   * @example\n   * ```ts\n   * graph.setTheme('dark');\n   * ```\n   * @apiCategory theme\n   */\n  public setTheme(theme: ThemeOptions | ((prev: ThemeOptions) => ThemeOptions)): void {\n    this.options.theme = isFunction(theme) ? theme(this.getTheme()) : theme;\n  }\n\n  /**\n   * <zh/> 设置布局\n   *\n   * <en/> Set layout\n   * @param layout - <zh/> 布局配置 | <en/> layout options\n   * @example\n   * ```ts\n   * graph.setLayout({\n   *  type: 'dagre',\n   * })\n   * ```\n   * @apiCategory layout\n   */\n  public setLayout(layout: LayoutOptions | ((prev: LayoutOptions) => LayoutOptions)): void {\n    this.options.layout = isFunction(layout) ? layout(this.getLayout()) : layout;\n  }\n\n  /**\n   * <zh/> 获取布局配置\n   *\n   * <en/> Get layout options\n   * @returns <zh/> 布局配置 | <en/> layout options\n   * @apiCategory layout\n   */\n  public getLayout(): LayoutOptions {\n    return this.options.layout!;\n  }\n\n  /**\n   * <zh/> 设置交互\n   *\n   * <en/> Set behaviors\n   * @param behaviors - <zh/> 交互配置 | <en/> behavior options\n   * @remarks\n   * <zh/> 设置的交互会全量替换原有的交互，如果需要新增交互可以使用如下方式：\n   *\n   * <en/> The set behavior will completely replace the original behavior. If you need to add behavior, you can use the following method:\n   *\n   * ```ts\n   * graph.setBehaviors((behaviors) => [...behaviors, { type: 'zoom-canvas' }])\n   * ```\n   * @apiCategory behavior\n   */\n  public setBehaviors(behaviors: BehaviorOptions | ((prev: BehaviorOptions) => BehaviorOptions)): void {\n    this.options.behaviors = isFunction(behaviors) ? behaviors(this.getBehaviors()) : behaviors;\n    this.context.behavior?.setBehaviors(this.options.behaviors);\n  }\n\n  /**\n   * <zh/> 更新指定的交互配置\n   *\n   * <en/> Update specified behavior options\n   * @param behavior - <zh/> 交互配置 | <en/> behavior options\n   * @remarks\n   * <zh/> 如果要更新一个交互，那么必须在交互配置中指定 key 字段，例如：\n   *\n   * <en/> If you want to update a behavior, you must specify the key field in the behavior options, for example:\n   * ```ts\n   * {\n   *   behaviors: [{ type: 'zoom-canvas', key: 'zoom-canvas' }]\n   * }\n   *\n   * graph.updateBehavior({ key: 'zoom-canvas', enable: false })\n   * ```\n   * @apiCategory behavior\n   */\n  public updateBehavior(behavior: UpdateBehaviorOption): void {\n    this.setBehaviors((behaviors) =>\n      behaviors.map((_behavior) => {\n        if (typeof _behavior === 'object' && _behavior.key === behavior.key) {\n          return { ..._behavior, ...behavior };\n        }\n        return _behavior;\n      }),\n    );\n  }\n\n  /**\n   * <zh/> 获取交互配置\n   *\n   * <en/> Get behaviors options\n   * @returns <zh/> 交互配置 | <en/> behavior options\n   * @apiCategory behavior\n   */\n  public getBehaviors(): BehaviorOptions {\n    return this.options.behaviors || [];\n  }\n\n  /**\n   * <zh/> 设置插件配置\n   *\n   * <en/> Set plugins options\n   * @param plugins - <zh/> 插件配置 | <en/> plugin options\n   * @remarks\n   * <zh/> 设置的插件会全量替换原有的插件配置，如果需要新增插件可以使用如下方式：\n   *\n   * <en/> The set plugin will completely replace the original plugin configuration. If you need to add a plugin, you can use the following method:\n   * ```ts\n   * graph.setPlugins((plugins) => [...plugins, { key: 'grid-line' }])\n   * ```\n   * @apiCategory plugin\n   */\n  public setPlugins(plugins: PluginOptions | ((prev: PluginOptions) => PluginOptions)): void {\n    this.options.plugins = isFunction(plugins) ? plugins(this.getPlugins()) : plugins;\n    this.context.plugin?.setPlugins(this.options.plugins);\n  }\n\n  /**\n   * <zh/> 更新插件配置\n   *\n   * <en/> Update plugin options\n   * @param plugin - <zh/> 插件配置 | <en/> plugin options\n   * @remarks\n   * <zh/> 如果要更新一个插件，那么必须在插件配置中指定 key 字段，例如：\n   *\n   * <en/> If you want to update a plugin, you must specify the key field in the plugin options, for example:\n   * ```ts\n   * {\n   *   plugins: [{ key: 'grid-line' }]\n   * }\n   *\n   * graph.updatePlugin({ key: 'grid-line', follow: true })\n   * ```\n   * @apiCategory plugin\n   */\n  public updatePlugin(plugin: UpdatePluginOption): void {\n    this.setPlugins((plugins) =>\n      plugins.map((_plugin) => {\n        if (typeof _plugin === 'object' && _plugin.key === plugin.key) {\n          return { ..._plugin, ...plugin };\n        }\n        return _plugin;\n      }),\n    );\n  }\n\n  /**\n   * <zh/> 获取插件配置\n   *\n   * <en/> Get plugins options\n   * @returns <zh/> 插件配置 | <en/> plugin options\n   * @apiCategory plugin\n   */\n  public getPlugins(): PluginOptions {\n    return this.options.plugins || [];\n  }\n\n  /**\n   * <zh/> 获取插件实例\n   *\n   * <en/> Get plugin instance\n   * @param key - <zh/> 插件 key（在配置 plugin 时需要手动传入指定） | <en/> plugin key(need to be specified manually when configuring plugin)\n   * @returns <zh/> 插件实例 | <en/> plugin instance\n   * @remarks\n   * <zh/> 一些插件提供了 API 方法可供调用，例如全屏插件可以调用 `request` 和 `exit` 方法来请求和退出全屏\n   *\n   * <en/> Some plugins provide API methods for calling, such as the full-screen plugin can call the `request` and `exit` methods to request and exit full-screen\n   * ```ts\n   * const fullscreen = graph.getPluginInstance('fullscreen');\n   *\n   * fullscreen.request();\n   *\n   * fullscreen.exit();\n   * ```\n   * @apiCategory plugin\n   */\n  public getPluginInstance<T extends Plugin>(key: string) {\n    return this.context.plugin!.getPluginInstance(key) as unknown as T;\n  }\n\n  /**\n   * <zh/> 设置数据转换器\n   *\n   * <en/> Set data transforms\n   * @param transforms - <zh/> 数据转换配置 | <en/> data transform options\n   * @remarks\n   * <zh/> 数据转换器能够在图渲染过程中执行数据转换，目前支持在渲染前对绘制数据进行转化。\n   *\n   * <en/> Data transforms can perform data transformation during the rendering process of the graph. Currently, it supports transforming the drawing data before rendering.\n   * @apiCategory transform\n   */\n  public setTransforms(transforms: TransformOptions | ((prev: TransformOptions) => TransformOptions)): void {\n    this.options.transforms = isFunction(transforms) ? transforms(this.getTransforms()) : transforms;\n    this.context.transform?.setTransforms(this.options.transforms);\n  }\n\n  /**\n   * <zh/> 更新数据转换器\n   *\n   * <en/> Update data transform\n   * @param transform - <zh/> 数据转换器配置 | <en/> data transform options\n   * @apiCategory transform\n   */\n  public updateTransform(transform: UpdateTransformOption): void {\n    this.setTransforms((transforms) =>\n      transforms.map((_transform) => {\n        if (typeof _transform === 'object' && _transform.key === transform.key) {\n          return { ..._transform, ...transform };\n        }\n        return _transform;\n      }),\n    );\n    this.context.model.refreshData();\n  }\n\n  /**\n   * <zh/> 获取数据转换器配置\n   *\n   * <en/> Get data transforms options\n   * @returns <zh/> 数据转换配置 | <en/> data transform options\n   * @apiCategory transform\n   */\n  public getTransforms(): TransformOptions {\n    return this.options.transforms || [];\n  }\n\n  /**\n   * <zh/> 获取图数据\n   *\n   * <en/> Get graph data\n   * @returns <zh/> 图数据 | <en/> Graph data\n   * <zh/> 获取当前图的数据，包括节点、边、组合数据\n   *\n   * <en/> Get the data of the current graph, including node, edge, and combo data\n   * @apiCategory data\n   */\n  public getData(): Required<GraphData> {\n    return this.context.model.getData();\n  }\n  /**\n   * <zh/> 判断图中是否存在指定节点\n   * <en/> Determine whether a specified node exists in the graph\n   * @param {ID} id\n   * @returns {boolean}\n   * @remarks <zh/> 判断图中是否存在指定节点,避免在不存在的节点上进行操作\n   * <en/> Determine whether a specified node exists in the graph and avoid operating on non-existent nodes\n   */\n  public hasNode(id: ID): boolean {\n    return this.context.model.hasNode(id);\n  }\n  /**\n   * <zh/> 判断图中是否存在指定边\n   * <en/> Determine whether a specified edge exists in the graph\n   * @param {ID} id\n   * @returns  {boolean}\n   * @remarks <zh/> 判断图中是否存在指定边,避免在不存在的边上进行操作\n   * <en/> Determine whether a specified edge exists in the graph and avoid operating on non-existent edges\n   */\n  public hasEdge(id: ID): boolean {\n    return this.context.model.hasEdge(id);\n  }\n\n  /**\n   * <zh/> 判断图中是否存在指定组合\n   * <en/> Determine whether a specified combo exists in the graph\n   * @param {ID} id\n   * @returns  {boolean}\n   * @remarks <zh/> 判断图中是否存在指定组合,避免在不存在的组合上进行操作\n   * <en/> Determine whether a specified combo exists in the graph and avoid operating on non-existent combos\n   */\n  public hasCombo(id: ID): boolean {\n    return this.context.model.hasCombo(id);\n  }\n\n  /**\n   * <zh/> 获取单个元素数据\n   *\n   * <en/> Get element data by ID\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @returns <zh/> 元素数据 | <en/> element data\n   * @remarks\n   * <zh/> 直接获取元素的数据而不必考虑元素类型\n   *\n   * <en/> Get element data directly without considering the element type\n   * @apiCategory data\n   */\n  public getElementData(id: ID): ElementDatum;\n  /**\n   * <zh/> 批量获取多个元素数据\n   *\n   * <en/> Get multiple element data in batch\n   * @param ids - <zh/> 元素 ID 数组 | <en/> element ID array\n   * @remarks\n   * <zh/> 直接获取元素的数据而不必考虑元素类型\n   *\n   * <en/> Get element data directly without considering the element type\n   * @apiCategory data\n   */\n  public getElementData(ids: ID[]): ElementDatum[];\n  public getElementData(ids: ID | ID[]): ElementDatum | ElementDatum[] {\n    if (Array.isArray(ids)) return ids.map((id) => this.context.model.getElementDataById(id));\n    return this.context.model.getElementDataById(ids);\n  }\n\n  /**\n   * <zh/> 获取所有节点数据\n   *\n   * <en/> Get all node data\n   * @returns <zh/> 节点数据 | <en/> node data\n   * @apiCategory data\n   */\n  public getNodeData(): NodeData[];\n  /**\n   * <zh/> 获取单个节点数据\n   *\n   * <en/> Get single node data\n   * @param id - <zh/> 节点 ID | <en/> node ID\n   * @returns <zh/> 节点数据 | <en/> node data\n   * @example\n   * ```ts\n   * const node1 = graph.getNodeData('node-1');\n   * ```\n   * @apiCategory data\n   * @remarks\n   * <zh/> 节点 id 必须存在，否则会抛出异常\n   *\n   * <en/> Node id must exist, otherwise an exception will be thrown\n   */\n  public getNodeData(id: ID): NodeData;\n  /**\n   * <zh/> 批量获取多个节点数据\n   *\n   * <en/> Get multiple node data in batch\n   * @param ids - <zh/> 节点 ID 数组 | <en/> node ID array\n   * @returns <zh/> 节点数据 | <en/> node data\n   * @example\n   * ```ts\n   * const [node1, node2] = graph.getNodeData(['node-1', 'node-2']);\n   * ```\n   * @apiCategory data\n   * @remarks\n   * <zh/> 数组中的每个节点 id 必须存在，否则将抛出异常\n   *\n   * <en/> Each node id in the array must exist, otherwise an exception will be thrown\n   */\n  public getNodeData(ids: ID[]): NodeData[];\n  public getNodeData(id?: ID | ID[]): NodeData | NodeData[] {\n    if (id === undefined) return this.context.model.getNodeData();\n    if (Array.isArray(id)) return this.context.model.getNodeData(id);\n    return this.context.model.getNodeLikeDatum(id);\n  }\n\n  /**\n   * <zh/> 获取所有边数据\n   *\n   * <en/> Get all edge data\n   * @returns <zh/> 边数据 | <en/> edge data\n   * @apiCategory data\n   */\n  public getEdgeData(): EdgeData[];\n  /**\n   * <zh/> 获取单条边数据\n   *\n   * <en/> Get single edge data\n   * @param id - <zh/> 边 ID | <en/> edge ID\n   * @returns <zh/> 边数据 | <en/> edge data\n   * @example\n   * ```ts\n   * const edge1 = graph.getEdgeData('edge-1');\n   * ```\n   * @apiCategory data\n   * @remarks\n   * <zh/> 边 id 必须存在，否则会抛出异常\n   *\n   * <en/> Edge id must exist, otherwise an exception will be thrown\n   */\n  public getEdgeData(id: ID): EdgeData;\n  /**\n   * <zh/> 批量获取多条边数据\n   *\n   * <en/> Get multiple edge data in batch\n   * @param ids - <zh/> 边 ID 数组 | <en/> edge ID array\n   * @returns <zh/> 边数据 | <en/> edge data\n   * @example\n   * ```ts\n   * const [edge1, edge2] = graph.getEdgeData(['edge-1', 'edge-2']);\n   * ```\n   * @apiCategory data\n   * @remarks\n   * <zh/> 数组中的每个边 id 必须存在，否则将抛出异常\n   *\n   * <en/> Each edge id in the array must exist, otherwise an exception will be thrown\n   */\n  public getEdgeData(ids: ID[]): EdgeData[];\n  public getEdgeData(id?: ID | ID[]): EdgeData | EdgeData[] {\n    if (id === undefined) return this.context.model.getEdgeData();\n    if (Array.isArray(id)) return this.context.model.getEdgeData(id);\n    return this.context.model.getEdgeDatum(id);\n  }\n\n  /**\n   * <zh/> 获取所有组合数据\n   *\n   * <en/> Get all combo data\n   * @returns <zh/> 组合数据 | <en/> combo data\n   * @apiCategory data\n   */\n  public getComboData(): ComboData[];\n  /**\n   * <zh/> 获取单个组合数据\n   *\n   * <en/> Get single combo data\n   * @param id - <zh/> 组合ID | <en/> combo ID\n   * @returns <zh/> 组合数据 | <en/> combo data\n   * @example\n   * ```ts\n   * const combo1 = graph.getComboData('combo-1');\n   * ```\n   * @apiCategory data\n   * @remarks\n   * <zh/> 组合 id 必须存在，否则会抛出异常\n   *\n   * <en/> Combo id must exist, otherwise an exception will be thrown\n   */\n  public getComboData(id: ID): ComboData;\n  /**\n   * <zh/> 批量获取多个组合数据\n   *\n   * <en/> Get multiple combo data in batch\n   * @param ids - <zh/> 组合ID 数组 | <en/> combo ID array\n   * @returns <zh/> 组合数据 | <en/> combo data\n   * @example\n   * ```ts\n   * const [combo1, combo2] = graph.getComboData(['combo-1', 'combo-2']);\n   * ```\n   * @apiCategory data\n   * @remarks\n   * <zh/> 数组中的每个组合 id 必须存在，否则将抛出异常\n   *\n   * <en/> Each combo id in the array must exist, otherwise an exception will be thrown\n   */\n  public getComboData(ids: ID[]): ComboData[];\n  public getComboData(id?: ID | ID[]): ComboData | ComboData[] {\n    if (id === undefined) return this.context.model.getComboData();\n    if (Array.isArray(id)) return this.context.model.getComboData(id);\n    return this.context.model.getNodeLikeDatum(id);\n  }\n\n  /**\n   * <zh/> 设置全量数据\n   *\n   * <en/> Set full data\n   * @param data - <zh/> 数据 | <en/> data\n   * @remarks\n   * <zh/> 设置全量数据会替换当前图中的所有数据，G6 会自动进行数据差异计算\n   *\n   * <en/> Setting full data will replace all data in the current graph, and G6 will automatically calculate the data difference\n   * @apiCategory data\n   */\n  public setData(data: GraphData | ((prev: GraphData) => GraphData)): void {\n    this.context.model.setData(isFunction(data) ? data(this.getData()) : data);\n  }\n\n  /**\n   * <zh/> 新增元素数据\n   *\n   * <en/> Add element data\n   * @param data - <zh/> 元素数据 | <en/> element data\n   * @example\n   * ```ts\n   * graph.addData({\n   *  nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n   *  edges: [{ source: 'node-1', target: 'node-2' }],\n   * });\n   * ```\n   * @apiCategory data\n   */\n  public addData(data: GraphData | ((prev: GraphData) => GraphData)): void {\n    this.context.model.addData(isFunction(data) ? data(this.getData()) : data);\n  }\n\n  /**\n   * <zh/> 新增节点数据\n   *\n   * <en/> Add node data\n   * @param data - <zh/> 节点数据 | <en/> node data\n   * @example\n   * ```ts\n   * graph.addNodeData([{ id: 'node-1' }, { id: 'node-2' }]);\n   * ```\n   * @apiCategory data\n   */\n  public addNodeData(data: NodeData[] | ((prev: NodeData[]) => NodeData[])): void {\n    this.context.model.addNodeData(isFunction(data) ? data(this.getNodeData()) : data);\n  }\n\n  /**\n   * <zh/> 新增边数据\n   *\n   * <en/> Add edge data\n   * @param data - <zh/> 边数据 | <en/> edge data\n   * @example\n   * ```ts\n   * graph.addEdgeData([{ source: 'node-1', target: 'node-2' }]);\n   * ```\n   * @apiCategory data\n   */\n  public addEdgeData(data: EdgeData[] | ((prev: EdgeData[]) => EdgeData[])): void {\n    this.context.model.addEdgeData(isFunction(data) ? data(this.getEdgeData()) : data);\n  }\n\n  /**\n   * <zh/> 新增组合数据\n   *\n   * <en/> Add combo data\n   * @param data - <zh/> 组合数据 | <en/> combo data\n   * @example\n   * ```ts\n   * graph.addComboData([{ id: 'combo-1' }]);\n   * ```\n   * @apiCategory data\n   */\n  public addComboData(data: ComboData[] | ((prev: ComboData[]) => ComboData[])): void {\n    this.context.model.addComboData(isFunction(data) ? data(this.getComboData()) : data);\n  }\n\n  /**\n   * <zh/> 为树图节点添加子节点数据\n   *\n   * <en/> Add child node data to the tree node\n   * @param parentId - <zh/> 父节点 ID | <en/> parent node ID\n   * @param childrenData - <zh/> 子节点数据 | <en/> child node data\n   * @remarks\n   * <zh/> 为组合添加子节点使用 addNodeData / addComboData 方法\n   *\n   * <en/> Use addNodeData / addComboData method to add child nodes to the combo\n   * @apiCategory data\n   */\n  public addChildrenData(parentId: ID, childrenData: NodeData[]) {\n    this.context.model.addChildrenData(parentId, childrenData);\n  }\n\n  /**\n   * <zh/> 更新元素数据\n   *\n   * <en/> Update element data\n   * @param data - <zh/> 元素数据 | <en/> element data\n   * @remarks\n   * <zh/> 只需要传入需要更新的数据即可，不必传入完整的数据\n   *\n   * <en/> Just pass in the data that needs to be updated, no need to pass in the complete data\n   * @example\n   * ```ts\n   * graph.updateData({\n   *   nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],\n   *   edges: [{ id: 'edge-1', style: { lineWidth: 2 } }]\n   * });\n   * ```\n   * @apiCategory data\n   */\n  public updateData(data: PartialGraphData | ((prev: GraphData) => PartialGraphData)): void {\n    this.context.model.updateData(isFunction(data) ? data(this.getData()) : data);\n  }\n\n  /**\n   * <zh/> 更新节点数据\n   *\n   * <en/> Update node data\n   * @param data - <zh/> 节点数据 | <en/> node data\n   * @remarks\n   * <zh/> 只需要传入需要更新的数据即可，不必传入完整的数据\n   *\n   * <en/> Just pass in the data that needs to be updated, no need to pass in the complete data\n   * @example\n   * ```ts\n   * graph.updateNodeData([{ id: 'node-1', style: { x: 100, y: 100 } }]);\n   * ```\n   * @apiCategory data\n   */\n  public updateNodeData(\n    data: PartialNodeLikeData<NodeData>[] | ((prev: NodeData[]) => PartialNodeLikeData<NodeData>[]),\n  ): void {\n    this.context.model.updateNodeData(isFunction(data) ? data(this.getNodeData()) : data);\n  }\n\n  /**\n   * <zh/> 更新边数据\n   *\n   * <en/> Update edge data\n   * @param data - <zh/> 边数据 | <en/> edge data\n   * @remarks\n   * <zh/> 只需要传入需要更新的数据即可，不必传入完整的数据\n   *\n   * <en/> Just pass in the data that needs to be updated, no need to pass in the complete data\n   * @example\n   * ```ts\n   * graph.updateEdgeData([{ id: 'edge-1', style: { lineWidth: 2 } }]);\n   * ```\n   * @apiCategory data\n   */\n  public updateEdgeData(data: PartialEdgeData<EdgeData>[] | ((prev: EdgeData[]) => PartialEdgeData<EdgeData>[])): void {\n    this.context.model.updateEdgeData(isFunction(data) ? data(this.getEdgeData()) : data);\n  }\n\n  /**\n   * <zh/> 更新组合数据\n   *\n   * <en/> Update combo data\n   * @param data - <zh/> 组合数据 | <en/> combo data\n   * @remarks\n   * <zh/> 只需要传入需要更新的数据即可，不必传入完整的数据\n   *\n   * <en/> Just pass in the data that needs to be updated, no need to pass in the complete data\n   * @example\n   * ```ts\n   * graph.updateComboData([{ id: 'combo-1', style: { x: 100, y: 100 } }]);\n   * ```\n   * @apiCategory data\n   */\n  public updateComboData(\n    data: PartialNodeLikeData<ComboData>[] | ((prev: ComboData[]) => PartialNodeLikeData<ComboData>[]),\n  ): void {\n    this.context.model.updateComboData(isFunction(data) ? data(this.getComboData()) : data);\n  }\n\n  /**\n   * <zh/> 删除元素数据\n   *\n   * <en/> Remove element data\n   * @param ids - <zh/> 元素 ID 数组 | <en/> element ID array\n   * @example\n   * ```ts\n   * graph.removeData({\n   *   nodes: ['node-1', 'node-2'],\n   *   edges: ['edge-1'],\n   * });\n   * ```\n   * @apiCategory data\n   */\n  public removeData(ids: DataID | ((data: GraphData) => DataID)): void {\n    this.context.model.removeData(isFunction(ids) ? ids(this.getData()) : ids);\n  }\n\n  /**\n   * <zh/> 删除节点数据\n   *\n   * <en/> Remove node data\n   * @param ids - <zh/> 节点 ID 数组 | <en/> node ID array\n   * @example\n   * ```ts\n   * graph.removeNodeData(['node-1', 'node-2']);\n   * ```\n   * @apiCategory data\n   */\n  public removeNodeData(ids: ID[] | ((data: NodeData[]) => ID[])): void {\n    this.context.model.removeNodeData(isFunction(ids) ? ids(this.getNodeData()) : ids);\n  }\n\n  /**\n   * <zh/> 删除边数据\n   *\n   * <en/> Remove edge data\n   * @param ids - <zh/> 边 ID 数组 | <en/> edge ID array\n   * @remarks\n   * <zh/> 如果传入边数据时仅提供了 source 和 target，那么需要通过 `idOf` 方法获取边的实际 ID\n   *\n   * <en/> If only the source and target are provided when passing in the edge data, you need to get the actual ID of the edge through the `idOf` method\n   * @example\n   * ```ts\n   * graph.removeEdgeData(['edge-1']);\n   * ```\n   * @apiCategory data\n   */\n  public removeEdgeData(ids: ID[] | ((data: EdgeData[]) => ID[])): void {\n    this.context.model.removeEdgeData(isFunction(ids) ? ids(this.getEdgeData()) : ids);\n  }\n\n  /**\n   * <zh/> 删除组合数据\n   *\n   * <en/> Remove combo data\n   * @param ids - <zh/> 组合 ID 数组 | <en/> 组合 ID array\n   * @example\n   * ```ts\n   * graph.removeComboData(['combo-1']);\n   * ```\n   * @apiCategory data\n   */\n  public removeComboData(ids: ID[] | ((data: ComboData[]) => ID[])): void {\n    this.context.model.removeComboData(isFunction(ids) ? ids(this.getComboData()) : ids);\n  }\n\n  /**\n   * <zh/> 获取元素类型\n   *\n   * <en/> Get element type\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @returns <zh/> 元素类型 | <en/> element type\n   * @apiCategory element\n   */\n  public getElementType(id: ID): ElementType {\n    return this.context.model.getElementType(id);\n  }\n\n  /**\n   * <zh/> 获取节点或组合关联边的数据\n   *\n   * <en/> Get edge data related to the node or combo\n   * @param id - <zh/> 节点或组合ID | <en/> node or combo ID\n   * @param direction - <zh/> 边的方向 | <en/> edge direction\n   * @returns <zh/> 边数据 | <en/> edge data\n   * @apiCategory data\n   */\n  public getRelatedEdgesData(id: ID, direction: EdgeDirection = 'both'): EdgeData[] {\n    return this.context.model.getRelatedEdgesData(id, direction);\n  }\n\n  /**\n   * <zh/> 获取节点或组合的一跳邻居节点数据\n   *\n   * <en/> Get the one-hop neighbor node data of the node or combo\n   * @param id - <zh/> 节点或组合ID | <en/> node or combo ID\n   * @returns <zh/> 邻居节点数据 | <en/> neighbor node data\n   * @apiCategory data\n   */\n  public getNeighborNodesData(id: ID): NodeData[] {\n    return this.context.model.getNeighborNodesData(id);\n  }\n\n  /**\n   * <zh/> 获取节点或组合的祖先元素数据\n   *\n   * <en/> Get the ancestor element data of the node or combo\n   * @param id - <zh/> 节点或组合ID | <en/> node or combo ID\n   * @param hierarchy - <zh/> 指定树图层级关系还是组合层级关系 | <en/> specify tree or combo hierarchy relationship\n   * @returns <zh/> 祖先元素数据 | <en/> ancestor element data\n   * @remarks\n   * <zh/> 数组中的顺序是从父节点到祖先节点\n   *\n   * <en/> The order in the array is from the parent node to the ancestor node\n   * @apiCategory data\n   */\n  public getAncestorsData(id: ID, hierarchy: HierarchyKey): NodeLikeData[] {\n    return this.context.model.getAncestorsData(id, hierarchy);\n  }\n\n  /**\n   * <zh/> 获取节点或组合的父元素数据\n   *\n   * <en/> Get the parent element data of the node or combo\n   * @param id - <zh/> 节点或组合ID | <en/> node or combo ID\n   * @param hierarchy - <zh/> 指定树图层级关系还是组合层级关系 | <en/> specify tree or combo hierarchy relationship\n   * @returns <zh/> 父元素数据 | <en/> parent element data\n   * @apiCategory data\n   */\n  public getParentData(id: ID, hierarchy: HierarchyKey): NodeLikeData | undefined {\n    return this.context.model.getParentData(id, hierarchy);\n  }\n\n  /**\n   * <zh/> 获取节点或组合的子元素数据\n   *\n   * <en/> Get the child element data of the node or combo\n   * @param id - <zh/> 节点或组合ID | <en/> node or combo ID\n   * @returns <zh/> 子元素数据 | <en/> child element data\n   * @apiCategory data\n   */\n  public getChildrenData(id: ID): NodeLikeData[] {\n    return this.context.model.getChildrenData(id);\n  }\n\n  /**\n   * <zh/> 获取节点或组合的后代元素数据\n   *\n   * <en/> Get the descendant element data of the node or combo\n   * @param id - <zh/> 节点或组合ID | <en/> node or combo ID\n   * @returns <zh/> 后代元素数据 | <en/> descendant element data\n   * @apiCategory data\n   */\n  public getDescendantsData(id: ID): NodeLikeData[] {\n    return this.context.model.getDescendantsData(id);\n  }\n\n  /**\n   * <zh/> 获取指定状态下的节点数据\n   *\n   * <en/> Get node data in a specific state\n   * @param state - <zh/> 状态 | <en/> state\n   * @returns <zh/> 节点数据 | <en/> node data\n   * @example\n   * ```ts\n   * const nodes = graph.getElementDataByState('node', 'selected');\n   * ```\n   * @apiCategory data\n   */\n  public getElementDataByState(elementType: 'node', state: State): NodeData[];\n  /**\n   * <zh/> 获取指定状态下的边数据\n   *\n   * <en/> Get edge data in a specific state\n   * @param state - <zh/> 状态 | <en/> state\n   * @returns <zh/> 边数据 | <en/> edge data\n   * @example\n   * ```ts\n   * const nodes = graph.getElementDataByState('edge', 'selected');\n   * ```\n   * @apiCategory data\n   */\n  public getElementDataByState(elementType: 'edge', state: State): EdgeData[];\n  /**\n   * <zh/> 获取指定状态下的组合数据\n   *\n   * <en/> Get combo data in a specific state\n   * @param state - <zh/> 状态 | <en/> state\n   * @returns <zh/> 组合数据 | <en/> combo data\n   * @example\n   * ```ts\n   * const nodes = graph.getElementDataByState('node', 'selected');\n   * ```\n   * @apiCategory data\n   */\n  public getElementDataByState(elementType: 'combo', state: State): ComboData[];\n  public getElementDataByState(elementType: ElementType, state: State): ElementDatum[] {\n    return this.context.model.getElementDataByState(elementType, state);\n  }\n\n  private async initCanvas() {\n    if (this.context.canvas) return await this.context.canvas.ready;\n\n    const {\n      container = 'container',\n      width,\n      height,\n      renderer,\n      cursor,\n      background,\n      canvas: canvasOptions,\n      devicePixelRatio = globalThis.devicePixelRatio ?? 1,\n    } = this.options;\n    if (container instanceof Canvas) {\n      this.context.canvas = container;\n      if (cursor) container.setCursor(cursor);\n      if (renderer) container.setRenderer(renderer);\n      await container.ready;\n    } else {\n      const $container = isString(container) ? document.getElementById(container!) : container;\n      const containerSize = sizeOf($container!);\n\n      this.emit(GraphEvent.BEFORE_CANVAS_INIT, { container: $container, width, height });\n      const options = {\n        ...canvasOptions,\n        container: $container!,\n        width: width || containerSize[0],\n        height: height || containerSize[1],\n        background,\n        renderer,\n        cursor,\n        devicePixelRatio,\n      };\n\n      const canvas = new Canvas(options);\n\n      this.context.canvas = canvas;\n      await canvas.ready;\n      this.emit(GraphEvent.AFTER_CANVAS_INIT, { canvas });\n    }\n  }\n\n  private updateCanvas(options: GraphOptions) {\n    const { renderer, cursor, height, width } = options;\n    const canvas = this.context.canvas;\n    if (!canvas) return;\n\n    if (renderer) {\n      this.emit(GraphEvent.BEFORE_RENDERER_CHANGE, { renderer: this.options.renderer });\n      canvas.setRenderer(renderer);\n      this.emit(GraphEvent.AFTER_RENDERER_CHANGE, { renderer });\n    }\n\n    if (cursor) canvas.setCursor(cursor);\n\n    if (isNumber(width) || isNumber(height))\n      this.setSize(width ?? this.options.width ?? 0, height ?? this.options.height ?? 0);\n  }\n\n  private initRuntime() {\n    this.context.options = this.options;\n    if (!this.context.batch) this.context.batch = new BatchController(this.context);\n    if (!this.context.plugin) this.context.plugin = new PluginController(this.context);\n    if (!this.context.viewport) this.context.viewport = new ViewportController(this.context);\n    if (!this.context.transform) this.context.transform = new TransformController(this.context);\n    if (!this.context.element) this.context.element = new ElementController(this.context);\n    if (!this.context.animation) this.context.animation = new Animation(this.context);\n    if (!this.context.layout) this.context.layout = new LayoutController(this.context);\n    if (!this.context.behavior) this.context.behavior = new BehaviorController(this.context);\n  }\n\n  private async prepare(): Promise<void> {\n    // 等待同步任务执行完成，避免 render 后立即调用 destroy 导致的问题\n    // Wait for synchronous tasks to complete, to avoid problems caused by calling destroy immediately after render\n    await Promise.resolve();\n\n    if (this.destroyed) {\n      // 如果图实例已经被销毁，则不再执行任何操作\n      // If the graph instance has been destroyed, no further operations will be performed\n      // eslint-disable-next-line no-console\n      console.error(format('The graph instance has been destroyed'));\n      return;\n    }\n\n    await this.initCanvas();\n    this.initRuntime();\n  }\n\n  /**\n   * <zh/> 执行渲染\n   *\n   * <en/> Render\n   * @remarks\n   * <zh/> 此过程会执行数据更新、绘制元素、执行布局\n   *\n   * > ⚠️ render 为异步方法，如果需要在 render 后执行一些操作，可以使用 `await graph.render()` 或者监听 GraphEvent.AFTER_RENDER 事件\n   *\n   * <en/> This process will execute data update, element rendering, and layout execution\n   *\n   * > ⚠️ render is an asynchronous method. If you need to perform some operations after render, you can use `await graph.render()` or listen to the GraphEvent.AFTER_RENDER event\n   * @apiCategory render\n   */\n  public async render(): Promise<void> {\n    await this.prepare();\n    emit(this, new GraphLifeCycleEvent(GraphEvent.BEFORE_RENDER));\n\n    if (!this.options.layout) {\n      const animation = this.context.element!.draw({ type: 'render' });\n      await Promise.all([animation?.finished, this.autoFit()]);\n    } else if (!this.rendered && isPreLayout(this.options.layout)) {\n      const animation = await this.context.element!.preLayoutDraw({ type: 'render' });\n      await Promise.all([animation?.finished, this.autoFit()]);\n    } else {\n      const animation = this.context.element!.draw({ type: 'render' });\n      await Promise.all([animation?.finished, this.context.layout!.postLayout()]);\n      await this.autoFit();\n    }\n\n    this.rendered = true;\n    emit(this, new GraphLifeCycleEvent(GraphEvent.AFTER_RENDER));\n  }\n\n  /**\n   * <zh/> 绘制元素\n   *\n   * <en/> Draw elements\n   * @returns <zh/> 渲染结果 | <en/> draw result\n   * @remarks\n   * <zh/> 仅执行元素绘制，不会重新布局\n   *\n   * <zh/> ⚠️ draw 为异步方法，如果需要在 draw 后执行一些操作，可以使用 `await graph.draw()` 或者监听 GraphEvent.AFTER_DRAW 事件\n   *\n   * <en/> Only execute element drawing, no re-layout\n   *\n   * <en/> ⚠️ draw is an asynchronous method. If you need to perform some operations after draw, you can use `await graph.draw()` or listen to the GraphEvent.AFTER_DRAW event\n   * @apiCategory render\n   */\n  public async draw(): Promise<void> {\n    await this.prepare();\n    await this.context.element!.draw()?.finished;\n  }\n\n  /**\n   * <zh/> 执行布局\n   *\n   * <en/> Execute layout\n   * @param layoutOptions - <zh/> 布局配置项 | <en/> Layout options\n   * @apiCategory layout\n   */\n  public async layout(layoutOptions?: LayoutOptions) {\n    await this.context.layout!.postLayout(layoutOptions);\n  }\n\n  /**\n   * <zh/> 停止布局\n   *\n   * <en/> Stop layout\n   * @remarks\n   * <zh/> 适用于带有迭代动画的布局，目前有 `force` 属于此类布局，即停止力导布局的迭代，一般用于布局迭代时间过长情况下的手动停止迭代动画，例如在点击画布/节点的监听中调用\n   *\n   * <en/> Suitable for layouts with iterative animations. Currently, `force` belongs to this type of layout, that is, stop the iteration of the force-directed layout. It is generally used to manually stop the iteration animation when the layout iteration time is too long, such as calling in the click canvas/node listener\n   * @apiCategory layout\n   */\n  public stopLayout() {\n    this.context.layout!.stopLayout();\n  }\n\n  /**\n   * <zh/> 清空画布元素\n   *\n   * <en/> Clear canvas elements\n   * @apiCategory canvas\n   */\n  public async clear(): Promise<void> {\n    const { model, element } = this.context;\n    model.setData({});\n    model.clearChanges();\n    element?.clear();\n  }\n\n  /**\n   * <zh/> 销毁当前图实例\n   *\n   * <en/> Destroy the current graph instance\n   * @remarks\n   * <zh/> 销毁后无法进行任何操作，如果需要重新使用，需要重新创建一个新的图实例\n   *\n   * <en/> After destruction, no operations can be performed. If you need to reuse it, you need to create a new graph instance\n   * @apiCategory instance\n   */\n  public destroy(): void {\n    emit(this, new GraphLifeCycleEvent(GraphEvent.BEFORE_DESTROY));\n\n    const { layout, animation, element, model, canvas, behavior, plugin } = this.context;\n    plugin?.destroy();\n    behavior?.destroy();\n    layout?.destroy();\n    animation?.destroy();\n    element?.destroy();\n    model.destroy();\n    canvas?.destroy();\n    this.options = {};\n    // @ts-expect-error force delete\n    this.context = {};\n\n    this.off();\n    globalThis.removeEventListener?.('resize', this.onResize);\n\n    this.destroyed = true;\n\n    emit(this, new GraphLifeCycleEvent(GraphEvent.AFTER_DESTROY));\n  }\n\n  /**\n   * <zh/> 获取画布实例\n   *\n   * <en/> Get canvas instance\n   * @returns - <zh/> 画布实例 | <en/> canvas instance\n   * @apiCategory canvas\n   */\n  public getCanvas(): Canvas {\n    return this.context.canvas;\n  }\n\n  /**\n   * <zh/> 调整画布大小为图容器大小\n   *\n   * <en/> Resize the canvas to the size of the graph container\n   * @apiCategory viewport\n   */\n  public resize(): void;\n  /**\n   * <zh/> 调整画布大小为指定宽高\n   *\n   * <en/> Resize the canvas to the specified width and height\n   * @param width - <zh/> 宽度 | <en/> width\n   * @param height - <zh/> 高度 | <en/> height\n   * @apiCategory viewport\n   */\n  public resize(width: number, height: number): void;\n  public resize(width?: number, height?: number): void {\n    const containerSize = sizeOf(this.context.canvas?.getContainer());\n    const specificSize: Vector2 = [width || containerSize[0], height || containerSize[1]];\n\n    if (!this.context.canvas) return;\n\n    const canvasSize = this.context.canvas!.getSize();\n    if (isEqual(specificSize, canvasSize)) return;\n\n    emit(this, new GraphLifeCycleEvent(GraphEvent.BEFORE_SIZE_CHANGE, { size: specificSize }));\n    this.context.canvas.resize(...specificSize);\n    emit(this, new GraphLifeCycleEvent(GraphEvent.AFTER_SIZE_CHANGE, { size: specificSize }));\n  }\n\n  /**\n   * <zh/> 将图缩放至合适大小并平移至视口中心\n   *\n   * <en/> Zoom the graph to fit the viewport and move it to the center of the viewport\n   * @param options - <zh/> 适配配置 | <en/> fit options\n   * @param animation - <zh/> 动画配置 | <en/> animation options\n   * @apiCategory viewport\n   */\n  public async fitView(options?: FitViewOptions, animation?: ViewportAnimationEffectTiming): Promise<void> {\n    await this.context.viewport?.fitView(options, animation);\n  }\n\n  /**\n   * <zh/> 将图平移至视口中心\n   *\n   * <en/> Move the graph to the center of the viewport\n   * @param animation - <zh/> 动画配置 | <en/> animation options\n   * @apiCategory viewport\n   */\n  public async fitCenter(animation?: ViewportAnimationEffectTiming): Promise<void> {\n    await this.context.viewport?.fitCenter({ animation });\n  }\n\n  private async autoFit(): Promise<void> {\n    const { autoFit } = this.context.options;\n    if (!autoFit) return;\n\n    if (isString(autoFit)) {\n      if (autoFit === 'view') await this.fitView();\n      else if (autoFit === 'center') await this.fitCenter();\n    } else {\n      const { type, animation } = autoFit;\n      if (type === 'view') await this.fitView(autoFit.options, animation);\n      else if (type === 'center') await this.fitCenter(animation);\n    }\n  }\n\n  /**\n   * <zh/> 聚焦元素\n   *\n   * <en/> Focus on element\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @param animation - <zh/> 动画配置 | <en/> animation options\n   * @remarks\n   * <zh/> 移动图，使得元素对齐到视口中心\n   *\n   * <en/> Move the graph so that the element is aligned to the center of the viewport\n   * @apiCategory viewport\n   */\n  public async focusElement(id: ID | ID[], animation?: ViewportAnimationEffectTiming): Promise<void> {\n    await this.context.viewport?.focusElements(Array.isArray(id) ? id : [id], { animation });\n  }\n\n  /**\n   * <zh/> 基于当前缩放比例进行缩放（相对缩放）\n   *\n   * <en/> Zoom based on the current zoom ratio (relative zoom)\n   * @param ratio - <zh/> 缩放比例 | <en/> zoom ratio\n   * @param animation - <zh/> 动画配置 | <en/> animation options\n   * @param origin - <zh/> 缩放中心(视口坐标) | <en/> zoom center(viewport coordinates)\n   * @remarks\n   * <zh/>\n   * - ratio > 1 放大\n   * - ratio < 1 缩小\n   *\n   * <en/>\n   * - ratio > 1 zoom in\n   * - ratio < 1 zoom out\n   * @apiCategory viewport\n   */\n  public async zoomBy(ratio: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise<void> {\n    await this.context.viewport!.transform({ mode: 'relative', scale: ratio, origin }, animation);\n  }\n\n  /**\n   * <zh/> 缩放画布至指定比例（绝对缩放）\n   *\n   * <en/> Zoom the canvas to the specified ratio (absolute zoom)\n   * @param zoom - <zh/> 指定缩放比例 | <en/> specified zoom ratio\n   * @param animation - <zh/> 动画配置 | <en/> animation options\n   * @param origin - <zh/> 缩放中心(视口坐标) | <en/> zoom center(viewport coordinates)\n   * @remarks\n   * <zh/>\n   * - zoom = 1 默认大小\n   * - zoom > 1 放大\n   * - zoom < 1 缩小\n   *\n   * <en/>\n   * - zoom = 1 default size\n   * - zoom > 1 zoom in\n   * - zoom < 1 zoom out\n   * @apiCategory viewport\n   */\n  public async zoomTo(zoom: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise<void> {\n    await this.context.viewport!.transform({ mode: 'absolute', scale: zoom, origin }, animation);\n  }\n\n  /**\n   * <zh/> 获取当前缩放比例\n   *\n   * <en/> Get the current zoom ratio\n   * @returns <zh/> 缩放比例 | <en/> zoom ratio\n   * @apiCategory viewport\n   */\n  public getZoom(): number {\n    return this.context.viewport!.getZoom();\n  }\n\n  /**\n   * <zh/> 基于当前旋转角度进行旋转（相对旋转）\n   *\n   * <en/> Rotate based on the current rotation angle (relative rotation)\n   * @param angle - <zh/> 旋转角度 | <en/> rotation angle\n   * @param animation - <zh/> 动画配置 | <en/> animation options\n   * @param origin - <zh/> 旋转中心(视口坐标) | <en/> rotation center(viewport coordinates)\n   * @apiCategory viewport\n   */\n  public async rotateBy(angle: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise<void> {\n    await this.context.viewport!.transform({ mode: 'relative', rotate: angle, origin }, animation);\n  }\n\n  /**\n   * <zh/> 旋转画布至指定角度 (绝对旋转)\n   *\n   * <en/> Rotate the canvas to the specified angle (absolute rotation)\n   * @param angle - <zh/> 目标角度 | <en/> target angle\n   * @param animation - <zh/> 动画配置 | <en/> animation options\n   * @param origin - <zh/> 旋转中心(视口坐标) | <en/> rotation center(viewport coordinates)\n   * @apiCategory viewport\n   */\n  public async rotateTo(angle: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise<void> {\n    await this.context.viewport!.transform({ mode: 'absolute', rotate: angle, origin }, animation);\n  }\n\n  /**\n   * <zh/> 获取当前旋转角度\n   *\n   * <en/> Get the current rotation angle\n   * @returns <zh/> 旋转角度 | <en/> rotation angle\n   * @apiCategory viewport\n   */\n  public getRotation(): number {\n    return this.context.viewport!.getRotation();\n  }\n\n  /**\n   * <zh/> 将图平移指定距离 (相对平移)\n   *\n   * <en/> Translate the graph by the specified distance (relative translation)\n   * @param offset - <zh/> 偏移量 | <en/> offset\n   * @param animation - <zh/> 动画配置 | <en/> animation options\n   * @apiCategory viewport\n   */\n  public async translateBy(offset: Point, animation?: ViewportAnimationEffectTiming): Promise<void> {\n    await this.context.viewport!.transform({ mode: 'relative', translate: offset }, animation);\n  }\n\n  /**\n   * <zh/> 将图平移至指定位置 (绝对平移)\n   *\n   * <en/> Translate the graph to the specified position (absolute translation)\n   * @param position - <zh/> 指定位置 | <en/> specified position\n   * @param animation - <zh/> 动画配置 | <en/> animation options\n   * @apiCategory viewport\n   */\n  public async translateTo(position: Point, animation?: ViewportAnimationEffectTiming): Promise<void> {\n    await this.context.viewport!.transform({ mode: 'absolute', translate: position }, animation);\n  }\n\n  /**\n   * <zh/> 获取图的位置\n   *\n   * <en/> Get the position of the graph\n   * @returns <zh/> 图的位置 | <en/> position of the graph\n   * @remarks\n   * <zh/> 即画布原点在视口坐标系下的位置。默认状态下，图的位置为 [0, 0]\n   *\n   * <en/> That is, the position of the canvas origin in the viewport coordinate system. By default, the position of the graph is [0, 0]\n   * @apiCategory viewport\n   */\n  public getPosition(): Point {\n    return subtract([0, 0], this.getCanvasByViewport([0, 0]));\n  }\n\n  /**\n   * <zh/> 将元素平移指定距离 (相对平移)\n   *\n   * <en/> Translate the element by the specified distance (relative translation)\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @param offset - <zh/> 偏移量 | <en/> offset\n   * @param animation - <zh/> 是否启用动画 | <en/> whether to enable animation\n   * @apiCategory element\n   */\n  public async translateElementBy(id: ID, offset: Point, animation?: boolean): Promise<void>;\n  /**\n   * <zh/> 批量将元素平移指定距离 (相对平移)\n   *\n   * <en/> Batch translate elements by the specified distance (relative translation)\n   * @param offsets - <zh/> 偏移量配置 | <en/> offset options\n   * @param animation - <zh/> 是否启用动画 | <en/> whether to enable animation\n   * @apiCategory element\n   */\n  public async translateElementBy(offsets: Record<ID, Point>, animation?: boolean): Promise<void>;\n  public async translateElementBy(\n    args1: ID | Record<ID, Point>,\n    args2?: Point | boolean,\n    args3: boolean = true,\n  ): Promise<void> {\n    const [config, animation] = isObject(args1)\n      ? [args1, (args2 as boolean) ?? true]\n      : [{ [args1 as ID]: args2 as Point }, args3];\n\n    Object.entries(config).forEach(([id, offset]) => this.context.model.translateNodeLikeBy(id, offset));\n    await this.context.element!.draw({ animation, stage: 'translate' })?.finished;\n  }\n\n  /**\n   * <zh/> 将元素平移至指定位置 (绝对平移)\n   *\n   * <en/> Translate the element to the specified position (absolute translation)\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @param position - <zh/> 指定位置 | <en/> specified position\n   * @param animation - <zh/> 是否启用动画 | <en/> whether to enable animation\n   * @apiCategory element\n   */\n  public async translateElementTo(id: ID, position: Point, animation?: boolean): Promise<void>;\n  /**\n   * <zh/> 批量将元素平移至指定位置 (绝对平移)\n   *\n   * <en/> Batch translate elements to the specified position (absolute translation)\n   * @param positions - <zh/> 位置配置 | <en/> position options\n   * @param animation - <zh/> 是否启用动画 | <en/> whether to enable animation\n   * @apiCategory element\n   */\n  public async translateElementTo(positions: Record<ID, Point>, animation?: boolean): Promise<void>;\n  public async translateElementTo(\n    args1: ID | Record<ID, Point>,\n    args2?: boolean | Point,\n    args3: boolean = true,\n  ): Promise<void> {\n    const [config, animation] = isObject(args1)\n      ? [args1, (args2 as boolean) ?? true]\n      : [{ [args1 as ID]: args2 as Point }, args3];\n\n    Object.entries(config).forEach(([id, position]) => this.context.model.translateNodeLikeTo(id, position));\n    await this.context.element!.draw({ animation, stage: 'translate' })?.finished;\n  }\n\n  /**\n   * <zh/> 获取元素位置\n   *\n   * <en/> Get element position\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @returns <zh/> 元素位置 | <en/> element position\n   * @apiCategory element\n   */\n  public getElementPosition(id: ID): Point {\n    return this.context.model.getElementPosition(id);\n  }\n\n  /**\n   * <zh/> 获取元素渲染样式\n   *\n   * <en/> Get element rendering style\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @returns <zh/> 元素渲染样式 | <en/> element rendering style\n   * @apiCategory element\n   */\n  public getElementRenderStyle(id: ID): Record<string, any> {\n    return omit(this.context.element!.getElement(id)!.attributes, ['context']);\n  }\n\n  /**\n   * <zh/> 设置元素可见性\n   *\n   * <en/> Set element visibility\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @param visibility - <zh/> 可见性 | <en/> visibility\n   * @param animation - <zh/> 动画配置 | <en/> animation options\n   * @remarks\n   * <zh/> 可见性配置包括 `visible` 和 `hidden` 两种状态\n   *\n   * <en/> Visibility configuration includes two states: `visible` and `hidden`\n   * @apiCategory element\n   */\n  public async setElementVisibility(\n    id: ID,\n    visibility: BaseStyleProps['visibility'],\n    animation?: boolean,\n  ): Promise<void>;\n  /**\n   * <zh/> 批量设置元素可见性\n   *\n   * <en/> Batch set element visibility\n   * @param visibility - <zh/> 可见性配置 | <en/> visibility options\n   * @param animation - <zh/> 动画配置 | <en/> animation options\n   * @apiCategory element\n   */\n  public async setElementVisibility(\n    visibility: Record<ID, BaseStyleProps['visibility']>,\n    animation?: boolean,\n  ): Promise<void>;\n  public async setElementVisibility(\n    args1: ID | Record<ID, BaseStyleProps['visibility']>,\n    args2?: boolean | BaseStyleProps['visibility'],\n    args3: boolean = true,\n  ): Promise<void> {\n    const [config, animation] = isObject(args1)\n      ? [args1, (args2 as boolean) ?? true]\n      : [{ [args1]: args2 as BaseStyleProps['visibility'] }, args3];\n\n    const dataToUpdate: Required<PartialGraphData> = { nodes: [], edges: [], combos: [] };\n    Object.entries(config).forEach(([id, value]) => {\n      const elementType = this.getElementType(id);\n      dataToUpdate[`${elementType}s`].push({ id, style: { visibility: value } });\n    });\n\n    const { model, element } = this.context;\n    model.preventUpdateNodeLikeHierarchy(() => {\n      model.updateData(dataToUpdate);\n    });\n    await element!.draw({ animation, stage: 'visibility' })?.finished;\n  }\n\n  /**\n   * <zh/> 显示元素\n   *\n   * <en/> Show element\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @param animation - <zh/> 是否启用动画 | <en/> whether to enable animation\n   * @apiCategory element\n   */\n  public async showElement(id: ID | ID[], animation?: boolean): Promise<void> {\n    const ids = Array.isArray(id) ? id : [id];\n    await this.setElementVisibility(\n      Object.fromEntries(ids.map((_id) => [_id, 'visible'] as [ID, BaseStyleProps['visibility']])),\n      animation,\n    );\n  }\n\n  /**\n   * <zh/> 隐藏元素\n   *\n   * <en/> Hide element\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @param animation - <zh/> 是否启用动画 | <en/> whether to enable animation\n   * @apiCategory element\n   */\n  public async hideElement(id: ID | ID[], animation?: boolean): Promise<void> {\n    const ids = Array.isArray(id) ? id : [id];\n    await this.setElementVisibility(\n      Object.fromEntries(ids.map((_id) => [_id, 'hidden'] as [ID, BaseStyleProps['visibility']])),\n      animation,\n    );\n  }\n\n  /**\n   * <zh/> 获取元素可见性\n   *\n   * <en/> Get element visibility\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @returns <zh/> 元素可见性 | <en/> element visibility\n   * @apiCategory element\n   */\n  public getElementVisibility(id: ID): BaseStyleProps['visibility'] {\n    const element = this.context.element!.getElement(id)!;\n    return element?.style?.visibility ?? 'visible';\n  }\n\n  /**\n   * <zh/> 设置元素层级\n   *\n   * <en/> Set element z-index\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @param zIndex - <zh/> 层级 | <en/> z-index\n   * @apiCategory element\n   */\n  public async setElementZIndex(id: ID, zIndex: number): Promise<void>;\n  /**\n   * <zh/> 批量设置元素层级\n   *\n   * <en/> Batch set element z-index\n   * @param zIndex - <zh/> 层级配置 | <en/> z-index options\n   * @apiCategory element\n   */\n  public async setElementZIndex(zIndex: Record<ID, number>): Promise<void>;\n  public async setElementZIndex(args1: ID | Record<ID, number>, args2?: number): Promise<void> {\n    const dataToUpdate: Required<PartialGraphData> = { nodes: [], edges: [], combos: [] };\n    const config = isObject(args1) ? args1 : { [args1 as ID]: args2 as number };\n\n    Object.entries(config).forEach(([id, value]) => {\n      const elementType = this.getElementType(id);\n      dataToUpdate[`${elementType}s`].push({ id, style: { zIndex: value } });\n    });\n\n    const { model, element } = this.context;\n    model.preventUpdateNodeLikeHierarchy(() => model.updateData(dataToUpdate));\n    await element!.draw({ animation: false, stage: 'zIndex' })?.finished;\n  }\n\n  /**\n   * <zh/> 将元素置于最顶层\n   *\n   * <en/> Bring the element to the front\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @apiCategory element\n   */\n  public async frontElement(id: ID | ID[]): Promise<void> {\n    const ids = Array.isArray(id) ? id : [id];\n    const { model } = this.context;\n    const zIndexes: Record<ID, number> = {};\n\n    ids.map((_id) => {\n      const zIndex = model.getFrontZIndex(_id);\n      const elementType = model.getElementType(_id);\n      if (elementType === 'combo') {\n        const ancestor = model.getAncestorsData(_id, COMBO_KEY).at(-1) || this.getComboData(_id);\n        const descendants = [ancestor, ...model.getDescendantsData(idOf(ancestor))];\n        const delta = zIndex - getZIndexOf(ancestor);\n        descendants.forEach((combo) => {\n          zIndexes[idOf(combo)] = this.getElementZIndex(idOf(combo)) + delta;\n        });\n\n        const { internal } = getSubgraphRelatedEdges(descendants.map(idOf), (id) => model.getRelatedEdgesData(id));\n        internal.forEach((edge) => {\n          const edgeId = idOf(edge);\n          zIndexes[edgeId] = this.getElementZIndex(edgeId) + delta;\n        });\n      } else zIndexes[_id] = zIndex;\n    });\n\n    await this.setElementZIndex(zIndexes);\n  }\n\n  /**\n   * <zh/> 获取元素层级\n   *\n   * <en/> Get element z-index\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @returns <zh/> 元素层级 | <en/> element z-index\n   * @apiCategory element\n   */\n  public getElementZIndex(id: ID): number {\n    return getZIndexOf(this.context.model.getElementDataById(id));\n  }\n\n  /**\n   * <zh/> 设置元素状态\n   *\n   * <en/> Set element state\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @param state - <zh/> 状态 | <en/> state\n   * @param animation - <zh/> 动画配置 | <en/> animation options\n   * @apiCategory element\n   */\n  public async setElementState(id: ID, state: State | State[], animation?: boolean): Promise<void>;\n  /**\n   * <zh/> 批量设置元素状态\n   *\n   * <en/> Batch set element state\n   * @param state - <zh/> 状态配置 | <en/> state options\n   * @param animation - <zh/> 动画配置 | <en/> animation options\n   * @apiCategory element\n   */\n  public async setElementState(state: Record<ID, State | State[]>, animation?: boolean): Promise<void>;\n  public async setElementState(\n    args1: ID | Record<ID, State | State[]>,\n    args2?: boolean | State | State[],\n    args3: boolean = true,\n  ): Promise<void> {\n    const [config, animation] = isObject(args1)\n      ? [args1, (args2 as boolean) ?? true]\n      : [{ [args1]: args2 as State | State[] }, args3];\n\n    const parseState = (state: State | State[]) => {\n      if (!state) return [];\n      return Array.isArray(state) ? state : [state];\n    };\n\n    const dataToUpdate: Required<PartialGraphData> = { nodes: [], edges: [], combos: [] };\n    Object.entries(config).forEach(([id, value]) => {\n      const elementType = this.getElementType(id);\n      dataToUpdate[`${elementType}s`].push({ id, states: parseState(value) });\n    });\n    this.updateData(dataToUpdate);\n\n    await this.context.element!.draw({ animation, stage: 'state' })?.finished;\n  }\n\n  /**\n   * <zh/> 获取元素状态\n   *\n   * <en/> Get element state\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @returns <zh/> 元素状态 | <en/> element state\n   * @apiCategory element\n   */\n  public getElementState(id: ID): State[] {\n    return this.context.model.getElementState(id);\n  }\n\n  /**\n   * <zh/> 获取元素自身以及子节点在世界坐标系下的渲染包围盒\n   *\n   * <en/> Get the rendering bounding box of the element itself and its child nodes in the world coordinate system\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @returns <zh/> 渲染包围盒 | <en/> render bounding box\n   * @apiCategory element\n   */\n  public getElementRenderBounds(id: ID): AABB {\n    return this.context.element!.getElement(id)!.getRenderBounds();\n  }\n\n  private isCollapsingExpanding = false;\n\n  /**\n   * <zh/> 收起元素\n   *\n   * <en/> Collapse element\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @param options - <zh/> 是否启用动画或者配置收起节点的配置项 | <en/> whether to enable animation or the options of collapsing node\n   * @apiCategory element\n   */\n  public async collapseElement(id: ID, options: boolean | CollapseExpandNodeOptions = true): Promise<void> {\n    const { model, element } = this.context;\n    if (isCollapsed(model.getNodeLikeData([id])[0])) return;\n    if (this.isCollapsingExpanding) return;\n\n    if (typeof options === 'boolean') options = { animation: options, align: false };\n\n    const elementType = model.getElementType(id);\n\n    await this.frontElement(id);\n    this.isCollapsingExpanding = true;\n\n    // 更新折叠状态 / Update collapse style\n    model.updateData(\n      elementType === 'node'\n        ? {\n            nodes: [{ id, style: { collapsed: true } }],\n          }\n        : {\n            combos: [{ id, style: { collapsed: true } }],\n          },\n    );\n\n    if (elementType === 'node') await element!.collapseNode(id, options);\n    else if (elementType === 'combo') await element!.collapseCombo(id, !!options.animation);\n\n    this.isCollapsingExpanding = false;\n  }\n\n  /**\n   * <zh/> 展开元素\n   *\n   * <en/> Expand Element\n   * @param id - <zh/> 元素 ID | <en/> element ID\n   * @param animation - <zh/> 是否启用动画或者配置收起节点的配置项 | <en/> whether to enable animation or the options of collapsing node\n   * @param options\n   * @apiCategory element\n   */\n  public async expandElement(id: ID, options: boolean | CollapseExpandNodeOptions = true): Promise<void> {\n    const { model, element } = this.context;\n    if (!isCollapsed(model.getNodeLikeData([id])[0])) return;\n    if (this.isCollapsingExpanding) return;\n\n    if (typeof options === 'boolean') options = { animation: options, align: false };\n\n    const elementType = model.getElementType(id);\n\n    this.isCollapsingExpanding = true;\n\n    // 更新折叠状态 / Update collapse style\n    model.updateData(\n      elementType === 'node'\n        ? {\n            nodes: [{ id, style: { collapsed: false } }],\n          }\n        : {\n            combos: [{ id, style: { collapsed: false } }],\n          },\n    );\n\n    if (elementType === 'node') await element!.expandNode(id, options);\n    else if (elementType === 'combo') await element!.expandCombo(id, !!options.animation);\n\n    this.isCollapsingExpanding = false;\n  }\n\n  private setElementCollapsibility(id: ID, collapsed: boolean) {\n    const elementType = this.getElementType(id);\n    if (elementType === 'node') this.updateNodeData([{ id, style: { collapsed } }]);\n    else if (elementType === 'combo') this.updateComboData([{ id, style: { collapsed } }]);\n  }\n\n  /**\n   * <zh/> 导出画布内容为 DataURL\n   *\n   * <en/> Export canvas content as DataURL\n   * @param options - <zh/> 导出配置 | <en/> export options\n   * @returns <zh/> DataURL | <en/> DataURL\n   * @apiCategory exportImage\n   */\n  public async toDataURL(options: Partial<DataURLOptions> = {}): Promise<string> {\n    return this.context.canvas!.toDataURL(options);\n  }\n\n  /**\n   * <zh/> 给定的视窗 DOM 坐标，转换为画布上的绘制坐标\n   *\n   * <en/> Convert the given viewport DOM coordinates to the drawing coordinates on the canvas\n   * @param point - <zh/> 视窗坐标 | <en/> viewport coordinates\n   * @returns <zh/> 画布上的绘制坐标 | <en/> drawing coordinates on the canvas\n   * @apiCategory viewport\n   */\n  public getCanvasByViewport(point: Point): Point {\n    return this.context.canvas.getCanvasByViewport(point);\n  }\n\n  /**\n   * <zh/> 给定画布上的绘制坐标，转换为视窗 DOM 的坐标\n   *\n   * <en/> Convert the given drawing coordinates on the canvas to the coordinates of the viewport DOM\n   * @param point - <zh/> 画布坐标 | <en/> canvas coordinates\n   * @returns <zh/> 视窗 DOM 的坐标 | <en/> coordinates of the viewport DOM\n   * @apiCategory viewport\n   */\n  public getViewportByCanvas(point: Point): Point {\n    return this.context.canvas.getViewportByCanvas(point);\n  }\n\n  /**\n   * <zh/> 给定画布上的绘制坐标，转换为浏览器坐标\n   *\n   * <en/> Convert the given drawing coordinates on the canvas to browser coordinates\n   * @param point - <zh/> 画布坐标 | <en/> canvas coordinates\n   * @returns <zh/> 浏览器坐标 | <en/> browser coordinates\n   * @apiCategory viewport\n   */\n  public getClientByCanvas(point: Point): Point {\n    return this.context.canvas.getClientByCanvas(point);\n  }\n\n  /**\n   * <zh/> 给定的浏览器坐标，转换为画布上的绘制坐标\n   *\n   * <en/> Convert the given browser coordinates to drawing coordinates on the canvas\n   * @param point - <zh/> 浏览器坐标 | <en/> browser coordinates\n   * @returns <zh/> 画布上的绘制坐标 | <en/> drawing coordinates on the canvas\n   * @apiCategory viewport\n   */\n  public getCanvasByClient(point: Point): Point {\n    return this.context.canvas.getCanvasByClient(point);\n  }\n\n  /**\n   * <zh/> 获取视口中心的画布坐标\n   *\n   * <en/> Get the canvas coordinates of the viewport center\n   * @returns <zh/> 视口中心的画布坐标 | <en/> Canvas coordinates of the viewport center\n   * @apiCategory viewport\n   */\n  public getViewportCenter(): Point {\n    return this.context.viewport!.getViewportCenter();\n  }\n\n  /**\n   * <zh/> 获取视口中心的视口坐标\n   *\n   * <en/> Get the viewport coordinates of the viewport center\n   * @returns <zh/> 视口中心的视口坐标 | <en/> Viewport coordinates of the viewport center\n   * @apiCategory viewport\n   */\n  public getCanvasCenter(): Point {\n    return this.context.viewport!.getCanvasCenter();\n  }\n\n  private onResize = debounce(() => {\n    this.resize();\n  }, 300);\n\n  /**\n   * <zh/> 监听事件\n   *\n   * <en/> Listen to events\n   * @param eventName - <zh/> 事件名称 | <en/> event name\n   * @param callback - <zh/> 回调函数 | <en/> callback function\n   * @param once - <zh/> 是否只监听一次 | <en/> whether to listen only once\n   * @returns <zh/> Graph 实例 | <en/> Graph instance\n   * @apiCategory event\n   */\n  public on<T extends IEvent = IEvent>(eventName: string, callback: (event: T) => void, once?: boolean): this {\n    return super.on(eventName, callback, once);\n  }\n\n  /**\n   * <zh/> 一次性监听事件\n   *\n   * <en/> Listen to events once\n   * @param eventName - <zh/> 事件名称 | <en/> event name\n   * @param callback - <zh/> 回调函数 | <en/> callback function\n   * @returns <zh/> Graph 实例 | <en/> Graph instance\n   * @apiCategory event\n   */\n  public once<T extends IEvent = IEvent>(eventName: string, callback: (event: T) => void): this {\n    return super.once(eventName, callback);\n  }\n\n  /**\n   * <zh/> 移除全部事件监听\n   *\n   * <en/> Remove all event listeners\n   * @returns <zh/> Graph 实例 | <en/> Graph instance\n   * @apiCategory event\n   */\n  public off(): this;\n  /**\n   * <zh/> 移除指定事件的全部监听\n   *\n   * <en/> Remove all listeners for the specified event\n   * @param eventName - <zh/> 事件名称 | <en/> event name\n   * @returns <zh/> Graph 实例 | <en/> Graph instance\n   * @apiCategory event\n   */\n  public off(eventName: string): this;\n  /**\n   * <zh/> 移除事件监听\n   *\n   * <en/> Remove event listener\n   * @param eventName - <zh/> 事件名称 | <en/> event name\n   * @param callback - <zh/> 回调函数 | <en/> callback function\n   * @returns <zh/> Graph 实例 | <en/> Graph instance\n   * @apiCategory event\n   */\n  public off(eventName: string, callback: (...args: any[]) => void): this;\n  public off(eventName?: string, callback?: (...args: any[]) => void) {\n    return super.off(eventName, callback);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/runtime/layout.ts",
    "content": "import type { IAnimation } from '@antv/g';\nimport { Graph as Graphlib } from '@antv/graphlib';\nimport { isLayoutWithIterations } from '@antv/layout';\nimport { deepMix } from '@antv/util';\nimport { COMBO_KEY, GraphEvent, TREE_KEY } from '../constants';\nimport { BaseLayout } from '../layouts';\nimport { AntVLayout } from '../layouts/types';\nimport { getExtension } from '../registry/get';\nimport type { GraphData, LayoutOptions, NodeData } from '../spec';\nimport type { STDLayoutOptions } from '../spec/layout';\nimport type { DrawData } from '../transforms/types';\nimport type { ID, TreeData } from '../types';\nimport { getAnimationOptions } from '../utils/animation';\nimport { isCollapsed } from '../utils/collapsibility';\nimport { isToBeDestroyed } from '../utils/element';\nimport { emit, GraphLifeCycleEvent } from '../utils/event';\nimport { createTreeStructure } from '../utils/graphlib';\nimport { idOf } from '../utils/id';\nimport { isLegacyAntVLayout, isTreeLayout, layoutAdapter, legacyLayoutAdapter } from '../utils/layout';\nimport { print } from '../utils/print';\nimport { dfs } from '../utils/traverse';\nimport type { RuntimeContext } from './types';\n\nexport class LayoutController {\n  private context: RuntimeContext;\n\n  private instance?: BaseLayout;\n\n  private instances: BaseLayout[] = [];\n\n  private animationResult?: IAnimation | null;\n\n  private get presetOptions() {\n    return {\n      animation: !!getAnimationOptions(this.context.options, true),\n    };\n  }\n\n  private get options() {\n    const { options } = this.context;\n    return options.layout;\n  }\n\n  constructor(context: RuntimeContext) {\n    this.context = context;\n  }\n\n  public getLayoutInstance(): BaseLayout[] {\n    return this.instances;\n  }\n\n  /**\n   * <zh/> 前布局，即在绘制前执行布局\n   *\n   * <en/> Pre-layout, that is, perform layout before drawing\n   * @param data - <zh/> 绘制数据 | <en/> Draw data\n   * @remarks\n   * <zh/> 前布局应该只在首次绘制前执行，后续更新不会触发\n   *\n   * <en/> Pre-layout should only be executed before the first drawing, and subsequent updates will not trigger\n   */\n  public async preLayout(data: DrawData) {\n    const { graph, model } = this.context;\n\n    const { add } = data;\n    emit(graph, new GraphLifeCycleEvent(GraphEvent.BEFORE_LAYOUT, { type: 'pre' }));\n    const simulate = await this.context.layout?.simulate();\n    simulate?.nodes?.forEach((l) => {\n      const id = idOf(l);\n      const node = add.nodes.get(id);\n      model.syncNodeLikeDatum(l);\n      if (node) Object.assign(node.style!, l.style);\n    });\n    simulate?.edges?.forEach((l) => {\n      const id = idOf(l);\n      const edge = add.edges.get(id);\n      model.syncEdgeDatum(l);\n      if (edge) Object.assign(edge.style!, l.style);\n    });\n    simulate?.combos?.forEach((l) => {\n      const id = idOf(l);\n      const combo = add.combos.get(id);\n      model.syncNodeLikeDatum(l);\n      if (combo) Object.assign(combo.style!, l.style);\n    });\n    emit(graph, new GraphLifeCycleEvent(GraphEvent.AFTER_LAYOUT, { type: 'pre' }));\n    this.transformDataAfterLayout('pre', data);\n  }\n\n  /**\n   * <zh/> 后布局，即在完成绘制后执行布局\n   *\n   * <en/> Post layout, that is, perform layout after drawing\n   * @param layoutOptions - <zh/> 布局配置项 | <en/> Layout options\n   */\n  public async postLayout(layoutOptions: LayoutOptions | undefined = this.options) {\n    if (!layoutOptions) return;\n    const pipeline = Array.isArray(layoutOptions) ? layoutOptions : [layoutOptions];\n    const { graph } = this.context;\n    emit(graph, new GraphLifeCycleEvent(GraphEvent.BEFORE_LAYOUT, { type: 'post' }));\n\n    for (let index = 0; index < pipeline.length; index++) {\n      const options = pipeline[index];\n      const data = this.getLayoutData(options);\n      const opts = { ...this.presetOptions, ...options };\n\n      emit(graph, new GraphLifeCycleEvent(GraphEvent.BEFORE_STAGE_LAYOUT, { options: opts, index }));\n      const result = await this.stepLayout(data, opts, index);\n      emit(graph, new GraphLifeCycleEvent(GraphEvent.AFTER_STAGE_LAYOUT, { options: opts, index }));\n\n      if (!options.animation) {\n        this.updateElementPosition(result, false);\n      }\n    }\n    emit(graph, new GraphLifeCycleEvent(GraphEvent.AFTER_LAYOUT, { type: 'post' }));\n    this.transformDataAfterLayout('post');\n  }\n\n  private transformDataAfterLayout(type: 'pre' | 'post', data?: DrawData) {\n    const transforms = this.context.transform.getTransformInstance();\n    // @ts-expect-error skip type check\n    Object.values(transforms).forEach((transform) => transform.afterLayout(type, data));\n  }\n\n  /**\n   * <zh/> 模拟布局\n   *\n   * <en/> Simulate layout\n   * @param options - <zh/> 布局配置项 | <en/> Layout options\n   * @returns <zh/> 模拟布局结果 | <en/> Simulated layout result\n   */\n  public async simulate(options: LayoutOptions | undefined = this.options): Promise<GraphData> {\n    if (!options) return {};\n    const pipeline = Array.isArray(options) ? options : [options];\n\n    let simulation: GraphData = {};\n\n    for (let index = 0; index < pipeline.length; index++) {\n      const options = pipeline[index];\n\n      const data = this.getLayoutData(options);\n      const result = await this.stepLayout(data, { ...this.presetOptions, ...options, animation: false }, index);\n\n      simulation = result;\n    }\n\n    return simulation;\n  }\n\n  public async stepLayout(data: GraphData, options: STDLayoutOptions, index: number): Promise<GraphData> {\n    if (isTreeLayout(options)) return await this.treeLayout(data, options, index);\n    return await this.graphLayout(data, options, index);\n  }\n\n  private async graphLayout(data: GraphData, options: STDLayoutOptions, index: number): Promise<GraphData> {\n    const { animation, iterations = 300 } = options;\n\n    const layout = this.initGraphLayout(options);\n    if (!layout) return {};\n\n    this.instances[index] = layout;\n    this.instance = layout;\n\n    if (isLayoutWithIterations(layout)) {\n      // 有动画，基于布局迭代 tick 更新位置 / Update position based on layout iteration tick\n      if (animation) {\n        return await layout.execute(data, {\n          animate: true,\n          maxIteration: iterations,\n          onTick: (tickData: GraphData) => this.updateElementPosition(tickData, false),\n        });\n      }\n\n      // 无动画，直接返回终态位置 / No animation, return final position directly\n      layout.execute(data);\n      layout.stop();\n      return layout.tick(iterations);\n    }\n\n    // 无迭代的布局，直接返回终态位置 / Layout without iteration, return final position directly\n    const layoutResult = await layout.execute(data);\n\n    if (animation) {\n      const animationResult = this.updateElementPosition(layoutResult, animation);\n      await animationResult?.finished;\n    }\n    return layoutResult;\n  }\n\n  private async treeLayout(data: GraphData, options: STDLayoutOptions, index: number): Promise<GraphData> {\n    const { type, animation } = options;\n    // @ts-expect-error @antv/hierarchy 布局格式与 @antv/layout 不一致，其导出的是一个方法，而非 class\n    // The layout format of @antv/hierarchy is inconsistent with @antv/layout, it exports a method instead of a class\n    const layout = getExtension('layout', type) as (tree: TreeData, options: STDLayoutOptions) => TreeData;\n    if (!layout) return {};\n\n    const { nodes = [], edges = [] } = data;\n\n    const model = new Graphlib({\n      nodes: nodes.map((node) => ({ id: idOf(node), data: node.data || {} })),\n      edges: edges.map((edge) => ({ id: idOf(edge), source: edge.source, target: edge.target, data: edge.data || {} })),\n    });\n\n    createTreeStructure(model);\n\n    const layoutPreset: GraphData = { nodes: [], edges: [] };\n    const layoutResult: GraphData = { nodes: [], edges: [] };\n\n    const roots = model.getRoots(TREE_KEY) as unknown as TreeData[];\n    roots.forEach((root) => {\n      dfs(\n        root,\n        (node) => {\n          node.children = model.getSuccessors(node.id) as TreeData[];\n        },\n        (node) => model.getSuccessors(node.id) as TreeData[],\n        'TB',\n      );\n\n      const result = layout(root, options);\n      const { x: rx, y: ry, z: rz = 0 } = result;\n      // 将布局结果转化为 LayoutMapping 格式 / Convert the layout result to LayoutMapping format\n      dfs(\n        result,\n        (node) => {\n          const { id, x, y, z = 0 } = node;\n          layoutPreset.nodes!.push({ id, style: { x: rx, y: ry, z: rz } });\n          layoutResult.nodes!.push({ id, style: { x, y, z } });\n        },\n        (node) => node.children,\n        'TB',\n      );\n    });\n\n    const offset = this.inferTreeLayoutOffset(layoutResult);\n    applyTreeLayoutOffset(layoutResult, offset);\n\n    if (animation) {\n      // 先将所有节点移动到根节点位置 / Move all nodes to the root node position first\n      applyTreeLayoutOffset(layoutPreset, offset);\n      this.updateElementPosition(layoutPreset, false);\n\n      const animationResult = this.updateElementPosition(layoutResult, animation);\n      await animationResult?.finished;\n    }\n\n    return layoutResult;\n  }\n\n  private inferTreeLayoutOffset(data: GraphData) {\n    let [minX, maxX] = [Infinity, -Infinity];\n    let [minY, maxY] = [Infinity, -Infinity];\n\n    data.nodes?.forEach((node) => {\n      const { x = 0, y = 0 } = node.style || {};\n      minX = Math.min(minX, x);\n      maxX = Math.max(maxX, x);\n      minY = Math.min(minY, y);\n      maxY = Math.max(maxY, y);\n    });\n\n    const { canvas } = this.context;\n    const canvasSize = canvas.getSize();\n    const [x1, y1] = canvas.getCanvasByViewport([0, 0]);\n    const [x2, y2] = canvas.getCanvasByViewport(canvasSize);\n\n    if (minX >= x1 && maxX <= x2 && minY >= y1 && maxY <= y2) return [0, 0] as [number, number];\n\n    const cx = (x1 + x2) / 2;\n    const cy = (y1 + y2) / 2;\n\n    return [cx - (minX + maxX) / 2, cy - (minY + maxY) / 2] as [number, number];\n  }\n\n  public stopLayout() {\n    if (this.instance && isLayoutWithIterations(this.instance)) {\n      this.instance.stop();\n      this.instance = undefined;\n    }\n\n    if (this.animationResult) {\n      this.animationResult.finish();\n      this.animationResult = undefined;\n    }\n  }\n\n  public getLayoutData(options: STDLayoutOptions): GraphData {\n    const {\n      nodeFilter = () => true,\n      comboFilter = () => true,\n      preLayout = false,\n      isLayoutInvisibleNodes = false,\n    } = options;\n    const { nodes, edges, combos } = this.context.model.getData();\n\n    const { element, model } = this.context;\n    const getElement = (id: ID) => element!.getElement(id);\n\n    const filterFn = preLayout\n      ? (node: NodeData) => {\n          if (!isLayoutInvisibleNodes) {\n            if (node.style?.visibility === 'hidden') return false;\n            if (model.getAncestorsData(node.id, TREE_KEY).some(isCollapsed)) return false;\n            if (model.getAncestorsData(node.id, COMBO_KEY).some(isCollapsed)) return false;\n          }\n          return nodeFilter(node);\n        }\n      : (node: NodeData) => {\n          const id = idOf(node);\n          const element = getElement(id);\n          if (!element) return false;\n          if (isToBeDestroyed(element)) return false;\n          return nodeFilter(node);\n        };\n\n    const nodesToLayout = nodes.filter(filterFn);\n    const combosToLayout = combos.filter(comboFilter);\n\n    const nodeLikeIdsMap = new Map<ID, NodeData>(nodesToLayout.map((node) => [idOf(node), node]));\n    combosToLayout.forEach((combo) => nodeLikeIdsMap.set(idOf(combo), combo));\n\n    const edgesToLayout = edges.filter(({ source, target }) => {\n      return nodeLikeIdsMap.has(source) && nodeLikeIdsMap.has(target);\n    });\n\n    return {\n      nodes: nodesToLayout,\n      edges: edgesToLayout,\n      combos: combosToLayout,\n    };\n  }\n\n  /**\n   * <zh/> 创建布局实例\n   *\n   * <en/> Create layout instance\n   * @param options - <zh/> 布局配置项 | <en/> Layout options\n   * @returns <zh/> 布局对象 | <en/> Layout object\n   */\n  private initGraphLayout(options: STDLayoutOptions) {\n    const { element, viewport } = this.context;\n    const { type, animation, iterations, ...restOptions } = options;\n\n    const [width, height] = viewport!.getCanvasSize();\n    const center = [width / 2, height / 2];\n\n    const nodeSize: number | ((node: NodeData) => number) =\n      (options?.nodeSize as number) ??\n      ((node) => {\n        const nodeElement = element?.getElement(node.id);\n        if (nodeElement) return nodeElement.attributes.size;\n        return element?.getElementComputedStyle('node', node).size;\n      });\n\n    const Ctor = getExtension('layout', type);\n    if (!Ctor) return print.warn(`The layout of ${type} is not registered.`);\n\n    const STDCtor =\n      Object.getPrototypeOf(Ctor.prototype) === BaseLayout.prototype\n        ? Ctor\n        : isLegacyAntVLayout(Ctor)\n          ? legacyLayoutAdapter(Ctor, this.context)\n          : layoutAdapter(Ctor as new (options?: Record<string, unknown>) => AntVLayout, this.context);\n\n    const layout = new STDCtor(this.context);\n\n    const config = { nodeSize, width, height, center };\n\n    switch (layout.id) {\n      case 'd3-force':\n      case 'd3-force-3d':\n        Object.assign(config, {\n          center: { x: width / 2, y: height / 2, z: 0 },\n        });\n        break;\n      default:\n        break;\n    }\n\n    deepMix(layout.options, config, restOptions);\n    return layout as unknown as BaseLayout;\n  }\n\n  private updateElementPosition(layoutResult: GraphData, animation: boolean) {\n    const { model, element } = this.context;\n    if (!element) return null;\n    model.updateData(layoutResult);\n\n    return element.draw({ animation, silence: true });\n  }\n\n  public destroy() {\n    this.stopLayout();\n    // @ts-expect-error force delete\n    this.context = {};\n    this.instance = undefined;\n    this.instances = [];\n    this.animationResult = undefined;\n  }\n}\n\n/**\n * <zh/> 对树形布局结果应用偏移\n *\n * <en/> Apply offset to tree layout result\n * @param data - <zh/> 布局数据 | <en/> Layout data\n * @param offset - <zh/> 偏移量 | <en/> Offset\n */\nconst applyTreeLayoutOffset = (data: GraphData, offset: [number, number]) => {\n  const [ox, oy] = offset;\n  data.nodes?.forEach((node) => {\n    if (node.style) {\n      const { x = 0, y = 0 } = node.style;\n      node.style.x = x + ox;\n      node.style.y = y + oy;\n    } else {\n      node.style = { x: ox, y: oy };\n    }\n  });\n};\n"
  },
  {
    "path": "packages/g6/src/runtime/options.ts",
    "content": "import type { GraphOptions } from '../spec';\n\n/**\n * <zh/> 基于用户传入的配置，推断出最终的配置\n *\n * <en/> Infer the final configuration based on the configuration passed by the user\n * @param options - <zh/> 用户传入的配置 | <en/> Configuration passed by the user\n * @returns <zh/> 最终的配置 | <en/> Final configuration\n */\nexport function inferOptions(options: GraphOptions): GraphOptions {\n  const flow = [inferLayoutOptions];\n  return flow.reduce((acc, infer) => infer(acc), options);\n}\n\n/**\n * <zh/> 推断布局配置\n *\n * <en/> Infer layout configuration\n * @param options - <zh/> 用户传入的配置 | <en/> Configuration passed by the user\n * @returns <zh/> 最终的配置 | <en/> Final configuration\n */\nfunction inferLayoutOptions(options: GraphOptions): GraphOptions {\n  if (!options.layout) return options;\n  if (Array.isArray(options.layout)) return options;\n  if ('preLayout' in options.layout) return options;\n\n  if (\n    [\n      'antv-dagre',\n      'combo-combined',\n      'compact-box',\n      'circular',\n      'concentric',\n      'dagre',\n      'fishbone',\n      'grid',\n      'indented',\n      'mds',\n      'radial',\n      'random',\n      'snake',\n      // <zh/> 下列布局的标签位置待适配，需要手动配置 preLayout false\n      // <en/> The label position of the following layouts needs to be adapted, and preLayout needs to be manually configured as false\n      'dendrogram',\n      'mindmap',\n    ].includes(options.layout.type)\n  ) {\n    options.layout.preLayout = true;\n  }\n\n  return options;\n}\n"
  },
  {
    "path": "packages/g6/src/runtime/plugin.ts",
    "content": "import type { BasePlugin } from '../plugins/base-plugin';\nimport { ExtensionController } from '../registry/extension';\nimport type { CustomPluginOption, PluginOptions } from '../spec/plugin';\nimport { print } from '../utils/print';\nimport type { RuntimeContext } from './types';\n\nexport class PluginController extends ExtensionController<BasePlugin<CustomPluginOption>> {\n  public category = 'plugin' as const;\n\n  constructor(context: RuntimeContext) {\n    super(context);\n    this.setPlugins(this.context.options.plugins || []);\n  }\n\n  public setPlugins(plugins: PluginOptions) {\n    this.setExtensions(plugins);\n  }\n\n  public getPluginInstance(key: string) {\n    const exactly = this.extensionMap[key];\n    if (exactly) return exactly;\n\n    print.warn(`Cannot find the plugin ${key}, will try to find it by type.`);\n\n    const fussily = this.extensions.find((extension) => extension.type === key);\n    if (fussily) return this.extensionMap[fussily.key];\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/runtime/transform.ts",
    "content": "import { ExtensionController } from '../registry/extension';\nimport type { CustomTransformOption, TransformOptions } from '../spec/transform';\nimport { BaseTransform } from '../transforms';\nimport type { RuntimeContext } from './types';\n\nexport const REQUIRED_TRANSFORMS: TransformOptions = [\n  'update-related-edges',\n  'collapse-expand-node',\n  'collapse-expand-combo',\n  'get-edge-actual-ends',\n  'arrange-draw-order',\n];\n\nexport class TransformController extends ExtensionController<BaseTransform<CustomTransformOption>> {\n  public category = 'transform' as const;\n\n  constructor(context: RuntimeContext) {\n    super(context);\n    this.setTransforms(this.context.options.transforms || []);\n  }\n\n  protected getTransforms() {}\n\n  public setTransforms(transforms: TransformOptions) {\n    this.setExtensions([\n      ...REQUIRED_TRANSFORMS.slice(0, REQUIRED_TRANSFORMS.length - 1),\n      ...transforms,\n      REQUIRED_TRANSFORMS[REQUIRED_TRANSFORMS.length - 1],\n    ]);\n  }\n\n  public getTransformInstance(): Record<string, BaseTransform>;\n  public getTransformInstance(key: string): BaseTransform;\n  public getTransformInstance(key?: string) {\n    return key ? this.extensionMap[key] : this.extensionMap;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/runtime/types.ts",
    "content": "import type { GraphOptions } from '../spec';\nimport type { Animation } from './animation';\nimport type { BatchController } from './batch';\nimport type { BehaviorController } from './behavior';\nimport type { Canvas } from './canvas';\nimport type { DataController } from './data';\nimport type { ElementController } from './element';\nimport type { Graph } from './graph';\nimport type { LayoutController } from './layout';\nimport type { PluginController } from './plugin';\nimport type { TransformController } from './transform';\nimport type { ViewportController } from './viewport';\n\nexport interface RuntimeContext {\n  /**\n   * <zh/> 图实例\n   *\n   * <en/> Graph instance\n   */\n  graph: Graph;\n  /**\n   * <zh/> 画布实例\n   *\n   * <en/> Canvas instance\n   */\n  canvas: Canvas;\n  /**\n   * <zh/> G6 配置项\n   *\n   * <en/> G6 options\n   */\n  options: GraphOptions;\n  /**\n   * <zh/> 数据模型\n   *\n   * <en/> Data model\n   */\n  model: DataController;\n  /**\n   * <zh/> 数据转换控制器\n   *\n   * <en/> Data transform controller\n   */\n  transform: TransformController;\n  /**\n   * <zh/> 元素控制器\n   *\n   * <en/> Element controller\n   * @remarks\n   * <zh/> 仅在绘制开始后才可用\n   *\n   * <en/> Only available after drawing starts\n   */\n  element?: ElementController;\n  /**\n   * <zh/> 元素动画执行器\n   *\n   * <en/> Element animation executor\n   */\n  animation?: Animation;\n  /**\n   * <zh/> 视口控制器\n   *\n   * <en/> Viewport controller\n   */\n  viewport?: ViewportController;\n  /**\n   * <zh/> 布局控制器\n   *\n   * <en/> Layout controller\n   */\n  layout?: LayoutController;\n  /**\n   * <zh/> 行为控制器\n   *\n   * <en/> Behavior controller\n   */\n  behavior?: BehaviorController;\n  /**\n   * <zh/> 插件控制器\n   *\n   * <en/> Plugin controller\n   */\n  plugin?: PluginController;\n  /**\n   * <zh/> 批量操作控制器\n   *\n   * <en/> Batch operation controller\n   */\n  batch?: BatchController;\n}\n"
  },
  {
    "path": "packages/g6/src/runtime/viewport.ts",
    "content": "import { AABB, ICamera } from '@antv/g';\nimport { clamp, isNumber, pick } from '@antv/util';\nimport { AnimationType, GraphEvent } from '../constants';\nimport type {\n  FitViewOptions,\n  ID,\n  Point,\n  TransformOptions,\n  Vector2,\n  Vector3,\n  ViewportAnimationEffectTiming,\n} from '../types';\nimport type { Element } from '../types/element';\nimport { getAnimationOptions } from '../utils/animation';\nimport { getBBoxSize, getCombinedBBox, getExpandedBBox, isBBoxInside, isPointInBBox } from '../utils/bbox';\nimport { AnimateEvent, ViewportEvent, emit } from '../utils/event';\nimport { isPoint } from '../utils/is';\nimport { parsePadding } from '../utils/padding';\nimport { add, divide, subtract } from '../utils/vector';\nimport type { RuntimeContext } from './types';\n\nexport class ViewportController {\n  private context: RuntimeContext;\n\n  private get padding() {\n    return parsePadding(this.context.options.padding);\n  }\n\n  private get paddingOffset(): Point {\n    const [top, right, bottom, left] = this.padding;\n    const [offsetX, offsetY, offsetZ] = [(left - right) / 2, (top - bottom) / 2, 0];\n    return [offsetX, offsetY, offsetZ];\n  }\n\n  constructor(context: RuntimeContext) {\n    this.context = context;\n    const [px, py] = this.paddingOffset;\n    const { zoom, rotation, x = px, y = py } = context.options;\n    this.transform({ mode: 'absolute', scale: zoom, translate: [x, y], rotate: rotation }, false);\n  }\n\n  private get camera() {\n    const { canvas } = this.context;\n    return new Proxy(canvas.getCamera(), {\n      get: (target, prop: keyof ICamera) => {\n        const layers = Object.entries(canvas.getLayers()).filter(([name]) => !['main'].includes(name));\n        const cameras = layers.map(([, layer]) => layer.getCamera());\n\n        const value = target[prop];\n        if (typeof value === 'function') {\n          return (...args: any[]) => {\n            const result = (value as (...args: any[]) => any).apply(target, args);\n            cameras.forEach((camera) => {\n              (camera[prop] as (...args: any[]) => any).apply(camera, args);\n            });\n\n            return result;\n          };\n        }\n      },\n    });\n  }\n\n  private landmarkCounter = 0;\n\n  private createLandmark(options: Parameters<typeof this.camera.createLandmark>[1]) {\n    return this.camera.createLandmark(`landmark-${this.landmarkCounter++}`, options);\n  }\n\n  private getAnimation(animation?: ViewportAnimationEffectTiming) {\n    const finalAnimation = getAnimationOptions(this.context.options, animation);\n    if (!finalAnimation) return false;\n    return pick({ ...finalAnimation }, ['easing', 'duration']) as Exclude<ViewportAnimationEffectTiming, boolean>;\n  }\n\n  public getCanvasSize(): [number, number] {\n    const { canvas } = this.context;\n    const { width = 0, height = 0 } = canvas.getConfig();\n    return [width, height];\n  }\n\n  /**\n   * <zh/> 获取画布中心坐标\n   *\n   * <en/> Get the center coordinates of the canvas\n   * @returns - <zh/> 画布中心坐标 | <en/> Center coordinates of the canvas\n   * @remarks\n   * <zh/> 基于画布的宽高计算中心坐标，不受视口变换影响\n   *\n   * <en/> Calculate the center coordinates based on the width and height of the canvas, not affected by the viewport transformation\n   */\n  public getCanvasCenter(): Point {\n    const { canvas } = this.context;\n    const { width = 0, height = 0 } = canvas.getConfig();\n    return [width / 2, height / 2, 0];\n  }\n\n  /**\n   * <zh/> 当前视口中心坐标\n   *\n   * <en/> Current viewport center coordinates\n   * @returns - <zh/> 视口中心坐标 | <en/> Viewport center coordinates\n   * @remarks\n   * <zh/> 以画布原点为原点，受到视口变换影响\n   *\n   * <en/> With the origin of the canvas as the origin, affected by the viewport transformation\n   */\n  public getViewportCenter(): Point {\n    // 理论上应该通过 camera.getFocalPoint() 获取\n    // 但在 2D 场景下，通过 pan 操作时，focalPoint 不会变化\n    const [x, y] = this.camera.getPosition();\n    return [x, y, 0];\n  }\n\n  public getGraphCenter(): Point {\n    return this.context.graph.getViewportByCanvas(this.getCanvasCenter());\n  }\n\n  public getZoom() {\n    return this.camera.getZoom();\n  }\n\n  public getRotation() {\n    return this.camera.getRoll();\n  }\n\n  private getTranslateOptions(options: TransformOptions) {\n    const { camera } = this;\n    const { mode, translate = [] } = options;\n    const currentZoom = this.getZoom();\n\n    const position = camera.getPosition();\n    const focalPoint = camera.getFocalPoint();\n    const [cx, cy] = this.getCanvasCenter();\n\n    const [x = 0, y = 0, z = 0] = translate;\n\n    const delta = divide([-x, -y, -z], currentZoom);\n\n    return mode === 'relative'\n      ? {\n          position: add(position as Vector3, delta),\n          focalPoint: add(focalPoint as Vector3, delta),\n        }\n      : {\n          position: add([cx, cy, position[2]], delta),\n          focalPoint: add([cx, cy, focalPoint[2]], delta),\n        };\n  }\n\n  private getRotateOptions(options: TransformOptions) {\n    const { mode, rotate = 0 } = options;\n    const roll = mode === 'relative' ? this.camera.getRoll() + rotate : rotate;\n    return { roll };\n  }\n\n  private getZoomOptions(options: TransformOptions) {\n    const { zoomRange } = this.context.options;\n    const currentZoom = this.camera.getZoom();\n    const { mode, scale = 1 } = options;\n    return clamp(mode === 'relative' ? currentZoom * scale : scale, ...zoomRange!);\n  }\n\n  private transformResolver?: () => void;\n\n  public async transform(options: TransformOptions, animation?: ViewportAnimationEffectTiming): Promise<void> {\n    const { graph } = this.context;\n    const { translate, rotate, scale, origin } = options;\n    this.cancelAnimation();\n\n    const _animation = this.getAnimation(animation);\n\n    emit(graph, new ViewportEvent(GraphEvent.BEFORE_TRANSFORM, options));\n\n    // 针对缩放操作，且不涉及平移、旋转、中心点、动画时，直接调用 setZoomByViewportPoint\n    // For zoom operations, and no translation, rotation, center point, and animation involved, call setZoomByViewportPoint directly\n    if (!rotate && scale && !translate && origin && !_animation) {\n      this.camera.setZoomByViewportPoint(this.getZoomOptions(options), origin as Vector2);\n      emit(graph, new ViewportEvent(GraphEvent.AFTER_TRANSFORM, options));\n      return;\n    }\n\n    const landmarkOptions: Parameters<typeof this.camera.createLandmark>[1] = {};\n    if (translate) Object.assign(landmarkOptions, this.getTranslateOptions(options));\n    if (isNumber(rotate)) Object.assign(landmarkOptions, this.getRotateOptions(options));\n    if (isNumber(scale)) Object.assign(landmarkOptions, { zoom: this.getZoomOptions(options) });\n\n    if (_animation) {\n      emit(graph, new AnimateEvent(GraphEvent.BEFORE_ANIMATE, AnimationType.TRANSFORM, null, options));\n\n      return new Promise<void>((resolve) => {\n        this.transformResolver = resolve;\n        this.camera.gotoLandmark(this.createLandmark(landmarkOptions), {\n          ..._animation,\n          onfinish: () => {\n            emit(graph, new AnimateEvent(GraphEvent.AFTER_ANIMATE, AnimationType.TRANSFORM, null, options));\n            emit(graph, new ViewportEvent(GraphEvent.AFTER_TRANSFORM, options));\n            this.transformResolver = undefined;\n            resolve();\n          },\n        });\n      });\n    } else {\n      this.camera.gotoLandmark(this.createLandmark(landmarkOptions), {\n        duration: 0,\n      });\n\n      emit(graph, new ViewportEvent(GraphEvent.AFTER_TRANSFORM, options));\n    }\n  }\n\n  public async fitView(options?: FitViewOptions, animation?: ViewportAnimationEffectTiming): Promise<void> {\n    const [top, right, bottom, left] = this.padding;\n    const { when = 'always', direction = 'both' } = options || {};\n\n    const [width, height] = this.context.canvas.getSize();\n    const innerWidth = width - left - right;\n    const innerHeight = height - top - bottom;\n\n    const canvasBounds = this.context.canvas.getBounds();\n    const bboxInViewPort = this.getBBoxInViewport(canvasBounds);\n    const [contentWidth, contentHeight] = getBBoxSize(bboxInViewPort);\n\n    const isOverflow =\n      (direction === 'x' && contentWidth >= innerWidth) ||\n      (direction === 'y' && contentHeight >= innerHeight) ||\n      (direction === 'both' && contentWidth >= innerWidth && contentHeight >= innerHeight);\n\n    if (when === 'overflow' && !isOverflow) return await this.fitCenter({ animation });\n\n    const scaleX = innerWidth / contentWidth;\n    const scaleY = innerHeight / contentHeight;\n    const scale = direction === 'x' ? scaleX : direction === 'y' ? scaleY : Math.min(scaleX, scaleY);\n\n    const _animation = this.getAnimation(animation);\n    if (!Number.isFinite(scale)) {\n      return;\n    }\n    await this.transform(\n      {\n        mode: 'relative',\n        scale,\n        translate: add(\n          subtract(this.getCanvasCenter(), this.getBBoxInViewport(canvasBounds).center),\n          divide(this.paddingOffset, scale),\n        ),\n      },\n      _animation,\n    );\n  }\n\n  public async fitCenter(options: FocusOptions): Promise<void> {\n    const canvasBounds = this.context.canvas.getBounds();\n    await this.focus(canvasBounds, options);\n  }\n\n  public async focusElements(ids: ID[], options: FocusOptions = {}): Promise<void> {\n    const { element } = this.context;\n    if (!element) return;\n\n    const getBoundsOf = (el: Element) =>\n      options.shapes ? el.getShape(options.shapes).getRenderBounds() : el.getRenderBounds();\n\n    const elementsBounds = getCombinedBBox(ids.map((id) => getBoundsOf(element.getElement(id)!)));\n    await this.focus(elementsBounds, options);\n  }\n\n  private async focus(bbox: AABB, options: FocusOptions) {\n    const center = this.context.graph.getViewportByCanvas(bbox.center);\n    const position = options.position || this.getCanvasCenter();\n    const delta = subtract(position, center);\n    await this.transform({ mode: 'relative', translate: add(delta, this.paddingOffset) }, options.animation);\n  }\n\n  /**\n   * <zh/> 获取画布元素在视口中的包围盒\n   *\n   * <en/> Get the bounding box of the canvas element in the viewport\n   * @param bbox - <zh/> 画布元素包围盒 | <en/> Canvas element bounding box\n   * @returns - <zh/> 视口中的包围盒 | <en/> Bounding box in the viewport\n   */\n  public getBBoxInViewport(bbox: AABB) {\n    const { min, max } = bbox;\n    const { graph } = this.context;\n    const [x1, y1] = graph.getViewportByCanvas(min);\n    const [x2, y2] = graph.getViewportByCanvas(max);\n\n    const bboxInViewport = new AABB();\n    bboxInViewport.setMinMax([x1, y1, 0], [x2, y2, 0]);\n    return bboxInViewport;\n  }\n\n  /**\n   * <zh/> 判断点或包围盒是否在视口中\n   *\n   * <en/> Determine whether the point or bounding box is in the viewport\n   * @param target - <zh/> 点或包围盒 | <en/> Point or bounding box\n   * @param complete - <zh/> 是否完全在视口中 | <en/> Whether it is completely in the viewport\n   * @param tolerance - <zh/> 视口外的容差 | <en/> Tolerance outside the viewport\n   * @returns - <zh/> 是否在视口中 | <en/> Whether it is in the viewport\n   */\n  public isInViewport(target: Point | AABB, complete = false, tolerance = 0) {\n    const { graph } = this.context;\n    const size = this.getCanvasSize();\n\n    const [x1, y1] = graph.getCanvasByViewport([0, 0]);\n    const [x2, y2] = graph.getCanvasByViewport(size);\n\n    let viewportBBox = new AABB();\n    viewportBBox.setMinMax([x1, y1, 0], [x2, y2, 0]);\n\n    if (tolerance) {\n      viewportBBox = getExpandedBBox(viewportBBox, tolerance);\n    }\n\n    return isPoint(target)\n      ? isPointInBBox(target, viewportBBox)\n      : !complete\n        ? viewportBBox.intersects(target)\n        : isBBoxInside(target, viewportBBox);\n  }\n\n  public cancelAnimation() {\n    // @ts-expect-error landmarks is private\n    if (this.camera.landmarks?.length) {\n      this.camera.cancelLandmarkAnimation();\n    }\n    this.transformResolver?.();\n  }\n}\n\nexport interface FocusOptions {\n  /**\n   * <zh/> 动画配置\n   *\n   * <en/> Animation configuration\n   */\n  animation?: ViewportAnimationEffectTiming;\n  /**\n   * <zh/> 使用子图形计算包围盒\n   *\n   * <en/> Calculate the bounding box by using sub-shapes\n   */\n  shapes?: string;\n  /**\n   * <zh/> 对齐位置，默认为画布中心\n   *\n   * <en/> Alignment position, default is the center of the canvas\n   */\n  position?: Point;\n}\n"
  },
  {
    "path": "packages/g6/src/spec/behavior.ts",
    "content": "import type { Graph } from '../runtime/graph';\n\nexport type BehaviorOptions = (string | CustomBehaviorOption | ((this: Graph) => CustomBehaviorOption))[];\n\nexport interface UpdateBehaviorOption {\n  key: string;\n  [key: string]: unknown;\n}\n\nexport interface CustomBehaviorOption extends Record<string, any> {\n  /**\n   * <zh/> 交互类型\n   *\n   * <en/> Behavior type\n   */\n  type: string;\n  /**\n   * <zh/> 交互 key，即唯一标识\n   *\n   * <en/> Behavior key, that is, the unique identifier\n   * @remarks\n   * <zh/> 用于标识交互，从而进一步操作此交互\n   *\n   * <en/> Used to identify the behavior for further operations\n   *\n   * ```ts\n   * // Update behavior options\n   * graph.updateBehavior({key: 'key', ...});\n   * ```\n   */\n  key?: string;\n}\n"
  },
  {
    "path": "packages/g6/src/spec/canvas.ts",
    "content": "import type { Cursor, IRenderer } from '@antv/g';\nimport type { Canvas, CanvasConfig } from '../runtime/canvas';\n\n/**\n * <zh/> 画布配置项\n *\n * <en/> Canvas spec\n * @public\n */\nexport interface CanvasOptions {\n  /**\n   * <zh/> 画布容器\n   *\n   * <en/> canvas container\n   */\n  container?: string | HTMLElement | Canvas;\n  /**\n   * <zh/> 画布宽度\n   *\n   * <en/> canvas width\n   * @remarks\n   * <zh/> 如果未设置，则会自动获取容器宽度\n   *\n   * <en/> If not set, the container width will be automatically obtained\n   */\n  width?: number;\n  /**\n   * <zh/> 画布高度\n   *\n   * <en/> canvas height\n   * @remarks\n   * <zh/> 如果未设置，则会自动获取容器高度\n   *\n   * <en/> If not set, the container height will be automatically obtained\n   */\n  height?: number;\n  /**\n   * <zh/> 手动置顶渲染器\n   *\n   * <en/> manually set renderer\n   * @remarks\n   * <zh/> G6 采用了分层渲染的方式，分为 background、main、label、transient 四层，用户可以通过该配置项分别设置每层画布的渲染器\n   *\n   * <en/> G6 adopts a layered rendering method, divided into four layers: background, main, label, transient. Users can set the renderer of each layer canvas separately through this configuration item\n   */\n  renderer?: (layer: 'background' | 'main' | 'label' | 'transient') => IRenderer;\n  /**\n   * <zh/> 是否自动调整画布大小\n   *\n   * <en/> whether to auto resize canvas\n   * @defaultValue false\n   * @remarks\n   * <zh/> 基于 window.onresize 事件自动调整画布大小\n   *\n   * <en/> Automatically adjust the canvas size based on the window.onresize event\n   */\n  autoResize?: boolean;\n  /**\n   * <zh/> 画布背景色\n   *\n   * <en/> canvas background color\n   * @remarks\n   * <zh/> 该颜色作为导出图片时的背景色\n   *\n   * <en/> This color is used as the background color when exporting images\n   */\n  background?: string;\n  /**\n   * <zh/> 设备像素比\n   *\n   * <en/> device pixel ratio\n   * @remarks\n   * <zh/> 用于高清屏的设备像素比，默认为 [window.devicePixelRatio](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/devicePixelRatio)\n   *\n   * <en/> Device pixel ratio for high-definition screens, default is [window.devicePixelRatio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio)\n   */\n  devicePixelRatio?: number;\n  /**\n   * <zh/> 指针样式\n   *\n   * <en/> cursor style\n   */\n  cursor?: Cursor;\n  /**\n   * <zh/> 画布配置\n   *\n   * <en/> canvas config\n   * @remarks\n   * <zh/> GraphOptions 下相关配置项为快捷配置项，会被转换为 canvas 配置项\n   *\n   * <en/> The related configuration items under GraphOptions are shortcut configuration items, which will be converted to canvas configuration items\n   */\n  canvas?: CanvasConfig;\n}\n"
  },
  {
    "path": "packages/g6/src/spec/data.ts",
    "content": "import type { ID, State } from '../types';\nimport type { ComboStyle } from './element/combo';\nimport type { EdgeStyle } from './element/edge';\nimport type { NodeStyle } from './element/node';\n/**\n * <zh/> 图数据\n *\n * <en/> Graph data\n * @remarks\n * <zh/> 图数据（GraphData）是 Graph 接收的数据类型之一，包含节点、边、组合的集合。\n *\n * <zh/> 一个图数据的示例如下：\n *\n * <en/> Graph data is one of the data types received by Graph, which contains a collection of nodes, edges, and combos.\n *\n * <en/> An example of a graph data is as follows:\n *\n * ```json\n * {\n *  \"nodes\": [\n *    { \"id\": \"node1\", \"combo\": \"combo-1\", \"style\": { \"x\": 100, \"y\": 100 } },\n *    { \"id\": \"node2\", \"style\": { \"x\": 200, \"y\": 200 } }\n *  ],\n *  \"edges\": [{ \"source\": \"node1\", \"target\": \"node2\" }],\n *  \"combos\": [{ \"id\": \"combo-1\", \"style\": { \"x\": 100, \"y\": 100 } }]\n * }\n * ```\n */\nexport interface GraphData {\n  /**\n   * <zh/> 节点数据\n   *\n   * <en/> node data\n   */\n  nodes?: NodeData[];\n  /**\n   * <zh/> 边数据\n   *\n   * <en/> edge data\n   */\n  edges?: EdgeData[];\n  /**\n   * <zh/> Combo 数据\n   *\n   * <en/> combo data\n   */\n  combos?: ComboData[];\n}\n\n/**\n * <zh/> 节点数据\n *\n * <en/> Node data\n */\nexport interface NodeData {\n  /**\n   * <zh/> 节点 ID\n   *\n   * <en/> Node ID\n   */\n  id: ID;\n  /**\n   * <zh/> 节点类型\n   *\n   * <en/> Node type\n   */\n  type?: string;\n  /**\n   * <zh/> 节点数据\n   *\n   * <en/> Node data\n   * @remarks\n   * <zh/> 用于存储节点的自定义数据，可以在样式映射中通过回调函数获取\n   *\n   * <en/> Used to store custom data of the node, which can be obtained through callback functions in the style mapping\n   */\n  data?: Record<string, unknown>;\n  /**\n   * <zh/> 节点样式\n   *\n   * <en/> Node style\n   */\n  style?: NodeStyle;\n  /**\n   * <zh/> 节点初始状态\n   *\n   * <en/> Initial state of the node\n   */\n  states?: State[];\n  /**\n   * <zh/> 所属组合 ID\n   *\n   * <en/> ID of the combo to which the node belongs\n   */\n  combo?: ID | null;\n  /**\n   * <zh/> 子节点 ID\n   *\n   * <en/> Child node ID\n   * @remarks\n   * <zh/> 适用于树图结构\n   *\n   * <en/> Suitable for tree graph structure\n   */\n  children?: ID[];\n  /**\n   * <zh/> 节点深度\n   *\n   * <en/> Node depth\n   * @remarks\n   * <zh/> 适用于树图结构\n   *\n   * <en/> Suitable for tree graph structure\n   */\n  depth?: number;\n  [key: string]: unknown;\n}\n\n/**\n * <zh/> 组合数据\n *\n * <en/> Combo data\n */\nexport interface ComboData {\n  /**\n   * <zh/> Combo ID\n   *\n   * <en/> Combo ID\n   */\n  id: ID;\n  /**\n   * <zh/> Combo 类型\n   *\n   * <en/> Combo type\n   */\n  type?: string;\n  /**\n   * <zh/> Combo 数据\n   *\n   * <en/> Combo data\n   * @remarks\n   * <zh/> 用于存储 Combo 的自定义数据，可以在样式映射中通过回调函数获取\n   *\n   * <en/> Used to store custom data of the Combo, which can be obtained through callback functions in the style mapping\n   */\n  data?: Record<string, unknown>;\n  /**\n   * <zh/> Combo 样式\n   *\n   * <en/> Combo style\n   */\n  style?: ComboStyle;\n  /**\n   * <zh/> 组合初始状态\n   *\n   * <en/> Initial state of the combo\n   */\n  states?: State[];\n  /**\n   * <zh/> 所属组合 ID\n   *\n   * <en/> ID of the combo to which the combo belongs\n   */\n  combo?: ID | null;\n  [key: string]: unknown;\n}\n\n/**\n * <zh/> 边数据\n *\n * <en/> Edge data\n */\nexport interface EdgeData {\n  /**\n   * <zh/> 边 ID\n   *\n   * <en/> Edge ID\n   */\n  id?: ID;\n  /**\n   * <zh/> 边起始节点 ID\n   *\n   * <en/> Source node ID\n   */\n  source: ID;\n  /**\n   * <zh/> 边目标节点 ID\n   *\n   * <en/> Target node ID\n   */\n  target: ID;\n  /**\n   * <zh/> 边类型\n   *\n   * <en/> Edge type\n   */\n  type?: string;\n  /**\n   * <zh/> 边数据\n   *\n   * <en/> Edge data\n   * @remarks\n   * <zh/> 用于存储边的自定义数据，可以在样式映射中通过回调函数获取\n   *\n   * <en/> Used to store custom data of the edge, which can be obtained through callback functions in the style mapping\n   */\n  data?: Record<string, unknown>;\n  /**\n   * <zh/> 边样式\n   *\n   * <en/> Edge style\n   */\n  style?: EdgeStyle;\n  /**\n   * <zh/> 边初始状态\n   *\n   * <en/> Initial state of the edge\n   */\n  states?: State[];\n  [key: string]: unknown;\n}\n"
  },
  {
    "path": "packages/g6/src/spec/element/animation.ts",
    "content": "/**\n * <zh/> 元素动画执行阶段\n *\n * <en/> Stage of element animation execution\n */\nexport type AnimationStage = 'enter' | 'update' | 'exit' | 'show' | 'hide' | 'collapse' | 'expand' | string;\n"
  },
  {
    "path": "packages/g6/src/spec/element/combo.ts",
    "content": "import type { AnimationOptions } from '../../animations/types';\nimport type { BaseComboStyleProps } from '../../elements/combos';\nimport type { Graph } from '../../runtime/graph';\nimport type { ComboData } from '../data';\nimport type { AnimationStage } from './animation';\nimport type { PaletteOptions } from './palette';\n\n/**\n * <zh/> Combo 配置项\n *\n * <en/> Combo spec\n */\nexport interface ComboOptions {\n  /**\n   * <zh/> 组合类型\n   *\n   * <en/> Combo type\n   */\n  type?: string | ((this: Graph, datum: ComboData) => string);\n  /**\n   * <zh/> 组合样式\n   *\n   * <en/> Combo style\n   */\n  style?:\n    | ComboStyle\n    | ((this: Graph, data: ComboData) => ComboStyle)\n    | {\n        [K in keyof ComboStyle]: ComboStyle[K] | ((this: Graph, data: ComboData) => ComboStyle[K]);\n      };\n  /**\n   * <zh/> 组合状态样式\n   *\n   * <en/> Combo state style\n   */\n  state?: Record<\n    string,\n    | ComboStyle\n    | ((this: Graph, data: ComboData) => ComboStyle)\n    | {\n        [K in keyof ComboStyle]: ComboStyle[K] | ((this: Graph, data: ComboData) => ComboStyle[K]);\n      }\n  >;\n  /**\n   * <zh/> 组合动画\n   *\n   * <en/> Combo animation\n   */\n  animation?: false | Record<AnimationStage, false | string | AnimationOptions[]>;\n  /**\n   * <zh/> 色板\n   *\n   * <en/> Palette\n   */\n  palette?: PaletteOptions;\n}\n\nexport interface StaticComboOptions {\n  style?: ComboStyle;\n  state?: Record<string, ComboStyle>;\n  animation?: false | Record<AnimationStage, false | string | AnimationOptions[]>;\n  palette?: PaletteOptions;\n}\n\nexport interface ComboStyle extends Partial<BaseComboStyleProps> {\n  [key: string]: any;\n}\n"
  },
  {
    "path": "packages/g6/src/spec/element/edge.ts",
    "content": "import type { AnimationOptions } from '../../animations/types';\nimport type { BaseEdgeStyleProps } from '../../elements/edges';\nimport type { Graph } from '../../runtime/graph';\nimport type { EdgeData } from '../data';\nimport type { AnimationStage } from './animation';\nimport type { PaletteOptions } from './palette';\n\n/**\n * <zh/> 边配置项\n *\n * <en/> Edge spec\n */\nexport interface EdgeOptions {\n  /**\n   * <zh/> 边类型\n   *\n   * <en/> Edge type\n   */\n  type?: string | ((this: Graph, datum: EdgeData) => string);\n  /**\n   * <zh/> 边样式\n   *\n   * <en/> Edge style\n   */\n  style?:\n    | EdgeStyle\n    | ((this: Graph, data: EdgeData) => EdgeStyle)\n    | {\n        [K in keyof EdgeStyle]: EdgeStyle[K] | ((this: Graph, data: EdgeData) => EdgeStyle[K]);\n      };\n  /**\n   * <zh/> 边状态样式\n   *\n   * <en/> Edge state style\n   */\n  state?: Record<\n    string,\n    | EdgeStyle\n    | ((this: Graph, data: EdgeData) => EdgeStyle)\n    | {\n        [K in keyof EdgeStyle]: EdgeStyle[K] | ((this: Graph, data: EdgeData) => EdgeStyle[K]);\n      }\n  >;\n  /**\n   * <zh/> 边动画\n   *\n   * <en/> Edge animation\n   */\n  animation?: false | Record<AnimationStage, false | string | AnimationOptions[]>;\n  /**\n   * <zh/> 色板\n   *\n   * <en/> Palette\n   */\n  palette?: PaletteOptions;\n}\n\nexport interface StaticEdgeOptions {\n  style?: EdgeStyle;\n  state?: Record<string, EdgeStyle>;\n  animation?: false | Record<AnimationStage, false | string | AnimationOptions[]>;\n  palette?: PaletteOptions;\n}\n\nexport interface EdgeStyle extends Partial<BaseEdgeStyleProps> {\n  [key: string]: unknown;\n}\n"
  },
  {
    "path": "packages/g6/src/spec/element/node.ts",
    "content": "import type { AnimationOptions } from '../../animations/types';\nimport type { BaseNodeStyleProps } from '../../elements/nodes';\nimport type { Graph } from '../../runtime/graph';\nimport type { NodeData } from '../data';\nimport type { AnimationStage } from './animation';\nimport type { PaletteOptions } from './palette';\n\n/**\n * <zh/> 节点配置项\n *\n * <en/> Node spec\n */\nexport interface NodeOptions {\n  /**\n   * <zh/> 节点类型\n   *\n   * <en/> Node type\n   */\n  type?: string | ((this: Graph, datum: NodeData) => string);\n  /**\n   * <zh/> 节点样式\n   *\n   * <en/> Node style\n   */\n  style?:\n    | NodeStyle\n    | ((this: Graph, data: NodeData) => NodeStyle)\n    | {\n        [K in keyof NodeStyle]: NodeStyle[K] | ((this: Graph, data: NodeData) => NodeStyle[K]);\n      };\n  /**\n   * <zh/> 节点状态样式\n   *\n   * <en/> Node state style\n   */\n  state?: Record<\n    string,\n    | NodeStyle\n    | ((this: Graph, data: NodeData) => NodeStyle)\n    | {\n        [K in keyof NodeStyle]: NodeStyle[K] | ((this: Graph, data: NodeData) => NodeStyle[K]);\n      }\n  >;\n  /**\n   * <zh/> 节点动画\n   *\n   * <en/> Node animation\n   */\n  animation?: false | Record<AnimationStage, false | string | AnimationOptions[]>;\n  /**\n   * <zh/> 色板\n   *\n   * <en/> Palette\n   */\n  palette?: PaletteOptions;\n}\n\nexport interface StaticNodeOptions {\n  style?: NodeStyle;\n  state?: Record<string, NodeStyle>;\n  animation?: false | Record<AnimationStage, false | string | AnimationOptions[]>;\n  palette?: PaletteOptions;\n}\n\nexport interface NodeStyle extends Partial<BaseNodeStyleProps> {\n  [key: string]: unknown;\n}\n"
  },
  {
    "path": "packages/g6/src/spec/element/palette.ts",
    "content": "import type { Palette } from '../../palettes/types';\nimport type { ElementDatum } from '../../types';\n\n/**\n * <zh/> 色板配置项\n *\n * <en/> Palette options\n * @public\n */\nexport type PaletteOptions = Palette | CategoricalPaletteOptions | ContinuousPaletteOptions;\n\nexport type STDPaletteOptions = CategoricalPaletteOptions | ContinuousPaletteOptions;\n\ninterface CategoricalPaletteOptions extends BasePaletteOptions {\n  /**\n   * <zh/> 分组取色\n   *\n   * <en/> Coloring by group\n   */\n  type?: 'group';\n  /**\n   * <zh/> 分组字段，未指定时不分组\n   *\n   * <en/> Group field, no grouping when not specified\n   */\n  field?: string | ((datum: ElementDatum) => string);\n}\n\ninterface ContinuousPaletteOptions extends BasePaletteOptions {\n  /**\n   * <zh/> 基于字段值取色\n   *\n   * <en/> Coloring based on field value\n   */\n  type?: 'value';\n  /**\n   * <zh/> 取值字段\n   *\n   * <en/> Value field\n   */\n  field?: string | ((datum: ElementDatum) => string);\n}\n\nexport interface BasePaletteOptions {\n  /**\n   * <zh/> 色板颜色\n   *\n   * <en/> Palette color\n   */\n  color?: Palette;\n  /**\n   * <zh/> 倒序取色\n   *\n   * <en/> Color in reverse order\n   */\n  invert?: boolean;\n}\n"
  },
  {
    "path": "packages/g6/src/spec/graph.ts",
    "content": "import type { AnimationEffectTiming } from '../animations/types';\nimport type { BehaviorOptions } from './behavior';\nimport type { CanvasOptions } from './canvas';\nimport type { GraphData } from './data';\nimport type { ComboOptions } from './element/combo';\nimport type { EdgeOptions } from './element/edge';\nimport type { NodeOptions } from './element/node';\nimport type { LayoutOptions } from './layout';\nimport type { PluginOptions } from './plugin';\nimport type { ThemeOptions } from './theme';\nimport type { TransformOptions } from './transform';\nimport type { ViewportOptions } from './viewport';\n\n/**\n * <zh/> Graph 配置项\n *\n * <en/> Graph options\n * @remarks\n * <zh/> Graph 的初始化通过 `new` 进行实例化，实例化时需要传入参数对象。目前所支持的参数如下：\n *\n * <en/> The initialization of Graph is instantiated through `new`, and the parameter object needs to be passed in when instantiated. The currently supported parameters are as follows:\n *\n * ```\n * new G6.Graph(options: GraphOptions) => Graph\n * ```\n */\n\nexport interface GraphOptions extends CanvasOptions, ViewportOptions {\n  /**\n   * <zh/> 启用或关闭全局动画\n   *\n   * <en/> Enable or disable global animation\n   * @remarks\n   * <zh/> 为动画配置项时，会启用动画，并将该动画配置作为全局动画的基础配置\n   *\n   * <en/> When it is an animation options, the animation will be enabled, and the animation configuration will be used as the basic configuration of the global animation\n   */\n  animation?: boolean | AnimationEffectTiming;\n  /**\n   * <zh/> 数据\n   *\n   * <en/> Data\n   * @remarks\n   * <zh/> 详见 [Data](/api/data/graph-data)\n   *\n   * <en/> See [Data](/en/api/data/graph-data)\n   */\n  data?: GraphData;\n  /**\n   * <zh/> 布局配置项\n   *\n   * <en/> Layout options\n   * @remarks\n   * <zh/> 详见 [Layout](/api/layouts/antv-dagre-layout)\n   *\n   * <en/> See [Layout](/en/api/layouts/antv-dagre-layout)\n   */\n  layout?: LayoutOptions;\n  /**\n   * <zh/> 节点配置项\n   *\n   * <en/> Node options\n   * @remarks\n   * <zh/> 详见 [Node](/api/elements/nodes/base-node)\n   *\n   * <en/> See [Node](/en/api/elements/nodes/base-node)\n   */\n  node?: NodeOptions;\n  /**\n   * <zh/> 边配置项\n   *\n   * <en/> Edge options\n   * @remarks\n   * <zh/> 详见 [Edge](/api/elements/edges/base-edge)\n   *\n   * <en/> See [Edge](/en/api/elements/edges/base-edge)\n   */\n  edge?: EdgeOptions;\n  /**\n   * <zh/> 组合配置项\n   *\n   * <en/> Combo options\n   * @remarks\n   * <zh/> 详见 [Combo](/api/elements/combos/base-combo)\n   *\n   * <en/> See [Combo](/en/api/elements/combos/base-combo)\n   */\n  combo?: ComboOptions;\n  /**\n   * <zh/> 主题\n   *\n   * <en/> Theme\n   */\n  theme?: ThemeOptions;\n  /**\n   * <zh/> 启用交互\n   *\n   * <en/> Enable interactions\n   * @remarks\n   * <zh/>\n   * - 概念：[核心概念 - 交互](/manual/core-concept/behavior)\n   *\n   * - 内置交互: [交互](/api/behaviors/auto-adapt-label)\n   *\n   * - 自定义交互: [自定义扩展 - 自定义交互](/manual/advanced/custom-behavior)\n   *\n   * <en/>\n   * - Concept: [Concepts - Behavior](/en/manual/core-concept/behavior)\n   *\n   * - Built-in behaviors: [Behavior](/en/api/behaviors/auto-adapt-label)\n   *\n   * - Custom behaviors: [Custom Extension - Custom Behavior](/en/manual/advanced/custom-behavior)\n   */\n  behaviors?: BehaviorOptions;\n  /**\n   * <zh/> 启用插件\n   *\n   * <en/> Enable plugins\n   * @remarks\n   * <zh/>\n   * - 概念：[核心概念 - 插件](/manual/core-concept/plugin)\n   *\n   * - 内置插件: [插件](/en/api/plugins/background)\n   *\n   * - 自定义插件: [自定义扩展 - 自定义插件](/manual/advanced/custom-plugin)\n   *\n   * <en/>\n   * - Concept: [Concepts - Plugin](/en/manual/core-concept/plugin)\n   *\n   * - Built-in plugins: [Plugin](/en/api/plugins/background)\n   *\n   * - Custom plugins: [Custom Extension - Custom Plugin](/en/manual/advanced/custom-plugin)\n   */\n  plugins?: PluginOptions;\n  /**\n   * <zh/> 数据转换器\n   *\n   * <en/> Data transforms\n   */\n  transforms?: TransformOptions;\n}\n"
  },
  {
    "path": "packages/g6/src/spec/index.ts",
    "content": "export type { BehaviorOptions } from './behavior';\nexport type { CanvasOptions } from './canvas';\nexport type { ComboData, EdgeData, GraphData, NodeData } from './data';\nexport type { ComboOptions } from './element/combo';\nexport type { EdgeOptions } from './element/edge';\nexport type { NodeOptions } from './element/node';\nexport type { GraphOptions } from './graph';\nexport type { LayoutOptions } from './layout';\nexport type { PluginOptions } from './plugin';\nexport type { ThemeOptions } from './theme';\nexport type { TransformOptions } from './transform';\nexport type { ViewportOptions } from './viewport';\n"
  },
  {
    "path": "packages/g6/src/spec/layout.ts",
    "content": "import type { BaseLayoutOptions, BuiltInLayoutOptions } from '../layouts/types';\n\nexport type LayoutOptions = SingleLayoutOptions | SingleLayoutOptions[];\n\nexport type STDLayoutOptions = BaseLayoutOptions;\n\nexport type SingleLayoutOptions = BuiltInLayoutOptions | BaseLayoutOptions;\n"
  },
  {
    "path": "packages/g6/src/spec/plugin.ts",
    "content": "import type { Graph } from '../runtime/graph';\n\nexport type PluginOptions = (string | CustomPluginOption | ((this: Graph) => CustomPluginOption))[];\n\nexport interface UpdatePluginOption {\n  key: string;\n  [key: string]: unknown;\n}\n\nexport interface CustomPluginOption extends Record<string, any> {\n  /**\n   * <zh/> 插件类型\n   *\n   * <en/> Plugin type\n   */\n  type: string;\n  /**\n   * <zh/> 插件 key，即唯一标识\n   *\n   * <en/> Plugin key, that is, the unique identifier\n   * @remarks\n   * <zh/> 用于标识插件，从而进一步操作此插件\n   *\n   * <en/> Used to identify the plugin for further operations\n   *\n   * ```ts\n   * // Get plugin instance\n   * const plugin = graph.getPluginInstance('key');\n   * // Update plugin options\n   * graph.updatePlugin({key: 'key', ...});\n   * ```\n   */\n  key?: string;\n}\n"
  },
  {
    "path": "packages/g6/src/spec/theme.ts",
    "content": "/**\n * <zh/> 主题配置项\n *\n * <en/> Theme Options\n * @public\n */\nexport type ThemeOptions = false | 'light' | 'dark' | string;\n"
  },
  {
    "path": "packages/g6/src/spec/transform.ts",
    "content": "import type { Graph } from '../runtime/graph';\n\nexport type TransformOptions = (string | CustomTransformOption | ((this: Graph) => CustomTransformOption))[];\n\nexport interface UpdateTransformOption {\n  key: string;\n  [key: string]: unknown;\n}\n\nexport interface CustomTransformOption {\n  type: string;\n  key?: string;\n  [key: string]: unknown;\n}\n"
  },
  {
    "path": "packages/g6/src/spec/viewport.ts",
    "content": "import type { FitViewOptions, ViewportAnimationEffectTiming } from '../types';\nimport type { Padding, STDPadding } from '../types/padding';\n\n/**\n * <zh/> 视口配置项\n *\n * <en/> Viewport\n * @public\n */\nexport interface ViewportOptions {\n  /**\n   * <zh/> 视口 x 坐标\n   *\n   * <en/> viewport x coordinate\n   */\n  x?: number;\n  /**\n   * <zh/> 视口 y 坐标\n   *\n   * <en/> viewport y coordinate\n   */\n  y?: number;\n  /**\n   * <zh/> 是否自动适应\n   *\n   * <en/> whether to auto fit\n   * @remarks\n   * <zh/> 每次执行 `render` 时，都会根据 `autoFit` 进行自适应\n   *\n   * <en/> Every time `render` is executed, it will be adapted according to `autoFit`\n   */\n  autoFit?:\n    | { type: 'view'; options?: FitViewOptions; animation?: ViewportAnimationEffectTiming }\n    | { type: 'center'; animation?: ViewportAnimationEffectTiming }\n    | 'view'\n    | 'center';\n  /**\n   * <zh/> 画布内边距\n   *\n   * <en/> canvas padding\n   * @remarks\n   * <zh/> 通常在自适应时，会根据内边距进行适配\n   *\n   * <en/> Usually, it will be adapted according to the padding when auto-fitting\n   */\n  padding?: Padding;\n  /**\n   * <zh/> 旋转角度\n   *\n   * <en/> rotation angle\n   * @defaultValue 0\n   */\n  rotation?: number;\n  /**\n   * <zh/> 缩放比例\n   *\n   * <en/> zoom ratio\n   * @defaultValue 1\n   */\n  zoom?: number;\n  /**\n   * <zh/> 缩放范围\n   *\n   * <en/> zoom range\n   * @defaultValue [0.01, 10]\n   */\n  zoomRange?: [number, number];\n}\n\n/**\n * @internal\n */\nexport interface STDViewportOptions {\n  autoFit?:\n    | { type: 'view'; options?: FitViewOptions; animation?: ViewportAnimationEffectTiming }\n    | { type: 'center'; animation?: ViewportAnimationEffectTiming };\n  padding?: STDPadding;\n  zoom?: number;\n  zoomRange?: [number, number];\n}\n"
  },
  {
    "path": "packages/g6/src/themes/base.ts",
    "content": "import type { CategoricalPalette } from '../palettes/types';\nimport type { PaletteOptions } from '../spec/element/palette';\nimport type { Theme } from './types';\n\nconst BADGE_PALETTE: CategoricalPalette = ['#7E92B5', '#F4664A', '#FFBE3A'];\n\nconst NODE_PALETTE_OPTIONS: PaletteOptions = {\n  type: 'group',\n  color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF', '#7863FF', '#DB9D0D', '#60C42D', '#FF80CA', '#2491B3', '#17C76F'],\n};\n\nconst EDGE_PALETTE_OPTIONS: PaletteOptions = {\n  type: 'group',\n  color: [\n    '#99ADD1',\n    '#1783FF',\n    '#00C9C9',\n    '#F08F56',\n    '#D580FF',\n    '#7863FF',\n    '#DB9D0D',\n    '#60C42D',\n    '#FF80CA',\n    '#2491B3',\n    '#17C76F',\n  ],\n};\n\ntype ThemeTokens = {\n  bgColor: string;\n  textColor: string;\n  nodeColor: string;\n  nodeColorDisabled: string;\n  nodeStroke: string;\n  nodeBadgePalette?: string[];\n  nodePaletteOptions?: PaletteOptions;\n  nodeHaloStrokeOpacityActive?: number;\n  nodeHaloStrokeOpacitySelected?: number;\n  nodeOpacityDisabled?: number;\n  nodeOpacityInactive?: number;\n  nodeIconOpacityInactive?: number;\n  donutPaletteOptions?: PaletteOptions;\n  edgeColor: string;\n  edgeColorDisabled: string;\n  edgeColorInactive: string;\n  edgePaletteOptions?: PaletteOptions;\n  comboColor: string;\n  comboColorDisabled: string;\n  comboStroke: string;\n  comboStrokeDisabled: string;\n};\n\n/**\n * <zh/> 创建主题\n *\n * <en/> Create a theme based on the given tokens\n * @param tokens - <zh/> 主题配置项 <en/> Theme tokens\n * @returns <zh/> 主题 <en/> Theme\n */\nexport function create(tokens: ThemeTokens): Theme {\n  const {\n    bgColor,\n    textColor,\n    nodeColor,\n    nodeColorDisabled,\n    nodeStroke,\n    nodeHaloStrokeOpacityActive = 0.15,\n    nodeHaloStrokeOpacitySelected = 0.25,\n    nodeOpacityDisabled = 0.06,\n    nodeIconOpacityInactive = 0.85,\n    nodeOpacityInactive = 0.25,\n    nodeBadgePalette = BADGE_PALETTE,\n    nodePaletteOptions = NODE_PALETTE_OPTIONS,\n    edgeColor,\n    edgeColorDisabled,\n    edgePaletteOptions = EDGE_PALETTE_OPTIONS,\n    comboColor,\n    comboColorDisabled,\n    comboStroke,\n    comboStrokeDisabled,\n    edgeColorInactive,\n  } = tokens;\n\n  return {\n    background: bgColor,\n    node: {\n      palette: nodePaletteOptions,\n      style: {\n        donutOpacity: 1,\n        badgeBackgroundOpacity: 1,\n        badgeFill: '#fff',\n        badgeFontSize: 8,\n        badgePadding: [0, 4],\n        badgePalette: nodeBadgePalette,\n        fill: nodeColor,\n        fillOpacity: 1,\n        halo: false,\n        iconFill: '#fff',\n        iconOpacity: 1,\n        labelBackground: false,\n        labelBackgroundFill: bgColor,\n        labelBackgroundLineWidth: 0,\n        labelBackgroundOpacity: 0.75,\n        labelFill: textColor,\n        labelFillOpacity: 0.85,\n        labelLineHeight: 16,\n        labelPadding: [0, 2],\n        labelFontSize: 12,\n        labelFontWeight: 400,\n        labelOpacity: 1,\n        labelOffsetY: 2,\n        lineWidth: 0,\n        portFill: nodeColor,\n        portLineWidth: 1,\n        portStroke: nodeStroke,\n        portStrokeOpacity: 0.65,\n        size: 32,\n        stroke: nodeStroke,\n        strokeOpacity: 1,\n        zIndex: 2,\n      },\n      state: {\n        selected: {\n          halo: true,\n          haloLineWidth: 24,\n          haloStrokeOpacity: nodeHaloStrokeOpacitySelected,\n          labelFontSize: 12,\n          labelFontWeight: 'bold',\n          lineWidth: 4,\n          stroke: nodeStroke,\n        },\n        active: {\n          halo: true,\n          haloLineWidth: 12,\n          haloStrokeOpacity: nodeHaloStrokeOpacityActive,\n        },\n        highlight: {\n          labelFontWeight: 'bold',\n          lineWidth: 4,\n          stroke: nodeStroke,\n          strokeOpacity: 0.85,\n        },\n        inactive: {\n          badgeBackgroundOpacity: nodeOpacityInactive,\n          donutOpacity: nodeOpacityInactive,\n          fillOpacity: nodeOpacityInactive,\n          iconOpacity: nodeIconOpacityInactive,\n          labelFill: textColor,\n          labelFillOpacity: nodeOpacityInactive,\n          strokeOpacity: nodeOpacityInactive,\n        },\n        disabled: {\n          badgeBackgroundOpacity: 0.25,\n          donutOpacity: nodeOpacityDisabled,\n          fill: nodeColorDisabled,\n          fillOpacity: nodeOpacityDisabled,\n          iconFill: nodeColorDisabled,\n          iconOpacity: 0.25,\n          labelFill: textColor,\n          labelFillOpacity: 0.25,\n          strokeOpacity: nodeOpacityDisabled,\n        },\n      },\n      animation: {\n        enter: 'fade',\n        exit: 'fade',\n        show: 'fade',\n        hide: 'fade',\n        expand: 'node-expand',\n        collapse: 'node-collapse',\n        update: [{ fields: ['x', 'y', 'fill', 'stroke'] }],\n        translate: [{ fields: ['x', 'y'] }],\n      },\n    },\n    edge: {\n      palette: edgePaletteOptions,\n      style: {\n        badgeBackgroundFill: edgeColor,\n        badgeFill: '#fff',\n        badgeFontSize: 8,\n        badgeOffsetX: 10,\n        badgeBackgroundOpacity: 1,\n        fillOpacity: 1,\n        halo: false,\n        haloLineWidth: 12,\n        haloStrokeOpacity: 1,\n        increasedLineWidthForHitTesting: 2,\n        labelBackground: false,\n        labelBackgroundFill: bgColor,\n        labelBackgroundLineWidth: 0,\n        labelBackgroundOpacity: 0.75,\n        labelBackgroundPadding: [4, 4, 4, 4],\n        labelFill: textColor,\n        labelFontSize: 12,\n        labelFontWeight: 400,\n        labelOpacity: 1,\n        labelPlacement: 'center',\n        labelTextBaseline: 'middle',\n        lineWidth: 1,\n        stroke: edgeColor,\n        strokeOpacity: 1,\n        zIndex: 1,\n      },\n      state: {\n        selected: {\n          halo: true,\n          haloStrokeOpacity: 0.25,\n          labelFontSize: 14,\n          labelFontWeight: 'bold',\n          lineWidth: 3,\n        },\n        active: {\n          halo: true,\n          haloStrokeOpacity: 0.15,\n        },\n        highlight: {\n          labelFontWeight: 'bold',\n          lineWidth: 3,\n        },\n        inactive: {\n          stroke: edgeColorInactive,\n          fillOpacity: 0.08,\n          labelOpacity: 0.25,\n          strokeOpacity: 0.08,\n          badgeBackgroundOpacity: 0.25,\n        },\n        disabled: {\n          stroke: edgeColorDisabled,\n          fillOpacity: 0.45,\n          strokeOpacity: 0.45,\n          labelOpacity: 0.25,\n          badgeBackgroundOpacity: 0.45,\n        },\n      },\n      animation: {\n        enter: 'fade',\n        exit: 'fade',\n        expand: 'path-in',\n        collapse: 'path-out',\n        show: 'fade',\n        hide: 'fade',\n        update: [{ fields: ['sourceNode', 'targetNode'] }, { fields: ['stroke'], shape: 'key' }],\n        translate: [{ fields: ['sourceNode', 'targetNode'] }],\n      },\n    },\n    combo: {\n      style: {\n        collapsedMarkerFill: bgColor,\n        collapsedMarkerFontSize: 12,\n        collapsedMarkerFillOpacity: 1,\n        collapsedSize: 32,\n        collapsedFillOpacity: 1,\n        fill: comboColor,\n        halo: false,\n        haloLineWidth: 12,\n        haloStroke: comboStroke,\n        haloStrokeOpacity: 0.25,\n        labelBackground: false,\n        labelBackgroundFill: bgColor,\n        labelBackgroundLineWidth: 0,\n        labelBackgroundOpacity: 0.75,\n        labelBackgroundPadding: [2, 4, 2, 4],\n        labelFill: textColor,\n        labelFontSize: 12,\n        labelFontWeight: 400,\n        labelOpacity: 1,\n        lineDash: 0,\n        lineWidth: 1,\n        fillOpacity: 0.04,\n        strokeOpacity: 1,\n        padding: 10,\n        stroke: comboStroke,\n      },\n      state: {\n        selected: {\n          halo: true,\n          labelFontSize: 14,\n          labelFontWeight: 700,\n          lineWidth: 4,\n        },\n        active: {\n          halo: true,\n        },\n        highlight: {\n          labelFontWeight: 700,\n          lineWidth: 4,\n        },\n        inactive: {\n          fillOpacity: 0.65,\n          labelOpacity: 0.25,\n          strokeOpacity: 0.65,\n        },\n        disabled: {\n          fill: comboColorDisabled,\n          fillOpacity: 0.25,\n          labelOpacity: 0.25,\n          stroke: comboStrokeDisabled,\n          strokeOpacity: 0.25,\n        },\n      },\n      animation: {\n        enter: 'fade',\n        exit: 'fade',\n        show: 'fade',\n        hide: 'fade',\n        expand: 'combo-expand',\n        collapse: 'combo-collapse',\n        update: [{ fields: ['x', 'y'] }, { fields: ['fill', 'stroke', 'lineWidth'], shape: 'key' }],\n        translate: [{ fields: ['x', 'y'] }],\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "packages/g6/src/themes/dark.ts",
    "content": "import type { PaletteOptions } from '../spec/element/palette';\nimport { create } from './base';\nimport type { Theme } from './types';\n\nconst EDGE_PALETTE_OPTIONS: PaletteOptions = {\n  type: 'group',\n  color: [\n    '#637088',\n    '#0F55A6',\n    '#008383',\n    '#9C5D38',\n    '#8B53A6',\n    '#4E40A6',\n    '#8F6608',\n    '#3E801D',\n    '#A65383',\n    '#175E75',\n    '#0F8248',\n  ],\n};\n\nconst tokens = {\n  bgColor: '#000000',\n  comboColor: '#fdfdfd',\n  comboColorDisabled: '#d0e4ff',\n  comboStroke: '#99add1',\n  comboStrokeDisabled: '#969696',\n  edgeColor: '#637088',\n  edgeColorDisabled: '#637088',\n  edgeColorInactive: '#D0E4FF',\n  edgePaletteOptions: EDGE_PALETTE_OPTIONS,\n  nodeColor: '#1783ff',\n  nodeColorDisabled: '#D0E4FF',\n  nodeHaloStrokeOpacityActive: 0.25,\n  nodeHaloStrokeOpacitySelected: 0.45,\n  nodeIconOpacityInactive: 0.45,\n  nodeOpacityDisabled: 0.25,\n  nodeOpacityInactive: 0.45,\n  nodeStroke: '#d0e4ff',\n  textColor: '#ffffff',\n};\n\nexport const dark: Theme = create(tokens);\n"
  },
  {
    "path": "packages/g6/src/themes/index.ts",
    "content": "export { dark } from './dark';\nexport { light } from './light';\n"
  },
  {
    "path": "packages/g6/src/themes/light.ts",
    "content": "import { create } from './base';\nimport type { Theme } from './types';\n\nconst tokens = {\n  bgColor: '#ffffff',\n  comboColor: '#99ADD1',\n  comboColorDisabled: '#f0f0f0',\n  comboStroke: '#99add1',\n  comboStrokeDisabled: '#d9d9d9',\n  edgeColor: '#99add1',\n  edgeColorDisabled: '#d9d9d9',\n  edgeColorInactive: '#1B324F',\n  nodeColor: '#1783ff',\n  nodeColorDisabled: '#1B324F',\n  nodeHaloStrokeOpacityActive: 0.15,\n  nodeHaloStrokeOpacitySelected: 0.25,\n  nodeIconOpacityInactive: 0.85,\n  nodeOpacityDisabled: 0.06,\n  nodeOpacityInactive: 0.25,\n  nodeStroke: '#000000',\n  textColor: '#000000',\n};\n\nexport const light: Theme = create(tokens);\n"
  },
  {
    "path": "packages/g6/src/themes/types.ts",
    "content": "import type { StaticComboOptions } from '../spec/element/combo';\nimport type { StaticEdgeOptions } from '../spec/element/edge';\nimport type { StaticNodeOptions } from '../spec/element/node';\n\nexport type Theme = {\n  node?: StaticNodeOptions;\n  edge?: StaticEdgeOptions;\n  combo?: StaticComboOptions;\n  background?: string;\n};\n"
  },
  {
    "path": "packages/g6/src/transforms/arrange-draw-order.ts",
    "content": "import type { ComboData } from '../spec';\nimport type { ID } from '../types';\nimport { idOf } from '../utils/id';\nimport { BaseTransform } from './base-transform';\nimport type { DrawData } from './types';\n\n/**\n * <zh/> 调整元素绘制顺序\n *\n * <en/> Adjust the drawing order of elements\n */\nexport class ArrangeDrawOrder extends BaseTransform {\n  public beforeDraw(input: DrawData): DrawData {\n    const { model } = this.context;\n\n    const combosToAdd = input.add.combos;\n\n    const arrangeCombo = (combos: Map<string, ComboData>): Map<string, ComboData> => {\n      // id, data, zIndex\n      const order: [ID, ComboData, number][] = [];\n      combos.forEach((combo, id) => {\n        const ancestors = model.getAncestorsData(id, 'combo');\n        const path = ancestors.map((ancestor) => idOf(ancestor)).reverse();\n        // combo 的 zIndex 为距离根 combo 的深度\n        // The zIndex of the combo is the depth from the root combo\n        order.push([id, combo, path.length]);\n      });\n\n      return new Map(\n        order\n          // 基于 zIndex 降序排序，优先绘制子 combo / Sort based on zIndex in descending order, draw child combo first\n          .sort(([, , zIndex1], [, , zIndex2]) => zIndex2 - zIndex1)\n          .map(([id, datum]) => [id, datum]),\n      );\n    };\n\n    input.add.combos = arrangeCombo(combosToAdd);\n    input.update.combos = arrangeCombo(input.update.combos);\n\n    return input;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/transforms/base-transform.ts",
    "content": "import { BaseExtension } from '../registry/extension';\nimport type { DrawContext } from '../runtime/element';\nimport type { CustomBehaviorOption } from '../spec/behavior';\nimport type { DrawData } from './types';\n\nexport type BaseTransformOptions = CustomBehaviorOption;\n\n/**\n * <zh/> 数据转换的基类\n *\n * <en/> Base class for data transforms\n */\nexport abstract class BaseTransform<T extends BaseTransformOptions = BaseTransformOptions> extends BaseExtension<T> {\n  public beforeDraw(data: DrawData, context: DrawContext): DrawData {\n    return data;\n  }\n\n  public afterLayout(type: 'pre', data: DrawData): void;\n  public afterLayout(type: 'post', data?: undefined): void;\n  public afterLayout(type: 'pre' | 'post', data?: DrawData) {}\n}\n"
  },
  {
    "path": "packages/g6/src/transforms/collapse-expand-combo.ts",
    "content": "import { COMBO_KEY } from '../constants';\nimport type { DrawContext } from '../runtime/element';\nimport type { ComboData } from '../spec';\nimport { isCollapsed } from '../utils/collapsibility';\nimport { getSubgraphRelatedEdges } from '../utils/edge';\nimport { idOf } from '../utils/id';\nimport { BaseTransform } from './base-transform';\nimport type { DrawData } from './types';\nimport { reassignTo } from './utils';\n\n/**\n * <zh/> 处理组合的收起和展开\n *\n * <en/> Process the collapse and expand of combos\n */\nexport class CollapseExpandCombo extends BaseTransform {\n  public beforeDraw(input: DrawData, context: DrawContext): DrawData {\n    if (context.stage === 'visibility') return input;\n    if (!this.context.model.model.hasTreeStructure(COMBO_KEY)) return input;\n\n    const { model } = this.context;\n    const { add, update } = input;\n    // combo 添加和更新的顺序为先子后父，因此采用倒序遍历\n    // The order of adding and updating combos is first child and then parent, so reverse traversal is used\n    const combos = [...input.update.combos.entries(), ...input.add.combos.entries()];\n    while (combos.length) {\n      const [id, combo] = combos.pop()!;\n\n      if (isCollapsed(combo)) {\n        const descendants = model.getDescendantsData(id);\n        const descendantIds = descendants.map(idOf);\n        const { internal, external } = getSubgraphRelatedEdges(descendantIds, (id) => model.getRelatedEdgesData(id));\n\n        // 移除所有后代元素 / Remove all descendant elements\n        descendants.forEach((descendant) => {\n          const descendantId = idOf(descendant);\n          // 不再处理当前 combo 的后代 combo\n          // No longer process the descendant combo of the current combo\n          const comboIndex = combos.findIndex(([id]) => id === descendantId);\n          if (comboIndex !== -1) combos.splice(comboIndex, 1);\n\n          const elementType = model.getElementType(descendantId);\n          reassignTo(input, 'remove', elementType, descendant);\n        });\n\n        // 如果是内部边/节点 销毁\n        // If it is an internal edge/node, destroy it\n        internal.forEach((edge) => reassignTo(input, 'remove', 'edge', edge));\n\n        // 如果是外部边，连接到收起对象上\n        // If it is an external edge, connect to the collapsed object\n        external.forEach((edge) => {\n          const id = idOf(edge);\n          const edgeElement = this.context.element?.getElement(id);\n          if (edgeElement) update.edges.set(id, edge);\n          else add.edges.set(id, edge);\n        });\n      } else {\n        const children = model.getChildrenData(id);\n        const childrenIds = children.map(idOf);\n        const { edges } = getSubgraphRelatedEdges(childrenIds, (id) => model.getRelatedEdgesData(id));\n\n        [...children, ...edges].forEach((descendant) => {\n          const id = idOf(descendant);\n          const elementType = model.getElementType(id);\n\n          const element = this.context.element?.getElement(id);\n          // 如果节点不存在，则添加到新增列表，如果存在，添加到更新列表\n          // If the node does not exist, add it to the new list, if it exists, add it to the update list\n          if (element) reassignTo(input, 'update', elementType, descendant);\n          else reassignTo(input, 'add', elementType, descendant);\n\n          // 继续展开子节点 / Continue to expand child nodes\n          if (elementType === 'combo') combos.push([id, descendant as ComboData]);\n        });\n      }\n    }\n\n    return input;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/transforms/collapse-expand-node.ts",
    "content": "import { TREE_KEY } from '../constants';\nimport type { NodeData } from '../spec';\nimport type { ElementDatum, ElementType, ID } from '../types';\nimport { isCollapsed } from '../utils/collapsibility';\nimport { idOf } from '../utils/id';\nimport { BaseTransform } from './base-transform';\nimport type { DrawData, ProcedureData } from './types';\nimport { reassignTo } from './utils';\n\n// 如果在任务列表中不存在，则添加到任务列表\n// If it does not exist in the task list, add it to the task list\nconst weakAssignTo = (input: DrawData, type: 'add' | 'update', elementType: ElementType, datum: ElementDatum) => {\n  const typeName = `${elementType}s` as keyof ProcedureData;\n  const id = idOf(datum);\n  if (!input.add[typeName].has(id) && !input.update[typeName].has(id)) {\n    input[type][typeName].set(idOf(datum), datum as any);\n  }\n};\n\n/**\n * <zh/> 处理(树图)节点的收起和展开\n *\n * <en/> Process the collapse and expand of (tree)nodes\n */\nexport class CollapseExpandNode extends BaseTransform {\n  private getElement(id: ID) {\n    return this.context.element!.getElement(id);\n  }\n\n  private handleExpand(node: NodeData, input: DrawData) {\n    weakAssignTo(input, 'add', 'node', node);\n    if (isCollapsed(node)) return;\n\n    const id = idOf(node);\n    weakAssignTo(input, 'add', 'node', node);\n\n    const relatedEdges = this.context.model.getRelatedEdgesData(id);\n    relatedEdges.forEach((edge) => {\n      reassignTo(input, 'add', 'edge', edge);\n    });\n\n    const children = this.context.model.getChildrenData(id);\n    children.forEach((child) => {\n      this.handleExpand(child, input);\n    });\n  }\n\n  public beforeDraw(input: DrawData): DrawData {\n    const { graph, model } = this.context;\n\n    if (!model.model.hasTreeStructure(TREE_KEY)) return input;\n\n    const {\n      add: { nodes: nodesToAdd, edges: edgesToAdd },\n      update: { nodes: nodesToUpdate },\n    } = input;\n    const nodesToCollapse = new Map<ID, NodeData>();\n    const nodesToExpand = new Map<ID, NodeData>();\n\n    nodesToAdd.forEach((node, id) => {\n      if (isCollapsed(node)) nodesToCollapse.set(id, node);\n    });\n\n    // 如果创建了一条连接到收起的节点的边，则将其添加到待展开列表\n    // If an edge is created that connects to a collapsed node, add it to the list to be expanded\n    edgesToAdd.forEach((edge) => {\n      if (graph.getElementType(edge.source) !== 'node') return;\n      const source = graph.getNodeData(edge.source);\n      if (isCollapsed(source)) nodesToCollapse.set(edge.source, source);\n    });\n\n    nodesToUpdate.forEach((node, id) => {\n      const nodeElement = this.getElement(id);\n      if (!nodeElement) return;\n\n      const isCurrentCollapsed = nodeElement.attributes.collapsed;\n      if (isCollapsed(node)) {\n        if (!isCurrentCollapsed) nodesToCollapse.set(id, node);\n      } else {\n        if (isCurrentCollapsed) nodesToExpand.set(id, node);\n      }\n    });\n\n    const handledNodes = new Set<ID>();\n\n    nodesToCollapse.forEach((node, id) => {\n      // 将子节点添加到待删除列表，并删除关联的边\n      // Add child nodes to the list to be deleted，and delete the associated edges\n      const descendants = model.getDescendantsData(id);\n      descendants.forEach((descendant) => {\n        const id = idOf(descendant);\n        if (handledNodes.has(id)) return;\n\n        reassignTo(input, 'remove', 'node', descendant);\n        const relatedEdges = model.getRelatedEdgesData(id);\n        relatedEdges.forEach((edge) => {\n          reassignTo(input, 'remove', 'edge', edge);\n        });\n\n        handledNodes.add(id);\n      });\n    });\n\n    nodesToExpand.forEach((node, id) => {\n      const ancestors = model.getAncestorsData(id, TREE_KEY);\n\n      // 如果祖先节点是收起的，添加到移除列表\n      // If the ancestor node is collapsed, add it to the removal list\n      if (ancestors.some(isCollapsed)) {\n        reassignTo(input, 'remove', 'node', node);\n        return;\n      }\n\n      this.handleExpand(node, input);\n    });\n\n    return input;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/transforms/get-edge-actual-ends.ts",
    "content": "import { COMBO_KEY } from '../constants';\nimport { idOf } from '../exports';\nimport { DataController } from '../runtime/data';\nimport type { EdgeData } from '../spec';\nimport type { NodeLikeData } from '../types';\nimport { findActualConnectNodeData } from '../utils/edge';\nimport { BaseTransform } from './base-transform';\nimport type { DrawData } from './types';\n/**\n * <zh/> 获取边的实际端点\n *\n * <en/> Get the actual endpoints of the edge\n */\nexport class GetEdgeActualEnds extends BaseTransform {\n  public beforeDraw(input: DrawData): DrawData {\n    const { add, update } = input;\n    const { model } = this.context;\n    [...add.edges.entries(), ...update.edges.entries()].forEach(([, edge]) => {\n      getEdgeEndsContext(model, edge);\n    });\n    return input;\n  }\n}\n\nexport const getEdgeEndsContext = (model: DataController, edge: EdgeData) => {\n  const { source, target } = edge;\n\n  const sourceNodeData = model.getElementDataById(source) as NodeLikeData;\n  const targetNodeData = model.getElementDataById(target) as NodeLikeData;\n\n  const actualSourceNode = findActualConnectNodeData(sourceNodeData, (id) => model.getParentData(id, COMBO_KEY));\n\n  const actualTargetNode = findActualConnectNodeData(targetNodeData, (id) => model.getParentData(id, COMBO_KEY));\n\n  const sourceNode = idOf(actualSourceNode);\n  const targetNode = idOf(actualTargetNode);\n\n  const ends = { sourceNode, targetNode };\n\n  if (edge.style) {\n    Object.assign(edge.style, ends);\n  } else edge.style = ends;\n\n  return edge;\n};\n"
  },
  {
    "path": "packages/g6/src/transforms/index.ts",
    "content": "export { ArrangeDrawOrder } from './arrange-draw-order';\nexport { BaseTransform } from './base-transform';\nexport { CollapseExpandCombo } from './collapse-expand-combo';\nexport { CollapseExpandNode } from './collapse-expand-node';\nexport { GetEdgeActualEnds } from './get-edge-actual-ends';\nexport { MapNodeSize } from './map-node-size';\nexport { PlaceRadialLabels } from './place-radial-labels';\nexport { ProcessParallelEdges } from './process-parallel-edges';\nexport { UpdateRelatedEdge } from './update-related-edge';\n\nexport type { BaseTransformOptions } from './base-transform';\nexport type { MapNodeSizeOptions } from './map-node-size';\nexport type { PlaceRadialLabelsOptions } from './place-radial-labels';\nexport type { ProcessParallelEdgesOptions } from './process-parallel-edges';\n"
  },
  {
    "path": "packages/g6/src/transforms/map-node-size.ts",
    "content": "import { deepMix, pick } from '@antv/util';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { GraphData, NodeData } from '../spec';\nimport type { NodeStyle } from '../spec/element/node';\nimport type { ID, Node, NodeCentralityOptions, Size, STDSize } from '../types';\nimport type { CentralityResult } from '../utils/centrality';\nimport { getNodeCentralities } from '../utils/centrality';\nimport { idOf } from '../utils/id';\nimport { getVerticalPadding } from '../utils/padding';\nimport { linear, log, pow, sqrt } from '../utils/scale';\nimport { parseSize } from '../utils/size';\nimport type { BaseTransformOptions } from './base-transform';\nimport { BaseTransform } from './base-transform';\nimport type { DrawData } from './types';\nimport { isStyleEqual, reassignTo } from './utils';\n\nexport interface MapNodeSizeOptions extends BaseTransformOptions {\n  /**\n   * <zh/> 节点中心性的度量方法\n   * - `'degree'`：度中心性，通过节点的度数（连接的边的数量）来衡量其重要性。度中心性高的节点通常具有较多的直接连接，在网络中可能扮演着重要的角色\n   * - `'betweenness'`：介数中心性，通过节点在所有最短路径中出现的次数来衡量其重要性。介数中心性高的节点通常在网络中起到桥梁作用，控制着信息的流动\n   * - `'closeness'`：接近中心性，通过节点到其他所有节点的最短路径长度总和的倒数来衡量其重要性。接近中心性高的节点通常能够更快地到达网络中的其他节点\n   * - `'eigenvector'`：特征向量中心性，通过节点与其他中心节点的连接程度来衡量其重要性。特征向量中心性高的节点通常连接着其他重要节点\n   * - `'pagerank'`：PageRank 中心性，通过节点被其他节点引用的次数来衡量其重要性，常用于有向图。PageRank 中心性高的节点通常在网络中具有较高的影响力，类似于网页排名算法\n   * - 自定义中心性计算方法：`(graphData: GraphData) => Map<ID, number>`，其中 `graphData` 为图数据，`Map<ID, number>` 为节点 ID 到中心性值的映射\n   *\n   * <en/> The method of measuring the node centrality\n   * - `'degree'`: Degree centrality, measures centrality by the degree (number of connected edges) of a node. Nodes with high degree centrality usually have more direct connections and may play important roles in the network\n   * - `'betweenness'`: Betweenness centrality, measures centrality by the number of times a node appears in all shortest paths. Nodes with high betweenness centrality usually act as bridges in the network, controlling the flow of information\n   * - `'closeness'`: Closeness centrality, measures centrality by the reciprocal of the average shortest path length from a node to all other nodes. Nodes with high closeness centrality usually can reach other nodes in the network more quickly\n   * - `'eigenvector'`: Eigenvector centrality, measures centrality by the degree of connection between a node and other central nodes. Nodes with high eigenvector centrality usually connect to other important nodes\n   * - `'pagerank'`: PageRank centrality, measures centrality by the number of times a node is referenced by other nodes, commonly used in directed graphs. Nodes with high PageRank centrality usually have high influence in the network, similar to the page ranking algorithm\n   * - Custom centrality calculation method: `(graphData: GraphData) => Map<ID, number>`, where `graphData` is the graph data, and `Map<ID, number>` is the mapping from node ID to centrality value\n   * @defaultValue { type: 'eigenvector' }\n   */\n  centrality?: NodeCentralityOptions | ((graphData: GraphData) => Map<ID, number>);\n  /**\n   * <zh/> 节点最大尺寸\n   *\n   * <en/> The maximum size of the node\n   * @defaultValue 80\n   */\n  maxSize?: Size;\n  /**\n   * <zh/> 节点最小尺寸\n   *\n   * <en/> The minimum size of the node\n   * @defaultValue 20\n   */\n  minSize?: Size;\n  /**\n   * <zh/> 插值函数，用于将节点中心性映射到节点大小\n   * - `'linear'`：线性插值函数，将一个值从一个范围线性映射到另一个范围，常用于处理中心性值的差异较小的情况\n   * - `'log'`：对数插值函数，将一个值从一个范围对数映射到另一个范围，常用于处理中心性值的差异较大的情况\n   * - `'pow'`：幂律插值函数，将一个值从一个范围幂律映射到另一个范围，常用于处理中心性值的差异较大的情况\n   * - `'sqrt'`：平方根插值函数，将一个值从一个范围平方根映射到另一个范围，常用于处理中心性值的差异较大的情况\n   * - 自定义插值函数：`(value: number, domain: [number, number], range: [number, number]) => number`，其中 `value` 为需要映射的值，`domain` 为输入值的范围，`range` 为输出值的范围\n   *\n   * <en/> Scale type\n   * - `'linear'`: Linear scale, maps a value from one range to another range linearly, commonly used for cases where the difference in centrality values is small\n   * - `'log'`: Logarithmic scale, maps a value from one range to another range logarithmically, commonly used for cases where the difference in centrality values is large\n   * - `'pow'`: Power-law scale, maps a value from one range to another range using power law, commonly used for cases where the difference in centrality values is large\n   * - `'sqrt'`: Square root scale, maps a value from one range to another range using square root, commonly used for cases where the difference in centrality values is large\n   * - Custom scale: `(value: number, domain: [number, number], range: [number, number]) => number`，where `value` is the value to be mapped, `domain` is the input range, and `range` is the output range\n   * @defaultValue 'log'\n   */\n  scale?:\n    | 'linear'\n    | 'log'\n    | 'pow'\n    | 'sqrt'\n    | ((value: number, domain: [number, number], range: [number, number]) => number);\n  /**\n   * <zh/> 是否同步调整标签大小\n   *\n   * <en/> Whether to map label size synchronously\n   * @defaultValue false\n   */\n  mapLabelSize?: boolean | [number, number];\n}\n\n/**\n * <zh/> 根据节点重要性调整节点的大小\n *\n * <en/> Map node size based on node importance\n * @remarks\n * <zh/> 在图可视化中，节点的大小通常用于传达节点的重要性或影响力。通过根据节点中心性调整节点的大小，我们可以更直观地展示网络中各个节点的重要性，从而帮助用户更好地理解和分析复杂的网络结构。\n *\n * <en/> In graph visualization, the size of a node is usually used to convey the importance or influence of the node. By adjusting the size of the node based on the centrality of the node, we can more intuitively show the importance of each node in the network, helping users better understand and analyze complex network structures.\n */\nexport class MapNodeSize extends BaseTransform<MapNodeSizeOptions> {\n  static defaultOptions: Partial<MapNodeSizeOptions> = {\n    centrality: { type: 'degree' },\n    maxSize: 80,\n    minSize: 20,\n    scale: 'linear',\n    mapLabelSize: false,\n  };\n\n  constructor(context: RuntimeContext, options: MapNodeSizeOptions) {\n    super(context, deepMix({}, MapNodeSize.defaultOptions, options));\n  }\n\n  public beforeDraw(input: DrawData): DrawData {\n    const { model } = this.context;\n    const nodes = model.getNodeData();\n\n    const maxSize = parseSize(this.options.maxSize);\n    const minSize = parseSize(this.options.minSize);\n\n    const centralities = this.getCentralities(this.options.centrality);\n\n    const maxCentrality = centralities.size > 0 ? Math.max(...centralities.values()) : 0;\n    const minCentrality = centralities.size > 0 ? Math.min(...centralities.values()) : 0;\n    nodes.forEach((datum) => {\n      const size = this.assignSizeByCentrality(\n        centralities.get(idOf(datum)) || 0,\n        minCentrality,\n        maxCentrality,\n        minSize,\n        maxSize,\n        this.options.scale,\n      );\n\n      const element = this.context.element?.getElement<Node>(idOf(datum));\n\n      const style: NodeStyle = { size };\n      this.assignLabelStyle(style, size, datum, element);\n\n      if (!element || !isStyleEqual(style, element.attributes)) {\n        reassignTo(input, element ? 'update' : 'add', 'node', deepMix(datum, { style }), true);\n      }\n    });\n    return input;\n  }\n\n  private assignLabelStyle(style: NodeStyle, size: STDSize, datum: NodeData, element?: Node) {\n    const configStyle = element ? element.config.style : this.context.element?.getElementComputedStyle('node', datum);\n\n    Object.assign(style, pick(configStyle, ['labelFontSize', 'labelLineHeight']));\n\n    if (this.options.mapLabelSize) {\n      const fontSize = this.getLabelSizeByNodeSize(size, Infinity, Number(style.labelFontSize));\n      Object.assign(style, {\n        labelFontSize: fontSize,\n        labelLineHeight: fontSize + getVerticalPadding(style.labelPadding),\n      });\n    }\n    return style;\n  }\n\n  private getLabelSizeByNodeSize(size: STDSize, defaultMaxFontSize: number, defaultMinFontSize: number): number {\n    const fontSize = Math.min(...size) / 2;\n    const [minFontSize, maxFontSize] = !Array.isArray(this.options.mapLabelSize)\n      ? [defaultMinFontSize, defaultMaxFontSize]\n      : this.options.mapLabelSize;\n    return Math.min(maxFontSize, Math.max(fontSize, minFontSize));\n  }\n\n  private getCentralities(centrality: Required<MapNodeSizeOptions>['centrality']): CentralityResult {\n    const { model } = this.context;\n    const graphData = model.getData();\n\n    if (typeof centrality === 'function') return centrality(graphData);\n\n    const getRelatedEdgesData = model.getRelatedEdgesData.bind(model);\n    return getNodeCentralities(graphData, getRelatedEdgesData, centrality);\n  }\n\n  private assignSizeByCentrality = (\n    centrality: number,\n    minCentrality: number,\n    maxCentrality: number,\n    minSize: STDSize,\n    maxSize: STDSize,\n    scale: MapNodeSizeOptions['scale'],\n  ): STDSize => {\n    const domain: [number, number] = [minCentrality, maxCentrality];\n    const rangeX: [number, number] = [minSize[0], maxSize[0]];\n    const rangeY: [number, number] = [minSize[1], maxSize[1]];\n    const rangeZ: [number, number] = [minSize[2], maxSize[2]];\n\n    const interpolate = (centrality: number, range: [number, number]): number => {\n      if (typeof scale === 'function') {\n        return scale(centrality, domain, range);\n      }\n      switch (scale) {\n        case 'linear':\n          return linear(centrality, domain, range);\n        case 'log':\n          return log(centrality, domain, range);\n        case 'pow':\n          return pow(centrality, domain, range, 2);\n        case 'sqrt':\n          return sqrt(centrality, domain, range);\n        default:\n          return range[0];\n      }\n    };\n\n    return [interpolate(centrality, rangeX), interpolate(centrality, rangeY), interpolate(centrality, rangeZ)];\n  };\n}\n"
  },
  {
    "path": "packages/g6/src/transforms/place-radial-labels.ts",
    "content": "import type { TransformArray } from '@antv/g';\nimport { rad2deg } from '@antv/g';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { NodeData } from '../spec';\nimport { idOf } from '../utils/id';\nimport { positionOf } from '../utils/position';\nimport { parseSize } from '../utils/size';\nimport { rad, subtract } from '../utils/vector';\nimport type { BaseTransformOptions } from './base-transform';\nimport { BaseTransform } from './base-transform';\n\n/**\n * <zh/> 根据径向布局自动调整节点标签样式的配置项\n *\n * <en/> Options for automatically adjusting the style of node labels according to the radial layout\n */\nexport interface PlaceRadialLabelsOptions extends BaseTransformOptions {\n  /**\n   * <zh/> 偏移量\n   *\n   * <en/> Offset\n   */\n  offset?: number;\n}\n\n/**\n * <zh/> 根据径向布局自动调整节点标签样式，包括位置和旋转角度\n *\n * <en/> Automatically adjust the style of node labels according to the radial layout, including position and rotation angle\n */\nexport class PlaceRadialLabels extends BaseTransform<PlaceRadialLabelsOptions> {\n  static defaultOptions: Partial<PlaceRadialLabelsOptions> = {\n    offset: 5,\n  };\n\n  constructor(context: RuntimeContext, options: PlaceRadialLabelsOptions) {\n    super(context, Object.assign({}, PlaceRadialLabels.defaultOptions, options));\n  }\n\n  private get ref(): NodeData {\n    return this.context.model.getRootsData()[0];\n  }\n\n  public afterLayout() {\n    const refPoint = positionOf(this.ref);\n\n    const { graph, model } = this.context;\n    const data = model.getData();\n\n    data.nodes?.forEach((datum) => {\n      if (idOf(datum) === idOf(this.ref)) return;\n\n      const radian = rad(subtract(positionOf(datum), refPoint));\n      const isLeft = Math.abs(radian) > Math.PI / 2;\n\n      const isLeaf = !datum.children || datum.children.length === 0;\n      const nodeId = idOf(datum);\n      const node = this.context.element?.getElement(nodeId);\n      if (!node || !node.isVisible()) return;\n\n      const nodeHalfWidth = parseSize(graph.getElementRenderStyle(nodeId).size)[0] / 2;\n      const offset = (isLeaf ? 1 : -1) * (nodeHalfWidth + this.options.offset);\n\n      const labelTransform: TransformArray = [\n        ['translate', offset * Math.cos(radian), offset * Math.sin(radian)],\n        ['rotate', isLeft ? rad2deg(radian) + 180 : rad2deg(radian)],\n      ];\n\n      model.updateNodeData([\n        {\n          id: idOf(datum),\n          style: {\n            labelTextAlign: isLeft === isLeaf ? 'right' : 'left',\n            labelTextBaseline: 'middle',\n            labelTransform,\n          },\n        },\n      ]);\n    });\n\n    graph.draw();\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/transforms/process-parallel-edges.ts",
    "content": "import type { PathStyleProps } from '@antv/g';\nimport { isBoolean, isEmpty, isEqual, isFunction } from '@antv/util';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { EdgeData } from '../spec';\nimport type { EdgeStyle } from '../spec/element/edge';\nimport type { ID, LoopPlacement, NodeLikeData } from '../types';\nimport { groupByChangeType, reduceDataChanges } from '../utils/change';\nimport { idOf } from '../utils/id';\nimport type { BaseTransformOptions } from './base-transform';\nimport { BaseTransform } from './base-transform';\nimport { getEdgeEndsContext } from './get-edge-actual-ends';\nimport type { DrawData } from './types';\nimport { isStyleEqual, reassignTo } from './utils';\n\nconst CUBIC_EDGE_TYPE = 'quadratic';\n\nconst CUBIC_LOOP_PLACEMENTS: LoopPlacement[] = [\n  'top',\n  'top-right',\n  'right',\n  'right-bottom',\n  'bottom',\n  'bottom-left',\n  'left',\n  'left-top',\n];\n\nexport interface ProcessParallelEdgesOptions extends BaseTransformOptions {\n  /**\n   * <zh/> 处理模式\n   * - `'merge'`: 将平行边合并为一条边，适用于不需要区分平行边的情况\n   * - '`bundle`': 每条边都会与其他所有平行边捆绑在一起，并通过改变曲率与其他边分开。如果一组平行边的数量是奇数，那么中心的边将被绘制为直线，其他的边将被绘制为曲线\n   *\n   * <en/> Processing mode\n   * - '`merge`': Merge parallel edges into one edge which is suitable for cases where parallel edges do not need to be distinguished\n   * - '`bundle`': Each edge will be bundled with all other parallel edges and separated from them by varying the curvature. If the number of parallel edges in a group is odd, the central edge will be drawn as a straight line, and the others will be drawn as curves\n   * @defaultValue 'bundle'\n   */\n  mode: 'bundle' | 'merge';\n  /**\n   * <zh/> 考虑要处理的边，默认为全部边\n   *\n   * <en/> The edges to be handled, all edges by default\n   */\n  edges?: ID[];\n  /**\n   * <zh/> 边之间的距离，仅在捆绑模式下有效\n   *\n   * <en/> The distance between edges, only valid for bundling mode\n   */\n  distance?: number;\n  /**\n   * <zh/> 合并边的样式，仅在合并模式下有效\n   *\n   * <en/> The style of the merged edge, only valid for merging mode\n   */\n  style?: PathStyleProps | ((prev: EdgeData[]) => PathStyleProps);\n}\n\n/**\n * <zh/> 处理平行边，即多条边共享同一源节点和目标节点\n *\n * <en/> Process parallel edges which share the same source and target nodes\n * @remarks\n * <zh/> 平行边（Parallel Edges）是指在图结构中，两个节点之间存在多条边。这些边共享相同的源节点和目标节点，但可能代表不同的关系或属性。为了避免边的重叠和混淆，提供了两种处理平行边的方式：(1) 捆绑模式（bundle）：将平行边捆绑在一起，通过改变曲率与其他边分开；(2) 合并模式（merge）：将平行边合并为一条聚合。\n *\n * <en/> Parallel Edges refer to multiple edges existing between two nodes in a graph structure. These edges share the same source and target nodes but may represent different relationships or attributes. To avoid edge overlap and confusion, two methods are provided for handling parallel edges: (1) Bundle Mode: Bundles parallel edges together and separates them from other edges by altering their curvature; (2) Merge Mode: Merges parallel edges into a single aggregated edge.\n */\nexport class ProcessParallelEdges extends BaseTransform<ProcessParallelEdgesOptions> {\n  static defaultOptions: Partial<ProcessParallelEdgesOptions> = {\n    mode: 'bundle',\n    distance: 15, // only valid for bundling mode\n  };\n\n  private cacheMergeStyle: Map<ID, PathStyleProps> = new Map();\n\n  constructor(context: RuntimeContext, options: ProcessParallelEdgesOptions) {\n    super(context, Object.assign({}, ProcessParallelEdges.defaultOptions, options));\n  }\n\n  /**\n   * <zh/> 在每次绘制前处理平行边\n   *\n   * <en/> Process parallel edges before each drawing\n   * @param input\n   */\n  public beforeDraw(input: DrawData): DrawData {\n    const edges = this.getAffectedParallelEdges(input);\n\n    if (edges.size === 0) return input;\n\n    this.options.mode === 'bundle'\n      ? this.applyBundlingStyle(input, edges, this.options.distance)\n      : this.applyMergingStyle(input, edges);\n\n    return input;\n  }\n\n  /**\n   * <zh/> 获取受影响的平行边\n   *\n   * <en/> Get affected parallel edges\n   * @param input\n   */\n  private getAffectedParallelEdges = (input: DrawData): Map<ID, EdgeData> => {\n    const {\n      add: { edges: edgesToAdd },\n      update: { nodes: nodesToUpdate, edges: edgesToUpdate, combos: combosToUpdate },\n      remove: { edges: edgesToRemove },\n    } = input;\n\n    const { model } = this.context;\n    const edges: Map<ID, EdgeData> = new Map();\n\n    const addRelatedEdges = (_: NodeLikeData, id: ID) => {\n      const relatedEdgesData = model.getRelatedEdgesData(id);\n      relatedEdgesData.forEach((edge) => !edges.has(idOf(edge)) && edges.set(idOf(edge), edge));\n    };\n\n    nodesToUpdate.forEach(addRelatedEdges);\n    combosToUpdate.forEach(addRelatedEdges);\n\n    const pushParallelEdges = (edge: EdgeData) => {\n      // 获取已被标记删除的边ID集合\n      // Get the set of edge IDs that have been marked for removal\n      const removedEdgeIds = new Set(input.remove.edges.keys());\n\n      // 过滤掉已删除的边，避免重定向后重新添加（修复combo收起时内部边变成loop边的问题）\n      // Filter out removed edges to prevent them from being re-added after redirection (fixes the issue where internal edges become loop edges when combo collapses)\n      const validEdgeData = model\n        .getEdgeData()\n        .filter((edge) => !removedEdgeIds.has(idOf(edge)))\n        .map((edge) => getEdgeEndsContext(model, edge));\n\n      // 查找平行边并添加到处理列表，确保只处理有效的边\n      // Find parallel edges and add them to the processing list, ensuring only valid edges are processed\n      getParallelEdges(edge, validEdgeData, true).forEach((e) => {\n        const id = idOf(e);\n        if (!edges.has(id)) edges.set(id, e);\n      });\n    };\n\n    if (edgesToRemove.size) edgesToRemove.forEach(pushParallelEdges);\n\n    if (edgesToAdd.size) edgesToAdd.forEach(pushParallelEdges);\n\n    if (edgesToUpdate.size) {\n      const changes = groupByChangeType(reduceDataChanges(model.getChanges())).update.edges;\n      edgesToUpdate.forEach((edge) => {\n        pushParallelEdges(edge);\n        // 当边的端点发生变化时，将原始边及其平行边一并添加到更新列表 | Add the original edge and its parallel edges to the update list when the endpoints of the edge change\n        const originalEdge = changes.find((e) => idOf(e.value) === idOf(edge))?.original;\n        if (originalEdge && !isParallelEdges(edge, originalEdge)) {\n          pushParallelEdges(originalEdge);\n        }\n      });\n    }\n\n    if (!isEmpty(this.options.edges)) {\n      edges.forEach((_: EdgeData, id: ID) => !this.options.edges.includes(id) && edges.delete(id));\n    }\n\n    // 按照用户指定的顺序排序，防止捆绑时的抖动\n    // Sort by user-set order to prevent jitter during bundling\n    const edgeIds = model.getEdgeData().map(idOf);\n    return new Map([...edges].sort((a, b) => edgeIds.indexOf(a[0]) - edgeIds.indexOf(b[0])));\n  };\n\n  protected applyBundlingStyle = (input: DrawData, edges: Map<ID, EdgeData>, distance: number) => {\n    const { edgeMap, reverses } = groupByEndpoints(edges);\n\n    edgeMap.forEach((arcEdges) => {\n      arcEdges.forEach((edge, i, edgeArr) => {\n        const length = edgeArr.length;\n        const style: EdgeStyle = edge.style || {};\n        if (edge.source === edge.target) {\n          const len = CUBIC_LOOP_PLACEMENTS.length;\n          style.loopPlacement = CUBIC_LOOP_PLACEMENTS[i % len];\n          style.loopDist = Math.floor(i / len) * distance + 50;\n        } else if (length === 1) {\n          style.curveOffset = 0;\n        } else {\n          const sign = (i % 2 === 0 ? 1 : -1) * (reverses[`${edge.source}|${edge.target}|${i}`] ? -1 : 1);\n          style.curveOffset =\n            length % 2 === 1\n              ? sign * Math.ceil(i / 2) * distance * 2\n              : sign * (Math.floor(i / 2) * distance * 2 + distance);\n        }\n        const mergedEdgeData = Object.assign(edge, { type: CUBIC_EDGE_TYPE, style });\n\n        const element = this.context.element?.getElement(idOf(edge));\n\n        if (!element || !isStyleEqual(mergedEdgeData.style, element.attributes)) {\n          reassignTo(input, element ? 'update' : 'add', 'edge', mergedEdgeData, true);\n        }\n      });\n    });\n  };\n\n  private resetEdgeStyle = (edge: EdgeData) => {\n    const style = edge.style || {};\n    const cacheStyle = this.cacheMergeStyle.get(idOf(edge)) || {};\n    Object.keys(cacheStyle).forEach((key) => {\n      if (isEqual(style[key], (cacheStyle as any)[key])) {\n        if (edge[key]) {\n          style[key] = edge[key];\n        } else {\n          delete style[key];\n        }\n      }\n    });\n    return Object.assign(edge, { style });\n  };\n\n  protected applyMergingStyle = (input: DrawData, edges: Map<ID, EdgeData>) => {\n    const { edgeMap, reverses } = groupByEndpoints(edges);\n\n    edgeMap.forEach((edges) => {\n      if (edges.length === 1) {\n        const edge = edges[0];\n        const element = this.context.element?.getElement(idOf(edge));\n        const edgeStyle = this.resetEdgeStyle(edge);\n        if (!element || !isStyleEqual(edgeStyle, element.attributes)) {\n          reassignTo(input, element ? 'update' : 'add', 'edge', edgeStyle);\n        }\n        return;\n      }\n\n      const mergedStyle = edges\n        .map(({ source, target, style = {} }, i) => {\n          const { startArrow, endArrow } = style;\n          const newStyle: EdgeData['style'] = {};\n          const [start, end] = reverses[`${source}|${target}|${i}`]\n            ? ['endArrow', 'startArrow']\n            : ['startArrow', 'endArrow'];\n          if (isBoolean(startArrow)) newStyle[start] = startArrow;\n          if (isBoolean(endArrow)) newStyle[end] = endArrow;\n          return newStyle;\n        })\n        .reduce((acc, style) => ({ ...acc, ...style }), {});\n\n      edges.forEach((edge, i, edges) => {\n        if (i !== 0) {\n          reassignTo(input, 'remove', 'edge', edge);\n          return;\n        }\n        const parsedStyle = Object.assign(\n          {},\n          isFunction(this.options.style) ? this.options.style(edges) : this.options.style,\n          { childrenData: edges },\n        );\n        this.cacheMergeStyle.set(idOf(edge), parsedStyle);\n        const mergedEdgeData = {\n          ...edge,\n          type: 'line',\n          style: { ...edge.style, ...mergedStyle, ...parsedStyle },\n        };\n\n        const element = this.context.element?.getElement(idOf(edge));\n        if (!element || !isStyleEqual(mergedEdgeData.style, element.attributes)) {\n          reassignTo(input, element ? 'update' : 'add', 'edge', mergedEdgeData, true);\n        }\n      });\n    });\n  };\n}\n\n/**\n * <zh/> 优化的按照端点分组方法，时间复杂度O(n)\n *\n * <en/> Optimized method to group by endpoints, time complexity O(n)\n * @param edges - <zh/> 边集合 | <en/> Edges\n * @returns <zh/> 端点分组后的边集合 | <en/> Edges grouped by endpoints\n */\nexport const groupByEndpoints = (edges: Map<ID, EdgeData>) => {\n  const edgeMap = new Map<string, EdgeData[]>();\n  const processedEdgesSet = new Set<ID>();\n  const reverses: Record<string, boolean> = {};\n  const includedEdgesInGroup = new Map<string, Set<ID>>();\n\n  for (const [id, edge] of edges) {\n    if (processedEdgesSet.has(id)) continue;\n\n    const { source, target } = edge;\n    const sourceTarget = `${source}-${target}`;\n\n    if (!edgeMap.has(sourceTarget)) {\n      edgeMap.set(sourceTarget, []);\n      includedEdgesInGroup.set(sourceTarget, new Set<ID>());\n    }\n\n    const sourceTargetEdges = edgeMap.get(sourceTarget);\n    const includedEdges = includedEdgesInGroup.get(sourceTarget);\n\n    if (sourceTargetEdges && includedEdges && !includedEdges.has(id)) {\n      sourceTargetEdges.push(edge);\n      includedEdges.add(id);\n      processedEdgesSet.add(id);\n    }\n\n    for (const [otherId, sedge] of edges) {\n      if (processedEdgesSet.has(otherId) || otherId === id) continue;\n\n      if (isParallelEdges(edge, sedge)) {\n        const groupEdges = edgeMap.get(sourceTarget);\n        const includedGroupEdges = includedEdgesInGroup.get(sourceTarget);\n\n        if (groupEdges && includedGroupEdges && !includedGroupEdges.has(otherId)) {\n          groupEdges.push(sedge);\n          includedGroupEdges.add(otherId);\n\n          if (source === sedge.target && target === sedge.source) {\n            reverses[`${sedge.source}|${sedge.target}|${groupEdges.length - 1}`] = true;\n          }\n\n          processedEdgesSet.add(otherId);\n        }\n      }\n    }\n  }\n\n  return { edgeMap, reverses };\n};\n\n/**\n * <zh/> 获取平行边\n *\n * <en/> Get parallel edges\n * @param edge - <zh/> 目标边 | <en/> Target edge\n * @param edges - <zh/> 边集合 | <en/> Edges\n * @param containsSelf - <zh/> 输出结果是否包含目标边 | <en/> Whether the output result contains the target edge\n * @returns <zh/> 平行边集合 | <en/> Parallel edges\n */\nexport const getParallelEdges = (edge: EdgeData, edges: EdgeData[], containsSelf?: boolean): EdgeData[] => {\n  return edges.filter((e) => (containsSelf || idOf(e) !== idOf(edge)) && isParallelEdges(e, edge));\n};\n\n/**\n * <zh/> 判断两条边是否平行\n *\n * <en/> Determine whether two edges are parallel\n * @param edge1 - <zh/> 边1 | <en/> Edge 1\n * @param edge2 - <zh/> 边2 | <en/> Edge 2\n * @returns <zh/> 是否平行 | <en/> Whether is parallel\n */\nexport const isParallelEdges = (edge1: EdgeData, edge2: EdgeData) => {\n  const { sourceNode: src1, targetNode: tgt1 } = edge1.style || {};\n  const { sourceNode: src2, targetNode: tgt2 } = edge2.style || {};\n  return (src1 === src2 && tgt1 === tgt2) || (src1 === tgt2 && tgt1 === src2);\n};\n"
  },
  {
    "path": "packages/g6/src/transforms/types.ts",
    "content": "import type { ComboData, EdgeData, NodeData } from '../spec';\nimport type { ID } from '../types';\nimport type { BaseTransform } from './base-transform';\n\nexport type Transform = BaseTransform<any>;\n\n/**\n * <zh/> 在 Element Controller 中，为了提高查询性能，统一使用 Map 存储数据\n *\n * <en/> In Element Controller, in order to improve query performance, use Map to store data uniformly\n */\nexport type ProcedureData = {\n  nodes: Map<ID, NodeData>;\n  edges: Map<ID, EdgeData>;\n  combos: Map<ID, ComboData>;\n};\n\nexport type DrawData = {\n  add: ProcedureData;\n  update: ProcedureData;\n  remove: ProcedureData;\n};\n"
  },
  {
    "path": "packages/g6/src/transforms/update-related-edge.ts",
    "content": "import type { DrawContext } from '../runtime/element';\nimport type { ID, NodeLikeData } from '../types';\nimport { idOf } from '../utils/id';\nimport { BaseTransform } from './base-transform';\nimport type { DrawData } from './types';\n\n/**\n * 如果更新了节点 / combo，需要更新连接的边\n * If the node / combo is updated, the connected edge and the combo it is in need to be updated\n */\nexport class UpdateRelatedEdge extends BaseTransform {\n  public beforeDraw(input: DrawData, context: DrawContext): DrawData {\n    const { stage } = context;\n    if (stage === 'visibility') return input;\n\n    const { model } = this.context;\n    const {\n      update: { nodes, edges, combos },\n    } = input;\n\n    const addRelatedEdges = (_: NodeLikeData, id: ID) => {\n      const relatedEdgesData = model.getRelatedEdgesData(id);\n      relatedEdgesData.forEach((edge) => !edges.has(idOf(edge)) && edges.set(idOf(edge), edge));\n    };\n\n    nodes.forEach(addRelatedEdges);\n    combos.forEach(addRelatedEdges);\n\n    return input;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/transforms/utils.ts",
    "content": "import type { ElementDatum, ElementType } from '../types';\nimport { idOf } from '../utils/id';\nimport type { DrawData, ProcedureData } from './types';\n\n/**\n * <zh/> 重新分配绘制任务\n *\n * <en/> Reassign drawing tasks\n * @param input - <zh/>绘制数据 | <en/>DrawData\n * @param type - <zh/>类型 | <en/>type\n * @param elementType - <zh/>元素类型 | <en/>element type\n * @param datum - <zh/>数据 | <en/>data\n * @param overwrite - <zh/>是否覆盖现有数据 | <en/>whether to overwrite existing data\n */\nexport function reassignTo(\n  input: DrawData,\n  type: 'add' | 'update' | 'remove',\n  elementType: ElementType,\n  datum: ElementDatum,\n  overwrite?: boolean,\n) {\n  const id = idOf(datum);\n  const typeName = `${elementType}s` as keyof ProcedureData;\n  const exitsDatum: any = overwrite\n    ? datum\n    : input.add[typeName].get(id) || input.update[typeName].get(id) || input.remove[typeName].get(id) || datum;\n  Object.entries(input).forEach(([_type, value]) => {\n    if (type === _type) value[typeName].set(id, exitsDatum);\n    else value[typeName].delete(id);\n  });\n}\n\n/**\n * <zh/> 判断样式是否与原始样式一致\n *\n * <en/> Determine whether the style is consistent with the original style\n * @param style - <zh/> 样式 | <en/> style\n * @param originalStyle - <zh/> 原始样式 | <en/> original style\n * @returns <zh/> 是否一致 | <en/> Whether it is consistent\n */\nexport function isStyleEqual(style: Record<string, unknown>, originalStyle: Record<string, unknown>) {\n  return Object.keys(style).every((key) => style[key] === originalStyle[key]);\n}\n"
  },
  {
    "path": "packages/g6/src/types/anchor.ts",
    "content": "import type { Vector2, Vector3 } from './vector';\n\nexport type Anchor = string | Vector2 | Vector3;\n\nexport type STDAnchor = Vector2;\n"
  },
  {
    "path": "packages/g6/src/types/animation.ts",
    "content": "import type { IAnimation } from '@antv/g';\n\nexport type Keyframe = {\n  [key: string]: any;\n};\n\nexport type AnimationTask = () => () => IAnimation | null;\n"
  },
  {
    "path": "packages/g6/src/types/canvas.ts",
    "content": "export type CanvasLayer = 'background' | 'main' | 'label' | 'transient';\n"
  },
  {
    "path": "packages/g6/src/types/centrality.ts",
    "content": "import type { EdgeDirection } from './edge';\n\nexport type NodeCentralityOptions =\n  | { type: 'degree'; direction?: EdgeDirection }\n  | { type: 'betweenness'; directed?: boolean; weightPropertyName?: string }\n  | { type: 'closeness'; directed?: boolean; weightPropertyName?: string }\n  | { type: 'eigenvector'; directed?: boolean }\n  | { type: 'pagerank'; epsilon?: number; linkProb?: number };\n"
  },
  {
    "path": "packages/g6/src/types/change.ts",
    "content": "import { ChangeType } from '../constants';\nimport type { ComboData, EdgeData, NodeData } from '../spec/data';\nimport { Loosen } from './enum';\n\n/**\n * <zh/> 数据变更\n *\n * <en/> Data change\n */\nexport type DataChange = DataAdded | DataUpdated | DataRemoved;\n\nexport type DataAdded = NodeAdded | EdgeAdded | ComboAdded;\n\nexport type DataUpdated = NodeUpdated | EdgeUpdated | ComboUpdated;\n\nexport type DataRemoved = NodeRemoved | EdgeRemoved | ComboRemoved;\n\nexport type NodeAdded = {\n  type: Loosen<ChangeType.NodeAdded>;\n  value: NodeData;\n};\n\nexport type NodeUpdated = {\n  type: Loosen<ChangeType.NodeUpdated>;\n  value: NodeData;\n  original: NodeData;\n};\n\nexport type NodeRemoved = {\n  type: Loosen<ChangeType.NodeRemoved>;\n  value: NodeData;\n};\n\nexport type EdgeAdded = {\n  type: Loosen<ChangeType.EdgeAdded>;\n  value: EdgeData;\n};\n\nexport type EdgeUpdated = {\n  type: Loosen<ChangeType.EdgeUpdated>;\n  value: EdgeData;\n  original: EdgeData;\n};\n\nexport type EdgeRemoved = {\n  type: Loosen<ChangeType.EdgeRemoved>;\n  value: EdgeData;\n};\n\nexport type ComboAdded = {\n  type: Loosen<ChangeType.ComboAdded>;\n  value: ComboData;\n};\n\nexport type ComboUpdated = {\n  type: Loosen<ChangeType.ComboUpdated>;\n  value: ComboData;\n  original: ComboData;\n};\n\nexport type ComboRemoved = {\n  type: Loosen<ChangeType.ComboRemoved>;\n  value: ComboData;\n};\n\nexport type DataChanges = {\n  add: {\n    nodes: NodeAdded[];\n    edges: EdgeAdded[];\n    combos: ComboAdded[];\n  };\n  update: {\n    nodes: NodeUpdated[];\n    edges: EdgeUpdated[];\n    combos: ComboUpdated[];\n  };\n  remove: {\n    nodes: NodeRemoved[];\n    edges: EdgeRemoved[];\n    combos: ComboRemoved[];\n  };\n};\n"
  },
  {
    "path": "packages/g6/src/types/combo.ts",
    "content": "import type { IconStyleProps } from '../elements/shapes';\nimport type { NodeLikeData } from './data';\n\n/**\n * <zh/> 组合收起时显示的标记样式配置项\n *\n * <en/> Style properties of the marker displayed when the combo is collapsed\n */\nexport interface CollapsedMarkerStyleProps extends IconStyleProps {\n  /**\n   * <zh/> 组合收起时显示的标记类型\n   * - `'child-count'`: 子元素数量（包括 Node 和 Combo）\n   * - `'descendant-count'`: 后代元素数量（包括 Node 和 Combo）\n   * - `'node-count'`: 后代元素数量（只包括 Node）\n   * - `(children: NodeLikeData[]) => string`: 自定义处理逻辑\n   *\n   * <en/> The type of marker displayed when the combo is collapsed\n   * - `'child-count'`: Number of child elements (including Nodes and Combos)\n   * - `'descendant-count'`: Number of descendant elements (including Nodes and Combos)\n   * - `'node-count'`: Number of descendant elements (only Nodes)\n   * - `(children: NodeLikeData[]) => string`: Custom processing logic\n   */\n  type?: 'child-count' | 'descendant-count' | 'node-count' | ((children: NodeLikeData[]) => string);\n}\n"
  },
  {
    "path": "packages/g6/src/types/data.ts",
    "content": "import type { ComboData, EdgeData, NodeData } from '../spec/data';\nimport type { ID } from '../types';\n\nexport type DataID = {\n  nodes?: ID[];\n  edges?: ID[];\n  combos?: ID[];\n};\n\nexport type NodeLikeData = NodeData | ComboData;\n\nexport type ElementDatum = NodeData | EdgeData | ComboData;\n\nexport type ElementData = NodeData[] | EdgeData[] | ComboData[];\n\n/**\n * <zh/> 节点、边更新可选数据\n *\n * <en/> Node, edge update optional data\n * @remarks\n * <zh/> 必须包含 id 字段，其他字段可选\n *\n * <en/> Must contain the id field, other fields are optional\n */\nexport type PartialNodeLikeData<T extends NodeLikeData> = Partial<T> & Pick<T, 'id'>;\n\n/**\n * <zh/> 边更新可选数据\n *\n * <en/> Edge update optional data\n * @remarks\n * <zh/> 包含两种情况：\n * 1. 必须包含 source、target 字段，其他字段可选\n * 2. 必须包含 id 字段，其他字段可选\n *\n * <en/> Contains two cases:\n * 1. Must contain the source and target fields, other fields are optional\n * 2. Must contain the id field, other fields are optional\n */\nexport type PartialEdgeData<T extends EdgeData> =\n  | (Partial<T> & Pick<T, 'source' | 'target'>)\n  | (Partial<T> & Pick<T, 'id'>);\n\n/**\n * <zh/> G6 数据更新可选数据\n *\n * <en/> G6 data update optional data\n */\nexport type PartialGraphData = {\n  nodes?: PartialNodeLikeData<NodeData>[];\n  edges?: PartialEdgeData<EdgeData>[];\n  combos?: PartialNodeLikeData<ComboData>[];\n};\n\n/**\n * <zh/> 层级结构类别\n *\n * <en/> Hierarchy structure category\n * @remarks\n * <zh/> G6 中树形层级结构和组合层级结构是相互独立的，分别对应不同的数据结构\n * 一些 API 需要指定层级结构类别，例如 getAncestorsData、getParentData\n *\n * <en/> The tree hierarchy structure and the combo hierarchy structure in G6 are independent of each other, corresponding to different data structures\n * Some APIs need to specify the hierarchy structure category, such as getAncestorsData, getParentData\n */\nexport type HierarchyKey = 'tree' | 'combo';\n"
  },
  {
    "path": "packages/g6/src/types/edge.ts",
    "content": "import { ImageStyleProps, Line, Path, PathStyleProps, Polyline } from '@antv/g';\nimport { PathArray } from '@antv/util';\nimport type { BadgeStyleProps, LabelStyleProps } from '../elements/shapes';\nimport type { CardinalPlacement, CornerPlacement } from './placement';\nimport { Size } from './size';\n\n/**\n * <zh/> 边的方向\n * - `'in'`: 入边\n * - `'out'`: 出边\n * - `'both'`: 双向边\n *\n * <en/> Edge direction\n * - `'in'`: Inbound edge\n * - `'out'`: Outbound edge\n * - `'both'`: Bidirectional edge\n */\nexport type EdgeDirection = 'in' | 'out' | 'both';\n\nexport type EdgeKey = Line | Path | Polyline;\n\n/**\n * <zh/> 边上标签样式配置项\n *\n * <en/> Edge label style properties\n */\nexport interface EdgeLabelStyleProps extends LabelStyleProps {\n  /**\n   * <zh/> 标签相对于边的位置。取值范围为 'start'、'center'、'end' 或特定比率（数字 0-1）\n   *\n   * <en/> Label position relative to the edge (keyShape) that can be 'start', 'center', 'end' or a specific ratio (number 0-1)\n   * @defaultValue 'center'\n   */\n  placement?: 'start' | 'center' | 'end' | number;\n  /**\n   * <zh/> 标签平行于边的水平偏移量\n   *\n   * <en/> The horizontal offset of the label parallel to the edge\n   * @defaultValue 4\n   */\n  offsetX?: number;\n  /**\n   * <zh/> 标签垂直于边的垂直偏移量\n   *\n   * <en/> The vertical offset of the label perpendicular to the edge\n   * @defaultValue 0\n   */\n  offsetY?: number;\n  /**\n   * <zh/> 是否自动旋转，保持与边的方向一致\n   *\n   * <en/> Indicates whether to automatically rotate the label to keep it consistent with the direction of the edge\n   * @defaultValue true\n   */\n  autoRotate?: boolean;\n  /**\n   * <zh/> 标签最大宽度（需要 [prefix]WordWrap 为 true）\n   * - string: 表示以相对于边长度的百分比形式定义最大宽度。例如 `\"50%\"` 表示标签宽度不超过边长度的一半\n   * - number: 表示以像素值为单位定义最大宽度。例如 `100` 表示标签的最大宽度为 100 像素\n   *\n   * <en/> The maximum width of the label(need [prefix]WordWrap to be true)\n   * - string: When set to a string, it defines the maximum width as a percentage of the edge length. For example, `\"50%\"` means the label width does not exceed half of the edge length\n   * - number: When set to a number, it defines the maximum width in pixels. For example, `100` means the maximum width of the label is 100 pixels\n   * @defaultValue '80%'\n   */\n  maxWidth?: string | number;\n}\n\n/**\n * <zh/> 边上徽标样式配置项\n *\n * <en/> Edge badge style properties\n */\nexport interface EdgeBadgeStyleProps extends BadgeStyleProps {\n  /**\n   * <zh/> 徽标的位置\n   * - `'prefix'`: 置于标签前\n   * - `'suffix'`: 置于标签后\n   *\n   * <en/> The position of the badge\n   * - `'prefix'`: Placed before the label\n   * - `'suffix'`: Placed after the label\n   */\n  placement: 'prefix' | 'suffix';\n  /**\n   * <zh/> 徽标在 X 轴上的偏移量\n   *\n   * <en/> The offset of the badge on the X-axis\n   */\n  offsetX?: number;\n  /**\n   * <zh/> 徽标在 Y 轴上的偏移量\n   *\n   * <en/> The offset of the badge on the Y-axis\n   */\n  offsetY?: number;\n}\n\n/**\n * <zh/> 边上箭头的样式配置项\n *\n * <en/> Edge arrow style properties\n */\nexport interface EdgeArrowStyleProps\n  extends PathStyleProps, Omit<ImageStyleProps, 'width' | 'height'>, Record<string, unknown> {\n  /**\n   * <zh/> 箭头大小\n   *\n   * <en/> Arrow size\n   * @defaultValue 8\n   */\n  size?: Size;\n  /**\n   * <zh/> 箭头类型\n   *\n   * <en/> Arrow type\n   * @defaultValue 'triangle'\n   */\n  type?:\n    | 'triangle'\n    | 'circle'\n    | 'diamond'\n    | 'vee'\n    | 'rect'\n    | 'triangleRect'\n    | 'simple'\n    | ((width: number, height: number) => PathArray);\n}\n\nexport type LoopPlacement = CardinalPlacement | CornerPlacement;\n\n/**\n * <zh/> 自环样式配置项\n *\n * <en/> Loop style properties\n */\nexport interface LoopStyleProps {\n  /**\n   * <zh/> 边的位置\n   *\n   * <en/> The position of the edge\n   * @defaultValue 'top'\n   */\n  placement?: LoopPlacement;\n  /**\n   * <zh/> 指定是否顺时针绘制环\n   *\n   * <en/> Specify whether to draw the loop clockwise\n   * @defaultValue true\n   */\n  clockwise?: boolean;\n  /**\n   * <zh/> 从节点 keyShape 边缘到自环顶部的距离，用于指定自环的曲率，默认为宽度或高度的最大值\n   *\n   * <en/> Determine the position from the edge of the node keyShape to the top of the self-loop, used to specify the curvature of the self-loop, the default value is the maximum of the width or height\n   */\n  dist?: number;\n}\n"
  },
  {
    "path": "packages/g6/src/types/element.ts",
    "content": "import type { DisplayObject } from '@antv/g';\nimport type { ComboOptions, EdgeOptions, NodeOptions } from '../spec';\nimport type { Point, Port } from '../types';\n\n/**\n * <zh/> 节点类型\n *\n * <en/> Node type\n */\nexport interface Node extends DisplayObject, ElementHooks, ElementMethods {\n  /**\n   * <zh/> 获取连接桩\n   *\n   * <en/> Get the ports\n   */\n  getPorts(): Record<string, Port>;\n\n  /**\n   * <zh/> 获取节点中心位置\n   *\n   * <en/> Get the center position of the node\n   */\n  getCenter(): Point;\n\n  /**\n   * <zh/> 获取交点位置\n   *\n   * <en/> Get the intersection point\n   * @param point - <zh/> 外部位置 | <en/> external position\n   * @param useExtendedLine - <zh/> 是否使用延长线 | <en/> whether to use the extended line\n   * @returns <zh/> 交点位置 | <en/> intersection point\n   * @remarks\n   * <zh/> 给定一个外部位置，返回当前节点与该位置的连边与节点的交点位置\n   *\n   * <en/> Given an external position, return the intersection point of the edge between the current node and the position and the node\n   */\n  getIntersectPoint(point: Point, useExtendedLine?: boolean): Point;\n}\n\n/**\n * <zh/> 边类型\n *\n * <en/> Edge type\n */\nexport interface Edge extends DisplayObject, ElementHooks, ElementMethods {}\n\n/**\n * <zh/> 组合类型\n *\n * <en/> Combo type\n */\nexport interface Combo extends Node {\n  /**\n   * <zh/> 获取组合的位置\n   *\n   * <en/> Get the position of the combo\n   * @param attributes - <zh/> 组合属性 | <en/> combo attributes\n   */\n  getComboPosition(attributes: Record<string, unknown>): Point;\n}\n\nexport type Element = Node | Edge | Combo;\n\nexport type ElementType = 'node' | 'edge' | 'combo';\n\nexport type ElementOptions = NodeOptions | EdgeOptions | ComboOptions;\n\n/**\n * <zh/> 元素方法\n *\n * <en/> Element methods\n */\nexport interface ElementMethods {\n  /**\n   * <zh/> 更新元素属性\n   *\n   * <en/> Update element attributes\n   * @param attr - <zh/> 属性 | <en/> Attributes\n   */\n  update(attr: any): void;\n  /**\n   * <zh/> 获取当前元素内的子图形\n   *\n   * <en/> Get the subgraph in the current element\n   * @param shapeID - <zh/> 子图形 ID | <en/> Subgraph ID\n   * @returns <zh/> 子图形 | <en/> Subgraph\n   */\n  getShape<T extends DisplayObject = DisplayObject>(shapeID: string): T;\n}\n\n/**\n * <zh/> 元素钩子方法\n *\n * <en/> Element hooks\n */\nexport interface ElementHooks {\n  /**\n   * <zh/> 在元素完成创建并执行完入场动画后调用\n   *\n   * <en/> Called after the element is created and the entrance animation is completed\n   * @override\n   */\n  onCreate?: () => void;\n\n  /**\n   * <zh/> 在元素更新并执行完过渡动画后调用\n   *\n   * <en/> Called after the element is updated and the transition animation is completed\n   * @override\n   */\n  onUpdate?: () => void;\n\n  /**\n   * <zh/> 在元素完成退场动画并销毁后调用\n   *\n   * <en/> Called after the element completes the exit animation and is destroyed\n   * @override\n   */\n  onDestroy?: () => void;\n}\n"
  },
  {
    "path": "packages/g6/src/types/enum.ts",
    "content": "export type Loosen<T extends string> = `${T}`;\n"
  },
  {
    "path": "packages/g6/src/types/event.ts",
    "content": "import type {\n  DisplayObject,\n  Document,\n  FederatedEvent,\n  FederatedPointerEvent,\n  FederatedWheelEvent,\n  IAnimation,\n} from '@antv/g';\nimport type { AnimationType } from '../constants';\nimport type { ElementDatum } from './data';\nimport type { Element, ElementType } from './element';\nimport type { TransformOptions } from './viewport';\n\nexport type IEvent =\n  | IGraphLifeCycleEvent\n  | IAnimateEvent\n  | IElementLifeCycleEvent\n  | IViewportEvent\n  | IPointerEvent\n  | IWheelEvent\n  | IKeyboardEvent\n  | IDragEvent;\n\nexport interface IPointerEvent<T extends Target = Target> extends TargetedEvent<FederatedPointerEvent, T> {}\n\nexport interface IWheelEvent<T extends Target = Target> extends TargetedEvent<FederatedWheelEvent, T> {}\n\nexport interface IKeyboardEvent extends KeyboardEvent {}\n\nexport interface IElementEvent extends IPointerEvent<Element> {}\n\nexport interface IElementDragEvent extends IDragEvent<Element> {}\n\nexport interface IDragEvent<T extends Target = Target> extends TargetedEvent<FederatedPointerEvent, T> {\n  dx: number;\n  dy: number;\n}\n\nexport interface IGraphLifeCycleEvent extends NativeEvent {\n  data?: any;\n}\n\nexport interface IElementLifeCycleEvent extends NativeEvent {\n  elementType: ElementType;\n  data: ElementDatum;\n}\n\nexport interface IViewportEvent extends NativeEvent {\n  data: TransformOptions;\n}\n\nexport interface IAnimateEvent extends NativeEvent {\n  animationType: AnimationType;\n  animation: IAnimation | null;\n  data?: any;\n}\n\n/**\n * <zh/> G6 原生事件\n *\n * <en/> G6 native event\n */\ninterface NativeEvent {\n  type: string;\n}\n\n/**\n * <zh/> 具有目标的事件\n *\n * <en/> Event with target\n */\ntype TargetedEvent<E extends FederatedEvent, T extends Target = Target> = Omit<E, 'target'> & {\n  originalTarget: DisplayObject;\n  target: T;\n  targetType: 'canvas' | 'node' | 'edge' | 'combo';\n};\n\nexport type Target = Document | Element;\n"
  },
  {
    "path": "packages/g6/src/types/graphlib.ts",
    "content": "import type {\n  EdgeAdded,\n  EdgeDataUpdated,\n  EdgeRemoved,\n  EdgeUpdated,\n  NodeAdded,\n  NodeDataUpdated,\n  NodeRemoved,\n  TreeStructureAttached,\n  TreeStructureChanged,\n  TreeStructureDetached,\n} from '@antv/graphlib';\nimport type { EdgeData } from '../spec';\nimport type { NodeLikeData } from './data';\n\nexport type GraphLibGroupedChanges = {\n  NodeRemoved: NodeRemoved<NodeLikeData>[];\n  EdgeRemoved: EdgeRemoved<EdgeData>[];\n  NodeAdded: NodeAdded<NodeLikeData>[];\n  EdgeAdded: EdgeAdded<EdgeData>[];\n  NodeDataUpdated: NodeDataUpdated<NodeLikeData>[];\n  EdgeUpdated: EdgeUpdated<EdgeData>[];\n  EdgeDataUpdated: EdgeDataUpdated<EdgeData>[];\n  TreeStructureChanged: TreeStructureChanged[];\n  ComboStructureChanged: TreeStructureChanged[];\n  TreeStructureAttached: TreeStructureAttached[];\n  TreeStructureDetached: TreeStructureDetached[];\n};\n"
  },
  {
    "path": "packages/g6/src/types/history.ts",
    "content": "import type { GraphData } from '../spec';\n\n/**\n * <zh/> 单条历史记录命令\n *\n * <en/> Single history record command\n */\nexport interface Command {\n  /**\n   * <zh/> 当前数据\n   *\n   * <en/> Current data\n   */\n  current: CommandData;\n  /**\n   * <zh/> 原始数据\n   *\n   * <en/> Original data\n   */\n  original: CommandData;\n  /**\n   * <zh/> 是否开启动画\n   *\n   * <en/> Whether to enable animation\n   */\n  animation: boolean;\n}\n\n/**\n * <zh/> 单条历史记录命令数据\n *\n * <en/> Single history record command data\n */\nexport interface CommandData {\n  /**\n   * <zh/> 新增的数据\n   *\n   * <en/> Added data\n   */\n  add: GraphData;\n  /**\n   * <zh/> 更新的数据\n   *\n   * <en/> Updated data\n   */\n  update: GraphData;\n  /**\n   * <zh/> 移除的数据\n   *\n   * <en/> Removed data\n   */\n  remove: GraphData;\n}\n"
  },
  {
    "path": "packages/g6/src/types/id.ts",
    "content": "export type ID = string;\n"
  },
  {
    "path": "packages/g6/src/types/index.ts",
    "content": "export type * from './anchor';\nexport type * from './animation';\nexport type * from './canvas';\nexport type * from './centrality';\nexport type * from './change';\nexport type * from './combo';\nexport type * from './data';\nexport type * from './edge';\nexport type * from './element';\nexport type * from './enum';\nexport type * from './event';\nexport type * from './graphlib';\nexport type * from './id';\nexport type * from './layout';\nexport type * from './node';\nexport type * from './padding';\nexport type * from './placement';\nexport type * from './point';\nexport type * from './prefix';\nexport type * from './router';\nexport type * from './size';\nexport type * from './state';\nexport type * from './style';\nexport type * from './tree';\nexport type * from './vector';\nexport type * from './viewport';\n"
  },
  {
    "path": "packages/g6/src/types/layout.ts",
    "content": "import type { AntVLayout, LegacyAntVLayout } from '../layouts/types';\nimport type { GraphData } from '../spec/data';\nimport type { STDLayoutOptions } from '../spec/layout';\n\nexport interface AdaptiveLayout {\n  instance: AntVLayout | LegacyAntVLayout;\n\n  execute(model: GraphData, options?: STDLayoutOptions): Promise<GraphData>;\n}\n"
  },
  {
    "path": "packages/g6/src/types/node.ts",
    "content": "import type { BaseStyleProps, CircleStyleProps, DisplayObject } from '@antv/g';\nimport type { BadgeStyleProps, LabelStyleProps } from '../elements/shapes';\nimport type {\n  CardinalPlacement,\n  CornerPlacement,\n  DirectionalPlacement,\n  Placement,\n  RelativePlacement,\n} from './placement';\nimport type { Point } from './point';\n\nexport type PortPlacement = RelativePlacement | CardinalPlacement;\nexport type StarPortPlacement = RelativePlacement | 'top' | 'left' | 'right' | 'left-bottom' | 'right-bottom';\nexport type TrianglePortPlacement = RelativePlacement | CardinalPlacement;\n\n/**\n * <zh/> 三角形指向\n *\n * <en/> Triangle direction\n */\nexport type TriangleDirection = 'up' | 'left' | 'right' | 'down';\n\n/**\n * <zh/> 节点标签样式配置项\n *\n * <en/> Node label style props\n */\nexport interface NodeLabelStyleProps extends LabelStyleProps {\n  /**\n   * <zh/> 标签相对于节点（keyShape）的位置\n   *\n   * <en/> Label position relative to the node (keyShape)\n   * @defaultValue 'bottom'\n   */\n  placement?: DirectionalPlacement;\n  /**\n   * <zh/> 标签最大宽度（需要 [prefix]WordWrap 为 true）\n   * - string: 表示以相对于节点宽度的百分比形式定义最大宽度。例如 `\"50%\"` 表示标签宽度不超过节点宽度的一半\n   * - number: 表示以像素值为单位定义最大宽度。例如 `100` 表示标签的最大宽度为 100 像素\n   *\n   * <en/> The maximum width of the label(need [prefix]WordWrap to be true)\n   * - string: When set to a string, it defines the maximum width as a percentage of the node width. For example, `\"50%\"` means the label width does not exceed half of the node width\n   * - number: When set to a number, it defines the maximum width in pixels. For example, `100` means the maximum width of the label is 100 pixels\n   * @defaultValue '200%'\n   */\n  maxWidth?: string | number;\n  /**\n   * <zh/> 标签在 x 轴方向上的偏移量\n   *\n   * <en/> The offset of the label in the x-axis direction\n   */\n  offsetX?: number;\n  /**\n   * <zh/> 标签在 y 轴方向上的偏移量\n   *\n   * <en/> The offset of the label in the y-axis direction\n   */\n  offsetY?: number;\n}\n\n/**\n * <zh/> 节点徽标样式配置项\n *\n * <en/> Node badge style props\n */\nexport interface NodeBadgeStyleProps extends BadgeStyleProps {\n  /**\n   * <zh/> 徽标相对于节点（keyShape）的位置\n   *\n   * <en/> Badge position relative to the node (keyShape)\n   */\n  placement?: CardinalPlacement | CornerPlacement;\n  /**\n   * <zh/> 徽标在 x 轴方向上的偏移量\n   *\n   * <en/> The offset of the badge in the x-axis direction\n   */\n  offsetX?: number;\n  /**\n   * <zh/> 徽标在 y 轴方向上的偏移量\n   *\n   * <en/> The offset of the badge in the y-axis direction\n   */\n  offsetY?: number;\n}\n\n/**\n * <zh/> 连接桩样式配置项\n *\n * <en/> Port style props\n */\nexport interface PortStyleProps extends Omit<CircleStyleProps, 'r'> {\n  /**\n   * <zh/> 边是否连接到连接桩的中心\n   * - 若为 `true`，则边连接到连接桩的中心\n   * - 若为 `false`，则边连接到连接桩的边缘\n   *\n   * <en/> Whether the edge is connected to the center of the port\n   * - If `true`, the edge is connected to the center of the port\n   * - If `false`, the edge is connected to the edge of the port\n   * @defaultValue false\n   */\n  linkToCenter?: boolean;\n  /**\n   * <zh/> 连接桩半径\n   * - 如果设置为 `undefined`，则连接桩被视为一个点，不在画布上显示但存在，边会优先连接到最近的连接桩\n   * - 如果设置为数字，则连接桩被视为一个圆，圆的半径由此处指定\n   *\n   * <en/> The radius of the port\n   * - If set to `undefined`, the port is treated as a point, not displayed on the canvas but exists, and the edge will be connected to the nearest port first\n   * - If set to a number, the port is treated as a circle, and the radius of the circle is specified here\n   */\n  r?: number;\n}\n\nexport type Port = DisplayObject<PortStyleProps> | Point;\n\n/**\n * <zh/> 节点连接桩样式配置项\n *\n * <en/> Node port style props\n */\nexport interface NodePortStyleProps extends PortStyleProps {\n  /**\n   * <zh/> 连接桩的键值，默认为连接桩的索引\n   *\n   * <en/> The key of the port. Default is the index of the port\n   */\n  key?: string;\n  /**\n   * <zh/> 连接桩相对于节点（keyShape）的位置。值可以是字符串或两个数字的元组。\n   *\n   * <en/> The position of the port relative to the node (keyShape). The value can be a string or a tuple of two numbers.\n   * - If the value is a string, it will be treated as the position direction.\n   * - If the value is a tuple of two numbers, it will be treated as the position coordinates(0 ~ 1).\n   */\n  placement: Placement;\n}\n\n/**\n * <zh/> 甜甜圈节点中的圆环样式配置项\n *\n * <en/> Ring style props in the donut node\n */\nexport interface DonutRound extends BaseStyleProps {\n  /**\n   * <zh/> 数值，用于计算比例\n   *\n   * <en/> Numerical value used to calculate the scale.\n   */\n  value: number;\n  /**\n   * <zh/> 颜色\n   *\n   * <en/> Color.\n   */\n  color?: string;\n}\n"
  },
  {
    "path": "packages/g6/src/types/padding.ts",
    "content": "export type Padding = number | number[];\n\nexport type STDPadding = [number, number, number, number];\n"
  },
  {
    "path": "packages/g6/src/types/placement.ts",
    "content": "export type CardinalPlacement = 'left' | 'right' | 'top' | 'bottom';\n\nexport type CornerPlacement =\n  | 'left-top'\n  | 'left-bottom'\n  | 'right-top'\n  | 'right-bottom'\n  | 'top-left'\n  | 'top-right'\n  | 'bottom-left'\n  | 'bottom-right';\n\nexport type RelativePlacement = [number, number];\n\nexport type DirectionalPlacement = CardinalPlacement | CornerPlacement | 'center';\n\nexport type Placement = RelativePlacement | DirectionalPlacement;\n"
  },
  {
    "path": "packages/g6/src/types/plugin.ts",
    "content": "import type { DisplayObject, FederatedEvent } from '@antv/g';\n\nexport type PluginEvent<T extends Event | FederatedEvent = Event> = Omit<T, 'target'> & {\n  targetType: 'canvas' | 'node' | 'edge' | 'combo';\n  target: DisplayObject;\n};\n"
  },
  {
    "path": "packages/g6/src/types/point.ts",
    "content": "export type Point = [number, number] | [number, number, number] | Float32Array;\n\nexport type PointObject = {\n  x: number;\n  y: number;\n  z?: number;\n};\n"
  },
  {
    "path": "packages/g6/src/types/prefix.ts",
    "content": "export type PrefixKey<P extends string = string, K extends string = string> = `${P}${Capitalize<K>}`;\n\n/**\n * @remarks\n * <zh/> `Prefix<P, T>` 是一种类型模式，用于将特定的前缀 `P` 应用到类型 `T` 上。在我们的配置中，这意味着我们将为一组属性或行为添加一个前缀，以表示它们属于某个具体的上下文或分类。\n *\n * <zh/> 当你在文档中看到类似 `Prefix<'label', StyleProps>` 的表达式，它表示给 `StyleProps` 类型的属性加上 `label` 前缀，以形成新的属性名称。例如，`color` 属性将以 `labelColor` 形式使用。\n *\n * <en/> `Prefix<P, T>` is a type pattern used to apply a specific prefix `P` to the type `T`. In our configuration, this means adding a prefix to a set of properties or behaviors to indicate that they belong to a specific context or category.\n *\n * <en/> When you see expressions like `Prefix<'label', StyleProps>` in the document, it means adding the `label` prefix to the properties of the `StyleProps` type to form a new property name. For example, the `color` property will be used in the form of `labelColor`.\n */\nexport type Prefix<P extends string, T extends object> = {\n  [K in keyof T as K extends string ? PrefixKey<P, K> : never]?: T[K];\n};\n\nexport type ReplacePrefix<T, OldPrefix extends string, NewPrefix extends string> = {\n  [K in keyof T as K extends `${OldPrefix}${infer Rest}` ? `${NewPrefix}${Rest}` : K]: T[K];\n};\n"
  },
  {
    "path": "packages/g6/src/types/router.ts",
    "content": "import type { Padding } from './padding';\nimport type { CardinalPlacement } from './placement';\nimport { Point } from './point';\n\nexport type Direction = CardinalPlacement;\n\nexport type PolylineRouter = false | OrthRouter | ShortestPathRouter;\n\nexport interface OrthRouter extends OrthRouterOptions {\n  /**\n   * <zh/> 正交路由，通过在路径上添加额外的控制点，使得边的每一段都保持水平或垂直\n   *\n   * <en/> Orthogonal routing that adds additional control points on the path to ensure each segment of the edge horizontal or vertical\n   * @remarks\n   * <zh/> 采用基于节点的相对位置和专家经验得出的寻径算法来模糊计算控制点，非最优解但计算速度快。该路由支持 `controlPoints` 来作为额外的控制点，但不支持自动避障。\n   *\n   * <en/> It uses a pathfinding algorithm based on the relative position of the nodes and expert experience to calculate the control points, which is not the optimal solution but is fast to calculate. This routing supports `controlPoints` as additional control points, but does not support automatic obstacle avoidance.\n   */\n  type: 'orth';\n}\n\nexport interface ShortestPathRouter extends ShortestPathRouterOptions {\n  /**\n   * <zh/> 最短路径路由，是正交路由 `'orth'` 的智能版本。该路由由水平或垂直的正交线段组成。采用 A* 算法计算最短路径，并支持自动避开路径上的其他节点（障碍）\n   *\n   * <en/> The shortest path routing is an intelligent version of the orthogonal routing `'orth'`. The routing consists of horizontal or vertical orthogonal line segments. It uses the A* algorithm to calculate the shortest path and supports automatic avoidance of other nodes (obstacles) on the path.\n   */\n  type: 'shortest-path';\n}\n\nexport type RouterOptions = OrthRouterOptions | ShortestPathRouterOptions;\n\nexport interface OrthRouterOptions {\n  /**\n   * <zh/> 指定节点连接点与转角的最小距离\n   *\n   * <en/> The minimum distance between the node connection point and the corner\n   */\n  padding?: Padding;\n}\n\nexport interface ShortestPathRouterOptions {\n  /**\n   * <zh/> 节点锚点与转角的最小距离\n   *\n   * <en/> The minimum distance between the node anchor point and the corner\n   */\n  offset?: Padding;\n  /**\n   * <zh/> grid 格子大小\n   *\n   * <en/> grid size\n   */\n  gridSize?: number;\n  /**\n   * <zh/> 支持的最大旋转角度（弧度）\n   *\n   * <en/> Maximum allowable rotation angle (radian)\n   */\n  maxAllowedDirectionChange?: number;\n  /**\n   * <zh/> 节点的可能起始方向\n   *\n   * <en/> Possible starting directions from a node\n   */\n  startDirections?: Direction[];\n  /**\n   * <zh/> 节点的可能结束方向\n   *\n   * <en/> Possible ending directions from a node\n   */\n  endDirections?: Direction[];\n  /**\n   * <zh/> 指定可移动的方向\n   *\n   * <en/> Allowed edge directions\n   */\n  directionMap?: {\n    [key in Direction]: { stepX: number; stepY: number };\n  };\n  /**\n   * <zh/> 表示在路径搜索过程中某些路径的额外代价。key 为弧度值，value 为代价\n   *\n   * <en/> Penalties for direction changes. Key is the radian value, value is the penalty\n   */\n  penalties?: {\n    [key: string]: number;\n  };\n  /**\n   * <zh/> 指定计算两点之间距离的函数\n   *\n   * <en/> Function to calculate the distance between two points\n   */\n  distFunc?: (p1: Point, p2: Point) => number;\n  /**\n   * <zh/> 最大迭代次数\n   *\n   * <en/> Maximum loops\n   */\n  maximumLoops?: number;\n  /**\n   * <zh/> 是否开启避障\n   *\n   * <en/> Whether to enable obstacle avoidance while computing the path\n   */\n  enableObstacleAvoidance?: boolean;\n}\n"
  },
  {
    "path": "packages/g6/src/types/size.ts",
    "content": "import type { Vector2, Vector3 } from './vector';\n\nexport type Size = number | Vector2 | Vector3;\n\nexport type STDSize = Vector3;\n"
  },
  {
    "path": "packages/g6/src/types/state.ts",
    "content": "export type State = string;\n"
  },
  {
    "path": "packages/g6/src/types/style.ts",
    "content": "import type { Graph } from '../runtime/graph';\nimport type { ElementDatum } from './data';\n\n/**\n * <zh/> 样式计算迭代上下文\n *\n * <en/> Style iteration context\n */\nexport type StyleIterationContext = {\n  datum: ElementDatum;\n  graph: Graph;\n};\n"
  },
  {
    "path": "packages/g6/src/types/tree.ts",
    "content": "export type TreeData = {\n  id: string;\n  children?: TreeData[];\n  depth?: number;\n  [key: string]: any;\n};\n"
  },
  {
    "path": "packages/g6/src/types/utility.ts",
    "content": "export type UnknownStruct = Record<string, unknown>;\n"
  },
  {
    "path": "packages/g6/src/types/vector.ts",
    "content": "export type Vector2 = [number, number] | Float32Array;\n\nexport type Vector3 = [number, number, number] | Float32Array;\n"
  },
  {
    "path": "packages/g6/src/types/viewport.ts",
    "content": "import type { Point } from './point';\n\nexport type ViewportAnimationEffectTiming =\n  | boolean\n  | {\n      easing?: string;\n      duration?: number;\n    };\n\nexport interface TransformOptions {\n  mode: 'relative' | 'absolute';\n  origin?: Point;\n  translate?: Point;\n  rotate?: number;\n  scale?: number;\n}\n\nexport interface FitViewOptions {\n  /**\n   * <zh/> 在以下情况下进行适配\n   * - 'overflow' 仅当图内容超出视口时进行适配\n   * - 'always' 总是进行适配\n   *\n   * <en/> Fit the view in the following cases\n   * - 'overflow' Only fit when the graph content exceeds the viewport\n   * - 'always' Always fit\n   */\n  when?: 'overflow' | 'always';\n  /**\n   * <zh/> 仅对指定方向进行适配\n   * - 'x' 仅适配 x 方向\n   * - 'y' 仅适配 y 方向\n   * - 'both' 适配 x 和 y 方向\n   *\n   * <en/> Only adapt to the specified direction\n   * - 'x' Only adapt to the x direction\n   * - 'y' Only adapt to the y direction\n   * - 'both' Adapt to the x and y directions\n   */\n  direction?: 'x' | 'y' | 'both';\n}\n"
  },
  {
    "path": "packages/g6/src/utils/anchor.ts",
    "content": "import type { Anchor, STDAnchor } from '../types/anchor';\nimport { isBetween } from './math';\n\n/**\n * <zh/> 解析原点（锚点）\n *\n * <en/> Parse the origin/anchor\n * @param anchor - <zh/> 原点 | <en/> Anchor\n * @returns <zh/> 标准原点 | <en/> Standard anchor\n */\nexport function parseAnchor(anchor: Anchor): STDAnchor {\n  const parsedAnchor = (\n    typeof anchor === 'string' ? anchor.split(' ').map((v) => parseFloat(v)) : anchor.slice(0, 2)\n  ) as [number, number];\n  if (!isBetween(parsedAnchor[0], 0, 1) || !isBetween(parsedAnchor[1], 0, 1)) {\n    return [0.5, 0.5];\n  }\n  return parsedAnchor;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/animation.ts",
    "content": "import type { IAnimation } from '@antv/g';\nimport { isEqual, isNil, isObject } from '@antv/util';\nimport type { AnimationEffectTiming, AnimationOptions, STDAnimation } from '../animations/types';\nimport { DEFAULT_ANIMATION_OPTIONS, DEFAULT_ELEMENTS_ANIMATION_OPTIONS, ExtensionCategory } from '../constants';\nimport { getExtension } from '../registry/get';\nimport type { GraphOptions } from '../spec';\nimport type { AnimationStage } from '../spec/element/animation';\nimport type { ElementType, Keyframe } from '../types';\nimport { print } from './print';\nimport { themeOf } from './theme';\n\nexport function createAnimationsProxy(animations: IAnimation[]): IAnimation | null;\nexport function createAnimationsProxy(sourceAnimation: IAnimation, targetAnimations: IAnimation[]): IAnimation;\n/**\n * <zh/> 创建动画代理，对一个动画实例的操作同步到多个动画实例上\n *\n * <en/> create animation proxy, synchronize animation to multiple animation instances\n * @param args1 - <zh/> 源动画实例 | <en/> source animation instance\n * @param args2 - <zh/> 目标动画实例 | <en/> target animation instance\n * @returns <zh/> 动画代理 | <en/> animation proxy\n */\nexport function createAnimationsProxy(args1: IAnimation | IAnimation[], args2?: IAnimation[]): IAnimation | null {\n  if (Array.isArray(args1) && args1.length === 0) return null;\n\n  const sourceAnimation = Array.isArray(args1) ? args1[0] : args1;\n  const targetAnimations = Array.isArray(args1) ? args1.slice(1) : args2 || [];\n\n  return new Proxy(sourceAnimation, {\n    get(target, propKey: keyof IAnimation) {\n      if (typeof target[propKey] === 'function' && !['onframe', 'onfinish'].includes(propKey)) {\n        return (...args: unknown[]) => {\n          (target[propKey] as any)(...args);\n          targetAnimations.forEach((animation) => (animation[propKey] as any)?.(...args));\n        };\n      }\n      if (propKey === 'finished') {\n        return Promise.all([sourceAnimation.finished, ...targetAnimations.map((animation) => animation.finished)]);\n      }\n      return Reflect.get(target, propKey);\n    },\n    set(target, propKey: keyof IAnimation, value) {\n      // onframe 和 onfinish 特殊处理，不用同步到所有动画实例上\n      // onframe and onfinish are specially processed and do not need to be synchronized to all animation instances\n      if (!['onframe', 'onfinish'].includes(propKey)) {\n        targetAnimations.forEach((animation) => {\n          (animation[propKey] as any) = value;\n        });\n      }\n      return Reflect.set(target, propKey, value);\n    },\n  });\n}\n\n/**\n * <zh/> 预处理关键帧，过滤掉无用动画的属性\n *\n * <en/> Preprocess keyframes, filter out the properties of useless animations\n * @param keyframes - <zh/> 关键帧 | <en/> keyframes\n * @returns <zh/> 关键帧 | <en/> keyframes\n */\nexport function preprocessKeyframes(keyframes: Keyframe[]): Keyframe[] {\n  // 转化为 PropertyIndexedKeyframes 格式方便后续处理\n  // convert to PropertyIndexedKeyframes format for subsequent processing\n  const propertyIndexedKeyframes: Record<string, any[]> = keyframes.reduce((acc, kf) => {\n    Object.entries(kf).forEach(([key, value]) => {\n      if (acc[key] === undefined) acc[key] = [value];\n      else acc[key].push(value);\n    });\n    return acc;\n  }, {});\n\n  // 过滤掉无用动画的属性（属性值为 undefined、或者值完全一致）\n  // filter out useless animation properties (property value is undefined, or value is exactly the same)\n  Object.entries(propertyIndexedKeyframes).forEach(([key, values]) => {\n    if (\n      // 属性值必须在每一帧都存在 / property value must exist in every frame\n      values.length !== keyframes.length ||\n      // 属性值不能为空 / property value cannot be empty\n      values.some((value) => isNil(value)) ||\n      // 属性值必须不完全一致 / property value must not be exactly the same\n      // 属性值可以是保留属性 / property value can be the reserved property\n      values.every((value) => !['sourceNode', 'targetNode', 'childrenNode'].includes(key) && isEqual(value, values[0]))\n    ) {\n      delete propertyIndexedKeyframes[key];\n    }\n  });\n\n  // 将 PropertyIndexedKeyframes 转化为 Keyframe 格式\n  // convert PropertyIndexedKeyframes to Keyframe format\n  const output = Object.entries(propertyIndexedKeyframes).reduce((acc, [key, values]) => {\n    values.forEach((value, index) => {\n      if (!acc[index]) acc[index] = { [key]: value };\n      else acc[index][key] = value;\n    });\n    return acc;\n  }, [] as Keyframe[]);\n\n  // 如果处理后所有的属性都被过滤掉，则添加一个没有实际作用的属性用于触发动画\n  // If all properties are filtered out after processing, add a property that has no actual effect to trigger the animation\n  if (keyframes.length !== 0 && output.length === 0) output.push(...[{ _: 0 }, { _: 0 }]);\n\n  return output;\n}\n\n/**\n * <zh/> 获取属性的默认值\n *\n * <en/> Get default value of attribute\n * @param name - <zh/> 属性名 | <en/> Attribute name\n * @returns <zh/> 属性默认值 | <en/> Attribute default value\n * @remarks\n * <zh/> 执行动画过程中，一些属性没有显式指定属性值，但实际上在 G 中存在属性值，因此通过该方法获取其实际默认值\n *\n * <en/> During the animation, some attributes do not explicitly specify the attribute value, but in fact there is an attribute value in G, so use this method to get the actual default value\n */\nexport function inferDefaultValue(name: string) {\n  switch (name) {\n    case 'opacity':\n      return 1;\n    case 'x':\n    case 'y':\n    case 'z':\n    case 'zIndex':\n      return 0;\n    case 'visibility':\n      return 'visible';\n    case 'collapsed':\n      return false;\n    case 'states':\n      return [];\n    default:\n      return undefined;\n  }\n}\n\n/**\n * <zh/> 获取动画配置\n *\n * <en/> Get global animation configuration\n * @param options - <zh/> G6 配置项（用于获取全局动画配置） | <en/> G6 configuration(used to get global animation configuration)\n * @param localAnimation - <zh/> 局部动画配置 | <en/> local animation configuration\n * @returns <zh/> 动画配置 | <en/> animation configuration\n */\nexport function getAnimationOptions(\n  options: GraphOptions,\n  localAnimation: boolean | AnimationEffectTiming | undefined,\n): false | AnimationEffectTiming {\n  const { animation } = options;\n  if (animation === false || localAnimation === false) return false;\n\n  const effectTiming: AnimationEffectTiming = { ...DEFAULT_ANIMATION_OPTIONS };\n  if (isObject(animation)) Object.assign(effectTiming, animation);\n  if (isObject(localAnimation)) Object.assign(effectTiming, localAnimation);\n  return effectTiming;\n}\n\n/**\n * <zh/> 获取动画配置\n *\n * <en/> Get animation configuration\n * @param options - <zh/> 动画配置项 | <en/> animation configuration\n * @returns <zh/> 动画配置 | <en/> animation configuration\n */\nfunction animationOf(options: string | AnimationOptions[]): STDAnimation {\n  if (typeof options === 'string') {\n    const animation = getExtension(ExtensionCategory.ANIMATION, options);\n    if (animation) return animation;\n\n    print.warn(`The animation of ${options} is not registered.`);\n    return [];\n  }\n  return options;\n}\n\n/**\n * <zh/> 获取元素的动画\n *\n * <en/> Get element animation\n * @param options - <zh/> G6 配置项 | <en/> G6 configuration\n * @param elementType - <zh/> 元素类型 | <en/> element type\n * @param stage - <zh/> 动画阶段 | <en/> animation stage\n * @param localAnimation - <zh/> 局部动画配置 | <en/> local animation configuration\n * @returns <zh/> 动画时序配置 | <en/> animation timing configuration\n */\nexport function getElementAnimationOptions(\n  options: GraphOptions,\n  elementType: ElementType,\n  stage: AnimationStage,\n  localAnimation?: AnimationEffectTiming | boolean,\n): STDAnimation {\n  const { animation: globalAnimation } = options;\n  if (globalAnimation === false || localAnimation === false) return [];\n\n  const userElementAnimation = options?.[elementType]?.animation;\n  if (userElementAnimation === false) return [];\n  const useElementStageAnimation = userElementAnimation?.[stage];\n  if (useElementStageAnimation === false) return [];\n\n  // 优先级：用户局部动画配置 > 用户动画配置 > 全局动画配置 > 主题动画配置 > 默认动画配置\n  // Priority: user local animation configuration > user animation configuration > global animation configuration > theme animation configuration > default animation configuration\n\n  const themeElementAnimation = themeOf(options)[elementType]?.animation;\n\n  const combine = (_: string | AnimationOptions[] = []) =>\n    animationOf(_).map((animation) => ({\n      ...DEFAULT_ELEMENTS_ANIMATION_OPTIONS,\n      ...(isObject(globalAnimation) && globalAnimation),\n      ...animation,\n      ...(isObject(localAnimation) && localAnimation),\n    }));\n\n  if (useElementStageAnimation) return combine(useElementStageAnimation);\n\n  if (!themeElementAnimation) return [];\n\n  // 此时取决于主题动画配置\n  // At this time, it depends on the theme animation configuration\n  const themeElementStageAnimation = themeElementAnimation[stage];\n  if (themeElementStageAnimation === false) return [];\n  return combine(themeElementStageAnimation);\n}\n"
  },
  {
    "path": "packages/g6/src/utils/array.ts",
    "content": "/**\n * <zh/> 数组去重\n *\n * <en/> deduplicate array\n * @param arr - <zh/> 数组 | <en/> array\n * @param by - <zh/> 通过某个属性去重 | <en/> deduplicate by some property\n * @returns <zh/> 去重后的数组 | <en/> deduplicated array\n */\nexport function deduplicate<T>(arr: T[], by: (item: T) => unknown = (item) => item) {\n  const set = new Set();\n  return arr.filter((item) => {\n    const key = by ? by(item) : item;\n    return set.has(key) ? false : set.add(key);\n  }) as T[];\n}\n"
  },
  {
    "path": "packages/g6/src/utils/bbox.ts",
    "content": "import { AABB } from '@antv/g';\nimport { clone } from '@antv/util';\nimport type { Node, Padding, Point, TriangleDirection } from '../types';\nimport { isPoint } from './is';\nimport { isBetween } from './math';\nimport { parsePadding } from './padding';\n\n/**\n * <zh/> 获取包围盒的宽度\n *\n * <en/> Retrieves the width of a bounding box\n * @param bbox - <zh/> 包围盒 | <en/> Bounding box\n * @returns <zh/> 包围盒的宽度 | <en/> Width of box\n */\nexport function getBBoxWidth(bbox: AABB): number {\n  return bbox.max[0] - bbox.min[0];\n}\n\n/**\n * <zh/> 获取包围盒的高度\n *\n * <en/> Retrieve the height of a bounding box\n * @param bbox - <zh/> 包围盒 | <en/> Bounding box\n * @returns <zh/> 包围盒的高度 | <en/> Height of box\n */\nexport function getBBoxHeight(bbox: AABB): number {\n  return bbox.max[1] - bbox.min[1];\n}\n\n/**\n * <zh/> 获取包围盒的尺寸\n * @param bbox - <zh/> 包围盒 | <en/> Bounding box\n * @returns <zh/> 包围盒的尺寸 | <en/> Size of box\n */\nexport function getBBoxSize(bbox: AABB): [number, number] {\n  return [getBBoxWidth(bbox), getBBoxHeight(bbox)];\n}\n\n/**\n * <zh/> 获取节点的包围盒，兼容节点为点的情况\n *\n * <en/> Get the bounding box of the node, compatible with the case where the node is a point\n * @param node - <zh/> 节点或者点 | <en/> node or point\n * @param padding - <zh/> 内边距 | <en/> padding\n * @returns <zh/> 包围盒 | <en/> bounding box\n */\nexport function getNodeBBox(node: Point | Node, padding?: Padding): AABB {\n  const bbox = isPoint(node) ? getPointBBox(node) : node.getShape('key').getBounds();\n  return padding ? getExpandedBBox(bbox, padding) : bbox;\n}\n\n/**\n * <zh/> 获取单点的包围盒\n *\n * <en/> Get the bounding box of a single point\n * @param point - <zh/> 点 | <en/> Point\n * @returns <zh/> 包围盒 | <en/> Bounding box\n */\nexport function getPointBBox(point: Point): AABB {\n  const [x, y, z = 0] = point;\n  const bbox = new AABB();\n  bbox.setMinMax([x, y, z], [x, y, z]);\n  return bbox;\n}\n\n/**\n * <zh/> 获取扩大后的包围盒\n *\n * <en/> Get the expanded bounding box\n * @param bbox - <zh/> 包围盒 | <en/> Bounding box\n * @param padding - <zh/> 内边距 | <en/> Padding\n * @returns <zh/> 扩大后的包围盒 | <en/> The expanded bounding box\n */\nexport function getExpandedBBox(bbox: AABB, padding: Padding): AABB {\n  const [top, right, bottom, left] = parsePadding(padding);\n  const [minX, minY, minZ] = bbox.min;\n  const [maxX, maxY, maxZ] = bbox.max;\n  const eBbox = new AABB();\n  eBbox.setMinMax([minX - left, minY - top, minZ], [maxX + right, maxY + bottom, maxZ]);\n  return eBbox;\n}\n\n/**\n * <zh/> 计算整体包围盒\n *\n * <en/> Calculate the overall bounding box\n * @param bboxes - <zh/> 包围盒列表 | <en/> List of bounding boxes\n * @returns <zh/> 整体包围盒 | <en/> Overall bounding box\n */\nexport function getCombinedBBox(bboxes: AABB[]): AABB {\n  if (bboxes.length === 0) return new AABB();\n  if (bboxes.length === 1) return bboxes[0];\n\n  const bbox = new AABB();\n  bbox.setMinMax(bboxes[0].min, bboxes[0].max);\n\n  for (let i = 1; i < bboxes.length; i++) {\n    const b2 = bboxes[i];\n    bbox.setMinMax(\n      [Math.min(bbox.min[0], b2.min[0]), Math.min(bbox.min[1], b2.min[1]), Math.min(bbox.min[2], b2.min[2])],\n      [Math.max(bbox.max[0], b2.max[0]), Math.max(bbox.max[1], b2.max[1]), Math.max(bbox.max[2], b2.max[2])],\n    );\n  }\n\n  return bbox;\n}\n\n/**\n * <zh/> 判断 bbox1 是否完全包含在 bbox2 内\n *\n * <en/> Determine whether bbox1 is completely contained in bbox2\n * @param bbox1 - <zh/> 目标包围盒 | <en/> Target bounding box\n * @param bbox2 - <zh/> 参考包围盒 | <en/> Reference bounding box\n * @returns <zh/> 如果 bbox1 完全包含在 bbox2 内返回 true，否则返回 false | <en/> Returns true if bbox1 is completely contained in bbox2, false otherwise\n */\nexport function isBBoxInside(bbox1: AABB, bbox2: AABB): boolean {\n  const [minX1, minY1] = bbox1.min;\n  const [maxX1, maxY1] = bbox1.max;\n  const [minX2, minY2] = bbox2.min;\n  const [maxX2, maxY2] = bbox2.max;\n\n  return minX1 >= minX2 && maxX1 <= maxX2 && minY1 >= minY2 && maxY1 <= maxY2;\n}\n\n/**\n * <zh/> 判断点是否在给定的包围盒内\n *\n * <en/> Whether the point is contained in the given box\n * @param point - <zh/> 点 | <en/> Point\n * @param bbox - <zh/> 包围盒 | <en/> Bounding box\n * @returns  <zh/> 如果点在包围盒内返回 true，否则返回 false | <en/> Returns true if the point is inside the bounding box, false otherwise\n */\nexport function isPointInBBox(point: Point, bbox: AABB) {\n  return isBetween(point[0], bbox.min[0], bbox.max[0]) && isBetween(point[1], bbox.min[1], bbox.max[1]);\n}\n\n/**\n * <zh/> 判断点是否在给定的包围盒的边界或边界的延长线上\n *\n * <en/> Whether the point is on the boundary or extension line of the given box\n * @param point - <zh/> 点 | <en/> Point\n * @param bbox - <zh/> 包围盒 | <en/> Bounding box\n * @param extended - <zh/> 是否判断边界的延长线 | <en/> Whether to judge the extension line of the boundary\n * @returns <zh/> 如果点在包围盒的边界或边界的延长线上返回 true，否则返回 false | <en/> Returns true if the point is on the boundary or extension line of the bounding box, false otherwise\n */\nexport function isPointOnBBoxBoundary(point: Point, bbox: AABB, extended = false): boolean {\n  const {\n    min: [minX, minY],\n    max: [maxX, maxY],\n  } = bbox;\n\n  const onTopOrBottomLine = (point[1] === minY || point[1] === maxY) && (extended || isBetween(point[0], minX, maxX));\n  const onLeftOrRightLine = (point[0] === minX || point[0] === maxX) && (extended || isBetween(point[1], minY, maxY));\n\n  return onTopOrBottomLine || onLeftOrRightLine;\n}\n\n/**\n * <zh/> 判断点是否在给定的包围盒外\n *\n * <en/> Whether the point is outside the given box\n * @param point - <zh/> 点 | <en/> Point\n * @param bbox - <zh/> 包围盒 | <en/> Bounding box\n * @returns <zh/> 如果点在包围盒外返回 true，否则返回 false | <en/> Returns true if the point is outside the bounding box, false otherwise\n */\nexport function isPointOutsideBBox(point: Point, bbox: AABB) {\n  return !isPointInBBox(point, bbox);\n}\n\n/**\n * <zh/> 判断点是否位于包围盒中心\n *\n * <en/> When the point is at the center of the bounding box\n * @param point - <zh/> 点 | <en/> Point\n * @param bbox - <zh/> 包围盒 | <en/> Bounding box\n * @returns <zh/> 如果点在包围盒中心返回 true，否则返回 false | <en/> Returns true if the point is at the center of the bounding box, false otherwise\n */\nexport function isPointBBoxCenter(point: Point, bbox: AABB) {\n  const { center } = bbox;\n  return point[0] === center[0] && point[1] === center[1];\n}\n\n/**\n * <zh/> 获取包围盒上离点 `p` 最近的边\n *\n * <en/> Get a side of the boundary which is nearest to the point `p`\n * @param bbox - <zh/> 包围盒 | <en/> Bounding box\n * @param p - <zh/> 点 | <en/> Point\n * @returns <zh/> 离点 `p` 最近的边 | <en/> The side nearest to the point `p`\n */\nexport function getNearestBoundarySide(p: Point, bbox: AABB): 'left' | 'right' | 'top' | 'bottom' {\n  const [x, y] = p;\n  const [minX, minY] = bbox.min;\n  const [maxX, maxY] = bbox.max;\n  const left = x - minX;\n  const right = maxX - x;\n  const top = y - minY;\n  const bottom = maxY - y;\n  const min = Math.min(left, right, top, bottom);\n  return min === left ? 'left' : min === right ? 'right' : min === top ? 'top' : min === bottom ? 'bottom' : 'left';\n}\n\n/**\n * <zh/> 获取包围盒上离点 `p` 最近的边界点\n *\n * <en/> Get a point on the boundary nearest to the point `p`\n * @param bbox - <zh/> 包围盒 | <en/> Bounding box\n * @param p - <zh/> 点 | <en/> Point\n * @returns <zh/> 离点 `p` 最近的点 | <en/> The point nearest to the point `p`\n */\nexport function getNearestBoundaryPoint(p: Point, bbox: AABB): Point {\n  const ref = clone(p);\n  if (isPointInBBox(p, bbox)) {\n    const side = getNearestBoundarySide(p, bbox);\n    switch (side) {\n      case 'left':\n        ref[0] = bbox.min[0];\n        break;\n      case 'right':\n        ref[0] = bbox.max[0];\n        break;\n      case 'top':\n        ref[1] = bbox.min[1];\n        break;\n      case 'bottom':\n        ref[1] = bbox.max[1];\n        break;\n    }\n  } else {\n    const [x, y] = p;\n    const [minX, minY] = bbox.min;\n    const [maxX, maxY] = bbox.max;\n    ref[0] = isBetween(x, minX, maxX) ? x : x < minX ? minX : maxX;\n    ref[1] = isBetween(y, minY, maxY) ? y : y < minY ? minY : maxY;\n  }\n  return ref;\n}\n\n/**\n * The triangle center point of the bounding box\n * @param bbox - bounding box\n * @param direction - direction\n * @returns Point\n */\nexport function getTriangleCenter(bbox: AABB, direction: TriangleDirection): Point {\n  // todo 算法只对矩形有效\n  const { center } = bbox;\n  const [width, height] = getBBoxSize(bbox);\n\n  const x =\n    direction === 'up' || direction === 'down'\n      ? center[0]\n      : direction === 'right'\n        ? center[0] - width / 6\n        : center[0] + width / 6;\n  const y =\n    direction === 'left' || direction === 'right'\n      ? center[1]\n      : direction === 'down'\n        ? center[1] - height / 6\n        : center[1] + height / 6;\n\n  return [x, y];\n}\n\n/**\n * Get incircle radius\n * @param bbox - bounding box\n * @param direction - direction\n * @returns number\n */\nexport function getIncircleRadius(bbox: AABB, direction: TriangleDirection): number {\n  let [w, h] = getBBoxSize(bbox);\n\n  [w, h] = direction === 'up' || direction === 'down' ? [w, h] : [h, w];\n\n  // 三角形的内切圆半径\n  return (h ** 2 - (Math.sqrt((w / 2) ** 2 + h ** 2) - w / 2) ** 2) / (2 * h);\n}\n\n/**\n * <zh/> 获取包围盒的四条边，顺序依次为上、右、下、左\n *\n * <en/> Get the four segments of the bounding box, in order from top, right, bottom, left\n * @param bbox - <zh/> 包围盒 | <en/> Bounding box\n * @returns <zh/> 包围盒的四条边 | <en/> The four segments of the bounding box\n */\nexport function getBBoxSegments(bbox: AABB): [Point, Point][] {\n  const {\n    min: [minX, minY],\n    max: [maxX, maxY],\n  } = bbox;\n  const topLeftCorner: Point = [minX, maxY];\n  const topRightCorner: Point = [maxX, maxY];\n  const bottomRightCorner: Point = [maxX, minY];\n  const bottomLeftCorner: Point = [minX, minY];\n\n  const top = [topLeftCorner, topRightCorner];\n  const right = [topRightCorner, bottomRightCorner];\n  const bottom = [bottomRightCorner, bottomLeftCorner];\n  const left = [bottomLeftCorner, topLeftCorner];\n  return [top, right, bottom, left] as [Point, Point][];\n}\n"
  },
  {
    "path": "packages/g6/src/utils/cache.ts",
    "content": "import type { DisplayObject } from '@antv/g';\nimport { get, set } from '@antv/util';\n\nconst CacheTargetKey = 'cachedStyle';\n\nconst getStyleCacheKey = (name: string) => `__${name}__`;\n\n/**\n * <zh/> 缓存图形样式\n *\n * <en/> Cache shape style\n * @param element - <zh/> 图形元素 | <en/> shape element\n * @param name - <zh/> 样式名 | <en/> style name\n */\nexport function cacheStyle(element: DisplayObject, name: string | string[]) {\n  const names = Array.isArray(name) ? name : [name];\n  if (!get(element, CacheTargetKey)) set(element, CacheTargetKey, {});\n  names.forEach((n) => {\n    set(get(element, CacheTargetKey), getStyleCacheKey(n), element.attributes[n]);\n  });\n}\n\n/**\n * <zh/> 获取缓存的样式\n *\n * <en/> Get cached style\n * @param element - <zh/> 图形元素 | <en/> shape element\n * @param name - <zh/> 样式名 | <en/> style name\n * @returns <zh/> 样式值 | <en/> style value\n */\nexport function getCachedStyle(element: DisplayObject, name: string) {\n  return get(element, [CacheTargetKey, getStyleCacheKey(name)]);\n}\n\n/**\n * <zh/> 是否有缓存的样式\n *\n * <en/> Whether there is a cached style\n * @param element - <zh/> 图形元素 | <en/> shape element\n * @param name - <zh/> 样式名 | <en/> style name\n * @returns <zh/> 是否有缓存的样式 | <en/> Whether there is a cached style\n */\nexport function hasCachedStyle(element: DisplayObject, name: string) {\n  return getStyleCacheKey(name) in (get(element, CacheTargetKey) || {});\n}\n\n/**\n * <zh/> 设置缓存的样式\n *\n * <en/> Set cached style\n * @param element - <zh/> 图形元素 | <en/> shape element\n * @param name - <zh/> 样式名 | <en/> style name\n * @param value - <zh/> 样式值 | <en/> style value\n */\nexport function setCacheStyle(element: DisplayObject, name: string, value: any) {\n  set(element, [CacheTargetKey, getStyleCacheKey(name)], value);\n}\n"
  },
  {
    "path": "packages/g6/src/utils/centrality.ts",
    "content": "import { findShortestPath, pageRank } from '@antv/algorithm';\nimport type { EdgeData, GraphData } from '../spec';\nimport type { EdgeDirection, ID, NodeCentralityOptions } from '../types';\nimport { idOf } from './id';\n\nexport type CentralityResult = Map<ID, number>;\n\nexport const getNodeCentralities = (\n  graphData: GraphData,\n  getRelatedEdgesData: (id: ID, direction?: EdgeDirection) => EdgeData[],\n  centrality: NodeCentralityOptions,\n) => {\n  switch (centrality.type) {\n    case 'degree': {\n      const centralityResult = new Map<ID, number>();\n      graphData.nodes?.forEach((node) => {\n        const degree = getRelatedEdgesData(idOf(node), centrality.direction).length;\n        centralityResult.set(idOf(node), degree);\n      });\n      return centralityResult;\n    }\n    case 'betweenness':\n      return computeNodeBetweennessCentrality(graphData, centrality.directed, centrality.weightPropertyName);\n    case 'closeness':\n      return computeNodeClosenessCentrality(graphData, centrality.directed, centrality.weightPropertyName);\n    case 'eigenvector':\n      return computeNodeEigenvectorCentrality(graphData, centrality.directed);\n    case 'pagerank':\n      return computeNodePageRankCentrality(graphData, centrality.epsilon, centrality.linkProb);\n    default:\n      return initCentralityResult(graphData);\n  }\n};\n\nexport const initCentralityResult = (graphData: GraphData): CentralityResult => {\n  const centralityResult = new Map<ID, number>();\n  graphData.nodes?.forEach((node) => {\n    centralityResult.set(idOf(node), 0);\n  });\n  return centralityResult;\n};\n\n/**\n * <zh/> 计算图中每个节点的中介中心性\n *\n * <en/> Calculate the betweenness centrality for each node in the graph\n * @param graphData - <zh/> 图数据 | <en/>Graph data\n * @param directed - <zh/> 是否为有向图 | <en/>Whether the graph is directed\n * @param weightPropertyName - <zh/> 边的权重属性名 | <en/>The weight property name of the edge\n * @returns <zh/> 每个节点的中介中心性值 | <en/>The betweenness centrality for each node\n */\nexport const computeNodeBetweennessCentrality = (\n  graphData: GraphData,\n  directed?: boolean,\n  weightPropertyName?: string,\n): CentralityResult => {\n  const centralityResult = initCentralityResult(graphData);\n  const { nodes = [] } = graphData;\n  nodes.forEach((source) => {\n    nodes.forEach((target) => {\n      if (source !== target) {\n        const { allPath } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName);\n        const pathCount = allPath.length;\n        (allPath as ID[][]).flat().forEach((nodeId) => {\n          if (nodeId !== idOf(source) && nodeId !== idOf(target)) {\n            centralityResult.set(nodeId, centralityResult.get(nodeId)! + 1 / pathCount);\n          }\n        });\n      }\n    });\n  });\n  return centralityResult;\n};\n\n/**\n * <zh/> 计算图中每个节点的接近中心性\n *\n * <en/> Calculate the closeness centrality for each node in the graph\n * @param graphData - <zh/> 图数据 | <en/>Graph data\n * @param directed - <zh/> 是否为有向图 | <en/>Whether the graph is directed\n * @param weightPropertyName - <zh/> 边的权重属性名 | <en/>The weight property name of the edge\n * @returns <zh/> 每个节点的接近中心性值 | <en/>The closeness centrality for each node\n */\nexport const computeNodeClosenessCentrality = (\n  graphData: GraphData,\n  directed?: boolean,\n  weightPropertyName?: string,\n): CentralityResult => {\n  const centralityResult = new Map<ID, number>();\n  const { nodes = [] } = graphData;\n  nodes.forEach((source) => {\n    const totalLength = nodes.reduce((acc, target) => {\n      if (source !== target) {\n        const { length } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName);\n        acc += length;\n      }\n      return acc;\n    }, 0);\n    centralityResult.set(idOf(source), 1 / totalLength);\n  });\n  return centralityResult;\n};\n\n/**\n * <zh/> 计算图中每个节点的 PageRank 中心性\n *\n * <en/> Calculate the PageRank centrality for each node in the graph\n * @param graphData - <zh/> 图数据 | <en/>Graph data\n * @param epsilon - <zh/> PageRank 算法的收敛容差 | <en/>The convergence tolerance of the PageRank algorithm\n * @param linkProb - <zh/> PageRank 算法的阻尼系数，指任意时刻，用户访问到某节点后继续访问该节点链接的下一个节点的概率，经验值 0.85 | <en/>The damping factor of the PageRank algorithm, which refers to the probability that a user will continue to visit the next node linked to a node at any time, with an empirical value of 0.85\n * @returns <zh/> 每个节点的 PageRank 中心性值 | <en/>The PageRank centrality for each node\n */\nexport const computeNodePageRankCentrality = (\n  graphData: GraphData,\n  epsilon?: number,\n  linkProb?: number,\n): CentralityResult => {\n  const centralityResult = new Map<ID, number>();\n  const data = pageRank(graphData, epsilon, linkProb);\n  graphData.nodes?.forEach((node) => {\n    centralityResult.set(idOf(node), data[idOf(node)]);\n  });\n  return centralityResult;\n};\n\n/**\n * <zh/> 计算图中每个节点的特征向量中心性\n *\n * <en/> Calculate the eigenvector centrality for each node in the graph.\n * @param graphData - <zh/> 图数据 | <en/>Graph data\n * @param directed - <zh/> 是否为有向图 | <en/>Whether the graph is directed\n * @returns 每个节点的特征向量中心性值 The eigenvector centrality for each node.\n */\nexport const computeNodeEigenvectorCentrality = (graphData: GraphData, directed?: boolean): CentralityResult => {\n  const { nodes = [] } = graphData;\n  const adjacencyMatrix = createAdjacencyMatrix(graphData, directed);\n  const eigenvector = powerIteration(adjacencyMatrix, nodes.length);\n\n  const centralityResult = new Map<ID, number>();\n  nodes.forEach((node, index) => {\n    centralityResult.set(idOf(node), eigenvector[index]);\n  });\n\n  return centralityResult;\n};\n\n/**\n * <zh/> 创建图的邻接矩阵\n *\n * <en/> Create the adjacency matrix for the graph.\n * @param graphData - <zh/> 图数据 | <en/>Graph data\n * @param directed - <zh/> 是否为有向图 | <en/>Whether the graph is directed\n * @returns <zh/> 邻接矩阵 | <en/>The adjacency matrix\n */\nexport const createAdjacencyMatrix = (graphData: GraphData, directed?: boolean): number[][] => {\n  const { nodes = [], edges = [] } = graphData;\n  const matrix: number[][] = Array(nodes.length)\n    .fill(null)\n    .map(() => Array(nodes.length).fill(0));\n\n  edges.forEach(({ source, target }) => {\n    const uIndex = nodes.findIndex((node) => idOf(node) === source);\n    const vIndex = nodes.findIndex((node) => idOf(node) === target);\n    if (directed) {\n      matrix[uIndex][vIndex] = 1;\n    } else {\n      matrix[uIndex][vIndex] = 1;\n      matrix[vIndex][uIndex] = 1;\n    }\n  });\n\n  return matrix;\n};\n\n/**\n * <zh/> 使用幂迭代法计算主特征向量\n *\n * <en/> Calculate the principal eigenvector using the power iteration method\n * @see https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors\n * @param matrix - <zh/> 邻接矩阵 | <en/> The adjacency matrix\n * @param numNodes - <zh/> 节点数量 | <en/> The number of nodes\n * @param maxIterations - <zh/> 最大迭代次数 | <en/> The maximum number of iterations\n * @param tolerance - <zh/> 收敛容差 | <en/> The convergence tolerance\n * @returns <zh/> 主特征向量 | <en/> The principal eigenvector\n */\nconst powerIteration = (matrix: number[][], numNodes: number, maxIterations = 100, tolerance = 1e-6): number[] => {\n  let eigenvector = Array(numNodes).fill(1);\n  let diff = Infinity;\n\n  for (let iter = 0; iter < maxIterations && diff > tolerance; iter++) {\n    const newEigenvector = Array(numNodes).fill(0);\n\n    for (let i = 0; i < numNodes; i++) {\n      for (let j = 0; j < numNodes; j++) {\n        newEigenvector[i] += matrix[i][j] * eigenvector[j];\n      }\n    }\n\n    const norm = Math.sqrt(newEigenvector.reduce((sum, val) => sum + val * val, 0));\n    for (let i = 0; i < numNodes; i++) {\n      newEigenvector[i] /= norm;\n    }\n\n    diff = Math.sqrt(newEigenvector.reduce((sum, val, index) => sum + (val - eigenvector[index]) * val, 0));\n    eigenvector = newEigenvector;\n  }\n\n  return eigenvector;\n};\n"
  },
  {
    "path": "packages/g6/src/utils/change.ts",
    "content": "import { groupBy } from '@antv/util';\nimport { ChangeType } from '../constants';\nimport type { DataAdded, DataChange, DataChanges, DataRemoved, DataUpdated, ID } from '../types';\nimport { idOf } from './id';\n\n/**\n * <zh/> 对数据操作进行约简\n *\n * <en/> Reduce data changes\n * @param changes - <zh/> 数据操作 | <en/> data changes\n * @returns <zh/> 约简后的数据操作 | <en/> reduced data changes\n */\nexport function reduceDataChanges(changes: DataChange[]): DataChange[] {\n  const results = {\n    Added: new Map<ID, DataAdded>(),\n    Updated: new Map<ID, DataUpdated>(),\n    Removed: new Map<ID, DataRemoved>(),\n  };\n\n  changes.forEach((change) => {\n    const { type, value } = change;\n    const id = idOf(value);\n\n    if (type === 'NodeAdded' || type === 'EdgeAdded' || type === 'ComboAdded') {\n      results.Added.set(id, change);\n    } else if (type === 'NodeUpdated' || type === 'EdgeUpdated' || type === 'ComboUpdated') {\n      // 如果存在 Added，将当前操作置为 Added 操作\n      // If there is an Added operation, set the current operation to Added\n      if (results.Added.has(id)) {\n        results.Added.set(id, { type: type.replace('Updated', 'Added'), value } as DataAdded);\n      }\n      // 如果存在 Updated，将当前操作置为 Updated 操作，但使用更早版本的 original\n      // If there is an Updated operation, set the current operation to Updated, but use an earlier version of original\n      else if (results.Updated.has(id)) {\n        const { original } = results.Updated.get(id)!;\n        results.Updated.set(id, { type, value, original } as DataUpdated);\n      } else if (results.Removed.has(id)) {\n        // 如果存在 Removed，不做任何操作\n        // If there is a Removed operation, do nothing\n      } else results.Updated.set(id, change);\n    } else if (type === 'NodeRemoved' || type === 'EdgeRemoved' || type === 'ComboRemoved') {\n      // 如果存在 Added 或者 Updated 的操作，删除 Removed 操作\n      // If there is an Added or Updated operation, delete the Removed operation\n      if (results.Added.has(id)) {\n        results.Added.delete(id);\n      } else if (results.Updated.has(id)) {\n        results.Updated.delete(id);\n        results.Removed.set(id, change);\n      } else {\n        results.Removed.set(id, change);\n      }\n    }\n  });\n\n  // 顺序并不重要\n  // The order is not important\n  return [\n    ...Array.from(results.Added.values()),\n    ...Array.from(results.Updated.values()),\n    ...Array.from(results.Removed.values()),\n  ];\n}\n\n/**\n * <zh/> 对数据操作进行分类\n *\n * <en/> Classify data changes\n * @param changes - <zh/> 数据操作 | <en/> data changes\n * @returns <zh/> 分类后的数据操作 | <en/> classified data changes\n */\nexport function groupByChangeType(changes: DataChange[]): DataChanges {\n  const {\n    NodeAdded = [],\n    NodeUpdated = [],\n    NodeRemoved = [],\n    EdgeAdded = [],\n    EdgeUpdated = [],\n    EdgeRemoved = [],\n    ComboAdded = [],\n    ComboUpdated = [],\n    ComboRemoved = [],\n  } = groupBy(changes, (change) => change.type) as unknown as Record<`${ChangeType}`, DataChange[]>;\n\n  return {\n    add: {\n      nodes: NodeAdded,\n      edges: EdgeAdded,\n      combos: ComboAdded,\n    },\n    update: {\n      nodes: NodeUpdated,\n      edges: EdgeUpdated,\n      combos: ComboUpdated,\n    },\n    remove: {\n      nodes: NodeRemoved,\n      edges: EdgeRemoved,\n      combos: ComboRemoved,\n    },\n  } as DataChanges;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/collapsibility.ts",
    "content": "import type { NodeLikeData } from '../types';\n\n/**\n * <zh/> 判断节点/组合是否收起\n *\n * <en/> Determine whether the node/combo is collapsed\n * @param nodeLike - <zh/>节点/组合数据 | <en/>Node/Combo data\n * @returns <zh/>是否收起 | <en/>Whether it is collapsed\n */\nexport function isCollapsed(nodeLike: NodeLikeData) {\n  return !!nodeLike.style?.collapsed;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/data.ts",
    "content": "import { get } from '@antv/util';\nimport type { ComboData, EdgeData, GraphData, NodeData } from '../spec';\nimport type { ElementDatum, ID } from '../types';\n\n/**\n * <zh/> 合并两个 节点/边/Combo 的数据\n *\n * <en/> Merge the data of two nodes/edges/combos\n * @param original - <zh/> 原始数据 | <en/> original data\n * @param modified - <zh/> 待合并的数据 | <en/> data to be merged\n * @returns <zh/> 合并后的数据 | <en/> merged data\n * @remarks\n * <zh/> 只会合并第一层的数据，data、style 下的二级数据会被覆盖\n *\n * <en/> Only the first level of data will be merged, the second level of data under data and style will be overwritten\n */\nexport function mergeElementsData<T extends NodeData | EdgeData | ComboData>(original: T, modified: Partial<T>): T {\n  const { data: originalData, style: originalStyle, ...originalAttrs } = original;\n  const { data: modifiedData, style: modifiedStyle, ...modifiedAttrs } = modified;\n\n  const result = {\n    ...originalAttrs,\n    ...modifiedAttrs,\n  };\n\n  if (originalData || modifiedData) {\n    Object.assign(result, { data: { ...originalData, ...modifiedData } });\n  }\n\n  if (originalStyle || modifiedStyle) {\n    Object.assign(result, { style: { ...originalStyle, ...modifiedStyle } });\n  }\n\n  return result as T;\n}\n\n/**\n * <zh/> 克隆元素数据\n *\n * <en/> Clone clement data\n * @param data - <zh/> 待克隆的数据 | <en/> data to be cloned\n * @returns <zh/> 克隆后的数据 | <en/> cloned data\n * @remarks\n * <zh/> 只会克隆到第二层（data、style）\n *\n * <en/> Only clone to the second level (data, style)\n */\nexport function cloneElementData<T extends NodeData | EdgeData | ComboData>(data: T): T {\n  const { data: customData, style, ...restAttrs } = data;\n  const clonedData = restAttrs as T;\n  if (customData) clonedData.data = { ...customData };\n  if (style) clonedData.style = { ...style };\n  return clonedData;\n}\n\n/**\n * <zh/> 判断数据是否为空\n *\n * <en/> Determine if the data is empty\n * @param data - <zh/> 图数据 | <en/> graph data\n * @returns <zh/> 是否为空 | <en/> is empty\n */\nexport function isEmptyData(data: GraphData) {\n  return !get(data, ['nodes', 'length']) && !get(data, ['edges', 'length']) && !get(data, ['combos', 'length']);\n}\n\n/**\n * <zh/> 判断两个元素数据是否相等\n *\n * <en/> Determine if two element data are equal\n * @param original - <zh/> 原始数据 | <en/> original data\n * @param modified - <zh/> 修改后的数据 | <en/> modified data\n * @returns <zh/> 是否相等 | <en/> is equal\n * @remarks\n * <zh/> 相比于 isEqual，这个方法不会比较更下层的数据\n *\n * <en/> Compared to isEqual, this method does not compare data at a lower level\n */\nexport function isElementDataEqual(original: Partial<ElementDatum> = {}, modified: Partial<ElementDatum> = {}) {\n  const {\n    states: originalStates = [],\n    data: originalData = {},\n    style: originalStyle = {},\n    children: originalChildren = [],\n    ...originalAttrs\n  } = original;\n  const {\n    states: modifiedStates = [],\n    data: modifiedData = {},\n    style: modifiedStyle = {},\n    children: modifiedChildren = [],\n    ...modifiedAttrs\n  } = modified;\n\n  const isArrayEqual = (arr1: unknown[], arr2: unknown[]) => {\n    if (arr1.length !== arr2.length) return false;\n    return arr1.every((item, index) => item === arr2[index]);\n  };\n  const isObjectEqual = (obj1: Record<string, unknown>, obj2: Record<string, unknown>) => {\n    const keys1 = Object.keys(obj1);\n    const keys2 = Object.keys(obj2);\n    if (keys1.length !== keys2.length) return false;\n    return keys1.every((key) => obj1[key] === obj2[key]);\n  };\n\n  if (!isObjectEqual(originalAttrs, modifiedAttrs)) return false;\n  if (!isArrayEqual(originalChildren as ID[], modifiedChildren as ID[])) return false;\n  if (!isArrayEqual(originalStates, modifiedStates)) return false;\n  if (!isObjectEqual(originalData, modifiedData)) return false;\n  if (!isObjectEqual(originalStyle, modifiedStyle)) return false;\n\n  return true;\n}\n\nexport const OVERRIDE_KEY = '__internal_override__';\n/**\n * <zh/> 判断元素数据是否允许被覆盖\n *\n * <en/> Determine whether the element data can be overridden\n * @param datum - <zh/> 元素数据 | <en/> element data\n * @returns <zh/> 是否允许被覆盖 | <en/> is overridable\n */\nexport function isOverridable(datum: ElementDatum): boolean {\n  return datum[OVERRIDE_KEY] !== false;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/diff.ts",
    "content": "import { isEqual } from '@antv/util';\n\n/**\n * <zh/> 比较两个数组的差异\n *\n * <en/> compare the difference between two arrays\n * @param original - <zh/> 原始数组 | <en/> original array\n * @param modified - <zh/> 修改后的数组 | <en/> modified array\n * @param key - <zh/> 比较的 key | <en/> key to compare\n * @param comparator - <zh/> 比较函数 | <en/> compare function\n * @returns <zh/> 数组差异 | <en/> array diff\n */\nexport function arrayDiff<T>(\n  original: T[],\n  modified: T[],\n  key: (d: T) => string | number,\n  comparator: (a?: T, b?: T) => boolean = isEqual,\n) {\n  const originalMap = new Map(original.map((d) => [key(d), d]));\n  const modifiedMap = new Map(modified.map((d) => [key(d), d]));\n\n  const originalSet = new Set(originalMap.keys());\n  const modifiedSet = new Set(modifiedMap.keys());\n\n  const enter: T[] = [];\n  const update: T[] = [];\n  const exit: T[] = [];\n  const keep: T[] = [];\n\n  modifiedSet.forEach((key) => {\n    if (originalSet.has(key)) {\n      if (!comparator(originalMap.get(key), modifiedMap.get(key))) {\n        update.push(modifiedMap.get(key)!);\n      } else {\n        keep.push(modifiedMap.get(key)!);\n      }\n    } else {\n      enter.push(modifiedMap.get(key)!);\n    }\n  });\n\n  originalSet.forEach((key) => {\n    if (!modifiedSet.has(key)) {\n      exit.push(originalMap.get(key)!);\n    }\n  });\n\n  return { enter, exit, keep, update };\n}\n"
  },
  {
    "path": "packages/g6/src/utils/dom.ts",
    "content": "import { isNumber } from '@antv/util';\n\nconst parseInt10 = (d: string) => (d ? parseInt(d) : 0);\n\n/**\n * Get the container's bounding size.\n * @param container dom element.\n * @returns the container width and height\n */\nfunction getContainerSize(container: HTMLElement): [number, number] {\n  const style = getComputedStyle(container);\n\n  const wrapperWidth = container.clientWidth || parseInt10(style.width);\n  const wrapperHeight = container.clientHeight || parseInt10(style.height);\n\n  const widthPadding = parseInt10(style.paddingLeft) + parseInt10(style.paddingRight);\n  const heightPadding = parseInt10(style.paddingTop) + parseInt10(style.paddingBottom);\n\n  return [wrapperWidth - widthPadding, wrapperHeight - heightPadding];\n}\n\n/**\n * Get the size of Graph.\n * @param container container of Graph\n * @returns Size of Graph\n */\nexport function sizeOf(container: HTMLElement | null): [number, number] {\n  if (!container) return [0, 0];\n\n  let effectiveWidth = 640;\n  let effectiveHeight = 480;\n\n  const [containerWidth, containerHeight] = getContainerSize(container);\n  effectiveWidth = containerWidth || effectiveWidth;\n  effectiveHeight = containerHeight || effectiveHeight;\n\n  /** Minimum width */\n  const MIN_CHART_WIDTH = 1;\n  /** Minimum height */\n  const MIN_CHART_HEIGHT = 1;\n\n  return [\n    Math.max(isNumber(effectiveWidth) ? effectiveWidth : MIN_CHART_WIDTH, MIN_CHART_WIDTH),\n    Math.max(isNumber(effectiveHeight) ? effectiveHeight : MIN_CHART_HEIGHT, MIN_CHART_HEIGHT),\n  ];\n}\n"
  },
  {
    "path": "packages/g6/src/utils/edge.ts",
    "content": "import type { AABB, DisplayObject, TransformArray } from '@antv/g';\nimport type { PathArray } from '@antv/util';\nimport { isEqual, isNumber } from '@antv/util';\nimport type { EdgeData } from '../spec';\nimport type {\n  EdgeBadgeStyleProps,\n  EdgeKey,\n  EdgeLabelStyleProps,\n  ID,\n  LoopPlacement,\n  Node,\n  NodeLikeData,\n  Point,\n  Port,\n  Size,\n  Vector2,\n} from '../types';\nimport { getBBoxHeight, getBBoxSize, getBBoxWidth, getNearestBoundarySide, getNodeBBox } from './bbox';\nimport { isCollapsed } from './collapsibility';\nimport { getAllPorts, getNodeConnectionPoint, getPortConnectionPoint, getPortPosition } from './element';\nimport { idOf } from './id';\nimport { isCollinear, isHorizontal, moveTo, parsePoint } from './point';\nimport { freeJoin } from './router/orth';\nimport { add, distance, manhattanDistance, multiply, normalize, perpendicular, subtract } from './vector';\n\n/**\n * <zh/> 获取标签的位置样式\n *\n * <en/> Get the style of the label's position\n * @param key - <zh/> 边对象 | <en/> The edge object\n * @param placement - <zh/> 标签位置 | <en/> Position of the label\n * @param autoRotate - <zh/> 是否自动旋转 | <en/> Whether to auto-rotate\n * @param offsetX - <zh/> 标签相对于边的水平偏移量 | <en/> Horizontal offset of the label relative to the edge\n * @param offsetY - <zh/> 标签相对于边的垂直偏移量 | <en/> Vertical offset of the label relative to the edge\n * @returns <zh/> 标签的位置样式 | <en/> Returns the style of the label's position\n */\nexport function getLabelPositionStyle(\n  key: EdgeKey,\n  placement: EdgeLabelStyleProps['placement'],\n  autoRotate: boolean,\n  offsetX: number,\n  offsetY: number,\n): Partial<EdgeLabelStyleProps> {\n  const START_RATIO = 0;\n  const MIDDLE_RATIO = 0.5;\n  const END_RATIO = 0.99;\n\n  let ratio = typeof placement === 'number' ? placement : MIDDLE_RATIO;\n  if (placement === 'start') ratio = START_RATIO;\n  if (placement === 'end') ratio = END_RATIO;\n\n  const point = parsePoint(key.getPoint(ratio));\n  const pointOffset = parsePoint(key.getPoint(ratio + 0.01));\n\n  let textAlign: 'left' | 'right' | 'center' =\n    placement === 'start' ? 'left' : placement === 'end' ? 'right' : 'center';\n\n  if (isHorizontal(point, pointOffset) || !autoRotate) {\n    const [x, y] = getXYByPlacement(key, ratio, offsetX, offsetY);\n    return { transform: [['translate', x, y]], textAlign };\n  }\n\n  let angle = Math.atan2(pointOffset[1] - point[1], pointOffset[0] - point[0]);\n\n  const isRevert = pointOffset[0] < point[0];\n  if (isRevert) {\n    textAlign = textAlign === 'center' ? textAlign : textAlign === 'left' ? 'right' : 'left';\n    offsetX! *= -1;\n    angle += Math.PI;\n  }\n\n  const [x, y] = getXYByPlacement(key, ratio, offsetX, offsetY, angle);\n  const transform: TransformArray = [\n    ['translate', x, y],\n    ['rotate', (angle / Math.PI) * 180],\n  ];\n\n  return {\n    textAlign,\n    transform,\n  };\n}\n\n/**\n * <zh/> 获取边上徽标的位置样式\n *\n * <en/> Get the position style of the badge on the edge\n * @param shapeMap - <zh/> 边上的图形映射 | <en/> Shape map on the edge\n * @param placement - <zh/> 徽标位置 | <en/> Badge position\n * @param labelPlacement - <zh/> 标签位置 | <en/> Label position\n * @param offsetX - <zh/> 水平偏移量 | <en/> Horizontal offset\n * @param offsetY - <zh/> 垂直偏移量 | <en/> Vertical offset\n * @returns <zh/> 徽标的位置样式 | <en/> Position style of the badge\n */\nexport function getBadgePositionStyle(\n  shapeMap: Record<string, DisplayObject<any, any>>,\n  placement: EdgeBadgeStyleProps['placement'],\n  labelPlacement: EdgeLabelStyleProps['placement'],\n  offsetX: number,\n  offsetY: number,\n) {\n  const badgeWidth = shapeMap.badge?.getGeometryBounds().halfExtents[0] * 2 || 0;\n  const labelWidth = shapeMap.label?.getGeometryBounds().halfExtents[0] * 2 || 0;\n\n  return getLabelPositionStyle(\n    shapeMap.key as EdgeKey,\n    labelPlacement,\n    true,\n    (labelWidth ? (labelWidth / 2 + badgeWidth / 2) * (placement === 'suffix' ? 1 : -1) : 0) + offsetX,\n    offsetY,\n  );\n}\n\n/**\n * <zh/> 获取给定边上的指定位置的坐标\n *\n * <en/> Get the coordinates at the specified position on the given edge\n * @param key - <zh/> 边实例 | <en/> Edge instance\n * @param ratio - <zh/> 位置比率 | <en/> Position ratio\n * @param offsetX - <zh/> 水平偏移量 | <en/> Horizontal offset\n * @param offsetY - <zh/> 垂直偏移量 | <en/> Vertical offset\n * @param angle - <zh/> 旋转角度 | <en/> Rotation angle\n * @returns <zh/> 坐标 | <en/> Coordinates\n */\nfunction getXYByPlacement(key: EdgeKey, ratio: number, offsetX: number, offsetY: number, angle?: number) {\n  const [pointX, pointY] = parsePoint(key.getPoint(ratio));\n  let actualOffsetX = offsetX;\n  let actualOffsetY = offsetY;\n\n  if (angle) {\n    actualOffsetX = offsetX * Math.cos(angle) - offsetY * Math.sin(angle);\n    actualOffsetY = offsetX * Math.sin(angle) + offsetY * Math.cos(angle);\n  }\n\n  return [pointX + actualOffsetX, pointY + actualOffsetY];\n}\n\n/** ==================== Curve Edge =========================== */\n\n/**\n * <zh/> 计算曲线的控制点\n *\n * <en/> Calculate the control point of the curve\n * @param sourcePoint - <zh/> 起点 | <en/> Source point\n * @param targetPoint - <zh/> 终点 | <en/> Target point\n * @param curvePosition - <zh/> 控制点在连线上的相对位置（取值范围为 0-1） | <en/> The relative position of the control point on the line (value range from 0 to 1)\n * @param curveOffset - <zh/> 控制点距离两端点连线的距离 | <en/> The distance between the control point and the line\n * @returns <zh/> 控制点 | <en/> Control points\n */\nexport function getCurveControlPoint(\n  sourcePoint: Point,\n  targetPoint: Point,\n  curvePosition: number,\n  curveOffset: number,\n): Point {\n  if (isEqual(sourcePoint, targetPoint)) return sourcePoint;\n  const lineVector = subtract(targetPoint, sourcePoint);\n  const controlPoint: Point = [\n    sourcePoint[0] + curvePosition * lineVector[0],\n    sourcePoint[1] + curvePosition * lineVector[1],\n  ];\n  const perpVector = normalize(perpendicular(lineVector as Vector2, false));\n  controlPoint[0] += curveOffset * perpVector[0];\n  controlPoint[1] += curveOffset * perpVector[1];\n  return controlPoint;\n}\n\n/**\n * <zh/> 解析控制点距离两端点连线的距离 `curveOffset`\n *\n * <en/> parse the distance of the control point from the line `curveOffset`\n * @param curveOffset - <zh/> curveOffset | <en/> curveOffset\n * @returns <zh/> 标准 curveOffset | <en/> standard curveOffset\n */\nexport function parseCurveOffset(curveOffset: number | [number, number]): [number, number] {\n  if (isNumber(curveOffset)) return [curveOffset, -curveOffset];\n  return curveOffset;\n}\n\n/**\n * <zh/> 解析控制点在两端点连线上的相对位置 `curvePosition`，范围为`0-1`\n *\n * <en/> parse the relative position of the control point on the line `curvePosition`\n * @param curvePosition - <zh/> curvePosition | <en/> curvePosition\n * @returns <zh/> 标准 curvePosition | <en/> standard curvePosition\n */\nexport function parseCurvePosition(curvePosition: number | [number, number]): [number, number] {\n  if (isNumber(curvePosition)) return [curvePosition, 1 - curvePosition];\n  return curvePosition;\n}\n\n/**\n * <zh/> 获取二次贝塞尔曲线绘制路径\n *\n * <en/> Calculate the path for drawing a quadratic Bessel curve\n * @param sourcePoint - <zh/> 边的起点 | <en/> Source point\n * @param targetPoint - <zh/> 边的终点 | <en/> Target point\n * @param controlPoint - <zh/> 控制点 | <en/> Control point\n * @returns <zh/> 返回绘制曲线的路径 | <en/> Returns curve path\n */\nexport function getQuadraticPath(sourcePoint: Point, targetPoint: Point, controlPoint: Point): PathArray {\n  return [\n    ['M', sourcePoint[0], sourcePoint[1]],\n    ['Q', controlPoint[0], controlPoint[1], targetPoint[0], targetPoint[1]],\n  ];\n}\n\n/**\n * <zh/> 获取三次贝塞尔曲线绘制路径\n *\n * <en/> Calculate the path for drawing a cubic Bessel curve\n * @param sourcePoint - <zh/> 边的起点 | <en/> Source point\n * @param targetPoint - <zh/> 边的终点 | <en/> Target point\n * @param controlPoints - <zh/> 控制点 | <en/> Control point\n * @returns <zh/> 返回绘制曲线的路径 | <en/> Returns curve path\n */\nexport function getCubicPath(sourcePoint: Point, targetPoint: Point, controlPoints: [Point, Point]): PathArray {\n  return [\n    ['M', sourcePoint[0], sourcePoint[1]],\n    [\n      'C',\n      controlPoints[0][0],\n      controlPoints[0][1],\n      controlPoints[1][0],\n      controlPoints[1][1],\n      targetPoint[0],\n      targetPoint[1],\n    ],\n  ];\n}\n\n/** ==================== Polyline Edge =========================== */\n\n/**\n * <zh/> 获取折线的绘制路径\n *\n * <en/> Calculates the path for drawing a polyline\n * @param points - <zh/> 折线的顶点 | <en/> The vertices of the polyline\n * @param radius - <zh/> 圆角半径 | <en/> Radius of the rounded corner\n * @param z - <zh/> 路径是否闭合 | <en/> Whether the path is closed\n * @returns <zh/> 返回绘制折线的路径 | <en/> Returns the path for drawing a polyline\n */\nexport function getPolylinePath(points: Point[], radius = 0, z = false): PathArray {\n  const targetIndex = points.length - 1;\n  const sourcePoint = points[0];\n  const targetPoint = points[targetIndex];\n  const controlPoints = points.slice(1, targetIndex);\n  const pathArray: PathArray = [['M', sourcePoint[0], sourcePoint[1]]];\n  controlPoints.forEach((midPoint, i) => {\n    const prevPoint = controlPoints[i - 1] || sourcePoint;\n    const nextPoint = controlPoints[i + 1] || targetPoint;\n    if (!isCollinear(prevPoint, midPoint, nextPoint) && radius) {\n      const [ps, pt] = getBorderRadiusPoints(prevPoint, midPoint, nextPoint, radius);\n      pathArray.push(['L', ps[0], ps[1]], ['Q', midPoint[0], midPoint[1], pt[0], pt[1]], ['L', pt[0], pt[1]]);\n    } else {\n      pathArray.push(['L', midPoint[0], midPoint[1]]);\n    }\n  });\n  pathArray.push(['L', targetPoint[0], targetPoint[1]]);\n  if (z) pathArray.push(['Z']);\n  return pathArray;\n}\n\n/**\n * <zh/> 根据给定的半径计算出不共线的三点生成贝塞尔曲线的控制点，以模拟接近圆弧\n *\n * <en/> Calculates the control points of the Bezier curve generated by three non-collinear points according to the given radius to simulate an arc\n * @param prevPoint - <zh/> 前一个点 | <en/> Previous point\n * @param midPoint - <zh/> 中间点 | <en/> Middle point\n * @param nextPoint - <zh/> 后一个点 | <en/> Next point\n * @param radius - <zh/> 圆角半径 | <en/> Radius of the rounded corner\n * @returns <zh/> 返回控制点 | <en/> Returns control points\n */\nexport function getBorderRadiusPoints(\n  prevPoint: Point,\n  midPoint: Point,\n  nextPoint: Point,\n  radius: number,\n): [Point, Point] {\n  const d0 = manhattanDistance(prevPoint, midPoint);\n  const d1 = manhattanDistance(nextPoint, midPoint);\n  // 取给定的半径和最小半径之间的较小值 | use the smaller value between the given radius and the minimum radius\n  const r = Math.min(radius, Math.min(d0, d1) / 2);\n  const ps: Point = [\n    midPoint[0] - (r / d0) * (midPoint[0] - prevPoint[0]),\n    midPoint[1] - (r / d0) * (midPoint[1] - prevPoint[1]),\n  ];\n  const pt: Point = [\n    midPoint[0] - (r / d1) * (midPoint[0] - nextPoint[0]),\n    midPoint[1] - (r / d1) * (midPoint[1] - nextPoint[1]),\n  ];\n  return [ps, pt];\n}\n\n/** ==================== Loop Edge =========================== */\n\nexport const getRadians = (bbox: AABB): Record<LoopPlacement, [number, number]> => {\n  const halfPI = Math.PI / 2;\n  const halfHeight = getBBoxHeight(bbox) / 2;\n  const halfWidth = getBBoxWidth(bbox) / 2;\n  const angleWithX = Math.atan2(halfHeight, halfWidth) / 2;\n  const angleWithY = Math.atan2(halfWidth, halfHeight) / 2;\n  return {\n    top: [-halfPI - angleWithY, -halfPI + angleWithY],\n    'top-right': [-halfPI + angleWithY, -angleWithX],\n    'right-top': [-halfPI + angleWithY, -angleWithX],\n    right: [-angleWithX, angleWithX],\n    'bottom-right': [angleWithX, halfPI - angleWithY],\n    'right-bottom': [angleWithX, halfPI - angleWithY],\n    bottom: [halfPI - angleWithY, halfPI + angleWithY],\n    'bottom-left': [halfPI + angleWithY, Math.PI - angleWithX],\n    'left-bottom': [halfPI + angleWithY, Math.PI - angleWithX],\n    left: [Math.PI - angleWithX, Math.PI + angleWithX],\n    'top-left': [Math.PI + angleWithX, -halfPI - angleWithY],\n    'left-top': [Math.PI + angleWithX, -halfPI - angleWithY],\n  };\n};\n\n/**\n * <zh/> 获取环形边的起点和终点\n *\n * <en/> Get the start and end points of the loop edge\n * @param node - <zh/> 节点实例 | <en/> Node instance\n * @param placement - <zh/> 环形边相对于节点位置 | <en/> Loop position relative to the node\n * @param clockwise - <zh/> 是否顺时针 | <en/> Whether to draw the loop clockwise\n * @param sourcePort - <zh/> 起点连接桩 | <en/> Source port\n * @param targetPort - <zh/> 终点连接桩 | <en/> Target port\n * @returns <zh/> 起点和终点 | <en/> Start and end points\n */\nexport function getLoopEndpoints(\n  node: Node,\n  placement: LoopPlacement,\n  clockwise: boolean,\n  sourcePort?: Port,\n  targetPort?: Port,\n): [Point, Point] {\n  const bbox = getNodeBBox(node);\n  const center = node.getCenter();\n\n  let sourcePoint = sourcePort && getPortPosition(sourcePort);\n  let targetPoint = targetPort && getPortPosition(targetPort);\n\n  if (!sourcePoint || !targetPoint) {\n    const radians = getRadians(bbox);\n    const angle1 = radians[placement][0];\n    const angle2 = radians[placement][1];\n    const [width, height] = getBBoxSize(bbox);\n    const r = Math.max(width, height);\n    const point1: Point = add(center, [r * Math.cos(angle1), r * Math.sin(angle1), 0]);\n    const point2: Point = add(center, [r * Math.cos(angle2), r * Math.sin(angle2), 0]);\n\n    sourcePoint = getNodeConnectionPoint(node, point1);\n    targetPoint = getNodeConnectionPoint(node, point2);\n\n    if (!clockwise) {\n      [sourcePoint, targetPoint] = [targetPoint, sourcePoint];\n    }\n  }\n\n  return [sourcePoint, targetPoint];\n}\n\n/**\n * <zh/> 获取环形边的绘制路径\n *\n * <en/> Get the path of the loop edge\n * @param node - <zh/> 节点实例 | <en/> Node instance\n * @param placement - <zh/> 环形边相对于节点位置 | <en/> Loop position relative to the node\n * @param clockwise - <zh/> 是否顺时针 | <en/> Whether to draw the loop clockwise\n * @param dist - <zh/> 从节点 keyShape 边缘到自环顶部的距离 | <en/> The distance from the edge of the node keyShape to the top of the self-loop\n * @param sourcePortKey - <zh/> 起点连接桩 key | <en/> Source port key\n * @param targetPortKey - <zh/> 终点连接桩 key | <en/> Target port key\n * @returns <zh/> 返回绘制环形边的路径 | <en/> Returns the path of the loop edge\n */\nexport function getCubicLoopPath(\n  node: Node,\n  placement: LoopPlacement,\n  clockwise: boolean,\n  dist: number,\n  sourcePortKey?: string,\n  targetPortKey?: string,\n) {\n  const sourcePort = node.getPorts()[(sourcePortKey || targetPortKey)!];\n  const targetPort = node.getPorts()[(targetPortKey || sourcePortKey)!];\n\n  // 1. 获取起点和终点 | Get the start and end points\n  let [sourcePoint, targetPoint] = getLoopEndpoints(node, placement, clockwise, sourcePort, targetPort);\n\n  // 2. 获取控制点 | Get the control points\n  const controlPoints = getCubicLoopControlPoints(node, sourcePoint, targetPoint, dist);\n\n  // 3. 如果定义了连接桩，调整端点以与连接桩边界相交 | If the port is defined, adjust the endpoint to intersect with the port boundary\n  if (sourcePort) sourcePoint = getPortConnectionPoint(sourcePort, controlPoints[0]);\n  if (targetPort) targetPoint = getPortConnectionPoint(targetPort, controlPoints.at(-1) as Point);\n\n  return getCubicPath(sourcePoint, targetPoint, controlPoints);\n}\n\n/**\n * <zh/> 获取环形边的控制点\n *\n * <en/> Get the control points of the loop edge\n * @param node - <zh/> 节点实例 | <en/> Node instance\n * @param sourcePoint - <zh/> 起点 | <en/> Source point\n * @param targetPoint - <zh/> 终点 | <en/> Target point\n * @param dist - <zh/> 从节点 keyShape 边缘到自环顶部的距离 | <en/> The distance from the edge of the node keyShape to the top of the self-loop\n * @returns <zh/> 控制点 | <en/> Control points\n */\nexport function getCubicLoopControlPoints(\n  node: Node,\n  sourcePoint: Point,\n  targetPoint: Point,\n  dist: number,\n): [Point, Point] {\n  const center = node.getCenter();\n\n  if (isEqual(sourcePoint, targetPoint)) {\n    const direction = subtract(sourcePoint, center);\n    const adjustment: Point = [\n      dist * Math.sign(direction[0]) || dist / 2,\n      dist * Math.sign(direction[1]) || -dist / 2,\n      0,\n    ];\n    return [add(sourcePoint, adjustment), add(targetPoint, multiply(adjustment, [1, -1, 1]))];\n  }\n\n  return [\n    moveTo(center, sourcePoint, distance(center, sourcePoint) + dist),\n    moveTo(center, targetPoint, distance(center, targetPoint) + dist),\n  ];\n}\n\n/**\n * <zh/> 获取环形折线边的绘制路径\n *\n * <en/> Get the path of the loop polyline edge\n * @param node - <zh/> 节点实例 | <en/> Node instance\n * @param radius - <zh/> 圆角半径 | <en/> Radius of the rounded corner\n * @param placement - <zh/> 环形边相对于节点位置 | <en/> Loop position relative to the node\n * @param clockwise - <zh/> 是否顺时针 | <en/> Whether to draw the loop clockwise\n * @param dist - <zh/> 从节点 keyShape 边缘到自环顶部的距离 | <en/> The distance from the edge of the node keyShape to the top of the self-loop\n * @param sourcePortKey - <zh/> 起点连接桩 key | <en/> Source port key\n * @param targetPortKey - <zh/> 终点连接桩 key | <en/> Target port key\n * @returns <zh/> 返回绘制环形折线边的路径 | <en/> Returns the path of the loop polyline edge\n */\nexport function getPolylineLoopPath(\n  node: Node,\n  radius: number,\n  placement: LoopPlacement,\n  clockwise: boolean,\n  dist: number,\n  sourcePortKey?: string,\n  targetPortKey?: string,\n) {\n  const allPortsMap = getAllPorts(node);\n  const sourcePort = allPortsMap[(sourcePortKey || targetPortKey)!];\n  const targetPort = allPortsMap[(targetPortKey || sourcePortKey)!];\n\n  // 1. 获取起点和终点 | Get the start and end points\n  let [sourcePoint, targetPoint] = getLoopEndpoints(node, placement, clockwise, sourcePort, targetPort);\n\n  // 2. 获取控制点 | Get the control points\n  const controlPoints = getPolylineLoopControlPoints(node, sourcePoint, targetPoint, dist);\n\n  // 3. 如果定义了连接桩，调整端点以与连接桩边界相交 | If the port is defined, adjust the endpoint to intersect with the port boundary\n  if (sourcePort) sourcePoint = getPortConnectionPoint(sourcePort, controlPoints[0]);\n  if (targetPort) targetPoint = getPortConnectionPoint(targetPort, controlPoints.at(-1) as Point);\n\n  return getPolylinePath([sourcePoint, ...controlPoints, targetPoint], radius);\n}\n\n/**\n * <zh/> 获取环形折线边的控制点\n *\n * <en/> Get the control points of the loop polyline edge\n * @param node - <zh/> 节点实例 | <en/> Node instance\n * @param sourcePoint - <zh/> 起点 | <en/> Source point\n * @param targetPoint - <zh/> 终点 | <en/> Target point\n * @param dist - <zh/> 从节点 keyShape 边缘到自环顶部的距离 | <en/> The distance from the edge of the node keyShape to the top of the self-loop\n * @returns <zh/> 控制点 | <en/> Control points\n */\nexport function getPolylineLoopControlPoints(node: Node, sourcePoint: Point, targetPoint: Point, dist: number) {\n  const controlPoints: Point[] = [];\n  const bbox = getNodeBBox(node);\n\n  // 1. 起点和终点相同 | The start and end points are the same\n  if (isEqual(sourcePoint, targetPoint)) {\n    const side = getNearestBoundarySide(sourcePoint, bbox);\n    switch (side) {\n      case 'left':\n        controlPoints.push([sourcePoint[0] - dist, sourcePoint[1]]);\n        controlPoints.push([sourcePoint[0] - dist, sourcePoint[1] + dist]);\n        controlPoints.push([sourcePoint[0], sourcePoint[1] + dist]);\n        break;\n      case 'right':\n        controlPoints.push([sourcePoint[0] + dist, sourcePoint[1]]);\n        controlPoints.push([sourcePoint[0] + dist, sourcePoint[1] + dist]);\n        controlPoints.push([sourcePoint[0], sourcePoint[1] + dist]);\n        break;\n      case 'top':\n        controlPoints.push([sourcePoint[0], sourcePoint[1] - dist]);\n        controlPoints.push([sourcePoint[0] + dist, sourcePoint[1] - dist]);\n        controlPoints.push([sourcePoint[0] + dist, sourcePoint[1]]);\n        break;\n      case 'bottom':\n        controlPoints.push([sourcePoint[0], sourcePoint[1] + dist]);\n        controlPoints.push([sourcePoint[0] + dist, sourcePoint[1] + dist]);\n        controlPoints.push([sourcePoint[0] + dist, sourcePoint[1]]);\n        break;\n    }\n  } else {\n    const sourceSide = getNearestBoundarySide(sourcePoint, bbox);\n    const targetSide = getNearestBoundarySide(targetPoint, bbox);\n    // 2. 起点与终点同边 | The start and end points are on the same side\n    if (sourceSide === targetSide) {\n      const side = sourceSide;\n      let x, y;\n      switch (side) {\n        case 'left':\n          x = Math.min(sourcePoint[0], targetPoint[0]) - dist;\n          controlPoints.push([x, sourcePoint[1]]);\n          controlPoints.push([x, targetPoint[1]]);\n          break;\n        case 'right':\n          x = Math.max(sourcePoint[0], targetPoint[0]) + dist;\n          controlPoints.push([x, sourcePoint[1]]);\n          controlPoints.push([x, targetPoint[1]]);\n          break;\n        case 'top':\n          y = Math.min(sourcePoint[1], targetPoint[1]) - dist;\n          controlPoints.push([sourcePoint[0], y]);\n          controlPoints.push([targetPoint[0], y]);\n          break;\n        case 'bottom':\n          y = Math.max(sourcePoint[1], targetPoint[1]) + dist;\n          controlPoints.push([sourcePoint[0], y]);\n          controlPoints.push([targetPoint[0], y]);\n          break;\n      }\n    } else {\n      // 3. 起点与终点不同边 | The start and end points are on different sides\n      const getPointOffSide = (side: 'left' | 'right' | 'top' | 'bottom', point: Point): Point => {\n        return {\n          left: [point[0] - dist, point[1]],\n          right: [point[0] + dist, point[1]],\n          top: [point[0], point[1] - dist],\n          bottom: [point[0], point[1] + dist],\n        }[side] as Point;\n      };\n      const p1 = getPointOffSide(sourceSide, sourcePoint);\n      const p2 = getPointOffSide(targetSide, targetPoint);\n      const p3 = freeJoin(p1, p2, bbox);\n      controlPoints.push(p1, p3, p2);\n    }\n  }\n\n  return controlPoints;\n}\n\n/**\n * <zh/> 获取子图内的所有边，并按照内部边和外部边分组\n *\n * <en/> Get all the edges in the subgraph and group them into internal and external edges\n * @param ids - <zh/> 节点 ID 数组 | <en/> Node ID array\n * @param getRelatedEdges - <zh/> 获取节点邻边 | <en/> Get node edges\n * @returns <zh/> 子图边 | <en/> Subgraph edges\n */\nexport function getSubgraphRelatedEdges(ids: ID[], getRelatedEdges: (id: ID) => EdgeData[]) {\n  const edges = new Set<EdgeData>();\n  const internal = new Set<EdgeData>();\n  const external = new Set<EdgeData>();\n\n  ids.forEach((id) => {\n    const relatedEdges = getRelatedEdges(id);\n    relatedEdges.forEach((edge) => {\n      edges.add(edge);\n      if (ids.includes(edge.source) && ids.includes(edge.target)) internal.add(edge);\n      else external.add(edge);\n    });\n  });\n\n  return { edges: Array.from(edges), internal: Array.from(internal), external: Array.from(external) };\n}\n\n/**\n * <zh/> 获取边的实际连接节点\n *\n * <en/> Get the actual connected object of the edge\n * @param node - <zh/> 逻辑连接节点数据 | <en/> Logical connection node data\n * @param getParentData - <zh/> 获取父节点数据 | <en/> Get parent node data\n * @returns <zh/> 实际连接节点数据 | <en/> Actual connected node data\n */\nexport function findActualConnectNodeData(node: NodeLikeData, getParentData: (id: ID) => NodeLikeData | undefined) {\n  const path: NodeLikeData[] = [];\n  let current = node;\n  while (current) {\n    path.push(current);\n    const parent = getParentData(idOf(current));\n    if (parent) current = parent;\n    else break;\n  }\n\n  if (path.some((n) => n.style?.collapsed)) {\n    const index = path.reverse().findIndex(isCollapsed);\n    return path[index] || path.at(-1);\n  }\n\n  return node;\n}\n\n/**\n * <zh/> 获取箭头大小，若用户未指定，则根据线宽自动计算\n *\n * <en/> Get the size of the arrow\n * @param lineWidth - <zh/> 箭头所在边的线宽 | <en/> The line width of the edge where the arrow is located\n * @param size - <zh/> 自定义箭头大小 | <en/> Custom arrow size\n * @returns <zh/> 箭头大小 | <en/> Arrow size\n */\nexport function getArrowSize(lineWidth: number, size?: Size): Size {\n  if (size) return size;\n\n  if (lineWidth < 4) return 10;\n  if (lineWidth === 4) return 12;\n  return lineWidth * 2.5;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/element.ts",
    "content": "import type { AABB, DisplayObject, TextStyleProps } from '@antv/g';\nimport { get, isNumber, isString, set } from '@antv/util';\nimport { BaseCombo, BaseEdge, BaseNode } from '../elements';\nimport type { BaseShape, BaseShapeStyleProps } from '../elements/shapes';\nimport type { Combo, Edge, Element, Node, NodePortStyleProps, Placement, Point, TriangleDirection } from '../types';\nimport type { NodeLabelStyleProps, Port } from '../types/node';\nimport { getBBoxHeight, getBBoxWidth } from './bbox';\nimport { isPoint } from './is';\nimport { findNearestPoints, getEllipseIntersectPoint } from './point';\nimport { getXYByPlacement } from './position';\n\n/**\n * <zh/> 判断是否是 Node 的实例\n *\n * <en/> Judge whether the instance is Node\n * @param shape - <zh/> 实例 | <en/> instance\n * @returns <zh/> 是否是 Node 的实例 | <en/> whether the instance is Node\n */\nexport function isNode(shape: DisplayObject | Port): shape is Node {\n  return shape instanceof BaseNode && shape.type === 'node';\n}\n\n/**\n * <zh/> 判断是否是 Edge 的实例\n *\n * <en/> Judge whether the instance is Edge\n * @param shape - <zh/> 实例 | <en/> instance\n * @returns <zh/> 是否是 Edge 的实例 | <en/> whether the instance is Edge\n */\nexport function isEdge(shape: DisplayObject): shape is Edge {\n  return shape instanceof BaseEdge;\n}\n\n/**\n * <zh/> 判断是否是 Combo 的实例\n *\n * <en/> Judge whether the instance is Combo\n * @param shape - <zh/> 实例 | <en/> instance\n * @returns <zh/> 是否是 Combo 的实例 | <en/> whether the instance is Combo\n */\nexport function isCombo(shape: any): shape is Combo {\n  return shape instanceof BaseCombo;\n}\n\n/**\n * <zh/> 判断是否是 Element 的实例\n *\n * <en/> Judge whether the instance is Element\n * @param shape - <zh/> 实例 | <en/> instance\n * @returns <zh/> 是否是 Element 的实例 | <en/> whether the instance is Element\n */\nexport function isElement(shape: any): shape is Element {\n  return isNode(shape) || isEdge(shape) || isCombo(shape);\n}\n\n/**\n * <zh/> 判断两个节点是否相同\n *\n * <en/> Whether the two nodes are the same\n * @param node1 - <zh/> 节点1 | <en/> Node1\n * @param node2 - <zh/> 节点2 | <en/> Node2\n * @returns <zh/> 是否相同 | <en/> Whether the same\n */\nexport function isSameNode(node1: Node, node2: Node): boolean {\n  if (!node1 || !node2) return false;\n  return node1 === node2;\n}\n\nconst PORT_MAP: Record<string, Point> = {\n  top: [0.5, 0],\n  right: [1, 0.5],\n  bottom: [0.5, 1],\n  left: [0, 0.5],\n  'left-top': [0, 0],\n  'top-left': [0, 0],\n  'left-bottom': [0, 1],\n  'bottom-left': [0, 1],\n  'right-top': [1, 0],\n  'top-right': [1, 0],\n  'right-bottom': [1, 1],\n  'bottom-right': [1, 1],\n  default: [0.5, 0.5],\n};\n\n/**\n * Get the Port x, y by `position`.\n * @param bbox - BBox of element.\n * @param placement - The position relative with element.\n * @param portMap - The map of position.\n * @param isRelative - Whether the position in MAP is relative.\n * @returns [x, y]\n */\nexport function getPortXYByPlacement(\n  bbox: AABB,\n  placement?: Placement,\n  portMap: Record<string, Point> = PORT_MAP,\n  isRelative = true,\n): Point {\n  const DEFAULT = [0.5, 0.5];\n  const p: [number, number] = isString(placement) ? get(portMap, placement.toLocaleLowerCase(), DEFAULT) : placement;\n\n  if (!isRelative && isString(placement)) return p;\n\n  const [x, y] = p || DEFAULT;\n  return [bbox.min[0] + getBBoxWidth(bbox) * x, bbox.min[1] + getBBoxHeight(bbox) * y];\n}\n\n/**\n * <zh/> 获取节点上的所有连接桩\n *\n * <en/> Get all ports\n * @param node - <zh/> 节点 | <en/> Node\n * @returns <zh/> 所有连接桩 | <en/> All Ports\n */\nexport function getAllPorts(node: Node): Record<string, Port> {\n  if (!node) return {};\n  // 1. 需要绘制的连接桩 | Get the ports that need to be drawn\n  const ports = node.getPorts();\n\n  // 2. 不需要额外绘制的连接桩 | Get the ports that do not need to be drawn\n  const portsStyle = node.attributes.ports || [];\n  portsStyle.forEach((portStyle: NodePortStyleProps, i: number) => {\n    const { key, placement } = portStyle;\n    if (isSimplePort(portStyle)) {\n      ports[key || i] ||= getXYByPlacement(node.getShape('key').getBounds(), placement);\n    }\n  });\n  return ports;\n}\n\n/**\n * <zh/> 是否为简单连接桩，如果是则不会额外绘制图形\n *\n * <en/> Whether it is a simple port, which will not draw additional graphics\n * @param portStyle - <zh/> 连接桩样式 | <en/> Port Style\n * @returns <zh/> 是否是简单连接桩 | <en/> Whether it is a simple port\n */\nexport function isSimplePort(portStyle: NodePortStyleProps): boolean {\n  const { r } = portStyle;\n  return !r || Number(r) === 0;\n}\n\n/**\n * <zh/> 获取连接桩的位置\n *\n * <en/> Get the position of the port\n * @param port - <zh/> 连接桩 | <en/> Port\n * @returns <zh/> 连接桩的位置 | <en/> Port Position\n */\nexport function getPortPosition(port: Port): Point {\n  return isPoint(port) ? port : (port.getPosition() as Point);\n}\n\n/**\n * <zh/> 查找起始连接桩和目标连接桩\n *\n * <en/> Find the source port and target port\n * @param sourceNode - <zh/> 起始节点 | <en/> Source Node\n * @param targetNode - <zh/> 目标节点 | <en/> Target Node\n * @param sourcePortKey - <zh/> 起始连接桩的 key | <en/> Source Port Key\n * @param targetPortKey - <zh/> 目标连接桩的 key | <en/> Target Port Key\n * @returns <zh/> 起始连接桩和目标连接桩 | <en/> Source Port and Target Port\n */\nexport function findPorts(\n  sourceNode: Node,\n  targetNode: Node,\n  sourcePortKey?: string,\n  targetPortKey?: string,\n): [Port | undefined, Port | undefined] {\n  const sourcePort = findPort(sourceNode, targetNode, sourcePortKey, targetPortKey);\n  const targetPort = findPort(targetNode, sourceNode, targetPortKey, sourcePortKey);\n  return [sourcePort, targetPort];\n}\n\n/**\n * <zh/> 查找节点上的最有可能连接的连接桩\n *\n * <en/> Find the most likely connected port on the node\n * @remarks\n * 1. If `portKey` is specified, return the port.\n * 2. If `portKey` is not specified, return the port closest to the opposite connection points.\n * 3. If the node has no ports, return undefined.\n * @param node - <zh/> 节点 | <en/> Node\n * @param oppositeNode - <zh/> 对端节点 | <en/> Opposite Node\n * @param portKey - <zh/> 连接桩的键值（key） | <en/> Port Key\n * @param oppositePortKey - <zh/> 对端连接桩的 key | <en/> Opposite Port Key\n * @returns <zh/> 连接桩 | <en/> Port\n */\nexport function findPort(node: Node, oppositeNode: Node, portKey?: string, oppositePortKey?: string): Port | undefined {\n  const portsMap = getAllPorts(node);\n  if (portKey) return portsMap[portKey];\n\n  const ports = Object.values(portsMap);\n  if (ports.length === 0) return undefined;\n\n  const positions = ports.map((port) => getPortPosition(port));\n  const oppositePositions = findConnectionPoints(oppositeNode, oppositePortKey);\n  const [nearestPosition] = findNearestPoints(positions, oppositePositions);\n  return ports.find((port) => getPortPosition(port) === nearestPosition);\n}\n\n/**\n * <zh/> 寻找节点上所有可能的连接点\n *\n * <en/> Find all possible connection points on the node\n * @remarks\n * 1. If `portKey` is specified, return the position of the port.\n * 2. If `portKey` is not specified, return positions of all ports.\n * 3. If the node has no ports, return the center of the node.\n * @param node - <zh/> 节点 | <en/> Node\n * @param portKey - <zh/> 连接桩的键值（key），如不指定则返回所有 | <en/> Port Key, return all if not specified\n * @returns <zh/> 连接点 | <en/> Connection Point\n */\nfunction findConnectionPoints(node: Node, portKey?: string): Point[] {\n  const allPortsMap = getAllPorts(node);\n  if (portKey) return [getPortPosition(allPortsMap[portKey])];\n  const oppositePorts = Object.values(allPortsMap);\n  return oppositePorts.length > 0 ? oppositePorts.map((port) => getPortPosition(port)) : [node.getCenter()];\n}\n\n/**\n * <zh/> 获取连接点, 即从节点或连接桩中心到另一端的连线在节点或连接桩边界上的交点\n *\n * <en/> Get the connection point\n * @param node - <zh/> 节点或连接桩 | <en/> Node or Port\n * @param opposite - <zh/> 对端的具体点或节点 | <en/> Opposite Point or Node\n * @returns <zh/> 连接点 | <en/> Connection Point\n */\nexport function getConnectionPoint(node: Port | Node | Combo, opposite: Node | Port): Point {\n  return isCombo(node) || isNode(node)\n    ? getNodeConnectionPoint(node, opposite)\n    : getPortConnectionPoint(node, opposite);\n}\n\n/**\n * <zh/> 获取连接桩的连接点，即从连接桩中心到另一端的连线在连接桩边界上的交点\n *\n * <en/> Get the connection point of the port\n * @param port - <zh/> 连接桩 | <en/> Port\n * @param opposite - <zh/> 对端的具体点或节点 | <en/> Opposite Point or Node\n * // @param oppositePort - <zh/> 对端连接桩 | <en/> Opposite Port\n * @returns <zh/> 连接桩的连接点 | <en/> Port Point\n */\nexport function getPortConnectionPoint(port: Port, opposite: Node | Port): Point {\n  if (!port || !opposite) return [0, 0, 0];\n  if (isPoint(port)) return port;\n\n  // 1. linkToCenter 为 true，则返回连接桩的中心 | If linkToCenter is true, return the center of the port\n  if (port.attributes.linkToCenter) return port.getPosition() as Point;\n\n  // 2. 推导对端的具体点：如果是连接桩或节点，则返回它的中心；如果是具体点，则直接返回\n  // 2. Get a specific opposite point: if it is a port or a node, return its center; if it is a specific point, return directly\n  const oppositePosition = isPoint(opposite)\n    ? opposite\n    : isNode(opposite)\n      ? opposite.getCenter()\n      : opposite.getPosition();\n\n  // 3. 返回连接桩边界上的交点 | Return the intersection point on the port boundary\n  return getEllipseIntersectPoint(oppositePosition as Point, port.getBounds());\n}\n\n/**\n * <zh/> 获取节点的连接点\n *\n * <en/> Get the Node Connection Point\n * @param nodeLike - <zh/> 节点或组合 | <en/> Node or Combo\n * @param opposite - <zh/> 对端的具体点或节点 | <en/> Opposite Point or Node\n * // @param oppositePort - <zh/> 对端连接桩 | <en/> Opposite Port\n * @returns <zh/> 节点的连接点 | <en/> Node Point\n */\nexport function getNodeConnectionPoint(nodeLike: Node | Combo, opposite: Node | Port): Point {\n  if (!nodeLike || !opposite) return [0, 0, 0];\n  const oppositePosition = isPoint(opposite)\n    ? opposite\n    : isNode(opposite)\n      ? opposite.getCenter()\n      : (opposite.getPosition() as Point);\n  return nodeLike.getIntersectPoint(oppositePosition) || nodeLike.getCenter();\n}\n\n/**\n * Get the Text style by `position`.\n * @param bbox - BBox of element.\n * @param placement - The position relative with element.\n * @param offsetX - The offset x.\n * @param offsetY - The offset y.\n * @param isReverseBaseline - Whether reverse the baseline.\n * @returns Partial<TextStyleProps>\n */\nexport function getTextStyleByPlacement(\n  bbox: AABB,\n  placement: NodeLabelStyleProps['placement'] = 'bottom',\n  offsetX: number = 0,\n  offsetY: number = 0,\n  isReverseBaseline = false,\n): Partial<TextStyleProps> {\n  const direction = placement.split('-');\n  const [x, y] = getXYByPlacement(bbox, placement);\n\n  const [top, bottom]: TextStyleProps['textBaseline'][] = isReverseBaseline ? ['bottom', 'top'] : ['top', 'bottom'];\n\n  const textBaseline: TextStyleProps['textBaseline'] = direction.includes('top')\n    ? bottom\n    : direction.includes('bottom')\n      ? top\n      : 'middle';\n\n  const textAlign: TextStyleProps['textAlign'] = direction.includes('left')\n    ? 'right'\n    : direction.includes('right')\n      ? 'left'\n      : 'center';\n\n  return {\n    transform: [['translate', x + offsetX, y + offsetY]],\n    textBaseline,\n    textAlign,\n  };\n}\n\n/**\n * <zh/> 获取五角星的顶点\n *\n * <en/> Get Star Points\n * @param outerR - <zh/> 外半径 | <en/> outer radius\n * @param innerR - <zh/> 内半径 | <en/> inner radius\n * @returns <zh/> 五角星的顶点 | <en/> Star Points\n */\nexport function getStarPoints(outerR: number, innerR: number): Point[] {\n  return [\n    [0, -outerR],\n    [innerR * Math.cos((3 * Math.PI) / 10), -innerR * Math.sin((3 * Math.PI) / 10)],\n    [outerR * Math.cos(Math.PI / 10), -outerR * Math.sin(Math.PI / 10)],\n    [innerR * Math.cos(Math.PI / 10), innerR * Math.sin(Math.PI / 10)],\n    [outerR * Math.cos((3 * Math.PI) / 10), outerR * Math.sin((3 * Math.PI) / 10)],\n    [0, innerR],\n    [-outerR * Math.cos((3 * Math.PI) / 10), outerR * Math.sin((3 * Math.PI) / 10)],\n    [-innerR * Math.cos(Math.PI / 10), innerR * Math.sin(Math.PI / 10)],\n    [-outerR * Math.cos(Math.PI / 10), -outerR * Math.sin(Math.PI / 10)],\n    [-innerR * Math.cos((3 * Math.PI) / 10), -innerR * Math.sin((3 * Math.PI) / 10)],\n  ];\n}\n\n/**\n * Get Star Port Point.\n * @param outerR - outer radius\n * @param innerR - inner radius\n * @returns Port points for Star.\n */\nexport function getStarPorts(outerR: number, innerR: number): Record<string, Point> {\n  const r: Record<string, Point> = {};\n\n  r['top'] = [0, -outerR];\n\n  r['left'] = [-outerR * Math.cos(Math.PI / 10), -outerR * Math.sin(Math.PI / 10)];\n\n  r['left-bottom'] = [-outerR * Math.cos((3 * Math.PI) / 10), outerR * Math.sin((3 * Math.PI) / 10)];\n\n  r['bottom'] = [0, innerR];\n\n  r['right-bottom'] = [outerR * Math.cos((3 * Math.PI) / 10), outerR * Math.sin((3 * Math.PI) / 10)];\n\n  r['right'] = r['default'] = [outerR * Math.cos(Math.PI / 10), -outerR * Math.sin(Math.PI / 10)];\n\n  return r;\n}\n\n/**\n * <zh/> 获取三角形的顶点\n *\n * <en/> Get the points of a triangle\n * @param width - <zh/> 宽度 | <en/> width\n * @param height - <zh/> 高度 | <en/> height\n * @param direction - <zh/> 三角形的方向 | <en/> The direction of the triangle\n * @returns <zh/> 矩形的顶点 | <en/> The points of a rectangle\n */\nexport function getTrianglePoints(width: number, height: number, direction: TriangleDirection): Point[] {\n  const halfHeight = height / 2;\n  const halfWidth = width / 2;\n  const MAP: Record<TriangleDirection, Point[]> = {\n    up: [\n      [-halfWidth, halfHeight],\n      [halfWidth, halfHeight],\n      [0, -halfHeight],\n    ],\n    left: [\n      [-halfWidth, 0],\n      [halfWidth, halfHeight],\n      [halfWidth, -halfHeight],\n    ],\n    right: [\n      [-halfWidth, halfHeight],\n      [-halfWidth, -halfHeight],\n      [halfWidth, 0],\n    ],\n    down: [\n      [-halfWidth, -halfHeight],\n      [halfWidth, -halfHeight],\n      [0, halfHeight],\n    ],\n  };\n  return MAP[direction] || MAP['up'];\n}\n\n/**\n * <zh/> 获取三角形的连接桩\n *\n * <en/> Get the Ports of Triangle.\n * @param width - <zh/> 宽度 | <en/> width\n * @param height - <zh/> 高度 | <en/> height\n * @param direction - <zh/> 三角形的方向 | <en/> The direction of the triangle\n * @returns <zh/> 三角形的连接桩 | <en/> The Ports of Triangle\n */\nexport function getTrianglePorts(width: number, height: number, direction: TriangleDirection): Record<string, Point> {\n  const halfHeight = height / 2;\n  const halfWidth = width / 2;\n  const ports: Record<string, Point> = {};\n  if (direction === 'down') {\n    ports['bottom'] = ports['default'] = [0, halfHeight];\n    ports['right'] = [halfWidth, -halfHeight];\n    ports['left'] = [-halfWidth, -halfHeight];\n  } else if (direction === 'left') {\n    ports['top'] = [halfWidth, -halfHeight];\n    ports['bottom'] = [halfWidth, halfHeight];\n    ports['left'] = ports['default'] = [-halfWidth, 0];\n  } else if (direction === 'right') {\n    ports['top'] = [-halfWidth, -halfHeight];\n    ports['bottom'] = [-halfWidth, halfHeight];\n    ports['right'] = ports['default'] = [halfWidth, 0];\n  } else {\n    //up\n    ports['left'] = [-halfWidth, halfHeight];\n    ports['top'] = ports['default'] = [0, -halfHeight];\n    ports['right'] = [halfWidth, halfHeight];\n  }\n  return ports;\n}\n\n/**\n * <zh/> 获取矩形的顶点\n *\n * <en/> Get the points of a rectangle\n * @param width - <zh/> 宽度 | <en/> width\n * @param height - <zh/> 高度 | <en/> height\n * @returns <zh/> 矩形的顶点 | <en/> The points of a rectangle\n */\nexport function getBoundingPoints(width: number, height: number): Point[] {\n  return [\n    [width / 2, -height / 2],\n    [width / 2, height / 2],\n    [-width / 2, height / 2],\n    [-width / 2, -height / 2],\n  ];\n}\n\n/**\n * Get Diamond PathArray.\n * @param width - diamond width\n * @param height - diamond height\n * @returns The PathArray for G\n */\nexport function getDiamondPoints(width: number, height: number): Point[] {\n  return [\n    [0, -height / 2],\n    [width / 2, 0],\n    [0, height / 2],\n    [-width / 2, 0],\n  ];\n}\n/**\n * <zh/> 元素是否可见\n *\n * <en/> Whether the element is visible\n * @param element - <zh/> 元素 | <en/> element\n * @returns <zh/> 是否可见 | <en/> whether the element is visible\n */\nexport function isVisible(element: DisplayObject) {\n  return get(element, ['style', 'visibility']) !== 'hidden';\n}\n\n/**\n * <zh/> 设置元素属性（优化性能）\n *\n * <en/> Set element attributes (optimize performance)\n * @param element - <zh/> 元素 | <en/> element\n * @param style - <zh/> 样式 | <en/> style\n */\nexport function setAttributes(element: BaseShape<any>, style: Partial<BaseShapeStyleProps> & Record<string, any>) {\n  const { zIndex, transform, transformOrigin, visibility, cursor, clipPath, component, ...rest } = style;\n  Object.assign(element.attributes, rest);\n\n  if (transform) element.setAttribute('transform', transform);\n  if (isNumber(zIndex)) element.setAttribute('zIndex', zIndex);\n  if (transformOrigin) element.setAttribute('transformOrigin', transformOrigin);\n  if (visibility) element.setAttribute('visibility', visibility);\n  if (cursor) element.setAttribute('cursor', cursor);\n  if (clipPath) element.setAttribute('clipPath', clipPath);\n  if (component) element.setAttribute('component', component);\n}\n\n/**\n * <zh/> 更新图形样式\n *\n * <en/> Update shape style\n * @param shape - <zh/> 图形 | <en/> shape\n * @param style - <zh/> 样式 | <en/> style\n */\nexport function updateStyle<T extends DisplayObject>(shape: T, style: Record<string, unknown>) {\n  if ('update' in shape) (shape.update as (style: Record<string, unknown>) => void)(style);\n  else shape.attr(style);\n}\n\n/**\n * Get Hexagon PathArray\n * @param outerR - <zh/> 外接圆半径 | <en/> the  radius of circumscribed circle\n *  @returns The PathArray for G\n */\nexport function getHexagonPoints(outerR: number): Point[] {\n  return [\n    [0, outerR],\n    [(outerR * Math.sqrt(3)) / 2, outerR / 2],\n    [(outerR * Math.sqrt(3)) / 2, -outerR / 2],\n    [0, -outerR],\n    [(-outerR * Math.sqrt(3)) / 2, -outerR / 2],\n    [(-outerR * Math.sqrt(3)) / 2, outerR / 2],\n  ];\n}\n\n/**\n * <zh/> 将图形标记为即将销毁，用于在 element controller 中识别要销毁的元素\n *\n * <en/> Mark the element as to be destroyed, used to identify the element to be destroyed in the element controller\n * @param element - <zh/> 图形 | <en/> element\n */\nexport function markToBeDestroyed(element: DisplayObject) {\n  set(element, '__to_be_destroyed__', true);\n}\n\n/**\n * <zh/> 判断图形是否即将销毁\n *\n * <en/> Determine whether the element is to be destroyed\n * @param element - <zh/> 图形 | <en/> element\n * @returns <zh/> 是否即将销毁 | <en/> whether the element is to be destroyed\n */\nexport function isToBeDestroyed(element: DisplayObject | unknown) {\n  return get(element, '__to_be_destroyed__', false);\n}\n"
  },
  {
    "path": "packages/g6/src/utils/event/events.ts",
    "content": "import type { IAnimation } from '@antv/g';\nimport type { AnimationType, GraphEvent } from '../../constants';\nimport type {\n  ElementDatum,\n  ElementType,\n  IAnimateEvent,\n  IElementLifeCycleEvent,\n  IGraphLifeCycleEvent,\n  IViewportEvent,\n  TransformOptions,\n} from '../../types';\n\nexport class BaseEvent {\n  constructor(public type: string) {}\n}\n\nexport class GraphLifeCycleEvent extends BaseEvent implements IGraphLifeCycleEvent {\n  constructor(\n    type:\n      | GraphEvent.BEFORE_RENDER\n      | GraphEvent.AFTER_RENDER\n      | GraphEvent.BEFORE_DRAW\n      | GraphEvent.AFTER_DRAW\n      | GraphEvent.BEFORE_LAYOUT\n      | GraphEvent.AFTER_LAYOUT\n      | GraphEvent.BEFORE_STAGE_LAYOUT\n      | GraphEvent.AFTER_STAGE_LAYOUT\n      | GraphEvent.BEFORE_SIZE_CHANGE\n      | GraphEvent.AFTER_SIZE_CHANGE\n      | GraphEvent.BATCH_START\n      | GraphEvent.BATCH_END\n      | GraphEvent.BEFORE_DESTROY\n      | GraphEvent.AFTER_DESTROY,\n    public data?: any,\n  ) {\n    super(type);\n  }\n}\n\nexport class AnimateEvent extends BaseEvent implements IAnimateEvent {\n  constructor(\n    type: GraphEvent.BEFORE_ANIMATE | GraphEvent.AFTER_ANIMATE,\n    public animationType: AnimationType,\n    public animation: IAnimation | null,\n    public data?: any,\n  ) {\n    super(type);\n  }\n}\n\nexport class ElementLifeCycleEvent extends BaseEvent implements IElementLifeCycleEvent {\n  constructor(\n    type:\n      | GraphEvent.BEFORE_ELEMENT_CREATE\n      | GraphEvent.AFTER_ELEMENT_CREATE\n      | GraphEvent.BEFORE_ELEMENT_UPDATE\n      | GraphEvent.AFTER_ELEMENT_UPDATE\n      | GraphEvent.BEFORE_ELEMENT_DESTROY\n      | GraphEvent.AFTER_ELEMENT_DESTROY,\n    public elementType: ElementType,\n    public data: ElementDatum,\n  ) {\n    super(type);\n  }\n}\n\nexport class ViewportEvent extends BaseEvent implements IViewportEvent {\n  constructor(\n    type: GraphEvent.BEFORE_TRANSFORM | GraphEvent.AFTER_TRANSFORM,\n    public data: TransformOptions,\n  ) {\n    super(type);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/utils/event/index.ts",
    "content": "import type EventEmitter from '@antv/event-emitter';\nimport type { DisplayObject } from '@antv/g';\nimport { Document } from '@antv/g';\nimport { Target } from '../../types';\nimport { isCombo, isEdge, isNode } from '../element';\nimport type { BaseEvent } from './events';\n\nexport * from './events';\n\n/**\n * <zh/> 基于 Event 对象触发事件\n *\n * <en/> Trigger event based on Event object\n * @param emitter - <zh/> 事件目标 | <en/> event target\n * @param event - <zh/> 事件对象 | <en/> event object\n */\nexport function emit(emitter: EventEmitter, event: BaseEvent) {\n  emitter.emit(event.type, event);\n}\n\n/**\n * <zh/> 获取事件目标元素\n *\n * <en/> Get the event target element\n * @param shape - <zh/> 事件图形 | <en/> event shape\n * @returns <zh/> 目标元素 | <en/> target element\n * @remarks\n * <zh/> 事件响应大多数情况会命中元素的内部图形，通过该方法可以获取到其所属元素\n *\n * <en/> Most of the event responses will hit the internal graphics of the element, and this method can be used to get the element to which it belongs\n */\nexport function eventTargetOf(shape?: DisplayObject | Document): { type: string; element: Target } | null {\n  if (!shape) return null;\n\n  if (shape instanceof Document) {\n    return { type: 'canvas', element: shape };\n  }\n\n  let element: DisplayObject | null = shape;\n  while (element) {\n    // 此判断条件不适用于 label 和 节点分开渲染的情况\n    // This condition is not applicable to the case where the label and node are rendered separately\n    if (isNode(element)) return { type: 'node', element };\n    if (isEdge(element)) return { type: 'edge', element };\n    if (isCombo(element)) return { type: 'combo', element };\n    element = element.parentElement as DisplayObject | null;\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/extension.ts",
    "content": "import type { STDExtensionOption } from '../registry/extension/types';\nimport type { Graph } from '../runtime/graph';\nimport type { TransformOptions } from '../spec/transform';\n\n/**\n * <zh/> 将模块配置项转换为标准模块格式\n *\n * <en/> Convert extension options to standard format\n * @param graph - <zh/> 图实例 <en/> graph instance\n * @param category - <zh/> 模块类型 <en/> extension type\n * @param extensions - <zh/> 模块配置项 <en/> extension options\n * @returns <zh/> 标准模块配置项 <en/> Standard extension options\n */\nexport function parseExtensions(graph: Graph, category: string, extensions: TransformOptions): STDExtensionOption[] {\n  const counter: Record<string, number> = {};\n\n  const getKey = (type: string) => {\n    if (!(type in counter)) counter[type] = 0;\n    return `${category}-${type}-${counter[type]++}`;\n  };\n\n  return extensions.map((extension) => {\n    if (typeof extension === 'string') {\n      return { type: extension, key: getKey(extension) };\n    }\n    if (typeof extension === 'function') {\n      return extension.call(graph);\n    }\n\n    if (extension.key) return extension;\n    return { ...extension, key: getKey(extension.type!) };\n  }) as STDExtensionOption[];\n}\n"
  },
  {
    "path": "packages/g6/src/utils/graphlib.ts",
    "content": "import type { Edge, Graph as Graphlib, Node } from '@antv/graphlib';\nimport { TREE_KEY } from '../constants';\nimport type { ComboData, EdgeData, NodeData } from '../spec';\nimport { NodeLikeData } from '../types/data';\nimport { idOf } from './id';\nimport { isEdgeData } from './is';\n\nexport function toGraphlibData(datums: EdgeData): Edge<EdgeData>;\nexport function toGraphlibData(datums: NodeLikeData): Node<NodeLikeData>;\n/**\n * <zh/> 将 NodeData、EdgeData、ComboData 转换为 graphlib 的数据结构\n *\n * <en/> Transform NodeData, EdgeData, ComboData to graphlib data structure\n * @param data - <zh/> 节点、边、combo 数据 | <en/> node, combo data\n * @returns <zh/> graphlib 数据 | <en/> graphlib data\n */\nexport function toGraphlibData(data: NodeData | EdgeData | ComboData): Node<NodeLikeData> | Edge<EdgeData> {\n  const { id = idOf(data), style, data: customData, ...rest } = data;\n  const _data = { ...data, style: { ...style }, data: { ...customData } };\n\n  if (isEdgeData(data)) return { id, data: _data, ...rest } as Edge<EdgeData>;\n  return { id, data: _data } as Node<NodeLikeData>;\n}\n\nexport function toG6Data<T extends EdgeData>(data: Edge<T>): T;\nexport function toG6Data<T extends NodeLikeData>(data: Node<T>): T;\n/**\n * <zh/> 将 Node、Edge、Combo 转换为 G6 的数据结构\n *\n * <en/> Transform Node, Edge, Combo to G6 data structure\n * @param data - <zh/> graphlib 节点、边、Combo 数据 | <en/> graphlib node, edge, combo data\n * @returns <zh/> G6 数据 | <en/> G6 data\n */\nexport function toG6Data<T extends NodeData | EdgeData | ComboData>(data: Node<T> | Edge<T>): T {\n  return data.data;\n}\n\n/**\n * <zh/> 创建树形结构\n *\n * <en/> Create tree structure\n * @param model - <zh/> 数据模型 | <en/> data model\n */\nexport function createTreeStructure(model: Graphlib<any, any>) {\n  if (model.hasTreeStructure(TREE_KEY)) return;\n\n  model.attachTreeStructure(TREE_KEY);\n  const edges = model.getAllEdges();\n\n  for (const edge of edges) {\n    const { source, target } = edge;\n    model.setParent(target, source, TREE_KEY);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/utils/id.ts",
    "content": "import type { ComboData, EdgeData, GraphData, NodeData } from '../spec';\nimport type { DataID, ID } from '../types';\nimport { format } from './print';\n\n/**\n * <zh/> 获取节点/边/Combo 的 ID\n *\n * <en/> get the id of node/edge/combo\n * @param data - <zh/> 节点/边/Combo 的数据 | <en/> data of node/edge/combo\n * @returns <zh/> 节点/边/Combo 的 ID | <en/> ID of node/edge/combo\n */\nexport function idOf(data: Partial<NodeData | EdgeData | ComboData>): ID {\n  if (data.id !== undefined) return data.id;\n  if (data.source !== undefined && data.target !== undefined) return `${data.source}-${data.target}`;\n\n  throw new Error(format('The datum does not have available id.'));\n}\n\n/**\n * <zh/> 获取节点/Combo 的父节点 ID\n *\n * <en/> get the parent id of node/combo\n * @param data - <zh/> 节点/Combo 的数据 | <en/> data of node/combo\n * @returns <zh/> 节点/Combo 的父节点 ID | <en/> parent id of node/combo\n */\nexport function parentIdOf(data: Partial<NodeData | ComboData>) {\n  return data.combo;\n}\n\nexport function idsOf(data: GraphData, flat: true): ID[];\nexport function idsOf(data: GraphData, flat: false): DataID;\n/**\n * <zh/> 获取图数据中所有节点/边/Combo 的 ID\n *\n * <en/> Get the IDs of all nodes/edges/combos in the graph data\n * @param data - <zh/> 图数据 | <en/> graph data\n * @param flat - <zh/> 是否扁平化返回 | <en/> Whether to return flat\n * @returns - <zh/> 返回元素 ID 数组 | <en/> Returns an array of element IDs\n */\nexport function idsOf(data: GraphData, flat: boolean): ID[] | DataID {\n  const ids = {\n    nodes: (data.nodes || []).map(idOf),\n    edges: (data.edges || []).map(idOf),\n    combos: (data.combos || []).map(idOf),\n  };\n  return flat ? Object.values(ids).flat() : ids;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/is.ts",
    "content": "import type { EdgeData } from '../spec';\nimport type { ElementDatum, Point, Vector2, Vector3 } from '../types';\n\n/**\n * <zh/> 判断是否为边数据\n *\n * <en/> judge whether the data is edge data\n * @param data - <zh/> 元素数据 | <en/> element data\n * @returns - <zh/> 是否为边数据 | <en/> whether the data is edge data\n */\nexport function isEdgeData(data: Partial<ElementDatum>): data is EdgeData {\n  if ('source' in data && 'target' in data) return true;\n  return false;\n}\n\n/**\n * <zh/> 判断是否为二维向量\n *\n * <en/> Judge whether the vector is 2d\n * @param vector - <zh/> 向量 | <en/> vector\n * @returns <zh/> 是否为二维向量 | <en/> whether the vector is 2d\n */\nexport function isVector2(vector: Point): vector is Vector2 {\n  return vector.length === 2;\n}\n\n/**\n * <zh/> 判断是否为三维向量\n *\n * <en/> Judge whether the vector is 3d\n * @param vector - <zh/> 向量 | <en/> vector\n * @returns <zh/> 是否为三维向量 | <en/> whether the vector is 3d\n */\nexport function isVector3(vector: Point): vector is Vector3 {\n  return vector.length === 3;\n}\n\n/**\n * <zh/> 判断是否为点\n *\n * <en/> Judge whether the point is valid\n * @param p - <zh/> 点 | <en/> point\n * @returns <zh/> 是否为点 | <en/> whether the point is valid\n */\nexport function isPoint(p: any): p is Point {\n  if (p instanceof Float32Array) return true;\n\n  if (Array.isArray(p) && (p.length === 2 || p.length === 3)) {\n    return p.every((elem) => typeof elem === 'number');\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/layout.ts",
    "content": "import { Edge, Graph as Graphlib, Node } from '@antv/graphlib';\nimport { deepMix, isNumber } from '@antv/util';\nimport { COMBO_KEY } from '../constants';\nimport { BaseLayout } from '../layouts/base-layout';\nimport { idOf } from './id';\n\nimport type { AntVGraphData, AntVLayout, LegacyAntVLayout, LegacyGraph } from '../layouts/types';\nimport type { RuntimeContext } from '../runtime/types';\nimport type { EdgeData, GraphData, NodeData } from '../spec/data';\nimport type { NodeStyle } from '../spec/element/node';\nimport type { LayoutOptions, STDLayoutOptions } from '../spec/layout';\nimport type { AdaptiveLayout, ID } from '../types';\nimport { parsePoint } from './point';\n\n/**\n * <zh/> 判断是否是 combo 布局\n *\n * <en/> Determine if it is a combo layout\n * @param options - <zh/> 布局配置项 | <en/> Layout options\n * @returns <zh/> 是否是 combo 布局 | <en/> Whether it is a combo layout\n */\nexport function isComboLayout(options: STDLayoutOptions) {\n  const { type } = options;\n  if (['comboCombined', 'comboForce'].includes(type)) return true;\n  if (type === 'antv-dagre' && options.sortByCombo) return true;\n  return false;\n}\n\n/**\n * <zh/> 判断是否是树图布局\n *\n * <en/> Determine if it is a tree layout\n * @param options - <zh/> 布局配置项 | <en/> Layout options\n * @returns <zh/> 是否是树图布局 | <en/> Whether it is a tree layout\n */\nexport function isTreeLayout(options: STDLayoutOptions) {\n  const { type } = options;\n  return ['compact-box', 'mindmap', 'dendrogram', 'indented'].includes(type);\n}\n\n/**\n * <zh/> 数据中是否指定了位置\n *\n * <en/> Is the position specified in the data\n * @param data - <zh/> 数据 | <en/> Data\n * @returns <zh/> 是否指定了位置 | <en/> Is the position specified\n */\nexport function isPositionSpecified(data: Record<string, unknown>) {\n  return isNumber(data.x) && isNumber(data.y);\n}\n\n/**\n * <zh/> 是否是前布局\n *\n * <en/> Is pre-layout\n * @remarks\n * <zh/> 前布局是指在初始化元素前计算布局，适用于一些布局需要提前计算位置的场景。\n *\n * <en/> Pre-layout refers to calculating the layout before initializing the elements, which is suitable for some layouts that need to calculate the position in advance.\n * @param options - <zh/> 布局配置项 | <en/> Layout options\n * @returns <zh/> 是否是前布局 | <en/> Is it a pre-layout\n */\nexport function isPreLayout(options?: LayoutOptions) {\n  return !Array.isArray(options) && options?.preLayout;\n}\n\n/**\n * <zh/> 将 @antv/layout 布局适配为 G6 布局\n *\n * <en/> Adapt @antv/layout layout to G6 layout\n * @param Ctor - <zh/> 布局类 | <en/> Layout class\n * @param context - <zh/> 运行时上下文 | <en/> Runtime context\n * @returns <zh/> G6 布局类 | <en/> G6 layout class\n */\nexport function layoutAdapter(\n  Ctor: new (options: Record<string, unknown>) => AntVLayout,\n  context: RuntimeContext,\n): new (context: RuntimeContext, options?: Record<string, unknown>) => BaseLayout {\n  class AdaptLayout extends BaseLayout implements AdaptiveLayout {\n    public instance: AntVLayout;\n\n    public id: string;\n\n    constructor(context: RuntimeContext, options?: Record<string, unknown>) {\n      super(context, options);\n      this.instance = new Ctor({});\n      this.id = this.instance.id;\n\n      if ('stop' in this.instance && 'tick' in this.instance) {\n        const instance = this.instance;\n        this.stop = instance.stop.bind(instance);\n        this.tick = (iterations?: number) => {\n          instance.tick(iterations);\n          return this.getLayoutResult(instance);\n        };\n      }\n    }\n\n    public async execute(model: GraphData, options?: STDLayoutOptions): Promise<GraphData> {\n      await this.instance.execute(\n        this.graphData2LayoutModel(model),\n        this.transformOptions(deepMix({}, this.options, options)),\n      );\n      return this.getLayoutResult(this.instance);\n    }\n\n    private graphData2LayoutModel(data: GraphData): AntVGraphData {\n      const { nodes = [], edges = [], combos = [] } = data;\n\n      return {\n        nodes: [...nodes, ...combos],\n        edges,\n      };\n    }\n\n    private transformOptions(options: STDLayoutOptions) {\n      const isCombo = (id: string) => context.model.isCombo(id);\n\n      const defaultNode = (datum: NodeData) => {\n        const { style } = datum || {};\n        const parentId = 'combo' in (datum || {}) ? (datum.combo ?? null) : null;\n        const id = idOf(datum);\n\n        return {\n          id,\n          ...(isNumber(style?.x) ? { x: style.x } : {}),\n          ...(isNumber(style?.y) ? { y: style.y } : {}),\n          ...(isNumber(style?.z) ? { z: style.z } : {}),\n          parentId,\n          ...(isCombo(id) ? { isCombo: true } : {}),\n        };\n      };\n\n      const defaultEdge = (datum: EdgeData) => ({ id: idOf(datum), source: datum.source, target: datum.target });\n\n      options.node = defaultNode;\n      options.edge = defaultEdge;\n\n      if (!('onTick' in options)) return options;\n\n      const onTick = options.onTick as (data: GraphData) => void;\n      options.onTick = (layout: AntVLayout) => onTick(this.getLayoutResult(layout));\n      return options;\n    }\n\n    private getLayoutResult(layout: AntVLayout): GraphData {\n      const { model } = this.context;\n\n      const result: GraphData = { nodes: [], edges: [], combos: [] };\n\n      layout.forEachNode((node) => {\n        const nodeId = String(node.id);\n        const target = model.isCombo(nodeId) ? result.combos : result.nodes;\n\n        const style = { x: node.x, y: node.y, z: node.z ?? 0, ...(node.size ? { size: node.size } : {}) } as NodeStyle;\n\n        target?.push({ id: nodeId, style });\n      });\n\n      layout.forEachEdge((edge) => {\n        const style = { controlPoints: edge.points || [] };\n        result.edges!.push({ id: String(edge.id), source: String(edge.source), target: String(edge.target), style });\n      });\n\n      return result;\n    }\n  }\n\n  return AdaptLayout;\n}\n\n/**\n * <zh/> 将图布局结果转换为 G6 数据\n *\n * <en/> Convert the layout result to G6 data\n * @param layoutMapping - <zh/> 布局映射 | <en/> Layout mapping\n * @returns <zh/> G6 数据 | <en/> G6 data\n */\nexport function layoutMapping2GraphData(layoutMapping: AntVGraphData): GraphData {\n  const { nodes, edges = [] } = layoutMapping;\n  const data: GraphData = { nodes: [], edges: [], combos: [] };\n\n  nodes.forEach((nodeLike) => {\n    const target = nodeLike.data._isCombo ? data.combos : data.nodes;\n    const { x, y, z = 0 } = nodeLike.data;\n    target?.push({\n      id: nodeLike.id as ID,\n      style: { x, y, z },\n    });\n  });\n\n  edges.forEach((edge) => {\n    const {\n      id,\n      source,\n      target,\n      data: { points = [], controlPoints = points.slice(1, points.length - 1) },\n    } = edge;\n\n    data.edges!.push({\n      id: id as ID,\n      source: source as ID,\n      target: target as ID,\n      style: {\n        /**\n         * antv-dagre 返回 controlPoints，dagre 返回 points\n         * antv-dagre returns controlPoints, dagre returns points\n         */\n        ...(controlPoints?.length ? { controlPoints: controlPoints.map(parsePoint) } : {}),\n      },\n    });\n  });\n\n  return data;\n}\n\n/**\n * <zh/> 判断是否为 AntV Layout 1.x\n *\n * <en/> Determine if it is AntV Layout 1.x\n * @param Ctor - <zh/> 布局类 | <en/> Layout class\n * @returns <zh/> 是否为 AntV Layout 1.x | <en/> Whether it is AntV Layout 1.x\n */\nexport function isLegacyAntVLayout(\n  Ctor: new (options: Record<string, unknown>) => unknown,\n): Ctor is new (options: Record<string, unknown>) => LegacyAntVLayout {\n  return !('forEachNode' in Ctor.prototype) && !('forEachEdge' in Ctor.prototype);\n}\n\n/**\n * <zh/> 将 @antv/layout 布局适配为 G6 布局\n *\n * <en/> Adapt @antv/layout layout to G6 layout\n * @param Ctor - <zh/> 布局类 | <en/> Layout class\n * @param context - <zh/> 运行时上下文 | <en/> Runtime context\n * @returns <zh/> G6 布局类 | <en/> G6 layout class\n */\nexport function legacyLayoutAdapter(\n  Ctor: new (options: Record<string, unknown>) => LegacyAntVLayout,\n  context: RuntimeContext,\n): new (context: RuntimeContext, options?: Record<string, unknown>) => BaseLayout {\n  class AdaptLayout extends BaseLayout implements AdaptiveLayout {\n    public instance: LegacyAntVLayout;\n\n    public id: string;\n\n    constructor(context: RuntimeContext, options?: Record<string, unknown>) {\n      super(context, options);\n      this.instance = new Ctor({});\n      this.id = this.instance.id;\n\n      if ('stop' in this.instance && 'tick' in this.instance) {\n        const instance = this.instance;\n        this.stop = instance.stop.bind(instance);\n        this.tick = (iterations?: number) => {\n          const tickResult = instance.tick?.(iterations);\n          return layoutMapping2GraphData(tickResult);\n        };\n      }\n    }\n\n    public async execute(model: GraphData, options?: STDLayoutOptions): Promise<GraphData> {\n      return layoutMapping2GraphData(\n        await this.instance.execute(\n          this.graphData2LayoutModel(model),\n          this.transformOptions(deepMix({}, this.options, options)),\n        ),\n      );\n    }\n\n    private transformOptions(options: STDLayoutOptions) {\n      if (!('onTick' in options)) return options;\n      const onTick = options.onTick as (data: GraphData) => void;\n      options.onTick = (data: AntVGraphData) => onTick(layoutMapping2GraphData(data));\n      return options;\n    }\n\n    private graphData2LayoutModel(data: GraphData): LegacyGraph {\n      const { nodes = [], edges = [], combos = [] } = data;\n      const nodesToLayout: Node<NodeData>[] = nodes.map((datum) => {\n        const id = idOf(datum);\n        const { data, style, combo, ...rest } = datum;\n\n        const result = {\n          id,\n          data: {\n            // grid 布局会直接读取 data[sortBy]，兼容处理，需要避免用户 data 下使用 data, style 等字段\n            // The grid layout will directly read data[sortBy], compatible processing, need to avoid users using data, style and other fields under data\n            ...data,\n            data,\n            // antv-dagre 会读取 data.parentId\n            // antv-dagre will read data.parentId\n            ...(combo ? { parentId: combo } : {}),\n            style,\n            ...rest,\n          },\n        };\n        // 一些布局会从 data 中读取位置信息\n        if (style?.x) Object.assign(result.data, { x: style.x });\n        if (style?.y) Object.assign(result.data, { y: style.y });\n        if (style?.z) Object.assign(result.data, { z: style.z });\n\n        return result;\n      });\n      const nodesIdMap = new Map(nodesToLayout.map((node) => [node.id, node]));\n\n      const edgesToLayout = edges\n        .filter((edge) => {\n          const { source, target } = edge;\n          return nodesIdMap.has(source) && nodesIdMap.has(target);\n        })\n        .map((edge) => {\n          const { source, target, data, style } = edge;\n          return {\n            id: idOf(edge),\n            source,\n            target,\n            data: { ...data },\n            style: { ...style },\n          } as unknown as Edge<EdgeData>;\n        });\n\n      const combosToLayout: Node<NodeData>[] = combos.map((combo) => {\n        return {\n          id: idOf(combo),\n          data: { _isCombo: true, ...combo.data },\n          style: { ...combo.style },\n        } as unknown as Node<NodeData>;\n      });\n\n      const layoutModel = new Graphlib<NodeData, EdgeData>({\n        nodes: [...nodesToLayout, ...combosToLayout],\n        edges: edgesToLayout,\n      });\n\n      if (context.model.model.hasTreeStructure(COMBO_KEY)) {\n        layoutModel.attachTreeStructure(COMBO_KEY);\n        // 同步层级关系 / Synchronize hierarchical relationships\n        nodesToLayout.forEach((node) => {\n          const parent = context.model.model.getParent(node.id, COMBO_KEY);\n          if (parent && layoutModel.hasNode(parent.id)) {\n            layoutModel.setParent(node.id, parent.id, COMBO_KEY);\n          }\n        });\n      }\n\n      return layoutModel;\n    }\n  }\n\n  return AdaptLayout;\n}\n\n/**\n * <zh/> 调用布局成员方法\n *\n * <en/> Call layout member methods\n * @remarks\n * <zh/> 提供一种通用的调用方式来调用 G6 布局和 \\@antv/layout 布局上的方法\n *\n * <en/> Provide a common way to call methods on G6 layout and \\@antv/layout layout\n * @param layout - <zh/> 布局实例 | <en/> Layout instance\n * @param method - <zh/> 方法名 | <en/> Method name\n * @param args - <zh/> 参数 | <en/> Arguments\n * @returns <zh/> 返回值 | <en/> Return value\n */\nexport function invokeLayoutMethod(layout: BaseLayout, method: string, ...args: unknown[]) {\n  if (method in layout) {\n    return (layout as any)[method](...args);\n  }\n  // invoke AdaptLayout method\n  if ('instance' in layout) {\n    const instance = (layout as any).instance;\n    if (method in instance) return instance[method](...args);\n  }\n  return null;\n}\n\n/**\n * <zh/> 获取布局成员属性\n *\n * <en/> Get layout member properties\n * @param layout - <zh/> 布局实例 | <en/> Layout instance\n * @param name - <zh/> 属性名 | <en/> Property name\n * @returns <zh/> 返回值 | <en/> Return value\n */\nexport function getLayoutProperty(layout: BaseLayout, name: string) {\n  if (name in layout) return (layout as any)[name];\n  if ('instance' in layout) {\n    const instance = (layout as any).instance;\n    if (name in instance) return instance[name];\n  }\n  return null;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/line.ts",
    "content": "import type { Point } from '../types';\nimport { isBetween } from './math';\nimport { cross, subtract } from './vector';\n\nexport type LineSegment = [Point, Point];\n\n/**\n * <zh/> 判断两条线段是否平行\n *\n * <en/> Judge whether two line segments are parallel\n * @param l1 - <zh/> 第一条线段 | <en/> the first line segment\n * @param l2 - <zh/> 第二条线段 | <en/> the second line segment\n * @returns <zh/> 是否平行 | <en/> whether parallel or not\n */\nexport function isLinesParallel(l1: LineSegment, l2: LineSegment): boolean {\n  const [p1, p2] = l1;\n  const [p3, p4] = l2;\n  const v1 = subtract(p1, p2);\n  const v2 = subtract(p3, p4);\n  return cross(v1, v2).every((v) => v === 0);\n}\n\n/**\n * <zh/> 获取两条线段的交点\n *\n * <en/> Get the intersection of two line segments\n * @param l1 - <zh/> 第一条线段 | <en/> the first line segment\n * @param l2 - <zh/> 第二条线段 | <en/> the second line segment\n * @param extended - <zh/> 是否包含延长线上的交点 | <en/> whether to include the intersection on the extension line\n * @returns <zh/> 交点 | <en/> intersection\n */\nexport function getLinesIntersection(l1: LineSegment, l2: LineSegment, extended = false): Point | undefined {\n  if (isLinesParallel(l1, l2)) return undefined;\n\n  const [p1, p2] = l1;\n  const [p3, p4] = l2;\n\n  const t =\n    ((p1[0] - p3[0]) * (p3[1] - p4[1]) - (p1[1] - p3[1]) * (p3[0] - p4[0])) /\n    ((p1[0] - p2[0]) * (p3[1] - p4[1]) - (p1[1] - p2[1]) * (p3[0] - p4[0]));\n\n  const u =\n    p4[0] - p3[0]\n      ? (p1[0] - p3[0] + t * (p2[0] - p1[0])) / (p4[0] - p3[0])\n      : (p1[1] - p3[1] + t * (p2[1] - p1[1])) / (p4[1] - p3[1]);\n\n  if (!extended && (!isBetween(t, 0, 1) || !isBetween(u, 0, 1))) return undefined;\n\n  return [p1[0] + t * (p2[0] - p1[0]), p1[1] + t * (p2[1] - p1[1])];\n}\n"
  },
  {
    "path": "packages/g6/src/utils/math.ts",
    "content": "/**\n * <zh/> 判断值是否在区间内\n *\n * <en/> Judge whether the value is in the interval\n * @param value - <zh/> 值 | <en/> value\n * @param min - <zh/> 最小值 | <en/> minimum value\n * @param max - <zh/> 最大值 | <en/> maximum value\n * @returns <zh/> 是否在区间内 | <en/> whether in the interval\n */\nexport function isBetween(value: number, min: number, max: number): boolean {\n  return value >= min && value <= max;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/node.ts",
    "content": "import type { IconStyleProps } from '../elements/shapes';\nimport type { Size } from '../types';\nimport { parseSize } from './size';\n\n/**\n * <zh/> 如果没有手动指定图标大小，则根据主图形尺寸自动推断\n *\n * <en/> Infer the icon size according to key size if icon size is not manually specified\n * @param size - <zh/> 主图形尺寸 | <en/> Key size\n * @param iconStyle - <zh/> 图标样式 | <en/> Icon style\n * @returns <zh/> 图标样式 | <en/> Icon style\n */\nexport function inferIconStyle(size: Size, iconStyle: IconStyleProps): IconStyleProps {\n  const stdSize = parseSize(size);\n  let style = {};\n\n  if (iconStyle.text && !iconStyle.fontSize) style = { fontSize: Math.min(...stdSize) * 0.5 };\n\n  if (iconStyle.src && (!iconStyle.width || !iconStyle.height))\n    style = { width: stdSize[0] * 0.5, height: stdSize[1] * 0.5 };\n\n  return style;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/padding.ts",
    "content": "import type { Padding, STDPadding } from '../types/padding';\n\n/**\n * <zh/> 解析 padding\n *\n * <en/> parse padding\n * @param padding - <zh/> padding | <en/> padding\n * @returns <zh/> 标准 padding | <en/> standard padding\n */\nexport function parsePadding(padding: Padding = 0): STDPadding {\n  if (Array.isArray(padding)) {\n    const [top = 0, right = top, bottom = top, left = right] = padding;\n    return [top, right, bottom, left];\n  }\n  return [padding, padding, padding, padding];\n}\n\n/**\n * <zh/> 获取在垂直方向上的 padding\n *\n * <en/> get vertical padding\n * @param padding - <zh/> padding | <en/> padding\n * @returns <zh/> 垂直方向上的 padding | <en/> vertical padding\n */\nexport function getVerticalPadding(padding: Padding = 0): number {\n  const parsedPadding = parsePadding(padding);\n  return parsedPadding[0] + parsedPadding[2];\n}\n\n/**\n * <zh/> 获取在水平方向上的 padding\n *\n * <en/> get horizontal padding\n * @param padding - <zh/> padding | <en/> padding\n * @returns <zh/> 水平方向上的 padding | <en/> horizontal padding\n */\nexport function getHorizontalPadding(padding: Padding = 0): number {\n  const parsedPadding = parsePadding(padding);\n  return parsedPadding[1] + parsedPadding[3];\n}\n"
  },
  {
    "path": "packages/g6/src/utils/palette.ts",
    "content": "import { groupBy } from '@antv/util';\nimport type { CategoricalPalette } from '../palettes/types';\nimport { getExtension } from '../registry/get';\nimport type { PaletteOptions, STDPaletteOptions } from '../spec/element/palette';\nimport type { ID } from '../types';\nimport type { ElementData, ElementDatum } from '../types/data';\nimport { idOf } from './id';\nimport { format } from './print';\n\n/**\n * <zh/> 解析色板配置\n *\n * <en/> Parse palette options\n * @param palette - <zh/> 色板配置 | <en/> PaletteOptions options\n * @returns <zh/> 标准色板配置 | <en/> Standard palette options\n */\nexport function parsePalette(palette?: PaletteOptions): STDPaletteOptions | undefined {\n  if (!palette) return undefined;\n\n  if (\n    // 色板名 palette name\n    typeof palette === 'string' ||\n    // 插值函数 interpolate function\n    typeof palette === 'function' ||\n    // 颜色数组 color array\n    Array.isArray(palette)\n  ) {\n    // 默认为离散色板\n    // Default to discrete palette, default group field is id\n    return {\n      type: 'group',\n      field: (d: any) => d.id,\n      color: palette,\n      invert: false,\n    };\n  }\n  return palette;\n}\n\n/**\n * <zh/> 根据色板分配颜色\n *\n * <en/> Assign colors according to the palette\n * @param data - <zh/> 元素数据 | <en/> Element data\n * @param palette - <zh/> 色板配置 | <en/> PaletteOptions options\n * @returns <zh/> 元素颜色 | <en/> Element color\n * @remarks\n * <zh/> 返回值结果是一个以元素 id 为 key，颜色值为 value 的对象\n *\n * <en/> The return value is an object with element id as key and color value as value\n */\nexport function assignColorByPalette(data: ElementData, palette?: STDPaletteOptions) {\n  if (!palette) return {};\n\n  const { type, color: colorPalette, field, invert } = palette;\n\n  const assignColor = (args: [ID, number][]): Record<ID, string> => {\n    const palette = typeof colorPalette === 'string' ? getExtension('palette', colorPalette) : colorPalette;\n\n    if (typeof palette === 'function') {\n      // assign by continuous\n      const result: Record<ID, string> = {};\n      args.forEach(([id, value]) => {\n        result[id] = palette(invert ? 1 - value : value);\n      });\n      return result;\n    } else if (Array.isArray(palette)) {\n      // assign by discrete\n      const colors = invert ? [...palette].reverse() : palette;\n      const result: Record<ID, string> = {};\n      args.forEach(([id, index]) => {\n        result[id] = colors[index % palette.length];\n      });\n      return result;\n    }\n    return {};\n  };\n\n  const parseField = (field: STDPaletteOptions['field'], datum: ElementDatum) =>\n    typeof field === 'string' ? datum.data?.[field] : field?.(datum);\n\n  if (type === 'group') {\n    const groupData = groupBy<ElementDatum>(data, (datum) => {\n      if (!field) return 'default';\n      const key = parseField(field, datum);\n      return key ? String(key) : 'default';\n    });\n\n    const groupKeys = Object.keys(groupData);\n    const assignResult = assignColor(groupKeys.map((key, index) => [key, index]));\n\n    const result: Record<ID, string> = {};\n    Object.entries(groupData).forEach(([groupKey, groupData]) => {\n      groupData.forEach((datum) => {\n        result[idOf(datum)] = assignResult[groupKey];\n      });\n    });\n    return result;\n  } else if (type === 'value') {\n    const [min, max] = data.reduce(\n      ([min, max], datum) => {\n        const value = parseField(field, datum);\n        if (typeof value !== 'number') throw new Error(format(`Palette field ${field} is not a number`));\n        return [Math.min(min, value), Math.max(max, value)];\n      },\n      [Infinity, -Infinity],\n    );\n    const range = max - min;\n\n    return assignColor(\n      data.map((datum) => [datum.id, ((parseField(field, datum) as number) - min) / range]) as [ID, number][],\n    );\n  }\n}\n\n/**\n * <zh/> 获取离散色板配色\n *\n * <en/> Get discrete palette colors\n * @param colorPalette - <zh/> 色板名或着颜色数组 | <en/> Palette name or color array\n * @returns <zh/> 色板上具体颜色 | <en/> Specific color on the palette\n */\nexport function getPaletteColors(colorPalette?: string | CategoricalPalette): CategoricalPalette | undefined {\n  const palette = typeof colorPalette === 'string' ? getExtension('palette', colorPalette) : colorPalette;\n  if (typeof palette === 'function') return undefined;\n  return palette;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/path.ts",
    "content": "import type { PathArray, PathCommand } from '@antv/util';\nimport type { Point } from '../types';\n\n/**\n * <zh/> points 转化为 path 路径\n *\n * <en/> points transform path.\n * @param points Point[]\n * @param isClose boolean\n * @returns path string[][]\n */\nexport function pointsToPath(points: Point[], isClose = true): PathArray {\n  const path = [];\n\n  points.forEach((point, index) => {\n    path.push([index === 0 ? 'M' : 'L', ...point]);\n  });\n\n  if (isClose) {\n    path.push(['Z']);\n  }\n  return path as PathArray;\n}\n\nconst PATH_COMMANDS: Record<PathCommand, string[]> = {\n  M: ['x', 'y'],\n  m: ['dx', 'dy'],\n  H: ['x'],\n  h: ['dx'],\n  V: ['y'],\n  v: ['dy'],\n  L: ['x', 'y'],\n  l: ['dx', 'dy'],\n  Z: [],\n  z: [],\n  C: ['x1', 'y1', 'x2', 'y2', 'x', 'y'],\n  c: ['dx1', 'dy1', 'dx2', 'dy2', 'dx', 'dy'],\n  S: ['x2', 'y2', 'x', 'y'],\n  s: ['dx2', 'dy2', 'dx', 'dy'],\n  Q: ['x1', 'y1', 'x', 'y'],\n  q: ['dx1', 'dy1', 'dx', 'dy'],\n  T: ['x', 'y'],\n  t: ['dx', 'dy'],\n  A: ['rx', 'ry', 'rotation', 'large-arc', 'sweep', 'x', 'y'],\n  a: ['rx', 'ry', 'rotation', 'large-arc', 'sweep', 'dx', 'dy'],\n};\n\n/**\n * <zh/> 将路径字符串转换为路径段数组\n *\n * <en/> Convert a path string to an array of path segments.\n * @param path - <zh/> 路径字符串 | <en/> path string\n * @returns <zh/> 路径段数组 | <en/> path segment array\n */\nexport function parsePath(path: string): PathArray {\n  const items = path\n    .replace(/[\\n\\r]/g, '')\n    .replace(/-/g, ' -')\n    .replace(/(\\d*\\.)(\\d+)(?=\\.)/g, '$1$2 ')\n    .trim()\n    .split(/\\s*,|\\s+/);\n  const segments = [];\n  let currentCommand = '' as PathCommand;\n  let currentElement: Record<string, any> = {};\n  while (items.length > 0) {\n    let it = items.shift()!;\n    if (it in PATH_COMMANDS) {\n      currentCommand = it as PathCommand;\n    } else {\n      items.unshift(it);\n    }\n    currentElement = { type: currentCommand };\n    PATH_COMMANDS[currentCommand].forEach((prop) => {\n      it = items.shift()!; // TODO sanity check\n      currentElement[prop] = it;\n    });\n    if (currentCommand === 'M') {\n      currentCommand = 'L';\n    } else if (currentCommand === 'm') {\n      currentCommand = 'l';\n    }\n    const [type, ...values] = Object.values(currentElement);\n    segments.push([type, ...values.map(Number)]);\n  }\n  return segments as unknown as PathArray;\n}\n\n/**\n * <zh/> 将路径转换为点数组\n *\n * <en/> Convert path to points array\n * @param path - <zh/> 路径数组 <en/> path array\n * @returns\n */\nexport function pathToPoints(path: string | PathArray): Point[] {\n  const points: Point[] = [];\n  const segments = typeof path === 'string' ? parsePath(path) : path;\n\n  segments.forEach((seg) => {\n    const command = seg[0];\n    if (command === 'Z') {\n      points.push(points[0]);\n      return;\n    }\n    if (command !== 'A') {\n      for (let i = 1; i < seg.length; i = i + 2) {\n        points.push([seg[i] as number, seg[i + 1] as number, 0]);\n      }\n    } else {\n      const length = seg.length;\n      points.push([seg[length - 2] as number, seg[length - 1] as number, 0]);\n    }\n  });\n  return points;\n}\n\n/**\n * <zh/> 生成平滑闭合曲线\n *\n * <en/> Generate smooth closed curves\n * @param points - <zh/> 点集 | <en/> points\n * @returns <zh/> 平滑闭合曲线 | <en/> smooth closed curves\n */\nexport const getClosedSpline = (points: Point[]): PathArray => {\n  if (points.length < 2)\n    return [\n      ['M', 0, 0],\n      ['L', 0, 0],\n    ];\n  const first = points[0];\n  const second = points[1];\n  const last = points[points.length - 1];\n  const lastSecond = points[points.length - 2];\n\n  points.unshift(lastSecond, last);\n  points.push(first, second);\n\n  const closedPath = [['M', last[0], last[1]]];\n  for (let i = 1; i < points.length - 2; i += 1) {\n    const [x0, y0] = points[i - 1];\n    const [x1, y1] = points[i];\n    const [x2, y2] = points[i + 1];\n    const [x3, y3] = i !== points.length - 2 ? points[i + 2] : [x2, y2];\n\n    const cp1x = x1 + (x2 - x0) / 6;\n    const cp1y = y1 + (y2 - y0) / 6;\n    const cp2x = x2 - (x3 - x1) / 6;\n    const cp2y = y2 - (y3 - y1) / 6;\n    closedPath.push(['C', cp1x, cp1y, cp2x, cp2y, x2, y2]);\n  }\n\n  return closedPath as PathArray;\n};\n"
  },
  {
    "path": "packages/g6/src/utils/pinch.ts",
    "content": "import EventEmitter from '@antv/event-emitter';\nimport { CommonEvent } from '../constants';\nimport { IPointerEvent } from '../types';\n\n/**\n * <zh/> 表示指针位置的点坐标\n *\n * <en/> Represents the coordinates of a pointer position\n */\nexport interface PointerPoint {\n  x: number;\n  y: number;\n  pointerId: number;\n}\n\n/**\n * <zh/> 捏合事件参数\n *\n * <en/> Pinch event parameters\n * @remarks\n * <zh/> 包含与捏合手势相关的参数，当前支持缩放比例，未来可扩展中心点坐标、旋转角度等参数\n *\n * <en/> Contains parameters related to pinch gestures, currently supports scale factor,\n * can be extended with center coordinates, rotation angle etc. in the future\n */\nexport interface PinchEventOptions {\n  /**\n   * <zh/> 缩放比例因子，>1 表示放大，<1 表示缩小\n   *\n   * <en/> Scaling factor, >1 indicates zoom in, <1 indicates zoom out\n   */\n  scale: number;\n}\n\n/**\n * <zh/> 捏合手势阶段类型\n * <en/> Pinch gesture phase type\n * @remarks\n * <zh/> 包含三个手势阶段：\n * - start: 手势开始\n * - move: 手势移动中\n * - end: 手势结束\n *\n * <en/> Contains three gesture phases:\n * - pinchstart: Gesture started\n * - pinchmove: Gesture in progress\n * - pinchend: Gesture ended\n */\nexport type PinchEvent = 'pinchstart' | 'pinchmove' | 'pinchend';\n\n/**\n * <zh/> 捏合手势回调函数类型\n *\n * <en/> Pinch gesture callback function type\n * @param event - <zh/> 原始指针事件对象 | <en/> Original pointer event object\n * @param options - <zh/> 捏合事件参数对象 | <en/> Pinch event parameters object\n */\nexport type PinchCallback = (event: IPointerEvent, options: PinchEventOptions) => void;\n\n/**\n * <zh/> 捏合手势处理器\n *\n * <en/> Pinch gesture handler\n * @remarks\n * <zh/> 处理双指触摸事件，计算缩放比例并触发回调。通过跟踪两个触摸点的位置变化，计算两点间距离变化率来确定缩放比例。\n *\n * <en/> Handles two-finger touch events, calculates zoom ratio and triggers callbacks. Tracks position changes of two touch points to determine zoom ratio based on distance variation.\n */\nexport class PinchHandler {\n  /**\n   * <zh/> 是否处于 Pinch 阶段\n   *\n   * <en/> Whether it is in the Pinch stage\n   */\n  public static isPinching: boolean = false;\n\n  /**\n   * <zh/> 当前跟踪的触摸点集合\n   *\n   * <en/> Currently tracked touch points collection\n   */\n  private pointerByTouch: PointerPoint[] = [];\n\n  /**\n   * <zh/> 初始两点间距离\n   *\n   * <en/> Initial distance between two points\n   */\n  private initialDistance: number | null = null;\n\n  private emitter: EventEmitter;\n  private static instance: PinchHandler | null = null;\n  private static callbacks: {\n    pinchstart: PinchCallback[];\n    pinchmove: PinchCallback[];\n    pinchend: PinchCallback[];\n  } = { pinchstart: [], pinchmove: [], pinchend: [] };\n\n  constructor(\n    emitter: EventEmitter,\n    private phase: PinchEvent,\n    callback: PinchCallback,\n  ) {\n    this.emitter = emitter;\n    if (PinchHandler.instance) {\n      PinchHandler.callbacks[this.phase].push(callback);\n      return PinchHandler.instance;\n    }\n    this.onPointerDown = this.onPointerDown.bind(this);\n    this.onPointerMove = this.onPointerMove.bind(this);\n    this.onPointerUp = this.onPointerUp.bind(this);\n    this.bindEvents();\n    PinchHandler.instance = this;\n    PinchHandler.callbacks[this.phase].push(callback);\n  }\n\n  private bindEvents() {\n    const { emitter } = this;\n    emitter.on(CommonEvent.POINTER_DOWN, this.onPointerDown);\n    emitter.on(CommonEvent.POINTER_MOVE, this.onPointerMove);\n    emitter.on(CommonEvent.POINTER_UP, this.onPointerUp);\n  }\n\n  /**\n   * <zh/> 更新指定指针的位置\n   *\n   * <en/> Update position of specified pointer\n   * @param pointerId - <zh/> 指针唯一标识符 | <en/> Pointer unique identifier<sup>1</sup>\n   * @param x - <zh/> 新的X坐标 | <en/> New X coordinate\n   * @param y - <zh/> 新的Y坐标 | <en/> New Y coordinate\n   */\n  private updatePointerPosition(pointerId: number, x: number, y: number) {\n    const index = this.pointerByTouch.findIndex((p) => p.pointerId === pointerId);\n    if (index >= 0) {\n      this.pointerByTouch[index] = { x, y, pointerId };\n    }\n  }\n\n  /**\n   * <zh/> 处理指针按下事件\n   *\n   * <en/> Handle pointer down event\n   * @param event - <zh/> 指针事件对象 | <en/> Pointer event object\n   * @remarks\n   * <zh/> 当检测到两个触摸点时记录初始距离\n   *\n   * <en/> Record initial distance when detecting two touch points\n   */\n  onPointerDown(event: IPointerEvent) {\n    const { x, y } = event.client || {};\n    if (x === undefined || y === undefined) return;\n    this.pointerByTouch.push({ x, y, pointerId: event.pointerId });\n\n    if (event.pointerType === 'touch' && this.pointerByTouch.length === 2) {\n      PinchHandler.isPinching = true;\n      const dx = this.pointerByTouch[0].x - this.pointerByTouch[1].x;\n      const dy = this.pointerByTouch[0].y - this.pointerByTouch[1].y;\n      this.initialDistance = Math.sqrt(dx * dx + dy * dy);\n      PinchHandler.callbacks.pinchstart.forEach((cb) => cb(event, { scale: 0 }));\n    }\n  }\n\n  /**\n   * <zh/> 处理指针移动事件\n   *\n   * <en/> Handle pointer move event\n   * @param event - <zh/> 指针事件对象 | <en/> Pointer event object\n   * @remarks\n   * <zh/> 当存在两个有效触摸点时计算缩放比例\n   *\n   * <en/> Calculate zoom ratio when two valid touch points exist\n   */\n  onPointerMove(event: IPointerEvent) {\n    if (this.pointerByTouch.length !== 2 || this.initialDistance === null) return;\n    const { x, y } = event.client || {};\n    if (x === undefined || y === undefined) return;\n    this.updatePointerPosition(event.pointerId, x, y);\n    const dx = this.pointerByTouch[0].x - this.pointerByTouch[1].x;\n    const dy = this.pointerByTouch[0].y - this.pointerByTouch[1].y;\n    const currentDistance = Math.sqrt(dx * dx + dy * dy);\n    const ratio = currentDistance / this.initialDistance;\n\n    PinchHandler.callbacks.pinchmove.forEach((cb) => cb(event, { scale: (ratio - 1) * 5 }));\n  }\n\n  /**\n   * <zh/> 处理指针抬起事件\n   *\n   * <en/> Handle pointer up event\n   * @param event\n   * @remarks\n   * <zh/> 重置触摸状态和初始距离\n   *\n   * <en/> Reset touch state and initial distance\n   */\n  onPointerUp(event: IPointerEvent) {\n    PinchHandler.callbacks.pinchend.forEach((cb) => cb(event, { scale: 0 }));\n    PinchHandler.isPinching = false;\n    this.initialDistance = null;\n    this.pointerByTouch = [];\n    PinchHandler.instance?.tryDestroy();\n  }\n\n  /**\n   * <zh/> 销毁捏合手势相关监听\n   *\n   * <en/> Destroy pinch gesture listeners\n   * @remarks\n   * <zh/> 移除指针按下、移动、抬起事件的监听\n   *\n   * <en/> Remove listeners for pointer down, move, and up events\n   */\n  public destroy() {\n    this.emitter.off(CommonEvent.POINTER_DOWN, this.onPointerDown);\n    this.emitter.off(CommonEvent.POINTER_MOVE, this.onPointerMove);\n    this.emitter.off(CommonEvent.POINTER_UP, this.onPointerUp);\n    PinchHandler.instance = null;\n  }\n\n  /**\n   * <zh/> 解绑指定阶段的手势回调\n   * <en/> Unregister gesture callback for specific phase\n   * @param phase - <zh/> 手势阶段：开始(pinchstart)/移动(pinchmove)/结束(pinchend) | <en/> Gesture phase: start/move/end\n   * @param callback - <zh/> 要解绑的回调函数 | <en/> Callback function to unregister\n   * @remarks\n   * <zh/> 从指定阶段的回调列表中移除特定回调，当所有回调都解绑后自动销毁事件监听\n   * <en/> Remove specific callback from the phase's callback list, auto-destroy event listeners when all callbacks are unregistered\n   */\n  public off(phase: PinchEvent, callback: PinchCallback) {\n    const index = PinchHandler.callbacks[phase].indexOf(callback);\n    if (index > -1) PinchHandler.callbacks[phase].splice(index, 1);\n    this.tryDestroy();\n  }\n\n  /**\n   * <zh/> 尝试销毁手势处理器\n   * <en/> Attempt to destroy the gesture handler\n   * @remarks\n   * <zh/> 当所有阶段（开始/移动/结束）的回调列表都为空时，执行实际销毁操作\n   * <en/> Perform actual destruction when all phase (pinchstart/pinchmove/pinchend) callback lists are empty\n   * <zh/> 自动解除事件监听并重置单例实例\n   * <en/> Automatically remove event listeners and reset singleton instance\n   */\n  private tryDestroy() {\n    if (Object.values(PinchHandler.callbacks).every((arr) => arr.length === 0)) {\n      this.destroy();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/utils/placement.ts",
    "content": "import type { Placement, RelativePlacement } from '../types';\nimport { isBetween } from './math';\n\n/**\n * <zh/> 解析位置\n *\n * <en/> Parse position\n * @param placement - <zh/> 位置 | <en/> placement\n * @returns <zh/> 相对位置 | <en/> relative placement\n */\nexport function parsePlacement(placement: Placement): RelativePlacement {\n  if (Array.isArray(placement)) {\n    return isBetween(placement[0], 0, 1) && isBetween(placement[1], 0, 1) ? placement : [0.5, 0.5];\n  }\n  const direction = placement.split('-');\n  const x = direction.includes('left') ? 0 : direction.includes('right') ? 1 : 0.5;\n  const y = direction.includes('top') ? 0 : direction.includes('bottom') ? 1 : 0.5;\n  return [x, y];\n}\n"
  },
  {
    "path": "packages/g6/src/utils/point.ts",
    "content": "import type { AABB } from '@antv/g';\nimport { isEqual } from '@antv/util';\nimport type { Point, PointObject } from '../types';\nimport { getBBoxHeight, getBBoxWidth } from './bbox';\nimport type { LineSegment } from './line';\nimport { getLinesIntersection, isLinesParallel } from './line';\nimport { getXYByPlacement } from './position';\nimport { add, distance, divide, normalize, subtract, toVector2 } from './vector';\n\n/**\n * <zh/> 将对象坐标转换为数组坐标\n * <en/> Convert object coordinates to array coordinates\n * @param point - <zh/> 对象坐标 | <en/> object coordinates\n * @returns <zh/> 数组坐标 | <en/> array coordinates\n */\nexport function parsePoint(point: PointObject): Point {\n  return [point.x, point.y, point.z ?? 0];\n}\n\n/**\n * <zh/> 将数组坐标转换为对象坐标\n *\n * <en/> Convert array coordinates to object coordinates\n * @param point - <zh/> 数组坐标 | <en/> array coordinates\n * @returns <zh/> 对象坐标 | <en/> object coordinates\n */\nexport function toPointObject(point: Point): PointObject {\n  return { x: point[0], y: point[1], z: point[2] ?? 0 };\n}\n\n/**\n * <zh/> 根据 X 轴坐标排序\n * <en/> Sort by X-axis coordinates\n * @param points - <zh/> 点集 | <en/> point set\n * @returns <zh/> 排序后的点集 | <en/> sorted point set\n */\nexport function sortByX(points: Point[]): Point[] {\n  return points.sort((a, b) => a[0] - b[0] || a[1] - b[1]);\n}\n\n/**\n * <zh/> 点集去重\n *\n * <en/> Deduplicate point set\n * @param points - <zh/> 点集 | <en/> pointset\n * @returns <zh/> 去重后的点集 | <en/> deduplicated pointset\n */\nexport function deduplicate(points: Point[]): Point[] {\n  const set = new Set<string>();\n  return points.filter((p) => {\n    const key = p.join(',');\n    if (set.has(key)) return false;\n    set.add(key);\n    return true;\n  });\n}\n\n/**\n * <zh/> 对点格式化，精确到 `digits` 位的数字\n *\n * <en/> Round the point to the given precision\n * @param point - <zh/> 要舍入的点 | <en/> the point to round\n * @param digits - <zh/> 小数点后的位数 | <en/> the number of digits after the decimal point\n * @returns <zh/> 舍入后的点 | <en/> the rounded point\n */\nexport function round(point: Point, digits = 0): Point {\n  return point.map((p) => parseFloat(p.toFixed(digits))) as Point;\n}\n\n/**\n * <zh/> 移动点，将点朝向参考点移动一定的距离\n *\n * <en/> Move `p` point along the line starting from `ref` to this point by a certain `distance`\n * @param p - <zh/> 要移动的点 | <en/> the point to move\n * @param ref - <zh/> 参考点 | <en/> the reference point\n * @param distance - <zh/> 移动的距离 | <en/> the distance to move\n * @param reverse\n * @returns <zh/> 移动后的点 | <en/> the moved point\n */\nexport function moveTo(p: Point, ref: Point, distance: number, reverse = false): Point {\n  if (isEqual(p, ref)) return p;\n  const direction = reverse ? subtract(p, ref) : subtract(ref, p);\n  const normalizedDirection = normalize(direction);\n  const moveVector: Point = [normalizedDirection[0] * distance, normalizedDirection[1] * distance];\n  return add(toVector2(p), moveVector);\n}\n\n/**\n * <zh/> 判断两个点是否在同一水平线上\n *\n * <en/> whether two points are on the same horizontal line\n * @param p1 - <zh/> 第一个点 | <en/> the first point\n * @param p2 - <zh/> 第二个点 | <en/> the second point\n * @returns <zh/> 返回两个点是否在同一水平线上 | <en/> is horizontal or not\n */\nexport function isHorizontal(p1: Point, p2: Point): boolean {\n  return p1[1] === p2[1];\n}\n\n/**\n * <zh/> 判断两个点是否在同一垂直线上\n *\n * <en/> whether two points are on the same vertical line\n * @param p1 - <zh/> 第一个点 | <en/> the first point\n * @param p2 - <zh/> 第二个点 | <en/> the second point\n * @returns <zh/> 返回两个点是否在同一垂直线上 | <en/> is vertical or not\n */\nexport function isVertical(p1: Point, p2: Point): boolean {\n  return p1[0] === p2[0];\n}\n\n/**\n * <zh/> 判断两个点是否正交，即是否在同一水平线或垂直线上\n *\n * <en/> Judges whether two points are orthogonal, that is, whether they are on the same horizontal or vertical line\n * @param p1 - <zh/> 第一个点 | <en/> the first point\n * @param p2 - <zh/> 第二个点 | <en/> the second point\n * @returns <zh/> 是否正交 | <en/> whether orthogonal or not\n */\nexport function isOrthogonal(p1: Point, p2: Point): boolean {\n  return isHorizontal(p1, p2) || isVertical(p1, p2);\n}\n\n/**\n * <zh/> 判断是否三点共线\n *\n * <en/> Judge whether three points are collinear\n * @param p1 - <zh/> 第一个点 | <en/> the first point\n * @param p2 - <zh/> 第二个点 | <en/> the second point\n * @param p3 - <zh/> 第三个点 | <en/> the third point\n * @returns <zh/> 是否三点共线 | <en/> whether three points are collinear\n */\nexport function isCollinear(p1: Point, p2: Point, p3: Point): boolean {\n  return isLinesParallel([p1, p2], [p2, p3]);\n}\n\n/**\n * <zh/> 计算一个点相对于另一个点的中心对称点\n *\n * <en/> Calculate the center symmetric point of a point relative to another point\n * @param p - <zh/> 要计算的点 | <en/> the point to calculate\n * @param center - <zh/> 中心点 | <en/> the center point\n * @returns <zh/> 中心对称点 | <en/> the center symmetric point\n */\nexport function getSymmetricPoint(p: Point, center: Point): Point {\n  return [2 * center[0] - p[0], 2 * center[1] - p[1]];\n}\n\n/**\n * <zh/> 获取从多边形中心到给定点的连线与多边形边缘的交点\n *\n * <en/> Gets the intersection point between the line from the center of a polygon to a given point and the polygon's edge\n * @param p - <zh/> 从多边形中心到多边形边缘的连线的外部点 | <en/> The point outside the polygon from which the line to the polygon's center is drawn\n * @param center - <zh/> 多边形中心 | <en/> the center of the polygon\n * @param points - <zh/> 多边形顶点 | <en/> the vertices of the polygon\n * @param isRelativePos - <zh/> 顶点坐标是否相对中心点 | <en/> whether the vertex coordinates are relative to the center point\n * @param useExtendedLine - <zh/> 是否使用延长线 | <en/> whether to use the extended line\n * @returns <zh/> 交点与相交线段 | <en/> intersection and intersecting line segment\n */\nexport function getPolygonIntersectPoint(\n  p: Point,\n  center: Point,\n  points: Point[],\n  isRelativePos = true,\n  useExtendedLine = false,\n): { point: Point; line?: LineSegment } {\n  for (let i = 0; i < points.length; i++) {\n    let start = points[i];\n    let end = points[(i + 1) % points.length];\n\n    if (isRelativePos) {\n      start = add(center, start);\n      end = add(center, end);\n    }\n\n    const refP = useExtendedLine ? getSymmetricPoint(p, center) : p;\n    const intersect = getLinesIntersection([center, refP], [start, end]);\n    if (intersect) {\n      return {\n        point: intersect,\n        line: [start, end],\n      };\n    }\n  }\n  return {\n    point: center,\n    line: undefined,\n  };\n}\n\n/**\n * <zh/> 判断点是否在多边形内部\n *\n * <en/> Whether point is inside the polygon (ray algo)\n * @param point - <zh/> 点 | <en/> point\n * @param points - <zh/> 多边形顶点 | <en/> polygon vertices\n * @param start - <zh/> 起始索引 | <en/> start index\n * @param end - <zh/> 结束索引 | <en/> end index\n * @returns <zh/> 是否在多边形内部 | <en/> whether inside the polygon\n */\nexport function isPointInPolygon(point: Point, points: Point[], start?: number, end?: number): boolean {\n  const x = point[0];\n  const y = point[1];\n  let inside = false;\n  if (start === undefined) start = 0;\n  if (end === undefined) end = points.length;\n  const len = end - start;\n  for (let i = 0, j = len - 1; i < len; j = i++) {\n    const xi = points[i + start][0];\n    const yi = points[i + start][1];\n    const xj = points[j + start][0];\n    const yj = points[j + start][1];\n    const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;\n    if (intersect) inside = !inside;\n  }\n  return inside;\n}\n\n/**\n * <zh/> 获取给定点到矩形中心的连线与矩形边缘的交点\n *\n * <en/> Gets the intersection point between the line from the center of a rectangle to a given point and the rectangle's edge\n * @param p - <zh/> 从矩形中心到矩形边缘的连线的外部点 | <en/> The point outside the rectangle from which the line to the rectangle's center is drawn\n * @param bbox - <zh/> 矩形包围盒 | <en/> the bounding box of the rectangle\n * @param useExtendedLine - <zh/> 是否使用延长线 | <en/> whether to use the extended line\n * @returns <zh/> 交点 | <en/> intersection\n */\nexport function getRectIntersectPoint(p: Point, bbox: AABB, useExtendedLine = false): Point {\n  const center = getXYByPlacement(bbox, 'center');\n  const corners = [\n    getXYByPlacement(bbox, 'left-top'),\n    getXYByPlacement(bbox, 'right-top'),\n    getXYByPlacement(bbox, 'right-bottom'),\n    getXYByPlacement(bbox, 'left-bottom'),\n  ];\n  return getPolygonIntersectPoint(p, center, corners, false, useExtendedLine).point;\n}\n\n/**\n * <zh/> 获取给定点到椭圆中心的连线与椭圆边缘的交点\n *\n * <en/> Gets the intersection point between the line from the center of an ellipse to a given point and the ellipse's edge\n * @param p - <zh/> 从椭圆中心到椭圆边缘的连线的外部点 | <en/> The point outside the ellipse from which the line to the ellipse's center is drawn\n * The point outside the ellipse from which the line to the ellipse's center is drawn.\n * @param bbox - <zh/> 椭圆包围盒 | <en/> the bounding box of the ellipse\n * @param useExtendedLine - <zh/> 是否使用延长线 | <en/> whether to use the extended line\n * @returns <zh/> 交点 | <en/> intersection\n */\nexport function getEllipseIntersectPoint(p: Point, bbox: AABB, useExtendedLine = false): Point {\n  const center = bbox.center;\n  const refP = useExtendedLine ? getSymmetricPoint(p, center) : p;\n  const vec = subtract(refP, bbox.center);\n  const angle = Math.atan2(vec[1], vec[0]);\n  if (isNaN(angle)) return center;\n\n  const rx = getBBoxWidth(bbox) / 2;\n  const ry = getBBoxHeight(bbox) / 2;\n  const intersectX = center[0] + rx * Math.cos(angle);\n  const intersectY = center[1] + ry * Math.sin(angle);\n\n  return [intersectX, intersectY];\n}\n\n/**\n * <zh/> 从两组点中找到距离最近的两个点\n * @param group1 - <zh/> 第一组点 | <en/> the first group of points\n * @param group2 - <zh/> 第二组点 | <en/> the second group of points\n * @returns <zh/> 距离最近的两个点 | <en/> the nearest two points\n */\nexport function findNearestPoints(group1: Point[], group2: Point[]): [Point, Point] {\n  let minDistance = Infinity;\n  let nearestPoints: [Point, Point] = [group1[0], group2[0]];\n  group1.forEach((p1) => {\n    group2.forEach((p2) => {\n      const dist = distance(p1, p2);\n      if (dist < minDistance) {\n        minDistance = dist;\n        nearestPoints = [p1, p2];\n      }\n    });\n  });\n  return nearestPoints;\n}\n\n/**\n * <zh/> 从一组线段中找到距离给定点最近的线段\n *\n * <en/> Find the line segment closest to the given point from a group of line segments\n * @param point - <zh/> 给定点 | <en/> the given point\n * @param lines - <zh/> 一组线段 | <en/> a group of line segments\n * @returns <zh/> 距离最近的线段 | <en/> the nearest line segment\n */\nexport function findNearestLine(point: Point, lines: LineSegment[]) {\n  let minDistance = Infinity;\n  let nearestLine: [Point, Point] = [\n    [0, 0],\n    [0, 0],\n  ];\n  lines.forEach((line) => {\n    const distance = getDistanceToLine(point, line);\n    if (distance < minDistance) {\n      minDistance = distance;\n      nearestLine = line;\n    }\n  });\n  return nearestLine;\n}\n\n/**\n * <zh/> 获取点到线段的距离\n *\n * <en/> Get the distance from a point to a line segment\n * @param point - <zh/> 点 | <en/> the point\n * @param line - <zh/> 线段 | <en/> the line segment\n * @returns <zh/> 点到线段的距离 | <en/> the distance from the point to the line segment\n */\nexport function getDistanceToLine(point: Point, line: LineSegment) {\n  const nearestPoint = findNearestPointOnLine(point, line);\n  return distance(point, nearestPoint);\n}\n\n/**\n * <zh/> 获取线段上距离给定点最近的点\n *\n * <en/> Get the point on the line segment closest to the given point\n * @param point - <zh/> 给定点 | <en/> the given point\n * @param line - <zh/> 线段 | <en/> the line segment\n * @returns <zh/> 线段上距离给定点最近的点 | <en/> the point on the line segment closest to the given point\n */\nexport function findNearestPointOnLine(point: Point, line: LineSegment): Point {\n  const [x1, y1] = line[0];\n  const [x2, y2] = line[1];\n  const [x3, y3] = point;\n\n  const px = x2 - x1;\n  const py = y2 - y1;\n\n  // 若线段实际上是一个点 | If the line segment is actually a point\n  if (px === 0 && py === 0) {\n    return [x1, y1];\n  }\n\n  let u = ((x3 - x1) * px + (y3 - y1) * py) / (px * px + py * py);\n\n  if (u > 1) {\n    u = 1;\n  } else if (u < 0) {\n    u = 0;\n  }\n\n  const x = x1 + u * px;\n  const y = y1 + u * py;\n\n  return [x, y];\n}\n\n/**\n * <zh/> 获取点集的中心点\n *\n * <en/> Get the center point of a set of points\n * @param points - <zh/> 点集 | <en/> point set\n * @returns <zh/> 中心点 | <en/> center point\n */\nexport function centerOf(points: Point[]): Point {\n  const totalPosition = points.reduce((acc, p) => add(acc, p), [0, 0]);\n  return divide(totalPosition, points.length);\n}\n\n/**\n * <zh/> 按顺时针或逆时针方向对点集排序\n *\n * <en/> Sort the point set in a clockwise or counterclockwise direction\n * @param points - <zh/> 点集 | <en/> point set\n * @param clockwise - <zh/> 是否顺时针 | <en/> whether clockwise\n * @returns <zh/> 排序后的点集 | <en/> sorted point set\n */\nexport function sortByClockwise(points: Point[], clockwise = true): Point[] {\n  const center = centerOf(points);\n  return points.sort(([x1, y1], [x2, y2]) => {\n    const angle1 = Math.atan2(y1 - center[1], x1 - center[0]);\n    const angle2 = Math.atan2(y2 - center[1], x2 - center[0]);\n    return clockwise ? angle2 - angle1 : angle1 - angle2;\n  });\n}\n\n/**\n * <zh/> 给定的起点和终点，返回一个由这两个点和它们的对角点组成的数组\n * @param start - <zh/> 起点 | <en/> start point\n * @param end - <zh/> 终点 | <en/> end point\n * @returns <zh/> 由这两个点和它们的对角点组成的数组 | <en/> an array consisting of these two points and their diagonal points\n */\nexport function getBoundingPoints(start: Point, end: Point): Point[] {\n  return [start, [start[0], end[1]], end, [end[0], start[1]]];\n}\n"
  },
  {
    "path": "packages/g6/src/utils/polygon.ts",
    "content": "import type { AABB, TextStyleProps } from '@antv/g';\nimport type { PathArray } from '@antv/util';\nimport { isEqual } from '@antv/util';\nimport type { CardinalPlacement, Point } from '../types';\nimport { pathToPoints } from './path';\nimport { findNearestLine, findNearestPointOnLine } from './point';\nimport { getXYByPlacement } from './position';\n\n/**\n * <zh/> 计算文本位置样式\n *\n * <en/> Calculate text position style\n * @param bounds - <zh/> 外包围盒 | <en/> contour bounds\n * @param placement - <zh/> 位置 | <en/> placement\n * @param offsetX - <zh/> x轴偏移 | <en/> x-axis offset\n * @param offsetY - <zh/> y轴偏移 | <en/> y-axis offset\n * @param closeToContour - <zh/> 标签位置是否贴合轮廓 | <en/> whether the label position is close to the contour\n * @param path - <zh/> 路径 | <en/> path\n * @param autoRotate - <zh/> 是否跟随轮廓旋转 | <en/> whether to rotate with the contour\n * @returns <zh/> 文本样式 | <en/> text style\n */\nexport function getPolygonTextStyleByPlacement(\n  bounds: AABB,\n  placement: CardinalPlacement | 'center',\n  offsetX: number,\n  offsetY: number,\n  closeToContour: boolean,\n  path: PathArray | string,\n  autoRotate: boolean,\n) {\n  const [x, y] = getXYByPlacement(bounds, placement);\n  const style: Partial<TextStyleProps> = {\n    textAlign: placement === 'left' ? 'right' : placement === 'right' ? 'left' : 'center',\n    textBaseline: placement === 'top' ? 'bottom' : placement === 'bottom' ? 'top' : 'middle',\n    transform: [['translate', x + offsetX, y + offsetY]],\n  };\n  if (placement === 'center' || !closeToContour) return style;\n\n  const points = pathToPoints(path);\n\n  if (!points || points.length <= 3) return style;\n\n  const lines = points\n    .map((point, index) => {\n      const p1 = point;\n      const p2 = points[(index + 1) % points.length];\n      if (isEqual(p1, p2)) return null;\n      return [p1, p2];\n    })\n    .filter(Boolean) as [Point, Point][];\n  const line = findNearestLine([x, y], lines);\n  const intersection = findNearestPointOnLine([x, y], line);\n  if (intersection && line) {\n    style.transform = [['translate', intersection[0] + offsetX, intersection[1] + offsetY]];\n    if (autoRotate) {\n      const angle = Math.atan((line[0][1] - line[1][1]) / (line[0][0] - line[1][0]));\n      style.transform.push(['rotate', (angle / Math.PI) * 180]);\n      style.textAlign = 'center';\n      if (placement === 'right' || placement === 'left') {\n        if (angle > 0) {\n          style.textBaseline = placement === 'right' ? 'bottom' : 'top';\n        } else {\n          style.textBaseline = placement === 'right' ? 'top' : 'bottom';\n        }\n      }\n    }\n  }\n  return style;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/position.ts",
    "content": "import type { AABB } from '@antv/g';\nimport type { Anchor, NodeLikeData, Placement, Point, RelativePlacement } from '../types';\nimport { parseAnchor } from './anchor';\nimport { parsePlacement } from './placement';\n\n/**\n * <zh/> 获取节点/ combo 的位置坐标\n *\n * <en/> Get the position of node/combo\n * @param datum - <zh/> 节点/ combo 的数据 | <en/> data of node/combo\n * @returns - <zh/> 坐标 | <en/> position\n */\nexport function positionOf(datum: NodeLikeData): Point {\n  const { x = 0, y = 0, z = 0 } = datum.style || {};\n  return [+x, +y, +z];\n}\n\n/**\n * <zh/> 检查数据是否有位置坐标\n *\n * <en/> Check if the data has a position coordinate\n * @param datum - <zh/> 节点/ combo 的数据 | <en/> data of node/combo\n * @returns - <zh/> 是否有位置坐标 | <en/> Whether there is a position coordinate\n */\nexport function hasPosition(datum: NodeLikeData): boolean {\n  const { x, y, z } = datum.style || {};\n  return x !== undefined || y !== undefined || z !== undefined;\n}\n\n/**\n * <zh/> 获取相对位置坐标\n *\n * <en/> Get position by relative placement\n * @param bbox - <zh/> 元素包围盒 | <en/> element bounding box\n * @param placement - <zh/> 相对于元素的位置 | <en/> Point relative to element\n * @returns - <zh/> 坐标 | <en/> position\n */\nexport function getXYByRelativePlacement(bbox: AABB, placement: RelativePlacement): Point {\n  const [x, y] = placement;\n  const { min, max } = bbox;\n  return [min[0] + x * (max[0] - min[0]), min[1] + y * (max[1] - min[1])];\n}\n\n/**\n * <zh/> 获取位置坐标\n *\n * <en/> Get position by placement\n * @param bbox - <zh/> 元素包围盒 | <en/> element bounding box\n * @param placement - <zh/> 相对于元素的位置 | <en/> Point relative to element\n * @returns - <zh/> 坐标 | <en/> position\n */\nexport function getXYByPlacement(bbox: AABB, placement: Placement = 'center'): Point {\n  const relativePlacement = parsePlacement(placement);\n  return getXYByRelativePlacement(bbox, relativePlacement);\n}\n\n/**\n * <zh/> 获取锚点坐标\n *\n * <en/> Get anchor position\n * @param bbox - <zh/> 元素包围盒 | <en/> element bounding box\n * @param anchor - <zh/> 锚点位置 | <en/> Anchor\n * @returns - <zh/> 坐标 | <en/> position\n */\nexport function getXYByAnchor(bbox: AABB, anchor: Anchor): Point {\n  const parsedAnchor = parseAnchor(anchor);\n  return getXYByRelativePlacement(bbox, parsedAnchor as RelativePlacement);\n}\n\n/**\n * <zh/> 通过 rect points 路径点获取 position 方位配置.\n *\n * <en/> The rect points command is used to obtain the position and orientation configuration.\n * @param points Points\n * @returns `{ left: number; right: number; top: number; bottom: number }`\n */\nexport const getPositionByRectPoints = (points: Point[]) => {\n  const [p1, p2] = points;\n  return {\n    left: Math.min(p1[0], p2[0]),\n    right: Math.max(p1[0], p2[0]),\n    top: Math.min(p1[1], p2[1]),\n    bottom: Math.max(p1[1], p2[1]),\n  };\n};\n"
  },
  {
    "path": "packages/g6/src/utils/prefix.ts",
    "content": "import { lowerFirst, upperFirst } from '@antv/util';\nimport type { ReplacePrefix } from '../types';\n\n/**\n * <zh/> 是否以某个前缀开头\n *\n * <en/> Whether starts with prefix\n * @param str - <zh/> 字符串 | <en/> string\n * @param prefix - <zh/> 前缀 | <en/> prefix\n * @returns <zh/> 是否以某个前缀开头 | <en/> whether starts with prefix\n */\nexport function startsWith(str: string, prefix: string) {\n  if (!str.startsWith(prefix)) return false;\n  const nextChart = str[prefix.length];\n  return nextChart >= 'A' && nextChart <= 'Z';\n}\n\n/**\n * <zh/> 添加前缀\n *\n * <en/> Add prefix\n * @param str - <zh/> 字符串 | <en/> string\n * @param prefix - <zh/> 前缀 | <en/> prefix\n * @returns <zh/> 添加前缀后的字符串 | <en/> string with prefix\n */\nexport function addPrefix(str: string, prefix: string): string {\n  return `${prefix}${upperFirst(str)}`;\n}\n\n/**\n * <zh/> 移除前缀\n *\n * <en/> Remove prefix\n * @param string - <zh/> 字符串 | <en/> string\n * @param prefix - <zh/> 前缀 | <en/> prefix\n * @param lowercaseFirstLetter - <zh/> 是否小写首字母 | <en/> whether lowercase first letter\n * @returns <zh/> 移除前缀后的字符串 | <en/> string without prefix\n */\nexport function removePrefix(string: string, prefix?: string, lowercaseFirstLetter: boolean = true) {\n  if (!prefix) return string;\n  if (!startsWith(string, prefix)) return string;\n  const str = string.slice(prefix.length);\n  return lowercaseFirstLetter ? lowerFirst(str) : str;\n}\n\n/**\n * <zh/> 从样式中提取子样式\n *\n * <en/> Extract sub style from style\n * @param style - <zh/> 样式 | <en/> style\n * @param prefix - <zh/> 子样式前缀 | <en/> sub style prefix\n * @returns <zh/> 子样式 | <en/> sub style\n */\nexport function subStyleProps<T extends Record<string, any>>(style: object, prefix: string) {\n  const subStyle = Object.entries(style).reduce((acc, [key, value]) => {\n    if (key === 'className' || key === 'class') return acc;\n    if (startsWith(key, prefix)) {\n      Object.assign(acc, { [removePrefix(key, prefix)]: value });\n    }\n    return acc;\n  }, {} as T);\n\n  // 向下传递透明度，但避免覆盖子样式中的透明度属性\n  // Pass down opacity, but avoid overwriting the opacity property in the sub-style\n  if ('opacity' in style) {\n    const subOpacityKey = addPrefix('opacity', prefix) as keyof typeof style;\n    const opacity = style.opacity as number;\n\n    if (subOpacityKey in style) {\n      const subOpacity = style[subOpacityKey] as number;\n      Object.assign(subStyle, { opacity: opacity * subOpacity });\n    } else Object.assign(subStyle, { opacity });\n  }\n\n  return subStyle;\n}\n\n/**\n * <zh/> 从对象中提取指定前缀的属性，并移除前缀\n *\n * <en/> Extract properties with the specified prefix from the object and remove the prefix\n * @param obj - <zh/> 对象 | <en/> object\n * @param prefix - <zh/> 前缀 | <en/> prefix\n * @returns <zh/> 新对象 | <en/> new object\n */\nexport function subObject(obj: Record<string, any>, prefix: string): Record<string, any> {\n  const prefixLength = prefix.length;\n\n  return Object.keys(obj).reduce(\n    (acc, key) => {\n      if (key.startsWith(prefix)) {\n        const newKey = key.slice(prefixLength);\n        acc[newKey] = obj[key];\n      }\n      return acc;\n    },\n    {} as Record<string, any>,\n  );\n}\n\n/**\n * <zh/> 从样式中排除子样式\n *\n * <en/> Omit sub style from style\n * @param style - <zh/> 样式 | <en/> style\n * @param prefix - <zh/> 子样式前缀 | <en/> sub style prefix\n * @returns <zh/> 排除子样式后的样式 | <en/> style without sub style\n */\nexport function omitStyleProps<T extends Record<string, any>>(style: Record<string, any>, prefix: string | string[]) {\n  const prefixArray = typeof prefix === 'string' ? [prefix] : prefix;\n  const omitStyle: Record<string, any> = {};\n  Object.keys(style).forEach((key) => {\n    if (!prefixArray.find((p) => key.startsWith(p))) {\n      omitStyle[key] = style[key];\n    }\n  });\n  return omitStyle as T;\n}\n\n/**\n * <zh/> 替换前缀\n *\n * <en/> Replace prefix\n * @param style - <zh/> 样式 | <en/> style\n * @param oldPrefix - <zh/> 旧前缀 | <en/> old prefix\n * @param newPrefix - <zh/> 新前缀 | <en/> new prefix\n * @returns <zh/> 替换前缀后的样式 | <en/> style with replaced prefix\n */\nexport function replacePrefix<T extends object>(style: T, oldPrefix: string, newPrefix: string) {\n  return Object.entries(style).reduce(\n    (acc, [key, value]) => {\n      if (startsWith(key, oldPrefix)) {\n        acc[addPrefix(removePrefix(key, oldPrefix, false), newPrefix) as keyof typeof acc] = value;\n      } else {\n        acc[key as keyof typeof acc] = value;\n      }\n      return acc;\n    },\n    {} as ReplacePrefix<T, typeof oldPrefix, typeof newPrefix>,\n  );\n}\n"
  },
  {
    "path": "packages/g6/src/utils/print.ts",
    "content": "/* global console */\n/* eslint no-console: \"off\" */\nimport { version } from '../version';\n\nconst BRAND = 'G6';\n\n/**\n * <zh/> 格式化打印\n *\n * <en/> Format print\n * @param message - <zh/> 消息 | <en/> Message\n * @returns <zh/> 格式化后的消息 | <en/> Formatted message\n */\nexport function format(message: string) {\n  return `[${BRAND} v${version}] ${message}`;\n}\n\nexport const print = {\n  mute: false,\n  debug: (message: string): void => {\n    !print.mute && console.debug(format(message));\n  },\n  info: (message: string): void => {\n    !print.mute && console.info(format(message));\n  },\n  warn: (message: string): void => {\n    !print.mute && console.warn(format(message));\n  },\n  error: (message: string): void => {\n    !print.mute && console.error(format(message));\n  },\n};\n"
  },
  {
    "path": "packages/g6/src/utils/relation.ts",
    "content": "import type { Graph } from '../runtime/graph';\nimport type { EdgeDirection, ElementType, ID } from '../types';\nimport { idOf } from './id';\nimport { bfs } from './traverse';\n\n/**\n * <zh/> 获取指定元素在 n 度关系内的所有元素的 ID\n *\n * <en/> Get the IDs of all elements within an n-degree relationship from the specified element\n * @remarks\n * <zh/> 对于节点，0 度关系是节点本身，1 度关系包括节点的直接相邻节点和边，以此类推。\n * 对于边，0 度关系是边本身，1 度关系包括边本身及其两个端点，以此类推。\n *\n * <en/> For a node, 0-degree relationship includes the node itself; 1-degree relationship includes the node's direct neighbors and their connecting edges, etc.\n * For an edge, 0-degree relationship includes the edge itself; 1-degree relationship includes the edge and its two endpoints, etc\n * @param graph - <zh/> 图实例 | <en/> graph instance\n * @param elementType - <zh/> 元素类型 | <en/> element type\n * @param elementId - <zh/> 起始元素的 ID | <en/> start element ID\n * @param degree - <zh/> 指定的度数 | <en/> the specified degree\n * @param direction - <zh/> 边的方向 | <en/> edge direction\n * @returns - <zh/> 返回节点和边的 ID 数组 | <en/> Returns an array of node and edge IDs\n */\nexport function getElementNthDegreeIds(\n  graph: Graph,\n  elementType: ElementType,\n  elementId: ID,\n  degree: number,\n  direction: EdgeDirection = 'both',\n): ID[] {\n  if (elementType === 'combo' || elementType === 'node') {\n    return getNodeNthDegreeIds(graph, elementId, degree, direction);\n  }\n\n  const edgeData = graph.getEdgeData(elementId);\n  if (!edgeData) return [];\n\n  const sourceRelations = getNodeNthDegreeIds(graph, edgeData.source, degree - 1, direction);\n  const targetRelations = getNodeNthDegreeIds(graph, edgeData.target, degree - 1, direction);\n\n  return Array.from(new Set<ID>([...sourceRelations, ...targetRelations, elementId]));\n}\n\n/**\n * <zh/> 获取指定节点在 n 度关系内的所有元素的 ID\n *\n * <en/> Get all elements IDs within n-degree relationship of the specified node\n * @remarks\n * <zh/> 节点的 0 度关系是节点本身，1 度关系是节点的直接相邻节点和边，以此类推\n * @param direction\n * <en/> 0-degree relationship of a node is the node itself; 1-degree relationship is the node's neighboring nodes and related edges, etc\n * @param graph - <zh/> 图实例 | <en/> graph instance\n * @param startNodeId - <zh/> 起始节点的 ID | <en/> The ID of the starting node\n * @param degree - <zh/> 指定的度数 | <en/> The specified degree\n * @param direction - <zh/> 边的方向 | <en/> The direction of the edge\n * @returns - <zh/> 返回节点和边的 ID 数组 | <en/> Returns an array of node and edge IDs\n */\nexport function getNodeNthDegreeIds(\n  graph: Graph,\n  startNodeId: ID,\n  degree: number,\n  direction: EdgeDirection = 'both',\n): ID[] {\n  const visitedNodes = new Set<ID>();\n  const visitedEdges = new Set<ID>();\n  const relations = new Set<ID>();\n\n  bfs(\n    startNodeId,\n    (nodeId: ID, depth: number) => {\n      if (depth > degree) return;\n      relations.add(nodeId);\n\n      graph.getRelatedEdgesData(nodeId, direction).forEach((edge) => {\n        const edgeId = idOf(edge);\n        if (!visitedEdges.has(edgeId) && depth < degree) {\n          relations.add(edgeId);\n          visitedEdges.add(edgeId);\n        }\n      });\n    },\n    (nodeId: ID) => {\n      return graph\n        .getRelatedEdgesData(nodeId, direction)\n        .map((edge) => (edge.source === nodeId ? edge.target : edge.source))\n        .filter((neighborNodeId) => {\n          if (!visitedNodes.has(neighborNodeId)) {\n            visitedNodes.add(neighborNodeId);\n            return true;\n          }\n          return false;\n        });\n    },\n  );\n\n  return Array.from(relations);\n}\n"
  },
  {
    "path": "packages/g6/src/utils/router/orth.ts",
    "content": "import type { AABB } from '@antv/g';\nimport { difference, isEqual } from '@antv/util';\nimport type { Node, OrthRouterOptions, Point } from '../../types';\nimport {\n  getBBoxHeight,\n  getBBoxWidth,\n  getCombinedBBox,\n  getNearestBoundaryPoint,\n  getNearestBoundarySide,\n  getNodeBBox,\n  isPointBBoxCenter,\n  isPointInBBox,\n  isPointOnBBoxBoundary,\n  isPointOutsideBBox,\n} from '../bbox';\nimport { isOrthogonal, moveTo, round } from '../point';\nimport { angle, distance, subtract, toVector2, toVector3 } from '../vector';\n\nexport type Direction = 'N' | 'S' | 'W' | 'E' | null;\n\ntype Route = {\n  points: Point[];\n  direction: Direction;\n};\n\nconst defaultOptions: OrthRouterOptions = {\n  padding: 10,\n};\n\n/**\n * <zh/> 获取两点之间的正交线段路径\n *\n * <en/> Get orthogonal line segments between two points\n * @param sourcePoint - <zh/> 起始点 | <en/> start point\n * @param targetPoint - <zh/> 终止点 | <en/> end point\n * @param sourceNode - <zh/> 起始节点 | <en/> source node\n * @param targetNode - <zh/> 终止节点 | <en/> target node\n * @param controlPoints - <zh/> 控制点 | <en/> control points\n * @param options - <zh/> 配置项 | <en/> options\n * @returns <zh/> 路径点集 | <en/> vertices\n */\nexport function orth(\n  sourcePoint: Point,\n  targetPoint: Point,\n  sourceNode: Node,\n  targetNode: Node,\n  controlPoints: Point[],\n  options: OrthRouterOptions,\n) {\n  const { padding } = Object.assign(defaultOptions, options);\n\n  const sourceBBox = getNodeBBox(sourceNode, padding);\n  const targetBBox = getNodeBBox(targetNode, padding);\n\n  const points: Point[] = [sourcePoint, ...controlPoints, targetPoint];\n\n  // direction of previous route segment\n  let direction: Direction = null;\n  const result: Point[] = [];\n\n  for (let fromIdx = 0, len = points.length; fromIdx < len - 1; fromIdx++) {\n    const toIdx = fromIdx + 1;\n    const from = points[fromIdx];\n    const to = points[toIdx];\n    const isOrth = isOrthogonal(from, to);\n\n    let route = null;\n\n    if (fromIdx === 0) {\n      if (toIdx === len - 1) {\n        // source -> target\n        if (sourceBBox.intersects(targetBBox)) {\n          route = insideNode(from, to, sourceBBox, targetBBox);\n        } else if (!isPointBBoxCenter(from, sourceBBox) && !isPointBBoxCenter(to, targetBBox)) {\n          const fromWithPadding = getNearestBoundaryPoint(from, sourceBBox);\n          const toWithPadding = getNearestBoundaryPoint(to, targetBBox);\n          route = pointToPoint(fromWithPadding, toWithPadding, getDirection(fromWithPadding, toWithPadding));\n          route.points.unshift(fromWithPadding);\n          route.points.push(toWithPadding);\n        } else if (!isOrth) {\n          route = nodeToNode(from, to, sourceBBox, targetBBox);\n        }\n      } else {\n        // source -> point\n        if (isPointInBBox(to, sourceBBox)) {\n          route = insideNode(from, to, sourceBBox, getNodeBBox(to, padding), direction);\n        } else if (!isOrth) {\n          route = nodeToPoint(from, to, sourceBBox);\n        }\n      }\n    } else if (toIdx === len - 1) {\n      // point -> target\n      if (isPointInBBox(from, targetBBox)) {\n        route = insideNode(from, to, getNodeBBox(from, padding), targetBBox, direction);\n      } else if (!isOrth) {\n        route = pointToNode(from, to, targetBBox, direction);\n      }\n    } else if (!isOrth) {\n      // point -> point\n      route = pointToPoint(from, to, direction);\n    }\n\n    // set direction for next iteration\n    if (route) {\n      result.push(...route.points);\n      direction = route.direction;\n    } else {\n      // orthogonal route and not looped\n      direction = getDirection(from, to);\n    }\n\n    if (toIdx < len - 1) result.push(to);\n  }\n\n  return result.map(toVector2);\n}\n\n/**\n * Direction to opposites direction map\n */\nconst opposites = {\n  N: 'S',\n  S: 'N',\n  W: 'E',\n  E: 'W',\n};\n\n/**\n * Direction to radians map\n */\nconst radians = {\n  N: -Math.PI / 2,\n  S: Math.PI / 2,\n  E: 0,\n  W: Math.PI,\n};\n\n/**\n * <zh/> 获取两点之间的方向，从 `from` 到 `to` 的方向\n *\n * <en/> Get the direction between two points, the direction from `from` to `to`\n * @param from - <zh/> 起始点 | <en/> start point\n * @param to - <zh/> 终止点 | <en/> end point\n * @returns <zh/> 方向 | <en/> direction\n */\nexport function getDirection(from: Point, to: Point): Direction | null {\n  const [fx, fy] = from;\n  const [tx, ty] = to;\n  if (fx === tx) {\n    return fy > ty ? 'N' : 'S';\n  }\n  if (fy === ty) {\n    return fx > tx ? 'W' : 'E';\n  }\n  return null;\n}\n\n/**\n * <zh/> 获取包围盒的尺寸，根据方向返回宽度或者高度\n *\n * <en/> Get the size of the bounding box, return the width or height according to the direction\n * @param bbox - <zh/> 包围盒 | <en/> bounding box\n * @param direction - <zh/> 方向 | <en/> direction\n * @returns <zh/> 尺寸 | <en/> size\n */\nexport function getBBoxSize(bbox: AABB, direction: Direction): number {\n  return direction === 'N' || direction === 'S' ? getBBoxHeight(bbox) : getBBoxWidth(bbox);\n}\n\n/**\n * <zh/> 从一个点到另一个点计算正交路由\n *\n * <en/> Calculate orthogonal route from one point to another\n * @param from - <zh/> 起始点 | <en/> start point\n * @param to - <zh/> 终止点 | <en/> end point\n * @param direction - <zh/> 前一条线段的方向 | <en/> direction of the previous segment\n * @returns <zh/> 正交路由 | <en/> orthogonal route\n */\nexport function pointToPoint(from: Point, to: Point, direction: Direction): Route {\n  const p1: Point = [from[0], to[1]];\n  const p2: Point = [to[0], from[1]];\n  const d1 = getDirection(from, p1);\n  const d2 = getDirection(from, p2);\n  const opposite = direction ? opposites[direction] : null;\n  const p = d1 === direction || (d1 !== opposite && d2 !== direction) ? p1 : p2;\n\n  return { points: [p], direction: getDirection(p, to) };\n}\n\n/**\n * <zh/> 从节点到点计算正交路由\n *\n * <en/> Calculate orthogonal route from node to point\n * @param from - <zh/> 起始点 | <en/> start point\n * @param to - <zh/> 终止点 | <en/> end point\n * @param fromBBox - <zh/> 起始节点的包围盒 | <en/> bounding box of the start node\n * @returns <zh/> 正交路由 | <en/> orthogonal route\n */\nexport function nodeToPoint(from: Point, to: Point, fromBBox: AABB): Route {\n  if (isPointBBoxCenter(from, fromBBox)) {\n    const p = freeJoin(from, to, fromBBox);\n\n    return { points: [p], direction: getDirection(p, to) };\n  } else {\n    const fromWithPadding = getNearestBoundaryPoint(from, fromBBox);\n    const isHorizontal = ['left', 'right'].includes(getNearestBoundarySide(from, fromBBox));\n    const p: Point = isHorizontal ? [to[0], fromWithPadding[1]] : [fromWithPadding[0], to[1]];\n\n    return { points: [p], direction: getDirection(p, to) };\n  }\n}\n\n/**\n * <zh/> 从点到节点计算正交路由\n *\n * <en/> Calculate orthogonal route from point to node\n * @param from - <zh/> 起始点 | <en/> start point\n * @param to - <zh/> 终止点 | <en/> end point\n * @param toBBox - <zh/> 终止节点的包围盒 | <en/> bounding box of the end node\n * @param direction - <zh/> 前一条线段的方向 | <en/> direction of the previous segment\n * @returns <zh/> 正交路由 | <en/> orthogonal route\n */\nexport function pointToNode(from: Point, to: Point, toBBox: AABB, direction: Direction): Route {\n  const toWithPadding = isPointBBoxCenter(to, toBBox) ? to : getNearestBoundaryPoint(to, toBBox);\n  const points: Point[] = [\n    [toWithPadding[0], from[1]],\n    [from[0], toWithPadding[1]],\n  ];\n  const freePoints = points.filter((p) => isPointOutsideBBox(p, toBBox) && !isPointOnBBoxBoundary(p, toBBox, true));\n\n  const freeDirectionPoints = freePoints.filter((p) => getDirection(p, from) !== direction);\n\n  if (freeDirectionPoints.length > 0) {\n    // Pick a point which bears the same direction as the previous segment.\n    const p = freeDirectionPoints.find((p) => getDirection(from, p) === direction) || freeDirectionPoints[0];\n    return {\n      points: [p],\n      direction: getDirection(p, to),\n    };\n  } else {\n    // Here we found only points which are either contained in the element or they would create\n    // a link segment going in opposites direction from the previous one.\n    // We take the point inside element and move it outside the element in the direction the\n    // route is going. Now we can join this point with the current end (using freeJoin).\n    const p = difference(points, freePoints)[0];\n    const p2 = moveTo(to, p, getBBoxSize(toBBox, direction) / 2);\n    const p1 = freeJoin(p2, from, toBBox);\n    return {\n      points: [p1, p2],\n      direction: getDirection(p2, to),\n    };\n  }\n}\n\n/**\n * <zh/> 从节点到节点计算正交路由\n *\n * <en/> Calculate orthogonal route from node to node\n * @param from - <zh/> 起始点 | <en/> start point\n * @param to - <zh/> 终止点 | <en/> end point\n * @param fromBBox - <zh/> 起始节点的包围盒 | <en/> bounding box of the start node\n * @param toBBox - <zh/> 终止节点的包围盒 | <en/> bounding box of the end node\n * @returns <zh/> 正交路由 | <en/> orthogonal route\n */\nexport function nodeToNode(from: Point, to: Point, fromBBox: AABB, toBBox: AABB): Route {\n  let route = nodeToPoint(from, to, fromBBox);\n  const p1 = toVector3(route.points[0]);\n\n  if (isPointInBBox(p1, toBBox)) {\n    route = nodeToPoint(to, from, toBBox);\n    const p2 = toVector3(route.points[0]);\n\n    if (isPointInBBox(p2, fromBBox)) {\n      const fromBorder = moveTo(from, p1, getBBoxSize(fromBBox, getDirection(from, p1)) / 2);\n      const toBorder = moveTo(to, p2, getBBoxSize(toBBox, getDirection(to, p2)) / 2);\n      const midPoint: Point = [(fromBorder[0] + toBorder[0]) / 2, (fromBorder[1] + toBorder[1]) / 2];\n\n      const startRoute = nodeToPoint(from, midPoint, fromBBox);\n      const endRoute = pointToNode(midPoint, to, toBBox, startRoute.direction);\n\n      route.points = [startRoute.points[0], endRoute.points[0]];\n      route.direction = endRoute.direction;\n    }\n  }\n\n  return route;\n}\n\n/**\n * <zh/> 在两个节点内部计算路由\n *\n * <en/> Calculate route inside two nodes\n * @param from - <zh/> 起始点 | <en/> start point\n * @param to - <zh/> 终止点 | <en/> end point\n * @param fromBBox - <zh/> 起始节点的包围盒 | <en/> bounding box of the start node\n * @param toBBox - <zh/> 终止节点的包围盒 | <en/> bounding box of the end node\n * @param direction - <zh/> 方向 | <en/> direction\n * @returns <zh/> 正交路由 | <en/> orthogonal route\n */\nexport function insideNode(from: Point, to: Point, fromBBox: AABB, toBBox: AABB, direction?: Direction): Route {\n  const DEFAULT_OFFSET = 0.01;\n  const boundary = getCombinedBBox([fromBBox, toBBox]);\n  const reversed = distance(to, boundary.center) > distance(from, boundary.center);\n  const [start, end] = reversed ? [to, from] : [from, to];\n  const halfPerimeter = getBBoxHeight(boundary) + getBBoxWidth(boundary);\n\n  let p1: Point;\n  if (direction) {\n    const ref: Point = [\n      start[0] + halfPerimeter * Math.cos(radians[direction]),\n      start[1] + halfPerimeter * Math.sin(radians[direction]),\n    ];\n    // `getNearestBoundaryPoint` returns a point on the boundary, so we need to move it a bit to ensure it's outside the element and then get the correct `p2` via `freeJoin`.\n    p1 = moveTo(getNearestBoundaryPoint(ref, boundary), ref, DEFAULT_OFFSET);\n  } else {\n    p1 = moveTo(getNearestBoundaryPoint(start, boundary), start, -DEFAULT_OFFSET);\n  }\n\n  let p2 = freeJoin(p1, end, boundary);\n\n  let points = [round(p1, 2), round(p2, 2)];\n\n  if (isEqual(round(p1), round(p2))) {\n    const rad = angle(subtract(p1, start), [1, 0, 0]) + Math.PI / 2;\n    p2 = [end[0] + halfPerimeter * Math.cos(rad), end[1] + halfPerimeter * Math.sin(rad), 0];\n    p2 = round(moveTo(getNearestBoundaryPoint(p2, boundary), end, -DEFAULT_OFFSET), 2);\n    const p3 = freeJoin(p1, p2, boundary);\n    points = [p1, p3, p2];\n  }\n\n  return {\n    points: reversed ? points.reverse() : points,\n    direction: reversed ? getDirection(p1, to) : getDirection(p2, to),\n  };\n}\n\n/**\n * <zh/> 返回一个点 `p`，使得线段 p,p1 和 p,p2 互相垂直，p 尽可能不在给定的包围盒内\n *\n * <en/> Returns a point `p` where lines p,p1 and p,p2 are perpendicular and p is not contained in the given box\n * @param p1 - <zh/> 点 | <en/> point\n * @param p2 - <zh/> 点 | <en/> point\n * @param bbox - <zh/> 包围盒 | <en/> bounding box\n * @returns <zh/> 点 | <en/> point\n */\nexport function freeJoin(p1: Point, p2: Point, bbox: AABB): Point {\n  let p: Point = [p1[0], p2[1]];\n  if (isPointInBBox(p, bbox)) {\n    p = [p2[0], p1[1]];\n  }\n  return p;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/router/shortest-path.ts",
    "content": "import { isNumber } from '@antv/util';\nimport type { Direction, ID, Node, Point, ShortestPathRouterOptions } from '../../types';\nimport { getBBoxSegments, getBBoxSize, getExpandedBBox, isPointInBBox, isPointOnBBoxBoundary } from '../bbox';\nimport { getLinesIntersection } from '../line';\nimport { add, manhattanDistance, toVector2 } from '../vector';\n\nconst defaultCfg: ShortestPathRouterOptions = {\n  enableObstacleAvoidance: false,\n  offset: 10,\n  maxAllowedDirectionChange: Math.PI / 2,\n  maximumLoops: 3000,\n  gridSize: 5,\n  startDirections: ['top', 'right', 'bottom', 'left'],\n  endDirections: ['top', 'right', 'bottom', 'left'],\n  directionMap: {\n    right: { stepX: 1, stepY: 0 },\n    left: { stepX: -1, stepY: 0 },\n    bottom: { stepX: 0, stepY: 1 },\n    top: { stepX: 0, stepY: -1 },\n  },\n  penalties: { 0: 0, 90: 0 },\n  distFunc: manhattanDistance,\n};\n\nconst keyOf = (point: Point) => `${Math.round(point[0])}|||${Math.round(point[1])}`;\n\nfunction alignToGrid(p: Point, gridSize: number): Point;\nfunction alignToGrid(p: number, gridSize: number): number;\n/**\n * <zh/> 将坐标对齐到网格\n *\n * <en/> Align to the grid\n * @param p - <zh/> 坐标 | <en/> point\n * @param gridSize - <zh/> 网格大小 | <en/> grid size\n * @returns <zh/> 对齐后的坐标 | <en/> aligned point\n */\nfunction alignToGrid(p: number | Point, gridSize: number): number | Point {\n  const align = (value: number) => Math.round(value / gridSize);\n  if (isNumber(p)) return align(p);\n  return p.map(align) as Point;\n}\n\n/**\n * <zh/> 获取两个角度的变化方向，并确保小于 180 度\n *\n * <en/ >Get changed direction angle and make sure less than 180 degrees\n * @param angle1 - <zh/> 第一个角度 | <en/> the first angle\n * @param angle2 - <zh/> 第二个角度 | <en/> the second angle\n * @returns <zh/> 两个角度的变化方向 | <en/> changed direction angle\n */\nfunction getAngleDiff(angle1: number, angle2: number) {\n  const directionChange = Math.abs(angle1 - angle2);\n  return directionChange > Math.PI ? 2 * Math.PI - directionChange : directionChange;\n}\n\n/**\n * <zh/> 获取从 p1 指向 p2 的向量与 x 轴正方向之间的夹角，单位为弧度\n *\n * <en/> Get the angle between the vector from p1 to p2 and the positive direction of the x-axis, in radians\n * @param p1 - <zh/> 点 p1 | <en/> point p1\n * @param p2 - <zh/> 点 p2 | <en/> point p2\n * @returns <zh/> 夹角 | <en/> angle\n */\nfunction getDirectionAngle(p1: Point, p2: Point) {\n  const deltaX = p2[0] - p1[0];\n  const deltaY = p2[1] - p1[1];\n\n  if (!deltaX && !deltaY) return 0;\n\n  return Math.atan2(deltaY, deltaX);\n}\n\n/**\n * <zh/> 获取两个点之间的方向变化\n *\n * <en/> Get direction change between two points\n * @param current - <zh/> 当前点 | <en/> current point\n * @param neighbor - <zh/> 邻居点 | <en/> neighbor point\n * @param cameFrom - <zh/> 来源点 | <en/> source point\n * @param scaleStartPoint - <zh/> 缩放后的起点 | <en/> scaled start point\n * @returns <zh/> 方向变化 | <en/> direction change\n */\nfunction getDirectionChange(\n  current: Point,\n  neighbor: Point,\n  cameFrom: Record<string, Point>,\n  scaleStartPoint: Point,\n): number {\n  const directionAngle = getDirectionAngle(current, neighbor);\n\n  const currentCameFrom = cameFrom[keyOf(current)];\n  const prev = !currentCameFrom ? scaleStartPoint : currentCameFrom;\n  const prevDirectionAngle = getDirectionAngle(prev, current);\n\n  return getAngleDiff(prevDirectionAngle, directionAngle);\n}\n\n/**\n * <zh/> 获取障碍物地图\n *\n * <en/> Get obstacle map\n * @param nodes - <zh/> 图上所有节点 | <en/> all nodes on the graph\n * @param options - <zh/> 路由配置 | <en/> router options\n * @returns <zh/> 障碍物地图 | <en/> obstacle map\n */\nconst getObstacleMap = (nodes: Node[], options: Required<ShortestPathRouterOptions>) => {\n  const { offset, gridSize } = options;\n  const obstacleMap: Record<string, boolean> = {};\n\n  nodes.forEach((item: Node) => {\n    if (!item || item.destroyed || !item.isVisible()) return;\n    const bbox = getExpandedBBox(item.getRenderBounds(), offset);\n    for (let x = alignToGrid(bbox.min[0], gridSize); x <= alignToGrid(bbox.max[0], gridSize); x += 1) {\n      for (let y = alignToGrid(bbox.min[1], gridSize); y <= alignToGrid(bbox.max[1], gridSize); y += 1) {\n        obstacleMap[`${x}|||${y}`] = true;\n      }\n    }\n  });\n  return obstacleMap;\n};\n\n/**\n * <zh/> 估算从起点到多个锚点的最小代价\n *\n * <en/> Estimate minimum cost from the starting point to multiple anchor points\n * @param from - <zh/> 起点 | <en/> source point\n * @param anchors - <zh/> 锚点 | <en/> anchor points\n * @param distFunc - <zh/> 距离函数 | <en/> distance function\n * @returns <zh/> 最小成本 | <en/> minimum cost\n */\nexport function estimateCost(from: Point, anchors: Point[], distFunc: (p1: Point, p2: Point) => number) {\n  return Math.min(...anchors.map((anchor) => distFunc(from, anchor)));\n}\n\n/**\n * <zh/> 已知一个点集与一个参考点，从点集中找到距离参考点最近的点\n *\n * <en/> Given a set of points and a reference point, find the point closest to the reference point from the set of points\n * @param points - <zh/> 点集 | <en/> set of points\n * @param refPoint - <zh/> 参考点 | <en/> reference point\n * @param distFunc - <zh/> 距离函数 | <en/> distance function\n * @returns <zh/> 最近的点 | <en/> nearest point\n */\nexport function getNearestPoint(points: Point[], refPoint: Point, distFunc: (p1: Point, p2: Point) => number): Point {\n  let nearestPoint = points[0];\n  let minDistance = distFunc(points[0], refPoint);\n  for (let i = 0; i < points.length; i++) {\n    const point = points[i];\n    const dis = distFunc(point, refPoint);\n    if (dis < minDistance) {\n      nearestPoint = point;\n      minDistance = dis;\n    }\n  }\n  return nearestPoint;\n}\n\n/**\n * Calculate the connection points on the expanded BBox\n * @param point\n * @param node\n * @param directions\n * @param options\n */\nconst getBoxPoints = (\n  point: Point,\n  node: Node,\n  directions: Direction[],\n  options: Required<ShortestPathRouterOptions>,\n): Point[] => {\n  // create-edge 生成边的过程中，endNode 为 null\n  if (!node) return [point];\n\n  const { directionMap, offset } = options;\n  const expandedBBox = getExpandedBBox(node.getRenderBounds(), offset);\n\n  const points = (Object.keys(directionMap) as Direction[]).reduce<Point[]>((res, directionKey) => {\n    if (directions.includes(directionKey)) {\n      const direction = directionMap[directionKey];\n      const [width, height] = getBBoxSize(expandedBBox);\n      const otherPoint: Point = [point[0] + direction.stepX * width, point[1] + direction.stepY * height];\n      const segments = getBBoxSegments(expandedBBox);\n      for (let i = 0; i < segments.length; i++) {\n        const intersectP = getLinesIntersection([point, otherPoint], segments[i]);\n        if (intersectP && isPointOnBBoxBoundary(intersectP, expandedBBox)) {\n          res.push(intersectP);\n        }\n      }\n    }\n\n    return res;\n  }, []);\n\n  if (!isPointInBBox(point, expandedBBox)) {\n    points.push(point);\n  }\n\n  return points.map((point) => alignToGrid(point, options.gridSize));\n};\n\nconst getControlPoints = (\n  current: Point,\n  cameFrom: Record<ID, Point>,\n  scaleStartPoint: Point,\n  endPoint: Point,\n  startPoints: Point[],\n  scaleEndPoint: Point,\n  gridSize: number,\n) => {\n  const controlPoints: Point[] = [];\n\n  // append endPoint\n  let pointZero: Point = [\n    scaleEndPoint[0] === endPoint[0] ? endPoint[0] : current[0] * gridSize,\n    scaleEndPoint[1] === endPoint[1] ? endPoint[1] : current[1] * gridSize,\n  ];\n  controlPoints.unshift(pointZero);\n\n  let _current = current;\n  let _currentCameFrom = cameFrom[keyOf(_current)];\n\n  while (_currentCameFrom) {\n    const prePoint = _currentCameFrom;\n    const point = _current;\n    const directionChange = getDirectionChange(prePoint, point, cameFrom, scaleStartPoint);\n\n    if (directionChange) {\n      pointZero = [\n        prePoint[0] === point[0] ? pointZero[0] : prePoint[0] * gridSize,\n        prePoint[1] === point[1] ? pointZero[1] : prePoint[1] * gridSize,\n      ];\n      controlPoints.unshift(pointZero);\n    }\n    _currentCameFrom = cameFrom[keyOf(prePoint)];\n    _current = prePoint;\n  }\n\n  // append startPoint\n  const realStartPoints = startPoints.map((point) => [point[0] * gridSize, point[1] * gridSize] as Point);\n  const startPoint = getNearestPoint(realStartPoints, pointZero, manhattanDistance);\n\n  controlPoints.unshift(startPoint);\n\n  return controlPoints;\n};\n\n/**\n * Find the shortest path between a given source node to a destination node according to A* Search Algorithm：https://www.geeksforgeeks.org/a-search-algorithm/\n * @param sourceNode - <zh/> 源节点 | <en/> source node\n * @param targetNode - <zh/> 目标节点 | <en/> target node\n * @param nodes - <zh/> 图上所有节点 | <en/> all nodes on the graph\n * @param config - <zh/> 路由配置 | <en/> router options\n * @returns <zh/> 控制点数组 | <en/> control point array\n */\nexport function aStarSearch(\n  sourceNode: Node,\n  targetNode: Node,\n  nodes: Node[],\n  config: ShortestPathRouterOptions,\n): Point[] {\n  const startPoint = toVector2(sourceNode.getCenter());\n  const endPoint = toVector2(targetNode.getCenter());\n\n  const options = Object.assign(defaultCfg, config) as Required<ShortestPathRouterOptions>;\n\n  const { gridSize } = options;\n\n  const obstacles = options.enableObstacleAvoidance ? nodes : [sourceNode, targetNode];\n  const obstacleMap = getObstacleMap(obstacles, options);\n\n  const scaleStartPoint = alignToGrid(startPoint, gridSize);\n  const scaleEndPoint = alignToGrid(endPoint, gridSize);\n\n  const startPoints = getBoxPoints(startPoint, sourceNode, options.startDirections, options);\n  const endPoints = getBoxPoints(endPoint, targetNode, options.endDirections, options);\n\n  startPoints.forEach((point) => delete obstacleMap[keyOf(point)]);\n  endPoints.forEach((point) => delete obstacleMap[keyOf(point)]);\n\n  const openList: Record<string, Point> = {};\n  const closedList: Record<string, boolean> = {};\n  const cameFrom: Record<string, Point> = {};\n\n  // The movement cost to move from the starting point to the current point on the grid.\n  const gScore: Record<string, number> = {};\n\n  // The estimated movement cost to move from the starting point to the end point after passing through the current point.\n  // f = g + h\n  const fScore: Record<string, number> = {};\n\n  const sortedOpenSet = new SortedArray();\n\n  for (let i = 0; i < startPoints.length; i++) {\n    const firstStep = startPoints[i];\n    const key = keyOf(firstStep);\n    openList[key] = firstStep;\n    gScore[key] = 0;\n    fScore[key] = estimateCost(firstStep, endPoints, options.distFunc);\n\n    // Push start point to sortedOpenSet\n    sortedOpenSet.add({\n      id: key,\n      value: fScore[key],\n    });\n  }\n\n  const endPointsKeys = endPoints.map((point) => keyOf(point));\n\n  let remainLoops = options.maximumLoops;\n  let current: Point;\n  let curCost = Infinity;\n\n  for (const [id, value] of Object.entries(openList)) {\n    if (fScore[id] <= curCost) {\n      curCost = fScore[id];\n      current = value;\n    }\n  }\n\n  while (Object.keys(openList).length > 0 && remainLoops > 0) {\n    const minId = sortedOpenSet.minId(false);\n    if (minId) {\n      current = openList[minId];\n    } else {\n      break;\n    }\n    const key = keyOf(current);\n\n    // If currentNode is final, return the successful path\n    if (endPointsKeys.includes(key)) {\n      return getControlPoints(current, cameFrom, scaleStartPoint, endPoint, startPoints, scaleEndPoint, gridSize);\n    }\n\n    // Set currentNode as closed\n    delete openList[key];\n    sortedOpenSet.remove(key);\n    closedList[key] = true;\n\n    // Get the neighbor points of the next step\n    for (const dir of Object.values(options.directionMap)) {\n      const neighbor = add(current, [dir.stepX, dir.stepY]);\n      const neighborId = keyOf(neighbor);\n      if (closedList[neighborId]) continue;\n\n      const directionChange = getDirectionChange(current, neighbor, cameFrom, scaleStartPoint);\n      if (directionChange > options.maxAllowedDirectionChange) continue;\n\n      if (obstacleMap[neighborId]) continue; // skip if intersects\n\n      // Add neighbor points to openList, and calculate the cost of each neighbor point\n      if (!openList[neighborId]) {\n        openList[neighborId] = neighbor;\n      }\n\n      const directionPenalties = options.penalties[directionChange];\n      const neighborCost =\n        options.distFunc(current, neighbor) + (isNaN(directionPenalties) ? gridSize : directionPenalties);\n      const costFromStart = gScore[key] + neighborCost;\n      const neighborGScore = gScore[neighborId];\n\n      if (neighborGScore && costFromStart >= neighborGScore) continue;\n\n      cameFrom[neighborId] = current;\n      gScore[neighborId] = costFromStart;\n      fScore[neighborId] = costFromStart + estimateCost(neighbor, endPoints, options.distFunc);\n\n      sortedOpenSet.add({\n        id: neighborId,\n        value: fScore[neighborId],\n      });\n    }\n    remainLoops -= 1;\n  }\n\n  return [];\n}\n\ntype Item = {\n  id: string;\n  value: number;\n};\n\n/**\n * <zh/> 有序数组，按升序排列\n *\n * <en/> Sorted array, sorted in ascending order\n */\nexport class SortedArray {\n  public arr: Item[] = [];\n\n  private map: Record<string, boolean> = {};\n\n  constructor() {\n    this.arr = [];\n    this.map = {};\n  }\n\n  private _innerAdd(item: Item, length: number) {\n    let low = 0,\n      high = length - 1;\n    while (high - low > 1) {\n      const mid = Math.floor((low + high) / 2);\n      if (this.arr[mid].value > item.value) {\n        high = mid;\n      } else if (this.arr[mid].value < item.value) {\n        low = mid;\n      } else {\n        this.arr.splice(mid, 0, item);\n        this.map[item.id] = true;\n        return;\n      }\n    }\n    this.arr.splice(high, 0, item);\n    this.map[item.id] = true;\n  }\n\n  /**\n   * <zh/> 将新项添加到适当的索引位置\n   *\n   * <en/> Add the new item to the appropriate index\n   * @param item - <zh/> 新项 | <en/> new item\n   */\n  public add(item: Item) {\n    // 已经存在，先移除\n    // If exists, remove it\n    delete this.map[item.id];\n\n    const length = this.arr.length;\n    // 如果为空或者最后一个元素小于当前元素，直接添加到最后\n    // If empty or the last element is less than the current element, add to the end\n    if (!length || this.arr[length - 1].value < item.value) {\n      this.arr.push(item);\n      this.map[item.id] = true;\n      return;\n    }\n\n    // 按照升序排列，找到合适的位置插入\n    // Find the appropriate position to insert in ascending order\n    this._innerAdd(item, length);\n  }\n\n  public remove(id: string) {\n    if (!this.map[id]) return;\n    delete this.map[id];\n  }\n\n  private _clearAndGetMinId() {\n    let res;\n    for (let i = this.arr.length - 1; i >= 0; i--) {\n      if (this.map[this.arr[i].id]) res = this.arr[i].id;\n      else this.arr.splice(i, 1);\n    }\n    return res;\n  }\n\n  private _findFirstId() {\n    while (this.arr.length) {\n      const first = this.arr.shift()!;\n      if (this.map[first.id]) return first.id;\n    }\n  }\n\n  public minId(clear: boolean) {\n    if (clear) {\n      return this._clearAndGetMinId();\n    } else {\n      return this._findFirstId();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/utils/scale.ts",
    "content": "/**\n * <zh/> 将一个值从一个范围线性映射到另一个范围\n *\n * <en/> Linearly maps a value from one range to another range\n * @param value - <zh/> 需要映射的值 | <en/> The value to be mapped\n * @param domain - <zh/> 输入值的范围 [最小值, 最大值] | <en/> The input range [min, max]\n * @param range - <zh/> 输出值的范围 [最小值, 最大值] | <en/> The output range [min, max]\n * @returns <zh/> 映射后的值 | <en/> The mapped value\n */\nexport const linear = (value: number, domain: [number, number], range: [number, number]) => {\n  const [d0, d1] = domain;\n  const [r0, r1] = range;\n\n  if (d1 === d0) return r0;\n\n  const ratio = (value - d0) / (d1 - d0);\n  return r0 + ratio * (r1 - r0);\n};\n\n/**\n * <zh/> 将一个值从一个范围对数映射到另一个范围\n *\n * <en/> Logarithmically maps a value from one range to another range\n * @param value - <zh/> 需要映射的值 | <en/> The value to be mapped\n * @param domain - <zh/> 输入值的范围 [最小值, 最大值] | <en/> The input range [min, max]\n * @param range - <zh/> 输出值的范围 [最小值, 最大值] | <en/> The output range [min, max]\n * @returns <zh/> 映射后的值 | <en/> The mapped value\n */\nexport const log = (value: number, domain: [number, number], range: [number, number]) => {\n  const [d0, d1] = domain;\n  const [r0, r1] = range;\n\n  const ratio = Math.log(value - d0 + 1) / Math.log(d1 - d0 + 1);\n  return r0 + ratio * (r1 - r0);\n};\n\n/**\n * <zh/> 将一个值从一个范围幂映射到另一个范围\n *\n * <en/> Maps a value from one range to another range\n * @param value - <zh/> 需要映射的值 | <en/> The value to be mapped\n * @param domain - <zh/> 输入值的范围 [最小值, 最大值] | <en/> The input range [min, max]\n * @param range - <zh/> 输出值的范围 [最小值, 最大值] | <en/> The output range [min, max]\n * @param exponent - <zh/> 幂指数 | <en/> The exponent\n * @returns <zh/> 映射后的值 | <en/> The mapped value\n */\nexport const pow = (value: number, domain: [number, number], range: [number, number], exponent: number = 2): number => {\n  const [d0, d1] = domain;\n  const [r0, r1] = range;\n\n  const ratio = Math.pow((value - d0) / (d1 - d0), exponent);\n  return r0 + ratio * (r1 - r0);\n};\n\n/**\n * <zh/> 将一个值从一个范围平方根映射到另一个范围\n *\n * <en/> Maps a value from one range to another range using square root\n * @param value - <zh/> 需要映射的值 | <en/> The value to be mapped\n * @param domain - <zh/> 输入值的范围 [最小值, 最大值] | <en/> The input range [min, max]\n * @param range - <zh/> 输出值的范围 [最小值, 最大值] | <en/> The output range [min, max]\n * @returns <zh/> 映射后的值 | <en/> The mapped value\n */\nexport const sqrt = (value: number, domain: [number, number], range: [number, number]) => {\n  const [d0, d1] = domain;\n  const [r0, r1] = range;\n\n  const ratio = Math.sqrt((value - d0) / (d1 - d0));\n  return r0 + ratio * (r1 - r0);\n};\n"
  },
  {
    "path": "packages/g6/src/utils/shape.ts",
    "content": "import type { DisplayObject } from '@antv/g';\n\n/**\n * <zh/> 获取图形的所有子元素\n *\n * <en/> Get all the child elements of the shape\n * @param shape - <zh/> 图形元素 | <en/> shape\n * @returns <zh/> 子元素数组 | <en/> child elements array\n */\nexport function getDescendantShapes<T extends DisplayObject>(shape: T) {\n  const succeeds: DisplayObject[] = [];\n\n  // 遍历所有子元素，并将子元素的子元素加入到数组中\n  const traverse = (shape: DisplayObject) => {\n    if (shape?.children.length) {\n      (shape.children as DisplayObject[]).forEach((child) => {\n        succeeds.push(child);\n        traverse(child);\n      });\n    }\n  };\n\n  traverse(shape);\n\n  return succeeds;\n}\n\n/**\n * <zh/> 获取图形的所有祖先元素\n *\n * <en/> Get all the ancestor elements of the shape\n * @param shape - <zh/> 图形元素 | <en/> shape\n * @returns <zh/> 祖先元素数组 | <en/> ancestor elements array\n */\nexport function getAncestorShapes<T extends DisplayObject>(shape: T) {\n  const ancestors: DisplayObject[] = [];\n  let currentNode = shape.parentNode as DisplayObject;\n  while (currentNode) {\n    ancestors.push(currentNode);\n    currentNode = currentNode.parentNode as DisplayObject;\n  }\n  return ancestors;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/shortcut.ts",
    "content": "import EventEmitter from '@antv/event-emitter';\nimport type { FederatedMouseEvent } from '@antv/g';\nimport { isEqual, isString } from '@antv/util';\nimport { CommonEvent } from '../constants';\nimport type { PinchCallback } from './pinch';\nimport { PinchHandler } from './pinch';\n\nexport interface ShortcutOptions {}\n\nexport type ShortcutKey = string[];\n\ntype Handler = (event: any) => void;\n\nconst MODIFIER_KEYS = new Set(['Control', 'Alt', 'Meta', 'Shift']);\nfunction isModifierKey(key: string) {\n  return MODIFIER_KEYS.has(key);\n}\n\nconst lowerCaseKeys = (keys: ShortcutKey) => keys.map((key) => (isString(key) ? key.toLocaleLowerCase() : key));\n\nexport class Shortcut {\n  private map: Map<ShortcutKey, Handler> = new Map();\n  public pinchHandler: PinchHandler | undefined;\n  private boundHandlePinch: PinchCallback = () => {};\n\n  private emitter: EventEmitter;\n\n  private recordKey = new Set<string>();\n\n  constructor(emitter: EventEmitter) {\n    this.emitter = emitter;\n    this.bindEvents();\n  }\n\n  public bind(key: ShortcutKey, handler: Handler) {\n    if (key.length === 0) return;\n    if (key.includes(CommonEvent.PINCH) && !this.pinchHandler) {\n      this.boundHandlePinch = this.handlePinch.bind(this);\n      this.pinchHandler = new PinchHandler(this.emitter, 'pinchmove', this.boundHandlePinch);\n    }\n    this.map.set(key, handler);\n  }\n\n  public unbind(key: ShortcutKey, handler?: Handler) {\n    this.map.forEach((h, k) => {\n      if (isEqual(k, key)) {\n        if (!handler || handler === h) this.map.delete(k);\n      }\n    });\n  }\n\n  public unbindAll() {\n    this.map.clear();\n  }\n\n  /**\n   * Check whether the given keys are being held down currently.\n   * @param key - array of keys to check\n   * @returns true if the given keys are being held down, false otherwise.\n   */\n  public match(key: ShortcutKey) {\n    // 排序\n    const recordKeyList = lowerCaseKeys(Array.from(this.recordKey)).sort();\n    const keyList = lowerCaseKeys(key).sort();\n    return isEqual(recordKeyList, keyList);\n  }\n\n  private bindEvents() {\n    const { emitter } = this;\n\n    // These window listeners are added purely to listen to modifier keys at the window level,\n    // and the key presses are only recorded into this.recordKey for the purpose of matching\n    // in the match() function. This allows just these keypresses alone to be registered\n    // before the canvas is focused, which prevents a problem where when shortcuts involving\n    // a modifier and clicking on the canvas are bound, match() will return false for that\n    // modifier key because the canvas has not been clicked (and therefore focused) yet.\n    window.addEventListener(CommonEvent.KEY_DOWN, this.onKeyDownWindow);\n    window.addEventListener(CommonEvent.KEY_UP, this.onKeyUpWindow);\n    emitter.on(CommonEvent.KEY_DOWN, this.onKeyDown);\n    emitter.on(CommonEvent.KEY_UP, this.onKeyUp);\n    emitter.on(CommonEvent.WHEEL, this.onWheel);\n    emitter.on(CommonEvent.DRAG, this.onDrag);\n\n    // 窗口重新获得焦点后清空按键，避免按键状态异常\n    // Clear the keys when the window regains focus to avoid abnormal key states\n    globalThis.addEventListener?.('focus', this.onFocus);\n  }\n\n  private onKeyDown = (event: KeyboardEvent) => {\n    if (!event?.key) return;\n    this.recordKey.add(event.key);\n    this.trigger(event);\n  };\n\n  private onKeyUp = (event: KeyboardEvent) => {\n    if (!event?.key) return;\n    this.recordKey.delete(event.key);\n  };\n\n  private onKeyDownWindow = (event: KeyboardEvent) => {\n    if (!isModifierKey(event.key)) return;\n    this.recordKey.add(event.key);\n  };\n\n  private onKeyUpWindow = (event: KeyboardEvent) => {\n    if (!isModifierKey(event.key)) return;\n    this.recordKey.delete(event.key);\n  };\n\n  private trigger(event: KeyboardEvent) {\n    this.map.forEach((handler, key) => {\n      if (this.match(key)) handler(event);\n    });\n  }\n\n  /**\n   * <zh/> 扩展 wheel, drag 操作\n   *\n   * <en/> Extend wheel, drag operations\n   * @param eventType - event name\n   * @param event - event\n   */\n  private triggerExtendKey(eventType: CommonEvent, event: unknown) {\n    this.map.forEach((handler, key) => {\n      if (key.includes(eventType)) {\n        if (\n          isEqual(\n            Array.from(this.recordKey),\n            key.filter((k) => k !== eventType),\n          )\n        ) {\n          handler(event);\n        }\n      }\n    });\n  }\n\n  private onWheel = (event: WheelEvent) => {\n    this.triggerExtendKey(CommonEvent.WHEEL, event);\n  };\n\n  private onDrag = (event: FederatedMouseEvent) => {\n    this.triggerExtendKey(CommonEvent.DRAG, event);\n  };\n\n  private handlePinch: PinchCallback = (event, options) => {\n    this.triggerExtendKey(CommonEvent.PINCH, { ...event, ...options });\n  };\n\n  private onFocus = () => {\n    this.recordKey.clear();\n  };\n\n  public destroy() {\n    this.unbindAll();\n    this.emitter.off(CommonEvent.KEY_DOWN, this.onKeyDown);\n    this.emitter.off(CommonEvent.KEY_UP, this.onKeyUp);\n    window.removeEventListener(CommonEvent.KEY_DOWN, this.onKeyDownWindow);\n    window.removeEventListener(CommonEvent.KEY_UP, this.onKeyUpWindow);\n    this.emitter.off(CommonEvent.WHEEL, this.onWheel);\n    this.emitter.off(CommonEvent.DRAG, this.onDrag);\n    this.pinchHandler?.off('pinchmove', this.boundHandlePinch);\n    globalThis.removeEventListener?.('focus', this.onFocus);\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/utils/size.ts",
    "content": "import type { STDSize, Size } from '../types';\n\n/**\n * <zh/> 解析尺寸配置\n *\n * <en/> Parse size configuration\n * @param size - <zh/> 尺寸配置 | <en/> size configuration\n * @returns <zh/> 标准尺寸格式 | <en/> standard size format\n */\nexport function parseSize(size: Size = 0): STDSize {\n  if (typeof size === 'number') return [size, size, size] as STDSize;\n  const [x, y = x, z = x] = size;\n  return [x, y, z];\n}\n"
  },
  {
    "path": "packages/g6/src/utils/state.ts",
    "content": "import type { ElementDatum } from '../types';\n\n/**\n * <zh/> 获取元素的状态\n *\n * <en/> Get the state of the element\n * @param datum - <zh/> 元素数据 <en/> Element data\n * @returns <zh/> 状态列表 <en/> State list\n */\nexport function statesOf(datum: ElementDatum) {\n  return datum.states || [];\n}\n"
  },
  {
    "path": "packages/g6/src/utils/style.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { Graph } from '../runtime/graph';\nimport type { ElementDatum, StyleIterationContext } from '../types';\n\n/**\n * <zh/> 计算支持回调的动态样式\n *\n * <en/> compute dynamic style that supports callback\n * @param callableStyle - <zh/> 动态样式 | <en/> dynamic style\n * @param context - <zh/> 样式计算迭代上下文 | <en/> style iteration context\n * @returns <zh/> 静态样式 | <en/> static style\n */\nexport function computeElementCallbackStyle(\n  callableStyle:\n    | Record<string, unknown>\n    | ((this: Graph, datum: ElementDatum) => Record<string, unknown>)\n    | {\n        [key: string]: (this: Graph, datum: ElementDatum) => unknown;\n      },\n  context: StyleIterationContext,\n) {\n  const { datum, graph } = context;\n\n  if (typeof callableStyle === 'function') return callableStyle.call(graph, datum);\n\n  return Object.fromEntries(\n    Object.entries(callableStyle).map(([key, style]) => {\n      if (typeof style === 'function') return [key, style.call(graph, datum)];\n      return [key, style];\n    }),\n  );\n}\n\n/**\n * <zh/> 合并图形配置项\n *\n * <en/> Merge shape configuration\n * @param defaultOptions - <zh/> 配置项1 | <en/> configuration 1\n * @param modifiedOptions - <zh/> 配置项2 | <en/> configuration 2\n * @returns <zh/> 合并后的配置项 | <en/> merged configuration\n */\nexport function mergeOptions(\n  defaultOptions: DisplayObjectConfig<any>,\n  modifiedOptions: DisplayObjectConfig<any>,\n): DisplayObjectConfig<any> {\n  const s1 = defaultOptions?.style || {};\n  const s2 = modifiedOptions?.style || {};\n  for (const key in s1) {\n    if (!(key in s2)) s2[key] = s1[key];\n  }\n\n  return Object.assign({}, defaultOptions, modifiedOptions, {\n    style: s2,\n  });\n}\n\n/**\n * <zh/> 获取图形子图形样式\n *\n * <en/> Get the style of the sub-shape of the shape\n * @param style - <zh/> 图形样式 | <en/> shape style\n * @returns <zh/> 子图形样式 | <en/> sub-shape style\n * @remarks\n * <zh/> 从给定的属性对象中提取图形样式属性。删除特定的属性，如位置、变换和类名\n *\n * <en/> Extracts the shape styles from a given attribute object.\n * Removes specific styles like position, transformation, and class name.\n */\nexport function getSubShapeStyle<T extends Record<string, any>>(\n  style: T,\n): Omit<T, 'x' | 'y' | 'z' | 'transform' | 'transformOrigin' | 'className' | 'class' | 'zIndex' | 'visibility'> {\n  const { x, y, z, class: cls, className, transform, transformOrigin, zIndex, visibility, ...rest } = style;\n  return rest;\n}\n"
  },
  {
    "path": "packages/g6/src/utils/symbol.ts",
    "content": "/* eslint-disable jsdoc/require-returns */\n/* eslint-disable jsdoc/require-param */\nimport type { PathArray } from '@antv/util';\n\nexport type SymbolFactor = (width: number, height: number) => PathArray;\n\n/**\n * ○\n */\nexport const circle: SymbolFactor = (width: number, height: number) => {\n  const r = Math.max(width, height) / 2;\n  return [['M', -width / 2, 0], ['A', r, r, 0, 1, 0, 2 * r - width / 2, 0], ['A', r, r, 0, 1, 0, -width / 2, 0], ['Z']];\n};\n\n/**\n * ▷\n */\nexport const triangle: SymbolFactor = (width: number, height: number) => {\n  return [['M', -width / 2, 0], ['L', width / 2, -height / 2], ['L', width / 2, height / 2], ['Z']];\n};\n\n/**\n * ◇\n */\nexport const diamond: SymbolFactor = (width: number, height: number) => {\n  return [['M', -width / 2, 0], ['L', 0, -height / 2], ['L', width / 2, 0], ['L', 0, height / 2], ['Z']];\n};\n\n/**\n * >>\n */\nexport const vee: SymbolFactor = (width: number, height: number) => {\n  return [\n    ['M', -width / 2, 0],\n    ['L', width / 2, -height / 2],\n    ['L', (4 * width) / 5 - width / 2, 0],\n    ['L', width / 2, height / 2],\n    ['Z'],\n  ];\n};\n\n/**\n * □\n */\nexport const rect: SymbolFactor = (width: number, height: number) => {\n  return [\n    ['M', -width / 2, -height / 2],\n    ['L', width / 2, -height / 2],\n    ['L', width / 2, height / 2],\n    ['L', -width / 2, height / 2],\n    ['Z'],\n  ];\n};\n\n/**\n * □▷\n */\nexport const triangleRect: SymbolFactor = (width: number, height: number) => {\n  const tWidth = width / 2;\n  const rWidth = width / 7;\n  const rBeginX = width - rWidth;\n  return [\n    ['M', -tWidth, 0],\n    ['L', 0, -height / 2],\n    ['L', 0, height / 2],\n    ['Z'],\n    ['M', rBeginX - tWidth, -height / 2],\n    ['L', rBeginX + rWidth - tWidth, -height / 2],\n    ['L', rBeginX + rWidth - tWidth, height / 2],\n    ['L', rBeginX - tWidth, height / 2],\n    ['Z'],\n  ];\n};\n\n/**\n * >\n */\nexport const simple: SymbolFactor = (width: number, height: number) => {\n  return [\n    ['M', width / 2, -height / 2],\n    ['L', -width / 2, 0],\n    ['L', width / 2, 0],\n    ['L', -width / 2, 0],\n    ['L', width / 2, height / 2],\n  ];\n};\n"
  },
  {
    "path": "packages/g6/src/utils/text.ts",
    "content": "import { AABB } from '@antv/g';\nimport type { Point } from '../types';\nimport { distance } from './vector';\n\n/**\n * Get WordWrapWidth for a text according the the length of the label and 'maxWidth'.\n * @param length  - length\n * @param maxWidth - maxWidth\n * @returns wordWrapWidth\n */\nexport function getWordWrapWidthWithBase(length: number, maxWidth: string | number): number {\n  let wordWrapWidth = 2 * length;\n  if (typeof maxWidth === 'string') {\n    wordWrapWidth = (length * Number(maxWidth.replace('%', ''))) / 100;\n  } else if (typeof maxWidth === 'number') {\n    wordWrapWidth = maxWidth;\n  }\n  if (isNaN(wordWrapWidth)) wordWrapWidth = 2 * length;\n  return wordWrapWidth;\n}\n\n/**\n * Get the proper wordWrapWidth for a labelShape according the the 'maxWidth' of keyShape.\n * @param keyShapeBox - keyShapeBox\n * @param maxWidth - maxWidth\n * @param zoom - zoom\n * @param enableBalanceShape - enableBalanceShape\n * @returns Get WordWrapWidth by bbox\n */\nexport function getWordWrapWidthByBox(\n  keyShapeBox: AABB,\n  maxWidth: string | number,\n  zoom = 1,\n  enableBalanceShape = false,\n): number {\n  const balanceZoom = enableBalanceShape ? zoom : 1;\n  const keyShapeWidth = (keyShapeBox.max[0] - keyShapeBox.min[0]) * balanceZoom;\n  return getWordWrapWidthWithBase(keyShapeWidth, maxWidth);\n}\n\n/**\n * Get the proper wordWrapWidth for a labelShape according the the distance between two end points and 'maxWidth'.\n * @param points - points\n * @param maxWidth - maxWidth\n * @param zoom - zoom\n * @returns - wordWrapWidth for text\n */\nexport function getWordWrapWidthByEnds(points: [Point, Point], maxWidth: string | number, zoom = 1): number {\n  const dist = distance(points[0], points[1]) * zoom;\n  return getWordWrapWidthWithBase(dist, maxWidth);\n}\n"
  },
  {
    "path": "packages/g6/src/utils/theme.ts",
    "content": "import { ExtensionCategory } from '../constants';\nimport { getExtension } from '../registry/get';\nimport type { GraphOptions } from '../spec';\nimport { print } from './print';\n\n/**\n * <zh/> 获取主题配置\n *\n * <en/> Get theme options\n * @param options - <zh/> 图配置项 <en/> graph options\n * @returns <zh/> 主题配置 <en/> theme options\n */\nexport function themeOf(options: GraphOptions) {\n  const { theme } = options;\n  if (!theme) return {};\n\n  const themeOptions = getExtension(ExtensionCategory.THEME, theme);\n\n  if (themeOptions) return themeOptions;\n\n  print.warn(`The theme of ${theme} is not registered.`);\n  return {};\n}\n"
  },
  {
    "path": "packages/g6/src/utils/transform.ts",
    "content": "import type { TransformArray } from '@antv/g';\nimport { isNumber } from '@antv/util';\n\n/**\n * <zh/> 从 transform 字符串中替换 translate 部分\n *\n * <en/> replace the translate part from the transform string\n * @param x - <zh/> x | <en/> x\n * @param y - <zh/> y | <en/> y\n * @param z - <zh/> z | <en/> z\n * @param transform - <zh/> transform 字符串 | <en/> transform string\n * @returns <zh/> 替换后的 transform 字符串，返回 null 表示无需替换 | <en/> the replaced transform string, return null means no need to replace\n */\nexport function replaceTranslateInTransform(\n  x: number,\n  y: number,\n  z?: number,\n  transform: string | TransformArray = [],\n): string | TransformArray | null {\n  if (!transform && x === 0 && y === 0 && z === 0) return null;\n\n  if (Array.isArray(transform)) {\n    let translateIndex = -1;\n    const newTransform: TransformArray = [];\n\n    for (let i = 0; i < transform.length; i++) {\n      const t = transform[i];\n      if (t[0] === 'translate') {\n        if (t[1] === x && t[2] === y) return null;\n        translateIndex = i;\n        newTransform.push(['translate', x, y]);\n      } else if (t[0] === 'translate3d') {\n        if (t[1] === x && t[2] === y && t[3] === z) return null;\n        translateIndex = i;\n        newTransform.push(['translate3d', x, y, z ?? 0]);\n      } else {\n        newTransform.push(t);\n      }\n    }\n\n    if (translateIndex === -1) {\n      newTransform.splice(0, 0, isNumber(z) ? ['translate3d', x, y, z ?? 0] : ['translate', x, y]);\n    }\n    if (newTransform.length === 0) return null;\n    return newTransform;\n  }\n\n  const removedTranslate = transform ? transform.replace(/translate(3d)?\\([^)]*\\)/g, '') : '';\n  if (z === 0) {\n    return `translate(${x}, ${y})${removedTranslate}`;\n  } else {\n    return `translate3d(${x}, ${y}, ${z})${removedTranslate}`;\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/utils/traverse.ts",
    "content": "export type HierarchyStructure<T> = T & {\n  children?: HierarchyStructure<T>[];\n};\n\n/**\n * <zh/> 执行深度优先遍历\n *\n * <en/> perform depth first traversal\n * @param node - <zh/> 起始节点 | <en/> start node\n * @param visitor - <zh/> 访问节点函数 | <en/> visitor function\n * @param navigator - <zh/> 获取子节点函数 | <en/> get children function\n * @param mode - <zh/> 访问模式，BT: 自底向上访问，TB: 自顶向下访问 | <en/> traverse mode, BT: bottom to top, TB: top to bottom\n * @param depth - <zh/> 当前深度 | <en/> current depth\n */\nexport function dfs<N>(\n  node: N,\n  visitor: (node: N, depth: number) => void,\n  navigator: (node: N) => N[] | undefined,\n  mode: 'BT' | 'TB',\n  depth: number = 0,\n) {\n  if (mode === 'TB') visitor(node, depth);\n\n  const children = navigator(node);\n\n  if (children) {\n    for (const child of children) {\n      dfs(child, visitor, navigator, mode, depth + 1);\n    }\n  }\n\n  if (mode === 'BT') visitor(node, depth);\n}\n\n/**\n * <zh/> 执行广度优先遍历\n *\n * <en/> perform breadth first traversal\n * @param node - <zh/> 起始节点 | <en/> start node\n * @param visitor - <zh/> 访问节点函数 | <en/> visitor function\n * @param navigator - <zh/> 获取子节点函数 | <en/> get children function\n */\nexport function bfs<N>(node: N, visitor: (node: N, depth: number) => void, navigator: (node: N) => N[] | undefined) {\n  const queue: [N, number][] = [[node, 0]];\n\n  while (queue.length) {\n    const [current, depth] = queue.shift()!;\n\n    visitor(current, depth);\n\n    const children = navigator(current);\n\n    if (children) {\n      for (const child of children) {\n        queue.push([child, depth + 1]);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/g6/src/utils/tree.ts",
    "content": "import type { EdgeData, GraphData, NodeData } from '../spec';\nimport type { TreeData } from '../types';\nimport { dfs } from './traverse';\n\ntype TreeDataGetter = {\n  getNodeData?: (datum: TreeData, depth: number) => NodeData;\n  getEdgeData?: (source: TreeData, target: TreeData) => EdgeData;\n  getChildren?: (datum: TreeData) => TreeData[];\n};\n\n/**\n * <zh/> 将树数据转换为图数据\n *\n * <en/> Convert tree data to graph data\n * @param treeData - <zh/> 树数据 | <en/> Tree data\n * @param getter - <zh/> 获取节点和边的方法 | <en/> Methods to get nodes and edges\n * @returns <zh/> 图数据 | <en/> Graph data\n */\nexport function treeToGraphData(treeData: TreeData, getter?: TreeDataGetter): GraphData {\n  const {\n    getNodeData = (datum: TreeData, depth: number) => {\n      datum.depth = depth;\n      if (!datum.children) return datum as NodeData;\n      const { children, ...restDatum } = datum;\n      return { ...restDatum, children: children.map((child) => child.id) } as NodeData;\n    },\n    getEdgeData = (source: TreeData, target: TreeData) => ({ source: source.id, target: target.id }),\n    getChildren = (datum: TreeData) => datum.children || [],\n  } = getter || {};\n\n  const nodes: NodeData[] = [];\n  const edges: EdgeData[] = [];\n\n  dfs(\n    treeData,\n    (node, depth) => {\n      nodes.push(getNodeData(node, depth));\n      const children = getChildren(node);\n      for (const child of children) {\n        edges.push(getEdgeData(node, child));\n      }\n    },\n    (node) => getChildren(node),\n    'TB',\n  );\n\n  return { nodes, edges };\n}\n"
  },
  {
    "path": "packages/g6/src/utils/vector.ts",
    "content": "import type { Vector2, Vector3 } from '../types';\nimport { isVector2, isVector3 } from './is';\nimport { format } from './print';\n\nconst VECTOR_ZERO: Vector3 = [0, 0, 0];\n\n/**\n * <zh/> 填充两个向量至相同维度\n *\n * <en/> Pads two vectors to the same dimension\n * @param a - <zh/> 第一个向量 | <en/> The first vector\n * @param b - <zh/> 第二个向量 | <en/> The second vector\n * @returns <zh/> 两个向量填充后的结果 | <en/> The result of padded vectors\n */\nfunction padVectors(a: Vector2 | Vector3, b: Vector2 | Vector3): [Vector2 | Vector3, Vector2 | Vector3] {\n  if (a.length == b.length) {\n    return [a, b];\n  } else {\n    if ((isVector3(a) && a[2] !== 0) || (isVector3(b) && b[2] !== 0)) {\n      throw new Error(format('Vectors could not operate due to different dimensions.'));\n    }\n    return [toVector2(a), toVector2(b)];\n  }\n}\n\n/**\n * <zh/> 两个向量求和\n *\n * <en/> Adds two vectors\n * @param a - <zh/> 第一个向量 | <en/> The first vector\n * @param b - <zh/> 第二个向量 | <en/> The second vector\n * @returns <zh/> 两个向量的和 | <en/> The sum of the two vectors\n */\nexport function add(a: Vector2 | Vector3, b: Vector2 | Vector3): Vector2 | Vector3 {\n  [a, b] = padVectors(a, b);\n  return a.map((v, i) => v + b[i]) as Vector2 | Vector3;\n}\n\n/**\n * <zh/> 两个向量求差\n *\n * <en/> Subtracts two vectors\n * @param a - <zh/> 第一个向量 | <en/> The first vector\n * @param b - <zh/> 第二个向量 | <en/> The second vector\n * @returns <zh/> 两个向量的差 | <en/> The difference of the two vectors\n */\nexport function subtract(a: Vector2 | Vector3, b: Vector2 | Vector3): Vector2 | Vector3 {\n  [a, b] = padVectors(a, b);\n  return a.map((v, i) => v - b[i]) as Vector2 | Vector3;\n}\n\n/**\n * <zh/> 两个向量求积或者向量和标量求积\n *\n * <en/> Multiplies two vectors or a vector and a scalar\n * @param a - <zh/> 向量 | <en/> The vector\n * @param b - <zh/> 向量或者标量 | <en/> The vector or scalar\n * @returns <zh/> 两个向量的积或者向量和标量的积 | <en/> The product of the two vectors or the product of the vector and scalar\n */\nexport function multiply(a: Vector2 | Vector3, b: number | Vector2 | Vector3): Vector2 | Vector3 {\n  if (typeof b === 'number') return a.map((v) => v * (b as number)) as Vector2 | Vector3;\n  [a, b] = padVectors(a, b);\n  return a.map((v, i) => v * b[i]) as Vector2 | Vector3;\n}\n\n/**\n * <zh/> 两个向量求商或者向量和标量求商\n *\n * <en/> Divides two vectors or a vector and a scalar\n * @param a - <zh/> 向量 | <en/> The vector\n * @param b - <zh/> 向量或者标量 | <en/> The vector or scalar\n * @returns <zh/> 两个向量的商或者向量和标量的商 | <en/> The quotient of the two vectors or the quotient of the vector and scalar\n */\nexport function divide(a: Vector2 | Vector3, b: number | Vector2 | Vector3): Vector2 | Vector3 {\n  if (typeof b === 'number') return a.map((v) => v / (b as number)) as Vector2 | Vector3;\n  [a, b] = padVectors(a, b);\n  return a.map((v, i) => {\n    if (b[i] == 0) {\n      throw new Error(format('Vector could not be divided by zero'));\n    }\n    return v / b[i];\n  }) as Vector2 | Vector3;\n}\n\n/**\n * <zh/> 两个向量求点积\n *\n * <en/> Calculates the dot product of two vectors\n * @param a - <zh/> 第一个向量 | <en/> The first vector\n * @param b - <zh/> 第二个向量 | <en/> The second vector\n * @returns <zh/> 两个向量的点积 | <en/> The dot product of the two vectors\n */\nexport function dot(a: Vector2 | Vector3, b: Vector2 | Vector3): number {\n  [a, b] = padVectors(a, b);\n  return (a as number[]).reduce((sum, v, i) => sum + v * b[i], 0);\n}\n\n/**\n * <zh/> 两个二维向量求叉积\n *\n * <en/> Calculates the cross product of two vectors in three-dimensional Euclidean space\n * @param a - <zh/> 第一个向量 | <en/> The first vector\n * @param b - <zh/> 第二个向量 | <en/> The second vector\n * @returns <zh/> 两个向量的叉积 | <en/> The cross product of the two vectors\n */\nexport function cross(a: Vector2 | Vector3, b: Vector2 | Vector3): Vector3 {\n  const a2 = toVector3(a);\n  const b2 = toVector3(b);\n  return [a2[1] * b2[2] - a2[2] * b2[1], a2[2] * b2[0] - a2[0] * b2[2], a2[0] * b2[1] - a2[1] * b2[0]];\n}\n\n/**\n * <zh/> 向量缩放\n *\n * <en/> Scales a vector by a scalar number\n * @param a  - <zh/> 向量 | <en/> The vector to scale\n * @param s - <zh/> 缩放系数 | <en/> Scale factor\n * @returns <zh/> 缩放后的向量 | <en/> The scaled vector\n */\nexport function scale(a: Vector2 | Vector3, s: number): Vector2 | Vector3 {\n  return a.map((v) => v * s) as Vector2 | Vector3;\n}\n\n/**\n * <zh/> 计算两个向量间的欧几里得距离\n *\n * <en/> Calculates the Euclidean distance between two vectors\n * @param a - <zh/> 第一个向量 | <en/> The first vector\n * @param b - <zh/> 第二个向量 | <en/> The second vector\n * @returns <zh/> 两个向量间的距离 | <en/> The distance between the two vectors\n */\nexport function distance(a: Vector2 | Vector3, b: Vector2 | Vector3): number {\n  [a, b] = padVectors(a, b);\n  return Math.sqrt((a as number[]).reduce((sum, v, i) => sum + (v - b[i]) ** 2, 0));\n}\n\n/**\n * <zh/> 计算两个向量间的曼哈顿距离\n *\n * <en/> Calculates the Manhattan distance between two vectors\n * @param a - <zh/> 第一个向量 | <en/> The first vector\n * @param b - <zh/> 第二个向量 | <en/> The second vector\n * @returns <zh/> 两个向量间的距离 | <en/> The distance between the two vectors\n */\nexport function manhattanDistance(a: Vector2 | Vector3, b: Vector2 | Vector3): number {\n  [a, b] = padVectors(a, b);\n  return (a as number[]).reduce((sum, v, i) => sum + Math.abs(v - b[i]), 0);\n}\n\n/**\n * <zh/> 标准化向量（使长度为 1）\n *\n * <en/> Normalizes a vector (making its length 1)\n * @param a - <zh/> 要标准化的向量 | <en/> The vector to normalize\n * @returns <zh/> 标准化后的向量 | <en/> The normalized vector\n */\nexport function normalize(a: Vector2 | Vector3): Vector2 | Vector3 {\n  const length = (a as number[]).reduce((sum, v) => sum + v ** 2, 0);\n  return a.map((v) => v / Math.sqrt(length)) as Vector2 | Vector3;\n}\n\n/**\n * <zh/> 计算两个向量间的夹角，输出为锐角余弦值\n *\n * <en/> Get the angle between two vectors\n * @param a - <zh/> 第一个向量 | <en/> The first vector\n * @param b - <zh/> 第二个向量 | <en/> The second vector\n * @param clockwise - <zh/> 是否顺时针 | <en/> Whether to calculate the angle in a clockwise direction\n * @returns  <zh/> 弧度值 | <en/> The angle in radians\n */\nexport function angle(a: Vector2 | Vector3, b: Vector2 | Vector3, clockwise = false): number {\n  [a, b] = padVectors(a, b);\n  const determinant = a[0] * b[1] - a[1] * b[0];\n  let angle = Math.acos(\n    (multiply(a, b) as number[]).reduce((sum: number, v: number) => sum + v, 0) /\n      (distance(a, VECTOR_ZERO) * distance(b, VECTOR_ZERO)),\n  );\n  // If clockwise is true and determinant is negative, adjust the angle\n  if (clockwise && determinant < 0) {\n    angle = 2 * Math.PI - angle;\n  }\n  return angle;\n}\n\n/**\n * <zh/> 判断两个向量是否完全相等（使用 === 比较）\n *\n * <en/> Returns whether or not the vectors exactly have the same elements in the same position (when compared with ===)\n * @param a - <zh/> 第一个向量 | <en/> The first vector\n * @param b - <zh/> 第二个向量 | <en/> The second vector\n * @returns  - <zh/> 是否相等 | <en/> Whether or not the vectors are equal\n */\nexport function exactEquals(a: Vector2 | Vector3, b: Vector2 | Vector3): boolean {\n  return (a as number[]).every((v, i) => v === b[i]);\n}\n\n/**\n * <zh/> 计算向量的垂直向量\n *\n * <en/> Calculates the perpendicular vector to a given vector\n * @param a - <zh/> 原始向量 | <en/> The original vector\n * @param clockwise - <zh/> 是否顺时针 | <en/> Whether to calculate the perpendicular vector in a clockwise direction\n * @returns <zh/> 原始向量的垂直向量 | <en/> The perpendicular vector to the original vector\n */\nexport function perpendicular(a: Vector2, clockwise = true): Vector2 {\n  return clockwise ? [-a[1], a[0]] : [a[1], -a[0]];\n}\n\n/**\n * <zh/> 计算向量的模\n *\n * <en/> Calculates the modulus of a vector\n * @param a - <zh/> 原始向量 | <en/> The original vector\n * @param b - <zh/> 模 | <en/> The modulus\n * @returns - <zh/> 向量的模 | <en/> The modulus of the vector\n */\nexport function mod(a: Vector2 | Vector3, b: number): Vector2 | Vector3 {\n  return a.map((v) => v % b) as Vector2 | Vector3;\n}\n\n/**\n * <zh/> 向量强制转换为二维向量\n *\n * <en/> Force vector to be two-dimensional\n * @param a - <zh/> 原始向量 | <en/> The original vector\n * @returns <zh/> 二维向量 | <en/> Two-dimensional vector\n */\nexport function toVector2(a: Vector2 | Vector3): Vector2 {\n  return [a[0], a[1]];\n}\n\n/**\n * <zh/> 向量强制转换为三维向量\n *\n * <en/> Force vector to be three-dimensional\n * @param a - <zh/> 原始向量 | <en/> The original vector\n * @returns  - <zh/> 三维向量 | <en/> Three-dimensional vector\n */\nexport function toVector3(a: Vector2 | Vector3): Vector3 {\n  return isVector2(a) ? [a[0], a[1], 0] : a;\n}\n\n/**\n * <zh/> 计算向量与 x 轴正方向的夹角（弧度制）\n *\n * <en/> The angle between the vector and the positive direction of the x-axis (radians)\n * @param a - <zh/> 向量 | <en/> The vector\n * @returns <zh/> 弧度值 | <en/> The angle in radians\n */\nexport function rad(a: Vector2 | Vector3): number {\n  const [x, y] = a;\n  if (!x && !y) return 0;\n  return Math.atan2(y, x);\n}\n\n/**\n * <zh/> 旋转向量（角度制）\n *\n * <en/> Rotational vector (Angle system)\n * @param a - <zh/> 向量 | <en/> The vector\n * @param angle - <zh/> 旋转角度 | <en/> The rotation angle\n * @returns <zh/> 向量 | <en/> The vector\n */\nexport function rotate(a: Vector2, angle: number): Vector2 {\n  const [dx, dy] = a;\n  if (angle % 360 === 0) return [dx, dy];\n  const rad = (angle * Math.PI) / 180;\n  const cos = Math.cos(rad);\n  const sin = Math.sin(rad);\n  return [dx * cos - dy * sin, dx * sin + dy * cos];\n}\n"
  },
  {
    "path": "packages/g6/src/utils/visibility.ts",
    "content": "import type { BaseStyleProps, DisplayObject } from '@antv/g';\n\n/**\n * <zh/> 设置图形实例的可见性\n *\n * <en/> Set the visibility of the shape instance\n * @param shape - <zh/> 图形实例 | <en/> shape instance\n * @param value - <zh/> 可见性 | <en/> visibility\n * @param filter - <zh/> 筛选出需要设置可见性的图形 | <en/> Filter out the shapes that need to set visibility\n * @remarks\n * <zh/> 在设置 enableCSSParsing 为 false 的情况下，复合图形无法继承父属性，因此需要对所有子图形应用相同的可见性\n *\n * <en/> After setting enableCSSParsing to false, the compound shape cannot inherit the parent attribute, so the same visibility needs to be applied to all child shapes\n */\nexport function setVisibility(\n  shape: DisplayObject,\n  value: BaseStyleProps['visibility'],\n  filter?: (shape: DisplayObject) => boolean,\n) {\n  const callback = (node: DisplayObject) => {\n    if (filter && !filter(node)) return;\n    node.style.visibility = value;\n  };\n\n  shape.forEach((node) => {\n    callback(node as DisplayObject);\n  });\n}\n"
  },
  {
    "path": "packages/g6/src/utils/z-index.ts",
    "content": "import { ElementDatum } from '../types';\n\n/**\n * <zh/> 获取元素的 zIndex\n * <en/> Get the zIndex of the element\n * @param datum - <zh/> 元素数据 | <en/> element data\n * @returns - <zh/> zIndex | <en/> zIndex\n */\nexport function getZIndexOf(datum: ElementDatum): number {\n  return datum?.style?.zIndex || 0;\n}\n"
  },
  {
    "path": "packages/g6/src/version.ts",
    "content": "export const version = '5.1.0';\n"
  },
  {
    "path": "packages/g6/tsconfig.build.json",
    "content": "{\n  \"compilerOptions\": {\n    \"paths\": {}\n  },\n  \"include\": [\"src/**/*\"],\n  \"extends\": \"./tsconfig.json\"\n}\n"
  },
  {
    "path": "packages/g6/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"experimentalDecorators\": true,\n    \"lib\": [\"DOM\", \"ESNext\"],\n    \"outDir\": \"lib\",\n    \"paths\": {\n      \"@/*\": [\"./*\"],\n      \"@@/*\": [\"__tests__/*\"],\n      \"@antv/g6\": [\"./src/index.ts\"]\n    }\n  },\n  \"exclude\": [\"node_modules\", \"dist\", \"lib\", \"esm\"],\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src/**/*\", \"__tests__/**/*\"]\n}\n"
  },
  {
    "path": "packages/g6/tsdoc.json",
    "content": "{\n  \"$schema\": \"https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json\",\n  \"noStandardTags\": true,\n  \"tagDefinitions\": [\n    {\n      \"tagName\": \"@alpha\",\n      \"syntaxKind\": \"modifier\"\n    },\n    {\n      \"tagName\": \"@beta\",\n      \"syntaxKind\": \"modifier\"\n    },\n    {\n      \"tagName\": \"@defaultValue\",\n      \"syntaxKind\": \"block\"\n    },\n    {\n      \"tagName\": \"@decorator\",\n      \"syntaxKind\": \"block\",\n      \"allowMultiple\": true\n    },\n    {\n      \"tagName\": \"@deprecated\",\n      \"syntaxKind\": \"block\"\n    },\n    {\n      \"tagName\": \"@eventProperty\",\n      \"syntaxKind\": \"modifier\"\n    },\n    {\n      \"tagName\": \"@example\",\n      \"syntaxKind\": \"block\",\n      \"allowMultiple\": true\n    },\n    {\n      \"tagName\": \"@experimental\",\n      \"syntaxKind\": \"modifier\"\n    },\n    {\n      \"tagName\": \"@inheritDoc\",\n      \"syntaxKind\": \"inline\"\n    },\n    {\n      \"tagName\": \"@internal\",\n      \"syntaxKind\": \"modifier\"\n    },\n    {\n      \"tagName\": \"@label\",\n      \"syntaxKind\": \"inline\"\n    },\n    {\n      \"tagName\": \"@link\",\n      \"syntaxKind\": \"inline\",\n      \"allowMultiple\": true\n    },\n    {\n      \"tagName\": \"@override\",\n      \"syntaxKind\": \"modifier\"\n    },\n    {\n      \"tagName\": \"@packageDocumentation\",\n      \"syntaxKind\": \"modifier\"\n    },\n    {\n      \"tagName\": \"@param\",\n      \"syntaxKind\": \"block\",\n      \"allowMultiple\": true\n    },\n    {\n      \"tagName\": \"@privateRemarks\",\n      \"syntaxKind\": \"block\"\n    },\n    {\n      \"tagName\": \"@public\",\n      \"syntaxKind\": \"modifier\"\n    },\n    {\n      \"tagName\": \"@readonly\",\n      \"syntaxKind\": \"modifier\"\n    },\n    {\n      \"tagName\": \"@remarks\",\n      \"syntaxKind\": \"block\"\n    },\n    {\n      \"tagName\": \"@returns\",\n      \"syntaxKind\": \"block\"\n    },\n    {\n      \"tagName\": \"@sealed\",\n      \"syntaxKind\": \"modifier\"\n    },\n    {\n      \"tagName\": \"@see\",\n      \"syntaxKind\": \"block\"\n    },\n    {\n      \"tagName\": \"@throws\",\n      \"syntaxKind\": \"block\",\n      \"allowMultiple\": true\n    },\n    {\n      \"tagName\": \"@typeParam\",\n      \"syntaxKind\": \"block\",\n      \"allowMultiple\": true\n    },\n    {\n      \"tagName\": \"@virtual\",\n      \"syntaxKind\": \"modifier\"\n    },\n    {\n      \"tagName\": \"@betaDocumentation\",\n      \"syntaxKind\": \"modifier\"\n    },\n    {\n      \"tagName\": \"@internalRemarks\",\n      \"syntaxKind\": \"block\"\n    },\n    {\n      \"tagName\": \"@preapproved\",\n      \"syntaxKind\": \"modifier\"\n    },\n    {\n      \"tagName\": \"@apiCategory\",\n      \"syntaxKind\": \"block\"\n    }\n  ],\n  \"supportForTags\": {\n    \"@alpha\": true,\n    \"@beta\": true,\n    \"@defaultValue\": true,\n    \"@decorator\": true,\n    \"@deprecated\": true,\n    \"@eventProperty\": true,\n    \"@example\": true,\n    \"@experimental\": true,\n    \"@inheritDoc\": true,\n    \"@internal\": true,\n    \"@label\": true,\n    \"@link\": true,\n    \"@override\": true,\n    \"@packageDocumentation\": true,\n    \"@param\": true,\n    \"@privateRemarks\": true,\n    \"@public\": true,\n    \"@readonly\": true,\n    \"@remarks\": true,\n    \"@returns\": true,\n    \"@sealed\": true,\n    \"@see\": true,\n    \"@throws\": true,\n    \"@typeParam\": true,\n    \"@virtual\": true,\n    \"@betaDocumentation\": true,\n    \"@internalRemarks\": true,\n    \"@preapproved\": true,\n    \"@apiCategory\": true\n  },\n  \"reportUnsupportedHtmlElements\": false\n}\n"
  },
  {
    "path": "packages/g6/vite.config.js",
    "content": "import path from 'path';\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n  root: './__tests__',\n  server: {\n    port: 8080,\n    open: '/',\n  },\n  optimizeDeps: {\n    // @see https://github.com/vitejs/vite/issues/10839#issuecomment-1345193175\n    // @see https://vitejs.dev/guide/dep-pre-bundling.html#customizing-the-behavior\n    // @see https://vitejs.dev/config/dep-optimization-options.html#optimizedeps-exclude\n    exclude: [],\n  },\n  plugins: [\n    {\n      name: 'isolation',\n      configureServer(server) {\n        // The multithreads version of @antv/layout-wasm needs to use SharedArrayBuffer, which should be used in a secure context.\n        // @see https://gist.github.com/mizchi/afcc5cf233c9e6943720fde4b4579a2b\n        server.middlewares.use((_req, res, next) => {\n          res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');\n          res.setHeader('Cross-Origin-Embedder-Policy', 'same-origin');\n          next();\n        });\n      },\n    },\n  ],\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, '.'),\n      '@@': path.resolve(__dirname, './__tests__'),\n      '@antv/g6': path.resolve(__dirname, './src'),\n    },\n  },\n});\n"
  },
  {
    "path": "packages/g6-extension-3d/README.md",
    "content": "## 3D extension for G6\n\n<img width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*lEL3TrCLnPsAAAAAAAAAAAAADmJ7AQ/original\" />\n<img width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*yFa5RKilu6kAAAAAAAAAAAAADmJ7AQ/original\" />\n\nThis extension package provides 3D elements, behaviors and plugins for G6.\n\n## Usage\n\n1. Install\n\n```bash\nnpm install @antv/g6-extension-3d\n```\n\n2. Import and Register\n\n> Where renderer, elements and lighting are necessary\n\n```js\nimport { ExtensionCategory, register } from '@antv/g6';\nimport { DragCanvas3D, Light, Line3D, Sphere, renderer } from '@antv/g6-extension-3d';\n\n// 3d light plugin\nregister(ExtensionCategory.PLUGIN, '3d-light', Light);\n// sphere node element\nregister(ExtensionCategory.NODE, 'sphere', Sphere);\n// line edge element\nregister(ExtensionCategory.EDGE, 'line3d', Line3D);\n// drag canvas in 3d scene\nregister(ExtensionCategory.BEHAVIOR, 'drag-canvas-3d', DragCanvas3D);\n// camera setting plugin\nregister(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting);\n```\n\n3. Use\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  renderer, // use 3d renderer\n  data: {\n    // your data\n  },\n  node: {\n    type: 'sphere', // use sphere node\n  },\n  edge: {\n    type: 'line3d', // use 3d line edge\n  },\n  behaviors: ['drag-canvas-3d'],\n  plugins: [\n    // set camera configs, see: https://g.antv.antgroup.com/en/api/camera/intro\n    {\n      type: 'camera-setting',\n      projectionMode: 'perspective',\n      near: 0.1,\n      far: 1000,\n      fov: 45,\n      aspect: 1,\n    },\n    // add directional light\n    {\n      type: '3d-light',\n      directional: {\n        direction: [0, 0, 1],\n      },\n    },\n  ],\n});\n```\n\n## Resources\n\n- [Lite Solar System](https://g6.antv.antgroup.com/en/examples/feature/default/#lite-solar-system)\n- [3D Node](https://g6.antv.antgroup.com/en/examples/element/node/#3d-node)\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/.eslintrc",
    "content": "{\n  \"rules\": {\n    \"no-console\": \"off\"\n  }\n}"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/dataset/cubic.json",
    "content": "{\n  \"nodes\": [\n    { \"id\": \"0\", \"style\": { \"x\": 0, \"y\": 0, \"labelText\": \"center\" } },\n    { \"id\": \"1\", \"style\": { \"x\": -50, \"y\": -50, \"z\": -50, \"labelText\": \"(-1, -1, -1)\" } },\n    { \"id\": \"2\", \"style\": { \"x\": -50, \"y\": 50, \"z\": -50, \"labelText\": \"(-1, 1, -1)\" } },\n    { \"id\": \"3\", \"style\": { \"x\": 50, \"y\": 50, \"z\": -50, \"labelText\": \"(1, 1, -1)\" } },\n    { \"id\": \"4\", \"style\": { \"x\": 50, \"y\": -50, \"z\": -50, \"labelText\": \"(1, -1, -1)\" } },\n    { \"id\": \"5\", \"style\": { \"x\": -50, \"y\": -50, \"z\": 50, \"labelText\": \"(-1, -1, 1)\" } },\n    { \"id\": \"6\", \"style\": { \"x\": -50, \"y\": 50, \"z\": 50, \"labelText\": \"(-1, 1, 1)\" } },\n    { \"id\": \"7\", \"style\": { \"x\": 50, \"y\": 50, \"z\": 50, \"labelText\": \"(1, 1, 1)\" } },\n    { \"id\": \"8\", \"style\": { \"x\": 50, \"y\": -50, \"z\": 50, \"labelText\": \"(1, -1, 1)\" } }\n  ]\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/dataset/force-3d.json",
    "content": "{\n  \"nodes\": [\n    {\n      \"id\": \"Myriel\",\n      \"data\": { \"group\": 1 }\n    },\n    {\n      \"id\": \"Napoleon\",\n      \"data\": { \"group\": 1 }\n    },\n    {\n      \"id\": \"Mlle.Baptistine\",\n      \"data\": { \"group\": 1 }\n    },\n    {\n      \"id\": \"Mme.Magloire\",\n      \"data\": { \"group\": 1 }\n    },\n    {\n      \"id\": \"CountessdeLo\",\n      \"data\": { \"group\": 1 }\n    },\n    {\n      \"id\": \"Geborand\",\n      \"data\": { \"group\": 1 }\n    },\n    {\n      \"id\": \"Champtercier\",\n      \"data\": { \"group\": 1 }\n    },\n    {\n      \"id\": \"Cravatte\",\n      \"data\": { \"group\": 1 }\n    },\n    {\n      \"id\": \"Count\",\n      \"data\": { \"group\": 1 }\n    },\n    {\n      \"id\": \"OldMan\",\n      \"data\": { \"group\": 1 }\n    },\n    {\n      \"id\": \"Labarre\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Valjean\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Marguerite\",\n      \"data\": { \"group\": 3 }\n    },\n    {\n      \"id\": \"Mme.deR\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Isabeau\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Gervais\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Tholomyes\",\n      \"data\": { \"group\": 3 }\n    },\n    {\n      \"id\": \"Listolier\",\n      \"data\": { \"group\": 3 }\n    },\n    {\n      \"id\": \"Fameuil\",\n      \"data\": { \"group\": 3 }\n    },\n    {\n      \"id\": \"Blacheville\",\n      \"data\": { \"group\": 3 }\n    },\n    {\n      \"id\": \"Favourite\",\n      \"data\": { \"group\": 3 }\n    },\n    {\n      \"id\": \"Dahlia\",\n      \"data\": { \"group\": 3 }\n    },\n    {\n      \"id\": \"Zephine\",\n      \"data\": { \"group\": 3 }\n    },\n    {\n      \"id\": \"Fantine\",\n      \"data\": { \"group\": 3 }\n    },\n    {\n      \"id\": \"Mme.Thenardier\",\n      \"data\": { \"group\": 4 }\n    },\n    {\n      \"id\": \"Thenardier\",\n      \"data\": { \"group\": 4 }\n    },\n    {\n      \"id\": \"Cosette\",\n      \"data\": { \"group\": 5 }\n    },\n    {\n      \"id\": \"Javert\",\n      \"data\": { \"group\": 4 }\n    },\n    {\n      \"id\": \"Fauchelevent\",\n      \"data\": { \"group\": 0 }\n    },\n    {\n      \"id\": \"Bamatabois\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Perpetue\",\n      \"data\": { \"group\": 3 }\n    },\n    {\n      \"id\": \"Simplice\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Scaufflaire\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Woman1\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Judge\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Champmathieu\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Brevet\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Chenildieu\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Cochepaille\",\n      \"data\": { \"group\": 2 }\n    },\n    {\n      \"id\": \"Pontmercy\",\n      \"data\": { \"group\": 4 }\n    },\n    {\n      \"id\": \"Boulatruelle\",\n      \"data\": { \"group\": 6 }\n    },\n    {\n      \"id\": \"Eponine\",\n      \"data\": { \"group\": 4 }\n    },\n    {\n      \"id\": \"Anzelma\",\n      \"data\": { \"group\": 4 }\n    },\n    {\n      \"id\": \"Woman2\",\n      \"data\": { \"group\": 5 }\n    },\n    {\n      \"id\": \"MotherInnocent\",\n      \"data\": { \"group\": 0 }\n    },\n    {\n      \"id\": \"Gribier\",\n      \"data\": { \"group\": 0 }\n    },\n    {\n      \"id\": \"Jondrette\",\n      \"data\": { \"group\": 7 }\n    },\n    {\n      \"id\": \"Mme.Burgon\",\n      \"data\": { \"group\": 7 }\n    },\n    {\n      \"id\": \"Gavroche\",\n      \"data\": { \"group\": 8 }\n    },\n    {\n      \"id\": \"Gillenormand\",\n      \"data\": { \"group\": 5 }\n    },\n    {\n      \"id\": \"Magnon\",\n      \"data\": { \"group\": 5 }\n    },\n    {\n      \"id\": \"Mlle.Gillenormand\",\n      \"data\": { \"group\": 5 }\n    },\n    {\n      \"id\": \"Mme.Pontmercy\",\n      \"data\": { \"group\": 5 }\n    },\n    {\n      \"id\": \"Mlle.Vaubois\",\n      \"data\": { \"group\": 5 }\n    },\n    {\n      \"id\": \"Lt.Gillenormand\",\n      \"data\": { \"group\": 5 }\n    },\n    {\n      \"id\": \"Marius\",\n      \"data\": { \"group\": 8 }\n    },\n    {\n      \"id\": \"BaronessT\",\n      \"data\": { \"group\": 5 }\n    },\n    {\n      \"id\": \"Mabeuf\",\n      \"data\": { \"group\": 8 }\n    },\n    {\n      \"id\": \"Enjolras\",\n      \"data\": { \"group\": 8 }\n    },\n    {\n      \"id\": \"Combeferre\",\n      \"data\": { \"group\": 8 }\n    },\n    {\n      \"id\": \"Prouvaire\",\n      \"data\": { \"group\": 8 }\n    },\n    {\n      \"id\": \"Feuilly\",\n      \"data\": { \"group\": 8 }\n    },\n    {\n      \"id\": \"Courfeyrac\",\n      \"data\": { \"group\": 8 }\n    },\n    {\n      \"id\": \"Bahorel\",\n      \"data\": { \"group\": 8 }\n    },\n    {\n      \"id\": \"Bossuet\",\n      \"data\": { \"group\": 8 }\n    },\n    {\n      \"id\": \"Joly\",\n      \"data\": { \"group\": 8 }\n    },\n    {\n      \"id\": \"Grantaire\",\n      \"data\": { \"group\": 8 }\n    },\n    {\n      \"id\": \"MotherPlutarch\",\n      \"data\": { \"group\": 9 }\n    },\n    {\n      \"id\": \"Gueulemer\",\n      \"data\": { \"group\": 4 }\n    },\n    {\n      \"id\": \"Babet\",\n      \"data\": { \"group\": 4 }\n    },\n    {\n      \"id\": \"Claquesous\",\n      \"data\": { \"group\": 4 }\n    },\n    {\n      \"id\": \"Montparnasse\",\n      \"data\": { \"group\": 4 }\n    },\n    {\n      \"id\": \"Toussaint\",\n      \"data\": { \"group\": 5 }\n    },\n    {\n      \"id\": \"Child1\",\n      \"data\": { \"group\": 10 }\n    },\n    {\n      \"id\": \"Child2\",\n      \"data\": { \"group\": 10 }\n    },\n    {\n      \"id\": \"Brujon\",\n      \"data\": { \"group\": 4 }\n    },\n    {\n      \"id\": \"Mme.Hucheloup\",\n      \"data\": { \"group\": 8 }\n    }\n  ],\n  \"edges\": [\n    {\n      \"id\": \"Napoleon-Myriel\",\n      \"source\": \"Napoleon\",\n      \"target\": \"Myriel\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mlle.Baptistine-Myriel\",\n      \"source\": \"Mlle.Baptistine\",\n      \"target\": \"Myriel\",\n      \"data\": { \"value\": 8 }\n    },\n    {\n      \"id\": \"Mme.Magloire-Myriel\",\n      \"source\": \"Mme.Magloire\",\n      \"target\": \"Myriel\",\n      \"data\": { \"value\": 10 }\n    },\n    {\n      \"id\": \"Mme.Magloire-Mlle.Baptistine\",\n      \"source\": \"Mme.Magloire\",\n      \"target\": \"Mlle.Baptistine\",\n      \"data\": { \"value\": 6 }\n    },\n    {\n      \"id\": \"CountessdeLo-Myriel\",\n      \"source\": \"CountessdeLo\",\n      \"target\": \"Myriel\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Geborand-Myriel\",\n      \"source\": \"Geborand\",\n      \"target\": \"Myriel\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Champtercier-Myriel\",\n      \"source\": \"Champtercier\",\n      \"target\": \"Myriel\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Cravatte-Myriel\",\n      \"source\": \"Cravatte\",\n      \"target\": \"Myriel\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Count-Myriel\",\n      \"source\": \"Count\",\n      \"target\": \"Myriel\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"OldMan-Myriel\",\n      \"source\": \"OldMan\",\n      \"target\": \"Myriel\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Valjean-Labarre\",\n      \"source\": \"Valjean\",\n      \"target\": \"Labarre\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Valjean-Mme.Magloire\",\n      \"source\": \"Valjean\",\n      \"target\": \"Mme.Magloire\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Valjean-Mlle.Baptistine\",\n      \"source\": \"Valjean\",\n      \"target\": \"Mlle.Baptistine\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Valjean-Myriel\",\n      \"source\": \"Valjean\",\n      \"target\": \"Myriel\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Marguerite-Valjean\",\n      \"source\": \"Marguerite\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mme.deR-Valjean\",\n      \"source\": \"Mme.deR\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Isabeau-Valjean\",\n      \"source\": \"Isabeau\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Gervais-Valjean\",\n      \"source\": \"Gervais\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Listolier-Tholomyes\",\n      \"source\": \"Listolier\",\n      \"target\": \"Tholomyes\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Fameuil-Tholomyes\",\n      \"source\": \"Fameuil\",\n      \"target\": \"Tholomyes\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Fameuil-Listolier\",\n      \"source\": \"Fameuil\",\n      \"target\": \"Listolier\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Blacheville-Tholomyes\",\n      \"source\": \"Blacheville\",\n      \"target\": \"Tholomyes\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Blacheville-Listolier\",\n      \"source\": \"Blacheville\",\n      \"target\": \"Listolier\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Blacheville-Fameuil\",\n      \"source\": \"Blacheville\",\n      \"target\": \"Fameuil\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Favourite-Tholomyes\",\n      \"source\": \"Favourite\",\n      \"target\": \"Tholomyes\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Favourite-Listolier\",\n      \"source\": \"Favourite\",\n      \"target\": \"Listolier\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Favourite-Fameuil\",\n      \"source\": \"Favourite\",\n      \"target\": \"Fameuil\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Favourite-Blacheville\",\n      \"source\": \"Favourite\",\n      \"target\": \"Blacheville\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Dahlia-Tholomyes\",\n      \"source\": \"Dahlia\",\n      \"target\": \"Tholomyes\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Dahlia-Listolier\",\n      \"source\": \"Dahlia\",\n      \"target\": \"Listolier\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Dahlia-Fameuil\",\n      \"source\": \"Dahlia\",\n      \"target\": \"Fameuil\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Dahlia-Blacheville\",\n      \"source\": \"Dahlia\",\n      \"target\": \"Blacheville\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Dahlia-Favourite\",\n      \"source\": \"Dahlia\",\n      \"target\": \"Favourite\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Zephine-Tholomyes\",\n      \"source\": \"Zephine\",\n      \"target\": \"Tholomyes\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Zephine-Listolier\",\n      \"source\": \"Zephine\",\n      \"target\": \"Listolier\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Zephine-Fameuil\",\n      \"source\": \"Zephine\",\n      \"target\": \"Fameuil\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Zephine-Blacheville\",\n      \"source\": \"Zephine\",\n      \"target\": \"Blacheville\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Zephine-Favourite\",\n      \"source\": \"Zephine\",\n      \"target\": \"Favourite\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Zephine-Dahlia\",\n      \"source\": \"Zephine\",\n      \"target\": \"Dahlia\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Fantine-Tholomyes\",\n      \"source\": \"Fantine\",\n      \"target\": \"Tholomyes\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Fantine-Listolier\",\n      \"source\": \"Fantine\",\n      \"target\": \"Listolier\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Fantine-Fameuil\",\n      \"source\": \"Fantine\",\n      \"target\": \"Fameuil\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Fantine-Blacheville\",\n      \"source\": \"Fantine\",\n      \"target\": \"Blacheville\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Fantine-Favourite\",\n      \"source\": \"Fantine\",\n      \"target\": \"Favourite\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Fantine-Dahlia\",\n      \"source\": \"Fantine\",\n      \"target\": \"Dahlia\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Fantine-Zephine\",\n      \"source\": \"Fantine\",\n      \"target\": \"Zephine\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Fantine-Marguerite\",\n      \"source\": \"Fantine\",\n      \"target\": \"Marguerite\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Fantine-Valjean\",\n      \"source\": \"Fantine\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 9 }\n    },\n    {\n      \"id\": \"Mme.Thenardier-Fantine\",\n      \"source\": \"Mme.Thenardier\",\n      \"target\": \"Fantine\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Mme.Thenardier-Valjean\",\n      \"source\": \"Mme.Thenardier\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 7 }\n    },\n    {\n      \"id\": \"Thenardier-Mme.Thenardier\",\n      \"source\": \"Thenardier\",\n      \"target\": \"Mme.Thenardier\",\n      \"data\": { \"value\": 13 }\n    },\n    {\n      \"id\": \"Thenardier-Fantine\",\n      \"source\": \"Thenardier\",\n      \"target\": \"Fantine\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Thenardier-Valjean\",\n      \"source\": \"Thenardier\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 12 }\n    },\n    {\n      \"id\": \"Cosette-Mme.Thenardier\",\n      \"source\": \"Cosette\",\n      \"target\": \"Mme.Thenardier\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Cosette-Valjean\",\n      \"source\": \"Cosette\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 31 }\n    },\n    {\n      \"id\": \"Cosette-Tholomyes\",\n      \"source\": \"Cosette\",\n      \"target\": \"Tholomyes\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Cosette-Thenardier\",\n      \"source\": \"Cosette\",\n      \"target\": \"Thenardier\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Javert-Valjean\",\n      \"source\": \"Javert\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 17 }\n    },\n    {\n      \"id\": \"Javert-Fantine\",\n      \"source\": \"Javert\",\n      \"target\": \"Fantine\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Javert-Thenardier\",\n      \"source\": \"Javert\",\n      \"target\": \"Thenardier\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Javert-Mme.Thenardier\",\n      \"source\": \"Javert\",\n      \"target\": \"Mme.Thenardier\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Javert-Cosette\",\n      \"source\": \"Javert\",\n      \"target\": \"Cosette\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Fauchelevent-Valjean\",\n      \"source\": \"Fauchelevent\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 8 }\n    },\n    {\n      \"id\": \"Fauchelevent-Javert\",\n      \"source\": \"Fauchelevent\",\n      \"target\": \"Javert\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Bamatabois-Fantine\",\n      \"source\": \"Bamatabois\",\n      \"target\": \"Fantine\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Bamatabois-Javert\",\n      \"source\": \"Bamatabois\",\n      \"target\": \"Javert\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Bamatabois-Valjean\",\n      \"source\": \"Bamatabois\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Perpetue-Fantine\",\n      \"source\": \"Perpetue\",\n      \"target\": \"Fantine\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Simplice-Perpetue\",\n      \"source\": \"Simplice\",\n      \"target\": \"Perpetue\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Simplice-Valjean\",\n      \"source\": \"Simplice\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Simplice-Fantine\",\n      \"source\": \"Simplice\",\n      \"target\": \"Fantine\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Simplice-Javert\",\n      \"source\": \"Simplice\",\n      \"target\": \"Javert\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Scaufflaire-Valjean\",\n      \"source\": \"Scaufflaire\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Woman1-Valjean\",\n      \"source\": \"Woman1\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Woman1-Javert\",\n      \"source\": \"Woman1\",\n      \"target\": \"Javert\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Judge-Valjean\",\n      \"source\": \"Judge\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Judge-Bamatabois\",\n      \"source\": \"Judge\",\n      \"target\": \"Bamatabois\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Champmathieu-Valjean\",\n      \"source\": \"Champmathieu\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Champmathieu-Judge\",\n      \"source\": \"Champmathieu\",\n      \"target\": \"Judge\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Champmathieu-Bamatabois\",\n      \"source\": \"Champmathieu\",\n      \"target\": \"Bamatabois\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Brevet-Judge\",\n      \"source\": \"Brevet\",\n      \"target\": \"Judge\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Brevet-Champmathieu\",\n      \"source\": \"Brevet\",\n      \"target\": \"Champmathieu\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Brevet-Valjean\",\n      \"source\": \"Brevet\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Brevet-Bamatabois\",\n      \"source\": \"Brevet\",\n      \"target\": \"Bamatabois\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Chenildieu-Judge\",\n      \"source\": \"Chenildieu\",\n      \"target\": \"Judge\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Chenildieu-Champmathieu\",\n      \"source\": \"Chenildieu\",\n      \"target\": \"Champmathieu\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Chenildieu-Brevet\",\n      \"source\": \"Chenildieu\",\n      \"target\": \"Brevet\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Chenildieu-Valjean\",\n      \"source\": \"Chenildieu\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Chenildieu-Bamatabois\",\n      \"source\": \"Chenildieu\",\n      \"target\": \"Bamatabois\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Cochepaille-Judge\",\n      \"source\": \"Cochepaille\",\n      \"target\": \"Judge\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Cochepaille-Champmathieu\",\n      \"source\": \"Cochepaille\",\n      \"target\": \"Champmathieu\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Cochepaille-Brevet\",\n      \"source\": \"Cochepaille\",\n      \"target\": \"Brevet\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Cochepaille-Chenildieu\",\n      \"source\": \"Cochepaille\",\n      \"target\": \"Chenildieu\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Cochepaille-Valjean\",\n      \"source\": \"Cochepaille\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Cochepaille-Bamatabois\",\n      \"source\": \"Cochepaille\",\n      \"target\": \"Bamatabois\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Pontmercy-Thenardier\",\n      \"source\": \"Pontmercy\",\n      \"target\": \"Thenardier\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Boulatruelle-Thenardier\",\n      \"source\": \"Boulatruelle\",\n      \"target\": \"Thenardier\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Eponine-Mme.Thenardier\",\n      \"source\": \"Eponine\",\n      \"target\": \"Mme.Thenardier\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Eponine-Thenardier\",\n      \"source\": \"Eponine\",\n      \"target\": \"Thenardier\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Anzelma-Eponine\",\n      \"source\": \"Anzelma\",\n      \"target\": \"Eponine\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Anzelma-Thenardier\",\n      \"source\": \"Anzelma\",\n      \"target\": \"Thenardier\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Anzelma-Mme.Thenardier\",\n      \"source\": \"Anzelma\",\n      \"target\": \"Mme.Thenardier\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Woman2-Valjean\",\n      \"source\": \"Woman2\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Woman2-Cosette\",\n      \"source\": \"Woman2\",\n      \"target\": \"Cosette\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Woman2-Javert\",\n      \"source\": \"Woman2\",\n      \"target\": \"Javert\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"MotherInnocent-Fauchelevent\",\n      \"source\": \"MotherInnocent\",\n      \"target\": \"Fauchelevent\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"MotherInnocent-Valjean\",\n      \"source\": \"MotherInnocent\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Gribier-Fauchelevent\",\n      \"source\": \"Gribier\",\n      \"target\": \"Fauchelevent\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Mme.Burgon-Jondrette\",\n      \"source\": \"Mme.Burgon\",\n      \"target\": \"Jondrette\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Gavroche-Mme.Burgon\",\n      \"source\": \"Gavroche\",\n      \"target\": \"Mme.Burgon\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Gavroche-Thenardier\",\n      \"source\": \"Gavroche\",\n      \"target\": \"Thenardier\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Gavroche-Javert\",\n      \"source\": \"Gavroche\",\n      \"target\": \"Javert\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Gavroche-Valjean\",\n      \"source\": \"Gavroche\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Gillenormand-Cosette\",\n      \"source\": \"Gillenormand\",\n      \"target\": \"Cosette\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Gillenormand-Valjean\",\n      \"source\": \"Gillenormand\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Magnon-Gillenormand\",\n      \"source\": \"Magnon\",\n      \"target\": \"Gillenormand\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Magnon-Mme.Thenardier\",\n      \"source\": \"Magnon\",\n      \"target\": \"Mme.Thenardier\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mlle.Gillenormand-Gillenormand\",\n      \"source\": \"Mlle.Gillenormand\",\n      \"target\": \"Gillenormand\",\n      \"data\": { \"value\": 9 }\n    },\n    {\n      \"id\": \"Mlle.Gillenormand-Cosette\",\n      \"source\": \"Mlle.Gillenormand\",\n      \"target\": \"Cosette\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Mlle.Gillenormand-Valjean\",\n      \"source\": \"Mlle.Gillenormand\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Mme.Pontmercy-Mlle.Gillenormand\",\n      \"source\": \"Mme.Pontmercy\",\n      \"target\": \"Mlle.Gillenormand\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mme.Pontmercy-Pontmercy\",\n      \"source\": \"Mme.Pontmercy\",\n      \"target\": \"Pontmercy\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mlle.Vaubois-Mlle.Gillenormand\",\n      \"source\": \"Mlle.Vaubois\",\n      \"target\": \"Mlle.Gillenormand\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Lt.Gillenormand-Mlle.Gillenormand\",\n      \"source\": \"Lt.Gillenormand\",\n      \"target\": \"Mlle.Gillenormand\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Lt.Gillenormand-Gillenormand\",\n      \"source\": \"Lt.Gillenormand\",\n      \"target\": \"Gillenormand\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Lt.Gillenormand-Cosette\",\n      \"source\": \"Lt.Gillenormand\",\n      \"target\": \"Cosette\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Marius-Mlle.Gillenormand\",\n      \"source\": \"Marius\",\n      \"target\": \"Mlle.Gillenormand\",\n      \"data\": { \"value\": 6 }\n    },\n    {\n      \"id\": \"Marius-Gillenormand\",\n      \"source\": \"Marius\",\n      \"target\": \"Gillenormand\",\n      \"data\": { \"value\": 12 }\n    },\n    {\n      \"id\": \"Marius-Pontmercy\",\n      \"source\": \"Marius\",\n      \"target\": \"Pontmercy\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Marius-Lt.Gillenormand\",\n      \"source\": \"Marius\",\n      \"target\": \"Lt.Gillenormand\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Marius-Cosette\",\n      \"source\": \"Marius\",\n      \"target\": \"Cosette\",\n      \"data\": { \"value\": 21 }\n    },\n    {\n      \"id\": \"Marius-Valjean\",\n      \"source\": \"Marius\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 19 }\n    },\n    {\n      \"id\": \"Marius-Tholomyes\",\n      \"source\": \"Marius\",\n      \"target\": \"Tholomyes\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Marius-Thenardier\",\n      \"source\": \"Marius\",\n      \"target\": \"Thenardier\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Marius-Eponine\",\n      \"source\": \"Marius\",\n      \"target\": \"Eponine\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Marius-Gavroche\",\n      \"source\": \"Marius\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"BaronessT-Gillenormand\",\n      \"source\": \"BaronessT\",\n      \"target\": \"Gillenormand\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"BaronessT-Marius\",\n      \"source\": \"BaronessT\",\n      \"target\": \"Marius\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mabeuf-Marius\",\n      \"source\": \"Mabeuf\",\n      \"target\": \"Marius\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mabeuf-Eponine\",\n      \"source\": \"Mabeuf\",\n      \"target\": \"Eponine\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mabeuf-Gavroche\",\n      \"source\": \"Mabeuf\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Enjolras-Marius\",\n      \"source\": \"Enjolras\",\n      \"target\": \"Marius\",\n      \"data\": { \"value\": 7 }\n    },\n    {\n      \"id\": \"Enjolras-Gavroche\",\n      \"source\": \"Enjolras\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 7 }\n    },\n    {\n      \"id\": \"Enjolras-Javert\",\n      \"source\": \"Enjolras\",\n      \"target\": \"Javert\",\n      \"data\": { \"value\": 6 }\n    },\n    {\n      \"id\": \"Enjolras-Mabeuf\",\n      \"source\": \"Enjolras\",\n      \"target\": \"Mabeuf\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Enjolras-Valjean\",\n      \"source\": \"Enjolras\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Combeferre-Enjolras\",\n      \"source\": \"Combeferre\",\n      \"target\": \"Enjolras\",\n      \"data\": { \"value\": 15 }\n    },\n    {\n      \"id\": \"Combeferre-Marius\",\n      \"source\": \"Combeferre\",\n      \"target\": \"Marius\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Combeferre-Gavroche\",\n      \"source\": \"Combeferre\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 6 }\n    },\n    {\n      \"id\": \"Combeferre-Mabeuf\",\n      \"source\": \"Combeferre\",\n      \"target\": \"Mabeuf\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Prouvaire-Gavroche\",\n      \"source\": \"Prouvaire\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Prouvaire-Enjolras\",\n      \"source\": \"Prouvaire\",\n      \"target\": \"Enjolras\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Prouvaire-Combeferre\",\n      \"source\": \"Prouvaire\",\n      \"target\": \"Combeferre\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Feuilly-Gavroche\",\n      \"source\": \"Feuilly\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Feuilly-Enjolras\",\n      \"source\": \"Feuilly\",\n      \"target\": \"Enjolras\",\n      \"data\": { \"value\": 6 }\n    },\n    {\n      \"id\": \"Feuilly-Prouvaire\",\n      \"source\": \"Feuilly\",\n      \"target\": \"Prouvaire\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Feuilly-Combeferre\",\n      \"source\": \"Feuilly\",\n      \"target\": \"Combeferre\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Feuilly-Mabeuf\",\n      \"source\": \"Feuilly\",\n      \"target\": \"Mabeuf\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Feuilly-Marius\",\n      \"source\": \"Feuilly\",\n      \"target\": \"Marius\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Courfeyrac-Marius\",\n      \"source\": \"Courfeyrac\",\n      \"target\": \"Marius\",\n      \"data\": { \"value\": 9 }\n    },\n    {\n      \"id\": \"Courfeyrac-Enjolras\",\n      \"source\": \"Courfeyrac\",\n      \"target\": \"Enjolras\",\n      \"data\": { \"value\": 17 }\n    },\n    {\n      \"id\": \"Courfeyrac-Combeferre\",\n      \"source\": \"Courfeyrac\",\n      \"target\": \"Combeferre\",\n      \"data\": { \"value\": 13 }\n    },\n    {\n      \"id\": \"Courfeyrac-Gavroche\",\n      \"source\": \"Courfeyrac\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 7 }\n    },\n    {\n      \"id\": \"Courfeyrac-Mabeuf\",\n      \"source\": \"Courfeyrac\",\n      \"target\": \"Mabeuf\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Courfeyrac-Eponine\",\n      \"source\": \"Courfeyrac\",\n      \"target\": \"Eponine\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Courfeyrac-Feuilly\",\n      \"source\": \"Courfeyrac\",\n      \"target\": \"Feuilly\",\n      \"data\": { \"value\": 6 }\n    },\n    {\n      \"id\": \"Courfeyrac-Prouvaire\",\n      \"source\": \"Courfeyrac\",\n      \"target\": \"Prouvaire\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Bahorel-Combeferre\",\n      \"source\": \"Bahorel\",\n      \"target\": \"Combeferre\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Bahorel-Gavroche\",\n      \"source\": \"Bahorel\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Bahorel-Courfeyrac\",\n      \"source\": \"Bahorel\",\n      \"target\": \"Courfeyrac\",\n      \"data\": { \"value\": 6 }\n    },\n    {\n      \"id\": \"Bahorel-Mabeuf\",\n      \"source\": \"Bahorel\",\n      \"target\": \"Mabeuf\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Bahorel-Enjolras\",\n      \"source\": \"Bahorel\",\n      \"target\": \"Enjolras\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Bahorel-Feuilly\",\n      \"source\": \"Bahorel\",\n      \"target\": \"Feuilly\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Bahorel-Prouvaire\",\n      \"source\": \"Bahorel\",\n      \"target\": \"Prouvaire\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Bahorel-Marius\",\n      \"source\": \"Bahorel\",\n      \"target\": \"Marius\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Bossuet-Marius\",\n      \"source\": \"Bossuet\",\n      \"target\": \"Marius\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Bossuet-Courfeyrac\",\n      \"source\": \"Bossuet\",\n      \"target\": \"Courfeyrac\",\n      \"data\": { \"value\": 12 }\n    },\n    {\n      \"id\": \"Bossuet-Gavroche\",\n      \"source\": \"Bossuet\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Bossuet-Bahorel\",\n      \"source\": \"Bossuet\",\n      \"target\": \"Bahorel\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Bossuet-Enjolras\",\n      \"source\": \"Bossuet\",\n      \"target\": \"Enjolras\",\n      \"data\": { \"value\": 10 }\n    },\n    {\n      \"id\": \"Bossuet-Feuilly\",\n      \"source\": \"Bossuet\",\n      \"target\": \"Feuilly\",\n      \"data\": { \"value\": 6 }\n    },\n    {\n      \"id\": \"Bossuet-Prouvaire\",\n      \"source\": \"Bossuet\",\n      \"target\": \"Prouvaire\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Bossuet-Combeferre\",\n      \"source\": \"Bossuet\",\n      \"target\": \"Combeferre\",\n      \"data\": { \"value\": 9 }\n    },\n    {\n      \"id\": \"Bossuet-Mabeuf\",\n      \"source\": \"Bossuet\",\n      \"target\": \"Mabeuf\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Bossuet-Valjean\",\n      \"source\": \"Bossuet\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Joly-Bahorel\",\n      \"source\": \"Joly\",\n      \"target\": \"Bahorel\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Joly-Bossuet\",\n      \"source\": \"Joly\",\n      \"target\": \"Bossuet\",\n      \"data\": { \"value\": 7 }\n    },\n    {\n      \"id\": \"Joly-Gavroche\",\n      \"source\": \"Joly\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Joly-Courfeyrac\",\n      \"source\": \"Joly\",\n      \"target\": \"Courfeyrac\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Joly-Enjolras\",\n      \"source\": \"Joly\",\n      \"target\": \"Enjolras\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Joly-Feuilly\",\n      \"source\": \"Joly\",\n      \"target\": \"Feuilly\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Joly-Prouvaire\",\n      \"source\": \"Joly\",\n      \"target\": \"Prouvaire\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Joly-Combeferre\",\n      \"source\": \"Joly\",\n      \"target\": \"Combeferre\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Joly-Mabeuf\",\n      \"source\": \"Joly\",\n      \"target\": \"Mabeuf\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Joly-Marius\",\n      \"source\": \"Joly\",\n      \"target\": \"Marius\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Grantaire-Bossuet\",\n      \"source\": \"Grantaire\",\n      \"target\": \"Bossuet\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Grantaire-Enjolras\",\n      \"source\": \"Grantaire\",\n      \"target\": \"Enjolras\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Grantaire-Combeferre\",\n      \"source\": \"Grantaire\",\n      \"target\": \"Combeferre\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Grantaire-Courfeyrac\",\n      \"source\": \"Grantaire\",\n      \"target\": \"Courfeyrac\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Grantaire-Joly\",\n      \"source\": \"Grantaire\",\n      \"target\": \"Joly\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Grantaire-Gavroche\",\n      \"source\": \"Grantaire\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Grantaire-Bahorel\",\n      \"source\": \"Grantaire\",\n      \"target\": \"Bahorel\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Grantaire-Feuilly\",\n      \"source\": \"Grantaire\",\n      \"target\": \"Feuilly\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Grantaire-Prouvaire\",\n      \"source\": \"Grantaire\",\n      \"target\": \"Prouvaire\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"MotherPlutarch-Mabeuf\",\n      \"source\": \"MotherPlutarch\",\n      \"target\": \"Mabeuf\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Gueulemer-Thenardier\",\n      \"source\": \"Gueulemer\",\n      \"target\": \"Thenardier\",\n      \"data\": { \"value\": 5 }\n    },\n    {\n      \"id\": \"Gueulemer-Valjean\",\n      \"source\": \"Gueulemer\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Gueulemer-Mme.Thenardier\",\n      \"source\": \"Gueulemer\",\n      \"target\": \"Mme.Thenardier\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Gueulemer-Javert\",\n      \"source\": \"Gueulemer\",\n      \"target\": \"Javert\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Gueulemer-Gavroche\",\n      \"source\": \"Gueulemer\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Gueulemer-Eponine\",\n      \"source\": \"Gueulemer\",\n      \"target\": \"Eponine\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Babet-Thenardier\",\n      \"source\": \"Babet\",\n      \"target\": \"Thenardier\",\n      \"data\": { \"value\": 6 }\n    },\n    {\n      \"id\": \"Babet-Gueulemer\",\n      \"source\": \"Babet\",\n      \"target\": \"Gueulemer\",\n      \"data\": { \"value\": 6 }\n    },\n    {\n      \"id\": \"Babet-Valjean\",\n      \"source\": \"Babet\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Babet-Mme.Thenardier\",\n      \"source\": \"Babet\",\n      \"target\": \"Mme.Thenardier\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Babet-Javert\",\n      \"source\": \"Babet\",\n      \"target\": \"Javert\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Babet-Gavroche\",\n      \"source\": \"Babet\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Babet-Eponine\",\n      \"source\": \"Babet\",\n      \"target\": \"Eponine\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Claquesous-Thenardier\",\n      \"source\": \"Claquesous\",\n      \"target\": \"Thenardier\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Claquesous-Babet\",\n      \"source\": \"Claquesous\",\n      \"target\": \"Babet\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Claquesous-Gueulemer\",\n      \"source\": \"Claquesous\",\n      \"target\": \"Gueulemer\",\n      \"data\": { \"value\": 4 }\n    },\n    {\n      \"id\": \"Claquesous-Valjean\",\n      \"source\": \"Claquesous\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Claquesous-Mme.Thenardier\",\n      \"source\": \"Claquesous\",\n      \"target\": \"Mme.Thenardier\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Claquesous-Javert\",\n      \"source\": \"Claquesous\",\n      \"target\": \"Javert\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Claquesous-Eponine\",\n      \"source\": \"Claquesous\",\n      \"target\": \"Eponine\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Claquesous-Enjolras\",\n      \"source\": \"Claquesous\",\n      \"target\": \"Enjolras\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Montparnasse-Javert\",\n      \"source\": \"Montparnasse\",\n      \"target\": \"Javert\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Montparnasse-Babet\",\n      \"source\": \"Montparnasse\",\n      \"target\": \"Babet\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Montparnasse-Gueulemer\",\n      \"source\": \"Montparnasse\",\n      \"target\": \"Gueulemer\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Montparnasse-Claquesous\",\n      \"source\": \"Montparnasse\",\n      \"target\": \"Claquesous\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Montparnasse-Valjean\",\n      \"source\": \"Montparnasse\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Montparnasse-Gavroche\",\n      \"source\": \"Montparnasse\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Montparnasse-Eponine\",\n      \"source\": \"Montparnasse\",\n      \"target\": \"Eponine\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Montparnasse-Thenardier\",\n      \"source\": \"Montparnasse\",\n      \"target\": \"Thenardier\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Toussaint-Cosette\",\n      \"source\": \"Toussaint\",\n      \"target\": \"Cosette\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Toussaint-Javert\",\n      \"source\": \"Toussaint\",\n      \"target\": \"Javert\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Toussaint-Valjean\",\n      \"source\": \"Toussaint\",\n      \"target\": \"Valjean\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Child1-Gavroche\",\n      \"source\": \"Child1\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Child2-Gavroche\",\n      \"source\": \"Child2\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 2 }\n    },\n    {\n      \"id\": \"Child2-Child1\",\n      \"source\": \"Child2\",\n      \"target\": \"Child1\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Brujon-Babet\",\n      \"source\": \"Brujon\",\n      \"target\": \"Babet\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Brujon-Gueulemer\",\n      \"source\": \"Brujon\",\n      \"target\": \"Gueulemer\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Brujon-Thenardier\",\n      \"source\": \"Brujon\",\n      \"target\": \"Thenardier\",\n      \"data\": { \"value\": 3 }\n    },\n    {\n      \"id\": \"Brujon-Gavroche\",\n      \"source\": \"Brujon\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Brujon-Eponine\",\n      \"source\": \"Brujon\",\n      \"target\": \"Eponine\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Brujon-Claquesous\",\n      \"source\": \"Brujon\",\n      \"target\": \"Claquesous\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Brujon-Montparnasse\",\n      \"source\": \"Brujon\",\n      \"target\": \"Montparnasse\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mme.Hucheloup-Bossuet\",\n      \"source\": \"Mme.Hucheloup\",\n      \"target\": \"Bossuet\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mme.Hucheloup-Joly\",\n      \"source\": \"Mme.Hucheloup\",\n      \"target\": \"Joly\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mme.Hucheloup-Grantaire\",\n      \"source\": \"Mme.Hucheloup\",\n      \"target\": \"Grantaire\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mme.Hucheloup-Bahorel\",\n      \"source\": \"Mme.Hucheloup\",\n      \"target\": \"Bahorel\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mme.Hucheloup-Courfeyrac\",\n      \"source\": \"Mme.Hucheloup\",\n      \"target\": \"Courfeyrac\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mme.Hucheloup-Gavroche\",\n      \"source\": \"Mme.Hucheloup\",\n      \"target\": \"Gavroche\",\n      \"data\": { \"value\": 1 }\n    },\n    {\n      \"id\": \"Mme.Hucheloup-Enjolras\",\n      \"source\": \"Mme.Hucheloup\",\n      \"target\": \"Enjolras\",\n      \"data\": { \"value\": 1 }\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/demos/behavior-drag-canvas.ts",
    "content": "import { CameraSetting, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { DragCanvas3D, Light, Line3D, Sphere, renderer } from '../../src';\nimport data from '../dataset/cubic.json';\n\nexport const behaviorDragCanvas: TestCase = async (context) => {\n  register(ExtensionCategory.PLUGIN, '3d-light', Light);\n  register(ExtensionCategory.NODE, 'sphere', Sphere);\n  register(ExtensionCategory.EDGE, 'line3d', Line3D);\n  register(ExtensionCategory.BEHAVIOR, 'drag-canvas-3d', DragCanvas3D);\n  register(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting);\n\n  const graph = new Graph({\n    ...context,\n    renderer,\n    data,\n    node: {\n      type: 'sphere',\n      style: {\n        materialType: 'phong',\n        labelText: '',\n        x: (d) => +d.style!.x! + 250,\n        y: (d) => +d.style!.y! + 250,\n      },\n      palette: 'spectral',\n    },\n    edge: {\n      type: 'line3d',\n    },\n    behaviors: [\n      'drag-canvas-3d',\n      {\n        type: 'drag-canvas-3d',\n        trigger: {\n          up: ['ArrowUp'],\n          down: ['ArrowDown'],\n          right: ['ArrowRight'],\n          left: ['ArrowLeft'],\n        },\n      },\n    ],\n    plugins: [\n      {\n        type: 'camera-setting',\n        projectionMode: 'perspective',\n        near: 0.1,\n        far: 1000,\n        fov: 45,\n        aspect: 1,\n      },\n      {\n        type: '3d-light',\n        directional: {\n          direction: [0, 0, 1],\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/demos/behavior-observe-canvas.ts",
    "content": "import { CameraSetting, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { Light, Line3D, ObserveCanvas3D, Sphere, ZoomCanvas3D, renderer } from '../../src';\nimport data from '../dataset/cubic.json';\n\nexport const behaviorObserveCanvas: TestCase = async (context) => {\n  register(ExtensionCategory.PLUGIN, '3d-light', Light);\n  register(ExtensionCategory.NODE, 'sphere', Sphere);\n  register(ExtensionCategory.EDGE, 'line3d', Line3D);\n  register(ExtensionCategory.BEHAVIOR, 'observe-canvas-3d', ObserveCanvas3D);\n  register(ExtensionCategory.BEHAVIOR, 'zoom-canvas-3d', ZoomCanvas3D);\n  register(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting);\n\n  const graph = new Graph({\n    ...context,\n    renderer,\n    data,\n    node: {\n      type: 'sphere',\n      style: {\n        materialType: 'phong',\n        labelText: '',\n        x: (d) => +d.style!.x! + 250,\n        y: (d) => +d.style!.y! + 250,\n      },\n      palette: 'spectral',\n    },\n    edge: {\n      type: 'line3d',\n    },\n    behaviors: ['zoom-canvas-3d', { key: 'observe-canvas-3d', type: 'observe-canvas-3d' }],\n    plugins: [\n      {\n        type: 'camera-setting',\n        projectionMode: 'perspective',\n        near: 0.1,\n        far: 1000,\n        fov: 45,\n        aspect: 1,\n      },\n      {\n        type: '3d-light',\n        directional: {\n          direction: [0, 0, 1],\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n\n  behaviorObserveCanvas.form = (panel) => [\n    panel.add({ mode: 'orbiting' }, 'mode', ['orbiting', 'exploring', 'tracking']).onChange((mode: string) => {\n      graph.updateBehavior({\n        key: 'observe-canvas-3d',\n        type: 'observe-canvas-3d',\n        mode,\n      });\n    }),\n  ];\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/demos/behavior-roll-canvas.ts",
    "content": "import { CameraSetting, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { Light, Line3D, RollCanvas3D, Sphere, renderer } from '../../src';\nimport data from '../dataset/cubic.json';\n\nexport const behaviorRollCanvas: TestCase = async (context) => {\n  register(ExtensionCategory.PLUGIN, '3d-light', Light);\n  register(ExtensionCategory.NODE, 'sphere', Sphere);\n  register(ExtensionCategory.EDGE, 'line3d', Line3D);\n  register(ExtensionCategory.BEHAVIOR, 'roll-canvas-3d', RollCanvas3D);\n  register(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting);\n\n  const graph = new Graph({\n    ...context,\n    renderer,\n    data,\n    node: {\n      type: 'sphere',\n      style: {\n        materialType: 'phong',\n        labelText: '',\n        x: (d) => +d.style!.x! + 250,\n        y: (d) => +d.style!.y! + 250,\n      },\n      palette: 'spectral',\n    },\n    edge: {\n      type: 'line3d',\n    },\n    behaviors: ['roll-canvas-3d'],\n    plugins: [\n      {\n        type: 'camera-setting',\n        projectionMode: 'perspective',\n        near: 0.1,\n        far: 1000,\n        fov: 45,\n        aspect: 1,\n      },\n      {\n        type: '3d-light',\n        directional: {\n          direction: [0, 0, 1],\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/demos/behavior-zoom-canvas.ts",
    "content": "import { CameraSetting, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { Light, Line3D, Sphere, ZoomCanvas3D, renderer } from '../../src';\nimport data from '../dataset/cubic.json';\n\nexport const behaviorZoomCanvas: TestCase = async (context) => {\n  register(ExtensionCategory.PLUGIN, '3d-light', Light);\n  register(ExtensionCategory.NODE, 'sphere', Sphere);\n  register(ExtensionCategory.EDGE, 'line3d', Line3D);\n  register(ExtensionCategory.BEHAVIOR, 'zoom-canvas-3d', ZoomCanvas3D);\n  register(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting);\n\n  const graph = new Graph({\n    ...context,\n    renderer,\n    data,\n    node: {\n      type: 'sphere',\n      style: {\n        materialType: 'phong',\n        labelText: '',\n        x: (d) => +d.style!.x! + 250,\n        y: (d) => +d.style!.y! + 250,\n      },\n      palette: 'spectral',\n    },\n    edge: {\n      type: 'line3d',\n    },\n    behaviors: ['zoom-canvas-3d'],\n    plugins: [\n      {\n        type: 'camera-setting',\n        projectionMode: 'perspective',\n        near: 0.1,\n        far: 1000,\n        fov: 45,\n        aspect: 1,\n      },\n      {\n        type: '3d-light',\n        directional: {\n          direction: [0, 0, 1],\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/demos/index.ts",
    "content": "export * from './behavior-drag-canvas';\nexport * from './behavior-observe-canvas';\nexport * from './behavior-roll-canvas';\nexport * from './behavior-zoom-canvas';\nexport * from './layer-top';\nexport * from './layout-d3-force-3d';\nexport { massiveElements } from './massive-elements';\nexport * from './position';\nexport * from './shapes';\nexport * from './solar-system';\nexport { switchRenderer } from './switch-renderer';\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/demos/layer-top.ts",
    "content": "import type { GraphData } from '@antv/g6';\nimport { ExtensionCategory, Graph, register } from '@antv/g6';\nimport { Light, Line3D, ObserveCanvas3D, Plane, Sphere, renderer } from '../../src';\n\nexport const layerTop: TestCase = async (context) => {\n  register(ExtensionCategory.PLUGIN, '3d-light', Light);\n  register(ExtensionCategory.NODE, 'sphere', Sphere);\n  register(ExtensionCategory.NODE, 'plane', Plane);\n  register(ExtensionCategory.EDGE, 'line3d', Line3D);\n  register(ExtensionCategory.BEHAVIOR, 'observe-canvas-3d', ObserveCanvas3D);\n\n  const result = await fetch('https://assets.antv.antgroup.com/g6/3-layer-top.json');\n  const { nodes, edges } = await result.json();\n\n  const colors = ['rgb(240, 134, 82)', 'rgb(30, 160, 230)', 'rgb(122, 225, 116)'];\n  const data: GraphData = {};\n  data.nodes = nodes.map(({ name, pos, layer }: any) => ({\n    id: name,\n    data: { layer },\n    type: 'sphere',\n    style: {\n      radius: 10,\n      color: colors[layer - 1],\n      materialType: 'phong',\n      ...pos,\n    },\n  }));\n\n  new Array(3).fill(0).forEach((_, i) => {\n    data.nodes!.push({\n      id: `plane-${i + 1}`,\n      type: 'plane',\n      style: {\n        size: 1000,\n        color: colors[i],\n        y: -300 + 300 * i + 10,\n      },\n    });\n  });\n\n  data.edges = edges.map(({ source, target }: any) => ({\n    source,\n    target,\n  }));\n\n  const graph = new Graph({\n    ...context,\n    renderer,\n    x: 100,\n    y: 100,\n    data,\n    zoom: 0.4,\n    edge: {\n      type: 'line3d',\n      style: {\n        lineWidth: 5,\n      },\n    },\n    behaviors: ['observe-canvas-3d'],\n    plugins: [\n      {\n        type: 'camera-setting',\n        projectionMode: 'perspective',\n        near: 0.1,\n        far: 50000,\n        fov: 45,\n        aspect: 1,\n      },\n      {\n        type: '3d-light',\n        directional: {\n          direction: [0, 0, 1],\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/demos/layout-d3-force-3d.ts",
    "content": "import { CameraSetting, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { D3Force3DLayout, Light, Line3D, ObserveCanvas3D, Sphere, ZoomCanvas3D, renderer } from '../../src';\nimport data from '../dataset/force-3d.json';\n\nexport const layoutD3Force3D: TestCase = async (context) => {\n  register(ExtensionCategory.PLUGIN, '3d-light', Light);\n  register(ExtensionCategory.NODE, 'sphere', Sphere);\n  register(ExtensionCategory.EDGE, 'line3d', Line3D);\n  register(ExtensionCategory.LAYOUT, 'd3-force-3d', D3Force3DLayout as any);\n  register(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting);\n  register(ExtensionCategory.BEHAVIOR, 'zoom-canvas-3d', ZoomCanvas3D);\n  register(ExtensionCategory.BEHAVIOR, 'observe-canvas-3d', ObserveCanvas3D);\n\n  const graph = new Graph({\n    ...context,\n    animation: true,\n    renderer,\n    data,\n    layout: {\n      type: 'd3-force-3d',\n    },\n    node: {\n      type: 'sphere',\n      style: {\n        materialType: 'phong',\n      },\n      palette: {\n        color: 'tableau',\n        type: 'group',\n        field: 'group',\n      },\n    },\n    edge: {\n      type: 'line3d',\n    },\n    behaviors: ['observe-canvas-3d', 'zoom-canvas-3d'],\n    plugins: [\n      {\n        type: 'camera-setting',\n        projectionMode: 'perspective',\n        near: 0.1,\n        far: 1000,\n        fov: 45,\n        aspect: 1,\n      },\n      {\n        type: '3d-light',\n        directional: {\n          direction: [0, 0, 1],\n        },\n      },\n    ],\n  });\n\n  await graph.render();\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/demos/massive-elements.ts",
    "content": "import { CameraSetting, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { Light, Line3D, ObserveCanvas3D, Sphere, ZoomCanvas3D, renderer } from '../../src';\n\nexport const massiveElements: TestCase = async (context) => {\n  register(ExtensionCategory.PLUGIN, '3d-light', Light);\n  register(ExtensionCategory.NODE, 'sphere', Sphere);\n  register(ExtensionCategory.EDGE, 'line3d', Line3D);\n  register(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting);\n  register(ExtensionCategory.BEHAVIOR, 'zoom-canvas-3d', ZoomCanvas3D);\n  register(ExtensionCategory.BEHAVIOR, 'observe-canvas-3d', ObserveCanvas3D);\n\n  const data = await fetch('https://assets.antv.antgroup.com/g6/eva-3d-data.json').then((res) => res.json());\n\n  const graph = new Graph({\n    ...context,\n    animation: false,\n    renderer,\n    data,\n    node: {\n      type: 'sphere',\n      style: {\n        materialType: 'phong',\n        size: 50,\n        x: (d) => d.data!.x,\n        y: (d) => d.data!.y,\n        z: (d) => d.data!.z,\n      },\n      palette: {\n        color: 'tableau',\n        type: 'group',\n        field: 'cluster',\n      },\n    },\n    edge: {\n      type: 'line3d',\n    },\n    behaviors: ['observe-canvas-3d', 'zoom-canvas-3d'],\n    plugins: [\n      {\n        type: 'camera-setting',\n        projectionMode: 'orthographic',\n        near: 1,\n        far: 10000,\n        fov: 45,\n        aspect: 1,\n      },\n      {\n        type: '3d-light',\n        directional: {\n          direction: [0, 0, 1],\n        },\n      },\n    ],\n  });\n\n  console.time('time');\n\n  await graph.draw();\n\n  console.timeEnd('time');\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/demos/position.ts",
    "content": "import { CameraSetting, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { DragCanvas3D, Light, Line3D, Sphere, renderer } from '../../src';\n\nexport const positionValidate: TestCase = async (context) => {\n  register(ExtensionCategory.PLUGIN, '3d-light', Light);\n  register(ExtensionCategory.NODE, 'sphere', Sphere);\n  register(ExtensionCategory.EDGE, 'line3d', Line3D);\n  register(ExtensionCategory.BEHAVIOR, 'drag-canvas-3d', DragCanvas3D);\n  register(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting);\n\n  const graph = new Graph({\n    ...context,\n    renderer,\n    data: {\n      nodes: [\n        { id: '0', style: { labelText: 'center' } },\n        { id: '1', style: { x: -50, y: -50, z: -50, labelText: '(-1, -1, -1)' } },\n        { id: '2', style: { x: -50, y: 50, z: -50, labelText: '(-1, 1, -1)' } },\n        { id: '3', style: { x: 50, y: 50, z: -50, labelText: '(1, 1, -1)' } },\n        { id: '4', style: { x: 50, y: -50, z: -50, labelText: '(1, -1, -1)' } },\n        { id: '5', style: { x: -50, y: -50, z: 50, labelText: '(-1, -1, 1)' } },\n        { id: '6', style: { x: -50, y: 50, z: 50, labelText: '(-1, 1, 1)' } },\n        { id: '7', style: { x: 50, y: 50, z: 50, labelText: '(1, 1, 1)' } },\n        { id: '8', style: { x: 50, y: -50, z: 50, labelText: '(1, -1, 1)' } },\n      ],\n      edges: [\n        // { source: '1', target: '2' },\n        // { source: '2', target: '3' },\n        // { source: '1', target: '3' },\n      ],\n    },\n    node: {\n      type: 'sphere',\n      style: {\n        materialType: 'phong',\n        labelText: '',\n      },\n      palette: 'spectral',\n    },\n    edge: {\n      type: 'line3d',\n    },\n    behaviors: ['drag-canvas-3d'],\n    plugins: [\n      {\n        type: '3d-light',\n        directional: {\n          direction: [0, 0, 1],\n        },\n      },\n      {\n        type: 'camera-setting',\n        projectionMode: 'perspective',\n        near: 0.1,\n        far: 1000,\n        fov: 45,\n        aspect: 1,\n      },\n    ],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/demos/shapes.ts",
    "content": "import type { NodeData } from '@antv/g6';\nimport { ExtensionCategory, Graph, register } from '@antv/g6';\nimport { Capsule, Cone, Cube, Cylinder, Light, ObserveCanvas3D, Plane, Sphere, Torus, renderer } from '../../src';\n\nexport const shapes: TestCase = async (context) => {\n  register(ExtensionCategory.PLUGIN, '3d-light', Light);\n  register(ExtensionCategory.NODE, 'sphere', Sphere);\n  register(ExtensionCategory.NODE, 'plane', Plane);\n  register(ExtensionCategory.NODE, 'cylinder', Cylinder);\n  register(ExtensionCategory.NODE, 'cone', Cone);\n  register(ExtensionCategory.NODE, 'cube', Cube);\n  register(ExtensionCategory.NODE, 'capsule', Capsule);\n  register(ExtensionCategory.NODE, 'torus', Torus);\n  register(ExtensionCategory.BEHAVIOR, 'observe-canvas-3d', ObserveCanvas3D);\n\n  const nodes: NodeData[] = [\n    {\n      id: '1',\n      type: 'sphere',\n      style: {\n        texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*cdTdTI2bNl8AAAAAAAAAAAAADmJ7AQ/original',\n      },\n    },\n    { id: '2', type: 'plane', style: { size: 50 } },\n    { id: '3', type: 'cylinder' },\n    { id: '4', type: 'cone' },\n    {\n      id: '5',\n      type: 'cube',\n      style: {\n        texture: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*8TlCRIsKeUkAAAAAAAAAAAAAARQnAQ',\n      },\n    },\n    { id: '6', type: 'capsule' },\n    { id: '7', type: 'torus' },\n  ];\n\n  const graph = new Graph({\n    ...context,\n    renderer,\n    data: {\n      nodes,\n    },\n    node: {\n      style: {\n        materialType: 'phong',\n        x: (d) => 100 + (nodes.findIndex((n) => n.id === d.id) % 5) * 100,\n        y: (d) => 100 + Math.floor(nodes.findIndex((n) => n.id === d.id) / 5) * 100,\n      },\n    },\n    plugins: [\n      {\n        type: '3d-light',\n        directional: {\n          direction: [0, 0, 1],\n        },\n      },\n    ],\n    behaviors: ['observe-canvas-3d'],\n  });\n\n  await graph.render();\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/demos/solar-system.ts",
    "content": "import type { DisplayObject } from '@antv/g';\nimport type { Vector3 } from '@antv/g6';\nimport { Graph, register } from '@antv/g6';\nimport { Light, Sphere, renderer } from '../../src';\n\nexport const solarSystem: TestCase = async (context) => {\n  register('plugin', '3d-light', Light);\n  register('node', 'sphere', Sphere);\n\n  const graph = new Graph({\n    ...context,\n    renderer,\n    data: {\n      nodes: [\n        {\n          id: 'sum',\n          style: {\n            x: 300,\n            y: 300,\n            radius: 100,\n            texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-mZfQr8LtPUAAAAAAAAAAAAADmJ7AQ/original',\n          },\n        },\n        {\n          id: 'mars',\n          style: {\n            x: 430,\n            y: 300,\n            z: 0,\n            radius: 20,\n            texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*mniGTZktpecAAAAAAAAAAAAADmJ7AQ/original',\n          },\n        },\n        {\n          id: 'earth',\n          style: {\n            x: 500,\n            y: 300,\n            z: 0,\n            radius: 30,\n            texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*cdTdTI2bNl8AAAAAAAAAAAAADmJ7AQ/original',\n          },\n        },\n        {\n          id: 'jupiter',\n          style: {\n            x: 600,\n            y: 300,\n            z: 0,\n            radius: 50,\n            texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*t_mQSZYAT70AAAAAAAAAAAAADmJ7AQ/original',\n          },\n        },\n      ],\n    },\n    node: {\n      type: 'sphere',\n      style: {\n        materialShininess: 0,\n        labelText: (d) => d.id,\n      },\n    },\n    plugins: [\n      {\n        type: '3d-light',\n        directional: {\n          direction: [0, 0, 1],\n        },\n      },\n      {\n        type: 'background',\n        background: 'black',\n      },\n    ],\n  });\n\n  await graph.render();\n\n  // @ts-expect-error graph is private\n  const element = graph.context.element!;\n\n  const sum = element.getElement('sum')!;\n  const mars = element.getElement('mars')!;\n  const earth = element.getElement('earth')!;\n  const jupiter = element.getElement('jupiter')!;\n\n  const setRotation = (element: DisplayObject, speed: number) => {\n    setInterval(() => {\n      element.rotate(0, -speed, 0);\n    }, 30);\n  };\n  setRotation(sum, 0.1);\n  setRotation(mars, 0.8);\n  setRotation(earth, 1);\n  setRotation(jupiter, 0.5);\n\n  const setRevolution = (element: DisplayObject, center: Vector3, speed: number) => {\n    setInterval(() => {\n      const [x, y, z] = element.getPosition();\n      const [cx, cy, cz] = center;\n      const angle = (speed * Math.PI) / 180;\n\n      const newX = (x - cx) * Math.cos(angle) + (z - cz) * Math.sin(angle) + cx;\n      const newZ = -(x - cx) * Math.sin(angle) + (z - cz) * Math.cos(angle) + cz;\n\n      element.setPosition(newX, y, newZ);\n    }, 30);\n  };\n\n  setRevolution(mars, [300, 300, 0], 1.5);\n  setRevolution(earth, [300, 300, 0], 1);\n  setRevolution(jupiter, [300, 300, 0], 0.5);\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/demos/switch-renderer.ts",
    "content": "import { Renderer as CanvasRenderer } from '@antv/g-canvas';\nimport type { NodeData } from '@antv/g6';\nimport { ExtensionCategory, Graph, register } from '@antv/g6';\nimport { Light, Line3D, ObserveCanvas3D, Sphere, ZoomCanvas3D, renderer } from '../../src';\n\nexport const switchRenderer: TestCase = async (context) => {\n  register(ExtensionCategory.PLUGIN, '3d-light', Light);\n  register(ExtensionCategory.NODE, 'sphere', Sphere);\n  register(ExtensionCategory.EDGE, 'line3d', Line3D);\n  register(ExtensionCategory.BEHAVIOR, 'observe-canvas-3d', ObserveCanvas3D);\n  register(ExtensionCategory.BEHAVIOR, 'zoom-canvas-3d', ZoomCanvas3D);\n\n  const nodes: NodeData[] = [{ id: '1' }, { id: '2' }];\n\n  const graph = new Graph({\n    ...context,\n    data: {\n      nodes,\n    },\n    layout: {\n      type: 'grid',\n    },\n  });\n\n  await graph.render();\n\n  switchRenderer.form = (panel) => {\n    panel.add({ renderer: '2d' }, 'renderer', ['2d', '3d']).onChange((name: string) => {\n      if (name === '2d') {\n        graph.setOptions({\n          renderer: () => new CanvasRenderer(),\n          behaviors: [\n            'zoom-canvas',\n            'drag-canvas',\n            'drag-element',\n            {\n              type: 'hover-activate',\n              degree: 1,\n              state: 'highlight',\n            },\n          ],\n          node: {\n            type: 'circle',\n            state: {\n              highlight: {\n                fill: '#D580FF',\n              },\n            },\n          },\n          edge: {\n            style: {\n              labelBackgroundFill: '#FFF',\n              labelBackground: true,\n            },\n          },\n          layout: {\n            type: 'force',\n            preventOverlap: true,\n            animation: false,\n          },\n        });\n      } else {\n        graph.setOptions({\n          renderer,\n          node: {\n            type: 'sphere',\n            style: {\n              materialType: 'phong',\n            },\n          },\n          edge: {\n            type: 'line3d',\n          },\n          plugins: [\n            {\n              type: 'camera-setting',\n              projectionMode: 'orthographic',\n              near: 1,\n              far: 10000,\n              fov: 45,\n              aspect: 1,\n            },\n            {\n              type: '3d-light',\n              directional: {\n                direction: [0, 0, 1],\n              },\n            },\n            {\n              type: 'background',\n              backgroundImage:\n                'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*M_OaRrzIZOEAAAAAAAAAAAAADmJ7AQ/original)',\n              backgroundPosition: 'center',\n            },\n          ],\n          behaviors: ['observe-canvas-3d', 'zoom-canvas-3d'],\n        });\n      }\n\n      graph.draw();\n    });\n    return [];\n  };\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>@antv/g6-extension-3d</title>\n    <style>\n      body {\n        margin: 0;\n      }\n\n      #container {\n        width: 500px;\n        height: 500px;\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"container\"></div>\n    <script type=\"module\" src=\"./main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/main.ts",
    "content": "import type { Controller } from 'lil-gui';\nimport GUI from 'lil-gui';\nimport * as demos from './demos';\n\nconst demoNames = Object.keys(demos);\n\nconst options = {\n  demo: '',\n};\n\nconst customForm: Controller[] = [];\n\nconst panel = new GUI({ autoPlace: true });\nconst __STORAGE__ = '__G6_EXTENSION_3D_DEMO__';\nconst load = () => {\n  const data = localStorage.getItem(__STORAGE__);\n  if (data) panel.load(JSON.parse(data));\n};\nconst save = () => {\n  localStorage.setItem(__STORAGE__, JSON.stringify(panel.save()));\n};\npanel\n  .add(options, 'demo', demoNames)\n  .name('Demo')\n  .onChange((name: string) => {\n    render(name);\n    save();\n  });\nload();\n\nfunction initContainer() {\n  const container = document.getElementById('container')!;\n  container.innerHTML = '';\n  return container;\n}\n\nfunction initContext() {\n  const container = initContainer();\n  return { container, width: 500, height: 500 };\n}\n\nasync function render(name: string) {\n  destroyForm();\n  const context = initContext();\n  const demo = demos[name as keyof typeof demos];\n  const graph = await demo(context);\n  customForm.push(...(demo?.form?.(panel) || []));\n  Object.assign(window, { graph });\n}\n\nfunction destroyForm() {\n  customForm.forEach((controller) => controller.destroy());\n  customForm.length = 0;\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/types.d.ts",
    "content": "import type { G6Spec, Graph } from '@antv/g6';\nimport type { Controller, GUI } from 'lil-gui';\n\ndeclare global {\n  export interface TestCase {\n    (context: G6Spec): Promise<Graph>;\n    form?: (gui: GUI) => Controller[];\n  }\n\n  export type TestContext = G6Spec;\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/unit/default.spec.ts",
    "content": "describe('suite', () => {\n  it('case', () => {\n    expect(1).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/unit/utils/cache.spec.ts",
    "content": "import { getCacheKey } from '../../../src/utils/cache';\n\ndescribe('cache', () => {\n  it('getCacheKey plain', () => {\n    const key = Symbol.for('latitudeBands:16 longitudeBands:16 radius:10');\n\n    expect(\n      getCacheKey({\n        radius: 10,\n        latitudeBands: 16,\n        longitudeBands: 16,\n      }),\n    ).toBe(key);\n\n    expect(\n      getCacheKey({\n        longitudeBands: 16,\n        latitudeBands: 16,\n        radius: 10,\n      }),\n    ).toBe(key);\n  });\n\n  it('getCacheKey object', () => {\n    const object = { a: { b: 1 } };\n\n    const key1 = getCacheKey(object);\n    const key2 = getCacheKey(object);\n    expect(key1).not.toBe(key2);\n  });\n});\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/unit/utils/geometry.spec.ts",
    "content": "import { CubeGeometry } from '@antv/g-plugin-3d';\nimport { createGeometry } from '../../../src/utils/geometry';\n\ndescribe('geometry', () => {\n  it('createGeometry', () => {\n    const device: any = {};\n    const geometry1 = createGeometry('cube', device, CubeGeometry, { width: 1, height: 1, depth: 1 });\n    const geometry2 = createGeometry('cube', device, CubeGeometry, { depth: 1, height: 1, width: 1 });\n    const geometry3 = createGeometry('cube', device, CubeGeometry, { width: 2, height: 2, depth: 2 });\n\n    expect(geometry1).toBe(geometry2);\n    expect(geometry1).not.toBe(geometry3);\n  });\n});\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/unit/utils/map.spec.ts",
    "content": "import { TupleMap } from '../../../src/utils/map';\n\ndescribe('map', () => {\n  it('TupleMap', () => {\n    const map = new TupleMap<Date, string, number>();\n\n    const key1 = new Date();\n    const key2 = 'key2';\n    map.set(key1, key2, 1);\n\n    expect(map.has(key1, key2)).toBe(true);\n    expect(map.get(key1, key2)).toBe(1);\n\n    map.set(key1, key2, 2);\n    expect(map.get(key1, key2)).toBe(2);\n\n    const key3 = 'key3';\n    expect(map.has(key1, key3)).toBe(false);\n    expect(map.get(key1, key3)).toBe(undefined);\n  });\n});\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/unit/utils/material.spec.ts",
    "content": "import { createMaterial } from '../../../src/utils/material';\n\ndescribe('material', () => {\n  it('createMaterial', () => {\n    const plugin: any = {\n      loadTexture: () => new Object(),\n      getDevice: () => ({}),\n    };\n\n    const materialWithoutTexture = createMaterial(plugin, { type: 'basic' });\n    const materialWithTexture = createMaterial(plugin, { type: 'basic' }, 'texture');\n\n    const image: any = new Object();\n    const materialWithImage = createMaterial(plugin, { type: 'basic' }, image);\n\n    expect(materialWithoutTexture).toBe(createMaterial(plugin, { type: 'basic' }));\n    expect(materialWithTexture).not.toBe(materialWithoutTexture);\n    expect(materialWithImage).not.toBe(materialWithTexture);\n\n    expect(materialWithTexture).toBe(createMaterial(plugin, { type: 'basic' }, 'texture'));\n    expect(materialWithImage).toBe(createMaterial(plugin, { type: 'basic' }, image));\n  });\n});\n"
  },
  {
    "path": "packages/g6-extension-3d/__tests__/unit/utils/texture.spec.ts",
    "content": "import { createTexture } from '../../../src/utils/texture';\n\ndescribe('texture', () => {\n  it('createTexture', () => {\n    const img1 = 'texture1';\n    const img2 = 'texture2';\n\n    const plugin: any = {\n      loadTexture: () => new Object(),\n    };\n\n    const texture1 = createTexture(plugin, img1);\n    const texture2 = createTexture(plugin, img2);\n\n    expect(texture1).toBe(createTexture(plugin, img1));\n    expect(texture2).not.toBe(texture1);\n  });\n});\n"
  },
  {
    "path": "packages/g6-extension-3d/jest.config.js",
    "content": "module.exports = {\n  transform: {\n    '^.+\\\\.[tj]s$': ['@swc/jest'],\n  },\n  testRegex: '(/__tests__/.*\\\\.(test|spec))\\\\.(ts|tsx|js)$',\n  collectCoverageFrom: ['src/**/*.ts'],\n  moduleFileExtensions: ['ts', 'js', 'json'],\n  transformIgnorePatterns: [`<rootDir>/node_modules/.pnpm/(?!(d3-*))`],\n};\n"
  },
  {
    "path": "packages/g6-extension-3d/package.json",
    "content": "{\n  \"name\": \"@antv/g6-extension-3d\",\n  \"version\": \"0.1.22\",\n  \"description\": \"3D extension for G6\",\n  \"keywords\": [\n    \"antv\",\n    \"g6\",\n    \"extension\",\n    \"3d\"\n  ],\n  \"license\": \"MIT\",\n  \"author\": \"Aarebecca\",\n  \"main\": \"lib/index.js\",\n  \"module\": \"esm/index.js\",\n  \"types\": \"lib/index.d.ts\",\n  \"files\": [\n    \"src\",\n    \"esm\",\n    \"lib\",\n    \"dist\",\n    \"README\"\n  ],\n  \"scripts\": {\n    \"build\": \"run-p build:*\",\n    \"build:cjs\": \"rimraf ./lib && tsc --module commonjs --outDir lib -p tsconfig.build.json\",\n    \"build:esm\": \"rimraf ./esm && tsc --module ESNext --outDir esm -p tsconfig.build.json\",\n    \"build:umd\": \"rimraf ./dist && rollup -c\",\n    \"ci\": \"run-s lint type-check build test\",\n    \"dev\": \"vite\",\n    \"lint\": \"eslint ./src __tests__ --quiet && prettier ./src __tests__ --check\",\n    \"prepublishOnly\": \"npm run ci\",\n    \"test\": \"jest\",\n    \"type-check\": \"tsc --noEmit -p tsconfig.test.json\"\n  },\n  \"dependencies\": {\n    \"@antv/g-device-api\": \"^1.6.13\",\n    \"@antv/g-plugin-3d\": \"^2.0.45\",\n    \"@antv/g-plugin-device-renderer\": \"^2.2.22\",\n    \"@antv/g-plugin-dragndrop\": \"^2.0.35\",\n    \"@antv/g-webgl\": \"^2.0.47\",\n    \"@antv/layout\": \"1.2.14-beta.8\",\n    \"@antv/util\": \"^3.3.10\"\n  },\n  \"devDependencies\": {\n    \"@antv/g\": \"^6.1.24\",\n    \"@antv/g-canvas\": \"^2.0.43\",\n    \"@antv/g6\": \"workspace:^\"\n  },\n  \"peerDependencies\": {\n    \"@antv/g\": \"^6.1.2\",\n    \"@antv/g-canvas\": \"^2.0.18\",\n    \"@antv/g6\": \"workspace:^\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/rollup.config.mjs",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport terser from '@rollup/plugin-terser';\nimport typescript from '@rollup/plugin-typescript';\nimport nodePolyfills from 'rollup-plugin-polyfill-node';\nimport { visualizer } from 'rollup-plugin-visualizer';\n\nconst isBundleVis = !!process.env.BUNDLE_VIS;\n\nexport default [\n  {\n    input: 'src/index.ts',\n    output: {\n      file: 'dist/g6-extension-3d.min.js',\n      name: 'G6Extension3D',\n      format: 'umd',\n      sourcemap: false,\n    },\n    plugins: [\n      nodePolyfills(),\n      resolve(),\n      commonjs(),\n      typescript({\n        tsconfig: 'tsconfig.build.json',\n      }),\n      terser(),\n      ...(isBundleVis ? [visualizer()] : []),\n    ],\n  },\n];\n"
  },
  {
    "path": "packages/g6-extension-3d/src/behaviors/drag-canvas-3d.ts",
    "content": "import type { DragCanvasOptions, Vector2, ViewportAnimationEffectTiming } from '@antv/g6';\nimport { DragCanvas } from '@antv/g6';\n\n/**\n * <zh/> 拖拽 3D 画布交互\n *\n * <en/> Drag 3D canvas behavior\n */\nexport interface DragCanvas3DOptions extends DragCanvasOptions {}\n\n/**\n * <zh/> 平移画布\n *\n * <en/> Pan canvas\n */\nexport class DragCanvas3D extends DragCanvas {\n  protected async translate(offset: Vector2, animation?: ViewportAnimationEffectTiming | undefined) {\n    this.context.canvas.getCamera().pan(-offset[0], -offset[1]);\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/behaviors/index.ts",
    "content": "export { DragCanvas3D } from './drag-canvas-3d';\nexport { ObserveCanvas3D } from './observe-canvas-3d';\nexport { RollCanvas3D } from './roll-canvas-3d';\nexport { ZoomCanvas3D } from './zoom-canvas-3d';\n\nexport type { DragCanvas3DOptions } from './drag-canvas-3d';\nexport type { ObserveCanvas3DOptions } from './observe-canvas-3d';\nexport type { RollCanvas3DOptions } from './roll-canvas-3d';\nexport type { ZoomCanvas3DOptions } from './zoom-canvas-3d';\n"
  },
  {
    "path": "packages/g6-extension-3d/src/behaviors/observe-canvas-3d.ts",
    "content": "import { CameraType } from '@antv/g';\nimport type { BaseBehaviorOptions, IDragEvent, RuntimeContext, ShortcutKey } from '@antv/g6';\nimport { BaseBehavior, GraphEvent, Shortcut } from '@antv/g6';\n\n/**\n * <zh/> 观察 3D 画布交互配置项\n *\n * <en/> Observe 3D canvas options\n */\nexport interface ObserveCanvas3DOptions extends BaseBehaviorOptions {\n  enable?: boolean;\n  /**\n   * <zh/> 相机模式\n   * - `orbiting` 固定视点(`focalPoint`)，改变相机位置。不能跨越南北极\n   * - `exploring` 固定视点(`focalPoint`)，改变相机位置。可以跨越南北极\n   * - `tracking` 第一人称模式，固定相机位置，改变视点(`focalPoint`)位置\n   *\n   * <en/> Camera mode\n   * - `orbiting` Fixed viewpoint(`focalPoint`), change camera position. Cannot cross the north and south poles\n   * - `exploring` Fixed viewpoint(`focalPoint`), change camera position. Can cross the north and south poles\n   * - `tracking` First-person mode, fixed camera position, change viewpoint(`focalPoint`) position\n   */\n  mode?: 'orbiting' | 'exploring' | 'tracking';\n  /**\n   * <zh/> 按下该快捷键配合指针观察场景\n   *\n   * <en/> Press this shortcut key to observe the scene with the pointer\n   */\n  trigger?: ShortcutKey;\n  /**\n   * <zh/> 灵敏度\n   *\n   * <en/> Sensitivity\n   */\n  sensitivity?: number;\n}\n\n/**\n * <zh/> 3D 场景控制器，提供缩放、平移、旋转等能力\n *\n * <en/> 3D scene controller, providing zoom, pan, rotate and other capabilities\n */\nexport class ObserveCanvas3D extends BaseBehavior<ObserveCanvas3DOptions> {\n  static defaultOptions: Partial<ObserveCanvas3DOptions> = {\n    enable: true,\n    mode: 'orbiting',\n    trigger: [],\n  };\n\n  private shortcut: Shortcut;\n\n  private get camera() {\n    return this.context.canvas.getCamera();\n  }\n\n  constructor(context: RuntimeContext, options: ObserveCanvas3DOptions) {\n    super(context, { ...ObserveCanvas3D.defaultOptions, ...options });\n    this.shortcut = new Shortcut(context.graph);\n    this.bindEvents();\n  }\n\n  public update(options: Partial<ObserveCanvas3DOptions>): void {\n    super.update(options);\n    this.setCameraType();\n  }\n\n  private setCameraType = () => {\n    const { mode } = this.options;\n    const CameraModeMap = {\n      orbiting: CameraType.ORBITING,\n      exploring: CameraType.EXPLORING,\n      tracking: CameraType.TRACKING,\n    };\n    this.camera.setType(CameraModeMap[mode]);\n  };\n\n  // tracking 模式下需要减速，否则容易出现抖动\n  // Deceleration is required in tracking mode, otherwise jitter is easy to occur\n  private getRatio() {\n    const { sensitivity, mode } = this.options;\n    if (sensitivity) return sensitivity / 10;\n    if (mode === 'tracking') return 0.1;\n    return 1;\n  }\n\n  private onDrag = (event: IDragEvent) => {\n    if (!this.options.enable) return;\n    const { x, y } = event.movement;\n    const ratio = this.getRatio();\n    this.camera.rotate(x * ratio, -y * ratio, 0);\n  };\n\n  private bindEvents() {\n    const { graph } = this.context;\n    graph.once(GraphEvent.BEFORE_DRAW, this.setCameraType);\n    this.shortcut.unbindAll();\n    this.shortcut.bind([...this.options.trigger, 'drag'], this.onDrag);\n  }\n\n  public destroy() {\n    this.shortcut.destroy();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/behaviors/roll-canvas-3d.ts",
    "content": "import type { BaseBehaviorOptions, IWheelEvent, RuntimeContext, ShortcutKey } from '@antv/g6';\nimport { BasePlugin, Shortcut } from '@antv/g6';\n\n/**\n * <zh/> 滚动画布配置项\n *\n * <en/> Roll Canvas Options\n */\nexport interface RollCanvas3DOptions extends BaseBehaviorOptions {\n  enable?: boolean;\n  /**\n   * <zh/> 按下该快捷键配合滚轮操作进行旋转\n   *\n   * <en/> Press this shortcut key to rotate with the mouse wheel\n   */\n  trigger?: ShortcutKey;\n  /**\n   * <zh/> 灵敏度\n   *\n   * <en/> Sensitivity\n   */\n  sensitivity?: number;\n}\n\n/**\n * <zh/> 滚动画布\n *\n * <en/> Roll Canvas\n */\nexport class RollCanvas3D extends BasePlugin<RollCanvas3DOptions> {\n  static defaultOptions: Partial<RollCanvas3DOptions> = {\n    enable: true,\n    trigger: ['wheel'],\n    sensitivity: 1,\n  };\n\n  private shortcut: Shortcut;\n\n  private get camera() {\n    return this.context.canvas.getCamera();\n  }\n\n  constructor(context: RuntimeContext, options: RollCanvas3DOptions) {\n    super(context, { ...RollCanvas3D.defaultOptions, ...options });\n    this.shortcut = new Shortcut(context.graph);\n    this.bindEvents();\n  }\n\n  private getAngle(delta: number): number {\n    const { sensitivity } = this.options;\n    return -(delta * sensitivity) / 10;\n  }\n\n  private onRoll = (event: IWheelEvent) => {\n    const roll = this.camera.getRoll();\n    const delta = event.deltaY;\n    this.camera.setRoll(roll + this.getAngle(delta));\n  };\n\n  private bindEvents() {\n    const { trigger } = this.options;\n    this.shortcut.unbindAll();\n    this.shortcut.bind([...trigger, 'wheel'], this.onRoll);\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/behaviors/zoom-canvas-3d.ts",
    "content": "import type {\n  IKeyboardEvent,\n  IPointerEvent,\n  IWheelEvent,\n  ViewportAnimationEffectTiming,\n  ZoomCanvasOptions,\n} from '@antv/g6';\nimport { ZoomCanvas } from '@antv/g6';\nimport { clamp } from '@antv/util';\n\n/**\n * <zh/> 缩放画布配置项\n *\n * <en/> Zoom Canvas Options\n */\nexport interface ZoomCanvas3DOptions extends ZoomCanvasOptions {}\n\n/**\n * <zh/> 缩放画布\n *\n * <en/> Zoom Canvas\n */\nexport class ZoomCanvas3D extends ZoomCanvas {\n  protected zoom = async (\n    value: number,\n    event: IWheelEvent | IKeyboardEvent | IPointerEvent,\n    animation: ViewportAnimationEffectTiming | undefined,\n  ) => {\n    if (!this.validate(event)) return;\n    const { graph } = this.context;\n\n    const { sensitivity, onFinish } = this.options;\n    const ratio = 1 + (clamp(value, -50, 50) * sensitivity) / 100;\n    const zoom = graph.getZoom();\n\n    this.context.canvas.getCamera().setZoom(zoom * ratio);\n\n    onFinish?.();\n  };\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/elements/base-node-3d.ts",
    "content": "import type { BaseStyleProps, DisplayObjectConfig, Group } from '@antv/g';\nimport type { ProceduralGeometry as GGeometry, Material as GMaterial } from '@antv/g-plugin-3d';\nimport { Mesh } from '@antv/g-plugin-3d';\nimport type { IMaterial, Plugin } from '@antv/g-plugin-device-renderer';\nimport type { BaseNodeStyleProps, Prefix } from '@antv/g6';\nimport { BaseNode, omitStyleProps, subStyleProps } from '@antv/g6';\nimport { deepMix } from '@antv/util';\nimport { Material } from '../types';\nimport { createMaterial } from '../utils/material';\n\n/**\n * <zh/> 3D 节点样式\n *\n * <en/> 3D node style props\n */\nexport interface BaseNode3DStyleProps extends BaseNodeStyleProps, Prefix<'material', IMaterial> {\n  geometry?: GGeometry<any>;\n  material?: GMaterial<any>;\n  texture?: string | TexImageSource;\n  materialType?: Material['type'];\n}\n\n/**\n * <zh/> 3D 节点基类\n *\n * <en/> 3D node base class\n */\nexport abstract class BaseNode3D<S extends BaseNode3DStyleProps> extends BaseNode<S> {\n  static defaultStyleProps: Partial<BaseNode3DStyleProps> = {\n    materialType: 'basic',\n  };\n\n  public type = 'node-3d';\n\n  protected get plugin() {\n    const renderer = this.context.canvas.getRenderer('main');\n    const plugin = renderer.getPlugin('device-renderer');\n    return plugin as unknown as Plugin;\n  }\n\n  protected get device() {\n    return this.plugin.getDevice();\n  }\n\n  constructor(options: DisplayObjectConfig<S>) {\n    super(deepMix({}, { style: BaseNode3D.defaultStyleProps }, options));\n  }\n\n  public render(attributes: Required<S>, container: Group) {\n    super.render(attributes, container);\n  }\n\n  protected getKeyStyle(attributes: Required<S>): MeshStyleProps {\n    const style = omitStyleProps(super.getKeyStyle(attributes), 'material');\n    const geometry = this.getGeometry(attributes);\n    const material = this.getMaterial(attributes);\n    return { x: 0, y: 0, z: 0, ...style, geometry, material };\n  }\n\n  protected drawKeyShape(attributes: Required<S>, container: Group = this) {\n    return this.upsert('key', Mesh, this.getKeyStyle(attributes), container);\n  }\n\n  protected abstract getGeometry(attributes: Required<S>): GGeometry<any>;\n\n  protected getMaterial(attributes: Required<S>): GMaterial<any> {\n    const { texture } = attributes;\n    const materialStyle = subStyleProps<Material>(attributes, 'material');\n    return createMaterial(this.plugin, materialStyle, texture);\n  }\n}\nexport interface MeshStyleProps extends BaseStyleProps {\n  x?: number | string;\n  y?: number | string;\n  z?: number | string;\n  geometry: GGeometry<any>;\n  material: GMaterial<any>;\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/elements/capsule.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { CapsuleGeometryProps, ProceduralGeometry as GGeometry } from '@antv/g-plugin-3d';\nimport { CapsuleGeometry } from '@antv/g-plugin-3d';\nimport type { Vector3 } from '@antv/g6';\nimport { deepMix } from '@antv/util';\nimport { createGeometry } from '../utils/geometry';\nimport type { BaseNode3DStyleProps } from './base-node-3d';\nimport { BaseNode3D } from './base-node-3d';\n\n/**\n * <zh/> 胶囊节点样式\n *\n * <en/> Capsule Node Style Props\n */\nexport type CapsuleStyleProps = BaseNode3DStyleProps & CapsuleGeometryProps;\n\n/**\n * <zh/> 胶囊节点\n *\n * <en/> Capsule Node\n */\nexport class Capsule extends BaseNode3D<CapsuleStyleProps> {\n  static defaultStyleProps: Partial<CapsuleStyleProps> = {\n    // radius, height\n    size: [24, 48],\n    heightSegments: 1,\n    sides: 20,\n  };\n\n  constructor(options: DisplayObjectConfig<CapsuleStyleProps>) {\n    super(deepMix({}, { style: Capsule.defaultStyleProps }, options));\n  }\n\n  protected getSize(attributes: CapsuleStyleProps = this.attributes): Vector3 {\n    const { size } = attributes;\n    if (typeof size === 'number') return [size / 4, size, size];\n    return super.getSize();\n  }\n\n  protected getGeometry(attributes: Required<CapsuleStyleProps>): GGeometry<any> {\n    const size = this.getSize();\n    const { radius = size[0], height = size[1], heightSegments, sides } = attributes;\n\n    return createGeometry('capsule', this.device, CapsuleGeometry, { radius, height, heightSegments, sides });\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/elements/cone.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { ConeGeometryProps, ProceduralGeometry as GGeometry } from '@antv/g-plugin-3d';\nimport { ConeGeometry } from '@antv/g-plugin-3d';\nimport type { Vector3 } from '@antv/g6';\nimport { deepMix } from '@antv/util';\nimport { createGeometry } from '../utils/geometry';\nimport type { BaseNode3DStyleProps } from './base-node-3d';\nimport { BaseNode3D } from './base-node-3d';\n\n/**\n * <zh/> 圆锥节点样式\n *\n * <en/> Cone Node Style Props\n */\nexport type ConeStyleProps = BaseNode3DStyleProps & ConeGeometryProps;\n\n/**\n * <zh/> 圆锥节点\n *\n * <en/> Cone Node\n */\nexport class Cone extends BaseNode3D<ConeStyleProps> {\n  static defaultStyleProps: Partial<ConeStyleProps> = {\n    // baseRadius, peakRadius, height\n    size: [24, 0, 48],\n    heightSegments: 5,\n    capSegments: 20,\n  };\n\n  constructor(options: DisplayObjectConfig<ConeStyleProps>) {\n    super(deepMix({}, { style: Cone.defaultStyleProps }, options));\n  }\n\n  protected getSize(attributes: ConeStyleProps = this.attributes): Vector3 {\n    const { size } = attributes;\n    if (typeof size === 'number') return [size / 2, 0, size];\n    return super.getSize();\n  }\n\n  protected getGeometry(attributes: Required<ConeStyleProps>): GGeometry<any> {\n    const size = this.getSize();\n    const { baseRadius = size[0], peakRadius = size[1], height = size[2], heightSegments, capSegments } = attributes;\n    return createGeometry('cone', this.device, ConeGeometry, {\n      baseRadius,\n      peakRadius,\n      height,\n      heightSegments,\n      capSegments,\n    });\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/elements/cube.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { CubeGeometryProps, ProceduralGeometry as GGeometry } from '@antv/g-plugin-3d';\nimport { CubeGeometry } from '@antv/g-plugin-3d';\nimport { deepMix } from '@antv/util';\nimport { createGeometry } from '../utils/geometry';\nimport type { BaseNode3DStyleProps } from './base-node-3d';\nimport { BaseNode3D } from './base-node-3d';\n\n/**\n * <zh/> 立方体节点样式\n *\n * <en/> Cube Node Style Props\n */\nexport type CubeStyleProps = BaseNode3DStyleProps & CubeGeometryProps;\n\n/**\n * <zh/> 立方体节点\n *\n * <en/> Cube Node\n */\nexport class Cube extends BaseNode3D<CubeStyleProps> {\n  static defaultStyleProps: Partial<CubeStyleProps> = {\n    widthSegments: 1,\n    heightSegments: 1,\n    depthSegments: 1,\n  };\n\n  constructor(options: DisplayObjectConfig<CubeStyleProps>) {\n    super(deepMix({}, { style: Cube.defaultStyleProps }, options));\n  }\n\n  protected getGeometry(attributes: Required<CubeStyleProps>): GGeometry<any> {\n    const size = this.getSize();\n    const {\n      width = size[0],\n      height = size[1],\n      depth = size[2],\n      widthSegments,\n      heightSegments,\n      depthSegments,\n    } = attributes;\n    return createGeometry('cube', this.device, CubeGeometry, {\n      width,\n      height,\n      depth,\n      widthSegments,\n      heightSegments,\n      depthSegments,\n    });\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/elements/cylinder.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { CylinderGeometryProps, ProceduralGeometry as GGeometry } from '@antv/g-plugin-3d';\nimport { CylinderGeometry } from '@antv/g-plugin-3d';\nimport type { Vector3 } from '@antv/g6';\nimport { deepMix } from '@antv/util';\nimport { createGeometry } from '../utils/geometry';\nimport type { BaseNode3DStyleProps } from './base-node-3d';\nimport { BaseNode3D } from './base-node-3d';\n\n/**\n * <zh/> 圆柱节点样式\n *\n * <en/> Cylinder Node Style Props\n */\nexport type CylinderStyleProps = BaseNode3DStyleProps & CylinderGeometryProps;\n\n/**\n * <zh/> 圆柱节点\n *\n * <en/> Cylinder Node\n */\nexport class Cylinder extends BaseNode3D<CylinderStyleProps> {\n  static defaultStyleProps: Partial<CylinderStyleProps> = {\n    // radius, height\n    size: [24, 48],\n    heightSegments: 5,\n    capSegments: 20,\n  };\n\n  constructor(options: DisplayObjectConfig<CylinderStyleProps>) {\n    super(deepMix({}, { style: Cylinder.defaultStyleProps }, options));\n  }\n\n  protected getSize(attributes: CylinderStyleProps = this.attributes): Vector3 {\n    const { size } = attributes;\n    if (typeof size === 'number') return [size / 2, size, 0];\n    return super.getSize();\n  }\n\n  protected getGeometry(attributes: Required<CylinderStyleProps>): GGeometry<any> {\n    const size = this.getSize();\n    const { radius = size[0], height = size[1], heightSegments, capSegments } = attributes;\n    return createGeometry('cylinder', this.device, CylinderGeometry, { radius, height, heightSegments, capSegments });\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/elements/index.ts",
    "content": "export { BaseNode3D } from './base-node-3d';\nexport { Capsule } from './capsule';\nexport { Cone } from './cone';\nexport { Cube } from './cube';\nexport { Cylinder } from './cylinder';\nexport { Line3D } from './line-3d';\nexport { Plane } from './plane';\nexport { Sphere } from './sphere';\nexport { Torus } from './torus';\n\nexport type { BaseNode3DStyleProps } from './base-node-3d';\nexport type { CapsuleStyleProps } from './capsule';\nexport type { ConeStyleProps } from './cone';\nexport type { CubeStyleProps } from './cube';\nexport type { CylinderStyleProps } from './cylinder';\nexport type { Line3DStyleProps } from './line-3d';\nexport type { PlaneStyleProps } from './plane';\nexport type { SphereStyleProps } from './sphere';\nexport type { TorusStyleProps } from './torus';\n"
  },
  {
    "path": "packages/g6-extension-3d/src/elements/line-3d.ts",
    "content": "import type { Group } from '@antv/g';\nimport { Line } from '@antv/g';\nimport type { BaseEdgeStyleProps } from '@antv/g6';\nimport { BaseEdge } from '@antv/g6';\n\n/**\n * <zh/> 3D 直线样式\n *\n * <en/> 3D Line Style Props\n */\nexport interface Line3DStyleProps extends BaseEdgeStyleProps {}\n\n/**\n * <zh/> 直线\n *\n * <en/> Line Edge\n */\nexport class Line3D extends BaseEdge {\n  protected getKeyPath(): any {\n    return [];\n  }\n\n  protected getKeyStyle(attributes: Required<Line3DStyleProps>): any {\n    const { sourceNode, targetNode } = this;\n    const [x1, y1, z1] = sourceNode.getPosition();\n    const [x2, y2, z2] = targetNode.getPosition();\n\n    // omit path\n    const { d, ...style } = super.getKeyStyle(attributes);\n    return { x1, y1, z1, x2, y2, z2, ...style };\n  }\n\n  protected drawKeyShape(attributes = this.parsedAttributes, container: Group = this): any {\n    return this.upsert('key', Line, this.getKeyStyle(attributes), container);\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/elements/plane.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { ProceduralGeometry as GGeometry, PlaneGeometryProps } from '@antv/g-plugin-3d';\nimport { CullMode, PlaneGeometry } from '@antv/g-plugin-3d';\nimport { deepMix } from '@antv/util';\nimport { createGeometry } from '../utils/geometry';\nimport type { BaseNode3DStyleProps } from './base-node-3d';\nimport { BaseNode3D } from './base-node-3d';\n\n/**\n * <zh/> 平面节点样式\n *\n * <en/> Plane Node Style Props\n */\nexport type PlaneStyleProps = BaseNode3DStyleProps & PlaneGeometryProps;\n\n/**\n * <zh/> 平面节点\n *\n * <en/> Plane Node\n */\nexport class Plane extends BaseNode3D<PlaneStyleProps> {\n  static defaultStyleProps: Partial<PlaneStyleProps> = {\n    materialCullMode: CullMode.NONE,\n  };\n\n  constructor(options: DisplayObjectConfig<PlaneStyleProps>) {\n    super(deepMix({}, { style: Plane.defaultStyleProps }, options));\n  }\n\n  protected getGeometry(attributes: Required<PlaneStyleProps>): GGeometry<any> {\n    const size = this.getSize();\n    const { width = size[0], depth = size[1], widthSegments, depthSegments } = attributes;\n    return createGeometry('plane', this.device, PlaneGeometry, { width, depth, widthSegments, depthSegments });\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/elements/sphere.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { ProceduralGeometry as GGeometry, SphereGeometryProps } from '@antv/g-plugin-3d';\nimport { SphereGeometry } from '@antv/g-plugin-3d';\nimport { deepMix } from '@antv/util';\nimport { createGeometry } from '../utils/geometry';\nimport type { BaseNode3DStyleProps } from './base-node-3d';\nimport { BaseNode3D } from './base-node-3d';\n\n/**\n * <zh/> 球体节点样式\n *\n * <en/> Sphere Node Style Props\n */\nexport type SphereStyleProps = BaseNode3DStyleProps & SphereGeometryProps;\n\n/**\n * <zh/> 球体节点\n *\n * <en/> Sphere Node\n */\nexport class Sphere extends BaseNode3D<SphereStyleProps> {\n  static defaultStyleProps: Partial<SphereStyleProps> = {\n    // radius\n    size: 24,\n    latitudeBands: 16,\n    longitudeBands: 16,\n  };\n\n  constructor(options: DisplayObjectConfig<SphereStyleProps>) {\n    super(deepMix({}, { style: Sphere.defaultStyleProps }, options));\n  }\n\n  protected getGeometry(attributes: Required<SphereStyleProps>): GGeometry<any> {\n    const size = this.getSize();\n    const { radius = size[0] / 2, latitudeBands, longitudeBands } = attributes;\n    return createGeometry('sphere', this.device, SphereGeometry, { radius, latitudeBands, longitudeBands });\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/elements/torus.ts",
    "content": "import type { DisplayObjectConfig } from '@antv/g';\nimport type { ProceduralGeometry as GGeometry, TorusGeometryProps } from '@antv/g-plugin-3d';\nimport { TorusGeometry } from '@antv/g-plugin-3d';\nimport type { Vector3 } from '@antv/g6';\nimport { deepMix } from '@antv/util';\nimport { createGeometry } from '../utils/geometry';\nimport type { BaseNode3DStyleProps } from './base-node-3d';\nimport { BaseNode3D } from './base-node-3d';\n\n/**\n * <zh/> 圆环节点样式\n *\n * <en/> Torus Node Style Props\n */\nexport type TorusStyleProps = BaseNode3DStyleProps & TorusGeometryProps;\n\n/**\n * <zh/> 圆环节点\n *\n * <en/> Torus Node\n */\nexport class Torus extends BaseNode3D<TorusStyleProps> {\n  static defaultStyleProps: Partial<TorusStyleProps> = {\n    // tubeRadius, ringRadius\n    size: [8, 48],\n    segments: 30,\n    sides: 20,\n  };\n\n  constructor(options: DisplayObjectConfig<TorusStyleProps>) {\n    super(deepMix({}, { style: Torus.defaultStyleProps }, options));\n  }\n\n  protected getSize(attributes: TorusStyleProps = this.attributes): Vector3 {\n    const { size } = attributes;\n    if (typeof size === 'number') return [size / 8, size / 2, 0];\n    return super.getSize();\n  }\n\n  protected getGeometry(attributes: Required<TorusStyleProps>): GGeometry<any> {\n    const size = this.getSize();\n    const { tubeRadius = size[0], ringRadius = size[1], segments, sides } = attributes;\n    return createGeometry('torus', this.device, TorusGeometry, { tubeRadius, ringRadius, segments, sides });\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/exports.ts",
    "content": "export { D3Force3DLayout } from '@antv/layout';\nexport { DragCanvas3D, ObserveCanvas3D, RollCanvas3D, ZoomCanvas3D } from './behaviors';\nexport { BaseNode3D, Capsule, Cone, Cube, Cylinder, Line3D, Plane, Sphere, Torus } from './elements';\nexport { Light } from './plugins';\nexport { renderer } from './renderer';\n\nexport type {\n  DragCanvas3DOptions,\n  ObserveCanvas3DOptions,\n  RollCanvas3DOptions,\n  ZoomCanvas3DOptions,\n} from './behaviors';\nexport type {\n  BaseNode3DStyleProps,\n  CapsuleStyleProps,\n  ConeStyleProps,\n  CubeStyleProps,\n  CylinderStyleProps,\n  Line3DStyleProps,\n  PlaneStyleProps,\n  SphereStyleProps,\n  TorusStyleProps,\n} from './elements';\nexport type { LightOptions } from './plugins';\n"
  },
  {
    "path": "packages/g6-extension-3d/src/index.ts",
    "content": "export * from './exports';\n"
  },
  {
    "path": "packages/g6-extension-3d/src/plugins/index.ts",
    "content": "export { Light } from './light';\n\nexport type { LightOptions } from './light';\n"
  },
  {
    "path": "packages/g6-extension-3d/src/plugins/light.ts",
    "content": "import type { AmbientLightProps, DirectionalLightProps } from '@antv/g-plugin-3d';\nimport { AmbientLight, DirectionalLight } from '@antv/g-plugin-3d';\nimport type { BasePluginOptions, RuntimeContext } from '@antv/g6';\nimport { BasePlugin, GraphEvent } from '@antv/g6';\nimport { deepMix } from '@antv/util';\n\n/**\n * <zh/> 光照插件配置项\n *\n * <en/> Light plugin options\n */\nexport interface LightOptions extends BasePluginOptions {\n  /**\n   * <zh/> 环境光\n   *\n   * <en/> Ambient light\n   */\n  ambient?: AmbientLightProps;\n  /**\n   * <zh/> 平行光\n   *\n   * <en/> Directional light\n   */\n  directional?: DirectionalLightProps;\n}\n\n/**\n * <zh/> 光照插件\n *\n * <en/> Light plugin\n */\nexport class Light extends BasePlugin<LightOptions> {\n  static defaultOptions: Partial<LightOptions> = {\n    ambient: {\n      fill: '#fff',\n      intensity: Math.PI * 2,\n    },\n    directional: {\n      fill: '#fff',\n      direction: [-1, 0, 1],\n      intensity: Math.PI * 0.7,\n    },\n  };\n\n  private ambient?: AmbientLight;\n\n  private directional?: DirectionalLight;\n\n  constructor(context: RuntimeContext, options: LightOptions) {\n    super(context, deepMix({}, Light.defaultOptions, options));\n    this.bindEvents();\n  }\n\n  private bindEvents() {\n    this.context.graph.on(GraphEvent.BEFORE_DRAW, this.setLight);\n  }\n\n  private unbindEvents() {\n    this.context.graph.off(GraphEvent.BEFORE_DRAW, this.setLight);\n  }\n\n  private setLight = () => {\n    const { ambient, directional } = this.options;\n\n    this.upsertLight('directional', directional);\n    this.upsertLight('ambient', ambient);\n  };\n\n  private upsertLight(type: 'ambient', options?: AmbientLightProps): void;\n  private upsertLight(type: 'directional', options?: DirectionalLightProps): void;\n  private upsertLight(type: 'ambient' | 'directional', options?: AmbientLightProps | DirectionalLightProps) {\n    if (options) {\n      const light = this[type];\n      if (light) light.attr(options);\n      else {\n        const Ctor = type === 'ambient' ? AmbientLight : DirectionalLight;\n        const light = new Ctor({ style: options });\n        this[type] = light as any;\n        this.context.canvas.appendChild(light);\n      }\n    } else this[type]?.remove();\n  }\n\n  /**\n   * <zh/> 销毁插件\n   *\n   * <en/> Destroy the plugin\n   * @internal\n   */\n  public destroy() {\n    this.ambient?.remove();\n    this.directional?.remove();\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/renderer.ts",
    "content": "import { Renderer as CanvasRenderer } from '@antv/g-canvas';\nimport { Plugin as Plugin3D } from '@antv/g-plugin-3d';\nimport { Renderer as WebGLRenderer } from '@antv/g-webgl';\nimport type { CanvasOptions } from '@antv/g6';\n\n/**\n * <zh/> 3D 渲染器\n *\n * <en/> 3D renderer\n * @param layer - <zh/> 图层 | <en/> Layer\n * @returns <zh/> 渲染器实例 | <en/> Renderer instance\n */\nexport const renderer: CanvasOptions['renderer'] = (layer) => {\n  if (layer === 'label') {\n    return new CanvasRenderer();\n  }\n\n  const renderer = new WebGLRenderer();\n\n  if (layer === 'main') {\n    renderer.registerPlugin(new Plugin3D());\n  }\n\n  return renderer;\n};\n"
  },
  {
    "path": "packages/g6-extension-3d/src/types/index.ts",
    "content": "export * from './material';\n"
  },
  {
    "path": "packages/g6-extension-3d/src/types/material.ts",
    "content": "import type { IPointMaterial } from '@antv/g-plugin-3d';\nimport { IMeshBasicMaterial, IMeshLambertMaterial, IMeshPhongMaterial } from '@antv/g-plugin-3d';\n\nexport type Material = PointMaterial | BasicMaterial | LambertMaterial | PhongMaterial;\n\nexport interface PointMaterial extends Partial<Omit<IPointMaterial, 'map'>> {\n  type: 'point';\n  map?: string | TexImageSource;\n}\n\ninterface BasicMaterial extends Partial<Omit<IMeshBasicMaterial, 'map' | 'aoMap'>> {\n  type: 'basic';\n  map?: string | TexImageSource;\n}\n\ninterface LambertMaterial extends Partial<Omit<IMeshLambertMaterial, 'map' | 'aoMap'>> {\n  type: 'lambert';\n  map?: string | TexImageSource;\n  // aoMap?: string | Texture;\n}\n\ninterface PhongMaterial extends Partial<Omit<IMeshPhongMaterial, 'map' | 'aoMap'>> {\n  type: 'phong';\n  map?: string | TexImageSource;\n  // aoMap?: string | Texture;\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/utils/cache.ts",
    "content": "/**\n * <zh/> 生成对象配置的缓存键\n *\n * <en/> Generate cache key of geometry configuration\n * @param props - <zh/> 对象配置 <en/> geometry configuration\n * @returns <zh/> 缓存键 <en/> cache key\n */\nexport function getCacheKey(props: Record<string, any>): symbol {\n  const entries = Object.entries(props);\n\n  if (entries.some(([, value]) => typeof value === 'object')) {\n    return Symbol();\n  }\n\n  const str = entries\n    .sort((a, b) => a[0].localeCompare(b[0]))\n    .map(([key, value]) => {\n      return `${key}:${value}`;\n    })\n    .join(' ');\n\n  return Symbol.for(str);\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/utils/geometry.ts",
    "content": "import type { Device } from '@antv/g-device-api';\nimport type { ProceduralGeometry } from '@antv/g-plugin-3d';\n\nlet DEVICE: Device;\n\nconst GEOMETRY_CACHE = new Map<string, unknown>();\n\n/**\n * <zh/> 创建几何体\n *\n * <en/> Create geometry\n * @param type - <zh/> 几何体类型 <en/> geometry type\n * @param device - <zh/> 设备对象 <en/> device object\n * @param Ctor - <zh/> 几何体构造函数 <en/> geometry constructor\n * @param style - <zh/> 几何体样式 <en/> geometry style\n * @returns <zh/> 几何体对象 <en/> geometry object\n */\nexport function createGeometry<T extends ProceduralGeometry<any>>(\n  type: string,\n  device: Device,\n  Ctor: new (...args: any[]) => T,\n  style: Record<string, unknown>,\n) {\n  if (!DEVICE) DEVICE = device;\n  else if (DEVICE !== device) {\n    DEVICE = device;\n    GEOMETRY_CACHE.clear();\n  }\n\n  const cacheKey =\n    type +\n    '|' +\n    Object.entries(style)\n      .sort(([a], [b]) => a.localeCompare(b))\n      .map(([k, v]) => `${k}:${v}`)\n      .join(',');\n\n  if (GEOMETRY_CACHE.has(cacheKey)) {\n    return GEOMETRY_CACHE.get(cacheKey) as T;\n  }\n\n  const geometry = new Ctor(device, style);\n\n  GEOMETRY_CACHE.set(cacheKey, geometry);\n\n  return geometry;\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/utils/map.ts",
    "content": "export class TupleMap<K1, K2, V> {\n  private map = new Map<K1, Map<K2, V>>();\n\n  has(key1: K1, key2: K2) {\n    return this.map.has(key1) && this.map.get(key1)!.has(key2);\n  }\n\n  get(key1: K1, key2: K2) {\n    return this.map.get(key1)?.get(key2);\n  }\n\n  set(key1: K1, key2: K2, value: V) {\n    if (!this.map.has(key1)) {\n      this.map.set(key1, new Map());\n    }\n    this.map.get(key1)!.set(key2, value);\n  }\n\n  clear() {\n    this.map.clear();\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/utils/material.ts",
    "content": "import type { Material as GMaterial } from '@antv/g-plugin-3d';\nimport { MeshBasicMaterial, MeshLambertMaterial, MeshPhongMaterial, PointMaterial } from '@antv/g-plugin-3d';\nimport type { Plugin } from '@antv/g-plugin-device-renderer';\nimport { get, set } from '@antv/util';\nimport type { Material } from '../types';\nimport { getCacheKey } from './cache';\nimport { TupleMap } from './map';\nimport { createTexture } from './texture';\n\ntype MaterialCache = TupleMap<symbol, string | TexImageSource | undefined, GMaterial>;\n\nconst MATERIAL_CACHE_KEY = '__MATERIAL_CACHE__';\n\nconst MATERIAL_MAP = {\n  basic: MeshBasicMaterial,\n  point: PointMaterial,\n  lambert: MeshLambertMaterial,\n  phong: MeshPhongMaterial,\n};\n\n/**\n * <zh/> 基于配置创建材质，支持缓存\n *\n * <en/> Create material based on configuration, support cache\n * @param plugin - <zh/> 插件对象 <en/> plugin\n * @param options - <zh/> 材质配置 <en/> material configuration\n * @param texture - <zh/> 纹理 <en/> texture\n * @returns <zh/> 材质对象 <en/> material object\n */\nexport function createMaterial(plugin: Plugin, options: Material, texture?: string | TexImageSource): GMaterial {\n  let cache: MaterialCache = get(plugin, MATERIAL_CACHE_KEY);\n  if (!cache) {\n    cache = new TupleMap();\n    set(plugin, MATERIAL_CACHE_KEY, cache);\n  }\n\n  const key = getCacheKey(options);\n\n  if (cache.has(key, texture)) {\n    return cache.get(key, texture)!;\n  }\n\n  const device = plugin.getDevice();\n  const { type, map = texture, ...opts } = options;\n  const Ctor = MATERIAL_MAP[type];\n\n  // @ts-expect-error ignore\n  const material = new Ctor(device, { map: createTexture(plugin, map), ...opts });\n  cache.set(key, texture, material);\n  return material;\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/src/utils/texture.ts",
    "content": "import type { Texture } from '@antv/g-device-api';\nimport type { Plugin } from '@antv/g-plugin-device-renderer';\nimport { get, set } from '@antv/util';\n\ntype TextureCache = Map<string | TexImageSource, Texture>;\n\nconst TEXTURE_CACHE_KEY = '__TEXTURE_CACHE__';\n\n// const TEXTURE_CACHE = new Map<string | TexImageSource, Texture>();\n\n/**\n * <zh/> 创建纹理，支持缓存\n *\n * <en/> Create texture, support cache\n * @param plugin - <zh/> 插件对象 <en/> plugin\n * @param src - <zh/> 纹理路径或者图片对象 <en/> texture path or image object\n * @returns <zh/> 纹理对象 <en/> texture object\n */\nexport function createTexture(plugin: Plugin, src?: string | TexImageSource): Texture | undefined {\n  if (!src) return;\n\n  let cache: TextureCache = get(plugin, TEXTURE_CACHE_KEY);\n  if (!cache) {\n    cache = new Map();\n    set(plugin, TEXTURE_CACHE_KEY, cache);\n  }\n\n  if (cache.has(src)) {\n    return cache.get(src);\n  }\n  const texture = plugin.loadTexture(src);\n  cache.set(src, texture);\n  return texture;\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/tsconfig.build.json",
    "content": "{\n  \"compilerOptions\": {\n    \"paths\": {}\n  },\n  \"include\": [\"src/**/*\"],\n  \"extends\": \"./tsconfig.json\"\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"outDir\": \"lib\",\n    \"paths\": {\n      \"@antv/g6\": [\"../g6/src/index.ts\"]\n    }\n  },\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src/**/*\", \"__tests__/**/*\"]\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/tsconfig.test.json",
    "content": "{\n  \"compilerOptions\": {\n    \"paths\": {}\n  },\n  \"include\": [\"src/**/*\", \"__tests__/**/*\"],\n  \"extends\": \"./tsconfig.json\"\n}\n"
  },
  {
    "path": "packages/g6-extension-3d/vite.config.js",
    "content": "import path from 'path';\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n  root: './__tests__',\n  server: {\n    port: 8081,\n    open: '/',\n  },\n  plugins: [{ name: 'isolation' }],\n  resolve: {\n    alias: {\n      '@antv/g6': path.resolve(__dirname, '../g6/src'),\n    },\n  },\n});\n"
  },
  {
    "path": "packages/g6-extension-react/README.md",
    "content": "## React extension for G6\n\n<img width=\"500\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*rWSiT6dnwfcAAAAAAAAAAAAADmJ7AQ/original\" />\n\nThis extension allows you to define G6 node by React component and JSX syntax.\n\n## Usage\n\n1. Install\n\n```bash\nnpm install @antv/g6-extension-react\n```\n\n2. Import and Register\n\n```js\nimport { ExtensionCategory, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\n\nregister(ExtensionCategory.NODE, 'react', ReactNode);\n```\n\n3. Define Node\n\nReact Node:\n\n```jsx\nconst ReactNode = () => {\n  return <div>node</div>;\n};\n```\n\nG Node:\n\n```jsx\nimport { Group, Rect, Text } from '@antv/g6-extension-react';\n\nconst GNode = () => {\n  return <Group>\n    <Rect width={100} height={100}></Rect>\n    <Text text={\"node\"} />\n  <Group>\n};\n```\n\n4. Use\n\nUse ReactNode:\n\n```jsx\nconst graph = new Graph({\n  // ... other options\n  node: {\n    type: 'react',\n    style: {\n      component: () => <ReactNode />,\n    },\n  },\n});\n```\n\nUse GNode:\n\n```jsx\nconst graph = new Graph({\n  // ... other options\n  node: {\n    type: 'g',\n    style: {\n      component: () => <GNode />,\n    },\n  },\n});\n```\n\n## Q&A\n\n1. Difference between ReactNode and GNode\n\nReactNode is a React component, while GNode support jsx syntax but can only use G tag node.\n\n## Resources\n\n- [React node](https://g6.antv.antgroup.com/examples/element/custom-node/#react-node)\n- [G node with JSX syntax](https://g6.antv.antgroup.com/en/examples/element/custom-node/#react-g)\n"
  },
  {
    "path": "packages/g6-extension-react/__tests__/.eslintrc",
    "content": "{\n  \"rules\": {\n    \"no-console\": \"off\"\n  }\n}"
  },
  {
    "path": "packages/g6-extension-react/__tests__/dataset/euro-cup.json",
    "content": "{\n  \"nodes\": [\n    {\n      \"id\": \"50251337\",\n      \"x\": 50,\n      \"y\": 68,\n      \"isTeamA\": \"1\",\n      \"player_id\": \"50251337\",\n      \"player_shirtnumber\": \"19\",\n      \"player_enName\": \"Justin Kluivert\",\n      \"player_name\": \"尤斯廷-克鲁伊维特\"\n    },\n    {\n      \"id\": \"50436685\",\n      \"x\": 25,\n      \"y\": 68,\n      \"isTeamA\": \"1\",\n      \"player_id\": \"50436685\",\n      \"player_shirtnumber\": \"24\",\n      \"player_enName\": \"Antoine Semenyo\",\n      \"player_name\": \"塞门约\"\n    },\n    {\n      \"id\": \"50204813\",\n      \"x\": 50,\n      \"y\": 89,\n      \"isTeamA\": \"1\",\n      \"player_id\": \"50204813\",\n      \"player_shirtnumber\": \"9\",\n      \"player_enName\": \"Dominic Solanke\",\n      \"player_name\": \"索兰克\"\n    },\n    {\n      \"id\": \"50250175\",\n      \"x\": 75,\n      \"y\": 68,\n      \"isTeamA\": \"1\",\n      \"player_id\": \"50250175\",\n      \"player_shirtnumber\": \"16\",\n      \"player_enName\": \"Marcus Tavernier\",\n      \"player_name\": \"塔韦尼耶\"\n    },\n    {\n      \"id\": \"50213675\",\n      \"x\": 65,\n      \"y\": 48,\n      \"isTeamA\": \"1\",\n      \"player_id\": \"50213675\",\n      \"player_shirtnumber\": \"4\",\n      \"player_enName\": \"Lewis Cook\",\n      \"player_name\": \"刘易斯-库克\"\n    },\n    {\n      \"id\": \"50186648\",\n      \"x\": 35,\n      \"y\": 48,\n      \"isTeamA\": \"1\",\n      \"player_id\": \"50186648\",\n      \"player_shirtnumber\": \"10\",\n      \"player_enName\": \"Ryan Christie\",\n      \"player_name\": \"克里斯蒂\"\n    },\n    {\n      \"id\": \"50279448\",\n      \"x\": 38,\n      \"y\": 28,\n      \"isTeamA\": \"1\",\n      \"player_id\": \"50279448\",\n      \"player_shirtnumber\": \"6\",\n      \"player_enName\": \"Chris Mepham\",\n      \"player_name\": \"迈帕姆\"\n    },\n    {\n      \"id\": \"50061646\",\n      \"x\": 15,\n      \"y\": 28,\n      \"isTeamA\": \"1\",\n      \"player_id\": \"50061646\",\n      \"player_shirtnumber\": \"15\",\n      \"player_enName\": \"Adam Smith\",\n      \"player_name\": \"亚当-史密斯\"\n    },\n    {\n      \"id\": \"50472140\",\n      \"x\": 62,\n      \"y\": 28,\n      \"isTeamA\": \"1\",\n      \"player_id\": \"50472140\",\n      \"player_shirtnumber\": \"27\",\n      \"player_enName\": \"Ilya Zabarnyi\",\n      \"player_name\": \"扎巴尔尼\"\n    },\n    {\n      \"id\": \"50544346\",\n      \"x\": 85,\n      \"y\": 28,\n      \"isTeamA\": \"1\",\n      \"player_id\": \"50544346\",\n      \"player_shirtnumber\": \"3\",\n      \"player_enName\": \"Milos Kerkez\",\n      \"player_name\": \"科尔克兹\"\n    },\n    {\n      \"id\": \"50062598\",\n      \"x\": 50,\n      \"y\": 7,\n      \"isTeamA\": \"1\",\n      \"player_id\": \"50062598\",\n      \"player_shirtnumber\": \"1\",\n      \"player_enName\": \"Neto\",\n      \"player_name\": \"内托\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/g6-extension-react/__tests__/demos/euro-cup.tsx",
    "content": "import { ExtensionCategory, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\nimport styled from 'styled-components';\nimport data from '../dataset/euro-cup.json';\nimport { Graph } from '../graph';\n\nconst Player = styled.div`\n  width: 100%;\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n`;\n\nconst Shirt = styled.div`\n  width: 40px;\n  height: 40px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  position: relative;\n\n  img {\n    width: 40px;\n    position: absolute;\n    left: 0;\n    top: 0;\n  }\n`;\n\nconst Number = styled.div`\n  color: #fff;\n  font-family: 'DingTalk-JinBuTi';\n  font-size: 10px;\n  top: 20px;\n  left: 15px;\n  z-index: 1;\n  margin-top: 16px;\n  margin-left: -2px;\n`;\n\nconst Label = styled.div`\n  max-width: 120px;\n  padding: 0 8px;\n  color: #fff;\n  font-size: 10px;\n  background-image: url('https://mdn.alipayobjects.com/huamei_92awrc/afts/img/A*s2csQ48M0AkAAAAAAAAAAAAADsvfAQ/original');\n  background-repeat: no-repeat;\n  background-size: cover;\n  display: flex;\n  justify-content: center;\n  overflow: hidden;\n  text-overflow: ellipsis;\n`;\n\nconst PlayerNode = ({ playerInfo }: any) => {\n  const { isTeamA, player_shirtnumber, player_name } = playerInfo;\n  return (\n    <Player>\n      <Shirt>\n        <img\n          src={\n            isTeamA\n              ? 'https://mdn.alipayobjects.com/huamei_92awrc/afts/img/A*0oAaS42vqWcAAAAAAAAAAAAADsvfAQ/original'\n              : 'https://mdn.alipayobjects.com/huamei_92awrc/afts/img/A*BYH5SauBNecAAAAAAAAAAAAADsvfAQ/original'\n          }\n        />\n        <Number>{player_shirtnumber}</Number>\n      </Shirt>\n      <Label>{player_name}</Label>\n    </Player>\n  );\n};\n\nregister(ExtensionCategory.NODE, 'react', ReactNode);\n\nexport const EuroCup = () => {\n  return (\n    <div>\n      <Graph\n        options={{\n          data,\n          animation: false,\n          x: 10,\n          y: 50,\n          width: 480,\n          height: 720,\n          node: {\n            type: 'react',\n            style: {\n              size: [100, 60],\n              ports: [{ placement: 'center' }],\n              x: (d: any) => d.x * 3.5,\n              y: (d: any) => d.y * 3.5,\n              fill: 'transparent',\n              component: (data: any) => <PlayerNode playerInfo={data} />,\n            },\n          },\n          plugins: [\n            {\n              type: 'background',\n              width: '480px',\n              height: '720px',\n              backgroundImage:\n                'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*EmPXQLrX2xIAAAAAAAAAAAAADmJ7AQ/original)',\n              backgroundRepeat: 'no-repeat',\n              backgroundSize: 'contain',\n              opacity: 1,\n            },\n          ],\n        }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/g6-extension-react/__tests__/demos/graph.tsx",
    "content": "import { Graph } from '../graph';\n\nexport const G6Graph = () => {\n  return (\n    <Graph\n      options={{\n        data: {\n          nodes: [\n            { id: 'node-1', style: { x: 100, y: 100, labelText: 'Hello' } },\n            { id: 'node-2', style: { x: 300, y: 100, labelText: 'World' } },\n          ],\n          edges: [{ source: 'node-1', target: 'node-2' }],\n        },\n        behaviors: ['drag-element'],\n      }}\n      onRender={() => {\n        console.log('render');\n      }}\n      onDestroy={() => {\n        console.log('destroy');\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "packages/g6-extension-react/__tests__/demos/index.tsx",
    "content": "export * from './euro-cup';\nexport * from './graph';\nexport * from './performance-diagnosis';\nexport * from './react-node';\n"
  },
  {
    "path": "packages/g6-extension-react/__tests__/demos/performance-diagnosis.tsx",
    "content": "import { BugOutlined } from '@ant-design/icons';\nimport type { EdgeData, Element, GraphData, GraphOptions, IPointerEvent, NodeData } from '@antv/g6';\nimport { ExtensionCategory, HoverActivate, idOf, register } from '@antv/g6';\nimport { Flex, Typography } from 'antd';\nimport { CSSProperties, useEffect, useState } from 'react';\nimport { Graph } from '../graph';\n\nconst { Text } = Typography;\n\nconst ACTIVE_COLOR = '#f6c523';\nconst COLOR_MAP: Record<string, string> = {\n  'pre-inspection': '#3fc1c9',\n  problem: '#8983f3',\n  inspection: '#f48db4',\n  solution: '#ffaa64',\n};\n\nclass HoverElement extends HoverActivate {\n  protected getActiveIds(event: IPointerEvent<Element>) {\n    const { model, graph } = this.context;\n    const elementId = event.target.id;\n    const { targetType: elementType } = event;\n\n    const ids = [elementId];\n    if (elementType === 'edge') {\n      const edge = model.getEdgeDatum(elementId);\n      ids.push(edge.source, edge.target);\n    } else if (elementType === 'node') {\n      ids.push(...model.getRelatedEdgesData(elementId).map(idOf));\n    }\n\n    graph.frontElement(ids);\n\n    return ids;\n  }\n}\n\nregister(ExtensionCategory.BEHAVIOR, 'hover-element', HoverElement);\n\nconst Node = ({ data }: { data: NodeData }) => {\n  const { text, type } = data.data as { text: string; type: string };\n\n  const isHovered = data.states?.includes('active');\n  const isSelected = data.states?.includes('selected');\n  const color = isHovered ? ACTIVE_COLOR : COLOR_MAP[type];\n\n  const containerStyle: CSSProperties = {\n    width: '100%',\n    height: '100%',\n    background: color,\n    border: `3px solid ${color}`,\n    borderRadius: 16,\n    cursor: 'pointer',\n  };\n\n  if (isSelected) {\n    Object.assign(containerStyle, { border: `3px solid #000` });\n  }\n\n  return (\n    <Flex style={containerStyle} align=\"center\" justify=\"center\">\n      <Flex vertical style={{ padding: '8px 16px', textAlign: 'center' }} align=\"center\" justify=\"center\">\n        {type === 'problem' && <BugOutlined style={{ color: '#fff', fontSize: 24, marginBottom: 8 }} />}\n        <Text style={{ color: '#fff', fontWeight: 600, fontSize: 16 }}>{text}</Text>\n      </Flex>\n    </Flex>\n  );\n};\n\nexport const PerformanceDiagnosis = () => {\n  const [data, setData] = useState<GraphData>();\n\n  useEffect(() => {\n    fetch('https://assets.antv.antgroup.com/g6/performance-diagnosis.json')\n      .then((res) => res.json())\n      .then(setData);\n  }, []);\n\n  const options: GraphOptions = {\n    data,\n    animation: false,\n    width: 800,\n    height: 600,\n    autoFit: 'view',\n    node: {\n      type: 'react',\n      style: (d: NodeData) => {\n        const style: NodeData['style'] = {\n          component: <Node data={d} />,\n          ports: [{ placement: 'top' }, { placement: 'bottom' }],\n        };\n\n        const size = {\n          'pre-inspection': [240, 120],\n          problem: [200, 120],\n          inspection: [330, 100],\n          solution: [200, 120],\n        }[d.data!.type as string] || [200, 80];\n\n        Object.assign(style, {\n          size,\n          dx: -size[0] / 2,\n          dy: -size[1] / 2,\n        });\n        return style;\n      },\n      state: {\n        active: {\n          halo: false,\n        },\n        selected: {\n          halo: false,\n        },\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        lineWidth: 3,\n        radius: 20,\n        stroke: '#8b9baf',\n        endArrow: true,\n        labelText: (d: EdgeData) => d.data!.text as string,\n        labelFill: '#8b9baf',\n        labelFontWeight: 600,\n        labelBackground: true,\n        labelBackgroundFill: '#f8f8f8',\n        labelBackgroundOpacity: 1,\n        labelBackgroundLineWidth: 3,\n        labelBackgroundStroke: '#8b9baf',\n        labelPadding: [1, 10],\n        labelBackgroundRadius: 4,\n        router: {\n          type: 'orth',\n        },\n      },\n      state: {\n        active: {\n          stroke: ACTIVE_COLOR,\n          labelBackgroundStroke: ACTIVE_COLOR,\n          halo: false,\n        },\n      },\n    },\n    layout: {\n      type: 'antv-dagre',\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas', 'hover-element', 'click-select'],\n  };\n\n  return <Graph options={options} />;\n};\n"
  },
  {
    "path": "packages/g6-extension-react/__tests__/demos/react-node.tsx",
    "content": "import { DatabaseFilled } from '@ant-design/icons';\nimport type { Graph as G6Graph, GraphOptions, NodeData } from '@antv/g6';\nimport { ExtensionCategory, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\nimport { Badge, Button, Flex, Form, Input, Layout, Select, Table, Tag, Typography } from 'antd';\nimport { useRef, useState } from 'react';\nimport { Graph } from '../graph';\n\nconst { Content, Footer } = Layout;\nconst { Text } = Typography;\n\nregister(ExtensionCategory.NODE, 'react', ReactNode);\n\ntype Datum = {\n  name: string;\n  status: 'success' | 'error' | 'warning';\n  type: 'local' | 'remote';\n  url: string;\n};\n\nconst Node = ({ data, onChange }: { data: NodeData; onChange?: (value: string) => void }) => {\n  const { status, type } = data.data as Datum;\n\n  return (\n    <Flex style={{ width: '100%', height: '100%', background: '#fff', padding: 10, borderRadius: 5 }} vertical>\n      <Flex align=\"center\" justify=\"space-between\">\n        <Text>\n          <DatabaseFilled />\n          Server\n          <Tag>{type}</Tag>\n        </Text>\n        <Badge status={status} />\n      </Flex>\n      <Text type=\"secondary\">{data.id}</Text>\n      <Flex align=\"center\">\n        <Text style={{ flexShrink: 0 }}>\n          <Text type=\"danger\">*</Text>URL:\n        </Text>\n        <Input\n          style={{ borderRadius: 0, borderBottom: '1px solid #d9d9d9' }}\n          variant=\"borderless\"\n          value={data.data?.url as string}\n          onChange={(event) => {\n            const url = event.target.value;\n            onChange?.(url);\n          }}\n        />\n      </Flex>\n    </Flex>\n  );\n};\n\nexport const ReactNodeDemo = () => {\n  const graphRef = useRef<G6Graph | null>(null);\n\n  const [form] = Form.useForm();\n  const isValidUrl = (url: string) => {\n    return /https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/.test(\n      url,\n    );\n  };\n\n  const [options, setOptions] = useState<GraphOptions>({\n    data: {\n      nodes: [\n        {\n          id: 'local-server-1',\n          data: { status: 'success', type: 'local', url: 'http://localhost:3000' },\n          style: { x: 50, y: 50 },\n        },\n        {\n          id: 'remote-server-1',\n          data: { status: 'warning', type: 'remote' },\n          style: { x: 350, y: 50 },\n        },\n      ],\n      edges: [{ source: 'local-server-1', target: 'remote-server-1' }],\n    },\n    node: {\n      type: 'react',\n      style: {\n        size: [240, 100],\n        component: (data: NodeData) => (\n          <Node\n            data={data}\n            onChange={(url) => {\n              setOptions((prev) => {\n                if (!graphRef.current || graphRef.current.destroyed) return prev;\n                const nodes = graphRef.current.getNodeData();\n                const index = nodes.findIndex((node) => node.id === data.id);\n                const node = nodes[index];\n                const datum = {\n                  ...node.data,\n                  url,\n                  status: url === '' ? 'warning' : isValidUrl(url) ? 'success' : 'error',\n                } as Datum;\n                nodes[index] = { ...node, data: datum };\n                return { ...prev, data: { ...prev.data, nodes } };\n              });\n            }}\n          />\n        ),\n      },\n    },\n    behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],\n  });\n\n  return (\n    <Layout style={{ width: 800, height: 400 }}>\n      <Content style={{ minHeight: 400 }}>\n        <Graph options={options} onRender={(graph) => (graphRef.current = graph)} />\n      </Content>\n      <Footer>\n        <Form form={form} initialValues={{ serverType: 'local' }}>\n          <Form.Item label=\"Server Type\" name=\"serverType\">\n            <Select\n              options={[\n                { label: 'Local', value: 'local' },\n                { label: 'Remote', value: 'remote' },\n              ]}\n            />\n          </Form.Item>\n          <Form.Item>\n            <Button.Group>\n              <Button\n                style={{ width: '100%' }}\n                type=\"primary\"\n                onClick={() => {\n                  if (!graphRef.current || graphRef.current.destroyed) return;\n                  const type = form.getFieldValue('serverType');\n                  const status = 'warning';\n                  const length = (options.data?.nodes || []).filter((node) => node?.data?.type === type).length;\n                  setOptions((options) => ({\n                    ...options,\n                    data: {\n                      ...options.data,\n                      nodes: [\n                        ...graphRef.current!.getNodeData(),\n                        {\n                          id: `${type}-server-${length + 1}`,\n                          data: { type, status },\n                          style: { x: type === 'local' ? 50 : 350, y: 50 + length * 120 },\n                        },\n                      ],\n                    },\n                  }));\n                }}\n              >\n                Add Node\n              </Button>\n              <Button\n                onClick={() => {\n                  const { data } = options;\n                  const nodes = data?.nodes || [];\n                  setOptions((options) => ({\n                    ...options,\n                    data: {\n                      ...options.data,\n                      nodes: nodes.map((node, index) => {\n                        // return node;\n                        if (index === nodes.length - 1) {\n                          return {\n                            ...node,\n                            data: {\n                              ...node.data,\n                              status: node.data!.status === 'success' ? 'warning' : 'success',\n                            },\n                          };\n                        }\n                        return node;\n                      }),\n                    },\n                  }));\n\n                  graphRef.current?.draw();\n                }}\n              >\n                Update Node\n              </Button>\n              <Button\n                danger\n                onClick={() => {\n                  const { data } = options;\n                  const nodes = data?.nodes || [];\n                  setOptions((options) => ({\n                    ...options,\n                    data: {\n                      ...options.data,\n                      nodes: nodes.filter((node, index) => index !== nodes.length - 1),\n                    },\n                  }));\n                }}\n              >\n                Remove Node\n              </Button>\n            </Button.Group>\n          </Form.Item>\n        </Form>\n\n        <Table\n          columns={[\n            { title: 'Server', key: 'server', dataIndex: 'server' },\n            { title: 'URL', key: 'url', dataIndex: 'url' },\n          ]}\n          dataSource={(options.data?.nodes || []).map((node) => ({\n            key: node.id,\n            server: node.id,\n            url: (node?.data as Datum).url || 'Not Configured',\n          }))}\n        ></Table>\n      </Footer>\n    </Layout>\n  );\n};\n"
  },
  {
    "path": "packages/g6-extension-react/__tests__/graph.tsx",
    "content": "import type { GraphOptions } from '@antv/g6';\nimport { Graph as G6Graph } from '@antv/g6';\nimport { useEffect, useRef } from 'react';\n\nexport interface GraphProps {\n  options: GraphOptions;\n  onRender?: (graph: G6Graph) => void;\n  onDestroy?: () => void;\n}\n\nexport const Graph = (props: GraphProps) => {\n  const { options, onRender, onDestroy } = props;\n  const graphRef = useRef<G6Graph | null>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const graph = new G6Graph({ container: containerRef.current! });\n    graphRef.current = graph;\n\n    return () => {\n      const graph = graphRef.current;\n      if (graph) {\n        graph.destroy();\n        onDestroy?.();\n        graphRef.current = null;\n      }\n    };\n  }, []);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    const graph = graphRef.current;\n\n    if (!options || !container || !graph || graph.destroyed) return;\n\n    graph.setOptions(options);\n    graph\n      .render()\n      .then(() => onRender?.(graph))\n      // eslint-disable-next-line no-console\n      .catch((error) => console.debug(error));\n  }, [options]);\n\n  return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;\n};\n"
  },
  {
    "path": "packages/g6-extension-react/__tests__/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>@antv/g6-extension-react</title>\n    <style>\n      body {\n        margin: 0;\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"./main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/g6-extension-react/__tests__/main.tsx",
    "content": "import { render } from '@antv/g6-extension-react';\nimport { Alert, Flex, Select } from 'antd';\nimport React from 'react';\nimport { Outlet, RouterProvider, createBrowserRouter, useMatch, useNavigate } from 'react-router-dom';\nimport * as demos from './demos';\n\nconst App = () => {\n  const navigate = useNavigate();\n  const match = useMatch('/*');\n\n  return (\n    <Flex vertical>\n      <Select\n        value={match?.params['*'] || Object.keys(demos)[0]}\n        options={Object.keys(demos).map((label) => ({ label, value: label }))}\n        style={{ width: 100 }}\n        onChange={(value) => navigate(value)}\n      />\n      <Outlet />\n    </Flex>\n  );\n};\n\nconst NotFount = () => {\n  return <Alert message=\"Demo Not Found\" type=\"error\" />;\n};\n\nconst router = createBrowserRouter([\n  {\n    path: '/',\n    element: <App />,\n    errorElement: <NotFount />,\n    children: Object.entries(demos).map(([key, Demo]) => ({\n      path: key,\n      element: <Demo />,\n    })),\n  },\n]);\n\nconst container = document.getElementById('root')!;\n\nrender(\n  <React.StrictMode>\n    <RouterProvider router={router} />\n  </React.StrictMode>,\n  container,\n);\n"
  },
  {
    "path": "packages/g6-extension-react/__tests__/unit/attribute-changed-callback.spec.tsx",
    "content": "import { ReactNode } from '../../src';\n\n// TODO: The test is not working properly, need to fix it later.\nit.skip('attribute changed callback', () => {\n  const oldComponent = () => <div>test</div>;\n  const node = new ReactNode({\n    style: {\n      component: oldComponent,\n    },\n  });\n  Object.assign(node, {\n    isConnected: true,\n    ownerDocument: {\n      defaultView: {\n        dispatchEvent: () => {},\n      },\n    },\n  });\n\n  const spy = jest.fn();\n  node.attributeChangedCallback = spy;\n\n  const component = () => <div>test1</div>;\n\n  try {\n    node.update({ component });\n  } catch (e) {\n    // ignore\n  }\n\n  expect(spy).toHaveBeenCalled();\n  expect(spy).toHaveBeenLastCalledWith('component', oldComponent, component, undefined, undefined);\n});\n"
  },
  {
    "path": "packages/g6-extension-react/__tests__/unit/default.spec.ts",
    "content": "describe('default', () => {\n  it('expect', () => {\n    expect(1).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/g6-extension-react/jest.config.js",
    "content": "const esm = ['internmap', 'd3-*', 'lodash-es', 'chalk'].map((d) => `_${d}|${d}`).join('|');\n\nmodule.exports = {\n  transform: {\n    '^.+\\\\.[tj]sx?$': [\n      '@swc/jest',\n      {\n        jsc: {\n          parser: {\n            syntax: 'typescript',\n            decorators: true,\n            jsx: true,\n          },\n        },\n      },\n    ],\n  },\n  testRegex: '(/__tests__/.*\\\\.(test|spec))\\\\.(ts|tsx|js)$',\n  collectCoverageFrom: ['src/**/*.ts'],\n  moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],\n  transformIgnorePatterns: [`<rootDir>/node_modules/.pnpm/(?!(${esm}))`],\n  moduleNameMapper: {\n    '@antv/g6': '<rootDir>/../g6/src',\n  },\n};\n"
  },
  {
    "path": "packages/g6-extension-react/package.json",
    "content": "{\n  \"name\": \"@antv/g6-extension-react\",\n  \"version\": \"0.2.6\",\n  \"description\": \"Using React Component to Define Your G6 Graph Node\",\n  \"keywords\": [\n    \"antv\",\n    \"g6\",\n    \"extension\",\n    \"react\",\n    \"node\"\n  ],\n  \"repository\": \"https://github.com/antvis/G6.git\",\n  \"license\": \"MIT\",\n  \"author\": \"Aarebecca\",\n  \"main\": \"lib/index.js\",\n  \"module\": \"esm/index.js\",\n  \"types\": \"lib/index.d.ts\",\n  \"files\": [\n    \"src\",\n    \"esm\",\n    \"lib\",\n    \"dist\",\n    \"README\"\n  ],\n  \"scripts\": {\n    \"build\": \"run-p build:*\",\n    \"build:cjs\": \"rimraf ./lib && tsc --module commonjs --outDir lib -p tsconfig.build.json\",\n    \"build:esm\": \"rimraf ./esm && tsc --module ESNext --outDir esm -p tsconfig.build.json\",\n    \"build:umd\": \"rimraf ./dist && rollup -c\",\n    \"ci\": \"run-s lint type-check test build\",\n    \"dev\": \"vite\",\n    \"lint\": \"eslint ./src __tests__ --quiet && prettier ./src __tests__ --check\",\n    \"prepublishOnly\": \"npm run ci\",\n    \"test\": \"jest\",\n    \"type-check\": \"tsc --noEmit -p tsconfig.test.json\"\n  },\n  \"dependencies\": {\n    \"@antv/g\": \"^6.1.24\",\n    \"@antv/g-svg\": \"^2.0.38\"\n  },\n  \"devDependencies\": {\n    \"@ant-design/icons\": \"^5.6.1\",\n    \"@antv/g6\": \"workspace:*\",\n    \"@types/react\": \"^19.1.4\",\n    \"@types/react-dom\": \"^19.1.5\",\n    \"antd\": \"^5.25.1\",\n    \"react\": \"^19.1.0\",\n    \"react-dom\": \"^19.1.0\",\n    \"react-router-dom\": \"^6.30.0\",\n    \"styled-components\": \"^6.1.18\"\n  },\n  \"peerDependencies\": {\n    \"@antv/g6\": \"workspace:^\",\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-react/rollup.config.mjs",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport resolve from '@rollup/plugin-node-resolve';\nimport terser from '@rollup/plugin-terser';\nimport typescript from '@rollup/plugin-typescript';\nimport nodePolyfills from 'rollup-plugin-polyfill-node';\nimport { visualizer } from 'rollup-plugin-visualizer';\n\nconst isBundleVis = !!process.env.BUNDLE_VIS;\n\nexport default [\n  {\n    input: 'src/index.ts',\n    output: {\n      file: 'dist/g6-extension-react.min.js',\n      name: 'G6ExtensionReact',\n      format: 'umd',\n      sourcemap: false,\n      inlineDynamicImports: true,\n    },\n    plugins: [\n      nodePolyfills(),\n      resolve(),\n      commonjs(),\n      typescript({\n        tsconfig: 'tsconfig.build.json',\n      }),\n      terser(),\n      ...(isBundleVis ? [visualizer()] : []),\n    ],\n  },\n];\n"
  },
  {
    "path": "packages/g6-extension-react/src/index.ts",
    "content": "export { ReactNode, render, unmount } from './react-node';\n\nexport type { ReactNodeStyleProps } from './react-node';\n"
  },
  {
    "path": "packages/g6-extension-react/src/react-node/index.ts",
    "content": "export { ReactNode } from './node';\nexport { render, unmount } from './render';\n\nexport type { ReactNodeStyleProps } from './node';\n"
  },
  {
    "path": "packages/g6-extension-react/src/react-node/node.tsx",
    "content": "import type { DisplayObjectConfig, HTMLStyleProps as GHTMLStyleProps } from '@antv/g';\nimport type { BaseNodeStyleProps, HTMLStyleProps } from '@antv/g6';\nimport { HTML } from '@antv/g6';\nimport type { FC, ReactElement } from 'react';\nimport { render, unmount } from './render';\n\nexport interface ReactNodeStyleProps extends BaseNodeStyleProps {\n  /**\n   * <zh/> React 组件\n   *\n   * <en/> React component\n   */\n  component: FC;\n}\n\nexport class ReactNode extends HTML {\n  protected getKeyStyle(attributes: Required<HTMLStyleProps>): GHTMLStyleProps {\n    return { ...super.getKeyStyle(attributes) };\n  }\n\n  constructor(options: DisplayObjectConfig<ReactNodeStyleProps>) {\n    super(options as any);\n  }\n\n  public update(attr?: Partial<ReactNodeStyleProps> | undefined): void {\n    super.update(attr);\n  }\n\n  public connectedCallback() {\n    super.connectedCallback();\n    // this.root = createRoot(this.getDomElement());\n    const { component } = this.attributes as unknown as ReactNodeStyleProps;\n    // component 已经被回调机制自动创建为 ReactNode\n    // component has been automatically created as ReactNode by the callback mechanism\n    render(component as unknown as ReactElement, this.getDomElement());\n  }\n\n  public attributeChangedCallback(name: any, oldValue: any, newValue: any) {\n    super.attributeChangedCallback(name, oldValue, newValue);\n    if (name === 'component' && oldValue !== newValue) {\n      render(\n        (this.attributes as unknown as ReactNodeStyleProps).component as unknown as ReactElement,\n        this.getDomElement(),\n      );\n    }\n  }\n\n  public destroy(): void {\n    unmount(this.getDomElement());\n    super.destroy();\n  }\n}\n"
  },
  {
    "path": "packages/g6-extension-react/src/react-node/render.ts",
    "content": "import type * as React from 'react';\nimport * as ReactDOM from 'react-dom';\n\ntype ContainerType = Element | DocumentFragment;\n\nconst { version } = ReactDOM;\n\n/**\n * <zh/> 获取 React 主版本号\n *\n * <en/> Get React major version\n * @returns <zh/> 主版本号 | <en/> Major version\n */\nfunction getReactMajorVersion(): number {\n  return Number((version || '').split('.')[0]);\n}\n\nconst getRenderer = (() => {\n  let rendererPromise: Promise<{\n    render: (node: React.ReactElement, container: ContainerType) => unknown;\n    unmount: (container: ContainerType) => unknown;\n  }>;\n\n  return () => {\n    if (!rendererPromise) {\n      const majorVersion = getReactMajorVersion();\n      if (majorVersion >= 18) {\n        rendererPromise = import('./render18');\n      } else {\n        rendererPromise = import('./render16');\n      }\n    }\n    return rendererPromise;\n  };\n})();\n\n/**\n * <zh/> 渲染 React 节点(兼容 React 16 ~ 19)\n *\n * <en/> Render React node(Compatible with React 16 ~ 19)\n * @param node - <zh/> React 节点 | <en/> React node\n * @param container - <zh/> 容器 | <en/> Container\n * @returns <zh/> Promise | <en/> Promise\n */\nexport async function render(node: React.ReactElement, container: ContainerType) {\n  const { render } = await getRenderer();\n  return render(node, container);\n}\n\n/**\n * <zh/> 卸载 React 节点(兼容 React 16 ~ 19)\n *\n * <en/> Unmount React node(Compatible with React 16 ~ 19)\n * @param container - <zh/> 容器 | <en/> Container\n * @returns <zh/> Promise | <en/> Promise\n */\nexport async function unmount(container: ContainerType) {\n  const { unmount } = await getRenderer();\n  return unmount(container);\n}\n"
  },
  {
    "path": "packages/g6-extension-react/src/react-node/render16.ts",
    "content": "import type * as React from 'react';\nimport * as ReactDOM from 'react-dom';\n\ntype ContainerType = Element | DocumentFragment;\n\nconst { render: reactRender, unmountComponentAtNode } = ReactDOM as any;\n\n/**\n * <zh/> 渲染 React 节点(React 16/17)\n *\n * <en/> Render React node(React 16/17)\n * @param node - <zh/> React 节点 | <en/> React node\n * @param container - <zh/> 容器 | <en/> Container\n */\nexport function render(node: React.ReactElement, container: ContainerType) {\n  reactRender(node, container);\n}\n\n/**\n * <zh/> 卸载 React 节点(React 16/17)\n *\n * <en/> Unmount React node(React 16/17)\n * @param container - <zh/> 容器 | <en/> Container\n */\nexport function unmount(container: ContainerType) {\n  unmountComponentAtNode(container);\n}\n"
  },
  {
    "path": "packages/g6-extension-react/src/react-node/render18.ts",
    "content": "import type * as React from 'react';\nimport * as ReactDOM from 'react-dom';\nimport type { Root } from 'react-dom/client';\n\ntype ContainerType = (Element | DocumentFragment) & {\n  __rc_react_root__?: Root;\n};\n\nconst MARK = '__rc_react_root__';\n\nlet ReactDOMClientPromise: Promise<typeof import('react-dom/client') | null> | null = null;\n\n/**\n * <zh/> 初始化 React 18+ 的 createRoot\n *\n * <en/> Initialize React 18+ createRoot\n * @returns ReactDOMClient\n */\nfunction initReactDOMClient() {\n  if (ReactDOMClientPromise === null) {\n    ReactDOMClientPromise = import('react-dom/client').catch(() => null);\n  }\n  return ReactDOMClientPromise;\n}\n\n/**\n * <zh/> 切换警告\n *\n * <en/> Toggle warning\n * @param skip <zh/> 是否跳过警告 | <en/> Whether to skip the warning\n */\nfunction toggleWarning(skip: boolean) {\n  try {\n    const { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } = ReactDOM as typeof ReactDOM & {\n      __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?: {\n        usingClientEntryPoint?: boolean;\n      };\n    };\n\n    if (\n      __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED &&\n      typeof __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED === 'object'\n    ) {\n      __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = skip;\n    }\n  } catch {\n    // Silent error\n  }\n}\n\n/**\n * <zh/> 渲染 React 节点(React 18/19)\n *\n * <en/> Render React node(React 18/19)\n * @param node - <zh/> React 节点 | <en/> React node\n * @param container - <zh/> 容器 | <en/> Container\n */\nexport async function render(node: React.ReactNode, container: ContainerType) {\n  const client = await initReactDOMClient();\n  if (!client?.createRoot) {\n    throw new Error('React 18+ createRoot not available');\n  }\n\n  toggleWarning(true);\n  const root = container[MARK] || client.createRoot(container);\n  toggleWarning(false);\n\n  root.render(node);\n  container[MARK] = root;\n}\n\n/**\n * <zh/> 卸载 React 节点(React 18/19)\n *\n * <en/> Unmount React node(React 18/19)\n * @param container - <zh/> 容器 | <en/> Container\n * @returns <zh/> Promise | <en/> Promise\n */\nexport async function unmount(container: ContainerType) {\n  // Delay to unmount to avoid React 18 sync warning\n  return Promise.resolve().then(() => {\n    container[MARK]?.unmount();\n    delete container[MARK];\n  });\n}\n"
  },
  {
    "path": "packages/g6-extension-react/tsconfig.build.json",
    "content": "{\n  \"compilerOptions\": {\n    \"paths\": {}\n  },\n  \"include\": [\"src/**/*\"],\n  \"extends\": \"./tsconfig.json\"\n}\n"
  },
  {
    "path": "packages/g6-extension-react/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"jsx\": \"react-jsx\",\n    \"outDir\": \"lib\",\n    \"paths\": {\n      \"@antv/g6\": [\"../g6/src/index.ts\"],\n      \"@antv/g6-extension-react\": [\"./src/index.ts\"]\n    }\n  },\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src/**/*\", \"__tests__/**/*\"]\n}\n"
  },
  {
    "path": "packages/g6-extension-react/tsconfig.test.json",
    "content": "{\n  \"compilerOptions\": {\n    \"paths\": {\n      \"@antv/g6\": [\"../g6/src/index.ts\"],\n      \"@antv/g6-extension-react\": [\"./src/index.ts\"]\n    },\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src/**/*\", \"__tests__/**/*\", \"../g6/src/layouts/hierarchy.d.ts\"],\n  \"extends\": \"./tsconfig.json\"\n}\n"
  },
  {
    "path": "packages/g6-extension-react/vite.config.js",
    "content": "import path from 'path';\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n  root: './__tests__',\n  server: {\n    port: 8082,\n    open: '/',\n  },\n  plugins: [{ name: 'isolation' }],\n  resolve: {\n    alias: {\n      '@antv/g6': path.resolve(__dirname, '../g6/src'),\n      '@antv/g6-extension-react': path.resolve(__dirname, './src'),\n    },\n  },\n});\n"
  },
  {
    "path": "packages/g6-ssr/.gitignore",
    "content": "**/*-actual.*"
  },
  {
    "path": "packages/g6-ssr/README.md",
    "content": "## SSR extension for G6 5.0\n\nThis extension package provides SSR support for G6 5.0, which supports canvas rendering in server side.\n\n## Usage\n\n### Install\n\n```bash\nnpm install @antv/g6-ssr\n```\n\n### Render in JavaScript API\n\n> For complete options, please refer to [G6 Graph Options](https://g6.antv.antgroup.com/api/graph/option)\n\n```js\nimport { createGraph } from '@antv/g6-ssr';\n\nconst graph = await createGraph({\n  width: 500,\n  height: 500,\n  imageType: 'png', // or 'jpeg'\n  data: {\n    // data\n  },\n  // other options\n});\n\ngraph.exportToFile('image');\n// -> image.png\n\ngraph.toBuffer();\n// -> get buffer\n```\n\n### Render in CLI\n\n```bash\nnpx g6-ssr export -i [graph-options].json -o ./image\n```\n\n### Export SVG / PDF\n\nWhen render in JavaScript API, you can pass `outputType` option to export SVG or PDF.\n\n```js\nconst graph = await createGraph({\n  width: 500,\n  height: 500,\n  data: {\n    // data\n  },\n  outputType: 'svg', // or 'pdf'\n  // other options\n});\n```\n\nWhen render in CLI, you can pass `-t` or `--type` option to export SVG or PDF.\n\n```bash\nnpx g6-ssr export -i [graph-options].json -o ./file -t pdf\n```\n\n### Register Custom G6 Extensions\n\nIf you need to register custom extensions of G6, please use the `registry` function exported from `@antv/g6-ssr`.\n\n```js\nimport { createGraph, registry } from '@antv/g6-ssr';\nimport { BaseNode, ExtensionCategory } from '@antv/g6';\n\nclass CustomNode extends BaseNode {\n  // custom node\n}\n\nregistry(ExtensionCategory.Node, 'custom-node', CustomNode);\n\nconst graph = await createGraph({\n  width: 500,\n  height: 500,\n  node: {\n    type: 'custom-node',\n    // other options\n  },\n  // other options\n});\n```\n\n### Use Plugins\n\nWhen using G6-SSR, you can also use G render plugins. Here's how to use plugins in server-side rendering:\n\n```js\nimport { createGraph } from '@antv/g6-ssr';\nimport { Plugin as RoughCanvasPlugin } from '@antv/g-plugin-rough-canvas-renderer';\n\nconst graph = await createGraph({\n  width: 500,\n  height: 500,\n  renderPlugins: [new RoughCanvasPlugin()],\n  data: {\n    // data\n  },\n});\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "packages/g6-ssr/__tests__/graph-options.json",
    "content": "{\n  \"width\": 500,\n  \"height\": 500,\n  \"autoFit\": \"view\",\n  \"background\": \"rgba(100, 80, 180, 0.4)\",\n  \"data\": {\n    \"nodes\": [\n      { \"id\": \"0\" },\n      { \"id\": \"1\" },\n      { \"id\": \"2\" },\n      { \"id\": \"3\" },\n      { \"id\": \"4\" },\n      { \"id\": \"5\" },\n      { \"id\": \"6\" },\n      { \"id\": \"7\" },\n      { \"id\": \"8\" },\n      { \"id\": \"9\" },\n      { \"id\": \"10\" },\n      { \"id\": \"11\" },\n      { \"id\": \"12\" },\n      { \"id\": \"13\" },\n      { \"id\": \"14\" },\n      { \"id\": \"15\" },\n      { \"id\": \"16\" },\n      { \"id\": \"17\" },\n      { \"id\": \"18\" },\n      { \"id\": \"19\" },\n      { \"id\": \"20\" },\n      { \"id\": \"21\" },\n      { \"id\": \"22\" },\n      { \"id\": \"23\" },\n      { \"id\": \"24\" },\n      { \"id\": \"25\" },\n      { \"id\": \"26\" },\n      { \"id\": \"27\" },\n      { \"id\": \"28\" },\n      { \"id\": \"29\" },\n      { \"id\": \"30\" },\n      { \"id\": \"31\" },\n      { \"id\": \"32\" },\n      { \"id\": \"33\" }\n    ],\n    \"edges\": [\n      { \"source\": \"0\", \"target\": \"1\" },\n      { \"source\": \"0\", \"target\": \"2\" },\n      { \"source\": \"0\", \"target\": \"3\" },\n      { \"source\": \"0\", \"target\": \"4\" },\n      { \"source\": \"0\", \"target\": \"5\" },\n      { \"source\": \"0\", \"target\": \"7\" },\n      { \"source\": \"0\", \"target\": \"8\" },\n      { \"source\": \"0\", \"target\": \"9\" },\n      { \"source\": \"0\", \"target\": \"10\" },\n      { \"source\": \"0\", \"target\": \"11\" },\n      { \"source\": \"0\", \"target\": \"13\" },\n      { \"source\": \"0\", \"target\": \"14\" },\n      { \"source\": \"0\", \"target\": \"15\" },\n      { \"source\": \"0\", \"target\": \"16\" },\n      { \"source\": \"2\", \"target\": \"3\" },\n      { \"source\": \"4\", \"target\": \"5\" },\n      { \"source\": \"4\", \"target\": \"6\" },\n      { \"source\": \"5\", \"target\": \"6\" },\n      { \"source\": \"7\", \"target\": \"13\" },\n      { \"source\": \"8\", \"target\": \"14\" },\n      { \"source\": \"9\", \"target\": \"10\" },\n      { \"source\": \"10\", \"target\": \"22\" },\n      { \"source\": \"10\", \"target\": \"14\" },\n      { \"source\": \"10\", \"target\": \"12\" },\n      { \"source\": \"10\", \"target\": \"24\" },\n      { \"source\": \"10\", \"target\": \"21\" },\n      { \"source\": \"10\", \"target\": \"20\" },\n      { \"source\": \"11\", \"target\": \"24\" },\n      { \"source\": \"11\", \"target\": \"22\" },\n      { \"source\": \"11\", \"target\": \"14\" },\n      { \"source\": \"12\", \"target\": \"13\" },\n      { \"source\": \"16\", \"target\": \"17\" },\n      { \"source\": \"16\", \"target\": \"18\" },\n      { \"source\": \"16\", \"target\": \"21\" },\n      { \"source\": \"16\", \"target\": \"22\" },\n      { \"source\": \"17\", \"target\": \"18\" },\n      { \"source\": \"17\", \"target\": \"20\" },\n      { \"source\": \"18\", \"target\": \"19\" },\n      { \"source\": \"19\", \"target\": \"20\" },\n      { \"source\": \"19\", \"target\": \"33\" },\n      { \"source\": \"19\", \"target\": \"22\" },\n      { \"source\": \"19\", \"target\": \"23\" },\n      { \"source\": \"20\", \"target\": \"21\" },\n      { \"source\": \"21\", \"target\": \"22\" },\n      { \"source\": \"22\", \"target\": \"24\" },\n      { \"source\": \"22\", \"target\": \"25\" },\n      { \"source\": \"22\", \"target\": \"26\" },\n      { \"source\": \"22\", \"target\": \"23\" },\n      { \"source\": \"22\", \"target\": \"28\" },\n      { \"source\": \"22\", \"target\": \"30\" },\n      { \"source\": \"22\", \"target\": \"31\" },\n      { \"source\": \"22\", \"target\": \"32\" },\n      { \"source\": \"22\", \"target\": \"33\" },\n      { \"source\": \"23\", \"target\": \"28\" },\n      { \"source\": \"23\", \"target\": \"27\" },\n      { \"source\": \"23\", \"target\": \"29\" },\n      { \"source\": \"23\", \"target\": \"30\" },\n      { \"source\": \"23\", \"target\": \"31\" },\n      { \"source\": \"23\", \"target\": \"33\" },\n      { \"source\": \"32\", \"target\": \"33\" }\n    ]\n  },\n  \"node\": {\n    \"style\": {\n      \"labelFill\": \"#fff\",\n      \"labelPlacement\": \"center\"\n    }\n  },\n  \"layout\": { \"type\": \"circular\" }\n}\n"
  },
  {
    "path": "packages/g6-ssr/__tests__/graph.spec.ts",
    "content": "import { Plugin as RoughCanvasPlugin } from '@antv/g-plugin-rough-canvas-renderer';\nimport { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport type { Graph, MetaData } from '../src';\nimport { createGraph } from '../src';\n\ndeclare global {\n  // eslint-disable-next-line @typescript-eslint/no-namespace\n  namespace jest {\n    interface Matchers<R> {\n      toMatchFile(path: string, meta?: MetaData): R;\n    }\n  }\n}\n\nexpect.extend({\n  toMatchFile: (received: Graph, path: string, meta?: MetaData) => {\n    const _path = join(__dirname, path);\n\n    const file = received.toBuffer(meta);\n    const pass = existsSync(_path) ? file.equals(readFileSync(_path)) : true;\n\n    const actualName = _path.replace('.', '-actual.');\n    if (!pass) {\n      writeFileSync(actualName, file);\n    } else if (existsSync(actualName)) {\n      unlinkSync(actualName);\n    }\n\n    if (pass) {\n      return {\n        message: () => 'passed',\n        pass: true,\n      };\n    } else {\n      return {\n        message: () => 'expected files are equal',\n        pass: false,\n      };\n    }\n  },\n});\n\ndescribe('createGraph', () => {\n  const fn = async (outputType?: any, imageType: any = 'png', options = {}) => {\n    const data = {\n      nodes: [\n        { id: '0' },\n        { id: '1' },\n        { id: '2' },\n        { id: '3' },\n        { id: '4' },\n        { id: '5' },\n        { id: '6' },\n        { id: '7' },\n        { id: '8' },\n        { id: '9' },\n        { id: '10' },\n        { id: '11' },\n        { id: '12' },\n        { id: '13' },\n        { id: '14' },\n        { id: '15' },\n        { id: '16' },\n        { id: '17' },\n        { id: '18' },\n        { id: '19' },\n        { id: '20' },\n        { id: '21' },\n        { id: '22' },\n        { id: '23' },\n        { id: '24' },\n        { id: '25' },\n        { id: '26' },\n        { id: '27' },\n        { id: '28' },\n        { id: '29' },\n        { id: '30' },\n        { id: '31' },\n        { id: '32' },\n        { id: '33' },\n      ],\n      edges: [\n        { source: '0', target: '1' },\n        { source: '0', target: '2' },\n        { source: '0', target: '3' },\n        { source: '0', target: '4' },\n        { source: '0', target: '5' },\n        { source: '0', target: '7' },\n        { source: '0', target: '8' },\n        { source: '0', target: '9' },\n        { source: '0', target: '10' },\n        { source: '0', target: '11' },\n        { source: '0', target: '13' },\n        { source: '0', target: '14' },\n        { source: '0', target: '15' },\n        { source: '0', target: '16' },\n        { source: '2', target: '3' },\n        { source: '4', target: '5' },\n        { source: '4', target: '6' },\n        { source: '5', target: '6' },\n        { source: '7', target: '13' },\n        { source: '8', target: '14' },\n        { source: '9', target: '10' },\n        { source: '10', target: '22' },\n        { source: '10', target: '14' },\n        { source: '10', target: '12' },\n        { source: '10', target: '24' },\n        { source: '10', target: '21' },\n        { source: '10', target: '20' },\n        { source: '11', target: '24' },\n        { source: '11', target: '22' },\n        { source: '11', target: '14' },\n        { source: '12', target: '13' },\n        { source: '16', target: '17' },\n        { source: '16', target: '18' },\n        { source: '16', target: '21' },\n        { source: '16', target: '22' },\n        { source: '17', target: '18' },\n        { source: '17', target: '20' },\n        { source: '18', target: '19' },\n        { source: '19', target: '20' },\n        { source: '19', target: '33' },\n        { source: '19', target: '22' },\n        { source: '19', target: '23' },\n        { source: '20', target: '21' },\n        { source: '21', target: '22' },\n        { source: '22', target: '24' },\n        { source: '22', target: '25' },\n        { source: '22', target: '26' },\n        { source: '22', target: '23' },\n        { source: '22', target: '28' },\n        { source: '22', target: '30' },\n        { source: '22', target: '31' },\n        { source: '22', target: '32' },\n        { source: '22', target: '33' },\n        { source: '23', target: '28' },\n        { source: '23', target: '27' },\n        { source: '23', target: '29' },\n        { source: '23', target: '30' },\n        { source: '23', target: '31' },\n        { source: '23', target: '33' },\n        { source: '32', target: '33' },\n      ],\n    };\n\n    return await createGraph({\n      width: 500,\n      height: 500,\n      outputType,\n      imageType,\n      autoFit: 'view',\n      background: 'rgba(100, 80, 180, 0.4)',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelFill: '#fff',\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'circular',\n      },\n      ...options,\n    });\n  };\n\n  it('image png', async () => {\n    const graph = await fn();\n\n    expect(graph).toMatchFile('./assets/image.png');\n\n    graph.exportToFile(join(__dirname, './assets/image'));\n\n    graph.destroy();\n  });\n\n  it('image png with render plugin', async () => {\n    const graph = await fn('image', 'png', {\n      renderPlugins: [new RoughCanvasPlugin()],\n      background: 'white',\n      node: {\n        style: {\n          labelText: (d: any) => d.id,\n          labelPlacement: 'center',\n          size: 40,\n          lineWidth: 2,\n          fill: '#b3de69',\n          stroke: '#b3de69',\n        },\n      },\n    });\n\n    expect(graph).toMatchFile('./assets/image-rough.png');\n\n    graph.exportToFile(join(__dirname, './assets/image-rough'));\n\n    graph.destroy();\n  });\n\n  it('image jpeg', async () => {\n    const graph = await fn('image', 'jpeg');\n\n    expect(graph).toMatchFile('./assets/image.jpeg');\n\n    graph.exportToFile(join(__dirname, './assets/image'));\n\n    graph.destroy();\n  });\n\n  it('file pdf', async () => {\n    const graph = await fn('pdf');\n\n    const metadata = {\n      title: 'Chart',\n      author: 'AntV',\n      creator: 'Aaron',\n      subject: 'Test',\n      keywords: 'antv g2 chart pdf',\n      creationDate: new Date(1730304000000), // 2024-10-31 00:00:00 UTC+8\n      modDate: new Date(1730304000000), // 2024-10-31 00:00:00 UTC+8\n    };\n\n    expect(graph).toMatchFile('./assets/file.pdf', metadata);\n\n    graph.exportToFile(join(__dirname, '/assets/file'), metadata);\n\n    graph.destroy();\n  });\n\n  it('file svg', async () => {\n    const graph = await fn('svg');\n\n    expect(graph).toMatchFile('./assets/file.svg');\n\n    graph.exportToFile(join(__dirname, './assets/file'));\n\n    graph.destroy();\n  });\n\n  it('devicePixelRatio', async () => {\n    const graph = await fn('image', 'jpeg', { devicePixelRatio: 1 });\n\n    expect(graph).toMatchFile('./assets/image_x1.jpeg');\n\n    graph.exportToFile(join(__dirname, './assets/image_x1'));\n\n    graph.destroy();\n  });\n\n  it('node image', async () => {\n    const graph = await fn('image', 'jpeg', {\n      waitForRender: 1000,\n      node: {\n        style: {\n          iconSrc: 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',\n        },\n      },\n    });\n\n    expect(graph).toMatchFile('./assets/node-image.jpeg');\n\n    graph.exportToFile(join(__dirname, './assets/node-image'));\n\n    graph.destroy();\n  });\n});\n"
  },
  {
    "path": "packages/g6-ssr/bin/g6-ssr.js",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\n/* eslint-disable no-console */\nconst cac = require('cac');\nconst fs = require('fs');\nconst { createGraph } = require('../dist/g6-ssr.cjs');\nconst { version } = require('../package.json');\n\nconst cli = cac();\n\ncli.version(version);\n\ncli.command('version', 'Show version').action(() => {\n  console.log(version);\n});\n\ncli\n  .command('export', 'Export G6 Options to Image, PDF or SVG')\n  .option('-i, --input <inputPath>', 'Path to the G6 options file')\n  .option('-o, --output <outputPath>', 'Path to the export file')\n  .option('-t, --type [type]', 'File type, default is image')\n  .action(async (options) => {\n    const { input, output, type } = options;\n\n    if (!input) {\n      console.log('\\x1b[31m%s\\x1b[0m', 'Please provide a path to the G6 options file');\n      process.exit(1);\n    }\n\n    if (!fs.existsSync(input)) {\n      console.log('\\x1b[31m%s\\x1b[0m', 'File does not exist: ', input);\n      process.exit(1);\n    }\n\n    let graphOptions;\n\n    try {\n      graphOptions = JSON.parse(fs.readFileSync(input, 'utf-8'));\n    } catch (e) {\n      console.log('\\x1b[31m%s\\x1b[0m', 'Invalid JSON file');\n      process.exit(1);\n    }\n\n    if (!graphOptions.outputType) {\n      if (type === 'svg' || type === 'pdf') {\n        graphOptions.outputType = type;\n      }\n    }\n\n    console.log(`Exporting to ${type || 'image'}...`);\n\n    const graph = await createGraph(graphOptions);\n\n    graph.exportToFile(output, type);\n\n    console.log('\\x1b[32m%s\\x1b[0m', 'Exported successfully!');\n\n    process.exit(0);\n  });\n\ncli.help();\n\ncli.parse();\n"
  },
  {
    "path": "packages/g6-ssr/jest.config.js",
    "content": "module.exports = {\n  forceExit: true,\n  transform: {\n    '^.+\\\\.[tj]s$': ['@swc/jest'],\n  },\n  collectCoverageFrom: ['src/**/*.ts'],\n  moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],\n  transformIgnorePatterns: [`<rootDir>/node_modules/.pnpm/(?!(d3-*))`],\n  moduleNameMapper: {\n    '@antv/g6': '<rootDir>/../g6/src',\n  },\n};\n"
  },
  {
    "path": "packages/g6-ssr/package.json",
    "content": "{\n  \"name\": \"@antv/g6-ssr\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Support SSR for G6\",\n  \"keywords\": [\n    \"antv\",\n    \"g6\",\n    \"ssr\"\n  ],\n  \"license\": \"MIT\",\n  \"author\": \"Aarebecca\",\n  \"main\": \"./dist/g6-ssr.cjs\",\n  \"types\": \"./dist/lib/index.d.ts\",\n  \"bin\": \"./bin/g6-ssr.js\",\n  \"files\": [\n    \"dist\",\n    \"bin\"\n  ],\n  \"scripts\": {\n    \"build\": \"rimraf ./dist && rollup -c\",\n    \"ci\": \"run-s lint type-check build\",\n    \"lint\": \"eslint ./src __tests__ --quiet && prettier ./src __tests__ --check\",\n    \"prepublishOnly\": \"npm run ci\",\n    \"test\": \"run-s test:*\",\n    \"test:bin\": \"node ./bin/g6-ssr.js export -i ./__tests__/graph-options.json -o __tests__/assets/bin\",\n    \"test:unit\": \"jest\",\n    \"type-check\": \"tsc --noEmit -p tsconfig.test.json\"\n  },\n  \"dependencies\": {\n    \"@antv/g\": \"^6.1.24\",\n    \"@antv/g-canvas\": \"^2.0.43\",\n    \"@antv/g6\": \"workspace:^\",\n    \"cac\": \"^6.7.14\",\n    \"canvas\": \"^3\"\n  },\n  \"devDependencies\": {\n    \"@antv/g-plugin-rough-canvas-renderer\": \"^2.0.44\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/g6-ssr/rollup.config.mjs",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport json from '@rollup/plugin-json';\nimport resolve from '@rollup/plugin-node-resolve';\nimport terser from '@rollup/plugin-terser';\nimport typescript from '@rollup/plugin-typescript';\nimport nodePolyfills from 'rollup-plugin-polyfill-node';\n\nexport default [\n  {\n    input: 'src/index.ts',\n    output: {\n      file: 'dist/g6-ssr.cjs',\n      format: 'cjs',\n      exports: 'named',\n      sourcemap: true,\n    },\n    plugins: [\n      nodePolyfills(),\n      resolve(),\n      commonjs(),\n      json(),\n      typescript({\n        tsconfig: 'tsconfig.build.json',\n      }),\n      terser(),\n    ],\n    external: ['fs', 'path', 'canvas'],\n  },\n];\n"
  },
  {
    "path": "packages/g6-ssr/src/canvas.ts",
    "content": "import { Renderer } from '@antv/g-canvas';\nimport { Canvas as G6Canvas } from '@antv/g6';\nimport type { Canvas as NodeCanvas } from 'canvas';\nimport { createCanvas as createNodeCanvas, Image as NodeImage } from 'canvas';\nimport type { Options } from './types';\n\n/**\n * <zh/> 创建画布\n *\n * <en/> create canvas\n * @param options <zh/> options 画布配置 | <en/> options canvas configuration\n * @returns <zh/> [G6 画布, NodeCanvas 画布] | <en/> [G6Canvas, NodeCanvas]\n */\nexport function createCanvas(options: Options): [G6Canvas, NodeCanvas] {\n  const { width, height, background = 'white', outputType, devicePixelRatio = 2 } = options;\n  const nodeCanvas = createNodeCanvas(width, height, outputType as any);\n  const offscreenNodeCanvas = createNodeCanvas(1, 1);\n\n  const renderPlugins = options.renderPlugins || [];\n\n  const g6Canvas = new G6Canvas({\n    width,\n    height,\n    background,\n    // @ts-expect-error missing types\n    canvas: nodeCanvas as any,\n    offscreenCanvas: offscreenNodeCanvas as any,\n    devicePixelRatio,\n    enableMultiLayer: false,\n    createImage: () => new NodeImage(),\n    renderer: () => {\n      const renderer = new Renderer();\n\n      // register render plugins\n      renderPlugins.forEach((plugin) => {\n        renderer.registerPlugin(plugin);\n      });\n\n      const htmlRendererPlugin = renderer.getPlugin('html-renderer');\n      const domInteractionPlugin = renderer.getPlugin('dom-interaction');\n      renderer.unregisterPlugin(htmlRendererPlugin);\n      renderer.unregisterPlugin(domInteractionPlugin);\n      return renderer;\n    },\n  });\n\n  return [g6Canvas, nodeCanvas];\n}\n"
  },
  {
    "path": "packages/g6-ssr/src/graph.ts",
    "content": "import { Graph as G6Graph } from '@antv/g6';\nimport { existsSync, lstatSync, writeFileSync } from 'fs';\nimport { createCanvas } from './canvas';\nimport type { Graph, MetaData, Options } from './types';\n\n/**\n * <zh/> 获取输出文件的扩展名\n *\n * <en/> Get the extension name of the output file\n * @param options - <zh/>配置项 | <en/>options\n * @returns <zh/>输出文件的扩展名 | <en/>The extension name of the output file\n */\nfunction getInfoOf(options: Options) {\n  const { outputType, imageType } = options;\n  if (outputType === 'pdf') return ['.pdf', 'application/pdf'] as const;\n  if (outputType === 'svg') return ['.svg', undefined] as const;\n\n  if (imageType === 'jpeg') return ['.jpeg', 'image/jpeg'] as const;\n  return ['.png', 'image/png'] as const;\n}\n\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * <zh/> 创建图并等待渲染完成\n *\n * <en/> Create a graph and wait for the rendering to complete\n * @param options - <zh/>图配置项 | <en/>Graph options\n * @returns <zh/>扩展图实例 | <en/>Extended graph instance\n */\nexport async function createGraph(options: Options) {\n  const [g6Canvas, nodeCanvas] = createCanvas(options);\n\n  const { outputType, waitForRender = 32, ...restOptions } = options;\n  const graph = new G6Graph({\n    animation: false,\n    ...restOptions,\n    container: g6Canvas,\n  });\n\n  const [extendName, mimeType] = getInfoOf(options);\n\n  await graph.render();\n\n  await sleep(waitForRender); // wait for the rendering to complete\n\n  const returns: Graph = {\n    getGraph: () => graph,\n    getCanvas: () => nodeCanvas,\n    destroy: () => graph.destroy(),\n    exportToFile: (file: string, meta?: MetaData) => {\n      if (!file.endsWith(extendName)) {\n        if (!existsSync(file)) file += extendName;\n        else if (lstatSync(file).isDirectory()) file = `${file}/image${extendName}`;\n        else file += extendName;\n      }\n\n      // @ts-expect-error skip type check\n      writeFileSync(file, nodeCanvas.toBuffer(mimeType, meta));\n    },\n    // @ts-expect-error extend Graph\n    toBuffer: (meta?: MetaData) => nodeCanvas.toBuffer(mimeType, meta),\n    toDataURL: () => nodeCanvas.toDataURL(mimeType as any),\n  };\n\n  return returns;\n}\n"
  },
  {
    "path": "packages/g6-ssr/src/index.ts",
    "content": "import * as G6 from '@antv/g6';\n\nexport { getExtension, getExtensions, register } from '@antv/g6';\nexport { createCanvas } from './canvas';\nexport { createGraph } from './graph';\nexport type { Graph, MetaData, Options } from './types';\nexport { G6 };\n"
  },
  {
    "path": "packages/g6-ssr/src/types.ts",
    "content": "import type { RendererPlugin } from '@antv/g';\nimport type { GraphOptions } from '@antv/g6';\nimport { Graph as G6Graph } from '@antv/g6';\nimport type { Canvas, JpegConfig, PdfConfig, PngConfig } from 'canvas';\n\nexport interface Options extends Omit<GraphOptions, 'renderer' | 'container'> {\n  width: number;\n  height: number;\n  /**\n   * <zh/> 等待渲染的时间，默认为 16ms\n   *\n   * <en/> The time to wait for rendering, default is 16ms\n   * @defaultValue 16\n   */\n  waitForRender?: number;\n  /**\n   * <zh/> 输出文件类型，默认导出为图片\n   *\n   * <en/> output file type, default export as image\n   * @defaultValue 'image'\n   */\n  outputType?: 'image' | 'pdf' | 'svg';\n  /**\n   * <zh/> 图片类型，默认为 png\n   *\n   * <en/> Image type, default is png\n   */\n  imageType?: 'png' | 'jpeg';\n  /**\n   * <zh/> 渲染插件\n   *\n   * <en/> render plugins\n   */\n  renderPlugins?: RendererPlugin[];\n}\n\nexport type MetaData = PdfConfig | PngConfig | JpegConfig;\n\nexport interface Graph {\n  getGraph: () => G6Graph;\n  getCanvas: () => Canvas;\n  destroy: () => void;\n\n  exportToFile: (file: string, meta?: MetaData) => void;\n  toBuffer: (meta?: MetaData) => Buffer;\n  toDataURL: () => string;\n}\n"
  },
  {
    "path": "packages/g6-ssr/tsconfig.build.json",
    "content": "{\n  \"compilerOptions\": {\n    \"paths\": {}\n  },\n  \"include\": [\"src/**/*\"],\n  \"extends\": \"./tsconfig.json\"\n}\n"
  },
  {
    "path": "packages/g6-ssr/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"outDir\": \"lib\",\n    \"target\": \"ESNext\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ESNext\"],\n    \"paths\": {\n      \"@antv/g6\": [\"../g6/src/index.ts\"]\n    }\n  },\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src/**/*\", \"__tests__/**/*\"]\n}\n"
  },
  {
    "path": "packages/g6-ssr/tsconfig.test.json",
    "content": "{\n  \"compilerOptions\": {\n    \"paths\": {}\n  },\n  \"include\": [\"src/**/*\", \"__tests__/**/*\"],\n  \"extends\": \"./tsconfig.json\"\n}\n"
  },
  {
    "path": "packages/site/.dumi/app.tsx",
    "content": "if (typeof window !== 'undefined') {\n  window.onresize = () => {\n    const { graph, container, widthOffset = 0, heightOffset = 0 } = window as any;\n\n    if (!graph || graph.destroyed) return;\n    if (!container || !container.scrollWidth || !container.scrollHeight) return;\n    graph.setSize(container.scrollWidth + widthOffset, container.scrollHeight + heightOffset);\n  };\n}\n"
  },
  {
    "path": "packages/site/.dumi/global.less",
    "content": ".ant-layout-sider-children > :first-child[class^='toc'] {\n  height: 100%;\n  overflow-y: auto;\n}\n\nsummary {\n  color: #873bf4;\n  cursor: pointer;\n}\n\n.ant-layout {\n  background: none !important;\n}\n\n.prism-code {\n  background-color: #f6f8fa !important;\n}\n"
  },
  {
    "path": "packages/site/.dumi/global.ts",
    "content": "// @ts-nocheck\nif (typeof window !== 'undefined' && window) {\n  window.d3Hierarchy = require('d3-hierarchy');\n  window.gCanvas = require('@antv/g-canvas');\n  window.gPluginRoughCanvasRenderer = require('@antv/g-plugin-rough-canvas-renderer');\n\n  window.g6 = require('@antv/g6');\n  window.g6Extension3d = require('@antv/g6-extension-3d');\n  window.g6ExtensionReact = require('@antv/g6-extension-react');\n\n  window.layout = require('@antv/layout');\n  window.layoutGpu = require('@antv/layout-gpu');\n  window.algorithm = require('@antv/algorithm');\n  window.layoutWasm = require('@antv/layout-wasm');\n\n  window.insertCss = require('insert-css');\n  window.util = require('@antv/util');\n  window.stats = require('stats.js');\n  window.g = require('@antv/g');\n  window.gSvg = require('@antv/g-svg');\n  window.gWebgl = require('@antv/g-webgl');\n  window.g2 = require('@antv/g2');\n  window.antd = require('antd');\n  window.icons = require('@ant-design/icons');\n\n  window.react = require('react');\n  window.React = window.react;\n  window.client = require('react-dom/client');\n  window.styledComponents = require('styled-components');\n\n  window.addPanel = async (renderPanel: (gui) => void) => {\n    const container = document.getElementById('container')?.parentNode;\n    const gui = new window.lil.GUI({ container, autoPlace: true });\n    gui.title('Control');\n    renderPanel(gui);\n    Object.assign(gui.domElement.style, {\n      position: 'absolute',\n      top: 0,\n      right: 0,\n    });\n  };\n\n  window.createContainer = (style = {}) => {\n    const container = document.createElement('div');\n\n    Object.entries(style).forEach(([key, value]) => {\n      if (key === 'width' || key === 'height') {\n        if (typeof value === 'number') {\n          value = `${value}px`;\n        }\n      }\n      container.style[key] = value;\n    });\n    return container;\n  };\n\n  // 用于文档中快速创建 ob demo 示例\n  window.createGraph = async (options, style = {}, renderPanel?: (gui) => void) => {\n    const container = createContainer(style);\n\n    const graph = new window.g6.Graph({\n      width: style.width,\n      height: style.height,\n      container,\n      ...options,\n    });\n\n    await graph.render();\n\n    if (renderPanel) {\n      const $wrapper = createContainer({ width: style.width + 245, height: style.height, display: 'flex' });\n      $wrapper.appendChild(container);\n\n      const gui = new window.lil.GUI({ container: $wrapper, autoPlace: false });\n      gui.title('Playground');\n      renderPanel(gui, graph);\n      $wrapper.appendChild(gui.domElement);\n\n      return $wrapper;\n    }\n\n    return container;\n  };\n\n  if (location.host === 'g6.antv.vision') {\n    (window as any).location.href = location.href.replace(location.origin, 'https://g6.antv.antgroup.com');\n  }\n}\n"
  },
  {
    "path": "packages/site/.dumi/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"include\": [\"**/*\"]\n}\n"
  },
  {
    "path": "packages/site/.dumirc.ts",
    "content": "import { defineConfig } from 'dumi';\nimport { version } from '../g6/package.json';\nimport { homepage, repository } from './package.json';\n\nexport default defineConfig({\n  ...(process.env.NODE_ENV === 'production' ? { ssr: { builder: 'webpack', mako: false } } : { ssr: false, mako: {} }),\n  locales: [\n    { id: 'zh', name: '中文' },\n    { id: 'en', name: 'English' },\n  ],\n  favicons: ['https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*7svFR6wkPMoAAAAAAAAAAAAADmJ7AQ/original'], // 网站 favicon\n  metas: [\n    // 自定义 meta 标签\n    {\n      name: 'google-site-verification',\n      content: 'D2DFQzn8bn6vTvIqonu0FSFoF-y5ZihUR9WYteGI684',\n    },\n  ],\n  themeConfig: {\n    title: 'G6', // 网站header标题\n    metas: {\n      title: { zh: 'G6 图可视化引擎', en: 'G6 Graph Visualization Framework in JavaScript' },\n      description: {\n        zh: 'G6 是一个简单、易用、完备的图可视化引擎，它在高定制能力的基础上，提供了一系列设计优雅、便于使用的图可视化解决方案。能帮助开发者搭建属于自己的图可视化、图分析、或图编辑器应用。',\n        en: 'G6 is a graph visualization framework with simplicity and convenience. Based on the ability of customization, it provides elegant graph visualization solutions, helping developers build applications for graph visualization, analysis, and editing.',\n      },\n    },\n    defaultLanguage: 'zh', // 默认语言\n    isAntVSite: false, // 是否是 AntV 的大官网\n    siteUrl: homepage, // 官网地址\n    sitePackagePath: 'packages/site', // 站点包地址\n    githubUrl: repository.url, // GitHub 地址\n    footerTheme: 'light', // 白色 底部主题\n    showSearch: true, // 是否显示搜索框\n    showGithubCorner: true, // 是否显示头部的 GitHub icon\n    showGithubStars: true, // 是否显示 GitHub star 数量\n    showAntVProductsCard: true, // 是否显示 AntV 产品汇总的卡片\n    showLanguageSwitcher: true, // 是否显示官网语言切换\n    showWxQrcode: true, // 是否显示头部菜单的微信公众号\n    showChartResize: true, // 是否在 demo 页展示图表视图切换\n    showAPIDoc: false, // 是否在 demo 页展示API文档\n    feedback: true, // 是否显示反馈组件\n    links: true, // 是否显示links答疑小蜜\n    prefersColor: {\n      default: 'light',\n      switch: false,\n    },\n    versions: {\n      // 历史版本以及切换下拉菜单\n      [version]: 'https://g6.antv.antgroup.com',\n      '4.x': 'https://g6-v4.antv.vision',\n      '3.2.x': 'https://g6-v3-2.antv.vision',\n    },\n    docsearchOptions: {\n      // 头部搜索框配置\n      apiKey: '9d1cd586972bb492b7b41b13a949ef30',\n      indexName: 'antv_g6',\n      sort: ['!/api/reference'],\n    },\n    navs: [\n      {\n        slug: 'docs/manual',\n        title: {\n          zh: '文档',\n          en: 'Docs',\n        },\n      },\n      {\n        slug: 'docs/api',\n        title: {\n          zh: 'API',\n          en: 'API',\n        },\n      },\n      {\n        slug: 'examples',\n        title: {\n          zh: '图表示例',\n          en: 'Playground',\n        },\n      },\n      {\n        title: {\n          zh: '社区',\n          en: 'Community',\n        },\n        dropdownItems: [\n          {\n            url: 'https://www.yuque.com/antv/g6-blog',\n            name: {\n              zh: '文章博客',\n              en: 'Blog',\n            },\n          },\n          {\n            url: 'https://g6.antv.antgroup.com',\n            name: {\n              zh: '国内镜像',\n              en: 'Site in China',\n            },\n          },\n        ],\n      },\n    ],\n    docs: [\n      // Docs folder\n      {\n        slug: 'manual/getting-started',\n        title: {\n          zh: '开始使用',\n          en: 'Getting Started',\n        },\n        order: 2,\n      },\n      {\n        slug: 'manual/getting-started/integration',\n        title: {\n          zh: '前端框架集成',\n          en: 'Integration',\n        },\n        order: 2,\n      },\n      {\n        slug: 'manual/graph',\n        title: {\n          zh: '图 Graph',\n          en: 'Graph',\n        },\n        order: 3,\n      },\n      {\n        slug: 'manual/element',\n        title: {\n          zh: '元素 Element',\n          en: 'Element',\n        },\n        order: 5,\n      },\n      {\n        slug: 'manual/element/node',\n        title: {\n          zh: '节点 Node',\n          en: 'Node',\n        },\n        order: 3,\n      },\n      {\n        slug: 'manual/element/edge',\n        title: {\n          zh: '边 Edge',\n          en: 'Edge',\n        },\n        order: 4,\n      },\n      {\n        slug: 'manual/element/combo',\n        title: {\n          zh: '组合 Combo',\n          en: 'Combo',\n        },\n        order: 5,\n      },\n      {\n        slug: 'manual/element/shape',\n        title: {\n          zh: '图形 Shape',\n          en: 'Shape',\n        },\n        order: 6,\n      },\n      {\n        slug: 'manual/layout',\n        title: {\n          zh: '布局 Layout',\n          en: 'Layout',\n        },\n        order: 5,\n      },\n      {\n        slug: 'manual/behavior',\n        title: {\n          zh: '交互 Behavior',\n          en: 'Behavior',\n        },\n        order: 6,\n      },\n      {\n        slug: 'manual/plugin',\n        title: {\n          zh: '插件 Plugin',\n          en: 'Plugin',\n        },\n        order: 7,\n      },\n      {\n        slug: 'manual/transform',\n        title: {\n          zh: '数据处理 Transform',\n          en: 'Transform',\n        },\n        order: 8,\n      },\n      {\n        slug: 'manual/theme',\n        title: {\n          zh: '主题 Theme',\n          en: 'Theme',\n        },\n        order: 9,\n      },\n      {\n        slug: 'manual/animation',\n        title: {\n          zh: '动画 Animation',\n          en: 'Animation',\n        },\n        order: 10,\n      },\n      {\n        slug: 'manual/further-reading',\n        title: {\n          zh: '扩展阅读',\n          en: 'Further Reading',\n        },\n        order: 11,\n      },\n      {\n        slug: 'manual/whats-new',\n        title: {\n          zh: '版本特性',\n          en: \"What's new\",\n        },\n        order: 12,\n      },\n    ],\n    examples: [\n      {\n        slug: 'feature',\n        icon: 'gallery',\n        title: {\n          zh: '特性',\n          en: 'Feature',\n        },\n      },\n      {\n        slug: 'scene-case',\n        icon: 'gallery',\n        title: {\n          zh: '场景案例',\n          en: 'Scene Case',\n        },\n      },\n      {\n        slug: 'layout',\n        icon: 'net',\n        title: {\n          zh: '图布局',\n          en: 'Graph Layout',\n        },\n      },\n      {\n        slug: 'element',\n        icon: 'shape',\n        title: {\n          zh: '元素',\n          en: 'Element',\n        },\n      },\n      {\n        slug: 'behavior',\n        icon: 'interaction',\n        title: {\n          zh: '交互',\n          en: 'Behavior',\n        },\n      },\n      {\n        slug: 'animation',\n        icon: 'scatter',\n        title: {\n          zh: '动画',\n          en: 'Animation',\n        },\n      },\n      {\n        slug: 'plugin',\n        icon: 'tool',\n        title: {\n          zh: '插件',\n          en: 'Plugin',\n        },\n      },\n      {\n        slug: 'transform',\n        icon: 'tag-flow',\n        title: {\n          zh: '数据处理',\n          en: 'Transform',\n        },\n      },\n      {\n        slug: 'algorithm',\n        icon: 'gallery',\n        title: {\n          zh: '算法',\n          en: 'Algorithm',\n        },\n      },\n      {\n        slug: 'performance',\n        icon: 'net',\n        title: {\n          zh: '性能',\n          en: 'Performance',\n        },\n      },\n    ],\n    mdPlayground: {\n      // 第一个分块的大小\n      splitPaneMainSize: '62%',\n    },\n    playground: {},\n    /** 公告 */\n    announcement: {\n      title: {\n        zh: 'AntV 首个声明式信息图渲染框架正式开源，让数据叙事更简单、更优雅、更高效，现诚邀体验共建！',\n        en: \"AntV's declarative infographic framework is now open source. Simpler, more elegant data storytelling!\",\n      },\n      link: {\n        url: 'https://infographic.antv.vision/',\n        text: {\n          zh: '点击了解详情',\n          en: 'Learn More',\n        },\n      },\n    },\n    /** 首页技术栈介绍 */\n    detail: {\n      engine: {\n        zh: 'G6',\n        en: 'G6',\n      },\n      title: {\n        zh: 'G6·图可视化引擎',\n        en: 'G6·Graph Visualization Engine',\n      },\n      description: {\n        zh: 'G6 是一个简单、易用、完备的图可视化引擎，它在高定制能力的基础上，提供了一系列设计优雅、便于使用的图可视化解决方案。能帮助开发者搭建属于自己的图可视化、图分析、或图编辑器应用。',\n        en: 'G6 is graph visualization engine with simplicity and convenience. Based on the ability of customize, it provides a set of elegant graph visualization solutions, and helps developers to build up applications for graph visualization, graph analysis, and graph editor.',\n      },\n      image: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*6dSUSo3QTk0AAAAAAAAAAAAADmJ7AQ/original',\n      imageStyle: {\n        transform: 'scale(0.7)',\n        marginLeft: '100px',\n        marginTop: '70px',\n      },\n      buttons: [\n        {\n          text: {\n            zh: '开始使用',\n            en: 'Getting Started',\n          },\n          link: {\n            zh: '/manual/introduction',\n            en: '/en/manual/introduction',\n          },\n        },\n        {\n          text: {\n            zh: '图表示例',\n            en: 'Playground',\n          },\n          link: {\n            zh: '/examples',\n            en: '/en/examples',\n          },\n          type: 'primary',\n        },\n      ],\n    },\n    /** 新闻公告，优先选择配置的，如果没有配置则使用远程的！ */\n    // news: [] // 统一使用 site-data 配置\n    /** 首页特性介绍 */\n    features: [\n      {\n        icon: 'https://gw.alipayobjects.com/zos/basement_prod/0e03c123-031b-48ed-9050-4ee18c903e94.svg',\n        title: {\n          zh: '专注关系，完备基建',\n          en: 'Dedicated & Complete',\n        },\n        description: {\n          zh: 'G6 是一个专注于关系数据的、完备的图可视化引擎',\n          en: 'G6 is a complete graph visualization engine, which focuses on relational data',\n        },\n      },\n      {\n        icon: 'https://gw.alipayobjects.com/zos/basement_prod/42d17359-8607-4227-af93-7509eabb3163.svg',\n        title: {\n          zh: '领域深钻，顶尖方案',\n          en: 'Top Solution',\n        },\n        description: {\n          zh: '扎根实际具体业务场景、结合业界领先成果，沉淀顶尖解决方案',\n          en: 'According to practical business scenarios, we found out the top solutions',\n        },\n      },\n      {\n        icon: 'https://gw.alipayobjects.com/zos/basement_prod/acd8d1f3-d256-42b7-8340-27e5d5fde92c.svg',\n        title: {\n          zh: '简单易用，扩展灵活',\n          en: 'Simple & Extendable',\n        },\n        description: {\n          zh: 'Vivid, 精心设计的简单、灵活、高可拓展的接口，满足你的无限创意',\n          en: 'Well-designed simple, flexible, and extendable interfaces will satisfy your infinite originality',\n        },\n      },\n    ],\n    /** 首页案例 */\n    cases: [\n      {\n        logo: 'https://camo.githubusercontent.com/53886f0e306c9f01c96dee2edca3992830b7cbb769118029a7e5d677deb7e67e/68747470733a2f2f67772e616c697061796f626a656374732e636f6d2f7a6f732f616e7466696e63646e2f306234487a4f63454a592f4772617068696e2e737667',\n        title: {\n          zh: 'Graphin 图可视分析组件',\n          en: 'Graphin: Graph Insight',\n        },\n        description: {\n          zh: 'Graphin 是一款基于 G6 封装的 React 分析组件库，专注在关系可视分析领域，简单高效，开箱即用。',\n          en: \"Graphin stands for Graph Insight. It's a toolkit based on G6 and React, that focuses on relational visual analysis.It's simple, efficient, out of the box.\",\n        },\n        link: `https://graphin.antv.vision`,\n        image: 'https://gw.alipayobjects.com/mdn/rms_00edcb/afts/img/A*LKq7Q5wPA0AAAAAAAAAAAAAAARQnAQ',\n      },\n      {\n        logo: 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*ch6rTrCxb6YAAAAAAAAAAABkARQnAQ',\n        title: {\n          zh: '基于 G6 的动态决策树',\n          en: 'Interactive Decision Graph Powered by G6',\n        },\n        description: {\n          zh: '基于 G6 实现的动态决策树，辅助用户寻找合适的可视化方式。它展示了 G6 强大的自定义节点和动画的能力。',\n          en: 'It is an interactive graph for users to find out an appropriate visualization method for their requirements. The demo shows the powerful custom node and animation ability of G6.',\n        },\n        link: `/examples/case/graphDemos/#decisionBubbles`,\n        image: 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*10b6R5fkyJ4AAAAAAAAAAABkARQnAQ',\n      },\n      {\n        logo: 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*95GYRI0zPx8AAAAAAAAAAABkARQnAQ',\n        title: {\n          zh: '基于 G6 的图分析应用',\n          en: 'Graph Analysis App Powered by G6',\n        },\n        description: {\n          zh: '社交网络分析是图可视化中一个重要的应用场景。随着社交网络越来越流行，人与人、人与组织之间的关系变得越来越复杂，使用传统的分析手段，已经很难满足我们的分析需求。在这种情况下，图分析及图可视化显得愈发重要。',\n          en: 'Social network is an important scenario in graph visualization. The relationships become complicate with the development of social network. Graph visualization and analysis do well on these complex cases.',\n        },\n        link: `/manual/cases/relations`,\n        image: 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*RYFQSZYewokAAAAAAAAAAABkARQnAQ',\n      },\n      {\n        logo: 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*IEQFS5VtXX8AAAAAAAAAAABkARQnAQ',\n        title: {\n          zh: '基于 G6 的关系时序分析应用',\n          en: 'Dynamic Relationships Analysis Powered by G6',\n        },\n        description: {\n          zh: '基于 G6 的关系时序分析应用，解决应急过程中流程、影响面、应急预案等一系列应急决策辅助信息和手段，快速止血以减少和避免故障升级。',\n          en: 'This is an application for dynamic relationships analysis based on G6, which helps people deal with the flow, influence, and find out solutions to avoid losses and faults.',\n        },\n        link: `/manual/cases/sequenceTime`,\n        image: 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*m41kSpg17ZkAAAAAAAAAAABkARQnAQ',\n      },\n    ],\n    /** 首页合作公司 */\n    companies: [\n      {\n        name: '阿里云',\n        img: 'https://gw.alipayobjects.com/mdn/rms_2274c3/afts/img/A*V_xMRIvw2iwAAAAAAAAAAABkARQnAQ',\n      },\n      {\n        name: '支付宝',\n        img: 'https://gw.alipayobjects.com/mdn/rms_2274c3/afts/img/A*lYDrRZvcvD4AAAAAAAAAAABkARQnAQ',\n      },\n      {\n        name: '天猫',\n        img: 'https://gw.alipayobjects.com/mdn/rms_2274c3/afts/img/A*BQrxRK6oemMAAAAAAAAAAABkARQnAQ',\n      },\n      {\n        name: '淘宝网',\n        img: 'https://gw.alipayobjects.com/mdn/rms_2274c3/afts/img/A*1l8-TqUr7UcAAAAAAAAAAABkARQnAQ',\n      },\n      {\n        name: '网商银行',\n        img: 'https://gw.alipayobjects.com/mdn/rms_2274c3/afts/img/A*ZAKFQJ5Bz4MAAAAAAAAAAABkARQnAQ',\n      },\n      {\n        name: '京东',\n        img: 'https://gw.alipayobjects.com/mdn/rms_2274c3/afts/img/A*yh-HRr3hCpgAAAAAAAAAAABkARQnAQ',\n      },\n      {\n        name: 'yunos',\n        img: 'https://gw.alipayobjects.com/mdn/rms_2274c3/afts/img/A*_js7SaNosUwAAAAAAAAAAABkARQnAQ',\n      },\n      {\n        name: '菜鸟',\n        img: 'https://gw.alipayobjects.com/mdn/rms_2274c3/afts/img/A*TgV-RZDODJIAAAAAAAAAAABkARQnAQ',\n      },\n    ],\n    /** 死链检查配置  */\n    deadLinkChecker: {\n      checkExternalLinks: false, // 是否检查外部链接\n    },\n    /** 站点地图配置 */\n    sitemap: {},\n  },\n  mfsu: false,\n  alias: {\n    '@': __dirname,\n    '@antv/g6': require.resolve('../g6/src/index.ts'),\n    '@antv/g6-extension-3d': require.resolve('../g6-extension-3d/src/index.ts'),\n    '@antv/g6-extension-react': require.resolve('../g6-extension-react/src/index.ts'),\n  },\n  links: [],\n  jsMinifier: 'terser',\n  analytics: {\n    ga_v2: 'G-YLQBGDK1GT',\n  },\n  headScripts: ['https://cdn.jsdelivr.net/npm/lil-gui@0.19'],\n});\n"
  },
  {
    "path": "packages/site/.github/workflows/mirror.yml",
    "content": "name: 🤖 Sync to Gitee Mirror\n\non: [push]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: 🔁 Sync to Gitee\n        uses: wearerequired/git-mirror-action@master\n        env:\n          # 注意在 Settings->Secrets 配置 GITEE_RSA_PRIVATE_KEY\n          SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }}\n        with:\n          # 注意替换为你的 GitHub 源仓库地址\n          source-repo: 'git@github.com:antvis/G6.git'\n          # 注意替换为你的 Gitee 目标仓库地址\n          destination-repo: 'git@gitee.com:antv-g6/antv-g6.git'\n\n      - name: ✅ Build Gitee Pages\n        uses: yanglbme/gitee-pages-action@master\n        with:\n          # 注意替换为你的 Gitee 用户名\n          gitee-username: afc163\n          # 注意在 Settings->Secrets 配置 GITEE_PASSWORD\n          gitee-password: ${{ secrets.GITEE_PASSWORD }}\n          # 注意替换为你的 Gitee 仓库\n          gitee-repo: antv-g6/antv-g6\n          # 要部署的分支\n          branch: gh-pages\n"
  },
  {
    "path": "packages/site/.gitignore",
    "content": "/dist\n.dumi/tmp\n.dumi/tmp-production\n/support\n\n## api doc\ndocs/api/.gitignore\n\n/server\n\n# Dead links report\ndead-links-report.log\n"
  },
  {
    "path": "packages/site/CNAME",
    "content": "g6.antv.vision"
  },
  {
    "path": "packages/site/api-extractor.json",
    "content": "/**\n * Config file for API Extractor.  For more info, please visit: https://api-extractor.com\n */\n{\n  \"$schema\": \"https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json\",\n\n  /**\n   * Optionally specifies another JSON config file that this file extends from.  This provides a way for\n   * standard settings to be shared across multiple projects.\n   *\n   * If the path starts with \"./\" or \"../\", the path is resolved relative to the folder of the file that contains\n   * the \"extends\" field.  Otherwise, the first path segment is interpreted as an NPM package name, and will be\n   * resolved using NodeJS require().\n   *\n   * SUPPORTED TOKENS: none\n   * DEFAULT VALUE: \"\"\n   */\n  // \"extends\": \"./shared/api-extractor-base.json\"\n  // \"extends\": \"my-package/include/api-extractor-base.json\"\n\n  /**\n   * Determines the \"<projectFolder>\" token that can be used with other config file settings.  The project folder\n   * typically contains the tsconfig.json and package.json config files, but the path is user-defined.\n   *\n   * The path is resolved relative to the folder of the config file that contains the setting.\n   *\n   * The default value for \"projectFolder\" is the token \"<lookup>\", which means the folder is determined by traversing\n   * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder\n   * that contains a tsconfig.json file.  If a tsconfig.json file cannot be found in this way, then an error\n   * will be reported.\n   *\n   * SUPPORTED TOKENS: <lookup>\n   * DEFAULT VALUE: \"<lookup>\"\n   */\n  // \"projectFolder\": \"..\",\n\n  /**\n   * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis.  API Extractor\n   * analyzes the symbols exported by this module.\n   *\n   * The file extension must be \".d.ts\" and not \".ts\".\n   *\n   * The path is resolved relative to the folder of the config file that contains the setting; to change this,\n   * prepend a folder token such as \"<projectFolder>\".\n   *\n   * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>\n   */\n  \"mainEntryPointFilePath\": \"<projectFolder>/../g6/lib/index.d.ts\",\n\n  /**\n   * A list of NPM package names whose exports should be treated as part of this package.\n   *\n   * For example, suppose that Webpack is used to generate a distributed bundle for the project \"library1\",\n   * and another NPM package \"library2\" is embedded in this bundle.  Some types from library2 may become part\n   * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly\n   * imports library2.  To avoid this, we can specify:\n   *\n   *   \"bundledPackages\": [ \"library2\" ],\n   *\n   * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been\n   * local files for library1.\n   */\n  \"bundledPackages\": [\"@antv/layout\"],\n\n  /**\n   * Specifies what type of newlines API Extractor should use when writing output files.  By default, the output files\n   * will be written with Windows-style newlines.  To use POSIX-style newlines, specify \"lf\" instead.\n   * To use the OS's default newline kind, specify \"os\".\n   *\n   * DEFAULT VALUE: \"crlf\"\n   */\n  // \"newlineKind\": \"crlf\",\n\n  /**\n   * Set to true when invoking API Extractor's test harness. When `testMode` is true, the `toolVersion` field in the\n   * .api.json file is assigned an empty string to prevent spurious diffs in output files tracked for tests.\n   *\n   * DEFAULT VALUE: \"false\"\n   */\n  // \"testMode\": false,\n\n  /**\n   * Specifies how API Extractor sorts members of an enum when generating the .api.json file. By default, the output\n   * files will be sorted alphabetically, which is \"by-name\". To keep the ordering in the source code, specify\n   * \"preserve\".\n   *\n   * DEFAULT VALUE: \"by-name\"\n   */\n  \"enumMemberOrder\": \"preserve\",\n\n  /**\n   * Determines how the TypeScript compiler engine will be invoked by API Extractor.\n   */\n  \"compiler\": {\n    /**\n     * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project.\n     *\n     * The path is resolved relative to the folder of the config file that contains the setting; to change this,\n     * prepend a folder token such as \"<projectFolder>\".\n     *\n     * Note: This setting will be ignored if \"overrideTsconfig\" is used.\n     *\n     * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>\n     * DEFAULT VALUE: \"<projectFolder>/tsconfig.json\"\n     */\n    // \"tsconfigFilePath\": \"<projectFolder>/tsconfig.json\",\n    /**\n     * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk.\n     * The object must conform to the TypeScript tsconfig schema:\n     *\n     * http://json.schemastore.org/tsconfig\n     *\n     * If omitted, then the tsconfig.json file will be read from the \"projectFolder\".\n     *\n     * DEFAULT VALUE: no overrideTsconfig section\n     */\n    // \"overrideTsconfig\": {\n    //   . . .\n    // }\n    /**\n     * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended\n     * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when\n     * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses\n     * for its analysis.  Where possible, the underlying issue should be fixed rather than relying on skipLibCheck.\n     *\n     * DEFAULT VALUE: false\n     */\n    // \"skipLibCheck\": true,\n  },\n\n  /**\n   * Configures how the API report file (*.api.md) will be generated.\n   */\n  \"apiReport\": {\n    /**\n     * (REQUIRED) Whether to generate an API report.\n     */\n    \"enabled\": true\n\n    /**\n     * The filename for the API report files.  It will be combined with \"reportFolder\" or \"reportTempFolder\" to produce\n     * a full file path.\n     *\n     * The file extension should be \".api.md\", and the string should not contain a path separator such as \"\\\" or \"/\".\n     *\n     * SUPPORTED TOKENS: <packageName>, <unscopedPackageName>\n     * DEFAULT VALUE: \"<unscopedPackageName>.api.md\"\n     */\n    // \"reportFileName\": \"<unscopedPackageName>.api.md\",\n\n    /**\n     * Specifies the folder where the API report file is written.  The file name portion is determined by\n     * the \"reportFileName\" setting.\n     *\n     * The API report file is normally tracked by Git.  Changes to it can be used to trigger a branch policy,\n     * e.g. for an API review.\n     *\n     * The path is resolved relative to the folder of the config file that contains the setting; to change this,\n     * prepend a folder token such as \"<projectFolder>\".\n     *\n     * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>\n     * DEFAULT VALUE: \"<projectFolder>/temp/\"\n     */\n    // \"reportFolder\": \"<projectFolder>/temp/\",\n\n    /**\n     * Specifies the folder where the temporary report file is written.  The file name portion is determined by\n     * the \"reportFileName\" setting.\n     *\n     * After the temporary file is written to disk, it is compared with the file in the \"reportFolder\".\n     * If they are different, a production build will fail.\n     *\n     * The path is resolved relative to the folder of the config file that contains the setting; to change this,\n     * prepend a folder token such as \"<projectFolder>\".\n     *\n     * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>\n     * DEFAULT VALUE: \"<projectFolder>/temp/\"\n     */\n    // \"reportTempFolder\": \"<projectFolder>/temp/\",\n\n    /**\n     * Whether \"forgotten exports\" should be included in the API report file. Forgotten exports are declarations\n     * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to\n     * learn more.\n     *\n     * DEFAULT VALUE: \"false\"\n     */\n    // \"includeForgottenExports\": false\n  },\n\n  /**\n   * Configures how the doc model file (*.api.json) will be generated.\n   */\n  \"docModel\": {\n    /**\n     * (REQUIRED) Whether to generate a doc model file.\n     */\n    \"enabled\": true\n\n    /**\n     * The output path for the doc model file.  The file extension should be \".api.json\".\n     *\n     * The path is resolved relative to the folder of the config file that contains the setting; to change this,\n     * prepend a folder token such as \"<projectFolder>\".\n     *\n     * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>\n     * DEFAULT VALUE: \"<projectFolder>/temp/<unscopedPackageName>.api.json\"\n     */\n    // \"apiJsonFilePath\": \"<projectFolder>/temp/<unscopedPackageName>.api.json\",\n\n    /**\n     * Whether \"forgotten exports\" should be included in the doc model file. Forgotten exports are declarations\n     * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to\n     * learn more.\n     *\n     * DEFAULT VALUE: \"false\"\n     */\n    // \"includeForgottenExports\": false,\n\n    /**\n     * The base URL where the project's source code can be viewed on a website such as GitHub or\n     * Azure DevOps. This URL path corresponds to the `<projectFolder>` path on disk.\n     *\n     * This URL is concatenated with the file paths serialized to the doc model to produce URL file paths to individual API items.\n     * For example, if the `projectFolderUrl` is \"https://github.com/microsoft/rushstack/tree/main/apps/api-extractor\" and an API\n     * item's file path is \"api/ExtractorConfig.ts\", the full URL file path would be\n     * \"https://github.com/microsoft/rushstack/tree/main/apps/api-extractor/api/ExtractorConfig.js\".\n     *\n     * Can be omitted if you don't need source code links in your API documentation reference.\n     *\n     * SUPPORTED TOKENS: none\n     * DEFAULT VALUE: \"\"\n     */\n    // \"projectFolderUrl\": \"http://github.com/path/to/your/projectFolder\"\n  },\n\n  /**\n   * Configures how the .d.ts rollup file will be generated.\n   */\n  \"dtsRollup\": {\n    /**\n     * (REQUIRED) Whether to generate the .d.ts rollup file.\n     */\n    \"enabled\": true\n\n    /**\n     * Specifies the output path for a .d.ts rollup file to be generated without any trimming.\n     * This file will include all declarations that are exported by the main entry point.\n     *\n     * If the path is an empty string, then this file will not be written.\n     *\n     * The path is resolved relative to the folder of the config file that contains the setting; to change this,\n     * prepend a folder token such as \"<projectFolder>\".\n     *\n     * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>\n     * DEFAULT VALUE: \"<projectFolder>/dist/<unscopedPackageName>.d.ts\"\n     */\n    // \"untrimmedFilePath\": \"<projectFolder>/dist/<unscopedPackageName>.d.ts\",\n\n    /**\n     * Specifies the output path for a .d.ts rollup file to be generated with trimming for an \"alpha\" release.\n     * This file will include only declarations that are marked as \"@public\", \"@beta\", or \"@alpha\".\n     *\n     * The path is resolved relative to the folder of the config file that contains the setting; to change this,\n     * prepend a folder token such as \"<projectFolder>\".\n     *\n     * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>\n     * DEFAULT VALUE: \"\"\n     */\n    // \"alphaTrimmedFilePath\": \"<projectFolder>/dist/<unscopedPackageName>-alpha.d.ts\",\n\n    /**\n     * Specifies the output path for a .d.ts rollup file to be generated with trimming for a \"beta\" release.\n     * This file will include only declarations that are marked as \"@public\" or \"@beta\".\n     *\n     * The path is resolved relative to the folder of the config file that contains the setting; to change this,\n     * prepend a folder token such as \"<projectFolder>\".\n     *\n     * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>\n     * DEFAULT VALUE: \"\"\n     */\n    // \"betaTrimmedFilePath\": \"<projectFolder>/dist/<unscopedPackageName>-beta.d.ts\",\n\n    /**\n     * Specifies the output path for a .d.ts rollup file to be generated with trimming for a \"public\" release.\n     * This file will include only declarations that are marked as \"@public\".\n     *\n     * If the path is an empty string, then this file will not be written.\n     *\n     * The path is resolved relative to the folder of the config file that contains the setting; to change this,\n     * prepend a folder token such as \"<projectFolder>\".\n     *\n     * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>\n     * DEFAULT VALUE: \"\"\n     */\n    // \"publicTrimmedFilePath\": \"<projectFolder>/dist/<unscopedPackageName>-public.d.ts\",\n\n    /**\n     * When a declaration is trimmed, by default it will be replaced by a code comment such as\n     * \"Excluded from this release type: exampleMember\".  Set \"omitTrimmingComments\" to true to remove the\n     * declaration completely.\n     *\n     * DEFAULT VALUE: false\n     */\n    // \"omitTrimmingComments\": true\n  },\n\n  /**\n   * Configures how the tsdoc-metadata.json file will be generated.\n   */\n  \"tsdocMetadata\": {\n    /**\n     * Whether to generate the tsdoc-metadata.json file.\n     *\n     * DEFAULT VALUE: true\n     */\n    // \"enabled\": true,\n    /**\n     * Specifies where the TSDoc metadata file should be written.\n     *\n     * The path is resolved relative to the folder of the config file that contains the setting; to change this,\n     * prepend a folder token such as \"<projectFolder>\".\n     *\n     * The default value is \"<lookup>\", which causes the path to be automatically inferred from the \"tsdocMetadata\",\n     * \"typings\" or \"main\" fields of the project's package.json.  If none of these fields are set, the lookup\n     * falls back to \"tsdoc-metadata.json\" in the package folder.\n     *\n     * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>\n     * DEFAULT VALUE: \"<lookup>\"\n     */\n    // \"tsdocMetadataFilePath\": \"<projectFolder>/dist/tsdoc-metadata.json\"\n  },\n\n  /**\n   * Configures how API Extractor reports error and warning messages produced during analysis.\n   *\n   * There are three sources of messages:  compiler messages, API Extractor messages, and TSDoc messages.\n   */\n  \"messages\": {\n    /**\n     * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing\n     * the input .d.ts files.\n     *\n     * TypeScript message identifiers start with \"TS\" followed by an integer.  For example: \"TS2551\"\n     *\n     * DEFAULT VALUE:  A single \"default\" entry with logLevel=warning.\n     */\n    \"compilerMessageReporting\": {\n      /**\n       * Configures the default routing for messages that don't match an explicit rule in this table.\n       */\n      \"default\": {\n        /**\n         * Specifies whether the message should be written to the the tool's output log.  Note that\n         * the \"addToApiReportFile\" property may supersede this option.\n         *\n         * Possible values: \"error\", \"warning\", \"none\"\n         *\n         * Errors cause the build to fail and return a nonzero exit code.  Warnings cause a production build fail\n         * and return a nonzero exit code.  For a non-production build (e.g. when \"api-extractor run\" includes\n         * the \"--local\" option), the warning is displayed but the build will not fail.\n         *\n         * DEFAULT VALUE: \"warning\"\n         */\n        \"logLevel\": \"warning\"\n\n        /**\n         * When addToApiReportFile is true:  If API Extractor is configured to write an API report file (.api.md),\n         * then the message will be written inside that file; otherwise, the message is instead logged according to\n         * the \"logLevel\" option.\n         *\n         * DEFAULT VALUE: false\n         */\n        // \"addToApiReportFile\": false\n      }\n\n      // \"TS2551\": {\n      //   \"logLevel\": \"warning\",\n      //   \"addToApiReportFile\": true\n      // },\n      //\n      // . . .\n    },\n\n    /**\n     * Configures handling of messages reported by API Extractor during its analysis.\n     *\n     * API Extractor message identifiers start with \"ae-\".  For example: \"ae-extra-release-tag\"\n     *\n     * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings\n     */\n    \"extractorMessageReporting\": {\n      \"default\": {\n        \"logLevel\": \"warning\"\n        // \"addToApiReportFile\": false\n      }\n\n      // \"ae-extra-release-tag\": {\n      //   \"logLevel\": \"warning\",\n      //   \"addToApiReportFile\": true\n      // },\n      //\n      // . . .\n    },\n\n    /**\n     * Configures handling of messages reported by the TSDoc parser when analyzing code comments.\n     *\n     * TSDoc message identifiers start with \"tsdoc-\".  For example: \"tsdoc-link-tag-unescaped-text\"\n     *\n     * DEFAULT VALUE:  A single \"default\" entry with logLevel=warning.\n     */\n    \"tsdocMessageReporting\": {\n      \"default\": {\n        \"logLevel\": \"warning\"\n        // \"addToApiReportFile\": false\n      }\n\n      // \"tsdoc-link-tag-unescaped-text\": {\n      //   \"logLevel\": \"warning\",\n      //   \"addToApiReportFile\": true\n      // },\n      //\n      // . . .\n    }\n  }\n}\n"
  },
  {
    "path": "packages/site/common/angular-snippet.md",
    "content": "<iframe src=\"https://stackblitz.com/edit/g6-in-angular?embed=1&file=src%2Fmain.ts&theme=light\"\n     style=\"width:100%; height: 500px; border:0; border-radius: 4px; overflow:hidden;\"></iframe>\n\n**app.component.html**\n\n```html\n<div>\n  <h1>{{ title }}</h1>\n  <div #container></div>\n</div>\n```\n\n**app.component.ts**\n\n```ts\nimport { Component, ViewChild, ElementRef } from '@angular/core';\nimport { Graph } from '@antv/g6';\n\n@Component({\n  selector: 'app-root',\n  templateUrl: './app.component.html',\n  styleUrls: ['./app.component.css'],\n})\nexport class AppComponent {\n  title = 'Use G6 in Angular';\n\n  @ViewChild('container') container: ElementRef;\n\n  ngAfterViewInit() {\n    const graph = new Graph({\n      container: this.container.nativeElement,\n      width: 500,\n      height: 500,\n      data: {\n        nodes: [\n          {\n            id: 'node-1',\n            style: { x: 50, y: 100 },\n          },\n          {\n            id: 'node-2',\n            style: { x: 150, y: 100 },\n          },\n        ],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n      },\n    });\n\n    graph.render();\n  }\n}\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/auto-adapt-label.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node1', style: { x: 200, y: 100, labelText: '短标签' } },\n        { id: 'node2', style: { x: 360, y: 100, labelText: '中等长度的标签' } },\n        { id: 'node3', style: { x: 280, y: 220, labelText: '这是一个非常非常长的标签，需要自适应显示' } },\n      ],\n      edges: [\n        { source: 'node1', target: 'node2' },\n        { source: 'node1', target: 'node3' },\n        { source: 'node2', target: 'node3' },\n      ],\n    },\n    node: {\n      style: { label: true, fill: '#7e3feb', labelFill: '#666', labelFontSize: 14, labelPlacement: 'bottom' },\n      state: {\n        custom: { fill: '#ffa940' },\n      },\n    },\n    edge: {\n      stroke: '#8b9baf',\n      state: {\n        custom: { stroke: '#ffa940' },\n      },\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas', { key: 'auto-adapt-label', type: 'auto-adapt-label' }],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const options = {\n      key: 'auto-adapt-label',\n      type: 'auto-adapt-label',\n      animation: true,\n      enable: true,\n      throttle: 100,\n      padding: 0,\n    };\n    const optionFolder = gui.addFolder('CollapseExpand Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'animation');\n    optionFolder.add(options, 'enable');\n    optionFolder.add(options, 'throttle', 0, 900, 100);\n    optionFolder.add(options, 'padding', 0, 20, 1);\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'auto-adapt-label',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/brush-select.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 200, y: 100 } },\n        { id: 'node-2', style: { x: 360, y: 100 } },\n        { id: 'node-3', style: { x: 280, y: 220 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-1', target: 'node-3' },\n        { source: 'node-2', target: 'node-3' },\n      ],\n    },\n    node: {\n      style: { fill: '#7e3feb' },\n      state: {\n        custom: { fill: '#ffa940' },\n      },\n    },\n    edge: {\n      stroke: '#8b9baf',\n      state: {\n        custom: { stroke: '#ffa940' },\n      },\n    },\n    behaviors: [\n      {\n        type: 'brush-select',\n        key: 'brush-select',\n      },\n    ],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      key: 'brush-select',\n      type: 'brush-select',\n      animation: false,\n      enable: true,\n      enableElements: ['node', 'edge', 'combo'],\n      immediately: false,\n      mode: 'default',\n      state: 'selected',\n      trigger: 'shift+drag',\n    };\n    const optionFolder = gui.addFolder('BrushSelect Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'animation');\n    optionFolder.add(options, 'enable');\n    optionFolder.add(options, 'enableElements', [\n      ['node', 'edge', 'combo'],\n      ['node', 'edge'],\n      ['node', 'combo'],\n      ['combo', 'edge'],\n      ['node'],\n      ['edge'],\n      ['combo'],\n    ]);\n    optionFolder.add(options, 'trigger', {\n      'shift+drag': ['shift'],\n      drag: [],\n    });\n    optionFolder.add(options, 'state', ['active', 'selected', 'custom']);\n    optionFolder.add(options, 'mode', ['union', 'intersect', 'diff', 'default']).onChange((e) => {\n      immediately.show(e === 'default');\n    });\n    const immediately = optionFolder.add(options, 'immediately');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'brush-select',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/click-element.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 280, y: 60, fill: '#E4504D', labelText: 'degree: 0' } },\n        { id: 'node-2-1', style: { x: 330, y: 140, fill: '#FFC40C', labelText: 'degree: 1' } },\n        { id: 'node-2-2', style: { x: 230, y: 140, fill: '#FFC40C', labelText: 'degree: 1' } },\n        { id: 'node-3-1', style: { x: 380, y: 220, fill: '#0f0', labelText: 'degree: 2' } },\n        { id: 'node-3-2', style: { x: 180, y: 220, fill: '#0f0', labelText: 'degree: 2' } },\n\n        {\n          id: 'degree引导',\n          style: {\n            x: 525,\n            y: 110,\n            fill: null,\n            labelText: '这里可以修改degree ->',\n            labelFontWeight: 700,\n            labelFontSize: 10,\n          },\n        },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2-1' },\n        { source: 'node-1', target: 'node-2-2' },\n        { source: 'node-2-1', target: 'node-3-1' },\n        { source: 'node-2-2', target: 'node-3-2' },\n      ],\n    },\n    node: {\n      style: { label: true, labelFill: '#666', labelFontSize: 14, labelPlacement: 'bottom' },\n      state: {\n        custom: { fill: '#ffa940' },\n      },\n    },\n    edge: {\n      stroke: '#8b9baf',\n      state: {\n        custom: { stroke: '#ffa940' },\n      },\n    },\n    behaviors: [\n      {\n        type: 'click-select',\n        key: 'click-select',\n      },\n    ],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      key: 'click-select',\n      type: 'click-select',\n      animation: true,\n      enable: true,\n      multiple: false,\n      trigger: 'shift+click',\n      state: 'selected',\n      unselectedState: undefined,\n      degree: 0,\n    };\n    const optionFolder = gui.addFolder('Click Select Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'animation');\n    optionFolder.add(options, 'enable');\n    optionFolder.add(options, 'degree', 0, 2, 1);\n    optionFolder.add(options, 'state', ['active', 'selected', 'custom']);\n    optionFolder.add(options, 'unselectedState', [undefined, 'inactive']);\n    const trigger = optionFolder\n      .add(options, 'trigger', {\n        'shift+click': ['shift'],\n        'meta+click': ['Meta'],\n      })\n      .hide();\n    optionFolder.add(options, 'multiple').onChange((v) => trigger.show(v));\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'click-select',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/collapse-expand.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n        { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n        { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n      ],\n      edges: [],\n      combos: [\n        { id: 'combo1', combo: 'combo2' },\n        { id: 'combo2', style: {} },\n      ],\n    },\n    node: { style: { fill: '#7e3feb' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    behaviors: [\n      {\n        type: 'collapse-expand',\n        key: 'collapse-expand',\n      },\n    ],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const options = {\n      key: 'collapse-expand',\n      type: 'collapse-expand',\n      animation: true,\n      enable: true,\n    };\n    const optionFolder = gui.addFolder('CollapseExpand Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'animation');\n    optionFolder.add(options, 'enable');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'collapse-expand',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/create-edge.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n        { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n        { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n      ],\n      edges: [],\n      combos: [\n        { id: 'combo1', combo: 'combo2' },\n        { id: 'combo2', style: {} },\n      ],\n    },\n    node: { style: { fill: '#873bf4' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    behaviors: [\n      {\n        type: 'create-edge',\n        key: 'create-edge',\n      },\n    ],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const options = {\n      key: 'create-edge',\n      type: 'create-edge',\n      animation: true,\n      enable: true,\n      trigger: 'drag',\n    };\n    const optionFolder = gui.addFolder('CollapseExpand Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'animation');\n    optionFolder.add(options, 'enable');\n    optionFolder.add(options, 'trigger', ['drag', 'click']);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'create-edge',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/drag-canvas.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: { nodes: [{ id: 'node-1' }] },\n    layout: { type: 'force' },\n    behaviors: [\n      {\n        type: 'drag-canvas',\n        key: 'drag-canvas',\n      },\n    ],\n    node: { style: { fill: '#7e3feb' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      key: 'drag-canvas',\n      type: 'drag-canvas',\n      enable: true,\n      sensitivity: 1,\n      trigger: 'Use cursor by default',\n    };\n    const optionFolder = gui.addFolder('ZoomCanvas Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'enable');\n    optionFolder.add(options, 'sensitivity', 0, 10, 1);\n    optionFolder.add(options, 'trigger', {\n      'Use cursor by default': [],\n      'Shift+Arrow Key': {\n        up: ['Shift', 'ArrowUp'],\n        down: ['Shift', 'ArrowDown'],\n        left: ['Shift', 'ArrowLeft'],\n        right: ['Shift', 'ArrowRight'],\n      },\n    });\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'drag-canvas',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/drag-element.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n        { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n        { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n      ],\n      edges: [],\n      combos: [\n        { id: 'combo1', combo: 'combo2' },\n        { id: 'combo2', style: {} },\n      ],\n    },\n    node: { style: { fill: '#873bf4' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    behaviors: [\n      {\n        type: 'drag-element',\n        key: 'drag-element',\n      },\n    ],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const options = {\n      key: 'drag-element',\n      type: 'drag-element',\n      animation: true,\n      enable: 'node,combo',\n      dropEffect: 'move',\n      state: 'selected',\n      hideEdge: 'none',\n      shadow: false,\n    };\n    const optionFolder = gui.addFolder('DragElement Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'animation');\n    optionFolder.add(options, 'enable', {\n      'node,combo': (event) => ['node', 'combo'].includes(event.targetType),\n      node: (event) => ['node'].includes(event.targetType),\n      combo: (event) => ['combo'].includes(event.targetType),\n      none: false,\n    });\n    optionFolder.add(options, 'dropEffect', ['link', 'move', 'none']);\n    optionFolder.add(options, 'hideEdge', ['none', 'all', 'in', 'out', 'both']);\n    optionFolder.add(options, 'shadow');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'drag-element',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/fix-element-size.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node1', style: { x: 200, y: 100, labelText: 'node1' } },\n        { id: 'node2', style: { x: 360, y: 100, labelText: 'node2' } },\n        { id: 'node3', style: { x: 280, y: 220, labelText: 'node3' } },\n      ],\n      edges: [\n        { source: 'node1', target: 'node2' },\n        { source: 'node1', target: 'node3' },\n        { source: 'node2', target: 'node3' },\n      ],\n    },\n    node: {\n      style: { label: true, labelFill: '#666', labelFontSize: 14, labelPlacement: 'bottom' },\n      state: {\n        custom: { fill: '#ffa940' },\n      },\n    },\n    edge: {\n      stroke: '#8b9baf',\n      state: {\n        custom: { stroke: '#ffa940' },\n      },\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas', { key: 'fix-element-size', type: 'fix-element-size' }],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 800, height: 400 },\n  (gui, graph) => {\n    const options = {\n      key: 'fix-element-size',\n      type: 'fix-element-size',\n      animation: true,\n      enable: true,\n      reset: true,\n    };\n    const optionFolder = gui.addFolder('CollapseExpand Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'animation');\n    optionFolder.add(options, 'enable');\n    optionFolder.add(options, 'reset');\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'fix-element-size',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/focus-element.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 200, y: 100 } },\n        { id: 'node-2', style: { x: 360, y: 100 } },\n        { id: 'node-3', style: { x: 280, y: 220 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-1', target: 'node-3' },\n        { source: 'node-2', target: 'node-3' },\n      ],\n    },\n    node: { style: { fill: '#7e3feb' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    behaviors: [\n      {\n        type: 'focus-element',\n        key: 'focus-element',\n      },\n    ],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      key: 'focus-element',\n      type: 'focus-element',\n      animation: true,\n      enable: true,\n    };\n    const optionFolder = gui.addFolder('FocusElement Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'animation');\n    optionFolder.add(options, 'enable');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'focus-element',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/hover-activate.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node0', size: 50, label: '0', style: { x: 326, y: 268 } },\n        { id: 'node1', size: 30, label: '1', style: { x: 280, y: 384 } },\n        { id: 'node2', size: 30, label: '2', style: { x: 234, y: 167 } },\n        { id: 'node3', size: 30, label: '3', style: { x: 391, y: 368 } },\n        { id: 'node4', size: 30, label: '4', style: { x: 444, y: 209 } },\n        { id: 'node5', size: 30, label: '5', style: { x: 378, y: 157 } },\n        { id: 'node6', size: 15, label: '6', style: { x: 229, y: 400 } },\n        { id: 'node7', size: 15, label: '7', style: { x: 281, y: 440 } },\n        { id: 'node8', size: 15, label: '8', style: { x: 188, y: 119 } },\n        { id: 'node9', size: 15, label: '9', style: { x: 287, y: 157 } },\n        { id: 'node10', size: 15, label: '10', style: { x: 185, y: 200 } },\n        { id: 'node11', size: 15, label: '11', style: { x: 238, y: 110 } },\n        { id: 'node12', size: 15, label: '12', style: { x: 239, y: 221 } },\n        { id: 'node13', size: 15, label: '13', style: { x: 176, y: 160 } },\n        { id: 'node14', size: 15, label: '14', style: { x: 389, y: 423 } },\n        { id: 'node15', size: 15, label: '15', style: { x: 441, y: 341 } },\n        { id: 'node16', size: 15, label: '16', style: { x: 442, y: 398 } },\n      ],\n      edges: [\n        { source: 'node0', target: 'node1', label: '0-1' },\n        { source: 'node0', target: 'node2', label: '0-2' },\n        { source: 'node0', target: 'node3', label: '0-3' },\n        { source: 'node0', target: 'node4', label: '0-4' },\n        { source: 'node0', target: 'node5', label: '0-5' },\n        { source: 'node1', target: 'node6', label: '1-6' },\n        { source: 'node1', target: 'node7', label: '1-7' },\n        { source: 'node2', target: 'node8', label: '2-8' },\n        { source: 'node2', target: 'node9', label: '2-9' },\n        { source: 'node2', target: 'node10', label: '2-10' },\n        { source: 'node2', target: 'node11', label: '2-11' },\n        { source: 'node2', target: 'node12', label: '2-12' },\n        { source: 'node2', target: 'node13', label: '2-13' },\n        { source: 'node3', target: 'node14', label: '3-14' },\n        { source: 'node3', target: 'node15', label: '3-15' },\n        { source: 'node3', target: 'node16', label: '3-16' },\n      ],\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas', { key: 'hover-activate', type: 'hover-activate' }],\n    autoFit: 'center',\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      key: 'hover-activate',\n      type: 'hover-activate',\n      animation: true,\n      enable: true,\n      degree: 1,\n      direction: 'both',\n    };\n    const optionFolder = gui.addFolder('Hover Activate Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'animation');\n    optionFolder.add(options, 'enable');\n    optionFolder.add(options, 'degree', 0, 10, 1);\n    optionFolder.add(options, 'direction', {\n      both: ['both'],\n      in: ['in'],\n      out: ['out'],\n    });\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'hover-activate',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/hover-element.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 200, y: 100 } },\n        { id: 'node-2', style: { x: 360, y: 100 } },\n        { id: 'node-3', style: { x: 280, y: 220 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-1', target: 'node-3' },\n        { source: 'node-2', target: 'node-3' },\n      ],\n    },\n    node: {\n      style: { fill: '#7e3feb' },\n      state: {\n        custom: { fill: '#ffa940' },\n      },\n    },\n    edge: {\n      stroke: '#8b9baf',\n      state: {\n        custom: { stroke: '#ffa940' },\n      },\n    },\n    behaviors: [\n      {\n        type: 'hover-activate',\n        key: 'hover-activate',\n      },\n    ],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      key: 'hover-activate',\n      type: 'hover-activate',\n      animation: true,\n      enable: true,\n      degree: 0,\n      state: 'active',\n      inactiveState: undefined,\n    };\n    const optionFolder = gui.addFolder('ZoomCanvas Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'animation');\n    optionFolder.add(options, 'enable');\n    optionFolder.add(options, 'degree', 0, 2, 1);\n    optionFolder.add(options, 'state', ['active', 'selected', 'custom']);\n    optionFolder.add(options, 'inactiveState', [undefined, 'inactive']);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'hover-activate',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/lasso-select.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 200, y: 100 } },\n        { id: 'node-2', style: { x: 360, y: 100 } },\n        { id: 'node-3', style: { x: 280, y: 220 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-1', target: 'node-3' },\n        { source: 'node-2', target: 'node-3' },\n      ],\n    },\n    node: {\n      style: { fill: '#7e3feb' },\n      state: {\n        custom: { fill: '#ffa940' },\n      },\n    },\n    edge: {\n      stroke: '#8b9baf',\n      state: {\n        custom: { stroke: '#ffa940' },\n      },\n    },\n    behaviors: [\n      {\n        type: 'lasso-select',\n        key: 'lasso-select',\n      },\n    ],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      key: 'lasso-select',\n      type: 'lasso-select',\n      animation: false,\n      enable: true,\n      enableElements: ['node', 'edge', 'combo'],\n      immediately: false,\n      mode: 'default',\n      state: 'selected',\n      trigger: 'shift+drag',\n    };\n    const optionFolder = gui.addFolder('LassoSelect Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'animation');\n    optionFolder.add(options, 'enable');\n    optionFolder.add(options, 'enableElements', [\n      ['node', 'edge', 'combo'],\n      ['node', 'edge'],\n      ['node', 'combo'],\n      ['combo', 'edge'],\n      ['node'],\n      ['edge'],\n      ['combo'],\n    ]);\n    optionFolder.add(options, 'trigger', {\n      'shift+drag': ['shift'],\n      drag: [],\n    });\n    optionFolder.add(options, 'state', ['active', 'selected', 'custom']);\n    optionFolder.add(options, 'mode', ['union', 'intersect', 'diff', 'default']).onChange((e) => {\n      immediately.show(e === 'default');\n    });\n    const immediately = optionFolder.add(options, 'immediately');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'lasso-select',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/scroll-canvas.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: { nodes: [{ id: 'node-1' }] },\n    layout: { type: 'force' },\n    behaviors: [\n      {\n        type: 'scroll-canvas',\n        key: 'scroll-canvas',\n      },\n    ],\n    node: { style: { fill: '#873bf4' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      key: 'scroll-canvas',\n      type: 'scroll-canvas',\n      direction: 'No limit',\n      enable: true,\n      sensitivity: 1,\n      trigger: 'Use wheel by default',\n    };\n    const optionFolder = gui.addFolder('ZoomCanvas Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'direction', {\n      'No limit': '',\n      'Only allow horizontal scrolling': 'x',\n      'Only allow vertical scrolling': 'y',\n    });\n    optionFolder.add(options, 'enable');\n    optionFolder.add(options, 'sensitivity', 0, 10, 1);\n    optionFolder.add(options, 'trigger', {\n      'Use wheel by default': [],\n      'Shift+Arrow Key': {\n        up: ['Shift', 'ArrowUp'],\n        down: ['Shift', 'ArrowDown'],\n        left: ['Shift', 'ArrowLeft'],\n        right: ['Shift', 'ArrowRight'],\n      },\n    });\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'scroll-canvas',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/behaviors/zoom-canvas.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: { nodes: [{ id: 'node-1' }] },\n    layout: { type: 'force' },\n    behaviors: [\n      {\n        type: 'zoom-canvas',\n        key: 'zoom-canvas',\n      },\n    ],\n    node: { style: { fill: '#873bf4' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      key: 'zoom-canvas',\n      type: 'zoom-canvas',\n      animation: true,\n      enable: true,\n      sensitivity: 1,\n      trigger: 'Use wheel by default',\n    };\n    const optionFolder = gui.addFolder('ZoomCanvas Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'animation');\n    optionFolder.add(options, 'enable');\n    optionFolder.add(options, 'sensitivity', 0, 10, 1);\n    optionFolder.add(options, 'trigger', {\n      'Use wheel by default': [],\n      'Control+Wheel': ['Control'],\n      'zoomIn:Ctrl+1 zoomOut:Ctrl+2 reset:Ctrl+0': {\n        zoomIn: ['Control', '1'],\n        zoomOut: ['Control', '2'],\n        reset: ['Control', '0'],\n      },\n    });\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'zoom-canvas',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/elements/combos/base-combo.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n        { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n        { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n      ],\n      combos: [\n        { id: 'combo1', combo: 'combo2' },\n        { id: 'combo2', style: {} },\n      ],\n    },\n    node: { style: { fill: '#7e3feb' } },\n    combo: {\n      style: {\n        labelText: (d) => d.id,\n        labelPadding: [1, 10],\n        labelFill: '#fff',\n        labelBackground: true,\n        labelBackgroundRadius: 4,\n        labelBackgroundFill: '#7e3feb',\n      },\n    },\n    behaviors: ['collapse-expand'],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const options = {\n      collapsed: false,\n      collapsedSize: 32,\n      collapsedMarker: true,\n      collapsedMarkerFontSize: 12,\n      collapsedMarkerType: 'child-count',\n    };\n\n    const optionFolder = gui.addFolder('combo2.style');\n\n    optionFolder.add(options, 'collapsed');\n    optionFolder.add(options, 'collapsedSize', 0, 100, 1);\n    optionFolder.add(options, 'collapsedMarker');\n    optionFolder.add(options, 'collapsedMarkerFontSize', 12, 20, 1);\n    optionFolder.add(options, 'collapsedMarkerType', ['child-count', 'descendant-count', 'node-count']);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateComboData([{ id: 'combo2', style: { [property]: value } }]);\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/elements/combos/circle-combo-interest.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 600,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo2', style: { x: 150, y: 150 } },\n      { id: 'node2', combo: 'combo2', style: { x: 200, y: 150 } },\n      { id: 'node3', combo: 'combo3', style: { x: 300, y: 150 } },\n      { id: 'node4', combo: 'combo3', style: { x: 350, y: 150 } },\n      { id: 'node5', combo: 'combo4', style: { x: 230, y: 300 } },\n      { id: 'node6', combo: 'combo4', style: { x: 280, y: 300 } },\n    ],\n    combos: [\n      { id: 'combo1', style: { labelText: '兴趣小组' } },\n      { id: 'combo2', combo: 'combo1', style: { labelText: '书法' } },\n      { id: 'combo3', combo: 'combo1', style: { labelText: '影视' } },\n      { id: 'combo4', combo: 'combo1', style: { labelText: '游戏' } },\n    ],\n  },\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  behaviors: ['drag-element', 'collapse-expand'],\n  animation: true,\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/api/elements/combos/circle-combo.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n        { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n        { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n      ],\n      combos: [\n        { id: 'combo1', combo: 'combo2' },\n        { id: 'combo2', style: {} },\n      ],\n    },\n    node: { style: { fill: '#7e3feb' } },\n    behaviors: ['drag-element', 'collapse-expand'],\n    plugins: ['grid-line'],\n    animation: true,\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    gui.add({ type: 'circle' }, 'type').disable();\n  },\n);\n```\n\n设置 `combo.type` 为 `circle` 以使用圆形组合。\n"
  },
  {
    "path": "packages/site/common/api/elements/combos/rect-combo-architecture.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 400,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo2', style: { x: 100, y: 100, labelText: '微服务1' } },\n      { id: 'node2', combo: 'combo2', style: { x: 200, y: 100, labelText: '微服务2' } },\n      { id: 'node3', combo: 'combo2', style: { x: 100, y: 200, labelText: '微服务3' } },\n      { id: 'node4', combo: 'combo2', style: { x: 200, y: 200, labelText: '微服务4' } },\n      { id: 'node5', combo: 'combo3', style: { x: 300, y: 100, labelText: '第三方登录' } },\n      { id: 'node6', combo: 'combo3', style: { x: 300, y: 150, labelText: '任务调度' } },\n      { id: 'node7', combo: 'combo3', style: { x: 300, y: 200, labelText: '消息服务' } },\n    ],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n    ],\n    combos: [\n      { id: 'combo1', style: { labelText: '服务层' } },\n      { id: 'combo2', combo: 'combo1', style: { labelText: '业务微服务' } },\n      { id: 'combo3', combo: 'combo1', style: { labelText: '集成模块' } },\n    ],\n  },\n  node: {\n    type: 'rect',\n  },\n  edge: {\n    style: {\n      endArrow: true,\n    },\n  },\n  combo: {\n    type: 'rect',\n    style: {\n      padding: 16,\n    },\n  },\n  behaviors: ['drag-element', 'collapse-expand'],\n  animation: true,\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/api/elements/combos/rect-combo.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n        { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n        { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n      ],\n      combos: [\n        { id: 'combo1', combo: 'combo2' },\n        { id: 'combo2', style: {} },\n      ],\n    },\n    node: { style: { fill: '#7e3feb' } },\n    combo: { type: 'rect' },\n    behaviors: ['collapse-expand'],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    gui.add({ type: 'rect' }, 'type').disable();\n  },\n);\n```\n\n设置 `combo.type` 为 `rect` 以使用矩形组合。\n"
  },
  {
    "path": "packages/site/common/api/elements/edges/base-edge.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [{ id: 'node1' }, { id: 'node2' }],\n      edges: [\n        {\n          id: 'edge1',\n          source: 'node1',\n          target: 'node2',\n          style: {\n            stroke: '#7e3feb',\n            lineWidth: 2,\n            labelText: 'node1 👉 node2',\n            labelBackground: true,\n            labelBackgroundFill: '#f9f0ff',\n            labelBackgroundOpacity: 1,\n            labelBackgroundLineWidth: 2,\n            labelBackgroundStroke: '#7e3feb',\n            labelPadding: [1, 10],\n            labelBackgroundRadius: 4,\n          },\n        },\n      ],\n    },\n    node: {\n      style: {\n        fill: '#f8f8f8',\n        stroke: '#8b9baf',\n        lineWidth: 1,\n        labelText: (d) => d.id,\n      },\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n    layout: { type: 'grid', cols: 2 },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 500 },\n  (gui, graph) => {\n    const options = {\n      lineWidth: 2,\n      opacity: 1,\n      stroke: '#7e3feb',\n\n      startArrow: false,\n      startArrowSize: 8,\n      startArrowType: 'triangle',\n\n      endArrow: false,\n      endArrowSize: 8,\n      endArrowType: 'triangle',\n\n      label: true,\n      labelAutoRotate: true,\n      labelMaxWidth: '80%',\n      labelOffsetX: 0,\n      labelOffsetY: 0,\n      labelPadding: 0,\n      labelPlacement: 'center',\n      labelText: 'node1 👉 node2',\n\n      labelBackground: true,\n      labelBackgroundFill: '#f9f0ff',\n      labelBackgroundStroke: '#7e3feb',\n      labelBackgroundLineDash: 0,\n      labelBackgroundLineWidth: 2,\n      labelBackgroundOpacity: 1,\n      labelBackgroundRadius: 4,\n\n      halo: false,\n      haloLineDash: 0,\n      haloLineWidth: 12,\n      haloStrokeOpacity: 0.25,\n    };\n    const optionFolder = gui.addFolder('edge.style');\n\n    optionFolder.add(options, 'lineWidth', 0, 20);\n    optionFolder.add(options, 'opacity', 0, 1);\n    optionFolder.addColor(options, 'stroke');\n\n    // startArrow\n    optionFolder.add(options, 'startArrow').onChange((v) => {\n      startArrowSize.show(v);\n      startArrowType.show(v);\n    });\n    const startArrowSize = optionFolder.add(options, 'startArrowSize', 0, 20).hide();\n    const startArrowType = optionFolder\n      .add(options, 'startArrowType', ['triangle', 'circle', 'diamond', 'vee', 'rect', 'triangleRect', 'simple'])\n      .hide();\n\n    // endArrow\n    optionFolder.add(options, 'endArrow').onChange((v) => {\n      endArrowSize.show(v);\n      endArrowType.show(v);\n    });\n    const endArrowSize = optionFolder.add(options, 'endArrowSize', 0, 20).hide();\n    const endArrowType = optionFolder\n      .add(options, 'endArrowType', ['triangle', 'circle', 'diamond', 'vee', 'rect', 'triangleRect', 'simple'])\n      .hide();\n\n    // label\n    optionFolder.add(options, 'label').onChange((v) => {\n      [labelAutoRotate, labelMaxWidth, labelOffsetX, labelOffsetY, labelPadding, labelPlacement, labelText].forEach(\n        (i) => i.show(v),\n      );\n    });\n    const labelAutoRotate = optionFolder.add(options, 'labelAutoRotate');\n    const labelMaxWidth = optionFolder.add(options, 'labelMaxWidth', ['80%', '20px', '200%']);\n    const labelOffsetX = optionFolder.add(options, 'labelOffsetX', 0, 50);\n    const labelOffsetY = optionFolder.add(options, 'labelOffsetY', 0, 50);\n    const labelPadding = optionFolder.add(options, 'labelPadding', 0, 20);\n    const labelPlacement = optionFolder.add(options, 'labelPlacement', ['start', 'center', 'end', 0.2, 0.8]);\n    const labelText = optionFolder.add(options, 'labelText');\n\n    const labelBackground = optionFolder.add(options, 'labelBackground').onChange((v) => {\n      [\n        labelBackgroundFill,\n        labelBackgroundStroke,\n        labelBackgroundLineDash,\n        labelBackgroundLineWidth,\n        labelBackgroundOpacity,\n        labelBackgroundRadius,\n      ].forEach((i) => i.show(v));\n    });\n    const labelBackgroundFill = optionFolder.addColor(options, 'labelBackgroundFill').hide();\n    const labelBackgroundStroke = optionFolder.addColor(options, 'labelBackgroundStroke').hide();\n    const labelBackgroundLineDash = optionFolder.add(options, 'labelBackgroundLineDash', 0, 10).hide();\n    const labelBackgroundLineWidth = optionFolder.add(options, 'labelBackgroundLineWidth', 0, 10).hide();\n    const labelBackgroundOpacity = optionFolder.add(options, 'labelBackgroundOpacity', 0, 1).hide();\n    const labelBackgroundRadius = optionFolder.add(options, 'labelBackgroundRadius', 0, 30).hide();\n\n    const halo = optionFolder.add(options, 'halo').onChange((v) => {\n      [haloStrokeOpacity, haloLineDash, haloLineWidth].forEach((i) => i.show(v));\n    });\n    const haloStrokeOpacity = optionFolder.addColor(options, 'haloStrokeOpacity', 0, 1).hide();\n    const haloLineDash = optionFolder.add(options, 'haloLineDash', 0, 10).hide();\n    const haloLineWidth = optionFolder.add(options, 'haloLineWidth', 0, 10).hide();\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateEdgeData([{ id: 'edge1', style: { [property]: value } }]);\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/elements/edges/cubic-horizontal.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: {\n      nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }, { id: 'node6' }],\n      edges: [\n        { source: 'node1', target: 'node2' },\n        { source: 'node1', target: 'node3' },\n        { source: 'node1', target: 'node4', text: 'cubic-horizontal' },\n        { source: 'node1', target: 'node5' },\n        { source: 'node1', target: 'node6' },\n      ],\n    },\n    node: {\n      style: {\n        fill: '#f8f8f8',\n        stroke: '#8b9baf',\n        lineWidth: 1,\n        port: true,\n        ports: [{ placement: 'left' }, { placement: 'right' }],\n      },\n    },\n    edge: {\n      type: 'cubic-horizontal',\n      style: {\n        stroke: '#7e3feb',\n        lineWidth: 2,\n        labelText: (d) => d.text,\n        labelBackground: true,\n        labelBackgroundFill: '#f9f0ff',\n        labelBackgroundOpacity: 1,\n        labelBackgroundLineWidth: 2,\n        labelBackgroundStroke: '#7e3feb',\n        labelPadding: [1, 10],\n        labelBackgroundRadius: 4,\n      },\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n    layout: {\n      type: 'antv-dagre',\n      rankdir: 'LR',\n      nodesep: 15,\n      ranksep: 100,\n    },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    gui.add({ type: 'cubic-horizontal' }, 'type').disable();\n\n    const options = {\n      curveOffset: 20,\n      curvePosition: 0.5,\n    };\n    const optionFolder = gui.addFolder('cubic-horizontal.style');\n    optionFolder.add(options, 'curveOffset', 0, 100);\n    optionFolder.add(options, 'curvePosition', 0, 1);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateEdgeData((prev) => prev.map((edge) => ({ ...edge, style: { [property]: value } })));\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `edge.type` 为 `cubic-horizontal` 以使用水平方向的三次贝塞尔曲线。\n"
  },
  {
    "path": "packages/site/common/api/elements/edges/cubic-vertical.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: {\n      nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }, { id: 'node6' }],\n      edges: [\n        { source: 'node1', target: 'node2' },\n        { source: 'node1', target: 'node3' },\n        { source: 'node1', target: 'node4', text: 'cubic-vertical' },\n        { source: 'node1', target: 'node5' },\n        { source: 'node1', target: 'node6' },\n      ],\n    },\n    node: {\n      style: {\n        fill: '#f8f8f8',\n        stroke: '#8b9baf',\n        lineWidth: 1,\n        port: true,\n        ports: [{ placement: 'top' }, { placement: 'bottom' }],\n      },\n    },\n    edge: {\n      type: 'cubic-vertical',\n      style: {\n        stroke: '#7e3feb',\n        lineWidth: 2,\n        labelText: (d) => d.text,\n        labelBackground: true,\n        labelBackgroundFill: '#f9f0ff',\n        labelBackgroundOpacity: 1,\n        labelBackgroundLineWidth: 2,\n        labelBackgroundStroke: '#7e3feb',\n        labelPadding: [1, 10],\n        labelBackgroundRadius: 4,\n      },\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n    layout: {\n      type: 'antv-dagre',\n      rankdir: 'TB',\n      nodesep: 25,\n      ranksep: 80,\n    },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    gui.add({ type: 'cubic-vertical' }, 'type').disable();\n\n    const options = {\n      curveOffset: 20,\n      curvePosition: 0.5,\n    };\n    const optionFolder = gui.addFolder('cubic-vertical.style');\n    optionFolder.add(options, 'curveOffset', 0, 100);\n    optionFolder.add(options, 'curvePosition', 0, 1);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateEdgeData((prev) => prev.map((edge) => ({ ...edge, style: { [property]: value } })));\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `edge.type` 为 `cubic-vertical` 以使用垂直方向的三次贝塞尔曲线。\n"
  },
  {
    "path": "packages/site/common/api/elements/edges/cubic.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [{ id: 'node1' }, { id: 'node2' }],\n      edges: [{ id: 'edge1', source: 'node1', target: 'node2', text: 'cubic' }],\n    },\n    node: {\n      style: {\n        fill: '#f8f8f8',\n        stroke: '#8b9baf',\n        lineWidth: 1,\n      },\n    },\n    edge: {\n      type: 'cubic',\n      style: {\n        stroke: '#7e3feb',\n        lineWidth: 2,\n        labelText: (d) => d.text,\n        labelBackground: true,\n        labelBackgroundFill: '#f9f0ff',\n        labelBackgroundOpacity: 1,\n        labelBackgroundLineWidth: 2,\n        labelBackgroundStroke: '#7e3feb',\n        labelPadding: [1, 10],\n        labelBackgroundRadius: 4,\n      },\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n    layout: { type: 'grid', cols: 2 },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    gui.add({ type: 'cubic' }, 'type').disable();\n\n    const options = {\n      curveOffset: 20,\n      curvePosition: 0.5,\n    };\n    const optionFolder = gui.addFolder('cubic.style');\n    optionFolder.add(options, 'curveOffset', 0, 100, 1);\n    optionFolder.add(options, 'curvePosition', 0, 1, 0.1);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateEdgeData([{ id: 'edge1', style: { [property]: value } }]);\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `edge.type` 为 `cubic` 以使用三次贝塞尔曲线。\n"
  },
  {
    "path": "packages/site/common/api/elements/edges/line.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [{ id: 'node1' }, { id: 'node2' }],\n      edges: [{ id: 'edge1', source: 'node1', target: 'node2', text: 'line' }],\n    },\n    node: {\n      style: {\n        fill: '#f8f8f8',\n        stroke: '#8b9baf',\n        lineWidth: 1,\n      },\n    },\n    edge: {\n      style: {\n        stroke: '#7e3feb',\n        lineWidth: 2,\n        labelText: (d) => d.text,\n        labelBackground: true,\n        labelBackgroundFill: '#f9f0ff',\n        labelBackgroundOpacity: 1,\n        labelBackgroundLineWidth: 2,\n        labelBackgroundStroke: '#7e3feb',\n        labelPadding: [1, 10],\n        labelBackgroundRadius: 4,\n      },\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n    layout: { type: 'grid', cols: 2 },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    gui.add({ type: 'line' }, 'type').disable();\n  },\n);\n```\n\n设置 `edge.type` 为 `line` 以使用直线。\n"
  },
  {
    "path": "packages/site/common/api/elements/edges/polyline.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        {\n          id: 'node1',\n          style: { x: 150, y: 150 },\n        },\n        {\n          id: 'node2',\n          style: {\n            x: 400,\n            y: 150,\n            labelText: 'Drag Me!',\n            labelPadding: [1, 5],\n            labelBackground: true,\n            labelBackgroundRadius: 10,\n            labelBackgroundFill: '#99add1',\n          },\n        },\n      ],\n      edges: [\n        {\n          id: 'edge1',\n          source: 'node1',\n          target: 'node2',\n          text: 'polyline',\n        },\n      ],\n    },\n    node: {\n      style: {\n        fill: '#f8f8f8',\n        stroke: '#8b9baf',\n        lineWidth: 1,\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        stroke: '#7e3feb',\n        lineWidth: 2,\n        labelText: (d) => d.text,\n        labelBackground: true,\n        labelBackgroundFill: '#f9f0ff',\n        labelBackgroundOpacity: 1,\n        labelBackgroundLineWidth: 2,\n        labelBackgroundStroke: '#7e3feb',\n        labelPadding: [1, 10],\n        labelBackgroundRadius: 4,\n        router: { type: 'orth' },\n      },\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    gui.add({ type: 'polyline' }, 'type').disable();\n\n    let index = 3;\n    const options = {\n      radius: 0,\n      router: {\n        type: 'orth',\n      },\n      random: () => {\n        const x = Math.floor(Math.random() * 600);\n        const y = Math.floor(Math.random() * 300);\n        graph.addNodeData([\n          {\n            id: `node-${index}`,\n            style: {\n              size: 5,\n              fill: '#7e3feb',\n              x,\n              y,\n            },\n          },\n        ]);\n        index++;\n        graph.updateEdgeData((prev) => {\n          const targetEdgeData = prev.find((edge) => edge.id === 'edge1');\n          const controlPoints = [...(targetEdgeData.style.controlPoints || [])];\n          controlPoints.push([x, y]);\n          return [{ ...targetEdgeData, style: { ...targetEdgeData.style, controlPoints } }];\n        });\n        graph.render();\n      },\n    };\n    const optionFolder = gui.addFolder('polyline.style');\n    optionFolder.add(options, 'radius', 0, 100, 1);\n    optionFolder.add(options, 'router');\n    optionFolder.add(options, 'random').name('Add random node as control points');\n\n    optionFolder.onChange(({ property, value }) => {\n      if (property === 'random') return;\n      graph.updateEdgeData([{ id: 'edge1', style: { [property]: value } }]);\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `edge.type` 为 `polyline` 以使用折线。\n"
  },
  {
    "path": "packages/site/common/api/elements/edges/quadratic.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [{ id: 'node1' }, { id: 'node2' }],\n      edges: [{ id: 'edge1', source: 'node1', target: 'node2', text: 'quadratic' }],\n    },\n    node: {\n      style: {\n        fill: '#f8f8f8',\n        stroke: '#8b9baf',\n        lineWidth: 1,\n      },\n    },\n    edge: {\n      type: 'quadratic',\n      style: {\n        stroke: '#7e3feb',\n        lineWidth: 2,\n        labelText: (d) => d.text,\n        labelBackground: true,\n        labelBackgroundFill: '#f9f0ff',\n        labelBackgroundOpacity: 1,\n        labelBackgroundLineWidth: 2,\n        labelBackgroundStroke: '#7e3feb',\n        labelPadding: [1, 10],\n        labelBackgroundRadius: 4,\n      },\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n    layout: { type: 'grid', cols: 2 },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    gui.add({ type: 'quadratic' }, 'type').disable();\n\n    const options = {\n      curveOffset: 30,\n      curvePosition: 0.5,\n    };\n    const optionFolder = gui.addFolder('quadratic.style');\n    optionFolder.add(options, 'curveOffset', 0, 100);\n    optionFolder.add(options, 'curvePosition', 0, 1);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateEdgeData([{ id: 'edge1', style: { [property]: value } }]);\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `edge.type` 为 `quadratic` 以使用曲线。\n"
  },
  {
    "path": "packages/site/common/api/elements/nodes/base-node.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: {\n      nodes: [\n        {\n          id: 'node1',\n          style: {\n            fill: '#7e3feb',\n            size: 40,\n            label: true,\n            labelText: 'node',\n            labelBackground: false,\n            icon: true,\n            iconSrc: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',\n            donuts: [30, 30, 20, 20],\n            donutPalette: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n            badge: true,\n            badges: [{ placement: 'top-right', text: 'Important', offsetX: -4 }],\n            port: true,\n            ports: [{ placement: 'left' }, { placement: 'right' }],\n            portFill: '#f9f0ff',\n            portR: 3,\n            portStroke: '#7e3feb',\n          },\n        },\n      ],\n    },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 600 },\n  (gui, graph) => {\n    const global = { type: 'circle' };\n    gui\n      .add(global, 'type', ['circle', 'diamond', 'donut', 'ellipse', 'hexagon', 'image', 'rect', 'star', 'triangle'])\n      .onChange((v) => {\n        graph.updateNodeData([{ id: 'node1', type: v }]);\n        graph.render();\n      });\n\n    const options = {\n      fill: '#7e3feb',\n      fillOpacity: 1,\n      lineWidth: 0,\n      'size[0]': 40,\n      'size[1]': 40,\n      stroke: '#000000',\n      strokeOpacity: 1,\n\n      label: true,\n      labelFill: '000000d9',\n      labelMaxWidth: '200%',\n      labelPadding: 0,\n      labelPlacement: 'bottom',\n      labelText: 'node',\n      labelWordWrap: false,\n      labelOpacity: 1,\n\n      labelBackground: true,\n      labelBackgroundFill: '#fff',\n      labelBackgroundLineDash: 0,\n      labelBackgroundLineWidth: 0,\n      labelBackgroundOpacity: 0.5,\n      labelBackgroundRadius: 0,\n      labelBackgroundStroke: '#fff',\n\n      halo: false,\n      haloLineDash: 0,\n      haloLineWidth: 12,\n      haloStrokeOpacity: 0.25,\n\n      icon: true,\n      iconFill: '#fff',\n      iconFontSize: 16,\n      iconOpacity: 1,\n      iconText: '',\n      iconSrc: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',\n      iconWidth: 20,\n      iconHeight: 20,\n\n      badge: true,\n      badgeFill: '000000d9',\n      badgeMaxWidth: '200%',\n      badgeOpacity: 1,\n      badgePadding: 0,\n      badgePlacement: 'top-right',\n      badgeText: 'Important',\n      badgeWordWrap: false,\n\n      badgeBackground: true,\n      badgeBackgroundFill: '#fff',\n      badgeBackgroundLineDash: 0,\n      badgeBackgroundLineWidth: 0,\n      badgeBackgroundOpacity: 0.5,\n      badgeBackgroundRadius: 0,\n      badgeBackgroundStroke: '#fff',\n\n      port: true,\n      portFill: '#f9f0ff',\n      portR: 3,\n      portStroke: '#7e3feb',\n    };\n    const optionFolder = gui.addFolder('node.style');\n\n    optionFolder.add(options, 'size[0]', 0, 100).name('width(size[0])');\n    optionFolder.add(options, 'size[1]', 0, 100).name('height(size[1])');\n    optionFolder.add(options, 'lineWidth', 0, 20);\n    optionFolder.addColor(options, 'fill');\n    optionFolder.add(options, 'fillOpacity', 0, 1);\n    optionFolder.addColor(options, 'stroke');\n    optionFolder.add(options, 'strokeOpacity', 0, 1);\n\n    optionFolder.add(options, 'label').onChange((v) => {\n      [labelFill, labelMaxWidth, labelWordWrap, labelPadding, labelPlacement, labelText, labelOpacity].forEach((i) =>\n        i.show(v),\n      );\n    });\n    const labelFill = optionFolder.addColor(options, 'labelFill').hide();\n    const labelMaxWidth = optionFolder.add(options, 'labelMaxWidth', ['200%', '20px', '80%']).hide();\n    const labelWordWrap = optionFolder.add(options, 'labelWordWrap').hide();\n    const labelPadding = optionFolder.add(options, 'labelPadding', 0, 20).hide();\n    const labelPlacement = optionFolder\n      .add(options, 'labelPlacement', [\n        'left',\n        'right',\n        'top',\n        'bottom',\n        'left-top',\n        'left-bottom',\n        'right-top',\n        'right-bottom',\n        'top-left',\n        'top-right',\n        'bottom-left',\n        'bottom-right',\n        'center',\n      ])\n      .hide();\n    const labelText = optionFolder.add(options, 'labelText').hide();\n    const labelOpacity = optionFolder.add(options, 'labelOpacity', 0, 1).hide();\n\n    const labelBackground = optionFolder.add(options, 'labelBackground').onChange((v) => {\n      [\n        labelBackgroundFill,\n        labelBackgroundStroke,\n        labelBackgroundLineDash,\n        labelBackgroundLineWidth,\n        labelBackgroundOpacity,\n        labelBackgroundRadius,\n      ].forEach((i) => i.show(v));\n    });\n    const labelBackgroundFill = optionFolder.addColor(options, 'labelBackgroundFill').hide();\n    const labelBackgroundStroke = optionFolder.addColor(options, 'labelBackgroundStroke').hide();\n    const labelBackgroundLineDash = optionFolder.add(options, 'labelBackgroundLineDash', 0, 10).hide();\n    const labelBackgroundLineWidth = optionFolder.add(options, 'labelBackgroundLineWidth', 0, 10).hide();\n    const labelBackgroundOpacity = optionFolder.add(options, 'labelBackgroundOpacity', 0, 1).hide();\n    const labelBackgroundRadius = optionFolder.add(options, 'labelBackgroundRadius', 0, 30).hide();\n\n    const halo = optionFolder.add(options, 'halo').onChange((v) => {\n      [haloStrokeOpacity, haloLineDash, haloLineWidth].forEach((i) => i.show(v));\n    });\n    const haloStrokeOpacity = optionFolder.add(options, 'haloStrokeOpacity', 0, 1).hide();\n    const haloLineDash = optionFolder.add(options, 'haloLineDash', 0, 10).hide();\n    const haloLineWidth = optionFolder.add(options, 'haloLineWidth', 0, 10).hide();\n\n    const icon = optionFolder.add(options, 'icon').onChange((v) => {\n      [iconSrc, iconText, iconFill, iconFontSize, iconOpacity, iconWidth, iconHeight].forEach((i) => i.show(v));\n    });\n    const iconSrc = optionFolder.add(options, 'iconSrc').hide();\n    const iconText = optionFolder.add(options, 'iconText').hide();\n    const iconFill = optionFolder.addColor(options, 'iconFill').hide();\n    const iconFontSize = optionFolder.add(options, 'iconFontSize', 12, 20, 1).hide();\n    const iconOpacity = optionFolder.add(options, 'iconOpacity', 0, 1).hide();\n    const iconWidth = optionFolder.add(options, 'iconWidth', 0, 100, 1).hide();\n    const iconHeight = optionFolder.add(options, 'iconHeight', 0, 100, 1).hide();\n\n    const badge = optionFolder.add(options, 'badge').onChange((v) => {\n      [badgeFill, badgeMaxWidth, badgeWordWrap, badgePadding, badgePlacement, badgeText, badgeOpacity].forEach((i) =>\n        i.show(v),\n      );\n    });\n    const badgeFill = optionFolder.addColor(options, 'badgeFill').hide();\n    const badgeMaxWidth = optionFolder.add(options, 'badgeMaxWidth', ['200%', '20px', '80%']).hide();\n    const badgeWordWrap = optionFolder.add(options, 'badgeWordWrap').hide();\n    const badgePadding = optionFolder.add(options, 'badgePadding', 0, 20).hide();\n    const badgePlacement = optionFolder\n      .add(options, 'badgePlacement', [\n        'left',\n        'right',\n        'top',\n        'bottom',\n        'left-top',\n        'left-bottom',\n        'right-top',\n        'right-bottom',\n        'top-left',\n        'top-right',\n        'bottom-left',\n        'bottom-right',\n      ])\n      .hide();\n    const badgeText = optionFolder.add(options, 'badgeText').hide();\n    const badgeOpacity = optionFolder.add(options, 'badgeOpacity', 0, 1).hide();\n\n    const badgeBackground = optionFolder.add(options, 'badgeBackground').onChange((v) => {\n      [\n        badgeBackgroundFill,\n        badgeBackgroundStroke,\n        badgeBackgroundLineDash,\n        badgeBackgroundLineWidth,\n        badgeBackgroundOpacity,\n        badgeBackgroundRadius,\n      ].forEach((i) => i.show(v));\n    });\n    const badgeBackgroundFill = optionFolder.addColor(options, 'badgeBackgroundFill').hide();\n    const badgeBackgroundStroke = optionFolder.addColor(options, 'badgeBackgroundStroke').hide();\n    const badgeBackgroundLineDash = optionFolder.add(options, 'badgeBackgroundLineDash', 0, 10).hide();\n    const badgeBackgroundLineWidth = optionFolder.add(options, 'badgeBackgroundLineWidth', 0, 10).hide();\n    const badgeBackgroundOpacity = optionFolder.add(options, 'badgeBackgroundOpacity', 0, 1).hide();\n    const badgeBackgroundRadius = optionFolder.add(options, 'badgeBackgroundRadius', 0, 30).hide();\n\n    const port = optionFolder.add(options, 'port').onChange((v) => {\n      [portR, portFill, portStroke].forEach((i) => i.show(v));\n    });\n    const portR = optionFolder.add(options, 'portR', 0, 20, 1).hide();\n    const portFill = optionFolder.addColor(options, 'portFill').hide();\n    const portStroke = optionFolder.addColor(options, 'portStroke').hide();\n\n    optionFolder.onChange(({ property, value, object }) => {\n      let updateStyle = { [property]: value };\n      if (['size[0]', 'size[1]'].includes(property)) {\n        updateStyle.size = [object['size[0]'], object['size[1]']];\n      } else if (['badgePlacement', 'badgeText'].includes(property)) {\n        const nodeData = graph.getNodeData('node1').badges;\n        updateStyle.badges = [{ text: object.badgeText, placement: object.badgePlacement }];\n      }\n      graph.updateNodeData([{ id: 'node1', style: updateStyle }]);\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/elements/nodes/circle.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: {\n      nodes: [{ id: 'node1', style: { size: 40, fill: '#7e3feb' } }],\n    },\n    node: {\n      type: 'circle',\n    },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 220 },\n  (gui, graph) => {\n    gui.add({ type: 'circle' }, 'type').disable();\n\n    const options = { size: 40 };\n    const optionFolder = gui.addFolder('circle.style');\n    optionFolder.add(options, 'size', 0, 100, 1);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateNodeData([{ id: 'node1', style: { [property]: value } }]);\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/elements/nodes/diamond.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: { nodes: [{ id: 'node1', style: { size: [48, 24], fill: '#7e3feb' } }] },\n    node: { type: 'diamond' },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 220 },\n  (gui, graph) => {\n    gui.add({ type: 'diamond' }, 'type').disable();\n\n    const options = { 'size[0]': 48, 'size[1]': 24 };\n\n    const optionFolder = gui.addFolder('diamond.style');\n    optionFolder.add(options, 'size[0]', 0, 100, 1);\n    optionFolder.add(options, 'size[1]', 0, 100, 1);\n\n    optionFolder.onChange(({ object }) => {\n      graph.updateNodeData([{ id: 'node1', style: { size: [object['size[0]'], object['size[1]']] } }]);\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `node.type` 为 `diamond` 以使用菱形节点。\n"
  },
  {
    "path": "packages/site/common/api/elements/nodes/donut.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: {\n      nodes: [\n        {\n          id: 'node1',\n          style: {\n            fill: 'transparent',\n            size: 60,\n            donuts: [30, 30, 20, 20],\n            donutPalette: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n          },\n        },\n      ],\n    },\n    node: { type: 'donut' },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 220 },\n  (gui, graph) => {\n    gui.add({ type: 'donut' }, 'type').disable();\n\n    const options = {\n      size: 60,\n      innerR: 50,\n      donutPalette: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n    };\n    const optionFolder = gui.addFolder('donut.style');\n    optionFolder.add(options, 'size', 0, 100, 1);\n    optionFolder.add(options, 'innerR', 0, 100, 1).name('innerR(%)');\n    optionFolder.add(options, 'donutPalette', ['spectral', 'tableau', ['#1783FF', '#00C9C9', '#F08F56', '#D580FF']]);\n\n    optionFolder.onChange(({ property, value }) => {\n      if (property === 'innerR') value = value + '%';\n      graph.updateNodeData([{ id: 'node1', style: { [property]: value } }]);\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `node.type` 为 `donut` 以使用甜甜圈节点。\n"
  },
  {
    "path": "packages/site/common/api/elements/nodes/ellipse.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: { nodes: [{ id: 'node1', style: { size: 40, fill: '#7e3feb' } }] },\n    node: { type: 'ellipse' },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 220 },\n  (gui, graph) => {\n    gui.add({ type: 'ellipse' }, 'type').disable();\n\n    const options = { 'size[0]': 80, 'size[1]': 40 };\n\n    const optionFolder = gui.addFolder('ellipse.style');\n    optionFolder.add(options, 'size[0]', 0, 100, 1);\n    optionFolder.add(options, 'size[1]', 0, 100, 1);\n\n    optionFolder.onChange(({ object }) => {\n      graph.updateNodeData([{ id: 'node1', style: { size: [object['size[0]'], object['size[1]']] } }]);\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `node.type` 为 `ellipse` 以使用椭圆形节点。\n"
  },
  {
    "path": "packages/site/common/api/elements/nodes/hexagon.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: { nodes: [{ id: 'node1', style: { size: 40, fill: '#7e3feb' } }] },\n    node: { type: 'hexagon' },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 220 },\n  (gui, graph) => {\n    gui.add({ type: 'hexagon' }, 'type').disable();\n\n    const options = {\n      size: 40,\n      outerR: 0,\n    };\n    const optionFolder = gui.addFolder('hexagon.style');\n    optionFolder.add(options, 'size', 0, 100, 1);\n    optionFolder.add(options, 'outerR', 0, 100);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateNodeData([{ id: 'node1', style: { [property]: value } }]);\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `node.type` 为 `hexagon` 以使用六边形节点。\n"
  },
  {
    "path": "packages/site/common/api/elements/nodes/html.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        {\n          id: 'node1',\n          style: {\n            x: 300,\n            y: 110,\n            size: [120, 40],\n            innerHTML: `\n<div style=\"width: 100%; height: 100%; background: #7e3feb; display: flex; justify-content: center; align-items: center;\">\n  <span style=\"color: #fff; font-size: 12px;\">\n    HTML Node\n  </span>\n</div>`,\n          },\n        },\n      ],\n    },\n    node: { type: 'html' },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 220 },\n  (gui, graph) => {\n    gui.add({ type: 'html' }, 'type').disable();\n\n    const options = {\n      size: 50,\n      innerHTML: `\n<div style=\"width: 100%; height: 100%; background: #7863FF; display: flex; justify-content: center; align-items: center;\">\n  <span style=\"color: #fff; font-size: 20px;\">\n    'HTML Node'\n  </span>\n</div>`,\n    };\n    const optionFolder = gui.addFolder('html.style');\n    optionFolder.add(options, 'size', 0, 100, 1);\n    optionFolder.add(options, 'innerHTML');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateNodeData([{ id: 'node1', style: { [property]: value } }]);\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `node.type` 为 `html` 以使用 HTML 节点。\n"
  },
  {
    "path": "packages/site/common/api/elements/nodes/image.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: {\n      nodes: [\n        {\n          id: 'node1',\n          style: {\n            size: 60,\n            src: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*N4ZMS7gHsUIAAAAAAAAAAABkARQnAQ',\n          },\n        },\n      ],\n    },\n    node: { type: 'image' },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 220 },\n  (gui, graph) => {\n    gui.add({ type: 'image' }, 'type').disable();\n\n    const options = {\n      size: 60,\n      src: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*N4ZMS7gHsUIAAAAAAAAAAABkARQnAQ',\n    };\n    const optionFolder = gui.addFolder('image.style');\n    optionFolder.add(options, 'size', 0, 100, 1);\n    optionFolder.add(options, 'src');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateNodeData([{ id: 'node1', style: { [property]: value } }]);\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `node.type` 为 `image` 以使用图片节点。\n"
  },
  {
    "path": "packages/site/common/api/elements/nodes/rect.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: { nodes: [{ id: 'node1', style: { size: 40, fill: '#7e3feb' } }] },\n    node: { type: 'rect' },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 220 },\n  (gui, graph) => {\n    gui.add({ type: 'rect' }, 'type').disable();\n\n    const options = { 'size[0]': 48, 'size[1]': 24 };\n\n    const optionFolder = gui.addFolder('rect.style');\n    optionFolder.add(options, 'size[0]', 0, 100, 1);\n    optionFolder.add(options, 'size[1]', 0, 100, 1);\n\n    optionFolder.onChange(({ object }) => {\n      graph.updateNodeData([{ id: 'node1', style: { size: [object['size[0]'], object['size[1]']] } }]);\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `node.type` 为 `rect` 以使用矩形节点。\n"
  },
  {
    "path": "packages/site/common/api/elements/nodes/star.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: { nodes: [{ id: 'node1', style: { size: 40, fill: '#7e3feb' } }] },\n    node: { type: 'star' },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 220 },\n  (gui, graph) => {\n    gui.add({ type: 'star' }, 'type').disable();\n\n    const options = {\n      size: 40,\n      innerR: 0,\n    };\n    const optionFolder = gui.addFolder('star.style');\n    optionFolder.add(options, 'size', 0, 100, 1);\n    optionFolder.add(options, 'innerR', 0, 100);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateNodeData([{ id: 'node1', style: { [property]: value } }]);\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `node.type` 为 `star` 以使用星形节点。\n"
  },
  {
    "path": "packages/site/common/api/elements/nodes/triangle.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: { nodes: [{ id: 'node1', style: { size: 40, fill: '#7e3feb' } }] },\n    node: { type: 'triangle' },\n    plugins: [{ type: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 220 },\n  (gui, graph) => {\n    gui.add({ type: 'triangle' }, 'type').disable();\n\n    const options = {\n      size: 40,\n      direction: 'up',\n    };\n    const optionFolder = gui.addFolder('triangle.style');\n    optionFolder.add(options, 'size', 0, 100, 1);\n    optionFolder.add(options, 'direction', ['up', 'left', 'right', 'down']);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateNodeData([{ id: 'node1', style: { [property]: value } }]);\n      graph.render();\n    });\n  },\n);\n```\n\n设置 `node.type` 为 `triangle` 以使用三角形节点。\n"
  },
  {
    "path": "packages/site/common/api/layout/fishbone.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        {\n          id: 'Quality',\n          depth: 0,\n          children: ['Machine', 'Method', 'Material', 'Man Power', 'Measurement', 'Milieu'],\n        },\n        {\n          id: 'Machine',\n          depth: 1,\n          children: ['Mill', 'Mixer', 'Metal Lathe'],\n        },\n        {\n          id: 'Mill',\n          depth: 2,\n        },\n        {\n          id: 'Mixer',\n          depth: 2,\n        },\n        {\n          id: 'Metal Lathe',\n          depth: 2,\n          children: ['Milling'],\n        },\n        {\n          id: 'Milling',\n          depth: 3,\n        },\n        {\n          id: 'Method',\n          depth: 1,\n        },\n        {\n          id: 'Material',\n          depth: 1,\n          children: ['Masonite', 'Marscapone', 'Meat'],\n        },\n        {\n          id: 'Masonite',\n          depth: 2,\n          children: ['spearMint', 'pepperMint', 'test1'],\n        },\n        {\n          id: 'spearMint',\n          depth: 3,\n        },\n        {\n          id: 'pepperMint',\n          depth: 3,\n          children: ['test3'],\n        },\n        {\n          id: 'test3',\n          depth: 4,\n        },\n        {\n          id: 'test1',\n          depth: 3,\n          children: ['test4'],\n        },\n        {\n          id: 'test4',\n          depth: 4,\n        },\n        {\n          id: 'Marscapone',\n          depth: 2,\n          children: ['Malty', 'Minty'],\n        },\n        {\n          id: 'Malty',\n          depth: 3,\n        },\n        {\n          id: 'Minty',\n          depth: 3,\n        },\n        {\n          id: 'Meat',\n          depth: 2,\n          children: ['Mutton'],\n        },\n        {\n          id: 'Mutton',\n          depth: 3,\n        },\n        {\n          id: 'Man Power',\n          depth: 1,\n          children: ['Manager', \"Master's Student\", 'Magician', 'Miner', 'Magister', 'Massage Artist'],\n        },\n        {\n          id: 'Manager',\n          depth: 2,\n        },\n        {\n          id: \"Master's Student\",\n          depth: 2,\n        },\n        {\n          id: 'Magician',\n          depth: 2,\n        },\n        {\n          id: 'Miner',\n          depth: 2,\n        },\n        {\n          id: 'Magister',\n          depth: 2,\n          children: ['Malpractice'],\n        },\n        {\n          id: 'Malpractice',\n          depth: 3,\n        },\n        {\n          id: 'Massage Artist',\n          depth: 2,\n          children: ['Masseur', 'Masseuse'],\n        },\n        {\n          id: 'Masseur',\n          depth: 3,\n        },\n        {\n          id: 'Masseuse',\n          depth: 3,\n        },\n        {\n          id: 'Measurement',\n          depth: 1,\n          children: ['Malleability'],\n        },\n        {\n          id: 'Malleability',\n          depth: 2,\n        },\n        {\n          id: 'Milieu',\n          depth: 1,\n          children: ['Marine'],\n        },\n        {\n          id: 'Marine',\n          depth: 2,\n        },\n      ],\n      edges: [\n        {\n          source: 'Quality',\n          target: 'Machine',\n        },\n        {\n          source: 'Quality',\n          target: 'Method',\n        },\n        {\n          source: 'Quality',\n          target: 'Material',\n        },\n        {\n          source: 'Quality',\n          target: 'Man Power',\n        },\n        {\n          source: 'Quality',\n          target: 'Measurement',\n        },\n        {\n          source: 'Quality',\n          target: 'Milieu',\n        },\n        {\n          source: 'Machine',\n          target: 'Mill',\n        },\n        {\n          source: 'Machine',\n          target: 'Mixer',\n        },\n        {\n          source: 'Machine',\n          target: 'Metal Lathe',\n        },\n        {\n          source: 'Metal Lathe',\n          target: 'Milling',\n        },\n        {\n          source: 'Material',\n          target: 'Masonite',\n        },\n        {\n          source: 'Material',\n          target: 'Marscapone',\n        },\n        {\n          source: 'Material',\n          target: 'Meat',\n        },\n        {\n          source: 'Masonite',\n          target: 'spearMint',\n        },\n        {\n          source: 'Masonite',\n          target: 'pepperMint',\n        },\n        {\n          source: 'Masonite',\n          target: 'test1',\n        },\n        {\n          source: 'pepperMint',\n          target: 'test3',\n        },\n        {\n          source: 'test1',\n          target: 'test4',\n        },\n        {\n          source: 'Marscapone',\n          target: 'Malty',\n        },\n        {\n          source: 'Marscapone',\n          target: 'Minty',\n        },\n        {\n          source: 'Meat',\n          target: 'Mutton',\n        },\n        {\n          source: 'Man Power',\n          target: 'Manager',\n        },\n        {\n          source: 'Man Power',\n          target: \"Master's Student\",\n        },\n        {\n          source: 'Man Power',\n          target: 'Magician',\n        },\n        {\n          source: 'Man Power',\n          target: 'Miner',\n        },\n        {\n          source: 'Man Power',\n          target: 'Magister',\n        },\n        {\n          source: 'Man Power',\n          target: 'Massage Artist',\n        },\n        {\n          source: 'Magister',\n          target: 'Malpractice',\n        },\n        {\n          source: 'Massage Artist',\n          target: 'Masseur',\n        },\n        {\n          source: 'Massage Artist',\n          target: 'Masseuse',\n        },\n        {\n          source: 'Measurement',\n          target: 'Malleability',\n        },\n        {\n          source: 'Milieu',\n          target: 'Marine',\n        },\n      ],\n    },\n    node: {\n      type: 'rect',\n      style: {\n        size: [32, 32],\n        // fill: () => randomColor(),\n        label: false,\n        labelFill: '#262626',\n        labelFontFamily: 'Gill Sans',\n        labelMaxLines: 2,\n        labelMaxWidth: '100%',\n        labelPlacement: 'center',\n        labelText: (d) => d.id,\n        labelWordWrap: true,\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        lineWidth: 3,\n      },\n    },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n    autoFit: 'view',\n    layout: {\n      type: 'fishbone',\n      direction: 'RL',\n      hGap: 50,\n      vGap: 50,\n      getRibSep: () => 60,\n    },\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const options = {\n      type: 'fishbone',\n      direction: 'RL',\n      hGap: 50,\n      vGap: 50,\n      getRibSep: 60,\n    };\n\n    const optionFolder = gui.addFolder('Fishbone Layout Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'direction', ['RL', 'LR']);\n    optionFolder.add(options, 'hGap', 20, 100, 10);\n    optionFolder.add(options, 'vGap', 20, 100, 10);\n    optionFolder.add(options, 'getRibSep', 30, 100, 10);\n\n    optionFolder.onChange(async ({ property, value }) => {\n      graph.setLayout(\n        Object.assign({}, graph.getLayout(), {\n          [property]: property === 'getRibSep' ? () => value : value,\n        }),\n      );\n      await graph.layout();\n      // 调整 direction 后部分node可能会溢出屏幕，重新执行下fitView\n      if (property === 'direction') {\n        graph.fitView();\n      }\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/layout/force-atlas2.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'Myriel' },\n        { id: 'Napoleon' },\n        { id: 'Mlle.Baptistine' },\n        { id: 'Mme.Magloire' },\n        { id: 'CountessdeLo' },\n        { id: 'Geborand' },\n        { id: 'Champtercier' },\n        { id: 'Cravatte' },\n        { id: 'Count' },\n        { id: 'OldMan' },\n        { id: 'Labarre' },\n        { id: 'Valjean' },\n        { id: 'Marguerite' },\n        { id: 'Mme.deR' },\n        { id: 'Isabeau' },\n        { id: 'Gervais' },\n        { id: 'Tholomyes' },\n        { id: 'Listolier' },\n        { id: 'Fameuil' },\n        { id: 'Blacheville' },\n        { id: 'Favourite' },\n        { id: 'Dahlia' },\n        { id: 'Zephine' },\n        { id: 'Fantine' },\n        { id: 'Mme.Thenardier' },\n        { id: 'Thenardier' },\n        { id: 'Cosette' },\n        { id: 'Javert' },\n        { id: 'Fauchelevent' },\n        { id: 'Bamatabois' },\n        { id: 'Perpetue' },\n        { id: 'Simplice' },\n        { id: 'Scaufflaire' },\n        { id: 'Woman1' },\n        { id: 'Judge' },\n        { id: 'Champmathieu' },\n        { id: 'Brevet' },\n        { id: 'Chenildieu' },\n        { id: 'Cochepaille' },\n        { id: 'Pontmercy' },\n        { id: 'Boulatruelle' },\n        { id: 'Eponine' },\n        { id: 'Anzelma' },\n        { id: 'Woman2' },\n        { id: 'MotherInnocent' },\n        { id: 'Gribier' },\n        { id: 'Jondrette' },\n        { id: 'Mme.Burgon' },\n        { id: 'Gavroche' },\n        { id: 'Gillenormand' },\n        { id: 'Magnon' },\n        { id: 'Mlle.Gillenormand' },\n        { id: 'Mme.Pontmercy' },\n        { id: 'Mlle.Vaubois' },\n        { id: 'Lt.Gillenormand' },\n        { id: 'Marius' },\n        { id: 'BaronessT' },\n        { id: 'Mabeuf' },\n        { id: 'Enjolras' },\n        { id: 'Combeferre' },\n        { id: 'Prouvaire' },\n        { id: 'Feuilly' },\n        { id: 'Courfeyrac' },\n        { id: 'Bahorel' },\n        { id: 'Bossuet' },\n        { id: 'Joly' },\n        { id: 'Grantaire' },\n        { id: 'MotherPlutarch' },\n        { id: 'Gueulemer' },\n        { id: 'Babet' },\n        { id: 'Claquesous' },\n        { id: 'Montparnasse' },\n        { id: 'Toussaint' },\n        { id: 'Child1' },\n        { id: 'Child2' },\n        { id: 'Brujon' },\n        { id: 'Mme.Hucheloup' },\n      ],\n      edges: [\n        { source: 'Napoleon', target: 'Myriel', value: 1 },\n        { source: 'Mlle.Baptistine', target: 'Myriel', value: 8 },\n        { source: 'Mme.Magloire', target: 'Myriel', value: 10 },\n        { source: 'Mme.Magloire', target: 'Mlle.Baptistine', value: 6 },\n        { source: 'CountessdeLo', target: 'Myriel', value: 1 },\n        { source: 'Geborand', target: 'Myriel', value: 1 },\n        { source: 'Champtercier', target: 'Myriel', value: 1 },\n        { source: 'Cravatte', target: 'Myriel', value: 1 },\n        { source: 'Count', target: 'Myriel', value: 2 },\n        { source: 'OldMan', target: 'Myriel', value: 1 },\n        { source: 'Valjean', target: 'Labarre', value: 1 },\n        { source: 'Valjean', target: 'Mme.Magloire', value: 3 },\n        { source: 'Valjean', target: 'Mlle.Baptistine', value: 3 },\n        { source: 'Valjean', target: 'Myriel', value: 5 },\n        { source: 'Marguerite', target: 'Valjean', value: 1 },\n        { source: 'Mme.deR', target: 'Valjean', value: 1 },\n        { source: 'Isabeau', target: 'Valjean', value: 1 },\n        { source: 'Gervais', target: 'Valjean', value: 1 },\n        { source: 'Listolier', target: 'Tholomyes', value: 4 },\n        { source: 'Fameuil', target: 'Tholomyes', value: 4 },\n        { source: 'Fameuil', target: 'Listolier', value: 4 },\n        { source: 'Blacheville', target: 'Tholomyes', value: 4 },\n        { source: 'Blacheville', target: 'Listolier', value: 4 },\n        { source: 'Blacheville', target: 'Fameuil', value: 4 },\n        { source: 'Favourite', target: 'Tholomyes', value: 3 },\n        { source: 'Favourite', target: 'Listolier', value: 3 },\n        { source: 'Favourite', target: 'Fameuil', value: 3 },\n        { source: 'Favourite', target: 'Blacheville', value: 4 },\n        { source: 'Dahlia', target: 'Tholomyes', value: 3 },\n        { source: 'Dahlia', target: 'Listolier', value: 3 },\n        { source: 'Dahlia', target: 'Fameuil', value: 3 },\n        { source: 'Dahlia', target: 'Blacheville', value: 3 },\n        { source: 'Dahlia', target: 'Favourite', value: 5 },\n        { source: 'Zephine', target: 'Tholomyes', value: 3 },\n        { source: 'Zephine', target: 'Listolier', value: 3 },\n        { source: 'Zephine', target: 'Fameuil', value: 3 },\n        { source: 'Zephine', target: 'Blacheville', value: 3 },\n        { source: 'Zephine', target: 'Favourite', value: 4 },\n        { source: 'Zephine', target: 'Dahlia', value: 4 },\n        { source: 'Fantine', target: 'Tholomyes', value: 3 },\n        { source: 'Fantine', target: 'Listolier', value: 3 },\n        { source: 'Fantine', target: 'Fameuil', value: 3 },\n        { source: 'Fantine', target: 'Blacheville', value: 3 },\n        { source: 'Fantine', target: 'Favourite', value: 4 },\n        { source: 'Fantine', target: 'Dahlia', value: 4 },\n        { source: 'Fantine', target: 'Zephine', value: 4 },\n        { source: 'Fantine', target: 'Marguerite', value: 2 },\n        { source: 'Fantine', target: 'Valjean', value: 9 },\n        { source: 'Mme.Thenardier', target: 'Fantine', value: 2 },\n        { source: 'Mme.Thenardier', target: 'Valjean', value: 7 },\n        { source: 'Thenardier', target: 'Mme.Thenardier', value: 13 },\n        { source: 'Thenardier', target: 'Fantine', value: 1 },\n        { source: 'Thenardier', target: 'Valjean', value: 12 },\n        { source: 'Cosette', target: 'Mme.Thenardier', value: 4 },\n        { source: 'Cosette', target: 'Valjean', value: 31 },\n        { source: 'Cosette', target: 'Tholomyes', value: 1 },\n        { source: 'Cosette', target: 'Thenardier', value: 1 },\n        { source: 'Javert', target: 'Valjean', value: 17 },\n        { source: 'Javert', target: 'Fantine', value: 5 },\n        { source: 'Javert', target: 'Thenardier', value: 5 },\n        { source: 'Javert', target: 'Mme.Thenardier', value: 1 },\n        { source: 'Javert', target: 'Cosette', value: 1 },\n        { source: 'Fauchelevent', target: 'Valjean', value: 8 },\n        { source: 'Fauchelevent', target: 'Javert', value: 1 },\n        { source: 'Bamatabois', target: 'Fantine', value: 1 },\n        { source: 'Bamatabois', target: 'Javert', value: 1 },\n        { source: 'Bamatabois', target: 'Valjean', value: 2 },\n        { source: 'Perpetue', target: 'Fantine', value: 1 },\n        { source: 'Simplice', target: 'Perpetue', value: 2 },\n        { source: 'Simplice', target: 'Valjean', value: 3 },\n        { source: 'Simplice', target: 'Fantine', value: 2 },\n        { source: 'Simplice', target: 'Javert', value: 1 },\n        { source: 'Scaufflaire', target: 'Valjean', value: 1 },\n        { source: 'Woman1', target: 'Valjean', value: 2 },\n        { source: 'Woman1', target: 'Javert', value: 1 },\n        { source: 'Judge', target: 'Valjean', value: 3 },\n        { source: 'Judge', target: 'Bamatabois', value: 2 },\n        { source: 'Champmathieu', target: 'Valjean', value: 3 },\n        { source: 'Champmathieu', target: 'Judge', value: 3 },\n        { source: 'Champmathieu', target: 'Bamatabois', value: 2 },\n        { source: 'Brevet', target: 'Judge', value: 2 },\n        { source: 'Brevet', target: 'Champmathieu', value: 2 },\n        { source: 'Brevet', target: 'Valjean', value: 2 },\n        { source: 'Brevet', target: 'Bamatabois', value: 1 },\n        { source: 'Chenildieu', target: 'Judge', value: 2 },\n        { source: 'Chenildieu', target: 'Champmathieu', value: 2 },\n        { source: 'Chenildieu', target: 'Brevet', value: 2 },\n        { source: 'Chenildieu', target: 'Valjean', value: 2 },\n        { source: 'Chenildieu', target: 'Bamatabois', value: 1 },\n        { source: 'Cochepaille', target: 'Judge', value: 2 },\n        { source: 'Cochepaille', target: 'Champmathieu', value: 2 },\n        { source: 'Cochepaille', target: 'Brevet', value: 2 },\n        { source: 'Cochepaille', target: 'Chenildieu', value: 2 },\n        { source: 'Cochepaille', target: 'Valjean', value: 2 },\n        { source: 'Cochepaille', target: 'Bamatabois', value: 1 },\n        { source: 'Pontmercy', target: 'Thenardier', value: 1 },\n        { source: 'Boulatruelle', target: 'Thenardier', value: 1 },\n        { source: 'Eponine', target: 'Mme.Thenardier', value: 5 },\n        { source: 'Eponine', target: 'Thenardier', value: 1 },\n        { source: 'Anzelma', target: 'Eponine', value: 1 },\n        { source: 'Anzelma', target: 'Thenardier', value: 1 },\n        { source: 'Anzelma', target: 'Mme.Thenardier', value: 1 },\n        { source: 'Woman2', target: 'Valjean', value: 3 },\n        { source: 'Woman2', target: 'Cosette', value: 1 },\n        { source: 'Woman2', target: 'Javert', value: 1 },\n        { source: 'MotherInnocent', target: 'Fauchelevent', value: 3 },\n        { source: 'MotherInnocent', target: 'Valjean', value: 1 },\n        { source: 'Gribier', target: 'Fauchelevent', value: 2 },\n        { source: 'Mme.Burgon', target: 'Jondrette', value: 1 },\n        { source: 'Jondrette', target: 'Mme.Burgon', value: 2 },\n        { source: 'Jondrette', target: 'Valjean', value: 1 },\n        { source: 'Gavroche', target: 'Mme.Burgon', value: 2 },\n        { source: 'Gavroche', target: 'Thenardier', value: 1 },\n        { source: 'Gavroche', target: 'Javert', value: 1 },\n        { source: 'Gavroche', target: 'Valjean', value: 2 },\n        { source: 'Gillenormand', target: 'Cosette', value: 3 },\n        { source: 'Gillenormand', target: 'Valjean', value: 2 },\n        { source: 'Magnon', target: 'Gillenormand', value: 1 },\n        { source: 'Magnon', target: 'Mme.Thenardier', value: 1 },\n        { source: 'Mlle.Gillenormand', target: 'Gillenormand', value: 9 },\n        { source: 'Mlle.Gillenormand', target: 'Cosette', value: 2 },\n        { source: 'Mlle.Gillenormand', target: 'Valjean', value: 2 },\n        { source: 'Mme.Pontmercy', target: 'Mlle.Gillenormand', value: 1 },\n        { source: 'Mme.Pontmercy', target: 'Pontmercy', value: 1 },\n        { source: 'Mlle.Vaubois', target: 'Mlle.Gillenormand', value: 1 },\n        { source: 'Lt.Gillenormand', target: 'Mlle.Gillenormand', value: 2 },\n        { source: 'Lt.Gillenormand', target: 'Gillenormand', value: 1 },\n        { source: 'Lt.Gillenormand', target: 'Cosette', value: 1 },\n        { source: 'Marius', target: 'Mlle.Gillenormand', value: 6 },\n        { source: 'Marius', target: 'Gillenormand', value: 12 },\n        { source: 'Marius', target: 'Pontmercy', value: 1 },\n        { source: 'Marius', target: 'Lt.Gillenormand', value: 1 },\n        { source: 'Marius', target: 'Cosette', value: 21 },\n        { source: 'Marius', target: 'Valjean', value: 19 },\n        { source: 'Marius', target: 'Tholomyes', value: 1 },\n        { source: 'Marius', target: 'Thenardier', value: 2 },\n        { source: 'Marius', target: 'Eponine', value: 5 },\n        { source: 'Marius', target: 'Gavroche', value: 4 },\n        { source: 'BaronessT', target: 'Gillenormand', value: 1 },\n        { source: 'BaronessT', target: 'Marius', value: 1 },\n        { source: 'Mabeuf', target: 'Marius', value: 1 },\n        { source: 'Mabeuf', target: 'Eponine', value: 1 },\n        { source: 'Mabeuf', target: 'Gavroche', value: 1 },\n        { source: 'Enjolras', target: 'Marius', value: 7 },\n        { source: 'Enjolras', target: 'Gavroche', value: 7 },\n        { source: 'Enjolras', target: 'Javert', value: 6 },\n        { source: 'Enjolras', target: 'Mabeuf', value: 1 },\n        { source: 'Enjolras', target: 'Valjean', value: 4 },\n        { source: 'Combeferre', target: 'Enjolras', value: 15 },\n        { source: 'Combeferre', target: 'Marius', value: 5 },\n        { source: 'Combeferre', target: 'Gavroche', value: 6 },\n        { source: 'Combeferre', target: 'Mabeuf', value: 2 },\n        { source: 'Prouvaire', target: 'Gavroche', value: 1 },\n        { source: 'Prouvaire', target: 'Enjolras', value: 4 },\n        { source: 'Prouvaire', target: 'Combeferre', value: 2 },\n        { source: 'Feuilly', target: 'Gavroche', value: 2 },\n        { source: 'Feuilly', target: 'Enjolras', value: 6 },\n        { source: 'Feuilly', target: 'Prouvaire', value: 2 },\n        { source: 'Feuilly', target: 'Combeferre', value: 5 },\n        { source: 'Feuilly', target: 'Mabeuf', value: 1 },\n        { source: 'Feuilly', target: 'Marius', value: 1 },\n        { source: 'Courfeyrac', target: 'Marius', value: 9 },\n        { source: 'Courfeyrac', target: 'Enjolras', value: 17 },\n        { source: 'Courfeyrac', target: 'Combeferre', value: 13 },\n        { source: 'Courfeyrac', target: 'Gavroche', value: 7 },\n        { source: 'Courfeyrac', target: 'Mabeuf', value: 2 },\n        { source: 'Courfeyrac', target: 'Eponine', value: 1 },\n        { source: 'Courfeyrac', target: 'Feuilly', value: 6 },\n        { source: 'Courfeyrac', target: 'Prouvaire', value: 3 },\n        { source: 'Bahorel', target: 'Combeferre', value: 5 },\n        { source: 'Bahorel', target: 'Gavroche', value: 5 },\n        { source: 'Bahorel', target: 'Courfeyrac', value: 6 },\n        { source: 'Bahorel', target: 'Mabeuf', value: 2 },\n        { source: 'Bahorel', target: 'Enjolras', value: 4 },\n        { source: 'Bahorel', target: 'Feuilly', value: 3 },\n        { source: 'Bahorel', target: 'Prouvaire', value: 2 },\n        { source: 'Bahorel', target: 'Marius', value: 1 },\n        { source: 'Bossuet', target: 'Marius', value: 5 },\n        { source: 'Bossuet', target: 'Courfeyrac', value: 12 },\n        { source: 'Bossuet', target: 'Gavroche', value: 5 },\n        { source: 'Bossuet', target: 'Bahorel', value: 4 },\n        { source: 'Bossuet', target: 'Enjolras', value: 10 },\n        { source: 'Bossuet', target: 'Feuilly', value: 6 },\n        { source: 'Bossuet', target: 'Prouvaire', value: 2 },\n        { source: 'Bossuet', target: 'Combeferre', value: 9 },\n        { source: 'Bossuet', target: 'Mabeuf', value: 1 },\n        { source: 'Bossuet', target: 'Valjean', value: 1 },\n        { source: 'Joly', target: 'Bahorel', value: 5 },\n        { source: 'Joly', target: 'Bossuet', value: 7 },\n        { source: 'Joly', target: 'Gavroche', value: 3 },\n        { source: 'Joly', target: 'Courfeyrac', value: 5 },\n        { source: 'Joly', target: 'Enjolras', value: 5 },\n        { source: 'Joly', target: 'Feuilly', value: 5 },\n        { source: 'Joly', target: 'Prouvaire', value: 2 },\n        { source: 'Joly', target: 'Combeferre', value: 5 },\n        { source: 'Joly', target: 'Mabeuf', value: 1 },\n        { source: 'Joly', target: 'Marius', value: 2 },\n        { source: 'Grantaire', target: 'Bossuet', value: 3 },\n        { source: 'Grantaire', target: 'Enjolras', value: 3 },\n        { source: 'Grantaire', target: 'Combeferre', value: 1 },\n        { source: 'Grantaire', target: 'Courfeyrac', value: 2 },\n        { source: 'Grantaire', target: 'Joly', value: 2 },\n        { source: 'Grantaire', target: 'Gavroche', value: 1 },\n        { source: 'Grantaire', target: 'Bahorel', value: 1 },\n        { source: 'Grantaire', target: 'Feuilly', value: 1 },\n        { source: 'Grantaire', target: 'Prouvaire', value: 1 },\n        { source: 'MotherPlutarch', target: 'Mabeuf', value: 3 },\n        { source: 'Gueulemer', target: 'Thenardier', value: 5 },\n        { source: 'Gueulemer', target: 'Valjean', value: 1 },\n        { source: 'Gueulemer', target: 'Mme.Thenardier', value: 1 },\n        { source: 'Gueulemer', target: 'Javert', value: 1 },\n        { source: 'Gueulemer', target: 'Gavroche', value: 1 },\n        { source: 'Gueulemer', target: 'Eponine', value: 1 },\n        { source: 'Babet', target: 'Thenardier', value: 6 },\n        { source: 'Babet', target: 'Gueulemer', value: 6 },\n        { source: 'Babet', target: 'Valjean', value: 1 },\n        { source: 'Babet', target: 'Mme.Thenardier', value: 1 },\n        { source: 'Babet', target: 'Javert', value: 2 },\n        { source: 'Babet', target: 'Gavroche', value: 1 },\n        { source: 'Babet', target: 'Eponine', value: 1 },\n        { source: 'Claquesous', target: 'Thenardier', value: 4 },\n        { source: 'Claquesous', target: 'Babet', value: 4 },\n        { source: 'Claquesous', target: 'Gueulemer', value: 4 },\n        { source: 'Claquesous', target: 'Valjean', value: 1 },\n        { source: 'Claquesous', target: 'Mme.Thenardier', value: 1 },\n        { source: 'Claquesous', target: 'Javert', value: 1 },\n        { source: 'Claquesous', target: 'Eponine', value: 1 },\n        { source: 'Claquesous', target: 'Enjolras', value: 1 },\n        { source: 'Montparnasse', target: 'Javert', value: 1 },\n        { source: 'Montparnasse', target: 'Babet', value: 2 },\n        { source: 'Montparnasse', target: 'Gueulemer', value: 2 },\n        { source: 'Montparnasse', target: 'Claquesous', value: 2 },\n        { source: 'Montparnasse', target: 'Valjean', value: 1 },\n        { source: 'Montparnasse', target: 'Gavroche', value: 1 },\n        { source: 'Montparnasse', target: 'Eponine', value: 1 },\n        { source: 'Montparnasse', target: 'Thenardier', value: 1 },\n        { source: 'Toussaint', target: 'Cosette', value: 2 },\n        { source: 'Toussaint', target: 'Javert', value: 1 },\n        { source: 'Toussaint', target: 'Valjean', value: 1 },\n        { source: 'Child1', target: 'Gavroche', value: 2 },\n        { source: 'Child2', target: 'Gavroche', value: 2 },\n        { source: 'Child2', target: 'Child1', value: 3 },\n        { source: 'Brujon', target: 'Babet', value: 3 },\n        { source: 'Brujon', target: 'Gueulemer', value: 3 },\n        { source: 'Brujon', target: 'Thenardier', value: 3 },\n        { source: 'Brujon', target: 'Gavroche', value: 1 },\n        { source: 'Brujon', target: 'Eponine', value: 1 },\n        { source: 'Brujon', target: 'Claquesous', value: 1 },\n        { source: 'Brujon', target: 'Montparnasse', value: 1 },\n        { source: 'Mme.Hucheloup', target: 'Bossuet', value: 1 },\n        { source: 'Mme.Hucheloup', target: 'Joly', value: 1 },\n        { source: 'Mme.Hucheloup', target: 'Grantaire', value: 1 },\n        { source: 'Mme.Hucheloup', target: 'Bahorel', value: 1 },\n        { source: 'Mme.Hucheloup', target: 'Courfeyrac', value: 1 },\n        { source: 'Mme.Hucheloup', target: 'Gavroche', value: 1 },\n        { source: 'Mme.Hucheloup', target: 'Enjolras', value: 1 },\n      ],\n    },\n    autoFit: 'view',\n    layout: {\n      type: 'force-atlas2',\n      preventOverlap: true,\n      kr: 20,\n      center: [250, 250],\n      ks: 0.1,\n      ksmax: 10,\n      tao: 0.1,\n      mode: 'normal',\n    },\n    behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n    autoResize: true,\n    zoomRange: [0.1, 5],\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const options = {\n      type: 'force-atlas2',\n      preventOverlap: true,\n      kr: 20,\n      ks: 0.1,\n      ksmax: 10,\n      tao: 0.1,\n      mode: 'normal',\n      kg: 1,\n      barnesHut: false,\n      dissuadeHubs: false,\n      prune: false,\n    };\n\n    const optionFolder = gui.addFolder('ForceAtlas2 Layout Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'preventOverlap');\n    optionFolder.add(options, 'kr', 1, 100, 1);\n    optionFolder.add(options, 'ks', 0.01, 1, 0.01);\n    optionFolder.add(options, 'ksmax', 1, 20, 1);\n    optionFolder.add(options, 'tao', 0.01, 1, 0.01);\n    optionFolder.add(options, 'kg', 0, 10, 0.1);\n    optionFolder.add(options, 'barnesHut');\n    optionFolder.add(options, 'dissuadeHubs');\n    optionFolder.add(options, 'prune');\n    optionFolder.add(options, 'mode', ['normal', 'linlog']);\n\n    optionFolder.onChange(async ({ property, value }) => {\n      graph.setLayout(\n        Object.assign({}, graph.getLayout(), {\n          [property]: value,\n        }),\n      );\n      await graph.layout();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/layouts/radial.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'view',\n    data: {\n      nodes: [\n        { id: '0' },\n        { id: '1' },\n        { id: '2' },\n        { id: '3' },\n        { id: '4' },\n        { id: '5' },\n        { id: '6' },\n        { id: '7' },\n        { id: '8' },\n        { id: '9' },\n        { id: '10' },\n        { id: '11' },\n        { id: '12' },\n        { id: '13' },\n        { id: '14' },\n        { id: '15' },\n        { id: '16' },\n        { id: '17' },\n        { id: '18' },\n        { id: '19' },\n        { id: '20' },\n        { id: '21' },\n        { id: '22' },\n        { id: '23' },\n        { id: '24' },\n        { id: '25' },\n        { id: '26' },\n        { id: '27' },\n        { id: '28' },\n        { id: '29' },\n        { id: '30' },\n        { id: '31' },\n        { id: '32' },\n        { id: '33' },\n      ],\n      edges: [\n        { source: '0', target: '1' },\n        { source: '0', target: '2' },\n        { source: '0', target: '3' },\n        { source: '0', target: '4' },\n        { source: '0', target: '5' },\n        { source: '0', target: '7' },\n        { source: '0', target: '8' },\n        { source: '0', target: '9' },\n        { source: '0', target: '10' },\n        { source: '0', target: '11' },\n        { source: '0', target: '13' },\n        { source: '0', target: '14' },\n        { source: '0', target: '15' },\n        { source: '0', target: '16' },\n        { source: '2', target: '3' },\n        { source: '4', target: '5' },\n        { source: '4', target: '6' },\n        { source: '5', target: '6' },\n        { source: '7', target: '13' },\n        { source: '8', target: '14' },\n        { source: '10', target: '22' },\n        { source: '10', target: '14' },\n        { source: '10', target: '12' },\n        { source: '10', target: '24' },\n        { source: '10', target: '21' },\n        { source: '10', target: '20' },\n        { source: '11', target: '24' },\n        { source: '11', target: '22' },\n        { source: '11', target: '14' },\n        { source: '12', target: '13' },\n        { source: '16', target: '17' },\n        { source: '16', target: '18' },\n        { source: '16', target: '21' },\n        { source: '16', target: '22' },\n        { source: '17', target: '18' },\n        { source: '17', target: '20' },\n        { source: '18', target: '19' },\n        { source: '19', target: '20' },\n        { source: '19', target: '33' },\n        { source: '19', target: '22' },\n        { source: '19', target: '23' },\n        { source: '20', target: '21' },\n        { source: '21', target: '22' },\n        { source: '22', target: '24' },\n        { source: '22', target: '26' },\n        { source: '22', target: '23' },\n        { source: '22', target: '28' },\n        { source: '22', target: '30' },\n        { source: '22', target: '31' },\n        { source: '22', target: '32' },\n        { source: '22', target: '33' },\n        { source: '23', target: '28' },\n        { source: '23', target: '27' },\n        { source: '23', target: '29' },\n        { source: '23', target: '30' },\n        { source: '23', target: '31' },\n        { source: '23', target: '33' },\n        { source: '32', target: '33' },\n      ],\n    },\n    node: {\n      style: {\n        labelFill: '#fff',\n        labelPlacement: 'center',\n        labelText: (d) => d.id,\n      },\n    },\n    layout: {\n      type: 'radial',\n      nodeSize: 32,\n      unitRadius: 100,\n      linkDistance: 200,\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const options = {\n      nodeSize: 32,\n      unitRadius: 100,\n      linkDistance: 200,\n      preventOverlap: false,\n      strictRadial: true,\n      sortBy: undefined,\n      sortStrength: 10,\n    };\n    const optionFolder = gui.addFolder('Radial Layout Options');\n    optionFolder.add(options, 'nodeSize', 1, 100, 1);\n    optionFolder.add(options, 'unitRadius', 10, 300, 1);\n    optionFolder.add(options, 'linkDistance', 10, 400, 1);\n    optionFolder.add(options, 'preventOverlap');\n    optionFolder.add(options, 'strictRadial');\n    optionFolder.add(options, 'sortStrength', 1, 100, 1);\n    optionFolder.add(options, 'sortBy', [undefined, 'data', 'id']);\n    optionFolder.onChange(async ({ property, value }) => {\n      graph.setLayout(\n        Object.assign({}, graph.getLayout(), {\n          [property]: value,\n        }),\n      );\n      await graph.layout();\n      graph.fitView();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/layouts/snake.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: {\n      nodes: new Array(16).fill(0).map((_, i) => ({ id: `${i}` })),\n      edges: new Array(15).fill(0).map((_, i) => ({ source: `${i}`, target: `${i + 1}` })),\n    },\n    node: {\n      style: {\n        labelFill: '#fff',\n        labelPlacement: 'center',\n        labelText: (d) => d.id,\n      },\n    },\n    behaviors: ['drag-canvas'],\n    layout: {\n      type: 'snake',\n      clockwise: true,\n      cols: 4,\n      colGap: 30,\n      rowGap: 30,\n      padding: 15,\n      nodeSize: 30,\n    },\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const options = {\n      type: 'snake',\n      clockwise: true,\n      cols: 4,\n      colGap: 30,\n      rowGap: 30,\n      padding: 15,\n      nodeSize: 30,\n    };\n\n    const optionFolder = gui.addFolder('Grid Layout Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'cols', 2, 10, 1);\n    optionFolder.add(options, 'colGap', 10, 150, 1);\n    optionFolder.add(options, 'rowGap', 10, 150, 1);\n    optionFolder.add(options, 'padding', 5, 100, 1);\n    optionFolder.add(options, 'nodeSize', 10, 50, 30);\n    optionFolder.add(options, 'clockwise');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.setLayout({\n        type: 'snake',\n        [property]: value,\n      });\n      graph.layout();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/background.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-0' },\n        { id: 'node-1' },\n        { id: 'node-2' },\n        { id: 'node-3' },\n        { id: 'node-4' },\n        { id: 'node-5' },\n      ],\n      edges: [\n        { source: 'node-0', target: 'node-1' },\n        { source: 'node-0', target: 'node-2' },\n        { source: 'node-0', target: 'node-3' },\n        { source: 'node-0', target: 'node-4' },\n        { source: 'node-1', target: 'node-0' },\n        { source: 'node-2', target: 'node-0' },\n        { source: 'node-3', target: 'node-0' },\n        { source: 'node-4', target: 'node-0' },\n        { source: 'node-5', target: 'node-0' },\n      ],\n    },\n    node: { style: { fill: '#7e3feb' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    layout: { type: 'grid' },\n    behaviors: ['drag-canvas'],\n    plugins: [\n      {\n        type: 'background',\n        key: 'background',\n        backgroundColor: '#f0f2f5',\n      },\n    ],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      type: 'background',\n      backgroundSize: 'contain',\n    };\n    const optionFolder = gui.addFolder('Background Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'backgroundSize', ['cover', 'contain', 'auto', '50%']);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'background',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/bubble-sets.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: {\n      nodes: [\n        {\n          id: 'node-0',\n          data: { cluster: 'a' },\n          style: { x: 555, y: 151 },\n        },\n        {\n          id: 'node-1',\n          data: { cluster: 'a' },\n          style: { x: 532, y: 323 },\n        },\n        {\n          id: 'node-2',\n          data: { cluster: 'a' },\n          style: { x: 473, y: 227 },\n        },\n        {\n          id: 'node-3',\n          data: { cluster: 'a' },\n          style: { x: 349, y: 212 },\n        },\n        {\n          id: 'node-4',\n          data: { cluster: 'b' },\n          style: { x: 234, y: 201 },\n        },\n        {\n          id: 'node-5',\n          data: { cluster: 'b' },\n          style: { x: 338, y: 333 },\n        },\n        {\n          id: 'node-6',\n          data: { cluster: 'b' },\n          style: { x: 365, y: 91 },\n        },\n      ],\n      edges: [\n        {\n          id: 'edge-0',\n          source: 'node-0',\n          target: 'node-2',\n        },\n        {\n          id: 'edge-1',\n          source: 'node-1',\n          target: 'node-2',\n        },\n        {\n          id: 'edge-2',\n          source: 'node-2',\n          target: 'node-3',\n        },\n        {\n          id: 'edge-3',\n          source: 'node-3',\n          target: 'node-4',\n        },\n        {\n          id: 'edge-4',\n          source: 'node-3',\n          target: 'node-5',\n        },\n        {\n          id: 'edge-5',\n          source: 'node-3',\n          target: 'node-6',\n        },\n      ],\n    },\n    node: {\n      style: { labelText: (d) => d.id },\n      palette: { field: 'cluster', color: ['#7e3feb', '#ffa940'] },\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n    plugins: [\n      'grid-line',\n      {\n        type: 'bubble-sets',\n        key: 'bubble-sets',\n        members: ['node-0', 'node-1', 'node-2', 'node-3'],\n        labelText: 'bubblesets-a',\n        fill: '#7e3feb',\n        fillOpacity: 0.1,\n        stroke: '#7e3feb',\n        strokeOpacity: 1,\n        labelFill: '#fff',\n        labelPadding: 2,\n        labelBackgroundFill: '#7e3feb',\n        labelBackgroundRadius: 5,\n      },\n    ],\n  },\n  { width: 600, height: 450 },\n  (gui, graph) => {\n    const options = {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      avoidMembers: [],\n      // style\n      fill: '#7e3feb',\n      fillOpacity: 0.1,\n      stroke: '#7e3feb',\n      strokeOpacity: 1,\n      // label\n      label: true,\n      labelCloseToPath: true,\n      labelAutoRotate: true,\n      labelOffsetX: 0,\n      labelOffsetY: 0,\n      labelPlacement: 'bottom',\n      // bubblesets\n      maxRoutingIterations: 100,\n      maxMarchingIterations: 20,\n      pixelGroup: 4,\n      edgeR0: 10,\n      edgeR1: 20,\n      nodeR0: 15,\n      nodeR1: 50,\n      morphBuffer: 10,\n      threshold: 1,\n      memberInfluenceFactor: 1,\n      edgeInfluenceFactor: 1,\n      nonMemberInfluenceFactor: -0.8,\n      virtualEdges: true,\n    };\n\n    const optionFolder = gui.addFolder('Bubblesets Options');\n    optionFolder.add(options, 'type').disable();\n    optionFolder.addColor(options, 'fill');\n    optionFolder.addColor(options, 'stroke');\n    optionFolder.add(options, 'fillOpacity', 0, 1, 0.1);\n    optionFolder.add(options, 'strokeOpacity', 0, 1, 0.1);\n    optionFolder.add(options, 'label');\n    optionFolder.add(options, 'labelCloseToPath');\n    optionFolder.add(options, 'labelAutoRotate');\n    optionFolder.add(options, 'labelOffsetX', 0, 20, 1);\n    optionFolder.add(options, 'labelOffsetY', 0, 20, 1);\n    optionFolder.add(options, 'labelPlacement', ['left', 'right', 'top', 'bottom', 'center']);\n    optionFolder.add(options, 'maxRoutingIterations', 0, 200, 1);\n    optionFolder.add(options, 'maxMarchingIterations', 0, 40, 1);\n    optionFolder.add(options, 'pixelGroup', 0, 20, 1);\n    optionFolder.add(options, 'edgeR0', 0, 50, 1);\n    optionFolder.add(options, 'edgeR1', 0, 50, 1);\n    optionFolder.add(options, 'nodeR0', 0, 50, 1);\n    optionFolder.add(options, 'nodeR1', 0, 50, 1);\n    optionFolder.add(options, 'morphBuffer', 0, 20, 1);\n    optionFolder.add(options, 'threshold', -1, 1, 0.1);\n    optionFolder.add(options, 'memberInfluenceFactor', -1, 1, 0.1);\n    optionFolder.add(options, 'edgeInfluenceFactor', -1, 1, 0.1);\n    optionFolder.add(options, 'nonMemberInfluenceFactor', -1, 1, 0.1);\n    optionFolder.add(options, 'virtualEdges');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'bubble-sets',\n        [property]: value,\n      });\n      graph.render();\n    });\n\n    const apiConfig = {\n      member: 'node-1',\n      avoidMember: 'node-1',\n    };\n    const apiFolder = gui.addFolder('Bubblesets API');\n    const instance = graph.getPluginInstance('bubble-sets');\n    const nodeIds = graph.getData().nodes.map((node) => node.id);\n    const edgeIds = graph.getData().edges.map((edge) => edge.id);\n    apiFolder.add(apiConfig, 'member', [...nodeIds, ...edgeIds]);\n    apiFolder.add({ addMember: () => instance.addMember(apiConfig.member) }, 'addMember').name('add member');\n    apiFolder\n      .add({ removeMember: () => instance.removeMember(apiConfig.member) }, 'removeMember')\n      .name('remove member');\n    apiFolder\n      .add({ removeMember: () => alert('Members in Bubblesets: ' + instance.getMember()) }, 'removeMember')\n      .name('get member');\n    apiFolder.add(apiConfig, 'avoidMember', nodeIds);\n    apiFolder\n      .add({ addAvoidMember: () => instance.addAvoidMember(apiConfig.avoidMember) }, 'addAvoidMember')\n      .name('add avoid member');\n    apiFolder\n      .add({ removeAvoidMember: () => instance.removeAvoidMember(apiConfig.avoidMember) }, 'removeAvoidMember')\n      .name('remove avoid member');\n    apiFolder\n      .add({ removeMember: () => alert('Avoid members in Bubblesets: ' + instance.getAvoidMember()) }, 'removeMember')\n      .name('get avoid member');\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/contextmenu.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-0' },\n        { id: 'node-1' },\n        { id: 'node-2' },\n        { id: 'node-3' },\n        { id: 'node-4' },\n        { id: 'node-5' },\n      ],\n      edges: [\n        { source: 'node-0', target: 'node-1' },\n        { source: 'node-0', target: 'node-2' },\n        { source: 'node-0', target: 'node-3' },\n        { source: 'node-0', target: 'node-4' },\n        { source: 'node-1', target: 'node-0' },\n        { source: 'node-2', target: 'node-0' },\n        { source: 'node-3', target: 'node-0' },\n        { source: 'node-4', target: 'node-0' },\n        { source: 'node-5', target: 'node-0' },\n      ],\n    },\n    node: { style: { fill: '#7e3feb' } },\n    layout: { type: 'grid' },\n    behaviors: ['drag-canvas'],\n    plugins: ['grid-line', { type: 'contextmenu', key: 'contextmenu' }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      type: 'contextmenu',\n      trigger: 'contextmenu',\n      offsetX: 4,\n      offsetY: 4,\n      enable: 'always',\n    };\n    const optionFolder = gui.addFolder('Contextmenu Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'trigger', ['click', 'contextmenu']);\n    optionFolder.add(options, 'offsetX', 0, 10, 1);\n    optionFolder.add(options, 'offsetY', 0, 10, 1);\n    optionFolder.add(options, 'enable', ['always', 'node', 'edge']);\n\n    optionFolder.onChange((e) => {\n      const { offsetX, offsetY, enable, ...rest } = e.object;\n      let enableFn = () => true;\n      if ((enable === 'node') | (enable === 'edge')) {\n        enableFn = (e) => e.targetType === enable;\n      }\n      graph.updatePlugin({\n        key: 'contextmenu',\n        offset: [offsetX, offsetY],\n        enable: enableFn,\n        ...rest,\n      });\n      graph.render();\n    });\n\n    const apiFolder = gui.addFolder('Contextmenu API');\n    const instance = graph.getPluginInstance('contextmenu');\n    apiFolder.add(instance, 'hide');\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/edge-bundling.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-1' },\n        { id: 'node-2' },\n        { id: 'node-3' },\n        { id: 'node-4' },\n        { id: 'node-5' },\n        { id: 'node-6' },\n        { id: 'node-7' },\n        { id: 'node-8' },\n        { id: 'node-9' },\n        { id: 'node-10' },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-1', target: 'node-3' },\n        { source: 'node-1', target: 'node-4' },\n        { source: 'node-1', target: 'node-5' },\n        { source: 'node-2', target: 'node-4' },\n        { source: 'node-2', target: 'node-6' },\n        { source: 'node-2', target: 'node-8' },\n        { source: 'node-3', target: 'node-5' },\n        { source: 'node-3', target: 'node-7' },\n        { source: 'node-3', target: 'node-9' },\n        { source: 'node-4', target: 'node-6' },\n        { source: 'node-4', target: 'node-10' },\n        { source: 'node-5', target: 'node-7' },\n        { source: 'node-5', target: 'node-8' },\n        { source: 'node-6', target: 'node-8' },\n        { source: 'node-6', target: 'node-9' },\n        { source: 'node-7', target: 'node-9' },\n        { source: 'node-7', target: 'node-10' },\n        { source: 'node-8', target: 'node-10' },\n        { source: 'node-9', target: 'node-1' },\n        { source: 'node-10', target: 'node-2' },\n        { source: 'node-10', target: 'node-3' },\n        { source: 'node-10', target: 'node-4' },\n        { source: 'node-10', target: 'node-5' },\n        { source: 'node-10', target: 'node-6' },\n        { source: 'node-10', target: 'node-7' },\n        { source: 'node-10', target: 'node-8' },\n        { source: 'node-10', target: 'node-9' },\n      ],\n    },\n    node: { style: { fill: '#7e3feb' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    layout: { type: 'circular' },\n    behaviors: ['drag-canvas', 'zoom-canvas'],\n    plugins: [\n      {\n        type: 'edge-bundling',\n        key: 'edge-bundling',\n        bundleThreshold: 0.6,\n        cycles: 6,\n        divisions: 3,\n        divRate: 2,\n        iterations: 90,\n        iterRate: 2 / 3,\n        K: 0.1,\n        lambda: 0.1,\n      },\n    ],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      type: 'edge-bundling',\n      bundleThreshold: 0.6,\n      cycles: 6,\n      divisions: 1,\n      divRate: 2,\n      iterations: 90,\n      iterRate: 2 / 3,\n      K: 0.1,\n      lambda: 0.1,\n    };\n    const optionFolder = gui.addFolder('Edge Bundling Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'bundleThreshold', 0, 1, 0.01);\n    optionFolder.add(options, 'cycles', 1, 20, 1);\n    optionFolder.add(options, 'divisions', 1, 10, 1);\n    optionFolder.add(options, 'divRate', 1, 5, 0.1);\n    optionFolder.add(options, 'iterations', 10, 200, 10);\n    optionFolder.add(options, 'iterRate', 0.1, 1, 0.01);\n    optionFolder.add(options, 'K', 0.01, 1, 0.01);\n    optionFolder.add(options, 'lambda', 0.01, 1, 0.01);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'edge-bundling',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/edge-filter-lens.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        // 上部疏散区域\n        { id: 'Myriel', style: { x: 207, y: 78, label: 'Myriel' } },\n        { id: 'Napoleon', style: { x: 127, y: 62, label: 'Napoleon' } },\n        { id: 'CountessdeLo', style: { x: 171, y: 47, label: 'CountessdeLo' } },\n        { id: 'Geborand', style: { x: 106, y: 81, label: 'Geborand' } },\n        { id: 'Champtercier', style: { x: 247, y: 58, label: 'Champtercier' } },\n        { id: 'Cravatte', style: { x: 152, y: 50, label: 'Cravatte' } },\n\n        // 中上部区域\n        { id: 'Mlle.Baptistine', style: { x: 205, y: 141, label: 'Mlle.Baptistine' } },\n        { id: 'Mme.Magloire', style: { x: 275, y: 120, label: 'Mme.Magloire' } },\n        { id: 'Labarre', style: { x: 246, y: 183, label: 'Labarre' } },\n        { id: 'Valjean', style: { x: 342, y: 221, label: 'Valjean' } },\n        { id: 'Marguerite', style: { x: 285, y: 171, label: 'Marguerite' } },\n\n        // 中部密集区域\n        { id: 'Tholomyes', style: { x: 379, y: 158, label: 'Tholomyes' } },\n        { id: 'Listolier', style: { x: 288, y: 80, label: 'Listolier' } },\n        { id: 'Fameuil', style: { x: 349, y: 89, label: 'Fameuil' } },\n        { id: 'Blacheville', style: { x: 381, y: 95, label: 'Blacheville' } },\n        { id: 'Favourite', style: { x: 264, y: 153, label: 'Favourite' } },\n        { id: 'Dahlia', style: { x: 323, y: 170, label: 'Dahlia' } },\n        { id: 'Zephine', style: { x: 306, y: 114, label: 'Zephine' } },\n        { id: 'Fantine', style: { x: 357, y: 187, label: 'Fantine' } },\n\n        // 右侧区域\n        { id: 'Bamatabois', style: { x: 411, y: 156, label: 'Bamatabois' } },\n        { id: 'Perpetue', style: { x: 454, y: 195, label: 'Perpetue' } },\n        { id: 'Simplice', style: { x: 406, y: 227, label: 'Simplice' } },\n\n        // 下部区域\n        { id: 'Cosette', style: { x: 343, y: 248, label: 'Cosette' } },\n        { id: 'Javert', style: { x: 388, y: 263, label: 'Javert' } },\n        { id: 'Fauchelevent', style: { x: 397, y: 276, label: 'Fauchelevent' } },\n        { id: 'Thenardier', style: { x: 317, y: 300, label: 'Thenardier' } },\n        { id: 'Eponine', style: { x: 268, y: 365, label: 'Eponine' } },\n        { id: 'Anzelma', style: { x: 234, y: 303, label: 'Anzelma' } },\n        { id: 'Woman2', style: { x: 304, y: 254, label: 'Woman2' } },\n\n        // 最右侧独立节点\n        { id: 'Gribier', style: { x: 457, y: 160, label: 'Gribier' } },\n        { id: 'Jondrette', style: { x: 510, y: 327, label: 'Jondrette' } },\n      ],\n      edges: [\n        // 上部连接\n        { id: 'e1', source: 'Myriel', target: 'CountessdeLo' },\n        { id: 'e2', source: 'Napoleon', target: 'Myriel' },\n        { id: 'e3', source: 'Geborand', target: 'Napoleon' },\n        { id: 'e4', source: 'Champtercier', target: 'Myriel' },\n        { id: 'e5', source: 'Cravatte', target: 'CountessdeLo' },\n\n        // 中上部连接\n        { id: 'e6', source: 'Mlle.Baptistine', target: 'Mme.Magloire' },\n        { id: 'e7', source: 'Labarre', target: 'Valjean' },\n        { id: 'e8', source: 'Valjean', target: 'Marguerite' },\n        { id: 'e9', source: 'Marguerite', target: 'Mme.Magloire' },\n\n        // 中部密集连接\n        { id: 'e10', source: 'Tholomyes', target: 'Listolier' },\n        { id: 'e11', source: 'Listolier', target: 'Fameuil' },\n        { id: 'e12', source: 'Fameuil', target: 'Blacheville' },\n        { id: 'e13', source: 'Blacheville', target: 'Favourite' },\n        { id: 'e14', source: 'Favourite', target: 'Dahlia' },\n        { id: 'e15', source: 'Dahlia', target: 'Zephine' },\n        { id: 'e16', source: 'Zephine', target: 'Fantine' },\n        { id: 'e17', source: 'Tholomyes', target: 'Fantine' },\n        { id: 'e18', source: 'Valjean', target: 'Fantine' },\n\n        // 右侧连接\n        { id: 'e19', source: 'Bamatabois', target: 'Perpetue' },\n        { id: 'e20', source: 'Perpetue', target: 'Simplice' },\n        { id: 'e21', source: 'Bamatabois', target: 'Gribier' },\n\n        // 下部连接\n        { id: 'e22', source: 'Valjean', target: 'Cosette' },\n        { id: 'e23', source: 'Cosette', target: 'Javert' },\n        { id: 'e24', source: 'Javert', target: 'Fauchelevent' },\n        { id: 'e25', source: 'Fauchelevent', target: 'Thenardier' },\n        { id: 'e26', source: 'Thenardier', target: 'Eponine' },\n        { id: 'e27', source: 'Eponine', target: 'Anzelma' },\n        { id: 'e28', source: 'Woman2', target: 'Cosette' },\n\n        // 跨区域连接\n        { id: 'e29', source: 'Fantine', target: 'Bamatabois' },\n        { id: 'e30', source: 'Javert', target: 'Bamatabois' },\n        { id: 'e31', source: 'Simplice', target: 'Jondrette' },\n        { id: 'e32', source: 'Thenardier', target: 'Jondrette' },\n        { id: 'e33', source: 'Favourite', target: 'Valjean' },\n        { id: 'e34', source: 'Tholomyes', target: 'Cosette' },\n      ],\n    },\n    node: {\n      style: {\n        label: true,\n        size: 16,\n      },\n      palette: {\n        field: (datum) => Math.floor(datum.style?.y / 60),\n      },\n    },\n    edge: {\n      style: {\n        label: true,\n        labelText: (d) => d.data.value?.toString(),\n        stroke: '#ccc',\n        endArrow: true,\n        endArrowType: 'triangle',\n      },\n    },\n    plugins: [\n      {\n        type: 'edge-filter-lens',\n        key: 'edge-filter-lens',\n        r: 80,\n        trigger: 'pointermove',\n      },\n    ],\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const TRIGGER_TYPES = ['pointermove', 'click', 'drag'];\n    const NODE_TYPES = ['both', 'source', 'target', 'either'];\n\n    const options = {\n      type: 'edge-filter-lens',\n      r: 80, // 透镜半径\n      trigger: 'pointermove', // 触发方式\n      nodeType: 'both', // 边显示条件\n      minR: 50, // 最小半径\n      maxR: 150, // 最大半径\n      scaleRBy: 'wheel', // 缩放方式\n      style: {\n        fill: '#f0f5ff',\n        fillOpacity: 0.4,\n        stroke: '#1d39c4',\n        strokeOpacity: 0.8,\n        lineWidth: 1.5,\n      },\n      nodeStyle: {\n        size: 35,\n        fill: '#d6e4ff',\n        stroke: '#2f54eb',\n        lineWidth: 2,\n        labelFontSize: 14,\n        labelFontWeight: 'bold',\n        labelFill: '#1d39c4',\n      },\n      edgeStyle: {\n        stroke: '#1d39c4',\n        lineWidth: 2,\n        strokeOpacity: 0.8,\n      },\n    };\n\n    const optionFolder = gui.addFolder('Edge Filter Lens Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'r', 50, 150, 5);\n    optionFolder.add(options, 'trigger', TRIGGER_TYPES);\n    optionFolder.add(options, 'nodeType', NODE_TYPES);\n    optionFolder.add(options, 'minR', 20, 100, 5);\n    optionFolder.add(options, 'maxR', 100, 200, 5);\n\n    optionFolder.onChange(({ property, value }) => {\n      if (property.includes('.')) {\n        const [group, prop] = property.split('.');\n        graph.updatePlugin({\n          key: 'edge-filter-lens',\n          [group]: {\n            ...options[group],\n            [prop]: value,\n          },\n        });\n      } else {\n        graph.updatePlugin({\n          key: 'edge-filter-lens',\n          [property]: value,\n        });\n      }\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/fisheye.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        // 上部节点\n        { id: 'Myriel', style: { x: 197, y: 58 } },\n        { id: 'Napoleon', style: { x: 147, y: 22 } },\n        { id: 'Mlle.Baptistine', style: { x: 225, y: 141 } },\n        { id: 'Mme.Magloire', style: { x: 255, y: 120 } },\n        { id: 'CountessdeLo', style: { x: 151, y: -3 } },\n        { id: 'Geborand', style: { x: 136, y: 41 } },\n        { id: 'Champtercier', style: { x: 227, y: 8 } },\n        { id: 'Cravatte', style: { x: 172, y: -10 } },\n        { id: 'Count', style: { x: 172, y: 12 } },\n        { id: 'OldMan', style: { x: 198, y: -6 } },\n        // 中上部节点\n        { id: 'Labarre', style: { x: 266, y: 203 } },\n        { id: 'Marguerite', style: { x: 265, y: 171 } },\n        { id: 'Mme.deR', style: { x: 299, y: 133 } },\n        { id: 'Isabeau', style: { x: 282, y: 191 } },\n        { id: 'Gervais', style: { x: 334, y: 148 } },\n        { id: 'Simplice', style: { x: 286, y: 227 } },\n        { id: 'Scaufflaire', style: { x: 250, y: 231 } },\n        { id: 'Woman1', style: { x: 375, y: 202 } },\n        { id: 'Judge', style: { x: 370, y: 139 } },\n        { id: 'Champmathieu', style: { x: 404, y: 216 } },\n        // 中部主要节点\n        { id: 'Valjean', style: { x: 322, y: 221 } },\n        { id: 'Fantine', style: { x: 337, y: 187 } },\n        { id: 'Cosette', style: { x: 343, y: 248 } },\n        { id: 'Javert', style: { x: 368, y: 263 } },\n        { id: 'Thenardier', style: { x: 317, y: 300 } },\n        { id: 'Mme.Thenardier', style: { x: 283, y: 267 } },\n        { id: 'Eponine', style: { x: 268, y: 365 } },\n        { id: 'Gavroche', style: { x: 393, y: 380 } },\n        { id: 'Marius', style: { x: 336, y: 350 } },\n        { id: 'Enjolras', style: { x: 376, y: 371 } },\n        // 右侧和右上节点\n        { id: 'Gribier', style: { x: 437, y: 160 } },\n        { id: 'Jondrette', style: { x: 510, y: 327 } },\n        { id: 'Mme.Burgon', style: { x: 466, y: 368 } },\n        { id: 'Brevet', style: { x: 399, y: 183 } },\n        { id: 'Chenildieu', style: { x: 425, y: 194 } },\n        { id: 'Cochepaille', style: { x: 419, y: 148 } },\n        { id: 'Child1', style: { x: 361, y: 387 } },\n        { id: 'Child2', style: { x: 415, y: 432 } },\n        { id: 'Brujon', style: { x: 330, y: 394 } },\n        { id: 'Mme.Hucheloup', style: { x: 394, y: 450 } },\n        // 中部其他节点\n        { id: 'Favourite', style: { x: 284, y: 153 } },\n        { id: 'Dahlia', style: { x: 303, y: 170 } },\n        { id: 'Zephine', style: { x: 286, y: 94 } },\n        { id: 'Tholomyes', style: { x: 359, y: 158 } },\n        { id: 'Listolier', style: { x: 308, y: 80 } },\n        { id: 'Fameuil', style: { x: 329, y: 89 } },\n        { id: 'Blacheville', style: { x: 351, y: 95 } },\n        { id: 'Perpetue', style: { x: 234, y: 195 } },\n        { id: 'Woman2', style: { x: 304, y: 254 } },\n        { id: 'MotherInnocent', style: { x: 350, y: 214 } },\n        // 下部节点\n        { id: 'Pontmercy', style: { x: 375, y: 307 } },\n        { id: 'Boulatruelle', style: { x: 260, y: 279 } },\n        { id: 'Anzelma', style: { x: 234, y: 303 } },\n        { id: 'Gillenormand', style: { x: 338, y: 286 } },\n        { id: 'Magnon', style: { x: 277, y: 317 } },\n        { id: 'Mlle.Gillenormand', style: { x: 257, y: 306 } },\n        { id: 'Mme.Pontmercy', style: { x: 307, y: 318 } },\n        { id: 'Mlle.Vaubois', style: { x: 197, y: 325 } },\n        { id: 'Lt.Gillenormand', style: { x: 294, y: 296 } },\n        { id: 'Toussaint', style: { x: 306, y: 277 } },\n        { id: 'Gueulemer', style: { x: 344, y: 323 } },\n        { id: 'Babet', style: { x: 367, y: 319 } },\n        { id: 'Claquesous', style: { x: 303, y: 347 } },\n        { id: 'Montparnasse', style: { x: 322, y: 330 } },\n        // 最下部节点\n        { id: 'Combeferre', style: { x: 397, y: 416 } },\n        { id: 'Prouvaire', style: { x: 309, y: 426 } },\n        { id: 'Feuilly', style: { x: 314, y: 456 } },\n        { id: 'Courfeyrac', style: { x: 332, y: 435 } },\n        { id: 'Bahorel', style: { x: 343, y: 466 } },\n        { id: 'Bossuet', style: { x: 305, y: 382 } },\n        { id: 'Joly', style: { x: 371, y: 415 } },\n        { id: 'Grantaire', style: { x: 370, y: 466 } },\n        { id: 'MotherPlutarch', style: { x: 424, y: 461 } },\n      ],\n      edges: [\n        // 主要连接\n        { id: 'e1', source: 'Valjean', target: 'Javert' },\n        { id: 'e2', source: 'Valjean', target: 'Cosette' },\n        { id: 'e3', source: 'Javert', target: 'Thenardier' },\n        { id: 'e4', source: 'Cosette', target: 'Marius' },\n        { id: 'e5', source: 'Eponine', target: 'Marius' },\n        { id: 'e6', source: 'Enjolras', target: 'Marius' },\n        { id: 'e7', source: 'Gavroche', target: 'Enjolras' },\n        { id: 'e8', source: 'Valjean', target: 'Fantine' },\n        { id: 'e9', source: 'Cosette', target: 'Thenardier' },\n        { id: 'e10', source: 'Eponine', target: 'Thenardier' },\n        // 上部连接\n        { id: 'e11', source: 'Myriel', target: 'Napoleon' },\n        { id: 'e12', source: 'Myriel', target: 'Mlle.Baptistine' },\n        { id: 'e13', source: 'Mlle.Baptistine', target: 'Mme.Magloire' },\n        { id: 'e14', source: 'CountessdeLo', target: 'Myriel' },\n        { id: 'e15', source: 'Geborand', target: 'Myriel' },\n        // 中部连接\n        { id: 'e16', source: 'Favourite', target: 'Tholomyes' },\n        { id: 'e17', source: 'Dahlia', target: 'Favourite' },\n        { id: 'e18', source: 'Zephine', target: 'Favourite' },\n        { id: 'e19', source: 'Tholomyes', target: 'Listolier' },\n        { id: 'e20', source: 'Fameuil', target: 'Blacheville' },\n        // 下部连接\n        { id: 'e21', source: 'Combeferre', target: 'Enjolras' },\n        { id: 'e22', source: 'Prouvaire', target: 'Combeferre' },\n        { id: 'e23', source: 'Feuilly', target: 'Courfeyrac' },\n        { id: 'e24', source: 'Bahorel', target: 'Bossuet' },\n        { id: 'e25', source: 'Joly', target: 'Grantaire' },\n        // 额外的中部连接\n        { id: 'e26', source: 'Gueulemer', target: 'Thenardier' },\n        { id: 'e27', source: 'Babet', target: 'Gueulemer' },\n        { id: 'e28', source: 'Claquesous', target: 'Montparnasse' },\n        { id: 'e29', source: 'Brujon', target: 'Babet' },\n        { id: 'e30', source: 'Child1', target: 'Gavroche' },\n        // 新增更多连接\n        { id: 'e31', source: 'Valjean', target: 'Simplice' },\n        { id: 'e32', source: 'Fantine', target: 'Simplice' },\n        { id: 'e33', source: 'Javert', target: 'Simplice' },\n        { id: 'e34', source: 'Marius', target: 'Gillenormand' },\n        { id: 'e35', source: 'Cosette', target: 'Gillenormand' },\n        { id: 'e36', source: 'Marius', target: 'Lt.Gillenormand' },\n        { id: 'e37', source: 'Gillenormand', target: 'Lt.Gillenormand' },\n        { id: 'e38', source: 'Cosette', target: 'Toussaint' },\n        { id: 'e39', source: 'Javert', target: 'Toussaint' },\n        { id: 'e40', source: 'Valjean', target: 'Toussaint' },\n        // 随机添加更多连接\n        ...Array.from({ length: 50 }, (_, i) => ({\n          // 从40增加到50个随机连接\n          id: `edge-${i + 41}`,\n          source: [\n            'Valjean',\n            'Javert',\n            'Cosette',\n            'Marius',\n            'Enjolras',\n            'Fantine',\n            'Thenardier',\n            'Eponine',\n            'Gavroche',\n            'Gueulemer',\n            'Babet',\n            'Claquesous',\n            'Favourite',\n            'Tholomyes',\n            'Simplice',\n          ][Math.floor(Math.random() * 15)],\n          target: [\n            'Favourite',\n            'Dahlia',\n            'Tholomyes',\n            'Combeferre',\n            'Prouvaire',\n            'Feuilly',\n            'Courfeyrac',\n            'Bahorel',\n            'Bossuet',\n            'Montparnasse',\n            'Brujon',\n            'Child1',\n            'Simplice',\n            'Toussaint',\n            'Gillenormand',\n          ][Math.floor(Math.random() * 15)],\n        })),\n      ],\n    },\n    autoFit: 'view',\n    node: {\n      style: {\n        size: (datum) => datum.id.length * 2 + 10,\n        label: false,\n        labelText: (datum) => datum.id,\n        labelBackground: true,\n        icon: false,\n        iconFontFamily: 'iconfont',\n        iconText: '\\ue6f6',\n        iconFill: '#fff',\n      },\n      palette: {\n        type: 'group',\n        field: (datum) => datum.id,\n        color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n      },\n    },\n    edge: {\n      style: {\n        stroke: '#bfbfbf',\n      },\n    },\n    behaviors: ['drag-canvas'],\n    plugins: [\n      {\n        type: 'fisheye',\n        key: 'fisheye',\n        r: 120,\n        d: 1.5,\n        nodeStyle: {\n          label: true,\n          icon: true,\n        },\n      },\n    ],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const TRIGGER_OPTIONS = ['pointermove', 'drag', 'click'];\n    const SCALE_OPTIONS = ['wheel', 'drag', '-'];\n\n    const options = {\n      type: 'fisheye',\n      trigger: 'pointermove',\n      r: 120,\n      d: 1.5,\n      maxR: 200,\n      minR: 50,\n      maxD: 5,\n      minD: 0.5,\n      scaleRBy: '-',\n      scaleDBy: '-',\n      showDPercent: true,\n      preventDefault: true,\n    };\n\n    const optionFolder = gui.addFolder('Fisheye Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'trigger', TRIGGER_OPTIONS);\n    optionFolder.add(options, 'r', 50, 200, 10);\n    optionFolder.add(options, 'd', 0.5, 5, 0.1);\n    optionFolder.add(options, 'scaleRBy', SCALE_OPTIONS);\n    optionFolder.add(options, 'scaleDBy', SCALE_OPTIONS);\n    optionFolder.add(options, 'showDPercent');\n    optionFolder.add(options, 'preventDefault');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'fisheye',\n        [property]: value === '-' ? undefined : value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/fullscreen.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: { nodes: [{ id: 'node-1' }, { id: 'node-2' }], edges: [{ source: 'node-1', target: 'node-2' }] },\n    node: { style: { fill: '#7e3feb', width: 30, height: 30 } },\n    edge: { style: { stroke: '#8b9baf', strokeWidth: 2 } },\n    layout: { type: 'force', center: [300, 150] },\n    behaviors: ['drag-canvas', 'drag-node'],\n    plugins: [\n      {\n        type: 'fullscreen',\n        key: 'fullscreen',\n        enabled: false,\n      },\n      function () {\n        const graph = this;\n        return {\n          type: 'toolbar',\n          key: 'toolbar',\n          position: 'top-left',\n          onClick: (item) => {\n            const fullscreenPlugin = graph.getPluginInstance('fullscreen');\n            if (item === 'request-fullscreen') {\n              fullscreenPlugin.request();\n            }\n            if (item === 'exit-fullscreen') {\n              fullscreenPlugin.exit();\n            }\n          },\n          getItems: () => {\n            return [\n              { id: 'request-fullscreen', value: 'request-fullscreen' },\n              { id: 'exit-fullscreen', value: 'exit-fullscreen' },\n            ];\n          },\n        };\n      },\n    ],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const KEY_OPTIONS = ['f11', 'ctrl+f', 'meta+f']; // 常见全屏快捷键\n    const options = {\n      type: 'fullscreen',\n      enabled: true,\n      triggerKey: 'f11', // 触发全屏的快捷键\n      closeOnEscape: true, // 按ESC键退出全屏\n    };\n\n    const optionFolder = gui.addFolder('Fullscreen Options');\n    optionFolder.add(options, 'type').disable(true); // 固定插件类型\n    optionFolder.add(options, 'enabled').name('启用全屏'); // 开关选项\n    optionFolder.add(options, 'triggerKey', KEY_OPTIONS).name('触发快捷键'); // 快捷键选择\n    optionFolder.add(options, 'closeOnEscape').name('ESC键退出'); // 退出全屏方式\n\n    optionFolder.onChange(({ property, value }) => {\n      // 更新插件配置\n      graph.updatePlugin({\n        key: 'fullscreen', // 对应插件的key\n        [property]: property === 'triggerKey' ? value : value, // 直接赋值对应属性\n      });\n\n      // 特殊处理：如果启用状态变化，直接触发全屏切换\n      if (property === 'enabled') {\n        value ? graph.plugins['fullscreen'].enter() : graph.plugins['fullscreen'].exit();\n      }\n\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/grid-line.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: { nodes: [{ id: 'node-1' }] },\n    node: { style: { fill: '#7e3feb' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    layout: { type: 'force' },\n    behaviors: ['drag-canvas'],\n    plugins: [{ type: 'grid-line', key: 'grid-line', size: 30 }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const LINE_STYLE = ['none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'];\n    const options = {\n      type: 'grid-line',\n      border: true,\n      borderLineWidth: 1,\n      borderStroke: '#eee',\n      borderStyle: 'solid',\n      follow: false,\n      lineWidth: 1,\n      size: 20,\n      stroke: '#eee',\n    };\n    const optionFolder = gui.addFolder('Gird Line Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'size', 1, 50, 1);\n    optionFolder.add(options, 'lineWidth', 1, 10, 1);\n    optionFolder.addColor(options, 'stroke');\n    optionFolder.add(options, 'border');\n    optionFolder.add(options, 'borderLineWidth', 1, 10, 1);\n    optionFolder.add(options, 'borderStyle', LINE_STYLE);\n    optionFolder.addColor(options, 'borderStroke');\n    optionFolder.add(options, 'follow');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'grid-line',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/history.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: { nodes: [{ id: 'node-1' }] },\n    layout: { type: 'force' },\n    node: {\n      style: {\n        size: 60,\n        labelText: 'Drag Me!',\n        labelPlacement: 'middle',\n        labelFill: '#fff',\n        fill: '#7e3feb',\n      },\n    },\n    edge: { style: { stroke: '#8b9baf' } },\n    behaviors: ['drag-element'],\n    plugins: ['grid-line', { type: 'history', key: 'history' }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      type: 'history',\n      stackSize: 0,\n    };\n    const optionFolder = gui.addFolder('History Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'stackSize', 0, 10, 1);\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'history',\n        [property]: value,\n      });\n      graph.render();\n    });\n\n    const apiFolder = gui.addFolder('History API');\n    const instance = graph.getPluginInstance('history');\n    apiFolder.add(instance, 'undo');\n    apiFolder.add(instance, 'redo');\n    apiFolder.add(instance, 'clear');\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/hull.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        {\n          id: 'node-0',\n          data: { cluster: 'a' },\n          style: { x: 555, y: 151 },\n        },\n        {\n          id: 'node-1',\n          data: { cluster: 'a' },\n          style: { x: 532, y: 323 },\n        },\n        {\n          id: 'node-2',\n          data: { cluster: 'a' },\n          style: { x: 473, y: 227 },\n        },\n        {\n          id: 'node-3',\n          data: { cluster: 'a' },\n          style: { x: 349, y: 212 },\n        },\n        {\n          id: 'node-4',\n          data: { cluster: 'b' },\n          style: { x: 234, y: 201 },\n        },\n        {\n          id: 'node-5',\n          data: { cluster: 'b' },\n          style: { x: 338, y: 333 },\n        },\n        {\n          id: 'node-6',\n          data: { cluster: 'b' },\n          style: { x: 365, y: 91 },\n        },\n      ],\n      edges: [\n        {\n          source: 'node-0',\n          target: 'node-2',\n        },\n        {\n          source: 'node-1',\n          target: 'node-2',\n        },\n        {\n          source: 'node-2',\n          target: 'node-3',\n        },\n        {\n          source: 'node-3',\n          target: 'node-4',\n        },\n        {\n          source: 'node-3',\n          target: 'node-5',\n        },\n        {\n          source: 'node-3',\n          target: 'node-6',\n        },\n      ],\n    },\n    node: {\n      style: { labelText: (d) => d.id },\n      palette: { field: 'cluster', color: ['#7e3feb', '#ffa940'] },\n    },\n    behaviors: ['drag-canvas', 'drag-element'],\n    plugins: [\n      'grid-line',\n      {\n        type: 'hull',\n        key: 'hull-a',\n        members: ['node-0', 'node-1', 'node-2', 'node-3'],\n        labelText: 'hull-a',\n        fill: '#7e3feb',\n        stroke: '#7e3feb',\n        fillOpacity: 0.1,\n        strokeOpacity: 1,\n        labelFill: '#fff',\n        labelPadding: 2,\n        labelBackgroundFill: '#7e3feb',\n        labelBackgroundRadius: 5,\n      },\n    ],\n  },\n  { width: 600, height: 450 },\n  (gui, graph) => {\n    const options = {\n      type: 'hull',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      concavity: Infinity,\n      corner: 'rounded',\n      padding: 10,\n      // style\n      fill: '#7e3feb',\n      stroke: '#7e3feb',\n      fillOpacity: 0.1,\n      strokeOpacity: 1,\n      // label\n      label: true,\n      labelCloseToPath: true,\n      labelAutoRotate: true,\n      labelOffsetX: 0,\n      labelOffsetY: 0,\n      labelPlacement: 'bottom',\n    };\n\n    const optionFolder = gui.addFolder('Hull Options');\n    optionFolder.add(options, 'type').disable();\n    optionFolder.add(options, 'concavity', 0, 200, 1);\n    optionFolder.add(options, 'corner', ['rounded', 'smooth', 'sharp']);\n    optionFolder.add(options, 'padding', 0, 20, 1);\n    optionFolder.addColor(options, 'fill');\n    optionFolder.addColor(options, 'stroke');\n    optionFolder.add(options, 'fillOpacity', 0, 1, 0.1);\n    optionFolder.add(options, 'strokeOpacity', 0, 1, 0.1);\n    optionFolder.add(options, 'label');\n    optionFolder.add(options, 'labelCloseToPath');\n    optionFolder.add(options, 'labelAutoRotate');\n    optionFolder.add(options, 'labelOffsetX', 0, 20, 1);\n    optionFolder.add(options, 'labelOffsetY', 0, 20, 1);\n    optionFolder.add(options, 'labelPlacement', ['left', 'right', 'top', 'bottom', 'center']);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'hull-a',\n        [property]: value,\n      });\n      graph.render();\n    });\n\n    const apiConfig = {\n      member: 'node-1',\n    };\n    const apiFolder = gui.addFolder('Hull API');\n    const instance = graph.getPluginInstance('hull-a');\n    apiFolder.add(\n      apiConfig,\n      'member',\n      new Array(7).fill(0).map((_, index) => `node-${index}`),\n    );\n    apiFolder.add({ addMember: () => instance.addMember(apiConfig.member) }, 'addMember').name('add member');\n    apiFolder\n      .add({ removeMember: () => instance.removeMember(apiConfig.member) }, 'removeMember')\n      .name('remove member');\n    apiFolder\n      .add({ removeMember: () => alert('Members in Hull-a: ' + instance.getMember()) }, 'removeMember')\n      .name('get member');\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/legend.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        {\n          id: 'node-1',\n          type: 'circle',\n          data: { cluster: 'node-type1' },\n        },\n        {\n          id: 'node-2',\n          type: 'rect',\n          data: { cluster: 'node-type2' },\n        },\n        {\n          id: 'node-3',\n          type: 'triangle',\n          data: { cluster: 'node-type3' },\n        },\n        {\n          id: 'node-4',\n          type: 'diamond',\n          data: { cluster: 'node-type4' },\n        },\n      ],\n      edges: [\n        {\n          source: 'node-1',\n          target: 'node-2',\n          data: { cluster: 'edge-type1' },\n        },\n        {\n          source: 'node-1',\n          target: 'node-4',\n          data: { cluster: 'edge-type2' },\n        },\n        {\n          source: 'node-3',\n          target: 'node-4',\n        },\n        {\n          source: 'node-2',\n          target: 'node-4',\n          data: { cluster: 'edge-type3' },\n        },\n      ],\n    },\n    layout: { type: 'grid', cols: 2 },\n    node: {\n      style: { size: 24 },\n      palette: {\n        field: 'cluster',\n      },\n    },\n    behaviors: ['drag-canvas'],\n    plugins: [\n      'grid-line',\n      {\n        type: 'legend',\n        key: 'legend',\n        nodeField: 'cluster',\n        edgeField: 'cluster',\n        itemLabelFontSize: 12,\n      },\n    ],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      type: 'legend',\n      trigger: 'hover',\n      position: 'bottom',\n      itemSpacing: 4,\n      rowPadding: 10,\n      colPadding: 10,\n      itemMarkerSize: 16,\n      itemLabelFontSize: 12,\n    };\n    const optionFolder = gui.addFolder('Legend Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'trigger', ['hover', 'click']);\n    optionFolder.add(options, 'position', ['left', 'right', 'top', 'bottom']);\n    optionFolder.add(options, 'itemSpacing', 0, 20, 1);\n    optionFolder.add(options, 'colPadding', 0, 20, 1);\n    optionFolder.add(options, 'rowPadding', 0, 20, 1);\n    optionFolder.add(options, 'itemMarkerSize', 12, 20, 1);\n    optionFolder.add(options, 'itemLabelFontSize', 12, 20, 1);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'legend',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/minimap.md",
    "content": "minimap.md\n\n```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: Array.from({ length: 50 }).map((_, i) => ({\n        id: `node-${i}`,\n        x: Math.random() * 500,\n        y: Math.random() * 300,\n      })),\n      edges: Array.from({ length: 100 }).map((_, i) => ({\n        id: `edge-${i}`,\n        source: `node-${Math.floor(Math.random() * 50)}`,\n        target: `node-${Math.floor(Math.random() * 50)}`,\n      })),\n    },\n    node: { style: { fill: '#7e3feb' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    layout: { type: 'force' },\n    behaviors: ['drag-canvas'],\n    plugins: [{ type: 'minimap', key: 'minimap', size: [240, 160], position: 'right-bottom' }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      type: 'minimap',\n      width: 240,\n      height: 160,\n      shape: 'key',\n      padding: 10,\n      position: 'right-bottom',\n      maskStyleBorder: '1px solid #ddd',\n      maskStyleBackground: 'rgba(0, 0, 0, 0.1)',\n      containerStyleBorder: '1px solid #ddd',\n      containerStyleBackground: '#fff',\n      delay: 128,\n    };\n    const optionFolder = gui.addFolder('Minimap Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder\n      .add(options, 'width', 100, 500, 1)\n      .listen()\n      .onChange((value) => {\n        graph.updatePlugin({\n          key: 'minimap',\n          size: [value, options.height],\n        });\n        graph.render();\n      });\n    optionFolder\n      .add(options, 'height', 100, 500, 1)\n      .listen()\n      .onChange((value) => {\n        graph.updatePlugin({\n          key: 'minimap',\n          size: [options.width, value],\n        });\n        graph.render();\n      });\n    optionFolder\n      .add(options, 'shape', ['key'])\n      .listen()\n      .onChange((value) => {\n        graph.updatePlugin({\n          key: 'minimap',\n          shape: value,\n        });\n        graph.render();\n      });\n    optionFolder\n      .add(options, 'padding', 0, 50, 1)\n      .listen()\n      .onChange((value) => {\n        graph.updatePlugin({\n          key: 'minimap',\n          padding: value,\n        });\n        graph.render();\n      });\n    optionFolder\n      .add(options, 'position', ['right-bottom', 'left-bottom', 'right-top', 'left-top'])\n      .listen()\n      .onChange((value) => {\n        graph.updatePlugin({\n          key: 'minimap',\n          position: value,\n        });\n        graph.render();\n      });\n    optionFolder\n      .addColor(options, 'maskStyleBorder')\n      .listen()\n      .onChange((value) => {\n        graph.updatePlugin({\n          key: 'minimap',\n          maskStyle: { ...options.maskStyle, border: value },\n        });\n        graph.render();\n      });\n    optionFolder\n      .addColor(options, 'maskStyleBackground')\n      .listen()\n      .onChange((value) => {\n        graph.updatePlugin({\n          key: 'minimap',\n          maskStyle: { ...options.maskStyle, background: value },\n        });\n        graph.render();\n      });\n    optionFolder\n      .addColor(options, 'containerStyleBorder')\n      .listen()\n      .onChange((value) => {\n        graph.updatePlugin({\n          key: 'minimap',\n          containerStyle: { ...options.containerStyle, border: value },\n        });\n        graph.render();\n      });\n    optionFolder\n      .addColor(options, 'containerStyleBackground')\n      .listen()\n      .onChange((value) => {\n        graph.updatePlugin({\n          key: 'minimap',\n          containerStyle: { ...options.containerStyle, background: value },\n        });\n        graph.render();\n      });\n    optionFolder\n      .add(options, 'delay', 0, 500, 1)\n      .listen()\n      .onChange((value) => {\n        graph.updatePlugin({\n          key: 'minimap',\n          delay: value,\n        });\n        graph.render();\n      });\n\n    // Update the maskStyle and containerStyle in the options object\n    Object.defineProperty(options, 'maskStyle', {\n      get: () => ({\n        border: options.maskStyleBorder,\n        background: options.maskStyleBackground,\n      }),\n      set: (value) => {\n        options.maskStyleBorder = value.border;\n        options.maskStyleBackground = value.background;\n      },\n    });\n\n    Object.defineProperty(options, 'containerStyle', {\n      get: () => ({\n        border: options.containerStyleBorder,\n        background: options.containerStyleBackground,\n      }),\n      set: (value) => {\n        options.containerStyleBorder = value.border;\n        options.containerStyleBackground = value.background;\n      },\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/snapline.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-0' },\n        { id: 'node-1' },\n        { id: 'node-2' },\n        { id: 'node-3' },\n        { id: 'node-4' },\n        { id: 'node-5' },\n      ],\n      edges: [\n        { source: 'node-0', target: 'node-1' },\n        { source: 'node-0', target: 'node-2' },\n        { source: 'node-0', target: 'node-3' },\n        { source: 'node-0', target: 'node-4' },\n        { source: 'node-1', target: 'node-0' },\n        { source: 'node-2', target: 'node-0' },\n        { source: 'node-3', target: 'node-0' },\n        { source: 'node-4', target: 'node-0' },\n        { source: 'node-5', target: 'node-0' },\n      ],\n    },\n    layout: { type: 'grid' },\n    behaviors: ['drag-canvas', 'drag-element'],\n    plugins: [\n      { type: 'grid-line', key: 'grid-line', size: 30 },\n      {\n        type: 'snapline',\n        key: 'snapline',\n        tolerance: 5,\n        offset: 20,\n        verticalLineStyle: { stroke: '#F08F56', lineWidth: 2 },\n        horizontalLineStyle: { stroke: '#17C76F', lineWidth: 2 },\n      },\n    ],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      type: 'snapline',\n      tolerance: 5,\n      offset: 20,\n      autoSnap: true,\n    };\n    const optionFolder = gui.addFolder('Snapline Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'tolerance', 1, 20, 1);\n    optionFolder.add(options, 'offset', 1, 50, 1);\n    optionFolder.add(options, 'autoSnap');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'snapline',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/timebar.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: new Array(25).fill(0).map((_, index) => ({\n        id: `node-${index}`,\n        data: {\n          timestamp: new Date('2023-08-01').getTime() + (index % 5) * 3600 * 24 * 1000,\n          value: index % 10,\n          label: new Date(new Date('2023-08-01').getTime() + (index % 5) * 3600 * 24 * 1000).toLocaleString(),\n        },\n      })),\n      edges: new Array(25).fill(0).map((_, i) => ({\n        id: `edge-${i}`,\n        source: `node-${i % 12}`,\n        target: `node-${(i % 10) + 15}`,\n        data: {\n          edgeType: 'e1',\n        },\n      })),\n    },\n    layout: { type: 'grid', cols: 5 },\n    node: {\n      style: { size: 24, fill: '#7e3feb' },\n      palette: { field: 'cluster' },\n    },\n    edge: { style: { stroke: '#8b9baf' } },\n    behaviors: ['drag-canvas'],\n    plugins: [\n      'grid-line',\n      {\n        type: 'timebar',\n        key: 'timebar',\n        data: [10, 2, 3, 4, 15].map((value, index) => ({\n          time: new Date(new Date('2023-08-01').getTime() + index * 3600 * 24 * 1000),\n          value,\n          label: new Date(new Date('2023-08-01').getTime() + index * 3600 * 24 * 1000).toLocaleString(),\n        })),\n        timebarType: 'time',\n        height: 100,\n      },\n    ],\n    autoFit: 'view',\n    padding: [10, 0, 100, 0],\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const options = {\n      type: 'timebar',\n      position: 'bottom',\n      enable: true,\n      timebarType: 'time',\n      className: 'g6-timebar',\n      width: 450,\n      height: 100,\n      zIndex: 3,\n      elementTypes: ['node'],\n      mode: 'modify',\n      loop: false,\n    };\n    const optionFolder = gui.addFolder('Timebar Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'height', 40, 100, 1);\n    optionFolder.add(options, 'width', 200, 800, 1);\n    optionFolder.add(options, 'position', ['bottom', 'top']);\n    optionFolder.add(options, 'timebarType', ['time', 'chart']);\n    optionFolder.add(options, 'loop');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'timebar',\n        [property]: value,\n      });\n      graph.render();\n    });\n\n    const apiFolder = gui.addFolder('Timebar API');\n    const instance = graph.getPluginInstance('timebar');\n    apiFolder.add(instance, 'play');\n    apiFolder.add(instance, 'pause');\n    apiFolder.add(instance, 'forward');\n    apiFolder.add(instance, 'backward');\n    apiFolder.add(instance, 'reset');\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/toolbar.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-0' },\n        { id: 'node-1' },\n        { id: 'node-2' },\n        { id: 'node-3' },\n        { id: 'node-4' },\n        { id: 'node-5' },\n      ],\n      edges: [\n        { source: 'node-0', target: 'node-1' },\n        { source: 'node-0', target: 'node-2' },\n        { source: 'node-0', target: 'node-3' },\n        { source: 'node-0', target: 'node-4' },\n        { source: 'node-1', target: 'node-0' },\n        { source: 'node-2', target: 'node-0' },\n        { source: 'node-3', target: 'node-0' },\n        { source: 'node-4', target: 'node-0' },\n        { source: 'node-5', target: 'node-0' },\n      ],\n    },\n    node: { style: { fill: '#7e3feb' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    layout: { type: 'grid' },\n    behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n    plugins: [\n      {\n        type: 'toolbar',\n        key: 'toolbar',\n        position: 'top-left',\n        onClick: (item) => {\n          alert('item clicked:' + item);\n        },\n        getItems: () => {\n          // G6 内置了 9 个 icon，分别是 zoom-in、zoom-out、redo、undo、edit、delete、auto-fit、export、reset\n          return [\n            { id: 'zoom-in', value: 'zoom-in' },\n            { id: 'zoom-out', value: 'zoom-out' },\n            { id: 'redo', value: 'redo' },\n            { id: 'undo', value: 'undo' },\n            { id: 'edit', value: 'edit' },\n            { id: 'delete', value: 'delete' },\n            { id: 'auto-fit', value: 'auto-fit' },\n            { id: 'export', value: 'export' },\n            { id: 'reset', value: 'reset' },\n          ];\n        },\n      },\n    ],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      type: 'toolbar',\n      position: 'top-left',\n    };\n    const optionFolder = gui.addFolder('Toolbar Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'position', [\n      'left-top',\n      'left-bottom',\n      'right-top',\n      'right-bottom',\n      'top-left',\n      'top-right',\n      'bottom-left',\n      'bottom-right',\n    ]);\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'toolbar',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/tooltip.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-0' },\n        { id: 'node-1' },\n        { id: 'node-2' },\n        { id: 'node-3' },\n        { id: 'node-4' },\n        { id: 'node-5' },\n      ],\n      edges: [\n        { source: 'node-0', target: 'node-1' },\n        { source: 'node-0', target: 'node-2' },\n        { source: 'node-0', target: 'node-3' },\n        { source: 'node-0', target: 'node-4' },\n        { source: 'node-1', target: 'node-0' },\n        { source: 'node-2', target: 'node-0' },\n        { source: 'node-3', target: 'node-0' },\n        { source: 'node-4', target: 'node-0' },\n        { source: 'node-5', target: 'node-0' },\n      ],\n    },\n    node: { style: { fill: '#7e3feb' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    layout: { type: 'grid' },\n    behaviors: ['drag-canvas'],\n    plugins: ['grid-line', { type: 'tooltip', key: 'tooltip' }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      type: 'tooltip',\n      trigger: 'hover',\n      enable: 'always',\n      position: 'top-left',\n      enterable: false,\n    };\n    const optionFolder = gui.addFolder('Tooltip Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'trigger', ['click', 'hover']);\n    optionFolder.add(options, 'enable', ['always', 'node', 'edge']);\n    optionFolder.add(options, 'position', [\n      'top',\n      'bottom',\n      'left',\n      'right',\n      'top-left',\n      'top-right',\n      'bottom-left',\n      'bottom-right',\n    ]);\n    optionFolder.add(options, 'enterable');\n\n    optionFolder.onChange((e) => {\n      const { enable, ...rest } = e.object;\n      let enableFn = () => true;\n      if ((enable === 'node') | (enable === 'edge')) {\n        enableFn = (e) => e.targetType === enable;\n      }\n      graph.updatePlugin({\n        key: 'tooltip',\n        enable: enableFn,\n        ...rest,\n      });\n      graph.render();\n    });\n    // const apiFolder = gui.addFolder('Contextmenu API');\n    // const instance = graph.getPluginInstance('contextmenu');\n    // apiFolder.add(instance, 'hide');\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/plugins/watermark.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    data: { nodes: [{ id: 'node-1' }] },\n    node: { style: { fill: '#7e3feb' } },\n    edge: { style: { stroke: '#8b9baf' } },\n    layout: { type: 'force' },\n    behaviors: ['drag-canvas'],\n    plugins: [{ type: 'watermark', key: 'watermark', text: 'G6: Graph Visualization' }],\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      type: 'watermark',\n      width: 200,\n      height: 100,\n      opacity: 0.2,\n      rotate: Math.PI / 12,\n      text: 'G6: Graph Visualization',\n    };\n    const optionFolder = gui.addFolder('Watermark Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'width', 1, 1280, 1);\n    optionFolder.add(options, 'height', 1, 800, 1);\n    optionFolder.add(options, 'opacity', 0, 1, 0.1);\n    optionFolder.add(options, 'rotate', 0, 2 * Math.PI, Math.PI / 12);\n    optionFolder.add(options, 'text');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'watermark',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/transforms/map-node-size-centrality.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: {\n      nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n      edges: [\n        { source: 'node1', target: 'node2' },\n        { source: 'node2', target: 'node3' },\n        { source: 'node3', target: 'node4' },\n        { source: 'node4', target: 'node5' },\n        { source: 'node1', target: 'node4' },\n        { source: 'node1', target: 'node3' },\n      ],\n    },\n    node: {\n      type: 'circle',\n      style: {\n        labelText: (d) => d.id + ' - ' + d.style.size[0].toFixed(0),\n      },\n    },\n    layout: {\n      type: 'circular',\n      radius: 180,\n    },\n    behaviors: ['drag-canvas'],\n    transforms: [\n      {\n        key: 'map-node-size',\n        type: 'map-node-size',\n        centrality: {\n          type: 'pagerank',\n        },\n      },\n    ],\n  },\n  { width: 600, height: 460 },\n  (gui, graph) => {\n    const options = {\n      type: 'degree',\n    };\n    const optionFolder = gui.addFolder('Centrality Options');\n    optionFolder.add(options, 'type', ['degree', 'betweenness', 'closeness', 'eigenvector', 'pagerank']);\n    optionFolder.onChange(async ({ property, value }) => {\n      graph.updateTransform({\n        key: 'map-node-size',\n        centrality: {\n          [property]: value,\n        },\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/api/transforms/map-node-size-scale.md",
    "content": "```js | ob { pin: false }\ncreateGraph(\n  {\n    autoFit: 'center',\n    data: {\n      nodes: [\n        { id: 'node1' },\n        { id: 'node2' },\n        { id: 'node3' },\n        { id: 'node4' },\n        { id: 'node5' },\n        { id: 'node6' },\n        { id: 'node7' },\n      ],\n      edges: [\n        { source: 'node1', target: 'node2' },\n        { source: 'node1', target: 'node3' },\n        { source: 'node1', target: 'node4' },\n        { source: 'node2', target: 'node5' },\n        { source: 'node3', target: 'node6' },\n        { source: 'node4', target: 'node7' },\n      ],\n    },\n    node: {\n      type: 'circle',\n      style: {\n        labelText: (d) => d.id + ' - ' + d.style.size[0].toFixed(0),\n      },\n    },\n    layout: {\n      type: 'antv-dagre',\n    },\n    behaviors: ['drag-canvas'],\n    transforms: [\n      {\n        key: 'map-node-size',\n        type: 'map-node-size',\n        centrality: {\n          type: 'degree',\n        },\n        scale: 'log',\n      },\n    ],\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const options = {\n      scale: 'log',\n    };\n    const optionFolder = gui.addFolder('MapNodeSize Options');\n    optionFolder.add(options, 'scale', ['log', 'linear', 'pow', 'sqrt']);\n    optionFolder.onChange(async ({ property, value }) => {\n      graph.updateTransform({\n        key: 'map-node-size',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n"
  },
  {
    "path": "packages/site/common/manual/core-concept/animation/ant-line.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { register, Line, Graph } from '@antv/g6';\n\nclass AntLine extends Line {\n  onCreate() {\n    this.shapeMap.key.animate([{ lineDashOffset: 20 }, { lineDashOffset: 0 }], {\n      duration: 500,\n      iterations: Infinity,\n    });\n  }\n}\n\nregister('edge', 'ant-line', AntLine);\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 50,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 25, y: 25 } },\n      { id: 'node-2', style: { x: 175, y: 25 } },\n    ],\n    edges: [{ source: 'node-1', target: 'node-2', style: { lineDash: [10, 10] } }],\n  },\n  edge: {\n    type: 'ant-line',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/core-concept/animation/breathing-circle.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { register, Circle, Graph } from '@antv/g6';\n\nclass BreathingCircle extends Circle {\n  onCreate() {\n    this.shapeMap.halo.animate([{ lineWidth: 5 }, { lineWidth: 10 }], {\n      duration: 1000,\n      iterations: Infinity,\n      direction: 'alternate',\n    });\n  }\n}\n\nregister('node', 'breathing-circle', BreathingCircle);\n\nconst graph = new Graph({\n  container: 'container',\n  width: 50,\n  height: 50,\n  data: {\n    nodes: [{ id: 'node-1', style: { x: 25, y: 25 } }],\n  },\n  node: {\n    type: 'breathing-circle',\n    style: {\n      halo: true,\n      haloLineWidth: 5,\n    },\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/core-concept/palette/continuous-palette.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 50,\n  data: {\n    nodes: new Array(6).fill(0).map((_, i) => ({ id: `node-${i}`, data: { value: (i + 1) * 20 } })),\n  },\n  layout: { type: 'grid', cols: 6 },\n  node: {\n    palette: {\n      type: 'value',\n      field: 'value',\n      color: (value) => `rgb(${value * 255}, 0, 0)`,\n    },\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/core-concept/palette/default-config.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 100,\n  data: {\n    nodes: new Array(30).fill(0).map((_, i) => ({ id: `node-${i}` })),\n  },\n  layout: { type: 'grid', cols: 10, rows: 3 },\n  node: {\n    palette: 'spectral',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/core-concept/palette/standard-config.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 50,\n  data: {\n    nodes: new Array(6).fill(0).map((_, i) => ({ id: `node-${i}`, data: { category: ['A', 'B', 'C'][i % 3] } })),\n  },\n  layout: { type: 'grid', cols: 6 },\n  node: {\n    palette: {\n      type: 'group',\n      field: 'category',\n      color: 'tableau',\n    },\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/custom-extension/animation/composite-animation-1.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 50,\n  data: {\n    nodes: [{ id: 'node-1', style: { x: 25, y: 25, size: 20 } }],\n  },\n  node: {\n    animation: {\n      update: [\n        {\n          fields: ['x', 'y'],\n        },\n        { fields: ['r'], shape: 'key' },\n      ],\n    },\n  },\n});\n\ngraph.draw().then(() => {\n  graph.updateNodeData([{ id: 'node-1', style: { x: 175, size: 40 } }]);\n  graph.draw();\n});\n```\n"
  },
  {
    "path": "packages/site/common/manual/custom-extension/animation/composite-animation-2.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 50,\n  data: {\n    nodes: [{ id: 'node-1', style: { x: 25, y: 25, size: 20 } }],\n  },\n  node: {\n    animation: {\n      update: [\n        {\n          fields: ['x', 'y'],\n        },\n        { fields: ['r', 'fill'], shape: 'key' },\n      ],\n    },\n  },\n});\n\ngraph.draw().then(() => {\n  graph.updateNodeData([{ id: 'node-1', style: { x: 175, size: 40, fill: 'pink' } }]);\n  graph.draw();\n});\n```\n"
  },
  {
    "path": "packages/site/common/manual/custom-extension/animation/implement-animation.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  width: 50,\n  height: 50,\n  container,\n  data: {\n    nodes: [{ id: 'node-1', style: { x: 25, y: 25, size: 20 } }],\n  },\n  node: {\n    animation: {\n      update: [\n        {\n          fields: ['r'],\n          shape: 'key',\n        },\n      ],\n    },\n  },\n});\n\ngraph.draw().then(() => {\n  graph.updateNodeData([{ id: 'node-1', style: { size: 40 } }]);\n  graph.draw();\n});\n```\n"
  },
  {
    "path": "packages/site/common/manual/custom-extension/behavior/implement-behaviors.md",
    "content": "```js | ob { pin: false, inject: true }\n(async () => {\n  const { BaseBehavior, CanvasEvent, register, ExtensionCategory, Graph } = window.g6;\n\n  class ClickAddNode extends BaseBehavior {\n    constructor(context, options) {\n      super(context, options);\n\n      const { graph } = this.context;\n      graph.on(CanvasEvent.CLICK, (event) => {\n        const { layerX, layerY } = event.nativeEvent;\n        graph.addNodeData([\n          {\n            id: 'node-' + Date.now(),\n            style: { x: layerX, y: layerY, fill: options.fill },\n          },\n        ]);\n        graph.draw();\n      });\n    }\n  }\n\n  register(ExtensionCategory.BEHAVIOR, 'click-add-node', ClickAddNode);\n\n  const wrapEl = await createGraph(\n    {\n      data: {\n        nodes: [],\n      },\n      behaviors: [\n        {\n          type: 'click-add-node',\n          key: 'click-add-node',\n          fill: 'red',\n        },\n      ],\n    },\n    { width: 600, height: 300 },\n    (gui, graph) => {\n      const options = {\n        key: 'click-add-node',\n        type: 'click-add-node',\n        fill: 'red',\n      };\n      const optionFolder = gui.addFolder('ClickAddNode Options');\n      optionFolder.add(options, 'fill', ['red', 'black', 'blue', 'green', 'yellow', 'purple']);\n\n      optionFolder.onChange(({ property, value }) => {\n        graph.updateBehavior({\n          key: 'click-add-node',\n          [property]: value,\n        });\n        graph.render();\n      });\n    },\n  );\n\n  return wrapEl;\n})();\n```\n"
  },
  {
    "path": "packages/site/common/manual/custom-extension/layout/iterative-layout.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph, BaseLayout, register, ExtensionCategory } from '@antv/g6';\n\nclass TickTockLayout extends BaseLayout {\n  id = 'tick-tock-layout';\n\n  async execute(data, options) {\n    const { onTick } = { ...this.options, ...options };\n\n    this.tickCount = 0;\n    this.data = data;\n\n    this.promise = new Promise((resolve) => {\n      this.resolve = resolve;\n    });\n\n    this.timer = window.setInterval(() => {\n      onTick(this.simulateTick());\n      if (this.tickCount === 10) this.stop();\n    }, 200);\n\n    await this.promise;\n\n    return this.simulateTick();\n  }\n\n  simulateTick = () => {\n    const x = this.tickCount++ % 2 === 0 ? 50 : 150;\n\n    return {\n      nodes: (this?.data?.nodes || []).map((node, index) => ({\n        id: node.id,\n        style: { x, y: (index + 1) * 30 },\n      })),\n    };\n  };\n\n  tick = () => {\n    return this.simulateTick();\n  };\n\n  stop = () => {\n    clearInterval(this.timer);\n    this.resolve?.();\n  };\n}\n\nregister(ExtensionCategory.LAYOUT, 'tick-tock', TickTockLayout);\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 200,\n  animation: true,\n  data: {\n    nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  },\n  layout: {\n    type: 'tick-tock',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/custom-extension/layout/non-iterative-layout.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph, BaseLayout, register, ExtensionCategory } from '@antv/g6';\n\nclass DiagonalLayout extends BaseLayout {\n  id = 'diagonal-layout';\n\n  async execute(data) {\n    const { nodes = [] } = data;\n    return {\n      nodes: nodes.map((node, index) => ({\n        id: node.id,\n        style: {\n          x: 50 * index + 25,\n          y: 50 * index + 25,\n        },\n      })),\n    };\n  }\n}\n\nregister(ExtensionCategory.LAYOUT, 'diagonal', DiagonalLayout);\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 200,\n  data: {\n    nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }],\n  },\n  layout: {\n    type: 'diagonal',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/custom-extension/plugin/implement-plugin.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { BasePlugin, Graph, register, ExtensionCategory } from '@antv/g6';\n\nclass RemoteDataSource extends BasePlugin {\n  constructor(context, options) {\n    super(context, options);\n    this.loadData();\n  }\n\n  async loadData() {\n    // mock remote data\n    const data = {\n      nodes: [\n        { id: 'node-1', style: { x: 25, y: 50 } },\n        { id: 'node-2', style: { x: 175, y: 50 } },\n      ],\n      edges: [{ source: 'node-1', target: 'node-2' }],\n    };\n\n    const { graph } = this.context;\n    graph.setData(data);\n    await graph.render();\n  }\n}\n\nregister(ExtensionCategory.PLUGIN, 'remote-data-source', RemoteDataSource);\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  plugins: ['remote-data-source'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/custom-extension/transform/circular-radial-labels.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph, BaseTransform, register, ExtensionCategory } from '@antv/g6';\n\n// 目前circular布局没有暴露方法可以获取布局中心，这里简单处理先固定一个\nconst circularCenter = [300, 300];\n\n// 下面的函数 G6 没有暴露出来，先自行声明\nfunction subtract(a, b) {\n  return a.map((v, i) => v - b[i]);\n}\nfunction rad(a) {\n  const [x, y] = a;\n  if (!x && !y) return 0;\n  return Math.atan2(y, x);\n}\nfunction rad2deg(rad) {\n  return rad * (180 / Math.PI);\n}\n\nclass CircularRadialLabels extends BaseTransform {\n  static defaultOptions = {\n    offset: 5,\n  };\n  constructor(context, options) {\n    super(context, Object.assign({}, CircularRadialLabels.defaultOptions, options));\n  }\n  get center() {\n    return circularCenter;\n  }\n  afterLayout() {\n    const { graph, model } = this.context;\n    const data = model.getData();\n    data.nodes?.forEach((datum) => {\n      const radian = rad(subtract([datum.style.x, datum.style.y], this.center));\n      const isLeft = Math.abs(radian) > Math.PI / 2;\n      const isLeaf = !datum.children || datum.children.length === 0;\n      const nodeId = datum.id;\n      const node = this.context.element?.getElement(nodeId);\n      if (!node || !node.isVisible()) return;\n\n      const nodeHalfWidth = graph.getElementRenderStyle(nodeId).size / 2;\n      const offset = (isLeaf ? 1 : -1) * (nodeHalfWidth + this.options.offset);\n\n      const labelTransform = [\n        ['translate', offset * Math.cos(radian), offset * Math.sin(radian)],\n        ['rotate', isLeft ? rad2deg(radian) + 180 : rad2deg(radian)],\n      ];\n\n      model.updateNodeData([\n        {\n          id: datum.id,\n          style: {\n            labelTextAlign: isLeft === isLeaf ? 'right' : 'left',\n            labelTextBaseline: 'middle',\n            labelTransform,\n          },\n        },\n      ]);\n    });\n\n    graph.draw();\n  }\n}\n\nregister(ExtensionCategory.TRANSFORM, 'circular-radial-labels', CircularRadialLabels);\n\nconst data = {\n  nodes: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }, { id: '5' }, { id: '6' }],\n  edges: [\n    { source: '1', target: '2' },\n    { source: '2', target: '3' },\n    { source: '3', target: '4' },\n    { source: '4', target: '5' },\n    { source: '5', target: '6' },\n    { source: '6', target: '1' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 400,\n  autoFit: 'center',\n  data,\n  node: {\n    style: {\n      labelText: (d) => 'label' + d.id,\n      size: 30,\n    },\n  },\n  layout: {\n    type: 'circular',\n    width: 200,\n    center: circularCenter,\n    preLayout: false, // 不能是渲染前布局，否则不生效\n  },\n  transforms: ['circular-radial-labels'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/custom-extension/transform/hide-free-node.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph, BaseTransform, register, ExtensionCategory } from '@antv/g6';\n\nclass HideFreeNode extends BaseTransform {\n  beforeDraw(input, context) {\n    const { model } = this.context;\n    const { add, update, remove } = input;\n\n    add.nodes.forEach((nodeData, nodeId) => {\n      // 获取节点的相关连线\n      const edges = model.getRelatedEdgesData(nodeId);\n      // 没有任何连线的的节点则从add里面移除，添加到remove里面\n      if (!edges.length) {\n        add.nodes.delete(nodeId);\n        remove.nodes.set(nodeId, nodeData);\n      }\n    });\n\n    return input;\n  }\n}\n\nregister(ExtensionCategory.TRANSFORM, 'hide-free-node', HideFreeNode);\n\nconst data = {\n  nodes: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }, { id: '5' }],\n  edges: [\n    { source: '1', target: '2' },\n    { source: '2', target: '3' },\n    { source: '3', target: '5' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 400,\n  autoFit: 'center',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  layout: {\n    type: 'antv-dagre',\n    rankdir: 'LR',\n  },\n  transforms: ['hide-free-node'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/feature/treeToGraphData.md",
    "content": "```ts\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nconst data = {\n  id: 'root',\n  children: [\n    { id: 'node1', children: [{ id: 'node1-1' }, { id: 'node1-2' }] },\n    { id: 'node2', children: [{ id: 'node2-1' }, { id: 'node2-2' }] },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'compact-box',\n    direction: 'TB',\n  },\n  data: treeToGraphData(data),\n  edge: {\n    type: 'cubic-vertical',\n  },\n});\n\ngraph.render();\n```\n\n```js | ob { pin:false , inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 200,\n  autoFit: 'view',\n  data: g6.treeToGraphData({\n    id: 'root',\n    children: [\n      { id: 'node1', children: [{ id: 'node1-1' }, { id: 'node1-2' }] },\n      { id: 'node2', children: [{ id: 'node2-1' }, { id: 'node2-2' }] },\n    ],\n  }),\n  layout: {\n    type: 'compact-box',\n    direction: 'TB',\n  },\n  node: {\n    style: {\n      ports: [{ placement: 'center' }],\n    },\n  },\n  edge: {\n    type: 'cubic-vertical',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/feature/webpack4.md",
    "content": "```js\nmodule.exports = {\n  entry: './src/index.js',\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: 'index.js',\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        use: {\n          loader: 'babel-loader',\n          options: {\n            presets: ['@babel/preset-env'],\n          },\n        },\n      },\n      {\n        test: /\\.js$/,\n        loader: '@open-wc/webpack-import-meta-loader',\n      },\n    ],\n  },\n  mode: 'production',\n};\n```\n"
  },
  {
    "path": "packages/site/common/manual/getting-started/extensions/palettes.md",
    "content": "- spectral\n\n<div style=\"display: flex; width: 100%; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(158, 1, 66);\"></div><div style=\"background: rgb(213, 62, 79);\"></div><div style=\"background: rgb(244, 109, 67);\"></div><div style=\"background: rgb(253, 174, 97);\"></div><div style=\"background: rgb(254, 224, 139);\"></div><div style=\"background: rgb(255, 255, 191);\"></div><div style=\"background: rgb(230, 245, 152);\"></div><div style=\"background: rgb(171, 221, 164);\"></div><div style=\"background: rgb(102, 194, 165);\"></div><div style=\"background: rgb(50, 136, 189);\"></div><div style=\"background: rgb(94, 79, 162);\"></div></div>\n\n- tableau\n\n<div style=\"display: flex; width: 100%; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(78, 121, 167);\"></div><div style=\"background: rgb(242, 142, 44);\"></div><div style=\"background: rgb(225, 87, 89);\"></div><div style=\"background: rgb(118, 183, 178);\"></div><div style=\"background: rgb(89, 161, 79);\"></div><div style=\"background: rgb(237, 201, 73);\"></div><div style=\"background: rgb(175, 122, 161);\"></div><div style=\"background: rgb(255, 157, 167);\"></div><div style=\"background: rgb(156, 117, 95);\"></div><div style=\"background: rgb(186, 176, 171);\"></div></div>\n\n- oranges\n\n<div style=\"display: flex; width: 100%; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(255, 245, 235);\"></div><div style=\"background: rgb(254, 230, 206);\"></div><div style=\"background: rgb(253, 208, 162);\"></div><div style=\"background: rgb(253, 174, 107);\"></div><div style=\"background: rgb(253, 141, 60);\"></div><div style=\"background: rgb(241, 105, 19);\"></div><div style=\"background: rgb(217, 72, 1);\"></div><div style=\"background: rgb(166, 54, 3);\"></div><div style=\"background: rgb(127, 39, 4);\"></div></div>\n\n- greens\n\n<div style=\"display: flex; width: 100%; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(247, 252, 245);\"></div><div style=\"background: rgb(229, 245, 224);\"></div><div style=\"background: rgb(199, 233, 192);\"></div><div style=\"background: rgb(161, 217, 155);\"></div><div style=\"background: rgb(116, 196, 118);\"></div><div style=\"background: rgb(65, 171, 93);\"></div><div style=\"background: rgb(35, 139, 69);\"></div><div style=\"background: rgb(0, 109, 44);\"></div><div style=\"background: rgb(0, 68, 27);\"></div></div>\n\n- blues\n\n<div style=\"display: flex; width: 100%; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(247, 251, 255);\"></div><div style=\"background: rgb(222, 235, 247);\"></div><div style=\"background: rgb(198, 219, 239);\"></div><div style=\"background: rgb(158, 202, 225);\"></div><div style=\"background: rgb(107, 174, 214);\"></div><div style=\"background: rgb(66, 146, 198);\"></div><div style=\"background: rgb(33, 113, 181);\"></div><div style=\"background: rgb(8, 81, 156);\"></div><div style=\"background: rgb(8, 48, 107);\"></div></div>\n"
  },
  {
    "path": "packages/site/common/manual/getting-started/quick-start/simple-graph.md",
    "content": "```js | ob { pin: false, inject: true }\nfetch('https://assets.antv.antgroup.com/g6/graph.json')\n  .then((res) => res.json())\n  .then((data) =>\n    createGraph(\n      {\n        data,\n        autoFit: 'view',\n        animation: false,\n        node: {\n          style: {\n            size: 10,\n          },\n          palette: {\n            field: 'group',\n            color: 'tableau',\n          },\n        },\n        layout: {\n          type: 'd3-force',\n          animation: false,\n          manyBody: {},\n          x: {},\n          y: {},\n        },\n        behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n      },\n      { width: 500, height: 500 },\n    ),\n  );\n```\n"
  },
  {
    "path": "packages/site/common/manual/getting-started/step-by-step/behaviors.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 200,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 50, y: 50 } },\n      { id: 'node-2', style: { x: 150, y: 50 } },\n    ],\n    edges: [{ source: 'node-1', target: 'node-2' }],\n  },\n  node: {\n    type: (datum) => (datum.id === 'node-1' ? 'circle' : 'rect'),\n    style: {\n      fill: 'pink',\n      size: 20,\n    },\n  },\n  edge: {\n    style: {\n      stroke: 'lightgreen',\n    },\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/getting-started/step-by-step/create-chart.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 50, y: 50 } },\n      { id: 'node-2', style: { x: 150, y: 50 } },\n    ],\n    edges: [{ source: 'node-1', target: 'node-2' }],\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/getting-started/step-by-step/elements-1.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 50, y: 50 } },\n      { id: 'node-2', style: { x: 150, y: 50 } },\n    ],\n    edges: [{ source: 'node-1', target: 'node-2' }],\n  },\n  node: {\n    style: {\n      fill: 'pink',\n    },\n  },\n  edge: {\n    style: {\n      stroke: 'lightgreen',\n    },\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/getting-started/step-by-step/elements-2.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 50, y: 50 } },\n      { id: 'node-2', style: { x: 150, y: 50 } },\n    ],\n    edges: [{ source: 'node-1', target: 'node-2' }],\n  },\n  node: {\n    type: (datum) => (datum.id === 'node-1' ? 'circle' : 'rect'),\n    style: {\n      fill: 'pink',\n      size: 20,\n    },\n  },\n  edge: {\n    style: {\n      stroke: 'lightgreen',\n    },\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/getting-started/step-by-step/layout.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 200,\n  data: {\n    nodes: Array.from({ length: 10 }).map((_, i) => ({ id: `node-${i}` })),\n    edges: Array.from({ length: 9 }).map((_, i) => ({ source: `node-0`, target: `node-${i + 1}` })),\n  },\n  node: {\n    style: {\n      size: 20,\n      fill: 'pink',\n    },\n  },\n  edge: {\n    style: {\n      stroke: 'lightgreen',\n    },\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n  layout: {\n    type: 'd3-force',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/getting-started/step-by-step/palette.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 200,\n  data: {\n    nodes: Array.from({ length: 10 }).map((_, i) => ({\n      id: `node-${i}`,\n      data: { category: i === 0 ? 'central' : 'around' },\n    })),\n    edges: Array.from({ length: 9 }).map((_, i) => ({ source: `node-0`, target: `node-${i + 1}` })),\n  },\n  node: {\n    style: {\n      size: 20,\n    },\n    palette: {\n      field: 'category',\n      color: 'tableau',\n    },\n  },\n  edge: {\n    style: {\n      stroke: 'lightgreen',\n    },\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n  layout: {\n    type: 'd3-force',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/getting-started/step-by-step/plugins-1.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 200,\n  data: {\n    nodes: Array.from({ length: 10 }).map((_, i) => ({\n      id: `node-${i}`,\n      data: { category: i === 0 ? 'central' : 'around' },\n    })),\n    edges: Array.from({ length: 9 }).map((_, i) => ({ source: `node-0`, target: `node-${i + 1}` })),\n  },\n  node: {\n    style: {\n      size: 20,\n    },\n    palette: {\n      field: 'category',\n      color: 'tableau',\n    },\n  },\n  edge: {\n    style: {\n      stroke: 'lightgreen',\n    },\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n  layout: {\n    type: 'd3-force',\n  },\n  plugins: ['grid-line'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/manual/getting-started/step-by-step/plugins-2.md",
    "content": "```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 200,\n  data: {\n    nodes: Array.from({ length: 10 }).map((_, i) => ({\n      id: `node-${i}`,\n      data: { category: i === 0 ? 'central' : 'around' },\n    })),\n    edges: Array.from({ length: 9 }).map((_, i) => ({ source: `node-0`, target: `node-${i + 1}` })),\n  },\n  node: {\n    style: {\n      size: 20,\n    },\n    palette: {\n      field: 'category',\n      color: 'tableau',\n    },\n  },\n  edge: {\n    style: {\n      stroke: 'lightgreen',\n    },\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n  layout: {\n    type: 'd3-force',\n  },\n  plugins: [{ type: 'grid-line', follow: true }],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/common/react-snippet-strict.md",
    "content": "```tsx\nimport type { GraphOptions } from '@antv/g6';\nimport { Graph as G6Graph } from '@antv/g6';\nimport { useEffect, useRef } from 'react';\n\nexport interface GraphProps {\n  options: GraphOptions;\n  onRender?: (graph: G6Graph) => void;\n  onDestroy?: () => void;\n}\n\nexport const Graph = (props: GraphProps) => {\n  const { options, onRender, onDestroy } = props;\n  const graphRef = useRef<G6Graph>();\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const graph = new G6Graph({ container: containerRef.current! });\n    graphRef.current = graph;\n\n    return () => {\n      const graph = graphRef.current;\n      if (graph) {\n        graph.destroy();\n        onDestroy?.();\n        graphRef.current = undefined;\n      }\n    };\n  }, []);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    const graph = graphRef.current;\n\n    if (!options || !container || !graph || graph.destroyed) return;\n\n    graph.setOptions(options);\n    graph\n      .render()\n      .then(() => onRender?.(graph))\n      // eslint-disable-next-line no-console\n      .catch((error) => console.debug(error));\n  }, [options]);\n\n  return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;\n};\n```\n"
  },
  {
    "path": "packages/site/common/react-snippet.md",
    "content": "<iframe src=\"https://stackblitz.com/edit/g6-in-react?embed=1&file=src/App.tsx&theme=light\"\n     style=\"width:100%; height: 500px; border:0; border-radius: 4px; overflow:hidden;\"\n   ></iframe>\n\n```jsx\nimport { Graph } from '@antv/g6';\nimport { useEffect, useRef } from 'react';\n\nexport default () => {\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const graph = new Graph({\n      container: containerRef.current!,\n      width: 500,\n      height: 500,\n      data: {\n        nodes: [\n          {\n            id: 'node-1',\n            style: { x: 50, y: 100 },\n          },\n          {\n            id: 'node-2',\n            style: { x: 150, y: 100 },\n          },\n        ],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n      },\n    });\n\n    graph.render();\n  }, []);\n\n  return <div ref={containerRef} />;\n};\n```\n"
  },
  {
    "path": "packages/site/common/vue-snippet.md",
    "content": "<iframe src=\"https://stackblitz.com/edit/g6-in-vue?embed=1&file=src%2FApp.vue&theme=light\"\n     style=\"width:100%; height: 500px; border:0; border-radius: 4px; overflow:hidden;\"\n     title=\"G6 Vue\"></iframe>\n\n```html\n<template>\n  <div id=\"container\"></div>\n</template>\n\n<script setup>\n  import { onMounted } from 'vue';\n  import { Graph } from '@antv/g6';\n\n  onMounted(() => {\n    const graph = new Graph({\n      container: document.getElementById('container'),\n      width: 500,\n      height: 500,\n      data: {\n        nodes: [\n          {\n            id: 'node-1',\n            style: { x: 50, y: 100 },\n          },\n          {\n            id: 'node-2',\n            style: { x: 150, y: 100 },\n          },\n        ],\n        edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],\n      },\n    });\n\n    graph.render();\n  });\n</script>\n```\n"
  },
  {
    "path": "packages/site/docs/api/behavior.en.md",
    "content": "---\ntitle: Behavior\norder: 8\n---\n\n## Overview of Behavior\n\nBehavior is a core building block of G6, precisely defining the interaction between users and the graph. Each Behavior plugin is a highly encapsulated functional unit, integrating event listening, state management, and response handling logic for specific scenarios.\n\nG6's built-in Behaviors cover most common interaction needs and provide a flexible extension mechanism, allowing developers to create customized interaction experiences based on business scenarios. For a complete list of behavior types, configuration options, and development examples, please refer to the [Behavior Overview](/en/manual/behavior/overview) section.\n\n## API Reference\n\n### Graph.getBehaviors()\n\nGet all configured behaviors in the current graph.\n\n```typescript\ngetBehaviors(): BehaviorOptions;\n```\n\n**Return Value**\n\n- **Type**: [BehaviorOptions](#behavioroptions)\n- **Description**: All configured behaviors in the current graph\n\n**Example**\n\n```typescript\n// Get all current behaviors\nconst behaviors = graph.getBehaviors();\nconsole.log('Current graph behaviors:', behaviors);\n```\n\n### Graph.setBehaviors(behaviors)\n\nSet the behaviors of the graph, replacing all existing behaviors.\n\n```typescript\nsetBehaviors(behaviors: BehaviorOptions | ((prev: BehaviorOptions) => BehaviorOptions)): void;\n```\n\n**Parameters**\n\n| Parameter | Description                                                                                    | Type                                                                              | Default | Required |\n| --------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------- | -------- |\n| behaviors | New behavior configuration, or a function returning new configuration based on the current one | [BehaviorOptions](#behavioroptions) \\| (prev: BehaviorOptions) => BehaviorOptions | -       | ✓        |\n\n**Note**\n\nThe set behaviors will completely replace the original ones. To add new behaviors, you can use functional updates:\n\n```typescript\ngraph.setBehaviors((behaviors) => [...behaviors, { type: 'zoom-canvas' }]);\n```\n\n**Example 1**: Set basic behaviors\n\n```typescript\n// Set basic behaviors\ngraph.setBehaviors([\n  'drag-canvas', // Drag canvas\n  'zoom-canvas', // Zoom canvas\n  'drag-element', // Drag element\n]);\n```\n\n**Example 2**: Set behaviors with configuration\n\n```typescript\ngraph.setBehaviors([\n  // String form (using default configuration)\n  'drag-canvas',\n\n  // Object form (custom configuration)\n  {\n    type: 'zoom-canvas',\n    key: 'my-zoom', // Specify a unique identifier for subsequent updates\n    sensitivity: 1.5, // Zoom sensitivity\n  },\n\n  // Enable drag only on nodes\n  {\n    type: 'drag-element',\n    key: 'drag-node-only',\n    enable: (event) => event.targetType === 'node', // Enable drag only on nodes\n  },\n]);\n```\n\n**Example 3**: Use functional updates\n\n```typescript\n// Add new behavior\ngraph.setBehaviors((currentBehaviors) => [\n  ...currentBehaviors,\n  {\n    type: 'brush-select',\n    key: 'selection-brush',\n  },\n]);\n\n// Replace specific behavior\ngraph.setBehaviors((currentBehaviors) => {\n  // Filter out existing zoom behaviors\n  const filteredBehaviors = currentBehaviors.filter((behavior) => {\n    if (typeof behavior === 'string') return behavior !== 'zoom-canvas';\n    return behavior.type !== 'zoom-canvas';\n  });\n\n  // Add new zoom behavior configuration\n  return [\n    ...filteredBehaviors,\n    {\n      type: 'zoom-canvas',\n      key: 'new-zoom',\n      enableOptimize: true,\n    },\n  ];\n});\n```\n\n### Graph.updateBehavior(behavior)\n\nUpdate the configuration of a specific behavior, identified by the `key`.\n\n```typescript\nupdateBehavior(behavior: UpdateBehaviorOption): void;\n```\n\n**Parameters**\n\n| Parameter | Description                             | Type                                          | Default | Required |\n| --------- | --------------------------------------- | --------------------------------------------- | ------- | -------- |\n| behavior  | Configuration of the behavior to update | [UpdateBehaviorOption](#updatebehavioroption) | -       | ✓        |\n\n**Note**\n\nTo update a behavior, the original behavior configuration must specify the `key` field to accurately locate and update the behavior.\n\n**Example 1**: Update behavior configuration\n\n```typescript\n// Specify key when initially setting behaviors\ngraph.setBehaviors([\n  {\n    type: 'zoom-canvas',\n    key: 'my-zoom-canvas',\n    sensitivity: 1.0,\n  },\n]);\n\n// Update behavior configuration\ngraph.updateBehavior({\n  key: 'my-zoom-canvas', // Specify the behavior to update\n  sensitivity: 2.0, // New zoom sensitivity\n  enableOptimize: true, // Add new configuration\n});\n```\n\n**Example 2**: Disable/Enable behavior\n\n```typescript\n// Set behaviors with keys\ngraph.setBehaviors([\n  {\n    type: 'drag-canvas',\n    key: 'main-drag',\n  },\n  {\n    type: 'zoom-canvas',\n    key: 'main-zoom',\n  },\n]);\n\n// Disable drag functionality\ngraph.updateBehavior({\n  key: 'main-drag',\n  enable: false,\n});\n\n// Re-enable later\nsetTimeout(() => {\n  graph.updateBehavior({\n    key: 'main-drag',\n    enable: true,\n  });\n}, 5000);\n```\n\n## Type Definitions\n\n### BehaviorOptions\n\n```typescript\ntype BehaviorOptions = (string | CustomBehaviorOption | ((this: Graph) => CustomBehaviorOption))[];\n\ntype CustomBehaviorOption = {\n  // Interaction type\n  type: string;\n\n  // Interaction key, a unique identifier for identifying and further operating this interaction\n  key?: string;\n\n  // There may be other configuration items for different types of interactions\n  [configKey: string]: any;\n};\n```\n\n### UpdateBehaviorOption\n\n```typescript\ntype UpdateBehaviorOption = {\n  // Unique identifier of the behavior to update\n  key: string;\n\n  // Other configuration items to update\n  [configKey: string]: unknown;\n};\n```\n"
  },
  {
    "path": "packages/site/docs/api/behavior.zh.md",
    "content": "---\ntitle: 交互\norder: 8\n---\n\n## 交互概述\n\n交互（Behavior）是 G6 的核心构建模块，它精确定义了用户与图之间的互动行为。每个 Behavior 插件都是一个高度封装的功能单元，内部集成了特定场景下的事件监听、状态管理和响应处理逻辑。\n\nG6 的内置 Behavior 涵盖了大多数常见交互需求，同时提供了灵活的扩展机制，支持开发者根据业务场景构建定制化交互体验。\n有关完整的交互行为类型、配置选项及开发示例，请参阅 [交互总览](/manual/behavior/overview) 章节。\n\n## API 参考\n\n### Graph.getBehaviors()\n\n获取当前图表中所有已配置的交互行为。\n\n```typescript\ngetBehaviors(): BehaviorOptions;\n```\n\n**返回值**\n\n- **类型**: [BehaviorOptions](#behavioroptions)\n- **描述**: 当前图表中已配置的所有交互行为\n\n**示例**\n\n```typescript\n// 获取当前所有交互行为\nconst behaviors = graph.getBehaviors();\nconsole.log('当前图表的交互行为:', behaviors);\n```\n\n### Graph.setBehaviors(behaviors)\n\n设置图表的交互行为，将替换所有现有的交互行为。\n\n```typescript\nsetBehaviors(behaviors: BehaviorOptions | ((prev: BehaviorOptions) => BehaviorOptions)): void;\n```\n\n**参数**\n\n| 参数      | 描述                                                 | 类型                                                                              | 默认值 | 必选 |\n| --------- | ---------------------------------------------------- | --------------------------------------------------------------------------------- | ------ | ---- |\n| behaviors | 新的交互行为配置，或一个基于当前配置返回新配置的函数 | [BehaviorOptions](#behavioroptions) \\| (prev: BehaviorOptions) => BehaviorOptions | -      | ✓    |\n\n**说明**\n\n设置的交互会全量替换原有的交互，如果需要新增交互可以使用函数式更新：\n\n```typescript\ngraph.setBehaviors((behaviors) => [...behaviors, { type: 'zoom-canvas' }]);\n```\n\n**示例 1**: 设置基本交互\n\n```typescript\n// 设置基本交互\ngraph.setBehaviors([\n  'drag-canvas', // 拖拽画布\n  'zoom-canvas', // 缩放画布\n  'drag-element', // 拖拽元素\n]);\n```\n\n**示例 2**: 设置带配置的交互\n\n```typescript\ngraph.setBehaviors([\n  // 字符串形式（使用默认配置）\n  'drag-canvas',\n\n  // 对象形式（自定义配置）\n  {\n    type: 'zoom-canvas',\n    key: 'my-zoom', // 指定唯一标识，用于后续更新\n    sensitivity: 1.5, // 缩放灵敏度\n  },\n\n  // 只有节点上启用拖拽\n  {\n    type: 'drag-element',\n    key: 'drag-node-only',\n    enable: (event) => event.targetType === 'node', // 仅在节点上启用拖拽\n  },\n]);\n```\n\n**示例 3**: 使用函数式更新\n\n```typescript\n// 添加新的交互行为\ngraph.setBehaviors((currentBehaviors) => [\n  ...currentBehaviors,\n  {\n    type: 'brush-select',\n    key: 'selection-brush',\n  },\n]);\n\n// 替换特定交互行为\ngraph.setBehaviors((currentBehaviors) => {\n  // 过滤掉现有的缩放交互\n  const filteredBehaviors = currentBehaviors.filter((behavior) => {\n    if (typeof behavior === 'string') return behavior !== 'zoom-canvas';\n    return behavior.type !== 'zoom-canvas';\n  });\n\n  // 添加新的缩放交互配置\n  return [\n    ...filteredBehaviors,\n    {\n      type: 'zoom-canvas',\n      key: 'new-zoom',\n      enableOptimize: true,\n    },\n  ];\n});\n```\n\n### Graph.updateBehavior(behavior)\n\n更新指定的交互行为配置，需要通过 `key` 标识要更新的交互。\n\n```typescript\nupdateBehavior(behavior: UpdateBehaviorOption): void;\n```\n\n**参数**\n\n| 参数     | 描述               | 类型                                          | 默认值 | 必选 |\n| -------- | ------------------ | --------------------------------------------- | ------ | ---- |\n| behavior | 更新的交互行为配置 | [UpdateBehaviorOption](#updatebehavioroption) | -      | ✓    |\n\n**说明**\n\n如果要更新一个交互，必须在原始交互配置中指定 `key` 字段，以便能够准确找到并更新该交互。\n\n**示例 1**: 更新交互配置\n\n```typescript\n// 初始设置交互时指定 key\ngraph.setBehaviors([\n  {\n    type: 'zoom-canvas',\n    key: 'my-zoom-canvas',\n    sensitivity: 1.0,\n  },\n]);\n\n// 更新交互配置\ngraph.updateBehavior({\n  key: 'my-zoom-canvas', // 指定要更新的交互\n  sensitivity: 2.0, // 新的缩放灵敏度\n  enableOptimize: true, // 添加新配置\n});\n```\n\n**示例 2**: 禁用/启用交互\n\n```typescript\n// 设置带 key 的行为\ngraph.setBehaviors([\n  {\n    type: 'drag-canvas',\n    key: 'main-drag',\n  },\n  {\n    type: 'zoom-canvas',\n    key: 'main-zoom',\n  },\n]);\n\n// 禁用拖拽功能\ngraph.updateBehavior({\n  key: 'main-drag',\n  enable: false,\n});\n\n// 稍后重新启用\nsetTimeout(() => {\n  graph.updateBehavior({\n    key: 'main-drag',\n    enable: true,\n  });\n}, 5000);\n```\n\n## 类型定义\n\n### BehaviorOptions\n\n```typescript\ntype BehaviorOptions = (string | CustomBehaviorOption | ((this: Graph) => CustomBehaviorOption))[];\n\ntype CustomBehaviorOption = {\n  // 交互类型\n  type: string;\n\n  // 交互 key，即唯一标识，用于标识交互，从而进一步操作此交互\n  key?: string;\n\n  // 针对不同类型的交互，还可能有其他配置项\n  [configKey: string]: any;\n};\n```\n\n### UpdateBehaviorOption\n\n```typescript\ntype UpdateBehaviorOption = {\n  // 要更新的交互的唯一标识\n  key: string;\n\n  // 其他要更新的配置项\n  [configKey: string]: unknown;\n};\n```\n"
  },
  {
    "path": "packages/site/docs/api/canvas.en.md",
    "content": "---\ntitle: Canvas Operations\norder: 1\n---\n\n## Overview of Canvas Operations\n\nG6 provides a series of canvas operation APIs to control and obtain basic information about the canvas. With these APIs, you can:\n\n- Get the canvas instance\n- Get and set the canvas size\n- Operate the canvas renderer and layers\n\n## API Reference\n\n### Graph.getCanvas()\n\nGet the canvas instance, which can be used for low-level canvas operations.\n\n```typescript\ngetCanvas(): Canvas;\n```\n\n**Return Value Description**\n\nThe Canvas instance includes the following main functions:\n\n- `getLayer(name?: string)`: Get the specified layer\n- `getLayers()`: Get all layers\n- `getCamera()`: Get the camera instance\n- `getRoot()`: Get the root node\n- `setCursor(cursor: string)`: Set the mouse cursor style\n\n**Example**\n\n```typescript\n// Get the canvas instance\nconst canvas = graph.getCanvas();\n\n// Get the main layer\nconst mainLayer = canvas.getLayer('main');\n\n// Set the mouse cursor style\ncanvas.setCursor('pointer');\n\n// Get the root node of the canvas\nconst root = canvas.getRoot();\n```\n\n### Graph.getSize()\n\nGet the size of the current canvas container. Returns an array containing the width and height.\n\n```typescript\ngetSize(): [number, number];\n```\n\n**Example**\n\n```typescript\n// Get the canvas size\nconst [width, height] = graph.getSize();\nconsole.log('Canvas width:', width);\nconsole.log('Canvas height:', height);\n\n// Use the size information for calculations\nconst centerX = width / 2;\nconst centerY = height / 2;\n```\n\n### Graph.setSize(width, height)\n\nSet the size of the canvas container. This method will update both the canvas and container size.\n\n```typescript\nsetSize(width: number, height: number): void;\n```\n\n**Parameters**\n\n| Parameter | Description            | Type   | Default | Required |\n| --------- | ---------------------- | ------ | ------- | -------- |\n| width     | Canvas width (pixels)  | number | -       | ✓        |\n| height    | Canvas height (pixels) | number | -       | ✓        |\n\n**Example**\n\n```typescript\n// Set a fixed size\ngraph.setSize(800, 600);\n```\n"
  },
  {
    "path": "packages/site/docs/api/canvas.zh.md",
    "content": "---\ntitle: 画布操作\norder: 1\n---\n\n## 画布操作概述\n\nG6 提供了一系列画布操作 API，用于控制和获取画布的基本信息。通过这些 API，你可以：\n\n- 获取画布实例\n- 获取和设置画布尺寸\n- 操作画布渲染器和图层\n\n## API 参考\n\n### Graph.getCanvas()\n\n获取画布实例，返回的实例可用于进行底层的画布操作。\n\n```typescript\ngetCanvas(): Canvas;\n```\n\n**返回值类型说明**\n\nCanvas 实例包含以下主要功能：\n\n- `getLayer(name?: string)`: 获取指定图层\n- `getLayers()`: 获取所有图层\n- `getCamera()`: 获取相机实例\n- `getRoot()`: 获取根节点\n- `setCursor(cursor: string)`: 设置鼠标样式\n\n**示例**\n\n```typescript\n// 获取画布实例\nconst canvas = graph.getCanvas();\n\n// 获取主图层\nconst mainLayer = canvas.getLayer('main');\n\n// 设置鼠标样式\ncanvas.setCursor('pointer');\n\n// 获取画布根节点\nconst root = canvas.getRoot();\n```\n\n### Graph.getSize()\n\n获取当前画布容器的尺寸。返回一个包含宽度和高度的数组。\n\n```typescript\ngetSize(): [number, number];\n```\n\n**示例**\n\n```typescript\n// 获取画布尺寸\nconst [width, height] = graph.getSize();\nconsole.log('画布宽度:', width);\nconsole.log('画布高度:', height);\n\n// 使用尺寸信息进行计算\nconst centerX = width / 2;\nconst centerY = height / 2;\n```\n\n### Graph.setSize(width, height)\n\n设置画布容器的尺寸。这个方法会同时更新画布和容器的大小。\n\n```typescript\nsetSize(width: number, height: number): void;\n```\n\n**参数**\n\n| 参数   | 描述             | 类型   | 默认值 | 必选 |\n| ------ | ---------------- | ------ | ------ | ---- |\n| width  | 画布宽度（像素） | number | -      | ✓    |\n| height | 画布高度（像素） | number | -      | ✓    |\n\n**示例**\n\n```typescript\n// 设置固定尺寸\ngraph.setSize(800, 600);\n```\n"
  },
  {
    "path": "packages/site/docs/api/coordinate.en.md",
    "content": "---\ntitle: Coordinate Transformation\norder: 12\n---\n\n## Overview of Coordinate Systems\n\nUnderstanding different coordinate systems and their transformations is crucial in graph visualization. G6 involves multiple coordinate systems, each used for different scenarios:\n\n- **Client Coordinate System**: Origin is at the top-left corner of the browser viewport, measured in pixels. Typically used for handling browser events.\n- **Screen Coordinate System**: Origin is at the top-left corner of the screen, affected by page scrolling.\n- **Page Coordinate System**: Origin is at the top-left corner of the document, considering document scrolling.\n- **Canvas Coordinate System**: Also known as the world coordinate system, used for drawing and layout, with the origin at the top-left corner of the canvas element.\n- **Viewport Coordinate System**: The visible area of the canvas, with the origin at the top-left corner of the viewport. The viewport can be panned and zoomed to view different areas of the Canvas.\n\nIn this [example](https://g.antv.antgroup.com/en/examples/canvas/canvas-basic#coordinates), moving the mouse shows the position in various coordinate systems:\n\n![Coordinate System Diagram](https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*kPfcTKwZG90AAAAAAAAAAAAAARQnAQ)\n\nWhen the canvas is not panned or zoomed, the Viewport and Canvas coordinate systems coincide. With user interactions like dragging or zooming, the two systems may shift.\n\nG6 provides a series of APIs for converting between different coordinate systems, detailed below.\n\n## API Reference\n\n### Graph.getCanvasByClient(point)\n\nConvert browser coordinates (client coordinates) to canvas coordinates.\n\n```typescript\ngetCanvasByClient(point: Point): Point;\n```\n\n**Parameters**\n\n| Parameter | Description              | Type                                         | Default | Required |\n| --------- | ------------------------ | -------------------------------------------- | ------- | -------- |\n| point     | Browser coordinate point | [number, number] \\| [number, number, number] | -       | ✓        |\n\n**Return Value**\n\n- **Type**: [number, number] \\| [number, number, number]\n- **Description**: Coordinate point in the canvas coordinate system\n\n### Graph.getCanvasByViewport(point)\n\nConvert viewport coordinates to canvas coordinates.\n\n```typescript\ngetCanvasByViewport(point: Point): Point;\n```\n\n**Parameters**\n\n| Parameter | Description               | Type                                         | Default | Required |\n| --------- | ------------------------- | -------------------------------------------- | ------- | -------- |\n| point     | Viewport coordinate point | [number, number] \\| [number, number, number] | -       | ✓        |\n\n**Return Value**\n\n- **Type**: [number, number] \\| [number, number, number]\n- **Description**: Coordinate point in the canvas coordinate system\n\n### Graph.getClientByCanvas(point)\n\nConvert canvas coordinates to browser client coordinates.\n\n```typescript\ngetClientByCanvas(point: Point): Point;\n```\n\n**Parameters**\n\n| Parameter | Description             | Type                                         | Default | Required |\n| --------- | ----------------------- | -------------------------------------------- | ------- | -------- |\n| point     | Canvas coordinate point | [number, number] \\| [number, number, number] | -       | ✓        |\n\n**Return Value**\n\n- **Type**: [number, number] \\| [number, number, number]\n- **Description**: Coordinate point in the browser client coordinate system\n\n### Graph.getViewportByCanvas(point)\n\nConvert canvas coordinates to viewport coordinates.\n\n```typescript\ngetViewportByCanvas(point: Point): Point;\n```\n\n**Parameters**\n\n| Parameter | Description             | Type                                         | Default | Required |\n| --------- | ----------------------- | -------------------------------------------- | ------- | -------- |\n| point     | Canvas coordinate point | [number, number] \\| [number, number, number] | -       | ✓        |\n\n**Return Value**\n\n- **Type**: [number, number] \\| [number, number, number]\n- **Description**: Coordinate point in the viewport coordinate system\n"
  },
  {
    "path": "packages/site/docs/api/coordinate.zh.md",
    "content": "---\ntitle: 坐标转换\norder: 12\n---\n\n## 坐标系概述\n\n在图可视化中，理解不同的坐标系及其转换关系至关重要。G6 中涉及多种坐标系，它们各自用于不同的场景：\n\n- **Client 坐标系**：浏览器视口左上角为原点，单位为像素。通常用于处理浏览器事件。\n- **Screen 坐标系**：屏幕左上角为原点，会受页面滚动影响。\n- **Page 坐标系**：文档左上角为原点，考虑文档滚动。\n- **Canvas 坐标系**：也称为世界坐标系，图形绘制和布局时使用的坐标系，画布元素左上角为原点。\n- **Viewport 坐标系**：视口坐标系，当前可见的画布区域，视口左上角为原点。视口通过平移、缩放等操作，可以观察不同的 Canvas 区域。\n\n在这个[示例](https://g.antv.antgroup.com/zh/examples/canvas/canvas-basic#coordinates)中，移动鼠标可以看到鼠标所在位置在各个坐标系下的值：\n\n![坐标系关系图](https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*kPfcTKwZG90AAAAAAAAAAAAAARQnAQ)\n\n当画布没有发生平移和缩放时，Viewport 坐标系与 Canvas 坐标系重合。随着用户的交互（如拖拽画布、缩放），两个坐标系会出现偏移。\n\nG6 提供了一系列 API 用于在不同坐标系之间进行转换，下面将详细介绍这些 API。\n\n## API 参考\n\n### Graph.getCanvasByClient(point)\n\n将浏览器坐标（客户端坐标）转换为画布坐标。\n\n```typescript\ngetCanvasByClient(point: Point): Point;\n```\n\n**参数**\n\n| 参数  | 描述         | 类型                                         | 默认值 | 必选 |\n| ----- | ------------ | -------------------------------------------- | ------ | ---- |\n| point | 浏览器坐标点 | [number, number] \\| [number, number, number] | -      | ✓    |\n\n**返回值**\n\n- **类型**: [number, number] \\| [number, number, number]\n- **描述**: 画布坐标系下的坐标点\n\n### Graph.getCanvasByViewport(point)\n\n将视口坐标转换为画布坐标。\n\n```typescript\ngetCanvasByViewport(point: Point): Point;\n```\n\n**参数**\n\n| 参数  | 描述       | 类型                                         | 默认值 | 必选 |\n| ----- | ---------- | -------------------------------------------- | ------ | ---- |\n| point | 视口坐标点 | [number, number] \\| [number, number, number] | -      | ✓    |\n\n**返回值**\n\n- **类型**: [number, number] \\| [number, number, number]\n- **描述**: 画布坐标系下的坐标点\n\n### Graph.getClientByCanvas(point)\n\n将画布坐标转换为浏览器客户端坐标。\n\n```typescript\ngetClientByCanvas(point: Point): Point;\n```\n\n**参数**\n\n| 参数  | 描述       | 类型                                         | 默认值 | 必选 |\n| ----- | ---------- | -------------------------------------------- | ------ | ---- |\n| point | 画布坐标点 | [number, number] \\| [number, number, number] | -      | ✓    |\n\n**返回值**\n\n- **类型**: [number, number] \\| [number, number, number]\n- **描述**: 浏览器客户端坐标系下的坐标点\n\n### Graph.getViewportByCanvas(point)\n\n将画布坐标转换为视口坐标。\n\n```typescript\ngetViewportByCanvas(point: Point): Point;\n```\n\n**参数**\n\n| 参数  | 描述       | 类型                                         | 默认值 | 必选 |\n| ----- | ---------- | -------------------------------------------- | ------ | ---- |\n| point | 画布坐标点 | [number, number] \\| [number, number, number] | -      | ✓    |\n\n**返回值**\n\n- **类型**: [number, number] \\| [number, number, number]\n- **描述**: 视口坐标系下的坐标点\n"
  },
  {
    "path": "packages/site/docs/api/data.en.md",
    "content": "---\ntitle: Data\norder: 0\n---\n\n## Overview of Data Operations\n\nG6 provides a comprehensive [data](/en/manual/data) operation API, covering the complete lifecycle of graph data from query, modification to update.\n\n## API Reference\n\n### Graph.getData()\n\nGet the complete data of the graph.\n\n```typescript\ngetData(): Required<GraphData>;\n```\n\n**Return Value**:\n\n- **Type**: [GraphData](#graphdata)\n\n- **Description**: Returns the complete graph data containing all nodes, edges, and combo data\n\n**Example**:\n\n```typescript\nconst graphData = graph.getData();\nconsole.log('Node data:', graphData.nodes);\nconsole.log('Edge data:', graphData.edges);\nconsole.log('Combo data:', graphData.combos);\n```\n\n### Graph.getNodeData()\n\nGet node data, supporting three calling methods.\n\n```typescript\n// Get all node data\ngetNodeData(): NodeData[];\n\n// Get single node data\ngetNodeData(id: ID): NodeData;\n\n// Get multiple node data\ngetNodeData(ids: ID[]): NodeData[];\n```\n\n**Parameters**:\n\n| Parameter | Description   | Type     | Default | Required |\n| --------- | ------------- | -------- | ------- | -------- |\n| id        | Node ID       | string   | -       |          |\n| ids       | Node ID array | string[] | -       |          |\n\n**Return Value**:\n\n- **Type**: [NodeData](#nodedata) | [NodeData](#nodedata)[]\n- **Description**: Returns the specified node data or node data array\n\n**Example**:\n\n```typescript\n// Get all nodes\nconst nodes = graph.getNodeData();\n\n// Get single node\nconst node = graph.getNodeData('node1');\nconsole.log('Node position:', node.style.x, node.style.y);\n\n// Get multiple nodes\nconst [node1, node2] = graph.getNodeData(['node1', 'node2']);\n```\n\n### Graph.getEdgeData()\n\nGet edge data, supporting three calling methods.\n\n```typescript\n// Get all edge data\ngetEdgeData(): EdgeData[];\n\n// Get single edge data\ngetEdgeData(id: ID): EdgeData;\n\n// Get multiple edge data\ngetEdgeData(ids: ID[]): EdgeData[];\n```\n\n**Parameters**:\n\n| Parameter | Description   | Type     | Default | Required |\n| --------- | ------------- | -------- | ------- | -------- |\n| id        | Edge ID       | string   | -       |          |\n| ids       | Edge ID array | string[] | -       |          |\n\n**Return Value**:\n\n- **Type**: [EdgeData](#edgedata) | [EdgeData](#edgedata)[]\n- **Description**: Returns the specified edge data or edge data array\n\n**Example**:\n\n```typescript\n// Get all edges\nconst edges = graph.getEdgeData();\n\n// Get single edge\nconst edge = graph.getEdgeData('edge1');\nconsole.log('Edge source and target:', edge.source, edge.target);\n\n// Get multiple edges\nconst [edge1, edge2] = graph.getEdgeData(['edge1', 'edge2']);\n```\n\n### Graph.getComboData()\n\nGet combo data, supporting three calling methods.\n\n```typescript\n// Get all combo data\ngetComboData(): ComboData[];\n\n// Get single combo data\ngetComboData(id: ID): ComboData;\n\n// Get multiple combo data\ngetComboData(ids: ID[]): ComboData[];\n```\n\n**Parameters**:\n\n| Parameter | Description    | Type     | Default | Required |\n| --------- | -------------- | -------- | ------- | -------- |\n| id        | Combo ID       | string   | -       |          |\n| ids       | Combo ID array | string[] | -       |          |\n\n**Return Value**:\n\n- **Type**: [ComboData](#combodata) | [ComboData](#combodata)[]\n- **Description**: Returns the specified combo data or combo data array\n\n**Example**:\n\n```typescript\n// Get all combos\nconst combos = graph.getComboData();\n\n// Get single combo\nconst combo = graph.getComboData('combo1');\nconsole.log('Nodes in combo:', combo.children);\n\n// Get multiple combos\nconst [combo1, combo2] = graph.getComboData(['combo1', 'combo2']);\n```\n\n### Graph.getElementData()\n\nGet single element data, supporting two calling methods.\n\n⚠️ **Note**: This API directly gets the data of the element without considering the element type.\n\n```typescript\n// Get single element data\ngetElementData(id: ID): ElementDatum;\n\n// Get multiple element data\ngetElementData(ids: ID[]): ElementDatum[];\n```\n\n**Parameters**:\n\n| Parameter | Description      | Type     | Default | Required |\n| --------- | ---------------- | -------- | ------- | -------- |\n| id        | Element ID       | string   | -       |          |\n| ids       | Element ID array | string[] | -       |          |\n\n**Return Value**:\n\n- **Type**: ElementDatum \\| ElementDatum[]\n- **Description**: Directly gets the data of the element without considering the element type\n\n**Example**:\n\n```typescript\nconst element = graph.getElementData('node-1');\nconsole.log('Element data:', element);\n\nconst elements = graph.getElementData(['node-1', 'edge-1']);\nconsole.log('Multiple element data:', elements);\n```\n\n### Graph.getElementDataByState()\n\nGet element data in a specified state, supporting three calling methods.\n\n```typescript\n// Get node data in a specified state\ngetElementDataByState(elementType: 'node', state: string): NodeData[];\n\n// Get edge data in a specified state\ngetElementDataByState(elementType: 'edge', state: string): EdgeData[];\n\n// Get combo data in a specified state\ngetElementDataByState(elementType: 'combo', state: string): ComboData[];\n```\n\n**Parameters**:\n\n| Parameter   | Description  | Type                              | Default | Required |\n| ----------- | ------------ | --------------------------------- | ------- | -------- |\n| elementType | Element type | `'node'` \\| `'edge'` \\| `'combo'` | -       | ✓        |\n| state       | State        | string                            | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: NodeData[] \\| EdgeData[] \\| ComboData[]\n- **Description**: Returns node data, edge data, or combo data in the specified state\n\n**Example**:\n\n```typescript\nconst selectedNodes = graph.getElementDataByState('node', 'selected');\nconsole.log('Selected nodes:', selectedNodes);\n\nconst selectedEdges = graph.getElementDataByState('edge', 'selected');\nconsole.log('Selected edges:', selectedEdges);\n\nconst selectedCombos = graph.getElementDataByState('combo', 'selected');\nconsole.log('Selected combos:', selectedCombos);\n```\n\n**Built-in States**:\n\n- `'selected'`\n- `'highlight'`\n- `'active'`\n- `'inactive'`\n- `'disabled'`\n\n### Graph.getNeighborNodesData()\n\nGet the data of neighbor nodes of a node or combo.\n\n```typescript\ngetNeighborNodesData(id: ID): NodeData[];\n```\n\n**Parameters**:\n\n| Parameter | Description      | Type   | Default | Required |\n| --------- | ---------------- | ------ | ------- | -------- |\n| id        | Node or combo ID | string | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: NodeData[]\n- **Description**: Returns neighbor node data\n\n**Example**:\n\n```typescript\nconst neighbors = graph.getNeighborNodesData('node-1');\nconsole.log('Neighbor nodes:', neighbors);\n```\n\n### Graph.getRelatedEdgesData()\n\nGet the data of edges related to a node or combo.\n\n```typescript\ngetRelatedEdgesData(id: ID, direction?: EdgeDirection): EdgeData[];\n```\n\n**Parameters**:\n\n| Parameter | Description      | Type                          | Default | Required |\n| --------- | ---------------- | ----------------------------- | ------- | -------- |\n| id        | Node or combo ID | string                        | -       | ✓        |\n| direction | Edge direction   | `'in'` \\| `'out'` \\| `'both'` | -       |          |\n\n**Return Value**:\n\n- **Type**: EdgeData[]\n- **Description**: Returns the data of edges related to the specified node or combo\n\n**Example**:\n\n```typescript\nconst relatedEdges = graph.getRelatedEdgesData('node-1');\nconsole.log('Related edges:', relatedEdges);\n```\n\n### Graph.getParentData()\n\nGet the data of the parent element of a node or combo.\n\n```typescript\ngetParentData(id: ID, hierarchy: HierarchyKey): NodeLikeData | undefined;\n```\n\n**Parameters**:\n\n| Parameter | Description            | Type                  | Default | Required |\n| --------- | ---------------------- | --------------------- | ------- | -------- |\n| id        | Node or combo ID       | string                | -       | ✓        |\n| hierarchy | Specify hierarchy type | `'tree'` \\| `'combo'` | -       |          |\n\n**Return Value**:\n\n- **Type**: NodeData \\| ComboData \\| undefined\n- **Description**: Returns the parent element data, or undefined if it does not exist\n\n**Example**:\n\n```typescript\n// Get the parent node in a tree graph\nconst treeParent = graph.getParentData('node1', 'tree');\n\n// Get the parent combo in a combo\nconst comboParent = graph.getParentData('node1', 'combo');\n```\n\n### Graph.getChildrenData()\n\nGet the data of child elements of a node or combo.\n\n```typescript\ngetChildrenData(id: ID): (NodeData \\| ComboData)[];\n```\n\n**Parameters**:\n\n| Parameter | Description      | Type   | Default | Required |\n| --------- | ---------------- | ------ | ------- | -------- |\n| id        | Node or combo ID | string | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: (NodeData \\| ComboData)[]\n- **Description**: Returns an array of child element data\n\n**Note**:\n\n- **Querying combo's child elements**: If the id corresponds to a combo element, you can directly use this API to get all its child elements.\n- **Querying node's child elements**: If the id corresponds to a node, only when the graph data is a tree structure (i.e., the node data maintains a `children` field, and `children` is an array of child node IDs for that node), can you use this API to get the child elements of that node. Otherwise, an empty array is returned.\n\n**Example**:\n\n```typescript\n// Get the child elements of a combo\nconst children = graph.getChildrenData('combo1');\nconsole.log('Number of child nodes:', children.length);\n\n// Process each child element\nchildren.forEach((child) => {\n  console.log('Child element ID:', child.id);\n});\n```\n\n### Graph.getAncestorsData()\n\nGet the data of all ancestor elements of a node or combo.\n\n```typescript\ngetAncestorsData(id: ID, hierarchy: HierarchyKey): NodeLikeData[];\n```\n\n**Parameters**:\n\n| Parameter | Description            | Type                  | Default | Required |\n| --------- | ---------------------- | --------------------- | ------- | -------- |\n| id        | Node or combo ID       | string                | -       | ✓        |\n| hierarchy | Specify hierarchy type | `'tree'` \\| `'combo'` | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: [NodeData](#nodedata)[] \\| [ComboData](#combodata)[]\n- **Description**: Returns an array of ancestor element data, ordered from parent to root\n\n**Example**:\n\n```typescript\n// Get all ancestor nodes in a tree graph\nconst treeAncestors = graph.getAncestorsData('node1', 'tree');\nconsole.log(\n  'Ancestor node path:',\n  treeAncestors.map((node) => node.id),\n);\n\n// Get all parent combos in a combo\nconst comboAncestors = graph.getAncestorsData('node1', 'combo');\n```\n\n### Graph.getDescendantsData()\n\nGet the data of all descendant elements of a node or combo.\n\n```typescript\ngetDescendantsData(id: ID): NodeLikeData[];\n```\n\n**Parameters**:\n\n| Parameter | Description      | Type   | Default | Required |\n| --------- | ---------------- | ------ | ------- | -------- |\n| id        | Node or combo ID | string | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: [NodeData](#nodedata)[] \\| [ComboData](#combodata)[]\n- **Description**: Returns an array of descendant element data\n\n**Example**:\n\n```typescript\n// Get all descendants of a node\nconst descendants = graph.getDescendantsData('node1');\nconsole.log('Number of descendants:', descendants.length);\n\n// Process all descendant elements\ndescendants.forEach((descendant) => {\n  console.log('Descendant element ID:', descendant.id);\n});\n```\n\n### Graph.setData()\n\nSet the complete data of the graph.\n\n```typescript\nsetData(data: GraphData | ((prev: GraphData) => GraphData)): void;\n```\n\n**Parameters**:\n\n| Parameter | Description                                           | Type                                                        | Default | Required |\n| --------- | ----------------------------------------------------- | ----------------------------------------------------------- | ------- | -------- |\n| data      | New graph data or a function returning new graph data | [GraphData](#graphdata) \\| ((prev: GraphData) => GraphData) | -       | ✓        |\n\n**Example**:\n\n```typescript\n// Directly set data\ngraph.setData({\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 } },\n    { id: 'node2', style: { x: 200, y: 200 } },\n  ],\n  edges: [{ id: 'edge1', source: 'node1', target: 'node2' }],\n});\n\n// Use functional incremental update: get current graph data and return new graph data\ngraph.setData((prev) => ({\n  ...prev,\n  nodes: [...prev.nodes, { id: 'node3', style: { x: 300, y: 300 } }],\n}));\n```\n\n### Graph.addData()\n\nAdd new element data.\n\n```typescript\naddData(data: GraphData | ((prev: GraphData) => GraphData)): void;\n```\n\n**Parameters**:\n\n| Parameter | Description                                              | Type                                                        | Default | Required |\n| --------- | -------------------------------------------------------- | ----------------------------------------------------------- | ------- | -------- |\n| data      | Graph data to add or a function returning new graph data | [GraphData](#graphdata) \\| ((prev: GraphData) => GraphData) | -       | ✓        |\n\n**Example**:\n\n```typescript\ngraph.addData({\n  nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n  edges: [{ source: 'node-1', target: 'node-2' }],\n});\n```\n\n### Graph.addNodeData()\n\nAdd new node data.\n\n```typescript\naddNodeData(data: NodeData[] | ((prev: NodeData[]) => NodeData[])): void;\n```\n\n**Parameters**:\n\n| Parameter | Description                                        | Type                                                        | Default | Required |\n| --------- | -------------------------------------------------- | ----------------------------------------------------------- | ------- | -------- |\n| data      | Node data to add or a function returning node data | [NodeData](#nodedata)[] \\| (prev: NodeData[]) => NodeData[] | -       | ✓        |\n\n**Example**:\n\n```typescript\n// Add single node\ngraph.addNodeData([\n  {\n    id: 'node1',\n    style: { x: 100, y: 100 },\n    data: { label: 'Node 1' },\n  },\n]);\n\n// Add multiple nodes\ngraph.addNodeData([\n  { id: 'node2', style: { x: 200, y: 200 } },\n  { id: 'node3', style: { x: 300, y: 300 } },\n]);\n\n// Functional addition\ngraph.addNodeData((prev) => [...prev, { id: 'node4', style: { x: 400, y: 400 } }]);\n```\n\n### Graph.addEdgeData()\n\nAdd new edge data.\n\n```typescript\naddEdgeData(data: EdgeData[] | ((prev: EdgeData[]) => EdgeData[])): void;\n```\n\n**Parameters**:\n\n| Parameter | Description                                        | Type                                                          | Default | Required |\n| --------- | -------------------------------------------------- | ------------------------------------------------------------- | ------- | -------- |\n| data      | Edge data to add or a function returning edge data | [EdgeData](#edgedata)[] \\| ((prev: EdgeData[]) => EdgeData[]) | -       | ✓        |\n\n**Example**:\n\n```typescript\n// Add single edge\ngraph.addEdgeData([\n  {\n    id: 'edge1',\n    source: 'node1',\n    target: 'node2',\n    data: {\n      weight: 1,\n      label: 'Relation',\n    },\n  },\n]);\n\n// Add multiple edges\ngraph.addEdgeData([\n  { id: 'edge2', source: 'node2', target: 'node3' },\n  { id: 'edge3', source: 'node3', target: 'node1' },\n]);\n\n// Functional addition\ngraph.addEdgeData((prev) => [...prev, { id: 'edge4', source: 'node1', target: 'node4' }]);\n```\n\n### Graph.addComboData()\n\nAdd new combo data.\n\n```typescript\naddComboData(data: ComboData[] | ((prev: ComboData[]) => ComboData[])): void;\n```\n\n**Parameters**:\n\n| Parameter | Description                                          | Type                                                            | Default | Required |\n| --------- | ---------------------------------------------------- | --------------------------------------------------------------- | ------- | -------- |\n| data      | Combo data to add or a function returning combo data | [ComboData](#combodata)[] \\| (prev: ComboData[]) => ComboData[] | -       | ✓        |\n\n**Example**:\n\n```typescript\ngraph.addComboData([{ id: 'combo1', children: ['node1', 'node2'] }]);\n```\n\n### Graph.addChildrenData()\n\nAdd child node data to a tree graph node.\n\n⚠️ **Note**: Use addNodeData / addComboData methods to add child nodes to a combo.\n\n```typescript\naddChildrenData(parentId: ID, childrenData: NodeData[]): void;\n```\n\n**Parameters**:\n\n| Parameter    | Description     | Type                    | Default | Required |\n| ------------ | --------------- | ----------------------- | ------- | -------- |\n| parentId     | Parent node ID  | string                  | -       | ✓        |\n| childrenData | Child node data | [NodeData](#nodedata)[] | -       | ✓        |\n\n**Example**:\n\n```typescript\ngraph.addChildrenData('node1', [{ id: 'node2' }]);\n```\n\n### Graph.removeData()\n\nRemove element data.\n\n```typescript\nremoveData(ids: DataID | ((data: GraphData) => DataID)): void;\n```\n\n**Parameters**:\n\n| Parameter | Description                                               | Type                                               | Default | Required |\n| --------- | --------------------------------------------------------- | -------------------------------------------------- | ------- | -------- |\n| ids       | Element IDs to remove or a function returning element IDs | [DataID](#dataid) \\| ((data: GraphData) => DataID) | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: void\n\n**Example**:\n\n```typescript\ngraph.removeData({\n  nodes: ['node-1', 'node-2'],\n  edges: ['edge-1'],\n});\n```\n\n### Graph.removeNodeData()\n\nRemove node data.\n\n```typescript\nremoveNodeData(ids: ID[] | ((data: NodeData[]) => ID[])): void;\n```\n\n**Parameters**:\n\n| Parameter | Description                                         | Type                                                            | Default | Required |\n| --------- | --------------------------------------------------- | --------------------------------------------------------------- | ------- | -------- |\n| ids       | Node IDs to remove or a function returning node IDs | [ID](#id)[] \\| ((data: [NodeData](#nodedata)[]) => [ID](#id)[]) | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: void\n\n**Example**:\n\n```typescript\ngraph.removeNodeData(['node-1', 'node-2']);\n```\n\n### Graph.removeEdgeData()\n\nRemove edge data.\n\n```typescript\nremoveEdgeData(ids: ID[] | ((data: EdgeData[]) => ID[])): void;\n```\n\n**Parameters**:\n\n| Parameter | Description                                         | Type                                                            | Default | Required |\n| --------- | --------------------------------------------------- | --------------------------------------------------------------- | ------- | -------- |\n| ids       | Edge IDs to remove or a function returning edge IDs | [ID](#id)[] \\| ((data: [EdgeData](#edgedata)[]) => [ID](#id)[]) | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: void\n\n**Example**:\n\n```typescript\ngraph.removeEdgeData(['edge-1']);\n```\n\n### Graph.removeComboData()\n\nRemove combo data.\n\n```typescript\nremoveComboData(ids: ID[] | ((data: ComboData[]) => ID[])): void;\n```\n\n**Parameters**:\n\n| Parameter | Description                                           | Type                                                            | Default | Required |\n| --------- | ----------------------------------------------------- | --------------------------------------------------------------- | ------- | -------- |\n| ids       | Combo IDs to remove or a function returning combo IDs | [ID](#id)[] \\| (data: [ComboData](#combodata)[]) => [ID](#id)[] | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: void\n\n**Example**:\n\n```typescript\ngraph.removeComboData(['combo-1']);\n```\n\n### Graph.updateData()\n\nUpdate element data.\n\n⚠️ **Note**: Only the data that needs to be updated needs to be passed in, not the complete data.\n\n```typescript\nupdateData(data: PartialGraphData | ((prev: GraphData) => PartialGraphData)): void;\n```\n\n**Parameters**:\n\n| Parameter | Description                                                 | Type                                                                             | Default | Required |\n| --------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------- | ------- | -------- |\n| data      | Element data to update or a function returning element data | [PartialGraphData](#partialgraphdata) \\| ((prev: GraphData) => PartialGraphData) | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: void\n\n**Example**:\n\n```typescript\ngraph.updateData({\n  nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],\n  edges: [{ id: 'edge-1', style: { lineWidth: 2 } }],\n});\n```\n\n### Graph.updateNodeData()\n\nUpdate node data.\n\n⚠️ **Note**: Only the data that needs to be updated needs to be passed in, not the complete data.\n\n```typescript\nupdateNodeData(data: NodeData[] | ((prev: NodeData[]) => NodeData[])): void;\n```\n\n**Parameters**:\n\n| Parameter | Description                                           | Type                                           | Default | Required |\n| --------- | ----------------------------------------------------- | ---------------------------------------------- | ------- | -------- |\n| data      | Node data to update or a function returning node data | NodeData[] \\| (prev: NodeData[]) => NodeData[] | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: void\n\n**Example**:\n\n```typescript\ngraph.updateNodeData([{ id: 'node-1', style: { x: 100, y: 100 } }]);\n```\n\n### Graph.updateEdgeData()\n\nUpdate edge data.\n\n⚠️ **Note**: Only the data that needs to be updated needs to be passed in, not the complete data.\n\n```typescript\nupdateEdgeData(data: (PartialEdgeData<EdgeData>[] | ((prev: EdgeData[]) => PartialEdgeData<EdgeData>[]))): void;\n```\n\n**Parameters**:\n\n| Parameter | Description                                           | Type                                                                                                                         | Default | Required |\n| --------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |\n| data      | Edge data to update or a function returning edge data | [PartialEdgeData\\<EdgeData\\>](#partialedgedata)[] \\| (prev: EdgeData[]) => [PartialEdgeData\\<EdgeData\\>](#partialedgedata)[] | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: void\n\n**Example**:\n\n```typescript\ngraph.updateEdgeData([{ id: 'edge-1', style: { lineWidth: 2 } }]);\n```\n\n### Graph.updateComboData()\n\nUpdate combo data.\n\n⚠️ **Note**: Only the data that needs to be updated needs to be passed in, not the complete data.\n\n```typescript\nupdateComboData(data: (ComboData[] | ((prev: ComboData[]) => ComboData[]))): void;\n```\n\n**Parameters**:\n\n| Parameter | Description                                             | Type                                                            | Default | Required |\n| --------- | ------------------------------------------------------- | --------------------------------------------------------------- | ------- | -------- |\n| data      | Combo data to update or a function returning combo data | [ComboData](#combodata)[] \\| (prev: ComboData[]) => ComboData[] | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: void\n\n**Example**:\n\n```typescript\ngraph.updateComboData([{ id: 'combo-1', style: { x: 100, y: 100 } }]);\n```\n\n### Graph.hasNode()\n\nDetermine if an node exists\n\n```typescript\nhasNode(id:ID): boolean;\n```\n\n**参数**:\n| Parameter | Description | Type | Default | Required |\n| ---- | -------------------- | --------- | ------ | ---- |\n| id | Node ID to be judged | [ID](#id) | - | ✓ |\n\n**返回值**:\n\n- **类型**: boolean\n  **示例**:\n\n```typescript\ngraph.hasNode('node-1');\n```\n\n### Graph.hasEdge()\n\nDetermine if an edge exists\n\n```typescript\nhasEdge(id:ID): boolean;\n```\n\n**参数**:\n\n| Parameter | Description          | Type      | Default | Required |\n| --------- | -------------------- | --------- | ------- | -------- |\n| id        | Edge ID to be judged | [ID](#id) | -       | ✓        |\n\n**返回值**:\n\n- **类型**: boolean\n  **示例**:\n\n```typescript\ngraph.hasEdge('edge-1');\n```\n\n### Graph.hasCombo()\n\nDetermine if combo exists\n\n```typescript\nhasCombo(id:ID): boolean;\n```\n\n**参数**:\n\n| Parameter | Description           | Type      | Default | Required |\n| --------- | --------------------- | --------- | ------- | -------- |\n| id        | Combo ID to be judged | [ID](#id) | -       | ✓        |\n\n**返回值**:\n\n- **类型**: boolean\n  **示例**:\n\n```typescript\ngraph.hasCombo('combo-1');\n```\n\n## Type Definitions\n\n### ID\n\nElement ID type.\n\n```typescript\ntype ID = string;\n```\n\n### DataID\n\nMultiple element ID type.\n\n```typescript\ninterface DataID {\n  nodes?: ID[];\n  edges?: ID[];\n  combos?: ID[];\n}\n```\n\n### GraphData\n\nG6 graph data type.\n\n```typescript\ninterface GraphData {\n  nodes?: NodeData[];\n  edges?: EdgeData[];\n  combos?: ComboData[];\n}\n```\n\n### NodeData\n\nNode data type.\n\n```typescript\ninterface NodeData {\n  id: string; // Node ID\n  type?: string; // Node type\n  data?: Record<string, any>; // Node data\n  style?: Record<string, any>; // Node style\n  states?: string[]; // Initial node states\n  combo?: string; // Belonging combo\n  children?: string[]; // Array of child node IDs\n}\n```\n\nFor detailed type definitions, please refer to [Node Data](/en/manual/data#nodedata).\n\n### EdgeData\n\nEdge data type.\n\n```typescript\ninterface EdgeData {\n  source: string; // Source ID\n  target: string; // Target ID\n  id?: string; // Edge ID\n  type?: string; // Edge type\n  data?: Record<string, any>; // Edge data\n  style?: Record<string, any>; // Edge style\n  states?: string[]; // Initial edge states\n}\n```\n\nFor detailed type definitions, please refer to [Edge Data](/en/manual/data#edgedata).\n\n### ComboData\n\nCombo data type.\n\n```typescript\ninterface ComboData {\n  id: string; // Combo ID\n  type?: string; // Combo type\n  data?: Record<string, any>; // Combo data\n  style?: Record<string, any>; // Combo style\n  states?: string[]; // Initial combo states\n  combo?: string; // Parent combo ID\n}\n```\n\nFor detailed type definitions, please refer to [Combo Data](/en/manual/data#combodata).\n"
  },
  {
    "path": "packages/site/docs/api/data.zh.md",
    "content": "---\ntitle: 数据\norder: 0\n---\n\n## 数据操作概述\n\nG6 提供了一套全面的 [数据](/manual/data) 操作 API，覆盖了图数据从查询、修改到更新的完整生命周期。\n\n## API 参考\n\n### Graph.getData()\n\n获取图的完整数据。\n\n```typescript\ngetData(): Required<GraphData>;\n```\n\n**返回值**:\n\n- **类型**: [GraphData](#graphdata)\n\n- **描述**: 返回包含所有节点、边和组合数据的完整图数据\n\n**示例**:\n\n```typescript\nconst graphData = graph.getData();\nconsole.log('节点数据:', graphData.nodes);\nconsole.log('边数据:', graphData.edges);\nconsole.log('组合数据:', graphData.combos);\n```\n\n### Graph.getNodeData()\n\n获取节点数据，支持三种调用方式。\n\n```typescript\n// 获取所有节点数据\ngetNodeData(): NodeData[];\n\n// 获取单个节点数据\ngetNodeData(id: ID): NodeData;\n\n// 批量获取多个节点数据\ngetNodeData(ids: ID[]): NodeData[];\n```\n\n**参数**:\n\n| 参数 | 描述         | 类型     | 默认值 | 必选 |\n| ---- | ------------ | -------- | ------ | ---- |\n| id   | 节点 ID      | string   | -      |      |\n| ids  | 节点 ID 数组 | string[] | -      |      |\n\n**返回值**:\n\n- **类型**: [NodeData](#nodedata) | [NodeData](#nodedata)[]\n- **描述**: 返回指定的节点数据或节点数据数组\n\n**示例**:\n\n```typescript\n// 获取所有节点\nconst nodes = graph.getNodeData();\n\n// 获取单个节点\nconst node = graph.getNodeData('node1');\nconsole.log('节点位置:', node.style.x, node.style.y);\n\n// 获取多个节点\nconst [node1, node2] = graph.getNodeData(['node1', 'node2']);\n```\n\n### Graph.getEdgeData()\n\n获取边数据，支持三种调用方式。\n\n```typescript\n// 获取所有边数据\ngetEdgeData(): EdgeData[];\n\n// 获取单条边数据\ngetEdgeData(id: ID): EdgeData;\n\n// 批量获取多条边数据\ngetEdgeData(ids: ID[]): EdgeData[];\n```\n\n**参数**:\n\n| 参数 | 描述       | 类型     | 默认值 | 必选 |\n| ---- | ---------- | -------- | ------ | ---- |\n| id   | 边 ID      | string   | -      |      |\n| ids  | 边 ID 数组 | string[] | -      |      |\n\n**返回值**:\n\n- **类型**: [EdgeData](#edgedata) | [EdgeData](#edgedata)[]\n- **描述**: 返回指定的边数据或边数据数组\n\n**示例**:\n\n```typescript\n// 获取所有边\nconst edges = graph.getEdgeData();\n\n// 获取单条边\nconst edge = graph.getEdgeData('edge1');\nconsole.log('边的起点和终点:', edge.source, edge.target);\n\n// 获取多条边\nconst [edge1, edge2] = graph.getEdgeData(['edge1', 'edge2']);\n```\n\n### Graph.getComboData()\n\n获取组合数据,支持三种调用方式。\n\n```typescript\n// 获取所有组合数据\ngetComboData(): ComboData[];\n\n// 获取单个组合数据\ngetComboData(id: ID): ComboData;\n\n// 批量获取多个组合数据\ngetComboData(ids: ID[]): ComboData[];\n```\n\n**参数**:\n\n| 参数 | 描述         | 类型     | 默认值 | 必选 |\n| ---- | ------------ | -------- | ------ | ---- |\n| id   | 组合 ID      | string   | -      |      |\n| ids  | 组合 ID 数组 | string[] | -      |      |\n\n**返回值**:\n\n- **类型**: [ComboData](#combodata) | [ComboData](#combodata)[]\n- **描述**: 返回指定的组合数据或组合数据数组\n\n**示例**:\n\n```typescript\n// 获取所有组合\nconst combos = graph.getComboData();\n\n// 获取单个组合\nconst combo = graph.getComboData('combo1');\nconsole.log('组合包含的节点:', combo.children);\n\n// 获取多个组合\nconst [combo1, combo2] = graph.getComboData(['combo1', 'combo2']);\n```\n\n### Graph.getElementData()\n\n获取单个元素数据，支持两种调用方式。\n\n⚠️ **注意**: 此 API 直接获取元素的数据而不必考虑元素类型。\n\n```typescript\n// 获取单个元素数据\ngetElementData(id: ID): ElementDatum;\n\n// 批量获取多个元素数据\ngetElementData(ids: ID[]): ElementDatum[];\n```\n\n**参数**:\n\n| 参数 | 描述         | 类型     | 默认值 | 必选 |\n| ---- | ------------ | -------- | ------ | ---- |\n| id   | 元素 ID      | string   | -      |      |\n| ids  | 元素 ID 数组 | string[] | -      |      |\n\n**返回值**:\n\n- **类型**: ElementDatum \\| ElementDatum[]\n- **描述**: 直接获取元素的数据而不必考虑元素类型\n\n**示例**:\n\n```typescript\nconst element = graph.getElementData('node-1');\nconsole.log('元素数据:', element);\n\nconst elements = graph.getElementData(['node-1', 'edge-1']);\nconsole.log('多个元素数据:', elements);\n```\n\n### Graph.getElementDataByState()\n\n获取指定状态下的元素数据，支持三种调用方式。\n\n```typescript\n// 获取指定状态下的节点数据\ngetElementDataByState(elementType: 'node', state: string): NodeData[];\n\n// 获取指定状态下的边数据\ngetElementDataByState(elementType: 'edge', state: string): EdgeData[];\n\n// 获取指定状态下的组合数据\ngetElementDataByState(elementType: 'combo', state: string): ComboData[];\n```\n\n**参数**:\n\n| 参数        | 描述     | 类型                              | 默认值 | 必选 |\n| ----------- | -------- | --------------------------------- | ------ | ---- |\n| elementType | 元素类型 | `'node'` \\| `'edge'` \\| `'combo'` | -      | ✓    |\n| state       | 状态     | string                            | -      | ✓    |\n\n**返回值**:\n\n- **类型**: NodeData[] \\| EdgeData[] \\| ComboData[]\n- **描述**: 返回指定状态下的节点数据、边数据或组合数据\n\n**示例**:\n\n```typescript\nconst selectedNodes = graph.getElementDataByState('node', 'selected');\nconsole.log('选中的节点:', selectedNodes);\n\nconst selectedEdges = graph.getElementDataByState('edge', 'selected');\nconsole.log('选中的边:', selectedEdges);\n\nconst selectedCombos = graph.getElementDataByState('combo', 'selected');\nconsole.log('选中的组合:', selectedCombos);\n```\n\n**内置状态**:\n\n- `'selected'`\n- `'highlight'`\n- `'active'`\n- `'inactive'`\n- `'disabled'`\n\n### Graph.getNeighborNodesData()\n\n获取节点或组合的一跳邻居节点数据。\n\n```typescript\ngetNeighborNodesData(id: ID): NodeData[];\n```\n\n**参数**:\n\n| 参数 | 描述            | 类型   | 默认值 | 必选 |\n| ---- | --------------- | ------ | ------ | ---- |\n| id   | 节点或组合的 ID | string | -      | ✓    |\n\n**返回值**:\n\n- **类型**: NodeData[]\n- **描述**: 返回邻居节点数据\n\n**示例**:\n\n```typescript\nconst neighbors = graph.getNeighborNodesData('node-1');\nconsole.log('邻居节点:', neighbors);\n```\n\n### Graph.getRelatedEdgesData()\n\n获取节点或组合关联边的数据。\n\n```typescript\ngetRelatedEdgesData(id: ID, direction?: EdgeDirection): EdgeData[];\n```\n\n**参数**:\n\n| 参数      | 描述            | 类型                          | 默认值 | 必选 |\n| --------- | --------------- | ----------------------------- | ------ | ---- |\n| id        | 节点或组合的 ID | string                        | -      | ✓    |\n| direction | 边的方向        | `'in'` \\| `'out'` \\| `'both'` | -      |      |\n\n**返回值**:\n\n- **类型**: EdgeData[]\n- **描述**: 返回与指定节点或组合关联的边数据\n\n**示例**:\n\n```typescript\nconst relatedEdges = graph.getRelatedEdgesData('node-1');\nconsole.log('关联边:', relatedEdges);\n```\n\n### Graph.getParentData()\n\n获取节点或组合的父元素数据。\n\n```typescript\ngetParentData(id: ID, hierarchy: HierarchyKey): NodeLikeData | undefined;\n```\n\n**参数**:\n\n| 参数      | 描述             | 类型                  | 默认值 | 必选 |\n| --------- | ---------------- | --------------------- | ------ | ---- |\n| id        | 节点或组合的 ID  | string                | -      | ✓    |\n| hierarchy | 指定层级关系类型 | `'tree'` \\| `'combo'` | -      |      |\n\n**返回值**:\n\n- **类型**: NodeData \\| ComboData \\| undefined\n- **描述**: 返回父元素数据,如果不存在则返回 undefined\n\n**示例**:\n\n```typescript\n// 获取树图中节点的父节点\nconst treeParent = graph.getParentData('node1', 'tree');\n\n// 获取组合中节点的父组合\nconst comboParent = graph.getParentData('node1', 'combo');\n```\n\n### Graph.getChildrenData()\n\n获取节点或组合的子元素数据。\n\n```typescript\ngetChildrenData(id: ID):(NodeData \\| ComboData)[];\n```\n\n**参数**:\n\n| 参数 | 描述            | 类型   | 默认值 | 必选 |\n| ---- | --------------- | ------ | ------ | ---- |\n| id   | 节点或组合的 ID | string | -      | ✓    |\n\n**返回值**:\n\n- **类型**: (NodeData \\| ComboData)[]\n- **描述**: 返回子元素数据数组\n\n**注意**:\n\n- **查询 combo 的子元素**：如果 id 对应的是 combo 元素，可以直接通过此 API 获取其所有子元素。\n- **查询节点的子元素**：如果 id 对应的是节点，只有当图数据为树结构（即节点数据中维护有 `children` 字段，且 `children` 为该节点的子节点 ID 数组）时，才能通过此 API 获取到该节点的子元素。否则返回空数组。\n\n**示例**:\n\n```typescript\n// 获取组合的子元素\nconst children = graph.getChildrenData('combo1');\nconsole.log('子节点数量:', children.length);\n\n// 处理每个子元素\nchildren.forEach((child) => {\n  console.log('子元素ID:', child.id);\n});\n```\n\n### Graph.getAncestorsData()\n\n获取节点或组合的所有祖先元素数据。\n\n```typescript\ngetAncestorsData(id: ID, hierarchy: HierarchyKey): NodeLikeData[];\n```\n\n**参数**:\n\n| 参数      | 描述             | 类型                  | 默认值 | 必选 |\n| --------- | ---------------- | --------------------- | ------ | ---- |\n| id        | 节点或组合的 ID  | string                | -      | ✓    |\n| hierarchy | 指定层级关系类型 | `'tree'` \\| `'combo'` | -      | ✓    |\n\n**返回值**:\n\n- **类型**: [NodeData](#nodedata)[] \\| [ComboData](#combodata)[]\n- **描述**: 返回祖先元素数据数组，从父节点到根节点的顺序排列\n\n**示例**:\n\n```typescript\n// 获取树图中节点的所有祖先节点\nconst treeAncestors = graph.getAncestorsData('node1', 'tree');\nconsole.log(\n  '祖先节点路径:',\n  treeAncestors.map((node) => node.id),\n);\n\n// 获取组合中节点的所有父组合\nconst comboAncestors = graph.getAncestorsData('node1', 'combo');\n```\n\n### Graph.getDescendantsData()\n\n获取节点或组合的所有后代元素数据。\n\n```typescript\ngetDescendantsData(id: ID): NodeLikeData[];\n```\n\n**参数**:\n\n| 参数 | 描述            | 类型   | 默认值 | 必选 |\n| ---- | --------------- | ------ | ------ | ---- |\n| id   | 节点或组合的 ID | string | -      | ✓    |\n\n**返回值**:\n\n- **类型**: [NodeData](#nodedata)[] \\| [ComboData](#combodata)[]\n- **描述**: 返回后代元素数据数组\n\n**示例**:\n\n```typescript\n// 获取节点的所有后代\nconst descendants = graph.getDescendantsData('node1');\nconsole.log('后代数量:', descendants.length);\n\n// 处理所有后代元素\ndescendants.forEach((descendant) => {\n  console.log('后代元素ID:', descendant.id);\n});\n```\n\n### Graph.setData()\n\n设置图的完整数据。\n\n```typescript\nsetData(data: GraphData | ((prev: GraphData) => GraphData)): void;\n```\n\n**参数**:\n\n| 参数 | 描述                           | 类型                                                        | 默认值 | 必选 |\n| ---- | ------------------------------ | ----------------------------------------------------------- | ------ | ---- |\n| data | 新的图数据或返回新图数据的函数 | [GraphData](#graphdata) \\| ((prev: GraphData) => GraphData) | -      | ✓    |\n\n**示例**:\n\n```typescript\n// 直接设置数据\ngraph.setData({\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 } },\n    { id: 'node2', style: { x: 200, y: 200 } },\n  ],\n  edges: [{ id: 'edge1', source: 'node1', target: 'node2' }],\n});\n\n// 使用函数式增量更新：获取当前图数据，并返回新的图数据\ngraph.setData((prev) => ({\n  ...prev,\n  nodes: [...prev.nodes, { id: 'node3', style: { x: 300, y: 300 } }],\n}));\n```\n\n### Graph.addData()\n\n新增元素数据。\n\n```typescript\naddData(data: GraphData | ((prev: GraphData) => GraphData)): void;\n```\n\n**参数**:\n\n| 参数 | 描述                                 | 类型                                                        | 默认值 | 必选 |\n| ---- | ------------------------------------ | ----------------------------------------------------------- | ------ | ---- |\n| data | 需要添加的图数据或返回新图数据的函数 | [GraphData](#graphdata) \\| ((prev: GraphData) => GraphData) | -      | ✓    |\n\n**示例**:\n\n```typescript\ngraph.addData({\n  nodes: [{ id: 'node-1' }, { id: 'node-2' }],\n  edges: [{ source: 'node-1', target: 'node-2' }],\n});\n```\n\n### Graph.addNodeData()\n\n新增节点数据。\n\n```typescript\naddNodeData(data: NodeData[] | ((prev: NodeData[]) => NodeData[])): void;\n```\n\n**参数**:\n\n| 参数 | 描述                                 | 类型                                                        | 默认值 | 必选 |\n| ---- | ------------------------------------ | ----------------------------------------------------------- | ------ | ---- |\n| data | 要添加的节点数据或返回节点数据的函数 | [NodeData](#nodedata)[] \\| (prev: NodeData[]) => NodeData[] | -      | ✓    |\n\n**示例**:\n\n```typescript\n// 添加单个节点\ngraph.addNodeData([\n  {\n    id: 'node1',\n    style: { x: 100, y: 100 },\n    data: { label: '节点 1' },\n  },\n]);\n\n// 批量添加节点\ngraph.addNodeData([\n  { id: 'node2', style: { x: 200, y: 200 } },\n  { id: 'node3', style: { x: 300, y: 300 } },\n]);\n\n// 函数式添加\ngraph.addNodeData((prev) => [...prev, { id: 'node4', style: { x: 400, y: 400 } }]);\n```\n\n### Graph.addEdgeData()\n\n新增边数据。\n\n```typescript\naddEdgeData(data: EdgeData[] | ((prev: EdgeData[]) => EdgeData[])): void;\n```\n\n**参数**:\n\n| 参数 | 描述                             | 类型                                                          | 默认值 | 必选 |\n| ---- | -------------------------------- | ------------------------------------------------------------- | ------ | ---- |\n| data | 要添加的边数据或返回边数据的函数 | [EdgeData](#edgedata)[] \\| ((prev: EdgeData[]) => EdgeData[]) | -      | ✓    |\n\n**示例**:\n\n```typescript\n// 添加单条边\ngraph.addEdgeData([\n  {\n    id: 'edge1',\n    source: 'node1',\n    target: 'node2',\n    data: {\n      weight: 1,\n      label: '关系',\n    },\n  },\n]);\n\n// 批量添加边\ngraph.addEdgeData([\n  { id: 'edge2', source: 'node2', target: 'node3' },\n  { id: 'edge3', source: 'node3', target: 'node1' },\n]);\n\n// 函数式添加\ngraph.addEdgeData((prev) => [...prev, { id: 'edge4', source: 'node1', target: 'node4' }]);\n```\n\n### Graph.addComboData()\n\n新增组合数据。\n\n```typescript\naddComboData(data: ComboData[] | ((prev: ComboData[]) => ComboData[])): void;\n```\n\n**参数**:\n\n| 参数 | 描述                                 | 类型                                                            | 默认值 | 必选 |\n| ---- | ------------------------------------ | --------------------------------------------------------------- | ------ | ---- |\n| data | 要添加的组合数据或返回组合数据的函数 | [ComboData](#combodata)[] \\| (prev: ComboData[]) => ComboData[] | -      | ✓    |\n\n**示例**:\n\n```typescript\ngraph.addComboData([{ id: 'combo1', children: ['node1', 'node2'] }]);\n```\n\n### Graph.addChildrenData()\n\n为树图节点添加子节点数据。\n\n⚠️ **注意**: 为组合添加子节点使用 addNodeData / addComboData 方法。\n\n```typescript\naddChildrenData(parentId: ID, childrenData: NodeData[]): void;\n```\n\n**参数**:\n\n| 参数         | 描述       | 类型                    | 默认值 | 必选 |\n| ------------ | ---------- | ----------------------- | ------ | ---- |\n| parentId     | 父节点 ID  | string                  | -      | ✓    |\n| childrenData | 子节点数据 | [NodeData](#nodedata)[] | -      | ✓    |\n\n**示例**:\n\n```typescript\ngraph.addChildrenData('node1', [{ id: 'node2' }]);\n```\n\n### Graph.removeData()\n\n删除元素数据。\n\n```typescript\nremoveData(ids: DataID | ((data: GraphData) => DataID)): void;\n```\n\n**参数**:\n\n| 参数 | 描述                                 | 类型                                               | 默认值 | 必选 |\n| ---- | ------------------------------------ | -------------------------------------------------- | ------ | ---- |\n| ids  | 要删除的元素 ID 或返回元素 ID 的函数 | [DataID](#dataid) \\| ((data: GraphData) => DataID) | -      | ✓    |\n\n**返回值**:\n\n- **类型**: void\n\n**示例**:\n\n```typescript\ngraph.removeData({\n  nodes: ['node-1', 'node-2'],\n  edges: ['edge-1'],\n});\n```\n\n### Graph.removeNodeData()\n\n删除节点数据。\n\n```typescript\nremoveNodeData(ids: ID[] | ((data: NodeData[]) => ID[])): void;\n```\n\n**参数**:\n\n| 参数 | 描述                                 | 类型                                                            | 默认值 | 必选 |\n| ---- | ------------------------------------ | --------------------------------------------------------------- | ------ | ---- |\n| ids  | 要删除的节点 ID 或返回节点 ID 的函数 | [ID](#id)[] \\| ((data: [NodeData](#nodedata)[]) => [ID](#id)[]) | -      | ✓    |\n\n**返回值**:\n\n- **类型**: void\n\n**示例**:\n\n```typescript\ngraph.removeNodeData(['node-1', 'node-2']);\n```\n\n### Graph.removeEdgeData()\n\n删除边数据。\n\n```typescript\nremoveEdgeData(ids: ID[] | ((data: EdgeData[]) => ID[])): void;\n```\n\n**参数**:\n\n| 参数 | 描述                             | 类型                                                            | 默认值 | 必选 |\n| ---- | -------------------------------- | --------------------------------------------------------------- | ------ | ---- |\n| ids  | 要删除的边 ID 或返回边 ID 的函数 | [ID](#id)[] \\| ((data: [EdgeData](#edgedata)[]) => [ID](#id)[]) | -      | ✓    |\n\n**返回值**:\n\n- **类型**: void\n\n**示例**:\n\n```typescript\ngraph.removeEdgeData(['edge-1']);\n```\n\n### Graph.removeComboData()\n\n删除组合数据。\n\n```typescript\nremoveComboData(ids: ID[] | ((data: ComboData[]) => ID[])): void;\n```\n\n**参数**:\n\n| 参数 | 描述                                 | 类型                                                            | 默认值 | 必选 |\n| ---- | ------------------------------------ | --------------------------------------------------------------- | ------ | ---- |\n| ids  | 要删除的组合 ID 或返回组合 ID 的函数 | [ID](#id)[] \\| (data: [ComboData](#combodata)[]) => [ID](#id)[] | -      | ✓    |\n\n**返回值**:\n\n- **类型**: void\n\n**示例**:\n\n```typescript\ngraph.removeComboData(['combo-1']);\n```\n\n### Graph.updateData()\n\n更新元素数据。\n\n⚠️ **注意**: 只需要传入需要更新的数据即可，不必传入完整的数据。\n\n```typescript\nupdateData(data: PartialGraphData | ((prev: GraphData) => PartialGraphData)): void;\n```\n\n**参数**:\n\n| 参数 | 描述                                 | 类型                                                                             | 默认值 | 必选 |\n| ---- | ------------------------------------ | -------------------------------------------------------------------------------- | ------ | ---- |\n| data | 要更新的元素数据或返回元素数据的函数 | [PartialGraphData](#partialgraphdata) \\| ((prev: GraphData) => PartialGraphData) | -      | ✓    |\n\n**返回值**:\n\n- **类型**: void\n\n**示例**:\n\n```typescript\ngraph.updateData({\n  nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],\n  edges: [{ id: 'edge-1', style: { lineWidth: 2 } }],\n});\n```\n\n### Graph.updateNodeData()\n\n更新节点数据。\n\n⚠️ **注意**: 只需要传入需要更新的数据即可，不必传入完整的数据。\n\n```typescript\nupdateNodeData(data: NodeData[] | ((prev: NodeData[]) => NodeData[])): void;\n```\n\n**参数**:\n\n| 参数 | 描述                                 | 类型                                           | 默认值 | 必选 |\n| ---- | ------------------------------------ | ---------------------------------------------- | ------ | ---- |\n| data | 要更新的节点数据或返回节点数据的函数 | NodeData[] \\| (prev: NodeData[]) => NodeData[] | -      | ✓    |\n\n**返回值**:\n\n- **类型**: void\n\n**示例**:\n\n```typescript\ngraph.updateNodeData([{ id: 'node-1', style: { x: 100, y: 100 } }]);\n```\n\n### Graph.updateEdgeData()\n\n更新边数据。\n\n⚠️ **注意**: 只需要传入需要更新的数据即可，不必传入完整的数据。\n\n```typescript\nupdateEdgeData(data: (PartialEdgeData<EdgeData>[] | ((prev: EdgeData[]) => PartialEdgeData<EdgeData>[]))): void;\n```\n\n**参数**:\n\n| 参数 | 描述                             | 类型                                                                                                                         | 默认值 | 必选 |\n| ---- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------ | ---- |\n| data | 要更新的边数据或返回边数据的函数 | [PartialEdgeData\\<EdgeData\\>](#partialedgedata)[] \\| (prev: EdgeData[]) => [PartialEdgeData\\<EdgeData\\>](#partialedgedata)[] | -      | ✓    |\n\n**返回值**:\n\n- **类型**: void\n\n**示例**:\n\n```typescript\ngraph.updateEdgeData([{ id: 'edge-1', style: { lineWidth: 2 } }]);\n```\n\n### Graph.updateComboData()\n\n更新组合数据。\n\n⚠️ **注意**: 只需要传入需要更新的数据即可，不必传入完整的数据。\n\n```typescript\nupdateComboData(data: (ComboData[] | ((prev: ComboData[]) => ComboData[]))): void;\n```\n\n**参数**:\n\n| 参数 | 描述                                 | 类型                                                            | 默认值 | 必选 |\n| ---- | ------------------------------------ | --------------------------------------------------------------- | ------ | ---- |\n| data | 要更新的组合数据或返回组合数据的函数 | [ComboData](#combodata)[] \\| (prev: ComboData[]) => ComboData[] | -      | ✓    |\n\n**返回值**:\n\n- **类型**: void\n\n**示例**:\n\n```typescript\ngraph.updateComboData([{ id: 'combo-1', style: { x: 100, y: 100 } }]);\n```\n\n### Graph.hasNode()\n\n判断节点是否存在\n\n```typescript\nhasNode(id:ID): boolean;\n```\n\n**参数**:\n\n| 参数 | 描述                 | 类型      | 默认值 | 必选 |\n| ---- | -------------------- | --------- | ------ | ---- |\n| id   | 需要进行判断的节点id | [ID](#id) | -      | ✓    |\n\n**返回值**:\n\n- **类型**: boolean\n  **示例**:\n\n```typescript\ngraph.hasNode('node-1');\n```\n\n### Graph.hasEdge()\n\n判断边是否存在\n\n```typescript\nhasEdge(id:ID): boolean;\n```\n\n**参数**:\n\n| 参数 | 描述               | 类型      | 默认值 | 必选 |\n| ---- | ------------------ | --------- | ------ | ---- |\n| id   | 需要进行判断的边id | [ID](#id) | -      | ✓    |\n\n**返回值**:\n\n- **类型**: boolean\n  **示例**:\n\n```typescript\ngraph.hasEdge('edge-1');\n```\n\n### Graph.hasCombo()\n\n判断combo是否存在\n\n```typescript\nhasCombo(id:ID): boolean;\n```\n\n**参数**:\n\n| 参数 | 描述                      | 类型      | 默认值 | 必选 |\n| ---- | ------------------------- | --------- | ------ | ---- |\n| id   | 需要进行判断的combo组合id | [ID](#id) | -      | ✓    |\n\n**返回值**:\n\n- **类型**: boolean\n  **示例**:\n\n```typescript\ngraph.hasCombo('combo-1');\n```\n\n## 类型定义\n\n### ID\n\n元素 ID 类型。\n\n```typescript\ntype ID = string;\n```\n\n### DataID\n\n多个元素 ID 类型。\n\n```typescript\ninterface DataID {\n  nodes?: ID[];\n  edges?: ID[];\n  combos?: ID[];\n}\n```\n\n### GraphData\n\nG6 图数据类型。\n\n```typescript\ninterface GraphData {\n  nodes?: NodeData[];\n  edges?: EdgeData[];\n  combos?: ComboData[];\n}\n```\n\n### NodeData\n\n节点数据类型。\n\n```typescript\ninterface NodeData {\n  id: string; // 节点 ID\n  type?: string; // 节点类型\n  data?: Record<string, any>; // 节点数据\n  style?: Record<string, any>; // 节点样式\n  states?: string[]; // 节点初始状态\n  combo?: string; // 所属组合\n  children?: string[]; // 子节点 ID 数组\n}\n```\n\n详细类型定义请参考 [节点数据](/manual/data#节点数据nodedata)。\n\n### EdgeData\n\n边数据类型。\n\n```typescript\ninterface EdgeData {\n  source: string; // 起点 ID\n  target: string; // 终点 ID\n  id?: string; // 边 ID\n  type?: string; // 边类型\n  data?: Record<string, any>; // 边数据\n  style?: Record<string, any>; // 边样式\n  states?: string[]; // 边初始状态\n}\n```\n\n详细类型定义请参考 [边数据](/manual/data#边数据edgedata)。\n\n### ComboData\n\n组合数据类型。\n\n```typescript\ninterface ComboData {\n  id: string; // 组合 ID\n  type?: string; // 组合类型\n  data?: Record<string, any>; // 组合数据\n  style?: Record<string, any>; // 组合样式\n  states?: string[]; // 组合初始状态\n  combo?: string; // 父组合 ID\n}\n```\n\n详细类型定义请参考 [组合数据](/manual/data#组合数据combodata)。\n"
  },
  {
    "path": "packages/site/docs/api/element.en.md",
    "content": "---\ntitle: Element Operations\norder: 1\n---\n\n## Overview of Element Operations\n\nThe [Element](/en/manual/element/overview) operation API in G6 allows you to control the behavior and attributes of elements such as nodes, edges, and Combos in the graph. These APIs can be used for:\n\n1. **Element State Management**: Set, update, or remove the state of elements\n2. **Element Display Control**: Control the z-index and visibility of elements\n3. **Element Collapse/Expand**: Operate the collapse/expand state of collapsible elements\n4. **Element Position Operations**: Move and align element positions\n5. **Element Focus**: Focus the viewport on specific elements\n\nThrough these operations, you can achieve rich interactive effects and visual presentations.\n\n## API Reference\n\n### Graph.getElementPosition(id)\n\nGet the position of an element.\n\n```typescript\ngetElementPosition(id: ID): Point;\n```\n\n**Parameters**:\n\n| Parameter | Description | Type   | Default | Required |\n| --------- | ----------- | ------ | ------- | -------- |\n| id        | Element ID  | string | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: [number, number] \\| [number, number, number]\n- **Description**: Returns the coordinates of the element\n\n**Example**:\n\n```typescript\ngraph.getElementPosition('node1');\n```\n\n### Graph.getElementRenderBounds(id)\n\nGet the rendering bounding box of the element itself and its child nodes in the world coordinate system.\n\n```typescript\ngetElementRenderBounds(id: ID): AABB;\n```\n\n**Parameters**:\n\n| Parameter | Description | Type   | Default | Required |\n| --------- | ----------- | ------ | ------- | -------- |\n| id        | Element ID  | string | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: [AABB](#aabb)\n- **Description**: Returns the rendering bounding box of the element\n\n### Graph.getElementRenderStyle(id)\n\nGet the rendering style of an element.\n\n```typescript\ngetElementRenderStyle(id: ID): Record<string, any>;\n```\n\n**Parameters**:\n\n| Parameter | Description | Type   | Default | Required |\n| --------- | ----------- | ------ | ------- | -------- |\n| id        | Element ID  | string | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: Record<string, any>\n- **Description**: Returns the rendering style of the element\n\n### Graph.getElementState(id)\n\nGet the state of an element.\n\n```typescript\ngetElementState(id: ID): State[];\n```\n\n**Parameters**:\n\n| Parameter | Description | Type   | Default | Required |\n| --------- | ----------- | ------ | ------- | -------- |\n| id        | Element ID  | string | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: [State](#state)[]\n- **Description**: Returns the state of the element\n\n### Graph.getElementType(id)\n\nGet the type of an element.\n\n```typescript\ngetElementType(id: ID): string;\n```\n\n**Parameters**:\n\n| Parameter | Description | Type   | Default | Required |\n| --------- | ----------- | ------ | ------- | -------- |\n| id        | Element ID  | string | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: string\n- **Description**: Returns the type of the element\n\n### Graph.getElementVisibility(id)\n\nGet the visibility of an element.\n\n```typescript\ngetElementVisibility(id: ID): 'visible' | 'hidden';\n```\n\n**Parameters**:\n\n| Parameter | Description | Type   | Default | Required |\n| --------- | ----------- | ------ | ------- | -------- |\n| id        | Element ID  | string | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: 'visible' | 'hidden'\n- **Description**: Returns the visibility of the element\n\n### Graph.getElementZIndex(id)\n\nGet the z-index of an element.\n\n```typescript\ngetElementZIndex(id: ID): number;\n```\n\n**Parameters**:\n\n| Parameter | Description | Type   | Default | Required |\n| --------- | ----------- | ------ | ------- | -------- |\n| id        | Element ID  | string | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: number\n- **Description**: Returns the z-index of the element\n\n### Graph.setElementState(id, state, options)\n\nSet the state of an element, supporting two calling methods:\n\n```typescript\n// Set the state of a single element\nsetElementState(id: ID, state: State | State[], animation?: boolean): Promise<void>;\n\n// Set the state of multiple elements\nsetElementState(state: Record<ID, State | State[]>, animation?: boolean): Promise<void>;\n```\n\n**Parameters**:\n\n**Single Element State Setting**\n\n| Parameter | Description       | Type            | Default | Required |\n| --------- | ----------------- | --------------- | ------- | -------- | --- |\n| id        | Element ID to set | string          | -       | ✓        |\n| state     | State to set      | [State](#state) | State[] | -        | ✓   |\n| animation | Enable animation  | boolean         | -       |          |\n\n**Batch Element State Setting**\n\n| Parameter | Description                    | Type                       | Default  | Required |\n| --------- | ------------------------------ | -------------------------- | -------- | -------- | --- |\n| state     | Mapping of element ID to state | Record<ID, [State](#state) | State[]> | -        | ✓   |\n| animation | Enable animation               | boolean                    | -        |          |\n\n**Return Value**:\n\n- **Type**: Promise<void>\n- **Description**: Returns a Promise that resolves when the state setting operation is complete\n\n**Example**:\n\n```typescript\n// Set the state of a single element\nawait graph.setElementState('node1', 'selected');\n\n// Set the state of multiple elements\nawait graph.setElementState({\n  node1: 'selected',\n  node2: 'hover',\n  node3: ['selected', 'hover'],\n});\n```\n\n### Graph.setElementVisibility(id, visibility, animation)\n\nSet the visibility of an element, supporting two calling methods:\n\n```typescript\n// Set the visibility of a single element\nsetElementVisibility(id: ID, visibility: 'visible' | 'hidden', animation?: boolean): Promise<void>;\n\n// Set the visibility of multiple elements\nsetElementVisibility(visibility: Record<ID, 'visible' | 'hidden'>, animation?: boolean): Promise<void>;\n```\n\n**Parameters**:\n\n**Single Element Visibility Setting**\n\n| Parameter  | Description       | Type      | Default  | Required |\n| ---------- | ----------------- | --------- | -------- | -------- | --- |\n| id         | Element ID to set | string    | -        | ✓        |\n| visibility | Visibility to set | 'visible' | 'hidden' | -        | ✓   |\n| animation  | Enable animation  | boolean   | -        |          |\n\n**Batch Element Visibility Setting**\n\n| Parameter  | Description                         | Type                 | Default   | Required |\n| ---------- | ----------------------------------- | -------------------- | --------- | -------- | --- |\n| visibility | Mapping of element ID to visibility | Record<ID, 'visible' | 'hidden'> | -        | ✓   |\n| animation  | Enable animation                    | boolean              | -         |          |\n\n**Return Value**:\n\n- **Type**: Promise<void>\n- **Description**: Returns a Promise that resolves when the visibility setting operation is complete\n\n**Example**:\n\n```typescript\n// Set the visibility of a single element\nawait graph.setElementVisibility('node1', 'hidden');\n\n// Set the visibility of multiple elements\nawait graph.setElementVisibility({\n  node1: 'hidden',\n  node2: 'visibility',\n});\n```\n\n### Graph.setElementZIndex(id, zIndex)\n\nSet the z-index of an element, supporting two calling methods:\n\n```typescript\n// Set the z-index of a single element\nsetElementZIndex(id: ID, zIndex: number): Promise<void>;\n\n// Set the z-index of multiple elements\nsetElementZIndex(zIndex: Record<ID, number>): Promise<void>;\n```\n\n**Parameters**:\n\n**Single Element Z-Index Setting**\n\n| Parameter | Description | Type   | Default | Required |\n| --------- | ----------- | ------ | ------- | -------- |\n| id        | Element ID  | string | -       | ✓        |\n| zIndex    | Z-Index     | number | -       | ✓        |\n\n**Batch Element Z-Index Setting**\n\n| Parameter | Description                      | Type               | Default | Required |\n| --------- | -------------------------------- | ------------------ | ------- | -------- |\n| zIndex    | Mapping of element ID to z-index | Record<ID, number> | -       | ✓        |\n\n**Return Value**:\n\n- **Type**: Promise<void>\n- **Description**: Returns a Promise that resolves when the z-index setting operation is complete\n\n**Example**:\n\n```typescript\n// Set the z-index of a single element\nawait graph.setElementZIndex('node1', 10);\n\n// Set the z-index of multiple elements\nawait graph.setElementZIndex({\n  node1: 10,\n  node2: 20,\n  node3: 30,\n});\n```\n\n### Graph.setNode(node)\n\nSet the node style mapping, i.e., the value of `options.node`.\n\n```typescript\nsetNode(node: NodeOptions): void;\n```\n\n**Parameters**:\n\n| Parameter | Description        | Type                                            | Default | Required |\n| --------- | ------------------ | ----------------------------------------------- | ------- | -------- |\n| node      | Node configuration | [NodeOptions](/en/manual/element/node/overview) | -       | ✓        |\n\n**Example**:\n\n```typescript\n// Set the fill color of all nodes to red\ngraph.setNode({\n  style: {\n    fill: 'red',\n  },\n});\n```\n\n### Graph.setEdge(edge)\n\nSet the edge style mapping, i.e., the value of `options.edge`.\n\n```typescript\nsetEdge(edge: EdgeOptions): void;\n```\n\n**Parameters**:\n\n| Parameter | Description        | Type                                            | Default | Required |\n| --------- | ------------------ | ----------------------------------------------- | ------- | -------- |\n| edge      | Edge configuration | [EdgeOptions](/en/manual/element/edge/overview) | -       | ✓        |\n\n### Graph.setCombo(combo)\n\nSet the combo style mapping, i.e., the value of `options.combo`.\n\n```typescript\nsetCombo(combo: ComboOptions): void;\n```\n\n**Parameters**:\n\n| Parameter | Description         | Type                                              | Default | Required |\n| --------- | ------------------- | ------------------------------------------------- | ------- | -------- |\n| combo     | Combo configuration | [ComboOptions](/en/manual/element/combo/overview) | -       | ✓        |\n\n### Graph.collapseElement(id, options)\n\nCollapse the specified element, usually used to collapse Combos or nodes with child elements.\n\n```typescript\ncollapseElement(id: ID, options?: boolean | CollapseExpandNodeOptions): Promise<void>;\n```\n\n**Parameters**:\n\n| Parameter | Description                                                     | Type    | Default                                                 | Required |\n| --------- | --------------------------------------------------------------- | ------- | ------------------------------------------------------- | -------- | --- |\n| id        | Element ID to collapse                                          | string  | -                                                       | ✓        |\n| options   | Enable animation or detailed configuration for collapsing nodes | boolean | [CollapseExpandNodeOptions](#collapseexpandnodeoptions) | -        |     |\n\n**Return Value**:\n\n- **Type**: Promise<void>\n- **Description**: Returns a Promise that resolves when the collapse operation is complete\n\n**Example**:\n\n```typescript\n// Simple collapse with default configuration\nawait graph.collapseElement('combo1');\n\n// Collapse with animation\ngraph.collapseElement('combo1', true);\n\n// Collapse while ensuring the position of expanded/collapsed nodes remains unchanged\nawait graph.collapseElement('combo1', {\n  align: true,\n});\n```\n\n### Graph.expandElement(id, options)\n\nExpand the specified element, usually used to expand previously collapsed Combos or nodes.\n\n```typescript\nexpandElement(id: ID, options?: boolean | CollapseExpandNodeOptions): Promise<void>;\n```\n\n**Parameters**:\n\n| Parameter | Description                                                    | Type    | Default                                                 | Required |\n| --------- | -------------------------------------------------------------- | ------- | ------------------------------------------------------- | -------- | --- |\n| id        | Element ID to expand                                           | string  | -                                                       | ✓        |\n| options   | Enable animation or detailed configuration for expanding nodes | boolean | [CollapseExpandNodeOptions](#collapseexpandnodeoptions) | -        |     |\n\n**Return Value**:\n\n- **Type**: Promise<void>\n- **Description**: Returns a Promise that resolves when the expand operation is complete\n\n**Example**:\n\n```typescript\n// Simple expand with default configuration\nawait graph.expandElement('combo1');\n\n// Expand with animation\nawait graph.expandElement('combo1', true);\n\n// Expand while ensuring the position of expanded/collapsed nodes remains unchanged\nawait graph.expandElement('combo1', {\n  align: true,\n});\n```\n\n### Graph.frontElement(id)\n\nBring the specified element to the front, making it appear above other overlapping elements.\n\n```typescript\nfrontElement(id: ID | ID[]): void;\n```\n\n**Parameters**:\n\n| Parameter | Description | Type   | Default  | Required |\n| --------- | ----------- | ------ | -------- | -------- | --- |\n| id        | Element ID  | string | string[] | -        | ✓   |\n\n**Return Value**:\n\n- **Type**: void\n\n**Example**:\n\n```typescript\n// Bring a node to the front\ngraph.frontElement('node1');\n\n// Bring multiple selected nodes to the front\ngraph.frontElement(['node1', 'node2', 'node3']);\n```\n\n### Graph.showElement(id, animation)\n\nShow the specified element.\n\n```typescript\nshowElement(id: ID | ID[], animation?: boolean): Promise<void>;\n```\n\n**Parameters**:\n\n| Parameter | Description      | Type    | Default  | Required |\n| --------- | ---------------- | ------- | -------- | -------- | --- |\n| id        | Element ID       | string  | string[] | -        | ✓   |\n| animation | Enable animation | boolean | -        |          |\n\n**Return Value**:\n\n- **Type**: Promise<void>\n- **Description**: Returns a Promise that resolves when the show operation is complete\n\n**Example**:\n\n```typescript\n// Show a single element\nawait graph.showElement('node1');\n\n// Show an element with animation\nawait graph.showElement('node1', true);\n\n// Show multiple elements\nawait graph.showElement(['node1', 'node2', 'node3']);\n```\n\n### Graph.hideElement(id, animation)\n\nHide the specified element.\n\n```typescript\nhideElement(id: ID | ID[], animation?: boolean): Promise<void>;\n```\n\n**Parameters**:\n\n| Parameter | Description      | Type    | Default  | Required |\n| --------- | ---------------- | ------- | -------- | -------- | --- |\n| id        | Element ID       | string  | string[] | -        | ✓   |\n| animation | Enable animation | boolean | -        |          |\n\n**Return Value**:\n\n- **Type**: Promise<void>\n- **Description**: Returns a Promise that resolves when the hide operation is complete\n\n**Example**:\n\n```typescript\n// Hide an element without animation\nawait graph.hideElement('node1');\n\n// Hide an element with animation\nawait graph.hideElement('node1', true);\n\n// Hide multiple elements\nawait graph.hideElement(['node1', 'node2', 'node3'], true);\n```\n\n### Graph.translateElementBy(id, offset, animation)\n\nTranslate an element by a specified distance, supporting two calling methods:\n\n```typescript\n// Translate an element by a specified distance (relative translation)\ntranslateElement(id: ID, offset: Point, animation?: boolean): Promise<void>;\n\n// Translate multiple elements by a specified distance (relative translation)\ntranslateElement(offsets: Record<ID, Point>, animation?: boolean): Promise<void>;\n```\n\n**Parameters**:\n\n**Single Element Translation**\n\n| Parameter | Description                            | Type             | Default | Required |\n| --------- | -------------------------------------- | ---------------- | ------- | -------- |\n| id        | Element ID                             | string           | -       | ✓        |\n| offset    | Relative translation distance [dx, dy] | [number, number] | -       | ✓        |\n| animation | Enable animation                       | boolean          | -       |          |\n\n**Batch Element Translation**\n\n| Parameter | Description                                   | Type                         | Default | Required |\n| --------- | --------------------------------------------- | ---------------------------- | ------- | -------- |\n| offsets   | Mapping of element ID to translation distance | Record<ID, [number, number]> | -       | ✓        |\n| animation | Enable animation                              | boolean                      | -       |          |\n\n**Return Value**:\n\n- **Type**: Promise<void>\n- **Description**: Returns a Promise that resolves when the translation operation is complete\n\n**Example**:\n\n```typescript\n// Translate right by 100 pixels and down by 50 pixels\nawait graph.translateElementBy('node1', [100, 50]);\n\n// Translate with animation\nawait graph.translateElementBy('node1', [100, 50], true);\n\n// Apply the same translation to multiple nodes\nawait graph.translateElementBy(\n  {\n    node1: [50, 50],\n    node2: [100, 100],\n    node3: [150, 150],\n  },\n  true,\n);\n```\n\n### Graph.translateElementTo(id, position, animation)\n\nMove an element to a specified position, supporting two calling methods:\n\n```typescript\n// Move an element to a specified position (absolute position)\ntranslateElementTo(id: ID, position: Point, animation?: boolean): Promise<void>;\n\n// Move multiple elements to specified positions (absolute position)\ntranslateElementTo(positions: Record<ID, Point>, animation?: boolean): Promise<void>;\n```\n\n**Parameters**:\n\n**Single Element Movement**\n\n| Parameter | Description                     | Type             | Default | Required |\n| --------- | ------------------------------- | ---------------- | ------- | -------- |\n| id        | Element ID                      | string           | -       | ✓        |\n| position  | Target absolute position [x, y] | [number, number] | -       | ✓        |\n| animation | Enable animation                | boolean          | -       |          |\n\n**Batch Element Movement**\n\n| Parameter | Description                              | Type                             | Default | Required |\n| --------- | ---------------------------------------- | -------------------------------- | ------- | -------- |\n| positions | Mapping of element ID to target position | Record<string, [number, number]> | -       | ✓        |\n| animation | Enable animation                         | boolean                          | -       |          |\n\n**Return Value**:\n\n- **Type**: Promise<void>\n- **Description**: Returns a Promise that resolves when the movement operation is complete\n\n**Example**:\n\n```typescript\n// Move a node to position (200, 300) on the canvas\nawait graph.translateElementTo('node1', [200, 300]);\n\n// Move with animation\nawait graph.translateElementTo('node1', [200, 300], true);\n\n// Arrange a group of nodes neatly\nawait graph.translateElementTo(\n  {\n    node1: [100, 100],\n    node2: [200, 200],\n    node3: [300, 100],\n  },\n  true,\n);\n```\n\n### Graph.focusElement(id, animation)\n\nFocus on the specified element, centering it in the viewport.\n\n```typescript\nfocusElement(id: ID | ID[], animation?: ViewportAnimationEffectTiming): Promise<void>;\n```\n\n**Parameters**:\n\n| Parameter | Description                         | Type                                                            | Default  | Required |\n| --------- | ----------------------------------- | --------------------------------------------------------------- | -------- | -------- | --- |\n| id        | One or more element IDs to focus on | string                                                          | string[] | -        | ✓   |\n| animation | Viewport animation configuration    | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -        |          |\n\n**Return Value**:\n\n- **Type**: Promise<void>\n- **Description**: Returns a Promise that resolves when the focus operation is complete\n\n**Example**:\n\n```typescript\n// Focus on a single node\nawait graph.focusElement('node1');\n\n// Use custom animation configuration\nawait graph.focusElement('node1', {\n  duration: 800,\n  easing: 'ease-in-out',\n});\n\n// Focus on multiple nodes\nawait graph.focusElement(['node1', 'node2', 'node3']);\n```\n\n## Type Definitions\n\n### CollapseExpandNodeOptions\n\nConfiguration options for collapsing or expanding elements.\n\n```typescript\ninterface CollapseExpandNodeOptions {\n  /**\n   * Enable animation\n   */\n  animation?: boolean;\n  /**\n   * Ensure the position of expanded/collapsed nodes remains unchanged\n   */\n  align?: boolean;\n}\n```\n\n### ViewportAnimationEffectTiming\n\nViewport animation configuration type.\n\n```typescript\ntype ViewportAnimationEffectTiming =\n  | boolean // Enable animation\n  | {\n      easing?: string; // Easing function\n      duration?: number; // Animation duration (ms)\n    };\n```\n\n### AABB\n\nAABB (Axis-Aligned Bounding Box) is a fundamental concept in computer graphics.\n\n```typescript\ninterface AABB {\n  x: number; // x-coordinate of the top-left corner of the rectangle\n  y: number; // y-coordinate of the top-left corner of the rectangle\n  width: number; // Width of the rectangle\n  height: number; // Height of the rectangle\n}\n```\n\n### State\n\nElement state type.\n\n```typescript\ntype State = 'selected' | 'hover' | 'active' | 'inactive' | 'disabled' | string;\n```\n"
  },
  {
    "path": "packages/site/docs/api/element.zh.md",
    "content": "---\ntitle: 元素操作\norder: 1\n---\n\n## 元素操作概述\n\nG6 中的 [元素](/manual/element/overview) 操作 API 允许您控制图中节点、边和组合(Combo)等元素的行为和属性。这些 API 可以用于：\n\n1. **元素状态管理**：设置、更新或移除元素的状态\n2. **元素显示控制**：控制元素的层级、可见性\n3. **元素展开/收起**：操作可折叠元素的展开/收起状态\n4. **元素位置操作**：移动、对齐元素位置\n5. **元素聚焦**：将视口聚焦到特定元素\n\n通过这些操作，您可以实现丰富的交互效果和视觉呈现。\n\n## API 参考\n\n### Graph.getElementPosition(id)\n\n获取元素位置。\n\n```typescript\ngetElementPosition(id: ID): Point;\n```\n\n**参数**:\n\n| 参数 | 描述    | 类型   | 默认值 | 必选 |\n| ---- | ------- | ------ | ------ | ---- |\n| id   | 元素 ID | string | -      | ✓    |\n\n**返回值**:\n\n- **类型**: [number, number] \\| [number, number, number]\n- **描述**: 返回元素的坐标位置\n\n**示例**:\n\n```typescript\ngraph.getElementPosition('node1');\n```\n\n### Graph.getElementRenderBounds(id)\n\n获取元素自身以及子节点在世界坐标系下的渲染包围盒。\n\n```typescript\ngetElementRenderBounds(id: ID): AABB;\n```\n\n**参数**:\n\n| 参数 | 描述    | 类型   | 默认值 | 必选 |\n| ---- | ------- | ------ | ------ | ---- |\n| id   | 元素 ID | string | -      | ✓    |\n\n**返回值**:\n\n- **类型**: [AABB](#aabb)\n- **描述**: 返回元素的渲染包围盒\n\n### Graph.getElementRenderStyle(id)\n\n获取元素的渲染样式。\n\n```typescript\ngetElementRenderStyle(id: ID): Record<string, any>;\n```\n\n**参数**:\n\n| 参数 | 描述    | 类型   | 默认值 | 必选 |\n| ---- | ------- | ------ | ------ | ---- |\n| id   | 元素 ID | string | -      | ✓    |\n\n**返回值**:\n\n- **类型**: Record\\<string, any\\>\n- **描述**: 返回元素的渲染样式\n\n### Graph.getElementState(id)\n\n获取元素的状态。\n\n```typescript\ngetElementState(id: ID): State[];\n```\n\n**参数**:\n\n| 参数 | 描述    | 类型   | 默认值 | 必选 |\n| ---- | ------- | ------ | ------ | ---- |\n| id   | 元素 ID | string | -      | ✓    |\n\n**返回值**:\n\n- **类型**: [State](#state)[]\n- **描述**: 返回元素的状态\n\n### Graph.getElementType(id)\n\n获取元素类型。\n\n```typescript\ngetElementType(id: ID): string;\n```\n\n**参数**:\n\n| 参数 | 描述    | 类型   | 默认值 | 必选 |\n| ---- | ------- | ------ | ------ | ---- |\n| id   | 元素 ID | string | -      | ✓    |\n\n**返回值**:\n\n- **类型**: string\n- **描述**: 返回元素的类型\n\n### Graph.getElementVisibility(id)\n\n获取元素可见性。\n\n```typescript\ngetElementVisibility(id: ID): 'visible' | 'hidden' ;\n```\n\n**参数**:\n\n| 参数 | 描述    | 类型   | 默认值 | 必选 |\n| ---- | ------- | ------ | ------ | ---- |\n| id   | 元素 ID | string | -      | ✓    |\n\n**返回值**:\n\n- **类型**: 'visible' | 'hidden'\n- **描述**: 返回元素的可见性\n\n### Graph.getElementZIndex(id)\n\n获取元素层级。\n\n```typescript\ngetElementZIndex(id: ID): number;\n```\n\n**参数**:\n\n| 参数 | 描述    | 类型   | 默认值 | 必选 |\n| ---- | ------- | ------ | ------ | ---- |\n| id   | 元素 ID | string | -      | ✓    |\n\n**返回值**:\n\n- **类型**: number\n- **描述**: 返回元素的层级\n\n### Graph.setElementState(id, state, options)\n\n设置元素状态，支持两种调用方式：\n\n```typescript\n// 设置单个元素状态\nsetElementState(id: ID, state: State | State[], animation?: boolean): Promise<void>;\n\n// 批量设置元素状态\nsetElementState(state: Record<ID, State | State[]>, animation?: boolean): Promise<void>;\n```\n\n**参数**:\n\n**单个元素状态设置**\n\n| 参数      | 描述                | 类型                       | 默认值 | 必选 |\n| --------- | ------------------- | -------------------------- | ------ | ---- |\n| id        | 要设置状态的元素 ID | string                     | -      | ✓    |\n| state     | 要设置的状态        | [State](#state) \\| State[] | -      | ✓    |\n| animation | 是否启用动画        | boolean                    | -      |      |\n\n**批量元素状态设置**\n\n| 参数      | 描述               | 类型                                   | 默认值 | 必选 |\n| --------- | ------------------ | -------------------------------------- | ------ | ---- |\n| state     | 元素ID到状态的映射 | Record<ID, [State](#state) \\| State[]> | -      | ✓    |\n| animation | 是否启用动画       | boolean                                | -      |      |\n\n**返回值**:\n\n- **类型**: Promise\\<void\\>\n- **描述**: 返回一个 Promise，状态设置操作完成后 resolve\n\n**示例**:\n\n```typescript\n// 设置单个元素状态\nawait graph.setElementState('node1', 'selected');\n\n// 设置多个元素状态\nawait graph.setElementState({\n  node1: 'selected',\n  node2: 'hover',\n  node3: ['selected', 'hover'],\n});\n```\n\n### Graph.setElementVisibility(id, visibility, animation)\n\n设置元素的可见性，支持两种调用方式：\n\n```typescript\n// 设置单个元素可见性\nsetElementVisibility(id: ID, visibility: 'visible' | 'hidden', animation?: boolean): Promise<void>;\n\n// 批量设置元素可见性\nsetElementVisibility(visibility: Record<ID, 'visible' | 'hidden'>, animation?: boolean): Promise<void>;\n```\n\n**参数**:\n\n**单个元素可见性设置**\n\n| 参数       | 描述                  | 类型                      | 默认值 | 必选 |\n| ---------- | --------------------- | ------------------------- | ------ | ---- |\n| id         | 要设置可见性的元素 ID | string                    | -      | ✓    |\n| visibility | 要设置的可见性        | `'visible'` \\| `'hidden'` | -      | ✓    |\n| animation  | 是否启用动画          | boolean                   | -      |      |\n\n**批量元素可见性设置**\n\n| 参数       | 描述                 | 类型                                  | 默认值 | 必选 |\n| ---------- | -------------------- | ------------------------------------- | ------ | ---- |\n| visibility | 元素ID到可见性的映射 | Record<ID, `'visible'` \\| `'hidden'`> | -      | ✓    |\n| animation  | 是否启用动画         | boolean                               | -      |      |\n\n**返回值**:\n\n- **类型**: Promise\\<void\\>\n- **描述**: 返回一个 Promise，可见性设置操作完成后 resolve\n\n**示例**:\n\n```typescript\n// 设置单个元素可见性\nawait graph.setElementVisibility('node1', 'hidden');\n\n// 设置多个元素可见性\nawait graph.setElementVisibility({\n  node1: 'hidden',\n  node2: 'visibility',\n});\n```\n\n### Graph.setElementZIndex(id, zIndex)\n\n设置元素的层级，有两种调用方式：\n\n```typescript\n// 设置单个元素层级\nsetElementZIndex(id: ID, zIndex: number): Promise<void>;\n\n// 批量设置元素层级\nsetElementZIndex(zIndex: Record<ID, number>): Promise<void>;\n```\n\n**参数**:\n\n**单个元素层级设置**\n\n| 参数   | 描述    | 类型   | 默认值 | 必选 |\n| ------ | ------- | ------ | ------ | ---- |\n| id     | 元素 ID | string | -      | ✓    |\n| zIndex | 层级    | number | -      | ✓    |\n\n**批量元素层级设置**\n\n| 参数   | 描述               | 类型               | 默认值 | 必选 |\n| ------ | ------------------ | ------------------ | ------ | ---- |\n| zIndex | 元素ID到层级的映射 | Record<ID, number> | -      | ✓    |\n\n**返回值**:\n\n- **类型**: Promise\\<void\\>\n- **描述**: 返回一个 Promise，层级设置操作完成后 resolve\n\n**示例**:\n\n```typescript\n// 设置单个元素层级\nawait graph.setElementZIndex('node1', 10);\n\n// 设置多个元素层级\nawait graph.setElementZIndex({\n  node1: 10,\n  node2: 20,\n  node3: 30,\n});\n```\n\n### Graph.setNode(node)\n\n设置节点样式映射。即 `options.node` 的值。\n\n```typescript\nsetNode(node: NodeOptions): void;\n```\n\n**参数**:\n\n| 参数 | 描述     | 类型                                         | 默认值 | 必选 |\n| ---- | -------- | -------------------------------------------- | ------ | ---- |\n| node | 节点配置 | [NodeOptions](/manual/element/node/overview) | -      | ✓    |\n\n**示例**:\n\n```typescript\n// 设置所有的节点填充色为红色\ngraph.setNode({\n  style: {\n    fill: 'red',\n  },\n});\n```\n\n### Graph.setEdge(edge)\n\n设置边样式映射。即 `options.edge` 的值。\n\n```typescript\nsetEdge(edge: EdgeOptions): void;\n```\n\n**参数**:\n\n| 参数 | 描述   | 类型                                         | 默认值 | 必选 |\n| ---- | ------ | -------------------------------------------- | ------ | ---- |\n| edge | 边配置 | [EdgeOptions](/manual/element/edge/overview) | -      | ✓    |\n\n### Graph.setCombo(combo)\n\n设置组合样式映射。即 `options.combo` 的值。\n\n```typescript\nsetCombo(combo: ComboOptions): void;\n```\n\n**参数**:\n\n| 参数  | 描述     | 类型                                           | 默认值 | 必选 |\n| ----- | -------- | ---------------------------------------------- | ------ | ---- |\n| combo | 组合配置 | [ComboOptions](/manual/element/combo/overview) | -      | ✓    |\n\n### Graph.collapseElement(id, options)\n\n收起指定元素，通常用于折叠组合(Combo)或具有子元素的节点。\n\n```typescript\ncollapseElement(id: ID, options?: boolean | CollapseExpandNodeOptions): Promise<void>;\n```\n\n**参数**:\n\n| 参数    | 描述                               | 类型                                                               | 默认值 | 必选 |\n| ------- | ---------------------------------- | ------------------------------------------------------------------ | ------ | ---- |\n| id      | 要收起的元素 ID                    | string                                                             | -      | ✓    |\n| options | 是否启用动画或收起节点的详细配置项 | boolean \\| [CollapseExpandNodeOptions](#collapseexpandnodeoptions) | -      |      |\n\n**返回值**:\n\n- **类型**: Promise\\<void\\>\n- **描述**: 返回一个 Promise，收起操作完成后 resolve\n\n**示例**:\n\n```typescript\n// 简单收起，使用默认配置\nawait graph.collapseElement('combo1');\n\n// 收起并启用动画\nawait graph.collapseElement('combo1', true);\n\n// 收起并保证展开/收起的节点位置不变\nawait graph.collapseElement('combo1', {\n  align: true,\n});\n```\n\n### Graph.expandElement(id, options)\n\n展开指定元素，通常用于展开先前收起的组合(Combo)或节点。\n\n```typescript\nexpandElement(id: ID, options?: boolean | CollapseExpandNodeOptions): Promise<void>;\n```\n\n**参数**:\n\n| 参数    | 描述                               | 类型                                                               | 默认值 | 必选 |\n| ------- | ---------------------------------- | ------------------------------------------------------------------ | ------ | ---- |\n| id      | 要展开的元素 ID                    | string                                                             | -      | ✓    |\n| options | 是否启用动画或展开节点的详细配置项 | boolean \\| [CollapseExpandNodeOptions](#collapseexpandnodeoptions) | -      |      |\n\n**返回值**:\n\n- **类型**: Promise\\<void\\>\n- **描述**: 返回一个 Promise，展开操作完成后 resolve\n\n**示例**:\n\n```typescript\n// 简单展开，使用默认配置\nawait graph.expandElement('combo1');\n\n// 展开并启用动画\nawait graph.expandElement('combo1', true);\n\n// 展开并保证展开/收起的节点位置不变\nawait graph.expandElement('combo1', {\n  align: true,\n});\n```\n\n### Graph.frontElement(id)\n\n将指定元素置于最顶层，使其显示在其他重叠元素之上。\n\n```typescript\nfrontElement(id: ID | ID[]): void;\n```\n\n**参数**:\n\n| 参数 | 描述    | 类型               | 默认值 | 必选 |\n| ---- | ------- | ------------------ | ------ | ---- |\n| id   | 元素 ID | string \\| string[] | -      | ✓    |\n\n**返回值**:\n\n- **类型**: void\n\n**示例**:\n\n```typescript\n// 将节点置于最顶层\ngraph.frontElement('node1');\n\n// 对于选中的多个节点，都置于最顶层\ngraph.frontElement(['node1', 'node2', 'node3']);\n```\n\n### Graph.showElement(id, animation)\n\n显示指定元素。\n\n```typescript\nshowElement(id: ID | ID[], animation?: boolean): Promise<void>;\n```\n\n**参数**:\n\n| 参数      | 描述         | 类型               | 默认值 | 必选 |\n| --------- | ------------ | ------------------ | ------ | ---- |\n| id        | 元素 ID      | string \\| string[] | -      | ✓    |\n| animation | 是否启用动画 | boolean            | -      |      |\n\n**返回值**:\n\n- **类型**: Promise\\<void\\>\n- **描述**: 返回一个 Promise，显示操作完成后 resolve\n\n**示例**:\n\n```typescript\n// 显示单个元素\nawait graph.showElement('node1');\n\n// 带动画显示元素\nawait graph.showElement('node1', true);\n\n// 显示多个元素\nawait graph.showElement(['node1', 'node2', 'node3']);\n```\n\n### Graph.hideElement(id, animation)\n\n隐藏指定元素。\n\n```typescript\nhideElement(id: ID | ID[], animation?: boolean): Promise<void>;\n```\n\n**参数**:\n\n| 参数      | 描述         | 类型               | 默认值 | 必选 |\n| --------- | ------------ | ------------------ | ------ | ---- |\n| id        | 元素 ID      | string \\| string[] | -      | ✓    |\n| animation | 是否启用动画 | boolean            | -      |      |\n\n**返回值**:\n\n- **类型**: Promise\\<void\\>\n- **描述**: 返回一个 Promise，隐藏操作完成后 resolve\n\n**示例**:\n\n```typescript\n// 无动画隐藏元素\nawait graph.hideElement('node1');\n\n// 带动画隐藏元素\nawait graph.hideElement('node1', true);\n\n// 隐藏多个元素\nawait graph.hideElement(['node1', 'node2', 'node3'], true);\n```\n\n### Graph.translateElementBy(id, offset, animation)\n\n相对平移元素指定距离，支持两种调用方式：\n\n```typescript\n// 将元素平移指定距离（相对平移）\ntranslateElement(id: ID, offset: Point, animation?: boolean): Promise<void>;\n\n// 批量将元素平移指定距离（相对平移）\ntranslateElement(offsets: Record<ID, Point>, animation?: boolean): Promise<void>;\n```\n\n**参数**:\n\n**单个元素平移**\n\n| 参数      | 描述                    | 类型             | 默认值 | 必选 |\n| --------- | ----------------------- | ---------------- | ------ | ---- |\n| id        | 要平移的元素 ID         | string           | -      | ✓    |\n| offset    | 平移的相对距离 [dx, dy] | [number, number] | -      | ✓    |\n| animation | 是否启用动画            | boolean          | -      |      |\n\n**批量元素平移**\n\n| 参数      | 描述                   | 类型                         | 默认值 | 必选 |\n| --------- | ---------------------- | ---------------------------- | ------ | ---- |\n| offsets   | 元素ID到平移距离的映射 | Record<ID, [number, number]> | -      | ✓    |\n| animation | 是否启用动画           | boolean                      | -      |      |\n\n**返回值**:\n\n- **类型**: Promise\\<void\\>\n- **描述**: 返回一个 Promise，平移操作完成后 resolve\n\n**示例**:\n\n```typescript\n// 向右平移100像素，向下平移50像素\nawait graph.translateElementBy('node1', [100, 50]);\n\n// 带动画平移\nawait graph.translateElementBy('node1', [100, 50], true);\n\n// 对多个节点应用相同的平移\nawait graph.translateElementBy(\n  {\n    node1: [50, 50],\n    node2: [100, 100],\n    node3: [150, 150],\n  },\n  true,\n);\n```\n\n### Graph.translateElementTo(id, position, animation)\n\n将元素移动到指定位置，支持两种调用方式：\n\n```typescript\n// 将元素移动到指定位置（绝对位置）\ntranslateElementTo(id: ID, position: Point, animation?: boolean): Promise<void>;\n\n// 批量将元素移动到指定位置（绝对位置）\ntranslateElementTo(positions: Record<ID, Point>, animation?: boolean): Promise<void>;\n```\n\n**参数**:\n\n**单个元素移动**\n\n| 参数      | 描述                | 类型             | 默认值 | 必选 |\n| --------- | ------------------- | ---------------- | ------ | ---- |\n| id        | 要移动的元素 ID     | string           | -      | ✓    |\n| position  | 目标绝对位置 [x, y] | [number, number] | -      | ✓    |\n| animation | 是否启用动画        | boolean          | -      |      |\n\n**批量元素移动**\n\n| 参数      | 描述                   | 类型                             | 默认值 | 必选 |\n| --------- | ---------------------- | -------------------------------- | ------ | ---- |\n| positions | 元素ID到目标位置的映射 | Record<string, [number, number]> | -      | ✓    |\n| animation | 是否启用动画           | boolean                          | -      |      |\n\n**返回值**:\n\n- **类型**: Promise\\<void\\>\n- **描述**: 返回一个 Promise，移动操作完成后 resolve\n\n**示例**:\n\n```typescript\n// 将节点移动到画布的 (200, 300) 位置\nawait graph.translateElementTo('node1', [200, 300]);\n\n// 带动画移动\nawait graph.translateElementTo('node1', [200, 300], true);\n\n// 将一组节点整齐排列\nawait graph.translateElementTo(\n  {\n    node1: [100, 100],\n    node2: [200, 200],\n    node3: [300, 100],\n  },\n  true,\n);\n```\n\n### Graph.focusElement(id, animation)\n\n聚焦到指定元素，使元素居中于视口。\n\n```typescript\nfocusElement(id: ID | ID[], animation?: ViewportAnimationEffectTiming): Promise<void>;\n```\n\n**参数**:\n\n| 参数      | 描述                      | 类型                                                            | 默认值 | 必选 |\n| --------- | ------------------------- | --------------------------------------------------------------- | ------ | ---- |\n| id        | 要聚焦的一个或多个元素 ID | string \\| string[]                                              | -      | ✓    |\n| animation | 视口动画配置              | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -      |      |\n\n**返回值**:\n\n- **类型**: Promise\\<void\\>\n- **描述**: 返回一个 Promise，聚焦操作完成后 resolve\n\n**示例**:\n\n```typescript\n// 聚焦到单个节点\nawait graph.focusElement('node1');\n\n// 使用自定义动画配置\nawait graph.focusElement('node1', {\n  duration: 800,\n  easing: 'ease-in-out',\n});\n\n// 聚焦到多个节点\nawait graph.focusElement(['node1', 'node2', 'node3']);\n```\n\n## 类型定义\n\n### CollapseExpandNodeOptions\n\n收起或展开元素的配置选项。\n\n```typescript\ninterface CollapseExpandNodeOptions {\n  /**\n   * 是否使用动画\n   */\n  animation?: boolean;\n  /**\n   * 保证展开/收起的节点位置不变\n   */\n  align?: boolean;\n}\n```\n\n### ViewportAnimationEffectTiming\n\n视口动画配置类型。\n\n```typescript\ntype ViewportAnimationEffectTiming =\n  | boolean // 是否启用动画\n  | {\n      easing?: string; // 缓动函数\n      duration?: number; // 动画持续时间(ms)\n    };\n```\n\n### AABB\n\nAABB (Axis-Aligned Bounding Box) 是\"轴对齐包围盒\"的缩写，是计算机图形学中的一个基础概念。\n\n```typescript\ninterface AABB {\n  x: number; // 矩形左上角的 x 坐标\n  y: number; // 矩形左上角的 y 坐标\n  width: number; // 矩形宽度\n  height: number; // 矩形高度\n}\n```\n\n### State\n\n元素状态类型。\n\n```typescript\ntype State = 'selected' | 'hover' | 'active' | 'inactive' | 'disabled' | string;\n```\n"
  },
  {
    "path": "packages/site/docs/api/event.en.md",
    "content": "---\ntitle: Event Listening\norder: 11\n---\n\n## Overview of the Event System\n\nG6 provides a powerful event mechanism that allows you to respond to various interactive behaviors occurring in the chart, such as node clicks, edge hovers, canvas drags, etc. Through the event system, you can implement complex interactive logic to enhance user experience.\n\n### Event Categories\n\nEvents in G6 can be broadly categorized into the following types:\n\n1. **Element Events**: Events related to nodes, edges, and Combos, such as `node:click`, `edge:mouseenter`\n2. **Canvas Events**: Events related to the entire canvas, such as `canvas:drag`, `canvas:wheel`\n3. **Lifecycle Events**: Events related to the chart lifecycle, such as `beforerender`, `afterrender`\n\n### Event Naming Convention\n\nG6 events follow the `[object]:[event]` format, for example:\n\n- `node:click` - Node click event\n- `edge:mouseenter` - Mouse enters edge event\n- `canvas:drag` - Canvas drag event\n\n## Best Practice: Using Constant Enums\n\nG6 provides a complete set of event constant enums, and it is **strongly recommended** to use these constants instead of directly using string event names:\n\n```typescript\nimport { NodeEvent, EdgeEvent, CanvasEvent, GraphEvent } from '@antv/g6';\n\n// Use constant enums to listen to events\ngraph.on(NodeEvent.CLICK, handleNodeClick);\ngraph.on(EdgeEvent.POINTER_OVER, handleEdgeHover);\ngraph.on(CanvasEvent.DRAG, handleCanvasDrag);\ngraph.on(GraphEvent.AFTER_RENDER, handleAfterRender);\n```\n\n**Advantages**:\n\n- Type safety, avoiding string spelling errors\n- Provides intelligent code hints and auto-completion\n\n## API Reference\n\n### Graph.on(eventName, callback, once)\n\nListen to a specified event and execute a callback function when the event is triggered.\n\n```typescript\non<T extends IEvent = IEvent>(eventName: string, callback: (event: T) => void, once?: boolean): this;\n```\n\n#### Parameters\n\n| Parameter | Description                                            | Type               | Default | Required |\n| --------- | ------------------------------------------------------ | ------------------ | ------- | -------- |\n| eventName | Name of the event to listen to                         | string             | -       | ✓        |\n| callback  | Callback function executed when the event is triggered | (event: T) => void | -       | ✓        |\n| once      | Whether to listen only once                            | boolean            | -       |          |\n\n#### Return Value\n\n- **Type:** this (Graph instance)\n- **Description:** Returns the graph instance itself, supporting chain calls\n\n#### Example\n\n```typescript\nimport { NodeEvent, EdgeEvent, CanvasEvent } from '@antv/g6';\n\n// Listen to node click event\ngraph.on(NodeEvent.CLICK, (evt) => {\n  const { target } = evt; // Get the ID of the clicked node\n  console.log(`Node ${target.id} was clicked`);\n\n  // Get node data\n  const nodeData = graph.getNodeData(target.id);\n  console.log('Node data:', nodeData);\n\n  // Modify node state\n  graph.setElementState(target.id, 'selected');\n});\n\n// Listen to edge mouse enter event\ngraph.on(EdgeEvent.POINTER_OVER, (evt) => {\n  const { target } = evt;\n  graph.setElementState(target.id, 'highlight');\n});\n\n// Listen to canvas drag event\ngraph.on(CanvasEvent.DRAG, (evt) => {\n  console.log('Canvas is being dragged');\n});\n```\n\n### Graph.once(eventName, callback)\n\nListen to an event once, and automatically remove the listener after the event is triggered once.\n\n```typescript\nonce<T extends IEvent = IEvent>(eventName: string, callback: (event: T) => void): this;\n```\n\n#### Parameters\n\n| Parameter | Description                                            | Type               | Default | Required |\n| --------- | ------------------------------------------------------ | ------------------ | ------- | -------- |\n| eventName | Name of the event to listen to                         | string             | -       | ✓        |\n| callback  | Callback function executed when the event is triggered | (event: T) => void | -       | ✓        |\n\n#### Return Value\n\n- **Type:** this (Graph instance)\n- **Description:** Returns the graph instance itself, supporting chain calls\n\n#### Example\n\n```typescript\nimport { GraphEvent, NodeEvent } from '@antv/g6';\n\n// Listen to the chart's first load completion event, executed only once\ngraph.once(GraphEvent.AFTER_RENDER, () => {\n  console.log('Chart rendered for the first time');\n  // Execute one-time initialization operations\n  highlightImportantNodes();\n});\n\n// Wait for the user to click a node for the first time and then perform operations\ngraph.once(NodeEvent.CLICK, (evt) => {\n  console.log('User clicked a node for the first time:', evt.target.id);\n  showTutorialTip('You can drag nodes to change their position');\n});\n```\n\n### Graph.off()\n\nRemove all event listeners.\n\n```typescript\noff(): this;\n```\n\n#### Return Value\n\n- **Type:** this (Graph instance)\n- **Description:** Returns the graph instance itself, supporting chain calls\n\n#### Example\n\n```typescript\n// Remove all event listeners\ngraph.off();\nconsole.log('All event listeners have been removed');\n```\n\n### Graph.off(eventName)\n\nRemove all listeners of a specified event type.\n\n```typescript\noff(eventName: string): this;\n```\n\n#### Parameters\n\n| Parameter | Description                 | Type   | Default | Required |\n| --------- | --------------------------- | ------ | ------- | -------- |\n| eventName | Name of the event to remove | string | -       | ✓        |\n\n#### Return Value\n\n- **Type:** this (Graph instance)\n- **Description:** Returns the graph instance itself, supporting chain calls\n\n#### Example\n\n```typescript\nimport { NodeEvent } from '@antv/g6';\n\n// Remove all node click event listeners\ngraph.off(NodeEvent.CLICK);\nconsole.log('All node click event listeners have been removed');\n\n// Remove related temporary event listeners after a certain operation mode ends\nfunction exitEditMode() {\n  // Remove all listeners in edit mode\n  graph.off(NodeEvent.DRAG_END);\n  graph.off(NodeEvent.DROP);\n  console.log('Exited edit mode');\n}\n```\n\n### Graph.off(eventName, callback)\n\nRemove a specific callback function for a specific event.\n\n```typescript\noff(eventName: string, callback: (...args: any[]) => void): this;\n```\n\n#### Parameters\n\n| Parameter | Description                 | Type                     | Default | Required |\n| --------- | --------------------------- | ------------------------ | ------- | -------- |\n| eventName | Name of the event to remove | string                   | -       | ✓        |\n| callback  | Callback function to remove | (...args: any[]) => void | -       | ✓        |\n\n#### Return Value\n\n- **Type:** this (Graph instance)\n- **Description:** Returns the graph instance itself, supporting chain calls\n\n#### Example\n\n```typescript\nimport { NodeEvent } from '@antv/g6';\n\n// Define callback function\nconst handleNodeClick = (evt) => {\n  console.log('Node clicked:', evt.target.id);\n};\n\n// Add listener\ngraph.on(NodeEvent.CLICK, handleNodeClick);\n\n// Later, remove this specific listener at a certain point\ngraph.off(NodeEvent.CLICK, handleNodeClick);\nconsole.log('Specific node click event listener has been removed');\n```\n\n## Event Constant Enums\n\nG6 provides various event constant enums to facilitate developers in using standardized event names. Below is a detailed description of all event constants:\n\n### Node Events (NodeEvent)\n\n| Constant Name | Event Name          | Description                                                                   |\n| ------------- | ------------------- | ----------------------------------------------------------------------------- |\n| CLICK         | `node:click`        | Triggered when a node is clicked                                              |\n| DBLCLICK      | `node:dblclick`     | Triggered when a node is double-clicked                                       |\n| POINTER_OVER  | `node:pointerover`  | Triggered when the pointer enters a node                                      |\n| POINTER_LEAVE | `node:pointerleave` | Triggered when the pointer leaves a node                                      |\n| POINTER_ENTER | `node:pointerenter` | Triggered when the pointer enters a node or its child elements (non-bubbling) |\n| POINTER_MOVE  | `node:pointermove`  | Triggered when the pointer moves over a node                                  |\n| POINTER_OUT   | `node:pointerout`   | Triggered when the pointer leaves a node                                      |\n| POINTER_DOWN  | `node:pointerdown`  | Triggered when the pointer is pressed down on a node                          |\n| POINTER_UP    | `node:pointerup`    | Triggered when the pointer is released on a node                              |\n| CONTEXT_MENU  | `node:contextmenu`  | Triggered when the context menu is opened on a node                           |\n| DRAG_START    | `node:dragstart`    | Triggered when dragging a node starts                                         |\n| DRAG          | `node:drag`         | Triggered during node dragging                                                |\n| DRAG_END      | `node:dragend`      | Triggered when node dragging ends                                             |\n| DRAG_ENTER    | `node:dragenter`    | Triggered when a draggable item enters a node                                 |\n| DRAG_OVER     | `node:dragover`     | Triggered when a draggable item is over a node                                |\n| DRAG_LEAVE    | `node:dragleave`    | Triggered when a draggable item leaves a node                                 |\n| DROP          | `node:drop`         | Triggered when a draggable item is dropped on a node                          |\n\n### Edge Events (EdgeEvent)\n\n| Constant Name | Event Name          | Description                                                                    |\n| ------------- | ------------------- | ------------------------------------------------------------------------------ |\n| CLICK         | `edge:click`        | Triggered when an edge is clicked                                              |\n| DBLCLICK      | `edge:dblclick`     | Triggered when an edge is double-clicked                                       |\n| POINTER_OVER  | `edge:pointerover`  | Triggered when the pointer enters an edge                                      |\n| POINTER_LEAVE | `edge:pointerleave` | Triggered when the pointer leaves an edge                                      |\n| POINTER_ENTER | `edge:pointerenter` | Triggered when the pointer enters an edge or its child elements (non-bubbling) |\n| POINTER_MOVE  | `edge:pointermove`  | Triggered when the pointer moves over an edge                                  |\n| POINTER_OUT   | `edge:pointerout`   | Triggered when the pointer leaves an edge                                      |\n| POINTER_DOWN  | `edge:pointerdown`  | Triggered when the pointer is pressed down on an edge                          |\n| POINTER_UP    | `edge:pointerup`    | Triggered when the pointer is released on an edge                              |\n| CONTEXT_MENU  | `edge:contextmenu`  | Triggered when the context menu is opened on an edge                           |\n| DRAG_ENTER    | `edge:dragenter`    | Triggered when a draggable item enters an edge                                 |\n| DRAG_OVER     | `edge:dragover`     | Triggered when a draggable item is over an edge                                |\n| DRAG_LEAVE    | `edge:dragleave`    | Triggered when a draggable item leaves an edge                                 |\n| DROP          | `edge:drop`         | Triggered when a draggable item is dropped on an edge                          |\n\n### Combo Events (ComboEvent)\n\n| Constant Name | Event Name           | Description                                                                    |\n| ------------- | -------------------- | ------------------------------------------------------------------------------ |\n| CLICK         | `combo:click`        | Triggered when a Combo is clicked                                              |\n| DBLCLICK      | `combo:dblclick`     | Triggered when a Combo is double-clicked                                       |\n| POINTER_OVER  | `combo:pointerover`  | Triggered when the pointer enters a Combo                                      |\n| POINTER_LEAVE | `combo:pointerleave` | Triggered when the pointer leaves a Combo                                      |\n| POINTER_ENTER | `combo:pointerenter` | Triggered when the pointer enters a Combo or its child elements (non-bubbling) |\n| POINTER_MOVE  | `combo:pointermove`  | Triggered when the pointer moves over a Combo                                  |\n| POINTER_OUT   | `combo:pointerout`   | Triggered when the pointer leaves a Combo                                      |\n| POINTER_DOWN  | `combo:pointerdown`  | Triggered when the pointer is pressed down on a Combo                          |\n| POINTER_UP    | `combo:pointerup`    | Triggered when the pointer is released on a Combo                              |\n| CONTEXT_MENU  | `combo:contextmenu`  | Triggered when the context menu is opened on a Combo                           |\n| DRAG_START    | `combo:dragstart`    | Triggered when dragging a Combo starts                                         |\n| DRAG          | `combo:drag`         | Triggered during Combo dragging                                                |\n| DRAG_END      | `combo:dragend`      | Triggered when Combo dragging ends                                             |\n| DRAG_ENTER    | `combo:dragenter`    | Triggered when a draggable item enters a Combo                                 |\n| DRAG_OVER     | `combo:dragover`     | Triggered when a draggable item is over a Combo                                |\n| DRAG_LEAVE    | `combo:dragleave`    | Triggered when a draggable item leaves a Combo                                 |\n| DROP          | `combo:drop`         | Triggered when a draggable item is dropped on a Combo                          |\n\n### Canvas Events (CanvasEvent)\n\n| Constant Name | Event Name            | Description                                                                       |\n| ------------- | --------------------- | --------------------------------------------------------------------------------- |\n| CLICK         | `canvas:click`        | Triggered when clicking on the canvas blank area                                  |\n| DBLCLICK      | `canvas:dblclick`     | Triggered when double-clicking on the canvas blank area                           |\n| POINTER_OVER  | `canvas:pointerover`  | Triggered when the pointer enters the canvas                                      |\n| POINTER_LEAVE | `canvas:pointerleave` | Triggered when the pointer leaves the canvas                                      |\n| POINTER_ENTER | `canvas:pointerenter` | Triggered when the pointer enters the canvas or its child elements (non-bubbling) |\n| POINTER_MOVE  | `canvas:pointermove`  | Triggered when the pointer moves over the canvas                                  |\n| POINTER_OUT   | `canvas:pointerout`   | Triggered when the pointer leaves the canvas                                      |\n| POINTER_DOWN  | `canvas:pointerdown`  | Triggered when the pointer is pressed down on the canvas                          |\n| POINTER_UP    | `canvas:pointerup`    | Triggered when the pointer is released on the canvas                              |\n| CONTEXT_MENU  | `canvas:contextmenu`  | Triggered when the context menu is opened on the canvas                           |\n| DRAG_START    | `canvas:dragstart`    | Triggered when dragging the canvas starts                                         |\n| DRAG          | `canvas:drag`         | Triggered during canvas dragging                                                  |\n| DRAG_END      | `canvas:dragend`      | Triggered when canvas dragging ends                                               |\n| DRAG_ENTER    | `canvas:dragenter`    | Triggered when a draggable item enters the canvas                                 |\n| DRAG_OVER     | `canvas:dragover`     | Triggered when a draggable item is over the canvas                                |\n| DRAG_LEAVE    | `canvas:dragleave`    | Triggered when a draggable item leaves the canvas                                 |\n| DROP          | `canvas:drop`         | Triggered when a draggable item is dropped on the canvas                          |\n| WHEEL         | `canvas:wheel`        | Triggered when scrolling the mouse wheel on the canvas                            |\n\n### Graph Lifecycle Events (GraphEvent)\n\n| Constant Name            | Event Name               | Description                                    |\n| ------------------------ | ------------------------ | ---------------------------------------------- |\n| BEFORE_CANVAS_INIT       | `beforecanvasinit`       | Triggered before canvas initialization         |\n| AFTER_CANVAS_INIT        | `aftercanvasinit`        | Triggered after canvas initialization          |\n| BEFORE_SIZE_CHANGE       | `beforesizechange`       | Triggered before viewport size change          |\n| AFTER_SIZE_CHANGE        | `aftersizechange`        | Triggered after viewport size change           |\n| BEFORE_ELEMENT_CREATE    | `beforeelementcreate`    | Triggered before element creation              |\n| AFTER_ELEMENT_CREATE     | `afterelementcreate`     | Triggered after element creation               |\n| BEFORE_ELEMENT_UPDATE    | `beforeelementupdate`    | Triggered before element update                |\n| AFTER_ELEMENT_UPDATE     | `afterelementupdate`     | Triggered after element update                 |\n| BEFORE_ELEMENT_DESTROY   | `beforeelementdestroy`   | Triggered before element destruction           |\n| AFTER_ELEMENT_DESTROY    | `afterelementdestroy`    | Triggered after element destruction            |\n| BEFORE_ELEMENT_TRANSLATE | `beforeelementtranslate` | Triggered before element translation           |\n| AFTER_ELEMENT_TRANSLATE  | `afterelementtranslate`  | Triggered after element translation            |\n| BEFORE_DRAW              | `beforedraw`             | Triggered before drawing starts                |\n| AFTER_DRAW               | `afterdraw`              | Triggered after drawing ends                   |\n| BEFORE_RENDER            | `beforerender`           | Triggered before rendering starts              |\n| AFTER_RENDER             | `afterrender`            | Triggered after rendering completes            |\n| BEFORE_ANIMATE           | `beforeanimate`          | Triggered before animation starts              |\n| AFTER_ANIMATE            | `afteranimate`           | Triggered after animation ends                 |\n| BEFORE_LAYOUT            | `beforelayout`           | Triggered before layout starts                 |\n| AFTER_LAYOUT             | `afterlayout`            | Triggered after layout ends                    |\n| BEFORE_STAGE_LAYOUT      | `beforestagelayout`      | Triggered before each stage in pipeline layout |\n| AFTER_STAGE_LAYOUT       | `afterstagelayout`       | Triggered after each stage in pipeline layout  |\n| BEFORE_TRANSFORM         | `beforetransform`        | Triggered before viewport transformation       |\n| AFTER_TRANSFORM          | `aftertransform`         | Triggered after viewport transformation        |\n| BATCH_START              | `batchstart`             | Triggered when batch operation starts          |\n| BATCH_END                | `batchend`               | Triggered when batch operation ends            |\n| BEFORE_DESTROY           | `beforedestroy`          | Triggered before chart destruction             |\n| AFTER_DESTROY            | `afterdestroy`           | Triggered after chart destruction              |\n| BEFORE_RENDERER_CHANGE   | `beforerendererchange`   | Triggered before renderer change               |\n| AFTER_RENDERER_CHANGE    | `afterrendererchange`    | Triggered after renderer change                |\n\n### Container Events (ContainerEvent)\n\n| Constant Name | Event Name | Description                                   |\n| ------------- | ---------- | --------------------------------------------- |\n| KEY_DOWN      | `keydown`  | Triggered when a keyboard key is pressed down |\n| KEY_UP        | `keyup`    | Triggered when a keyboard key is released     |\n\n### Common Events (CommonEvent)\n\nThese are events without prefixes and can be used to listen to global events:\n\n| Constant Name | Event Name     | Description                                                                        |\n| ------------- | -------------- | ---------------------------------------------------------------------------------- |\n| CLICK         | `click`        | Triggered when any element is clicked                                              |\n| DBLCLICK      | `dblclick`     | Triggered when any element is double-clicked                                       |\n| POINTER_OVER  | `pointerover`  | Triggered when the pointer enters any element                                      |\n| POINTER_LEAVE | `pointerleave` | Triggered when the pointer leaves any element                                      |\n| POINTER_ENTER | `pointerenter` | Triggered when the pointer enters any element or its child elements (non-bubbling) |\n| POINTER_MOVE  | `pointermove`  | Triggered when the pointer moves over any element                                  |\n| POINTER_OUT   | `pointerout`   | Triggered when the pointer leaves any element                                      |\n| POINTER_DOWN  | `pointerdown`  | Triggered when the pointer is pressed down on any element                          |\n| POINTER_UP    | `pointerup`    | Triggered when the pointer is released on any element                              |\n| CONTEXT_MENU  | `contextmenu`  | Triggered when the context menu is opened on any element                           |\n| DRAG_START    | `dragstart`    | Triggered when dragging any element starts                                         |\n| DRAG          | `drag`         | Triggered during any element dragging                                              |\n| DRAG_END      | `dragend`      | Triggered when any element dragging ends                                           |\n| DRAG_ENTER    | `dragenter`    | Triggered when a draggable item enters any element                                 |\n| DRAG_OVER     | `dragover`     | Triggered when a draggable item is over any element                                |\n| DRAG_LEAVE    | `dragleave`    | Triggered when a draggable item leaves any element                                 |\n| DROP          | `drop`         | Triggered when a draggable item is dropped on any element                          |\n| KEY_DOWN      | `keydown`      | Triggered when a keyboard key is pressed down                                      |\n| KEY_UP        | `keyup`        | Triggered when a keyboard key is released                                          |\n| WHEEL         | `wheel`        | Triggered when scrolling the mouse wheel                                           |\n| PINCH         | `pinch`        | Triggered when pinching or spreading fingers on a multi-touch screen               |\n\n## Tips for Use\n\n### Chain Calls\n\nG6's event API supports chain calls, allowing you to register multiple events consecutively:\n\n```typescript\nimport { NodeEvent, EdgeEvent, CanvasEvent } from '@antv/g6';\n\n// Use constant enums + chain calls\ngraph.on(NodeEvent.CLICK, handleNodeClick).on(EdgeEvent.CLICK, handleEdgeClick).on(CanvasEvent.WHEEL, handleCanvasZoom);\n```\n\n### Event Delegation\n\nYou can use the event bubbling mechanism to listen to all child element events on the parent element:\n\n```typescript\nimport { CommonEvent } from '@antv/g6';\n\n// Handle all element click events uniformly\ngraph.on(CommonEvent.CLICK, (evt) => {\n  const { targetType, target } = evt;\n  if (targetType === 'node') {\n    console.log('Clicked on node:', target.id);\n  } else if (targetType === 'edge') {\n    console.log('Clicked on edge:', target.id);\n  } else {\n    console.log('Clicked on canvas blank area');\n  }\n});\n```\n\n### Event Object Properties\n\nMost event callback functions receive an event object containing the following common properties:\n\n- `target` - The element that triggered the event\n- `targetType` - The type of the element that triggered the event (node/edge/combo/canvas)\n- `originalTarget` - The original graphic that triggered the event\n- `currentTarget` - The current object that triggered the event\n- `originalEvent` - The original browser event object\n\nWith these properties, you can precisely control interactive behavior.\n"
  },
  {
    "path": "packages/site/docs/api/event.zh.md",
    "content": "---\ntitle: 事件监听\norder: 11\n---\n\n## 事件系统概述\n\nG6 提供了强大的事件机制，允许你响应图表中发生的各种交互行为。例如节点点击、边悬停、画布拖拽等。通过事件系统，你可以实现复杂的交互逻辑，提升用户体验。\n\n### 事件分类\n\nG6 中的事件大致可分为以下几类：\n\n1. **元素事件**：与节点、边、Combo 相关的事件，如 `node:click`, `edge:mouseenter`\n2. **画布事件**：与整个画布相关的事件，如 `canvas:drag`, `canvas:wheel`\n3. **生命周期事件**：与图表生命周期相关的事件，如 `beforerender`, `afterrender`\n\n### 事件命名规则\n\nG6 的事件命名遵循 `[对象]:[事件]` 的格式，例如：\n\n- `node:click` - 节点点击事件\n- `edge:mouseenter` - 鼠标进入边的事件\n- `canvas:drag` - 画布拖拽事件\n\n## 最佳实践：使用常量枚举\n\nG6 提供了完整的事件常量枚举，**强烈建议**使用这些常量而非直接使用字符串事件名：\n\n```typescript\nimport { NodeEvent, EdgeEvent, CanvasEvent, GraphEvent } from '@antv/g6';\n\n// 使用常量枚举监听事件\ngraph.on(NodeEvent.CLICK, handleNodeClick);\ngraph.on(EdgeEvent.POINTER_OVER, handleEdgeHover);\ngraph.on(CanvasEvent.DRAG, handleCanvasDrag);\ngraph.on(GraphEvent.AFTER_RENDER, handleAfterRender);\n```\n\n**优势**：\n\n- 类型安全，避免字符串拼写错误\n- 提供智能代码提示和自动完成\n\n## API 参考\n\n### Graph.on(eventName, callback, once)\n\n监听指定的事件，当事件触发时执行回调函数。\n\n```typescript\non<T extends IEvent = IEvent>(eventName: string, callback: (event: T) => void, once?: boolean): this;\n```\n\n#### 参数\n\n| 参数      | 描述                     | 类型               | 默认值 | 必选 |\n| --------- | ------------------------ | ------------------ | ------ | ---- |\n| eventName | 要监听的事件名称         | string             | -      | ✓    |\n| callback  | 事件触发时执行的回调函数 | (event: T) => void | -      | ✓    |\n| once      | 是否只监听一次           | boolean            | -      |      |\n\n#### 返回值\n\n- **类型：** this（Graph 实例）\n- **描述：** 返回图实例本身，支持链式调用\n\n#### 示例\n\n```typescript\nimport { NodeEvent, EdgeEvent, CanvasEvent } from '@antv/g6';\n\n// 监听节点点击事件\ngraph.on(NodeEvent.CLICK, (evt) => {\n  const { target } = evt; // 获取被点击节点的 ID\n  console.log(`节点 ${target.id} 被点击了`);\n\n  // 获取节点数据\n  const nodeData = graph.getNodeData(target.id);\n  console.log('节点数据:', nodeData);\n\n  // 修改节点状态\n  graph.setElementState(target.id, 'selected');\n});\n\n// 监听边的鼠标进入事件\ngraph.on(EdgeEvent.POINTER_OVER, (evt) => {\n  const { target } = evt;\n  graph.setElementState(target.id, 'highlight');\n});\n\n// 监听画布拖拽事件\ngraph.on(CanvasEvent.DRAG, (evt) => {\n  console.log('画布正在被拖拽');\n});\n```\n\n### Graph.once(eventName, callback)\n\n一次性监听事件，事件触发一次后自动移除监听器。\n\n```typescript\nonce<T extends IEvent = IEvent>(eventName: string, callback: (event: T) => void): this;\n```\n\n#### 参数\n\n| 参数      | 描述                     | 类型               | 默认值 | 必选 |\n| --------- | ------------------------ | ------------------ | ------ | ---- |\n| eventName | 要监听的事件名称         | string             | -      | ✓    |\n| callback  | 事件触发时执行的回调函数 | (event: T) => void | -      | ✓    |\n\n#### 返回值\n\n- **类型：** this（Graph 实例）\n- **描述：** 返回图实例本身，支持链式调用\n\n#### 示例\n\n```typescript\nimport { GraphEvent, NodeEvent } from '@antv/g6';\n\n// 监听图表首次加载完成事件，仅执行一次\ngraph.once(GraphEvent.AFTER_RENDER, () => {\n  console.log('图表首次渲染完成');\n  // 执行一次性的初始化操作\n  highlightImportantNodes();\n});\n\n// 等待用户第一次点击某个节点后执行操作\ngraph.once(NodeEvent.CLICK, (evt) => {\n  console.log('用户首次点击了节点:', evt.target.id);\n  showTutorialTip('您可以拖拽节点改变位置');\n});\n```\n\n### Graph.off()\n\n移除全部事件监听器。\n\n```typescript\noff(): this;\n```\n\n#### 返回值\n\n- **类型：** this（Graph 实例）\n- **描述：** 返回图实例本身，支持链式调用\n\n#### 示例\n\n```typescript\n// 移除所有事件监听器\ngraph.off();\nconsole.log('已移除所有事件监听器');\n```\n\n### Graph.off(eventName)\n\n移除指定事件类型的所有监听器。\n\n```typescript\noff(eventName: string): this;\n```\n\n#### 参数\n\n| 参数      | 描述             | 类型   | 默认值 | 必选 |\n| --------- | ---------------- | ------ | ------ | ---- |\n| eventName | 要移除的事件名称 | string | -      | ✓    |\n\n#### 返回值\n\n- **类型：** this（Graph 实例）\n- **描述：** 返回图实例本身，支持链式调用\n\n#### 示例\n\n```typescript\nimport { NodeEvent } from '@antv/g6';\n\n// 移除所有节点点击事件的监听器\ngraph.off(NodeEvent.CLICK);\nconsole.log('已移除所有节点点击事件监听器');\n\n// 在某个操作模式结束后，移除相关的临时事件监听\nfunction exitEditMode() {\n  // 移除编辑模式下的所有监听器\n  graph.off(NodeEvent.DRAG_END);\n  graph.off(NodeEvent.DROP);\n  console.log('已退出编辑模式');\n}\n```\n\n### Graph.off(eventName, callback)\n\n移除特定事件的特定回调函数。\n\n```typescript\noff(eventName: string, callback: (...args: any[]) => void): this;\n```\n\n#### 参数\n\n| 参数      | 描述             | 类型                     | 默认值 | 必选 |\n| --------- | ---------------- | ------------------------ | ------ | ---- |\n| eventName | 要移除的事件名称 | string                   | -      | ✓    |\n| callback  | 要移除的回调函数 | (...args: any[]) => void | -      | ✓    |\n\n#### 返回值\n\n- **类型：** this（Graph 实例）\n- **描述：** 返回图实例本身，支持链式调用\n\n#### 示例\n\n```typescript\nimport { NodeEvent } from '@antv/g6';\n\n// 定义回调函数\nconst handleNodeClick = (evt) => {\n  console.log('节点被点击:', evt.target.id);\n};\n\n// 添加监听器\ngraph.on(NodeEvent.CLICK, handleNodeClick);\n\n// 之后在某个时机移除这个特定的监听器\ngraph.off(NodeEvent.CLICK, handleNodeClick);\nconsole.log('已移除特定的节点点击事件监听器');\n```\n\n## 事件常量枚举\n\nG6 提供了多种事件常量枚举，便于开发者使用规范的事件名称。以下是所有事件常量的详细说明：\n\n### 节点事件 (NodeEvent)\n\n| 常量名        | 事件名              | 描述                                 |\n| ------------- | ------------------- | ------------------------------------ |\n| CLICK         | `node:click`        | 点击节点时触发                       |\n| DBLCLICK      | `node:dblclick`     | 双击节点时触发                       |\n| POINTER_OVER  | `node:pointerover`  | 指针移入节点时触发                   |\n| POINTER_LEAVE | `node:pointerleave` | 指针离开节点时触发                   |\n| POINTER_ENTER | `node:pointerenter` | 指针进入节点或其子元素时触发(不冒泡) |\n| POINTER_MOVE  | `node:pointermove`  | 指针在节点上移动时触发               |\n| POINTER_OUT   | `node:pointerout`   | 指针离开节点时触发                   |\n| POINTER_DOWN  | `node:pointerdown`  | 指针在节点上按下时触发               |\n| POINTER_UP    | `node:pointerup`    | 指针在节点上抬起时触发               |\n| CONTEXT_MENU  | `node:contextmenu`  | 节点上打开上下文菜单时触发           |\n| DRAG_START    | `node:dragstart`    | 开始拖拽节点时触发                   |\n| DRAG          | `node:drag`         | 拖拽节点过程中触发                   |\n| DRAG_END      | `node:dragend`      | 拖拽节点结束时触发                   |\n| DRAG_ENTER    | `node:dragenter`    | 拖拽物进入节点时触发                 |\n| DRAG_OVER     | `node:dragover`     | 拖拽物在节点上方时触发               |\n| DRAG_LEAVE    | `node:dragleave`    | 拖拽物离开节点时触发                 |\n| DROP          | `node:drop`         | 在节点上放置拖拽物时触发             |\n\n### 边事件 (EdgeEvent)\n\n| 常量名        | 事件名              | 描述                               |\n| ------------- | ------------------- | ---------------------------------- |\n| CLICK         | `edge:click`        | 点击边时触发                       |\n| DBLCLICK      | `edge:dblclick`     | 双击边时触发                       |\n| POINTER_OVER  | `edge:pointerover`  | 指针移入边时触发                   |\n| POINTER_LEAVE | `edge:pointerleave` | 指针离开边时触发                   |\n| POINTER_ENTER | `edge:pointerenter` | 指针进入边或其子元素时触发(不冒泡) |\n| POINTER_MOVE  | `edge:pointermove`  | 指针在边上移动时触发               |\n| POINTER_OUT   | `edge:pointerout`   | 指针离开边时触发                   |\n| POINTER_DOWN  | `edge:pointerdown`  | 指针在边上按下时触发               |\n| POINTER_UP    | `edge:pointerup`    | 指针在边上抬起时触发               |\n| CONTEXT_MENU  | `edge:contextmenu`  | 边上打开上下文菜单时触发           |\n| DRAG_ENTER    | `edge:dragenter`    | 拖拽物进入边时触发                 |\n| DRAG_OVER     | `edge:dragover`     | 拖拽物在边上方时触发               |\n| DRAG_LEAVE    | `edge:dragleave`    | 拖拽物离开边时触发                 |\n| DROP          | `edge:drop`         | 在边上放置拖拽物时触发             |\n\n### Combo事件 (ComboEvent)\n\n| 常量名        | 事件名               | 描述                                  |\n| ------------- | -------------------- | ------------------------------------- |\n| CLICK         | `combo:click`        | 点击Combo时触发                       |\n| DBLCLICK      | `combo:dblclick`     | 双击Combo时触发                       |\n| POINTER_OVER  | `combo:pointerover`  | 指针移入Combo时触发                   |\n| POINTER_LEAVE | `combo:pointerleave` | 指针离开Combo时触发                   |\n| POINTER_ENTER | `combo:pointerenter` | 指针进入Combo或其子元素时触发(不冒泡) |\n| POINTER_MOVE  | `combo:pointermove`  | 指针在Combo上移动时触发               |\n| POINTER_OUT   | `combo:pointerout`   | 指针离开Combo时触发                   |\n| POINTER_DOWN  | `combo:pointerdown`  | 指针在Combo上按下时触发               |\n| POINTER_UP    | `combo:pointerup`    | 指针在Combo上抬起时触发               |\n| CONTEXT_MENU  | `combo:contextmenu`  | Combo上打开上下文菜单时触发           |\n| DRAG_START    | `combo:dragstart`    | 开始拖拽Combo时触发                   |\n| DRAG          | `combo:drag`         | 拖拽Combo过程中触发                   |\n| DRAG_END      | `combo:dragend`      | 拖拽Combo结束时触发                   |\n| DRAG_ENTER    | `combo:dragenter`    | 拖拽物进入Combo时触发                 |\n| DRAG_OVER     | `combo:dragover`     | 拖拽物在Combo上方时触发               |\n| DRAG_LEAVE    | `combo:dragleave`    | 拖拽物离开Combo时触发                 |\n| DROP          | `combo:drop`         | 在Combo上放置拖拽物时触发             |\n\n### 画布事件 (CanvasEvent)\n\n| 常量名        | 事件名                | 描述                                 |\n| ------------- | --------------------- | ------------------------------------ |\n| CLICK         | `canvas:click`        | 点击画布空白处时触发                 |\n| DBLCLICK      | `canvas:dblclick`     | 双击画布空白处时触发                 |\n| POINTER_OVER  | `canvas:pointerover`  | 指针移入画布时触发                   |\n| POINTER_LEAVE | `canvas:pointerleave` | 指针离开画布时触发                   |\n| POINTER_ENTER | `canvas:pointerenter` | 指针进入画布或其子元素时触发(不冒泡) |\n| POINTER_MOVE  | `canvas:pointermove`  | 指针在画布上移动时触发               |\n| POINTER_OUT   | `canvas:pointerout`   | 指针离开画布时触发                   |\n| POINTER_DOWN  | `canvas:pointerdown`  | 指针在画布上按下时触发               |\n| POINTER_UP    | `canvas:pointerup`    | 指针在画布上抬起时触发               |\n| CONTEXT_MENU  | `canvas:contextmenu`  | 画布上打开上下文菜单时触发           |\n| DRAG_START    | `canvas:dragstart`    | 开始拖拽画布时触发                   |\n| DRAG          | `canvas:drag`         | 拖拽画布过程中触发                   |\n| DRAG_END      | `canvas:dragend`      | 拖拽画布结束时触发                   |\n| DRAG_ENTER    | `canvas:dragenter`    | 拖拽物进入画布时触发                 |\n| DRAG_OVER     | `canvas:dragover`     | 拖拽物在画布上方时触发               |\n| DRAG_LEAVE    | `canvas:dragleave`    | 拖拽物离开画布时触发                 |\n| DROP          | `canvas:drop`         | 在画布上放置拖拽物时触发             |\n| WHEEL         | `canvas:wheel`        | 在画布上滚动鼠标滚轮时触发           |\n\n### 图表生命周期事件 (GraphEvent)\n\n| 常量名                   | 事件名                   | 描述                               |\n| ------------------------ | ------------------------ | ---------------------------------- |\n| BEFORE_CANVAS_INIT       | `beforecanvasinit`       | 画布初始化之前触发                 |\n| AFTER_CANVAS_INIT        | `aftercanvasinit`        | 画布初始化之后触发                 |\n| BEFORE_SIZE_CHANGE       | `beforesizechange`       | 视口尺寸变更之前触发               |\n| AFTER_SIZE_CHANGE        | `aftersizechange`        | 视口尺寸变更之后触发               |\n| BEFORE_ELEMENT_CREATE    | `beforeelementcreate`    | 元素创建之前触发                   |\n| AFTER_ELEMENT_CREATE     | `afterelementcreate`     | 元素创建之后触发                   |\n| BEFORE_ELEMENT_UPDATE    | `beforeelementupdate`    | 元素更新之前触发                   |\n| AFTER_ELEMENT_UPDATE     | `afterelementupdate`     | 元素更新之后触发                   |\n| BEFORE_ELEMENT_DESTROY   | `beforeelementdestroy`   | 元素销毁之前触发                   |\n| AFTER_ELEMENT_DESTROY    | `afterelementdestroy`    | 元素销毁之后触发                   |\n| BEFORE_ELEMENT_TRANSLATE | `beforeelementtranslate` | 元素平移之前触发                   |\n| AFTER_ELEMENT_TRANSLATE  | `afterelementtranslate`  | 元素平移之后触发                   |\n| BEFORE_DRAW              | `beforedraw`             | 绘制开始之前触发                   |\n| AFTER_DRAW               | `afterdraw`              | 绘制结束之后触发                   |\n| BEFORE_RENDER            | `beforerender`           | 渲染开始之前触发                   |\n| AFTER_RENDER             | `afterrender`            | 渲染完成之后触发                   |\n| BEFORE_ANIMATE           | `beforeanimate`          | 动画开始之前触发                   |\n| AFTER_ANIMATE            | `afteranimate`           | 动画结束之后触发                   |\n| BEFORE_LAYOUT            | `beforelayout`           | 布局开始之前触发                   |\n| AFTER_LAYOUT             | `afterlayout`            | 布局结束之后触发                   |\n| BEFORE_STAGE_LAYOUT      | `beforestagelayout`      | 流水线布局过程中每个阶段开始前触发 |\n| AFTER_STAGE_LAYOUT       | `afterstagelayout`       | 流水线布局过程中每个阶段结束后触发 |\n| BEFORE_TRANSFORM         | `beforetransform`        | 可视区域变化之前触发               |\n| AFTER_TRANSFORM          | `aftertransform`         | 可视区域变化之后触发               |\n| BATCH_START              | `batchstart`             | 批处理操作开始时触发               |\n| BATCH_END                | `batchend`               | 批处理操作结束时触发               |\n| BEFORE_DESTROY           | `beforedestroy`          | 图表销毁前触发                     |\n| AFTER_DESTROY            | `afterdestroy`           | 图表销毁后触发                     |\n| BEFORE_RENDERER_CHANGE   | `beforerendererchange`   | 渲染器变更之前触发                 |\n| AFTER_RENDERER_CHANGE    | `afterrendererchange`    | 渲染器变更之后触发                 |\n\n### 容器事件 (ContainerEvent)\n\n| 常量名   | 事件名    | 描述               |\n| -------- | --------- | ------------------ |\n| KEY_DOWN | `keydown` | 键盘按键按下时触发 |\n| KEY_UP   | `keyup`   | 键盘按键抬起时触发 |\n\n### 通用事件 (CommonEvent)\n\n这些是不带前缀的事件，可用于监听全局事件：\n\n| 常量名        | 事件名         | 描述                                     |\n| ------------- | -------------- | ---------------------------------------- |\n| CLICK         | `click`        | 点击任何元素时触发                       |\n| DBLCLICK      | `dblclick`     | 双击任何元素时触发                       |\n| POINTER_OVER  | `pointerover`  | 指针移入任何元素时触发                   |\n| POINTER_LEAVE | `pointerleave` | 指针离开任何元素时触发                   |\n| POINTER_ENTER | `pointerenter` | 指针进入任何元素或其子元素时触发(不冒泡) |\n| POINTER_MOVE  | `pointermove`  | 指针在任何元素上移动时触发               |\n| POINTER_OUT   | `pointerout`   | 指针离开任何元素时触发                   |\n| POINTER_DOWN  | `pointerdown`  | 指针在任何元素上按下时触发               |\n| POINTER_UP    | `pointerup`    | 指针在任何元素上抬起时触发               |\n| CONTEXT_MENU  | `contextmenu`  | 任何元素上打开上下文菜单时触发           |\n| DRAG_START    | `dragstart`    | 开始拖拽任何元素时触发                   |\n| DRAG          | `drag`         | 拖拽任何元素过程中触发                   |\n| DRAG_END      | `dragend`      | 拖拽任何元素结束时触发                   |\n| DRAG_ENTER    | `dragenter`    | 拖拽物进入任何元素时触发                 |\n| DRAG_OVER     | `dragover`     | 拖拽物在任何元素上方时触发               |\n| DRAG_LEAVE    | `dragleave`    | 拖拽物离开任何元素时触发                 |\n| DROP          | `drop`         | 在任何元素上放置拖拽物时触发             |\n| KEY_DOWN      | `keydown`      | 键盘按键按下时触发                       |\n| KEY_UP        | `keyup`        | 键盘按键抬起时触发                       |\n| WHEEL         | `wheel`        | 滚动鼠标滚轮时触发                       |\n| PINCH         | `pinch`        | 多点触控屏幕上双指捏合或张开时触发       |\n\n## 使用技巧\n\n### 链式调用\n\nG6 的事件 API 支持链式调用，可以连续注册多个事件：\n\n```typescript\nimport { NodeEvent, EdgeEvent, CanvasEvent } from '@antv/g6';\n\n// 使用常量枚举+链式调用\ngraph.on(NodeEvent.CLICK, handleNodeClick).on(EdgeEvent.CLICK, handleEdgeClick).on(CanvasEvent.WHEEL, handleCanvasZoom);\n```\n\n### 事件代理\n\n你可以利用事件冒泡机制，在父元素上监听所有子元素的事件：\n\n```typescript\nimport { CommonEvent } from '@antv/g6';\n\n// 统一处理所有元素的点击事件\ngraph.on(CommonEvent.CLICK, (evt) => {\n  const { targetType, target } = evt;\n  if (targetType === 'node') {\n    console.log('点击了节点:', target.id);\n  } else if (targetType === 'edge') {\n    console.log('点击了边:', target.id);\n  } else {\n    console.log('点击了画布空白处');\n  }\n});\n```\n\n### 事件对象属性\n\n大多数事件的回调函数会接收一个事件对象，包含以下常用属性：\n\n- `target` - 触发事件的元素\n- `targetType` - 触发事件的元素类型（node/edge/combo/canvas）\n- `originalTarget` - 原始的触发事件的图形\n- `currentTarget` - 当前触发事件的对象\n- `originalEvent` - 原始的浏览器事件对象\n\n通过这些属性，你可以精确地控制交互行为。\n"
  },
  {
    "path": "packages/site/docs/api/export-image.en.md",
    "content": "---\ntitle: Export Image\norder: 12\n---\n\n## Overview of Image Export\n\nG6 provides the functionality to export the graph as an image, allowing you to export the current canvas content as a DataURL format. This is convenient for saving, sharing, or further processing. The exported image will retain all visible elements on the canvas, including nodes, edges, combos, and other custom graphics.\n\n## API Reference\n\n### Graph.toDataURL(options)\n\nExport the current canvas as an image in DataURL format.\n\n```typescript\ntoDataURL(options?: Partial<DataURLOptions>): Promise<string>;\n```\n\n**Parameters**\n\n| Parameter | Description                | Type                      | Default | Required |\n| --------- | -------------------------- | ------------------------- | ------- | -------- |\n| options   | Export image configuration | Partial\\<DataURLOptions\\> | -       |          |\n\n**Return Value**\n\nReturns a Promise that resolves to a DataURL string representing the image.\n\n**DataURLOptions Type Definition**\n\n| Parameter      | Type                                        | Required | Description                                                                                              |\n| -------------- | ------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------- |\n| mode           | 'viewport' \\| 'overall'                     | No       | Export mode <br/> - viewport: Export viewport content <br/> - overall: Export entire canvas              |\n| type           | 'image/png' \\| 'image/jpeg' \\| 'image/webp' | No       | Image type <br/> - image/png: PNG format <br/> - image/jpeg: JPEG format <br/> - image/webp: WebP format |\n| encoderOptions | number                                      | No       | Image quality, only effective for image/jpeg and image/webp, range 0 ~ 1                                 |\n\n## Download Image\n\nG6 5.0 only provides an API to export the canvas as a Base64 image ([toDataURL](#graphtodataurloptions)). If you need to download the image, you can use the following method:\n\n```typescript\nasync function downloadImage() {\n  const dataURL = await graph.toDataURL();\n  const [head, content] = dataURL.split(',');\n  const contentType = head.match(/:(.*?);/)![1];\n\n  const bstr = atob(content);\n  let length = bstr.length;\n  const u8arr = new Uint8Array(length);\n\n  while (length--) {\n    u8arr[length] = bstr.charCodeAt(length);\n  }\n\n  const blob = new Blob([u8arr], { type: contentType });\n\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement('a');\n  a.href = url;\n  a.download = 'graph.png';\n  a.click();\n}\n```\n\n<br />\n\n:::warning{title=Note}\nThe exported image content may not include the complete canvas content. The export range only includes the content within the Graph canvas. Some plugins use custom containers, canvases, etc., which will not appear in the exported image.\n:::\n"
  },
  {
    "path": "packages/site/docs/api/export-image.zh.md",
    "content": "---\ntitle: 导出图片\norder: 12\n---\n\n## 图片导出概述\n\nG6 提供了将图导出为图片的功能，可以将当前画布内容导出为 DataURL 格式，方便保存、分享或进一步处理。导出的图片会保留画布上的所有可见元素，包括节点、边、组合以及其他自定义图形。\n\n## API 参考\n\n### Graph.toDataURL(options)\n\n将当前画布导出为 DataURL 格式的图片。\n\n```typescript\ntoDataURL(options?: Partial<DataURLOptions>): Promise<string>;\n```\n\n**参数**\n\n| 参数    | 描述         | 类型                      | 默认值 | 必选 |\n| ------- | ------------ | ------------------------- | ------ | ---- |\n| options | 导出图片配置 | Partial\\<DataURLOptions\\> | -      |      |\n\n**返回值**\n\n返回一个 Promise，解析为表示图片的 DataURL 字符串。\n\n**DataURLOptions 类型定义**\n\n| 参数           | 类型                                        | 必选 | 描述                                                                                             |\n| -------------- | ------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------ |\n| mode           | 'viewport' \\| 'overall'                     | 否   | 导出模式 <br/> - viewport: 导出视口内容 <br/> - overall: 导出整个画布                            |\n| type           | 'image/png' \\| 'image/jpeg' \\| 'image/webp' | 否   | 图片类型 <br/> - image/png: PNG 格式 <br/> - image/jpeg: JPEG 格式 <br/> - image/webp: WebP 格式 |\n| encoderOptions | number                                      | 否   | 图片质量，仅对 image/jpeg 和 image/webp 有效，取值范围 0 ~ 1                                     |\n\n## 下载图片\n\nG6 5.0 仅提供导出画布为 Base64 图片的 API([toDataURL](#graphtodataurloptions))，如果需要下载图片，可以使用以下方法：\n\n```typescript\nasync function downloadImage() {\n  const dataURL = await graph.toDataURL();\n  const [head, content] = dataURL.split(',');\n  const contentType = head.match(/:(.*?);/)![1];\n\n  const bstr = atob(content);\n  let length = bstr.length;\n  const u8arr = new Uint8Array(length);\n\n  while (length--) {\n    u8arr[length] = bstr.charCodeAt(length);\n  }\n\n  const blob = new Blob([u8arr], { type: contentType });\n\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement('a');\n  a.href = url;\n  a.download = 'graph.png';\n  a.click();\n}\n```\n\n<br />\n\n:::warning{title=注意}\n导出的图片内容可能不会包含完整的画布内容，导出范围仅包含 Graph 画布中的内容。部分插件使用了自定义的容器、画布等，这部分内容不会出现在导出的图片中。\n:::\n"
  },
  {
    "path": "packages/site/docs/api/graph.en.md",
    "content": "---\ntitle: Graph Instance\norder: 2\n---\n\n## API Reference\n\n### Graph.destroy()\n\nDestroy the current graph instance and release all resources related to it.\n\n⚠️ **Note**: After destruction, no operations can be performed. If you need to use it again, you must create a new graph instance.\n\n```typescript\ndestroy(): void;\n```\n\n**Usage Scenarios**:\n\n- When the user closes the chart or switches to another view, this method can be called to release resources.\n- When needing to recreate a graph instance, ensure the old instance is destroyed first to avoid memory leaks.\n"
  },
  {
    "path": "packages/site/docs/api/graph.zh.md",
    "content": "---\ntitle: 图实例\norder: 2\n---\n\n## API 参考\n\n### Graph.destroy()\n\n销毁当前图实例，释放与图实例相关的所有资源。\n\n⚠️ **注意**: 销毁后无法进行任何操作，如果需要重新使用，需要重新创建一个新的图实例。\n\n```typescript\ndestroy(): void;\n```\n\n**使用场景**:\n\n- 当用户关闭图表或切换到其他视图时，可以调用此方法来释放资源。\n- 在需要重新创建图实例时，确保先销毁旧的实例，以避免内存泄漏。\n"
  },
  {
    "path": "packages/site/docs/api/layout.en.md",
    "content": "---\ntitle: Layout\norder: 6\n---\n\n## Overview of Layout\n\n[Layout](/en/manual/layout/overview) is a crucial part of graph visualization, determining the positioning of nodes on the canvas. G6 offers a variety of layout algorithms to meet different data structures and visualization needs. Through the layout API, you can:\n\n- Set and update the graph's layout configuration\n- Execute or stop layout calculations\n- Combine multiple layout strategies\n- Customize layout algorithms\n\nA suitable layout can clearly display the relationship patterns between nodes, enhancing the graph's readability and aesthetics.\n\n## API Reference\n\n### Graph.setLayout(layout)\n\nSet the graph's layout algorithm and configuration.\n\n⚠️ **Note**: Calling this function won't automatically re-layout, so there's need to call `graph.layout()` separately.\n\n```typescript\nsetLayout(layout: LayoutOptions | ((prev: LayoutOptions) => LayoutOptions)): void;\n```\n\n**Parameters**\n\n| Parameter | Description                                                                                        | Type                                                                        | Default | Required |\n| --------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ------- | -------- |\n| layout    | Layout configuration object, or a function returning a new configuration based on the previous one | [LayoutOptions](#layoutoptions) \\| ((prev: LayoutOptions) => LayoutOptions) | -       | ✓        |\n\n**Example 1**: Set a force-directed layout\n\n```typescript\n// Set a simple force-directed layout\ngraph.setLayout({\n  type: 'force',\n  preventOverlap: true, // Prevent node overlap\n  nodeStrength: -50, // Repulsion between nodes, negative value for repulsion\n  edgeStrength: 0.5, // Edge strength, affects edge length\n});\n```\n\n**Example 2**: Update layout using a function\n\n```typescript\n// Update based on the current layout configuration\ngraph.setLayout((prevLayout) => {\n  // If the previous layout was force-directed, adjust its parameters\n  if (prevLayout.type === 'force') {\n    return {\n      ...prevLayout,\n      preventOverlap: true,\n      nodeStrength: -100, // Increase repulsion\n      alphaDecay: 0.01, // Lower decay rate for more iteration time\n    };\n  }\n\n  // Otherwise, switch to radial layout\n  return {\n    type: 'radial',\n    unitRadius: 100,\n    preventOverlap: true,\n  };\n});\n```\n\n**Example 3**: Set a combined layout\n\n```typescript\n// Set a combined layout - different nodes use different layout algorithms\ngraph.setLayout([\n  {\n    type: 'grid',\n    // Filter function: only nodes with type 'main' participate in the layout\n    nodeFilter: (node) => node.data.type === 'main',\n    rows: 1,\n  },\n  {\n    type: 'circle',\n    nodeFilter: (node) => node.data.type === 'sub',\n    radius: 100,\n  },\n]);\n```\n\n### Graph.getLayout()\n\nGet the current layout configuration.\n\n```typescript\ngetLayout(): LayoutOptions;\n```\n\n**Return Value**\n\n- **Type**: [LayoutOptions](#layoutoptions)\n- **Description**: The current layout configuration object\n\n**Example**\n\n```typescript\n// Get the current layout configuration\nconst currentLayout = graph.getLayout();\nconsole.log('Current layout type:', currentLayout.type);\n```\n\n### Graph.layout(layoutOptions)\n\nExecute layout calculations. When graph data changes, call this method to trigger the layout algorithm to recalculate node positions.\n\n```typescript\nlayout(layoutOptions?: LayoutOptions): Promise<void>;\n```\n\n**Parameters**\n\n| Parameter     | Description                 | Type                                                                        | Default | Required |\n| ------------- | --------------------------- | --------------------------------------------------------------------------- | ------- | -------- |\n| layoutOptions | Layout configuration object | [LayoutOptions](#layoutoptions) \\| ((prev: LayoutOptions) => LayoutOptions) | -       |          |\n\nIf `layoutOptions` is provided, it takes precedence over the graph's current layout configuration.\n\n**Note**\n\nLayout calculation is an asynchronous process, especially for complex layout algorithms like force-directed layout. This method returns a Promise, which can be used to perform subsequent operations after the layout is complete.\n\n**Example 1**: Basic usage\n\n```typescript\n// Execute layout\nawait graph.layout();\nconsole.log('Layout calculation complete');\n```\n\n**Example 2**: Re-layout after adding data\n\n```typescript\n// Add new nodes and edges\ngraph.addData({\n  nodes: [{ id: 'newNode1' }, { id: 'newNode2' }],\n  edges: [{ id: 'newEdge', source: 'existingNode', target: 'newNode1' }],\n});\n\n// Draw new nodes and edges\nawait graph.draw();\n\n// Recalculate layout\nawait graph.layout();\n```\n\n**Example 3**: Listen to layout events\n\n```typescript\nimport { GraphEvent } from '@antv/g6';\n\n// Before layout starts\ngraph.on(GraphEvent.BEFORE_LAYOUT, () => {\n  console.log('Layout calculation starting...');\n});\n\n// After layout completes\ngraph.on(GraphEvent.AFTER_LAYOUT, () => {\n  console.log('Layout calculation complete');\n});\n\n// Execute layout\ngraph.layout();\n```\n\n### Graph.stopLayout()\n\nStop an ongoing layout calculation. Mainly used to stop iterative layout algorithms like force-directed layout.\n\n```typescript\nstopLayout(): void;\n```\n\n**Note**\n\nApplicable to layouts with iterative animations, currently `force` belongs to this category. If the layout calculation takes too long, you can manually stop the iteration.\n\n**Example 1**: Basic usage\n\n```typescript\n// Stop layout after 5 seconds\nsetTimeout(() => {\n  graph.stopLayout();\n  console.log('Layout manually stopped');\n}, 5000);\n```\n\n**Example 2**: Stop layout with user interaction\n\n```typescript\n// Stop layout when the user clicks the canvas\nimport { CanvasEvent } from '@antv/g6';\n\ngraph.on(CanvasEvent.CLICK, () => {\n  graph.stopLayout();\n  console.log('User clicked canvas, layout stopped');\n});\n```\n\n## Type Definitions\n\n### LayoutOptions\n\nLayout configuration type, can be a single layout configuration or an array of layout configurations.\n\n```typescript\ntype LayoutOptions = SingleLayoutOptions | SingleLayoutOptions[];\n```\n\n### SingleLayoutOptions\n\nSingle layout configuration, can be a built-in layout configuration or a custom base layout configuration.\n\n```typescript\ntype SingleLayoutOptions = BuiltInLayoutOptions | BaseLayoutOptions;\n```\n\n### BaseLayoutOptions\n\nBasic configuration items common to all layout types.\n\n```typescript\ninterface BaseLayoutOptions {\n  // Layout type\n  type: string;\n\n  // Node filter function for participating in the layout\n  nodeFilter?: (node: NodeData) => boolean;\n\n  // Whether to calculate the layout before initializing elements\n  preLayout?: boolean;\n\n  // Whether invisible nodes participate in the layout (effective when preLayout is true)\n  isLayoutInvisibleNodes?: boolean;\n\n  // Enable layout animation, for iterative layouts, animation transitions occur between iterations\n  animation?: boolean;\n\n  // Whether to run the layout in a WebWorker\n  enableWorker?: boolean;\n\n  // Number of iterations for iterative layouts\n  iterations?: number;\n\n  // Other specific layout configuration items\n  [key: string]: any;\n}\n```\n\n### BuiltInLayoutOptions\n\nConfiguration for G6's built-in layout types, see [API - Built-in Layouts](/en/manual/layout/antv-dagre-layout) for details.\n"
  },
  {
    "path": "packages/site/docs/api/layout.zh.md",
    "content": "---\ntitle: 布局\norder: 6\n---\n\n## 布局概述\n\n[布局](/manual/layout/overview) 是图可视化中至关重要的一环，它决定了节点在画布上的位置排布。G6 提供了多种布局算法，以满足不同数据结构和可视化需求。通过布局 API，你可以：\n\n- 设置和更新图的布局配置\n- 执行或停止布局计算\n- 组合多种布局策略\n- 自定义布局算法\n\n合适的布局可以清晰地展示节点间的关系模式，提高图的可读性和美观度。\n\n## API 参考\n\n### Graph.setLayout(layout)\n\n设置图的布局算法及配置。\n\n⚠️ **注意**: 调用此函数不会自动重新布局，需要单独调用 `graph.layout()`。\n\n```typescript\nsetLayout(layout: LayoutOptions | ((prev: LayoutOptions) => LayoutOptions)): void;\n```\n\n**参数**\n\n| 参数   | 描述                                               | 类型                                                                        | 默认值 | 必选 |\n| ------ | -------------------------------------------------- | --------------------------------------------------------------------------- | ------ | ---- |\n| layout | 布局配置对象，或者一个基于之前配置返回新配置的函数 | [LayoutOptions](#layoutoptions) \\| ((prev: LayoutOptions) => LayoutOptions) | -      | ✓    |\n\n**示例 1**: 设置力导向布局\n\n```typescript\n// 设置简单的力导向布局\ngraph.setLayout({\n  type: 'force',\n  preventOverlap: true, // 防止节点重叠\n  nodeStrength: -50, // 节点间斥力，负值为斥力\n  edgeStrength: 0.5, // 边的强度，会影响边的长度\n});\n```\n\n**示例 2**: 使用函数式更新布局\n\n```typescript\n// 基于当前布局配置进行更新\ngraph.setLayout((prevLayout) => {\n  // 如果之前是力导向布局，调整其参数\n  if (prevLayout.type === 'force') {\n    return {\n      ...prevLayout,\n      preventOverlap: true,\n      nodeStrength: -100, // 增加斥力\n      alphaDecay: 0.01, // 降低衰减率，让布局有更多迭代时间\n    };\n  }\n\n  // 否则切换到放射状布局\n  return {\n    type: 'radial',\n    unitRadius: 100,\n    preventOverlap: true,\n  };\n});\n```\n\n**示例 3**: 设置组合布局\n\n```typescript\n// 设置组合布局 - 不同的节点使用不同的布局算法\ngraph.setLayout([\n  {\n    type: 'grid',\n    // 过滤函数：只有type为'main'的节点参与布局\n    nodeFilter: (node) => node.data.type === 'main',\n    rows: 1,\n  },\n  {\n    type: 'circle',\n    nodeFilter: (node) => node.data.type === 'sub',\n    radius: 100,\n  },\n]);\n```\n\n### Graph.getLayout()\n\n获取当前的布局配置。\n\n```typescript\ngetLayout(): LayoutOptions;\n```\n\n**返回值**\n\n- **类型**: [LayoutOptions](#layoutoptions)\n- **描述**: 当前的布局配置对象\n\n**示例**\n\n```typescript\n// 获取当前布局配置\nconst currentLayout = graph.getLayout();\nconsole.log('当前布局类型:', currentLayout.type);\n```\n\n### Graph.layout(layoutOptions)\n\n执行布局计算。当图数据发生变化后，调用此方法可触发布局算法重新计算节点位置。\n\n```typescript\nlayout(layoutOptions?: LayoutOptions): Promise<void>;\n```\n\n**参数**\n\n| 参数          | 描述         | 类型                                                                        | 默认值 | 必选 |\n| ------------- | ------------ | --------------------------------------------------------------------------- | ------ | ---- |\n| layoutOptions | 布局配置对象 | [LayoutOptions](#layoutoptions) \\| ((prev: LayoutOptions) => LayoutOptions) | -      |      |\n\n如果传入 `layoutOptions`，则优先考虑传入的布局配置，否则使用图的当前布局配置进行布局。\n\n**说明**\n\n布局计算是一个异步过程，特别是对于复杂的布局算法（如力导向布局）。此方法返回一个 Promise，可以用于在布局完成后执行后续操作。\n\n**示例 1**: 基础用法\n\n```typescript\n// 执行布局\nawait graph.layout();\nconsole.log('布局计算完成');\n```\n\n**示例 2**: 添加数据后重新布局\n\n```typescript\n// 添加新节点和边\ngraph.addData({\n  nodes: [{ id: 'newNode1' }, { id: 'newNode2' }],\n  edges: [{ id: 'newEdge', source: 'existingNode', target: 'newNode1' }],\n});\n\n// 绘制新节点和边\nawait graph.draw();\n\n// 重新计算布局\nawait graph.layout();\n```\n\n**示例 3**: 监听布局事件\n\n```typescript\nimport { GraphEvent } from '@antv/g6';\n\n// 布局开始前\ngraph.on(GraphEvent.BEFORE_LAYOUT, () => {\n  console.log('布局计算开始...');\n});\n\n// 布局完成后\ngraph.on(GraphEvent.AFTER_LAYOUT, () => {\n  console.log('布局计算完成');\n});\n\n// 执行布局\ngraph.layout();\n```\n\n### Graph.stopLayout()\n\n停止正在进行中的布局计算。主要用于停止迭代类型的布局算法，如力导向布局。\n\n```typescript\nstopLayout(): void;\n```\n\n**说明**\n\n适用于带有迭代动画的布局，目前有 `force` 属于此类布局。当布局计算时间过长时，可以手动停止迭代。\n\n**示例 1**: 基本使用\n\n```typescript\n// 5秒后停止布局\nsetTimeout(() => {\n  graph.stopLayout();\n  console.log('布局已手动停止');\n}, 5000);\n```\n\n**示例 2**: 结合用户交互停止布局\n\n```typescript\n// 当用户点击画布时停止布局\nimport { CanvasEvent } from '@antv/g6';\n\ngraph.on(CanvasEvent.CLICK, () => {\n  graph.stopLayout();\n  console.log('用户点击画布，布局已停止');\n});\n```\n\n## 类型定义\n\n### LayoutOptions\n\n布局配置类型，可以是单一布局配置或布局配置数组。\n\n```typescript\ntype LayoutOptions = SingleLayoutOptions | SingleLayoutOptions[];\n```\n\n### SingleLayoutOptions\n\n单一布局配置，可以是内置布局配置或自定义基础布局配置。\n\n```typescript\ntype SingleLayoutOptions = BuiltInLayoutOptions | BaseLayoutOptions;\n```\n\n### BaseLayoutOptions\n\n所有布局类型共有的基础配置项。\n\n```typescript\ninterface BaseLayoutOptions {\n  // 布局类型\n  type: string;\n\n  // 参与该布局的节点过滤函数\n  nodeFilter?: (node: NodeData) => boolean;\n\n  // 是否在初始化元素前计算布局\n  preLayout?: boolean;\n\n  // 不可见节点是否参与布局（当 preLayout 为 true 时生效）\n  isLayoutInvisibleNodes?: boolean;\n\n  // 启用布局动画，对于迭代布局，会在两次迭代之间进行动画过渡\n  animation?: boolean;\n\n  // 是否在 WebWorker 中运行布局\n  enableWorker?: boolean;\n\n  // 迭代布局的迭代次数\n  iterations?: number;\n\n  // 其他特定布局的配置项\n  [key: string]: any;\n}\n```\n\n### BuiltInLayoutOptions\n\nG6 内置的布局类型配置，具体请查看 [API - 内置布局](/manual/layout/antv-dagre-layout)。\n"
  },
  {
    "path": "packages/site/docs/api/option.en.md",
    "content": "---\ntitle: Graph Options\norder: 7\n---\n\n## Overview of Graph Options\n\nThe [options](/en/manual/graph/option) of a G6 graph instance control various aspects of the graph, including canvas settings, viewport properties, data, layout, styles, interaction behaviors, plugins, and more. By configuring these options appropriately, you can flexibly customize the appearance and behavior of the graph.\n\nOptions can be specified when creating a graph instance or dynamically modified at runtime through the API. Some basic configurations (such as devicePixelRatio, container) require destroying and recreating the graph instance to take effect after modification.\n\n## API Reference\n\n### Graph.getOptions()\n\nRetrieve all configuration options of the current graph.\n\n```typescript\ngetOptions(): GraphOptions;\n```\n\n**Return Value**\n\n- **Type**: [GraphOptions](/en/manual/graph/option)\n- **Description**: Complete configuration options of the current graph\n\n**Example**\n\n```typescript\n// Retrieve the current graph's options\nconst options = graph.getOptions();\nconsole.log('Current graph options:', options);\n\n// Retrieve specific options\nconsole.log('Current canvas width:', options.width);\nconsole.log('Current layout options:', options.layout);\n```\n\n### Graph.setOptions(options)\n\nUpdate the graph's configuration options.\n\n```typescript\nsetOptions(options: GraphOptions): void;\n```\n\n**Parameters**\n\n| Parameter | Description               | Type                                    | Default | Required |\n| --------- | ------------------------- | --------------------------------------- | ------- | -------- |\n| options   | New configuration options | [GraphOptions](/en/manual/graph/option) | -       | ✓        |\n\n**Note**\n\n⚠️ **Attention**: To update basic properties like devicePixelRatio, container, etc., you need to destroy the current graph instance and recreate it. Most other configurations can be dynamically updated.\n\n**Example 1**: Basic Usage\n\n```typescript\n// Update graph configuration\ngraph.setOptions({\n  width: 1000, // Update width\n  height: 800, // Update height\n  autoFit: 'view', // Enable auto-fit\n  animation: true, // Enable animation\n});\n```\n\n**Example 2**: Update Theme\n\n```typescript\n// Update graph theme configuration\ngraph.setOptions({\n  theme: {\n    type: 'dark', // Switch to dark theme\n    // Custom theme configuration\n    node: {\n      palette: ['#1AAF8B', '#F8E71C', '#8B572A', '#7ED321'],\n    },\n    edge: {\n      palette: ['#F5A623', '#F8E71C', '#8B572A', '#7ED321'],\n    },\n  },\n});\n```\n\n**Example 3**: Update Layout Configuration\n\n```typescript\n// Update layout configuration\ngraph.setOptions({\n  layout: {\n    type: 'force', // Switch to force-directed layout\n    preventOverlap: true,\n    nodeStrength: -50,\n    edgeStrength: 0.7,\n  },\n});\n```\n\n**Example 4**: Update Default Node and Edge Configuration\n\n```typescript\n// Update default style configuration for nodes and edges\ngraph.setOptions({\n  node: {\n    style: {\n      fill: '#91d5ff',\n      stroke: '#40a9ff',\n      lineWidth: 1,\n      radius: 10,\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#91d5ff',\n      lineWidth: 2,\n      endArrow: true,\n    },\n  },\n});\n```\n\n## Type Definitions\n\n### GraphOptions\n\n```typescript\ntype GraphOptions = {\n  // Whether to enable zooming\n  enableZoom?: boolean;\n\n  // Whether to enable dragging\n  enableDrag?: boolean;\n\n  // Default style for nodes\n  defaultNodeStyle?: {\n    fill: string;\n    stroke: string;\n  };\n\n  // Additional configuration options for the graph\n  [configKey: string]: any;\n};\n```\n"
  },
  {
    "path": "packages/site/docs/api/option.zh.md",
    "content": "---\ntitle: 图配置项\norder: 7\n---\n\n## 图配置项概述\n\nG6 图实例的 [配置项](/manual/graph/option) 控制着图的各个方面，包括画布设置、视口属性、数据、布局、样式、交互行为、插件等。通过合理配置这些选项，可以灵活定制图的外观和行为。\n\n配置项可以在图实例创建时指定，也可以通过 API 在运行时动态修改。某些基础配置（如 devicePixelRatio、container）修改后需要销毁并重新创建图实例才能生效。\n\n## API 参考\n\n### Graph.getOptions()\n\n获取当前图表的所有配置项。\n\n```typescript\ngetOptions(): GraphOptions;\n```\n\n**返回值**\n\n- **类型**: [GraphOptions](/manual/graph/option)\n- **描述**: 当前图表的完整配置项\n\n**示例**\n\n```typescript\n// 获取当前图表的配置项\nconst options = graph.getOptions();\nconsole.log('当前图表配置:', options);\n\n// 获取特定配置\nconsole.log('当前画布宽度:', options.width);\nconsole.log('当前布局配置:', options.layout);\n```\n\n### Graph.setOptions(options)\n\n更新图表的配置项。\n\n```typescript\nsetOptions(options: GraphOptions): void;\n```\n\n**参数**\n\n| 参数    | 描述       | 类型                                 | 默认值 | 必选 |\n| ------- | ---------- | ------------------------------------ | ------ | ---- |\n| options | 新的配置项 | [GraphOptions](/manual/graph/option) | -      | ✓    |\n\n**说明**\n\n⚠️ **注意**: 要更新 devicePixelRatio、container 等基础属性，需要销毁当前图实例后重新创建。其他大部分配置可以动态更新。\n\n**示例 1**: 基本用法\n\n```typescript\n// 更新图表配置\ngraph.setOptions({\n  width: 1000, // 更新宽度\n  height: 800, // 更新高度\n  autoFit: 'view', // 开启自适应\n  animation: true, // 启用动画\n});\n```\n\n**示例 2**: 更新主题\n\n```typescript\n// 更新图表主题配置\ngraph.setOptions({\n  theme: {\n    type: 'dark', // 切换到暗色主题\n    // 自定义主题配置\n    node: {\n      palette: ['#1AAF8B', '#F8E71C', '#8B572A', '#7ED321'],\n    },\n    edge: {\n      palette: ['#F5A623', '#F8E71C', '#8B572A', '#7ED321'],\n    },\n  },\n});\n```\n\n**示例 3**: 更新布局配置\n\n```typescript\n// 更新布局配置\ngraph.setOptions({\n  layout: {\n    type: 'force', // 切换到力导向布局\n    preventOverlap: true,\n    nodeStrength: -50,\n    edgeStrength: 0.7,\n  },\n});\n```\n\n**示例 4**: 更新节点和边的默认配置\n\n```typescript\n// 更新节点和边的默认样式配置\ngraph.setOptions({\n  node: {\n    style: {\n      fill: '#91d5ff',\n      stroke: '#40a9ff',\n      lineWidth: 1,\n      radius: 10,\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#91d5ff',\n      lineWidth: 2,\n      endArrow: true,\n    },\n  },\n});\n```\n"
  },
  {
    "path": "packages/site/docs/api/plugin.en.md",
    "content": "---\ntitle: Plugin\norder: 8\n---\n\n## Overview of Plugins\n\n[Plugins](/en/manual/plugin/overview) are an important mechanism in G6 for extending functionality and enhancing the interactive experience of graphs. Plugins typically provide independent functional modules, such as thumbnails, toolbars, context menus, etc. They integrate well with the main graph while maintaining modular and maintainable code.\n\nThe plugin system is designed to follow the \"plug and play\" principle, allowing dynamic addition or removal as needed.\n\n## API Reference\n\n### Graph.getPluginInstance(key)\n\nRetrieve the plugin instance specified by the key, used to access and operate the methods provided by the plugin.\n\n```typescript\ngetPluginInstance<T extends Plugin>(key: string): T;\n```\n\n**Parameters**\n\n| Parameter | Description                     | Type   | Default | Required |\n| --------- | ------------------------------- | ------ | ------- | -------- |\n| key       | Unique identifier of the plugin | string | -       | ✓        |\n\n**Return Value**\n\n- **Type**: Plugin instance\n- **Description**: The plugin instance corresponding to the specified key\n\n**Note**\n\nMany plugins provide specific API methods, which can be directly called by obtaining the plugin instance. For example, the fullscreen plugin provides `request()` and `exit()` methods to control fullscreen status.\n\n**Example**: Operate the fullscreen plugin\n\n```typescript\n// Get the fullscreen plugin instance\nconst fullscreen = graph.getPluginInstance('fullscreen');\n\n// Request to enter fullscreen\nfullscreen.request();\n\n// Exit fullscreen later\nsetTimeout(() => {\n  fullscreen.exit();\n}, 5000);\n```\n\n### Graph.getPlugins()\n\nRetrieve all configured plugins in the current graph.\n\n```typescript\ngetPlugins(): PluginOptions;\n```\n\n**Return Value**\n\n- **Type**: [PluginOptions](#pluginoptions)\n- **Description**: All configured plugins in the current graph\n\n**Example**\n\n```typescript\n// Get all plugin configurations\nconst plugins = graph.getPlugins();\n\n// View currently active plugins\nconsole.log('Current graph plugin configurations:', plugins);\n```\n\n### Graph.setPlugins(plugins)\n\nSet the graph's plugins, replacing all existing plugin configurations.\n\n```typescript\nsetPlugins(plugins: PluginOptions | ((prev: PluginOptions) => PluginOptions)): void;\n```\n\n**Parameters**\n\n| Parameter | Description                                                                                     | Type                                                                        | Default | Required |\n| --------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ------- | -------- |\n| plugins   | New plugin configurations, or a function returning new configurations based on the current ones | [PluginOptions](#pluginoptions) \\| ((prev: PluginOptions) => PluginOptions) | -       | ✓        |\n\n**Note**\n\nThe set plugins will completely replace the original plugin configurations. To add new plugins based on existing ones, you can use functional updates:\n\n```typescript\ngraph.setPlugins((plugins) => [...plugins, { type: 'grid', key: 'grid-line' }]);\n```\n\n**Example 1**: Set basic plugins\n\n```typescript\n// Set multiple basic plugins\ngraph.setPlugins([\n  // String form (using default configuration)\n  'minimap',\n\n  // Object form (custom configuration)\n  {\n    type: 'grid',\n    key: 'grid-line',\n  },\n  {\n    type: 'toolbar',\n    key: 'graph-toolbar',\n    position: 'top-right',\n  },\n]);\n```\n\n**Example 2**: Use functional updates\n\n```typescript\n// Add new plugins to existing configurations\ngraph.setPlugins((currentPlugins) => [\n  ...currentPlugins,\n  {\n    type: 'grid',\n    key: 'grid-line',\n  },\n]);\n\n// Replace specific plugins\ngraph.setPlugins((currentPlugins) => {\n  // Filter out existing grid plugins\n  const filteredPlugins = currentPlugins.filter((plugin) => {\n    if (typeof plugin === 'string') return plugin !== 'grid';\n    if (typeof plugin === 'function') return true;\n    return plugin.type !== 'grid';\n  });\n\n  // Add new grid plugin configuration\n  return [\n    ...filteredPlugins,\n    {\n      type: 'grid',\n      key: 'new-grid',\n      follow: true,\n    },\n  ];\n});\n```\n\n### Graph.updatePlugin(plugin)\n\nUpdate the configuration of a specified plugin, identified by the `key` of the plugin to be updated.\n\n```typescript\nupdatePlugin(plugin: UpdatePluginOption): void;\n```\n\n**Parameters**\n\n| Parameter | Description                               | Type                                      | Default | Required |\n| --------- | ----------------------------------------- | ----------------------------------------- | ------- | -------- |\n| plugin    | Configuration of the plugin to be updated | [UpdatePluginOption](#updatepluginoption) | -       | ✓        |\n\n**Note**\n\nTo update a plugin, the `key` field must be specified in the original plugin configuration to accurately locate and update the plugin.\n\n**Example 1**: Update plugin configuration\n\n```typescript\n// Specify key when initially setting plugins\ngraph.setPlugins([\n  {\n    type: 'grid',\n    key: 'main-grid',\n    follow: true,\n  },\n]);\n\n// Update grid plugin configuration\ngraph.updatePlugin({\n  key: 'main-grid',\n  follow: false,\n});\n```\n\n## Type Definitions\n\n### PluginOptions\n\nPlugin configuration type, representing an array of plugin configurations.\n\n```typescript\ntype PluginOptions = (string | CustomPluginOption | ((this: Graph) => CustomPluginOption))[];\n```\n\n### CustomPluginOption\n\nCustom plugin configuration interface, used to configure plugin parameters.\n\n```typescript\ntype CustomPluginOption = {\n  // Plugin type\n  type: string;\n\n  // Plugin key, i.e., unique identifier\n  // Used to identify the plugin for further operations\n  key?: string;\n\n  // Other configuration items for different types of plugins\n  [configKey: string]: any;\n};\n```\n\n### UpdatePluginOption\n\nConfiguration interface for updating plugins, used to dynamically modify plugin parameters.\n\n```typescript\ntype UpdatePluginOption = {\n  // Unique identifier of the plugin to be updated\n  key: string;\n\n  // Other configuration items to be updated\n  [configKey: string]: unknown;\n};\n```\n"
  },
  {
    "path": "packages/site/docs/api/plugin.zh.md",
    "content": "---\ntitle: 插件\norder: 8\n---\n\n## 插件概述\n\n[插件](/manual/plugin/overview)（Plugin）是 G6 中扩展功能的重要机制，用于增强图表的功能和交互体验。插件通常提供一些独立的功能模块，如缩略图、工具栏、上下文菜单等，它们可以与图表主体良好集成，同时又保持代码的模块化和可维护性。\n\n插件系统设计遵循\"即插即用\"的原则，可以根据需要动态添加或移除。\n\n## API 参考\n\n### Graph.getPluginInstance(key)\n\n获取指定 key 的插件实例，用于访问和操作插件提供的方法。\n\n```typescript\ngetPluginInstance<T extends Plugin>(key: string): T;\n```\n\n**参数**\n\n| 参数 | 描述             | 类型   | 默认值 | 必选 |\n| ---- | ---------------- | ------ | ------ | ---- |\n| key  | 插件的唯一标识符 | string | -      | ✓    |\n\n**返回值**\n\n- **类型**: 插件实例\n- **描述**: 指定 key 对应的插件实例\n\n**说明**\n\n许多插件提供了特定的API方法，通过获取插件实例可以直接调用这些方法。例如，全屏插件提供了 `request()` 和 `exit()` 方法来控制全屏状态。\n\n**示例**: 操作全屏插件\n\n```typescript\n// 获取全屏插件实例\nconst fullscreen = graph.getPluginInstance('fullscreen');\n\n// 请求进入全屏\nfullscreen.request();\n\n// 稍后退出全屏\nsetTimeout(() => {\n  fullscreen.exit();\n}, 5000);\n```\n\n### Graph.getPlugins()\n\n获取当前图表中所有已配置的插件。\n\n```typescript\ngetPlugins(): PluginOptions;\n```\n\n**返回值**\n\n- **类型**: [PluginOptions](#pluginoptions)\n- **描述**: 当前图表中已配置的所有插件\n\n**示例**\n\n```typescript\n// 获取所有插件配置\nconst plugins = graph.getPlugins();\n\n// 查看当前激活的插件\nconsole.log('当前图表的插件配置:', plugins);\n```\n\n### Graph.setPlugins(plugins)\n\n设置图表的插件，将替换所有现有的插件配置。\n\n```typescript\nsetPlugins(plugins: PluginOptions | ((prev: PluginOptions) => PluginOptions)): void;\n```\n\n**参数**\n\n| 参数    | 描述                                             | 类型                                                                        | 默认值 | 必选 |\n| ------- | ------------------------------------------------ | --------------------------------------------------------------------------- | ------ | ---- |\n| plugins | 新的插件配置，或一个基于当前配置返回新配置的函数 | [PluginOptions](#pluginoptions) \\| ((prev: PluginOptions) => PluginOptions) | -      | ✓    |\n\n**说明**\n\n设置的插件会全量替换原有的插件配置。如果需要在现有插件基础上添加新插件，可以使用函数式更新方式：\n\n```typescript\ngraph.setPlugins((plugins) => [...plugins, { type: 'grid', key: 'grid-line' }]);\n```\n\n**示例 1**: 设置基本插件\n\n```typescript\n// 设置多个基本插件\ngraph.setPlugins([\n  // 字符串形式（使用默认配置）\n  'minimap',\n\n  // 对象形式（自定义配置）\n  {\n    type: 'grid',\n    key: 'grid-line',\n  },\n  {\n    type: 'toolbar',\n    key: 'graph-toolbar',\n    position: 'top-right',\n  },\n]);\n```\n\n**示例 2**: 使用函数式更新\n\n```typescript\n// 添加新插件到现有配置\ngraph.setPlugins((currentPlugins) => [\n  ...currentPlugins,\n  {\n    type: 'grid',\n    key: 'grid-line',\n  },\n]);\n\n// 替换特定插件\ngraph.setPlugins((currentPlugins) => {\n  // 过滤掉现有的网格插件\n  const filteredPlugins = currentPlugins.filter((plugin) => {\n    if (typeof plugin === 'string') return plugin !== 'grid';\n    if (typeof plugin === 'function') return true;\n    return plugin.type !== 'grid';\n  });\n\n  // 添加新的网格插件配置\n  return [\n    ...filteredPlugins,\n    {\n      type: 'grid',\n      key: 'new-grid',\n      follow: true,\n    },\n  ];\n});\n```\n\n### Graph.updatePlugin(plugin)\n\n更新指定的插件配置，需要通过 `key` 标识要更新的插件。\n\n```typescript\nupdatePlugin(plugin: UpdatePluginOption): void;\n```\n\n**参数**\n\n| 参数   | 描述           | 类型                                      | 默认值 | 必选 |\n| ------ | -------------- | ----------------------------------------- | ------ | ---- |\n| plugin | 更新的插件配置 | [UpdatePluginOption](#updatepluginoption) | -      | ✓    |\n\n**说明**\n\n如果要更新一个插件，必须在原始插件配置中指定 `key` 字段，以便能够准确找到并更新该插件。\n\n**示例 1**: 更新插件配置\n\n```typescript\n// 初始设置插件时指定 key\ngraph.setPlugins([\n  {\n    type: 'grid',\n    key: 'main-grid',\n    follow: true,\n  },\n]);\n\n// 更新网格插件配置\ngraph.updatePlugin({\n  key: 'main-grid',\n  follow: false,\n});\n```\n\n## 类型定义\n\n### PluginOptions\n\n插件配置类型，表示一组插件配置的数组。\n\n```typescript\ntype PluginOptions = (string | CustomPluginOption | ((this: Graph) => CustomPluginOption))[];\n```\n\n### CustomPluginOption\n\n自定义插件配置接口，用于配置插件参数。\n\n```typescript\ntype CustomPluginOption = {\n  // 插件类型\n  type: string;\n\n  // 插件 key，即唯一标识\n  // 用于标识插件，从而进一步操作此插件\n  key?: string;\n\n  // 针对不同类型的插件，还可能有其他配置项\n  [configKey: string]: any;\n};\n```\n\n### UpdatePluginOption\n\n更新插件的配置接口，用于动态修改插件参数。\n\n```typescript\ntype UpdatePluginOption = {\n  // 要更新的插件的唯一标识\n  key: string;\n\n  // 其他要更新的配置项\n  [configKey: string]: unknown;\n};\n```\n"
  },
  {
    "path": "packages/site/docs/api/render.en.md",
    "content": "---\ntitle: Drawing and Rendering\norder: 3\n---\n\n## Overview of Drawing and Rendering\n\nG6 provides a series of drawing and rendering-related APIs to control the display process of graphical elements. In G6, drawing and rendering are two different concepts:\n\n- **Drawing (draw)**: Responsible only for drawing graphical elements onto the canvas, without involving layout calculations.\n- **Rendering (render)**: A complete rendering process, including data processing, layout calculations, and final drawing.\n\nUnderstanding the differences between these APIs is crucial for optimizing performance and achieving specific effects.\n\n## API Reference\n\n### Graph.draw()\n\nDraw elements without performing layout calculations.\n\n```typescript\ndraw(): Promise<void>;\n```\n\n**Note**\n\nThe `draw` method only executes the drawing process of elements and does not recalculate the layout.\n\n⚠️ **Attention**: `draw` is an asynchronous method, requiring the use of `await` or Promise chaining to ensure subsequent operations are executed after drawing is complete.\n\n**Example 1**: Basic Usage\n\n```typescript\n// Basic usage\nawait graph.draw();\n```\n\n**Example 2**: Redraw after modifying node styles\n\n```javascript\n// Redraw after modifying node styles\ngraph.updateNodeData([\n  {\n    id: 'node1',\n    style: {\n      fill: 'red',\n      stroke: 'blue',\n      lineWidth: 2,\n    },\n  },\n]);\n\n// Only draw the updated styles without re-layout\nawait graph.draw();\n```\n\n**Example 3**: Batch update multiple elements and draw once\n\n```javascript\n// Update multiple nodes\ngraph.updateNodeData([{ id: 'node1', style: { fill: 'red' } }]);\ngraph.updateNodeData([{ id: 'node2', style: { fill: 'blue' } }]);\n\n// Update edges\ngraph.updateEdgeData([{ id: 'edge1', style: { stroke: 'green' } }]);\n\n// Draw after batch operations\nawait graph.draw();\n```\n\n**Example 4**: Use event listener to detect drawing completion\n\n```javascript\nimport { GraphEvent } from '@antv/g6';\n\ngraph.on(GraphEvent.AFTER_DRAW, () => {\n  console.log('Drawing complete');\n});\n\nawait graph.draw();\n```\n\n### Graph.render()\n\nExecute the complete rendering process, including data processing, layout calculations, and drawing.\n\n```typescript\nrender(): Promise<void>;\n```\n\n**Note**\n\nThe `render` method executes the complete rendering process:\n\n1. Process data updates\n2. Draw elements onto the canvas\n3. Execute layout algorithms\n\n**Example 1**: Basic Usage\n\n```typescript\n// Basic usage\nawait graph.render();\n```\n\n**Example 2**: Render after adding new data\n\n```typescript\ngraph.addData({\n  nodes: [{ id: 'node3' }, { id: 'node4' }],\n  edges: [{ id: 'edge2', source: 'node1', target: 'node3' }],\n});\nawait graph.render();\n```\n\n**Example 3**: Listen to rendering events\n\n```typescript\nimport { GraphEvent } from '@antv/g6';\n\n// Before rendering starts\ngraph.on(GraphEvent.BEFORE_RENDER, () => {\n  console.log('Rendering starts...');\n  // Show loading indicator\n  showLoadingIndicator();\n});\n\n// After rendering completes\ngraph.on(GraphEvent.AFTER_RENDER, () => {\n  console.log('Rendering complete');\n  // Hide loading indicator\n  hideLoadingIndicator();\n});\n\ngraph.render();\n```\n\n### Graph.clear()\n\nClear all elements on the canvas, including nodes, edges, and other graphical elements.\n\n```typescript\nclear(): Promise<void>;\n```\n\n**Note**\n\nThis method deletes all elements in the graph but retains the canvas configuration and styles. It is an asynchronous method that returns a Promise.\n\n**Example**\n\n```typescript\n// Basic usage\nawait graph.clear();\n```\n\n## Usage Tips\n\n### Choosing between draw and render\n\n- Use `draw()` when:\n  - Only the styles or states of elements are modified, without needing to recalculate positions.\n  - Performance-sensitive, aiming to avoid unnecessary layout calculations.\n- Use `render()` when:\n  - Initializing the graph.\n  - Changing layout configurations.\n  - Adding or removing a large number of nodes/edges.\n  - Need to recalculate positions of all elements.\n"
  },
  {
    "path": "packages/site/docs/api/render.zh.md",
    "content": "---\ntitle: 绘制与渲染\norder: 3\n---\n\n## 绘制与渲染概述\n\nG6 提供了一系列绘制和渲染相关的 API，用于控制图形元素的显示过程。在 G6 中，绘制和渲染是两个不同的概念：\n\n- **绘制(draw)**: 仅负责将图形元素绘制到画布上，不涉及布局计算\n- **渲染(render)**: 完整的渲染流程，包括数据处理、布局计算和最终绘制\n\n理解这些 API 的区别对于优化性能和实现特定效果至关重要。\n\n## API 参考\n\n### Graph.draw()\n\n绘制元素，但不执行布局计算。\n\n```typescript\ndraw(): Promise<void>;\n```\n\n**说明**\n\n`draw` 方法仅执行元素的绘制过程，不会重新计算布局。\n\n⚠️ **注意**: `draw` 为异步方法，需要使用 `await` 或 Promise 链式调用来确保绘制完成后再执行后续操作。\n\n**示例 1**: 基础用法\n\n```typescript\n// 基本用法\nawait graph.draw();\n```\n\n**示例 2**: 修改节点样式后重新绘制\n\n```javascript\n// 修改节点样式后重新绘制\ngraph.updateNodeData([\n  {\n    id: 'node1',\n    style: {\n      fill: 'red',\n      stroke: 'blue',\n      lineWidth: 2,\n    },\n  },\n]);\n\n// 仅绘制更新后的样式，不重新布局\nawait graph.draw();\n```\n\n**示例 3**: 批量更新多个元素后一次性绘制\n\n```javascript\n// 更新多个节点\ngraph.updateNodeData([{ id: 'node1', style: { fill: 'red' } }]);\ngraph.updateNodeData([{ id: 'node2', style: { fill: 'blue' } }]);\n\n// 更新边\ngraph.updateEdgeData([{ id: 'edge1', style: { stroke: 'green' } }]);\n\n// 批量操作完成后绘制\nawait graph.draw();\n```\n\n**示例 4**: 使用事件监听绘制完成\n\n```javascript\nimport { GraphEvent } from '@antv/g6';\n\ngraph.on(GraphEvent.AFTER_DRAW, () => {\n  console.log('绘制完成');\n});\n\nawait graph.draw();\n```\n\n### Graph.render()\n\n执行完整的渲染流程，包括数据处理、布局计算和绘制。\n\n```typescript\nrender(): Promise<void>;\n```\n\n**说明**\n\n`render` 方法会执行完整的渲染流程：\n\n1. 处理数据更新\n2. 绘制元素到画布上\n3. 执行布局算法\n\n**示例 1**: 基本用法\n\n```typescript\n// 基本用法\nawait graph.render();\n```\n\n**示例 2**: 添加新数据后渲染\n\n```typescript\ngraph.addData({\n  nodes: [{ id: 'node3' }, { id: 'node4' }],\n  edges: [{ id: 'edge2', source: 'node1', target: 'node3' }],\n});\nawait graph.render();\n```\n\n**示例 3**: 监听渲染事件\n\n```typescript\nimport { GraphEvent } from '@antv/g6';\n\n// 渲染开始前\ngraph.on(GraphEvent.BEFORE_RENDER, () => {\n  console.log('渲染开始...');\n  // 显示加载指示器\n  showLoadingIndicator();\n});\n\n// 渲染完成后\ngraph.on(GraphEvent.AFTER_RENDER, () => {\n  console.log('渲染完成');\n  // 隐藏加载指示器\n  hideLoadingIndicator();\n});\n\ngraph.render();\n```\n\n### Graph.clear()\n\n清空画布上的所有元素，包括节点、边和其他图形元素。\n\n```typescript\nclear(): Promise<void>;\n```\n\n**说明**\n\n此方法会删除图中的所有元素，但保留画布配置和样式。这是一个异步方法，返回一个 Promise。\n\n**示例**\n\n```typescript\n// 基本用法\nawait graph.clear();\n```\n\n## 使用技巧\n\n### draw 与 render 的选择\n\n- 使用 `draw()` 当:\n  - 仅修改了元素样式或状态，不需要重新计算位置\n  - 性能敏感，希望避免不必要的布局计算\n- 使用 `render()` 当:\n  - 初始化图表\n  - 更改了布局配置\n  - 添加或删除了大量节点/边\n  - 需要重新计算所有元素位置\n"
  },
  {
    "path": "packages/site/docs/api/theme.en.md",
    "content": "---\ntitle: Theme\norder: 9\n---\n\n## Overview of Theme\n\nG6 allows users to customize the appearance of graphs through themes. Themes can be used to define colors, shapes, and styles for nodes, edges, and other graph elements.\n\n## API Reference\n\n### Graph.getTheme()\n\nGet the theme\n\n```typescript\ngetTheme(): ThemeOptions;\n```\n\n<details><summary>Related Parameters</summary>\n\n**Return Value**:\n\n- **Type:** false \\| 'light' \\| 'dark' \\| string\n\n- **Description:** Current theme\n\n</details>\n\n### Graph.setTheme(theme)\n\nSet the theme\n\n```typescript\nsetTheme(theme: ThemeOptions | ((prev: ThemeOptions) => ThemeOptions)): void;\n```\n\n**Example**\n\n```ts\ngraph.setTheme('dark');\n```\n\n<details><summary>Related Parameters</summary>\n\n<table><thead><tr><th>\n\nParameter\n\n</th><th>\n\nType\n\n</th><th>\n\nDescription\n\n</th></tr></thead>\n<tbody><tr><td>\n\ntheme\n\n</td><td>\n\nfalse \\| 'light' \\| 'dark' \\| string \\| ((prev: false \\| 'light' \\| 'dark' \\| string) => false \\| 'light' \\| 'dark' \\| string)\n\n</td><td>\n\nTheme name\n\n</td></tr>\n</tbody></table>\n\n**Return Value**:\n\n- **Type:** void\n\n</details>\n\n## Type Definitions\n\n### ThemeOptions\n\n```typescript\ntype ThemeOptions = {\n  // Colors used in the theme\n  colors: string[];\n\n  // Node style settings\n  nodeStyle?: {\n    fill: string;\n    stroke: string;\n  };\n\n  // Edge style settings\n  edgeStyle?: {\n    stroke: string;\n  };\n\n  // Additional configuration options for the theme\n  [configKey: string]: any;\n};\n```\n"
  },
  {
    "path": "packages/site/docs/api/theme.zh.md",
    "content": "---\ntitle: 主题\norder: 9\n---\n\n## API 参考\n\n### Graph.getTheme()\n\n获取主题\n\n```typescript\ngetTheme(): ThemeOptions;\n```\n\n<details><summary>相关参数</summary>\n\n**返回值**：\n\n- **类型：** false \\| 'light' \\| 'dark' \\| string\n\n- **描述：** 当前主题\n\n</details>\n\n### Graph.setTheme(theme)\n\n设置主题\n\n```typescript\nsetTheme(theme: ThemeOptions | ((prev: ThemeOptions) => ThemeOptions)): void;\n```\n\n**示例**\n\n```ts\ngraph.setTheme('dark');\n```\n\n<details><summary>相关参数</summary>\n\n<table><thead><tr><th>\n\n参数\n\n</th><th>\n\n类型\n\n</th><th>\n\n描述\n\n</th></tr></thead>\n<tbody><tr><td>\n\ntheme\n\n</td><td>\n\nfalse \\| 'light' \\| 'dark' \\| string \\| ((prev: false \\| 'light' \\| 'dark' \\| string) =&gt; false \\| 'light' \\| 'dark' \\| string)\n\n</td><td>\n\n主题名\n\n</td></tr>\n</tbody></table>\n\n**返回值**：\n\n- **类型：** void\n\n</details>\n"
  },
  {
    "path": "packages/site/docs/api/transform.en.md",
    "content": "---\ntitle: Data Transformation\norder: 10\n---\n\n## Overview of Data Transformation\n\n[Data Transformation](/en/manual/transform/overview) is a powerful feature in G6 that allows for processing and transforming data during the graph rendering process. With data transformers, you can achieve various data processing needs, such as:\n\n- Data Filtering: Filter nodes and edges to be displayed based on conditions\n- Data Calculation: Generate new attributes based on original data, such as calculating node size based on the number of connections, without polluting the original data\n- Data Aggregation: Aggregate a large number of nodes into fewer nodes to improve the performance of large-scale graphs\n\nData transformation occurs at specific stages of the rendering process, allowing flexible changes to the final presentation without modifying the original data source.\n\n## API Reference\n\n### Graph.getTransforms()\n\nRetrieve all configured data transformers in the current graph.\n\n```typescript\ngetTransforms(): TransformOptions;\n```\n\n**Return Value**\n\n- **Type**: [TransformOptions](#transformoptions)\n- **Description**: All configured data transformers in the current graph\n\n**Example**\n\n```typescript\n// Retrieve all data transformers\nconst transforms = graph.getTransforms();\nconsole.log('Data transformers in the current graph:', transforms);\n```\n\n### Graph.setTransforms(transforms)\n\nSet the data transformers for the graph, replacing all existing transformers.\n\n```typescript\nsetTransforms(transforms: TransformOptions | ((prev: TransformOptions) => TransformOptions)): void;\n```\n\n**Parameters**\n\n| Parameter  | Description                                                                                               | Type                                                                                  | Default | Required |\n| ---------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------- | -------- |\n| transforms | New data transformer configurations, or a function returning new configurations based on the current ones | [TransformOptions](#transformoptions) \\| (prev: TransformOptions) => TransformOptions | -       | ✓        |\n\n**Note**\n\nData transformers can process data at different stages of the graph rendering process. The set data transformations will completely replace the original ones. To add new data transformations based on existing ones, you can use functional updates.\n\n**Example 1**: Set basic data transformations\n\n```typescript\ngraph.setTransforms(['process-parallel-edges', 'map-node-size']);\n```\n\n**Example 2**: Set data transformations with configurations\n\n```typescript\ngraph.setTransforms([\n  // String form (using default configuration)\n  'process-parallel-edges',\n\n  // Object form (custom configuration)\n  {\n    type: 'process-parallel-edges',\n    key: 'my-process-parallel-edges',\n    distance: 20, // Distance between parallel edges\n  },\n]);\n```\n\n**Example 3**: Use functional updates\n\n```typescript\n// Add new data transformations to existing configurations\ngraph.setTransforms((currentTransforms) => [\n  ...currentTransforms,\n  {\n    type: 'map-node-size',\n    key: 'my-map-node-size',\n    maxSize: 100,\n    minSize: 20,\n  },\n]);\n```\n\n### Graph.updateTransform(transform)\n\nUpdate the configuration of a specified data transformer, identified by the `key` of the transformer to be updated.\n\n```typescript\nupdateTransform(transform: UpdateTransformOption): void;\n```\n\n**Parameters**\n\n| Parameter | Description                                         | Type                                            | Default | Required |\n| --------- | --------------------------------------------------- | ----------------------------------------------- | ------- | -------- |\n| transform | Configuration of the data transformer to be updated | [UpdateTransformOption](#updatetransformoption) | -       | ✓        |\n\n**Note**\n\nTo update a data transformer, the `key` field must be specified in the original data transformer configuration to accurately locate and update the transformer.\n\n**Example**: Update data transformer configuration\n\n```typescript\n// Specify key when initially setting data transformers\ngraph.setTransforms([\n  {\n    type: 'process-parallel-edges',\n    key: 'my-process-parallel-edges',\n    distance: 20,\n  },\n]);\n\n// Update distance between parallel edges\ngraph.updateTransform({\n  key: 'my-process-parallel-edges',\n  distance: 30,\n});\n```\n\n## Type Definitions\n\n### TransformOptions\n\nData transformer configuration type, representing an array of data transformer configurations.\n\n```typescript\ntype TransformOptions = (CustomTransformOption | ((this: Graph) => CustomTransformOption))[];\n```\n\n### CustomTransformOption\n\nCustom data transformer configuration interface, used to configure data processing parameters.\n\n```typescript\ntype CustomTransformOption = {\n  // Data processing type\n  type: string;\n\n  // Unique identifier for the data transformer\n  key?: string;\n\n  // Other configuration items for different types of data processing\n  [configKey: string]: any;\n};\n```\n\n### UpdateTransformOption\n\nConfiguration interface for updating data transformers, used to dynamically modify data processing parameters.\n\n```typescript\ntype UpdateTransformOption = {\n  // Unique identifier of the data transformer to be updated\n  key: string;\n\n  // Other configuration items to be updated\n  [configKey: string]: unknown;\n};\n```\n"
  },
  {
    "path": "packages/site/docs/api/transform.zh.md",
    "content": "---\ntitle: 数据处理\norder: 10\n---\n\n## 数据处理概述\n\n[数据处理](/manual/transform/overview)（Transform）是 G6 中一项强大的功能，允许在图渲染过程中对数据进行处理和转换。通过数据处理器，您可以实现各种数据处理需求，比如：\n\n- 数据过滤：根据条件筛选需要显示的节点和边\n- 数据计算：基于原始数据生成新的属性，如根据节点连接数计算节点大小，但不污染原始数据\n- 数据聚合：将大量节点聚合为少量节点，提高大规模图表的性能\n\n数据处理发生在渲染流程的特定阶段，可以灵活地改变最终呈现的结果，而无需修改原始数据源。\n\n## API 参考\n\n### Graph.getTransforms()\n\n获取当前图表中所有已配置的数据处理器。\n\n```typescript\ngetTransforms(): TransformOptions;\n```\n\n**返回值**\n\n- **类型**: [TransformOptions](#transformoptions)\n- **描述**: 当前图表中已配置的所有数据处理器\n\n**示例**\n\n```typescript\n// 获取当前所有数据处理器\nconst transforms = graph.getTransforms();\nconsole.log('当前图表的数据处理器:', transforms);\n```\n\n### Graph.setTransforms(transforms)\n\n设置图表的数据处理器，将替换所有现有的数据处理器。\n\n```typescript\nsetTransforms(transforms: TransformOptions | ((prev: TransformOptions) => TransformOptions)): void;\n```\n\n**参数**\n\n| 参数       | 描述                                                   | 类型                                                                                  | 默认值 | 必选 |\n| ---------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------- | ------ | ---- |\n| transforms | 新的数据处理器配置，或一个基于当前配置返回新配置的函数 | [TransformOptions](#transformoptions) \\| (prev: TransformOptions) => TransformOptions | -      | ✓    |\n\n**说明**\n\n数据处理器能够在图渲染过程的不同阶段对数据进行处理。设置的数据处理会全量替换原有的数据处理，如果需要在现有数据处理基础上添加新的数据处理，可以使用函数式更新方式。\n\n**示例 1**: 设置基本数据处理\n\n```typescript\ngraph.setTransforms(['process-parallel-edges', 'map-node-size']);\n```\n\n**示例 2**: 设置带配置的数据处理\n\n```typescript\ngraph.setTransforms([\n  // 字符串形式（使用默认配置）\n  'process-parallel-edges',\n\n  // 对象形式（自定义配置）\n  {\n    type: 'process-parallel-edges',\n    key: 'my-process-parallel-edges',\n    distance: 20, // 平行边之间的距离\n  },\n]);\n```\n\n**示例 3**: 使用函数式更新\n\n```typescript\n// 添加新的数据处理到现有配置\ngraph.setTransforms((currentTransforms) => [\n  ...currentTransforms,\n  {\n    type: 'map-node-size',\n    key: 'my-map-node-size',\n    maxSize: 100,\n    minSize: 20,\n  },\n]);\n```\n\n### Graph.updateTransform(transform)\n\n更新指定的数据处理器配置，需要通过 `key` 标识要更新的数据处理。\n\n```typescript\nupdateTransform(transform: UpdateTransformOption): void;\n```\n\n**参数**\n\n| 参数      | 描述               | 类型                                            | 默认值 | 必选 |\n| --------- | ------------------ | ----------------------------------------------- | ------ | ---- |\n| transform | 更新的数据处理配置 | [UpdateTransformOption](#updatetransformoption) | -      | ✓    |\n\n**说明**\n\n如果要更新一个数据处理器，必须在原始数据处理配置中指定 `key` 字段，以便能够准确找到并更新该数据处理。\n\n**示例**: 更新数据处理配置\n\n```typescript\n// 初始设置数据处理时指定 key\ngraph.setTransforms([\n  {\n    type: 'process-parallel-edges',\n    key: 'my-process-parallel-edges',\n    distance: 20,\n  },\n]);\n\n// 更新平行边距离\ngraph.updateTransform({\n  key: 'my-process-parallel-edges',\n  distance: 30,\n});\n```\n\n## 类型定义\n\n### TransformOptions\n\n数据处理器配置类型，表示一组数据处理配置的数组。\n\n```typescript\ntype TransformOptions = (CustomTransformOption | ((this: Graph) => CustomTransformOption))[];\n```\n\n### CustomTransformOption\n\n自定义数据处理配置接口，用于配置数据处理参数。\n\n```typescript\ntype CustomTransformOption = {\n  // 数据处理类型\n  type: string;\n\n  // 数据处理唯一标识\n  key?: string;\n\n  // 针对不同类型的数据处理，还可能有其他配置项\n  [configKey: string]: any;\n};\n```\n\n### UpdateTransformOption\n\n更新数据处理的配置接口，用于动态修改数据处理参数。\n\n```typescript\ntype UpdateTransformOption = {\n  // 要更新的数据处理的唯一标识\n  key: string;\n\n  // 其他要更新的配置项\n  [configKey: string]: unknown;\n};\n```\n"
  },
  {
    "path": "packages/site/docs/api/viewport.en.md",
    "content": "---\ntitle: Viewport Operations\norder: 4\n---\n\n## Overview of Viewport Operations\n\nG6 provides a series of viewport operation APIs to control the zooming, panning, and rotating of the canvas. These operations help users better view and interact with graphical content. Through viewport operations, you can achieve the following functions:\n\n- Zoom the canvas to view details or the global view\n- Pan the canvas to view different areas\n- Rotate the canvas to get different perspectives\n- Automatically fit content to the viewport\n\n### Categories of Viewport Operations\n\nViewport operations in G6 are mainly divided into the following categories:\n\n1. **Zoom Operations**: such as `zoomTo`, `zoomBy`\n2. **Pan Operations**: such as `translateTo`, `translateBy`\n3. **Rotate Operations**: such as `rotateTo`, `rotateBy`\n4. **Fit Operations**: such as `fitView`, `fitCenter`\n5. **Viewport Information Retrieval**: such as `getZoom`, `getPosition`\n\n## API Reference\n\n### Graph.zoomTo(zoom, animation, origin)\n\nZoom the canvas to a specified scale (absolute zoom).\n\n```typescript\nzoomTo(zoom: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise<void>;\n```\n\n**Parameters**\n\n| Parameter | Description                                                    | Type                                                            | Default | Required |\n| --------- | -------------------------------------------------------------- | --------------------------------------------------------------- | ------- | -------- |\n| zoom      | Target zoom scale (1 = original size, >1 zoom in, <1 zoom out) | number                                                          | -       | ✓        |\n| animation | Animation configuration                                        | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -       |          |\n| origin    | Zoom center point (viewport coordinates)                       | [Point](#point)                                                 | -       |          |\n\n**Example**\n\n```typescript\n// Zoom in to 2x\ngraph.zoomTo(2);\n\n// Zoom out to 0.5x with animation\ngraph.zoomTo(0.5, {\n  duration: 500,\n  easing: 'ease',\n});\n\n// Zoom in with the viewport center as the origin\ngraph.zoomTo(1.5, false, graph.getCanvasCenter());\n```\n\n### Graph.zoomBy(ratio, animation, origin)\n\nZoom based on the current zoom scale (relative zoom).\n\n```typescript\nzoomBy(ratio: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise<void>;\n```\n\n**Parameters**\n\n| Parameter | Description                              | Type                                                            | Default | Required |\n| --------- | ---------------------------------------- | --------------------------------------------------------------- | ------- | -------- |\n| ratio     | Zoom ratio (>1 zoom in, <1 zoom out)     | number                                                          | -       | ✓        |\n| animation | Animation configuration                  | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -       |          |\n| origin    | Zoom center point (viewport coordinates) | [Point](#point)                                                 | -       |          |\n\n**Example**\n\n```typescript\n// Zoom in by 1.2x based on the current scale\ngraph.zoomBy(1.2);\n\n// Zoom out to 0.8x based on the current scale with animation\ngraph.zoomBy(0.8, {\n  duration: 300,\n});\n```\n\n### Graph.translateTo(position, animation)\n\nPan the graph to a specified position (absolute pan).\n\n```typescript\ntranslateTo(position: Point, animation?: ViewportAnimationEffectTiming): Promise<void>;\n```\n\n**Parameters**\n\n| Parameter | Description                 | Type                                                            | Default | Required |\n| --------- | --------------------------- | --------------------------------------------------------------- | ------- | -------- |\n| position  | Target position coordinates | [Point](#point)                                                 | -       | ✓        |\n| animation | Animation configuration     | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -       |          |\n\n**Example**\n\n```typescript\n// Pan to a specified position\ngraph.translateTo([100, 100]);\n\n// Pan with animation\ngraph.translateTo([200, 200], {\n  duration: 1000,\n  easing: 'ease-in-out',\n});\n```\n\n### Graph.translateBy(offset, animation)\n\nPan the graph by a specified distance relative to the current position (relative pan).\n\n```typescript\ntranslateBy(offset: Point, animation?: ViewportAnimationEffectTiming): Promise<void>;\n```\n\n**Parameters**\n\n| Parameter | Description             | Type                                                            | Default | Required |\n| --------- | ----------------------- | --------------------------------------------------------------- | ------- | -------- |\n| offset    | Pan offset              | [Point](#point)                                                 | -       | ✓        |\n| animation | Animation configuration | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -       |          |\n\n**Example**\n\n```typescript\n// Pan right by 100 pixels and down by 50 pixels\ngraph.translateBy([100, 50]);\n\n// Relative pan with animation\ngraph.translateBy([-50, -50], {\n  duration: 500,\n});\n```\n\n### Graph.rotateTo(angle, animation, origin)\n\nRotate the canvas to a specified angle (absolute rotation).\n\n```typescript\nrotateTo(angle: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise<void>;\n```\n\n**Parameters**\n\n| Parameter | Description                                  | Type                                                            | Default | Required |\n| --------- | -------------------------------------------- | --------------------------------------------------------------- | ------- | -------- |\n| angle     | Target rotation angle (radians)              | number                                                          | -       | ✓        |\n| animation | Animation configuration                      | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -       |          |\n| origin    | Rotation center point (viewport coordinates) | [Point](#point)                                                 | -       |          |\n\n**Example**\n\n```typescript\n// Rotate to 45 degrees\ngraph.rotateTo(Math.PI / 4);\n\n// Rotate to 90 degrees with animation\ngraph.rotateTo(Math.PI / 2, {\n  duration: 1000,\n});\n```\n\n### Graph.rotateBy(angle, animation, origin)\n\nRotate based on the current angle (relative rotation).\n\n```typescript\nrotateBy(angle: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise<void>;\n```\n\n**Parameters**\n\n| Parameter | Description                                  | Type                                                            | Default | Required |\n| --------- | -------------------------------------------- | --------------------------------------------------------------- | ------- | -------- |\n| angle     | Rotation angle increment (radians)           | number                                                          | -       | ✓        |\n| animation | Animation configuration                      | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -       |          |\n| origin    | Rotation center point (viewport coordinates) | [Point](#point)                                                 | -       |          |\n\n**Example**\n\n```typescript\n// Rotate clockwise by 30 degrees relative to the current angle\ngraph.rotateBy(Math.PI / 6);\n\n// Relative rotation with animation\ngraph.rotateBy(-Math.PI / 4, {\n  duration: 500,\n  easing: 'ease-out',\n});\n```\n\n### Graph.fitView(options, animation)\n\nScale the graph to fit the appropriate size and pan to the center of the viewport.\n\n```typescript\nfitView(options?: FitViewOptions, animation?: ViewportAnimationEffectTiming): Promise<void>;\n```\n\n**Parameters**\n\n| Parameter | Description             | Type                                                            | Default | Required |\n| --------- | ----------------------- | --------------------------------------------------------------- | ------- | -------- |\n| options   | Fit options             | FitViewOptions                                                  | -       |          |\n| animation | Animation configuration | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -       |          |\n\n**FitViewOptions Type Description**\n\n| Property  | Type                   | Default    | Description                                       |\n| --------- | ---------------------- | ---------- | ------------------------------------------------- |\n| when      | 'overflow' \\| 'always' | 'overflow' | Fit timing: only when overflow or always          |\n| direction | 'x' \\| 'y' \\| 'both'   | 'both'     | Fit direction: x-axis, y-axis, or both directions |\n\n**Example**\n\n```typescript\n// Basic usage\ngraph.fitView();\n\n// Configure fit options\ngraph.fitView(\n  {\n    when: 'always', // Always fit\n    direction: 'both', // Fit in both directions\n  },\n  {\n    duration: 1000, // With animation\n  },\n);\n\n// Fit in the x direction only when content overflows\ngraph.fitView({\n  when: 'overflow',\n  direction: 'x',\n});\n```\n\n### Graph.fitCenter(animation)\n\nPan the graph to the center of the viewport.\n\n```typescript\nfitCenter(animation?: ViewportAnimationEffectTiming): Promise<void>;\n```\n\n**Parameters**\n\n| Parameter | Description             | Type                                                            | Default | Required |\n| --------- | ----------------------- | --------------------------------------------------------------- | ------- | -------- |\n| animation | Animation configuration | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -       |          |\n\n**Example**\n\n```typescript\n// Center the graph\ngraph.fitCenter();\n\n// Center with animation\ngraph.fitCenter({\n  duration: 500,\n  easing: 'ease-in',\n});\n```\n\n### Graph.getZoom()\n\nGet the current zoom scale.\n\n```typescript\ngetZoom(): number;\n```\n\n**Example**\n\n```typescript\nconst currentZoom = graph.getZoom();\nconsole.log('Current zoom scale:', currentZoom);\n```\n\n### Graph.getPosition()\n\nGet the position of the graph (position of the canvas origin in the viewport coordinate system).\n\n```typescript\ngetPosition(): Point;\n```\n\n**Example**\n\n```typescript\nconst position = graph.getPosition();\nconsole.log('Current position:', position);\n```\n\n### Graph.getRotation()\n\nGet the current rotation angle.\n\n```typescript\ngetRotation(): number;\n```\n\n**Example**\n\n```typescript\nconst rotation = graph.getRotation();\nconsole.log('Current rotation angle (radians):', rotation);\nconsole.log('Current rotation angle (degrees):', (rotation * 180) / Math.PI);\n```\n\n### Graph.getCanvasCenter()\n\nGet the viewport coordinates of the viewport center.\n\n```typescript\ngetCanvasCenter(): Point;\n```\n\n**Example**\n\n```typescript\nconst center = graph.getCanvasCenter();\nconsole.log('Viewport center coordinates:', center);\n```\n\n### Graph.getViewportCenter()\n\nGet the canvas coordinates of the viewport center.\n\n```typescript\ngetViewportCenter(): Point;\n```\n\n**Example**\n\n```typescript\nconst viewportCenter = graph.getViewportCenter();\nconsole.log('Canvas coordinates of the viewport center:', viewportCenter);\n```\n\n### Graph.setZoomRange(zoomRange)\n\nSet the zoom range of the current graph.\n\n```typescript\nsetZoomRange(zoomRange: [number, number]): void;\n```\n\n**Parameters**\n\n| Parameter | Description | Type                          | Default | Required |\n| --------- | ----------- | ----------------------------- | ------- | -------- |\n| zoomRange | Zoom range  | [number, number] \\| undefined | -       | ✓        |\n\n**Example**\n\n```typescript\n// Limit the zoom range between 0.5x and 2x\ngraph.setZoomRange([0.5, 2]);\n\n// Remove zoom restrictions\ngraph.setZoomRange(undefined);\n```\n\n### Graph.getZoomRange()\n\nGet the zoom range of the current graph.\n\n```typescript\ngetZoomRange(): GraphOptions['zoomRange'];\n```\n\n**Example**\n\n```typescript\nconst range = graph.getZoomRange();\nconsole.log('Current zoom range:', range);\n```\n\n### Graph.resize()\n\nResize the canvas to the size of the graph container.\n\n```typescript\nresize(): void;\n```\n\n### Graph.resize(width, height)\n\nResize the canvas to the specified width and height.\n\n```typescript\nresize(width: number, height: number): void;\n```\n\n**Parameters**\n\n| Parameter | Description   | Type   | Default | Required |\n| --------- | ------------- | ------ | ------- | -------- |\n| width     | Target width  | number | -       | ✓        |\n| height    | Target height | number | -       | ✓        |\n\n**Example**\n\n```typescript\n// Set the canvas size to 800x600\ngraph.resize(800, 600);\n```\n\n## Type Definitions\n\n### ViewportAnimationEffectTiming\n\nViewport animation configuration type.\n\n```typescript\ntype ViewportAnimationEffectTiming =\n  | boolean // Whether to enable animation\n  | {\n      easing?: string; // Easing function\n      duration?: number; // Animation duration (ms)\n    };\n```\n\n### Point\n\nCoordinate point type.\n\n```typescript\ntype Point = [number, number] | [number, number, number] | Float32Array;\n```\n\n### FitViewOptions\n\nView fit options.\n\n```typescript\ninterface FitViewOptions {\n  when?: 'overflow' | 'always'; // Fit timing\n  direction?: 'x' | 'y' | 'both'; // Fit direction\n}\n```\n"
  },
  {
    "path": "packages/site/docs/api/viewport.zh.md",
    "content": "---\ntitle: 视口操作\norder: 4\n---\n\n## 视口操作概述\n\nG6 提供了一系列视口操作 API，用于控制画布的缩放、平移和旋转。这些操作可以帮助用户更好地查看和交互图形内容。通过视口操作，你可以实现以下功能：\n\n- 缩放画布以查看细节或全局视图\n- 平移画布以查看不同区域\n- 旋转画布以获得不同视角\n- 自动适配内容到视口\n\n### 视口操作分类\n\nG6 的视口操作主要分为以下几类：\n\n1. **缩放操作**：如 `zoomTo`、`zoomBy`\n2. **平移操作**：如 `translateTo`、`translateBy`\n3. **旋转操作**：如 `rotateTo`、`rotateBy`\n4. **自适应操作**：如 `fitView`、`fitCenter`\n5. **视口信息获取**：如 `getZoom`、`getPosition`\n\n## API 参考\n\n### Graph.zoomTo(zoom, animation, origin)\n\n缩放画布至指定比例（绝对缩放）。\n\n```typescript\nzoomTo(zoom: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise<void>;\n```\n\n**参数**\n\n| 参数      | 描述                                          | 类型                                                            | 默认值 | 必选 |\n| --------- | --------------------------------------------- | --------------------------------------------------------------- | ------ | ---- |\n| zoom      | 目标缩放比例 (1 = 原始大小, >1 放大, <1 缩小) | number                                                          | -      | ✓    |\n| animation | 动画配置                                      | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -      |      |\n| origin    | 缩放中心点(视口坐标)                          | [Point](#point)                                                 | -      |      |\n\n**示例**\n\n```typescript\n// 放大到2倍\ngraph.zoomTo(2);\n\n// 带动画效果的缩小到0.5倍\ngraph.zoomTo(0.5, {\n  duration: 500,\n  easing: 'ease',\n});\n\n// 以视口中心点为原点放大\ngraph.zoomTo(1.5, false, graph.getCanvasCenter());\n```\n\n### Graph.zoomBy(ratio, animation, origin)\n\n基于当前缩放比例进行缩放（相对缩放）。\n\n```typescript\nzoomBy(ratio: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise<void>;\n```\n\n**参数**\n\n| 参数      | 描述                        | 类型                                                            | 默认值 | 必选 |\n| --------- | --------------------------- | --------------------------------------------------------------- | ------ | ---- |\n| ratio     | 缩放比例 (>1 放大, <1 缩小) | number                                                          | -      | ✓    |\n| animation | 动画配置                    | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -      |      |\n| origin    | 缩放中心点(视口坐标)        | [Point](#point)                                                 | -      |      |\n\n**示例**\n\n```typescript\n// 在当前比例基础上放大1.2倍\ngraph.zoomBy(1.2);\n\n// 在当前比例基础上缩小到0.8倍，带动画\ngraph.zoomBy(0.8, {\n  duration: 300,\n});\n```\n\n### Graph.translateTo(position, animation)\n\n将图平移至指定位置（绝对平移）。\n\n```typescript\ntranslateTo(position: Point, animation?: ViewportAnimationEffectTiming): Promise<void>;\n```\n\n**参数**\n\n| 参数      | 描述         | 类型                                                            | 默认值 | 必选 |\n| --------- | ------------ | --------------------------------------------------------------- | ------ | ---- |\n| position  | 目标位置坐标 | [Point](#point)                                                 | -      | ✓    |\n| animation | 动画配置     | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -      |      |\n\n**示例**\n\n```typescript\n// 平移到指定位置\ngraph.translateTo([100, 100]);\n\n// 带动画效果的平移\ngraph.translateTo([200, 200], {\n  duration: 1000,\n  easing: 'ease-in-out',\n});\n```\n\n### Graph.translateBy(offset, animation)\n\n将图相对当前位置平移指定距离（相对平移）。\n\n```typescript\ntranslateBy(offset: Point, animation?: ViewportAnimationEffectTiming): Promise<void>;\n```\n\n**参数**\n\n| 参数      | 描述       | 类型                                                            | 默认值 | 必选 |\n| --------- | ---------- | --------------------------------------------------------------- | ------ | ---- |\n| offset    | 平移偏移量 | [Point](#point)                                                 | -      | ✓    |\n| animation | 动画配置   | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -      |      |\n\n**示例**\n\n```typescript\n// 向右平移100像素，向下平移50像素\ngraph.translateBy([100, 50]);\n\n// 带动画效果的相对平移\ngraph.translateBy([-50, -50], {\n  duration: 500,\n});\n```\n\n### Graph.rotateTo(angle, animation, origin)\n\n旋转画布至指定角度（绝对旋转）。\n\n```typescript\nrotateTo(angle: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise<void>;\n```\n\n**参数**\n\n| 参数      | 描述                 | 类型                                                            | 默认值 | 必选 |\n| --------- | -------------------- | --------------------------------------------------------------- | ------ | ---- |\n| angle     | 目标旋转角度(弧度制) | number                                                          | -      | ✓    |\n| animation | 动画配置             | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -      |      |\n| origin    | 旋转中心点(视口坐标) | [Point](#point)                                                 | -      |      |\n\n**示例**\n\n```typescript\n// 旋转到45度\ngraph.rotateTo(Math.PI / 4);\n\n// 带动画效果的旋转到90度\ngraph.rotateTo(Math.PI / 2, {\n  duration: 1000,\n});\n```\n\n### Graph.rotateBy(angle, animation, origin)\n\n基于当前角度进行旋转（相对旋转）。\n\n```typescript\nrotateBy(angle: number, animation?: ViewportAnimationEffectTiming, origin?: Point): Promise<void>;\n```\n\n**参数**\n\n| 参数      | 描述                 | 类型                                                            | 默认值 | 必选 |\n| --------- | -------------------- | --------------------------------------------------------------- | ------ | ---- |\n| angle     | 旋转角度增量(弧度制) | number                                                          | -      | ✓    |\n| animation | 动画配置             | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -      |      |\n| origin    | 旋转中心点(视口坐标) | [Point](#point)                                                 | -      |      |\n\n**示例**\n\n```typescript\n// 相对当前角度顺时针旋转30度\ngraph.rotateBy(Math.PI / 6);\n\n// 带动画效果的相对旋转\ngraph.rotateBy(-Math.PI / 4, {\n  duration: 500,\n  easing: 'ease-out',\n});\n```\n\n### Graph.fitView(options, animation)\n\n将图缩放至合适大小并平移至视口中心。\n\n```typescript\nfitView(options?: FitViewOptions, animation?: ViewportAnimationEffectTiming): Promise<void>;\n```\n\n**参数**\n\n| 参数      | 描述     | 类型                                                            | 默认值 | 必选 |\n| --------- | -------- | --------------------------------------------------------------- | ------ | ---- |\n| options   | 适配选项 | FitViewOptions                                                  | -      |      |\n| animation | 动画配置 | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -      |      |\n\n**FitViewOptions 类型说明**\n\n| 属性      | 类型                   | 默认值     | 描述                         |\n| --------- | ---------------------- | ---------- | ---------------------------- |\n| when      | 'overflow' \\| 'always' | 'overflow' | 适配时机：仅溢出时或始终适配 |\n| direction | 'x' \\| 'y' \\| 'both'   | 'both'     | 适配方向：x轴、y轴或两个方向 |\n\n**示例**\n\n```typescript\n// 基本使用\ngraph.fitView();\n\n// 配置适配选项\ngraph.fitView(\n  {\n    when: 'always', // 始终进行适配\n    direction: 'both', // 在两个方向上适配\n  },\n  {\n    duration: 1000, // 带动画效果\n  },\n);\n\n// 仅在内容溢出时适配x方向\ngraph.fitView({\n  when: 'overflow',\n  direction: 'x',\n});\n```\n\n### Graph.fitCenter(animation)\n\n将图平移至视口中心。\n\n```typescript\nfitCenter(animation?: ViewportAnimationEffectTiming): Promise<void>;\n```\n\n**参数**\n\n| 参数      | 描述     | 类型                                                            | 默认值 | 必选 |\n| --------- | -------- | --------------------------------------------------------------- | ------ | ---- |\n| animation | 动画配置 | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | -      |      |\n\n**示例**\n\n```typescript\n// 居中显示\ngraph.fitCenter();\n\n// 带动画效果的居中\ngraph.fitCenter({\n  duration: 500,\n  easing: 'ease-in',\n});\n```\n\n### Graph.getZoom()\n\n获取当前缩放比例。\n\n```typescript\ngetZoom(): number;\n```\n\n**示例**\n\n```typescript\nconst currentZoom = graph.getZoom();\nconsole.log('当前缩放比例:', currentZoom);\n```\n\n### Graph.getPosition()\n\n获取图的位置（画布原点在视口坐标系下的位置）。\n\n```typescript\ngetPosition(): Point;\n```\n\n**示例**\n\n```typescript\nconst position = graph.getPosition();\nconsole.log('当前位置:', position);\n```\n\n### Graph.getRotation()\n\n获取当前旋转角度。\n\n```typescript\ngetRotation(): number;\n```\n\n**示例**\n\n```typescript\nconst rotation = graph.getRotation();\nconsole.log('当前旋转角度(弧度):', rotation);\nconsole.log('当前旋转角度(度):', (rotation * 180) / Math.PI);\n```\n\n### Graph.getCanvasCenter()\n\n获取视口中心的视口坐标。\n\n```typescript\ngetCanvasCenter(): Point;\n```\n\n**示例**\n\n```typescript\nconst center = graph.getCanvasCenter();\nconsole.log('视口中心坐标:', center);\n```\n\n### Graph.getViewportCenter()\n\n获取视口中心的画布坐标。\n\n```typescript\ngetViewportCenter(): Point;\n```\n\n**示例**\n\n```typescript\nconst viewportCenter = graph.getViewportCenter();\nconsole.log('视口中心的画布坐标:', viewportCenter);\n```\n\n### Graph.setZoomRange(zoomRange)\n\n设置当前图的缩放区间。\n\n```typescript\nsetZoomRange(zoomRange: [number, number]): void;\n```\n\n**参数**\n\n| 参数      | 描述     | 类型                          | 默认值 | 必选 |\n| --------- | -------- | ----------------------------- | ------ | ---- |\n| zoomRange | 缩放区间 | [number, number] \\| undefined | -      | ✓    |\n\n**示例**\n\n```typescript\n// 限制缩放范围在0.5到2倍之间\ngraph.setZoomRange([0.5, 2]);\n\n// 移除缩放限制\ngraph.setZoomRange(undefined);\n```\n\n### Graph.getZoomRange()\n\n获取当前图的缩放区间。\n\n```typescript\ngetZoomRange(): GraphOptions['zoomRange'];\n```\n\n**示例**\n\n```typescript\nconst range = graph.getZoomRange();\nconsole.log('当前缩放区间:', range);\n```\n\n### Graph.resize()\n\n调整画布大小为图容器大小。\n\n```typescript\nresize(): void;\n```\n\n### Graph.resize(width, height)\n\n调整画布大小为指定宽高。\n\n```typescript\nresize(width: number, height: number): void;\n```\n\n**参数**\n\n| 参数   | 描述     | 类型   | 默认值 | 必选 |\n| ------ | -------- | ------ | ------ | ---- |\n| width  | 目标宽度 | number | -      | ✓    |\n| height | 目标高度 | number | -      | ✓    |\n\n**示例**\n\n```typescript\n// 设置画布大小为800x600\ngraph.resize(800, 600);\n```\n\n## 类型定义\n\n### ViewportAnimationEffectTiming\n\n视口动画配置类型。\n\n```typescript\ntype ViewportAnimationEffectTiming =\n  | boolean // 是否启用动画\n  | {\n      easing?: string; // 缓动函数\n      duration?: number; // 动画持续时间(ms)\n    };\n```\n\n### Point\n\n坐标点类型。\n\n```typescript\ntype Point = [number, number] | [number, number, number] | Float32Array;\n```\n\n### FitViewOptions\n\n视图适配选项。\n\n```typescript\ninterface FitViewOptions {\n  when?: 'overflow' | 'always'; // 适配时机\n  direction?: 'x' | 'y' | 'both'; // 适配方向\n}\n```\n"
  },
  {
    "path": "packages/site/docs/backup/CameraSetting.en.md",
    "content": "---\ntitle: CameraSetting\n---\n\n## Options\n\n### <Badge type=\"success\">Required</Badge> type\n\n> _string_\n\nPlugin type\n\n### aspect\n\n> _number \\| 'auto'_\n\nCamera viewport aspect ratio, only valid in perspective camera.\n\n- number : Specific aspect ratio\n\n- `'auto'` : Automatically set to the aspect ratio of the canvas\n\n### azimuth\n\n> _number_\n\nAzimuth\n\n### cameraType\n\n> _'orbiting' \\| 'exploring' \\| 'tracking'_\n\nCamera type\n\n- `'orbiting'`: Fixed viewpoint, change camera position\n\n- `'exploring'`: Similar to orbiting, but allows the camera to rotate between the North Pole and the South Pole\n\n- `'tracking'`: Fixed camera position, change viewpoint\n\n### distance\n\n> _number_ **Default:** `500`\n\nThe distance from the camera to the target\n\n### elevation\n\n> _number_\n\nElevation\n\n### far\n\n> _number_\n\nThe position of the far plane\n\n### fov\n\n> _number_\n\nCamera field of view, only valid in perspective camera\n\n### maxDistance\n\n> _number_\n\nMaximum distance\n\n### minDistance\n\n> _number_\n\nMinimum distance\n\n### near\n\n> _number_\n\nThe position of the near plane\n\n### projectionMode\n\n> _'perspective' \\| 'orthographic'_\n\nProjection mode, perspective projection is only valid in 3D scenes\n\n- `'perspective'` : perspective projection\n\n- `'orthographic'` : Orthogonal projection\n\n### roll\n\n> _number_\n\nRoll\n\n## API\n"
  },
  {
    "path": "packages/site/docs/backup/CameraSetting.zh.md",
    "content": "---\ntitle: CameraSetting 相机设置\n---\n\n## 配置项\n\n### <Badge type=\"success\">Required</Badge> type\n\n> _`camera-setting` \\| string_\n\n⚠️ **注意**：\n\n- 相机设置插件必须在 3D 场景下使用\n- 此插件在使用前需要自行注册：\n\n```javascript\nimport { register, CameraSetting, ExtensionCategory } from '@antv/g6';\n\nregister(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting); // type: 'camera-setting'\n```\n\n### aspect\n\n> _number \\| `'auto'`_\n\n相机视口宽高比，仅在透视相机下有效\n\n- number : 具体的宽高比\n\n- `'auto'` : 自动设置为画布的宽高比\n\n### azimuth\n\n> _number_\n\n方位角\n\n### cameraType\n\n> _'orbiting' \\| 'exploring' \\| 'tracking'_\n\n相机类型\n\n- `'orbiting'`: 固定视点，改变相机位置\n- `'exploring'`: 类似 orbiting，但允许相机在北极和南极之间旋转\n- `'tracking'`: 固定相机位置，改变视点\n\n### distance\n\n> _number_ **Default:** `500`\n\n相机距离目标的距离\n\n### elevation\n\n> _number_\n\n仰角\n\n### far\n\n> _number_\n\n远平面位置\n\n### fov\n\n> _number_\n\n相机视角，仅在透视相机下有效\n\n### maxDistance\n\n> _number_\n\n最大视距\n\n### minDistance\n\n> _number_\n\n最小视距\n\n### near\n\n> _number_\n\n近平面位置\n\n### projectionMode\n\n> _'perspective' \\| 'orthographic'_\n\n投影模式，透视投影仅在 3D 场景下有效\n\n- `'perspective'` : 透视投影\n- `'orthographic'` : 正交投影\n\n### roll\n\n> _number_\n\n滚转角\n\n## API\n"
  },
  {
    "path": "packages/site/docs/manual/animation/animation.en.md",
    "content": "---\ntitle: Animation Overview\norder: 1\n---\n\n## Overview\n\n<image width=\"150px\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*NkILT56xXp4AAAAAAAAAAAAADmJ7AQ/original\" />\n\nAnimation refers to the state changes of elements over a period of time, such as the position, size, and color of nodes. In G6, animations are often used to enhance user experience and improve the coherence and smoothness of the graph update process.\n\nG6 provides a set of animation paradigms to describe element animations and has built-in some common animation effects. Users can achieve different animation effects by configuring animation parameters.\n\nThe implementation of an animation paradigm is as follows:\n\n```typescript\n[\n  {\n    fields: ['x', 'y'],\n  },\n];\n```\n\nThe aforementioned animation paradigm indicates that when the `x` and `y` attributes of an element change, an animation will be executed.\n\n## Configure Animation\n\nIn G6, animation configuration is divided into global configuration and local configuration. Global configuration is mainly used to set whether animations are enabled globally, the duration of animations, and other parameters. Local configuration is primarily used to set the animation effects for elements.\n\n### Disabled Global Animation\n\nTo disable global animations, you can pass the `animation` option when instantiating the `Graph`:\n\n```typescript\n{\n  animation: false,\n}\n```\n\n### Configure Global Animation\n\nIf you want to enable animations and also configure the default duration for the animations, you can pass the `animation` option:\n\n```typescript\n{\n  animation: {\n    duration: 500,\n  },\n}\n```\n\n### Configure Element Animation\n\nFor individual elements, you can configure animations at different stages. For example, if you want an element to have a fade-in and fade-out effect when it enters and exits, you can configure it as follows:\n\n```typescript\n{\n  node: {\n    animation: {\n      enter: 'fade',\n      exit: 'fade'\n    }\n  }\n}\n```\n\nIf you want to update the position of an element with a translation transition, you can configure it as follows:\n\n```typescript\n{\n  node: {\n    animation: {\n      update: 'translate',\n    }\n  }\n}\n```\n\nIf you wish to disable animations for an element, you can configure it as follows:\n\n```typescript\n{\n  node: {\n    animation: false,\n  }\n}\n```\n\n## Animation Paradigm\n\nThe animation configuration mentioned in the previous section actually used the built-in animation paradigm. This section introduces how to customize the animation paradigm.\n\n:::info{title=Tip}\nBefore writing an animation paradigm, it is necessary to understand the compositional structure of an element. For details, please refer to the [Element](/en/manual/element/overview) section.\n:::\n\nThe [Element](/en/manual/element/overview) section mentioned that elements in G6 are composed of one or more atomic graphics. Therefore, the animation of an element is essentially a combination of these atomic shape animations.\n\nThus, the animation paradigm is an array that describes the animation effects of each atomic shape within the element. For the element itself, it is also a special composite shape and thus has basic shape attributes such as `x`, `y`, etc.\n\nTherefore, you can directly write an animation paradigm for the element itself:\n\n```typescript\n[\n  {\n    fields: ['x', 'y'],\n  },\n];\n```\n\n## Custom Animation\n\nIf the built-in animations do not meet your requirements, you can create custom animations. For details, please refer to [Custom Animation](/en/manual/animation/custom-animation).\n\n## Animation Priority\n\nAnimation priority refers to the precedence between global animation configuration and element-specific animation configuration. It can be summarized as follows:\n\n| Global Animation Config | Local Animation Config | Whether to Execute Animation                                                                     |\n| ----------------------- | ---------------------- | ------------------------------------------------------------------------------------------------ |\n| ✅ true                 | ✅ true                | ✅ Execute animation with default configuration                                                  |\n| ✅ true                 | ❌ false               | ❌ Won't execute animation                                                                       |\n| ✅ true                 | ✅ Custom Animation    | ✅ Execute animation with local animation configuration                                          |\n| ❌ false                | ✅ true                | ❌ Won't execute animation                                                                       |\n| ❌ false                | ❌ false               | ❌ Won't execute animation                                                                       |\n| ❌ false                | ✅ Custom Animation    | ❌ Won't execute animation                                                                       |\n| ✅ Custom Animation     | ✅ true                | ✅ Execute animation with global animation configuration                                         |\n| ✅ Custom Animation     | ✅ Custom Animation    | ✅ Execute animation, local animation configuration overrides the global animation configuration |\n| ✅ Custom Animation     | ❌ false               | ❌ Won't execute animation                                                                       |\n\n## Persistent Animation\n\nIf you want elements to have persistent animations, such as the undulating effect of nodes or the ant line effect of edges, this can be achieved by customizing the elements. Below is an implementation of an edge with an Ant Line animation provided:\n\n```typescript\nimport { Line } from '@antv/g6';\n\nclass AntLine extends Line {\n  onCreate() {\n    this.shapeMap.key.animate([{ lineDashOffset: -20 }, { lineDashOffset: 0 }], {\n      duration: 500,\n      iterations: Infinity,\n    });\n  }\n}\n```\n\nThe `onCreate` is a lifecycle hook used to execute animations when an element is created.\n\nConfigure the edge style in the options as follows:\n\n```typescript\n{\n  edge: {\n    type: 'ant-line',\n    style:{\n      lineDash: [10, 10]\n    }\n  }\n}\n```\n\n<embed src=\"@/common/manual/core-concept/animation/ant-line.md\"></embed>\n\nThe `lineDash` is an array for `lineDashOffset`, and the AntLine effect is achieved by continuously varying the `lineDashOffset`.\n\nSimilarly, you can also create a breathing effect for nodes:\n\n```typescript\nimport { Circle } from '@antv/g6';\n\nclass BreathingCircle extends Circle {\n  onCreate() {}\n}\n```\n\nThe `lineDashOffset` is the offset for `lineDash`, and the AntLine effect is achieved by continuously varying the `lineDashOffset`.\n\nSimilarly, you can also create a breathing effect for nodes:\n\n```typescript\nimport { Circle } from '@antv/g6';\n\nclass BreathingCircle extends Circle {\n  onCreate() {\n    this.shapeMap.halo.animate([{ lineWidth: 5 }, { lineWidth: 10 }], {\n      duration: 1000,\n      iterations: Infinity,\n      direction: 'alternate',\n    });\n  }\n}\n```\n\nNode Style Configuration:\n\n```typescript\n{\n  node: {\n    type: 'breathing-circle',\n    style: {\n      halo: true,\n      haloLineWidth: 5,\n    },\n  },\n}\n```\n\n<embed src=\"@/common/manual/core-concept/animation/breathing-circle.md\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/animation/animation.zh.md",
    "content": "---\ntitle: 动画总览\norder: 1\n---\n\n## 概述\n\n<image width=\"150px\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*NkILT56xXp4AAAAAAAAAAAAADmJ7AQ/original\" />\n\n动画是指元素在一段时间内的状态变化，例如节点的位置、大小、颜色等。在 G6 中，动画通常用于增强用户体验，提升图更新过程的连贯性和流畅度。\n\nG6 提供了一套动画范式来描述元素动画，并内置了一些常用的动画效果，用户可以通过配置动画参数来实现不同的动画效果。\n\n一个动画范式的实现如下：\n\n```typescript\n[\n  {\n    fields: ['x', 'y'],\n  },\n];\n```\n\n上述动画范式表示当元素的 `x` 和 `y` 属性发生变化时，会执行动画。\n\n## 配置动画\n\nG6 中动画配置分为全局配置和局部配置，全局配置主要用于配置全局是否开启动画、动画时长等参数，局部配置主要用于配置元素的动画效果。\n\n### 关闭全局动画\n\n若要关闭全局动画，可以在实例化 `Graph` 时传入 `animation` 配置项：\n\n```typescript\n{\n  animation: false,\n}\n```\n\n### 配置全局动画\n\n如果要启用动画且同时配置动画的默认播放时长，可以传入 `animation` 配置项：\n\n```typescript\n{\n  animation: {\n    duration: 500,\n  },\n}\n```\n\n### 配置元素动画\n\n对于单个元素，可以配置其在不同阶段的动画。例如希望元素在进场和退场时具有淡入淡出效果，可以以如下方式配置：\n\n```typescript\n{\n  node: {\n    animation: {\n      enter: 'fade',\n      exit: 'fade'\n    }\n  }\n}\n```\n\n如果希望更新元素位置时是以平移过渡的方式，可以配置如下：\n\n```typescript\n{\n  node: {\n    animation: {\n      update: 'translate',\n    }\n  }\n}\n```\n\n如果希望关闭元素的动画，可以配置如下：\n\n```typescript\n{\n  node: {\n    animation: false,\n  }\n}\n```\n\n## 动画范式\n\n上一节中提到的动画配置实际上使用了内置的动画范式，本节介绍如何自定义动画范式。\n\n:::info{title=提示}\n在编写动画范式之前需要了解元素的组成结构，具体请参考[元素](/manual/element/overview)一节\n:::\n\n[元素](/manual/element/overview)一节中提到：G6 中的元素是由一个或多个原子图形组合而成。因此元素的动画本质上是这些原子图形动画的组合。\n\n因此动画范式是一个数组，用于描述元素中各原子图形的动画效果。对于元素本身来说，其也是一个特殊的复合图形，因此具有基本的图形属性，如 `x`、`y` 等。\n\n因此可以直接为元素本身编写动画范式：\n\n```typescript\n[\n  {\n    fields: ['x', 'y'],\n  },\n];\n```\n\n## 自定义动画\n\n如果内置动画无法满足需求，可以自定义动画，具体请参考[自定义动画](/manual/animation/custom-animation)。\n\n## 动画优先级\n\n动画优先级是指全局动画配置和元素动画配置之前的优先级，可以归纳如下：\n\n| 全局动画配置 | 局部动画配置 | 是否执行动画                                      |\n| ------------ | ------------ | ------------------------------------------------- |\n| ✅ true      | ✅ true      | ✅ 执行动画，以默认动画配置执行                   |\n| ✅ true      | ❌ false     | ❌ 不执行该类元素动画                             |\n| ✅ true      | ✅ 配置动画  | ✅ 执行动画，以局部动画配置执行                   |\n| ❌ false     | ✅ true      | ❌ 不执行任何动画                                 |\n| ❌ false     | ❌ false     | ❌ 不执行任何动画                                 |\n| ❌ false     | ✅ 配置动画  | ❌ 不执行任何动画                                 |\n| ✅ 配置动画  | ✅ true      | ✅ 执行动画，以全局动画配置执行                   |\n| ✅ 配置动画  | ✅ 配置动画  | ✅ 执行动画，将局部动画配置覆盖全局动画配置后执行 |\n| ✅ 配置动画  | ❌ false     | ❌ 不执行该类元素动画                             |\n\n## 持续动画\n\n如果希望元素具有持续动画，例如节点的波动效果、边的蚂蚁线效果等，可以通过自定义元素方式实现，下面提供一个具有蚂蚁线(Ant Line)动画的边的实现：\n\n```typescript\nimport { Line } from '@antv/g6';\n\nclass AntLine extends Line {\n  onCreate() {\n    this.shapeMap.key.animate([{ lineDashOffset: -20 }, { lineDashOffset: 0 }], {\n      duration: 500,\n      iterations: Infinity,\n    });\n  }\n}\n```\n\n其中 `onCreate` 是一个生命周期钩子，用于在元素创建时执行动画。\n\n在 options 中配置边样式：\n\n```typescript\n{\n  edge: {\n    type: 'ant-line',\n    style:{\n      lineDash: [10, 10]\n    }\n  }\n}\n```\n\n<embed src=\"@/common/manual/core-concept/animation/ant-line.md\"></embed>\n\n其中 `lineDash` 是 `lineDashOffset` 的数组，通过不断变化 `lineDashOffset` 来实现飞线效果。\n\n同样的，还可以实现节点的呼吸效果：\n\n```typescript\nimport { Circle } from '@antv/g6';\n\nclass BreathingCircle extends Circle {\n  onCreate() {}\n}\n```\n\n其中 `lineDashOffset` 是 `lineDash` 的偏移量，通过不断变化 `lineDashOffset` 来实现飞线效果。\n\n同样的，还可以实现节点的呼吸效果：\n\n```typescript\nimport { Circle } from '@antv/g6';\n\nclass BreathingCircle extends Circle {\n  onCreate() {\n    this.shapeMap.halo.animate([{ lineWidth: 5 }, { lineWidth: 10 }], {\n      duration: 1000,\n      iterations: Infinity,\n      direction: 'alternate',\n    });\n  }\n}\n```\n\n节点样式配置：\n\n```typescript\n{\n  node: {\n    type: 'breathing-circle',\n    style: {\n      halo: true,\n      haloLineWidth: 5,\n    },\n  },\n}\n```\n\n<embed src=\"@/common/manual/core-concept/animation/breathing-circle.md\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/animation/custom-animation.en.md",
    "content": "---\ntitle: Custom Animation\norder: 2\n---\n\n## Overview\n\n## Implement Animation\n\nFor circular node (Circle) elements, the main shape is a circle. Now, let's create an animation for it so that when the size of the node changes, it transitions with a scaling animation:\n\n```typescript\n[\n  {\n    fields: ['r'],\n    shape: 'key',\n  },\n];\n```\n\nNow let's create a graph instance and update the element size to trigger the update animation:\n\n```typescript\nconst graph = new Graph({\n  container: 'container',\n  width: 50,\n  height: 50,\n  data: {\n    nodes: [{ id: 'node-1', style: { x: 25, y: 25, size: 20 } }],\n  },\n  node: {\n    animation: {\n      update: [{ fields: ['r'], shape: 'key' }],\n    },\n  },\n});\n\ngraph.draw().then(() => {\n  graph.updateNodeData([{ id: 'node-1', style: { size: 40 } }]);\n  graph.draw();\n});\n```\n\n> ⬇️ Move the pointer to the graph below and click the play button on the left to replay\n\n<embed src=\"@/common/manual/custom-extension/animation/implement-animation.md\"></embed>\n\n#### Principle Analysis\n\nWhen animating an element, the element converts its animation frame parameters into animation frame parameters for its individual sub-graphics and executes the corresponding animations.\n\nIn the example above, by updating the node size, an animation was performed on the node, and its animation frame parameters were:\n\n```json\n[{ \"size\": 20 }, { \"size\": 40 }]\n```\n\nAfter obtaining the attribute, the node element converts it into animation frame parameters for the main shape (circle):\n\n```json\n[{ \"r\": 10 }, { \"r\": 20 }]\n```\n\nTherefore, what is ultimately happening here is that a transition animation is being performed on the circle, changing its radius from 10 to 20.\n\n#### Composite Animation\n\nBy directly combining the position change animation with the size change animation into a single animation paradigm, you can obtain a composite animation paradigm:\n\n```typescript\n[\n  {\n    fields: ['x', 'y'],\n  },\n  {\n    fields: ['r'],\n    shape: 'key',\n  },\n];\n```\n\nAnd update both the position and size of the node simultaneously:\n\n```typescript\ngraph.updateNodeData([{ id: 'node-1', style: { x: 175, size: 40 } }]);\ngraph.draw();\n```\n\n> ⬇️ Move the pointer to the graph below and click the play button on the left to replay\n\n<embed src=\"@/common/manual/custom-extension/animation/composite-animation-1.md\"></embed>\n\nAdd color transition:\n\n```typescript\n[\n  {\n    fields: ['x', 'y'],\n  },\n  {\n    fields: ['r', 'fill'],\n    shape: 'key',\n  },\n];\n```\n\nExecute node update:\n\n```typescript\ngraph.updateNodeData([{ id: 'node-1', style: { x: 175, size: 40, fill: 'pink' } }]);\ngraph.draw();\n```\n\n> ⬇️ Move the pointer to the graph below and click the play button on the left to replay\n\n<embed src=\"@/common/manual/custom-extension/animation/composite-animation-2.md\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/animation/custom-animation.zh.md",
    "content": "---\ntitle: 自定义动画\norder: 2\n---\n\n## 概述\n\n## 实现动画\n\n对于圆形节点（Circle）元素，其主图形是一个圆形，现在为其编写一个动画，当节点的尺寸发生变化时，能够以缩放的方式进行过渡动画：\n\n```typescript\n[\n  {\n    fields: ['r'],\n    shape: 'key',\n  },\n];\n```\n\n下面我们创建一个图实例并更新元素尺寸来触发更新动画：\n\n```typescript\nconst graph = new Graph({\n  container: 'container',\n  width: 50,\n  height: 50,\n  data: {\n    nodes: [{ id: 'node-1', style: { x: 25, y: 25, size: 20 } }],\n  },\n  node: {\n    animation: {\n      update: [{ fields: ['r'], shape: 'key' }],\n    },\n  },\n});\n\ngraph.draw().then(() => {\n  graph.updateNodeData([{ id: 'node-1', style: { size: 40 } }]);\n  graph.draw();\n});\n```\n\n> ⬇️ 指针移动至下方图中，并点击左侧播放按钮进行重新播放\n\n<embed src=\"@/common/manual/custom-extension/animation/implement-animation.md\"></embed>\n\n#### 原理分析\n\n当对一个元素执行动画时，该元素会将其动画帧参数转化为其各个子图形上的动画帧参数，并执行对应的动画。\n\n在上面的例子中，通过更新节点尺寸(size)，对该节点执行了动画，其动画帧参数为：\n\n```json\n[{ \"size\": 20 }, { \"size\": 40 }]\n```\n\n节点元素拿到该属性后，将其转化为主图形（圆形）的动画帧参数：\n\n```json\n[{ \"r\": 10 }, { \"r\": 20 }]\n```\n\n因此这里最终是对圆形执行了半径从 10 到 20 的过渡动画。\n\n#### 复合动画\n\n直接将位置变化动画和尺寸变化动画合并到一个动画范式即可得到复合动画范式：\n\n```typescript\n[\n  {\n    fields: ['x', 'y'],\n  },\n  {\n    fields: ['r'],\n    shape: 'key',\n  },\n];\n```\n\n并同时更新该节点的位置和尺寸：\n\n```typescript\ngraph.updateNodeData([{ id: 'node-1', style: { x: 175, size: 40 } }]);\ngraph.draw();\n```\n\n> ⬇️ 指针移动至下方图中，并点击左侧播放按钮进行重新播放\n\n<embed src=\"@/common/manual/custom-extension/animation/composite-animation-1.md\"></embed>\n\n加入颜色过渡：\n\n```typescript\n[\n  {\n    fields: ['x', 'y'],\n  },\n  {\n    fields: ['r', 'fill'],\n    shape: 'key',\n  },\n];\n```\n\n执行节点更新：\n\n```typescript\ngraph.updateNodeData([{ id: 'node-1', style: { x: 175, size: 40, fill: 'pink' } }]);\ngraph.draw();\n```\n\n> ⬇️ 指针移动至下方图中，并点击左侧播放按钮进行重新播放\n\n<embed src=\"@/common/manual/custom-extension/animation/composite-animation-2.md\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/AutoAdaptLabel.en.md",
    "content": "---\ntitle: AutoAdaptLabel\norder: 1\n---\n\n## Overview\n\nAuto-adapt label display is a dynamic label management strategy designed to intelligently adjust which labels should be displayed or hidden based on factors such as spatial allocation of the current visible range and node importance. By analyzing the visible area in real-time, it ensures that users receive the most relevant and clear information display in different interaction scenarios, while avoiding visual overload and information redundancy.\n\n## Usage Scenarios\n\nThis interaction is mainly used for:\n\n- Node size changes\n- Graph scaling\n\n## Online Experience\n\n<embed src=\"@/common/api/behaviors/auto-adapt-label.md\"></embed>\n\n## Basic Usage\n\nAdd this interaction in the graph configuration\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form. This method is simple but only supports default configuration and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['auto-adapt-label'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and can dynamically update the configuration at runtime:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'auto-adapt-label',\n      throttle: 200, // Throttle time\n      padding: 10, // Extra spacing when detecting overlap\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option    | Description                                                                                                                                                                                                                                                                                      | Type                                                                                                                              | Default            | Required |\n| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -------- |\n| type      | Interaction type name                                                                                                                                                                                                                                                                            | string                                                                                                                            | `auto-adapt-label` | √        |\n| enable    | Whether to enable this interaction                                                                                                                                                                                                                                                               | boolean \\| ((event: [Event](/en/api/event#event-object-properties)) => boolean)                                                   | true               |          |\n| throttle  | Label update throttle time (ms)                                                                                                                                                                                                                                                                  | number                                                                                                                            | 100                |          |\n| padding   | Extra spacing when detecting label overlap                                                                                                                                                                                                                                                       | number \\| number[]                                                                                                                | 0                  |          |\n| sort      | Custom sorting function, sorting elements from high to low importance, with higher importance elements having higher label display priority. Generally, combo > node > edge                                                                                                                      | (a: ElementDatum, b: ElementDatum) => -1 \\| 0 \\| 1                                                                                |                    |          |\n| sortNode  | Sort nodes from high to low importance, with higher importance nodes having higher label display priority. Several built-in [centrality algorithms](#nodecentralityoptions) are available, or a custom sorting function can be used. Note that if `sort` is set, `sortNode` will not take effect | [NodeCentralityOptions](#nodecentralityoptions) \\| (nodeA: [NodeData](/en/manual/data#nodedata), nodeB: NodeData => -1 \\| 0 \\| 1) | `type: 'degree'`   |          |\n| sortEdge  | Sort edges from high to low importance, with higher importance edges having higher label display priority. By default, it is sorted according to the order of data. Note that if `sort` is set, `sortEdge` will not take effect                                                                  | (edgeA: [EdgeData](/en/manual/data#edgedata), edgeB: EdgeData) => -1 \\| 0 \\| 1                                                    |                    |          |\n| sortCombo | Sort groups from high to low importance, with higher importance groups having higher label display priority. By default, it is sorted according to the order of data. Note that if `sort` is set, `sortCombo` will not take effect                                                               | (comboA: [ComboData](/en/manual/data#combodata), comboB: ComboData) => -1 \\| 0 \\| 1                                               |                    |          |\n\n### NodeCentralityOptions\n\nMethods for measuring node centrality\n\n- `'degree'`: Degree centrality, measured by the degree of the node (number of connected edges). Nodes with high degree centrality usually have more direct connections and may play important roles in the network\n- `'betweenness'`: Betweenness centrality, measured by the number of times a node appears in all shortest paths. Nodes with high betweenness centrality usually act as bridges in the network, controlling the flow of information\n- `'closeness'`: Closeness centrality, measured by the reciprocal of the sum of the shortest path lengths from the node to all other nodes. Nodes with high closeness centrality can usually reach other nodes in the network more quickly\n- `'eigenvector'`: Eigenvector centrality, measured by the degree of connection of the node to other central nodes. Nodes with high eigenvector centrality are usually connected to other important nodes\n- `'pagerank'`: PageRank centrality, measured by the number of times a node is referenced by other nodes, commonly used in directed graphs. Nodes with high PageRank centrality usually have high influence in the network, similar to webpage ranking algorithms\n\n```typescript\ntype NodeCentralityOptions =\n  | { type: 'degree'; direction?: 'in' | 'out' | 'both' }\n  | { type: 'betweenness'; directed?: boolean; weightPropertyName?: string }\n  | { type: 'closeness'; directed?: boolean; weightPropertyName?: string }\n  | { type: 'eigenvector'; directed?: boolean }\n  | { type: 'pagerank'; epsilon?: number; linkProb?: number };\n```\n\n## Practical Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 200, y: 100, labelText: '短标签' } },\n    { id: 'node2', style: { x: 360, y: 100, labelText: '中等长度的标签' } },\n    { id: 'node3', style: { x: 280, y: 220, labelText: '这是一个非常非常长的标签，需要自适应显示' } },\n  ],\n  edges: [\n    { source: 'node1', target: 'node2' },\n    { source: 'node1', target: 'node3' },\n    { source: 'node2', target: 'node3' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  behaviors: [\n    'zoom-canvas',\n    'drag-canvas',\n    {\n      key: 'auto-adapt-label',\n      type: 'auto-adapt-label',\n      padding: 0,\n      throttle: 200,\n    },\n  ],\n  plugins: [{ type: 'grid-line', size: 30 }],\n  animation: true,\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/AutoAdaptLabel.zh.md",
    "content": "---\ntitle: 标签自适应显示 AutoAdaptLabel\norder: 1\n---\n\n## 概述\n\n标签自适应显示是一种动态标签管理策略，旨在根据当前可视范围的空间分配、节点重要性等因素，智能调整哪些标签应显示或隐藏。通过对可视区域的实时分析，确保用户在不同的交互场景下获得最相关最清晰的信息展示，同时避免视觉过载和信息冗余。\n\n## 使用场景\n\n这一交互主要用于：\n\n- 节点尺寸变化\n- 图形缩放\n\n## 在线体验\n\n<embed src=\"@/common/api/behaviors/auto-adapt-label.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一交互\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['auto-adapt-label'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'auto-adapt-label',\n      throttle: 200, // 节流时间\n      padding: 10, // 检测重叠时的额外间距\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项    | 说明                                                                                                                                                                                        | 类型                                                                                                                                   | 默认值             | 必选 |\n| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ---- |\n| type      | 交互类型名称                                                                                                                                                                                | string                                                                                                                                 | `auto-adapt-label` | √    |\n| enable    | 是否启用该交互                                                                                                                                                                              | boolean \\| ((event: [Event](/api/event#事件对象属性)) => boolean)                                                                      | true               |      |\n| throttle  | 标签更新节流时间（ms）                                                                                                                                                                      | number                                                                                                                                 | 100                |      |\n| padding   | 标签检测重叠时的额外间距                                                                                                                                                                    | number \\| number[]                                                                                                                     | 0                  |      |\n| sort      | 自定义排序函数，根据元素的重要性从高到低排序，重要性越高的元素其标签显示优先级越高。一般情况下 combo > node > edge                                                                          | (a: ElementDatum, b: ElementDatum) => -1 \\| 0 \\| 1                                                                                     |                    |      |\n| sortNode  | 根据节点的重要性从高到低排序，重要性越高的节点其标签显示优先级越高。内置几种[中心性算法](#nodecentralityoptions)，也可以自定义排序函数。需要注意，如果设置了 `sort`，则 `sortNode` 不会生效 | [NodeCentralityOptions](#nodecentralityoptions) \\| (nodeA: [NodeData](/manual/data#节点数据nodedata), nodeB: NodeData => -1 \\| 0 \\| 1) | `type: 'degree'`   |      |\n| sortEdge  | 根据边的重要性从高到低排序，重要性越高的边其标签显示优先级越高。默认按照数据先后进行排序。需要注意，如果设置了 `sort`，则 `sortEdge` 不会生效                                               | (edgeA: [EdgeData](/manual/data#边数据edgedata), edgeB: EdgeData) => -1 \\| 0 \\| 1                                                      |                    |      |\n| sortCombo | 根据群组的重要性从高到低排序，重要性越高的群组其标签显示优先级越高。默认按照数据先后进行排序。需要注意，如果设置了 `sort`，则 `sortCombo` 不会生效                                          | (comboA: [ComboData](/manual/data#组合数据combodata), comboB: ComboData) => -1 \\| 0 \\| 1                                               |                    |      |\n\n### NodeCentralityOptions\n\n节点中心性的度量方法\n\n- `'degree'`：度中心性，通过节点的度数（连接的边的数量）来衡量其重要性。度中心性高的节点通常具有较多的直接连接，在网络中可能扮演着重要的角色\n- `'betweenness'`：介数中心性，通过节点在所有最短路径中出现的次数来衡量其重要性。介数中心性高的节点通常在网络中起到桥梁作用，控制着信息的流动\n- `'closeness'`：接近中心性，通过节点到其他所有节点的最短路径长度总和的倒数来衡量其重要性。接近中心性高的节点通常能够更快地到达网络中的其他节点\n- `'eigenvector'`：特征向量中心性，通过节点与其他中心节点的连接程度来衡量其重要性。特征向量中心性高的节点通常连接着其他重要节点\n- `'pagerank'`：PageRank 中心性，通过节点被其他节点引用的次数来衡量其重要性，常用于有向图。PageRank 中心性高的节点通常在网络中具有较高的影响力，类似于网页排名算法\n\n```typescript\ntype NodeCentralityOptions =\n  | { type: 'degree'; direction?: 'in' | 'out' | 'both' }\n  | { type: 'betweenness'; directed?: boolean; weightPropertyName?: string }\n  | { type: 'closeness'; directed?: boolean; weightPropertyName?: string }\n  | { type: 'eigenvector'; directed?: boolean }\n  | { type: 'pagerank'; epsilon?: number; linkProb?: number };\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 200, y: 100, labelText: '短标签' } },\n    { id: 'node2', style: { x: 360, y: 100, labelText: '中等长度的标签' } },\n    { id: 'node3', style: { x: 280, y: 220, labelText: '这是一个非常非常长的标签，需要自适应显示' } },\n  ],\n  edges: [\n    { source: 'node1', target: 'node2' },\n    { source: 'node1', target: 'node3' },\n    { source: 'node2', target: 'node3' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  behaviors: [\n    'zoom-canvas',\n    'drag-canvas',\n    {\n      key: 'auto-adapt-label',\n      type: 'auto-adapt-label',\n      padding: 0,\n      throttle: 200,\n    },\n  ],\n  plugins: [{ type: 'grid-line', size: 30 }],\n  animation: true,\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/BrushSelect.en.md",
    "content": "---\ntitle: BrushSelect\norder: 2\n---\n\n## Overview\n\nBrush select allows users to click and drag a box to enclose elements, selecting the elements within the box.\n\n## Usage Scenarios\n\nThis interaction is mainly used for:\n\n- Quickly selecting a batch of elements\n- Quickly deselecting a batch of elements\n\n## Online Experience\n\n<embed src=\"@/common/api/behaviors/brush-select.md\"></embed>\n\n## Basic Usage\n\nAdd this interaction in the graph configuration:\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form. This method is simple but only supports default configuration and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['brush-select'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and can dynamically update the configuration at runtime:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'brush-select',\n      key: 'brush-select-1',\n      immediately: true, // Elements are immediately selected as the box encloses them\n      trigger: ['shift', 'alt', 'control'], // Use multiple keys for selection\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option         | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | Type                                                                             | Default                   | Required |\n| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------- | -------- |\n| type           | Interaction type name. This plugin is built-in, and you can use it with `type: 'brush-select'`.                                                                                                                                                                                                                                                                                                                                                                                                                              | `brush-select` \\| string                                                         | `brush-select`            | ✓        |\n| animation      | Whether to enable animation                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | boolean                                                                          | false                     |          |\n| enable         | Whether to enable brush select functionality                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | boolean \\| ((event: [Event](/en/api/event#event-object-properties)) => boolean)  | true                      |          |\n| enableElements | Types of elements that can be selected                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | ( `node` \\| `edge` \\| `combo` )[]                                                | [`node`, `combo`, `edge`] |          |\n| immediately    | Whether to select immediately, only effective when [selection mode](#mode) is `default`, [example](#immediately)                                                                                                                                                                                                                                                                                                                                                                                                             | boolean                                                                          | false                     |          |\n| mode           | Selection mode, [example](#mode)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `union` \\| `intersect` \\| `diff` \\| `default`                                    | `default`                 |          |\n| onSelect       | Callback for selected element state                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | (states:Record&lt;string,string\\|string[]>) =>Record&lt;string,string\\|string[]> |                           |          |\n| state          | Switch to this state when selected                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | string \\| `selected` \\| `active` \\| `inactive` \\| `disabled` \\| `highlight`      | `selected`                |          |\n| style          | Specify the style of the selection box, [configuration options](#style)                                                                                                                                                                                                                                                                                                                                                                                                                                                      |                                                                                  | See below                 |          |\n| trigger        | Press this shortcut key in combination with a mouse click to perform selection **Key reference:** _<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\" target=\"_blank\" rel=\"noopener noreferrer\">MDN Key Values</a>_. If set to an **empty array**, it means selection can be performed with a mouse click without pressing other keys <br/> ⚠️ Note, setting `trigger` to `['drag']` will cause the `drag-canvas` behavior to fail. The two cannot be configured simultaneously. | string[] \\| (`Control` \\| `Shift`\\| `Alt` \\| `......`)[]                         | [`shift`]                 |          |\n\n### immediately\n\nWhether to select immediately, only effective when selection mode is `default`\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'brush-select',\n      key: 'brush-select',\n      immediately: true, // Elements are immediately selected as the box encloses them\n      trigger: [], // No need to press other keys, just click and drag the mouse to select\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 100, y: 50 } },\n      { id: 'node-2', style: { x: 260, y: 50 } },\n      { id: 'node-3', style: { x: 280, y: 100 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-1', target: 'node-3' },\n      { source: 'node-2', target: 'node-3' },\n    ],\n  },\n  node: {\n    style: { fill: '#7e3feb' },\n  },\n  edge: {\n    stroke: '#8b9baf',\n  },\n  behaviors: [\n    {\n      type: 'brush-select',\n      key: 'brush-select',\n      immediately: true, // Immediate selection\n      trigger: [],\n    },\n  ],\n  plugins: [{ type: 'grid-line', size: 30 }],\n});\n\ngraph.render();\n```\n\n### mode\n\nFour selection modes are built-in:\n\n- `union`: Retain the current state of selected elements and add the specified state.\n- `intersect`: If the selected elements already have the specified state, retain it; otherwise, clear the state.\n- `diff`: Invert the specified state of the selected elements.\n- `default`: Clear the current state of selected elements and add the specified state.\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'brush-select',\n      key: 'brush-select',\n      mode: 'default', // Selection mode, default selection mode\n    },\n  ],\n});\n```\n\n```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 200, y: 100 } },\n        { id: 'node-2', style: { x: 360, y: 100 } },\n        { id: 'node-3', style: { x: 280, y: 220 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-1', target: 'node-3' },\n        { source: 'node-2', target: 'node-3' },\n      ],\n    },\n    node: {\n      style: { fill: '#7e3feb' },\n      state: {\n        custom: { fill: '#ffa940' },\n      },\n    },\n    edge: {\n      stroke: '#8b9baf',\n      state: {\n        custom: { stroke: '#ffa940' },\n      },\n    },\n    behaviors: [\n      {\n        type: 'brush-select',\n        key: 'brush-select',\n        trigger: [],\n        immediately: true,\n      },\n    ],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      key: 'brush-select',\n      type: 'brush-select',\n      animation: false,\n      enable: true,\n      enableElements: ['node', 'edge', 'combo'],\n      mode: 'default',\n      state: 'selected',\n    };\n    const optionFolder = gui.addFolder('BrushSelect Options');\n    optionFolder.add(options, 'type').disable(true);\n\n    optionFolder.add(options, 'state', ['active', 'selected', 'custom']);\n    optionFolder.add(options, 'mode', ['union', 'intersect', 'diff', 'default']);\n    // .onChange((e) => {\n    //   immediately.show(e === 'default');\n    // });\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'brush-select',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n\n### style\n\n| Attribute         | Description               | Type                                     | Default   |\n| ----------------- | ------------------------- | ---------------------------------------- | --------- |\n| cursor            | Mouse style               | string                                   |           |\n| fill              | Fill color                | string \\| Pattern \\| null                | `#1677FF` |\n| fillOpacity       | Fill opacity              | number \\| string                         | 0.1       |\n| isBillboard       | Enable billboard mode     | boolean                                  |           |\n| isSizeAttenuation | Enable size attenuation   | boolean                                  |           |\n| lineCap           | Line end style            | `butt` \\| `round` \\| `square`            |           |\n| lineDash          | Dash configuration        | number \\| string \\| (string \\| number)[] |           |\n| lineDashOffset    | Dash offset               | number                                   |           |\n| lineJoin          | Line join style           | `miter` \\| `round` \\| `bevel`            |           |\n| lineWidth         | Line width                | number \\| string                         | 1         |\n| opacity           | Overall opacity           | number \\| string                         |           |\n| radius            | Rectangle corner radius   | number \\| string \\| number[]             |           |\n| shadowBlur        | Shadow blur degree        | number                                   |           |\n| shadowColor       | Shadow color              | string                                   |           |\n| shadowOffsetX     | Shadow X direction offset | number                                   |           |\n| shadowOffsetY     | Shadow Y direction offset | number                                   |           |\n| stroke            | Stroke color              | string \\| Pattern \\| null                | `#1677FF` |\n| strokeOpacity     | Stroke opacity            | number \\| string                         |           |\n| visibility        | Visibility                | `visible` \\| `hidden`                    |           |\n| zIndex            | Rendering level           | number                                   | 2         |\n\n**Example**：\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'brush-select',\n      key: 'brush-select',\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2], // Dashed outline\n        // RGB super colorful box\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 200, y: 100 } },\n      { id: 'node-2', style: { x: 360, y: 100 } },\n      { id: 'node-3', style: { x: 280, y: 220 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-1', target: 'node-3' },\n      { source: 'node-2', target: 'node-3' },\n    ],\n  },\n  node: {\n    style: { fill: '#7e3feb' },\n  },\n  edge: {\n    stroke: '#8b9baf',\n  },\n  behaviors: [\n    {\n      type: 'brush-select',\n      key: 'brush-select',\n      trigger: [],\n      immediately: true,\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2], // Dashed outline\n        // RGB super colorful box\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n  plugins: [{ type: 'grid-line', size: 30 }],\n  animation: true,\n});\n\ngraph.render();\n```\n\n### Practical Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 200, y: 250 } },\n      { id: 'node-2', style: { x: 250, y: 200 } },\n      { id: 'node-3', style: { x: 300, y: 250 } },\n      { id: 'node-4', style: { x: 250, y: 300 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-2', target: 'node-3' },\n      { source: 'node-3', target: 'node-4' },\n      { source: 'node-4', target: 'node-1' },\n    ],\n  },\n  behaviors: [\n    {\n      key: 'brush-select',\n      type: 'brush-select',\n      enable: true,\n      animation: false,\n      mode: 'default', // union intersect diff default\n      state: 'selected', // 'active', 'selected', 'inactive', ...\n      trigger: [], // ['Shift', 'Alt', 'Control', 'Drag', 'Meta', ...]\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2],\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/BrushSelect.zh.md",
    "content": "---\ntitle: 框选 BrushSelect\norder: 2\n---\n\n## 概述\n\n鼠标点击拖一个框笼罩元素，框选范围内的元素会被选中。\n\n## 使用场景\n\n这一交互主要用于：\n\n- 快速选中一批元素\n- 快速取消选中一批元素\n\n## 在线体验\n\n<embed src=\"@/common/api/behaviors/brush-select.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一交互：\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['brush-select'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'brush-select',\n      key: 'brush-select-1',\n      immediately: true, // 可以看到框框笼罩过去时，元素立即被框选了\n      trigger: ['shift', 'alt', 'control'], // 配合多种按键进行框选\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项         | 说明                                                                                                                                                                                                                                                                                                                                                                            | 类型                                                                             | 默认值                    | 必选 |\n| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------- | ---- |\n| type           | 交互类型名称。此插件已内置，你可以通过 `type: 'brush-select'` 来使用它。                                                                                                                                                                                                                                                                                                        | `brush-select` \\| string                                                         | `brush-select`            | ✓    |\n| animation      | 是否启用动画                                                                                                                                                                                                                                                                                                                                                                    | boolean                                                                          | false                     |      |\n| enable         | 是否启用框选功能                                                                                                                                                                                                                                                                                                                                                                | boolean \\| ((event: [Event](/api/event#事件对象属性)) => boolean)                | true                      |      |\n| enableElements | 可框选的元素类型                                                                                                                                                                                                                                                                                                                                                                | ( `node` \\| `edge` \\| `combo` )[]                                                | [`node`, `combo`, `edge`] |      |\n| immediately    | 是否及时框选, 仅在 [框选模式 mode](#mode) 为 `default` 时生效，[示例](#immediately)                                                                                                                                                                                                                                                                                             | boolean                                                                          | false                     |      |\n| mode           | 框选的选择模式，[示例](#mode)                                                                                                                                                                                                                                                                                                                                                   | `union` \\| `intersect` \\| `diff` \\| `default`                                    | `default`                 |      |\n| onSelect       | 框选元素状态回调                                                                                                                                                                                                                                                                                                                                                                | (states:Record&lt;string,string\\|string[]>) =>Record&lt;string,string\\|string[]> |                           |      |\n| state          | 被选中时切换到该状态                                                                                                                                                                                                                                                                                                                                                            | string \\| `selected` \\| `active` \\| `inactive` \\| `disabled` \\| `highlight`      | `selected`                |      |\n| style          | 指定框选时的框样式，[配置项](#style)                                                                                                                                                                                                                                                                                                                                            |                                                                                  | 见下文                    |      |\n| trigger        | 按下该快捷键配合鼠标点击进行框选 **按键参考：** _<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/API/UI_Events/Keyboard_event_key_values\" target=\"_blank\" rel=\"noopener noreferrer\">MDN Key Values</a>_ 。若设为**空数组**时则表示鼠标点击进行框选，不需要按下其他按键配合 <br/> ⚠️ 注意，`trigger` 设置为 `['drag']` 时会导致 `drag-canvas` 行为失效。两者不可同时配置。 | string[] \\| (`Control` \\| `Shift`\\| `Alt` \\| `......`)[]                         | [`shift`]                 |      |\n\n### immediately\n\n是否及时框选, 仅在框选模式为 `default` 时生效\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'brush-select',\n      key: 'brush-select',\n      immediately: true, // 可以看到框框笼罩过去时，元素立即被框选了\n      trigger: [], // 不需要配合其他按键，点击鼠标拖动即可框选\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 100, y: 50 } },\n      { id: 'node-2', style: { x: 260, y: 50 } },\n      { id: 'node-3', style: { x: 280, y: 100 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-1', target: 'node-3' },\n      { source: 'node-2', target: 'node-3' },\n    ],\n  },\n  node: {\n    style: { fill: '#7e3feb' },\n  },\n  edge: {\n    stroke: '#8b9baf',\n  },\n  behaviors: [\n    {\n      type: 'brush-select',\n      key: 'brush-select',\n      immediately: true, // 立即框选\n      trigger: [],\n    },\n  ],\n  plugins: [{ type: 'grid-line', size: 30 }],\n});\n\ngraph.render();\n```\n\n### mode\n\n内置支持四种框选的选择模式：\n\n- `union`：保持已选元素的当前状态，并添加指定的 state 状态。\n- `intersect`：如果已选元素已有指定的 state 状态，则保留；否则清除该状态。\n- `diff`：对已选元素的指定 state 状态进行取反操作。\n- `default`：清除已选元素的当前状态，并添加指定的 state 状态。\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'brush-select',\n      key: 'brush-select',\n      mode: 'default', // 框选模式, 默认框选模式\n    },\n  ],\n});\n```\n\n```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 200, y: 100 } },\n        { id: 'node-2', style: { x: 360, y: 100 } },\n        { id: 'node-3', style: { x: 280, y: 220 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-1', target: 'node-3' },\n        { source: 'node-2', target: 'node-3' },\n      ],\n    },\n    node: {\n      style: { fill: '#7e3feb' },\n      state: {\n        custom: { fill: '#ffa940' },\n      },\n    },\n    edge: {\n      stroke: '#8b9baf',\n      state: {\n        custom: { stroke: '#ffa940' },\n      },\n    },\n    behaviors: [\n      {\n        type: 'brush-select',\n        key: 'brush-select',\n        trigger: [],\n        immediately: true,\n      },\n    ],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      key: 'brush-select',\n      type: 'brush-select',\n      animation: false,\n      enable: true,\n      enableElements: ['node', 'edge', 'combo'],\n      mode: 'default',\n      state: 'selected',\n    };\n    const optionFolder = gui.addFolder('BrushSelect Options');\n    optionFolder.add(options, 'type').disable(true);\n\n    optionFolder.add(options, 'state', ['active', 'selected', 'custom']);\n    optionFolder.add(options, 'mode', ['union', 'intersect', 'diff', 'default']);\n    // .onChange((e) => {\n    //   immediately.show(e === 'default');\n    // });\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'brush-select',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n\n### style\n\n| 属性              | 描述               | 类型                                     | 默认值    |\n| ----------------- | ------------------ | ---------------------------------------- | --------- |\n| cursor            | 鼠标样式           | string                                   |           |\n| fill              | 填充颜色           | string \\| Pattern \\| null                | `#1677FF` |\n| fillOpacity       | 填充透明度         | number \\| string                         | 0.1       |\n| isBillboard       | 是否启用公告牌模式 | boolean                                  |           |\n| isSizeAttenuation | 是否启用大小衰减   | boolean                                  |           |\n| lineCap           | 线段端点样式       | `butt` \\| `round` \\| `square`            |           |\n| lineDash          | 虚线配置           | number \\| string \\| (string \\| number)[] |           |\n| lineDashOffset    | 虚线偏移量         | number                                   |           |\n| lineJoin          | 线段连接处样式     | `miter` \\| `round` \\| `bevel`            |           |\n| lineWidth         | 线宽度             | number \\| string                         | 1         |\n| opacity           | 整体透明度         | number \\| string                         |           |\n| radius            | 矩形圆角半径       | number \\| string \\| number[]             |           |\n| shadowBlur        | 阴影模糊程度       | number                                   |           |\n| shadowColor       | 阴影颜色           | string                                   |           |\n| shadowOffsetX     | 阴影 X 方向偏移    | number                                   |           |\n| shadowOffsetY     | 阴影 Y 方向偏移    | number                                   |           |\n| stroke            | 描边颜色           | string \\| Pattern \\| null                | `#1677FF` |\n| strokeOpacity     | 描边透明度         | number \\| string                         |           |\n| visibility        | 可见性             | `visible` \\| `hidden`                    |           |\n| zIndex            | 渲染层级           | number                                   | 2         |\n\n**示例**：\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'brush-select',\n      key: 'brush-select',\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2], // 虚线外框\n        // rgb超级炫彩框框\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 200, y: 100 } },\n      { id: 'node-2', style: { x: 360, y: 100 } },\n      { id: 'node-3', style: { x: 280, y: 220 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-1', target: 'node-3' },\n      { source: 'node-2', target: 'node-3' },\n    ],\n  },\n  node: {\n    style: { fill: '#7e3feb' },\n  },\n  edge: {\n    stroke: '#8b9baf',\n  },\n  behaviors: [\n    {\n      type: 'brush-select',\n      key: 'brush-select',\n      trigger: [],\n      immediately: true,\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2], // 虚线外框\n        // rgb超级炫彩框框\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n  plugins: [{ type: 'grid-line', size: 30 }],\n  animation: true,\n});\n\ngraph.render();\n```\n\n### 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 200, y: 250 } },\n      { id: 'node-2', style: { x: 250, y: 200 } },\n      { id: 'node-3', style: { x: 300, y: 250 } },\n      { id: 'node-4', style: { x: 250, y: 300 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-2', target: 'node-3' },\n      { source: 'node-3', target: 'node-4' },\n      { source: 'node-4', target: 'node-1' },\n    ],\n  },\n  behaviors: [\n    {\n      key: 'brush-select',\n      type: 'brush-select',\n      enable: true,\n      animation: false,\n      mode: 'default', // union intersect diff default\n      state: 'selected', // 'active', 'selected', 'inactive', ...\n      trigger: [], // ['Shift', 'Alt', 'Control', 'Drag', 'Meta', ...]\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2],\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/ClickSelect.en.md",
    "content": "---\ntitle: ClickSelect\norder: 3\n---\n\n## Overview\n\nWhen an element is clicked, it will be highlighted.\n\n## Usage Scenarios\n\nThis behavior is mainly used for:\n\n- Focusing on elements\n- Viewing element details\n- Viewing element relationships\n\n## Online Experience\n\n<embed src=\"@/common/api/behaviors/click-element.md\"></embed>\n\n## Basic Usage\n\nAdd this behavior in the graph configuration:\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form. This method is simple but only supports default configuration and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['click-select'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and can dynamically update the configuration at runtime:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'click-select',\n      key: 'click-select-1',\n      degree: 2, // Selection spread range\n      state: 'active', // Selected state\n      neighborState: 'neighborActive', // Neighbor node attached state\n      unselectedState: 'inactive', // Unselected node state\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option          | Description                                                                                                                                                                                                                                                        | Type                                                                            | Default        | Required |\n| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | -------------- | -------- |\n| type            | Behavior type name. This behavior is built-in, and you can use it with `type: 'click-select'`.                                                                                                                                                                     | `click-select` \\| string                                                        | `click-select` | ✓        |\n| animation       | Whether to enable animation effects when switching element states                                                                                                                                                                                                  | boolean                                                                         | true           |          |\n| degree          | Controls the highlight spread range, [example](#degree)                                                                                                                                                                                                            | number \\| (event:[Event](/en/api/event#event-object-properties)) => number      | 0              |          |\n| enable          | Whether to enable the click element function, supports dynamic control through functions, [example](#enable)                                                                                                                                                       | boolean \\| ((event: [Event](/en/api/event#event-object-properties)) => boolean) | true           |          |\n| multiple        | Whether to allow multiple selections                                                                                                                                                                                                                               | boolean                                                                         | false          |          |\n| state           | The state applied when an element is selected                                                                                                                                                                                                                      | string \\| `selected` \\| `active`\\| `inactive`\\| `disabled`\\| `highlight`        | `selected`     |          |\n| neighborState   | The state applied to elements with n-degree relationships when an element is selected. The value of n is controlled by the degree attribute, for example, degree 1 means directly adjacent elements, [example](#neighborstate)                                     | string \\| `selected` \\| `active`\\| `inactive`\\| `disabled`\\| `highlight`        | `selected`     |          |\n| unselectedState | The state applied to all other elements except the selected element and its affected neighbor elements when an element is selected, [example](#unselectedState)                                                                                                    | string \\| `selected` \\| `active`\\| `inactive`\\| `disabled`\\| `highlight`        |                |          |\n| onClick         | Callback when an element is clicked                                                                                                                                                                                                                                | (event: [Event](/en/api/event#event-object-properties)) => void                 |                |          |\n| trigger         | Press this shortcut key in combination with a mouse click to perform multi-selection, key reference: _<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\" target=\"_blank\" rel=\"noopener noreferrer\">MDN Key Values</a>_ | string[] \\| (`Control` \\| `Shift`\\| `Alt` \\| `......`)[]                        | `['shift']`    |          |\n\n### degree\n\nControls the highlight spread range\n\n- For nodes, `0` means only the current node is selected, `1` means the current node and its directly adjacent nodes and edges are selected, and so on.\n- For edges, `0` means only the current edge is selected, `1` means the current edge and its directly adjacent nodes are selected, and so on.\n\n> In the following example, when `degree: 0` only the <span style='color:#E4504D'>red</span> point is highlighted;\n> When `degree: 1` the <span style='color:#E4504D'>red</span> and <span style='color:#FFC40C'>orange</span> points are highlighted.\n\n<embed src=\"@/common/api/behaviors/click-element.md\"></embed>\n\n### enable\n\nWhether to enable the click element function\n\nIt can be dynamically controlled through functions, for example, only enabled when a node is selected.\n\n```js\n{\n  //⚠️ Note, you need to set both the node and the canvas, otherwise the user will not listen to the event when clicking the canvas\n  enable: (event) => ['node', 'canvas'].includes(event.targetType);\n}\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 60 } },\n      { id: 'node2', style: { x: 200, y: 60 } },\n      { id: 'node3', style: { x: 300, y: 60 } },\n    ],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      active: {\n        fill: '#0f0',\n      },\n      neighborActive: {\n        fill: '#FFC40C',\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'active',\n      neighborState: 'neighborActive',\n      enable: (event) => ['node', 'canvas'].includes(event.targetType),\n    },\n  ],\n});\n\ngraph.render();\n```\n\nSimilarly, if you only want edges to be selected:\n\n```js\n{\n  enable: (event) => ['edge', 'canvas'].includes(event.targetType);\n}\n```\n\n### neighborState\n\nThe state applied to elements with n-degree relationships when an element is selected. The value of n is controlled by the degree attribute, for example, degree 1 means directly adjacent elements\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      // State attached to the directly clicked node\n      state: 'active',\n      // State attached to adjacent nodes\n      neighborState: 'neighborActive',\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      active: {\n        fill: '#0f0',\n      },\n      neighborActive: {\n        fill: '#FFC40C',\n        halo: true,\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'active',\n      neighborState: 'neighborActive',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### unselectedState\n\nWhen an element is selected, the state applied to all other elements except the selected element and the spread neighbor elements.\n\nBuilt-in states: `selected` `active` `inactive` `disabled` `highlight`\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      unselectedState: 'inactive',\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      active: {\n        fill: '#0f0',\n      },\n      neighborActive: {\n        fill: '#FFC40C',\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'active',\n      neighborState: 'neighborActive',\n      unselectedState: 'inactive',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n## Example\n\n### Click to select nodes and their directly connected nodes\n\n**Clicking a node** will switch from <span style='color:#E4504D'>default state</span> to <span style='color:#0f0'>active</span>\n<br>\n**Adjacent nodes** will switch from <span style='color:#E4504D'>default state</span> to <span style='color:#FFC40C'>neighborActive</span>\n\n```js\nconst graph = new Graph({\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      // Selected node state\n      active: {\n        fill: '#0f0',\n      },\n      // Adjacent node state\n      neighborActive: {\n        fill: '#FFC40C',\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'active',\n      // State attached to adjacent nodes\n      neighborState: 'neighborActive',\n      // Unselected node state\n      unselectedState: 'inactive',\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      active: {\n        fill: '#0f0',\n      },\n      neighborActive: {\n        fill: '#FFC40C',\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'active',\n      neighborState: 'neighborActive',\n      unselectedState: 'inactive',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### Practical Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      active: {\n        fill: '#0b0',\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'active',\n      unselectedState: 'inactive',\n      multiple: true,\n      trigger: ['shift'],\n    },\n    'drag-element',\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/ClickSelect.zh.md",
    "content": "---\ntitle: 点击选中 ClickSelect\norder: 3\n---\n\n## 概述\n\n当鼠标点击元素时，会使元素高亮。\n\n## 使用场景\n\n这一交互主要用于：\n\n- 聚焦元素\n- 查看元素详情\n- 查看元素关系\n\n## 在线体验\n\n<embed src=\"@/common/api/behaviors/click-element.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一交互：\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['click-select'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'click-select',\n      key: 'click-select-1',\n      degree: 2, // 选中扩散范围\n      state: 'active', // 选中的状态\n      neighborState: 'neighborActive', // 相邻节点附着状态\n      unselectedState: 'inactive', // 未选中节点状态\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项          | 说明                                                                                                                                                                                                       | 类型                                                                     | 默认值         | 必选 |\n| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------- | ---- |\n| type            | 交互类型名称。此交互已内置，你可以通过 `type: 'click-select'` 来使用它。                                                                                                                                   | `click-select` \\| string                                                 | `click-select` | ✓    |\n| animation       | 是否在元素状态切换时启用动画效果                                                                                                                                                                           | boolean                                                                  | true           |      |\n| degree          | 控制了高亮扩散范围，[示例](#degree)                                                                                                                                                                        | number \\| (event:[Event](/api/event#事件对象属性)) => number             | 0              |      |\n| enable          | 是否启用点击元素的功能，支持通过函数的方式动态控制是否启用，[示例](#enable)                                                                                                                                | boolean \\| ((event: [Event](/api/event#事件对象属性)) => boolean)        | true           |      |\n| multiple        | 是否允许多选                                                                                                                                                                                               | boolean                                                                  | false          |      |\n| state           | 当元素被选中时应用的状态                                                                                                                                                                                   | string \\| `selected` \\| `active`\\| `inactive`\\| `disabled`\\| `highlight` | `selected`     |      |\n| neighborState   | 当有元素选中时，其相邻 n 度关系的元素应用的状态。n 的值由属性 degree 控制，例如 degree 为 1 时表示直接相邻的元素，[示例](#neighborstate)                                                                   | string \\| `selected` \\| `active`\\| `inactive`\\| `disabled`\\| `highlight` | `selected`     |      |\n| unselectedState | 当有元素被选中时，除了选中元素及其受影响的邻居元素外，其他所有元素应用的状态，[示例](#unselectedState)                                                                                                     | string \\| `selected` \\| `active`\\| `inactive`\\| `disabled`\\| `highlight` |                |      |\n| onClick         | 点击元素时的回调                                                                                                                                                                                           | (event: [Event](/api/event#事件对象属性)) => void                        |                |      |\n| trigger         | 按下该快捷键配合鼠标点击进行多选，按键参考： _<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/API/UI_Events/Keyboard_event_key_values\" target=\"_blank\" rel=\"noopener noreferrer\">MDN Key Values</a>_ | string[] \\| (`Control` \\| `Shift`\\| `Alt` \\| `......`)[]                 | `['shift']`    |      |\n\n### degree\n\n控制了高亮扩散范围\n\n- 对于节点来说，`0` 表示只选中当前节点，`1` 表示选中当前节点及其直接相邻的节点和边，以此类推。\n- 对于边来说，`0` 表示只选中当前边，`1` 表示选中当前边及其直接相邻的节点，以此类推。\n\n> 如下示例，当 `degree: 0` 仅高亮<span style='color:#E4504D'>红色</span>点;\n> 当 `degree: 1` 高亮<span style='color:#E4504D'>红色</span>和<span style='color:#FFC40C'>橙色</span>点。\n\n<embed src=\"@/common/api/behaviors/click-element.md\"></embed>\n\n### enable\n\n是否启用点击元素的功能\n\n可以通过函数的方式动态控制是否启用，例如只有节点被选中时才启用。\n\n```js\n{\n  //⚠️ 注意，这里需要同时设置节点和画布，否则用户点击画布时将不会监听到事件\n  enable: (event) => ['node', 'canvas'].includes(event.targetType);\n}\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 60 } },\n      { id: 'node2', style: { x: 200, y: 60 } },\n      { id: 'node3', style: { x: 300, y: 60 } },\n    ],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      active: {\n        fill: '#0f0',\n      },\n      neighborActive: {\n        fill: '#FFC40C',\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'active',\n      neighborState: 'neighborActive',\n      enable: (event) => ['node', 'canvas'].includes(event.targetType),\n    },\n  ],\n});\n\ngraph.render();\n```\n\n同理，如果只希望边能被选中：\n\n```js\n{\n  enable: (event) => ['edge', 'canvas'].includes(event.targetType);\n}\n```\n\n### neighborState\n\n当有元素选中时，其相邻 n 度关系的元素应用的状态。n 的值由属性 degree 控制，例如 degree 为 1 时表示直接相邻的元素\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      // 被直接点击的节点附着的状态\n      state: 'active',\n      // 相邻的节点附着的状态\n      neighborState: 'neighborActive',\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      active: {\n        fill: '#0f0',\n      },\n      neighborActive: {\n        fill: '#FFC40C',\n        halo: true,\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'active',\n      neighborState: 'neighborActive',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### unselectedState\n\n当有元素被选中时，除了被选中元素和扩散的邻居元素外，其他所有元素应用的状态。\n\n内置状态： `selected` `active` `inactive` `disabled` `highlight`\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      unselectedState: 'inactive',\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      active: {\n        fill: '#0f0',\n      },\n      neighborActive: {\n        fill: '#FFC40C',\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'active',\n      neighborState: 'neighborActive',\n      unselectedState: 'inactive',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n## 示例\n\n### 点击选中节点及其直接相连的节点\n\n**点击节点** 会从 <span style='color:#E4504D'>默认状态</span> 切换为 <span style='color:#0f0'>active</span>\n<br>\n**相邻节点** 会从 <span style='color:#E4504D'>默认状态</span> 切换为 <span style='color:#FFC40C'>neighborActive</span>\n\n```js\nconst graph = new Graph({\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      // 选中节点状态\n      active: {\n        fill: '#0f0',\n      },\n      // 相邻节点状态\n      neighborActive: {\n        fill: '#FFC40C',\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'active',\n      // 相邻节点附着状态\n      neighborState: 'neighborActive',\n      // 未选中节点状态\n      unselectedState: 'inactive',\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      active: {\n        fill: '#0f0',\n      },\n      neighborActive: {\n        fill: '#FFC40C',\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'active',\n      neighborState: 'neighborActive',\n      unselectedState: 'inactive',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      active: {\n        fill: '#0b0',\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'active',\n      unselectedState: 'inactive',\n      multiple: true,\n      trigger: ['shift'],\n    },\n    'drag-element',\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/CollapseExpand.en.md",
    "content": "---\ntitle: CollapseExpand\norder: 4\n---\n\n## Overview\n\nCollapseExpand is a built-in behavior in G6 used to implement the expand/collapse functionality for nodes or combos. Through double-click (default) or single-click actions, users can flexibly control the expand and collapse states of graph elements, effectively managing the visualization hierarchy of the graph structure and reducing visual complexity.\n\n## Use Cases\n\nThis behavior is mainly used for:\n\n- Managing large hierarchical graphs, enabling layered browsing of tree or network graphs\n- Simplifying the display of complex graphs, expanding areas of interest as needed\n- Hiding branch nodes that are temporarily not needed, focusing on important information\n\n## Online Experience\n\n<embed src=\"@/common/api/behaviors/collapse-expand.md\"></embed>\n\n## Basic Usage\n\nAdd this behavior in the graph configuration:\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form. This method is simple but only supports default configurations and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // other configurations...\n  behaviors: ['collapse-expand'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and allowing dynamic updates to the configuration at runtime:\n\n```javascript\nconst graph = new Graph({\n  // other configurations...\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      key: 'collapse-expand-1',\n      trigger: 'click', // Change the trigger method to single-click\n      animation: true, // Enable animation effects\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option     | Description                                         | Type                                                                     | Default Value     | Required |\n| ---------- | --------------------------------------------------- | ------------------------------------------------------------------------ | ----------------- | -------- |\n| type       | Behavior type name                                  | `collapse-expand` \\| string                                              | `collapse-expand` | ✓        |\n| animation  | Enable expand/collapse animation effects            | boolean                                                                  | true              |          |\n| enable     | Enable expand/collapse functionality                | boolean \\| ((event: [/en/api/event#event-object-properties]) => boolean) | true              |          |\n| trigger    | Trigger method, can be single-click or double-click | `click` \\| `dblclick`                                                    | `dblclick`        |          |\n| onCollapse | Callback function when collapse is completed        | (id: string) => void                                                     | -                 |          |\n| onExpand   | Callback function when expand is completed          | (id: string) => void                                                     | -                 |          |\n| align      | Align with the target element to avoid view offset  | boolean                                                                  | true              |          |\n\n## Code Examples\n\n### Basic Expand/Collapse Functionality\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['collapse-expand'],\n  // other configurations...\n});\n```\n\n### Use Single-Click to Trigger Expand/Collapse\n\n```javascript\nconst graph = new Graph({\n  // other configurations...\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      trigger: 'click', // Change the default double-click trigger to single-click\n    },\n  ],\n});\n```\n\n### Custom Expand/Collapse Callback\n\n```javascript\nconst graph = new Graph({\n  // other configurations...\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      onCollapse: (id) => {\n        console.log(`Node ${id} has collapsed`);\n        // Execute custom logic\n      },\n      onExpand: (id) => {\n        console.log(`Node ${id} has expanded`);\n        // Execute custom logic\n      },\n    },\n  ],\n});\n```\n\n### Conditional Enablement of Expand/Collapse Functionality\n\n```javascript\nconst graph = new Graph({\n  // other configurations...\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      // Enable expand/collapse functionality only when the target is a node type\n      enable: (event) => event.targetType === 'node',\n    },\n  ],\n});\n```\n\n### Disable Animation Effects\n\n```javascript\nconst graph = new Graph({\n  // other configurations...\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      animation: false, // Disable expand/collapse animation effects\n    },\n  ],\n});\n```\n\n## FAQ\n\n### 1. How to determine if a node is collapsed?\n\nYou can check the `collapsed` property in the node data:\n\n```javascript\nconst isCollapsed = (nodeId) => {\n  const nodeData = graph.getNodeData(nodeId);\n  return nodeData?.style?.collapsed === true;\n};\n```\n\n### 2. How to programmatically expand or collapse a node?\n\nIn addition to being triggered by user interaction, you can also directly control using [collapseElement](/en/api/element#graphcollapseelementid-options) or [expandElement](/en/api/element#graphexpandelementid-options):\n\n```javascript\n// Collapse node\ngraph.collapseElement('nodeId', { animation: true });\n\n// Expand node\ngraph.expandElement('nodeId', { animation: true });\n```\n\n## Real Cases\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo1', style: { x: 300, y: 100 } },\n      { id: 'node2', combo: 'combo1', style: { x: 300, y: 150 } },\n      { id: 'node3', combo: 'combo2', style: { x: 100, y: 100 } },\n      { id: 'node4', combo: 'combo2', style: { x: 50, y: 150 } },\n      { id: 'node5', combo: 'combo2', style: { x: 150, y: 150 } },\n    ],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node3', target: 'node5' },\n    ],\n    combos: [\n      { id: 'combo1', style: { labelText: '双击折叠', collapsed: true } },\n      { id: 'combo2', style: { labelText: '单击折叠', collapsed: false } },\n    ],\n  },\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      trigger: 'dblclick',\n      enable: (event) => event.targetType === 'combo' && event.target.id === 'combo1',\n    },\n    {\n      type: 'collapse-expand',\n      trigger: 'click',\n      enable: (event) => event.targetType === 'combo' && event.target.id === 'combo2',\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/CollapseExpand.zh.md",
    "content": "---\ntitle: 展开/收起元素 CollapseExpand\norder: 4\n---\n\n## 概述\n\nCollapseExpand 是 G6 中用于实现节点或组合（Combo）展开/收起功能的内置交互。通过双击（默认）或单击操作，用户可以灵活控制图元素的展开与收起状态，有效管理图结构的可视化层次，降低视觉复杂度。\n\n## 使用场景\n\n这一交互主要用于：\n\n- 管理大型层次结构图，实现树状图或网络图的分层浏览\n- 简化复杂图的展示，按需展开关注区域\n- 隐藏暂时不需要查看的分支节点，聚焦于重要信息\n\n## 在线体验\n\n<embed src=\"@/common/api/behaviors/collapse-expand.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一 behavior：\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['collapse-expand'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      key: 'collapse-expand-1',\n      trigger: 'click', // 修改触发方式为单击\n      animation: true, // 启用动画效果\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项     | 说明                           | 类型                                                                      | 默认值            | 必选 |\n| ---------- | ------------------------------ | ------------------------------------------------------------------------- | ----------------- | ---- |\n| type       | 交互类型名称                   | `collapse-expand` \\| string                                               | `collapse-expand` | ✓    |\n| animation  | 是否启用展开/收起动画效果      | boolean                                                                   | true              |      |\n| enable     | 是否启用展开/收起功能          | boolean \\| ((event: [IPointerEvent](/api/event#事件对象属性)) => boolean) | true              |      |\n| trigger    | 触发方式，可选单击或双击       | `click` \\| `dblclick`                                                     | `dblclick`        |      |\n| onCollapse | 完成收起时的回调函数           | (id: string) => void                                                      | -                 |      |\n| onExpand   | 完成展开时的回调函数           | (id: string) => void                                                      | -                 |      |\n| align      | 是否对准目标元素，避免视图偏移 | boolean                                                                   | true              |      |\n\n## 代码示例\n\n### 基础展开/收起功能\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['collapse-expand'],\n  // 其他配置...\n});\n```\n\n### 使用单击触发展开/收起\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      trigger: 'click', // 将默认的双击触发改为单击触发\n    },\n  ],\n});\n```\n\n### 自定义展开/收起回调\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      onCollapse: (id) => {\n        console.log(`节点 ${id} 已收起`);\n        // 执行自定义逻辑\n      },\n      onExpand: (id) => {\n        console.log(`节点 ${id} 已展开`);\n        // 执行自定义逻辑\n      },\n    },\n  ],\n});\n```\n\n### 条件性启用展开/收起功能\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      // 只有当目标是节点类型时才启用展开/收起功能\n      enable: (event) => event.targetType === 'node',\n    },\n  ],\n});\n```\n\n### 关闭动画效果\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      animation: false, // 关闭展开/收起动画效果\n    },\n  ],\n});\n```\n\n## 常见问题\n\n### 1. 如何判断一个节点是否处于收起状态？\n\n可以通过检查节点数据中的 `collapsed` 属性：\n\n```javascript\nconst isCollapsed = (nodeId) => {\n  const nodeData = graph.getNodeData(nodeId);\n  return nodeData?.style?.collapsed === true;\n};\n```\n\n### 2. 如何以编程方式展开或收起节点？\n\n除了通过用户交互触发，你还可以使用 [collapseElement](/api/element#graphcollapseelementid-options) 或 [expandElement](/api/element#graphexpandelementid-options) 直接控制：\n\n```javascript\n// 收起节点\ngraph.collapseElement('nodeId', { animation: true });\n\n// 展开节点\ngraph.expandElement('nodeId', { animation: true });\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo1', style: { x: 300, y: 100 } },\n      { id: 'node2', combo: 'combo1', style: { x: 300, y: 150 } },\n      { id: 'node3', combo: 'combo2', style: { x: 100, y: 100 } },\n      { id: 'node4', combo: 'combo2', style: { x: 50, y: 150 } },\n      { id: 'node5', combo: 'combo2', style: { x: 150, y: 150 } },\n    ],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node3', target: 'node5' },\n    ],\n    combos: [\n      { id: 'combo1', style: { labelText: '双击折叠', collapsed: true } },\n      { id: 'combo2', style: { labelText: '单击折叠', collapsed: false } },\n    ],\n  },\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      trigger: 'dblclick',\n      enable: (event) => event.targetType === 'combo' && event.target.id === 'combo1',\n    },\n    {\n      type: 'collapse-expand',\n      trigger: 'click',\n      enable: (event) => event.targetType === 'combo' && event.target.id === 'combo2',\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/CreateEdge.en.md",
    "content": "---\ntitle: CreateEdge\norder: 5\n---\n\n## Overview\n\nCreateEdge is a built-in behavior in G6 for interactively creating edges on the canvas. After the user triggers the behavior (click or drag), the edge will follow the mouse movement and connect to the target node to complete the creation. If canceled, it will be automatically removed.\n\nAdditionally, this behavior supports customizing the style of the edge, such as color, line style, arrow, etc., to meet different visualization needs.\n\nThe elements that can be connected by this behavior are `node` and `combo`.\n\n## Usage Scenarios\n\nThis behavior is mainly used for:\n\n- Visualization scenarios that require interactive creation of connections between nodes, such as flowcharts, knowledge graphs, etc.\n\n## Online Experience\n\n<embed src=\"@/common/api/behaviors/create-edge.md\"></embed>\n\n## Basic Usage\n\nAdd this behavior in the graph configuration\n\n```javascript\n// Use default configuration\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['create-edge'], // Directly add, use default configuration\n});\n\n// Or use custom configuration\nconst graph = new Graph({\n  // Other configurations\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'click', // Behavior configuration, create edge by clicking\n      style: {}, // Custom edge style\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option   | Description                                                                                                 | Type                                                                                                     | Default       | Required |\n| -------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------- | -------- |\n| type     | Behavior type name                                                                                          | string                                                                                                   | `create-edge` | √        |\n| trigger  | The way to trigger the creation of a new edge: `click` means click to trigger; `drag` means drag to trigger | `click` \\| `drag`                                                                                        | `drag`        |          |\n| enable   | Whether to enable this behavior                                                                             | boolean \\| ((event: [Event](/en/api/event#event-object-properties)) => boolean)                          | true          |          |\n| onCreate | Callback function for creating an edge, returns edge data                                                   | (edge: [EdgeData](/en/manual/data#edge-data-edgedata)) => [EdgeData](/en/manual/data#edge-data-edgedata) | -             |          |\n| onFinish | Callback function for successfully creating an edge                                                         | (edge: [EdgeData](/en/manual/data#edge-data-edgedata)) => void                                           | -             |          |\n| style    | Style of the newly created edge, [configuration options](#style)                                            | See below                                                                                                | -             |          |\n\n### style\n\nConfigure the style of the newly created edge, for detailed configuration options, please refer to [Element - Edge - General Edge Properties - Style](/en/manual/element/edge/base-edge#style)\n\n```json\n{\n  \"style\": {\n    \"stroke\": \"red\",\n    \"lineWidth\": 2\n  }\n}\n```\n\n## Code Examples\n\n### Basic Edge Creation Function\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['create-edge'],\n});\n```\n\n### Custom Edge Creation Function\n\n```javascript\nconst graph = new Graph({\n  // Other configurations,\n  behaviors: [\n    {\n      type: 'create-edge',\n      style: {\n        stroke: 'red',\n        lineWidth: 3,\n      },\n    },\n  ],\n});\n```\n\n### Create Edge by Clicking\n\n```javascript\nconst graph = new Graph({\n  // Other configurations\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'click',\n    },\n  ],\n});\n```\n\n## Practical Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'drag',\n      style: {\n        fill: 'red',\n        lineWidth: 2,\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/CreateEdge.zh.md",
    "content": "---\ntitle: 创建边 CreateEdge\norder: 5\n---\n\n## 概述\n\nCreateEdge 是 G6 中用于实现画布中交互式创建边（Edge）的内置交互。用户触发交互（点击或拖拽）后，边会随鼠标移动，连接到目标节点即完成创建，若取消则自动移除。\n\n此外，该交互支持自定义边的样式，如颜色、线条样式、箭头等，以适应不同的可视化需求。\n\n该交互支持连接的元素为 `node` 和 `combo`。\n\n## 使用场景\n\n这一交互主要用于：\n\n- 需要交互式创建节点间连接关系的可视化场景，如流程图、知识图谱等\n\n## 在线体验\n\n<embed src=\"@/common/api/behaviors/create-edge.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一交互\n\n```javascript\n// 使用默认配置\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['create-edge'], // 直接添加，使用默认配置\n});\n\n// 或使用自定义配置\nconst graph = new Graph({\n  // 其他配置\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'click', // 交互配置，通过点击创建边\n      style: {}, // 边自定义样式\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项   | 说明                                                        | 类型                                                                                       | 默认值        | 必选 |\n| -------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------- | ---- |\n| type     | 交互类型名称                                                | string                                                                                     | `create-edge` | √    |\n| trigger  | 触发新建边的方式：`click` 表示点击触发；`drag` 表示拖拽触发 | `click` \\| `drag`                                                                          | `drag`        |      |\n| enable   | 是否启用该交互                                              | boolean \\| ((event: [Event](/api/event#事件对象属性)) => boolean)                          | true          |      |\n| onCreate | 创建边回调函数，返回边数据                                  | (edge: [EdgeData](/manual/data#边数据edgedata)) => [EdgeData](/manual/data#边数据edgedata) | -             |      |\n| onFinish | 成功创建边回调函数                                          | (edge: [EdgeData](/manual/data#边数据edgedata)) => void                                    | -             |      |\n| style    | 新建边的样式，[配置项](#style)                              | 见下面                                                                                     | -             |      |\n\n### style\n\n配置新创建边的样式，详细配置项请参考 [元素 - 边 - 通用边属性 - 样式](/manual/element/edge/base-edge#style)\n\n```json\n{\n  \"style\": {\n    \"stroke\": \"red\",\n    \"lineWidth\": 2\n  }\n}\n```\n\n## 代码示例\n\n### 基础创建边功能\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['create-edge'],\n});\n```\n\n### 自定义创建边功能\n\n```javascript\nconst graph = new Graph({\n  // 其他配置,\n  behaviors: [\n    {\n      type: 'create-edge',\n      style: {\n        stroke: red,\n        lineWidth: 3,\n      },\n    },\n  ],\n});\n```\n\n### 使用点击创建边\n\n```javascript\nconst graph = new Graph({\n  // 其他配置\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'click',\n    },\n  ],\n});\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'drag',\n      style: {\n        fill: 'red',\n        lineWidth: 2,\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/DragCanvas.en.md",
    "content": "---\ntitle: DragCanvas\norder: 6\n---\n\n## Overview\n\nDragCanvas is a built-in behavior in G6 for implementing canvas dragging functionality, supporting panning the entire canvas by dragging with a mouse or touching the screen. This is the most basic and commonly used navigation behavior in graph visualization, allowing users to freely explore graph content beyond the current viewport.\n\n## Usage Scenarios\n\nThis behavior is mainly used for:\n\n- Navigating and browsing large charts to view content outside the current viewport\n- Adjusting the view focus to move areas of interest to the center of the viewport\n- Combining with zoom interactions to achieve a complete canvas navigation experience\n\n## Online Experience\n\n<embed src=\"@/common/api/behaviors/drag-canvas.md\"></embed>\n\n## Basic Usage\n\nAdd this behavior in the graph configuration:\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form. This method is simple but only supports default configuration and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['drag-canvas'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and can dynamically update the configuration at runtime:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'drag-canvas',\n      key: 'drag-canvas-1',\n      direction: 'x', // Only allow horizontal dragging\n      key: 'drag-behavior', // Specify an identifier for the behavior for dynamic updates\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option      | Description                                                                                                                                                                                                          | Type                                                                                                                                                               | Default                                                                                                           | Required |\n| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | -------- |\n| type        | Behavior type name                                                                                                                                                                                                   | string                                                                                                                                                             | `drag-canvas`                                                                                                     | ✓        |\n| enable      | Whether to enable this behavior                                                                                                                                                                                      | boolean \\| ((event: [Event](/en/api/event#event-object-properties) \\| [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent)) => boolean) | `(event) => 'eventType' in event ? event.targetType === 'canvas': true`(Only enabled when clicking on the canvas) |          |\n| animation   | Drag animation configuration, only effective when using keyboard movement                                                                                                                                            | [ViewportAnimationEffectTiming](/en/api/graph#viewportanimationeffecttiming)                                                                                       | -                                                                                                                 |          |\n| direction   | Allowed drag direction, optional values are: <br/>- Set to `'both'` (default): Allow dragging in any direction <br/>- Set to `'x'`: Only allow horizontal dragging <br/>- Set to `'y'`: Only allow vertical dragging | `'x'` \\| `'y'` \\| `'both'`                                                                                                                                         | `'both'` (no direction restriction)                                                                               |          |\n| range       | Draggable viewport range (in viewport size units), [example](#range)                                                                                                                                                 | number \\| number[]                                                                                                                                                 | Infinity                                                                                                          |          |\n| sensitivity | Distance to trigger a single keyboard movement                                                                                                                                                                       | number                                                                                                                                                             | 10                                                                                                                |          |\n| trigger     | Keyboard keys to trigger dragging, [example](#trigger)                                                                                                                                                               | object                                                                                                                                                             | -                                                                                                                 |          |\n| onFinish    | Callback function when dragging is completed                                                                                                                                                                         | () => void                                                                                                                                                         | -                                                                                                                 |          |\n\n### range\n\n`range` is used to control the draggable range of the canvas:\n\n- Set as a single number: Use the same value for all four directions\n- Set as an array: Specify the range for [top, right, bottom, left] directions respectively\n\nFor example:\n\n```javascript\nrange: 2; // Can drag 2 viewport distances in any direction\nrange: [1, 2, 1, 2]; // Can drag 1 viewport up and down, 2 viewports left and right\n```\n\nThe value range for each direction is [0, Infinity], 0 means no dragging, Infinity means unlimited dragging.\n\n### trigger\n\n`trigger` allows you to configure keyboard keys to control canvas movement:\n\n```javascript\n{\n  trigger: {\n    up: ['ArrowUp'],     // Shortcut key for moving up\n    down: ['ArrowDown'], // Shortcut key for moving down\n    left: ['ArrowLeft'], // Shortcut key for moving left\n    right: ['ArrowRight'] // Shortcut key for moving right\n  }\n}\n```\n\nYou can also configure combination keys:\n\n```javascript\n{\n  trigger: {\n    up: ['Control', 'ArrowUp'],     // Ctrl + Up Arrow\n    down: ['Control', 'ArrowDown'], // Ctrl + Down Arrow\n    left: ['Control', 'ArrowLeft'], // Ctrl + Left Arrow\n    right: ['Control', 'ArrowRight'] // Ctrl + Right Arrow\n  }\n}\n```\n\n## Code Examples\n\n### Basic Dragging Function\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['drag-canvas'],\n});\n```\n\n### Only Allow Horizontal Dragging\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'drag-canvas',\n      direction: 'x', // Only allow horizontal dragging\n    },\n  ],\n});\n```\n\n### Limit Dragging Range\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'drag-canvas',\n      range: 1.5, // Limit dragging range to 1.5 viewport sizes\n    },\n  ],\n});\n```\n\n### Control Movement with Keyboard Arrow Keys\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'drag-canvas',\n      trigger: {\n        up: ['ArrowUp'],\n        down: ['ArrowDown'],\n        left: ['ArrowLeft'],\n        right: ['ArrowRight'],\n      },\n      animation: {\n        duration: 100, // Add smooth animation effect\n      },\n    },\n  ],\n});\n```\n\n## FAQ\n\n### 1. Difference between DragCanvas and other behaviors\n\n- `DragCanvas` is used for dragging the entire canvas view\n- `DragElement` is used for dragging individual graph elements (nodes/edges/combinations)\n- `ScrollCanvas` is used for scrolling the canvas with the mouse wheel without changing the zoom ratio\n\n## Practical Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: ['drag-canvas'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/DragCanvas.zh.md",
    "content": "---\ntitle: 拖拽画布 DragCanvas\norder: 6\n---\n\n## 概述\n\nDragCanvas 是 G6 中用于实现画布拖拽功能的内置交互，支持通过鼠标或触摸屏幕拖动来平移整个画布。这是图可视化中最基础且常用的导航交互，让用户能够自由探索超出当前视口的图内容。\n\n## 使用场景\n\n这一交互主要用于：\n\n- 导航和浏览大型图表，查看当前视口外的内容\n- 调整视图焦点，将感兴趣的区域移动到视口中心\n- 与缩放交互结合，实现完整的画布导航体验\n\n## 在线体验\n\n<embed src=\"@/common/api/behaviors/drag-canvas.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一交互：\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['drag-canvas'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'drag-canvas',\n      key: 'drag-canvas-1',\n      direction: 'x', // 只允许水平方向拖拽\n      key: 'drag-behavior', // 为交互指定标识符，方便动态更新\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项      | 说明                                                                                                                                                   | 类型                                                                                                                                                 | 默认值                                                                                      | 必选 |\n| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ---- |\n| type        | 交互类型名称                                                                                                                                           | string                                                                                                                                               | `drag-canvas`                                                                               | ✓    |\n| enable      | 是否启用该交互                                                                                                                                         | boolean \\| ((event: [Event](/api/event#事件对象属性) \\| [KeyboardEvent](https://developer.mozilla.org/zh-CN/docs/Web/API/KeyboardEvent)) => boolean) | `(event) => 'eventType' in event ? event.targetType === 'canvas': true`(仅在点击画布时启用) |      |\n| animation   | 拖拽动画配置，仅在使用按键移动时有效                                                                                                                   | [ViewportAnimationEffectTiming](/api/graph#viewportanimationeffecttiming)                                                                            | -                                                                                           |      |\n| direction   | 允许的拖拽方向，可选值有：<br/>- 设为 `'both'`（默认）：允许在任意方向拖拽 <br/>- 设为 `'x'`：只允许水平方向拖拽 <br/>- 设为 `'y'`：只允许垂直方向拖拽 | `'x'` \\| `'y'` \\| `'both'`                                                                                                                           | `'both'` (不限制方向)                                                                       |      |\n| range       | 可拖拽的视口范围(以视口大小为单位)，[示例](#range)                                                                                                     | number \\| number[]                                                                                                                                   | Infinity                                                                                    |      |\n| sensitivity | 触发一次按键移动的距离                                                                                                                                 | number                                                                                                                                               | 10                                                                                          |      |\n| trigger     | 触发拖拽的键盘按键，[示例](#trigger)                                                                                                                   | object                                                                                                                                               | -                                                                                           |      |\n| onFinish    | 拖拽完成时的回调函数                                                                                                                                   | () => void                                                                                                                                           | -                                                                                           |      |\n\n### range\n\n`range` 用于控制画布可拖拽的范围：\n\n- 设置为单个数字：四个方向使用相同的值\n- 设置为数组：分别指定 [上, 右, 下, 左] 四个方向的范围\n\n例如：\n\n```javascript\nrange: 2; // 在任何方向上都可以拖拽2个视口的距离\nrange: [1, 2, 1, 2]; // 上下方向可拖拽1个视口，左右方向可拖拽2个视口\n```\n\n每个方向的取值范围是 [0, Infinity]，0表示不能拖拽，Infinity表示无限拖拽。\n\n### trigger\n\n`trigger` 允许你配置键盘按键来控制画布移动：\n\n```javascript\n{\n  trigger: {\n    up: ['ArrowUp'],     // 向上移动的快捷键\n    down: ['ArrowDown'], // 向下移动的快捷键\n    left: ['ArrowLeft'], // 向左移动的快捷键\n    right: ['ArrowRight'] // 向右移动的快捷键\n  }\n}\n```\n\n你也可以配置组合键：\n\n```javascript\n{\n  trigger: {\n    up: ['Control', 'ArrowUp'],     // Ctrl + 上箭头\n    down: ['Control', 'ArrowDown'], // Ctrl + 下箭头\n    left: ['Control', 'ArrowLeft'], // Ctrl + 左箭头\n    right: ['Control', 'ArrowRight'] // Ctrl + 右箭头\n  }\n}\n```\n\n## 代码示例\n\n### 基础拖拽功能\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['drag-canvas'],\n});\n```\n\n### 只允许水平拖拽\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'drag-canvas',\n      direction: 'x', // 只允许水平拖拽\n    },\n  ],\n});\n```\n\n### 限制拖拽范围\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'drag-canvas',\n      range: 1.5, // 限制拖拽范围为1.5个视口大小\n    },\n  ],\n});\n```\n\n### 使用键盘方向键控制移动\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'drag-canvas',\n      trigger: {\n        up: ['ArrowUp'],\n        down: ['ArrowDown'],\n        left: ['ArrowLeft'],\n        right: ['ArrowRight'],\n      },\n      animation: {\n        duration: 100, // 添加平滑动画效果\n      },\n    },\n  ],\n});\n```\n\n## 常见问题\n\n### 1. DragCanvas与其他交互的区别\n\n- `DragCanvas` 用于拖拽整个画布视图\n- `DragElement` 用于拖拽单个图元素（节点/边/组合）\n- `ScrollCanvas` 用于滚轮滚动画布，不改变缩放比例\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: ['drag-canvas'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/DragElement.en.md",
    "content": "---\ntitle: DragElement\norder: 7\n---\n\n## Overview\n\nDragElement is a built-in behavior in G6 for implementing **element dragging** functionality. It has the following core features:\n\n1. **Support for multiple element types**: Supports dragging of both nodes and combos simultaneously\n2. **Intelligent multi-selection**: Supports dragging multiple selected elements at the same time\n3. **Visual feedback**: Provides various visual feedback mechanisms such as ghost nodes, edge visibility, mouse styles, etc.\n4. **Flexible drag effects**: Supports various drag operation effects such as move, link, free drag, etc.\n5. **Parent-child relationship handling**: Automatically handles element hierarchy during dragging, especially when dealing with combo structures\n\n## Online Experience\n\n<embed src=\"@/common/api/behaviors/drag-element.md\"></embed>\n\n## Basic Usage\n\nAdd this behavior in the graph configuration:\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form. This method is simple but only supports default configuration and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['drag-element'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and can dynamically update the configuration at runtime:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'drag-element',\n      key: 'drag-element-1',\n      enableAnimation: true,\n      dropEffect: 'move',\n      shadow: true, // Enable ghost node\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option     | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Type                                                     | Default                                        | Required |\n| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------- | -------- |\n| type       | Behavior type name                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | string                                                   | `drag-element`                                 | ✓        |\n| key        | Unique identifier for the behavior, used for subsequent operations                                                                                                                                                                                                                                                                                                                                                                                                                                              | string                                                   | -                                              |          |\n| enable     | Whether to enable the drag function, by default nodes and combos can be dragged                                                                                                                                                                                                                                                                                                                                                                                                                                 | boolean \\| ((event: IElementDragEvent) => boolean)       | `['node', 'combo'].includes(event.targetType)` |          |\n| animation  | Whether to enable drag animation                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | boolean                                                  | true                                           |          |\n| state      | Identifier for the selected state of nodes, when multi-selection is enabled, it will find the selected nodes based on this state                                                                                                                                                                                                                                                                                                                                                                                | string                                                   | `selected`                                     |          |\n| dropEffect | Defines the operation effect after dragging ends, optional values are: <br/>- `link`: Set the dragged element as a child of the target element <br/>- `move`: Move the element and automatically update the size of the parent element (such as combo) <br/>- `none`: Only update the position of the drag target without performing other operations                                                                                                                                                           | `link` \\| `move` \\| `none`                               | `move`                                         |          |\n| hideEdge   | Controls the display state of edges during dragging, optional values are: <br/>- `none`: Do not hide any edges <br/>- `out`: Hide edges with the current node as the source node <br/>- `in`: Hide edges with the current node as the target node <br/>- `both`: Hide all edges related to the current node <br/>- `all`: Hide all edges in the graph <br/>⚠️ Note: When `shadow` (ghost node) is enabled, the `hideEdge` configuration will not take effect.                                                   | `none` \\| `all` \\| `in` \\| `out` \\| `both`               | `none`                                         |          |\n| shadow     | Whether to enable ghost nodes, which use a shape to follow the mouse movement. [Customize ghost node style](#shadow-style-configuration) ⚠️Note: React nodes do not support enabling                                                                                                                                                                                                                                                                                                                            | boolean                                                  | false                                          |          |\n| cursor     | Customize the mouse style during dragging, [configuration options](#cursor)                                                                                                                                                                                                                                                                                                                                                                                                                                     | { default?: Cursor; grab: Cursor; grabbing: Cursor }     | -                                              |          |\n| trigger    | Press this shortcut key in combination with mouse perform drag element **Key reference:** _<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\" target=\"_blank\" rel=\"noopener noreferrer\">MDN Key Values</a>_. If set to an **empty array**, it means drag element can be performed with mouse without pressing other keys <br/> ⚠️ Note, setting `trigger` to `['drag']` will cause the `drag-canvas` behavior to fail. The two cannot be configured simultaneously. | string[] \\| (`Control` \\| `Shift`\\| `Alt` \\| `......`)[] | [`shift`]                                      |          |\n\n### cursor\n\n`cursor` is used to customize the mouse pointer style during dragging:\n\n- `default`: Pointer style in default state\n- `grab`: Pointer style when hovering over a draggable element\n- `grabbing`: Pointer style when dragging\n\nOptional values are: `auto` | `default` | `none` | `context-menu` | `help` | `pointer` | `progress` | `wait` | `cell` | `crosshair` | `text` | `vertical-text` | `alias` | `copy` | `move` | `no-drop` | `not-allowed` | `grab` | `grabbing` | `all-scroll` | `col-resize` | `row-resize` | `n-resize` | `e-resize` | `s-resize` | `w-resize` | `ne-resize` | `nw-resize` | `se-resize` | `sw-resize` | `ew-resize` | `ns-resize` | `nesw-resize` | `nwse-resize` | `zoom-in` | `zoom-out`\n\nExample configuration:\n\n```js\ncursor: {\n  default: 'default',    // Use normal pointer by default\n  grab: 'grab',         // Show grab pointer when draggable\n  grabbing: 'grabbing'  // Show grabbing pointer when dragging\n}\n```\n\n### shadow Style Configuration\n\nWhen `shadow: true` is enabled, you can customize the style of the ghost node with the following properties:\n\n| Option               | Description                       | Type                                | Default                                     |\n| -------------------- | --------------------------------- | ----------------------------------- | ------------------------------------------- |\n| shadowFill           | Ghost node fill color             | string                              | `#F3F9FF`                                   |\n| shadowFillOpacity    | Ghost node fill color opacity     | number                              | 0.5                                         |\n| shadowStroke         | Ghost node stroke color           | string                              | `#1890FF`                                   |\n| shadowStrokeOpacity  | Ghost node stroke opacity         | number                              | 0.9                                         |\n| shadowLineDash       | Ghost node dash configuration     | number[]                            | [5, 5]                                      |\n| shadowZIndex         | Ghost node rendering level        | number                              | 100                                         |\n| shadowWidth          | Ghost node width                  | number                              | Width of the target element's bounding box  |\n| shadowHeight         | Ghost node height                 | number                              | Height of the target element's bounding box |\n| shadowOpacity        | Overall opacity of the ghost node | number                              |                                             |\n| shadowLineWidth      | Ghost node line width             | number                              |                                             |\n| shadowLineCap        | Ghost node line cap style         | `'butt'` \\| `'round'` \\| `'square'` |                                             |\n| shadowLineJoin       | Ghost node line join style        | `'miter'` \\| `'round'` \\| `'bevel'` |                                             |\n| shadowLineDashOffset | Ghost node dash offset            | number                              |                                             |\n| shadowCursor         | Ghost node mouse style            | string                              |                                             |\n| shadowVisibility     | Ghost node visibility             | `'visible'` \\| `'hidden'`           |                                             |\n\nExample configuration:\n\n```javascript\n{\n  type: 'drag-element',\n  shadow: true,\n  // Customize ghost node style\n  shadowFill: '#E8F3FF',\n  shadowFillOpacity: 0.4,\n  shadowStroke: '#1890FF',\n  shadowStrokeOpacity: 0.8,\n  shadowLineDash: [4, 4],\n  shadowZIndex: 99\n}\n```\n\n> Note: The ghost node style inherits from [BaseStyleProps](/en/manual/element/shape/properties#baseshapestyle), the above configuration items are obtained by adding the `shadow` prefix to the property name.\n\n## Code Examples\n\n### Multi-selection Dragging\n\nNeed to cooperate with the `click-select` behavior to achieve multi-selection, and then associate the selected state through the `state` parameter:\n\n```javascript\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'click-select',\n      multiple: true,\n      state: 'selected',\n    },\n    {\n      type: 'drag-element',\n      state: 'selected', // All nodes in the selected state will be moved simultaneously during dragging\n    },\n  ],\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/DragElement.zh.md",
    "content": "---\ntitle: 拖拽元素 DragElement\norder: 7\n---\n\n## 概述\n\nDragElement 是 G6 中用于实现 **元素拖拽** 功能的内置交互。它具有以下核心特性：\n\n1. **多元素类型支持**：可以同时支持节点（Node）和组合（Combo）的拖拽\n2. **智能多选**：支持同时拖拽多个选中状态的元素\n3. **视觉反馈**：提供幽灵节点、边的显隐、鼠标样式等多种视觉反馈机制\n4. **灵活的拖拽效果**：支持移动、链接、自由拖拽等多种拖拽操作效果\n5. **父子关系处理**：自动处理拖拽过程中的元素层级关系，特别是在处理 Combo 结构时\n\n## 在线体验\n\n<embed src=\"@/common/api/behaviors/drag-element.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一交互：\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['drag-element'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'drag-element',\n      key: 'drag-element-1',\n      enableAnimation: true,\n      dropEffect: 'move',\n      shadow: true, // 启用拖拽幽灵节点\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项     | 说明                                                                                                                                                                                                                                                                                                                                                    | 类型                                                     | 默认值                                         | 必选 |\n| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------- | ---- |\n| type       | 交互类型名称                                                                                                                                                                                                                                                                                                                                            | string                                                   | `drag-element`                                 | ✓    |\n| key        | 交互唯一标识符，用于后续操作交互                                                                                                                                                                                                                                                                                                                        | string                                                   | -                                              |      |\n| enable     | 是否启用拖拽功能，默认可以拖拽节点和 Combo                                                                                                                                                                                                                                                                                                              | boolean \\| ((event: IElementDragEvent) => boolean)       | `['node', 'combo'].includes(event.targetType)` |      |\n| animation  | 是否启用拖拽动画                                                                                                                                                                                                                                                                                                                                        | boolean                                                  | true                                           |      |\n| state      | 节点选中状态的标识，启用多选时会基于该状态查找选中的节点                                                                                                                                                                                                                                                                                                | string                                                   | `selected`                                     |      |\n| dropEffect | 定义拖拽结束后的操作效果，可选值有：<br/>- `link`: 将拖拽元素设置为目标元素的子元素 <br/>- `move`: 移动元素并自动更新父元素（如 Combo）的尺寸 <br/>- `none`: 仅更新拖拽目标的位置，不执行其他操作                                                                                                                                                       | `link` \\| `move` \\| `none`                               | `move`                                         |      |\n| hideEdge   | 控制拖拽过程中边的显示状态，可选值有： <br/>- `none`: 不隐藏任何边 <br/>- `out`: 隐藏以当前节点为源节点的边 <br/>- `in`: 隐藏以当前节点为目标节点的边 <br/>- `both`: 隐藏与当前节点相关的所有边 <br/>- `all`: 隐藏图中所有边 <br/>⚠️ 注意：当启用 `shadow`（幽灵节点）时，`hideEdge` 配置将不生效。                                                     | `none` \\| `all` \\| `in` \\| `out` \\| `both`               | `none`                                         |      |\n| shadow     | 是否启用幽灵节点，即用一个图形代替节点跟随鼠标移动。[自定义幽灵节点样式](#shadow-样式配置) ⚠️注意：React 节点不支持启用                                                                                                                                                                                                                                 | boolean                                                  | false                                          |      |\n| cursor     | 自定义鼠标样式，[配置项](#cursor)                                                                                                                                                                                                                                                                                                                       | { default?: Cursor; grab: Cursor; grabbing: Cursor }     | -                                              |      |\n| trigger    | 同时按下快捷键才能拖拽元素 **按键参考：** _<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/API/UI_Events/Keyboard_event_key_values\" target=\"_blank\" rel=\"noopener noreferrer\">MDN Key Values</a>_ 。若设为**空数组**时则表示不需要按下其他按键配合 <br/> ⚠️ 注意，`trigger` 设置为 `['drag']` 时会导致 `drag-canvas` 行为失效。两者不可同时配置。 | string[] \\| (`Control` \\| `Shift`\\| `Alt` \\| `......`)[] | []                                             |      |\n\n### cursor\n\n`cursor` 用于自定义拖拽过程中的鼠标指针样式：\n\n- `default`: 默认状态下的指针样式\n- `grab`: 鼠标悬停在可拖拽元素上时的指针样式\n- `grabbing`: 正在拖拽时的指针样式\n\n可选值有：`auto` | `default` | `none` | `context-menu` | `help` | `pointer` | `progress` | `wait` | `cell` | `crosshair` | `text` | `vertical-text` | `alias` | `copy` | `move` | `no-drop` | `not-allowed` | `grab` | `grabbing` | `all-scroll` | `col-resize` | `row-resize` | `n-resize` | `e-resize` | `s-resize` | `w-resize` | `ne-resize` | `nw-resize` | `se-resize` | `sw-resize` | `ew-resize` | `ns-resize` | `nesw-resize` | `nwse-resize` | `zoom-in` | `zoom-out`\n\n示例配置：\n\n```js\ncursor: {\n  default: 'default',    // 默认使用普通指针\n  grab: 'grab',         // 可拖拽时显示抓取指针\n  grabbing: 'grabbing'  // 拖拽中显示抓取中指针\n}\n```\n\n### shadow 样式配置\n\n当启用 `shadow: true` 时，可以通过以下属性自定义幽灵节点的样式：\n\n| 配置项               | 说明                   | 类型                                | 默认值               |\n| -------------------- | ---------------------- | ----------------------------------- | -------------------- |\n| shadowFill           | 幽灵节点填充色         | string                              | `#F3F9FF`            |\n| shadowFillOpacity    | 幽灵节点填充色透明度   | number                              | 0.5                  |\n| shadowStroke         | 幽灵节点描边颜色       | string                              | `#1890FF`            |\n| shadowStrokeOpacity  | 幽灵节点描边透明度     | number                              | 0.9                  |\n| shadowLineDash       | 幽灵节点虚线配置       | number[]                            | [5, 5]               |\n| shadowZIndex         | 幽灵节点渲染层级       | number                              | 100                  |\n| shadowWidth          | 幽灵节点宽度           | number                              | 目标元素的包围盒宽度 |\n| shadowHeight         | 幽灵节点高度           | number                              | 目标元素的包围盒高度 |\n| shadowOpacity        | 幽灵节点整体透明度     | number                              |                      |\n| shadowLineWidth      | 幽灵节点线宽度         | number                              |                      |\n| shadowLineCap        | 幽灵节点线段端点样式   | `'butt'` \\| `'round'` \\| `'square'` |                      |\n| shadowLineJoin       | 幽灵节点线段连接处样式 | `'miter'` \\| `'round'` \\| `'bevel'` |                      |\n| shadowLineDashOffset | 幽灵节点虚线偏移量     | number                              |                      |\n| shadowCursor         | 幽灵节点鼠标样式       | string                              |                      |\n| shadowVisibility     | 幽灵节点可见性         | `'visible'` \\| `'hidden'`           |                      |\n\n示例配置：\n\n```javascript\n{\n  type: 'drag-element',\n  shadow: true,\n  // 自定义幽灵节点样式\n  shadowFill: '#E8F3FF',\n  shadowFillOpacity: 0.4,\n  shadowStroke: '#1890FF',\n  shadowStrokeOpacity: 0.8,\n  shadowLineDash: [4, 4],\n  shadowZIndex: 99\n}\n```\n\n> 注意：幽灵节点样式继承自 [BaseStyleProps](/manual/element/shape/properties#baseshapestyle)，上述配置项是在属性名前添加 `shadow` 前缀得到的。\n\n## 代码示例\n\n### 多选推拽\n\n需要配合 `click-select` 行为实现多选，然后通过 `state` 参数关联选中状态：\n\n```javascript\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'click-select',\n      multiple: true,\n      state: 'selected',\n    },\n    {\n      type: 'drag-element',\n      state: 'selected', // 拖拽时会同时移动所有 selected 状态的节点\n    },\n  ],\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/DragElementForce.en.md",
    "content": "---\ntitle: DragElementForce\norder: 8\n---\n\n## Overview\n\nDragElementForce is a built-in behavior in G6 for implementing node dragging under `d3-force` and `d3-force-3d` layouts. During dragging, the layout is **recalculated in real-time**, allowing the graph layout to dynamically adjust to accommodate the new position of the nodes.\n\n<img alt=\"Effect of DragElementForce\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*I5uDQZWTzMsAAAAAAAAAAAAADmJ7AQ/original\" />\n\n## Basic Usage\n\nAdd this behavior in the graph configuration:\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form. This method is simple but only supports default configuration and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['drag-element-force'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and can dynamically update the configuration at runtime:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'drag-element-force',\n      key: 'drag-element-force-1',\n      fixed: true, // Fix node position after dragging\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option                                     | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Type                                                     | Default                                        | Required |\n| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------- | -------- |\n| type                                       | Behavior type name, set `type: 'drag-element-force'` to enable this behavior                                                                                                                                                                                                                                                                                                                                                                                                                                    | string                                                   | `drag-element-force`                           | ✓        |\n| key                                        | Unique identifier for the behavior, used for subsequent operations                                                                                                                                                                                                                                                                                                                                                                                                                                              | string                                                   | -                                              |          |\n| fixed                                      | Whether to keep the node position fixed after dragging ends, boolean values represent: <br/>- true: After dragging ends, the node's position will remain fixed and not be affected by the layout algorithm <br/>- false: After dragging ends, the node's position will continue to be affected by the layout algorithm                                                                                                                                                                                          | boolean                                                  | false                                          |          |\n| enable                                     | Whether to enable the drag function, by default nodes and combos can be dragged                                                                                                                                                                                                                                                                                                                                                                                                                                 | boolean \\| ((event: IElementDragEvent) => boolean)       | `['node', 'combo'].includes(event.targetType)` |          |\n| state                                      | Identifier for the selected state of nodes, when multi-selection is enabled, it will find the selected nodes based on this state                                                                                                                                                                                                                                                                                                                                                                                | string                                                   | `selected`                                     |          |\n| hideEdge                                   | Controls the display state of edges during dragging, optional values are: <br/>- `none`: Do not hide any edges <br/>- `out`: Hide edges with the current node as the source node <br/>- `in`: Hide edges with the current node as the target node <br/>- `both`: Hide all edges related to the current node <br/>- `all`: Hide all edges in the graph <br/>⚠️ Note: When `shadow` (ghost node) is enabled, the `hideEdge` configuration will not take effect.                                                   | `none` \\| `all` \\| `in` \\| `out` \\| `both`               | `none`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |                                                          |\n| cursor                                     | Customize the mouse style during dragging, [example](#cursor)                                                                                                                                                                                                                                                                                                                                                                                                                                                   | { default?: Cursor; grab: Cursor; grabbing: Cursor }     | -                                              |          |\n| trigger                                    | Press this shortcut key in combination with mouse perform drag element **Key reference:** _<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\" target=\"_blank\" rel=\"noopener noreferrer\">MDN Key Values</a>_. If set to an **empty array**, it means drag element can be performed with mouse without pressing other keys <br/> ⚠️ Note, setting `trigger` to `['drag']` will cause the `drag-canvas` behavior to fail. The two cannot be configured simultaneously. | string[] \\| (`Control` \\| `Shift`\\| `Alt` \\| `......`)[] | [`shift`]                                      |          |\n\n### cursor\n\n`cursor` is used to customize the mouse pointer style during dragging:\n\n- `default`: Pointer style in default state\n- `grab`: Pointer style when hovering over a draggable element\n- `grabbing`: Pointer style when dragging\n\nOptional values are: `auto` | `default` | `none` | `context-menu` | `help` | `pointer` | `progress` | `wait` | `cell` | `crosshair` | `text` | `vertical-text` | `alias` | `copy` | `move` | `no-drop` | `not-allowed` | `grab` | `grabbing` | `all-scroll` | `col-resize` | `row-resize` | `n-resize` | `e-resize` | `s-resize` | `w-resize` | `ne-resize` | `nw-resize` | `se-resize` | `sw-resize` | `ew-resize` | `ns-resize` | `nesw-resize` | `nwse-resize` | `zoom-in` | `zoom-out`\n\nExample configuration:\n\n```js\ncursor: {\n  default: 'default',    // Use normal pointer by default\n  grab: 'grab',         // Show grab pointer when draggable\n  grabbing: 'grabbing'  // Show grabbing pointer when dragging\n}\n```\n\n## FAQ\n\n### 1. What is the difference between DragElementForce and DragElement?\n\n- `DragElementForce` is specifically used for `d3-force` or `d3-force-3d` layouts, and recalculates the layout in real-time during dragging\n- `DragElement` is a general drag interaction and does not trigger layout recalculation\n\n## Practical Example\n\n### Mesh Effect\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfunction getData(size = 10) {\n  const nodes = Array.from({ length: size * size }, (_, i) => ({ id: `${i}` }));\n  const edges = [];\n  for (let y = 0; y < size; ++y) {\n    for (let x = 0; x < size; ++x) {\n      if (y > 0) edges.push({ source: `${(y - 1) * size + x}`, target: `${y * size + x}` });\n      if (x > 0) edges.push({ source: `${y * size + (x - 1)}`, target: `${y * size + x}` });\n    }\n  }\n  return { nodes, edges };\n}\n\nconst graph = new Graph({\n  data: getData(),\n  layout: {\n    type: 'd3-force',\n    manyBody: {\n      strength: -30,\n    },\n    link: {\n      strength: 1,\n      distance: 20,\n      iterations: 10,\n    },\n  },\n  node: {\n    style: {\n      size: 10,\n      fill: '#000',\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#000',\n    },\n  },\n  behaviors: [{ type: 'drag-element-force' }, 'zoom-canvas'],\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  gui.add({ msg: 'Try to drag nodes' }, 'msg').name('Tips').disable();\n});\n```\n\n### Fix Dragged Nodes\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(10).fill(0).map((_, i) => ({ id: `${i}`, label: `${i}` })),\n  edges: [\n    { source: '0', target: '1' },\n    { source: '0', target: '2' },\n    { source: '0', target: '3' },\n    { source: '0', target: '4' },\n    { source: '0', target: '5' },\n    { source: '0', target: '7' },\n    { source: '0', target: '8' },\n    { source: '0', target: '9' },\n    { source: '2', target: '3' },\n    { source: '4', target: '5' },\n    { source: '4', target: '6' },\n    { source: '5', target: '6' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.label,\n      labelPlacement: 'middle',\n      labelFill: '#fff',\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    link: {\n      distance: 100,\n      strength: 2,\n    },\n    collide: {\n      radius: 40,\n    },\n  },\n  behaviors: [\n    {\n      type: 'drag-element-force',\n      fixed: true,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n## Shadow Style\n\n### shadow{[BaseStyleProps](https://g.antv.antgroup.com/api/basic/display-object#%E7%BB%98%E5%9B%BE%E5%B1%9E%E6%80%A7)}\n\n<details><summary>An expression like icon{TextStyleProps} indicates that properties of the TextStyleProps type are prefixed with icon in camelCase format.</summary>\n\nTextStyleProps includes the following properties:\n\n- fill\n- fontSize\n- fontWeight\n- ...\n\nicon{TextStyleProps} means you need to use the following property names:\n\n- iconFill\n- iconFontSize\n- iconFontWeight\n- ...\n\n</details>\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/DragElementForce.zh.md",
    "content": "---\ntitle: 力导向拖拽元素 DragElementForce\norder: 8\n---\n\n## 概述\n\nDragElementForce 是 G6 中用于实现 `d3-force` 和 `d3-force-3d` 布局下节点拖拽的内置交互。在拖拽过程中会 **实时重新计算布局**，使得图的布局能够动态调整以适应节点的新位置。\n\n<img alt=\"力导向拖拽元素效果图\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*I5uDQZWTzMsAAAAAAAAAAAAADmJ7AQ/original\" />\n\n## 基本用法\n\n在图配置中添加这一交互：\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['drag-element-force'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'drag-element-force',\n      key: 'drag-element-force-1',\n      fixed: true, // 拖拽后固定节点位置\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项                                     | 说明                                                                                                                                                                                                                                                                                                                                                    | 类型                                                     | 默认值                                         | 必选 |\n| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------- | ---- |\n| type                                       | 交互类型名称，这里设置 `type: 'drag-element-force'` 启用此交互                                                                                                                                                                                                                                                                                          | string                                                   | `drag-element-force`                           | ✓    |\n| key                                        | 交互唯一标识符，用于后续操作交互                                                                                                                                                                                                                                                                                                                        | string                                                   | -                                              |      |\n| fixed                                      | 在拖拽结束后，节点是否保持固定位置，布尔值代表：<br/>- true: 在拖拽结束后，节点的位置将保持固定，不受布局算法的影响 <br/>- false: 在拖拽结束后，节点的位置将继续受到布局算法的影响                                                                                                                                                                      | boolean                                                  | false                                          |      |\n| enable                                     | 是否启用拖拽功能，默认可以拖拽节点和 Combo                                                                                                                                                                                                                                                                                                              | boolean \\| ((event: IElementDragEvent) => boolean)       | `['node', 'combo'].includes(event.targetType)` |      |\n| state                                      | 节点选中状态的标识，启用多选时会基于该状态查找选中的节点                                                                                                                                                                                                                                                                                                | string                                                   | `selected`                                     |      |\n| hideEdge                                   | 控制拖拽过程中边的显示状态，可选值有：<br/>- `none`: 不隐藏任何边 <br/>- `out`: 隐藏以当前节点为源节点的边 <br/>- `in`: 隐藏以当前节点为目标节点的边 <br/>- `both`: 隐藏与当前节点相关的所有边 <br/>- `all`: 隐藏图中所有边 <br/>⚠️ 注意：当启用 `shadow`（幽灵节点）时，`hideEdge` 配置将不生效。                                                      | `none` \\| `all` \\| `in` \\| `out` \\| `both`                                      | `none`                                                                                                                                                                                                                                                                                                                                                  |                                                          |\n| cursor                                     | 自定义鼠标样式，[示例](#cursor)                                                                                                                                                                                                                                                                                                                         | { default?: Cursor; grab: Cursor; grabbing: Cursor }     | -                                              |      |\n| trigger                                    | 同时按下快捷键才能拖拽元素 **按键参考：** _<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/API/UI_Events/Keyboard_event_key_values\" target=\"_blank\" rel=\"noopener noreferrer\">MDN Key Values</a>_ 。若设为**空数组**时则表示不需要按下其他按键配合 <br/> ⚠️ 注意，`trigger` 设置为 `['drag']` 时会导致 `drag-canvas` 行为失效。两者不可同时配置。 | string[] \\| (`Control` \\| `Shift`\\| `Alt` \\| `......`)[] | []                                             |      |\n\n### cursor\n\n`cursor` 用于自定义拖拽过程中的鼠标指针样式：\n\n- `default`: 默认状态下的指针样式\n- `grab`: 鼠标悬停在可拖拽元素上时的指针样式\n- `grabbing`: 正在拖拽时的指针样式\n\n可选值有：`auto` | `default` | `none` | `context-menu` | `help` | `pointer` | `progress` | `wait` | `cell` | `crosshair` | `text` | `vertical-text` | `alias` | `copy` | `move` | `no-drop` | `not-allowed` | `grab` | `grabbing` | `all-scroll` | `col-resize` | `row-resize` | `n-resize` | `e-resize` | `s-resize` | `w-resize` | `ne-resize` | `nw-resize` | `se-resize` | `sw-resize` | `ew-resize` | `ns-resize` | `nesw-resize` | `nwse-resize` | `zoom-in` | `zoom-out`\n\n示例配置：\n\n```js\ncursor: {\n  default: 'default',    // 默认使用普通指针\n  grab: 'grab',         // 可拖拽时显示抓取指针\n  grabbing: 'grabbing'  // 拖拽中显示抓取中指针\n}\n```\n\n## 常见问题\n\n### 1. DragElementForce 和 DragElement 有什么区别？\n\n- `DragElementForce` 专门用于 `d3-force` 或 `d3-force-3d` 布局，拖拽时会实时重新计算布局\n- `DragElement` 是通用的拖拽交互，不会触发布局重新计算\n\n## 实际案例\n\n### 网格效果\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfunction getData(size = 10) {\n  const nodes = Array.from({ length: size * size }, (_, i) => ({ id: `${i}` }));\n  const edges = [];\n  for (let y = 0; y < size; ++y) {\n    for (let x = 0; x < size; ++x) {\n      if (y > 0) edges.push({ source: `${(y - 1) * size + x}`, target: `${y * size + x}` });\n      if (x > 0) edges.push({ source: `${y * size + (x - 1)}`, target: `${y * size + x}` });\n    }\n  }\n  return { nodes, edges };\n}\n\nconst graph = new Graph({\n  data: getData(),\n  layout: {\n    type: 'd3-force',\n    manyBody: {\n      strength: -30,\n    },\n    link: {\n      strength: 1,\n      distance: 20,\n      iterations: 10,\n    },\n  },\n  node: {\n    style: {\n      size: 10,\n      fill: '#000',\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#000',\n    },\n  },\n  behaviors: [{ type: 'drag-element-force' }, 'zoom-canvas'],\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  gui.add({ msg: 'Try to drag nodes' }, 'msg').name('Tips').disable();\n});\n```\n\n### 固定被拖拽的节点\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(10).fill(0).map((_, i) => ({ id: `${i}`, label: `${i}` })),\n  edges: [\n    { source: '0', target: '1' },\n    { source: '0', target: '2' },\n    { source: '0', target: '3' },\n    { source: '0', target: '4' },\n    { source: '0', target: '5' },\n    { source: '0', target: '7' },\n    { source: '0', target: '8' },\n    { source: '0', target: '9' },\n    { source: '2', target: '3' },\n    { source: '4', target: '5' },\n    { source: '4', target: '6' },\n    { source: '5', target: '6' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.label,\n      labelPlacement: 'middle',\n      labelFill: '#fff',\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    link: {\n      distance: 100,\n      strength: 2,\n    },\n    collide: {\n      radius: 40,\n    },\n  },\n  behaviors: [\n    {\n      type: 'drag-element-force',\n      fixed: true,\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/FixElementSize.en.md",
    "content": "---\ntitle: FixElementSize\norder: 9\n---\n\n## Overview\n\nFixElementSize is a built-in interaction provided by G6, used to **maintain the size of certain elements within nodes unchanged during the zooming process.** It enhances visual consistency and operability during zooming.\nBy listening to viewport changes, it automatically scales elements marked as \"fixed size\" to ensure they maintain a relatively constant display size at different zoom levels. It supports global enablement and also allows control over specific elements or nodes as needed.\n\n## Use Cases\n\nThis interaction is mainly used for:\n\n- Graphical elements or embedded components (buttons, labels, etc.) that need to maintain a fixed visual size\n\n## Online Experience\n\n<embed src=\"@/common/api/behaviors/fix-element-size.md\"></embed>\n\n## Basic Usage\n\nAdd this interaction in the graph configuration\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form. This method is simple but only supports default configuration and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['fix-element-size'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and can dynamically update the configuration at runtime:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'fix-element-size',\n      enable: true, // Enable this interaction\n      state: 'selected', // State of elements to fix size\n      reset: true, // Restore style when elements are redrawn\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option      | Description                                                                                                                                                                                                    | Type                                                                         | Default                                                                                             | Required |\n| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -------- |\n| type        | Interaction type name                                                                                                                                                                                          | string                                                                       | `fix-element-size`                                                                                  | ✓        |\n| enable      | Whether to enable this interaction, [example](#enable)                                                                                                                                                         | boolean \\| ((event: [Event](/api/event#event-object-properties)) => boolean) | true                                                                                                |          |\n| reset       | Whether to restore style when elements are redrawn                                                                                                                                                             | boolean                                                                      | `false`                                                                                             |          |\n| state       | Specify the state of elements to fix size                                                                                                                                                                      | string                                                                       | \"\"                                                                                                  |          |\n| node        | Node configuration item, used to define which attributes maintain a fixed visual size. If not specified (i.e., undefined), the entire node will be fixed, [example](#node)                                     | [FixShapeConfig](#fixshapeconfig) \\| FixShapeConfig[]                        |                                                                                                     |          |\n| nodeFilter  | Node filter, used to filter which nodes maintain a fixed size during zooming                                                                                                                                   | (datum: [NodeData](/manual/data#nodedata)) => boolean                        | `() => true`                                                                                        |          |\n| edge        | Edge configuration item, used to define which attributes maintain a fixed visual size. By default, the lineWidth and labelFontSize attributes are fixed, usage is the same as [node configuration item](#node) | [FixShapeConfig](#fixshapeconfig) \\| FixShapeConfig[]                        | `[ shape: 'key', fields: ['lineWidth'] ,  shape: 'halo', fields: ['lineWidth'] ,  shape: 'label' ]` |          |\n| edgeFilter  | Edge filter, used to filter which edges maintain a fixed size during zooming                                                                                                                                   | (datum: [EdgeData](/manual/data#edgedata)) => boolean                        | `() => true`                                                                                        |          |\n| combo       | Combo configuration item, used to define which attributes maintain a fixed visual size. By default, the entire Combo will be fixed, usage is the same as [node configuration item](#node)                      | [FixShapeConfig](#fixshapeconfig) \\| FixShapeConfig[]                        |                                                                                                     |          |\n| comboFilter | Combo filter, used to filter which Combos maintain a fixed size during zooming                                                                                                                                 | (datum: [ComboData](/manual/data#combodata)) => boolean                      | `() => true`                                                                                        |          |\n\n### enable\n\nWhether to enable the fixed element size interaction. By default, it is enabled when zooming out the canvas\n\nBy default, it is enabled when zooming out the canvas, set `enable: (event) => event.data.scale < 1`; if you want to enable it when zooming in, set `enable: (event) => event.data.scale > 1`; if you want to enable it when both zooming in and out, set `enable: true`\n\n### node\n\nNode configuration item, used to define which attributes maintain a fixed visual size. If not specified (i.e., undefined), the entire node will be fixed\n\n**Example**\n\nIf you want to fix the lineWidth of the main shape of the node during zooming, you can configure it like this:\n\n```ts\n{\n  node: [{ shape: 'key', fields: ['lineWidth'] }];\n}\n```\n\nIf you want to keep the size of the element label unchanged during zooming, you can configure it like this:\n\n```ts\n{\n  shape: 'label';\n}\n```\n\n### FixShapeConfig\n\n| Parameter | Description                                                                                                                                                        | Type                                                   | Default | Required |\n| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | ------- | -------- |\n| shape     | Specify the shape to fix size, it can be the class name of the shape, or a function that receives all shapes constituting the element and returns the target shape | string \\| ((shapes: DisplayObject[]) => DisplayObject) | -       | ✓        |\n| fields    | Specify the fields of the shape to fix size. If not specified, the entire shape size is fixed by default                                                           | string[]                                               | -       | ✘        |\n\n## Practical Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node0', size: 50, label: '0', style: { x: 326, y: 268 }, states: ['selected'] },\n    { id: 'node1', size: 30, label: '1', style: { x: 280, y: 384 }, states: ['selected'] },\n    { id: 'node2', size: 30, label: '2', style: { x: 234, y: 167 } },\n    { id: 'node3', size: 30, label: '3', style: { x: 391, y: 368 } },\n    { id: 'node4', size: 30, label: '4', style: { x: 444, y: 209 } },\n    { id: 'node5', size: 30, label: '5', style: { x: 378, y: 157 } },\n    { id: 'node6', size: 15, label: '6', style: { x: 229, y: 400 } },\n    { id: 'node7', size: 15, label: '7', style: { x: 281, y: 440 } },\n    { id: 'node8', size: 15, label: '8', style: { x: 188, y: 119 } },\n    { id: 'node9', size: 15, label: '9', style: { x: 287, y: 157 } },\n    { id: 'node10', size: 15, label: '10', style: { x: 185, y: 200 } },\n    { id: 'node11', size: 15, label: '11', style: { x: 238, y: 110 } },\n    { id: 'node12', size: 15, label: '12', style: { x: 239, y: 221 } },\n    { id: 'node13', size: 15, label: '13', style: { x: 176, y: 160 } },\n    { id: 'node14', size: 15, label: '14', style: { x: 389, y: 423 } },\n    { id: 'node15', size: 15, label: '15', style: { x: 441, y: 341 } },\n    { id: 'node16', size: 15, label: '16', style: { x: 442, y: 398 } },\n  ],\n  edges: [\n    { source: 'node0', target: 'node1', label: '0-1', states: ['selected'] },\n    { source: 'node0', target: 'node2', label: '0-2' },\n    { source: 'node0', target: 'node3', label: '0-3' },\n    { source: 'node0', target: 'node4', label: '0-4' },\n    { source: 'node0', target: 'node5', label: '0-5' },\n    { source: 'node1', target: 'node6', label: '1-6' },\n    { source: 'node1', target: 'node7', label: '1-7' },\n    { source: 'node2', target: 'node8', label: '2-8' },\n    { source: 'node2', target: 'node9', label: '2-9' },\n    { source: 'node2', target: 'node10', label: '2-10' },\n    { source: 'node2', target: 'node11', label: '2-11' },\n    { source: 'node2', target: 'node12', label: '2-12' },\n    { source: 'node2', target: 'node13', label: '2-13' },\n    { source: 'node3', target: 'node14', label: '3-14' },\n    { source: 'node3', target: 'node15', label: '3-15' },\n    { source: 'node3', target: 'node16', label: '3-16' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.label,\n      size: (d) => d.size,\n      lineWidth: 1,\n    },\n  },\n  edge: { style: { labelText: (d) => d.label } },\n  behaviors: [\n    'zoom-canvas',\n    'drag-canvas',\n    {\n      key: 'fix-element-size',\n      type: 'fix-element-size',\n      enable: (event) => event.data.scale < 1,\n      state: 'selected',\n      reset: true,\n    },\n    { type: 'click-select', key: 'click-select', multiple: true },\n  ],\n  autoFit: 'center',\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/FixElementSize.zh.md",
    "content": "---\ntitle: 缩放画布时固定元素大小 FixElementSize\norder: 9\n---\n\n## 概述\n\nFixElementSize 是 G6 提供的一种内置交互，用于在视图缩放过程中，**保持节点中某些元素的尺寸不随缩放变化。** 提升缩放过程中的视觉一致性与可操作性。\n通过监听视口变化，自动对标记为“固定尺寸”的元素进行缩放补偿，确保它们在不同缩放级别下保持相对恒定的显示尺寸。支持全局启用，也支持按需控制具体元素或节点的适配行为。\n\n## 使用场景\n\n这一交互主要用于：\n\n- 需要固定视觉大小的图形元素或嵌入式组件（按钮、标签等）\n\n## 在线体验\n\n<embed src=\"@/common/api/behaviors/fix-element-size.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一交互\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['fix-element-size'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'fix-element-size',\n      enable: true, // 开启该交互\n      state: 'selected', // 要固定大小的元素状态\n      reset: true, // 元素重绘时还原样式\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项      | 说明                                                                                                              | 类型                                                              | 默认值                                                                                              | 必选 |\n| ----------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ---- |\n| type        | 交互类型名称                                                                                                      | string                                                            | `fix-element-size`                                                                                  | √    |\n| enable      | 是否启用该交互，[示例](#enable)                                                                                   | boolean \\| ((event: [Event](/api/event#事件对象属性)) => boolean) | true                                                                                                |      |\n| reset       | 元素重绘时是否还原样式                                                                                            | boolean                                                           | `false`                                                                                             |      |\n| state       | 指定要固定大小的元素状态                                                                                          | string                                                            | \"\"                                                                                                  |      |\n| node        | 节点配置项，用于定义哪些属性在视觉上保持固定大小。若未指定（即为 undefined），则整个节点将被固定，[示例](#node)   | [FixShapeConfig](#fixshapeconfig) \\| FixShapeConfig[]             |                                                                                                     |      |\n| nodeFilter  | 节点过滤器，用于过滤哪些节点在缩放过程中保持固定大小                                                              | (datum: [NodeData](/manual/data#节点数据nodedata)) => boolean     | `() => true`                                                                                        |      |\n| edge        | 边配置项，用于定义哪些属性在视觉上保持固定大小。默认固定 lineWidth、labelFontSize 属性，用法同[node配置项](#node) | [FixShapeConfig](#fixshapeconfig) \\| FixShapeConfig[]             | `[ shape: 'key', fields: ['lineWidth'] ,  shape: 'halo', fields: ['lineWidth'] ,  shape: 'label' ]` |      |\n| edgeFilter  | 边过滤器，用于过滤哪些边在缩放过程中保持固定大小                                                                  | (datum: [EdgeData](/manual/data#边数据edgedata)) => boolean       | `() => true`                                                                                        |      |\n| combo       | Combo 配置项，用于定义哪些属性在视觉上保持固定大小。默认整个 Combo 将被固定，用法同[node配置项](#node)            | [FixShapeConfig](#fixshapeconfig) \\| FixShapeConfig[]             |                                                                                                     |      |\n| comboFilter | Combo 过滤器，用于过滤哪些 Combo 在缩放过程中保持固定大小                                                         | (datum: [ComboData](/manual/data#组合数据combodata)) => boolean   | `() => true`                                                                                        |      |\n\n### enable\n\n是否启用固定元素大小交互。默认在缩小画布时启用\n\n默认在缩小画布时启用，设置 `enable: (event) => event.data.scale < 1`；如果希望在放大画布时启用，设置 `enable: (event) => event.data.scale > 1`；如果希望在放大缩小画布时都启用，设置 `enable: true`\n\n### node\n\n节点配置项，用于定义哪些属性在视觉上保持固定大小。若未指定（即为 undefined），则整个节点将被固定\n\n**示例**\n\n如果在缩放过程中希望固定节点主图形的 lineWidth，可以这样配置：\n\n```ts\n{\n  node: [{ shape: 'key', fields: ['lineWidth'] }];\n}\n```\n\n如果在缩放过程中想保持元素标签大小不变，可以这样配置：\n\n```ts\n{\n  shape: 'label';\n}\n```\n\n### FixShapeConfig\n\n| 参数   | 描述                                                                                                 | 类型                                                   | 默认值 | 必选 |\n| ------ | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------ | ---- |\n| shape  | 指定要固定大小的图形，可以是图形的类名字，或者是一个函数，该函数接收构成元素的所有图形并返回目标图形 | string \\| ((shapes: DisplayObject[]) => DisplayObject) | -      | ✓    |\n| fields | 指定要固定大小的图形属性字段。如果未指定，则默认固定整个图形的大小                                   | string[]                                               | -      | ✘    |\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node0', size: 50, label: '0', style: { x: 326, y: 268 }, states: ['selected'] },\n    { id: 'node1', size: 30, label: '1', style: { x: 280, y: 384 }, states: ['selected'] },\n    { id: 'node2', size: 30, label: '2', style: { x: 234, y: 167 } },\n    { id: 'node3', size: 30, label: '3', style: { x: 391, y: 368 } },\n    { id: 'node4', size: 30, label: '4', style: { x: 444, y: 209 } },\n    { id: 'node5', size: 30, label: '5', style: { x: 378, y: 157 } },\n    { id: 'node6', size: 15, label: '6', style: { x: 229, y: 400 } },\n    { id: 'node7', size: 15, label: '7', style: { x: 281, y: 440 } },\n    { id: 'node8', size: 15, label: '8', style: { x: 188, y: 119 } },\n    { id: 'node9', size: 15, label: '9', style: { x: 287, y: 157 } },\n    { id: 'node10', size: 15, label: '10', style: { x: 185, y: 200 } },\n    { id: 'node11', size: 15, label: '11', style: { x: 238, y: 110 } },\n    { id: 'node12', size: 15, label: '12', style: { x: 239, y: 221 } },\n    { id: 'node13', size: 15, label: '13', style: { x: 176, y: 160 } },\n    { id: 'node14', size: 15, label: '14', style: { x: 389, y: 423 } },\n    { id: 'node15', size: 15, label: '15', style: { x: 441, y: 341 } },\n    { id: 'node16', size: 15, label: '16', style: { x: 442, y: 398 } },\n  ],\n  edges: [\n    { source: 'node0', target: 'node1', label: '0-1', states: ['selected'] },\n    { source: 'node0', target: 'node2', label: '0-2' },\n    { source: 'node0', target: 'node3', label: '0-3' },\n    { source: 'node0', target: 'node4', label: '0-4' },\n    { source: 'node0', target: 'node5', label: '0-5' },\n    { source: 'node1', target: 'node6', label: '1-6' },\n    { source: 'node1', target: 'node7', label: '1-7' },\n    { source: 'node2', target: 'node8', label: '2-8' },\n    { source: 'node2', target: 'node9', label: '2-9' },\n    { source: 'node2', target: 'node10', label: '2-10' },\n    { source: 'node2', target: 'node11', label: '2-11' },\n    { source: 'node2', target: 'node12', label: '2-12' },\n    { source: 'node2', target: 'node13', label: '2-13' },\n    { source: 'node3', target: 'node14', label: '3-14' },\n    { source: 'node3', target: 'node15', label: '3-15' },\n    { source: 'node3', target: 'node16', label: '3-16' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.label,\n      size: (d) => d.size,\n      lineWidth: 1,\n    },\n  },\n  edge: { style: { labelText: (d) => d.label } },\n  behaviors: [\n    'zoom-canvas',\n    'drag-canvas',\n    {\n      key: 'fix-element-size',\n      type: 'fix-element-size',\n      enable: (event) => event.data.scale < 1,\n      state: 'selected',\n      reset: true,\n    },\n    { type: 'click-select', key: 'click-select', multiple: true },\n  ],\n  autoFit: 'center',\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/FocusElement.en.md",
    "content": "---\ntitle: FocusElement\norder: 10\n---\n\n## Overview\n\nFocusElement is a built-in behavior in G6 used to implement the element focusing feature, allowing elements to be focused to the center of the view by clicking on them. This behavior helps users quickly locate and focus on specific graph elements.\n\n## Use Cases\n\n- Quickly center the focused nodes or edges in the display\n\n## Online Experience\n\n<embed src=\"@/common/api/behaviors/focus-element.md\"></embed>\n\n## Basic Usage\n\nAdd this behavior in the graph configuration:\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['focus-element'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'focus-element',\n      animation: {\n        duration: 500,\n        easing: 'ease-in',\n      },\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option    | Description                                                                                                                                                                                                                                                                                                                                                                 | Type                                                            | Default                                | Required |\n| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------------- | -------- |\n| type      | Behavior type name                                                                                                                                                                                                                                                                                                                                                          | string                                                          | `focus-element`                        | ✓        |\n| animation | Focus animation settings                                                                                                                                                                                                                                                                                                                                                    | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | `{ duration: 500, easing: 'ease-in' }` |          |\n| enable    | Whether to enable the focus feature                                                                                                                                                                                                                                                                                                                                         | boolean \\| ((event: IElementEvent) => boolean)                  | true                                   |          |\n| trigger   | Press this shortcut key in combination with mouse perform foucs element **Key reference:** _<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\" target=\"_blank\" rel=\"noopener noreferrer\">MDN Key Values</a>_. If set to an **empty array**, it means drag element can be performed with mouse without pressing other keys <br/> | string[] \\| (`Control` \\| `Shift`\\| `Alt` \\| `......`)[]        | [`shift`]                              |          |\n\n### ViewportAnimationEffectTiming\n\n```typescript\ntype ViewportAnimationEffectTiming =\n  | boolean // true to enable default animation, false to disable animation\n  | {\n      easing?: string; // Animation easing function: 'ease-in-out', 'ease-in', 'ease-out', 'linear'\n      duration?: number; // Animation duration (milliseconds)\n    };\n```\n\n## Code Examples\n\n### Basic Focus Feature\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['focus-element'],\n});\n```\n\n### Custom Animation Effects\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'focus-element',\n      animation: {\n        duration: 800,\n        easing: 'ease-in-out',\n      },\n    },\n  ],\n});\n```\n\n### Conditional Focus Enablement\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'focus-element',\n      enable: (event) => {\n        // Enable focus only for nodes, not edges\n        return event.target.type === 'node';\n      },\n    },\n  ],\n});\n```\n\n## Practical Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', combo: 'combo1', style: { x: 110, y: 150 } },\n    { id: 'node2', combo: 'combo1', style: { x: 190, y: 150 } },\n    { id: 'node3', combo: 'combo2', style: { x: 150, y: 260 } },\n  ],\n  edges: [{ source: 'node1', target: 'node2' }],\n  combos: [{ id: 'combo1', combo: 'combo2' }, { id: 'combo2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  node: {\n    style: { labelText: (d) => d.id },\n  },\n  data,\n  behaviors: ['collapse-expand', 'focus-element'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/FocusElement.zh.md",
    "content": "---\ntitle: 聚焦元素 FocusElement\norder: 10\n---\n\n## 概述\n\nFocusElement 是 G6 中用于实现元素聚焦功能的内置交互，支持通过点击元素将其聚焦到视图中心。这个交互可以帮助用户快速定位和关注特定的图元素。\n\n## 使用场景\n\n- 快速将关注的节点或边居中显示\n\n## 在线体验\n\n<embed src=\"@/common/api/behaviors/focus-element.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一交互：\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['focus-element'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'focus-element',\n      animation: {\n        duration: 500,\n        easing: 'ease-in',\n      },\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项    | 说明                                                                                                                                                                                                                                                           | 类型                                                            | 默认值                                 | 必选 |\n| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------------- | ---- |\n| type      | 交互类型名称                                                                                                                                                                                                                                                   | string                                                          | `focus-element`                        | ✓    |\n| animation | 聚焦动画效果设置                                                                                                                                                                                                                                               | [ViewportAnimationEffectTiming](#viewportanimationeffecttiming) | `{ duration: 500, easing: 'ease-in' }` |      |\n| enable    | 是否启用聚焦功能                                                                                                                                                                                                                                               | boolean \\| ((event: IElementEvent) => boolean)                  | true                                   |      |\n| trigger   | 同时按下快捷键才能聚焦元素 **按键参考：** _<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/API/UI_Events/Keyboard_event_key_values\" target=\"_blank\" rel=\"noopener noreferrer\">MDN Key Values</a>_ 。若设为**空数组**时则表示不需要按下其他按键配合 <br/> | string[] \\| (`Control` \\| `Shift`\\| `Alt` \\| `......`)[]        | []                                     |      |\n\n### ViewportAnimationEffectTiming\n\n```typescript\ntype ViewportAnimationEffectTiming =\n  | boolean // true 启用默认动画，false 禁用动画\n  | {\n      easing?: string; // 动画缓动函数：'ease-in-out'、'ease-in'、'ease-out'、'linear'\n      duration?: number; // 动画持续时间(毫秒)\n    };\n```\n\n## 代码示例\n\n### 基础聚焦功能\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['focus-element'],\n});\n```\n\n### 自定义动画效果\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'focus-element',\n      animation: {\n        duration: 800,\n        easing: 'ease-in-out',\n      },\n    },\n  ],\n});\n```\n\n### 条件性启用聚焦\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'focus-element',\n      enable: (event) => {\n        // 只对节点启用聚焦，边不聚焦\n        return event.target.type === 'node';\n      },\n    },\n  ],\n});\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', combo: 'combo1', style: { x: 110, y: 150 } },\n    { id: 'node2', combo: 'combo1', style: { x: 190, y: 150 } },\n    { id: 'node3', combo: 'combo2', style: { x: 150, y: 260 } },\n  ],\n  edges: [{ source: 'node1', target: 'node2' }],\n  combos: [{ id: 'combo1', combo: 'combo2' }, { id: 'combo2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  node: {\n    style: { labelText: (d) => d.id },\n  },\n  data,\n  behaviors: ['collapse-expand', 'focus-element'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/HoverActivate.en.md",
    "content": "---\ntitle: HoverActivate\norder: 11\n---\n\n## Overview\n\nHoverActivate is a built-in behavior in G6 used to implement the hover activation effect on elements. When the mouse hovers over nodes or edges, it automatically triggers visual feedback such as highlighting and displaying. This behavior is an important means of enhancing data exploration in graph visualization, helping users quickly focus on target elements and obtain related information.\n\n## Use Cases\n\nThis behavior is mainly used for:\n\n- Quickly locating elements of interest in complex relationship graphs\n- Displaying additional information of nodes through hover\n- Highlighting connection paths by activating edges when analyzing relationships between nodes\n\n## Online Experience\n\n<embed src=\"@/common/api/behaviors/hover-activate.md\"></embed>\n\n## Basic Usage\n\nAdd this behavior in the graph configuration:\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form. This method is simple but only supports default configuration and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['hover-activate'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and can dynamically update the configuration at runtime:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'hover-activate',\n      key: 'hover-activate-1', // Specify an identifier for the behavior for dynamic updates\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option        | Description                                 | Type                                           | Default          | Required |\n| ------------- | ------------------------------------------- | ---------------------------------------------- | ---------------- | -------- |\n| type          | Behavior type name                          | string                                         | `hover-activate` | ✓        |\n| animation     | Whether to enable animation                 | boolean                                        | true             |          |\n| enable        | Whether to enable hover feature             | boolean \\| ((event: IPointerEvent) => boolean) | true             |          |\n| degree        | Degree of relationship to activate elements | number \\| ((event: IPointerEvent) => number);  | 0                |          |\n| direction     | Specify edge direction                      | `both` \\| `in` \\| `out`                        | `both`           |          |\n| state         | State of activated elements                 | string                                         | `active`         |          |\n| inactiveState | State of inactive elements                  | string                                         | -                |          |\n| onHover       | Callback when element is hovered            | (event: IPointerEvent) => void                 | -                |          |\n| onHoverEnd    | Callback when hover ends                    | (event: IPointerEvent) => void                 | -                |          |\n\n### enable\n\n`enable` is used to control whether to enable hover highlighting of elements, and can receive a function for dynamic control\n\nFor example: Enable hover highlighting only for nodes\n\n```typescript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'hover-activate',\n      enable: (e) => {\n        if (e.targetType === 'node') {\n          return true;\n        }\n        return false;\n      },\n    },\n  ],\n});\n```\n\n## Code Examples\n\n### Basic Hover Usage\n\n```typescript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['hover-activate'],\n});\n```\n\n### Node Trigger Highlight\n\n```typescript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'hover-activate',\n      enable: (e) => {\n        if (e.targetType === 'node') {\n          return true;\n        }\n        return false;\n      },\n    },\n  ],\n});\n```\n\n### Flowchart Node Hover Next Node Highlight\n\n```typescript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'hover-activate',\n      degree: 1,\n      direction: 'out',\n      enable: (e) => {\n        if (e.targetType === 'node') {\n          return true;\n        }\n        return false;\n      },\n    },\n  ],\n});\n```\n\n## Practical Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst format = (data) => {\n  const { nodes, edges } = data;\n  return {\n    nodes: nodes.map(({ id, ...node }) => ({ id, data: node })),\n    edges: edges.map(({ id, source, target, ...edge }) => ({ id, source, target, data: edge })),\n  };\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/xiaomi.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: format(data),\n      behaviors: ['hover-activate'],\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        nodeSize: 24,\n      },\n      animation: false,\n    });\n\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/HoverActivate.zh.md",
    "content": "---\ntitle: 悬停激活 HoverActivate\norder: 11\n---\n\n## 概述\n\nHoverActivate 是 G6 中用于实现元素悬停激活效果的内置交互，当鼠标悬停在节点或边上时，会自动触发高亮、显示等视觉反馈。该交互是图可视化中增强数据探索的重要手段，有助于用户快速聚焦目标元素并获取相关信息。\n\n## 使用场景\n\n这一交互主要用于：\n\n- 在复杂关系图中快速定位关注元素\n- 通过悬停信息展示节点额外信息\n- 分析节点间关联关系时，通过激活边凸显连接路径\n\n## 在线体验\n\n<embed src=\"@/common/api/behaviors/hover-activate.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一交互：\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['hover-activate'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'hover-activate',\n      key: 'hover-activate-1', // 为交互指定标识符，方便动态更新\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项        | 说明                 | 类型                                           | 默认值           | 必选 |\n| ------------- | -------------------- | ---------------------------------------------- | ---------------- | ---- |\n| type          | 交互类型名称         | string                                         | `hover-activate` | ✓    |\n| animation     | 是否开启动画效果     | boolean                                        | true             |      |\n| enable        | 是否开启悬浮元素功能 | boolean \\| ((event: IPointerEvent) => boolean) | true             |      |\n| degree        | 激活元素的n度关系    | number \\| ((event: IPointerEvent) => number);  | 0                |      |\n| direction     | 指定边方向           | `both` \\| `in` \\| `out`                        | `both`           |      |\n| state         | 激活元素的状态       | string                                         | `active`         |      |\n| inactiveState | 不激活元素的状态     | string                                         | -                |      |\n| onHover       | 当元素被悬停时的回调 | (event: IPointerEvent) => void                 | -                |      |\n| onHoverEnd    | 当悬停结束时的回调   | (event: IPointerEvent) => void                 | -                |      |\n\n### enable\n\n`enable` 用于控制是否开启元素的悬浮高亮，可接收一个函数来动态控制\n\n例如：只有节点开启悬浮高亮\n\n```typescript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'hover-activate',\n      enable: (e) => {\n        if (e.targetType === 'node') {\n          return true;\n        }\n        return false;\n      },\n    },\n  ],\n});\n```\n\n## 代码示例\n\n### 基础悬浮用法\n\n```typescript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['hover-activate'],\n});\n```\n\n### 节点触发高亮\n\n```typescript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'hover-activate',\n      enable: (e) => {\n        if (e.targetType === 'node') {\n          return true;\n        }\n        return false;\n      },\n    },\n  ],\n});\n```\n\n### 流程图移入节点 下一步节点高亮\n\n```typescript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'hover-activate',\n      degree: 1,\n      direction: 'out',\n      enable: (e) => {\n        if (e.targetType === 'node') {\n          return true;\n        }\n        return false;\n      },\n    },\n  ],\n});\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst format = (data) => {\n  const { nodes, edges } = data;\n  return {\n    nodes: nodes.map(({ id, ...node }) => ({ id, data: node })),\n    edges: edges.map(({ id, source, target, ...edge }) => ({ id, source, target, data: edge })),\n  };\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/xiaomi.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: format(data),\n      behaviors: ['hover-activate'],\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        nodeSize: 24,\n      },\n      animation: false,\n    });\n\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/LassoSelect.en.md",
    "content": "---\ntitle: LassoSelect\norder: 12\n---\n\n## Overview\n\nClick and drag the mouse to draw an **irregular** box to enclose elements, and the elements within the selected range will be selected.\n\n## Use Cases\n\nThis behavior is mainly used for:\n\n- Quickly selecting a batch of elements, making it easier to avoid elements you don't want to select\n- Quickly deselecting a batch of elements, making it easier to avoid elements you want to keep\n\n## Online Experience\n\n<embed src=\"@/common/api/behaviors/lasso-select.md\"></embed>\n\n## Basic Usage\n\nAdd this behavior in the graph configuration:\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form. This method is simple but only supports default configuration and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['lasso-select'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and can dynamically update the configuration at runtime:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'lasso-select',\n      key: 'lasso-select',\n      immediately: true, // Elements are immediately selected when the box encloses them\n      trigger: ['shift', 'alt', 'control'], // Use multiple keys for selection\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option                      | Description                                                                                                                                                                                                                               | Type                                                                                                                           | Default                   | Required |\n| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------- | -------- |\n| type                        | Behavior type name. This plugin is built-in, you can use it by `type: 'lasso-select'`.                                                                                                                                                    | `lasso-select` \\| string                                                                                                       | `lasso-select`            | ✓        |\n| animation                   | Whether to enable animation                                                                                                                                                                                                               | boolean                                                                                                                        | false                     |          |\n| enable                      | Whether to enable lasso selection                                                                                                                                                                                                         | boolean \\| ((event: [Event](/api/event#event-object-properties)) => boolean)                                                   | true                      |          |\n| enableElements              | Types of elements that can be selected                                                                                                                                                                                                    | ( `node` \\| `edge` \\| `combo` )[]                                                                                              | [`node`, `combo`, `edge`] |          |\n| [immediately](#immediately) | Whether to select immediately, only effective when [selection mode](#mode) is `default`                                                                                                                                                   | boolean                                                                                                                        | false                     |          |\n| [mode](#mode)               | Selection mode                                                                                                                                                                                                                            | `union` \\| `intersect` \\| `diff` \\| `default`                                                                                  | `default`                 |          |\n| onSelect                    | Callback for selected element state                                                                                                                                                                                                       | (states:Record&lt;string,string\\|string[]>) =>Record&lt;string,string\\|string[]>                                               |                           |          |\n| state                       | State to switch to when selected                                                                                                                                                                                                          | string \\| `selected` \\| `active` \\| `inactive` \\| `disabled` \\| `highlight`                                                    | `selected`                |          |\n| [style](#style)             | Style of the box during selection                                                                                                                                                                                                         | <a href=\"/manual/element/shape/properties\" target=\"_blank\" rel=\"noopener noreferrer\">RectStyleProps extends BaseStyleProps</a> | [Default](#style)         |          |\n| trigger                     | Press this shortcut key along with mouse click to select **Key reference:** _<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values\" target=\"_blank\" rel=\"noopener noreferrer\">MDN Key Values</a>_ | string[] \\| (`Control` \\| `Shift`\\| `Alt` \\| `......`)[]                                                                       | [`shift`]                 |          |\n\n### immediately\n\nWhether to select immediately, only effective when selection mode is `default`\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'lasso-select',\n      key: 'lasso-select',\n      immediately: true, // Elements are immediately selected when the box encloses them\n      trigger: [], // No need for other keys, just click and drag the mouse to select\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 100, y: 50 } },\n      { id: 'node-2', style: { x: 260, y: 50 } },\n      { id: 'node-3', style: { x: 280, y: 100 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-1', target: 'node-3' },\n      { source: 'node-2', target: 'node-3' },\n    ],\n  },\n  node: {\n    style: { fill: '#7e3feb' },\n  },\n  edge: {\n    stroke: '#8b9baf',\n  },\n  behaviors: [\n    {\n      type: 'lasso-select',\n      key: 'lasso-select',\n      immediately: true, // Immediate selection\n      trigger: [],\n    },\n  ],\n  plugins: [{ type: 'grid-line', size: 30 }],\n});\n\ngraph.render();\n```\n\n### mode\n\nSelection mode\n\n- `union`: Keep the current state of selected elements and add the specified state.\n- `intersect`: Retain the specified state if the selected elements already have it; otherwise, clear the state.\n- `diff`: Toggle the specified state of the selected elements.\n- `default`: Clear the current state of selected elements and add the specified state.\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'lasso-select',\n      key: 'lasso-select',\n      mode: 'default', // Selection mode, default selection mode\n    },\n  ],\n});\n```\n\n```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 200, y: 100 } },\n        { id: 'node-2', style: { x: 360, y: 100 } },\n        { id: 'node-3', style: { x: 280, y: 220 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-1', target: 'node-3' },\n        { source: 'node-2', target: 'node-3' },\n      ],\n    },\n    node: {\n      style: { fill: '#7e3feb' },\n      state: {\n        custom: { fill: '#ffa940' },\n      },\n    },\n    edge: {\n      stroke: '#8b9baf',\n      state: {\n        custom: { stroke: '#ffa940' },\n      },\n    },\n    behaviors: [\n      {\n        type: 'lasso-select',\n        key: 'lasso-select',\n        trigger: [],\n        immediately: true,\n      },\n    ],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      key: 'lasso-select',\n      type: 'lasso-select',\n      animation: false,\n      enable: true,\n      enableElements: ['node', 'edge', 'combo'],\n      mode: 'default',\n      state: 'selected',\n    };\n    const optionFolder = gui.addFolder('lassoSelect Options');\n    optionFolder.add(options, 'type').disable(true);\n\n    optionFolder.add(options, 'state', ['active', 'selected', 'custom']);\n    optionFolder.add(options, 'mode', ['union', 'intersect', 'diff', 'default']);\n    // .onChange((e) => {\n    //   immediately.show(e === 'default');\n    // });\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'lasso-select',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n\n### style\n\n| Property          | Description             | Type                                     | Default   |\n| ----------------- | ----------------------- | ---------------------------------------- | --------- |\n| cursor            | Mouse style             | string                                   |           |\n| fill              | Fill color              | string \\| Pattern \\| null                | `#1677FF` |\n| fillOpacity       | Fill opacity            | number \\| string                         | 0.1       |\n| isBillboard       | Billboard mode          | boolean                                  |           |\n| isSizeAttenuation | Size attenuation        | boolean                                  |           |\n| lineCap           | Line cap style          | `butt` \\| `round` \\| `square`            |           |\n| lineDash          | Dash line config        | number \\| string \\| (string \\| number)[] |           |\n| lineDashOffset    | Dash line offset        | number                                   |           |\n| lineJoin          | Line join style         | `miter` \\| `round` \\| `bevel`            |           |\n| lineWidth         | Line width              | number \\| string                         | 1         |\n| opacity           | Overall opacity         | number \\| string                         |           |\n| radius            | Rectangle corner radius | number \\| string \\| number[]             |           |\n| shadowBlur        | Shadow blur level       | number                                   |           |\n| shadowColor       | Shadow color            | string                                   |           |\n| shadowOffsetX     | Shadow X offset         | number                                   |           |\n| shadowOffsetY     | Shadow Y offset         | number                                   |           |\n| stroke            | Stroke color            | string \\| Pattern \\| null                | `#1677FF` |\n| strokeOpacity     | Stroke opacity          | number \\| string                         |           |\n| visibility        | Visibility              | `visible` \\| `hidden`                    |           |\n| zIndex            | Rendering level         | number                                   | 2         |\n\n**Example**:\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'lasso-select',\n      key: 'lasso-select',\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2], // Dashed outline\n        // RGB super colorful box\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 200, y: 100 } },\n      { id: 'node-2', style: { x: 360, y: 100 } },\n      { id: 'node-3', style: { x: 280, y: 220 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-1', target: 'node-3' },\n      { source: 'node-2', target: 'node-3' },\n    ],\n  },\n  node: {\n    style: { fill: '#7e3feb' },\n  },\n  edge: {\n    stroke: '#8b9baf',\n  },\n  behaviors: [\n    {\n      type: 'lasso-select',\n      key: 'lasso-select',\n      trigger: [],\n      immediately: true,\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2], // Dashed outline\n        // RGB super colorful box\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n  plugins: [{ type: 'grid-line', size: 30 }],\n  animation: true,\n});\n\ngraph.render();\n```\n\n### trigger\n\nPress this shortcut key along with mouse click to select, if set to an **empty array**, it means mouse click to select without needing to press other keys.\n\nNote that setting `trigger` to `['drag']` will cause the `drag-canvas` behavior to be disabled. They cannot be configured simultaneously.\n\n### Practical Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 200, y: 250 } },\n      { id: 'node-2', style: { x: 250, y: 200 } },\n      { id: 'node-3', style: { x: 300, y: 250 } },\n      { id: 'node-4', style: { x: 250, y: 300 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-2', target: 'node-3' },\n      { source: 'node-3', target: 'node-4' },\n      { source: 'node-4', target: 'node-1' },\n    ],\n  },\n  behaviors: [\n    {\n      key: 'lasso-select',\n      type: 'lasso-select',\n      enable: true,\n      animation: false,\n      mode: 'default', // union intersect diff default\n      state: 'selected', // 'active', 'selected', 'inactive', ...\n      trigger: [], // ['Shift', 'Alt', 'Control', 'Drag', 'Meta', ...]\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2],\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/LassoSelect.zh.md",
    "content": "---\ntitle: 套索选择 LassoSelect\norder: 12\n---\n\n## 概述\n\n鼠标点击拖出一个 **不规则的** 框框笼罩元素，精准框选范围内的元素会被选中。\n\n## 使用场景\n\n这一交互主要用于：\n\n- 快速选中一批元素，并且更容易的避开不想选的元素\n- 快速取消选中一批元素，并且更容易的避开想保留的元素\n\n## 在线体验\n\n<embed src=\"@/common/api/behaviors/lasso-select.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一交互：\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['lasso-select'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'lasso-select',\n      key: 'lasso-select',\n      immediately: true, // 可以看到框框笼罩过去时，元素立即被框选了\n      trigger: ['shift', 'alt', 'control'], // 配合多种按键进行框选\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项                      | 说明                                                                                                                                                                                                          | 类型                                                                                                                           | 默认值                    | 必选 |\n| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------- | ---- |\n| type                        | 交互类型名称。此插件已内置，你可以通过 `type: 'lasso-select'` 来使用它。                                                                                                                                      | `lasso-select` \\| string                                                                                                       | `lasso-select`            | ✓    |\n| animation                   | 是否启用动画                                                                                                                                                                                                  | boolean                                                                                                                        | false                     |      |\n| enable                      | 是否启用框选功能                                                                                                                                                                                              | boolean \\| ((event: [Event](/api/event#事件对象属性)) => boolean)                                                              | true                      |      |\n| enableElements              | 可框选的元素类型                                                                                                                                                                                              | ( `node` \\| `edge` \\| `combo` )[]                                                                                              | [`node`, `combo`, `edge`] |      |\n| [immediately](#immediately) | 是否及时框选, 仅在[框选模式 mode](#mode)为 `default` 时生效                                                                                                                                                   | boolean                                                                                                                        | false                     |      |\n| [mode](#mode)               | 框选的选择模式                                                                                                                                                                                                | `union` \\| `intersect` \\| `diff` \\| `default`                                                                                  | `default`                 |      |\n| onSelect                    | 框选元素状态回调                                                                                                                                                                                              | (states:Record&lt;string,string\\|string[]>) =>Record&lt;string,string\\|string[]>                                               |                           |      |\n| state                       | 被选中时切换到该状态                                                                                                                                                                                          | string \\| `selected` \\| `active` \\| `inactive` \\| `disabled` \\| `highlight`                                                    | `selected`                |      |\n| [style](#style)             | 框选时的 框样式                                                                                                                                                                                               | <a href=\"/manual/element/shape/properties\" target=\"_blank\" rel=\"noopener noreferrer\">RectStyleProps extends BaseStyleProps</a> | [默认值](#style)          |      |\n| trigger                     | 按下该快捷键配合鼠标点击进行框选 **按键参考：** _<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/API/UI_Events/Keyboard_event_key_values\" target=\"_blank\" rel=\"noopener noreferrer\">MDN Key Values</a>_ | string[] \\| (`Control` \\| `Shift`\\| `Alt` \\| `......`)[]                                                                       | [`shift`]                 |      |\n\n### immediately\n\n是否及时框选, 仅在框选模式为 `default` 时生效\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'lasso-select',\n      key: 'lasso-select',\n      immediately: true, // 可以看到框框笼罩过去时，元素立即被框选了\n      trigger: [], // 不需要配合其他按键，点击鼠标拖动即可框选\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 100, y: 50 } },\n      { id: 'node-2', style: { x: 260, y: 50 } },\n      { id: 'node-3', style: { x: 280, y: 100 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-1', target: 'node-3' },\n      { source: 'node-2', target: 'node-3' },\n    ],\n  },\n  node: {\n    style: { fill: '#7e3feb' },\n  },\n  edge: {\n    stroke: '#8b9baf',\n  },\n  behaviors: [\n    {\n      type: 'lasso-select',\n      key: 'lasso-select',\n      immediately: true, // 立即框选\n      trigger: [],\n    },\n  ],\n  plugins: [{ type: 'grid-line', size: 30 }],\n});\n\ngraph.render();\n```\n\n### mode\n\n框选的选择模式\n\n- `union`：保持已选元素的当前状态，并添加指定的 state 状态。\n- `intersect`：如果已选元素已有指定的 state 状态，则保留；否则清除该状态。\n- `diff`：对已选元素的指定 state 状态进行取反操作。\n- `default`：清除已选元素的当前状态，并添加指定的 state 状态。\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'lasso-select',\n      key: 'lasso-select',\n      mode: 'default', // 框选模式, 默认框选模式\n    },\n  ],\n});\n```\n\n```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: [\n        { id: 'node-1', style: { x: 200, y: 100 } },\n        { id: 'node-2', style: { x: 360, y: 100 } },\n        { id: 'node-3', style: { x: 280, y: 220 } },\n      ],\n      edges: [\n        { source: 'node-1', target: 'node-2' },\n        { source: 'node-1', target: 'node-3' },\n        { source: 'node-2', target: 'node-3' },\n      ],\n    },\n    node: {\n      style: { fill: '#7e3feb' },\n      state: {\n        custom: { fill: '#ffa940' },\n      },\n    },\n    edge: {\n      stroke: '#8b9baf',\n      state: {\n        custom: { stroke: '#ffa940' },\n      },\n    },\n    behaviors: [\n      {\n        type: 'lasso-select',\n        key: 'lasso-select',\n        trigger: [],\n        immediately: true,\n      },\n    ],\n    plugins: [{ type: 'grid-line', size: 30 }],\n    animation: true,\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = {\n      key: 'lasso-select',\n      type: 'lasso-select',\n      animation: false,\n      enable: true,\n      enableElements: ['node', 'edge', 'combo'],\n      mode: 'default',\n      state: 'selected',\n    };\n    const optionFolder = gui.addFolder('lassoSelect Options');\n    optionFolder.add(options, 'type').disable(true);\n\n    optionFolder.add(options, 'state', ['active', 'selected', 'custom']);\n    optionFolder.add(options, 'mode', ['union', 'intersect', 'diff', 'default']);\n    // .onChange((e) => {\n    //   immediately.show(e === 'default');\n    // });\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.updateBehavior({\n        key: 'lasso-select',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n\n### style\n\n| 属性              | 描述               | 类型                                     | 默认值    |\n| ----------------- | ------------------ | ---------------------------------------- | --------- |\n| cursor            | 鼠标样式           | string                                   |           |\n| fill              | 填充颜色           | string \\| Pattern \\| null                | `#1677FF` |\n| fillOpacity       | 填充透明度         | number \\| string                         | 0.1       |\n| isBillboard       | 是否启用公告牌模式 | boolean                                  |           |\n| isSizeAttenuation | 是否启用大小衰减   | boolean                                  |           |\n| lineCap           | 线段端点样式       | `butt` \\| `round` \\| `square`            |           |\n| lineDash          | 虚线配置           | number \\| string \\| (string \\| number)[] |           |\n| lineDashOffset    | 虚线偏移量         | number                                   |           |\n| lineJoin          | 线段连接处样式     | `miter` \\| `round` \\| `bevel`            |           |\n| lineWidth         | 线宽度             | number \\| string                         | 1         |\n| opacity           | 整体透明度         | number \\| string                         |           |\n| radius            | 矩形圆角半径       | number \\| string \\| number[]             |           |\n| shadowBlur        | 阴影模糊程度       | number                                   |           |\n| shadowColor       | 阴影颜色           | string                                   |           |\n| shadowOffsetX     | 阴影 X 方向偏移    | number                                   |           |\n| shadowOffsetY     | 阴影 Y 方向偏移    | number                                   |           |\n| stroke            | 描边颜色           | string \\| Pattern \\| null                | `#1677FF` |\n| strokeOpacity     | 描边透明度         | number \\| string                         |           |\n| visibility        | 可见性             | `visible` \\| `hidden`                    |           |\n| zIndex            | 渲染层级           | number                                   | 2         |\n\n**示例**：\n\n```js\nconst graph = new Graph({\n  behaviors: [\n    {\n      type: 'lasso-select',\n      key: 'lasso-select',\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2], // 虚线外框\n        // rgb超级炫彩框框\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n});\n```\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 200, y: 100 } },\n      { id: 'node-2', style: { x: 360, y: 100 } },\n      { id: 'node-3', style: { x: 280, y: 220 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-1', target: 'node-3' },\n      { source: 'node-2', target: 'node-3' },\n    ],\n  },\n  node: {\n    style: { fill: '#7e3feb' },\n  },\n  edge: {\n    stroke: '#8b9baf',\n  },\n  behaviors: [\n    {\n      type: 'lasso-select',\n      key: 'lasso-select',\n      trigger: [],\n      immediately: true,\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2], // 虚线外框\n        // rgb超级炫彩框框\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n  plugins: [{ type: 'grid-line', size: 30 }],\n  animation: true,\n});\n\ngraph.render();\n```\n\n### trigger\n\n按下该快捷键配合鼠标点击进行框选，若设为**空数组**时则表示鼠标点击进行框选，不需要按下其他按键配合。\n\n注意，`trigger` 设置为 `['drag']` 时会导致 `drag-canvas` 行为失效。两者不可同时配置。\n\n### 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 200, y: 250 } },\n      { id: 'node-2', style: { x: 250, y: 200 } },\n      { id: 'node-3', style: { x: 300, y: 250 } },\n      { id: 'node-4', style: { x: 250, y: 300 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-2', target: 'node-3' },\n      { source: 'node-3', target: 'node-4' },\n      { source: 'node-4', target: 'node-1' },\n    ],\n  },\n  behaviors: [\n    {\n      key: 'lasso-select',\n      type: 'lasso-select',\n      enable: true,\n      animation: false,\n      mode: 'default', // union intersect diff default\n      state: 'selected', // 'active', 'selected', 'inactive', ...\n      trigger: [], // ['Shift', 'Alt', 'Control', 'Drag', 'Meta', ...]\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2],\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/OptimizeViewportTransform.en.md",
    "content": "---\ntitle: OptimizeViewportTransform\norder: 13\n---\n\n## Overview\n\nOptimizeViewportTransform is a built-in behavior in G6 used to enhance the performance of large-scale graph behaviors.\n\nThis behavior implements a **selective rendering strategy**, temporarily hiding non-critical visual elements during viewport transformations (such as dragging, zooming, scrolling, etc.) to significantly reduce rendering computation load, improve frame rate, and response speed. After the viewport transformation operation ends, the system automatically restores the visibility of all elements after a set delay to ensure complete visual presentation.\n\nThis behavior is implemented based on the [event system](/en/api/event) by listening to the `GraphEvent.BEFORE_TRANSFORM` and `GraphEvent.AFTER_TRANSFORM` events, precisely capturing the start and end timing of viewport transformations, and dynamically controlling element visibility. Therefore, it must be used in conjunction with viewport operation behaviors (such as `drag-canvas`, `zoom-canvas`, or `scroll-canvas`) to be effective.\n\n## Use Cases\n\nThis behavior is mainly used for:\n\n- Smooth behavior of large-scale graphs (thousands of nodes/edges)\n- Performance-sensitive application scenarios\n\n## Basic Usage\n\nAdd this behavior in the graph configuration:\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form. This method is simple but only supports default configuration and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['optimize-viewport-transform'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and can dynamically update the configuration at runtime:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'optimize-viewport-transform',\n      key: 'optimize-viewport-transform-1', // Specify an identifier for the behavior for dynamic updates\n      debounce: 300, // Set a longer debounce time\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option   | Description                                                                                                          | Type                                   | Default                       | Required |\n| -------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ----------------------------- | -------- |\n| type     | Behavior type name                                                                                                   | string                                 | `optimize-viewport-transform` | ✓        |\n| enable   | Whether to enable this behavior                                                                                      | boolean \\| ((event: Event) => boolean) | true                          |          |\n| debounce | How long after the operation ends to restore the visibility of all elements (milliseconds)                           | number                                 | 200                           |          |\n| shapes   | Specify the graphical elements that should remain visible during canvas operations, [configuration options](#shapes) | function                               | `(type) => type === 'node'`   |          |\n\n### Shapes\n\n`shapes` is used to specify the graphical elements that need to remain visible during canvas operations. By default, nodes are always visible, while edges and combos are temporarily hidden during canvas operations to improve performance.\n\n```javascript\n{\n  shapes: (type, shape) => {\n    // Dynamically decide whether to remain visible based on element type and graphical object\n    if (type === 'node') return true; // All nodes remain visible\n    if (type === 'edge' && shape.get('importante')) return true; // Important edges remain visible\n    return false; // Other graphics are hidden\n  };\n}\n```\n\n[Example](#keep-specific-elements-visible)\n\n## Code Examples\n\n### Basic Optimization Functionality\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['drag-canvas', 'zoom-canvas', 'optimize-viewport-transform'],\n});\n```\n\n### Custom Debounce Time\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    'drag-canvas',\n    'zoom-canvas',\n    {\n      type: 'optimize-viewport-transform',\n      debounce: 500, // Set a longer debounce time, restoring visibility of all elements 0.5 seconds after the operation stops\n    },\n  ],\n});\n```\n\n### Keep Specific Elements Visible\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  node: {\n    style: {\n      labelText: 'Drag Canvas!',\n    },\n  },\n  behaviors: [\n    'drag-canvas',\n    'zoom-canvas',\n    {\n      type: 'optimize-viewport-transform',\n      shapes: (type, shape) => {\n        if (type === 'node' && shape.className === 'key') return true;\n        return false;\n      },\n    },\n  ],\n});\n```\n\n> 👇 Try dragging the canvas to see the effect\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 200,\n  data: {\n    nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],\n  },\n  node: {\n    style: {\n      labelText: 'Drag Canvas!',\n    },\n  },\n  behaviors: [\n    'drag-canvas',\n    {\n      type: 'optimize-viewport-transform',\n      shapes: (type, shape) => {\n        if (type === 'node' && shape.className === 'key') return true;\n        return false;\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### Dynamically Enable/Disable Optimization Based on Graph Element Count\n\nYou can dynamically decide whether to enable optimization based on the number of graph elements:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    'drag-canvas',\n    'zoom-canvas',\n    function () {\n      // Enable optimization when exceeding 500 elements\n      const enable = graph.getNodeData().length + graph.getEdgeData().length > 500;\n      return {\n        type: 'optimize-viewport-transform',\n        key: 'optimize-behavior',\n        enable,\n      };\n    },\n  ],\n});\n```\n\n## FAQ\n\n### 1. When should this behavior be used?\n\nWhen the graph contains a large number of nodes and edges (usually more than 500 elements), using this behavior can significantly improve operational smoothness. It is especially useful in environments with high performance requirements or limited hardware performance.\n\n## Practical Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      labelText: (datum) => datum.id,\n    },\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'scroll-canvas', 'optimize-viewport-transform'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/OptimizeViewportTransform.zh.md",
    "content": "---\ntitle: 优化视口变换 OptimizeViewportTransform\norder: 13\n---\n\n## 概述\n\nOptimizeViewportTransform 是 G6 中用于提升大规模图表交互性能的内置交互。\n\n该交互通过实现**选择性渲染策略**，在视口变换过程中（即用户进行拖拽、缩放、滚动等操作时）临时隐藏非关键视觉元素，从而显著降低渲染计算负载，提高帧率和响应速度。当视口变换操作结束后，系统会在设定的延迟时间后自动恢复所有元素的可见性，确保完整的视觉呈现。\n\n此交互基于 [事件系统](/api/event) 实现，通过监听 `GraphEvent.BEFORE_TRANSFORM` 和 `GraphEvent.AFTER_TRANSFORM` 事件，精确捕捉视口变换的开始和结束时机，进而执行元素可见性的动态控制。因此，必须与视口操作类交互（如 `drag-canvas`、`zoom-canvas` 或 `scroll-canvas`）配合使用才能发挥作用。\n\n## 使用场景\n\n这一交互主要用于：\n\n- 大规模图表（上千节点/边）的流畅交互\n- 性能敏感的应用场景\n\n## 基本用法\n\n在图配置中添加这一交互：\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['optimize-viewport-transform'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'optimize-viewport-transform',\n      key: 'optimize-viewport-transform-1', // 为交互指定标识符，方便动态更新\n      debounce: 300, // 设置更长的防抖时间\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项   | 说明                                                          | 类型                                   | 默认值                        | 必选 |\n| -------- | ------------------------------------------------------------- | -------------------------------------- | ----------------------------- | ---- |\n| type     | 交互类型名称                                                  | string                                 | `optimize-viewport-transform` | ✓    |\n| enable   | 是否启用该交互                                                | boolean \\| ((event: Event) => boolean) | true                          |      |\n| debounce | 操作结束后多长时间恢复显示所有元素（毫秒）                    | number                                 | 200                           |      |\n| shapes   | 指定在操作画布过程中始终保持可见的图形元素，[配置项](#shapes) | function                               | `(type) => type === 'node'`   |      |\n\n### Shapes\n\n`shapes` 用于指定在画布操作过程中需要保持可见的图形元素。默认情况下，节点始终可见，而边和组合在操作画布时会被临时隐藏以提升性能。\n\n```javascript\n{\n  shapes: (type, shape) => {\n    // 根据元素类型和图形对象动态决定是否保持可见\n    if (type === 'node') return true; // 所有节点保持可见\n    if (type === 'edge' && shape.get('importante')) return true; // 重要的边保持可见\n    return false; // 其他图形隐藏\n  };\n}\n```\n\n[示例](#保持特定元素可见)\n\n## 代码示例\n\n### 基础优化功能\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['drag-canvas', 'zoom-canvas', 'optimize-viewport-transform'],\n});\n```\n\n### 自定义防抖时间\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    'drag-canvas',\n    'zoom-canvas',\n    {\n      type: 'optimize-viewport-transform',\n      debounce: 500, // 设置更长的防抖时间，在操作停止后0.5秒才恢复显示所有元素\n    },\n  ],\n});\n```\n\n### 保持特定元素可见\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  node: {\n    style: {\n      labelText: 'Drag Canvas!',\n    },\n  },\n  behaviors: [\n    'drag-canvas',\n    'zoom-canvas',\n    {\n      type: 'optimize-viewport-transform',\n      shapes: (type, shape) => {\n        if (type === 'node' && shape.className === 'key') return true;\n        return false;\n      },\n    },\n  ],\n});\n```\n\n> 👇 试试拖拽一下画布，看看效果吧\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 200,\n  data: {\n    nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],\n  },\n  node: {\n    style: {\n      labelText: 'Drag Canvas!',\n    },\n  },\n  behaviors: [\n    'drag-canvas',\n    {\n      type: 'optimize-viewport-transform',\n      shapes: (type, shape) => {\n        if (type === 'node' && shape.className === 'key') return true;\n        return false;\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### 根据图表元素数量动态启用/禁用优化\n\n可以根据图表元素数量动态决定是否启用优化：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    'drag-canvas',\n    'zoom-canvas',\n    function () {\n      // 超出500个元素时启用优化\n      const enable = graph.getNodeData().length + graph.getEdgeData().length > 500;\n      return {\n        type: 'optimize-viewport-transform',\n        key: 'optimize-behavior',\n        enable,\n      };\n    },\n  ],\n});\n```\n\n## 常见问题\n\n### 1. 什么情况下应该使用此交互？\n\n当图表包含大量节点和边（通常超过500个元素）时，使用此交互可以显著提升操作流畅度。在性能要求高或硬件性能有限的环境中尤其有用。\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      labelText: (datum) => datum.id,\n    },\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'scroll-canvas', 'optimize-viewport-transform'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/ScrollCanvas.en.md",
    "content": "---\ntitle: ScrollCanvas\norder: 14\n---\n\n## Overview\n\nScrollCanvas is a built-in behavior in G6 used to implement the canvas scrolling feature, supporting panning the canvas using the mouse wheel or keyboard arrow keys. This interaction is particularly useful for browsing larger charts, allowing users to explore different areas of the chart without changing the zoom level.\n\n## Use Cases\n\nThis behavior is mainly used for:\n\n- Browsing large chart content that exceeds the visible area\n- Exploring different parts of the graph while maintaining the current zoom level\n- Precisely adjusting the view position, especially when precise scrolling is needed in one-dimensional directions\n\n## Online Experience\n\n<embed src=\"@/common/api/behaviors/scroll-canvas.md\"></embed>\n\n## Basic Usage\n\nAdd this behavior in the graph configuration:\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form. This method is simple but only supports default configuration and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['scroll-canvas'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and can dynamically update the configuration at runtime:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'scroll-canvas',\n      key: 'scroll-canvas-1', // Specify an identifier for the behavior for dynamic updates\n      sensitivity: 1.5, // Set sensitivity\n      direction: 'y', // Allow only vertical scrolling\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option         | Description                                                                         | Type                                                                                                                                                                                          | Default                          | Required |\n| -------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | -------- |\n| type           | Behavior type name                                                                  | string                                                                                                                                                                                        | `scroll-canvas`                  | ✓        |\n| enable         | Whether to enable this behavior                                                     | boolean \\| ((event: [WheelEvent](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent) \\| [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent)) => boolean) | true                             |          |\n| direction      | Allowed scrolling direction, [configuration options](#direction)                    | `'x'` \\| `'y'` \\| `undefined`                                                                                                                                                                 | `undefined` (no direction limit) |          |\n| range          | Scrollable viewport range (in viewport size units), [configuration options](#range) | number \\| number[]                                                                                                                                                                            | 1                                |          |\n| sensitivity    | Scrolling sensitivity, the larger the value, the faster the scrolling               | number                                                                                                                                                                                        | 1                                |          |\n| trigger        | Keyboard shortcuts to trigger scrolling, [configuration options](#trigger)          | object                                                                                                                                                                                        | -                                |          |\n| onFinish       | Callback function when scrolling is finished                                        | () => void                                                                                                                                                                                    | -                                |          |\n| preventDefault | Whether to prevent the browser's default event                                      | boolean                                                                                                                                                                                       | true                             |          |\n\n### Direction\n\n`direction` is used to limit the scrolling direction:\n\n- Not set or set to `undefined`: Allow scrolling in any direction\n- Set to `'x'`: Allow only horizontal scrolling\n- Set to `'y'`: Allow only vertical scrolling\n\nThis is useful in specific visualization scenarios, such as in timeline charts where only horizontal scrolling may be needed.\n\n### Range\n\n`range` is used to control the scrollable range of the canvas:\n\n- Set to a single number: Use the same value for all four directions\n- Set to an array: Specify the range for [top, right, bottom, left] directions respectively\n\nFor example:\n\n```javascript\nrange: 2; // Can scroll 2 viewport distances in any direction\nrange: [1, 2, 1, 2]; // Can scroll 1 viewport up and down, 2 viewports left and right\n```\n\nThe value range for each direction is [0, Infinity], where 0 means no scrolling, and Infinity means unlimited scrolling.\n\n### Trigger\n\n`trigger` allows you to configure keyboard arrow keys to control canvas scrolling:\n\n```javascript\n{\n  trigger: {\n    up: ['ArrowUp'],     // Shortcut key for scrolling up\n    down: ['ArrowDown'], // Shortcut key for scrolling down\n    left: ['ArrowLeft'], // Shortcut key for scrolling left\n    right: ['ArrowRight'] // Shortcut key for scrolling right\n  }\n}\n```\n\nYou can also configure combination keys:\n\n```javascript\n{\n  trigger: {\n    up: ['Control', 'ArrowUp'],     // Ctrl + Up Arrow\n    down: ['Control', 'ArrowDown'], // Ctrl + Down Arrow\n    left: ['Control', 'ArrowLeft'], // Ctrl + Left Arrow\n    right: ['Control', 'ArrowRight'] // Ctrl + Right Arrow\n  }\n}\n```\n\n## Code Examples\n\n### Basic Scrolling Functionality\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['scroll-canvas'],\n});\n```\n\n### Allow Only Horizontal Scrolling\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'scroll-canvas',\n      direction: 'x', // Allow only horizontal scrolling\n    },\n  ],\n});\n```\n\n### Custom Scrolling Sensitivity and Range\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'scroll-canvas',\n      sensitivity: 1.8, // Increase scrolling sensitivity\n      range: [0.5, 2, 0.5, 2], // Smaller limits up and down, larger limits left and right\n    },\n  ],\n});\n```\n\n### Control Scrolling with Keyboard Arrow Keys\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'scroll-canvas',\n      trigger: {\n        up: ['ArrowUp'],\n        down: ['ArrowDown'],\n        left: ['ArrowLeft'],\n        right: ['ArrowRight'],\n      },\n    },\n  ],\n});\n```\n\n## FAQ\n\n### 1. What is the difference between ScrollCanvas and ZoomCanvas?\n\n- `ScrollCanvas` is used to pan the canvas without changing the zoom level\n- `ZoomCanvas` is used to zoom the canvas, changing the view's zoom level\n\nThey are often used together to provide complete canvas navigation functionality:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['drag-canvas', 'zoom-canvas', 'scroll-canvas'],\n});\n```\n\n### Practical Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: ['scroll-canvas'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/ScrollCanvas.zh.md",
    "content": "---\ntitle: 滚动画布 ScrollCanvas\norder: 14\n---\n\n## 概述\n\nScrollCanvas 是 G6 中用于实现画布滚动功能的内置交互，支持通过鼠标滚轮或键盘方向键平移画布。这种交互方式对于浏览较大的图表特别有用，能让用户在不改变缩放比例的情况下探索图表的不同区域。\n\n## 使用场景\n\n这一交互主要用于：\n\n- 浏览超出可视区域的大型图表内容\n- 在保持当前缩放比例的情况下探索图的不同部分\n- 精确调整查看位置，尤其是在一维方向上需要精确滚动时\n\n## 在线体验\n\n<embed src=\"@/common/api/behaviors/scroll-canvas.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一交互：\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['scroll-canvas'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'scroll-canvas',\n      key: 'scroll-canvas-1', // 为交互指定标识符，方便动态更新\n      sensitivity: 1.5, // 设置灵敏度\n      direction: 'y', // 只允许垂直方向滚动\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项         | 说明                                                 | 类型                                                                                                                                                                                          | 默认值                   | 必选 |\n| -------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | ---- |\n| type           | 交互类型名称                                         | string                                                                                                                                                                                        | `scroll-canvas`          | ✓    |\n| enable         | 是否启用该交互                                       | boolean \\| ((event: [WheelEvent](https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent) \\| [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent)) => boolean) | true                     |      |\n| direction      | 允许的滚动方向，[配置项](#direction)                 | `'x'` \\| `'y'` \\| `undefined`                                                                                                                                                                 | `undefined` (不限制方向) |      |\n| range          | 可滚动的视口范围(以视口大小为单位)，[配置项](#range) | number \\| number[]                                                                                                                                                                            | 1                        |      |\n| sensitivity    | 滚动灵敏度，值越大滚动速度越快                       | number                                                                                                                                                                                        | 1                        |      |\n| trigger        | 触发滚动的键盘快捷键，[配置项](#trigger)             | object                                                                                                                                                                                        | -                        |      |\n| onFinish       | 滚动完成时的回调函数                                 | () => void                                                                                                                                                                                    | -                        |      |\n| preventDefault | 是否阻止浏览器默认事件                               | boolean                                                                                                                                                                                       | true                     |      |\n\n### Direction\n\n`direction` 用于限制滚动的方向：\n\n- 不设置或设为 `undefined`：允许在任意方向滚动\n- 设为 `'x'`：只允许水平方向滚动\n- 设为 `'y'`：只允许垂直方向滚动\n\n这在特定的可视化场景下很有用，例如在时间轴图表中可能只需要水平滚动。\n\n### Range\n\n`range` 用于控制画布可滚动的范围：\n\n- 设置为单个数字：四个方向使用相同的值\n- 设置为数组：分别指定 [上, 右, 下, 左] 四个方向的范围\n\n例如：\n\n```javascript\nrange: 2; // 在任何方向上都可以滚动2个视口的距离\nrange: [1, 2, 1, 2]; // 上下方向可滚动1个视口，左右方向可滚动2个视口\n```\n\n每个方向的取值范围是 [0, Infinity]，0表示不能滚动，Infinity表示无限滚动。\n\n### Trigger\n\n`trigger` 允许你配置键盘方向键来控制画布滚动：\n\n```javascript\n{\n  trigger: {\n    up: ['ArrowUp'],     // 向上滚动的快捷键\n    down: ['ArrowDown'], // 向下滚动的快捷键\n    left: ['ArrowLeft'], // 向左滚动的快捷键\n    right: ['ArrowRight'] // 向右滚动的快捷键\n  }\n}\n```\n\n你也可以配置组合键：\n\n```javascript\n{\n  trigger: {\n    up: ['Control', 'ArrowUp'],     // Ctrl + 上箭头\n    down: ['Control', 'ArrowDown'], // Ctrl + 下箭头\n    left: ['Control', 'ArrowLeft'], // Ctrl + 左箭头\n    right: ['Control', 'ArrowRight'] // Ctrl + 右箭头\n  }\n}\n```\n\n## 代码示例\n\n### 基础滚动功能\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['scroll-canvas'],\n});\n```\n\n### 只允许水平滚动\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'scroll-canvas',\n      direction: 'x', // 只允许水平滚动\n    },\n  ],\n});\n```\n\n### 自定义滚动灵敏度和范围\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'scroll-canvas',\n      sensitivity: 1.8, // 提高滚动灵敏度\n      range: [0.5, 2, 0.5, 2], // 上下方向限制较小，左右方向限制较大\n    },\n  ],\n});\n```\n\n### 使用键盘方向键控制滚动\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'scroll-canvas',\n      trigger: {\n        up: ['ArrowUp'],\n        down: ['ArrowDown'],\n        left: ['ArrowLeft'],\n        right: ['ArrowRight'],\n      },\n    },\n  ],\n});\n```\n\n## 常见问题\n\n### 1. ScrollCanvas 和 ZoomCanvas 有什么区别？\n\n- `ScrollCanvas` 用于平移画布，不改变缩放比例\n- `ZoomCanvas` 用于缩放画布，改变视图的缩放比例\n\n两者常结合使用，提供完整的画布导航功能：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['drag-canvas', 'zoom-canvas', 'scroll-canvas'],\n});\n```\n\n### 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: ['scroll-canvas'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/ZoomCanvas.en.md",
    "content": "---\ntitle: ZoomCanvas\nordeR: 15\n---\n\n## Overview\n\nZoomCanvas is a built-in behavior in G6 used to implement the canvas zooming feature, supporting zooming in and out of the canvas using the mouse wheel or keyboard shortcuts. This is one of the most commonly used interactions in graph visualization, helping users view both the overall structure and local details of the graph.\n\n## Use Cases\n\nThis behavior is mainly used for:\n\n- Browsing large-scale graph data, freely switching between the whole and details\n- Focusing on specific areas for detailed analysis\n\n## Online Experience\n\n<embed src=\"@/common/api/behaviors/zoom-canvas.md\"></embed>\n\n## Basic Usage\n\nAdd this behavior in the graph configuration:\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string form. This method is simple but only supports default configuration and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['zoom-canvas'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and can dynamically update the configuration at runtime:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'zoom-canvas',\n      key: 'zoom-canvas-1', // Specify an identifier for the behavior for dynamic updates\n      sensitivity: 1.5, // Set sensitivity\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option         | Description                                                                                            | Type                                                                                | Default             | Required |\n| -------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | ------------------- | -------- |\n| type           | Behavior type name                                                                                     | string                                                                              | `zoom-canvas`       | ✓        |\n| animation      | Zoom animation effect settings                                                                         | [ViewportAnimationEffectTiming](/manual/graph/option#viewportanimationeffecttiming) | `{ duration: 200 }` |          |\n| enable         | Whether to enable this behavior                                                                        | boolean \\| ((event: Event) => boolean)                                              | true                |          |\n| origin         | Zoom center point (viewport coordinates)                                                               | [Point](/api/viewport#point)                                                        | -                   |          |\n| onFinish       | Callback function when zooming is finished                                                             | () => void                                                                          | -                   |          |\n| preventDefault | Whether to prevent the browser's default event                                                         | boolean                                                                             | true                |          |\n| sensitivity    | Zoom sensitivity, the larger the value, the faster the zoom                                            | number                                                                              | 1                   |          |\n| trigger        | How to trigger zooming, supports mouse wheel and keyboard shortcuts, [configuration options](#trigger) | string[] \\| object                                                                  | -                   |          |\n\n### Trigger\n\n`trigger` has two usage methods, suitable for different scenarios:\n\n#### Method 1: Modifier keys combined with the mouse wheel\n\nIf you want to trigger zooming only when certain keys are pressed while scrolling the mouse wheel, you can configure it like this:\n\n```javascript\n{\n  trigger: ['Control']; // Hold down the Control key and scroll the mouse wheel to zoom\n}\n```\n\nCommon modifier keys include:\n\n- `Control`\n- `Shift`\n- `Alt`\n\n> Not sure what value corresponds to a keyboard key? Refer to [MDN Key Values](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values).\n\n#### Method 2: Pure keyboard shortcuts\n\nIf you want to control zooming entirely using the keyboard, you can set up key combinations:\n\n```javascript\n{\n  trigger: {\n    zoomIn: ['Control', '+'],  // Zoom in shortcut\n    zoomOut: ['Control', '-'], // Zoom out shortcut\n    reset: ['Control', '0']    // Reset zoom ratio shortcut\n  }\n}\n```\n\n## Code Examples\n\n### Basic Zoom Functionality\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['zoom-canvas'],\n});\n```\n\n### Custom Zoom Center\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    function () {\n      return {\n        type: 'zoom-canvas',\n        origin: this.getCanvasCenter(), // Zoom with the viewport center as the origin\n      };\n    },\n  ],\n});\n```\n\n### Custom Zoom Sensitivity\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'zoom-canvas',\n      sensitivity: 0.8, // Lower sensitivity for smoother zoom changes\n    },\n  ],\n});\n```\n\n### Zoom with Shift + Mouse Wheel\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'zoom-canvas',\n      trigger: ['Shift'], // Hold down the Shift key and scroll to zoom\n    },\n  ],\n});\n```\n\n### Control Zoom with Keyboard Shortcuts\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    {\n      type: 'zoom-canvas',\n      trigger: {\n        zoomIn: ['Control', '='], // Ctrl + = to zoom in\n        zoomOut: ['Control', '-'], // Ctrl + - to zoom out\n        reset: ['Control', '0'], // Ctrl + 0 to reset\n      },\n    },\n  ],\n});\n```\n\n### Supports pinch-to-zoom on mobile devices\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'zoom-canvas',\n      // Other configurations for the PC side...\n    },\n    function () {\n      return {\n        type: 'zoom-canvas',\n        trigger: ['pinch'],\n        sensitivity: 0.8, // Lower sensitivity for smoother zoom changes\n        origin: this.getCanvasCenter(), // Zoom with the viewport center as the origin\n      };\n    },\n  ],\n});\n```\n\n## FAQ\n\n### 1. What if the canvas zoom exceeds the expected range?\n\nTo avoid excessive zooming in or out, you can set zoom limits:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  zoomRange: [0.5, 3], // Allow zooming out to 50% and zooming in to 300%\n  behaviors: ['zoom-canvas'],\n});\n```\n\n### 2. How to use it with other interactions?\n\nZooming and dragging are common combinations for a complete navigation experience:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n});\n```\n\n### 3. Conflicts when using two-finger touchpad input and scroll-canvas simultaneously\n\nOn a touchpad, both two-finger swipe (for scrolling) and pinch (for zooming) gestures are often interpreted as `wheel` events.\n\nBecause both `zoom-canvas` and `scroll-canvas` respond to `wheel` events by default, using them together can cause conflicts, such as a single gesture triggering both scrolling and zooming.\n\nYou can resolve this by checking the `event.ctrlKey` property. On most platforms, a pinch gesture sets `event.ctrlKey` to `true`, while a swipe does not. This allows you to conditionally enable `zoom-canvas` only for pinch gestures.\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }],\n  },\n  behaviors: [\n    'scroll-canvas',\n    {\n      key: 'custom-zoom-canvas',\n      type: 'zoom-canvas',\n      enable: (event) => {\n        return event.ctrlKey; // When ctrlKey is true, it performs a two-finger pinch or spread operation; when false, it performs a two-finger swipe operation.\n      },\n    },\n  ],\n});\ngraph.render();\n```\n\n## Practical Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: ['zoom-canvas'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/ZoomCanvas.zh.md",
    "content": "---\ntitle: 缩放画布 ZoomCanvas\nordeR: 15\n---\n\n## 概述\n\nZoomCanvas 是 G6 中用于实现画布缩放功能的内置交互，支持通过鼠标滚轮或键盘快捷键调整画布缩放比例。这是图可视化中最常用的交互之一，能帮助用户查看图的整体结构和局部细节。\n\n## 使用场景\n\n这一交互主要用于：\n\n- 浏览大规模图数据，在整体与细节之间自由切换\n- 聚焦到特定区域进行详细分析\n\n## 在线体验\n\n<embed src=\"@/common/api/behaviors/zoom-canvas.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一交互：\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['zoom-canvas'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'zoom-canvas',\n      key: 'zoom-canvas-1', // 为交互指定标识符，方便动态更新\n      sensitivity: 1.5, // 设置灵敏度\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 配置项         | 说明                                                     | 类型                                                                                | 默认值              | 必选 |\n| -------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------- | ---- |\n| type           | 交互类型名称                                             | string                                                                              | `zoom-canvas`       | ✓    |\n| animation      | 缩放动画效果设置                                         | [ViewportAnimationEffectTiming](/manual/graph/option#viewportanimationeffecttiming) | `{ duration: 200 }` |      |\n| enable         | 是否启用该交互                                           | boolean \\| ((event: Event) => boolean)                                              | true                |      |\n| origin         | 缩放中心点(视口坐标)                                     | [Point](/api/viewport#point)                                                        | -                   |      |\n| onFinish       | 缩放完成时的回调函数                                     | () => void                                                                          | -                   |      |\n| preventDefault | 是否阻止浏览器默认事件                                   | boolean                                                                             | true                |      |\n| sensitivity    | 缩放灵敏度，值越大缩放速度越快                           | number                                                                              | 1                   |      |\n| trigger        | 触发缩放的方式，支持滚轮和键盘快捷键，[配置项](#trigger) | string[] \\| object                                                                  | -                   |      |\n\n### Trigger\n\n`trigger`有两种使用方式，分别适用于不同场景：\n\n#### 方式一：与滚轮结合的修饰键\n\n当你希望只有在按下某些键的同时滚动滚轮才触发缩放时，可以这样配置：\n\n```javascript\n{\n  trigger: ['Control']; // 按住 Control 键同时滚动鼠标滚轮才能缩放\n}\n```\n\n常见的修饰键有：\n\n- `Control`\n- `Shift`\n- `Alt`\n\n> 不知道键盘按键对应什么值？请参考 [MDN Key Values](https://developer.mozilla.org/zh-CN/docs/Web/API/UI_Events/Keyboard_event_key_values)。\n\n#### 方式二：纯键盘快捷键\n\n当你希望完全使用键盘控制缩放操作时，可以设置组合键：\n\n```javascript\n{\n  trigger: {\n    zoomIn: ['Control', '+'],  // 放大快捷键\n    zoomOut: ['Control', '-'], // 缩小快捷键\n    reset: ['Control', '0']    // 重置缩放比例快捷键\n  }\n}\n```\n\n## 代码示例\n\n### 基础缩放功能\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  behaviors: ['zoom-canvas'],\n});\n```\n\n### 自定义缩放中心\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    function () {\n      return {\n        type: 'zoom-canvas',\n        origin: this.getCanvasCenter(), // 以视口中心为原点进行缩放\n      };\n    },\n  ],\n});\n```\n\n### 自定义缩放灵敏度\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'zoom-canvas',\n      sensitivity: 0.8, // 降低灵敏度，缩放变化更平缓\n    },\n  ],\n});\n```\n\n### 使用Shift+滚轮进行缩放\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'zoom-canvas',\n      trigger: ['Shift'], // 按住 Shift 键同时滚动才能缩放\n    },\n  ],\n});\n```\n\n### 使用键盘快捷键控制缩放\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'zoom-canvas',\n      trigger: {\n        zoomIn: ['Control', '='], // Ctrl + = 放大\n        zoomOut: ['Control', '-'], // Ctrl + - 缩小\n        reset: ['Control', '0'], // Ctrl + 0 重置\n      },\n    },\n  ],\n});\n```\n\n### 支持移动端双指缩放\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    {\n      type: 'zoom-canvas',\n      // PC 端其他配置...\n    },\n    function () {\n      return {\n        type: 'zoom-canvas',\n        trigger: ['pinch'],\n        sensitivity: 0.8, // 降低灵敏度，缩放变化更平缓\n        origin: this.getCanvasCenter(), // 以视口中心为原点进行缩放\n      };\n    },\n  ],\n});\n```\n\n## 常见问题\n\n### 1. 画布缩放超出了预期范围怎么办？\n\n为避免缩放过大或过小，可以设置缩放限制：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  zoomRange: [0.5, 3], // 允许缩小到50%和放大到300%\n  behaviors: ['zoom-canvas'],\n});\n```\n\n### 2. 如何与其他交互结合使用？\n\n缩放与拖拽是常见的组合，实现完整的导航体验：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n});\n```\n\n### 3. 触控板双指操作下，与 scroll-canvas 同时使用时的冲突\n\n在触控板上，双指滑动（用于滚动）和双指捏合（用于缩放）手势通常都会被解析为 `wheel` 事件。\n\n由于 `zoom-canvas` 和 `scroll-canvas` 默认都会响应 `wheel` 事件，当它们同时使用时会产生冲突，例如一个手势会同时触发滚动和缩放。\n\n你可以通过检查 `event.ctrlKey` 属性来解决这个问题。在多数平台上，捏合手势会使 `event.ctrlKey` 为 `true`，而滑动则不会。这允许你有条件地仅为捏合手势启用 `zoom-canvas`。\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }],\n  },\n  behaviors: [\n    'scroll-canvas',\n    {\n      key: 'custom-zoom-canvas',\n      type: 'zoom-canvas',\n      enable: (event) => {\n        return event.ctrlKey; // ctrlKey 为 true 时，是双指捏合或扩张操作，false 时是双指滑动操作\n      },\n    },\n  ],\n});\ngraph.render();\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: ['zoom-canvas'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/custom-behavior.en.md",
    "content": "---\ntitle: Custom Behavior\norder: 16\n---\n\n## Overview\n\nCustom behavior allows users to define one or more related interaction behaviors as a complete interaction based on the comprehensive [event mechanism](/en/api/event) provided by G6, thereby achieving interaction logic that fits business scenarios.\n\n### Execution Logic of Behavior\n\nTypically:\n\n1. Listen for user interaction events\n\n2. Update the canvas or perform other operations based on the events\n\nFor example, the `DragCanvas` behavior listens for pointer drag events and updates the camera position based on the drag distance.\n\n### Difference Between Behavior and Plugin\n\n- Both behavior and plugin base classes are derived from the [BaseExtension](https://github.com/antvis/G6/blob/v5/packages/g6/src/registry/extension/index.ts) base class within G6, so the implementation methods for behavior and plugin are basically the same.\n- However, based on the concept of visualization, behavior is usually used to handle user interaction events, while plugins are usually used to handle canvas rendering logic, additional component rendering, etc.\n\n:::info{title=Tip}\nDue to conceptual distinctions, behavior instances cannot be obtained, while plugin instances can be obtained ([getPluginInstance](/en/api/plugin#graphgetplugininstancekey)).\n:::\n\n## When to Use Custom Behavior?\n\n- **Purpose**:\n\n  When users need to implement interaction logic that fits business scenarios, we usually need to cooperate with G6's event system to respond to related events and execute the required interaction logic.\n\n- **Without Custom Behavior**:\n\n  If custom behavior is not used, users need to perform a series of event listening and response processing through `graph.on` after creating a Graph instance, making code logic processing and orchestration extremely difficult.\n\n- **Advantages of Behavior**:\n\n  Each behavior is an independent code module, and the existence of the behavior system facilitates users to decouple business logic, avoid code bloat, and facilitate subsequent maintenance.\n\n- **Conclusion**:\n\n  > 1. When users need to implement any interaction logic, they should first consider custom behavior.\n  > 2. When built-in behavior cannot fully meet business needs, users can also adjust and modify through custom behavior (inheriting built-in behavior).\n  >\n  > _(If the features supported by built-in behavior are more general, or if there are bugs in built-in behavior, you are welcome to submit issues or PRs on [Github](https://github.com/antvis/G6))_\n\n## Implementing Behavior\n\nThe implementation of a behavior is quite flexible, and you can implement your behavior in your preferred style.\n\nBelow is a simple custom behavior implementation. When the user clicks on the canvas, a node is added to the canvas (the fill color of the added node can be defined through behavior configuration):\n\n```typescript\nimport type { BaseBehaviorOptions, RuntimeContext, IPointerEvent } from '@antv/g6';\nimport { BaseBehavior, CanvasEvent } from '@antv/g6';\n\ninterface ClickAddNodeOptions extends BaseBehaviorOptions {\n  fill: string;\n}\n\nexport class ClickAddNode extends BaseBehavior<ClickAddNodeOptions> {\n  static defaultOptions: Partial<ClickAddNodeOptions> = {\n    fill: 'red',\n  };\n  constructor(context: RuntimeContext, options: ClickAddNodeOptions) {\n    super(context, Object.assign({}, ClickAddNode.defaultOptions, options));\n    this.bindEvents();\n  }\n  private bindEvents() {\n    const { graph } = this.context;\n    graph.on(CanvasEvent.CLICK, this.addNode);\n  }\n  private addNode = (event: IPointerEvent) => {\n    const { graph } = this.context;\n    const { layerX, layerY } = event.nativeEvent as PointerEvent;\n    graph.addNodeData([\n      {\n        id: 'node-' + Date.now(),\n        style: { x: layerX, y: layerY, fill: this.options.fill },\n      },\n    ]);\n    graph.draw();\n  };\n  private unbindEvents() {\n    const { graph } = this.context;\n    graph.off(CanvasEvent.CLICK, this.addNode);\n  }\n  public destroy() {\n    // Unbind events when destroyed\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n```\n\n- In the example code, we implemented a `ClickAddNode` behavior, which adds an event listener to the Graph in the constructor. When the user clicks on the canvas, a node is added at the click position, and the fill color of the added node can be configured.\n- `BaseBehavior` is the base class for all behaviors, and each custom behavior needs to inherit this base class.\n\n> Click on the blank area of the canvas below to add a node, and switch the right panel to configure the node color.\n\n<embed src=\"@/common/manual/custom-extension/behavior/implement-behaviors.md\"></embed>\n\n:::info{title=Tip}\nThe above example is the simplest behavior implementation. In actual development, you may also need to handle logic such as enabling and disabling behaviors.\n\nIn addition, there may be event conflicts between multiple behaviors, and you need to handle these conflicts carefully.\n:::\n\n## Registering Behavior\n\nRegister through the register method provided by G6\n\n```typescript\nimport { ExtensionCategory, register } from '@antv/g6';\nimport { ClickAddNode } from 'your-custom-behavior-path';\n\nregister(ExtensionCategory.BEHAVIOR, 'click-add-node', ClickAddNode);\n```\n\n## Configuring Behavior\n\nYou can pass in the behavior type name or configuration parameter object in `behaviors`, such as the above ClickAddNode. See [Configuring Behavior](/en/manual/behavior/overview#配置和使用) for details.\n\n```typescript\nconst graph = new Graph({\n  // Other configurations\n  behaviors: [\n    {\n      type: 'click-add-node',\n      fill: 'blue',\n    },\n  ],\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/custom-behavior.zh.md",
    "content": "---\ntitle: 自定义交互\norder: 16\n---\n\n## 概述\n\n自定义交互允许用户基于 G6 提供的完善的[事件机制](/api/event)，把一个或多个相关的交互行为定义为一个完整的交互，以此实现符合业务场景的交互逻辑。\n\n### 交互的执行逻辑\n\n通常为：\n\n1. 监听用户交互事件\n\n2. 根据事件更新画布或执行其他操作\n\n例如 `DragCanvas` 交互监听指针拖拽事件，根据拖拽的距离更新相机的位置。\n\n### 交互与插件的区别\n\n- 交互和插件的基类都是由 G6 内部的 [BaseExtension](https://github.com/antvis/G6/blob/v5/packages/g6/src/registry/extension/index.ts) 基类派生而来，因此交互和插件的实现方式基本相同\n- 但基于可视化的概念区分，交互通常用于处理用户交互事件，而插件通常用于处理画布的渲染逻辑、额外组件渲染等\n\n:::info{title=提示}\n因概念上的区分，交互实例不可获取，插件实例可获取（ [getPluginInstance](/api/plugin#graphgetplugininstancekey) ）\n:::\n\n## 什么时候需要自定义交互？\n\n- **目的**：\n\n  当用户在实现符合业务场景的交互逻辑时，我们通常需要配合 G6 的事件系统，对相关事件作出响应，执行需要的交互逻辑。\n\n- **不使用自定义交互**：\n\n  如果不使用自定义交互，用户需要在创建 Graph 实例后，通过 `graph.on` 进行一系列的事件监听和响应处理，代码逻辑处理和编排会显得异常艰难。\n\n- **交互的优势**：\n\n  每个交互行为都是独立的代码模块，交互系统的存在方便用户解耦业务逻辑、避免代码臃肿以及方便用户后续维护等。\n\n- **结论**：\n\n  > 1、当用户需要实现任何交互逻辑时，应当首先考虑自定义交互。\n  >\n  > 2、当内置交互无法完全满足业务需求时，用户也可以通过自定义交互（继承内置交互）进行调整和修改。\n  >\n  > _（如果需要内置交互支持的特性是较通用的，或者内置交互存在 Bug ，这种时候欢迎大家到 [Github](https://github.com/antvis/G6) 提 Issue 或者 PR ）_\n\n## 实现交互\n\n一个交互的实现相当灵活，你可以以你喜欢的风格实现你的交互。\n\n下面是一个简单的自定义交互实现，当用户点击画布时，会在画布上添加一个节点（可通过交互配置定义所添加节点的填充颜色）：\n\n```typescript\nimport type { BaseBehaviorOptions, RuntimeContext, IPointerEvent } from '@antv/g6';\nimport { BaseBehavior, CanvasEvent } from '@antv/g6';\n\ninterface ClickAddNodeOptions extends BaseBehaviorOptions {\n  fill: string;\n}\n\nexport class ClickAddNode extends BaseBehavior<ClickAddNodeOptions> {\n  static defaultOptions: Partial<ClickAddNodeOptions> = {\n    fill: 'red',\n  };\n  constructor(context: RuntimeContext, options: ClickAddNodeOptions) {\n    super(context, Object.assign({}, ClickAddNode.defaultOptions, options));\n    this.bindEvents();\n  }\n  private bindEvents() {\n    const { graph } = this.context;\n    graph.on(CanvasEvent.CLICK, this.addNode);\n  }\n  private addNode = (event: IPointerEvent) => {\n    const { graph } = this.context;\n    const { layerX, layerY } = event.nativeEvent as PointerEvent;\n    graph.addNodeData([\n      {\n        id: 'node-' + Date.now(),\n        style: { x: layerX, y: layerY, fill: this.options.fill },\n      },\n    ]);\n    graph.draw();\n  };\n  private unbindEvents() {\n    const { graph } = this.context;\n    graph.off(CanvasEvent.CLICK, this.addNode);\n  }\n  public destroy() {\n    // 销毁时解绑事件\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n```\n\n- 在示例代码中，我们实现了一个 `ClickAddNode` 交互，该交互在构造函数中添加了 Graph 的事件监听，当用户点击画布时，会在点击位置添加一个节点，并且支持配置所添加节点的填充颜色。\n- `BaseBehavior` 是所有交互的基类，每个自定义交互都需要继承这个基类实现。\n\n> 点击下面画布中的空白位置以添加一个节点，可切换右侧面板配置节点颜色\n\n<embed src=\"@/common/manual/custom-extension/behavior/implement-behaviors.md\"></embed>\n\n:::info{title=提示}\n上述示例是一个最简单的交互实现，实际开发过程中，你可能还需要处理交互的启用与禁用等逻辑。\n\n此外，多个交互之间可能会有事件冲突，你需要小心处理这些冲突。\n:::\n\n## 注册交互\n\n通过 G6 提供的 register 方法注册即可\n\n```typescript\nimport { ExtensionCategory, register } from '@antv/g6';\nimport { ClickAddNode } from 'your-custom-behavior-path';\n\nregister(ExtensionCategory.BEHAVIOR, 'click-add-node', ClickAddNode);\n```\n\n## 配置交互\n\n可在 `behaviors` 中传入交互类型名称或配置参数对象，比如上面的 ClickAddNode ，详见[配置交互](/manual/behavior/overview#配置和使用)\n\n```typescript\nconst graph = new Graph({\n  // 其他配置\n  behaviors: [\n    {\n      type: 'click-add-node',\n      fill: 'blue',\n    },\n  ],\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/overview.en.md",
    "content": "---\ntitle: Behavior Overview\norder: 0\n---\n\n## What is Behavior\n\n<image width=\"200px\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*sa3jRqp83K4AAAAAAAAAAAAADmJ7AQ/original\" />\n\nBehavior refers to the interactive operations between users and chart elements, such as dragging the canvas, selecting nodes, zooming the view, etc. Good behavior design allows users to explore and understand graph data more intuitively. **Proper configuration of behaviors is a key step in building efficient and usable charts**.\n\n### Changes in G6 5.0 Behavior System\n\nG6 5.0 removed the concept of \"Behavior Mode\" (Mode), and directly lists the required behavior behaviors in `behaviors`, simplifying the configuration. This makes behavior configuration more intuitive and easier to get started with.\n\n```javascript {4}\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  behaviors: ['drag-canvas', 'zoom-canvas', 'click-select'],\n});\n```\n\n## Built-in Behaviors\n\nG6 provides a variety of built-in behaviors that are **ready to use without registration**:\n\n| Category            | Behavior Name                                                                  | Registration Type             | Function Description                                         |\n| ------------------- | ------------------------------------------------------------------------------ | ----------------------------- | ------------------------------------------------------------ |\n| Navigation          |                                                                                |                               |                                                              |\n|                     | [Drag Canvas](/en/manual/behavior/drag-canvas)                                 | `drag-canvas`                 | Drag the entire canvas view                                  |\n|                     | [Zoom Canvas](/en/manual/behavior/zoom-canvas)                                 | `zoom-canvas`                 | Zoom the canvas view                                         |\n|                     | [Scroll Canvas](/en/manual/behavior/scroll-canvas)                             | `scroll-canvas`               | Scroll the canvas using the wheel                            |\n|                     | [Optimize Viewport Transform](/en/manual/behavior/optimize-viewport-transform) | `optimize-viewport-transform` | Optimize view transform performance                          |\n| Selection           |                                                                                |                               |                                                              |\n|                     | [Click Select](/en/manual/behavior/click-select)                               | `click-select`                | Click to select graph elements                               |\n|                     | [Brush Select](/en/manual/behavior/brush-select)                               | `brush-select`                | Select elements by dragging a rectangular area               |\n|                     | [Lasso Select](/en/manual/behavior/lasso-select)                               | `lasso-select`                | Freely draw an area to select elements                       |\n| Editing             |                                                                                |                               |                                                              |\n|                     | [Create Edge](/en/manual/behavior/create-edge)                                 | `create-edge`                 | Interactively create new edges                               |\n|                     | [Drag Element](/en/manual/behavior/drag-element)                               | `drag-element`                | Drag nodes or combos                                         |\n|                     | [Force-directed Drag](/en/manual/behavior/drag-element-force)                  | `drag-element-force`          | Drag nodes in force-directed layout                          |\n| Data Exploration    |                                                                                |                               |                                                              |\n|                     | [Collapse/Expand](/en/manual/behavior/collapse-expand)                         | `collapse-expand`             | Expand or collapse subtree nodes                             |\n|                     | [Focus Element](/en/manual/behavior/focus-element)                             | `focus-element`               | Focus on specific elements and automatically adjust the view |\n|                     | [Hover Activate](/en/manual/behavior/hover-activate)                           | `hover-activate`              | Highlight elements when hovering                             |\n| Visual Optimization |                                                                                |                               |                                                              |\n|                     | [Fix Element Size](/en/manual/behavior/fix-element-size)                       | `fix-element-size`            | Fix the element size to a specified value                    |\n|                     | [Auto-adapt Label](/en/manual/behavior/auto-adapt-label)                       | `auto-adapt-label`            | Automatically adjust label position                          |\n\nFor detailed configuration of each behavior, refer to the [Built-in Behavior Documentation](/en/manual/behavior/drag-canvas).\n\n:::warning{title=Behavior Compatibility}\nSome behaviors may overlap in triggering mechanisms, such as `brush-select` and `drag-canvas` both using mouse dragging. In such cases, you can avoid conflicts by modifying the trigger key (e.g., hold `Shift` to drag and select).\n:::\n\n## Custom Behaviors\n\nWhen built-in behaviors cannot meet the requirements, G6 provides powerful customization capabilities:\n\n- Extend by inheriting built-in behaviors\n- Create entirely new behavior behaviors\n\nUnlike built-in behaviors, **custom behaviors need to be registered before use**. For detailed tutorials, refer to the [Custom Behavior](/en/manual/behavior/custom-behavior) documentation.\n\n## Configuration and Usage\n\n### Basic Configuration\n\nThe simplest way is to directly specify the required behaviors through the `behaviors` array when initializing the graph instance:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['drag-canvas', 'zoom-canvas', 'click-select'],\n});\n```\n\n### Configure Behavior Parameters\n\nFor behaviors that require custom parameters, you can configure properties using the `object` form:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: [\n    'drag-canvas',\n    {\n      type: 'zoom-canvas',\n      sensitivity: 1.5, // Configure sensitivity\n      key: 'zoom-behavior', // Specify a key for the behavior for subsequent updates\n    },\n  ],\n});\n```\n\n### Dynamically Update Behaviors\n\nG6 supports dynamically managing behavior behaviors during the runtime of the graph instance to meet complex behavior needs:\n\nYou can adjust behaviors using the [setBehaviors](/en/api/behavior#graphsetbehaviorsbehaviors) method:\n\n```javascript\n// Add new behavior\ngraph.setBehaviors((behaviors) => [...behaviors, 'lasso-select']);\n\n// Remove behavior\ngraph.setBehaviors((behaviors) => behaviors.filter((b) => b !== 'click-select'));\n```\n\nYou can update the configuration of behaviors using the [updateBehavior](/en/api/behavior#graphupdatebehaviorbehavior) method:\n\n```javascript\n// Update a single behavior\ngraph.updateBehavior({\n  key: 'zoom-behavior',\n  sensitivity: 2,\n  enable: false, // Disable the behavior\n});\n```\n\n:::warning{title=Note}\nWhen using the `updateBehavior` method, you need to specify a unique `key` for the behavior during initialization.\n:::\n\n### Uninstall Behaviors\n\nYou can also uninstall behaviors using the [setBehaviors](/en/api/behavior#graphsetbehaviorsbehaviors) method by setting the behavior configuration list to empty:\n\n```javascript\ngraph.setBehaviors([]);\n```\n\nFor more behavior-related APIs, refer to the [Behavior API Documentation](/en/api/behavior).\n\n## Behavior and Events\n\nBehaviors are essentially implemented through event listening and response. Although built-in behaviors have encapsulated common behavior behaviors, you can also directly implement custom behavior logic through the event API.\n\n### Event Listening Example\n\n```javascript\n// Use event constants (recommended)\nimport { NodeEvent, EdgeEvent } from '@antv/g6';\n\n// Listen for node clicks\ngraph.on(NodeEvent.CLICK, (evt) => {\n  const { target } = evt;\n  graph.setElementState(target.id, 'selected');\n});\n\n// Listen for edge hover\ngraph.on(EdgeEvent.POINTER_OVER, (evt) => {\n  const { target } = evt;\n  graph.setElementState(target.id, 'highlight');\n});\n```\n\nThe event system is the foundation for implementing behaviors. Mastering the event API is crucial for understanding and extending behavior behaviors. For more event-related information, refer to the [Event Documentation](/en/api/event).\n"
  },
  {
    "path": "packages/site/docs/manual/behavior/overview.zh.md",
    "content": "---\ntitle: 交互总览\norder: 0\n---\n\n## 什么是交互\n\n<image width=\"200px\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*sa3jRqp83K4AAAAAAAAAAAAADmJ7AQ/original\" />\n\n交互(Behavior)是指用户与图表元素之间的互动操作，如拖拽画布、选择节点、缩放视图等。良好的交互设计能让用户更直观地探索和理解图数据。**合理配置交互是构建高效可用图表的关键环节**。\n\n### G6 5.0 交互系统变化\n\nG6 5.0 移除了 \"交互模式\"(Mode) 概念，直接在 `behaviors` 中列出需要的交互行为，简化了配置方式。这使得交互配置更加直观，上手更加简单。\n\n```javascript {4}\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  behaviors: ['drag-canvas', 'zoom-canvas', 'click-select'],\n});\n```\n\n## 内置交互\n\nG6 提供了多种开箱即用的内置交互，**无需注册，直接配置即可使用**：\n\n| 分类     | 交互名称                                                     | 注册类型                      | 功能描述                   |\n| -------- | ------------------------------------------------------------ | ----------------------------- | -------------------------- |\n| 导航     |                                                              |                               |                            |\n|          | [拖拽画布](/manual/behavior/drag-canvas)                     | `drag-canvas`                 | 拖动整个画布视图           |\n|          | [缩放画布](/manual/behavior/zoom-canvas)                     | `zoom-canvas`                 | 缩放画布视图               |\n|          | [滚动画布](/manual/behavior/scroll-canvas)                   | `scroll-canvas`               | 使用滚轮滚动画布           |\n|          | [优化视口变换](/manual/behavior/optimize-viewport-transform) | `optimize-viewport-transform` | 优化视图变换性能           |\n| 选择     |                                                              |                               |                            |\n|          | [点击选择](/manual/behavior/click-select)                    | `click-select`                | 点击选择图元素             |\n|          | [框选](/manual/behavior/brush-select)                        | `brush-select`                | 通过拖拽矩形区域选择元素   |\n|          | [套索选择](/manual/behavior/lasso-select)                    | `lasso-select`                | 自由绘制区域选择元素       |\n| 编辑     |                                                              |                               |                            |\n|          | [创建边](/manual/behavior/create-edge)                       | `create-edge`                 | 交互式创建新的边           |\n|          | [拖拽元素](/manual/behavior/drag-element)                    | `drag-element`                | 拖动节点或组合             |\n|          | [力导向拖拽](/manual/behavior/drag-element-force)            | `drag-element-force`          | 力导向布局中拖动节点       |\n| 数据探索 |                                                              |                               |                            |\n|          | [折叠/展开](/manual/behavior/collapse-expand)                | `collapse-expand`             | 展开或收起子树节点         |\n|          | [聚焦元素](/manual/behavior/focus-element)                   | `focus-element`               | 聚焦特定元素，自动调整视图 |\n|          | [悬停激活](/manual/behavior/hover-activate)                  | `hover-activate`              | 鼠标悬停时高亮元素         |\n| 视觉优化 |                                                              |                               |                            |\n|          | [固定元素大小](/manual/behavior/fix-element-size)            | `fix-element-size`            | 将元素大小固定为指定值     |\n|          | [自适应标签](/manual/behavior/auto-adapt-label)              | `auto-adapt-label`            | 自动调整标签位置           |\n\n各交互的详细配置可参考 [内置交互文档](/manual/behavior/drag-canvas)。\n\n:::warning{title=交互兼容性}\n某些交互在触发机制上可能存在重叠，如 `brush-select` 和 `drag-canvas` 都使用鼠标拖拽。这种情况下可以通过修改触发按键（如按住 `Shift` 拖拽选择）来避免冲突。\n:::\n\n## 自定义交互\n\n当内置交互无法满足需求时，G6 提供了强大的自定义能力：\n\n- 继承内置交互进行扩展\n- 创建全新的交互行为\n\n与内置交互不同，**自定义交互需要先注册后使用**。详细教程请参考 [自定义交互](/manual/behavior/custom-behavior) 文档。\n\n## 配置和使用\n\n### 基本配置\n\n最简单的方式是在图实例初始化时，通过 `behaviors` 数组直接指定需要的交互：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['drag-canvas', 'zoom-canvas', 'click-select'],\n});\n```\n\n### 配置交互参数\n\n对于需要自定义参数的交互，可以使用 `object` 的形式配置属性：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: [\n    'drag-canvas',\n    {\n      type: 'zoom-canvas',\n      sensitivity: 1.5, // 配置灵敏度\n      key: 'zoom-behavior', // 为交互指定key，便于后续更新\n    },\n  ],\n});\n```\n\n### 动态更新交互\n\nG6 支持在图实例运行期间动态管理交互行为，满足复杂交互需求：\n\n可以通过 [setBehaviors](/api/behavior#graphsetbehaviorsbehaviors) 方法调整交互：\n\n```javascript\n// 添加新交互\ngraph.setBehaviors((behaviors) => [...behaviors, 'lasso-select']);\n\n// 移除交互\ngraph.setBehaviors((behaviors) => behaviors.filter((b) => b !== 'click-select'));\n```\n\n可以使用 [updateBehavior](/api/behavior#graphupdatebehaviorbehavior) 方法更新交互的配置：\n\n```javascript\n// 更新单个交互\ngraph.updateBehavior({\n  key: 'zoom-behavior',\n  sensitivity: 2,\n  enable: false, // 禁用该交互\n});\n```\n\n:::warning{title=注意}\n使用`updateBehavior`方法时，需要在初始化时为交互指定唯一的`key`。\n:::\n\n### 卸载交互\n\n使用 [setBehaviors](/api/behavior#graphsetbehaviorsbehaviors) 方法同样可以卸载交互，将交互配置列表置为空即可：\n\n```javascript\ngraph.setBehaviors([]);\n```\n\n更多与交互相关的 API 请参考 [交互 API 文档](/api/behavior)。\n\n## 交互与事件\n\n交互本质上是通过事件监听和响应来实现的。虽然内置交互已经封装了常见的交互行为，但你也可以通过事件 API 直接实现自定义交互逻辑。\n\n### 事件监听示例\n\n```javascript\n// 使用事件常量（推荐）\nimport { NodeEvent, EdgeEvent } from '@antv/g6';\n\n// 监听节点点击\ngraph.on(NodeEvent.CLICK, (evt) => {\n  const { target } = evt;\n  graph.setElementState(target.id, 'selected');\n});\n\n// 监听边悬停\ngraph.on(EdgeEvent.POINTER_OVER, (evt) => {\n  const { target } = evt;\n  graph.setElementState(target.id, 'highlight');\n});\n```\n\n事件系统是实现交互的基础，掌握事件 API 对于理解和扩展交互行为至关重要。更多事件相关信息，请参考 [事件文档](/api/event)。\n"
  },
  {
    "path": "packages/site/docs/manual/contribute.en.md",
    "content": "---\ntitle: contribute\norder: 14\n---\n\nTo contribute, you need to understand G6's code structure and development workflow. The code repository for G6 is located at: https://github.com/antvis/G6\n\nIf you want to fix a bug or add a new feature, you need to first fork a copy of the code to your repository, then make modifications in your repository, and finally submit a Pull Request (PR) to the G6 repository.\n\n## Project Structure\n\n<Tree>\n  <ul>\n    <li>\n      packages\n      <ul>\n        <li>\n          g6\n          <small>G6 Core Implementation</small>\n          <ul>\n            <li>\n              __tests__\n              <small>Testing and Development Environment</small>\n              <ul>\n                <li>\n                  assets\n                  <small>Test Static Resources</small>\n                </li>\n                <li>\n                  bugs\n                  <small>Bugfix Test Case</small>\n                </li>\n                <li>\n                  dataset\n                  <small>Test Dataset</small>\n                </li>\n                <li>\n                  demos\n                  <small>Development Demos</small>\n                </li>\n                <li>\n                  snapshots\n                  <small>Test Snapshots</small>\n                </li>\n                <li>\n                  perf\n                  <small>Performance Test Case</small>\n                </li>\n                <li>\n                  perf-report\n                  <small>Performance Test Reports</small>\n                </li>\n                <li>\n                  unit\n                  <small>Test Cases</small>\n                </li>\n                <li>\n                  utils\n                  <small>Test Utility Functions</small>\n                </li>\n              </ul>\n            </li>\n            <li>\n              src\n              <ul>\n                <li>\n                  animations\n                  <small>Animation Executors and Built-in Animations</small>   \n                </li>\n                <li>\n                  behaviors\n                  <small>Built-in Interactions</small>   \n                </li>\n                <li>\n                  constants\n                  <small>Constants and Enumeration Values</small>   \n                </li>\n                <li>\n                  elements\n                  <small>Built-in Elements</small>\n                  <ul>\n                    <li>\n                      combos\n                      <small>Built-in Combos</small>\n                    </li>\n                    <li>\n                      edges\n                      <small>Built-in Edges</small>\n                    </li>\n                    <li>\n                      nodes\n                      <small>Built-in Nodes</small>\n                    </li>\n                    <li>\n                      shapes\n                      <small>Composite Shapes</small>\n                    </li>\n                  </ul>\n                </li>\n                <li>\n                  layouts\n                  <small>Layout References and Encapsulation</small>   \n                </li>\n                <li>\n                  palettes\n                  <small>Built-in Palettes</small>   \n                </li>\n                <li>\n                  plugins\n                  <small>Built-in Plugins</small>   \n                </li>\n                <li>\n                  registry\n                  <small>Registry Module</small>   \n                </li>\n                <li>\n                  runtime\n                  <small>Graph and Core Controller</small>   \n                </li>\n                <li>\n                  spec\n                  <small>Specification Type Definitions</small>   \n                </li>\n                <li>\n                  themes\n                  <small>Built-in Themes</small>   \n                </li>\n                <li>\n                  transforms\n                  <small>Built-in Data Transformations</small>   \n                </li>\n                <li>\n                  types\n                  <small>Type Definitions</small>   \n                </li>\n                <li>\n                  utils\n                  <small>Utility Functions</small>   \n                </li>\n                <li>\n                  exports.ts\n                  <small>Export Items</small>   \n                </li>\n                <li>\n                  preset.ts\n                  <small>Pre-operations</small>\n                </li>\n              </ul>\n            </li>\n            <li>\n              vite.config.js\n              <small>Development Environment Vite Configuration</small>\n            </li>\n          </ul>\n        </li>\n        <li>\n          g6-extension-3d/src\n          <small>3D Extension</small>\n          <ul>\n            <li>\n              behaviors\n              <small>3D Behaviors</small>\n            </li>\n            <li>\n              elements\n              <small>3D Elements</small>\n            </li>\n            <li>\n              plugins\n              <small>3D Plugins</small>\n            </li>\n            <li>\n              renderer.ts\n              <small>3D Renderer</small>\n            </li>\n          </ul>\n        </li>\n        <li>\n          g6-extension-react/src\n          <small>React Node Extension</small>\n          <ul>\n            <li>\n              elements\n              <small>React Elements</small>\n            </li>\n            <li>\n              graph\n              <small>React Graph Encapsulation</small>\n            </li>\n          </ul>\n        </li>\n        <li>\n          site\n          <small>Official Website and Documentation</small>\n          <ul>\n            <li>\n              docs\n              <small>Tutorials and API</small>\n            </li>\n            <li>\n              examples\n              <small>Graph Examples</small>\n            </li>\n            <li>\n              .dumirc.ts\n              <small>Configuration File</small>\n            </li>\n          </ul>\n        </li>\n      </ul>\n    </li>\n\n  </ul>\n</Tree>\n\n## Development Process\n\n1. Fork and Pull the Code\n\nFork the G6 repository on Github to your account, then clone it locally.\n\n```bash\n# Navigate to your workspace\ncd /path/to/your/workspace\n\n# Navigate to the G6 directory\ngit clone git@github.com:[your username]/G6.git\n```\n\n2. Install Dependencies\n\n:::warning{title=Warning}\nPlease ensure your local environment meets the following requirements:\n\n- [Node.js](https://nodejs.org/) version >= 18\n- [pnpm](https://pnpm.io/) version >= 8\n\n:::\n\n```bash\n# Enter the G6 code directory\ncd G6\n\n# Install Dependencies\npnpm install\n```\n\n3. Start the Development Environment\n\n```bash\n# Enter the g6 code directory.\ncd ./packages/g6\n\n# Start the development environment\npnpm dev\n```\n\nAt this point, you can access G6's development environment and preview the examples by visiting http://127.0.0.1:8080 in your web browser.\n\n4. Develop New Features or Fix Bugs\n\nSwitch to a development branch:\n\n```bash\ngit checkout -b [branch name]\n```\n\nMake code modifications according to your needs and test locally.\n\n5. Write Test Cases\n\nIn the `packages/g6/__tests__/unit` directory, write test cases to ensure your code behaves as expected.\n\nValidate Your Code with Tests：\n\n```bash\npnpm test\n```\n\n6. Submit a Pull Request (PR)\n\n```bash\n# Stage changes\ngit add .\n\n# Commit Changes\ngit commit -m \"[commit type]: commit message\"\n\n# Push to Your Repository\ngit push\n```\n\nSubmit a Pull Request (PR) to the G6 repository on GitHub.\n\n## Testing and Coverage\n\nG6 utilizes Jest for conducting unit tests, with the test cases situated in the `packages/g6/__tests__/unit` directory.\n\nWe require that all code submissions must pass tests to ensure code quality.\n\nThe coverage rate for the current PR (Pull Request) submission is advised not to fall below the coverage rate of the existing codebase, and it is <text style=\"color: red;\">not to fall below 90%</text>.\n\n### Update Test Snapshots\n\nG6 extends Jest tests and provides the `toMatchSnapshot` assertion for generating and comparing snapshots.\n\nIf the current possible modification affects the generation of some screenshots, it is necessary to check whether there are failed test cases by executing `pnpm test`.\n\nWhen a failed test case is found, the console will print the path of the failed test, as well as the path information of the benchmark screenshot and the current screenshot. You can hold the `Ctrl` or `Command` key and click the path to view the specific test case or screenshot.\n\n<img width=\"500px\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*39j5TKAePWsAAAAAAAAAAAAADmJ7AQ/original\"/>\n\nIf it is confirmed that this modification is correct, then please manually delete the corresponding benchmark screenshots and regenerate the screenshots:\n\n**Regenerate all screenshots**:\n\n1. Delete all files in the `packages/g6/__tests__/unit/snapshots` directory\n2. Execute `pnpm test`\n\n**Regenerate a single screenshot**:\n\n1. Delete the corresponding file in the `packages/g6/__tests__/snapshots` directory (test cases in the `unit` directory will generate the corresponding directory under `snapshots`)\n2. Execute `npx jest __tests__/unit/xx/xxx.spec.ts`\n\n## Code Standards\n\nThe G6 code adheres to the following standards:\n\n- eslint:recommended\n- @typescript-eslint/recommended\n- jsdoc/recommended-error\n\n## Commit Conventions\n\nG6 adopts the [Conventional Commits](https://www.conventionalcommits.org/) specification, and the commit message format is as follows:\n\n```\n<type>[optional scope]: <description>\n```\n\nThe type field can be one of the following:\n\n- feat: A new feature\n- fix: A bug fix\n- docs: Documentation update\n- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.)\n- refactor: Code change that neither fixes a bug nor adds a feature\n- perf: A code change that improves performance\n- test: Adding missing or correcting existing tests\n- build: Changes that affect the build system or external dependencies\n- ci: Changes to our CI configuration files and scripts\n- chore: Other changes that don't modify src or test files\n- revert: Reverts a previous commit\n\nFor example:\n\n```\nfeat: add new feature\nrefactor(behavior): refactor drag-canvas behavior\n```\n\nPlease ensure that your commit messages conform to the standards, and try to use English descriptions whenever possible. This helps us better manage the code.\n\n## Pull Request (PR) Standards\n\nAfter completing the above steps, you can submit a PR to the G6 repository. Please ensure that your PR adheres to the following standards:\n\n- A single PR should address only one issue.\n- The title of the PR should be concise and clear.\n- The description of the PR should be clear and detailed. If the changes involve the user interface, please include screenshots.\n- The PR must pass CI (Continuous Integration) checks.\n\n## Code Review\n\nAfter your PR is submitted, we will review your code. Please be patient and await the results of the review. If there are any areas that require modification, we will point them out within the PR.\n\n## Release Process\n\nWe regularly release new versions. If your PR is for a non-urgent bug fix, it will be included in the next release. If your PR addresses an urgent bug fix, we will release a new version as soon as possible.\n\nThis project uses changeset to manage version release, and the specific release process is as follows:\n\n1. Complete related development work\n2. Create a branch from v5 (any name you want)\n3. Run `npm run version` command, fill in the information according to the prompt, and the version number will be updated automatically\n4. Commit the changes to the remote repository\n5. Create a PR on GitHub, add the `publish` label, and merge the branch to v5\n6. After the branch is merged, GitHub Actions will be triggered automatically, and the package will be published to npm\n7. After the release, the Release note needs to be updated. Execute \"pnpm tag\" in the packages/g6\n8. Fill in the tag information on the newly opened Github link. First, select the previous tag, and then select the current tag to obtain the changes. After confirming that there are no issues, release it.\n"
  },
  {
    "path": "packages/site/docs/manual/contribute.zh.md",
    "content": "---\ntitle: 参与贡献\norder: 14\n---\n\n要参与贡献，你需要了解 G6 的代码结构和开发流程。G6 的代码仓库地址是：https://github.com/antvis/G6\n\n如果你想要修复一个 bug 或者增加一个新功能，你需要先 fork 一份代码到你的仓库，然后在你的仓库中进行修改，最后提交 PR 到 G6 的仓库。\n\n## 项目结构\n\n<Tree>\n  <ul>\n    <li>\n      packages\n      <ul>\n        <li>\n          g6\n          <small>G6 核心实现</small>\n          <ul>\n            <li>\n              __tests__\n              <small>测试及开发环境</small>\n              <ul>\n                <li>\n                  assets\n                  <small>测试静态资源</small>\n                </li>\n                <li>\n                  bugs\n                  <small>Bug 修复测试用例</small>\n                </li>\n                <li>\n                  dataset\n                  <small>测试数据集</small>\n                </li>\n                <li>\n                  demos\n                  <small>开发示例</small>\n                </li>\n                <li>\n                  snapshots\n                  <small>测试截图</small>\n                </li>\n                <li>\n                  perf\n                  <small>性能测试用例</small>\n                </li>\n                <li>\n                  perf-report\n                  <small>性能测试报告</small>\n                </li>\n                <li>\n                  unit\n                  <small>测试用例</small>\n                </li>\n                <li>\n                  utils\n                  <small>测试工具函数</small>\n                </li>\n              </ul>\n            </li>\n            <li>\n              src\n              <ul>\n                <li>\n                  animations\n                  <small>动画执行器及内置动画</small>   \n                </li>\n                <li>\n                  behaviors\n                  <small>内置交互</small>   \n                </li>\n                <li>\n                  constants\n                  <small>常量及枚举值</small>   \n                </li>\n                <li>\n                  elements\n                  <small>内置元素</small>\n                  <ul>\n                    <li>\n                      combos\n                      <small>内置组合</small>\n                    </li>\n                    <li>\n                      edges\n                      <small>内置边</small>\n                    </li>\n                    <li>\n                      nodes\n                      <small>内置节点</small>\n                    </li>\n                    <li>\n                      shapes\n                      <small>复合图形</small>\n                    </li>\n                  </ul>\n                </li>\n                <li>\n                  layouts\n                  <small>布局引用及封装</small>   \n                </li>\n                <li>\n                  palettes\n                  <small>内置色板</small>   \n                </li>\n                <li>\n                  plugins\n                  <small>内置插件</small>   \n                </li>\n                <li>\n                  registry\n                  <small>注册模块</small>   \n                </li>\n                <li>\n                  runtime\n                  <small>Graph 及核心控制器</small>   \n                </li>\n                <li>\n                  spec\n                  <small>Specification 类型定义</small>   \n                </li>\n                <li>\n                  themes\n                  <small>内置主题</small>   \n                </li>\n                <li>\n                  transforms\n                  <small>内置数据转换</small>   \n                </li>\n                <li>\n                  types\n                  <small>类型定义</small>   \n                </li>\n                <li>\n                  utils\n                  <small>工具函数</small>   \n                </li>\n                <li>\n                  exports.ts\n                  <small>导出项</small>   \n                </li>\n                <li>\n                  preset.ts\n                  <small>预操作</small>\n                </li>\n              </ul>\n            </li>\n            <li>\n              vite.config.js\n              <small>开发环境 Vite 配置</small>\n            </li>\n          </ul>\n        </li>\n        <li>\n          g6-extension-3d/src\n          <small>3D 扩展</small>\n          <ul>\n            <li>\n              behaviors\n              <small>3D 交互</small>\n            </li>\n            <li>\n              elements\n              <small>3D 元素</small>\n            </li>\n            <li>\n              plugins\n              <small>3D 插件</small>\n            </li>\n            <li>\n              renderer.ts\n              <small>3D 渲染器</small>\n            </li>\n          </ul>\n        </li>\n        <li>\n          g6-extension-react/src\n          <small>React 节点扩展</small>\n          <ul>\n            <li>\n              elements\n              <small>React 元素</small>\n            </li>\n            <li>\n              graph\n              <small>React Graph 封装</small>\n            </li>\n          </ul>\n        </li>\n        <li>\n          site\n          <small>官网及文档</small>\n          <ul>\n            <li>\n              docs\n              <small>教程及 API</small>\n            </li>\n            <li>\n              examples\n              <small>图表示例</small>\n            </li>\n            <li>\n              .dumirc.ts\n              <small>配置文件</small>\n            </li>\n          </ul>\n        </li>\n      </ul>\n    </li>\n\n  </ul>\n</Tree>\n\n## 开发流程\n\n1. Fork 并拉取代码\n\n在 Github 中 Fork [G6](https://github.com/antvis/G6) 到你的仓库，并拉取到本地。\n\n```bash\n# 进入你的工作目录\ncd /path/to/your/workspace\n\n# 克隆 G6 代码\ngit clone git@github.com:[your username]/G6.git\n```\n\n2. 安装依赖\n\n:::warning{title=注意}\n请验证你的本地环境是否符合要求：\n\n- [Node.js](https://nodejs.org/) 版本 >= 18\n- [pnpm](https://pnpm.io/) 版本 >= 8\n\n:::\n\n```bash\n# 进入 G6 代码目录\ncd G6\n\n# 安装依赖\npnpm install\n```\n\n3. 启动开发环境\n\n```bash\n# 进入 G6 代码目录\ncd ./packages/g6\n\n# 启动开发环境\npnpm dev\n```\n\n此时，你可以在浏览器中访问 `http://127.0.0.1:8080` 查看 G6 的开发环境并预览开发示例。\n\n4. 开发新功能或修复 bug\n\n切换到开发分支：\n\n```bash\ngit checkout -b [branch name]\n```\n\n根据你的需求，修改代码并在本地测试。\n\n5. 编写测试用例\n\n在 `packages/g6/__tests__/unit` 目录下编写测试用例，确保你的代码符合预期。\n\n确保你的代码通过测试：\n\n```bash\npnpm test\n```\n\n6. 提交 PR\n\n```bash\n# 添加修改\ngit add .\n\n# 提交修改\ngit commit -m \"[commit type]: commit message\"\n\n# 推送到你的仓库\ngit push\n```\n\n在 Github 中提交 PR 到 G6 仓库。\n\n## 测试与覆盖率\n\nG6 使用 Jest 进行单元测试，测试用例位于 `packages/g6/__tests__/unit` 目录下。\n\n我们要求所有的代码提交都需要通过测试，确保代码质量。\n\n当前 PR 提交的覆盖率不建议低于当前代码库的覆盖率，且<text style=\"color: red;\">不得低于 90%</text>。\n\n### 更新测试截图\n\nG6 扩展了 Jest 测试，提供了 `toMatchSnapshot` 断言用于生成以及对比快照。\n\n如果当前可能修改影响了部分截图的生成，需要通过执行 `pnpm test` 检查是否有测试用例失败。\n\n当发现测试失败的用例时，控制台会打印出失败的测试路径，以及基准截图和当前截图的路径信息。你可以按住 `Ctrl` 或 `Command` 键并点击路径，查看具体的测试用例或截图。\n\n<img width=\"500px\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*39j5TKAePWsAAAAAAAAAAAAADmJ7AQ/original\"/>\n\n如果确认本次修改是正确的，那么请手动删除对应的基准截图，并重新生成截图：\n\n**重新生成全部截图**：\n\n1. 删除 `packages/g6/__tests__/unit/snapshots` 目录下的所有文件\n2. 执行 `pnpm test`\n\n**重新生成单个截图**：\n\n1. 删除 `packages/g6/__tests__/snapshots` 目录下对应的文件（`unit`目录下测试用例会在`snapshots`下生成对应的目录）\n2. 执行 `npx jest __tests__/unit/xx/xxx.spec.ts`\n\n## 代码规范\n\nG6 编码尊循以下规范：\n\n- eslint:recommended\n- @typescript-eslint/recommended\n- jsdoc/recommended-error\n\n## 提交规范\n\nG6 采用 [Conventional Commits](https://www.conventionalcommits.org/) 规范，提交信息格式如下：\n\n```\n<type>[optional scope]: <description>\n```\n\ntype 有以下几种：\n\n- feat: 新功能\n- fix: 修复 bug\n- docs: 文档更新\n- style: 代码格式（不影响代码运行的变动）\n- refactor: 重构\n- perf: 性能优化\n- test: 测试\n- build: 构建工具相关的变动\n- ci: CI 配置\n- chore: 其他无关紧要的变动\n- revert: 撤销\n\n例如：\n\n```\nfeat: add new feature\nrefactor(behavior): refactor drag-canvas behavior\n```\n\n请确保你的提交信息符合规范，并尽量使用英文描述，这样有助于我们更好地管理代码。\n\n## PR 规范\n\n完成上述步骤后，你可以提交 PR 到 G6 仓库。请确保你的 PR 符合以下规范：\n\n- 一个 PR 只解决一个问题\n- PR 的标题简洁明了\n- PR 的描述清晰详细，涉及视图的变动请附上截图\n- PR 必需能够通过 CI 检查\n\n## 代码 Review\n\nPR 提交后，我们会对你的代码进行 Review。请耐心等待 Review 结果，如果有需要修改的地方，我们会在 PR 中提出。\n\n## 发布流程\n\n我们会定期发布新版本，如果你的 PR 是非紧急缺陷修复，我们会在下一个版本中发布。如果你的 PR 是紧急缺陷修复，我们会尽快发布新版本。\n\n本项目通过 changeset 来管理版本发布，具体的发布流程如下：\n\n1. 完成相关的开发工作\n2. 从 v5 分支创建一个分支（任意分支名均可）\n3. 根目录执行 `npm run version` 命令，根据提示填写相关信息，会自动更新版本号\n4. 将变更提交到远程仓库\n5. 在 GitHub 上创建一个 PR，并添加 `publish` 标签，将该分支合并到 v5 分支\n6. 分支合并后，会自动触发 GitHub Actions，发布到 npm\n7. 发布后，需更新 Release note，在 packages/g6 目录下执行 pnpm tag\n8. 在新打开的 Github 链接填写 tag 信息，先选择前一个 tag, 然后选择当前 tag 后得到变更，确认没有问题后发布\n"
  },
  {
    "path": "packages/site/docs/manual/data.en.md",
    "content": "---\ntitle: Data\norder: 1\n---\n\n## Overview\n\nG6 is a data-driven charting library, where data is one of the most important concepts. In G6, data is the core of the chart, and both display and interaction are based on data.\n\nCommon graph data formats include:CSV, [DOT](https://graphviz.org/doc/info/lang.html), GDF, GML, [GraphML](http://graphml.graphdrawing.org/), [GEXF](https://gexf.net/) etc。\n\nG6 uses JSON format to describe the graph structure, which includes information about nodes and edges. Here is a simple JSON data example:\n\n```json\n{\n  \"nodes\": [{ \"id\": \"node1\" }, { \"id\": \"node2\" }],\n  \"edges\": [{ \"source\": \"node1\", \"target\": \"node2\" }]\n}\n```\n\nCompared to the other formats mentioned above, the JSON format has a more intuitive and understandable data structure. It is also more flexible, allowing for easy expansion of node and edge attributes.\n\nIt is a data exchange format widely supported by computers, so you do not have to worry about data format compatibility issues.\n\n## Data Structure\n\nIn G6, graph data consists of three parts: `nodes` (node data), `edges` (edge data), and `combos` (combo data). Each part corresponds to different elements in the graph, and their types and data determine how the graph is displayed.\n\n```ts\ninterface GraphData {\n  nodes: NodeData[]; // Node data\n  edges?: EdgeData[]; // Edge data (optional)\n  combos?: ComboData[]; // Combo data (optional)\n}\n```\n\n### Node Data\n\nA node is the basic building block of a graph and represents an entity within the graph. Each node has a unique `id` used to identify it, and nodes can also have data, styles, and states.\n\n| Attribute                                 | Type               | Description                                                                                                        |\n| ----------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------ |\n| <Badge type=\"success\">Required</Badge> id | _string_           | Unique identifier for the node, used to distinguish different nodes                                                |\n| type                                      | _string_           | Node type. It can be the type of built-in Node, or the custom Node                                                 |\n| data                                      | _Object_           | Custom data for the node, such as name, description, etc. Can be accessed in style mappings via callback functions |\n| style                                     | _Object_           | Node style, including position, size, color, and other visual properties                                           |\n| states                                    | _string[]_         | Initial states for the node, such as selected, active, hover, etc.                                                 |\n| combo                                     | _string_ \\| _null_ | ID of the combo the node belongs to. Used to organize hierarchical relationships. If none, it is null              |\n| children                                  | _string[]_         | Collection of child node IDs, used only in tree diagrams                                                           |\n\n**Example:**\n\n```json\n{\n  \"id\": \"node-1\",\n  \"type\": \"circle\",\n  \"data\": { \"name\": \"alice\", \"role\": \"Admin\" },\n  \"style\": { \"x\": 100, \"y\": 200, \"size\": 32, \"fill\": \"violet\" },\n  \"states\": [\"selected\"],\n  \"combo\": null\n}\n```\n\n### Edge Data\n\nAn edge connects nodes and represents the relationship between them. Each edge is associated with two nodes (source and target), and edges themselves can have data, styles, and states. Edge data is often used to represent logical relationships, such as user connections in social networks or step flows in flowcharts.\n\n| Attribute                                     | Type       | Description                                                                                                             |\n| --------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------- |\n| <Badge type=\"success\">Required</Badge> source | _string_   | Source node ID                                                                                                          |\n| <Badge type=\"success\">Required</Badge> target | _string_   | Target node ID                                                                                                          |\n| id                                            | _string_   | Unique identifier for the edge. If not specified, `id` is automatically generated with the format `${source}-${target}` |\n| type                                          | _string_   | Edge type.It can be the type of built-in Edge, or the custom Edge                                                       |\n| data                                          | _Object_   | Custom data for the edge, accessible in style mappings via callback functions                                           |\n| style                                         | _Object_   | Edge style, including stroke color, line width, arrowhead, etc.                                                         |\n| states                                        | _string[]_ | Initial states for the edge                                                                                             |\n\n**Example:**\n\n```json\n{\n  \"source\": \"alice\",\n  \"target\": \"bob\",\n  \"type\": \"line\",\n  \"data\": { \"relationship\": \"friend\", \"strength\": 5 },\n  \"style\": { \"stroke\": \"green\", \"lineWidth\": 2 },\n  \"states\": [\"hover\"]\n}\n```\n\n### Combo Data\n\nCombos allow you to create a logical unit for multiple nodes, used for layering, grouping, or other structural purposes. A combo can contain child nodes or other combos, forming a nested structure.\n\n| Attribute                                 | Type               | Description                                                                    |\n| ----------------------------------------- | ------------------ | ------------------------------------------------------------------------------ |\n| <Badge type=\"success\">Required</Badge> id | _string_           | Unique identifier for the combo                                                |\n| type                                      | _string_           | Combo type.It can be the type of built-in Combo, or the custom Combo           |\n| data                                      | _Object_           | Custom data for the combo, accessible in style mappings via callback functions |\n| style                                     | _Object_           | Combo style                                                                    |\n| states                                    | _string[]_         | Initial states for the combo                                                   |\n| combo                                     | _string_ \\| _null_ | Parent combo ID. If there is no parent combo, it is null                       |\n\n**Example:**\n\n```json\n{\n  \"id\": \"combo1\",\n  \"type\": \"circle\",\n  \"data\": { \"groupName\": \"Group A\" },\n  \"style\": { \"fill\": \"lightblue\", \"stroke\": \"blue\", \"collapsed\": true },\n  \"states\": [],\n  \"combo\": null\n}\n```\n\n## Data Organization and Best Practices\n\nTo ensure correct rendering and interaction of the graph, it is recommended to organize the data according to G6's standard data structure. Each element (node, edge, combo) should contain a `data` field to store business data and custom properties.\n\n- **Avoid using identifiers that conflict with internal G6 field names**, such as `id`, `type`, `style`, etc., to prevent naming conflicts.\n- Store business data (such as user information, social network relationships, etc.) in the `data` field. This ensures flexibility and scalability of the data.\n\n**Example:**\n\n```json\n{\n  \"nodes\": [\n    {\n      \"id\": \"node1\",\n      \"data\": { \"name\": \"Alice\", \"role\": \"Admin\" }\n    },\n    {\n      \"id\": \"node2\",\n      \"data\": { \"name\": \"Bob\", \"role\": \"User\" }\n    }\n  ],\n  \"edges\": [\n    {\n      \"source\": \"node1\",\n      \"target\": \"node2\",\n      \"data\": { \"relationship\": \"friend\" }\n    }\n  ]\n}\n```\n\n## API\n\nG6 provides a series of APIs to access and manipulate data, including:\n\n- [getData](/en/api/data#graphgetdata)\n- [setData](/en/api/data#graphsetdata)\n- [getNodeData](/en/api/data#graphgetnodedata)\n- [getEdgeData](/en/api/data#graphgetedgedata)\n- [getComboData](/en/api/data#graphgetcombodata)\n- [addData](/en/api/data#graphadddata)\n- [addNodeData](/en/api/data#graphaddnodedata)\n- [addEdgeData](/en/api/data#graphaddedgedata)\n- [addComboData](/en/api/data#graphaddcombodata)\n- [updateData](/en/api/data#graphupdatedata)\n- [updateNodeData](/en/api/data#graphupdatenodedata)\n- [updateEdgeData](/en/api/data#graphupdateedgedata)\n- [updateComboData](/en/api/data#graphupdatecombodata)\n- [removeData](/en/api/data#graphremovedata)\n- [removeNodeData](/en/api/data#graphremovenodedata)\n- [removeEdgeData](/en/api/data#graphremoveedgedata)\n- [removeComboData](/en/api/data#graphremovecombodata)\n\nThrough different APIs, you can conveniently access and manipulate graph data, performing operations such as adding, deleting, modifying, and querying the graph.\n\n## Use Remote Data\n\nG6 does not provide functionality for data retrieval and parsing. For local JSON data, you can directly import and use it as follows:\n\n```typescript\nimport data from './path/to/data.json' assert { type: 'json' };\n```\n\nFor remote data, you can use `fetch` or other networking libraries to retrieve the data:\n\n```typescript\nfetch('https://path/to/data.json')\n  .then((res) => res.json())\n  .then((data) => {\n    // Use data\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/data.zh.md",
    "content": "---\ntitle: 数据 Data\norder: 4\n---\n\n## 概述\n\nG6 是一款数据驱动的图可视化引擎，数据是 G6 中最重要的概念之一。\n\nG6 使用标准的 JSON 格式描述图数据结构。以下是一个基础的图数据示例：\n\n```javascript {4-7}\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n});\n```\n\n## 数据结构\n\n图数据包含三部分：`nodes`（节点数据）、`edges`（边数据）、`combos`（组合数据）。每一部分对应图中的不同元素，它们的类型和数据决定了图如何展示。\n\n### 图数据（GraphData）\n\n| 属性   | 描述     | 类型                              | 默认值 | 必选 |\n| ------ | -------- | --------------------------------- | ------ | ---- |\n| nodes  | 节点数据 | [NodeData](#节点数据nodedata)[]   | -      |      |\n| edges  | 边数据   | [EdgeData](#边数据edgedata)[]     | -      |      |\n| combos | 组合数据 | [ComboData](#组合数据combodata)[] | -      |      |\n\n### 节点数据（NodeData）\n\n节点是图中的基本构成单元，它代表图中的实体。每个节点都有一个唯一的 `id`，用于标识节点，同时节点也可以有数据、样式和状态。\n\n| 属性     | 描述                                                                                         | 类型           | 默认值 | 必选 |\n| -------- | -------------------------------------------------------------------------------------------- | -------------- | ------ | ---- |\n| id       | 节点的唯一标识符，用于区分不同的节点                                                         | string         | -      | ✓    |\n| type     | 节点类型，内置节点类型名称或者自定义节点的名称                                               | string         | -      |      |\n| data     | 节点数据，用于存储节点的自定义数据，例如节点的名称、描述等。可以在样式映射中通过回调函数获取 | object         | -      |      |\n| style    | 节点样式，包括位置、大小、颜色等视觉属性                                                     | object         | -      |      |\n| states   | 节点初始状态，如选中、激活、悬停等                                                           | string[]       | -      |      |\n| combo    | 所属的组合 ID，用于组织节点的层级关系，如果没有则为 null                                     | string \\| null | -      |      |\n| children | 子节点 ID 集合，仅在树图场景下使用                                                           | string[]       | -      |      |\n\n**示例：**\n\n```json\n{\n  \"id\": \"node-1\",\n  \"type\": \"circle\",\n  \"data\": { \"name\": \"alice\", \"role\": \"Admin\" },\n  \"style\": { \"x\": 100, \"y\": 200, \"size\": 32, \"fill\": \"violet\" },\n  \"states\": [\"selected\"],\n  \"combo\": null\n}\n```\n\n### 边数据（EdgeData）\n\n边是连接节点的元素，表示节点之间的关系。每条边都与两个节点（起始节点和目标节点）关联，并且边本身可以有数据、样式和状态。边的数据常用于表示节点之间的逻辑或关系，如社交网络中的用户关系、流程图中的步骤流转等。\n\n| 属性   | 描述                                                             | 类型     | 默认值 | 必选 |\n| ------ | ---------------------------------------------------------------- | -------- | ------ | ---- |\n| source | 边起始节点 ID                                                    | string   | -      | ✓    |\n| target | 边目标节点 ID                                                    | string   | -      | ✓    |\n| id     | 边的唯一标识符                                                   | string   | -      |      |\n| type   | 边类型，内置边类型名称或者自定义边的名称                         | string   | -      |      |\n| data   | 边数据，用于存储边的自定义数据，可以在样式映射中通过回调函数获取 | object   | -      |      |\n| style  | 边样式，包括线条颜色、宽度、箭头等视觉属性                       | object   | -      |      |\n| states | 边初始状态                                                       | string[] | -      |      |\n\n**示例：**\n\n```json\n{\n  \"source\": \"alice\",\n  \"target\": \"bob\",\n  \"type\": \"line\",\n  \"data\": { \"relationship\": \"friend\", \"strength\": 5 },\n  \"style\": { \"stroke\": \"green\", \"lineWidth\": 2 },\n  \"states\": [\"hover\"]\n}\n```\n\n### 组合数据（ComboData）\n\n通过组合，可以为多个节点创建一个逻辑单元，用于图形的分层、分组或其他结构化需求。组合可以包含子节点或其他组合，从而形成嵌套层次。\n\n| 属性   | 描述                                                                 | 类型           | 默认值 | 必选 |\n| ------ | -------------------------------------------------------------------- | -------------- | ------ | ---- |\n| id     | 组合的唯一标识符                                                     | string         | -      | ✓    |\n| type   | 组合类型，内置组合类型名称或者自定义组合名称                         | string         | -      |      |\n| data   | 组合数据，用于存储组合的自定义数据，可以在样式映射中通过回调函数获取 | object         | -      |      |\n| style  | 组合样式                                                             | object         | -      |      |\n| states | 组合初始状态                                                         | string[]       | -      |      |\n| combo  | 组合的父组合 ID。如果没有父组合，则为 null                           | string \\| null | -      |      |\n\n**示例：**\n\n```json\n{\n  \"id\": \"combo1\",\n  \"type\": \"circle\",\n  \"data\": { \"groupName\": \"Group A\" },\n  \"style\": { \"fill\": \"lightblue\", \"stroke\": \"blue\", \"collapsed\": true },\n  \"states\": [],\n  \"combo\": null\n}\n```\n\n## 数据操作\n\nG6 提供了丰富的 API 来操作图数据，下面展示一些常见的数据操作示例。\n\n### 数据初始化\n\n在创建图实例时，可以直接传入数据：\n\n```javascript\nconst graph = new Graph({\n  data: {\n    nodes: [\n      { id: 'node1', data: { label: '节点1' } },\n      { id: 'node2', data: { label: '节点2' } },\n    ],\n    edges: [{ source: 'node1', target: 'node2', data: { label: '关系' } }],\n  },\n});\n```\n\n或者通过 `setData` 方法设置数据：\n\n```javascript\ngraph.setData({\n  nodes: [\n    { id: 'node3', data: { label: '节点3' } },\n    { id: 'node4', data: { label: '节点4' } },\n  ],\n  edges: [{ source: 'node3', target: 'node4', data: { label: '新关系' } }],\n});\n```\n\n### 数据增删改查\n\n#### 添加节点和边\n\n```javascript\n// 添加单个节点\ngraph.addNodeData([\n  {\n    id: 'node5',\n    data: {\n      label: '新节点',\n      category: 'person',\n    },\n    style: {\n      fill: '#6395F9',\n      stroke: '#5B8FF9',\n    },\n  },\n]);\n\n// 批量添加多个节点\ngraph.addNodeData([\n  { id: 'node6', data: { label: '批量节点1' } },\n  { id: 'node7', data: { label: '批量节点2' } },\n]);\n\n// 添加连接新节点的边\ngraph.addEdgeData([\n  {\n    source: 'node1',\n    target: 'node5',\n    data: {\n      label: '连接到新节点',\n      weight: 2,\n    },\n    style: {\n      stroke: '#F6BD16',\n      lineWidth: 3,\n    },\n  },\n]);\n```\n\n#### 更新数据\n\n```javascript\n// 更新单个节点\ngraph.updateNodeData([\n  {\n    id: 'node1',\n    data: {\n      label: '已更新的节点1',\n      status: 'updated',\n    },\n    style: {\n      fill: '#F6BD16',\n      stroke: '#EBEBEB',\n      lineWidth: 2,\n    },\n  },\n]);\n\n// 更新多个节点\ngraph.updateNodeData([\n  {\n    id: 'node2',\n    style: { size: 40, fill: '#5AD8A6' },\n  },\n  {\n    id: 'node3',\n    data: { importance: 'high' },\n  },\n]);\n\n// 更新边\ngraph.updateEdgeData([\n  {\n    source: 'node1',\n    target: 'node2',\n    style: {\n      stroke: '#5B8FF9',\n      lineWidth: 2,\n      lineDash: [5, 5],\n    },\n  },\n]);\n```\n\n#### 删除数据\n\n```javascript\n// 删除单个节点（以及与该节点相连的所有边）\ngraph.removeNodeData(['node7']);\n\n// 删除多个节点\ngraph.removeNodeData(['node5', 'node6']);\n\n// 删除边\ngraph.removeEdgeData(['node1-node2']);\n```\n\n#### 查询数据\n\n```javascript\n// 获取所有节点数据\nconst nodes = graph.getNodeData();\n\n// 获取所有边数据\nconst edges = graph.getEdgeData();\n\n// 获取特定节点数据\nconst node1 = graph.getNodeData('node1');\n\n// 获取特定边数据\nconst edge1 = graph.getEdgeData('node1-node2');\n```\n\n### 复杂数据结构操作\n\n#### 嵌套组合（Combo）\n\n下面是创建和操作嵌套组合的示例：\n\n```javascript\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', data: { label: '节点1' }, combo: 'combo1' },\n    { id: 'node2', data: { label: '节点2' }, combo: 'combo1' },\n    { id: 'node3', data: { label: '节点3' }, combo: 'combo2' },\n    { id: 'node4', data: { label: '节点4' }, combo: 'combo2' },\n    { id: 'node5', data: { label: '节点5' }, combo: 'combo3' },\n  ],\n  edges: [\n    { source: 'node1', target: 'node3' },\n    { source: 'node2', target: 'node4' },\n    { source: 'node4', target: 'node5' },\n  ],\n  combos: [\n    { id: 'combo1', data: { label: '组1' } },\n    { id: 'combo2', data: { label: '组2' } },\n    { id: 'combo3', data: { label: '组3' }, combo: 'combo1' }, // 嵌套组合\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'force',\n  },\n});\n\n// 添加新的组合\ngraph.addComboData([\n  {\n    id: 'combo4',\n    data: { label: '新组' },\n    combo: 'combo2', // 添加到现有组合中\n  },\n]);\n\n// 将节点移动到不同的组合\ngraph.updateNodeData([\n  {\n    id: 'node5',\n    combo: 'combo4', // 将节点5移动到新组合\n  },\n]);\n\n// 展开/折叠组合\ngraph.updateComboData([\n  {\n    id: 'combo1',\n    style: { collapsed: true }, // 折叠组合1\n  },\n]);\n\ngraph.render();\n```\n\n#### 树形结构数据\n\n对于树形结构，G6 支持使用 `children` 属性表示层次关系：\n\n```javascript\n// 树形结构数据\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nconst treeData = {\n  id: 'root',\n  children: [\n    {\n      id: 'child1',\n      children: [{ id: 'grandchild1' }],\n    },\n    {\n      id: 'child2',\n      children: [{ id: 'grandchild2' }],\n    },\n  ],\n};\n\nconst data = treeToGraphData(treeData);\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'dendrogram', // 或 'compactBox', 'mindmap' 等树布局\n    direction: 'TB', // 从上到下布局\n    nodeSep: 50, // 节点间距\n    rankSep: 100, // 层级间距\n  },\n});\n\ngraph.render();\n```\n\n通过 `treeToGraphData` 方法，可以将树形结构数据转换为 G6 的标准数据结构，实际传入到 `data` 中的数据结构如下：\n\n```json\n{\n  \"nodes\": [\n    { \"id\": \"root\", \"depth\": 0, \"children\": [\"child1\", \"child2\"] },\n    { \"id\": \"child1\", \"depth\": 1, \"children\": [\"grandchild1\"] },\n    { \"id\": \"grandchild1\", \"depth\": 2 },\n    { \"id\": \"child2\", \"depth\": 1, \"children\": [\"grandchild2\"] },\n    { \"id\": \"grandchild2\", \"depth\": 2 }\n  ],\n  \"edges\": [\n    { \"source\": \"root\", \"target\": \"child1\" },\n    { \"source\": \"root\", \"target\": \"child2\" },\n    { \"source\": \"child1\", \"target\": \"grandchild1\" },\n    { \"source\": \"child2\", \"target\": \"grandchild2\" }\n  ]\n}\n```\n\n## 数据组织与最佳实践\n\n为了确保图的正确渲染和交互，建议按照 G6 标准数据结构组织数据。每个元素（节点、边、组合）应包含一个 `data` 字段，用于存放业务数据和自定义属性。\n\n- **避免使用与 G6 内部字段名称相同的标识符**，如 `id`、`type`、`style` 等，防止发生命名冲突。\n- 将业务数据（如用户信息、社交网络关系等）存储在 `data` 字段中，这样可以确保数据的灵活性和可扩展性。\n- **使用样式映射**来根据业务数据动态设置视觉属性，而不是直接修改样式对象。\n\n### 数据与样式分离\n\n良好的做法是将数据和样式分离，通过映射函数将数据属性转换为视觉属性：\n\n```javascript\nimport { Graph } from '@antv/g6';\n\n// 使用数据驱动样式\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node1', data: { value: 10, category: 'A' } },\n      { id: 'node2', data: { value: 5, category: 'B' } },\n    ],\n  },\n  node: {\n    style: {\n      // 根据数据中的 value 字段动态设置节点大小\n      size: (node) => 20 + node.data.value * 2,\n      // 根据数据中的 category 字段设置不同颜色\n      fill: (node) => {\n        const categoryColors = { A: '#F6BD16', B: '#5B8FF9' };\n        return categoryColors[node.data.category] || '#CCC';\n      },\n    },\n  },\n});\n```\n\n## API\n\nG6 提供了一系列的 API 来访问和操作数据，包括：\n\n- [getData](/api/data#graphgetdata)\n- [setData](/api/data#graphsetdata)\n- [getNodeData](/api/data#graphgetnodedata)\n- [getEdgeData](/api/data#graphgetedgedata)\n- [getComboData](/api/data#graphgetcombodata)\n- [addData](/api/data#graphadddata)\n- [addNodeData](/api/data#graphaddnodedata)\n- [addEdgeData](/api/data#graphaddedgedata)\n- [addComboData](/api/data#graphaddcombodata)\n- [updateData](/api/data#graphupdatedata)\n- [updateNodeData](/api/data#graphupdatenodedata)\n- [updateEdgeData](/api/data#graphupdateedgedata)\n- [updateComboData](/api/data#graphupdatecombodata)\n- [removeData](/api/data#graphremovedata)\n- [removeNodeData](/api/data#graphremovenodedata)\n- [removeEdgeData](/api/data#graphremoveedgedata)\n- [removeComboData](/api/data#graphremovecombodata)\n\n通过不同的 API，你可以方便地访问和操作图数据，实现图的增删改查等操作。\n\n## 使用远程数据\n\nG6 并不提供数据的获取和解析功能，对于本地 JSON 数据，你可以直接引入使用：\n\n```typescript\nimport data from './path/to/data.json' assert { type: 'json' };\n```\n\n对于远程数据，你可以使用 `fetch` 或者其他网络请求库来获取数据：\n\n```typescript\nfetch('https://path/to/data.json')\n  .then((res) => res.json())\n  .then((data) => {\n    // 使用 data\n    const graph = new Graph({\n      container: 'container',\n      data,\n    });\n    // 触发布局和渲染\n    graph.render();\n  })\n  .catch((error) => {\n    console.error('加载数据失败:', error);\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/combo/BaseCombo.en.md",
    "content": "---\ntitle: Combo Common Options\norder: 1\n---\n\nThis document introduces the built-in combo common property configurations.\n\n## ComboOptions\n\n```js {5-9}\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  combo: {\n    type: 'circle', // Combo type\n    style: {}, // Combo style\n    state: {}, // State style\n    palette: {}, // Palette configuration\n    animation: {}, // Animation configuration\n  },\n});\n```\n\n| Property  | Description                                               | Type                    | Default  | Required |\n| --------- | --------------------------------------------------------- | ----------------------- | -------- | -------- |\n| type      | Combo type, built-in combo type name or custom combo name | [Type](#type)           | `circle` |          |\n| style     | Combo style configuration, including color, size, etc.    | [Style](#style)         | -        |          |\n| state     | Style configuration for different states                  | [State](#state)         | -        |          |\n| palette   | Define combo palette for mapping colors based on data     | [Palette](#palette)     | -        |          |\n| animation | Define combo animation effects                            | [Animation](#animation) | -        |          |\n\n## Type\n\nSpecifies the combo type, built-in combo type name or custom combo name. Default is `circle`. **⚠️ Note**: This determines the shape of the main graphic.\n\n```js {3}\nconst graph = new Graph({\n  combo: {\n    type: 'circle',\n  },\n});\n```\n\n**⚠️ Dynamic Configuration Note**: The `type` property also supports dynamic configuration, allowing you to dynamically select combo types based on combo data:\n\n```js\nconst graph = new Graph({\n  combo: {\n    // Static configuration\n    type: 'circle',\n\n    // Dynamic configuration - arrow function form\n    type: (datum) => datum.data.comboType || 'circle',\n\n    // Dynamic configuration - regular function form (can access graph instance)\n    type: function (datum) {\n      console.log(this); // graph instance\n      return datum.data.category === 'important' ? 'rect' : 'circle';\n    },\n  },\n});\n```\n\nAvailable values:\n\n- `circle`: [Circle Combo](/en/manual/element/combo/circle)\n- `rect`: [Rect Combo](/en/manual/element/combo/rect)\n\n## Style\n\nDefines combo style, including color, size, etc.\n\n```js {3}\nconst graph = new Graph({\n  combo: {\n    style: {},\n  },\n});\n```\n\n**⚠️ Dynamic Configuration Note**: All style properties below support dynamic configuration, meaning you can pass functions to dynamically calculate property values based on combo data:\n\n```js\nconst graph = new Graph({\n  combo: {\n    style: {\n      // Static configuration\n      fill: '#1783FF',\n\n      // Dynamic configuration - arrow function form\n      stroke: (datum) => (datum.data.isActive ? '#FF0000' : '#000000'),\n\n      // Dynamic configuration - regular function form (can access graph instance)\n      lineWidth: function (datum) {\n        console.log(this); // graph instance\n        return datum.data.importance > 5 ? 3 : 1;\n      },\n\n      // Nested properties also support dynamic configuration\n      labelText: (datum) => `Combo: ${datum.id}`,\n      badges: (datum) => datum.data.tags.map((tag) => ({ text: tag })),\n    },\n  },\n});\n```\n\nWhere the `datum` parameter is the combo data object (`ComboData`), containing all combo data information.\n\nA complete combo consists of the following parts:\n\n<img width=\"240\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*z-OxR4MAdUwAAAAAAAAAAAAADmJ7AQ/original\" />\n\n- `key`: The main graphic of the combo, representing the primary shape of the combo, such as circle, rectangle, etc.\n- `label`: Text label, usually used to display the combo's name or description\n- `halo`: Graphic displaying halo effect around the main graphic\n- `badge`: Badge displayed at the top-right corner of the combo by default\n\nThe following style configurations will be explained by atomic graphics:\n\n### Main Graphic Style\n\nThe main graphic is the core part of the combo, defining the basic shape and appearance of the combo. Here are common configuration scenarios:\n\n#### Basic Style Configuration\n\nSet the basic appearance of the combo:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      fill: '#5B8FF9', // Blue fill\n      stroke: '#1A1A1A', // Dark stroke\n      lineWidth: 2,\n      fillOpacity: 0.2,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Transparency and Shadow Effects\n\nAdd transparency and shadow effects to combos:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      fill: '#61DDAA',\n      fillOpacity: 0.15,\n      shadowColor: 'rgba(97, 221, 170, 0.4)',\n      shadowBlur: 12,\n      shadowOffsetX: 2,\n      shadowOffsetY: 4,\n      stroke: '#F0F0F0',\n      lineWidth: 1,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Dashed Border Style\n\nCreate combos with dashed borders:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      fill: '#FFF1F0',\n      fillOpacity: 0.1,\n      stroke: '#F5222D',\n      lineWidth: 2,\n      lineDash: [6, 4],\n      lineCap: 'round',\n    },\n  },\n});\n\ngraph.render();\n```\n\nHere is the complete main graphic style configuration:\n\n| Property                        | Description                                                                                                                              | Type                          | Default   | Required |\n| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | --------- | -------- |\n| collapsed                       | Whether the current combo is collapsed                                                                                                   | boolean                       | false     |          |\n| cursor                          | Combo mouse hover style, [options](#cursor)                                                                                              | string                        | default   |          |\n| fill                            | Combo fill color                                                                                                                         | string                        | `#99ADD1` |          |\n| fillOpacity                     | Combo fill opacity                                                                                                                       | number \\| string              | 0.04      |          |\n| increasedLineWidthForHitTesting | When lineWidth is small, the interactive area becomes small. Sometimes we want to increase this area to make \"thin lines\" easier to pick | number                        | 0         |          |\n| lineCap                         | Combo stroke end cap style                                                                                                               | `round` \\| `square` \\| `butt` | `butt`    |          |\n| lineDash                        | Combo stroke dash style                                                                                                                  | number[]                      | -         |          |\n| lineDashOffset                  | Combo stroke dash offset                                                                                                                 | number                        | -         |          |\n| lineJoin                        | Combo stroke join style                                                                                                                  | `round` \\| `bevel` \\| `miter` | `miter`   |          |\n| lineWidth                       | Combo stroke width                                                                                                                       | number                        | 1         |          |\n| opacity                         | Combo opacity                                                                                                                            | number \\| string              | 1         |          |\n| pointerEvents                   | How combo responds to pointer events, [options](#pointerevents)                                                                          | string                        | `auto`    |          |\n| shadowBlur                      | Combo shadow blur                                                                                                                        | number                        | -         |          |\n| shadowColor                     | Combo shadow color                                                                                                                       | string                        | -         |          |\n| shadowOffsetX                   | Combo shadow offset in x direction                                                                                                       | number \\| string              | -         |          |\n| shadowOffsetY                   | Combo shadow offset in y direction                                                                                                       | number \\| string              | -         |          |\n| shadowType                      | Combo shadow type                                                                                                                        | `inner` \\| `outer`            | `outer`   |          |\n| size                            | Combo size, quick setting for combo width and height, [options](#size)                                                                   | number \\| number[]            | -         |          |\n| stroke                          | Combo stroke color                                                                                                                       | string                        | `#99ADD1` |          |\n| strokeOpacity                   | Combo stroke opacity                                                                                                                     | number \\| string              | 1         |          |\n| transform                       | Transform property allows you to rotate, scale, skew or translate the given combo                                                        | string                        | -         |          |\n| transformOrigin                 | Rotation and scaling center, also called transformation center                                                                           | string                        | -         |          |\n| visibility                      | Whether combo is visible                                                                                                                 | `visible` \\| `hidden`         | `visible` |          |\n| x                               | Combo x coordinate                                                                                                                       | number                        | 0         |          |\n| y                               | Combo y coordinate                                                                                                                       | number                        | 0         |          |\n| z                               | Combo z coordinate                                                                                                                       | number                        | 0         |          |\n| zIndex                          | Combo rendering layer                                                                                                                    | number                        | 0         |          |\n\n#### Size\n\nCombo size, quick setting for combo width and height, supports three configuration methods:\n\n- number: Indicates that combo width and height are the same as the specified value\n- [number, number]: Indicates that combo width and height are represented by array elements in order for combo width and height\n- [number, number, number]: Indicates that combo width, height, and depth are represented by array elements in order\n\n#### PointerEvents\n\nThe `pointerEvents` property controls how graphics respond to interaction events. Refer to [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events).\n\nAvailable values: `visible` | `visiblepainted` | `visiblestroke` | `non-transparent-pixel` | `visiblefill` | `visible` | `painted` | `fill` | `stroke` | `all` | `none` | `auto` | `inherit` | `initial` | `unset`\n\nIn short, `fill`, `stroke`, and `visibility` can independently or in combination affect pick testing behavior. Currently supports the following keywords:\n\n- **`auto`**: Default value, equivalent to `visiblepainted`\n- **`none`**: Will never be the target of responding events\n- **`visiblepainted`**: Will respond to events only when the following conditions are met:\n  - `visibility` is set to `visible`, i.e., the graphic is visible\n  - Triggered in the graphic fill area and `fill` takes a non-`none` value; or triggered in the graphic stroke area and `stroke` takes a non-`none` value\n- **`visiblefill`**: Will respond to events only when the following conditions are met:\n  - `visibility` is set to `visible`, i.e., the graphic is visible\n  - Triggered in the graphic fill area, not affected by the `fill` value\n- **`visiblestroke`**: Will respond to events only when the following conditions are met:\n  - `visibility` is set to `visible`, i.e., the graphic is visible\n  - Triggered in the graphic stroke area, not affected by the `stroke` value\n- **`visible`**: Will respond to events only when the following conditions are met:\n  - `visibility` is set to `visible`, i.e., the graphic is visible\n  - Triggered in the graphic fill or stroke area, not affected by `fill` and `stroke` values\n- **`painted`**: Will respond to events only when the following conditions are met:\n  - Triggered in the graphic fill area and `fill` takes a non-`none` value; or triggered in the graphic stroke area and `stroke` takes a non-`none` value\n  - Not affected by `visibility` value\n- **`fill`**: Will respond to events only when the following conditions are met:\n  - Triggered in the graphic fill area, not affected by the `fill` value\n  - Not affected by `visibility` value\n- **`stroke`**: Will respond to events only when the following conditions are met:\n  - Triggered in the graphic stroke area, not affected by the `stroke` value\n  - Not affected by `visibility` value\n- **`all`**: Will respond to events as long as entering the graphic fill and stroke areas, not affected by `fill`, `stroke`, `visibility` values\n\n**Usage Examples:**\n\n```js\n// Example 1: Only stroke area responds to events\nconst graph = new Graph({\n  combo: {\n    style: {\n      fill: 'none',\n      stroke: '#000',\n      lineWidth: 2,\n      pointerEvents: 'stroke', // Only stroke responds to events\n    },\n  },\n});\n\n// Example 2: Does not respond to events at all\nconst graph = new Graph({\n  combo: {\n    style: {\n      pointerEvents: 'none', // Combo does not respond to any events\n    },\n  },\n});\n```\n\n#### Cursor\n\nAvailable values: `auto` | `default` | `none` | `context-menu` | `help` | `pointer` | `progress` | `wait` | `cell` | `crosshair` | `text` | `vertical-text` | `alias` | `copy` | `move` | `no-drop` | `not-allowed` | `grab` | `grabbing` | `all-scroll` | `col-resize` | `row-resize` | `n-resize` | `e-resize` | `s-resize` | `w-resize` | `ne-resize` | `nw-resize` | `se-resize` | `sw-resize` | `ew-resize` | `ns-resize` | `nesw-resize` | `nwse-resize` | `zoom-in` | `zoom-out`\n\n### Style When Expanded\n\nMain graphic style when the combo is expanded\n\n| Attribute                       | Description                                                                                                                                       | Type                                                                            | Default   | Required |\n| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------- | -------- |\n| collapsed                       | Whether the combo is currently collapsed                                                                                                          | boolean                                                                         | false     |          |\n| cursor                          | Combo mouse hover style, [configuration item](#cursor)                                                                                            | string                                                                          | default   |          |\n| fill                            | Combo fill color                                                                                                                                  | string                                                                          | `#99ADD1` |          |\n| fillOpacity                     | Combo fill color opacity                                                                                                                          | number  string                                                                 | 0.04      |          |\n| increasedLineWidthForHitTesting | When lineWidth is small, the interactive area also becomes smaller. Sometimes we want to enlarge this area to make \"thin lines\" easier to pick up | number                                                                          | 0         |          |\n| lineCap                         | Combo stroke end style                                                                                                                            | `round`  `square`  `butt`                                                     | `butt`    |          |\n| lineDash                        | Combo stroke dash style                                                                                                                           | number[]                                                                        | -         |          |\n| lineDashOffset                  | Combo stroke dash offset                                                                                                                          | number                                                                          | -         |          |\n| lineJoin                        | Combo stroke join style                                                                                                                           | `round`  `bevel`  `miter`                                                     | `miter`   |          |\n| lineWidth                       | Combo stroke width                                                                                                                                | number                                                                          | 1         |          |\n| opacity                         | Combo opacity                                                                                                                                     | number  string                                                                 | 1         |          |\n| shadowBlur                      | Combo shadow blur                                                                                                                                 | number                                                                          | -         |          |\n| shadowColor                     | Combo shadow color                                                                                                                                | string                                                                          | -         |          |\n| shadowOffsetX                   | Combo shadow offset in the x-axis direction                                                                                                       | number  string                                                                 | -         |          |\n| shadowOffsetY                   | Combo shadow offset in the y-axis direction                                                                                                       | number  string                                                                 | -         |          |\n| shadowType                      | Combo shadow type                                                                                                                                 | `inner`  `outer`                                                               | `outer`   |          |\n| stroke                          | Combo stroke color                                                                                                                                | string                                                                          | `#99add1` |          |\n| strokeOpacity                   | Combo stroke color opacity                                                                                                                        | number  string                                                                 | 1         |          |\n| visibility                      | Whether the combo is visible                                                                                                                      | `visible`  `hidden`                                                            | `visible` |          |\n| x                               | Combo x coordinate                                                                                                                                | number                                                                          | 0         |          |\n| y                               | Combo y coordinate                                                                                                                                | number                                                                          | 0         |          |\n| z                               | Combo z coordinate                                                                                                                                | number                                                                          | 0         |          |\n| zIndex                          | Combo rendering layer                                                                                                                             | number                                                                          | 0         |          |\n| `{styleProps}`                  | More graphic configurations, refer to [BaseStyleProps](https://g.antv.antgroup.com/api/basic/display-object#绘图属性) configuration items         | [BaseStyleProps](https://g.antv.antgroup.com/api/basic/display-object#绘图属性) | -         |          |\n\n#### Cursor\n\nOptional values are: `auto` | `default` | `none` | `context-menu` | `help` | `pointer` | `progress` | `wait` | `cell` | `crosshair` | `text` | `vertical-text` | `alias` | `copy` | `move` | `no-drop` | `not-allowed` | `grab` | `grabbing` | `all-scroll` | `col-resize` | `row-resize` | `n-resize` | `e-resize` | `s-resize` | `w-resize` | `ne-resize` | `nw-resize` | `se-resize` | `sw-resize` | `ew-resize` | `ns-resize` | `nesw-resize` | `nwse-resize` | `zoom-in` | `zoom-out`\n\n**Example:**\n\n```js {5-7}\nconst graph = new Graph({\n  // Other configurations...\n  combo: {\n    style: {\n      fill: '#1783FF', // Fill color\n      stroke: '#000', // Stroke color\n      lineWidth: 2, // Stroke width\n    },\n  },\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: { fill: '#1783FF', stroke: '#000', lineWidth: 2 },\n  },\n});\n\ngraph.render();\n```\n\n### Style When Collapsed\n\nEffective when `collapsed` is `true`\n\n| Attribute                                | Description                                                                                                                                                                  | Type                                                                            | Default                                  | Required |\n| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------- | -------- |\n| collapsedCursor                          | Mouse hover style when the combo is collapsed, [configuration item](#cursor)                                                                                                 | string                                                                          | Same as the cursor when expanded         |          |\n| collapsedFill                            | Fill color when the combo is collapsed                                                                                                                                       | string                                                                          | Same as the fill when expanded           |          |\n| collapsedFillOpacity                     | Fill color opacity when the combo is collapsed                                                                                                                               | number  string                                                                 | 1                                        |          |\n| collapsedIncreasedLineWidthForHitTesting | When the combo is collapsed, if lineWidth is small, the interactive area also becomes smaller. Sometimes we want to enlarge this area to make \"thin lines\" easier to pick up | number                                                                          | 0                                        |          |\n| collapsedLineCap                         | Stroke end style when the combo is collapsed                                                                                                                                 | `round`  `square`  `butt`                                                     | Same as the lineCap when expanded        |          |\n| collapsedLineDash                        | Stroke dash style when the combo is collapsed                                                                                                                                | number[]                                                                        | Same as the lineDash when expanded       |          |\n| collapsedLineDashOffset                  | Stroke dash offset when the combo is collapsed                                                                                                                               | number                                                                          | Same as the lineDashOffset when expanded |          |\n| collapsedLineJoin                        | Stroke join style when the combo is collapsed                                                                                                                                | `round`  `bevel`  `miter`                                                     | Same as the lineJoin when expanded       |          |\n| collapsedLineWidth                       | Stroke width when the combo is collapsed                                                                                                                                     | number                                                                          | Same as the lineWidth when expanded      |          |\n| collapsedMarker                          | Whether to display the marker when the combo is collapsed, [configuration item](#collapsedMarkerStyle)                                                                       | boolean                                                                         | true                                     |          |\n| collapsedOpacity                         | Opacity when the combo is collapsed                                                                                                                                          | number  string                                                                 | Same as the opacity when expanded        |          |\n| collapsedShadowBlur                      | Shadow blur when the combo is collapsed                                                                                                                                      | number                                                                          | Same as the shadowBlur when expanded     |          |\n| collapsedShadowColor                     | Shadow color when the combo is collapsed                                                                                                                                     | string                                                                          | Same as the shadowColor when expanded    |          |\n| collapsedShadowOffsetX                   | Shadow offset in the x-axis direction when the combo is collapsed                                                                                                            | number  string                                                                 | Same as the shadowOffsetX when expanded  |          |\n| collapsedShadowOffsetY                   | Shadow offset in the y-axis direction when the combo is collapsed                                                                                                            | number  string                                                                 | Same as the shadowOffsetY when expanded  |          |\n| collapsedShadowType                      | Shadow type when the combo is collapsed                                                                                                                                      | `inner`  `outer`                                                               | Same as the shadowType when expanded     |          |\n| collapsedSize                            | Size when the combo is collapsed                                                                                                                                             | number &#124; [number, number] &#124; [number, number, number]                  | 32                                       |          |\n| collapsedStroke                          | Stroke color when the combo is collapsed                                                                                                                                     | string                                                                          | Same as the stroke when expanded         |          |\n| collapsedStrokeOpacity                   | Stroke color opacity when the combo is collapsed                                                                                                                             | number  string                                                                 | Same as the strokeOpacity when expanded  |          |\n| collapsedVisibility                      | Whether the combo is visible when collapsed                                                                                                                                  | `visible`  `hidden`                                                            | Same as the visibility when expanded     |          |\n| `collapsed{styleProps}`                  | More graphic configurations, refer to [BaseStyleProps](https://g.antv.antgroup.com/api/basic/display-object#绘图属性) configuration items                                    | [BaseStyleProps](https://g.antv.antgroup.com/api/basic/display-object#绘图属性) | -                                        |          |\n\n**Example:**\n\n```js {5-7}\nconst graph = new Graph({\n  // Other configurations...\n  combo: {\n    style: {\n      collapsedFill: '#1783FF', // Fill color\n      collapsedStroke: '#000', // Stroke color\n      collapsedLineWidth: 2, // Stroke width\n    },\n  },\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1', style: { collapsed: true } }],\n  },\n  combo: {\n    style: { collapsedFill: '#1783FF', collapsedStroke: '#000', collapsedLineWidth: 2 },\n  },\n});\n\ngraph.render();\n```\n\n### Collapsed Marker Style\n\nEffective when `collapsedMarker` is `true`\n\n| Attribute                     | Description                                                                                                                                                                                                                                                                                                                                                         | Type                                                                                                                               | Default       | Required |\n| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- |\n| collapsedMarkerType           | Marker type displayed when the combo is collapsed <br> - `'child-count'`: Number of child elements (including Node and Combo) <br>- `'descendant-count'`: Number of descendant elements (including Node and Combo) <br>- `'node-count'`: Number of descendant elements (only including Node) <br> - `(children: NodeLikeData[]) => string`: Custom processing logic | `child-count` \\| `descendant-count` \\| `node-count` \\| ((children: NodeData \\| ComboData[]) => string)                             | `child-count` |          |\n| collapsedMarkerFill           | Icon text color                                                                                                                                                                                                                                                                                                                                                     | string                                                                                                                             | #fff          |          |\n| collapsedMarkerFillOpacity    | Icon text color opacity                                                                                                                                                                                                                                                                                                                                             | number                                                                                                                             | 1             |          |\n| collapsedMarkerFontSize       | Icon font size                                                                                                                                                                                                                                                                                                                                                      | number                                                                                                                             | 12            |          |\n| collapsedMarkerFontWeight     | Icon font weight                                                                                                                                                                                                                                                                                                                                                    | number \\| string                                                                                                                   | `normal`      |          |\n| collapsedMarkerRadius         | Icon corner radius                                                                                                                                                                                                                                                                                                                                                  | number                                                                                                                             | 0             |          |\n| collapsedMarkerSrc            | Image source. Its priority is higher than `collapsedMarkerText`                                                                                                                                                                                                                                                                                                     | string                                                                                                                             | -             |          |\n| collapsedMarkerText           | Icon text                                                                                                                                                                                                                                                                                                                                                           | string                                                                                                                             | -             |          |\n| collapsedMarkerTextAlign      | Icon text horizontal alignment                                                                                                                                                                                                                                                                                                                                      | `center`  `end`  `left`  `right`  `start`                                                                                      | `center`      |          |\n| collapsedMarkerTextBaseline   | Icon text alignment baseline                                                                                                                                                                                                                                                                                                                                        | `alphabetic`  `bottom`  `hanging`  `ideographic`  `middle`  `top`                                                             | `middle`      |          |\n| collapsedMarkerWidth          | Icon width                                                                                                                                                                                                                                                                                                                                                          | number                                                                                                                             | -             |          |\n| collapsedMarkerHeight         | Icon height                                                                                                                                                                                                                                                                                                                                                         | number                                                                                                                             | -             |          |\n| collapsedMarkerZIndex         | Icon rendering layer                                                                                                                                                                                                                                                                                                                                                | number                                                                                                                             | 1             |          |\n| `collapsedMarker{StyleProps}` | More icon style configurations, refer to [TextStyleProps](https://g.antv.antgroup.com/api/basic/text), [ImageStyleProps](https://g.antv.antgroup.com/api/basic/image) configuration items. For example, collapsedMarkerFontSize represents the font size of the text icon                                                                                           | [TextStyleProps](https://g.antv.antgroup.com/api/basic/text) &#124; [ImageStyleProps](https://g.antv.antgroup.com/api/basic/image) | -             |          |\n\n**Example:**\n\n```js {5-6}\nconst graph = new Graph({\n  // Other configurations...\n  combo: {\n    style: {\n      collapsedMarkerFill: '#1783FF', // Fill color\n      collapsedMarkerFontSize: 30, // Icon font size\n    },\n  },\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo1' },\n      { id: 'node2', combo: 'combo1' },\n    ],\n    combos: [{ id: 'combo1', style: { collapsed: true } }],\n  },\n  combo: {\n    style: {\n      collapsedMarkerFill: '#1783FF',\n      collapsedMarkerFontSize: 30,\n    },\n  },\n});\n\ngraph.render();\n```\n\n### Label Style\n\nLabels are used to display text information for combos, supporting rich text style configuration and flexible position layout.\n\n#### Basic Label Configuration\n\nAdd basic text label to combo:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      labelText: 'Sales Department', // Label text content\n      labelFill: '#1A1A1A', // Label text color\n      labelFontSize: 14, // Label font size\n      labelPlacement: 'bottom', // Label position: bottom\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Multi-line Text Label\n\nConfigure labels that support multi-line display:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 120,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      labelText: 'This is a combo label text content that supports multi-line display',\n      labelWordWrap: true, // Enable text wrapping\n      labelMaxWidth: 100, // Maximum width 100px\n      labelMaxLines: 3, // Maximum 3 lines\n      labelTextAlign: 'center', // Center text alignment\n      labelFontSize: 12,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Custom Style Label\n\nCreate labels with special styles:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      labelText: 'IMPORTANT',\n      labelFill: '#FF4D4F', // Red text\n      labelFontSize: 16,\n      labelFontWeight: 'bold', // Bold\n      labelFontStyle: 'italic', // Italic\n      labelTextDecorationLine: 'underline', // Underline\n      labelLetterSpacing: 2, // Letter spacing\n      labelPlacement: 'top',\n    },\n  },\n});\n\ngraph.render();\n```\n\nHere are the complete label style configurations:\n\n| Property                 | Description                                                                                                                                                                | Type                                                                        | Default   | Required |\n| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------- | -------- |\n| label                    | Whether to show combo label                                                                                                                                                | boolean                                                                     | true      |          |\n| labelCursor              | Cursor style when hovering over combo label, [options](#cursor)                                                                                                            | string                                                                      | `default` |          |\n| labelFill                | Combo label text color                                                                                                                                                     | string                                                                      | #000      |          |\n| labelFillOpacity         | Combo label text color opacity                                                                                                                                             | number                                                                      | 1         |          |\n| labelFontFamily          | Combo label font family                                                                                                                                                    | string                                                                      | -         |          |\n| labelFontSize            | Combo label font size                                                                                                                                                      | number                                                                      | 12        |          |\n| labelFontStyle           | Combo label font style                                                                                                                                                     | `normal` \\| `italic` \\| `oblique`                                           | -         |          |\n| labelFontVariant         | Combo label font variant                                                                                                                                                   | `normal` \\| `small-caps` \\| string                                          | -         |          |\n| labelFontWeight          | Combo label font weight                                                                                                                                                    | `normal` \\| `bold` \\| `bolder` \\| `lighter` \\| number                       | 400       |          |\n| labelLeading             | Line spacing                                                                                                                                                               | number                                                                      | 0         |          |\n| labelLetterSpacing       | Combo label letter spacing                                                                                                                                                 | number \\| string                                                            | -         |          |\n| labelLineHeight          | Combo label line height                                                                                                                                                    | number \\| string                                                            | -         |          |\n| labelMaxLines            | Combo label maximum lines                                                                                                                                                  | number                                                                      | 1         |          |\n| labelMaxWidth            | Combo label maximum width, [options](#labelmaxwidth)                                                                                                                       | number \\| string                                                            | `200%`    |          |\n| labelOffsetX             | Combo label X offset                                                                                                                                                       | number                                                                      | 0         |          |\n| labelOffsetY             | Combo label Y offset                                                                                                                                                       | number                                                                      | 0         |          |\n| labelPadding             | Combo label padding                                                                                                                                                        | number \\| number[]                                                          | 0         |          |\n| labelPlacement           | Combo label position relative to combo main graphic, [options](#labelplacement)                                                                                            | string                                                                      | `bottom`  |          |\n| labelText                | Combo label text content                                                                                                                                                   | string                                                                      | -         |          |\n| labelTextAlign           | Combo label text horizontal alignment                                                                                                                                      | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`               | `left`    |          |\n| labelTextBaseline        | Combo label text baseline                                                                                                                                                  | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom` | -         |          |\n| labelTextDecorationColor | Combo label text decoration color                                                                                                                                          | string                                                                      | -         |          |\n| labelTextDecorationLine  | Combo label text decoration line                                                                                                                                           | string                                                                      | -         |          |\n| labelTextDecorationStyle | Combo label text decoration style                                                                                                                                          | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                       | -         |          |\n| labelTextOverflow        | Combo label text overflow handling                                                                                                                                         | `clip` \\| `ellipsis` \\| string                                              | -         |          |\n| labelTextPath            | Combo label text path                                                                                                                                                      | Path                                                                        | -         |          |\n| labelWordWrap            | Whether combo label enables auto line wrapping. When labelWordWrap is enabled, parts exceeding labelMaxWidth automatically wrap                                            | boolean                                                                     | false     |          |\n| labelZIndex              | Combo label rendering layer                                                                                                                                                | number                                                                      | 0         |          |\n| `label{StyleProps}`      | More label style configurations, refer to [TextStyleProps](https://g.antv.antgroup.com/api/basic/text) property values. For example, labelOpacity represents label opacity | [TextStyleProps](https://g.antv.antgroup.com/api/basic/text)                | -         |          |\n\n#### LabelPlacement\n\nLabel position relative to combo main graphic, available values:\n\n- `center`: Label at combo center\n- `top`, `bottom`, `left`, `right`: Label at top, bottom, left, right of combo\n- `top-left`, `top-right`, `bottom-left`, `bottom-right`: Label at four corners of combo\n- `left-top`, `left-bottom`, `right-top`, `right-bottom`: Label at edge endpoints of combo\n\n#### LabelMaxWidth\n\nWhen auto line wrapping `labelWordWrap` is enabled, text wraps when exceeding this width:\n\n- string: Defines maximum width as percentage relative to combo element width. For example, `50%` means label width doesn't exceed half of combo width\n- number: Defines maximum width in pixels. For example, 100 means label maximum width is 100 pixels\n\nFor example, setting multi-line label text:\n\n```json\n{\n  \"labelWordWrap\": true,\n  \"labelMaxWidth\": 200,\n  \"labelMaxLines\": 3\n}\n```\n\n### Label Background Style\n\nLabel background provides background decoration for label text, improving label readability and visual effects.\n\n#### Basic Background Style\n\nAdd simple background to label:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      labelText: 'Important Combo',\n      labelFill: '#fff', // White text\n      labelBackground: true, // Enable background\n      labelBackgroundFill: '#1783FF', // Blue background\n      labelBackgroundPadding: [4, 8], // Padding: vertical 4px, horizontal 8px\n      labelBackgroundRadius: 4, // Border radius\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Gradient Background Effect\n\nCreate label background with gradient effect:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      labelText: 'VIP Combo',\n      labelFill: '#fff',\n      labelFontWeight: 'bold',\n      labelBackground: true,\n      labelBackgroundFill: 'linear-gradient(45deg, #FF6B6B, #4ECDC4)', // Gradient background\n      labelBackgroundPadding: [6, 12],\n      labelBackgroundRadius: 20, // Large border radius\n      labelBackgroundShadowColor: 'rgba(0,0,0,0.2)',\n      labelBackgroundShadowBlur: 4,\n      labelBackgroundShadowOffsetY: 2,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Stroke-only Background Style\n\nCreate label background with stroke only:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      labelText: 'Border Label',\n      labelFill: '#1783FF',\n      labelBackground: true,\n      labelBackgroundFill: 'transparent', // Transparent background\n      labelBackgroundStroke: '#1783FF', // Blue stroke\n      labelBackgroundLineWidth: 2, // Stroke width\n      labelBackgroundPadding: [4, 8],\n      labelBackgroundRadius: 8,\n    },\n  },\n});\n\ngraph.render();\n```\n\nHere are the complete label background style configurations:\n\n| Property                      | Description                                                                                                                                                                                                | Type                                                         | Default      |\n| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------ |\n| labelBackground               | Whether to show combo label background                                                                                                                                                                     | boolean                                                      | false        |\n| labelBackgroundCursor         | Combo label background cursor style, [options](#cursor)                                                                                                                                                    | string                                                       | `default`    |\n| labelBackgroundFill           | Combo label background fill color                                                                                                                                                                          | string                                                       | #000         |\n| labelBackgroundFillOpacity    | Combo label background opacity                                                                                                                                                                             | number                                                       | 0.75         |\n| labelBackgroundHeight         | Combo label background height                                                                                                                                                                              | string \\| number                                             | -            |\n| labelBackgroundLineDash       | Combo label background dash configuration                                                                                                                                                                  | number \\| string \\|(number \\| string )[]                     | -            |\n| labelBackgroundLineDashOffset | Combo label background dash offset                                                                                                                                                                         | number                                                       | -            |\n| labelBackgroundLineWidth      | Combo label background stroke line width                                                                                                                                                                   | number                                                       | -            |\n| labelBackgroundPadding        | Combo label background padding                                                                                                                                                                             | number \\| number[]                                           | [2, 4, 2, 4] |\n| labelBackgroundRadius         | Combo label background border radius <br> - number: Set all four corner radius uniformly <br> - number[]: Set four corner radius separately, missing values auto-filled                                    | number \\| number[]                                           | 0            |\n| labelBackgroundShadowBlur     | Combo label background shadow blur                                                                                                                                                                         | number                                                       | -            |\n| labelBackgroundShadowColor    | Combo label background shadow color                                                                                                                                                                        | string                                                       | -            |\n| labelBackgroundShadowOffsetX  | Combo label background shadow X offset                                                                                                                                                                     | number                                                       | -            |\n| labelBackgroundShadowOffsetY  | Combo label background shadow Y offset                                                                                                                                                                     | number                                                       | -            |\n| labelBackgroundStroke         | Combo label background stroke color                                                                                                                                                                        | string                                                       | -            |\n| labelBackgroundStrokeOpacity  | Combo label background stroke opacity                                                                                                                                                                      | number \\| string                                             | 1            |\n| labelBackgroundVisibility     | Whether combo label background is visible                                                                                                                                                                  | `visible` \\| `hidden`                                        | -            |\n| labelBackgroundZIndex         | Combo label background rendering layer                                                                                                                                                                     | number                                                       | 1            |\n| `labelBackground{StyleProps}` | More label background style configurations, refer to [RectStyleProps](https://g.antv.antgroup.com/api/basic/rect) property values. For example, labelBackgroundOpacity represents label background opacity | [RectStyleProps](https://g.antv.antgroup.com/api/basic/rect) | -            |\n\n### Badge Style\n\nBadges are small markers displayed on combos, usually used to show status, quantity, or other auxiliary information. Multiple badges can be displayed simultaneously with customizable positions.\n\n#### Single Badge\n\nAdd a simple badge to the combo:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      badges: [\n        { text: 'NEW' }, // Display at top by default\n      ],\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Multiple Badges\n\nAdd multiple badges at different positions to the combo:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      badge: true, // Whether to show badges\n      badges: [\n        { text: 'A', placement: 'right-top' },\n        { text: 'Important', placement: 'right' },\n        { text: 'Notice', placement: 'right-bottom' },\n      ],\n      badgePalette: ['#7E92B5', '#F4664A', '#FFBE3A'], // Badge background palette\n      badgeFontSize: 7, // Badge font size\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Custom Badge Style\n\nFully customize badge appearance:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      badges: [\n        {\n          text: '99+',\n          placement: 'right-top',\n          backgroundFill: '#FF4D4F', // Red background\n          fill: '#fff', // White text\n          fontSize: 10,\n          padding: [2, 6],\n          backgroundRadius: 8,\n        },\n      ],\n    },\n  },\n});\n\ngraph.render();\n```\n\nHere are the complete badge style configurations:\n\n| Property     | Description                    | Type                                  | Default                           |\n| ------------ | ------------------------------ | ------------------------------------- | --------------------------------- |\n| badge        | Whether to show combo badge    | boolean                               | true                              |\n| badgePalette | Combo badge background palette | string[]                              | [`#7E92B5`, `#F4664A`, `#FFBE3A`] |\n| badges       | Combo badge settings           | [BadgeStyleProps](#badgestyleprops)[] | -                                 |\n\n#### BadgeStyleProps\n\n| Property                 | Description                                                                                                                                                                                                                                                                                            | Type                                                                                                                                                                   | Default      |\n| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |\n| background               | Whether to show combo badge background                                                                                                                                                                                                                                                                 | boolean                                                                                                                                                                | true         |\n| backgroundCursor         | Combo badge background cursor style, [options](#cursor)                                                                                                                                                                                                                                                | string                                                                                                                                                                 | `default`    |\n| backgroundFill           | Combo badge background fill color. If not specified, consider badgePalette for sequential allocation                                                                                                                                                                                                   | string                                                                                                                                                                 | -            |\n| backgroundFillOpacity    | Combo badge background fill opacity                                                                                                                                                                                                                                                                    | number                                                                                                                                                                 | 1            |\n| backgroundFilter         | Combo badge background filter                                                                                                                                                                                                                                                                          | string                                                                                                                                                                 | -            |\n| backgroundHeight         | Combo badge background height                                                                                                                                                                                                                                                                          | number \\| string                                                                                                                                                       | -            |\n| backgroundLineDash       | Combo badge background dash configuration                                                                                                                                                                                                                                                              | number \\| string \\|(number \\| string )[]                                                                                                                               | -            |\n| backgroundLineDashOffset | Combo badge background dash offset                                                                                                                                                                                                                                                                     | number                                                                                                                                                                 | -            |\n| backgroundLineWidth      | Combo badge background stroke line width                                                                                                                                                                                                                                                               | number                                                                                                                                                                 | -            |\n| backgroundRadius         | Combo badge background border radius <br> - number: Set all four corner radius uniformly <br> - number[]: Set four corner radius separately, missing values will be filled <br> - string: Similar to [CSS padding](https://developer.mozilla.org/en-US/docs/Web/CSS/padding) property, space-separated | number \\| number[] \\| string                                                                                                                                           | 0            |\n| backgroundShadowBlur     | Combo badge background shadow blur                                                                                                                                                                                                                                                                     | number                                                                                                                                                                 | -            |\n| backgroundShadowColor    | Combo badge background shadow color                                                                                                                                                                                                                                                                    | string                                                                                                                                                                 | -            |\n| backgroundShadowOffsetX  | Combo badge background shadow X offset                                                                                                                                                                                                                                                                 | number                                                                                                                                                                 | -            |\n| backgroundShadowOffsetY  | Combo badge background shadow Y offset                                                                                                                                                                                                                                                                 | number                                                                                                                                                                 | -            |\n| backgroundStroke         | Combo badge background stroke color                                                                                                                                                                                                                                                                    | string                                                                                                                                                                 | -            |\n| backgroundStrokeOpacity  | Combo badge background stroke opacity                                                                                                                                                                                                                                                                  | number \\| string                                                                                                                                                       | 1            |\n| backgroundVisibility     | Whether combo badge background is visible                                                                                                                                                                                                                                                              | `visible` \\| `hidden`                                                                                                                                                  | -            |\n| fill                     | Combo badge text color                                                                                                                                                                                                                                                                                 | string                                                                                                                                                                 | -            |\n| fontFamily               | Combo badge font family                                                                                                                                                                                                                                                                                | string                                                                                                                                                                 | -            |\n| fontSize                 | Combo badge font size                                                                                                                                                                                                                                                                                  | number                                                                                                                                                                 | 8            |\n| fontStyle                | Combo badge font style                                                                                                                                                                                                                                                                                 | `normal` \\| `italic` \\| `oblique`                                                                                                                                      | `normal`     |\n| fontVariant              | Combo badge font variant                                                                                                                                                                                                                                                                               | `normal` \\| `small-caps` \\| string                                                                                                                                     | `normal`     |\n| fontWeight               | Combo badge font weight                                                                                                                                                                                                                                                                                | number \\| string                                                                                                                                                       | `normal`     |\n| lineHeight               | Combo badge line height                                                                                                                                                                                                                                                                                | string \\| number                                                                                                                                                       | -            |\n| lineWidth                | Combo badge line width                                                                                                                                                                                                                                                                                 | string \\| number                                                                                                                                                       | -            |\n| maxLines                 | Combo badge text maximum lines                                                                                                                                                                                                                                                                         | number                                                                                                                                                                 | 1            |\n| offsetX                  | Combo badge X offset                                                                                                                                                                                                                                                                                   | number                                                                                                                                                                 | 0            |\n| offsetY                  | Combo badge Y offset                                                                                                                                                                                                                                                                                   | number                                                                                                                                                                 | 0            |\n| padding                  | Combo badge padding                                                                                                                                                                                                                                                                                    | number \\| number[]                                                                                                                                                     | 0            |\n| placement                | Combo badge position relative to combo main graphic. If not specified, defaults to clockwise placement starting from top-right                                                                                                                                                                         | `left` \\| `right` \\| `top` \\| `bottom` \\| `left-top` \\| `left-bottom` \\| `right-top` \\| `right-bottom` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right` | -            |\n| text                     | Combo badge text content                                                                                                                                                                                                                                                                               | string                                                                                                                                                                 | -            |\n| textAlign                | Combo badge text horizontal alignment                                                                                                                                                                                                                                                                  | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`                                                                                                          | `left`       |\n| textBaseline             | Combo badge text baseline                                                                                                                                                                                                                                                                              | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom`                                                                                            | `alphabetic` |\n| textDecorationColor      | Combo badge text decoration color                                                                                                                                                                                                                                                                      | string                                                                                                                                                                 | -            |\n| textDecorationLine       | Combo badge text decoration line                                                                                                                                                                                                                                                                       | string                                                                                                                                                                 | -            |\n| textDecorationStyle      | Combo badge text decoration style                                                                                                                                                                                                                                                                      | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                                                                                                                  | `solid`      |\n| textOverflow             | Combo badge text overflow handling                                                                                                                                                                                                                                                                     | `clip` \\| `ellipsis` \\| string                                                                                                                                         | `clip`       |\n| visibility               | Whether combo badge is visible                                                                                                                                                                                                                                                                         | `visible` \\| `hidden`                                                                                                                                                  | -            |\n| wordWrap                 | Whether combo badge text auto-wraps                                                                                                                                                                                                                                                                    | boolean                                                                                                                                                                | -            |\n| zIndex                   | Combo badge rendering layer                                                                                                                                                                                                                                                                            | number                                                                                                                                                                 | 3            |\n\n### Halo Style\n\nHalo effect is used to highlight combos, usually used in mouse hover, selected, or active states, adding glow effect around combos.\n\n#### Basic Halo Effect\n\nAdd simple halo effect to combo:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      halo: true, // Enable halo\n      haloStroke: '#1783FF', // Blue halo\n      haloLineWidth: 8, // Halo width\n      haloStrokeOpacity: 0.3, // Halo opacity\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Colorful Halo Effect\n\nCreate colorful gradient halo effect:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      halo: true,\n      haloStroke: '#FF4D4F', // Red halo\n      haloLineWidth: 12, // Thicker halo\n      haloStrokeOpacity: 0.5,\n      haloFilter: 'blur(2px)', // Blur filter effect\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Dynamic Halo Effect\n\nUse halo effect in state transitions:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      // No halo in default state\n      halo: false,\n    },\n    state: {\n      // Show orange halo in hover state\n      hover: {\n        halo: true,\n        haloStroke: '#FF7A00',\n        haloLineWidth: 10,\n        haloStrokeOpacity: 0.4,\n      },\n      // Show green halo in selected state\n      selected: {\n        halo: true,\n        haloStroke: '#52C41A',\n        haloLineWidth: 6,\n        haloStrokeOpacity: 0.6,\n      },\n    },\n  },\n});\n\ngraph.render();\n```\n\nHere are the complete halo style configurations:\n\n| Property           | Description                                                                                                                                                                       | Type                                                                  | Default                         | Required |\n| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------- | -------- |\n| halo               | Whether to show combo halo                                                                                                                                                        | boolean                                                               | false                           |          |\n| haloCursor         | Combo halo cursor style, [options](#cursor)                                                                                                                                       | string                                                                | `default`                       |          |\n| haloDraggable      | Whether combo halo allows dragging                                                                                                                                                | boolean                                                               | true                            |          |\n| haloDroppable      | Whether combo halo allows receiving dragged elements                                                                                                                              | boolean                                                               | false                           |          |\n| haloFill           | Halo fill color                                                                                                                                                                   | string                                                                | Same as main graphic fill color |          |\n| haloFillRule       | Combo halo fill rule                                                                                                                                                              | `nonzero` \\| `evenodd`                                                | -                               |          |\n| haloFilter         | Combo halo filter effect, such as 'blur(2px)' for blur effect                                                                                                                     | string                                                                | -                               |          |\n| haloLineWidth      | Combo halo stroke width, controls halo thickness                                                                                                                                  | number                                                                | 12                              |          |\n| haloPointerEvents  | Whether combo halo effect responds to pointer events, [options](#pointerevents)                                                                                                   | string                                                                | `none`                          |          |\n| haloStroke         | Combo halo stroke color, **this property is used to set the color of halo around combo, helping to highlight the combo**                                                          | string                                                                | `#99add1`                       |          |\n| haloStrokeOpacity  | Combo halo stroke opacity, recommended to use 0.2-0.6 values for natural halo effect                                                                                              | number                                                                | 0.25                            |          |\n| haloVisibility     | Combo halo visibility                                                                                                                                                             | `visible` \\| `hidden`                                                 | `visible`                       |          |\n| haloZIndex         | Combo halo rendering layer, usually set to negative value to ensure halo is below combo main graphic                                                                              | number                                                                | -1                              |          |\n| `halo{StyleProps}` | More halo style configurations, refer to [DisplayObject](https://g.antv.antgroup.com/api/basic/display-object) options. For example, haloFillOpacity represents halo fill opacity | [DisplayObject](https://g.antv.antgroup.com/api/basic/display-object) | -                               |          |\n\n**Halo Usage Recommendations:**\n\n1. **Performance Consideration**: Halo effects increase rendering burden, recommend enabling only when necessary\n2. **Color Matching**: Halo color should coordinate with combo main color tone, avoid being too abrupt\n3. **Opacity Setting**: Reasonable opacity (0.2-0.6) can create natural halo effect\n4. **State Application**: Halo is usually used for hover, selected, active and other interactive states\n\n### Icon Style\n\nIcons are used to display text or image content in combos, usually located at the center of the combo, can be used to represent combo type or function.\n\n#### Text Icon\n\nUse text as combo icon:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      iconText: 'A', // Display letter A\n      iconFill: '#1783FF', // Blue text\n      iconFontSize: 24, // Large font\n      iconFontWeight: 'bold', // Bold\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Image Icon\n\nUse image as combo icon:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      fill: '#1890FF',\n      iconSrc: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',\n      iconWidth: 32,\n      iconHeight: 32,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Colorful Text Icon\n\nCreate text icon with special styles:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      iconText: 'Dept',\n      iconFill: '#FF4D4F', // Red text\n      iconFontSize: 16,\n      iconFontWeight: 'bold',\n      iconFontStyle: 'italic', // Italic\n      iconTextDecorationLine: 'underline', // Underline\n      iconLetterSpacing: 1, // Letter spacing\n    },\n  },\n});\n\ngraph.render();\n```\n\nHere are the complete icon style configurations:\n\n| Property                | Description                                                                                                               | Type                                                                        | Default                     |\n| ----------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------- |\n| icon                    | Whether to show combo icon                                                                                                | boolean                                                                     | true                        |\n| iconCursor              | Combo icon cursor style, [options](#cursor)                                                                               | string                                                                      | `default`                   |\n| iconFill                | Combo icon text color                                                                                                     | string                                                                      | -                           |\n| iconFillOpacity         | Combo icon text color opacity                                                                                             | number                                                                      | 1                           |\n| iconFontFamily          | Combo icon font family                                                                                                    | string                                                                      | -                           |\n| iconFontSize            | Combo icon font size                                                                                                      | number                                                                      | 16                          |\n| iconFontStyle           | Combo icon font style                                                                                                     | `normal` \\| `italic` \\| `oblique`                                           | `normal`                    |\n| iconFontVariant         | Combo icon font variant                                                                                                   | `normal` \\| `small-caps` \\| string                                          | `normal`                    |\n| iconFontWeight          | Combo icon font weight                                                                                                    | number \\| string                                                            | `normal`                    |\n| iconHeight              | Combo icon height, used to control image size when using image icon                                                       | number                                                                      | Half of main graphic height |\n| iconLetterSpacing       | Combo icon text letter spacing                                                                                            | number \\| string                                                            | -                           |\n| iconLineHeight          | Combo icon text line height                                                                                               | number \\| string                                                            | -                           |\n| iconMaxLines            | Combo icon text maximum lines                                                                                             | number                                                                      | 1                           |\n| iconOffsetX             | Combo icon X offset                                                                                                       | number                                                                      | 0                           |\n| iconOffsetY             | Combo icon Y offset                                                                                                       | number                                                                      | 0                           |\n| iconOpacity             | Combo icon opacity                                                                                                        | number                                                                      | 1                           |\n| iconRadius              | Combo icon border radius (only effective for rectangular icons)                                                           | number                                                                      | 0                           |\n| iconSrc                 | Combo image source. Has higher priority than iconText, supports local and network images                                  | string                                                                      | -                           |\n| iconText                | Combo icon text content, supports text, Unicode characters, etc.                                                          | string                                                                      | -                           |\n| iconTextAlign           | Combo icon text horizontal alignment                                                                                      | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`               | `center`                    |\n| iconTextBaseline        | Combo icon text baseline                                                                                                  | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom` | `middle`                    |\n| iconTextDecorationColor | Combo icon text decoration color                                                                                          | string                                                                      | -                           |\n| iconTextDecorationLine  | Combo icon text decoration line, such as underline, strikethrough, etc.                                                   | string                                                                      | -                           |\n| iconTextDecorationStyle | Combo icon text decoration style                                                                                          | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                       | `solid`                     |\n| iconTextOverflow        | Combo icon text overflow handling                                                                                         | `clip` \\| `ellipsis` \\| string                                              | `clip`                      |\n| iconVisibility          | Whether combo icon is visible                                                                                             | `visible` \\| `hidden`                                                       | `visible`                   |\n| iconWidth               | Combo icon width, used to control image size when using image icon                                                        | number                                                                      | Half of main graphic width  |\n| iconWordWrap            | Whether combo icon text auto-wraps                                                                                        | boolean                                                                     | false                       |\n| iconZIndex              | Combo icon rendering layer                                                                                                | number                                                                      | 1                           |\n| `icon{StyleProps}`      | More icon style configurations, refer to specific icon type options. For example, iconStroke represents icon stroke color | -                                                                           | -                           |\n\n**Icon Usage Recommendations:**\n\n1. **Priority**: `iconSrc` (image) has higher priority than `iconText` (text), if both are set, image will be displayed first\n2. **Size Control**: Recommend setting icon size reasonably according to combo size, avoid icons being too large or small affecting visual effect\n3. **Performance Optimization**: Text icons have better performance, image icons require additional network requests and rendering overhead\n4. **Style Consistency**: Icon styles in the same graph should be consistent to improve overall visual effect\n5. **Accessibility**: Ensure icon color has sufficient contrast with background for easy user identification\n\n## State\n\nIn some interactive behaviors, such as clicking to select a combo or hovering to activate an edge, it is merely marking certain states on the element. To reflect these states in the visual space seen by the end user, we need to set different graphic element styles for different states to respond to changes in the element's state.\n\nG6 provides several built-in states, including selected, highlight, active, inactive, and disabled. In addition, it also supports custom states to meet more specific needs. For each state, developers can define a set of style rules that will override the default styles of the element.\n\n<img width=\"520\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Iv_dS5XR2TcAAAAAAAAAAAAADmJ7AQ/original\" />\n\nThe data structure is as follows:\n\n```typescript\ntype ComboState = {\n  [state: string]: ComboStyle;\n};\n```\n\nFor example, when the combo is in the `focus` state, you can add a stroke with a width of 3 and a color of orange.\n\n```js {4-7}\nconst graph = new Graph({\n  combo: {\n    state: {\n      focus: {\n        lineWidth: 3, // Stroke width\n        stroke: 'orange', // Stroke color\n      },\n    },\n  },\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1', states: ['focus'] }],\n  },\n  combo: {\n    state: {\n      focus: {\n        lineWidth: 3,\n        stroke: 'orange',\n        fill: 'orange',\n        fillOpacity: 0.2,\n      },\n    },\n  },\n});\n\ngraph.render();\n```\n\n**⚠️ Dynamic Configuration**: State configuration also supports dynamic configuration, which can be used to set styles dynamically based on combo data:\n\n```js\nconst graph = new Graph({\n  combo: {\n    state: {\n      // Static configuration\n      selected: {\n        stroke: '#1783FF',\n        lineWidth: 2,\n      },\n\n      // Dynamic configuration - arrow function form\n      hover: (datum) => ({\n        fill: datum.data.isVIP ? '#FFD700' : '#1783FF',\n        fillOpacity: 0.3,\n      }),\n\n      // Dynamic configuration - regular function form (access to graph instance)\n      active: function (datum) {\n        console.log(this); // graph instance\n        return {\n          stroke: datum.data.level > 3 ? '#FF4D4F' : '#52C41A',\n          lineWidth: 3,\n        };\n      },\n    },\n  },\n});\n```\n\n**⚠️ State Priority**: When a combo has multiple states simultaneously, the style merge follows the following priority (high to low):\n\n1. Later defined states override earlier defined states\n2. More specific selectors have higher priority\n3. Dynamic configuration has higher priority than static configuration\n\nFor example, if a combo has both `selected` and `hover` states, and `hover` is defined after `selected`, then `hover` state styles will override `selected` state styles.\n\n## Animation\n\nDefines the animation effects for combos, supporting the following two configuration methods:\n\n1. Disable all combo animations\n\n```json\n{\n  \"combo\": {\n    \"animation\": false\n  }\n}\n```\n\n2. Configure stage animations\n\nStage animations refer to animation effects when combos enter the canvas, update, or leave the canvas. Currently supported stages include:\n\n- `enter`: Animation when combo enters the canvas\n- `update`: Animation when combo updates\n- `exit`: Animation when combo leaves the canvas\n- `show`: Animation when combo shows from hidden state\n- `hide`: Animation when combo hides\n- `collapse`: Animation when combo collapses\n- `expand`: Animation when combo expands\n\nYou can refer to [Animation Paradigm](/en/manual/animation/animation#animation-paradigm) to use animation syntax to configure combos, such as:\n\n#### Enter Animation\n\nConfigure animation when combo enters the canvas:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    animation: {\n      enter: [\n        {\n          fields: ['opacity'], // Animate opacity property\n          from: 0, // Start from 0\n          to: 1, // End at 1\n          duration: 1000, // Animation duration\n          easing: 'ease-out', // Easing function\n        },\n      ],\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Update Animation\n\nConfigure animation when combo updates:\n\n```js\nconst graph = new Graph({\n  combo: {\n    animation: {\n      update: [\n        {\n          fields: ['x', 'y'], // Only animate x and y properties during update\n          duration: 1000, // Animation duration\n          easing: 'linear', // Easing function\n        },\n      ],\n    },\n  },\n});\n```\n\n#### Exit Animation\n\nConfigure animation when combo leaves the canvas:\n\n```js\nconst graph = new Graph({\n  combo: {\n    animation: {\n      exit: [\n        {\n          fields: ['opacity'], // Animate opacity property\n          to: 0, // End at 0\n          duration: 500, // Animation duration\n          easing: 'ease-in', // Easing function\n        },\n      ],\n    },\n  },\n});\n```\n\n#### Show/Hide Animation\n\nConfigure animation when combo shows/hides:\n\n```js\nconst graph = new Graph({\n  combo: {\n    animation: {\n      show: [\n        {\n          fields: ['opacity'],\n          from: 0,\n          to: 1,\n          duration: 300,\n        },\n      ],\n      hide: [\n        {\n          fields: ['opacity'],\n          to: 0,\n          duration: 300,\n        },\n      ],\n    },\n  },\n});\n```\n\nYou can also use built-in animation effects:\n\n```json\n{\n  \"combo\": {\n    \"animation\": {\n      \"enter\": \"fade\", // Use fade animation\n      \"update\": \"translate\", // Use translate animation\n      \"exit\": \"fade\" // Use fade animation\n    }\n  }\n}\n```\n\nYou can pass `false` to disable specific stage animations:\n\n```json\n{\n  \"combo\": {\n    \"animation\": {\n      \"enter\": false // Disable combo enter animation\n    }\n  }\n}\n```\n\n**Animation Configuration Options:**\n\n| Property  | Description                             | Type                                                        | Default  |\n| --------- | --------------------------------------- | ----------------------------------------------------------- | -------- |\n| fields    | Properties to animate                   | string[]                                                    | -        |\n| from      | Starting value                          | number \\| string                                            | -        |\n| to        | Ending value                            | number \\| string                                            | -        |\n| duration  | Animation duration (milliseconds)       | number                                                      | 1000     |\n| easing    | Easing function                         | string                                                      | 'ease'   |\n| delay     | Animation delay (milliseconds)          | number                                                      | 0        |\n| repeat    | Number of repetitions (-1 for infinite) | number                                                      | 0        |\n| direction | Animation direction                     | 'normal' \\| 'reverse' \\| 'alternate' \\| 'alternate-reverse' | 'normal' |\n\n## Palette\n\nDefines combo color palette, i.e., predefined combo color pool, and allocates according to rules, mapping colors to the `fill` property.\n\n> For palette definition, please refer to [Palette](/en/manual/theme/palette).\n\n| Property | Description                                                                                                          | Type                              | Default |\n| -------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------- |\n| type     | Specifies current palette type. <br> - `group`: Discrete palette <br> - `value`: Continuous palette                  | `group` &#124; `value`            | `group` |\n| field    | Specifies grouping field in element data. If not specified, defaults to id as grouping field                         | string &#124; ((datum) => string) | `id`    |\n| color    | Palette colors. If palette is registered, you can directly specify its registration name, also accepts a color array | string &#124; string[]            | -       |\n| invert   | Whether to invert the palette                                                                                        | boolean                           | false   |\n\nFor example, assign combo colors to a group of data by `category` field, so that combos of the same category have the same color:\n\n```json\n{\n  \"combo\": {\n    \"palette\": {\n      \"type\": \"group\",\n      \"field\": \"category\",\n      \"color\": [\"#1783FF\", \"#F08F56\", \"#D580FF\", \"#00C9C9\", \"#7863FF\"]\n    }\n  }\n}\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 100,\n  data: {\n    combos: new Array(8)\n      .fill(0)\n      .map((_, i) => ({ id: `combo-${i}`, data: { category: ['A', 'B', 'C', 'D', 'E'][i % 5] } })),\n  },\n  layout: { type: 'grid', cols: 8 },\n  combo: {\n    style: { fillOpacity: 0.4 },\n    palette: {\n      type: 'group',\n      field: 'category',\n      color: ['#1783FF', '#F08F56', '#D580FF', '#00C9C9', '#7863FF'],\n    },\n  },\n});\n\ngraph.render();\n```\n\nYou can also use default configuration:\n\n```json\n{\n  \"combo\": {\n    \"palette\": \"tableau\" // tableau is palette name, defaults to assign colors by ID\n  }\n}\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 100,\n  data: {\n    combos: new Array(8)\n      .fill(0)\n      .map((_, i) => ({ id: `combo-${i}`, data: { category: ['A', 'B', 'C', 'D', 'E'][i % 5] } })),\n  },\n  layout: { type: 'grid', cols: 8 },\n  combo: {\n    style: { fillOpacity: 0.4 },\n    palette: 'tableau',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/combo/BaseCombo.zh.md",
    "content": "---\ntitle: 组合通用配置项\norder: 1\n---\n\n本文介绍内置组合通用属性配置。\n\n## ComboOptions\n\n```js {5-9}\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  combo: {\n    type: 'circle', // 组合类型\n    style: {}, // 组合样式\n    state: {}, // 状态样式\n    palette: {}, // 色板配置\n    animation: {}, // 动画配置\n  },\n});\n```\n\n| 属性      | 描述                                         | 类型                    | 默认值   | 必选 |\n| --------- | -------------------------------------------- | ----------------------- | -------- | ---- |\n| type      | 组合类型，内置组合类型名称或自定义组合的名称 | [Type](#type)           | `circle` |      |\n| style     | 组合样式配置，包括颜色、大小等               | [Style](#style)         | -        |      |\n| state     | 不同状态下的样式配置                         | [State](#state)         | -        |      |\n| palette   | 定义组合的色板，用于根据不同数据映射颜色     | [Palette](#palette)     | -        |      |\n| animation | 定义组合的动画效果                           | [Animation](#animation) | -        |      |\n\n## Type\n\n指定组合类型，内置组合类型名称或自定义组合的名称。默认为 `circle`(圆形)。**⚠️ 注意**：这里决定了主图形的形状。\n\n```js {3}\nconst graph = new Graph({\n  combo: {\n    type: 'circle',\n  },\n});\n```\n\n**⚠️ 动态配置说明**：`type` 属性同样支持动态配置，可以根据组合数据动态选择组合类型：\n\n```js\nconst graph = new Graph({\n  combo: {\n    // 静态配置\n    type: 'circle',\n\n    // 动态配置 - 箭头函数形式\n    type: (datum) => datum.data.comboType || 'circle',\n\n    // 动态配置 - 普通函数形式（可访问 graph 实例）\n    type: function (datum) {\n      console.log(this); // graph 实例\n      return datum.data.category === 'important' ? 'rect' : 'circle';\n    },\n  },\n});\n```\n\n可选值有：\n\n- `circle`：[圆形组合](/manual/element/combo/circle)\n- `rect`：[矩形组合](/manual/element/combo/rect)\n\n## Style\n\n定义组合的样式，包括颜色、大小等。\n\n```js {3}\nconst graph = new Graph({\n  combo: {\n    style: {},\n  },\n});\n```\n\n**⚠️ 动态配置说明**：以下所有样式属性都支持动态配置，即可以传入函数来根据组合数据动态计算属性值：\n\n```js\nconst graph = new Graph({\n  combo: {\n    style: {\n      // 静态配置\n      fill: '#1783FF',\n\n      // 动态配置 - 箭头函数形式\n      stroke: (datum) => (datum.data.isActive ? '#FF0000' : '#000000'),\n\n      // 动态配置 - 普通函数形式（可访问 graph 实例）\n      lineWidth: function (datum) {\n        console.log(this); // graph 实例\n        return datum.data.importance > 5 ? 3 : 1;\n      },\n\n      // 嵌套属性也支持动态配置\n      labelText: (datum) => `组合: ${datum.id}`,\n      badges: (datum) => datum.data.tags.map((tag) => ({ text: tag })),\n    },\n  },\n});\n```\n\n其中 `datum` 参数为组合数据对象 (`ComboData`)，包含组合的所有数据信息。\n\n一个完整的组合由以下几部分构成：\n\n<img width=\"240\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*z-OxR4MAdUwAAAAAAAAAAAAADmJ7AQ/original\" />\n\n- `key` ：组合的主图形，表示组合的主要形状，例如圆形、矩形等；\n- `label` ：文本标签，通常用于展示组合的名称或描述；\n- `halo` ：主图形周围展示的光晕效果的图形；\n- `badge` ：默认位于组合右上角的徽标；\n\n以下样式配置将按原子图形依次说明：\n\n### 主图形样式\n\n主图形是组合的核心部分，定义了组合的基本形状和外观。以下是常见的配置场景：\n\n#### 基础样式配置\n\n设置组合的基本外观：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      fill: '#5B8FF9', // 蓝色填充\n      stroke: '#1A1A1A', // 深色描边\n      lineWidth: 2,\n      fillOpacity: 0.2,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 透明度和阴影效果\n\n为组合添加透明度和阴影效果：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      fill: '#61DDAA',\n      fillOpacity: 0.15,\n      shadowColor: 'rgba(97, 221, 170, 0.4)',\n      shadowBlur: 12,\n      shadowOffsetX: 2,\n      shadowOffsetY: 4,\n      stroke: '#F0F0F0',\n      lineWidth: 1,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 虚线边框样式\n\n创建带虚线边框的组合：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      fill: '#FFF1F0',\n      fillOpacity: 0.1,\n      stroke: '#F5222D',\n      lineWidth: 2,\n      lineDash: [6, 4],\n      lineCap: 'round',\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的主图形样式配置：\n\n| 属性                            | 描述                                                                                      | 类型                          | 默认值    | 必选 |\n| ------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------- | --------- | ---- |\n| collapsed                       | 当前组合是否折叠                                                                          | boolean                       | false     |      |\n| cursor                          | 组合鼠标移入样式，[配置项](#cursor)                                                       | string                        | default   |      |\n| fill                            | 组合填充色                                                                                | string                        | `#99ADD1` |      |\n| fillOpacity                     | 组合填充色透明度                                                                          | number \\| string              | 0.04      |      |\n| increasedLineWidthForHitTesting | 当 lineWidth 较小时，可交互区域也随之变小，有时我们想增大这个区域，让\"细线\"更容易被拾取到 | number                        | 0         |      |\n| lineCap                         | 组合描边端点样式                                                                          | `round` \\| `square` \\| `butt` | `butt`    |      |\n| lineDash                        | 组合描边虚线样式                                                                          | number[]                      | -         |      |\n| lineDashOffset                  | 组合描边虚线偏移量                                                                        | number                        | -         |      |\n| lineJoin                        | 组合描边连接处样式                                                                        | `round` \\| `bevel` \\| `miter` | `miter`   |      |\n| lineWidth                       | 组合描边宽度                                                                              | number                        | 1         |      |\n| opacity                         | 组合透明度                                                                                | number \\| string              | 1         |      |\n| pointerEvents                   | 组合如何响应指针事件，[配置项](#pointerevents)                                            | string                        | `auto`    |      |\n| shadowBlur                      | 组合阴影模糊度                                                                            | number                        | -         |      |\n| shadowColor                     | 组合阴影颜色                                                                              | string                        | -         |      |\n| shadowOffsetX                   | 组合阴影在 x 轴方向上的偏移量                                                             | number \\| string              | -         |      |\n| shadowOffsetY                   | 组合阴影在 y 轴方向上的偏移量                                                             | number \\| string              | -         |      |\n| shadowType                      | 组合阴影类型                                                                              | `inner` \\| `outer`            | `outer`   |      |\n| size                            | 组合大小，快捷设置组合宽高，[配置项](#size)                                               | number \\| number[]            | -         |      |\n| stroke                          | 组合描边色                                                                                | string                        | `#99ADD1` |      |\n| strokeOpacity                   | 组合描边色透明度                                                                          | number \\| string              | 1         |      |\n| transform                       | transform 属性允许你旋转、缩放、倾斜或平移给定组合                                        | string                        | -         |      |\n| transformOrigin                 | 旋转与缩放中心，也称作变换中心                                                            | string                        | -         |      |\n| visibility                      | 组合是否可见                                                                              | `visible` \\| `hidden`         | `visible` |      |\n| x                               | 组合 x 坐标                                                                               | number                        | 0         |      |\n| y                               | 组合 y 坐标                                                                               | number                        | 0         |      |\n| z                               | 组合 z 坐标                                                                               | number                        | 0         |      |\n| zIndex                          | 组合渲染层级                                                                              | number                        | 0         |      |\n\n#### Size\n\n组合大小，快捷设置组合宽高，支持三种配置方式：\n\n- number：表示组合宽高相同为指定值\n- [number, number]：表示组合宽高分别为数组元素依次表示组合的宽度、高度\n- [number, number, number]：表示组合宽高分别为数组元素依次表示组合的宽度、高度以及深度\n\n#### PointerEvents\n\n`pointerEvents` 属性控制图形如何响应交互事件，可参考 [MDN 文档](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events)。\n\n可选值有：`visible` | `visiblepainted` | `visiblestroke` | `non-transparent-pixel` | `visiblefill` | `visible` | `painted` | `fill` | `stroke` | `all` | `none` | `auto` | `inherit` | `initial` | `unset`\n\n简而言之，`fill`、`stroke` 和 `visibility` 都可以独立或组合影响拾取判定行为。目前支持以下关键词：\n\n- **`auto`**：默认值，等同于 `visiblepainted`\n- **`none`**：永远不会成为响应事件的目标\n- **`visiblepainted`**：满足以下条件才会响应事件：\n  - `visibility` 设置为 `visible`，即图形为可见的\n  - 在图形填充区域触发同时 `fill` 取非 `none` 的值；或者在图形描边区域触发同时 `stroke` 取非 `none` 的值\n- **`visiblefill`**：满足以下条件才会响应事件：\n  - `visibility` 设置为 `visible`，即图形为可见的\n  - 在图形填充区域触发，不受 `fill` 取值的影响\n- **`visiblestroke`**：满足以下条件才会响应事件：\n  - `visibility` 设置为 `visible`，即图形为可见的\n  - 在图形描边区域触发，不受 `stroke` 取值的影响\n- **`visible`**：满足以下条件才会响应事件：\n  - `visibility` 设置为 `visible`，即图形为可见的\n  - 在图形填充或者描边区域触发，不受 `fill` 和 `stroke` 取值的影响\n- **`painted`**：满足以下条件才会响应事件：\n  - 在图形填充区域触发同时 `fill` 取非 `none` 的值；或者在图形描边区域触发同时 `stroke` 取非 `none` 的值\n  - 不受 `visibility` 取值的影响\n- **`fill`**：满足以下条件才会响应事件：\n  - 在图形填充区域触发，不受 `fill` 取值的影响\n  - 不受 `visibility` 取值的影响\n- **`stroke`**：满足以下条件才会响应事件：\n  - 在图形描边区域触发，不受 `stroke` 取值的影响\n  - 不受 `visibility` 取值的影响\n- **`all`**：只要进入图形的填充和描边区域就会响应事件，不会受 `fill`、`stroke`、`visibility` 的取值影响\n\n**使用示例：**\n\n```js\n// 示例1：只有描边区域响应事件\nconst graph = new Graph({\n  combo: {\n    style: {\n      fill: 'none',\n      stroke: '#000',\n      lineWidth: 2,\n      pointerEvents: 'stroke', // 只有描边响应事件\n    },\n  },\n});\n\n// 示例2：完全不响应事件\nconst graph = new Graph({\n  combo: {\n    style: {\n      pointerEvents: 'none', // 组合不响应任何事件\n    },\n  },\n});\n```\n\n#### Cursor\n\n可选值有：`auto` | `default` | `none` | `context-menu` | `help` | `pointer` | `progress` | `wait` | `cell` | `crosshair` | `text` | `vertical-text` | `alias` | `copy` | `move` | `no-drop` | `not-allowed` | `grab` | `grabbing` | `all-scroll` | `col-resize` | `row-resize` | `n-resize` | `e-resize` | `s-resize` | `w-resize` | `ne-resize` | `nw-resize` | `se-resize` | `sw-resize` | `ew-resize` | `ns-resize` | `nesw-resize` | `nwse-resize` | `zoom-in` | `zoom-out`\n\n### 收起时样式\n\n当组合处于收起状态时（`collapsed` 为 `true`），可以为其配置特殊的样式。收起时的样式属性以 `collapsed` 为前缀。\n\n#### 基础收起样式\n\n为收起状态的组合设置不同的外观：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo1' },\n      { id: 'node2', combo: 'combo1' },\n    ],\n    combos: [{ id: 'combo1', style: { collapsed: true } }],\n  },\n  combo: {\n    style: {\n      collapsedFill: '#1783FF',\n      collapsedStroke: '#000',\n      collapsedLineWidth: 2,\n      collapsedSize: 40,\n      collapsedMarkerFill: '#fff',\n      collapsedMarkerFontSize: 12,\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为收起时样式的完整配置：\n\n| 属性                                     | 描述                                                                                                  | 类型                          | 默认值                             | 必选 |\n| ---------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------- | ---------------------------------- | ---- |\n| collapsedCursor                          | 组合收起时的鼠标移入样式，[配置项](#cursor)                                                           | string                        | 默认与展开时的 cursor 一致         |      |\n| collapsedFill                            | 组合收起时的填充色                                                                                    | string                        | 默认与展开时的 fill 一致           |      |\n| collapsedFillOpacity                     | 组合收起时的填充色透明度                                                                              | number \\| string              | 1                                  |      |\n| collapsedIncreasedLineWidthForHitTesting | 组合收起时，当 lineWidth 较小时，可交互区域也随之变小，有时我们想增大这个区域，让\"细线\"更容易被拾取到 | number                        | 0                                  |      |\n| collapsedLineCap                         | 组合收起时的描边端点样式                                                                              | `round` \\| `square` \\| `butt` | 默认与展开时的 lineCap 一致        |      |\n| collapsedLineDash                        | 组合收起时的描边虚线样式                                                                              | number[]                      | 默认与展开时的 lineDash 一致       |      |\n| collapsedLineDashOffset                  | 组合收起时的描边虚线偏移量                                                                            | number                        | 默认与展开时的 lineDashOffset 一致 |      |\n| collapsedLineJoin                        | 组合收起时的描边连接处样式                                                                            | `round` \\| `bevel` \\| `miter` | 默认与展开时的 lineJoin 一致       |      |\n| collapsedLineWidth                       | 组合收起时的描边宽度                                                                                  | number                        | 默认与展开时的 lineWidth 一致      |      |\n| collapsedMarker                          | 组合收起时是否显示标记，[配置项](#收起时标记样式)                                                     | boolean                       | true                               |      |\n| collapsedOpacity                         | 组合收起时的透明度                                                                                    | number \\| string              | 默认与展开时的 opacity 一致        |      |\n| collapsedShadowBlur                      | 组合收起时的阴影模糊度                                                                                | number                        | 默认与展开时的 shadowBlur 一致     |      |\n| collapsedShadowColor                     | 组合收起时的阴影颜色                                                                                  | string                        | 默认与展开时的 shadowColor 一致    |      |\n| collapsedShadowOffsetX                   | 组合收起时的阴影在 x 轴方向上的偏移量                                                                 | number \\| string              | 默认与展开时的 shadowOffsetX 一致  |      |\n| collapsedShadowOffsetY                   | 组合收起时的阴影在 y 轴方向上的偏移量                                                                 | number \\| string              | 默认与展开时的 shadowOffsetY 一致  |      |\n| collapsedShadowType                      | 组合收起时的阴影类型                                                                                  | `inner` \\| `outer`            | 默认与展开时的 shadowType 一致     |      |\n| collapsedSize                            | 组合收起时的大小                                                                                      | number \\| [number, number]    | 32                                 |      |\n| collapsedStroke                          | 组合收起时的描边色                                                                                    | string                        | 默认与展开时的 stroke 一致         |      |\n| collapsedStrokeOpacity                   | 组合收起时的描边色透明度                                                                              | number \\| string              | 默认与展开时的 strokeOpacity 一致  |      |\n| collapsedVisibility                      | 组合收起时是否可见                                                                                    | `visible` \\| `hidden`         | 默认与展开时的 visibility 一致     |      |\n\n### 收起时标记样式\n\n当 `collapsedMarker` 为 `true` 时显示的标记，用于显示收起组合内包含的元素数量。\n\n#### 自定义标记内容\n\n可以自定义收起标记显示的内容：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo1' },\n      { id: 'node2', combo: 'combo1' },\n      { id: 'node3', combo: 'combo1' },\n    ],\n    combos: [{ id: 'combo1', style: { collapsed: true } }],\n  },\n  combo: {\n    style: {\n      collapsedMarkerType: 'child-count',\n      collapsedMarkerFill: '#1783FF',\n      collapsedMarkerFontSize: 14,\n      collapsedMarkerFontWeight: 'bold',\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为收起时标记样式的完整配置：\n\n| 属性                        | 描述                                                                                                                                                                                                                                                          | 类型                                                                                                   | 默认值        | 必选 |\n| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------- | ---- |\n| collapsedMarkerType         | 组合收起时显示的标记类型 <br> - `'child-count'`: 子元素数量（包括 Node 和 Combo）<br>- `'descendant-count'`: 后代元素数量（包括 Node 和 Combo）<br>- `'node-count'`: 后代元素数量（只包括 Node）<br> - `(children: NodeLikeData[]) => string`: 自定义处理逻辑 | `child-count` \\| `descendant-count` \\| `node-count` \\| ((children: NodeData \\| ComboData[]) => string) | `child-count` |      |\n| collapsedMarkerFill         | 标记文字颜色                                                                                                                                                                                                                                                  | string                                                                                                 | #fff          |      |\n| collapsedMarkerFillOpacity  | 标记文字颜色透明度                                                                                                                                                                                                                                            | number                                                                                                 | 1             |      |\n| collapsedMarkerFontSize     | 标记字体大小                                                                                                                                                                                                                                                  | number                                                                                                 | 12            |      |\n| collapsedMarkerFontWeight   | 标记字体粗细                                                                                                                                                                                                                                                  | number \\| string                                                                                       | `normal`      |      |\n| collapsedMarkerRadius       | 标记圆角半径                                                                                                                                                                                                                                                  | number                                                                                                 | 0             |      |\n| collapsedMarkerSrc          | 图片来源。其优先级高于 `collapsedMarkerText`                                                                                                                                                                                                                  | string                                                                                                 | -             |      |\n| collapsedMarkerText         | 标记文字                                                                                                                                                                                                                                                      | string                                                                                                 | -             |      |\n| collapsedMarkerTextAlign    | 标记文字水平对齐方式                                                                                                                                                                                                                                          | `center` \\| `end` \\| `left` \\| `right` \\| `start`                                                      | `center`      |      |\n| collapsedMarkerTextBaseline | 标记文字对齐基线                                                                                                                                                                                                                                              | `alphabetic` \\| `bottom` \\| `hanging` \\| `ideographic` \\| `middle` \\| `top`                            | `middle`      |      |\n| collapsedMarkerWidth        | 标记宽度                                                                                                                                                                                                                                                      | number                                                                                                 | -             |      |\n| collapsedMarkerHeight       | 标记高度                                                                                                                                                                                                                                                      | number                                                                                                 | -             |      |\n| collapsedMarkerZIndex       | 标记层级                                                                                                                                                                                                                                                      | number                                                                                                 | 1             |      |\n\n### 徽标样式\n\n徽标是组合上显示的小标记，通常用于展示状态、数量或其他辅助信息。支持多个徽标同时显示，并可自定义位置。\n\n#### 单个徽标\n\n为组合添加一个简单的徽标：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      badges: [\n        { text: 'NEW' }, // 默认显示在上方\n      ],\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 多个徽标\n\n为组合添加多个不同位置的徽标：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      badge: true, // 是否显示徽标\n      badges: [\n        { text: 'A', placement: 'right-top' },\n        { text: 'Important', placement: 'right' },\n        { text: 'Notice', placement: 'right-bottom' },\n      ],\n      badgePalette: ['#7E92B5', '#F4664A', '#FFBE3A'], // 徽标的背景色板\n      badgeFontSize: 7, // 徽标字体大小\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 自定义徽标样式\n\n完全自定义徽标的外观：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      badges: [\n        {\n          text: '99+',\n          placement: 'right-top',\n          backgroundFill: '#FF4D4F', // 红色背景\n          fill: '#fff', // 白色文字\n          fontSize: 10,\n          padding: [2, 6],\n          backgroundRadius: 8,\n        },\n      ],\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的徽标样式配置：\n\n| 属性         | 描述               | 类型                                  | 默认值                            |\n| ------------ | ------------------ | ------------------------------------- | --------------------------------- |\n| badge        | 组合是否显示徽标   | boolean                               | true                              |\n| badgePalette | 组合徽标的背景色板 | string[]                              | [`#7E92B5`, `#F4664A`, `#FFBE3A`] |\n| badges       | 组合徽标设置       | [BadgeStyleProps](#badgestyleprops)[] | -                                 |\n\n#### BadgeStyleProps\n\n| 属性                     | 描述                                                                                                                                                                                                                              | 类型                                                                                                                                                                   | 默认值       |\n| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |\n| background               | 组合徽标是否显示背景                                                                                                                                                                                                              | boolean                                                                                                                                                                | true         |\n| backgroundCursor         | 组合徽标背景鼠标移入样式，[配置项](#cursor)                                                                                                                                                                                       | string                                                                                                                                                                 | `default`    |\n| backgroundFill           | 组合徽标背景填充色。若不指定，优先考虑 badgePalette 按顺序分配                                                                                                                                                                    | string                                                                                                                                                                 | -            |\n| backgroundFillOpacity    | 组合徽标背景填充透明度                                                                                                                                                                                                            | number                                                                                                                                                                 | 1            |\n| backgroundFilter         | 组合徽标背景滤镜                                                                                                                                                                                                                  | string                                                                                                                                                                 | -            |\n| backgroundHeight         | 组合徽标背景高度                                                                                                                                                                                                                  | number \\| string                                                                                                                                                       | -            |\n| backgroundLineDash       | 组合徽标背景虚线配置                                                                                                                                                                                                              | number \\| string \\|(number \\| string )[]                                                                                                                               | -            |\n| backgroundLineDashOffset | 组合徽标背景虚线偏移量                                                                                                                                                                                                            | number                                                                                                                                                                 | -            |\n| backgroundLineWidth      | 组合徽标背景描边线宽                                                                                                                                                                                                              | number                                                                                                                                                                 | -            |\n| backgroundRadius         | 组合徽标背景圆角半径 <br> - number: 统一设置四个圆角半径 <br> - number[]: 分别设置四个圆角半径，会补足缺省的分量 <br> - string: 与 [CSS padding](https://developer.mozilla.org/zh-CN/docs/Web/CSS/padding) 属性类似，使用空格分隔 | number \\| number[] \\| string                                                                                                                                           | 0            |\n| backgroundShadowBlur     | 组合徽标背景阴影模糊程度                                                                                                                                                                                                          | number                                                                                                                                                                 | -            |\n| backgroundShadowColor    | 组合徽标背景阴影颜色                                                                                                                                                                                                              | string                                                                                                                                                                 | -            |\n| backgroundShadowOffsetX  | 组合徽标背景阴影 X 方向偏移                                                                                                                                                                                                       | number                                                                                                                                                                 | -            |\n| backgroundShadowOffsetY  | 组合徽标背景阴影 Y 方向偏移                                                                                                                                                                                                       | number                                                                                                                                                                 | -            |\n| backgroundStroke         | 组合徽标背景描边颜色                                                                                                                                                                                                              | string                                                                                                                                                                 | -            |\n| backgroundStrokeOpacity  | 组合徽标背景描边透明度                                                                                                                                                                                                            | number \\| string                                                                                                                                                       | 1            |\n| backgroundVisibility     | 组合徽标背景是否可见                                                                                                                                                                                                              | `visible` \\| `hidden`                                                                                                                                                  | -            |\n| fill                     | 组合徽标文字颜色                                                                                                                                                                                                                  | string                                                                                                                                                                 | -            |\n| fontFamily               | 组合徽标字体族                                                                                                                                                                                                                    | string                                                                                                                                                                 | -            |\n| fontSize                 | 组合徽标字体大小                                                                                                                                                                                                                  | number                                                                                                                                                                 | 8            |\n| fontStyle                | 组合徽标字体样式                                                                                                                                                                                                                  | `normal` \\| `italic` \\| `oblique`                                                                                                                                      | `normal`     |\n| fontVariant              | 组合徽标字体变种                                                                                                                                                                                                                  | `normal` \\| `small-caps` \\| string                                                                                                                                     | `normal`     |\n| fontWeight               | 组合徽标字体粗细                                                                                                                                                                                                                  | number \\| string                                                                                                                                                       | `normal`     |\n| lineHeight               | 组合徽标行高                                                                                                                                                                                                                      | string \\| number                                                                                                                                                       | -            |\n| lineWidth                | 组合徽标行宽                                                                                                                                                                                                                      | string \\| number                                                                                                                                                       | -            |\n| maxLines                 | 组合徽标文本最大行数                                                                                                                                                                                                              | number                                                                                                                                                                 | 1            |\n| offsetX                  | 组合徽标在 x 轴方向上的偏移量                                                                                                                                                                                                     | number                                                                                                                                                                 | 0            |\n| offsetY                  | 组合徽标在 y 轴方向上的偏移量                                                                                                                                                                                                     | number                                                                                                                                                                 | 0            |\n| padding                  | 组合徽标内边距                                                                                                                                                                                                                    | number \\| number[]                                                                                                                                                     | 0            |\n| placement                | 组合徽标相对于组合主图形的位置。若不指定，默认从右上角顺时针依次排放                                                                                                                                                              | `left` \\| `right` \\| `top` \\| `bottom` \\| `left-top` \\| `left-bottom` \\| `right-top` \\| `right-bottom` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right` | -            |\n| text                     | 组合徽标文字内容                                                                                                                                                                                                                  | string                                                                                                                                                                 | -            |\n| textAlign                | 组合徽标文本水平对齐方式                                                                                                                                                                                                          | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`                                                                                                          | `left`       |\n| textBaseline             | 组合徽标文本基线                                                                                                                                                                                                                  | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom`                                                                                            | `alphabetic` |\n| textDecorationColor      | 组合徽标文本装饰线颜色                                                                                                                                                                                                            | string                                                                                                                                                                 | -            |\n| textDecorationLine       | 组合徽标文本装饰线                                                                                                                                                                                                                | string                                                                                                                                                                 | -            |\n| textDecorationStyle      | 组合徽标文本装饰线样式                                                                                                                                                                                                            | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                                                                                                                  | `solid`      |\n| textOverflow             | 组合徽标文本溢出处理方式                                                                                                                                                                                                          | `clip` \\| `ellipsis` \\| string                                                                                                                                         | `clip`       |\n| visibility               | 组合徽标是否可见                                                                                                                                                                                                                  | `visible` \\| `hidden`                                                                                                                                                  | -            |\n| wordWrap                 | 组合徽标文本是否自动换行                                                                                                                                                                                                          | boolean                                                                                                                                                                | -            |\n| zIndex                   | 组合徽标渲染层级                                                                                                                                                                                                                  | number                                                                                                                                                                 | 3            |\n\n### 标签样式\n\n标签用于显示组合的文本信息，支持丰富的文本样式配置和灵活的位置布局。\n\n#### 基础标签配置\n\n为组合添加基本的文本标签：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      labelText: '销售部门', // 标签文字内容\n      labelFill: '#1A1A1A', // 标签文字颜色\n      labelFontSize: 14, // 标签字体大小\n      labelPlacement: 'bottom', // 标签位置：底部\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 多行文本标签\n\n配置支持多行显示的标签：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 120,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      labelText: '这是一个支持多行显示的组合标签文本内容',\n      labelWordWrap: true, // 开启文本换行\n      labelMaxWidth: 100, // 最大宽度 100px\n      labelMaxLines: 3, // 最多显示 3 行\n      labelTextAlign: 'center', // 文本居中对齐\n      labelFontSize: 12,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 自定义样式标签\n\n创建具有特殊样式的标签：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      labelText: 'IMPORTANT',\n      labelFill: '#FF4D4F', // 红色文字\n      labelFontSize: 16,\n      labelFontWeight: 'bold', // 粗体\n      labelFontStyle: 'italic', // 斜体\n      labelTextDecorationLine: 'underline', // 下划线\n      labelLetterSpacing: 2, // 字间距\n      labelPlacement: 'top',\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的标签样式配置：\n\n| 属性                     | 描述                                                                                                                         | 类型                                                                        | 默认值    | 必选 |\n| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------- | ---- |\n| label                    | 是否显示组合标签                                                                                                             | boolean                                                                     | true      |      |\n| labelCursor              | 鼠标移入组合标签时显示的样式，[配置项](#cursor)                                                                              | string                                                                      | `default` |      |\n| labelFill                | 组合标签文字颜色                                                                                                             | string                                                                      | #000      |      |\n| labelFillOpacity         | 组合标签文字颜色的透明度                                                                                                     | number                                                                      | 1         |      |\n| labelFontFamily          | 组合标签字体族                                                                                                               | string                                                                      | -         |      |\n| labelFontSize            | 组合标签字体大小                                                                                                             | number                                                                      | 12        |      |\n| labelFontStyle           | 组合标签字体样式                                                                                                             | `normal` \\| `italic` \\| `oblique`                                           | -         |      |\n| labelFontVariant         | 组合标签字体变种                                                                                                             | `normal` \\| `small-caps` \\| string                                          | -         |      |\n| labelFontWeight          | 组合标签字体粗细                                                                                                             | `normal` \\| `bold` \\| `bolder` \\| `lighter` \\| number                       | 400       |      |\n| labelLeading             | 行间距                                                                                                                       | number                                                                      | 0         |      |\n| labelLetterSpacing       | 组合标签字间距                                                                                                               | number \\| string                                                            | -         |      |\n| labelLineHeight          | 组合标签行高                                                                                                                 | number \\| string                                                            | -         |      |\n| labelMaxLines            | 组合标签最大行数                                                                                                             | number                                                                      | 1         |      |\n| labelMaxWidth            | 组合标签最大宽度，[配置项](#labelmaxwidth)                                                                                   | number \\| string                                                            | `200%`    |      |\n| labelOffsetX             | 组合标签在 x 轴方向上的偏移量                                                                                                | number                                                                      | 0         |      |\n| labelOffsetY             | 组合标签在 y 轴方向上的偏移量                                                                                                | number                                                                      | 0         |      |\n| labelPadding             | 组合标签内边距                                                                                                               | number \\| number[]                                                          | 0         |      |\n| labelPlacement           | 组合标签相对于组合主图形的位置，[配置项](#labelplacement)                                                                    | string                                                                      | `bottom`  |      |\n| labelText                | 组合标签文字内容                                                                                                             | string                                                                      | -         |      |\n| labelTextAlign           | 组合标签文本水平对齐方式                                                                                                     | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`               | `left`    |      |\n| labelTextBaseline        | 组合标签文本基线                                                                                                             | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom` | -         |      |\n| labelTextDecorationColor | 组合标签文本装饰线颜色                                                                                                       | string                                                                      | -         |      |\n| labelTextDecorationLine  | 组合标签文本装饰线                                                                                                           | string                                                                      | -         |      |\n| labelTextDecorationStyle | 组合标签文本装饰线样式                                                                                                       | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                       | -         |      |\n| labelTextOverflow        | 组合标签文本溢出处理方式                                                                                                     | `clip` \\| `ellipsis` \\| string                                              | -         |      |\n| labelTextPath            | 组合标签文本路径                                                                                                             | Path                                                                        | -         |      |\n| labelWordWrap            | 组合标签是否开启自动折行。开启 labelWordWrap 后，超出 labelMaxWidth 的部分自动换行                                           | boolean                                                                     | false     |      |\n| labelZIndex              | 组合标签渲染层级                                                                                                             | number                                                                      | 0         |      |\n| `label{StyleProps}`      | 更多标签样式配置，参考 [TextStyleProps](https://g.antv.antgroup.com/api/basic/text) 属性值。比如 labelOpacity 代表标签透明度 | [TextStyleProps](https://g.antv.antgroup.com/api/basic/text)                | -         |      |\n\n#### LabelPlacement\n\n标签相对于组合主图形的位置，可选值有：\n\n- `center`：标签位于组合中心\n- `top`、`bottom`、`left`、`right`：标签位于组合的上、下、左、右方\n- `top-left`、`top-right`、`bottom-left`、`bottom-right`：标签位于组合的四个角\n- `left-top`、`left-bottom`、`right-top`、`right-bottom`：标签位于组合边的端点\n\n#### LabelMaxWidth\n\n开启自动折行 `labelWordWrap` 后，超出该宽度则换行:\n\n- string: 表示以相对于组合元素宽度的百分比形式定义最大宽度。例如 `50%` 表示标签宽度不超过组合宽度的一半\n- number: 表示以像素值为单位定义最大宽度。例如 100 表示标签的最大宽度为 100 像素\n\n比如，设置多行标签文字：\n\n```json\n{\n  \"labelWordWrap\": true,\n  \"labelMaxWidth\": 200,\n  \"labelMaxLines\": 3\n}\n```\n\n### 标签背景样式\n\n标签背景为标签文字提供背景装饰，可以提升标签的可读性和视觉效果。\n\n#### 基础背景样式\n\n为标签添加简单的背景：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      labelText: '重要组合',\n      labelFill: '#fff', // 白色文字\n      labelBackground: true, // 启用背景\n      labelBackgroundFill: '#1783FF', // 蓝色背景\n      labelBackgroundPadding: [4, 8], // 内边距：垂直4px，水平8px\n      labelBackgroundRadius: 4, // 圆角半径\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 渐变背景效果\n\n创建带渐变效果的标签背景：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      labelText: 'VIP组合',\n      labelFill: '#fff',\n      labelFontWeight: 'bold',\n      labelBackground: true,\n      labelBackgroundFill: 'linear-gradient(45deg, #FF6B6B, #4ECDC4)', // 渐变背景\n      labelBackgroundPadding: [6, 12],\n      labelBackgroundRadius: 20, // 大圆角\n      labelBackgroundShadowColor: 'rgba(0,0,0,0.2)',\n      labelBackgroundShadowBlur: 4,\n      labelBackgroundShadowOffsetY: 2,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 描边背景样式\n\n创建只有描边的标签背景：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      labelText: '边框标签',\n      labelFill: '#1783FF',\n      labelBackground: true,\n      labelBackgroundFill: 'transparent', // 透明背景\n      labelBackgroundStroke: '#1783FF', // 蓝色描边\n      labelBackgroundLineWidth: 2, // 描边宽度\n      labelBackgroundPadding: [4, 8],\n      labelBackgroundRadius: 8,\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的标签背景样式配置：\n\n| 属性                          | 描述                                                                                                                                           | 类型                                                         | 默认值       |\n| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------ |\n| labelBackground               | 组合标签背景是否显示                                                                                                                           | boolean                                                      | false        |\n| labelBackgroundCursor         | 组合标签背景鼠标移入样式，[配置项](#cursor)                                                                                                    | string                                                       | `default`    |\n| labelBackgroundFill           | 组合标签背景填充色                                                                                                                             | string                                                       | #000         |\n| labelBackgroundFillOpacity    | 组合标签背景透明度                                                                                                                             | number                                                       | 0.75         |\n| labelBackgroundHeight         | 组合标签背景高度                                                                                                                               | string \\| number                                             | -            |\n| labelBackgroundLineDash       | 组合标签背景虚线配置                                                                                                                           | number \\| string \\|(number \\| string )[]                     | -            |\n| labelBackgroundLineDashOffset | 组合标签背景虚线偏移量                                                                                                                         | number                                                       | -            |\n| labelBackgroundLineWidth      | 组合标签背景描边线宽                                                                                                                           | number                                                       | -            |\n| labelBackgroundPadding        | 组合标签背景内间距                                                                                                                             | number \\| number[]                                           | [2, 4, 2, 4] |\n| labelBackgroundRadius         | 组合标签背景圆角半径 <br> - number: 统一设置四个圆角半径 <br> - number[]: 分别设置四个圆角半径，不足则自动补充                                 | number \\| number[]                                           | 0            |\n| labelBackgroundShadowBlur     | 组合标签背景阴影模糊程度                                                                                                                       | number                                                       | -            |\n| labelBackgroundShadowColor    | 组合标签背景阴影颜色                                                                                                                           | string                                                       | -            |\n| labelBackgroundShadowOffsetX  | 组合标签背景阴影 X 方向偏移                                                                                                                    | number                                                       | -            |\n| labelBackgroundShadowOffsetY  | 组合标签背景阴影 Y 方向偏移                                                                                                                    | number                                                       | -            |\n| labelBackgroundStroke         | 组合标签背景描边颜色                                                                                                                           | string                                                       | -            |\n| labelBackgroundStrokeOpacity  | 组合标签背景描边透明度                                                                                                                         | number \\| string                                             | 1            |\n| labelBackgroundVisibility     | 组合标签背景是否可见                                                                                                                           | `visible` \\| `hidden`                                        | -            |\n| labelBackgroundZIndex         | 组合标签背景渲染层级                                                                                                                           | number                                                       | 1            |\n| `labelBackground{StyleProps}` | 更多标签背景样式配置，参考 [RectStyleProps](https://g.antv.antgroup.com/api/basic/rect) 属性值。例如 labelBackgroundOpacity 代表标签背景透明度 | [RectStyleProps](https://g.antv.antgroup.com/api/basic/rect) | -            |\n\n### 光晕样式\n\n光晕效果用于突出显示组合，通常在鼠标悬停、选中或激活状态下使用，为组合周围添加发光效果。\n\n#### 基础光晕效果\n\n为组合添加简单的光晕效果：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      halo: true, // 启用光晕\n      haloStroke: '#1783FF', // 蓝色光晕\n      haloLineWidth: 8, // 光晕宽度\n      haloStrokeOpacity: 0.3, // 光晕透明度\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 彩色光晕效果\n\n创建彩色渐变的光晕效果：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      halo: true,\n      haloStroke: '#FF4D4F', // 红色光晕\n      haloLineWidth: 12, // 较粗的光晕\n      haloStrokeOpacity: 0.5,\n      haloFilter: 'blur(2px)', // 模糊滤镜效果\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 动态光晕效果\n\n在状态切换时使用光晕效果：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      // 默认状态下不显示光晕\n      halo: false,\n    },\n    state: {\n      // 悬停状态显示橙色光晕\n      hover: {\n        halo: true,\n        haloStroke: '#FF7A00',\n        haloLineWidth: 10,\n        haloStrokeOpacity: 0.4,\n      },\n      // 选中状态显示绿色光晕\n      selected: {\n        halo: true,\n        haloStroke: '#52C41A',\n        haloLineWidth: 6,\n        haloStrokeOpacity: 0.6,\n      },\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的光晕样式配置：\n\n| 属性               | 描述                                                                                                                                           | 类型                                                                  | 默认值                       | 必选 |\n| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------- | ---- |\n| halo               | 是否显示组合光晕                                                                                                                               | boolean                                                               | false                        |      |\n| haloCursor         | 组合光晕鼠标移入样式，[配置项](#cursor)                                                                                                        | string                                                                | `default`                    |      |\n| haloDraggable      | 组合光晕是否允许拖拽                                                                                                                           | boolean                                                               | true                         |      |\n| haloDroppable      | 组合光晕是否允许接收被拖拽的元素                                                                                                               | boolean                                                               | false                        |      |\n| haloFill           | 光晕填充色                                                                                                                                     | string                                                                | 与主图形的填充色 `fill` 一致 |      |\n| haloFillRule       | 组合光晕填充规则                                                                                                                               | `nonzero` \\| `evenodd`                                                | -                            |      |\n| haloFilter         | 组合光晕滤镜效果，如 'blur(2px)' 可创建模糊效果                                                                                                | string                                                                | -                            |      |\n| haloLineWidth      | 组合光晕描边宽度，控制光晕的粗细程度                                                                                                           | number                                                                | 12                           |      |\n| haloPointerEvents  | 组合光晕效果是否响应指针事件，[配置项](#pointerevents)                                                                                         | string                                                                | `none`                       |      |\n| haloStroke         | 组合光晕描边色，**此属性用于设置组合周围光晕的颜色，帮助突出显示组合**                                                                         | string                                                                | `#99add1`                    |      |\n| haloStrokeOpacity  | 组合光晕描边色透明度，建议使用 0.2-0.6 的值以获得自然的光晕效果                                                                                | number                                                                | 0.25                         |      |\n| haloVisibility     | 组合光晕可见性                                                                                                                                 | `visible` \\| `hidden`                                                 | `visible`                    |      |\n| haloZIndex         | 组合光晕渲染层级，通常设置为负值以确保光晕在组合主图形下方                                                                                     | number                                                                | -1                           |      |\n| `halo{StyleProps}` | 更多光晕样式配置，参考 [DisplayObject](https://g.antv.antgroup.com/api/basic/display-object) 配置项。例如 haloFillOpacity 代表光晕填充色透明度 | [DisplayObject](https://g.antv.antgroup.com/api/basic/display-object) | -                            |      |\n\n**光晕使用建议：**\n\n1. **性能考虑**：光晕效果会增加渲染负担，建议在必要时才启用\n2. **颜色搭配**：光晕颜色应与组合主色调协调，避免过于突兀\n3. **透明度设置**：合理的透明度（0.2-0.6）可以创造自然的光晕效果\n4. **状态应用**：光晕通常用于 hover、selected、active 等交互状态\n\n### 图标样式\n\n图标用于在组合中显示文字或图片内容，通常位于组合的中心位置，可以用来表示组合的类型或功能。\n\n#### 文字图标\n\n使用文字作为组合的图标：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      iconText: 'A', // 显示字母 A\n      iconFill: '#1783FF', // 蓝色文字\n      iconFontSize: 24, // 大字体\n      iconFontWeight: 'bold', // 粗体\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 图片图标\n\n使用图片作为组合的图标：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      fill: '#1890FF',\n      iconSrc: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',\n      iconWidth: 32,\n      iconHeight: 32,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 彩色文字图标\n\n创建带有特殊样式的文字图标：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    combos: [{ id: 'combo1' }],\n  },\n  combo: {\n    style: {\n      iconText: '部门',\n      iconFill: '#FF4D4F', // 红色文字\n      iconFontSize: 16,\n      iconFontWeight: 'bold',\n      iconFontStyle: 'italic', // 斜体\n      iconTextDecorationLine: 'underline', // 下划线\n      iconLetterSpacing: 1, // 字间距\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的图标样式配置：\n\n| 属性                    | 描述                                                                         | 类型                                                                        | 默认值           |\n| ----------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ---------------- |\n| icon                    | 是否显示组合图标                                                             | boolean                                                                     | true             |\n| iconCursor              | 组合图标鼠标移入样式，[配置项](#cursor)                                      | string                                                                      | `default`        |\n| iconFill                | 组合图标文字颜色                                                             | string                                                                      | -                |\n| iconFillOpacity         | 组合图标文字颜色透明度                                                       | number                                                                      | 1                |\n| iconFontFamily          | 组合图标字体族                                                               | string                                                                      | -                |\n| iconFontSize            | 组合图标字体大小                                                             | number                                                                      | 16               |\n| iconFontStyle           | 组合图标字体样式                                                             | `normal` \\| `italic` \\| `oblique`                                           | `normal`         |\n| iconFontVariant         | 组合图标字体变种                                                             | `normal` \\| `small-caps` \\| string                                          | `normal`         |\n| iconFontWeight          | 组合图标字体粗细                                                             | number \\| string                                                            | `normal`         |\n| iconHeight              | 组合图标高度，当使用图片图标时用于控制图片尺寸                               | number                                                                      | 主图形高度的一半 |\n| iconLetterSpacing       | 组合图标文本字间距                                                           | number \\| string                                                            | -                |\n| iconLineHeight          | 组合图标文本行高                                                             | number \\| string                                                            | -                |\n| iconMaxLines            | 组合图标文本最大行数                                                         | number                                                                      | 1                |\n| iconOffsetX             | 组合图标在 x 轴方向上的偏移量                                                | number                                                                      | 0                |\n| iconOffsetY             | 组合图标在 y 轴方向上的偏移量                                                | number                                                                      | 0                |\n| iconOpacity             | 组合图标透明度                                                               | number                                                                      | 1                |\n| iconRadius              | 组合图标圆角半径（仅对矩形图标有效）                                         | number                                                                      | 0                |\n| iconSrc                 | 组合图片来源。其优先级高于 iconText，支持本地图片和网络图片                  | string                                                                      | -                |\n| iconText                | 组合图标文字内容，支持文字、Unicode 字符等                                   | string                                                                      | -                |\n| iconTextAlign           | 组合图标文本水平对齐方式                                                     | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`               | `center`         |\n| iconTextBaseline        | 组合图标文本基线                                                             | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom` | `middle`         |\n| iconTextDecorationColor | 组合图标文本装饰线颜色                                                       | string                                                                      | -                |\n| iconTextDecorationLine  | 组合图标文本装饰线，如下划线、删除线等                                       | string                                                                      | -                |\n| iconTextDecorationStyle | 组合图标文本装饰线样式                                                       | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                       | `solid`          |\n| iconTextOverflow        | 组合图标文本溢出处理方式                                                     | `clip` \\| `ellipsis` \\| string                                              | `clip`           |\n| iconVisibility          | 组合图标是否可见                                                             | `visible` \\| `hidden`                                                       | `visible`        |\n| iconWidth               | 组合图标宽度，当使用图片图标时用于控制图片尺寸                               | number                                                                      | 主图形宽度的一半 |\n| iconWordWrap            | 组合图标文本是否自动换行                                                     | boolean                                                                     | false            |\n| iconZIndex              | 组合图标渲染层级                                                             | number                                                                      | 1                |\n| `icon{StyleProps}`      | 更多图标样式配置，参考图标的具体类型配置项。例如 iconStroke 代表图标描边颜色 | -                                                                           | -                |\n\n**图标使用建议：**\n\n1. **优先级**：`iconSrc`（图片）的优先级高于 `iconText`（文字），如果同时设置，会优先显示图片\n2. **尺寸控制**：建议根据组合大小合理设置图标尺寸，避免图标过大或过小影响视觉效果\n3. **性能优化**：使用文字图标性能更好，图片图标需要额外的网络请求和渲染开销\n4. **样式一致性**：在同一个图中的组合图标样式应保持一致，提升整体视觉效果\n5. **可访问性**：确保图标颜色与背景有足够的对比度，便于用户识别\n\n## State\n\n在一些交互行为中，比如点击选中一个组合或鼠标悬停激活一个边，仅仅是在该元素做了某些状态的标识。为了将这些状态反应到终端用户所见的视觉空间中，我们需要为不同的状态设置不同的图元素样式，以响应该图元素状态的变化。\n\nG6 提供了几种内置的状态，包括选中（selected）、高亮（highlight）、激活（active）、不活跃（inactive）和禁用（disabled）。此外，它还支持自定义状态，以满足更特定的需求。对于每个状态，开发者可以定义一套样式规则，这些规则会覆盖元素的默认样式。\n\n<img width=\"520\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Iv_dS5XR2TcAAAAAAAAAAAAADmJ7AQ/original\" />\n\n数据结构如下：\n\n```typescript\ntype ComboState = {\n  [state: string]: ComboStyle;\n};\n```\n\n例如，当组合处于 `focus` 状态时，可以为其添加一个宽度为 3 且颜色为橙色的描边。\n\n```js {4-7}\nconst graph = new Graph({\n  combo: {\n    state: {\n      focus: {\n        lineWidth: 3, // 描边宽度\n        stroke: 'orange', // 描边颜色\n      },\n    },\n  },\n});\n```\n\n效果如下图所示：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', combo: 'combo1' }],\n    combos: [{ id: 'combo1', states: ['focus'] }],\n  },\n  combo: {\n    state: {\n      focus: {\n        lineWidth: 3,\n        stroke: 'orange',\n        fill: 'orange',\n        fillOpacity: 0.2,\n      },\n    },\n  },\n});\n\ngraph.render();\n```\n\n## Animation\n\n定义组合的动画效果，支持下列两种配置方式：\n\n1. 关闭组合全部动画\n\n```json\n{\n  \"combo\": {\n    \"animation\": false\n  }\n}\n```\n\n2. 配置阶段动画\n\n阶段动画是指组合在进入画布、更新、离开画布时的动画效果。目前支持的阶段包括：\n\n- `enter`: 组合进入画布时的动画\n- `update`: 组合更新时的动画\n- `exit`: 组合离开画布时的动画\n- `show`: 组合从隐藏状态显示时的动画\n- `hide`: 组合隐藏时的动画\n- `collapse`: 组合收起时的动画\n- `expand`: 组合展开时的动画\n\n你可以参考 [动画范式](/manual/animation/animation#动画范式) 使用动画语法来配置组合，如：\n\n```json\n{\n  \"combo\": {\n    \"animation\": {\n      \"update\": [\n        {\n          \"fields\": [\"x\", \"y\"], // 更新时只对 x 和 y 属性进行动画\n          \"duration\": 1000, // 动画持续时间\n          \"easing\": \"linear\" // 缓动函数\n        }\n      ],\n  }\n}\n```\n\n也可以使用内置的动画效果：\n\n```json\n{\n  \"combo\": {\n    \"animation\": {\n      \"enter\": \"fade\", // 使用渐变动画\n      \"update\": \"translate\", // 使用平移动画\n      \"exit\": \"fade\" // 使用渐变动画\n    }\n  }\n}\n```\n\n你可以传入 false 来关闭特定阶段的动画：\n\n```json\n{\n  \"combo\": {\n    \"animation\": {\n      \"enter\": false // 关闭组合入场动画\n    }\n  }\n}\n```\n\n## Palette\n\n定义组合的色板，即预定义组合颜色池，并根据规则进行分配，将颜色映射到 `fill` 属性。\n\n> 有关色板的定义，请参考 [色板](/manual/theme/palette)。\n\n| 属性   | 描述                                                                | 类型                              | 默认值  |\n| ------ | ------------------------------------------------------------------- | --------------------------------- | ------- |\n| type   | 指定当前色板类型。<br> - `group`: 离散色板 <br> - `value`: 连续色板 | `group` &#124; `value`            | `group` |\n| field  | 指定元素数据中的分组字段。若不指定，默认取 id 作为分组字段          | string &#124; ((datum) => string) | `id`    |\n| color  | 色板颜色。如果色板注册过，可以直接指定其注册名，也接受一个颜色数组  | string &#124; string[]            | -       |\n| invert | 是否反转色板                                                        | boolean                           | false   |\n\n如将一组数据按 `category` 字段分配组合颜色，使得同类别的组合颜色相同：\n\n```json\n{\n  \"combo\": {\n    \"palette\": {\n      \"type\": \"group\",\n      \"field\": \"category\",\n      \"color\": [\"#1783FF\", \"#F08F56\", \"#D580FF\", \"#00C9C9\", \"#7863FF\"]\n    }\n  }\n}\n```\n\n效果如下图所示：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 100,\n  data: {\n    combos: new Array(8)\n      .fill(0)\n      .map((_, i) => ({ id: `combo-${i}`, data: { category: ['A', 'B', 'C', 'D', 'E'][i % 5] } })),\n  },\n  layout: { type: 'grid', cols: 8 },\n  combo: {\n    style: { fillOpacity: 0.4 },\n    palette: {\n      type: 'group',\n      field: 'category',\n      color: ['#1783FF', '#F08F56', '#D580FF', '#00C9C9', '#7863FF'],\n    },\n  },\n});\n\ngraph.render();\n```\n\n也可以使用默认配置：\n\n```json\n{\n  \"combo\": {\n    \"palette\": \"tableau\" // tableau 为色板名，默认根据 ID 分配颜色\n  }\n}\n```\n\n效果如下图所示：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 100,\n  data: {\n    combos: new Array(8)\n      .fill(0)\n      .map((_, i) => ({ id: `combo-${i}`, data: { category: ['A', 'B', 'C', 'D', 'E'][i % 5] } })),\n  },\n  layout: { type: 'grid', cols: 8 },\n  combo: {\n    style: { fillOpacity: 0.4 },\n    palette: 'tableau',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/combo/CircleCombo.en.md",
    "content": "---\ntitle: Circle Combo\norder: 2\n---\n\n## Overview\n\nThe circular combo wraps child nodes or child combos with a circular boundary, suitable for representing equal or non-hierarchical group relationships.\n\nApplicable scenarios:\n\n- Suitable for representing node groups without a clear hierarchical relationship. The circular combo can reflect the equality of members, such as user groups in social networks or decentralized team structures (highlighting collaboration).\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/combos/circle-combo.md\"></embed>\n\n## Style Configuration\n\n> If the element has its specific attributes, we will list them below. For all general style attributes, see [BaseCombo](/en/manual/element/combo/base-combo)\n\n## Example\n\nThe following example shows the distribution of interest group members:\n\n<embed src=\"@/common/api/elements/combos/circle-combo-interest.md\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/element/combo/CircleCombo.zh.md",
    "content": "---\ntitle: 圆形组合 Circle\norder: 2\n---\n\n## 概述\n\n圆形组合以圆形边界包裹子节点或子组合，适合表示平等或非层级化的群组关系。\n\n适用场景：\n\n- 适合表示无明确层级关系的节点群组，圆形组合能体现成员的平等性，如社交网络中的用户群体、分散式团队结构（突出协作性）。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/combos/circle-combo.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseCombo](/manual/element/combo/base-combo)\n\n## 示例\n\n以下示例为兴趣小组人员分布：\n\n<embed src=\"@/common/api/elements/combos/circle-combo-interest.md\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/element/combo/RectCombo.en.md",
    "content": "---\ntitle: Rect Combo\norder: 3\n---\n\n## Overview\n\nThe rectangular combo organizes content with right-angle boundaries, supporting strict hierarchical structures.\n\nApplicable scenarios:\n\n- **System Architecture Diagrams**: Such as service layering within system architecture, and subdivisions within each layer.\n- **Geographical Area Division**: Such as cities containing multiple areas, where the rectangular combo can intuitively display administrative boundaries or functional divisions.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/combos/rect-combo.md\"></embed>\n\n## Style Configuration\n\n> If the element has its specific attributes, we will list them below. For all general style attributes, see [BaseCombo](/en/manual/element/combo/base-combo)\n\n## Example\n\nThe following example is a simple microservice architecture service layer:\n\n<embed src=\"@/common/api/elements/combos/rect-combo-architecture.md\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/element/combo/RectCombo.zh.md",
    "content": "---\ntitle: 矩形组合 Rect\norder: 3\n---\n\n## 概述\n\n矩形组合以直角边界组织内容，支持严格的层级结构。\n\n适用场景：\n\n- **系统架构图**：如系统架构里面的服务分层，以及每层服务里面的细分等。\n- **地理区域划分**：如城市包含多个区域，矩形组合能直观展示行政边界或功能分区。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/combos/rect-combo.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseCombo](/manual/element/combo/base-combo)\n\n## 示例\n\n以下示例为简单的微服务架构服务层：\n\n<embed src=\"@/common/api/elements/combos/rect-combo-architecture.md\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/element/combo/custom-combo.en.md",
    "content": "---\ntitle: Custom Combo\norder: 4\n---\n\nG6 provides two types of [built-in combos](/en/manual/element/combo/base-combo): circular combos and rectangular combos. However, in complex business scenarios, you may need to create custom combos with specific styles, interactive effects, or behavior logic.\n\n## Before You Start: Understanding the Basic Composition of Combos\n\nIn G6, a complete combo typically consists of the following parts:\n\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*z-OxR4MAdUwAAAAAAAAAAAAADmJ7AQ/original\" />\n\n- `key`: The main graphic of the combo, representing the main shape of the combo, such as a circle, rectangle, etc.\n- `label`: Text label, usually used to display the name or description of the combo.\n- `halo`: A graphic that displays a halo effect around the main graphic.\n\n### Special Characteristics of Combos\n\nCombos differ from ordinary nodes and have the following characteristics:\n\n1. **Containment**: Combos can contain nodes and other combos, forming a hierarchical structure.\n2. **Two States**: Expanded and Collapsed states.\n3. **Adaptive Size**: Automatically adjusts size based on internal elements.\n4. **Drag Behavior**: Supports overall dragging and dragging elements in/out.\n\n## Ways to Customize Combos <Badge type=\"warning\">Choose the Right Way</Badge>\n\nThere are two ways to create custom combos:\n\n### 1. Inherit Existing Combo Types <Badge type=\"success\">Recommended</Badge>\n\nThis is the most common way, and you can choose to inherit one of the following types:\n\n- [`BaseCombo`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/combos/base-combo.ts) - The most basic combo class, providing core functionality for combos.\n- [`Circle`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/combos/circle.ts) - Circular combo.\n- [`Rect`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/combos/rect.ts) - Rectangular combo.\n\n**Why choose this way?**\n\n- 📌 **Less Code**: Reuse existing combo properties and methods, focusing only on new features.\n- 📌 **Fast Development**: Suitable for most project needs, quickly achieving business goals.\n- 📌 **Easy Maintenance**: Clear code structure and clear inheritance relationships.\n\n:::tip{title=Get Started Now}\nIf you choose to inherit from existing combo types (recommended), you can jump directly to [Create Your First Custom Combo in Three Steps](#create-your-first-custom-combo-in-three-steps) to start practicing. Most users will choose this approach!\n:::\n\n### 2. Develop from Scratch Based on the G Graphics System <Badge>Advanced Usage</Badge>\n\nIf existing combo types do not meet your needs, you can create combos from scratch based on the underlying graphics system of G.\n\n**Why choose this way?**\n\n- 📌 **Maximum Freedom**: Full control over every detail of the combo, achieving any complex effect.\n- 📌 **Special Needs**: Highly customized scenarios that existing combo types cannot meet.\n- 📌 **Performance Optimization**: Performance optimization for specific scenarios.\n\n:::warning{title=Note}\nDeveloping custom combos from scratch requires handling all details yourself, including graphic drawing, event response, state changes, expand/collapse logic, etc., which is quite challenging. You can directly refer to the [source code](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/combos/base-combo.ts) for implementation.\n:::\n\n## Create Your First Custom Combo in Three Steps\n\nLet's start by inheriting `BaseCombo` to implement a custom hexagon combo:\n\n```js | ob { pin:false, inject: true }\nimport { Graph, register, BaseCombo, ExtensionCategory } from '@antv/g6';\n\n// Define the path for the collapsed state button\nconst collapse = (x, y, r) => {\n  return [\n    ['M', x - r, y],\n    ['a', r, r, 0, 1, 0, r * 2, 0],\n    ['a', r, r, 0, 1, 0, -r * 2, 0],\n    ['M', x - r + 4, y],\n    ['L', x + r - 4, y],\n  ];\n};\n\n// Define the path for the expanded state button\nconst expand = (x, y, r) => {\n  return [\n    ['M', x - r, y],\n    ['a', r, r, 0, 1, 0, r * 2, 0],\n    ['a', r, r, 0, 1, 0, -r * 2, 0],\n    ['M', x - r + 4, y],\n    ['L', x - r + 2 * r - 4, y],\n    ['M', x - r + r, y - r + 4],\n    ['L', x, y + r - 4],\n  ];\n};\n\nclass HexagonCombo extends BaseCombo {\n  // Get the path of the hexagon\n  getKeyPath(attributes) {\n    const [width, height] = this.getKeySize(attributes);\n    const padding = 10;\n    const size = Math.min(width, height) + padding;\n\n    // Calculate the vertices of the hexagon\n    const points = [];\n    for (let i = 0; i < 6; i++) {\n      const angle = (Math.PI / 3) * i;\n      const x = (size / 2) * Math.cos(angle);\n      const y = (size / 2) * Math.sin(angle);\n      points.push([x, y]);\n    }\n\n    // Construct the SVG path\n    const path = [['M', points[0][0], points[0][1]]];\n    for (let i = 1; i < 6; i++) {\n      path.push(['L', points[i][0], points[i][1]]);\n    }\n    path.push(['Z']);\n\n    return path;\n  }\n\n  // Get the style of the main graphic\n  getKeyStyle(attributes) {\n    const style = super.getKeyStyle(attributes);\n\n    return {\n      ...style,\n      d: this.getKeyPath(attributes),\n      fill: attributes.collapsed ? '#FF9900' : '#F04864',\n      fillOpacity: attributes.collapsed ? 0.5 : 0.2,\n      stroke: '#54BECC',\n      lineWidth: 2,\n    };\n  }\n\n  // Draw the main graphic\n  drawKeyShape(attributes, container) {\n    return this.upsert('key', 'path', this.getKeyStyle(attributes), container);\n  }\n\n  // Draw the expand/collapse button, using paths for finer control\n  drawCollapseButton(attributes) {\n    const { collapsed } = attributes;\n    const [width] = this.getKeySize(attributes);\n    const btnR = 8;\n    const x = width / 2 + btnR;\n    const d = collapsed ? expand(x, 0, btnR) : collapse(x, 0, btnR);\n\n    // Create the clickable area and button graphic\n    const hitArea = this.upsert('hit-area', 'circle', { cx: x, r: 8, fill: '#fff', cursor: 'pointer' }, this);\n    this.upsert('button', 'path', { stroke: '#54BECC', d, cursor: 'pointer', lineWidth: 1.4 }, hitArea);\n  }\n\n  // Override the render method to add more custom graphics\n  render(attributes, container) {\n    super.render(attributes, container);\n    this.drawCollapseButton(attributes, container);\n  }\n\n  // Use lifecycle hooks to add event listeners\n  onCreate() {\n    this.shapeMap['hit-area'].addEventListener('click', () => {\n      const id = this.id;\n      const collapsed = !this.attributes.collapsed;\n      const { graph } = this.context;\n      if (collapsed) graph.collapseElement(id);\n      else graph.expandElement(id);\n    });\n  }\n}\n\n// Register the custom combo\nregister(ExtensionCategory.COMBO, 'hexagon-combo', HexagonCombo);\n\n// Create a graph instance and use the custom combo\nconst graph = new Graph({\n  container: 'container',\n  height: 250,\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo1', style: { x: 100, y: 100 } },\n      { id: 'node2', combo: 'combo1', style: { x: 150, y: 150 } },\n      { id: 'node3', combo: 'combo2', style: { x: 300, y: 100 } },\n      { id: 'node4', combo: 'combo2', style: { x: 350, y: 150 } },\n    ],\n    combos: [\n      { id: 'combo1', data: { label: 'Hexagon 1' } },\n      { id: 'combo2', data: { label: 'Hexagon 2' }, style: { collapsed: true } },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#91d5ff',\n      stroke: '#1890ff',\n      lineWidth: 1,\n    },\n  },\n  combo: {\n    type: 'hexagon-combo',\n    style: {\n      padding: 20,\n      showCollapseButton: true,\n      labelText: (d) => d.data?.label,\n      labelPlacement: 'top',\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n```\n\n### Step 1: Write the Custom Combo Class\n\n```typescript\nimport { BaseCombo } from '@antv/g6';\nimport type { BaseComboStyleProps } from '@antv/g6';\n\n// Define button path generation functions\nconst collapse = (x, y, r) => {\n  return [\n    ['M', x - r, y],\n    ['a', r, r, 0, 1, 0, r * 2, 0],\n    ['a', r, r, 0, 1, 0, -r * 2, 0],\n    ['M', x - r + 4, y],\n    ['L', x + r - 4, y],\n  ];\n};\n\nconst expand = (x, y, r) => {\n  return [\n    ['M', x - r, y],\n    ['a', r, r, 0, 1, 0, r * 2, 0],\n    ['a', r, r, 0, 1, 0, -r * 2, 0],\n    ['M', x - r + 4, y],\n    ['L', x - r + 2 * r - 4, y],\n    ['M', x - r + r, y - r + 4],\n    ['L', x, y + r - 4],\n  ];\n};\n\nclass HexagonCombo extends BaseCombo {\n  // Get the path of the hexagon\n  protected getKeyPath(attributes: Required<BaseComboStyleProps>) {\n    const [width, height] = this.getKeySize(attributes);\n    const padding = 10;\n    const size = Math.min(width, height) + padding;\n\n    // Calculate the vertices of the hexagon\n    const points = [];\n    for (let i = 0; i < 6; i++) {\n      const angle = (Math.PI / 3) * i;\n      const x = (size / 2) * Math.cos(angle);\n      const y = (size / 2) * Math.sin(angle);\n      points.push([x, y]);\n    }\n\n    // Construct the SVG path\n    const path = [['M', points[0][0], points[0][1]]];\n    for (let i = 1; i < 6; i++) {\n      path.push(['L', points[i][0], points[i][1]]);\n    }\n    path.push(['Z']);\n\n    return path;\n  }\n\n  // Get the style of the main graphic, directly using path data\n  protected getKeyStyle(attributes: Required<BaseComboStyleProps>) {\n    const style = super.getKeyStyle(attributes);\n\n    return {\n      ...style,\n      d: this.getKeyPath(attributes),\n      fill: attributes.collapsed ? '#FF9900' : '#F04864',\n      fillOpacity: attributes.collapsed ? 0.5 : 0.2,\n      stroke: '#54BECC',\n      lineWidth: 2,\n    };\n  }\n\n  // Draw the main graphic, using path type to directly pass in style objects\n  protected drawKeyShape(attributes: Required<BaseComboStyleProps>, container: Group) {\n    return this.upsert('key', 'path', this.getKeyStyle(attributes), container);\n  }\n\n  // Draw the collapse/expand button, using SVG paths for finer control\n  protected drawCollapseButton(attributes: Required<BaseComboStyleProps>) {\n    const { collapsed } = attributes;\n    const [width] = this.getKeySize(attributes);\n    const btnR = 8;\n    const x = width / 2 + btnR;\n    const d = collapsed ? expand(x, 0, btnR) : collapse(x, 0, btnR);\n\n    // Create the clickable area and button graphic\n    const hitArea = this.upsert('hit-area', 'circle', { cx: x, r: 8, fill: '#fff', cursor: 'pointer' }, this);\n    this.upsert('button', 'path', { stroke: '#54BECC', d, cursor: 'pointer', lineWidth: 1.4 }, hitArea);\n  }\n\n  // Use lifecycle hook methods to bind events\n  onCreate() {\n    this.shapeMap['hit-area'].addEventListener('click', () => {\n      const id = this.id;\n      const collapsed = !this.attributes.collapsed;\n      const { graph } = this.context;\n      if (collapsed) graph.collapseElement(id);\n      else graph.expandElement(id);\n    });\n  }\n}\n```\n\n### Step 2: Register the Custom Combo\n\n```js\nimport { ExtensionCategory } from '@antv/g6';\n\nregister(ExtensionCategory.COMBO, 'hexagon-combo', HexagonCombo);\n```\n\n### Step 3: Apply the Custom Combo\n\n```js\nconst graph = new Graph({\n  // ...other configurations\n  combo: {\n    type: 'hexagon-combo', // Use the name registered\n    style: {\n      padding: 20,\n      showCollapseButton: true,\n      labelText: (d) => d.data?.label,\n      labelPlacement: 'top',\n    },\n  },\n  // Since we implemented the collapse/expand feature ourselves, only drag behavior is needed here\n  behaviors: ['drag-element'],\n});\n```\n\n🎉 Congratulations! You have created your first custom combo.\n\n## Going Further: Understanding the Principles of Combo Drawing\n\n### Differences Between Combos and Nodes\n\nAlthough Combos inherit from `BaseNode`, there are some key differences:\n\n1. **Adaptive Size**: Combos automatically calculate the appropriate size based on internal elements.\n2. **Expand/Collapse States**: Combos have two display states and need to handle state transitions.\n3. **Hierarchical Structure**: Combos can be nested, forming hierarchical relationships.\n4. **Internal Element Management**: Combos need to manage the nodes and sub-combos they contain.\n\n### Atomic Graphics\n\nG6's Combos are drawn using atomic graphic units provided by the [G Graphics System](https://g.antv.antgroup.com/). For an introduction to atomic graphics, please refer to the [Element - Shape (Optional)](/en/manual/element/shape/overview) documentation.\n\nAll these graphics can be dynamically created or updated using `upsert()` and automatically manage graphic states and lifecycles.\n\n### Element Base Class\n\nBefore customizing Combos, you need to understand some important properties and methods in the G6 element base class:\n\n#### Properties\n\n| Property   | Type                          | Description                                               |\n| ---------- | ----------------------------- | --------------------------------------------------------- |\n| shapeMap   | Record<string, DisplayObject> | Mapping table of all graphics under the current element   |\n| animateMap | Record<string, IAnimation>    | Mapping table of all animations under the current element |\n\n#### Methods\n\n#### `upsert(name, Ctor, style, container, hooks)`: Graphic Creation/Update\n\nWhen creating custom Combos, you will frequently use the `upsert` method. It is short for \"update or insert\" and is responsible for adding or updating graphics in the element:\n\n```typescript\nupsert(key: string, Ctor: { new (...args: any[]): DisplayObject }, style: Record<string, any>, container: DisplayObject);\n```\n\n| Parameter | Type                                    | Description                                                                                                                                                                                                                                                                                                                    |\n| --------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| key       | string                                  | Key of the graphic, corresponding to the key in `shapeMap`. Built-in keys include `'key'`, `'label'`, `'halo'`, `'icon'`, `'port'`, `'badge'`<br/> Keys should not use special symbols, and will be converted to camel case to call `getXxxStyle` and `drawXxxShape` methods (see [Element Conventions](#element-conventions)) |\n| Ctor      | { new (...args: any[]): DisplayObject } | Graphic class                                                                                                                                                                                                                                                                                                                  |\n| style     | Record<string, any>                     | Graphic style                                                                                                                                                                                                                                                                                                                  |\n| container | DisplayObject                           | Container to mount the graphic                                                                                                                                                                                                                                                                                                 |\n\nFor example, insert a fixed-position purple circle:\n\n```js\nthis.upsert(\n  'element-key', // Unique identifier of the element\n  'circle', // Graphic type, such as 'rect', 'circle', etc.\n  { x: 100, y: 100, fill: '#a975f3' }, // Style configuration object\n  container, // Parent container\n);\n```\n\nWhy use `upsert` instead of directly creating graphics with `container.appendChild()`? Because:\n\n1. **Better Performance**: When state changes or data updates, it intelligently reuses existing graphics instead of deleting and recreating them, greatly improving rendering performance.\n2. **Simpler Code**: No need to manually check if elements exist.\n3. **Easy Management**: All graphics created through `upsert` are recorded in the node's `shapeMap`, and you can easily access them with `this.getShape(key)`.\n\n#### `render(attributes, container)`: Main Entry for Rendering Combos\n\nEvery custom combo class must implement the `render(attributes, container)` method, which defines how the combo is \"drawn\". You can use various atomic graphics here to create the structure you want.\n\n```typescript\nrender(style: Record<string, any>, container: Group): void;\n```\n\n| Parameter | Type                | Description   |\n| --------- | ------------------- | ------------- |\n| style     | Record<string, any> | Element style |\n| container | Group               | Container     |\n\n#### `getShape(name)`: Get Created Graphics\n\nSometimes, you need to modify the properties of a sub-graphic after creation or have interactions between sub-graphics. In this case, the `getShape` method can help you access any graphics previously created with `upsert`:\n\n**⚠️ Note**: The order of graphics is important. If graphic B depends on the position of graphic A, make sure A is created first.\n\n### Element Conventions\n\n- **Use Convention Properties**\n\nThe convention properties in combos include:\n\n- Use `this.getKeySize(attributes)` to get the size of the combo, considering the collapsed state and sub-elements.\n- Use `this.getContentBBox(attributes)` to get the bounding box of the content area.\n- Use `this.getComboPosition(attributes)` to get the current position of the combo, based on state and sub-elements.\n\n- **Use `getXxxStyle` and `drawXxxShape` Pairing for Graphic Drawing**\n\n`getXxxStyle` is used to get the graphic style, and `drawXxxShape` is used to draw the graphic. Graphics created this way support automatic animation execution.\n\n> `Xxx` is the camel case form of the key passed to the [upsert](#methods) method.\n\n- **Access Graph Context via `this.context`**\n\n### Lifecycle Hooks\n\nThe following lifecycle hook functions are provided, and you can override these methods in custom combos to execute specific logic at key moments:\n\n| Hook Function | Trigger Timing                                                     | Typical Use Cases                                                                |\n| ------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------- |\n| `onCreate`    | After the combo is created and the entrance animation is completed | Bind interactive events, initialize combo state, add external listeners          |\n| `onUpdate`    | After the combo is updated and the update animation is completed   | Update dependent data, adjust related elements, trigger linkage effects          |\n| `onDestroy`   | After the combo is destroyed and the exit animation is completed   | Clean up resources, remove external listeners, execute destruction notifications |\n\n### State Response\n\nOne of the most powerful aspects of G6 element design is the ability to separate **\"state response\"** from **\"drawing logic\"**.\n\nYou can define styles for each state in the combo configuration:\n\n```js\ncombo: {\n  type: 'custom-combo',\n  style: {\n    fill: '#f0f2f5',\n    stroke: '#d9d9d9'\n  },\n  state: {\n    selected: {\n      stroke: '#1890ff',\n      lineWidth: 2,\n      shadowColor: 'rgba(24,144,255,0.2)',\n      shadowBlur: 15,\n    },\n    hover: {\n      fill: '#e6f7ff',\n    },\n  },\n}\n```\n\nMethod to switch states:\n\n```js\ngraph.setElementState(comboId, ['selected']);\n```\n\nThis state will be passed into the `render()` method's `attributes` and automatically applied to the graphics as a result of the internal system merging.\n\nYou can also customize rendering logic based on the state:\n\n```typescript\nprotected getKeyStyle(attributes: Required<BaseComboStyleProps>) {\n  const style = super.getKeyStyle(attributes);\n\n  // Adjust style based on state\n  if (attributes.states?.includes('selected')) {\n    return {\n      ...style,\n      stroke: '#1890ff',\n      lineWidth: 2,\n      shadowColor: 'rgba(24,144,255,0.2)',\n      shadowBlur: 15,\n    };\n  }\n\n  return style;\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/combo/custom-combo.zh.md",
    "content": "---\ntitle: 自定义 Combo\norder: 4\n---\n\nG6 提供了两种 [内置组合](/manual/element/combo/base-combo) 类型：圆形组合和矩形 Combo 。但在复杂的业务场景中，你可能需要创建具有特定样式、交互效果或行为逻辑的自定义 Combo 。\n\n## 开始之前：了解 Combo 的基本构成\n\n在 G6 中，一个完整的 Combo 通常由以下几个部分组成：\n\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*z-OxR4MAdUwAAAAAAAAAAAAADmJ7AQ/original\" />\n\n- `key` ： Combo 的主图形，表示 Combo 的主要形状，如圆形、矩形等；\n- `label` ：文本标签，通常用于展示 Combo 的名称或描述；\n- `halo` ：主图形周围展示的光晕效果的图形；\n\n### Combo 的特殊性\n\nCombo 不同于普通节点，它具有以下特性：\n\n1. **包含性**： Combo 可以包含节点和其他 Combo ，形成层级结构\n2. **两种状态**：展开(Expanded)和收起(Collapsed)状态\n3. **自适应大小**：根据内部元素动态调整大小\n4. **拖拽行为**：支持整体拖拽及内部元素拖入/拖出\n\n## 自定义 Combo 的方式 <Badge type=\"warning\">选择合适的方式</Badge>\n\n创建自定义 Combo 的方式有两种途径：\n\n### 1. 继承现有 Combo 类型 <Badge type=\"success\">推荐</Badge>\n\n这是最常用的方式，你可以选择继承以下类型之一：\n\n- [`BaseCombo`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/combos/base-combo.ts) - 最基础的 Combo 类，提供 Combo 的核心功能\n- [`Circle`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/combos/circle.ts) - 圆形 Combo\n- [`Rect`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/combos/rect.ts) - 矩形 Combo\n\n**为什么选择这种方式？**\n\n- 📌 **代码量少**：复用现有 Combo 的属性和方法，只需专注于新增功能\n- 📌 **开发迅速**：适合大多数项目需求，快速实现业务目标\n- 📌 **易于维护**：代码结构清晰，继承关系明确\n\n:::tip{title=立即开始}\n如果你选择继承现有组合类型（推荐），可以直接跳到 [三步创建你的第一个自定义 Combo](#三步创建你的第一个自定义-combo) 开始实践。大部分用户都会选择这种方式！\n:::\n\n### 2. 基于 G 图形系统从零开发 <Badge>高级用法</Badge>\n\n如果现有 Combo 类型都不满足需求，你可以基于 G 的底层图形系统从零创建 Combo。\n\n**为什么选择这种方式？**\n\n- 📌 **最大自由度**：完全控制 Combo 的每个细节，实现任意复杂效果\n- 📌 **特殊需求**：现有 Combo 类型无法满足的高度定制场景\n- 📌 **性能优化**：针对特定场景的性能优化\n\n:::warning{title=注意事项}\n从零开发的自定义 Combo 需要自行处理所有细节，包括图形绘制、事件响应、状态变化、展开/收起逻辑等，开发难度较大。这里可以直接参考 [源码](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/combos/base-combo.ts) 进行实现。\n:::\n\n## 三步创建你的第一个自定义 Combo\n\n让我们从继承 `BaseCombo` 开始，实现一个自定义六边形 Combo ：\n\n```js | ob { pin:false, inject: true }\nimport { Graph, register, BaseCombo, ExtensionCategory } from '@antv/g6';\n\n// 定义收起状态的按钮路径\nconst collapse = (x, y, r) => {\n  return [\n    ['M', x - r, y],\n    ['a', r, r, 0, 1, 0, r * 2, 0],\n    ['a', r, r, 0, 1, 0, -r * 2, 0],\n    ['M', x - r + 4, y],\n    ['L', x + r - 4, y],\n  ];\n};\n\n// 定义展开状态的按钮路径\nconst expand = (x, y, r) => {\n  return [\n    ['M', x - r, y],\n    ['a', r, r, 0, 1, 0, r * 2, 0],\n    ['a', r, r, 0, 1, 0, -r * 2, 0],\n    ['M', x - r + 4, y],\n    ['L', x - r + 2 * r - 4, y],\n    ['M', x - r + r, y - r + 4],\n    ['L', x, y + r - 4],\n  ];\n};\n\nclass HexagonCombo extends BaseCombo {\n  // 获取六边形的路径\n  getKeyPath(attributes) {\n    const [width, height] = this.getKeySize(attributes);\n    const padding = 10;\n    const size = Math.min(width, height) + padding;\n\n    // 计算六边形的顶点\n    const points = [];\n    for (let i = 0; i < 6; i++) {\n      const angle = (Math.PI / 3) * i;\n      const x = (size / 2) * Math.cos(angle);\n      const y = (size / 2) * Math.sin(angle);\n      points.push([x, y]);\n    }\n\n    // 构建SVG路径\n    const path = [['M', points[0][0], points[0][1]]];\n    for (let i = 1; i < 6; i++) {\n      path.push(['L', points[i][0], points[i][1]]);\n    }\n    path.push(['Z']);\n\n    return path;\n  }\n\n  // 获取主图形样式\n  getKeyStyle(attributes) {\n    const style = super.getKeyStyle(attributes);\n\n    return {\n      ...style,\n      d: this.getKeyPath(attributes),\n      fill: attributes.collapsed ? '#FF9900' : '#F04864',\n      fillOpacity: attributes.collapsed ? 0.5 : 0.2,\n      stroke: '#54BECC',\n      lineWidth: 2,\n    };\n  }\n\n  // 绘制主图形\n  drawKeyShape(attributes, container) {\n    return this.upsert('key', 'path', this.getKeyStyle(attributes), container);\n  }\n\n  // 绘制展开/收起按钮，使用路径实现更精细的控制\n  drawCollapseButton(attributes) {\n    const { collapsed } = attributes;\n    const [width] = this.getKeySize(attributes);\n    const btnR = 8;\n    const x = width / 2 + btnR;\n    const d = collapsed ? expand(x, 0, btnR) : collapse(x, 0, btnR);\n\n    // 创建点击区域和按钮图形\n    const hitArea = this.upsert('hit-area', 'circle', { cx: x, r: 8, fill: '#fff', cursor: 'pointer' }, this);\n    this.upsert('button', 'path', { stroke: '#54BECC', d, cursor: 'pointer', lineWidth: 1.4 }, hitArea);\n  }\n\n  // 重写render方法，添加更多自定义图形\n  render(attributes, container) {\n    super.render(attributes, container);\n    this.drawCollapseButton(attributes, container);\n  }\n\n  // 使用生命周期钩子添加事件监听\n  onCreate() {\n    this.shapeMap['hit-area'].addEventListener('click', () => {\n      const id = this.id;\n      const collapsed = !this.attributes.collapsed;\n      const { graph } = this.context;\n      if (collapsed) graph.collapseElement(id);\n      else graph.expandElement(id);\n    });\n  }\n}\n\n// 注册自定义 Combo\nregister(ExtensionCategory.COMBO, 'hexagon-combo', HexagonCombo);\n\n// 创建图实例并使用自定义 Combo\nconst graph = new Graph({\n  container: 'container',\n  height: 250,\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo1', style: { x: 100, y: 100 } },\n      { id: 'node2', combo: 'combo1', style: { x: 150, y: 150 } },\n      { id: 'node3', combo: 'combo2', style: { x: 300, y: 100 } },\n      { id: 'node4', combo: 'combo2', style: { x: 350, y: 150 } },\n    ],\n    combos: [\n      { id: 'combo1', data: { label: 'Hexagon 1' } },\n      { id: 'combo2', data: { label: 'Hexagon 2' }, style: { collapsed: true } },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#91d5ff',\n      stroke: '#1890ff',\n      lineWidth: 1,\n    },\n  },\n  combo: {\n    type: 'hexagon-combo',\n    style: {\n      padding: 20,\n      showCollapseButton: true,\n      labelText: (d) => d.data?.label,\n      labelPlacement: 'top',\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n```\n\n### 第一步：编写自定义 Combo 类\n\n```typescript\nimport { BaseCombo } from '@antv/g6';\nimport type { BaseComboStyleProps } from '@antv/g6';\n\n// 定义按钮路径生成函数\nconst collapse = (x, y, r) => {\n  return [\n    ['M', x - r, y],\n    ['a', r, r, 0, 1, 0, r * 2, 0],\n    ['a', r, r, 0, 1, 0, -r * 2, 0],\n    ['M', x - r + 4, y],\n    ['L', x + r - 4, y],\n  ];\n};\n\nconst expand = (x, y, r) => {\n  return [\n    ['M', x - r, y],\n    ['a', r, r, 0, 1, 0, r * 2, 0],\n    ['a', r, r, 0, 1, 0, -r * 2, 0],\n    ['M', x - r + 4, y],\n    ['L', x - r + 2 * r - 4, y],\n    ['M', x - r + r, y - r + 4],\n    ['L', x, y + r - 4],\n  ];\n};\n\nclass HexagonCombo extends BaseCombo {\n  // 获取六边形的路径\n  protected getKeyPath(attributes: Required<BaseComboStyleProps>) {\n    const [width, height] = this.getKeySize(attributes);\n    const padding = 10;\n    const size = Math.min(width, height) + padding;\n\n    // 计算六边形的顶点\n    const points = [];\n    for (let i = 0; i < 6; i++) {\n      const angle = (Math.PI / 3) * i;\n      const x = (size / 2) * Math.cos(angle);\n      const y = (size / 2) * Math.sin(angle);\n      points.push([x, y]);\n    }\n\n    // 构建 SVG 路径\n    const path = [['M', points[0][0], points[0][1]]];\n    for (let i = 1; i < 6; i++) {\n      path.push(['L', points[i][0], points[i][1]]);\n    }\n    path.push(['Z']);\n\n    return path;\n  }\n\n  // 获取主图形样式，直接使用路径数据\n  protected getKeyStyle(attributes: Required<BaseComboStyleProps>) {\n    const style = super.getKeyStyle(attributes);\n\n    return {\n      ...style,\n      d: this.getKeyPath(attributes),\n      fill: attributes.collapsed ? '#FF9900' : '#F04864',\n      fillOpacity: attributes.collapsed ? 0.5 : 0.2,\n      stroke: '#54BECC',\n      lineWidth: 2,\n    };\n  }\n\n  // 绘制主图形，使用 path 类型直接传入样式对象\n  protected drawKeyShape(attributes: Required<BaseComboStyleProps>, container: Group) {\n    return this.upsert('key', 'path', this.getKeyStyle(attributes), container);\n  }\n\n  // 绘制收起/展开按钮，使用 SVG 路径实现更精细的控制\n  protected drawCollapseButton(attributes: Required<BaseComboStyleProps>) {\n    const { collapsed } = attributes;\n    const [width] = this.getKeySize(attributes);\n    const btnR = 8;\n    const x = width / 2 + btnR;\n    const d = collapsed ? expand(x, 0, btnR) : collapse(x, 0, btnR);\n\n    // 创建点击区域和按钮图形\n    const hitArea = this.upsert('hit-area', 'circle', { cx: x, r: 8, fill: '#fff', cursor: 'pointer' }, this);\n    this.upsert('button', 'path', { stroke: '#54BECC', d, cursor: 'pointer', lineWidth: 1.4 }, hitArea);\n  }\n\n  // 使用生命周期钩子方法绑定事件\n  onCreate() {\n    this.shapeMap['hit-area'].addEventListener('click', () => {\n      const id = this.id;\n      const collapsed = !this.attributes.collapsed;\n      const { graph } = this.context;\n      if (collapsed) graph.collapseElement(id);\n      else graph.expandElement(id);\n    });\n  }\n}\n```\n\n### 第二步：注册自定义 Combo\n\n```js\nimport { ExtensionCategory } from '@antv/g6';\n\nregister(ExtensionCategory.COMBO, 'hexagon-combo', HexagonCombo);\n```\n\n### 第三步：应用自定义 Combo\n\n```js\nconst graph = new Graph({\n  // ...其他配置\n  combo: {\n    type: 'hexagon-combo', // 使用注册时的名称\n    style: {\n      padding: 20,\n      showCollapseButton: true,\n      labelText: (d) => d.data?.label,\n      labelPlacement: 'top',\n    },\n  },\n  // 由于我们自己实现了折叠展开功能，这里只需要拖拽行为\n  behaviors: ['drag-element'],\n});\n```\n\n🎉 恭喜！你已经创建了第一个自定义 Combo 。\n\n## 更进一步：理解 Combo 绘制的原理\n\n### Combo 与节点的区别\n\n虽然 Combo 继承自 `BaseNode`，但有一些关键区别：\n\n1. **自适应大小**： Combo 会根据内部元素自动计算合适的大小\n2. **展开/收起状态**： Combo 有两种显示状态，并需要处理状态切换\n3. **层级结构**： Combo 可以嵌套，形成层级关系\n4. **内部元素管理**： Combo 需要管理其包含的节点和子 Combo\n\n### 原子图形\n\nG6 的 Combo 是由 [G 图形系统](https://g.antv.antgroup.com/) 提供的图形原子单元绘制而成。原子图形的介绍请参考 [元素 - 图形（可选）](/manual/element/shape/overview) 文档。\n\n所有这些图形都可通过 `upsert()` 动态创建或更新，并自动管理图形状态和生命周期。\n\n### 元素基类\n\n开始自定义 Combo 之前，你需要了解 G6 元素基类中的一些重要属性和方法：\n\n#### 属性\n\n| 属性       | 类型                          | 描述                       |\n| ---------- | ----------------------------- | -------------------------- |\n| shapeMap   | Record<string, DisplayObject> | 当前元素下所有图形的映射表 |\n| animateMap | Record<string, IAnimation>    | 当前元素下所有动画的映射表 |\n\n#### 方法\n\n#### `upsert(name, Ctor, style, container, hooks)`: 图形创建/更新\n\n在创建自定义 Combo 时，你会频繁用到 `upsert` 方法。它是 \"update or insert\" 的缩写，负责添加或更新元素中的图形：\n\n```typescript\nupsert(key: string, Ctor: { new (...args: any[]): DisplayObject }, style: Record<string, any>, container: DisplayObject);\n```\n\n| 参数      | 类型                                    | 描述                                                                                                                                                                                                                                    |\n| --------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| key       | string                                  | 图形的 key，即 `shapeMap` 中对应的 key。内置的 key 包括 `'key'` `'label'` `'halo'` `'icon'` `'port'` `'badge'`<br/> key 不应使用特殊符号，会基于该值转化为驼峰形式调用 `getXxxStyle` 和 `drawXxxShape` 方法（见 [元素约定](#元素约定)） |\n| Ctor      | { new (...args: any[]): DisplayObject } | 图形类                                                                                                                                                                                                                                  |\n| style     | Record<string, any>                     | 图形样式                                                                                                                                                                                                                                |\n| container | DisplayObject                           | 挂载图形的容器                                                                                                                                                                                                                          |\n\n例如，插入一个固定位置的紫色圆形：\n\n```js\nthis.upsert(\n  'element-key', // 元素的唯一标识\n  'circle', // 图形类型，如 'rect', 'circle' 等\n  { x: 100, y: 100, fill: '#a975f3' }, // 样式配置对象\n  container, // 父容器\n);\n```\n\n为什么要使用 `upsert` 而不直接通过 `container.appendChild()` 创建图形？因为：\n\n1. **性能更好**：当状态变化或数据更新时，会智能地复用已有图形，而不是删除再重建，大大提高了渲染性能\n2. **代码更简洁**：不需要手动判断元素是否存在\n3. **便于管理**：所有通过 `upsert` 创建的图形都会被记录在节点的 `shapeMap` 中，你可以通过 `this.getShape(key)` 轻松获取\n\n#### `render(attributes, container)`: 渲染组合的主入口\n\n每个自定义 Combo 类都必须实现 `render(attributes, container)` 方法，它定义了该组合如何被\"绘制\"出来。你可以在这里使用各种原子图形，组合出你想要的结构。\n\n```typescript\nrender(style: Record<string, any>, container: Group): void;\n```\n\n| 参数      | 类型                | 描述     |\n| --------- | ------------------- | -------- |\n| style     | Record<string, any> | 元素样式 |\n| container | Group               | 容器     |\n\n#### `getShape(name)`: 获取已创建的图形\n\n有时，你需要在创建后修改某个子图形的属性，或者让子图形之间有交互关联。这时，`getShape` 方法可以帮你获取之前通过 `upsert` 创建的任何图形：\n\n**⚠️ 注意**：图形的顺序很重要，如果图形 B 依赖图形 A 的位置，必须确保 A 先创建\n\n### 元素约定\n\n- **使用约定属性**\n\n组合中约定的元素属性包括：\n\n- 通过 `this.getKeySize(attributes)` 获取组合的尺寸，考虑折叠状态和子元素\n- 通过 `this.getContentBBox(attributes)` 获取内容区域的边界盒\n- 通过 `this.getComboPosition(attributes)` 获取组合的当前位置，基于状态和子元素\n\n- **采用 `getXxxStyle` 和 `drawXxxShape` 配对的方式进行图形绘制**\n\n`getXxxStyle` 用于获取图形样式，`drawXxxShape` 用于绘制图形。通过该方式创建的图形支持自动执行动画。\n\n> 其中 `Xxx` 是调用 [upsert](#方法) 方法时传入的 key 的驼峰形式。\n\n- **可通过 `this.context` 访问 Graph 上下文**\n\n### 生命周期钩子\n\n提供了以下生命周期钩子函数，你可以在自定义 Combo 中重写这些方法，在关键时刻执行特定逻辑：\n\n| 钩子函数    | 触发时机                   | 典型用途                                     |\n| ----------- | -------------------------- | -------------------------------------------- |\n| `onCreate`  | 当组合创建后完成入场动画时 | 绑定交互事件、初始化组合状态、添加外部监听器 |\n| `onUpdate`  | 当组合更新后完成更新动画时 | 更新依赖数据、调整相关元素、触发联动效果     |\n| `onDestroy` | 当组合完成退场动画并销毁后 | 清理资源、移除外部监听器、执行销毁通知       |\n\n### 状态响应\n\nG6 元素设计中最强大的一点，是可以将 **\"状态响应\"** 与 **\"绘制逻辑\"** 分离。\n\n你可以在组合配置中定义每种状态下的样式：\n\n```js\ncombo: {\n  type: 'custom-combo',\n  style: {\n    fill: '#f0f2f5',\n    stroke: '#d9d9d9'\n  },\n  state: {\n    selected: {\n      stroke: '#1890ff',\n      lineWidth: 2,\n      shadowColor: 'rgba(24,144,255,0.2)',\n      shadowBlur: 15,\n    },\n    hover: {\n      fill: '#e6f7ff',\n    },\n  },\n}\n```\n\n切换状态的方法:\n\n```js\ngraph.setElementState(comboId, ['selected']);\n```\n\n这个状态会传入到 `render()` 方法的 `attributes` 中，由内部系统合并后的结果自动应用在图形上。\n\n也可以根据状态自定义渲染逻辑：\n\n```typescript\nprotected getKeyStyle(attributes: Required<BaseComboStyleProps>) {\n  const style = super.getKeyStyle(attributes);\n\n  // 根据状态调整样式\n  if (attributes.states?.includes('selected')) {\n    return {\n      ...style,\n      stroke: '#1890ff',\n      lineWidth: 2,\n      shadowColor: 'rgba(24,144,255,0.2)',\n      shadowBlur: 15,\n    };\n  }\n\n  return style;\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/combo/overview.en.md",
    "content": "---\ntitle: Combo Overview\norder: 0\n---\n\n## What is a Combo\n\nA Combo, short for Combination, is a special type of graph element in G6 that can contain nodes and sub-combos, similar to the concept of \"groups\" or \"containers.\" It is typically used to represent set relationships, such as a department containing multiple employees or a city containing multiple regions.\n\n<image width=\"450\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*zPAzSZ3XxpUAAAAAAAAAAAAADmJ7AQ/original\" />\n\n:::warning{title=Note}\nIt is not recommended to use Combos in **tree graphs**. The layout mechanism of tree graphs is incompatible with that of Combos, which can lead to node misalignment or style confusion.\n:::\n\nG6 has built-in Combos including `circle` (circular combo) and `rect` (rectangular combo), as shown in the images below:\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Kbk1S5pzSY0AAAAAAAAAAAAADmJ7AQ/original\" width=\"200\" />\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*PKtgSZzmb3YAAAAAAAAAAAAADmJ7AQ/original\" width=\"200\" />\n\n## Data Structure\n\nWhen defining a Combo, you need to add a `combos` field to the graph's data object. Each Combo is an object with the following structure:\n\n| Attribute | Description                                                                                            | Type           | Default | Required |\n| --------- | ------------------------------------------------------------------------------------------------------ | -------------- | ------- | -------- |\n| id        | Unique identifier of the combo                                                                         | string         | -       | ✓        |\n| type      | Combo type, name of built-in combo type or custom combo name, such as `circle` or `rect`               | string         | -       |          |\n| data      | Combo data, used to store custom data of the combo, accessible via callback functions in style mapping | object         | -       |          |\n| style     | Combo style                                                                                            | object         | -       |          |\n| states    | Initial states of the combo                                                                            | string[]       | -       |          |\n| combo     | Parent combo ID. If there is no parent combo, it is null                                               | string \\| null | -       |          |\n\nAn example of a data item in the `combos` array:\n\n```json\n{\n  \"id\": \"combo1\",\n  \"type\": \"circle\",\n  \"data\": { \"groupName\": \"Group A\" },\n  \"style\": { \"fill\": \"lightblue\", \"stroke\": \"blue\", \"collapsed\": true },\n  \"states\": [],\n  \"combo\": null\n}\n```\n\nTo assign a node to a Combo, you can add a `combo` field to the node data:\n\n```json\n{\n  \"nodes\": [{ \"id\": \"node1\", \"combo\": \"comboA\" }], // node1 belongs to comboA\n  \"combos\": [{ \"id\": \"comboA\" }] // define comboA\n}\n```\n\n## Configuration Methods\n\nThere are three ways to configure Combos, listed in order of priority from highest to lowest:\n\n- Use `graph.setCombo()` for dynamic configuration\n- Global configuration during graph instantiation\n- Dynamic properties in data\n\nThese configuration methods can be used simultaneously. When there are identical configuration items, the method with higher priority will override the one with lower priority.\n\n### Using `graph.setCombo()`\n\nYou can dynamically set the style mapping logic of Combos using `graph.setCombo()` after the graph instance is created.\n\nThis method must be called before `graph.render()` to take effect and has the highest priority.\n\n```js\ngraph.setCombo({\n  style: {\n    type: 'circle',\n    style: { fill: '#7FFFD4', stroke: '#5CACEE', lineWidth: 2 },\n  },\n});\n\ngraph.render();\n```\n\n### Global Configuration During Graph Instantiation\n\nYou can configure Combo style mapping globally during graph instantiation. This configuration will apply to all Combos.\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  // Specify combo type and combo style type in the combo configuration\n  combo: {\n    type: 'circle',\n    style: { fill: '#7FFFD4', stroke: '#5CACEE', lineWidth: 2 },\n  },\n});\n```\n\n### Dynamic Configuration in Data\n\nIf you need different configurations for different Combos, you can write the configuration into the Combo data. This configuration method can be directly written into the data in the form of the following code:\n\n```typescript\n// Specify combo type and combo style type in the data\nconst data = {\n  combos: [\n    {\n      id: 'combo-1',\n      type: 'circle',\n      style: { size: 100, stroke: 'orange' },\n    },\n  ],\n};\n```\n\n### Adjusting Priority\n\nIf you want the configuration in the data to have a higher priority than the global configuration, you can take the following approach:\n\n```js\nconst data = {\n  combos: [\n    {\n      id: 'combo-1',\n      type: 'circle',\n      style: { size: 100, stroke: 'orange' },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  combo: {\n    type: (d) => d.type || 'rect',\n    style: {\n      stroke: (d) => d.style.stroke || 'blue',\n    },\n  },\n});\n```\n\n## Example\n\n```js | ob { inject: true }\nimport { Graph, register, Rect, ExtensionCategory } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n      combo: 'combo1',\n      style: { x: 245, y: 200 },\n    },\n    {\n      id: 'node2',\n      combo: 'combo1',\n      style: { x: 210, y: 250 },\n    },\n    {\n      id: 'node3',\n      combo: 'combo1',\n      style: { x: 280, y: 245 },\n    },\n    {\n      id: 'node4',\n      combo: 'combo2',\n      style: { x: 400, y: 165 },\n    },\n    {\n      id: 'node5',\n      combo: 'combo2',\n      style: { x: 450, y: 162 },\n    },\n    {\n      id: 'node6',\n      combo: 'combo3',\n      style: { x: 425, y: 300 },\n    },\n    {\n      id: 'node7',\n      combo: 'combo3',\n      style: { x: 360, y: 332 },\n    },\n  ],\n  edges: [],\n  combos: [\n    {\n      id: 'combo1',\n      combo: 'combo3',\n      data: { label: 'Combo A' },\n    },\n    {\n      id: 'combo2',\n      combo: 'combo3',\n      data: { label: 'Combo B' },\n    },\n    {\n      id: 'combo3',\n      data: { label: 'Combo C' },\n    },\n    {\n      id: 'combo4',\n      data: { label: 'Combo D' },\n      style: { x: 58, y: 248 },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  height: 450,\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n      labelPlacement: 'center',\n      labelFill: '#fff',\n      labelFontSize: 10,\n    },\n  },\n  combo: {\n    type: 'circle',\n    style: {\n      padding: 2,\n      labelText: (d) => d.data.label,\n      labelPlacement: 'top',\n    },\n  },\n  behaviors: [\n    'collapse-expand',\n    {\n      type: 'drag-element',\n      dropEffect: 'link',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n## Combo Interaction\n\nSimply rendering a Combo does not provide much practical value; it is only when a series of interactive operations are supported that the value of Combos can be maximized.\n\nIn G6, we have built-in interactions such as `drag-element` and `collapse-expand`.\n\n#### drag-element\n\nSupports dragging nodes and Combos. During the dragging of a Combo, the positions of nodes and edges within the Combo will dynamically change. After dragging is complete, the relative positions of the Combo and nodes remain unchanged. You can also change the affiliation of the Combo during dragging by setting `dropEffect: 'link'`.\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*rJiHQahxgj4AAAAAAAAAAAAAemJ7AQ/original\" width=\"400\" />\n\n#### collapse-expand\n\nSupports double-clicking a Combo to collapse and expand it. After collapsing a Combo, all nodes within the Combo are hidden. If there are connections between external nodes and nodes within the Combo, all connections will connect to the Combo.\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*PfnsSZSkRagAAAAAAAAAAAAAemJ7AQ/original\" width=\"400\" />\n\n## Custom Combos\n\nWhen built-in Combos cannot meet your needs, G6 provides powerful customization capabilities:\n\n- Extend built-in Combos\n- Create new Combo types\n\nUnlike Combos, custom Combos need to be registered before use. For detailed tutorials, please refer to the [Custom Combo](/en/manual/element/combo/custom-combo) documentation.\n"
  },
  {
    "path": "packages/site/docs/manual/element/combo/overview.zh.md",
    "content": "---\ntitle: 组合总览\norder: 0\n---\n\n## 什么是组合\n\n组合(Combo) 全称为 Combination，是 G6 中的一种特殊的图元素，它可以包含节点和子组合，类似“群组”或“容器”的概念。它通常用于表示集合关系，例如一个部门包含多个员工，一个城市包含多个区域等。\n\n<image width=\"450\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*zPAzSZ3XxpUAAAAAAAAAAAAADmJ7AQ/original\" />\n\n:::warning{title=注意}\n不推荐在**树图**中使用 Combo。因为树图的布局与 Combo 的布局机制不兼容，容易导致节点错位或样式混乱。\n:::\n\nG6 的内置 Combo 包括 `circle`（圆形组合）和 `rect` (矩形组合) 两种类型，分别如下图所示：\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Kbk1S5pzSY0AAAAAAAAAAAAADmJ7AQ/original\" width=\"200\" />\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*PKtgSZzmb3YAAAAAAAAAAAAADmJ7AQ/original\" width=\"200\" />\n\n## 数据结构\n\n定义 Combo 时，需要在图的数据对象中添加 `combos` 字段，每个 Combo 是一个对象，结构如下：\n\n| 属性   | 描述                                                                    | 类型           | 默认值 | 必选 |\n| ------ | ----------------------------------------------------------------------- | -------------- | ------ | ---- |\n| id     | 组合的唯一标识符                                                        | string         | -      | ✓    |\n| type   | 组合类型，内置组合类型名称或者自定义组合名称，比如 `circle` 或者 `rect` | string         | -      |      |\n| data   | 组合数据，用于存储组合的自定义数据，可以在样式映射中通过回调函数获取    | object         | -      |      |\n| style  | 组合样式                                                                | object         | -      |      |\n| states | 组合初始状态                                                            | string[]       | -      |      |\n| combo  | 组合的父组合 ID。如果没有父组合，则为 null                              | string \\| null | -      |      |\n\n`combos` 数组中一个数据项的示例：\n\n```json\n{\n  \"id\": \"combo1\",\n  \"type\": \"circle\",\n  \"data\": { \"groupName\": \"Group A\" },\n  \"style\": { \"fill\": \"lightblue\", \"stroke\": \"blue\", \"collapsed\": true },\n  \"states\": [],\n  \"combo\": null\n}\n```\n\n要将节点归属到某个 Combo，可以在节点数据中添加 `combo` 字段：\n\n```json\n{\n  \"nodes\": [{ \"id\": \"node1\", \"combo\": \"comboA\" }], // node1 属于 comboA\n  \"combos\": [{ \"id\": \"comboA\" }] // 定义 comboA\n}\n```\n\n## 配置方法\n\n配置 Combo 的方式有三种，按优先级从高到低如下：\n\n- 使用 `graph.setCombo()` 动态配置\n- 实例化图时全局配置\n- 在数据中动态属性\n\n这几个配置方法可以同时使用。有相同的配置项时，优先级高的方式将会覆盖优先级低的。\n\n### 使用 `graph.setCombo()`\n\n可在图实例创建后，使用 `graph.setCombo()` 动态设置 Combo 的样式映射逻辑。\n\n该方法需要在 `graph.render()` 之前调用才会生效，并拥有最高优先级。\n\n```js\ngraph.setCombo({\n  style: {\n    type: 'circle',\n    style: { fill: '#7FFFD4', stroke: '#5CACEE', lineWidth: 2 },\n  },\n});\n\ngraph.render();\n```\n\n### 实例化图时全局配置\n\n在实例化图时可以通过 `combo` 配置 Combo 样式映射，这里的配置是全局的配置，将会在所有 Combo 上生效。\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  // 在组合配置中指定组合类型以及组合样式类型\n  combo: {\n    type: 'circle',\n    style: { fill: '#7FFFD4', stroke: '#5CACEE', lineWidth: 2 },\n  },\n});\n```\n\n### 在数据中动态配置\n\n如果需要为不同 Combo 进行不同的配置，可以将配置写入到 Combo 数据中。这种配置方式可以通过下面代码的形式直接写入数据：\n\n```typescript\n// 在数据中指定组合类型以及组合样式类型\nconst data = {\n  combos: [\n    {\n      id: 'combo-1',\n      type: 'circle',\n      style: { size: 100, stroke: 'orange' },\n    },\n  ],\n};\n```\n\n### 调整优先级\n\n如果你想让数据中配置的优先级高于全局配置，你可以采取以下方式：\n\n```js\nconst data = {\n  combos: [\n    {\n      id: 'combo-1',\n      type: 'circle',\n      style: { size: 100, stroke: 'orange' },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  combo: {\n    type: (d) => d.type || 'rect',\n    style: {\n      stroke: (d) => d.style.stroke || 'blue',\n    },\n  },\n});\n```\n\n## 示例\n\n```js | ob { inject: true }\nimport { Graph, register, Rect, ExtensionCategory } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n      combo: 'combo1',\n      style: { x: 245, y: 200 },\n    },\n    {\n      id: 'node2',\n      combo: 'combo1',\n      style: { x: 210, y: 250 },\n    },\n    {\n      id: 'node3',\n      combo: 'combo1',\n      style: { x: 280, y: 245 },\n    },\n    {\n      id: 'node4',\n      combo: 'combo2',\n      style: { x: 400, y: 165 },\n    },\n    {\n      id: 'node5',\n      combo: 'combo2',\n      style: { x: 450, y: 162 },\n    },\n    {\n      id: 'node6',\n      combo: 'combo3',\n      style: { x: 425, y: 300 },\n    },\n    {\n      id: 'node7',\n      combo: 'combo3',\n      style: { x: 360, y: 332 },\n    },\n  ],\n  edges: [],\n  combos: [\n    {\n      id: 'combo1',\n      combo: 'combo3',\n      data: { label: 'Combo A' },\n    },\n    {\n      id: 'combo2',\n      combo: 'combo3',\n      data: { label: 'Combo B' },\n    },\n    {\n      id: 'combo3',\n      data: { label: 'Combo C' },\n    },\n    {\n      id: 'combo4',\n      data: { label: 'Combo D' },\n      style: { x: 58, y: 248 },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  height: 450,\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n      labelPlacement: 'center',\n      labelFill: '#fff',\n      labelFontSize: 10,\n    },\n  },\n  combo: {\n    type: 'circle',\n    style: {\n      padding: 2,\n      labelText: (d) => d.data.label,\n      labelPlacement: 'top',\n    },\n  },\n  behaviors: [\n    'collapse-expand',\n    {\n      type: 'drag-element',\n      dropEffect: 'link',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n## Combo 交互\n\n只是简单地将 Combo 渲染出来，并没有多大的实用价值，只有支持一系列的交互操作后，才能最大程度地体现 Combo 的价值。\n\n在 G6 中，我们内置了 `drag-element`、`collapse-expand` 三个交互。\n\n#### drag-element\n\n支持拖动节点和 Combo。拖动 Combo 过程中，会动态改变 Combo 中节点和边的位置，在拖拽完成以后，保持 Combo 和节点的相对位置不变。还可以通过设置 `dropEffect: 'link'` 在拖拽时改变 Combo 的从属关系。\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*rJiHQahxgj4AAAAAAAAAAAAAemJ7AQ/original\" width=\"400\" />\n\n#### collapse-expand\n\n支持双击 Combo 收起和展开 Combo ，收起 Combo 以后，隐藏 Combo 中的所有节点，外部节点和 Combo 中节点有连线的情况下，所有连接会连接到 Combo 上面。\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*PfnsSZSkRagAAAAAAAAAAAAAemJ7AQ/original\" width=\"400\" />\n\n## 自定义组合\n\n当内置组合无法满足需求时，G6 提供了强大的自定义能力：\n\n- 继承内置组合进行扩展\n- 创建全新的组合类型\n\n与组合不同，自定义组合需要先注册后使用。详细教程请参考 [自定义组合](/manual/element/combo/custom-combo) 文档。\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/BaseEdge.en.md",
    "content": "---\ntitle: Edge Common Configuration\norder: 1\n---\n\nThis document introduces the built-in edge common property configurations.\n\n## EdgeOptions\n\n```js {5-9}\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  edge: {\n    type: 'line', // Edge type\n    style: {}, // Edge style\n    state: {}, // State styles\n    palette: {}, // Palette configuration\n    animation: {}, // Animation configuration\n  },\n});\n```\n\n| Property  | Description                                                    | Type                    | Default | Required |\n| --------- | -------------------------------------------------------------- | ----------------------- | ------- | -------- |\n| type      | Edge type, built-in edge type name or custom edge name         | [Type](#type)           | `line`  |          |\n| style     | Edge style configuration, including color, thickness, etc.     | [Style](#style)         | -       |          |\n| state     | Style configuration for different states                       | [State](#state)         | -       |          |\n| palette   | Define edge palette for mapping colors based on different data | [Palette](#palette)     | -       |          |\n| animation | Define edge animation effects                                  | [Animation](#animation) | -       |          |\n\n## Type\n\nSpecify the edge type, built-in edge type name or custom edge name. Default is `line` (straight line edge). **⚠️ Note**: This determines the shape of the main graphic.\n\n```js {3}\nconst graph = new Graph({\n  edge: {\n    type: 'polyline',\n  },\n});\n```\n\n**⚠️ Dynamic Configuration Note**: The `type` property also supports dynamic configuration, allowing you to dynamically select edge types based on edge data:\n\n```js\nconst graph = new Graph({\n  edge: {\n    // Static configuration\n    type: 'line',\n\n    // Dynamic configuration - arrow function form\n    type: (datum) => datum.data.edgeType || 'line',\n\n    // Dynamic configuration - regular function form (can access graph instance)\n    type: function (datum) {\n      console.log(this); // graph instance\n      return datum.data.importance > 5 ? 'polyline' : 'line';\n    },\n  },\n});\n```\n\nAvailable values:\n\n- `line`: [Straight line edge](/en/manual/element/edge/line)\n- `polyline`: [Polyline edge](/en/manual/element/edge/polyline)\n- `cubic`: [Cubic Bezier curve edge](/en/manual/element/edge/cubic)\n- `cubic-horizontal`: [Horizontal cubic Bezier curve edge](/en/manual/element/edge/cubic-horizontal)\n- `cubic-vertical`: [Vertical cubic Bezier curve edge](/en/manual/element/edge/cubic-vertical)\n- `quadratic`: [Quadratic Bezier curve edge](/en/manual/element/edge/quadratic)\n\n## Style\n\nDefine edge styles, including color, thickness, etc.\n\n```js {3}\nconst graph = new Graph({\n  edge: {\n    style: {},\n  },\n});\n```\n\n**⚠️ Dynamic Configuration Note**: All the following style properties support dynamic configuration, meaning you can pass functions to dynamically calculate property values based on edge data:\n\n```js\nconst graph = new Graph({\n  edge: {\n    style: {\n      // Static configuration\n      stroke: '#1783FF',\n\n      // Dynamic configuration - arrow function form\n      lineWidth: (datum) => (datum.data.isImportant ? 3 : 1),\n\n      // Dynamic configuration - regular function form (can access graph instance)\n      lineDash: function (datum) {\n        console.log(this); // graph instance\n        return datum.data.type === 'dashed' ? [5, 5] : [];\n      },\n\n      // Nested properties also support dynamic configuration\n      labelText: (datum) => `Edge: ${datum.id}`,\n      endArrow: (datum) => datum.data.hasArrow,\n    },\n  },\n});\n```\n\nWhere the `datum` parameter is the edge data object (`EdgeData`), containing all data information of the edge.\n\nA complete edge consists of the following parts:\n\n<img width=\"320\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*cVHVQJKLOlgAAAAAAAAAAAAADmJ7AQ/original\" />\n\n- `key`: The main graphic of the edge, representing the main path of the edge, such as straight lines, curves, etc.\n- `label`: Text label, usually used to display the name or description of the edge\n- `badge`: Badge on the edge\n- `halo`: The halo effect graphic displayed around the main graphic\n- `startArrow`: Arrow at the starting end of the edge\n- `endArrow`: Arrow at the ending end of the edge\n\nThe following style configurations will be explained by atomic graphics in order:\n\n### Main Graphic Styles\n\nThe main graphic is the core part of the edge, defining the basic path and appearance of the edge. Here are common configuration scenarios:\n\n#### Basic Style Configuration\n\nSet the basic appearance of the edge:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 40 } },\n      { id: 'node2', style: { x: 180, y: 40 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      stroke: '#5B8FF9', // Blue edge\n      lineWidth: 2, // Edge width\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Dashed Line Style\n\nCreate edges with dashed line style:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 40 } },\n      { id: 'node2', style: { x: 180, y: 40 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      stroke: '#F5222D',\n      lineWidth: 2,\n      lineDash: [6, 4], // Dashed line style\n      lineDashOffset: 0,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Shadow Effect\n\nAdd shadow effect to edges:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 40 } },\n      { id: 'node2', style: { x: 180, y: 40 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      stroke: '#722ED1',\n      lineWidth: 3,\n      shadowColor: 'rgba(114, 46, 209, 0.3)',\n      shadowBlur: 8,\n      shadowOffsetX: 2,\n      shadowOffsetY: 2,\n    },\n  },\n});\n\ngraph.render();\n```\n\nThe following is the complete main graphic style configuration:\n\n| Property                        | Description                                                                                                                        | Type                  | Default   | Required |\n| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------- | --------- | -------- |\n| cursor                          | Mouse cursor style when hovering over edge, [options](#cursor)                                                                     | string                | `default` |          |\n| increasedLineWidthForHitTesting | When lineWidth is small, the interactive area also becomes small. We can increase this area to make \"thin lines\" easier to pick up | number                | 0         |          |\n| lineDash                        | Edge dash line style                                                                                                               | number[]              | -         |          |\n| lineDashOffset                  | Edge dash line offset                                                                                                              | number                | 0         |          |\n| lineWidth                       | Edge width                                                                                                                         | number                | 1         |          |\n| opacity                         | Edge opacity                                                                                                                       | number \\| string      | 1         |          |\n| pointerEvents                   | How edge responds to pointer events, [options](#pointerevents)                                                                     | string                | `auto`    |          |\n| shadowBlur                      | Edge shadow blur                                                                                                                   | number                | -         |          |\n| shadowColor                     | Edge shadow color                                                                                                                  | string                | -         |          |\n| shadowOffsetX                   | Edge shadow offset in x direction                                                                                                  | number \\| string      | -         |          |\n| shadowOffsetY                   | Edge shadow offset in y direction                                                                                                  | number \\| string      | -         |          |\n| shadowType                      | Edge shadow type                                                                                                                   | `inner` \\| `outer`    | `outer`   |          |\n| sourcePort                      | Connection port at the source end of the edge                                                                                      | string                | -         |          |\n| stroke                          | Edge color                                                                                                                         | string                | `#000`    |          |\n| strokeOpacity                   | Edge color opacity                                                                                                                 | number \\| string      | 1         |          |\n| targetPort                      | Connection port at the target end of the edge                                                                                      | string                | -         |          |\n| transform                       | Transform property allows you to rotate, scale, skew, or translate the given edge                                                  | string                | -         |          |\n| transformOrigin                 | The center of rotation and scaling, also known as the transform center                                                             | string                | -         |          |\n| visibility                      | Whether the edge is visible                                                                                                        | `visible` \\| `hidden` | `visible` |          |\n| zIndex                          | Edge rendering layer                                                                                                               | number                | 1         |          |\n\n#### PointerEvents\n\nThe `pointerEvents` property controls how graphics respond to interaction events. Refer to [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events).\n\nAvailable values: `visible` | `visiblepainted` | `visiblestroke` | `non-transparent-pixel` | `visiblefill` | `visible` | `painted` | `fill` | `stroke` | `all` | `none` | `auto` | `inherit` | `initial` | `unset`\n\nIn short, both `stroke` and `visibility` can independently or in combination affect hit testing behavior. Currently supports the following keywords:\n\n- **`auto`**: Default value, equivalent to `visiblepainted`\n- **`none`**: Never becomes a target for responding to events\n- **`visiblepainted`**: Responds to events only when the following conditions are met:\n  - `visibility` is set to `visible`, i.e., the graphic is visible\n  - Triggered in the graphic stroke area while `stroke` takes a non-`none` value\n- **`visiblestroke`**: Responds to events only when the following conditions are met:\n  - `visibility` is set to `visible`, i.e., the graphic is visible\n  - Triggered in the graphic stroke area, not affected by `stroke` value\n- **`visible`**: Responds to events only when the following conditions are met:\n  - `visibility` is set to `visible`, i.e., the graphic is visible\n  - Triggered in the graphic stroke area, not affected by `stroke` value\n- **`painted`**: Responds to events only when the following conditions are met:\n  - Triggered in the graphic stroke area while `stroke` takes a non-`none` value\n  - Not affected by `visibility` value\n- **`stroke`**: Responds to events only when the following conditions are met:\n  - Triggered in the graphic stroke area, not affected by `stroke` value\n  - Not affected by `visibility` value\n- **`all`**: Responds to events as long as entering the graphic stroke area, not affected by `stroke` or `visibility` values\n\n**Usage Examples:**\n\n```js\n// Example 1: Only stroke area responds to events\nconst graph = new Graph({\n  edge: {\n    style: {\n      stroke: '#000',\n      lineWidth: 2,\n      pointerEvents: 'stroke', // Only stroke responds to events\n    },\n  },\n});\n\n// Example 2: Completely non-responsive to events\nconst graph = new Graph({\n  edge: {\n    style: {\n      pointerEvents: 'none', // Edge does not respond to any events\n    },\n  },\n});\n```\n\n#### Cursor\n\nAvailable values: `auto` | `default` | `none` | `context-menu` | `help` | `pointer` | `progress` | `wait` | `cell` | `crosshair` | `text` | `vertical-text` | `alias` | `copy` | `move` | `no-drop` | `not-allowed` | `grab` | `grabbing` | `all-scroll` | `col-resize` | `row-resize` | `n-resize` | `e-resize` | `s-resize` | `w-resize` | `ne-resize` | `nw-resize` | `se-resize` | `sw-resize` | `ew-resize` | `ns-resize` | `nesw-resize` | `nwse-resize` | `zoom-in` | `zoom-out`\n\n### Label Styles\n\nLabels are used to display text information for edges, supporting various style configurations and layout options. Here are common usage scenarios:\n\n#### Basic Text Label\n\nThe simplest text label configuration:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 120,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 60 } },\n      { id: 'node2', style: { x: 180, y: 60 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      labelText: 'Edge Label',\n      labelFill: '#262626',\n      labelFontSize: 12,\n      labelPlacement: 'center',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Multi-line Text Label\n\nWhen text is long, you can set automatic line wrapping:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 120,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 60 } },\n      { id: 'node2', style: { x: 180, y: 60 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      labelText: 'This is a very long edge label that needs line wrapping',\n      labelWordWrap: true,\n      labelMaxWidth: '200%',\n      labelMaxLines: 2,\n      labelTextOverflow: 'ellipsis',\n      labelFill: '#434343',\n      labelPlacement: 'center',\n      labelTextAlign: 'center',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Label with Background\n\nAdd background to labels for better readability:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 120,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 60 } },\n      { id: 'node2', style: { x: 180, y: 60 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      labelText: 'Important Connection',\n      labelBackground: true,\n      labelBackgroundFill: 'rgba(250, 140, 22, 0.1)',\n      labelBackgroundRadius: 6,\n      labelPadding: [4, 8],\n      labelFill: '#D4380D',\n      labelFontWeight: 'bold',\n      labelPlacement: 'center',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Auto-rotating Label\n\nLabels can automatically rotate to align with edge direction:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 120,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 30 } },\n      { id: 'node2', style: { x: 180, y: 90 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      labelText: 'Auto Rotate',\n      labelAutoRotate: true, // Auto rotate\n      labelFill: '#1890FF',\n      labelFontWeight: 'bold',\n      labelPlacement: 'center',\n    },\n  },\n});\n\ngraph.render();\n```\n\nThe following is the complete label style configuration:\n\n| Property                 | Description                                                                                                     | Type                                                                        | Default   | Required |\n| ------------------------ | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------- | -------- |\n| label                    | Whether to show edge label                                                                                      | boolean                                                                     | true      |          |\n| labelAutoRotate          | Whether edge label automatically rotates to align with edge direction                                           | boolean                                                                     | true      |          |\n| labelCursor              | Mouse cursor style when hovering over edge label, [options](#cursor)                                            | string                                                                      | `default` |          |\n| labelFill                | Edge label text color                                                                                           | string                                                                      | -         |          |\n| labelFontFamily          | Edge label font family                                                                                          | string                                                                      | -         |          |\n| labelFontSize            | Edge label font size                                                                                            | number                                                                      | 12        |          |\n| labelFontStyle           | Edge label font style                                                                                           | `normal` \\| `italic` \\| `oblique`                                           | -         |          |\n| labelFontVariant         | Edge label font variant                                                                                         | `normal` \\| `small-caps` \\| string                                          | -         |          |\n| labelFontWeight          | Edge label font weight                                                                                          | `normal` \\| `bold` \\| `bolder` \\| `lighter` \\| number                       | -         |          |\n| labelLeading             | Line spacing                                                                                                    | number                                                                      | 0         |          |\n| labelLetterSpacing       | Edge label letter spacing                                                                                       | number \\| string                                                            | -         |          |\n| labelLineHeight          | Edge label line height                                                                                          | number \\| string                                                            | -         |          |\n| labelMaxLines            | Edge label maximum lines                                                                                        | number                                                                      | 1         |          |\n| labelMaxWidth            | Edge label maximum width, [options](#labelmaxwidth)                                                             | number \\| string                                                            | `200%`    |          |\n| labelOffsetX             | Edge label offset in x direction                                                                                | number                                                                      | 0         |          |\n| labelOffsetY             | Edge label offset in y direction                                                                                | number                                                                      | 0         |          |\n| labelPadding             | Edge label padding                                                                                              | number \\| number[]                                                          | 0         |          |\n| labelPlacement           | Edge label position relative to edge, [options](#labelplacement)                                                | string \\| number                                                            | `center`  |          |\n| labelText                | Edge label text content                                                                                         | `string` \\| `(datum) => string`                                             | -         |          |\n| labelTextAlign           | Edge label text horizontal alignment                                                                            | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`               | `left`    |          |\n| labelTextBaseline        | Edge label text baseline                                                                                        | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom` | -         |          |\n| labelTextDecorationColor | Edge label text decoration line color                                                                           | string                                                                      | -         |          |\n| labelTextDecorationLine  | Edge label text decoration line                                                                                 | string                                                                      | -         |          |\n| labelTextDecorationStyle | Edge label text decoration line style                                                                           | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                       | -         |          |\n| labelTextOverflow        | Edge label text overflow handling                                                                               | `clip` \\| `ellipsis` \\| string                                              | -         |          |\n| labelTextPath            | Edge label text path                                                                                            | Path                                                                        | -         |          |\n| labelWordWrap            | Whether to enable automatic line wrapping for edge labels. When enabled, text exceeding labelMaxWidth will wrap | boolean                                                                     | false     |          |\n| labelZIndex              | Edge label rendering layer                                                                                      | number                                                                      | 0         |          |\n\n#### LabelPlacement\n\nEdge label position relative to the edge, can be set to:\n\n- `start`: Label positioned at the starting point of the edge\n- `center`: Label positioned at the center of the edge (default)\n- `end`: Label positioned at the ending point of the edge\n- `number`: Value range 0-1, representing the specific position ratio of the label on the edge, 0 for start position, 1 for end position\n\n#### LabelMaxWidth\n\nAfter enabling automatic line wrapping `labelWordWrap`, text exceeding this width will wrap:\n\n- string: Represents the maximum width defined as a percentage relative to the edge length. For example, `50%` means the label width does not exceed half the edge length\n- number: Represents the maximum width defined in pixels. For example, 100 means the label's maximum width is 100 pixels\n\nFor example, setting multi-line label text:\n\n```json\n{\n  \"labelWordWrap\": true,\n  \"labelMaxWidth\": 200,\n  \"labelMaxLines\": 3\n}\n```\n\n### Label Background Styles\n\nLabel background is used to display the background of edge labels:\n\n| Property                      | Description                                                                                                                                                       | Type                                     | Default   |\n| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | --------- |\n| labelBackground               | Whether to show edge label background                                                                                                                             | boolean                                  | false     |\n| labelBackgroundCursor         | Edge label background mouse cursor style, [options](#cursor)                                                                                                      | string                                   | `default` |\n| labelBackgroundFill           | Edge label background fill color                                                                                                                                  | string                                   | -         |\n| labelBackgroundFillOpacity    | Edge label background opacity                                                                                                                                     | number                                   | 1         |\n| labelBackgroundHeight         | Edge label background height                                                                                                                                      | string \\| number                         | -         |\n| labelBackgroundLineDash       | Edge label background dash line configuration                                                                                                                     | number \\| string \\|(number \\| string )[] | -         |\n| labelBackgroundLineDashOffset | Edge label background dash line offset                                                                                                                            | number                                   | -         |\n| labelBackgroundLineWidth      | Edge label background stroke line width                                                                                                                           | number                                   | -         |\n| labelBackgroundRadius         | Edge label background border radius <br> - number: Uniform radius for all corners <br> - number[]: Individual radius for each corner, auto-filled if insufficient | number \\| number[]                       | 0         |\n| labelBackgroundShadowBlur     | Edge label background shadow blur                                                                                                                                 | number                                   | -         |\n| labelBackgroundShadowColor    | Edge label background shadow color                                                                                                                                | string                                   | -         |\n| labelBackgroundShadowOffsetX  | Edge label background shadow X offset                                                                                                                             | number                                   | -         |\n| labelBackgroundShadowOffsetY  | Edge label background shadow Y offset                                                                                                                             | number                                   | -         |\n| labelBackgroundStroke         | Edge label background stroke color                                                                                                                                | string                                   | -         |\n| labelBackgroundStrokeOpacity  | Edge label background stroke opacity                                                                                                                              | number \\| string                         | 1         |\n| labelBackgroundVisibility     | Edge label background visibility                                                                                                                                  | `visible` \\| `hidden`                    | -         |\n| labelBackgroundZIndex         | Edge label background rendering layer                                                                                                                             | number                                   | 1         |\n\n### Halo Styles\n\nHalo is an effect displayed around the edge main graphic, usually used for highlighting or indicating special states of the edge.\n\n#### Basic Halo Effect\n\nAdd basic halo effect to edges:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 50 } },\n      { id: 'node2', style: { x: 180, y: 50 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      lineWidth: 2,\n      halo: true,\n      haloStroke: '#1890FF',\n      haloLineWidth: 6,\n      haloStrokeOpacity: 0.3,\n    },\n  },\n});\n\ngraph.render();\n```\n\nThe following is the complete halo style configuration:\n\n| Property          | Description                                                                          | Type                   | Default                                   | Required |\n| ----------------- | ------------------------------------------------------------------------------------ | ---------------------- | ----------------------------------------- | -------- |\n| halo              | Whether to show edge halo                                                            | boolean                | false                                     |          |\n| haloCursor        | Edge halo mouse cursor style, [options](#cursor)                                     | string                 | `default`                                 |          |\n| haloDraggable     | Whether edge halo allows dragging                                                    | boolean                | true                                      |          |\n| haloDroppable     | Whether edge halo allows receiving dragged elements                                  | boolean                | true                                      |          |\n| haloFillRule      | Edge halo fill rule                                                                  | `nonzero` \\| `evenodd` | -                                         |          |\n| haloFilter        | Edge halo filter                                                                     | string                 | -                                         |          |\n| haloLineWidth     | Edge halo stroke width                                                               | number                 | 3                                         |          |\n| haloPointerEvents | Whether edge halo responds to pointer events, [options](#pointerevents)              | string                 | `none`                                    |          |\n| haloStroke        | Edge halo stroke color, **this property sets the color of the halo around the edge** | string                 | Consistent with main graphic stroke color |          |\n| haloStrokeOpacity | Edge halo stroke opacity                                                             | number                 | 0.25                                      |          |\n| haloVisibility    | Edge halo visibility                                                                 | `visible` \\| `hidden`  | `visible`                                 |          |\n| haloZIndex        | Edge halo rendering layer                                                            | number                 | -1                                        |          |\n\n### Arrow Styles\n\nEdges support adding arrows at the start and end points to indicate the directionality of the edge.\n\n#### Basic Arrow\n\nAdd basic arrow to the end of the edge:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 50 } },\n      { id: 'node2', style: { x: 180, y: 50 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      stroke: '#1890FF',\n      lineWidth: 2,\n      endArrow: true, // End arrow\n      endArrowType: 'vee', // Arrow type\n      endArrowSize: 10, // Arrow size\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Bidirectional Arrows\n\nAdd arrows to both ends of the edge:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 50 } },\n      { id: 'node2', style: { x: 180, y: 50 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      stroke: '#52C41A',\n      lineWidth: 2,\n      startArrow: true, // Start arrow\n      startArrowType: 'circle',\n      startArrowSize: 8,\n      endArrow: true, // End arrow\n      endArrowType: 'triangle',\n      endArrowSize: 10,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Custom Arrow Style\n\nCustomize arrow color and type:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 50 } },\n      { id: 'node2', style: { x: 180, y: 50 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      stroke: '#722ED1',\n      lineWidth: 3,\n      endArrow: true,\n      endArrowType: 'diamond', // Diamond arrow\n      endArrowSize: 12,\n      endArrowFill: '#FF4D4F', // Red arrow fill\n      endArrowStroke: '#722ED1', // Arrow stroke color\n      endArrowStrokeOpacity: 0.8,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Start Arrow Style Configuration\n\n| Property                | Description                                             | Type                                                                                 | Default                            | Required |\n| ----------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------- | -------- |\n| startArrow              | Whether to show edge start arrow                        | boolean                                                                              | false                              |          |\n| startArrowCursor        | Edge start arrow mouse cursor style, [options](#cursor) | string                                                                               | `default`                          |          |\n| startArrowFill          | Edge start arrow fill color                             | string                                                                               | Default consistent with edge color |          |\n| startArrowFillOpacity   | Edge start arrow fill opacity                           | number                                                                               | 1                                  |          |\n| startArrowOffset        | Edge start arrow offset                                 | number                                                                               | 0                                  |          |\n| startArrowSize          | Edge start arrow size                                   | number \\| [number, number]                                                           | 10                                 |          |\n| startArrowStroke        | Edge start arrow stroke color                           | string                                                                               | Default consistent with edge color |          |\n| startArrowStrokeOpacity | Edge start arrow stroke opacity                         | number                                                                               | 1                                  |          |\n| startArrowType          | Edge start arrow type                                   | `triangle` \\| `circle` \\| `diamond` \\| `vee` \\| `rect` \\| `triangleRect` \\| `simple` | `vee`                              |          |\n\n#### End Arrow Style Configuration\n\n| Property              | Description                                           | Type                                                                                 | Default                            | Required |\n| --------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------- | -------- |\n| endArrow              | Whether to show edge end arrow                        | boolean                                                                              | false                              |          |\n| endArrowCursor        | Edge end arrow mouse cursor style, [options](#cursor) | string                                                                               | `default`                          |          |\n| endArrowFill          | Edge end arrow fill color                             | string                                                                               | Default consistent with edge color |          |\n| endArrowFillOpacity   | Edge end arrow fill opacity                           | number                                                                               | 1                                  |          |\n| endArrowOffset        | Edge end arrow offset                                 | number                                                                               | 0                                  |          |\n| endArrowSize          | Edge end arrow size                                   | number \\| [number, number]                                                           | 10                                 |          |\n| endArrowStroke        | Edge end arrow stroke color                           | string                                                                               | Default consistent with edge color |          |\n| endArrowStrokeOpacity | Edge end arrow stroke opacity                         | number                                                                               | 1                                  |          |\n| endArrowType          | Edge end arrow type                                   | `triangle` \\| `circle` \\| `diamond` \\| `vee` \\| `rect` \\| `triangleRect` \\| `simple` | `vee`                              |          |\n\n### Loop Edge Styles\n\nLoop edges are special edges where the start and end nodes are the same node.\n\n#### Basic Loop Edge\n\nCreate a basic loop edge:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', style: { x: 100, y: 50 } }],\n    edges: [{ source: 'node1', target: 'node1' }],\n  },\n  edge: {\n    style: {\n      stroke: '#1890FF',\n      lineWidth: 2,\n      endArrow: true,\n      loopPlacement: 'top', // Loop position\n      loopDist: 30, // Loop size\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Multiple Loop Edges\n\nCreate multiple loop edges at different positions for the same node:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 120,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', style: { x: 100, y: 60 } }],\n    edges: [\n      { id: 'edge1', source: 'node1', target: 'node1' },\n      { id: 'edge2', source: 'node1', target: 'node1' },\n      { id: 'edge3', source: 'node1', target: 'node1' },\n    ],\n  },\n  edge: {\n    style: {\n      lineWidth: 2,\n      endArrow: true,\n      loopPlacement: (datum) => {\n        const placements = ['top', 'right', 'bottom'];\n        return placements[parseInt(datum.id.slice(-1)) - 1];\n      },\n      loopDist: 25,\n      stroke: (datum) => {\n        const colors = ['#1890FF', '#52C41A', '#722ED1'];\n        return colors[parseInt(datum.id.slice(-1)) - 1];\n      },\n    },\n  },\n});\n\ngraph.render();\n```\n\nThe following is the complete loop edge style configuration:\n\n| Property      | Description                                                         | Type                                                                                                                                                                   | Default                  | Required |\n| ------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | -------- |\n| loop          | Whether to enable loop edges                                        | boolean                                                                                                                                                                | true                     |          |\n| loopClockwise | Whether to draw the loop clockwise                                  | boolean                                                                                                                                                                | true                     |          |\n| loopDist      | Distance from node edge to loop top, used to specify loop curvature | number                                                                                                                                                                 | Default to max node size |          |\n| loopPlacement | Loop edge position                                                  | `left` \\| `right` \\| `top` \\| `bottom` \\| `left-top` \\| `left-bottom` \\| `right-top` \\| `right-bottom` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right` | `top`                    |          |\n\n## State\n\nIn some interactive behaviors, such as clicking to select an edge or hovering to activate an edge, it's simply marking certain states on that element. To reflect these states in the visual space seen by end users, we need to set different graphic element styles for different states to respond to changes in the state of that graphic element.\n\nG6 provides several built-in states, including selected, highlight, active, inactive, and disabled. Additionally, it supports custom states to meet more specific needs. For each state, developers can define a set of style rules that will override the element's default styles.\n\n<img width=\"520\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ebBlTpKu2WUAAAAAAAAAAAAADmJ7AQ/original\" />\n\nThe data structure is as follows:\n\n```typescript\ntype EdgeState = {\n  [state: string]: EdgeStyle;\n};\n```\n\nFor example, when an edge is in the `focus` state, you can add a halo with a width of 6 and orange color.\n\n```js {4-9}\nconst graph = new Graph({\n  edge: {\n    state: {\n      focus: {\n        halo: true,\n        haloLineWidth: 6,\n        haloStroke: 'orange',\n        haloStrokeOpacity: 0.6,\n      },\n    },\n  },\n});\n```\n\nThe effect is shown in the following image:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }],\n    edges: [{ source: 'node1', target: 'node2', states: ['focus'] }],\n  },\n  edge: {\n    state: {\n      focus: {\n        halo: true,\n        haloLineWidth: 6,\n        haloStroke: 'orange',\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n    cols: 2,\n  },\n});\n\ngraph.render();\n```\n\n## Animation\n\nDefine edge animation effects. Supports the following two configuration methods:\n\n1. Disable all edge animations\n\n```json\n{\n  \"edge\": {\n    \"animation\": false\n  }\n}\n```\n\n2. Configure stage animations\n\nStage animations refer to the animation effects when edges enter the canvas, update, or leave the canvas. Currently supported stages include:\n\n- `enter`: Animation when edge enters the canvas\n- `update`: Animation when edge updates\n- `exit`: Animation when edge leaves the canvas\n- `show`: Animation when edge shows from hidden state\n- `hide`: Animation when edge hides\n- `collapse`: Animation when edge collapses\n- `expand`: Animation when edge expands\n\nYou can refer to [Animation Paradigm](/en/manual/animation/animation#动画范式) to use animation syntax to configure edges, such as:\n\n```json\n{\n  \"edge\": {\n    \"animation\": {\n      \"update\": [\n        {\n          \"fields\": [\"stroke\"], // Only animate stroke property during update\n          \"duration\": 1000, // Animation duration\n          \"easing\": \"linear\" // Easing function\n        }\n      ]\n    }\n  }\n}\n```\n\nYou can also use built-in animation effects:\n\n```json\n{\n  \"edge\": {\n    \"animation\": {\n      \"enter\": \"fade\", // Use fade animation\n      \"update\": \"path-in\", // Use path animation\n      \"exit\": \"fade\" // Use fade animation\n    }\n  }\n}\n```\n\nYou can pass false to disable specific stage animations:\n\n```json\n{\n  \"edge\": {\n    \"animation\": {\n      \"enter\": false // Disable edge entrance animation\n    }\n  }\n}\n```\n\n## Palette\n\nDefine the edge palette, which is a predefined edge color pool that is allocated according to rules and maps colors to the `stroke` property.\n\n> For palette definitions, please refer to [Palette](/en/manual/theme/palette).\n\n| Property | Description                                                                                                           | Type                          | Default |\n| -------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ------- |\n| color    | Palette colors. If the palette is registered, you can directly specify its registration name, or accept a color array | string \\| string[]            | -       |\n| field    | Specify the grouping field in element data. If not specified, defaults to using id as grouping field                  | string \\| ((datum) => string) | `id`    |\n| invert   | Whether to invert the palette                                                                                         | boolean                       | false   |\n| type     | Specify current palette type. <br> - `group`: Discrete palette <br> - `value`: Continuous palette                     | `group` \\| `value`            | `group` |\n\nFor example, to assign edge colors to a group of data by the `direction` field, making edges of the same category have the same color:\n\n```json\n{\n  \"edge\": {\n    \"palette\": {\n      \"type\": \"group\",\n      \"field\": \"direction\",\n      \"color\": [\"#F08F56\", \"#00C9C9\", \"#D580FF\"]\n    }\n  }\n}\n```\n\nThe effect is shown in the following image:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  data: {\n    nodes: new Array(6).fill(0).map((_, i) => ({ id: `node-${i + 1}` })),\n    edges: [\n      { source: 'node-1', target: 'node-2', data: { direction: 'out' } },\n      { source: 'node-1', target: 'node-3', data: { direction: 'out' } },\n      { source: 'node-1', target: 'node-4', data: { direction: 'out' } },\n      { source: 'node-5', target: 'node-1', data: { direction: 'in' } },\n      { source: 'node-6', target: 'node-1', data: { direction: 'in' } },\n    ],\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 120,\n    linkDistance: 120,\n  },\n  edge: {\n    style: {\n      endArrow: true,\n    },\n    palette: {\n      type: 'group',\n      field: 'direction',\n      color: ['#F08F56', '#00C9C9'],\n    },\n  },\n});\n\ngraph.render();\n```\n\nYou can also use default configuration:\n\n```json\n{\n  \"edge\": {\n    \"palette\": \"tableau\" // tableau is the palette name, colors assigned by ID by default\n  }\n}\n```\n\nThe effect is shown in the following image:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  data: {\n    nodes: new Array(6).fill(0).map((_, i) => ({ id: `node-${i + 1}` })),\n    edges: [\n      { source: 'node-1', target: 'node-2', data: { direction: 'out' } },\n      { source: 'node-1', target: 'node-3', data: { direction: 'out' } },\n      { source: 'node-1', target: 'node-4', data: { direction: 'out' } },\n      { source: 'node-5', target: 'node-1', data: { direction: 'in' } },\n      { source: 'node-6', target: 'node-1', data: { direction: 'in' } },\n    ],\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 120,\n    linkDistance: 120,\n  },\n  edge: {\n    style: {\n      endArrow: true,\n    },\n    palette: 'tableau',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/BaseEdge.zh.md",
    "content": "---\ntitle: 边通用配置项\norder: 1\n---\n\n本文介绍内置边通用属性配置。\n\n## EdgeOptions\n\n```js {5-9}\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  edge: {\n    type: 'line', // 边类型\n    style: {}, // 边样式\n    state: {}, // 状态样式\n    palette: {}, // 色板配置\n    animation: {}, // 动画配置\n  },\n});\n```\n\n| 属性      | 描述                                   | 类型                    | 默认值 | 必选 |\n| --------- | -------------------------------------- | ----------------------- | ------ | ---- |\n| type      | 边类型，内置边类型名称或自定义边的名称 | [Type](#type)           | `line` |      |\n| style     | 边样式配置，包括颜色、粗细等           | [Style](#style)         | -      |      |\n| state     | 不同状态下的样式配置                   | [State](#state)         | -      |      |\n| palette   | 定义边的色板，用于根据不同数据映射颜色 | [Palette](#palette)     | -      |      |\n| animation | 定义边的动画效果                       | [Animation](#animation) | -      |      |\n\n## Type\n\n指定边类型，内置边类型名称或自定义边的名称。默认为 `line`（直线边）。**⚠️ 注意**：这里决定了主图形的形状。\n\n```js {3}\nconst graph = new Graph({\n  edge: {\n    type: 'polyline',\n  },\n});\n```\n\n**⚠️ 动态配置说明**：`type` 属性同样支持动态配置，可以根据边数据动态选择边类型：\n\n```js\nconst graph = new Graph({\n  edge: {\n    // 静态配置\n    type: 'line',\n\n    // 动态配置 - 箭头函数形式\n    type: (datum) => datum.data.edgeType || 'line',\n\n    // 动态配置 - 普通函数形式（可访问 graph 实例）\n    type: function (datum) {\n      console.log(this); // graph 实例\n      return datum.data.importance > 5 ? 'polyline' : 'line';\n    },\n  },\n});\n```\n\n可选值有：\n\n- `line`：[直线边](/manual/element/edge/line)\n- `polyline`：[折线边](/manual/element/edge/polyline)\n- `cubic`：[三次贝塞尔曲线边](/manual/element/edge/cubic)\n- `cubic-horizontal`：[水平三次贝塞尔曲线边](/manual/element/edge/cubic-horizontal)\n- `cubic-vertical`：[垂直三次贝塞尔曲线边](/manual/element/edge/cubic-vertical)\n- `quadratic`：[二次贝塞尔曲线边](/manual/element/edge/quadratic)\n\n## Style\n\n定义边的样式，包括颜色、粗细等。\n\n```js {3}\nconst graph = new Graph({\n  edge: {\n    style: {},\n  },\n});\n```\n\n**⚠️ 动态配置说明**：以下所有样式属性都支持动态配置，即可以传入函数来根据边数据动态计算属性值：\n\n```js\nconst graph = new Graph({\n  edge: {\n    style: {\n      // 静态配置\n      stroke: '#1783FF',\n\n      // 动态配置 - 箭头函数形式\n      lineWidth: (datum) => (datum.data.isImportant ? 3 : 1),\n\n      // 动态配置 - 普通函数形式（可访问 graph 实例）\n      lineDash: function (datum) {\n        console.log(this); // graph 实例\n        return datum.data.type === 'dashed' ? [5, 5] : [];\n      },\n\n      // 嵌套属性也支持动态配置\n      labelText: (datum) => `边: ${datum.id}`,\n      endArrow: (datum) => datum.data.hasArrow,\n    },\n  },\n});\n```\n\n其中 `datum` 参数为边数据对象 (`EdgeData`)，包含边的所有数据信息。\n\n一个完整的边由以下几部分构成：\n\n<img width=\"320\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*cVHVQJKLOlgAAAAAAAAAAAAADmJ7AQ/original\" />\n\n- `key` ：边的主图形，表示边的主要路径，例如直线、曲线等；\n- `label` ：文本标签，通常用于展示边的名称或描述；\n- `badge` ：边上的徽标；\n- `halo` ：主图形周围展示的光晕效果的图形；\n- `startArrow` ：边起始端的箭头；\n- `endArrow` ：边结束端的箭头。\n\n以下样式配置将按原子图形依次说明：\n\n### 主图形样式\n\n主图形是边的核心部分，定义了边的基本路径和外观。以下是常见的配置场景：\n\n#### 基础样式配置\n\n设置边的基本外观：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 40 } },\n      { id: 'node2', style: { x: 180, y: 40 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      stroke: '#5B8FF9', // 蓝色边\n      lineWidth: 2, // 边宽度\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 虚线样式\n\n创建带虚线样式的边：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 40 } },\n      { id: 'node2', style: { x: 180, y: 40 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      stroke: '#F5222D',\n      lineWidth: 2,\n      lineDash: [6, 4], // 虚线样式\n      lineDashOffset: 0,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 阴影效果\n\n为边添加阴影效果：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 40 } },\n      { id: 'node2', style: { x: 180, y: 40 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      stroke: '#722ED1',\n      lineWidth: 3,\n      shadowColor: 'rgba(114, 46, 209, 0.3)',\n      shadowBlur: 8,\n      shadowOffsetX: 2,\n      shadowOffsetY: 2,\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的主图形样式配置：\n\n| 属性                            | 描述                                                                                      | 类型                  | 默认值    | 必选 |\n| ------------------------------- | ----------------------------------------------------------------------------------------- | --------------------- | --------- | ---- |\n| cursor                          | 边鼠标移入样式，[配置项](#cursor)                                                         | string                | `default` |      |\n| increasedLineWidthForHitTesting | 当 lineWidth 较小时，可交互区域也随之变小，有时我们想增大这个区域，让\"细线\"更容易被拾取到 | number                | 0         |      |\n| lineDash                        | 边虚线样式                                                                                | number[]              | -         |      |\n| lineDashOffset                  | 边虚线偏移量                                                                              | number                | 0         |      |\n| lineWidth                       | 边宽度                                                                                    | number                | 1         |      |\n| opacity                         | 边透明度                                                                                  | number \\| string      | 1         |      |\n| pointerEvents                   | 边如何响应指针事件，[配置项](#pointerevents)                                              | string                | `auto`    |      |\n| shadowBlur                      | 边阴影模糊度                                                                              | number                | -         |      |\n| shadowColor                     | 边阴影颜色                                                                                | string                | -         |      |\n| shadowOffsetX                   | 边阴影在 x 轴方向上的偏移量                                                               | number \\| string      | -         |      |\n| shadowOffsetY                   | 边阴影在 y 轴方向上的偏移量                                                               | number \\| string      | -         |      |\n| shadowType                      | 边阴影类型                                                                                | `inner` \\| `outer`    | `outer`   |      |\n| sourcePort                      | 边起始连接的连接桩                                                                        | string                | -         |      |\n| stroke                          | 边颜色                                                                                    | string                | `#000`    |      |\n| strokeOpacity                   | 边颜色透明度                                                                              | number \\| string      | 1         |      |\n| targetPort                      | 边终点连接的连接桩                                                                        | string                | -         |      |\n| transform                       | transform 属性允许你旋转、缩放、倾斜或平移给定边                                          | string                | -         |      |\n| transformOrigin                 | 旋转与缩放中心，也称作变换中心                                                            | string                | -         |      |\n| visibility                      | 边是否可见                                                                                | `visible` \\| `hidden` | `visible` |      |\n| zIndex                          | 边渲染层级                                                                                | number                | 1         |      |\n\n#### PointerEvents\n\n`pointerEvents` 属性控制图形如何响应交互事件，可参考 [MDN 文档](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events)。\n\n可选值有：`visible` | `visiblepainted` | `visiblestroke` | `non-transparent-pixel` | `visiblefill` | `visible` | `painted` | `fill` | `stroke` | `all` | `none` | `auto` | `inherit` | `initial` | `unset`\n\n简而言之，`stroke` 和 `visibility` 都可以独立或组合影响拾取判定行为。目前支持以下关键词：\n\n- **`auto`**：默认值，等同于 `visiblepainted`\n- **`none`**：永远不会成为响应事件的目标\n- **`visiblepainted`**：满足以下条件才会响应事件：\n  - `visibility` 设置为 `visible`，即图形为可见的\n  - 在图形描边区域触发同时 `stroke` 取非 `none` 的值\n- **`visiblestroke`**：满足以下条件才会响应事件：\n  - `visibility` 设置为 `visible`，即图形为可见的\n  - 在图形描边区域触发，不受 `stroke` 取值的影响\n- **`visible`**：满足以下条件才会响应事件：\n  - `visibility` 设置为 `visible`，即图形为可见的\n  - 在图形描边区域触发，不受 `stroke` 取值的影响\n- **`painted`**：满足以下条件才会响应事件：\n  - 在图形描边区域触发同时 `stroke` 取非 `none` 的值\n  - 不受 `visibility` 取值的影响\n- **`stroke`**：满足以下条件才会响应事件：\n  - 在图形描边区域触发，不受 `stroke` 取值的影响\n  - 不受 `visibility` 取值的影响\n- **`all`**：只要进入图形的描边区域就会响应事件，不会受 `stroke`、`visibility` 的取值影响\n\n**使用示例：**\n\n```js\n// 示例1：只有描边区域响应事件\nconst graph = new Graph({\n  edge: {\n    style: {\n      stroke: '#000',\n      lineWidth: 2,\n      pointerEvents: 'stroke', // 只有描边响应事件\n    },\n  },\n});\n\n// 示例2：完全不响应事件\nconst graph = new Graph({\n  edge: {\n    style: {\n      pointerEvents: 'none', // 边不响应任何事件\n    },\n  },\n});\n```\n\n#### Cursor\n\n可选值有：`auto` | `default` | `none` | `context-menu` | `help` | `pointer` | `progress` | `wait` | `cell` | `crosshair` | `text` | `vertical-text` | `alias` | `copy` | `move` | `no-drop` | `not-allowed` | `grab` | `grabbing` | `all-scroll` | `col-resize` | `row-resize` | `n-resize` | `e-resize` | `s-resize` | `w-resize` | `ne-resize` | `nw-resize` | `se-resize` | `sw-resize` | `ew-resize` | `ns-resize` | `nesw-resize` | `nwse-resize` | `zoom-in` | `zoom-out`\n\n### 标签样式\n\n标签用于显示边的文本信息，支持多种样式配置和布局方式。以下是常见的使用场景：\n\n#### 基础文本标签\n\n最简单的文本标签配置：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 120,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 60 } },\n      { id: 'node2', style: { x: 180, y: 60 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      labelText: '边标签',\n      labelFill: '#262626',\n      labelFontSize: 12,\n      labelPlacement: 'center',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 多行文本标签\n\n当文本较长时，可以设置自动换行：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 120,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 60 } },\n      { id: 'node2', style: { x: 180, y: 60 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      labelText: '这是一个很长的边标签需要换行显示',\n      labelWordWrap: true,\n      labelMaxWidth: '200%',\n      labelMaxLines: 2,\n      labelTextOverflow: 'ellipsis',\n      labelFill: '#434343',\n      labelPlacement: 'center',\n      labelTextAlign: 'center',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 带背景的标签\n\n为标签添加背景，提高可读性：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 120,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 60 } },\n      { id: 'node2', style: { x: 180, y: 60 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      labelText: '重要连接',\n      labelBackground: true,\n      labelBackgroundFill: 'rgba(250, 140, 22, 0.1)',\n      labelBackgroundRadius: 6,\n      labelPadding: [4, 8],\n      labelFill: '#D4380D',\n      labelFontWeight: 'bold',\n      labelPlacement: 'center',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 自动旋转标签\n\n标签可以自动旋转以保持与边方向一致：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 120,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 30 } },\n      { id: 'node2', style: { x: 180, y: 90 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      labelText: '自动旋转',\n      labelAutoRotate: true, // 自动旋转\n      labelFill: '#1890FF',\n      labelFontWeight: 'bold',\n      labelPlacement: 'center',\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的标签样式配置：\n\n| 属性                     | 描述                                                                             | 类型                                                                        | 默认值    | 必选 |\n| ------------------------ | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------- | ---- |\n| label                    | 边标签是否显示                                                                   | boolean                                                                     | true      |      |\n| labelAutoRotate          | 边标签是否自动旋转，保持与边的方向一致                                           | boolean                                                                     | true      |      |\n| labelCursor              | 鼠标移入边标签时显示的样式，[配置项](#cursor)                                    | string                                                                      | `default` |      |\n| labelFill                | 边标签文字颜色                                                                   | string                                                                      | -         |      |\n| labelFontFamily          | 边标签字体族                                                                     | string                                                                      | -         |      |\n| labelFontSize            | 边标签字体大小                                                                   | number                                                                      | 12        |      |\n| labelFontStyle           | 边标签字体样式                                                                   | `normal` \\| `italic` \\| `oblique`                                           | -         |      |\n| labelFontVariant         | 边标签字体变种                                                                   | `normal` \\| `small-caps` \\| string                                          | -         |      |\n| labelFontWeight          | 边标签字体粗细                                                                   | `normal` \\| `bold` \\| `bolder` \\| `lighter` \\| number                       | -         |      |\n| labelLeading             | 行间距                                                                           | number                                                                      | 0         |      |\n| labelLetterSpacing       | 边标签字间距                                                                     | number \\| string                                                            | -         |      |\n| labelLineHeight          | 边标签行高                                                                       | number \\| string                                                            | -         |      |\n| labelMaxLines            | 边标签最大行数                                                                   | number                                                                      | 1         |      |\n| labelMaxWidth            | 边标签最大宽度，[配置项](#labelmaxwidth)                                         | number \\| string                                                            | `200%`    |      |\n| labelOffsetX             | 边标签在 x 轴方向上的偏移量                                                      | number                                                                      | 0         |      |\n| labelOffsetY             | 边标签在 y 轴方向上的偏移量                                                      | number                                                                      | 0         |      |\n| labelPadding             | 边标签内边距                                                                     | number \\| number[]                                                          | 0         |      |\n| labelPlacement           | 边标签相对于边的位置，[配置项](#labelplacement)                                  | string \\| number                                                            | `center`  |      |\n| labelText                | 边标签文字内容                                                                   | `string` \\| `(datum) => string`                                             | -         |      |\n| labelTextAlign           | 边标签文本水平对齐方式                                                           | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`               | `left`    |      |\n| labelTextBaseline        | 边标签文本基线                                                                   | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom` | -         |      |\n| labelTextDecorationColor | 边标签文本装饰线颜色                                                             | string                                                                      | -         |      |\n| labelTextDecorationLine  | 边标签文本装饰线                                                                 | string                                                                      | -         |      |\n| labelTextDecorationStyle | 边标签文本装饰线样式                                                             | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                       | -         |      |\n| labelTextOverflow        | 边标签文本溢出处理方式                                                           | `clip` \\| `ellipsis` \\| string                                              | -         |      |\n| labelTextPath            | 边标签文本路径                                                                   | Path                                                                        | -         |      |\n| labelWordWrap            | 边标签是否开启自动折行。开启 labelWordWrap 后，超出 labelMaxWidth 的部分自动换行 | boolean                                                                     | false     |      |\n| labelZIndex              | 边标签渲染层级                                                                   | number                                                                      | 0         |      |\n\n#### LabelPlacement\n\n边标签相对于边的位置，可以设置为：\n\n- `start`：标签位于边的起始位置\n- `center`：标签位于边的中心位置（默认）\n- `end`：标签位于边的结束位置\n- `number`：取值范围为 0-1，表示标签在边上的具体位置比例，0 为起始位置，1 为结束位置\n\n#### LabelMaxWidth\n\n开启自动折行 `labelWordWrap` 后，超出该宽度则换行:\n\n- string: 表示以相对于边长度的百分比形式定义最大宽度。例如 `50%` 表示标签宽度不超过边长度的一半\n- number: 表示以像素值为单位定义最大宽度。例如 100 表示标签的最大宽度为 100 像素\n\n比如，设置多行标签文字：\n\n```json\n{\n  \"labelWordWrap\": true,\n  \"labelMaxWidth\": 200,\n  \"labelMaxLines\": 3\n}\n```\n\n### 标签背景样式\n\n标签背景用于显示边标签的背景：\n\n| 属性                          | 描述                                                                                                         | 类型                                     | 默认值    |\n| ----------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------- | --------- |\n| labelBackground               | 边标签背景是否显示                                                                                           | boolean                                  | false     |\n| labelBackgroundCursor         | 边标签背景鼠标移入样式，[配置项](#cursor)                                                                    | string                                   | `default` |\n| labelBackgroundFill           | 边标签背景填充色                                                                                             | string                                   | -         |\n| labelBackgroundFillOpacity    | 边标签背景透明度                                                                                             | number                                   | 1         |\n| labelBackgroundHeight         | 边标签背景高度                                                                                               | string \\| number                         | -         |\n| labelBackgroundLineDash       | 边标签背景虚线配置                                                                                           | number \\| string \\|(number \\| string )[] | -         |\n| labelBackgroundLineDashOffset | 边标签背景虚线偏移量                                                                                         | number                                   | -         |\n| labelBackgroundLineWidth      | 边标签背景描边线宽                                                                                           | number                                   | -         |\n| labelBackgroundRadius         | 边标签背景圆角半径 <br> - number: 统一设置四个圆角半径 <br> - number[]: 分别设置四个圆角半径，不足则自动补充 | number \\| number[]                       | 0         |\n| labelBackgroundShadowBlur     | 边标签背景阴影模糊程度                                                                                       | number                                   | -         |\n| labelBackgroundShadowColor    | 边标签背景阴影颜色                                                                                           | string                                   | -         |\n| labelBackgroundShadowOffsetX  | 边标签背景阴影 X 方向偏移                                                                                    | number                                   | -         |\n| labelBackgroundShadowOffsetY  | 边标签背景阴影 Y 方向偏移                                                                                    | number                                   | -         |\n| labelBackgroundStroke         | 边标签背景描边颜色                                                                                           | string                                   | -         |\n| labelBackgroundStrokeOpacity  | 边标签背景描边透明度                                                                                         | number \\| string                         | 1         |\n| labelBackgroundVisibility     | 边标签背景是否可见                                                                                           | `visible` \\| `hidden`                    | -         |\n| labelBackgroundZIndex         | 边标签背景渲染层级                                                                                           | number                                   | 1         |\n\n### 光晕样式\n\n光晕是围绕边主图形显示的效果，通常用于高亮显示或表示边的特殊状态。\n\n#### 基础光晕效果\n\n为边添加基本的光晕效果：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 50 } },\n      { id: 'node2', style: { x: 180, y: 50 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      lineWidth: 2,\n      halo: true,\n      haloStroke: '#1890FF',\n      haloLineWidth: 6,\n      haloStrokeOpacity: 0.3,\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的光晕样式配置：\n\n| 属性              | 描述                                                 | 类型                   | 默认值                         | 必选 |\n| ----------------- | ---------------------------------------------------- | ---------------------- | ------------------------------ | ---- |\n| halo              | 边光晕是否显示                                       | boolean                | false                          |      |\n| haloCursor        | 边光晕鼠标移入样式，[配置项](#cursor)                | string                 | `default`                      |      |\n| haloDraggable     | 边光晕是否允许拖拽                                   | boolean                | true                           |      |\n| haloDroppable     | 边光晕是否允许接收被拖拽的元素                       | boolean                | true                           |      |\n| haloFillRule      | 边光晕填充规则                                       | `nonzero` \\| `evenodd` | -                              |      |\n| haloFilter        | 边光晕滤镜                                           | string                 | -                              |      |\n| haloLineWidth     | 边光晕描边宽度                                       | number                 | 3                              |      |\n| haloPointerEvents | 边光晕效果是否响应指针事件，[配置项](#pointerevents) | string                 | `none`                         |      |\n| haloStroke        | 边光晕描边色，**此属性用于设置边周围光晕的颜色**     | string                 | 与主图形的描边色 `stroke` 一致 |      |\n| haloStrokeOpacity | 边光晕描边色透明度                                   | number                 | 0.25                           |      |\n| haloVisibility    | 边光晕可见性                                         | `visible` \\| `hidden`  | `visible`                      |      |\n| haloZIndex        | 边光晕渲染层级                                       | number                 | -1                             |      |\n\n### 箭头样式\n\n边支持在起始端和结束端添加箭头，用于表示边的方向性。\n\n#### 基础箭头\n\n为边的结束端添加基本箭头：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 50 } },\n      { id: 'node2', style: { x: 180, y: 50 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      stroke: '#1890FF',\n      lineWidth: 2,\n      endArrow: true, // 结束端箭头\n      endArrowType: 'vee', // 箭头类型\n      endArrowSize: 10, // 箭头大小\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 双向箭头\n\n为边的两端都添加箭头：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 50 } },\n      { id: 'node2', style: { x: 180, y: 50 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      stroke: '#52C41A',\n      lineWidth: 2,\n      startArrow: true, // 起始端箭头\n      startArrowType: 'circle',\n      startArrowSize: 8,\n      endArrow: true, // 结束端箭头\n      endArrowType: 'triangle',\n      endArrowSize: 10,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 自定义箭头样式\n\n自定义箭头的颜色和类型：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 240,\n  height: 100,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 60, y: 50 } },\n      { id: 'node2', style: { x: 180, y: 50 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  edge: {\n    style: {\n      stroke: '#722ED1',\n      lineWidth: 3,\n      endArrow: true,\n      endArrowType: 'diamond', // 菱形箭头\n      endArrowSize: 12,\n      endArrowFill: '#FF4D4F', // 红色箭头填充\n      endArrowStroke: '#722ED1', // 箭头描边颜色\n      endArrowStrokeOpacity: 0.8,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 起始箭头样式配置\n\n| 属性                    | 描述                                      | 类型                                                                                 | 默认值             | 必选 |\n| ----------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------ | ------------------ | ---- |\n| startArrow              | 边起始箭头是否显示                        | boolean                                                                              | false              |      |\n| startArrowCursor        | 边起始箭头鼠标移入样式，[配置项](#cursor) | string                                                                               | `default`          |      |\n| startArrowFill          | 边起始箭头填充颜色                        | string                                                                               | 默认与边的颜色一致 |      |\n| startArrowFillOpacity   | 边起始箭头填充透明度                      | number                                                                               | 1                  |      |\n| startArrowOffset        | 边起始箭头的偏移量                        | number                                                                               | 0                  |      |\n| startArrowSize          | 边起始箭头大小                            | number \\| [number, number]                                                           | 10                 |      |\n| startArrowStroke        | 边起始箭头描边颜色                        | string                                                                               | 默认与边的颜色一致 |      |\n| startArrowStrokeOpacity | 边起始箭头描边透明度                      | number                                                                               | 1                  |      |\n| startArrowType          | 边起始箭头类型                            | `triangle` \\| `circle` \\| `diamond` \\| `vee` \\| `rect` \\| `triangleRect` \\| `simple` | `vee`              |      |\n\n#### 结束箭头样式配置\n\n| 属性                  | 描述                                      | 类型                                                                                 | 默认值             | 必选 |\n| --------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------ | ------------------ | ---- |\n| endArrow              | 边结束箭头是否显示                        | boolean                                                                              | false              |      |\n| endArrowCursor        | 边结束箭头鼠标移入样式，[配置项](#cursor) | string                                                                               | `default`          |      |\n| endArrowFill          | 边结束箭头填充颜色                        | string                                                                               | 默认与边的颜色一致 |      |\n| endArrowFillOpacity   | 边结束箭头填充透明度                      | number                                                                               | 1                  |      |\n| endArrowOffset        | 边结束箭头的偏移量                        | number                                                                               | 0                  |      |\n| endArrowSize          | 边结束箭头大小                            | number \\| [number, number]                                                           | 10                 |      |\n| endArrowStroke        | 边结束箭头描边颜色                        | string                                                                               | 默认与边的颜色一致 |      |\n| endArrowStrokeOpacity | 边结束箭头描边透明度                      | number                                                                               | 1                  |      |\n| endArrowType          | 边结束箭头类型                            | `triangle` \\| `circle` \\| `diamond` \\| `vee` \\| `rect` \\| `triangleRect` \\| `simple` | `vee`              |      |\n\n### 自环边样式\n\n自环边是指起始节点和结束节点为同一个节点的特殊边。\n\n#### 基础自环边\n\n创建基本的自环边：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  data: {\n    nodes: [{ id: 'node1', style: { x: 100, y: 50 } }],\n    edges: [{ source: 'node1', target: 'node1' }],\n  },\n  edge: {\n    style: {\n      stroke: '#1890FF',\n      lineWidth: 2,\n      endArrow: true,\n      loopPlacement: 'top', // 自环位置\n      loopDist: 30, // 自环大小\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 多个自环边\n\n为同一节点创建多个不同位置的自环边：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 120,\n  data: {\n    nodes: [{ id: 'node1', style: { x: 100, y: 60 } }],\n    edges: [\n      { id: 'edge1', source: 'node1', target: 'node1' },\n      { id: 'edge2', source: 'node1', target: 'node1' },\n      { id: 'edge3', source: 'node1', target: 'node1' },\n    ],\n  },\n  edge: {\n    style: {\n      lineWidth: 2,\n      endArrow: true,\n      loopPlacement: (datum) => {\n        const placements = ['top', 'right', 'bottom'];\n        return placements[parseInt(datum.id.slice(-1)) - 1];\n      },\n      loopDist: 25,\n      stroke: (datum) => {\n        const colors = ['#1890FF', '#52C41A', '#722ED1'];\n        return colors[parseInt(datum.id.slice(-1)) - 1];\n      },\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的自环边样式配置：\n\n| 属性          | 描述                                           | 类型                                                                                                                                                                   | 默认值                 | 必选 |\n| ------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ---- |\n| loop          | 是否启用自环边                                 | boolean                                                                                                                                                                | true                   |      |\n| loopClockwise | 指定是否顺时针绘制环                           | boolean                                                                                                                                                                | true                   |      |\n| loopDist      | 从节点边缘到自环顶部的距离，用于指定自环的曲率 | number                                                                                                                                                                 | 默认为节点尺寸的最大值 |      |\n| loopPlacement | 自环边的位置                                   | `left` \\| `right` \\| `top` \\| `bottom` \\| `left-top` \\| `left-bottom` \\| `right-top` \\| `right-bottom` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right` | `top`                  |      |\n\n## State\n\n在一些交互行为中，比如点击选中一个边或鼠标悬停激活一个边，仅仅是在该元素做了某些状态的标识。为了将这些状态反应到终端用户所见的视觉空间中，我们需要为不同的状态设置不同的图元素样式，以响应该图元素状态的变化。\n\nG6 提供了几种内置的状态，包括选中（selected）、高亮（highlight）、激活（active）、不活跃（inactive）和禁用（disabled）。此外，它还支持自定义状态，以满足更特定的需求。对于每个状态，开发者可以定义一套样式规则，这些规则会覆盖元素的默认样式。\n\n<img width=\"520\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ebBlTpKu2WUAAAAAAAAAAAAADmJ7AQ/original\" />\n\n数据结构如下：\n\n```typescript\ntype EdgeState = {\n  [state: string]: EdgeStyle;\n};\n```\n\n例如，当边处于 `focus` 状态时，可以为其添加一个宽度为 6 且颜色为橙色的光晕。\n\n```js {4-9}\nconst graph = new Graph({\n  edge: {\n    state: {\n      focus: {\n        halo: true,\n        haloLineWidth: 6,\n        haloStroke: 'orange',\n        haloStrokeOpacity: 0.6,\n      },\n    },\n  },\n});\n```\n\n效果如下图所示：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 100,\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }],\n    edges: [{ source: 'node1', target: 'node2', states: ['focus'] }],\n  },\n  edge: {\n    state: {\n      focus: {\n        halo: true,\n        haloLineWidth: 6,\n        haloStroke: 'orange',\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n    cols: 2,\n  },\n});\n\ngraph.render();\n```\n\n## Animation\n\n定义边的动画效果，支持下列两种配置方式：\n\n1. 关闭边全部动画\n\n```json\n{\n  \"edge\": {\n    \"animation\": false\n  }\n}\n```\n\n2. 配置阶段动画\n\n阶段动画是指边在进入画布、更新、离开画布时的动画效果。目前支持的阶段包括：\n\n- `enter`: 边进入画布时的动画\n- `update`: 边更新时的动画\n- `exit`: 边离开画布时的动画\n- `show`: 边从隐藏状态显示时的动画\n- `hide`: 边隐藏时的动画\n- `collapse`: 边收起时的动画\n- `expand`: 边展开时的动画\n\n你可以参考 [动画范式](/manual/animation/animation#动画范式) 使用动画语法来配置边，如：\n\n```json\n{\n  \"edge\": {\n    \"animation\": {\n      \"update\": [\n        {\n          \"fields\": [\"stroke\"], // 更新时只对 stroke 属性进行动画\n          \"duration\": 1000, // 动画持续时间\n          \"easing\": \"linear\" // 缓动函数\n        }\n      ]\n    }\n  }\n}\n```\n\n也可以使用内置的动画效果：\n\n```json\n{\n  \"edge\": {\n    \"animation\": {\n      \"enter\": \"fade\", // 使用渐变动画\n      \"update\": \"path-in\", // 使用路径动画\n      \"exit\": \"fade\" // 使用渐变动画\n    }\n  }\n}\n```\n\n你可以传入 false 来关闭特定阶段的动画：\n\n```json\n{\n  \"edge\": {\n    \"animation\": {\n      \"enter\": false // 关闭边入场动画\n    }\n  }\n}\n```\n\n## Palette\n\n定义边的色板，即预定义边颜色池，并根据规则进行分配，将颜色映射到 `stroke` 属性。\n\n> 有关色板的定义，请参考 [色板](/manual/theme/palette)。\n\n| 属性   | 描述                                                                | 类型                          | 默认值  |\n| ------ | ------------------------------------------------------------------- | ----------------------------- | ------- |\n| color  | 色板颜色。如果色板注册过，可以直接指定其注册名，也接受一个颜色数组  | string \\| string[]            | -       |\n| field  | 指定元素数据中的分组字段。若不指定，默认取 id 作为分组字段          | string \\| ((datum) => string) | `id`    |\n| invert | 是否反转色板                                                        | boolean                       | false   |\n| type   | 指定当前色板类型。<br> - `group`: 离散色板 <br> - `value`: 连续色板 | `group` \\| `value`            | `group` |\n\n如将一组数据按 `direction` 字段分配边颜色，使得同类别的边颜色相同：\n\n```json\n{\n  \"edge\": {\n    \"palette\": {\n      \"type\": \"group\",\n      \"field\": \"direction\",\n      \"color\": [\"#F08F56\", \"#00C9C9\", \"#D580FF\"]\n    }\n  }\n}\n```\n\n效果如下图所示：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  data: {\n    nodes: new Array(6).fill(0).map((_, i) => ({ id: `node-${i + 1}` })),\n    edges: [\n      { source: 'node-1', target: 'node-2', data: { direction: 'out' } },\n      { source: 'node-1', target: 'node-3', data: { direction: 'out' } },\n      { source: 'node-1', target: 'node-4', data: { direction: 'out' } },\n      { source: 'node-5', target: 'node-1', data: { direction: 'in' } },\n      { source: 'node-6', target: 'node-1', data: { direction: 'in' } },\n    ],\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 120,\n    linkDistance: 120,\n  },\n  edge: {\n    style: {\n      endArrow: true,\n    },\n    palette: {\n      type: 'group',\n      field: 'direction',\n      color: ['#F08F56', '#00C9C9'],\n    },\n  },\n});\n\ngraph.render();\n```\n\n也可以使用默认配置：\n\n```json\n{\n  \"edge\": {\n    \"palette\": \"tableau\" // tableau 为色板名，默认根据 ID 分配颜色\n  }\n}\n```\n\n效果如下图所示：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  data: {\n    nodes: new Array(6).fill(0).map((_, i) => ({ id: `node-${i + 1}` })),\n    edges: [\n      { source: 'node-1', target: 'node-2', data: { direction: 'out' } },\n      { source: 'node-1', target: 'node-3', data: { direction: 'out' } },\n      { source: 'node-1', target: 'node-4', data: { direction: 'out' } },\n      { source: 'node-5', target: 'node-1', data: { direction: 'in' } },\n      { source: 'node-6', target: 'node-1', data: { direction: 'in' } },\n    ],\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 120,\n    linkDistance: 120,\n  },\n  edge: {\n    style: {\n      endArrow: true,\n    },\n    palette: 'tableau',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/Cubic.en.md",
    "content": "---\ntitle: Cubic Bezier Curve Edge\norder: 2\n---\n\n## Overview\n\nA cubic Bezier curve is a versatile smooth curve with control points that can be freely distributed, suitable for connecting nodes in any direction.\n\nUse cases:\n\n- Suitable for graphs with any layout, such as network graphs and relationship graphs.\n\n- Use when smooth node connections are needed without specific directional requirements.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/edges/cubic.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseEdge](/en/manual/element/edge/base-edge)\n\n| Attribute     | Description                                                                                                                                                 | Type                               | Default | Required |\n| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------- | -------- |\n| controlPoints | Array of control points used to define the shape of the curve. If not specified, control points will be calculated using `curveOffset` and `curvePosition`. | [[Point](#point), [Point](#point)] | -       |          |\n| curvePosition | Relative position of the control point on the line connecting the two endpoints, ranging from `0-1`.                                                        | number &#124; number[]             | 0.5     |          |\n| curveOffset   | Distance of the control point from the line connecting the two endpoints, understood as the degree of curve bending.                                        | number &#124; number[]             | 20      |          |\n\n#### Point\n\n```typescript\ntype Point = [number, number] | [number, number, number] | Float32Array;\n```\n\n## Example\n\n### Built-in Cubic Bezier Curve Edge Effect\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'cubic',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      badge: true,\n      badgeText: '\\ue603',\n      badgeFontFamily: 'iconfont',\n      badgeBackgroundWidth: 12,\n      badgeBackgroundHeight: 12,\n    },\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 220,\n    linkDistance: 220,\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/Cubic.zh.md",
    "content": "---\ntitle: 三次贝塞尔曲线边 Cubic\norder: 2\n---\n\n## 概述\n\n三次贝塞尔曲线是一种通用的平滑曲线，其控制点可以自由分布，适合连接任意方向的节点。\n\n使用场景：\n\n- 适用于任意布局的图，如网络图、关系图。\n\n- 当需要平滑连接节点且无特定方向要求时使用。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/edges/cubic.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseEdge](/manual/element/edge/base-edge)\n\n| 属性          | 描述                                                                                               | 类型                               | 默认值 | 必选 |\n| ------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------- | ------ | ---- |\n| controlPoints | 控制点数组，用于定义曲线的形状。如果不指定，将会通过 `curveOffset` 和 `curvePosition` 来计算控制点 | [[Point](#point), [Point](#point)] | -      |      |\n| curvePosition | 控制点在两端点连线上的相对位置，范围为`0-1`                                                        | number &#124; number[]             | 0.5    |      |\n| curveOffset   | 控制点距离两端点连线的距离，可理解为控制边的弯曲程度                                               | number &#124; number[]             | 20     |      |\n\n#### Point\n\n```typescript\ntype Point = [number, number] | [number, number, number] | Float32Array;\n```\n\n## 示例\n\n### 内置三次贝塞尔曲线边效果\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'cubic',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      badge: true,\n      badgeText: '\\ue603',\n      badgeFontFamily: 'iconfont',\n      badgeBackgroundWidth: 12,\n      badgeBackgroundHeight: 12,\n    },\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 220,\n    linkDistance: 220,\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/CubicHorizontal.en.md",
    "content": "---\ntitle: CubicHorizontal Bezier Curve Edge\norder: 3\n---\n\n## Overview\n\nThe horizontal cubic Bezier curve is a smooth curve with control points primarily distributed along the horizontal direction, suitable for connecting nodes horizontally.\n\nUse cases:\n\n- Suitable for horizontally laid-out graphs, such as flowcharts and hierarchical diagrams.\n\n- Use when emphasizing horizontal connections is needed.\n\n> Note: When calculating control points, the distance on the x-axis is primarily considered, ignoring changes on the y-axis.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/edges/cubic-horizontal.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseEdge](/en/manual/element/edge/base-edge)\n\n| Attribute     | Description                                                                                                          | Type                   | Default   | Required |\n| ------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------- | --------- | -------- |\n| curvePosition | Relative position of the control point on the line connecting the two endpoints, ranging from `0-1`.                 | number &#124; number[] | [0.5,0.5] |          |\n| curveOffset   | Distance of the control point from the line connecting the two endpoints, understood as the degree of curve bending. | number &#124; number[] | [0,0]     |          |\n\n## Example\n\n### Built-in Horizontal Cubic Bezier Curve Edge Effect\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      port: true,\n      ports: [{ placement: 'right' }, { placement: 'left' }],\n    },\n  },\n  edge: {\n    type: 'cubic-horizontal',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'antv-dagre',\n    rankdir: 'LR',\n    nodesep: 20,\n    ranksep: 120,\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/CubicHorizontal.zh.md",
    "content": "---\ntitle: 水平三次贝塞尔曲线边 CubicHorizontal\norder: 3\n---\n\n## 概述\n\n水平三次贝塞尔曲线是一种平滑的曲线，其控制点主要沿水平方向分布，适合在水平方向上连接节点。\n\n使用场景：\n\n- 适用于水平布局的图，如流程图、层次结构图。\n\n- 当需要强调水平方向的连接关系时使用。\n\n> 特别注意，计算控制点时主要考虑 x 轴上的距离，忽略 y 轴的变化\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/edges/cubic-horizontal.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseEdge](/manual/element/edge/base-edge)\n\n| 属性          | 描述                                                 | 类型                   | 默认值    | 必选 |\n| ------------- | ---------------------------------------------------- | ---------------------- | --------- | ---- |\n| curvePosition | 控制点在两端点连线上的相对位置，范围为`0-1`          | number &#124; number[] | [0.5,0.5] |      |\n| curveOffset   | 控制点距离两端点连线的距离，可理解为控制边的弯曲程度 | number &#124; number[] | [0,0]     |      |\n\n## 示例\n\n### 内置水平三次贝塞尔曲线边效果\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      port: true,\n      ports: [{ placement: 'right' }, { placement: 'left' }],\n    },\n  },\n  edge: {\n    type: 'cubic-horizontal',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'antv-dagre',\n    rankdir: 'LR',\n    nodesep: 20,\n    ranksep: 120,\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/CubicVertical.en.md",
    "content": "---\ntitle: CubicVertical Bezier Curve Edge\norder: 4\n---\n\n## Overview\n\nThe vertical cubic Bezier curve is a smooth curve with control points primarily distributed along the vertical direction, suitable for connecting nodes vertically.\n\nUse cases:\n\n- Suitable for vertically laid-out graphs, such as organizational charts and tree diagrams.\n\n- Use when emphasizing vertical connections is needed.\n\n**Note: When calculating control points, the distance on the y-axis is primarily considered, ignoring changes on the x-axis.**\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/edges/cubic-vertical.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseEdge](/en/manual/element/edge/base-edge)\n\n| Attribute     | Description                                                                                                          | Type                   | Default   | Required |\n| ------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------- | --------- | -------- |\n| curvePosition | Relative position of the control point on the line connecting the two endpoints, ranging from `0-1`.                 | number &#124; number[] | [0.5,0.5] |          |\n| curveOffset   | Distance of the control point from the line connecting the two endpoints, understood as the degree of curve bending. | number &#124; number[] | [0,0]     |          |\n\n## Example\n\n### Built-in Vertical Cubic Bezier Curve Edge Effect\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      port: true,\n      ports: [{ placement: 'top' }, { placement: 'bottom' }],\n    },\n  },\n  edge: {\n    type: 'cubic-vertical',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'antv-dagre',\n    begin: [50, 50],\n    rankdir: 'TB',\n    nodesep: 20,\n    ranksep: 120,\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/CubicVertical.zh.md",
    "content": "---\ntitle: 垂直三次贝塞尔曲线边 CubicVertical\norder: 4\n---\n\n## 概述\n\n垂直三次贝塞尔曲线是一种平滑的曲线，其控制点主要沿垂直方向分布，适合在垂直方向上连接节点。\n\n使用场景：\n\n- 适用于垂直布局的图，如组织结构图、树状图。\n\n- 当需要强调垂直方向的连接关系时使用。\n\n**特别注意，计算控制点时主要考虑 y 轴上的距离，忽略 x 轴的变化**\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/edges/cubic-vertical.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseEdge](/manual/element/edge/base-edge)\n\n| 属性          | 描述                                                 | 类型                   | 默认值    | 必选 |\n| ------------- | ---------------------------------------------------- | ---------------------- | --------- | ---- |\n| curvePosition | 控制点在两端点连线上的相对位置，范围为`0-1`          | number &#124; number[] | [0.5,0.5] |      |\n| curveOffset   | 控制点距离两端点连线的距离，可理解为控制边的弯曲程度 | number &#124; number[] | [0,0]     |      |\n\n## 示例\n\n### 内置垂直三次贝塞尔曲线边效果\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      port: true,\n      ports: [{ placement: 'top' }, { placement: 'bottom' }],\n    },\n  },\n  edge: {\n    type: 'cubic-vertical',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'antv-dagre',\n    begin: [50, 50],\n    rankdir: 'TB',\n    nodesep: 20,\n    ranksep: 120,\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/Line.en.md",
    "content": "---\ntitle: Line Edge\norder: 5\n---\n\n## Overview\n\nA line is the simplest type of edge, directly connecting two nodes without any curvature.\n\nUse cases:\n\n- Suitable for simple graphs, such as topology diagrams and flowcharts.\n\n- Use when quick drawing is needed without complex visual effects.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/edges/line.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseEdge](/en/manual/element/edge/base-edge)\n\n## Example\n\n### Built-in Line Edge Effect\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'line',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      badge: true,\n      badgeText: '\\ue603',\n      badgeFontFamily: 'iconfont',\n      badgeBackgroundWidth: 12,\n      badgeBackgroundHeight: 12,\n    },\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 220,\n    linkDistance: 220,\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/Line.zh.md",
    "content": "---\ntitle: 直线边 Line\norder: 5\n---\n\n## 概述\n\n直线是最简单的边类型，直接连接两个节点，没有任何弯曲。\n\n使用场景：\n\n- 适用于简单的图，如拓扑图、流程图。\n\n- 当需要快速绘制且无需复杂视觉效果时使用。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/edges/line.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseEdge](/manual/element/edge/base-edge)\n\n## 示例\n\n### 内置直线边效果\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'line',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      badge: true,\n      badgeText: '\\ue603',\n      badgeFontFamily: 'iconfont',\n      badgeBackgroundWidth: 12,\n      badgeBackgroundHeight: 12,\n    },\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 220,\n    linkDistance: 220,\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/Polyline.en.md",
    "content": "---\ntitle: Polyline Edge\norder: 6\n---\n\n## Overview\n\nA polyline is an edge composed of multiple straight line segments, suitable for connecting nodes by bypassing obstacles in complex layouts.\n\nUse cases:\n\n- Suitable for graphs with complex layouts, such as circuit diagrams and pipeline diagrams.\n\n- Use when you need to bypass other nodes or obstacles.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/edges/polyline.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseEdge](/en/manual/element/edge/base-edge)\n\n| Attribute     | Description                                                                | Type                                                                                    | Default | Required |\n| ------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------- | -------- |\n| controlPoints | Array of control points used to define the turning points of the polyline. | [Point](#point)[]                                                                       | []      |          |\n| radius        | Corner radius of the turning points.                                       | number                                                                                  | 0       |          |\n| router        | Whether to enable routing.                                                 | false &#124; [OrthRouter](#orthrouter) &#124; [ShortestPathRouter](#shortestpathrouter) | false   |          |\n\n### OrthRouter\n\n| Attribute | Description                                                                                              | Type                | Default |\n| --------- | -------------------------------------------------------------------------------------------------------- | ------------------- | ------- |\n| type      | Orthogonal routing, adding extra control points on the path to keep each segment horizontal or vertical. | `'orth'`            | -       |\n| padding   | Minimum distance between the node connection point and the corner.                                       | [Padding](#padding) | `0`     |\n\n### ShortestPathRouter\n\n| Attribute                 | Description                                                                                                                                                                                                                                                                          | Type                                                                   | Default |\n| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | ------- |\n| type                      | Shortest path routing, an intelligent version of orthogonal routing `'orth'`. This routing consists of horizontal or vertical orthogonal segments. It uses the A\\* algorithm to calculate the shortest path and supports automatic avoidance of other nodes (obstacles) on the path. | `'shortest-path'`                                                      | -       |\n| offset                    | Minimum distance between the node anchor point and the corner.                                                                                                                                                                                                                       | [Padding](#padding)                                                    | 0       |\n| gridSize                  | Grid cell size.                                                                                                                                                                                                                                                                      | number                                                                 | 0       |\n| maxAllowedDirectionChange | Maximum allowed rotation angle (radians).                                                                                                                                                                                                                                            | number                                                                 | 0       |\n| startDirections           | Possible starting directions of the node.                                                                                                                                                                                                                                            | [Direction](#direction)[]                                              | 0       |\n| endDirections             | Possible ending directions of the node.                                                                                                                                                                                                                                              | [Direction](#direction)[]                                              | 0       |\n| directionMap              | Specifies the movable directions.                                                                                                                                                                                                                                                    | { [key in [Direction](#direction)]: { stepX: number; stepY: number } } | 0       |\n| penalties                 | Represents additional costs for certain paths during path searching. The key is the radian value, and the value is the cost.                                                                                                                                                         | { [key: string]: number }                                              | 0       |\n| distFunc                  | Specifies the function to calculate the distance between two points.                                                                                                                                                                                                                 | (p1: [Point](#point), p2: [Point](#point)) => number                   | 0       |\n| maximumLoops              | Maximum number of iterations.                                                                                                                                                                                                                                                        | number                                                                 | 0       |\n| enableObstacleAvoidance   | Whether to enable obstacle avoidance.                                                                                                                                                                                                                                                | boolean                                                                | false   |\n\n#### Direction\n\n```typescript\ntype Direction = 'left' | 'right' | 'top' | 'bottom';\n```\n\n#### Point\n\n```typescript\ntype Point = [number, number] | [number, number, number] | Float32Array;\n```\n\n#### Padding\n\n```typescript\ntype Padding = number | [number, number] | [number, number, number, number];\n```\n\n## Example\n\n### Built-in Polyline Edge Effect\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 200, y: 200 } },\n    { id: 'node-2', style: { x: 350, y: 120 } },\n  ],\n  edges: [\n    {\n      id: 'edge-1',\n      source: 'node-1',\n      target: 'node-2',\n      controlPoints: [[300, 190]],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'polyline',\n    style: {\n      controlPoints: (d) => d.controlPoints,\n    },\n  },\n  behaviors: [{ type: 'drag-element' }],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/Polyline.zh.md",
    "content": "---\ntitle: 折线边 Polyline\norder: 6\n---\n\n## 概述\n\n折线是由多条直线段组成的边，适合在复杂布局中绕过障碍物连接节点。\n\n使用场景：\n\n- 适用于复杂布局的图，如电路图、管道图。\n\n- 当需要绕过其他节点或障碍物时使用。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/edges/polyline.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseEdge](/manual/element/edge/base-edge)\n\n| 属性          | 描述                             | 类型                                                                                    | 默认值 | 必选 |\n| ------------- | -------------------------------- | --------------------------------------------------------------------------------------- | ------ | ---- |\n| controlPoints | 控制点数组，用于定义折线的转折点 | [Point](#point)[]                                                                       | []     |      |\n| radius        | 转折点圆角半径                   | number                                                                                  | 0      |      |\n| router        | 是否启用路由                     | false &#124; [OrthRouter](#orthrouter) &#124; [ShortestPathRouter](#shortestpathrouter) | false  |      |\n\n### OrthRouter\n\n| 属性    | 描述                                                                   | 类型                | 默认值 |\n| ------- | ---------------------------------------------------------------------- | ------------------- | ------ |\n| type    | 正交路由，通过在路径上添加额外的控制点，使得边的每一段都保持水平或垂直 | `'orth'`            | -      |\n| padding | 节点连接点与转角的最小距离                                             | [Padding](#padding) | `0`    |\n\n### ShortestPathRouter\n\n| 属性                      | 描述                                                                                                                                              | 类型                                                                   | 默认值 |\n| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------ |\n| type                      | 最短路径路由，是正交路由 `'orth'` 的智能版本。该路由由水平或垂直的正交线段组成。采用 A\\* 算法计算最短路径，并支持自动避开路径上的其他节点（障碍） | `'shortest-path'`                                                      | -      |\n| offset                    | 节点锚点与转角的最小距离                                                                                                                          | [Padding](#padding)                                                    | 0      |\n| gridSize                  | grid 格子大小                                                                                                                                     | number                                                                 | 0      |\n| maxAllowedDirectionChange | 支持的最大旋转角度（弧度）                                                                                                                        | number                                                                 | 0      |\n| startDirections           | 节点的可能起始方向                                                                                                                                | [Direction](#direction)[]                                              | 0      |\n| endDirections             | 节点的可能结束方向                                                                                                                                | [Direction](#direction)[]                                              | 0      |\n| directionMap              | 指定可移动的方向                                                                                                                                  | { [key in [Direction](#direction)]: { stepX: number; stepY: number } } | 0      |\n| penalties                 | 表示在路径搜索过程中某些路径的额外代价。key 为弧度值，value 为代价                                                                                | { [key: string]: number }                                              | 0      |\n| distFunc                  | 指定计算两点之间距离的函数                                                                                                                        | (p1: [Point](#point), p2: [Point](#point)) => number                   | 0      |\n| maximumLoops              | 最大迭代次数                                                                                                                                      | number                                                                 | 0      |\n| enableObstacleAvoidance   | 是否开启避障                                                                                                                                      | boolean                                                                | false  |\n\n#### Direction\n\n```typescript\ntype Direction = 'left' | 'right' | 'top' | 'bottom';\n```\n\n#### Point\n\n```typescript\ntype Point = [number, number] | [number, number, number] | Float32Array;\n```\n\n#### Padding\n\n```typescript\ntype Padding = number | [number, number] | [number, number, number, number];\n```\n\n## 示例\n\n### 内置折线边效果\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 200, y: 200 } },\n    { id: 'node-2', style: { x: 350, y: 120 } },\n  ],\n  edges: [\n    {\n      id: 'edge-1',\n      source: 'node-1',\n      target: 'node-2',\n      controlPoints: [[300, 190]],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'polyline',\n    style: {\n      controlPoints: (d) => d.controlPoints,\n    },\n  },\n  behaviors: [{ type: 'drag-element' }],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/Quadratic.en.md",
    "content": "---\ntitle: Quadratic Bezier Curve Edge\norder: 6\n---\n\n## Overview\n\nA quadratic Bezier curve is a smooth curve whose shape is determined by a start point, an end point, and a control point.\n\nUse cases:\n\n- Suitable for moderately complex graphs, such as relationship graphs and network graphs.\n\n- Use when smooth node connections are needed with limited computational resources.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/edges/quadratic.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseEdge](/en/manual/element/edge/base-edge)\n\n| Attribute     | Description                                                                                                                                                 | Type            | Default | Required |\n| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------- | -------- |\n| controlPoints | Array of control points used to define the shape of the curve. If not specified, control points will be calculated using `curveOffset` and `curvePosition`. | [Point](#point) | -       |          |\n| curvePosition | Relative position of the control point on the line connecting the two endpoints, ranging from `0-1`.                                                        | number          | 0.5     |          |\n| curveOffset   | Distance of the control point from the line connecting the two endpoints, understood as the degree of curve bending.                                        | number          | 30      |          |\n\n#### Point\n\n```typescript\ntype Point = [number, number] | [number, number, number] | Float32Array;\n```\n\n## Example\n\n### Built-in Quadratic Bezier Curve Edge Effect\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'quadratic',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      badge: true,\n      badgeText: '\\ue603',\n      badgeFontFamily: 'iconfont',\n      badgeBackgroundWidth: 12,\n      badgeBackgroundHeight: 12,\n    },\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 220,\n    linkDistance: 220,\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/Quadratic.zh.md",
    "content": "---\ntitle: 二次贝塞尔曲线边 Quadratic\norder: 6\n---\n\n## 概述\n\n二次贝塞尔曲线是一种平滑的曲线，其形状由起点、终点和一个控制点决定。\n\n使用场景：\n\n- 适用于中等复杂度的图，如关系图、网络图。\n\n- 当需要平滑连接节点且计算资源有限时使用。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/edges/quadratic.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseEdge](/manual/element/edge/base-edge)\n\n| 属性          | 描述                                                                                               | 类型            | 默认值 | 必选 |\n| ------------- | -------------------------------------------------------------------------------------------------- | --------------- | ------ | ---- |\n| controlPoints | 控制点数组，用于定义曲线的形状。如果不指定，将会通过 `curveOffset` 和 `curvePosition` 来计算控制点 | [Point](#point) | -      |      |\n| curvePosition | 控制点在两端点连线上的相对位置，范围为`0-1`                                                        | number          | 0.5    |      |\n| curveOffset   | 控制点距离两端点连线的距离，可理解为控制边的弯曲程度                                               | number          | 30     |      |\n\n#### Point\n\n```typescript\ntype Point = [number, number] | [number, number, number] | Float32Array;\n```\n\n## 示例\n\n### 内置二次贝塞尔曲线边效果\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'quadratic',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      badge: true,\n      badgeText: '\\ue603',\n      badgeFontFamily: 'iconfont',\n      badgeBackgroundWidth: 12,\n      badgeBackgroundHeight: 12,\n    },\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 220,\n    linkDistance: 220,\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/custom-edge.en.md",
    "content": "---\ntitle: Custom Edge\norder: 7\n---\n\nG6 provides multiple [built-in edge](/en/manual/element/edge/base-edge) types, including [line](/en/manual/element/edge/line), [polyline](/en/manual/element/edge/polyline), [quadratic (quadratic Bézier curve edge)](/en/manual/element/edge/quadratic), [cubic (cubic Bézier curve edge)](/en/manual/element/edge/cubic), [cubic-horizontal](/en/manual/element/edge/cubic-horizontal), [cubic-vertical](/en/manual/element/edge/cubic-vertical), and more. These built-in edges can meet most basic scenario requirements.\n\nHowever, in actual projects, you may encounter requirements that these basic edges cannot satisfy. In such cases, you need to create custom edges. Don't worry, it's simpler than you think!\n\n## Before Starting: Understanding the Basic Components of an Edge\n\nIn G6, a complete edge typically consists of the following parts:\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YKN7TasqOh4AAAAAAAAAAAAADmJ7AQ/original\" />\n\n- `key`: The main graphic of the edge, representing the primary shape of the edge, such as straight lines, polylines, etc.\n- `label`: Text label, usually used to display the name or description of the edge\n- `arrow`: Arrow, used to indicate the direction of the edge\n- `halo`: Graphic displaying halo effects around the main graphic\n\n## Ways to Create Custom Edges <Badge type=\"warning\">Choose the Right Approach</Badge>\n\nThere are two main ways to create custom edges:\n\n### 1. Inherit from Existing Edge Types <Badge type=\"success\">Recommended</Badge>\n\nThis is the most commonly used approach. You can choose to inherit from one of the following types:\n\n- [`BaseEdge`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/base-edge.ts) - The most basic edge class, providing core edge functionality\n- [`Line`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/line.ts) - Straight line edge\n- [`Polyline`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/polyline.ts) - Polyline edge\n- [`Quadratic`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/quadratic.ts) - Quadratic Bézier curve edge\n- [`Cubic`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/cubic.ts) - Cubic Bézier curve edge\n- [`CubicVertical`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/cubic-vertical.ts) - Vertical cubic Bézier curve edge\n- [`CubicHorizontal`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/cubic-horizontal.ts) - Horizontal cubic Bézier curve edge\n\n**Why choose this approach?**\n\n- 📌 **Less code**: Reuse existing edge properties and methods, only focus on new functionality\n- 📌 **Fast development**: Suitable for most project requirements, quickly achieve business goals\n- 📌 **Easy maintenance**: Clear code structure, clear inheritance relationships\n\n:::tip{title=Get Started Now}\nIf you choose to inherit from existing edge types (recommended), you can jump directly to [Create Your First Custom Edge in Three Steps](#create-your-first-custom-edge-in-three-steps) to start practicing. Most users will choose this approach!\n:::\n\n### 2. Build from Scratch Based on G Graphics System <Badge>Advanced Usage</Badge>\n\nIf existing edge types don't meet your requirements, you can create edges from scratch based on G's underlying graphics system.\n\n**Why choose this approach?**\n\n- 📌 **Maximum freedom**: Complete control over every detail of the edge, achieving any complex effects\n- 📌 **Special requirements**: Highly customized scenarios that existing edge types cannot satisfy\n- 📌 **Performance optimization**: Performance optimization for specific scenarios\n\n:::warning{title=Important Notes}\nCustom edges built from scratch need to handle all details by themselves, including graphic rendering, event response, state changes, etc., which is more challenging to develop. You can refer directly to the [source code](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/base-edge.ts) for implementation.\n:::\n\n## Create Your First Custom Edge in Three Steps\n\nLet's start with the most basic `BaseEdge` to implement a custom straight line edge:\n\n```js | ob { pin:false, inject: true }\nimport { Graph, register, BaseEdge, ExtensionCategory } from '@antv/g6';\n\nclass MyLineEdge extends BaseEdge {\n  getKeyStyle(attributes) {\n    return { ...super.getKeyStyle(attributes), lineWidth: 2, stroke: '#A4D3EE' };\n  }\n\n  getKeyPath(attributes) {\n    const { sourceNode, targetNode } = this;\n    const [x1, y1] = sourceNode.getPosition();\n    const [x2, y2] = targetNode.getPosition();\n\n    return [\n      ['M', x1, y1],\n      ['L', x2, y2],\n    ];\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'my-line-edge', MyLineEdge);\n\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 50 } },\n      { id: 'node2', style: { x: 300, y: 120 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  node: {\n    style: {\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n    },\n  },\n  edge: {\n    type: 'my-line-edge',\n    style: {\n      zIndex: 3,\n    },\n  },\n});\n\ngraph.render();\n```\n\n### Step 1: Write Custom Edge Class\n\n```typescript\nimport { BaseEdge } from '@antv/g6';\nimport type { BaseEdgeStyleProps } from '@antv/g6';\n\nclass MyLineEdge extends BaseEdge {\n  // Define edge style, can add or override default styles\n  protected getKeyStyle(attributes: Required<BaseEdgeStyleProps>) {\n    // Call parent class method to get basic style, then add custom styles\n    return { ...super.getKeyStyle(attributes), lineWidth: 2, stroke: '#A4D3EE' };\n  }\n\n  // Implement abstract method: define edge path\n  // This is an abstract method of BaseEdge, all subclasses must implement it\n  protected getKeyPath(attributes) {\n    // Get source node and target node\n    const { sourceNode, targetNode } = this;\n\n    // Get node position coordinates\n    const [x1, y1] = sourceNode.getPosition();\n    const [x2, y2] = targetNode.getPosition();\n\n    // Return SVG path array, defining a straight line from start to end\n    return [\n      ['M', x1, y1],\n      ['L', x2, y2],\n    ];\n  }\n}\n```\n\n:::success{title=Key Method Analysis}\n\n- `getKeyStyle`: Defines the basic style of the edge, such as line width, color, etc.\n- `getKeyPath`: An abstract method in `BaseEdge` that **must be implemented**, it defines the path shape of the edge\n  :::\n\n### Step 2: Register Custom Edge\n\nUse the `register` method to register the edge type so that G6 can recognize your custom edge:\n\n```js\nimport { ExtensionCategory } from '@antv/g6';\n\nregister(ExtensionCategory.EDGE, 'my-line-edge', MyLineEdge);\n```\n\nThe `register` method requires three parameters:\n\n- Extension category: `ExtensionCategory.EDGE` indicates this is an edge type\n- Type name: `my-line-edge` is the name we give to this custom edge, which will be used in configuration later\n- Class definition: `MyLineEdge` is the edge class we just created\n\n### Step 3: Apply Custom Edge\n\nIn the graph configuration, use our custom edge by setting `edge.type`:\n\n```js\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 100 } },\n      { id: 'node2', style: { x: 300, y: 150 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  node: {\n    style: {\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n    },\n  },\n  edge: {\n    type: 'my-line-edge',\n    style: {\n      zIndex: 3,\n    },\n  },\n});\n\ngraph.render();\n```\n\n🎉 Congratulations! You have created your first custom edge.\n\n## Going Further: Understanding the Principles of Edge Rendering\n\n### Atomic Graphics\n\nG6 nodes are drawn using atomic graphic units provided by the [G graphics system](https://g.antv.antgroup.com/). Here are common graphic elements and their uses:\n\n| Graphic Element | Type       | Description                                                                                                                                                                                                                                                                 |\n| --------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Circle          | `circle`   | Suitable for representing states, avatars, circular buttons, etc. Refer to SVG's [\\<circle\\>](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/circle) element                                                                                            |\n| Ellipse         | `ellipse`  | Similar to circle, but supports scenarios with different horizontal and vertical axes. Refer to SVG's [\\<ellipse\\>](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/ellipse) element                                                                     |\n| Image           | `image`    | Used to display icons, user avatars, LOGOs, etc. Refer to SVG's [\\<image\\>](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image) element                                                                                                                         |\n| Line            | `line`     | Used for decoration, auxiliary connections, etc. Refer to SVG's [\\<line\\>](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line) element                                                                                                                           |\n| Path            | `path`     | Supports complex graphics such as arrows, arcs, curves, Bézier paths, etc. The path contains a set of commands and parameters with different semantics, [specific usage](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths)                                   |\n| Polygon         | `polygon`  | Supports custom graphics such as pentagrams, arrows. Refer to SVG's [\\<polygon\\>](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon) element                                                                                                                 |\n| Polyline        | `polyline` | Multi-point polyline, suitable for complex connection structures. Refer to SVG's [\\<polyline\\>](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polyline) element                                                                                                  |\n| Rectangle       | `rect`     | Most commonly used graphic, suitable as containers, cards, buttons, and other basic structures. Refer to SVG's [\\<rect\\>](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect) element                                                                            |\n| Text            | `text`     | Displays names, descriptions, labels, and other content. Provides simple single-line/multi-line text layout capabilities, single-line supports horizontal alignment, character spacing; multi-line supports explicit line breaks and automatic wrapping, vertical alignment |\n\n> For more atomic graphics and detailed properties, please refer to [Element - Shape (Optional)](/en/manual/element/shape/overview)\n\nAll these graphics can be dynamically created or updated through `upsert()`, automatically managing graphic state and lifecycle.\n\n### Element Base Class\n\nBefore starting to customize elements, you need to understand some important properties and methods in G6 element base classes:\n\n#### Properties\n\n| Property   | Type                          | Description                                           |\n| ---------- | ----------------------------- | ----------------------------------------------------- |\n| shapeMap   | Record<string, DisplayObject> | Mapping table of all graphics under current element   |\n| animateMap | Record<string, IAnimation>    | Mapping table of all animations under current element |\n\n#### Methods\n\n#### `upsert(name, Ctor, style, container, hooks)`: Graphic Creation/Update\n\nWhen creating custom elements, you will frequently use the `upsert` method. It's short for \"update or insert\", responsible for adding or updating graphics in elements:\n\n```typescript\nupsert(key: string, Ctor: { new (...args: any[]): DisplayObject }, style: Record<string, any>, container: DisplayObject);\n```\n\n| Parameter | Type                                    | Description                                                                                                                                                                                                                                                                                                                       |\n| --------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| key       | string                                  | The key of the graphic, i.e., the corresponding key in `shapeMap`. Built-in keys include `'key'` `'label'` `'halo'` `'icon'` `'port'` `'badge'`<br/> The key should not use special symbols, it will be converted to camelCase to call `getXxxStyle` and `drawXxxShape` methods (see [Element Conventions](#element-conventions)) |\n| Ctor      | { new (...args: any[]): DisplayObject } | Graphic class                                                                                                                                                                                                                                                                                                                     |\n| style     | Record<string, any>                     | Graphic style                                                                                                                                                                                                                                                                                                                     |\n| container | DisplayObject                           | Container to mount the graphic                                                                                                                                                                                                                                                                                                    |\n\nFor example, inserting a purple circle at a fixed position:\n\n```js\nthis.upsert(\n  'element-key', // Unique identifier of the element\n  'circle', // Graphic type, such as 'rect', 'circle', etc.\n  { x: 100, y: 100, fill: '#a975f3' }, // Style configuration object\n  container, // Parent container\n);\n```\n\nWhy use `upsert` instead of directly creating graphics through `container.appendChild()`? Because:\n\n1. **Better performance**: When state changes or data updates, it intelligently reuses existing graphics instead of deleting and rebuilding, greatly improving rendering performance\n2. **Simpler code**: No need to manually check if elements exist\n3. **Easy management**: All graphics created through `upsert` are recorded in the node's `shapeMap`, you can easily get them through `this.getShape(key)`\n\n#### `render(attributes, container)`: Main Entry Point for Rendering Edges\n\nEvery custom edge class must implement the `render(attributes, container)` method, which defines how the edge is \"drawn\". You can use various atomic graphics here to compose the structure you want.\n\n```typescript\nrender(style: Record<string, any>, container: Group): void;\n```\n\n| Parameter | Type                | Description   |\n| --------- | ------------------- | ------------- |\n| style     | Record<string, any> | Element style |\n| container | Group               | Container     |\n\n#### `getShape(name)`: Get Created Graphics\n\nSometimes, you need to modify the properties of a sub-graphic after creation, or make sub-graphics interact with each other. In this case, the `getShape` method can help you get any graphic previously created through `upsert`:\n\n**⚠️ Note**: The order of graphics is important. If graphic B depends on the position of graphic A, you must ensure A is created first\n\n### Element Conventions\n\n- **Use Conventional Properties**\n\nCurrently conventional element properties include:\n\n- Get element size through `this.getSize()`\n- Get edge start and end points through `const [sourcePoint, targetPoint] = this.getEndpoints(attributes, false)` (simple mode - doesn't consider node shape, directly returns node center or nearest port center position)\n- Get edge start and end points through `const [sourcePoint, targetPoint] = this.getEndpoints(attributes)` (optimized mode - default is true, considers node shape, returns connection points on node boundary)\n\n- **Use Paired `getXxxStyle` and `drawXxxShape` Methods for Graphic Drawing**\n\n`getXxxStyle` is used to get graphic styles, `drawXxxShape` is used to draw graphics. Graphics created this way support automatic animation execution.\n\n> Where `Xxx` is the camelCase form of the key passed when calling the [upsert](#methods) method.\n\n- **Access Graph Context through `this.context`**\n\n### Lifecycle Hooks\n\nThe following lifecycle hook functions are provided, which you can override in custom edges to execute specific logic at key moments:\n\n| Hook Function | Trigger Time                                            | Typical Usage                                                                    |\n| ------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| `onCreate`    | When edge creation is completed with entrance animation | Bind interaction events, initialize edge state, add external listeners           |\n| `onUpdate`    | When edge update is completed with update animation     | Update dependent data, adjust related elements, trigger linkage effects          |\n| `onDestroy`   | When edge completes exit animation and is destroyed     | Clean up resources, remove external listeners, execute destruction notifications |\n\n### State Response\n\nOne of the most powerful aspects of G6 element design is the ability to separate **\"state response\"** from **\"rendering logic\"**.\n\nYou can define styles for each state in edge configuration:\n\n```js\nedge: {\n  type: 'custom-edge',\n  style: { stroke: '#eee' },\n  state: {\n    selected: {\n      stroke: '#f00',\n    },\n    hover: {\n      lineWidth: 3,\n      stroke: '#1890ff',\n    },\n  },\n}\n```\n\nMethod to switch states:\n\n```js\ngraph.setElementState(edgeId, ['selected']);\n```\n\nThis state will be passed to the `render()` method's `attributes`, and the merged result by the internal system will be automatically applied to the graphics.\n\nYou can also customize rendering logic based on state:\n\n```typescript\nprotected getKeyStyle(attributes: Required<BaseEdgeStyleProps>) {\n  const style = super.getKeyStyle(attributes);\n\n  // Adjust style based on state\n  if (attributes.states?.includes('selected')) {\n    return {\n      ...style,\n      stroke: '#1890ff',\n      lineWidth: 2,\n      shadowColor: 'rgba(24,144,255,0.2)',\n      shadowBlur: 15,\n    };\n  }\n\n  return style;\n}\n```\n\n## From Simple to Complex\n\n### Custom Path Polyline Edge\n\n```js | ob { inject: true }\nimport { Graph, register, BaseEdge, ExtensionCategory } from '@antv/g6';\n\nclass MyPolylineEdge extends BaseEdge {\n  getKeyPath(attributes) {\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes);\n\n    return [\n      ['M', sourcePoint[0], sourcePoint[1]],\n      ['L', targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], sourcePoint[1]],\n      ['L', targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], targetPoint[1]],\n      ['L', targetPoint[0], targetPoint[1]],\n    ];\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'my-polyline-edge', MyPolylineEdge);\n\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 100, y: 50, ports: [{ key: 'right', placement: [1, 0.5] }] } },\n      { id: 'node-1', style: { x: 250, y: 150, ports: [{ key: 'left', placement: [0, 0.5] }] } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  edge: {\n    type: 'my-polyline-edge',\n    style: {\n      startArrow: true,\n      endArrow: true,\n      stroke: '#F6BD16',\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n```\n\n### Additional Labels\n\n```js | ob { inject: true }\nimport { Graph, Line, register, BaseEdge, ExtensionCategory, subStyleProps } from '@antv/g6';\n\nclass LabelEdge extends Line {\n  render(attributes, container) {\n    super.render(attributes);\n    this.drawEndLabel(attributes, container, 'start');\n    this.drawEndLabel(attributes, container, 'end');\n  }\n\n  drawEndLabel(attributes, container, type) {\n    const key = type === 'start' ? 'startLabel' : 'endLabel';\n    const [x, y] = this.getEndpoints(attributes)[type === 'start' ? 0 : 1];\n\n    const fontStyle = {\n      x,\n      y,\n      dx: type === 'start' ? 15 : -15,\n      fontSize: 16,\n      fill: 'gray',\n      textBaseline: 'middle',\n      textAlign: type,\n    };\n    const style = subStyleProps(attributes, key);\n    const text = style.text;\n    this.upsert(`label-${type}`, 'text', text ? { ...fontStyle, ...style } : false, container);\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'extra-label-edge', LabelEdge);\n\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 100, y: 100 } },\n      { id: 'node-1', style: { x: 300, y: 100 } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  edge: {\n    type: 'extra-label-edge',\n    style: {\n      startArrow: true,\n      endArrow: true,\n      stroke: '#F6BD16',\n      startLabelText: 'start',\n      endLabelText: 'end',\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/custom-edge.zh.md",
    "content": "---\ntitle: 自定义边\norder: 7\n---\n\nG6 提供了多种[内置边](/manual/element/edge/base-edge)类型，包含 [line（直线边）](/manual/element/edge/line)、[polyline（折线边）](/manual/element/edge/polyline)、[quadratic（二次贝塞尔曲线边）](/manual/element/edge/quadratic)、[cubic（三次贝塞尔曲线边）](/manual/element/edge/cubic)、[cubic-horizontal（水平三次贝塞尔曲线边）](/manual/element/edge/cubic-horizontal)、[cubic-vertical（垂直三次贝塞尔曲线边）](/manual/element/edge/cubic-vertical) 等。这些内置边能够满足大部分基础场景需求。\n\n但在实际项目中，你可能会遇到这些基础边无法满足的需求。这时，你需要创建自定义边。别担心，这比你想象的要简单！\n\n## 开始之前：了解边的基本构成\n\n在 G6 中，一条完整的边通常由以下几个部分组成：\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YKN7TasqOh4AAAAAAAAAAAAADmJ7AQ/original\" />\n\n- `key` ：边的主图形，表示边的主要形状，例如直线、折线等；\n- `label` ：文本标签，通常用于展示边的名称或描述；\n- `arrow` ：箭头，用于表示边的方向；\n- `halo` ：主图形周围展示的光晕效果的图形。\n\n## 自定义边的方式 <Badge type=\"warning\">选择合适的方式</Badge>\n\n创建自定义边的方式主要有两种途径：\n\n### 1. 继承现有边类型 <Badge type=\"success\">推荐</Badge>\n\n这是最常用的方式，你可以选择继承以下类型之一：\n\n- [`BaseEdge`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/base-edge.ts) - 最基础的边类，提供边的核心功能\n- [`Line`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/line.ts) - 直线边\n- [`Polyline`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/polyline.ts) - 折线边\n- [`Quadratic`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/quadratic.ts) - 二次贝塞尔曲线边\n- [`Cubic`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/cubic.ts) - 三次贝塞尔曲线边\n- [`CubicVertical`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/cubic-vertical.ts) - 垂直三次贝塞尔曲线边\n- [`CubicHorizontal`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/cubic-horizontal.ts) - 水平三次贝塞尔曲线边\n\n**为什么选择这种方式？**\n\n- 📌 **代码量少**：复用现有边的属性和方法，只需专注于新增功能\n- 📌 **开发迅速**：适合大多数项目需求，快速实现业务目标\n- 📌 **易于维护**：代码结构清晰，继承关系明确\n\n:::tip{title=立即开始}\n如果你选择继承现有边类型（推荐），可以直接跳到 [三步创建你的第一个自定义边](#三步创建你的第一个自定义边) 开始实践。大部分用户都会选择这种方式！\n:::\n\n### 2. 基于 G 图形系统从零开发 <Badge>高级用法</Badge>\n\n如果现有边类型都不满足需求，你可以基于 G 的底层图形系统从零创建边。\n\n**为什么选择这种方式？**\n\n- 📌 **最大自由度**：完全控制边的每个细节，实现任意复杂效果\n- 📌 **特殊需求**：现有边类型无法满足的高度定制场景\n- 📌 **性能优化**：针对特定场景的性能优化\n\n:::warning{title=注意事项}\n从零开发的自定义边需要自行处理所有细节，包括图形绘制、事件响应、状态变化等，开发难度较大。这里可以直接参考 [源码](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/edges/base-edge.ts) 进行实现。\n:::\n\n## 三步创建你的第一个自定义边\n\n让我们从最基础的 `BaseEdge` 开始，实现一个自定义直线边：\n\n```js | ob { pin:false, inject: true }\nimport { Graph, register, BaseEdge, ExtensionCategory } from '@antv/g6';\n\nclass MyLineEdge extends BaseEdge {\n  getKeyStyle(attributes) {\n    return { ...super.getKeyStyle(attributes), lineWidth: 2, stroke: '#A4D3EE' };\n  }\n\n  getKeyPath(attributes) {\n    const { sourceNode, targetNode } = this;\n    const [x1, y1] = sourceNode.getPosition();\n    const [x2, y2] = targetNode.getPosition();\n\n    return [\n      ['M', x1, y1],\n      ['L', x2, y2],\n    ];\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'my-line-edge', MyLineEdge);\n\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 50 } },\n      { id: 'node2', style: { x: 300, y: 120 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  node: {\n    style: {\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n    },\n  },\n  edge: {\n    type: 'my-line-edge',\n    style: {\n      zIndex: 3,\n    },\n  },\n});\n\ngraph.render();\n```\n\n### 第一步：编写自定义边类\n\n```typescript\nimport { BaseEdge } from '@antv/g6';\nimport type { BaseEdgeStyleProps } from '@antv/g6';\n\nclass MyLineEdge extends BaseEdge {\n  // 定义边的样式，可以添加或覆盖默认样式\n  protected getKeyStyle(attributes: Required<BaseEdgeStyleProps>) {\n    // 调用父类方法获取基础样式，然后添加自定义样式\n    return { ...super.getKeyStyle(attributes), lineWidth: 2, stroke: '#A4D3EE' };\n  }\n\n  // 实现抽象方法：定义边的路径\n  // 这是 BaseEdge 的抽象方法，所有子类必须实现\n  protected getKeyPath(attributes) {\n    // 获取源节点和目标节点\n    const { sourceNode, targetNode } = this;\n\n    // 获取节点的位置坐标\n    const [x1, y1] = sourceNode.getPosition();\n    const [x2, y2] = targetNode.getPosition();\n\n    // 返回SVG路径数组，定义从起点到终点的直线\n    return [\n      ['M', x1, y1],\n      ['L', x2, y2],\n    ];\n  }\n}\n```\n\n:::success{title=关键方法解析}\n\n- `getKeyStyle`: 定义边的基本样式，如线宽、颜色等\n- `getKeyPath`: 是 `BaseEdge` 中的抽象方法，**必须实现**，它定义了边的路径形状\n  :::\n\n### 第二步：注册自定义边\n\n使用 `register` 方法注册边类型，这样 G6 才能识别你的自定义边：\n\n```js\nimport { ExtensionCategory } from '@antv/g6';\n\nregister(ExtensionCategory.EDGE, 'my-line-edge', MyLineEdge);\n```\n\n`register` 方法需要三个参数：\n\n- 扩展类别：`ExtensionCategory.EDGE` 表示这是一个边类型\n- 类型名称：`my-line-edge` 是我们给这个自定义边起的名字，后续会在配置中使用\n- 类定义：`MyLineEdge` 是我们刚刚创建的边类\n\n### 第三步：应用自定义边\n\n在图的配置中，通过设置 `edge.type` 来使用我们的自定义边：\n\n```js\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 100 } },\n      { id: 'node2', style: { x: 300, y: 150 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  node: {\n    style: {\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n    },\n  },\n  edge: {\n    type: 'my-line-edge',\n    style: {\n      zIndex: 3,\n    },\n  },\n});\n\ngraph.render();\n```\n\n🎉 恭喜！你已经创建了第一个自定义边。\n\n## 更进一步：理解边绘制的原理\n\n### 原子图形\n\nG6 的节点是由 [G 图形系统](https://g.antv.antgroup.com/) 提供的图形原子单元绘制而成。以下是常见图形元素及其用途：\n\n| 图形元素 | 类型       | 描述                                                                                                                                                                        |\n| -------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| 圆形     | `circle`   | 适合表示状态、头像、圆形按钮等。可以参考 SVG 的 [\\<circle\\>](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Reference/Element/circle) 元素                                |\n| 椭圆     | `ellipse`  | 与 circle 类似，但支持横纵轴不同的场景。可以参考 SVG 的 [\\<ellipse\\>](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Reference/Element/ellipse) 元素                      |\n| 图片     | `image`    | 用于展示图标、用户头像、LOGO 等。可以参考 SVG 的 [\\<image\\>](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/image) 元素                                           |\n| 直线     | `line`     | 用于装饰、辅助连接等。可以参考 SVG 的 [\\<line\\>](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/line) 元素                                                        |\n| 路径     | `path`     | 支持复杂图形，如箭头、圆弧、曲线、贝塞尔路径等。路径中包含一组命令与参数，这些命令有不同的语义，[具体用法](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Paths) |\n| 多边形   | `polygon`  | 支持自定义图形，如五角星、箭头。可以参考 SVG 的 [\\<polygon\\>](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/polygon) 元素                                        |\n| 折线     | `polyline` | 多点折线，适合复杂的连线结构。可以参考 SVG 的 [\\<polyline\\>](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/polyline) 元素                                        |\n| 矩形     | `rect`     | 最常用图形，适合作为容器、卡片、按钮等基础结构。可以参考 SVG 的 [\\<rect\\>](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/rect) 元素                              |\n| 文本     | `text`     | 显示名称、描述、标签等内容。提供简单的单行/多行文本排版能力，单行支持水平对齐、字符间距；多行支持显式换行符以及自动换行，垂直对齐                                           |\n\n> 更多原子图形和详细的属性请参考 [元素 - 图形（可选）](/manual/element/shape/overview)\n\n所有这些图形都可通过 `upsert()` 动态创建或更新，并自动管理图形状态和生命周期。\n\n### 元素基类\n\n开始自定义元素之前，你需要了解 G6 元素基类中的一些重要属性和方法：\n\n#### 属性\n\n| 属性       | 类型                          | 描述                       |\n| ---------- | ----------------------------- | -------------------------- |\n| shapeMap   | Record<string, DisplayObject> | 当前元素下所有图形的映射表 |\n| animateMap | Record<string, IAnimation>    | 当前元素下所有动画的映射表 |\n\n#### 方法\n\n#### `upsert(name, Ctor, style, container, hooks)`: 图形创建/更新\n\n在创建自定义元素时，你会频繁用到 `upsert` 方法。它是 \"update or insert\" 的缩写，负责添加或更新元素中的图形：\n\n```typescript\nupsert(key: string, Ctor: { new (...args: any[]): DisplayObject }, style: Record<string, any>, container: DisplayObject);\n```\n\n| 参数      | 类型                                    | 描述                                                                                                                                                                                                                                   |\n| --------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| key       | string                                  | 图形的 key，即 `shapeMap` 中对应的 key。内置的 key 包括 `'key'` `'label'` `'halo'` `'icon'` `'port'` `'badge'`<br/> key 不应使用特殊符号，会基于该值转化为驼峰形式调用 `getXxxStyle` 和 `drawXxxShape` 方法（见[元素约定](#元素约定)） |\n| Ctor      | { new (...args: any[]): DisplayObject } | 图形类                                                                                                                                                                                                                                 |\n| style     | Record<string, any>                     | 图形样式                                                                                                                                                                                                                               |\n| container | DisplayObject                           | 挂载图形的容器                                                                                                                                                                                                                         |\n\n例如，插入一个固定位置的紫色圆形：\n\n```js\nthis.upsert(\n  'element-key', // 元素的唯一标识\n  'circle', // 图形类型，如 'rect', 'circle' 等\n  { x: 100, y: 100, fill: '#a975f3' }, // 样式配置对象\n  container, // 父容器\n);\n```\n\n为什么要使用 `upsert` 而不直接通过 `container.appendChild()` 创建图形？因为：\n\n1. **性能更好**：当状态变化或数据更新时，会智能地复用已有图形，而不是删除再重建，大大提高了渲染性能\n2. **代码更简洁**：不需要手动判断元素是否存在\n3. **便于管理**：所有通过 `upsert` 创建的图形都会被记录在节点的 `shapeMap` 中，你可以通过 `this.getShape(key)` 轻松获取\n\n#### `render(attributes, container)`: 渲染边的主入口\n\n每个自定义边类都必须实现 `render(attributes, container)` 方法，它定义了该边如何被“绘制”出来。你可以在这里使用各种原子图形，组合出你想要的结构。\n\n```typescript\nrender(style: Record<string, any>, container: Group): void;\n```\n\n| 参数      | 类型                | 描述     |\n| --------- | ------------------- | -------- |\n| style     | Record<string, any> | 元素样式 |\n| container | Group               | 容器     |\n\n#### `getShape(name)`: 获取已创建的图形\n\n有时，你需要在创建后修改某个子图形的属性，或者让子图形之间有交互关联。这时，`getShape` 方法可以帮你获取之前通过 `upsert` 创建的任何图形：\n\n**⚠️ 注意**：图形的顺序很重要，如果图形 B 依赖图形 A 的位置，必须确保 A 先创建\n\n### 元素约定\n\n- **使用约定属性**\n\n目前约定的元素属性包括：\n\n- 通过 `this.getSize()` 获取元素的尺寸\n- 通过 `const [sourcePoint, targetPoint] = this.getEndpoints(attributes, false)` 获取边的起点和终点（简单模式 - 不考虑节点形状，直接返回节点中心点或最近连接桩中心˝位置）\n- 通过 `const [sourcePoint, targetPoint] = this.getEndpoints(attributes)` 获取边的起点和终点（优化模式 - 默认为 true，考虑节点形状，返回节点边界上的连接点）\n\n- **采用 `getXxxStyle` 和 `drawXxxShape` 配对的方式进行图形绘制**\n\n`getXxxStyle` 用于获取图形样式，`drawXxxShape` 用于绘制图形。通过该方式创建的图形支持自动执行动画。\n\n> 其中 `Xxx` 是调用 [upsert](#方法) 方法时传入的 key 的驼峰形式。\n\n- **可通过 `this.context` 访问 Graph 上下文**\n\n### 生命周期钩子\n\n提供了以下生命周期钩子函数，你可以在自定义边中重写这些方法，在关键时刻执行特定逻辑：\n\n| 钩子函数    | 触发时机                 | 典型用途                                   |\n| ----------- | ------------------------ | ------------------------------------------ |\n| `onCreate`  | 当边创建后完成入场动画时 | 绑定交互事件、初始化边状态、添加外部监听器 |\n| `onUpdate`  | 当边更新后完成更新动画时 | 更新依赖数据、调整相关元素、触发联动效果   |\n| `onDestroy` | 当边完成退场动画并销毁后 | 清理资源、移除外部监听器、执行销毁通知     |\n\n### 状态响应\n\nG6 元素设计中最强大的一点，是可以将 **“状态响应”** 与 **“绘制逻辑”** 分离。\n\n你可以在边配置中定义每种状态下的样式：\n\n```js\nedge: {\n  type: 'custom-edge',\n  style: { stroke: '#eee' },\n  state: {\n    selected: {\n      stroke: '#f00',\n    },\n    hover: {\n      lineWidth: 3,\n      stroke: '#1890ff',\n    },\n  },\n}\n```\n\n切换状态的方法:\n\n```js\ngraph.setElementState(edgeId, ['selected']);\n```\n\n这个状态会传入到 `render()` 方法的 `attributes` 中，由内部系统合并后的结果自动应用在图形上。\n\n也可以根据状态自定义渲染逻辑：\n\n```typescript\nprotected getKeyStyle(attributes: Required<BaseEdgeStyleProps>) {\n  const style = super.getKeyStyle(attributes);\n\n  // 根据状态调整样式\n  if (attributes.states?.includes('selected')) {\n    return {\n      ...style,\n      stroke: '#1890ff',\n      lineWidth: 2,\n      shadowColor: 'rgba(24,144,255,0.2)',\n      shadowBlur: 15,\n    };\n  }\n\n  return style;\n}\n```\n\n## 从简单到复杂\n\n### 自定义路径的折线边\n\n```js | ob { inject: true }\nimport { Graph, register, BaseEdge, ExtensionCategory } from '@antv/g6';\n\nclass MyPolylineEdge extends BaseEdge {\n  getKeyPath(attributes) {\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes);\n\n    return [\n      ['M', sourcePoint[0], sourcePoint[1]],\n      ['L', targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], sourcePoint[1]],\n      ['L', targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], targetPoint[1]],\n      ['L', targetPoint[0], targetPoint[1]],\n    ];\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'my-polyline-edge', MyPolylineEdge);\n\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 100, y: 50, ports: [{ key: 'right', placement: [1, 0.5] }] } },\n      { id: 'node-1', style: { x: 250, y: 150, ports: [{ key: 'left', placement: [0, 0.5] }] } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  edge: {\n    type: 'my-polyline-edge',\n    style: {\n      startArrow: true,\n      endArrow: true,\n      stroke: '#F6BD16',\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n```\n\n### 额外标签\n\n```js | ob { inject: true }\nimport { Graph, Line, register, BaseEdge, ExtensionCategory, subStyleProps } from '@antv/g6';\n\nclass LabelEdge extends Line {\n  render(attributes, container) {\n    super.render(attributes);\n    this.drawEndLabel(attributes, container, 'start');\n    this.drawEndLabel(attributes, container, 'end');\n  }\n\n  drawEndLabel(attributes, container, type) {\n    const key = type === 'start' ? 'startLabel' : 'endLabel';\n    const [x, y] = this.getEndpoints(attributes)[type === 'start' ? 0 : 1];\n\n    const fontStyle = {\n      x,\n      y,\n      dx: type === 'start' ? 15 : -15,\n      fontSize: 16,\n      fill: 'gray',\n      textBaseline: 'middle',\n      textAlign: type,\n    };\n    const style = subStyleProps(attributes, key);\n    const text = style.text;\n    this.upsert(`label-${type}`, 'text', text ? { ...fontStyle, ...style } : false, container);\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'extra-label-edge', LabelEdge);\n\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 100, y: 100 } },\n      { id: 'node-1', style: { x: 300, y: 100 } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  edge: {\n    type: 'extra-label-edge',\n    style: {\n      startArrow: true,\n      endArrow: true,\n      stroke: '#F6BD16',\n      startLabelText: 'start',\n      endLabelText: 'end',\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/overview.en.md",
    "content": "---\ntitle: Edge Overview\norder: 0\n---\n\n## What is an Edge\n\nAn edge is one of the basic elements in a graph, used to connect two nodes or combos, representing the relationship between them. In G6, edges are directional, pointing from `source` to `target`, but you can configure them to hide the arrow to represent undirected connections.\n\nYou can create edges between any two nodes, combos, or between a node and a combo, and you can express different types of relationships by creating multiple edges.\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YKN7TasqOh4AAAAAAAAAAAAADmJ7AQ/original\" />\n\nG6 provides the following built-in edges:\n\n- `line` Straight line edge\n- `polyline` Polyline edge\n- `quadratic` Quadratic Bezier curve edge\n- `cubic` Cubic Bezier curve edge\n- `cubicVertical` Vertical cubic Bezier curve edge\n- `cubicHorizontal` Horizontal cubic Bezier curve edge\n\n### Data Structure\n\nWhen defining an edge, you need to add an `edges` field to the graph's data object. Each edge is an object with the following structure:\n\n| Attribute | Description                                                                                                   | Type     | Default | Required |\n| --------- | ------------------------------------------------------------------------------------------------------------- | -------- | ------- | -------- |\n| source    | ID of the starting node of the edge                                                                           | string   | -       | ✓        |\n| target    | ID of the target node of the edge                                                                             | string   | -       | ✓        |\n| id        | Unique identifier of the edge                                                                                 | string   | -       |          |\n| type      | Type of edge, name of built-in edge type or custom edge, such as `line` or `polyline`                         | string   | -       |          |\n| data      | Edge data, used to store custom data of the edge, can be accessed in style mapping through callback functions | object   | -       |          |\n| style     | Edge style, including visual attributes like line color, width, arrow, etc.                                   | object   | -       |          |\n| states    | Initial states of the edge                                                                                    | string[] | -       |          |\n\nAn example of a data item in the `edges` array:\n\n```json\n{\n  \"source\": \"alice\",\n  \"target\": \"bob\",\n  \"type\": \"line\",\n  \"data\": { \"relationship\": \"friend\", \"strength\": 5 },\n  \"style\": { \"stroke\": \"green\", \"lineWidth\": 2 },\n  \"states\": [\"hover\"]\n}\n```\n\n### Configuration Methods\n\nThere are three ways to configure edges, listed in order of priority from high to low:\n\n- Use `graph.setEdge()` for dynamic configuration\n- Global configuration when instantiating the graph\n- Dynamic attributes in data\n\nThese configuration methods can be used simultaneously. When there are the same configuration items, the method with higher priority will override the one with lower priority.\n\n### Using `graph.setEdge()`\n\nYou can dynamically set the style mapping logic of edges using `graph.setEdge()` after the graph instance is created.\n\nThis method needs to be called before `graph.render()` to take effect and has the highest priority.\n\n```js\ngraph.setEdge({\n  style: {\n    type: 'line',\n    style: { stroke: '#5CACEE', lineWidth: 2 },\n  },\n});\n\ngraph.render();\n```\n\n### Global Configuration When Instantiating the Graph\n\nYou can configure edge style mapping globally when instantiating the graph, and this configuration will take effect on all edges.\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  edge: {\n    type: 'line',\n    style: { stroke: '#5CACEE', lineWidth: 2 },\n  },\n});\n```\n\n### Dynamic Configuration in Data\n\nIf you need different configurations for different edges, you can write the configuration into the edge data. This configuration method can be directly written into the data in the form of the following code:\n\n```typescript\nconst data = {\n  edges: [\n    {\n      source: 'node-1',\n      target: 'node-2',\n      type: 'line',\n      style: { stroke: 'orange' },\n    },\n  ],\n};\n```\n\n### Adjusting Priority\n\nIf you want the configuration in the data to have a higher priority than the global configuration, you can take the following approach:\n\n```js\nconst data = {\n  edges: [\n    {\n      source: 'node-1',\n      target: 'node-2',\n      type: 'line',\n      style: { stroke: 'orange' },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  edge: {\n    type: 'line',\n    style: {\n      stroke: (d) => d.style.stroke || '#5CACEE',\n      lineWidth: 2,\n    },\n  },\n});\n```\n\n## Custom Edges\n\nWhen built-in edges cannot meet the requirements, G6 provides powerful customization capabilities:\n\n- Extend built-in edges\n- Create entirely new edge types\n\nUnlike combos, custom edges need to be registered before use. For detailed tutorials, please refer to the [Custom Edge](/manual/element/edge/custom-edge) documentation.\n"
  },
  {
    "path": "packages/site/docs/manual/element/edge/overview.zh.md",
    "content": "---\ntitle: 边总览\norder: 0\n---\n\n## 什么是边\n\n边（Edge）是图中的基本元素之一，用于连接两个节点或组合，表示它们之间的关系。在 G6 中，边具有方向性，从 `source` 指向 `target`，也可以通过配置隐藏箭头以表示无方向连接。\n\n你可以在任意两个节点、组合，或节点与组合之间创建边，还可以通过创建多条边来表达不同的关系类型。\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YKN7TasqOh4AAAAAAAAAAAAADmJ7AQ/original\" />\n\nG6 提供了以下内置边：\n\n- `line` 直线边\n- `polyline` 折线边\n- `quadratic` 二次贝塞尔曲线边\n- `cubic` 三次贝塞尔曲线边\n- `cubicVertical` 垂直三次贝塞尔曲线边\n- `cubicHorizontal` 水平三次贝塞尔曲线边\n\n### 数据结构\n\n定义边时，需要在图的数据对象中添加 `edges` 字段。每条边是一个对象，结构如下：\n\n| 属性   | 描述                                                                  | 类型     | 默认值 | 必选 |\n| ------ | --------------------------------------------------------------------- | -------- | ------ | ---- |\n| source | 边起始节点 ID                                                         | string   | -      | ✓    |\n| target | 边目标节点 ID                                                         | string   | -      | ✓    |\n| id     | 边的唯一标识符                                                        | string   | -      |      |\n| type   | 边类型，内置边类型名称或者自定义边的名称，比如 `line` 或者 `polyline` | string   | -      |      |\n| data   | 边数据，用于存储边的自定义数据，可以在样式映射中通过回调函数获取      | object   | -      |      |\n| style  | 边样式，包括线条颜色、宽度、箭头等视觉属性                            | object   | -      |      |\n| states | 边初始状态                                                            | string[] | -      |      |\n\n`edges` 数组中一个数据项的示例：\n\n```json\n{\n  \"source\": \"alice\",\n  \"target\": \"bob\",\n  \"type\": \"line\",\n  \"data\": { \"relationship\": \"friend\", \"strength\": 5 },\n  \"style\": { \"stroke\": \"green\", \"lineWidth\": 2 },\n  \"states\": [\"hover\"]\n}\n```\n\n### 配置方法\n\n配置边的方式有三种，按优先级从高到低如下：\n\n- 使用 `graph.setEdge()` 动态配置\n- 实例化图时全局配置\n- 在数据中动态属性\n\n这几个配置方法可以同时使用。有相同的配置项时，优先级高的方式将会覆盖优先级低的。\n\n### 使用 `graph.setEdge()`\n\n可在图实例创建后，使用 `graph.setEdge()` 动态设置边的样式映射逻辑。\n\n该方法需要在 `graph.render()` 之前调用才会生效，并拥有最高优先级。\n\n```js\ngraph.setEdge({\n  style: {\n    type: 'line',\n    style: { stroke: '#5CACEE', lineWidth: 2 },\n  },\n});\n\ngraph.render();\n```\n\n### 实例化图时全局配置\n\n在实例化图时可以通过 `edge` 配置边样式映射，这里的配置是全局的配置，将会在所有边上生效。\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  edge: {\n    type: 'line',\n    style: { stroke: '#5CACEE', lineWidth: 2 },\n  },\n});\n```\n\n### 在数据中动态配置\n\n如果需要为不同边进行不同的配置，可以将配置写入到边数据中。这种配置方式可以通过下面代码的形式直接写入数据：\n\n```typescript\nconst data = {\n  edges: [\n    {\n      source: 'node-1',\n      target: 'node-2',\n      type: 'line',\n      style: { stroke: 'orange' },\n    },\n  ],\n};\n```\n\n### 调整优先级\n\n如果你想让数据中配置的优先级高于全局配置，你可以采取以下方式：\n\n```js\nconst data = {\n  edges: [\n    {\n      source: 'node-1',\n      target: 'node-2',\n      type: 'line',\n      style: { stroke: 'orange' },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  edge: {\n    type: 'line',\n    style: {\n      stroke: (d) => d.style.stroke || '#5CACEE',\n      lineWidth: 2,\n    },\n  },\n});\n```\n\n## 自定义边\n\n当内置边无法满足需求时，G6 提供了强大的自定义能力：\n\n- 继承内置边进行扩展\n- 创建全新的边类型\n\n与组合不同，自定义边需要先注册后使用。详细教程请参考 [自定义边](/manual/element/edge/custom-edge) 文档。\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/BaseNode.en.md",
    "content": "---\ntitle: Common Node Configuration\norder: 1\n---\n\nThis document introduces the common configuration properties for built-in nodes.\n\n## NodeOptions\n\n```js {5-9}\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  node: {\n    type: 'circle', // Node type\n    style: {}, // Node style\n    state: {}, // State style\n    palette: {}, // Palette configuration\n    animation: {}, // Animation configuration\n  },\n});\n```\n\n| Property  | Description                                                    | Type                    | Default  | Required |\n| --------- | -------------------------------------------------------------- | ----------------------- | -------- | -------- |\n| type      | Node type, built-in node type name or custom node name         | [Type](#type)           | `circle` |          |\n| style     | Node style configuration, including color, size, etc.          | [Style](#style)         | -        |          |\n| state     | Style configuration for different states                       | [State](#state)         | -        |          |\n| palette   | Define node palette for mapping colors based on different data | [Palette](#palette)     | -        |          |\n| animation | Define animation effects for nodes                             | [Animation](#animation) | -        |          |\n\n## Type\n\nSpecifies the node type, built-in node type name or custom node name. Default is `circle`. **⚠️ Note**: This determines the shape of the main graphic.\n\n```js {3}\nconst graph = new Graph({\n  node: {\n    type: 'circle',\n  },\n});\n```\n\n**⚠️ Dynamic Configuration**: The `type` property also supports dynamic configuration, allowing you to dynamically select node types based on node data:\n\n```js\nconst graph = new Graph({\n  node: {\n    // Static configuration\n    type: 'circle',\n\n    // Dynamic configuration - arrow function form\n    type: (datum) => datum.data.nodeType || 'circle',\n\n    // Dynamic configuration - regular function form (can access graph instance)\n    type: function (datum) {\n      console.log(this); // graph instance\n      return datum.data.category === 'important' ? 'diamond' : 'circle';\n    },\n  },\n});\n```\n\nAvailable values:\n\n- `circle`: [Circle Node](/en/manual/element/node/circle)\n- `diamond`: [Diamond Node](/en/manual/element/node/diamond)\n- `donut`: [Donut Node](/en/manual/element/node/donut)\n- `ellipse`: [Ellipse Node](/en/manual/element/node/ellipse)\n- `hexagon`: [Hexagon Node](/en/manual/element/node/hexagon)\n- `html`: [HTML Node](/en/manual/element/node/html)\n- `image`: [Image Node](/en/manual/element/node/image)\n- `rect`: [Rectangle Node](/en/manual/element/node/rect)\n- `star`: [Star Node](/en/manual/element/node/star)\n- `triangle`: [Triangle Node](/en/manual/element/node/triangle)\n\n## Style\n\nDefines the style of nodes, including color, size, etc.\n\n```js {3}\nconst graph = new Graph({\n  node: {\n    style: {},\n  },\n});\n```\n\n**⚠️ Dynamic Configuration**: All the following style properties support dynamic configuration, meaning you can pass functions to dynamically calculate property values based on node data:\n\n```js\nconst graph = new Graph({\n  node: {\n    style: {\n      // Static configuration\n      fill: '#1783FF',\n\n      // Dynamic configuration - arrow function form\n      stroke: (datum) => (datum.data.isActive ? '#FF0000' : '#000000'),\n\n      // Dynamic configuration - regular function form (can access graph instance)\n      lineWidth: function (datum) {\n        console.log(this); // graph instance\n        return datum.data.importance > 5 ? 3 : 1;\n      },\n\n      // Nested properties also support dynamic configuration\n      labelText: (datum) => `Node: ${datum.id}`,\n      badges: (datum) => datum.data.tags.map((tag) => ({ text: tag })),\n    },\n  },\n});\n```\n\nWhere the `datum` parameter is the node data object (`NodeData`), containing all data information of the node.\n\nA complete node consists of the following parts:\n\n<img width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Ot4bSbBx97EAAAAAAAAAAAAADmJ7AQ/original\" />\n\n- `key`: The main graphic of the node, representing the primary shape of the node, such as rectangle, circle, etc.\n- `label`: Text label, usually used to display the name or description of the node\n- `icon`: Icon graphic, usually used to display node icons, can be images or text icons\n- `badge`: Badge, by default located at the top-right corner of the node\n- `halo`: Graphic showing halo effect around the main graphic\n- `port`: Connection points on the node, used to connect edges\n\nThe following style configurations are explained in order by atomic graphics:\n\n### Main Graphic Style\n\nThe main graphic is the core part of the node, defining the basic shape and appearance of the node. Here are common configuration scenarios:\n\n#### Basic Style Configuration\n\nSetting the basic appearance of nodes:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      fill: '#5B8FF9', // Blue fill\n      stroke: '#1A1A1A', // Dark stroke\n      lineWidth: 2,\n      size: 40,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Transparency and Shadow Effects\n\nAdding transparency and shadow effects to nodes:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      fill: '#61DDAA',\n      fillOpacity: 0.85,\n      shadowColor: 'rgba(97, 221, 170, 0.4)',\n      shadowBlur: 12,\n      shadowOffsetX: 2,\n      shadowOffsetY: 4,\n      stroke: '#F0F0F0',\n      lineWidth: 1,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Dashed Border Style\n\nCreating nodes with dashed borders:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      fill: '#FFF1F0',\n      stroke: '#F5222D',\n      lineWidth: 2,\n      lineDash: [6, 4],\n      lineCap: 'round',\n    },\n  },\n});\n\ngraph.render();\n```\n\nThe complete main graphic style configuration is as follows:\n\n| Property                        | Description                                                                                                                                      | Type                          | Default   | Required |\n| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------- | --------- | -------- |\n| collapsed                       | Whether the current node/combo is collapsed                                                                                                      | boolean                       | false     |          |\n| cursor                          | Node mouse hover style, [options](#cursor)                                                                                                       | string                        | default   |          |\n| fill                            | Node fill color                                                                                                                                  | string                        | `#1783FF` |          |\n| fillOpacity                     | Node fill color transparency                                                                                                                     | number \\| string              | 1         |          |\n| increasedLineWidthForHitTesting | When lineWidth is small, the interactive area also becomes small. Sometimes we want to increase this area to make \"thin lines\" easier to pick up | number                        | 0         |          |\n| lineCap                         | Node stroke end style                                                                                                                            | `round` \\| `square` \\| `butt` | `butt`    |          |\n| lineDash                        | Node stroke dash style                                                                                                                           | number[]                      | -         |          |\n| lineDashOffset                  | Node stroke dash offset                                                                                                                          | number                        | -         |          |\n| lineJoin                        | Node stroke join style                                                                                                                           | `round` \\| `bevel` \\| `miter` | `miter`   |          |\n| lineWidth                       | Node stroke width                                                                                                                                | number                        | 1         |          |\n| opacity                         | Node transparency                                                                                                                                | number \\| string              | 1         |          |\n| pointerEvents                   | How the node responds to pointer events, [options](#pointerevents)                                                                               | string                        | `auto`    |          |\n| shadowBlur                      | Node shadow blur                                                                                                                                 | number                        | -         |          |\n| shadowColor                     | Node shadow color                                                                                                                                | string                        | -         |          |\n| shadowOffsetX                   | Node shadow offset in x-axis direction                                                                                                           | number \\| string              | -         |          |\n| shadowOffsetY                   | Node shadow offset in y-axis direction                                                                                                           | number \\| string              | -         |          |\n| shadowType                      | Node shadow type                                                                                                                                 | `inner` \\| `outer`            | `outer`   |          |\n| size                            | Node size, quick setting for node width and height, [options](#size)                                                                             | number \\| number[]            | 32        |          |\n| stroke                          | Node stroke color                                                                                                                                | string                        | `#000`    |          |\n| strokeOpacity                   | Node stroke color transparency                                                                                                                   | number \\| string              | 1         |          |\n| transform                       | Transform property allows you to rotate, scale, skew or translate the given node                                                                 | string                        | -         |          |\n| transformOrigin                 | Rotation and scaling center, also called transformation center                                                                                   | string                        | -         |          |\n| visibility                      | Whether the node is visible                                                                                                                      | `visible` \\| `hidden`         | `visible` |          |\n| x                               | Node x coordinate                                                                                                                                | number                        | 0         |          |\n| y                               | Node y coordinate                                                                                                                                | number                        | 0         |          |\n| z                               | Node z coordinate                                                                                                                                | number                        | 0         |          |\n| zIndex                          | Node rendering level                                                                                                                             | number                        | 0         |          |\n\n#### Size\n\nNode size, quick setting for node width and height, supports three configuration methods:\n\n- number: Indicates that the node width and height are the same as the specified value\n- [number, number]: Indicates that the node width and height are represented by array elements indicating the node's width and height respectively\n- [number, number, number]: Indicates that the node width, height, and depth are represented by array elements\n\n#### PointerEvents\n\nThe `pointerEvents` property controls how graphics respond to interaction events. You can refer to the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events).\n\nAvailable values: `visible` | `visiblepainted` | `visiblestroke` | `non-transparent-pixel` | `visiblefill` | `visible` | `painted` | `fill` | `stroke` | `all` | `none` | `auto` | `inherit` | `initial` | `unset`\n\nIn short, `fill`, `stroke`, and `visibility` can independently or in combination affect pick behavior. Currently supports the following keywords:\n\n- **`auto`**: Default value, equivalent to `visiblepainted`\n- **`none`**: Will never be a target for responding to events\n- **`visiblepainted`**: Will respond to events only if the following conditions are met:\n  - `visibility` is set to `visible`, i.e., the graphic is visible\n  - Triggered in the graphic fill area and `fill` takes a non-`none` value; or triggered in the graphic stroke area and `stroke` takes a non-`none` value\n- **`visiblefill`**: Will respond to events only if the following conditions are met:\n  - `visibility` is set to `visible`, i.e., the graphic is visible\n  - Triggered in the graphic fill area, not affected by the value of `fill`\n- **`visiblestroke`**: Will respond to events only if the following conditions are met:\n  - `visibility` is set to `visible`, i.e., the graphic is visible\n  - Triggered in the graphic stroke area, not affected by the value of `stroke`\n- **`visible`**: Will respond to events only if the following conditions are met:\n  - `visibility` is set to `visible`, i.e., the graphic is visible\n  - Triggered in the graphic fill or stroke area, not affected by the values of `fill` and `stroke`\n- **`painted`**: Will respond to events only if the following conditions are met:\n  - Triggered in the graphic fill area and `fill` takes a non-`none` value; or triggered in the graphic stroke area and `stroke` takes a non-`none` value\n  - Not affected by the value of `visibility`\n- **`fill`**: Will respond to events only if the following conditions are met:\n  - Triggered in the graphic fill area, not affected by the value of `fill`\n  - Not affected by the value of `visibility`\n- **`stroke`**: Will respond to events only if the following conditions are met:\n  - Triggered in the graphic stroke area, not affected by the value of `stroke`\n  - Not affected by the value of `visibility`\n- **`all`**: Will respond to events as long as entering the fill and stroke areas of the graphic, not affected by the values of `fill`, `stroke`, and `visibility`\n\n**Usage Examples:**\n\n```js\n// Example 1: Only stroke area responds to events\nconst graph = new Graph({\n  node: {\n    style: {\n      fill: 'none',\n      stroke: '#000',\n      lineWidth: 2,\n      pointerEvents: 'stroke', // Only stroke responds to events\n    },\n  },\n});\n\n// Example 2: Completely unresponsive to events\nconst graph = new Graph({\n  node: {\n    style: {\n      pointerEvents: 'none', // Node does not respond to any events\n    },\n  },\n});\n```\n\n#### Cursor\n\nAvailable values: `auto` | `default` | `none` | `context-menu` | `help` | `pointer` | `progress` | `wait` | `cell` | `crosshair` | `text` | `vertical-text` | `alias` | `copy` | `move` | `no-drop` | `not-allowed` | `grab` | `grabbing` | `all-scroll` | `col-resize` | `row-resize` | `n-resize` | `e-resize` | `s-resize` | `w-resize` | `ne-resize` | `nw-resize` | `se-resize` | `sw-resize` | `ew-resize` | `ns-resize` | `nesw-resize` | `nwse-resize` | `zoom-in` | `zoom-out`\n\n### Label Style\n\nLabels are used to display text information of nodes, supporting various style configurations and layout methods. Here are common usage scenarios:\n\n#### Basic Text Label\n\nThe simplest text label configuration:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 120,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      labelText: 'Node Name',\n      labelFill: '#262626',\n      labelFontSize: 12,\n      labelPlacement: 'bottom',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Multi-line Text Label\n\nWhen text is long, you can set automatic line wrapping:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 120,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      labelText: 'This is a very long node name that needs line wrapping',\n      labelWordWrap: true,\n      labelMaxWidth: '150%',\n      labelMaxLines: 3,\n      labelTextOverflow: 'ellipsis',\n      labelFill: '#434343',\n      labelPlacement: 'bottom',\n      labelTextAlign: 'center',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Label with Background\n\nAdding background to labels to improve readability:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 120,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      labelText: 'Important Node',\n      labelBackground: true,\n      labelBackgroundFill: 'rgba(250, 140, 22, 0.1)',\n      labelBackgroundRadius: 6,\n      labelPadding: [6, 12],\n      labelFill: '#D4380D',\n      labelFontWeight: 'bold',\n      labelPlacement: 'bottom',\n    },\n  },\n});\n\ngraph.render();\n```\n\nThe complete label style configuration is as follows:\n\n| Property                 | Description                                                                                                                        | Type                                                                        | Default   | Required |\n| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------- | -------- |\n| label                    | Whether to display node label                                                                                                      | boolean                                                                     | true      |          |\n| labelCursor              | Style displayed when mouse hovers over node label, [options](#cursor)                                                              | string                                                                      | `default` |          |\n| labelFill                | Node label text color                                                                                                              | string                                                                      | -         |          |\n| labelFontFamily          | Node label font family                                                                                                             | string                                                                      | -         |          |\n| labelFontSize            | Node label font size                                                                                                               | number                                                                      | 12        |          |\n| labelFontStyle           | Node label font style                                                                                                              | `normal` \\| `italic` \\| `oblique`                                           | -         |          |\n| labelFontVariant         | Node label font variant                                                                                                            | `normal` \\| `small-caps` \\| string                                          | -         |          |\n| labelFontWeight          | Node label font weight                                                                                                             | `normal` \\| `bold` \\| `bolder` \\| `lighter` \\| number                       | -         |          |\n| labelLeading             | Line spacing                                                                                                                       | number                                                                      | 0         |          |\n| labelLetterSpacing       | Node label letter spacing                                                                                                          | number \\| string                                                            | -         |          |\n| labelLineHeight          | Node label line height                                                                                                             | number \\| string                                                            | -         |          |\n| labelMaxLines            | Maximum number of lines for node label                                                                                             | number                                                                      | 1         |          |\n| labelMaxWidth            | Maximum width of node label, [options](#labelmaxwidth)                                                                             | number \\| string                                                            | `200%`    |          |\n| labelOffsetX             | Node label offset in x-axis direction                                                                                              | number                                                                      | 0         |          |\n| labelOffsetY             | Node label offset in y-axis direction                                                                                              | number                                                                      | 0         |          |\n| labelPadding             | Node label padding                                                                                                                 | number \\| number[]                                                          | 0         |          |\n| labelPlacement           | Position of node label relative to node main graphic, [options](#labelplacement)                                                   | string                                                                      | `bottom`  |          |\n| labelText                | Node label text content                                                                                                            | `string` \\| `(datum) => string`                                             | -         |          |\n| labelTextAlign           | Node label text horizontal alignment                                                                                               | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`               | `left`    |          |\n| labelTextBaseline        | Node label text baseline                                                                                                           | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom` | -         |          |\n| labelTextDecorationColor | Node label text decoration line color                                                                                              | string                                                                      | -         |          |\n| labelTextDecorationLine  | Node label text decoration line                                                                                                    | string                                                                      | -         |          |\n| labelTextDecorationStyle | Node label text decoration line style                                                                                              | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                       | -         |          |\n| labelTextOverflow        | Node label text overflow handling                                                                                                  | `clip` \\| `ellipsis` \\| string                                              | -         |          |\n| labelTextPath            | Node label text path                                                                                                               | Path                                                                        | -         |          |\n| labelWordWrap            | Whether node label enables automatic line wrapping. After enabling labelWordWrap, parts exceeding labelMaxWidth wrap automatically | boolean                                                                     | false     |          |\n| labelZIndex              | Node label rendering level                                                                                                         | number                                                                      | 0         |          |\n\n#### LabelPlacement\n\nAvailable values: `left` | `right` | `top` | `bottom` | `left-top` | `left-bottom` | `right-top` | `right-bottom` | `top-left` | `top-right` | `bottom-left` | `bottom-right` | `center` | `bottom`\n\n#### LabelMaxWidth\n\nAfter enabling automatic line wrapping `labelWordWrap`, text wraps when exceeding this width:\n\n- string: Defines maximum width as a percentage relative to node width. For example, `50%` means label width does not exceed half of the node width\n- number: Defines maximum width in pixels. For example, 100 means the maximum width of the label is 100 pixels\n\nFor example, setting multi-line label text:\n\n```json\n{\n  \"labelWordWrap\": true,\n  \"labelMaxWidth\": 200,\n  \"labelMaxLines\": 3\n}\n```\n\n### Label Background Style\n\nLabel background is used to display the background of node labels:\n\n| Property                      | Description                                                                                                                                                                         | Type                                     | Default   |\n| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | --------- |\n| labelBackground               | Whether to display node label background                                                                                                                                            | boolean                                  | false     |\n| labelBackgroundCursor         | Node label background mouse hover style, [options](#cursor)                                                                                                                         | string                                   | `default` |\n| labelBackgroundFill           | Node label background fill color                                                                                                                                                    | string                                   | -         |\n| labelBackgroundFillOpacity    | Node label background transparency                                                                                                                                                  | number                                   | 1         |\n| labelBackgroundHeight         | Node label background height                                                                                                                                                        | string \\| number                         | -         |\n| labelBackgroundLineDash       | Node label background dash configuration                                                                                                                                            | number \\| string \\|(number \\| string )[] | -         |\n| labelBackgroundLineDashOffset | Node label background dash offset                                                                                                                                                   | number                                   | -         |\n| labelBackgroundLineWidth      | Node label background stroke line width                                                                                                                                             | number                                   | -         |\n| labelBackgroundRadius         | Node label background border radius <br> - number: Uniform setting for four border radii <br> - number[]: Set four border radii separately, automatically supplement missing values | number \\| number[]                       | 0         |\n| labelBackgroundShadowBlur     | Node label background shadow blur degree                                                                                                                                            | number                                   | -         |\n| labelBackgroundShadowColor    | Node label background shadow color                                                                                                                                                  | string                                   | -         |\n| labelBackgroundShadowOffsetX  | Node label background shadow X direction offset                                                                                                                                     | number                                   | -         |\n| labelBackgroundShadowOffsetY  | Node label background shadow Y direction offset                                                                                                                                     | number                                   | -         |\n| labelBackgroundStroke         | Node label background stroke color                                                                                                                                                  | string                                   | -         |\n| labelBackgroundStrokeOpacity  | Node label background stroke transparency                                                                                                                                           | number \\| string                         | 1         |\n| labelBackgroundVisibility     | Whether node label background is visible                                                                                                                                            | `visible` \\| `hidden`                    | -         |\n| labelBackgroundZIndex         | Node label background rendering level                                                                                                                                               | number                                   | 1         |\n\n### Halo Style\n\nHalo is an effect displayed around the node's main graphic, usually used for highlighting or indicating special states of nodes.\n\n#### Basic Halo Effect\n\nAdding basic halo effect to nodes:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      lineWidth: 1.5,\n      halo: true,\n      haloStroke: '#1890FF',\n      haloLineWidth: 6,\n      haloStrokeOpacity: 0.3,\n    },\n  },\n});\n\ngraph.render();\n```\n\nThe complete halo style configuration is as follows:\n\n| Property          | Description                                                                                                                   | Type                   | Default                                 | Required |\n| ----------------- | ----------------------------------------------------------------------------------------------------------------------------- | ---------------------- | --------------------------------------- | -------- |\n| halo              | Whether to display node halo                                                                                                  | boolean                | false                                   |          |\n| haloCursor        | Node halo mouse hover style, [options](#cursor)                                                                               | string                 | `default`                               |          |\n| haloDraggable     | Whether node halo allows dragging                                                                                             | boolean                | true                                    |          |\n| haloDroppable     | Whether node halo allows receiving dragged elements                                                                           | boolean                | true                                    |          |\n| haloFillRule      | Node halo fill rule                                                                                                           | `nonzero` \\| `evenodd` | -                                       |          |\n| haloFilter        | Node halo filter                                                                                                              | string                 | -                                       |          |\n| haloLineWidth     | Node halo stroke width                                                                                                        | number                 | 3                                       |          |\n| haloPointerEvents | Whether node halo effect responds to pointer events, [options](#pointerevents)                                                | string                 | `none`                                  |          |\n| haloStroke        | Node halo stroke color, **this property is used to set the color of the halo around the node, helping to highlight the node** | string                 | Consistent with main graphic fill color |          |\n| haloStrokeOpacity | Node halo stroke color transparency                                                                                           | number                 | 0.25                                    |          |\n| haloVisibility    | Node halo visibility                                                                                                          | `visible` \\| `hidden`  | `visible`                               |          |\n| haloZIndex        | Node halo rendering level                                                                                                     | number                 | -1                                      |          |\n\n### Icon Style\n\nNode icons support three common usage methods: text icons, image icons, and IconFont icons. The configurations for these three methods are shown below:\n\n#### 1. Text Icons\n\nUsing text directly as icons, suitable for simple identifiers:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      fill: '#FFF0F6',\n      stroke: '#EB2F96',\n      lineWidth: 1.5,\n      iconText: 'A', // Icon text content\n      iconFill: '#C41D7F', // Deep pink icon\n      iconFontSize: 16,\n      iconFontWeight: 'bold',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 2. Image Icons\n\nUsing images as icons, supporting various image formats:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      fill: '#F6FFED',\n      stroke: '#52C41A',\n      lineWidth: 1.5,\n      iconSrc:\n        'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMTMuMDkgOC4yNkwyMSA5TDEzLjA5IDE1Ljc4TDEyIDIyTDEwLjkxIDE1Ljc4TDMgOUwxMC45MSA4LjI2TDEyIDJaIiBmaWxsPSIjNTJDNDFBIi8+Cjwvc3ZnPgo=',\n      iconWidth: 20,\n      iconHeight: 20,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 3. IconFont Icons\n\nUsing IconFont font icons, you need to import the corresponding font files first:\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      fill: '#E6F7FF', // Light blue background\n      stroke: '#1890FF', // Blue border\n      lineWidth: 1.5,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      iconFill: '#1890FF',\n    },\n  },\n});\n\ngraph.render();\n```\n\nThe complete icon style configuration is as follows:\n\n| Property                | Description                                          | Type                                                                        | Default                     |\n| ----------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------- |\n| icon                    | Whether to display node icon                         | boolean                                                                     | true                        |\n| iconFill                | Node icon text color                                 | string                                                                      | -                           |\n| iconFontFamily          | Node icon font family                                | string                                                                      | -                           |\n| iconFontSize            | Node icon font size                                  | number                                                                      | 16                          |\n| iconFontStyle           | Node icon font style                                 | `normal` \\| `italic` \\| `oblique`                                           | `normal`                    |\n| iconFontVariant         | Node icon font variant                               | `normal` \\| `small-caps` \\| string                                          | `normal`                    |\n| iconFontWeight          | Node icon font weight                                | number \\| string                                                            | `normal`                    |\n| iconHeight              | Node icon height                                     | number                                                                      | Half of main graphic height |\n| iconLetterSpacing       | Node icon text letter spacing                        | number \\| string                                                            | -                           |\n| iconLineHeight          | Node icon text line height                           | number \\| string                                                            | -                           |\n| iconMaxLines            | Maximum lines for node icon text                     | number                                                                      | 1                           |\n| iconRadius              | Node icon border radius                              | number                                                                      | 0                           |\n| iconSrc                 | Node image source. Has higher priority than iconText | string                                                                      | -                           |\n| iconText                | Node icon text                                       | string                                                                      | -                           |\n| iconTextAlign           | Node icon text horizontal alignment                  | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`               | `left`                      |\n| iconTextBaseline        | Node icon text baseline                              | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom` | `alphabetic`                |\n| iconTextDecorationColor | Node icon text decoration line color                 | string                                                                      | -                           |\n| iconTextDecorationLine  | Node icon text decoration line                       | string                                                                      | -                           |\n| iconTextDecorationStyle | Node icon text decoration line style                 | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                       | `solid`                     |\n| iconTextOverflow        | Node icon text overflow handling                     | `clip` \\| `ellipsis` \\| string                                              | `clip`                      |\n| iconWidth               | Node icon width                                      | number                                                                      | Half of main graphic width  |\n| iconWordWrap            | Whether node icon text automatically wraps           | boolean                                                                     | -                           |\n\n### Badge Style\n\nBadges are small markers displayed on nodes, usually used to show status, quantity, or other auxiliary information. Supports displaying multiple badges simultaneously with customizable positions.\n\n#### Single Badge\n\nAdding a simple badge to a node:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      badges: [\n        { text: 'NEW' }, // Default display at the top\n      ],\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Multiple Badges\n\nAdding multiple badges at different positions to a node:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      badge: true, // Whether to display badges\n      badges: [\n        { text: 'A', placement: 'right-top' },\n        { text: 'Important', placement: 'right' },\n        { text: 'Notice', placement: 'right-bottom' },\n      ],\n      badgePalette: ['#7E92B5', '#F4664A', '#FFBE3A'], // Badge background color palette\n      badgeFontSize: 7, // Badge font size\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Custom Badge Style\n\nCompletely customizing badge appearance:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      badges: [\n        {\n          text: '99+',\n          placement: 'right-top',\n          backgroundFill: '#FF4D4F', // Red background\n          fill: '#fff', // White text\n          fontSize: 10,\n          padding: [2, 6],\n          backgroundRadius: 8,\n        },\n      ],\n    },\n  },\n});\n\ngraph.render();\n```\n\nThe complete badge style configuration is as follows:\n\n| Property     | Description                      | Type                                  | Default                           |\n| ------------ | -------------------------------- | ------------------------------------- | --------------------------------- |\n| badge        | Whether the node displays badges | boolean                               | true                              |\n| badgePalette | Badge background color palette   | string[]                              | [`#7E92B5`, `#F4664A`, `#FFBE3A`] |\n| badges       | Node badge settings              | [BadgeStyleProps](#badgestyleprops)[] | -                                 |\n\n#### BadgeStyleProps\n\n| Property                 | Description                                                                                                                                                                                                                                                                                                         | Type                                                                                                                                                                   | Default      |\n| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |\n| background               | Whether node badge displays background                                                                                                                                                                                                                                                                              | boolean                                                                                                                                                                | true         |\n| backgroundCursor         | Node badge background mouse hover style, [options](#cursor)                                                                                                                                                                                                                                                         | string                                                                                                                                                                 | `default`    |\n| backgroundFill           | Node badge background fill color. If not specified, badgePalette is considered for allocation in order                                                                                                                                                                                                              | string                                                                                                                                                                 | -            |\n| backgroundFillOpacity    | Node badge background fill transparency                                                                                                                                                                                                                                                                             | number                                                                                                                                                                 | 1            |\n| backgroundFilter         | Node badge background filter                                                                                                                                                                                                                                                                                        | string                                                                                                                                                                 | -            |\n| backgroundHeight         | Node badge background height                                                                                                                                                                                                                                                                                        | number \\| string                                                                                                                                                       | -            |\n| backgroundLineDash       | Node badge background dash configuration                                                                                                                                                                                                                                                                            | number \\| string \\|(number \\| string )[]                                                                                                                               | -            |\n| backgroundLineDashOffset | Node badge background dash offset                                                                                                                                                                                                                                                                                   | number                                                                                                                                                                 | -            |\n| backgroundLineWidth      | Node badge background stroke line width                                                                                                                                                                                                                                                                             | number                                                                                                                                                                 | -            |\n| backgroundRadius         | Node badge background border radius <br> - number: Uniform setting for four border radii <br> - number[]: Set four border radii separately, automatically supplement missing values <br> - string: Similar to [CSS padding](https://developer.mozilla.org/en-US/docs/Web/CSS/padding) property, separated by spaces | number \\| number[] \\| string                                                                                                                                           | 0            |\n| backgroundShadowBlur     | Node badge background shadow blur degree                                                                                                                                                                                                                                                                            | number                                                                                                                                                                 | -            |\n| backgroundShadowColor    | Node badge background shadow color                                                                                                                                                                                                                                                                                  | string                                                                                                                                                                 | -            |\n| backgroundShadowOffsetX  | Node badge background shadow X direction offset                                                                                                                                                                                                                                                                     | number                                                                                                                                                                 | -            |\n| backgroundShadowOffsetY  | Node badge background shadow Y direction offset                                                                                                                                                                                                                                                                     | number                                                                                                                                                                 | -            |\n| backgroundStroke         | Node badge background stroke color                                                                                                                                                                                                                                                                                  | string                                                                                                                                                                 | -            |\n| backgroundStrokeOpacity  | Node badge background stroke transparency                                                                                                                                                                                                                                                                           | number \\| string                                                                                                                                                       | 1            |\n| backgroundVisibility     | Whether node badge background is visible                                                                                                                                                                                                                                                                            | `visible` \\| `hidden`                                                                                                                                                  | -            |\n| backgroundZIndex         | Node badge background rendering level                                                                                                                                                                                                                                                                               | number                                                                                                                                                                 | -            |\n| fill                     | Node badge text color                                                                                                                                                                                                                                                                                               | string                                                                                                                                                                 | -            |\n| fontFamily               | Node badge font family                                                                                                                                                                                                                                                                                              | string                                                                                                                                                                 | -            |\n| fontSize                 | Node badge font size                                                                                                                                                                                                                                                                                                | number                                                                                                                                                                 | 8            |\n| fontStyle                | Node badge font style                                                                                                                                                                                                                                                                                               | `normal` \\| `italic` \\| `oblique`                                                                                                                                      | `normal`     |\n| fontVariant              | Node badge font variant                                                                                                                                                                                                                                                                                             | `normal` \\| `small-caps` \\| string                                                                                                                                     | `normal`     |\n| fontWeight               | Node badge font weight                                                                                                                                                                                                                                                                                              | number \\| string                                                                                                                                                       | `normal`     |\n| lineHeight               | Node badge line height                                                                                                                                                                                                                                                                                              | string \\| number                                                                                                                                                       | -            |\n| lineWidth                | Node badge line width                                                                                                                                                                                                                                                                                               | string \\| number                                                                                                                                                       | -            |\n| maxLines                 | Maximum lines for node badge text                                                                                                                                                                                                                                                                                   | number                                                                                                                                                                 | 1            |\n| offsetX                  | Node badge offset in x-axis direction                                                                                                                                                                                                                                                                               | number                                                                                                                                                                 | 0            |\n| offsetY                  | Node badge offset in y-axis direction                                                                                                                                                                                                                                                                               | number                                                                                                                                                                 | 0            |\n| padding                  | Node badge padding                                                                                                                                                                                                                                                                                                  | number \\| number[]                                                                                                                                                     | 0            |\n| placement                | Position of node badge relative to node main graphic. If not specified, defaults to clockwise arrangement starting from top-right corner                                                                                                                                                                            | `left` \\| `right` \\| `top` \\| `bottom` \\| `left-top` \\| `left-bottom` \\| `right-top` \\| `right-bottom` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right` | -            |\n| text                     | Node badge text content                                                                                                                                                                                                                                                                                             | string                                                                                                                                                                 | -            |\n| textAlign                | Node badge text horizontal alignment                                                                                                                                                                                                                                                                                | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`                                                                                                          | `left`       |\n| textBaseline             | Node badge text baseline                                                                                                                                                                                                                                                                                            | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom`                                                                                            | `alphabetic` |\n| textDecorationColor      | Node badge text decoration line color                                                                                                                                                                                                                                                                               | string                                                                                                                                                                 | -            |\n| textDecorationLine       | Node badge text decoration line                                                                                                                                                                                                                                                                                     | string                                                                                                                                                                 | -            |\n| textDecorationStyle      | Node badge text decoration line style                                                                                                                                                                                                                                                                               | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                                                                                                                  | `solid`      |\n| textOverflow             | Node badge text overflow handling                                                                                                                                                                                                                                                                                   | `clip` \\| `ellipsis` \\| string                                                                                                                                         | `clip`       |\n| visibility               | Whether node badge is visible                                                                                                                                                                                                                                                                                       | `visible` \\| `hidden`                                                                                                                                                  | -            |\n| wordWrap                 | Whether node badge text automatically wraps                                                                                                                                                                                                                                                                         | boolean                                                                                                                                                                | -            |\n| zIndex                   | Node badge rendering level                                                                                                                                                                                                                                                                                          | number                                                                                                                                                                 | 3            |\n\n### Port Style\n\nPorts are connection points on nodes, used to connect edges. Supports adding multiple ports at different positions on nodes with customizable styles.\n\n#### Basic Ports\n\nAdding four basic directional ports to a node:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      port: true,\n      ports: [\n        { key: 'top', placement: 'top', fill: '#7E92B5' },\n        { key: 'right', placement: 'right', fill: '#F4664A' },\n        { key: 'bottom', placement: 'bottom', fill: '#FFBE3A' },\n        { key: 'left', placement: 'left', fill: '#D580FF' },\n      ],\n      portR: 3,\n      portLineWidth: 1,\n      portStroke: '#fff',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Custom Position Ports\n\nUsing percentages or absolute coordinates to precisely position ports:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      ports: [\n        { key: 'custom1', placement: [0.2, 0] }, // Relative position: 20% from top-left\n        { key: 'custom2', placement: [0.8, 0] }, // Relative position: 80% from top-right\n        { key: 'custom3', placement: [1, 0.5] }, // Relative position: right center\n      ],\n      portR: 4,\n      portLineWidth: 1,\n      portStroke: '#fff',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### Differentiated Port Styles\n\nSetting different styles for different ports:\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      ports: [\n        {\n          key: 'input',\n          placement: 'left',\n          fill: '#52C41A', // Green input port\n          r: 4,\n        },\n        {\n          key: 'output',\n          placement: 'right',\n          fill: '#FF4D4F', // Red output port\n          r: 4,\n        },\n      ],\n      portStroke: '#fff', // Unified stroke color\n      portLineWidth: 2,\n    },\n  },\n});\n\ngraph.render();\n```\n\nThe complete port style configuration is as follows:\n\n| Property | Description                                                  | Type                                | Default | Required |\n| -------- | ------------------------------------------------------------ | ----------------------------------- | ------- | -------- |\n| port     | Whether the node displays ports                              | boolean                             | true    |          |\n| ports    | Node port configuration, supports configuring multiple ports | [PortStyleProps](#portstyleprops)[] | -       |          |\n\n#### PortStyleProps\n\n| Property          | Description                                                                                                                                                                                                                                                  | Type                                                                                                                                                                                                   | Default   | Required |\n| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | -------- |\n| key               | Key value of node port, defaults to the index of the node port                                                                                                                                                                                               | string                                                                                                                                                                                                 | -         |          |\n| placement         | Position of node port relative to node main graphic                                                                                                                                                                                                          | `left` \\| `right` \\| `top` \\| `bottom` \\| `center` \\| `left-top` \\| `left-bottom` \\| `right-top` \\| `right-bottom` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right` \\| [number, number] | -         | ✓        |\n| r                 | Node port radius <br> - If set to undefined, the port is treated as a point, not displayed on canvas but exists, edges will preferentially connect to the nearest port <br> - If set to a number, the port is treated as a circle with radius specified here | number                                                                                                                                                                                                 | -         |          |\n| linkToCenter      | Whether edges connect to the center of the node port <br> - If true, edges connect to the center of the node port <br> - If false, edges connect to the edge of the node port                                                                                | boolean                                                                                                                                                                                                | false     |          |\n| cursor            | Node port mouse hover style, [options](#cursor)                                                                                                                                                                                                              | string                                                                                                                                                                                                 | `default` |          |\n| fill              | Node port fill color                                                                                                                                                                                                                                         | string                                                                                                                                                                                                 | -         |          |\n| fillOpacity       | Node port fill transparency                                                                                                                                                                                                                                  | number                                                                                                                                                                                                 | 1         |          |\n| isBillboard       | Whether node port has Billboard effect                                                                                                                                                                                                                       | boolean                                                                                                                                                                                                | -         |          |\n| isSizeAttenuation | Whether node port enables size attenuation                                                                                                                                                                                                                   | boolean                                                                                                                                                                                                | -         |          |\n| lineDash          | Node port stroke dash configuration                                                                                                                                                                                                                          | number \\| string \\|(number \\| string )[]                                                                                                                                                               | -         |          |\n| lineDashOffset    | Node port stroke dash offset                                                                                                                                                                                                                                 | number                                                                                                                                                                                                 | -         |          |\n| lineWidth         | Node port stroke line width                                                                                                                                                                                                                                  | number                                                                                                                                                                                                 | -         |          |\n| shadowBlur        | Node port shadow blur degree                                                                                                                                                                                                                                 | number                                                                                                                                                                                                 | -         |          |\n| shadowColor       | Node port shadow color                                                                                                                                                                                                                                       | string                                                                                                                                                                                                 | -         |          |\n| shadowOffsetX     | Node port shadow X direction offset                                                                                                                                                                                                                          | number                                                                                                                                                                                                 | -         |          |\n| shadowOffsetY     | Node port shadow Y direction offset                                                                                                                                                                                                                          | number                                                                                                                                                                                                 | -         |          |\n| stroke            | Node port stroke color                                                                                                                                                                                                                                       | string                                                                                                                                                                                                 | -         |          |\n| strokeOpacity     | Node port stroke transparency                                                                                                                                                                                                                                | number \\| string                                                                                                                                                                                       | 1         |          |\n| visibility        | Whether node port is visible                                                                                                                                                                                                                                 | `visible` \\| `hidden`                                                                                                                                                                                  | `visible` |          |\n| zIndex            | Node port rendering level                                                                                                                                                                                                                                    | number                                                                                                                                                                                                 | 2         |          |\n\n## State\n\nIn some interactive behaviors, such as clicking to select a node or hovering to activate an edge, only certain state identifications are made on the element. To reflect these states in the visual space seen by end users, we need to set different graphic element styles for different states to respond to changes in the state of the graphic element.\n\nG6 provides several built-in states, including selected, highlight, active, inactive, and disabled. In addition, it also supports custom states to meet more specific needs. For each state, developers can define a set of style rules that will override the element's default styles.\n\n<img width=\"520\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*t2qvRp92itkAAAAAAAAAAAAADmJ7AQ/original\" />\n\nThe data structure is as follows:\n\n```typescript\ntype NodeState = {\n  [state: string]: NodeStyle;\n};\n```\n\nFor example, when a node is in the `focus` state, you can add a stroke with width 3 and orange color.\n\n```js {4-7}\nconst graph = new Graph({\n  node: {\n    state: {\n      focus: {\n        lineWidth: 3, // Stroke width\n        stroke: 'orange', // Stroke color\n      },\n    },\n  },\n});\n```\n\nThe effect is shown in the figure below:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', states: ['focus'] }],\n  },\n  node: {\n    state: {\n      focus: {\n        lineWidth: 3,\n        stroke: 'orange',\n      },\n    },\n  },\n});\n\ngraph.render();\n```\n\n## Animation\n\nDefines animation effects for nodes, supporting the following two configuration methods:\n\n1. Disable all node animations\n\n```json\n{\n  \"node\": {\n    \"animation\": false\n  }\n}\n```\n\n2. Configure stage animations\n\nStage animations refer to animation effects when nodes enter the canvas, update, or leave the canvas. Currently supported stages include:\n\n- `enter`: Animation when nodes enter the canvas\n- `update`: Animation when nodes are updated\n- `exit`: Animation when nodes leave the canvas\n- `show`: Animation when nodes are shown from hidden state\n- `hide`: Animation when nodes are hidden\n- `collapse`: Animation when nodes are collapsed\n- `expand`: Animation when nodes are expanded\n\nYou can refer to [Animation Paradigm](/en/manual/animation/animation#animation-paradigm) to use animation syntax to configure nodes, such as:\n\n```json\n{\n  \"node\": {\n    \"animation\": {\n      \"update\": [\n        {\n          \"fields\": [\"x\", \"y\"], // Only animate x and y properties during updates\n          \"duration\": 1000, // Animation duration\n          \"easing\": \"linear\" // Easing function\n        }\n      ]\n    }\n  }\n}\n```\n\nYou can also use built-in animation effects:\n\n```json\n{\n  \"node\": {\n    \"animation\": {\n      \"enter\": \"fade\", // Use fade animation\n      \"update\": \"translate\", // Use translate animation\n      \"exit\": \"fade\" // Use fade animation\n    }\n  }\n}\n```\n\nYou can pass false to disable animations for specific stages:\n\n```json\n{\n  \"node\": {\n    \"animation\": {\n      \"enter\": false // Disable node entrance animation\n    }\n  }\n}\n```\n\n## Palette\n\nDefines the color palette for nodes, i.e., predefined node color pool, and allocates according to rules, mapping colors to the `fill` property.\n\n> For the definition of palettes, please refer to [Palette](/en/manual/theme/palette).\n\n| Property | Description                                                                                                           | Type                          | Default |\n| -------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ------- |\n| color    | Palette colors. If the palette is registered, you can directly specify its registration name, or accept a color array | string \\| string[]            | -       |\n| field    | Specify the grouping field in element data. If not specified, defaults to id as the grouping field                    | string \\| ((datum) => string) | `id`    |\n| invert   | Whether to invert the palette                                                                                         | boolean                       | false   |\n| type     | Specify the current palette type. <br> - `group`: Discrete palette <br> - `value`: Continuous palette                 | `group` \\| `value`            | `group` |\n\nFor example, assigning node colors to a group of data by `category` field, so that nodes of the same category have the same color:\n\n```json\n{\n  \"node\": {\n    \"palette\": {\n      \"type\": \"group\",\n      \"field\": \"category\",\n      \"color\": [\"#1783FF\", \"#F08F56\", \"#D580FF\", \"#00C9C9\", \"#7863FF\"]\n    }\n  }\n}\n```\n\nThe effect is shown in the figure below:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 100,\n  data: {\n    nodes: new Array(10)\n      .fill(0)\n      .map((_, i) => ({ id: `node-${i}`, data: { category: ['A', 'B', 'C', 'D', 'E'][i % 5] } })),\n  },\n  layout: { type: 'grid', cols: 10 },\n  node: {\n    palette: {\n      type: 'group',\n      field: 'category',\n      color: ['#1783FF', '#F08F56', '#D580FF', '#00C9C9', '#7863FF'],\n    },\n  },\n});\n\ngraph.render();\n```\n\nYou can also use default configuration:\n\n```json\n{\n  \"node\": {\n    \"palette\": \"tableau\" // tableau is the palette name, defaults to assigning colors based on ID\n  }\n}\n```\n\nThe effect is shown in the figure below:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 100,\n  data: {\n    nodes: new Array(10)\n      .fill(0)\n      .map((_, i) => ({ id: `node-${i}`, data: { category: ['A', 'B', 'C', 'D', 'E'][i % 5] } })),\n  },\n  layout: { type: 'grid', cols: 10 },\n  node: {\n    palette: 'tableau',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/BaseNode.zh.md",
    "content": "---\ntitle: 节点通用配置项\norder: 1\n---\n\n本文介绍内置节点通用属性配置。\n\n## NodeOptions\n\n```js {5-9}\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  node: {\n    type: 'circle', // 节点类型\n    style: {}, // 节点样式\n    state: {}, // 状态样式\n    palette: {}, // 色板配置\n    animation: {}, // 动画配置\n  },\n});\n```\n\n| 属性      | 描述                                         | 类型                    | 默认值   | 必选 |\n| --------- | -------------------------------------------- | ----------------------- | -------- | ---- |\n| type      | 节点类型，内置节点类型名称或自定义节点的名称 | [Type](#type)           | `circle` |      |\n| style     | 节点样式配置，包括颜色、大小等               | [Style](#style)         | -        |      |\n| state     | 不同状态下的样式配置                         | [State](#state)         | -        |      |\n| palette   | 定义节点的色板，用于根据不同数据映射颜色     | [Palette](#palette)     | -        |      |\n| animation | 定义节点的动画效果                           | [Animation](#animation) | -        |      |\n\n## Type\n\n指定节点类型，内置节点类型名称或自定义节点的名称。默认为 `circle`(圆形)。**⚠️ 注意**：这里决定了主图形的形状。\n\n```js {3}\nconst graph = new Graph({\n  node: {\n    type: 'circle',\n  },\n});\n```\n\n**⚠️ 动态配置说明**：`type` 属性同样支持动态配置，可以根据节点数据动态选择节点类型：\n\n```js\nconst graph = new Graph({\n  node: {\n    // 静态配置\n    type: 'circle',\n\n    // 动态配置 - 箭头函数形式\n    type: (datum) => datum.data.nodeType || 'circle',\n\n    // 动态配置 - 普通函数形式（可访问 graph 实例）\n    type: function (datum) {\n      console.log(this); // graph 实例\n      return datum.data.category === 'important' ? 'diamond' : 'circle';\n    },\n  },\n});\n```\n\n可选值有：\n\n- `circle`：[圆形节点](/manual/element/node/circle)\n- `diamond`：[菱形节点](/manual/element/node/diamond)\n- `donut`：[甜甜圈节点](/manual/element/node/donut)\n- `ellipse`：[椭圆节点](/manual/element/node/ellipse)\n- `hexagon`：[六边形节点](/manual/element/node/hexagon)\n- `html`：[HTML 节点](/manual/element/node/html)\n- `image`：[图片节点](/manual/element/node/image)\n- `rect`：[矩形节点](/manual/element/node/rect)\n- `star`：[星形节点](/manual/element/node/star)\n- `triangle`：[三角形节点](/manual/element/node/triangle)\n\n## Style\n\n定义节点的样式，包括颜色、大小等。\n\n```js {3}\nconst graph = new Graph({\n  node: {\n    style: {},\n  },\n});\n```\n\n**⚠️ 动态配置说明**：以下所有样式属性都支持动态配置，即可以传入函数来根据节点数据动态计算属性值：\n\n```js\nconst graph = new Graph({\n  node: {\n    style: {\n      // 静态配置\n      fill: '#1783FF',\n\n      // 动态配置 - 箭头函数形式\n      stroke: (datum) => (datum.data.isActive ? '#FF0000' : '#000000'),\n\n      // 动态配置 - 普通函数形式（可访问 graph 实例）\n      lineWidth: function (datum) {\n        console.log(this); // graph 实例\n        return datum.data.importance > 5 ? 3 : 1;\n      },\n\n      // 嵌套属性也支持动态配置\n      labelText: (datum) => `节点: ${datum.id}`,\n      badges: (datum) => datum.data.tags.map((tag) => ({ text: tag })),\n    },\n  },\n});\n```\n\n其中 `datum` 参数为节点数据对象 (`NodeData`)，包含节点的所有数据信息。\n\n一个完整的节点由以下几部分构成：\n\n<img width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Ot4bSbBx97EAAAAAAAAAAAAADmJ7AQ/original\" />\n\n- `key` ：节点的主图形，表示节点的主要形状，例如矩形、圆形等；\n- `label` ：文本标签，通常用于展示节点的名称或描述；\n- `icon` ：图标图形，通常用于展示节点的图标，可以是图片或者文本图标；\n- `badge` ：默认位于节点右上角的徽标；\n- `halo` ：主图形周围展示的光晕效果的图形；\n- `port` ：节点上的连接点，用于连接边。\n\n以下样式配置将按原子图形依次说明：\n\n### 主图形样式\n\n主图形是节点的核心部分，定义了节点的基本形状和外观。以下是常见的配置场景：\n\n#### 基础样式配置\n\n设置节点的基本外观：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      fill: '#5B8FF9', // 蓝色填充\n      stroke: '#1A1A1A', // 深色描边\n      lineWidth: 2,\n      size: 40,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 透明度和阴影效果\n\n为节点添加透明度和阴影效果：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      fill: '#61DDAA',\n      fillOpacity: 0.85,\n      shadowColor: 'rgba(97, 221, 170, 0.4)',\n      shadowBlur: 12,\n      shadowOffsetX: 2,\n      shadowOffsetY: 4,\n      stroke: '#F0F0F0',\n      lineWidth: 1,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 虚线边框样式\n\n创建带虚线边框的节点：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      fill: '#FFF1F0',\n      stroke: '#F5222D',\n      lineWidth: 2,\n      lineDash: [6, 4],\n      lineCap: 'round',\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的主图形样式配置：\n\n| 属性                            | 描述                                                                                      | 类型                          | 默认值    | 必选 |\n| ------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------- | --------- | ---- |\n| collapsed                       | 当前节点/组合是否折叠                                                                     | boolean                       | false     |      |\n| cursor                          | 节点鼠标移入样式，[配置项](#cursor)                                                       | string                        | default   |      |\n| fill                            | 节点填充色                                                                                | string                        | `#1783FF` |      |\n| fillOpacity                     | 节点填充色透明度                                                                          | number \\| string              | 1         |      |\n| increasedLineWidthForHitTesting | 当 lineWidth 较小时，可交互区域也随之变小，有时我们想增大这个区域，让\"细线\"更容易被拾取到 | number                        | 0         |      |\n| lineCap                         | 节点描边端点样式                                                                          | `round` \\| `square` \\| `butt` | `butt`    |      |\n| lineDash                        | 节点描边虚线样式                                                                          | number[]                      | -         |      |\n| lineDashOffset                  | 节点描边虚线偏移量                                                                        | number                        | -         |      |\n| lineJoin                        | 节点描边连接处样式                                                                        | `round` \\| `bevel` \\| `miter` | `miter`   |      |\n| lineWidth                       | 节点描边宽度                                                                              | number                        | 1         |      |\n| opacity                         | 节点透明度                                                                                | number \\| string              | 1         |      |\n| pointerEvents                   | 节点如何响应指针事件，[配置项](#pointerevents)                                            | string                        | `auto`    |      |\n| shadowBlur                      | 节点阴影模糊度                                                                            | number                        | -         |      |\n| shadowColor                     | 节点阴影颜色                                                                              | string                        | -         |      |\n| shadowOffsetX                   | 节点阴影在 x 轴方向上的偏移量                                                             | number \\| string              | -         |      |\n| shadowOffsetY                   | 节点阴影在 y 轴方向上的偏移量                                                             | number \\| string              | -         |      |\n| shadowType                      | 节点阴影类型                                                                              | `inner` \\| `outer`            | `outer`   |      |\n| size                            | 节点大小，快捷设置节点宽高，[配置项](#size)                                               | number \\| number[]            | 32        |      |\n| stroke                          | 节点描边色                                                                                | string                        | `#000`    |      |\n| strokeOpacity                   | 节点描边色透明度                                                                          | number \\| string              | 1         |      |\n| transform                       | transform 属性允许你旋转、缩放、倾斜或平移给定节点                                        | string                        | -         |      |\n| transformOrigin                 | 旋转与缩放中心，也称作变换中心                                                            | string                        | -         |      |\n| visibility                      | 节点是否可见                                                                              | `visible` \\| `hidden`         | `visible` |      |\n| x                               | 节点 x 坐标                                                                               | number                        | 0         |      |\n| y                               | 节点 y 坐标                                                                               | number                        | 0         |      |\n| z                               | 节点 z 坐标                                                                               | number                        | 0         |      |\n| zIndex                          | 节点渲染层级                                                                              | number                        | 0         |      |\n\n#### Size\n\n节点大小，快捷设置节点宽高，支持三种配置方式：\n\n- number：表示节点宽高相同为指定值\n- [number, number]：表示节点宽高分别为数组元素依次表示节点的宽度、高度\n- [number, number, number]：表示节点宽高分别为数组元素依次表示节点的宽度、高度以及深度\n\n#### PointerEvents\n\n`pointerEvents` 属性控制图形如何响应交互事件，可参考 [MDN 文档](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events)。\n\n可选值有：`visible` | `visiblepainted` | `visiblestroke` | `non-transparent-pixel` | `visiblefill` | `visible` | `painted` | `fill` | `stroke` | `all` | `none` | `auto` | `inherit` | `initial` | `unset`\n\n简而言之，`fill`、`stroke` 和 `visibility` 都可以独立或组合影响拾取判定行为。目前支持以下关键词：\n\n- **`auto`**：默认值，等同于 `visiblepainted`\n- **`none`**：永远不会成为响应事件的目标\n- **`visiblepainted`**：满足以下条件才会响应事件：\n  - `visibility` 设置为 `visible`，即图形为可见的\n  - 在图形填充区域触发同时 `fill` 取非 `none` 的值；或者在图形描边区域触发同时 `stroke` 取非 `none` 的值\n- **`visiblefill`**：满足以下条件才会响应事件：\n  - `visibility` 设置为 `visible`，即图形为可见的\n  - 在图形填充区域触发，不受 `fill` 取值的影响\n- **`visiblestroke`**：满足以下条件才会响应事件：\n  - `visibility` 设置为 `visible`，即图形为可见的\n  - 在图形描边区域触发，不受 `stroke` 取值的影响\n- **`visible`**：满足以下条件才会响应事件：\n  - `visibility` 设置为 `visible`，即图形为可见的\n  - 在图形填充或者描边区域触发，不受 `fill` 和 `stroke` 取值的影响\n- **`painted`**：满足以下条件才会响应事件：\n  - 在图形填充区域触发同时 `fill` 取非 `none` 的值；或者在图形描边区域触发同时 `stroke` 取非 `none` 的值\n  - 不受 `visibility` 取值的影响\n- **`fill`**：满足以下条件才会响应事件：\n  - 在图形填充区域触发，不受 `fill` 取值的影响\n  - 不受 `visibility` 取值的影响\n- **`stroke`**：满足以下条件才会响应事件：\n  - 在图形描边区域触发，不受 `stroke` 取值的影响\n  - 不受 `visibility` 取值的影响\n- **`all`**：只要进入图形的填充和描边区域就会响应事件，不会受 `fill`、`stroke`、`visibility` 的取值影响\n\n**使用示例：**\n\n```js\n// 示例1：只有描边区域响应事件\nconst graph = new Graph({\n  node: {\n    style: {\n      fill: 'none',\n      stroke: '#000',\n      lineWidth: 2,\n      pointerEvents: 'stroke', // 只有描边响应事件\n    },\n  },\n});\n\n// 示例2：完全不响应事件\nconst graph = new Graph({\n  node: {\n    style: {\n      pointerEvents: 'none', // 节点不响应任何事件\n    },\n  },\n});\n```\n\n#### Cursor\n\n可选值有：`auto` | `default` | `none` | `context-menu` | `help` | `pointer` | `progress` | `wait` | `cell` | `crosshair` | `text` | `vertical-text` | `alias` | `copy` | `move` | `no-drop` | `not-allowed` | `grab` | `grabbing` | `all-scroll` | `col-resize` | `row-resize` | `n-resize` | `e-resize` | `s-resize` | `w-resize` | `ne-resize` | `nw-resize` | `se-resize` | `sw-resize` | `ew-resize` | `ns-resize` | `nesw-resize` | `nwse-resize` | `zoom-in` | `zoom-out`\n\n### 标签样式\n\n标签用于显示节点的文本信息，支持多种样式配置和布局方式。以下是常见的使用场景：\n\n#### 基础文本标签\n\n最简单的文本标签配置：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 120,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      labelText: '节点名称',\n      labelFill: '#262626',\n      labelFontSize: 12,\n      labelPlacement: 'bottom',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 多行文本标签\n\n当文本较长时，可以设置自动换行：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 120,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      labelText: '这是一个很长的节点名称需要换行显示',\n      labelWordWrap: true,\n      labelMaxWidth: '150%',\n      labelMaxLines: 3,\n      labelTextOverflow: 'ellipsis',\n      labelFill: '#434343',\n      labelPlacement: 'bottom',\n      labelTextAlign: 'center',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 带背景的标签\n\n为标签添加背景，提高可读性：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 120,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      labelText: '重要节点',\n      labelBackground: true,\n      labelBackgroundFill: 'rgba(250, 140, 22, 0.1)',\n      labelBackgroundRadius: 6,\n      labelPadding: [6, 12],\n      labelFill: '#D4380D',\n      labelFontWeight: 'bold',\n      labelPlacement: 'bottom',\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的标签样式配置：\n\n| 属性                     | 描述                                                                               | 类型                                                                        | 默认值    | 必选 |\n| ------------------------ | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------- | ---- |\n| label                    | 是否显示节点标签                                                                   | boolean                                                                     | true      |      |\n| labelCursor              | 鼠标移入节点标签时显示的样式，[配置项](#cursor)                                    | string                                                                      | `default` |      |\n| labelFill                | 节点标签文字颜色                                                                   | string                                                                      | -         |      |\n| labelFontFamily          | 节点标签字体族                                                                     | string                                                                      | -         |      |\n| labelFontSize            | 节点标签字体大小                                                                   | number                                                                      | 12        |      |\n| labelFontStyle           | 节点标签字体样式                                                                   | `normal` \\| `italic` \\| `oblique`                                           | -         |      |\n| labelFontVariant         | 节点标签字体变种                                                                   | `normal` \\| `small-caps` \\| string                                          | -         |      |\n| labelFontWeight          | 节点标签字体粗细                                                                   | `normal` \\| `bold` \\| `bolder` \\| `lighter` \\| number                       | -         |      |\n| labelLeading             | 行间距                                                                             | number                                                                      | 0         |      |\n| labelLetterSpacing       | 节点标签字间距                                                                     | number \\| string                                                            | -         |      |\n| labelLineHeight          | 节点标签行高                                                                       | number \\| string                                                            | -         |      |\n| labelMaxLines            | 节点标签最大行数                                                                   | number                                                                      | 1         |      |\n| labelMaxWidth            | 节点标签最大宽度，[配置项](#labelmaxwidth)                                         | number \\| string                                                            | `200%`    |      |\n| labelOffsetX             | 节点标签在 x 轴方向上的偏移量                                                      | number                                                                      | 0         |      |\n| labelOffsetY             | 节点标签在 y 轴方向上的偏移量                                                      | number                                                                      | 0         |      |\n| labelPadding             | 节点标签内边距                                                                     | number \\| number[]                                                          | 0         |      |\n| labelPlacement           | 节点标签相对于节点主图形的位置，[配置项](#labelplacement)                          | string                                                                      | `bottom`  |      |\n| labelText                | 节点标签文字内容                                                                   | `string` \\| `(datum) => string`                                             | -         |      |\n| labelTextAlign           | 节点标签文本水平对齐方式                                                           | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`               | `left`    |      |\n| labelTextBaseline        | 节点标签文本基线                                                                   | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom` | -         |      |\n| labelTextDecorationColor | 节点标签文本装饰线颜色                                                             | string                                                                      | -         |      |\n| labelTextDecorationLine  | 节点标签文本装饰线                                                                 | string                                                                      | -         |      |\n| labelTextDecorationStyle | 节点标签文本装饰线样式                                                             | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                       | -         |      |\n| labelTextOverflow        | 节点标签文本溢出处理方式                                                           | `clip` \\| `ellipsis` \\| string                                              | -         |      |\n| labelTextPath            | 节点标签文本路径                                                                   | Path                                                                        | -         |      |\n| labelWordWrap            | 节点标签是否开启自动折行。开启 labelWordWrap 后，超出 labelMaxWidth 的部分自动换行 | boolean                                                                     | false     |      |\n| labelZIndex              | 节点标签渲染层级                                                                   | number                                                                      | 0         |      |\n\n#### LabelPlacement\n\n可选值有：`left` | `right` | `top` | `bottom` | `left-top` | `left-bottom` | `right-top` | `right-bottom` | `top-left` | `top-right` | `bottom-left` | `bottom-right` | `center` | `bottom`\n\n#### LabelMaxWidth\n\n开启自动折行 `labelWordWrap` 后，超出该宽度则换行:\n\n- string: 表示以相对于节点宽度的百分比形式定义最大宽度。例如 `50%` 表示标签宽度不超过节点宽度的一半\n- number: 表示以像素值为单位定义最大宽度。例如 100 表示标签的最大宽度为 100 像素\n\n比如，设置多行标签文字：\n\n```json\n{\n  \"labelWordWrap\": true,\n  \"labelMaxWidth\": 200,\n  \"labelMaxLines\": 3\n}\n```\n\n### 标签背景样式\n\n标签背景用于显示节点标签的背景：\n\n| 属性                          | 描述                                                                                                           | 类型                                     | 默认值    |\n| ----------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | --------- |\n| labelBackground               | 节点标签背景是否显示                                                                                           | boolean                                  | false     |\n| labelBackgroundCursor         | 节点标签背景鼠标移入样式，[配置项](#cursor)                                                                    | string                                   | `default` |\n| labelBackgroundFill           | 节点标签背景填充色                                                                                             | string                                   | -         |\n| labelBackgroundFillOpacity    | 节点标签背景透明度                                                                                             | number                                   | 1         |\n| labelBackgroundHeight         | 节点标签背景高度                                                                                               | string \\| number                         | -         |\n| labelBackgroundLineDash       | 节点标签背景虚线配置                                                                                           | number \\| string \\|(number \\| string )[] | -         |\n| labelBackgroundLineDashOffset | 节点标签背景虚线偏移量                                                                                         | number                                   | -         |\n| labelBackgroundLineWidth      | 节点标签背景描边线宽                                                                                           | number                                   | -         |\n| labelBackgroundRadius         | 节点标签背景圆角半径 <br> - number: 统一设置四个圆角半径 <br> - number[]: 分别设置四个圆角半径，不足则自动补充 | number \\| number[]                       | 0         |\n| labelBackgroundShadowBlur     | 节点标签背景阴影模糊程度                                                                                       | number                                   | -         |\n| labelBackgroundShadowColor    | 节点标签背景阴影颜色                                                                                           | string                                   | -         |\n| labelBackgroundShadowOffsetX  | 节点标签背景阴影 X 方向偏移                                                                                    | number                                   | -         |\n| labelBackgroundShadowOffsetY  | 节点标签背景阴影 Y 方向偏移                                                                                    | number                                   | -         |\n| labelBackgroundStroke         | 节点标签背景描边颜色                                                                                           | string                                   | -         |\n| labelBackgroundStrokeOpacity  | 节点标签背景描边透明度                                                                                         | number \\| string                         | 1         |\n| labelBackgroundVisibility     | 节点标签背景是否可见                                                                                           | `visible` \\| `hidden`                    | -         |\n| labelBackgroundZIndex         | 节点标签背景渲染层级                                                                                           | number                                   | 1         |\n\n### 光晕样式\n\n光晕是围绕节点主图形显示的效果，通常用于高亮显示或表示节点的特殊状态。\n\n#### 基础光晕效果\n\n为节点添加基本的光晕效果：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      lineWidth: 1.5,\n      halo: true,\n      haloStroke: '#1890FF',\n      haloLineWidth: 6,\n      haloStrokeOpacity: 0.3,\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的光晕样式配置：\n\n| 属性              | 描述                                                                   | 类型                   | 默认值                       | 必选 |\n| ----------------- | ---------------------------------------------------------------------- | ---------------------- | ---------------------------- | ---- |\n| halo              | 节点光晕是否显示                                                       | boolean                | false                        |      |\n| haloCursor        | 节点光晕鼠标移入样式，[配置项](#cursor)                                | strig                  | `default`                    |      |\n| haloDraggable     | 节点光晕是否允许拖拽                                                   | boolean                | true                         |      |\n| haloDroppable     | 节点光晕是否允许接收被拖拽的元素                                       | boolean                | true                         |      |\n| haloFillRule      | 节点光晕填充规则                                                       | `nonzero` \\| `evenodd` | -                            |      |\n| haloFilter        | 节点光晕滤镜                                                           | string                 | -                            |      |\n| haloLineWidth     | 节点光晕描边宽度                                                       | number                 | 3                            |      |\n| haloPointerEvents | 节点光晕效果是否响应指针事件，[配置项](#pointerevents)                 | string                 | `none`                       |      |\n| haloStroke        | 节点光晕描边色，**此属性用于设置节点周围光晕的颜色，帮助突出显示节点** | string                 | 与主图形的填充色 `fill` 一致 |      |\n| haloStrokeOpacity | 节点光晕描边色透明度                                                   | number                 | 0.25                         |      |\n| haloVisibility    | 节点光晕可见性                                                         | `visible` \\| `hidden`  | `visible`                    |      |\n| haloZIndex        | 节点光晕渲染层级                                                       | number                 | -1                           |      |\n\n### 图标样式\n\n节点图标支持三种常见的使用方式：文字图标、图片图标和 IconFont 图标。下面分别展示这三种方式的配置：\n\n#### 1. 文字图标\n\n直接使用文字作为图标，适合简单的标识：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      fill: '#FFF0F6',\n      stroke: '#EB2F96',\n      lineWidth: 1.5,\n      iconText: 'A', // 图标文字内容\n      iconFill: '#C41D7F', // 深粉色图标\n      iconFontSize: 16,\n      iconFontWeight: 'bold',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 2. 图片图标\n\n使用图片作为图标，支持各种图片格式：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      fill: '#F6FFED',\n      stroke: '#52C41A',\n      lineWidth: 1.5,\n      iconSrc:\n        'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMTMuMDkgOC4yNkwyMSA5TDEzLjA5IDE1Ljc4TDEyIDIyTDEwLjkxIDE1Ljc4TDMgOUwxMC45MSA4LjI2TDEyIDJaIiBmaWxsPSIjNTJDNDFBIi8+Cjwvc3ZnPgo=',\n      iconWidth: 20,\n      iconHeight: 20,\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 3. IconFont 图标\n\n使用 IconFont 字体图标，需要先引入相应的字体文件：\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      fill: '#E6F7FF', // 淡蓝色背景\n      stroke: '#1890FF', // 蓝色边框\n      lineWidth: 1.5,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      iconFill: '#1890FF',\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的图标样式配置：\n\n| 属性                    | 描述                                | 类型                                                                        | 默认值           |\n| ----------------------- | ----------------------------------- | --------------------------------------------------------------------------- | ---------------- |\n| icon                    | 是否显示节点图标                    | boolean                                                                     | true             |\n| iconFill                | 节点图标文字颜色                    | string                                                                      | -                |\n| iconFontFamily          | 节点图标字体族                      | string                                                                      | -                |\n| iconFontSize            | 节点图标字体大小                    | number                                                                      | 16               |\n| iconFontStyle           | 节点图标字体样式                    | `normal` \\| `italic` \\| `oblique`                                           | `normal`         |\n| iconFontVariant         | 节点图标字体变种                    | `normal` \\| `small-caps` \\| string                                          | `normal`         |\n| iconFontWeight          | 节点图标字体粗细                    | number \\| string                                                            | `normal`         |\n| iconHeight              | 节点图标高度                        | number                                                                      | 主图形高度的一半 |\n| iconLetterSpacing       | 节点图标文本字间距                  | number \\| string                                                            | -                |\n| iconLineHeight          | 节点图标文本行高                    | number \\| string                                                            | -                |\n| iconMaxLines            | 节点图标文本最大行数                | number                                                                      | 1                |\n| iconRadius              | 节点图标圆角半径                    | number                                                                      | 0                |\n| iconSrc                 | 节点图片来源。其优先级高于 iconText | string \\| ((datum) => string)                                                | -                |\n| iconText                | 节点图标文字                        | string \\| ((datum) => string)                                               | -                |\n| iconTextAlign           | 节点图标文本水平对齐方式            | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`               | `left`           |\n| iconTextBaseline        | 节点图标文本基线                    | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom` | `alphabetic`     |\n| iconTextDecorationColor | 节点图标文本装饰线颜色              | string                                                                      | -                |\n| iconTextDecorationLine  | 节点图标文本装饰线                  | string                                                                      | -                |\n| iconTextDecorationStyle | 节点图标文本装饰线样式              | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                       | `solid`          |\n| iconTextOverflow        | 节点图标文本溢出处理方式            | `clip` \\| `ellipsis` \\| string                                              | `clip`           |\n| iconWidth               | 节点图标宽度                        | number                                                                      | 主图形宽度的一半 |\n| iconWordWrap            | 节点图标文本是否自动换行            | boolean                                                                     | -                |\n\n### 徽标样式\n\n徽标是节点上显示的小标记，通常用于展示状态、数量或其他辅助信息。支持多个徽标同时显示，并可自定义位置。\n\n#### 单个徽标\n\n为节点添加一个简单的徽标：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      badges: [\n        { text: 'NEW' }, // 默认显示在上方\n      ],\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 多个徽标\n\n为节点添加多个不同位置的徽标：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      badge: true, // 是否显示徽标\n      badges: [\n        { text: 'A', placement: 'right-top' },\n        { text: 'Important', placement: 'right' },\n        { text: 'Notice', placement: 'right-bottom' },\n      ],\n      badgePalette: ['#7E92B5', '#F4664A', '#FFBE3A'], // 徽标的背景色板\n      badgeFontSize: 7, // 徽标字体大小\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 自定义徽标样式\n\n完全自定义徽标的外观：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      badges: [\n        {\n          text: '99+',\n          placement: 'right-top',\n          backgroundFill: '#FF4D4F', // 红色背景\n          fill: '#fff', // 白色文字\n          fontSize: 10,\n          padding: [2, 6],\n          backgroundRadius: 8,\n        },\n      ],\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的徽标样式配置：\n\n| 属性         | 描述               | 类型                                  | 默认值                            |\n| ------------ | ------------------ | ------------------------------------- | --------------------------------- |\n| badge        | 节点是否显示徽标   | boolean                               | true                              |\n| badgePalette | 节点徽标的背景色板 | string[]                              | [`#7E92B5`, `#F4664A`, `#FFBE3A`] |\n| badges       | 节点徽标设置       | [BadgeStyleProps](#badgestyleprops)[] | -                                 |\n\n#### BadgeStyleProps\n\n| 属性                     | 描述                                                                                                                                                                                                                              | 类型                                                                                                                                                                   | 默认值       |\n| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |\n| background               | 节点徽标是否显示背景                                                                                                                                                                                                              | boolean                                                                                                                                                                | true         |\n| backgroundCursor         | 节点徽标背景鼠标移入样式，[配置项](#cursor)                                                                                                                                                                                       | string                                                                                                                                                                 | `default`    |\n| backgroundFill           | 节点徽标背景填充色。若不指定，优先考虑 badgePalette 按顺序分配                                                                                                                                                                    | string                                                                                                                                                                 | -            |\n| backgroundFillOpacity    | 节点徽标背景填充透明度                                                                                                                                                                                                            | number                                                                                                                                                                 | 1            |\n| backgroundFilter         | 节点徽标背景滤镜                                                                                                                                                                                                                  | string                                                                                                                                                                 | -            |\n| backgroundHeight         | 节点徽标背景高度                                                                                                                                                                                                                  | number \\| string                                                                                                                                                       | -            |\n| backgroundLineDash       | 节点徽标背景虚线配置                                                                                                                                                                                                              | number \\| string \\|(number \\| string )[]                                                                                                                               | -            |\n| backgroundLineDashOffset | 节点徽标背景虚线偏移量                                                                                                                                                                                                            | number                                                                                                                                                                 | -            |\n| backgroundLineWidth      | 节点徽标背景描边线宽                                                                                                                                                                                                              | number                                                                                                                                                                 | -            |\n| backgroundRadius         | 节点徽标背景圆角半径 <br> - number: 统一设置四个圆角半径 <br> - number[]: 分别设置四个圆角半径，会补足缺省的分量 <br> - string: 与 [CSS padding](https://developer.mozilla.org/zh-CN/docs/Web/CSS/padding) 属性类似，使用空格分隔 | number \\| number[] \\| string                                                                                                                                           | 0            |\n| backgroundShadowBlur     | 节点徽标背景阴影模糊程度                                                                                                                                                                                                          | number                                                                                                                                                                 | -            |\n| backgroundShadowColor    | 节点徽标背景阴影颜色                                                                                                                                                                                                              | string                                                                                                                                                                 | -            |\n| backgroundShadowOffsetX  | 节点徽标背景阴影 X 方向偏移                                                                                                                                                                                                       | number                                                                                                                                                                 | -            |\n| backgroundShadowOffsetY  | 节点徽标背景阴影 Y 方向偏移                                                                                                                                                                                                       | number                                                                                                                                                                 | -            |\n| backgroundStroke         | 节点徽标背景描边颜色                                                                                                                                                                                                              | string                                                                                                                                                                 | -            |\n| backgroundStrokeOpacity  | 节点徽标背景描边透明度                                                                                                                                                                                                            | number \\| string                                                                                                                                                       | 1            |\n| backgroundVisibility     | 节点徽标背景是否可见                                                                                                                                                                                                              | `visible` \\| `hidden`                                                                                                                                                  | -            |\n| backgroundZIndex         | 节点徽标背景渲染层级                                                                                                                                                                                                              | number                                                                                                                                                                 | -            |\n| fill                     | 节点徽标文字颜色                                                                                                                                                                                                                  | string                                                                                                                                                                 | -            |\n| fontFamily               | 节点徽标字体族                                                                                                                                                                                                                    | string                                                                                                                                                                 | -            |\n| fontSize                 | 节点徽标字体大小                                                                                                                                                                                                                  | number                                                                                                                                                                 | 8            |\n| fontStyle                | 节点徽标字体样式                                                                                                                                                                                                                  | `normal` \\| `italic` \\| `oblique`                                                                                                                                      | `normal`     |\n| fontVariant              | 节点徽标字体变种                                                                                                                                                                                                                  | `normal` \\| `small-caps` \\| string                                                                                                                                     | `normal`     |\n| fontWeight               | 节点徽标字体粗细                                                                                                                                                                                                                  | number \\| string                                                                                                                                                       | `normal`     |\n| lineHeight               | 节点徽标行高                                                                                                                                                                                                                      | string \\| number                                                                                                                                                       | -            |\n| lineWidth                | 节点徽标行宽                                                                                                                                                                                                                      | string \\| number                                                                                                                                                       | -            |\n| maxLines                 | 节点徽标文本最大行数                                                                                                                                                                                                              | number                                                                                                                                                                 | 1            |\n| offsetX                  | 节点徽标在 x 轴方向上的偏移量                                                                                                                                                                                                     | number                                                                                                                                                                 | 0            |\n| offsetY                  | 节点徽标在 y 轴方向上的偏移量                                                                                                                                                                                                     | number                                                                                                                                                                 | 0            |\n| padding                  | 节点徽标内边距                                                                                                                                                                                                                    | number \\| number[]                                                                                                                                                     | 0            |\n| placement                | 节点徽标相对于节点主图形的位置。若不指定，默认从右上角顺时针依次排放                                                                                                                                                              | `left` \\| `right` \\| `top` \\| `bottom` \\| `left-top` \\| `left-bottom` \\| `right-top` \\| `right-bottom` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right` | -            |\n| text                     | 节点徽标文字内容                                                                                                                                                                                                                  | string                                                                                                                                                                 | -            |\n| textAlign                | 节点徽标文本水平对齐方式                                                                                                                                                                                                          | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`                                                                                                          | `left`       |\n| textBaseline             | 节点徽标文本基线                                                                                                                                                                                                                  | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom`                                                                                            | `alphabetic` |\n| textDecorationColor      | 节点徽标文本装饰线颜色                                                                                                                                                                                                            | string                                                                                                                                                                 | -            |\n| textDecorationLine       | 节点徽标文本装饰线                                                                                                                                                                                                                | string                                                                                                                                                                 | -            |\n| textDecorationStyle      | 节点徽标文本装饰线样式                                                                                                                                                                                                            | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                                                                                                                  | `solid`      |\n| textOverflow             | 节点徽标文本溢出处理方式                                                                                                                                                                                                          | `clip` \\| `ellipsis` \\| string                                                                                                                                         | `clip`       |\n| visibility               | 节点徽标是否可见                                                                                                                                                                                                                  | `visible` \\| `hidden`                                                                                                                                                  | -            |\n| wordWrap                 | 节点徽标文本是否自动换行                                                                                                                                                                                                          | boolean                                                                                                                                                                | -            |\n| zIndex                   | 节点徽标渲染层级                                                                                                                                                                                                                  | number                                                                                                                                                                 | 3            |\n\n### 连接桩样式\n\n连接桩是节点上的连接点，用于连接边。支持在节点的不同位置添加多个连接桩，并可自定义样式。\n\n#### 基础连接桩\n\n为节点添加四个基本方向的连接桩：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      port: true,\n      ports: [\n        { key: 'top', placement: 'top', fill: '#7E92B5' },\n        { key: 'right', placement: 'right', fill: '#F4664A' },\n        { key: 'bottom', placement: 'bottom', fill: '#FFBE3A' },\n        { key: 'left', placement: 'left', fill: '#D580FF' },\n      ],\n      portR: 3,\n      portLineWidth: 1,\n      portStroke: '#fff',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 自定义位置连接桩\n\n使用百分比或绝对坐标精确定位连接桩：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      ports: [\n        { key: 'custom1', placement: [0.2, 0] }, // 相对位置：左上角20%处\n        { key: 'custom2', placement: [0.8, 0] }, // 相对位置：右上角80%处\n        { key: 'custom3', placement: [1, 0.5] }, // 相对位置：右边中央\n      ],\n      portR: 4,\n      portLineWidth: 1,\n      portStroke: '#fff',\n    },\n  },\n});\n\ngraph.render();\n```\n\n#### 差异化连接桩样式\n\n为不同的连接桩设置不同的样式：\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: { nodes: [{ id: 'node1' }] },\n  node: {\n    style: {\n      ports: [\n        {\n          key: 'input',\n          placement: 'left',\n          fill: '#52C41A', // 绿色输入桩\n          r: 4,\n        },\n        {\n          key: 'output',\n          placement: 'right',\n          fill: '#FF4D4F', // 红色输出桩\n          r: 4,\n        },\n      ],\n      portStroke: '#fff', // 统一的描边颜色\n      portLineWidth: 2,\n    },\n  },\n});\n\ngraph.render();\n```\n\n以下为完整的连接桩样式配置：\n\n| 属性  | 描述                                 | 类型                                | 默认值 | 必选 |\n| ----- | ------------------------------------ | ----------------------------------- | ------ | ---- |\n| port  | 节点是否显示连接桩                   | boolean                             | true   |      |\n| ports | 节点连接桩配置项，支持配置多个连接桩 | [PortStyleProps](#portstyleprops)[] |        |      |\n\n#### PortStyleProps\n\n| 属性              | 描述                                                                                                                                                                               | 类型                                                                                                                                                                                                   | 默认值    | 必选 |\n| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ---- |\n| key               | 节点连接桩的键值，默认为节点连接桩的索引                                                                                                                                           | string                                                                                                                                                                                                 | -         |      |\n| placement         | 节点连接桩相对于节点主图形的位置                                                                                                                                                   | `left` \\| `right` \\| `top` \\| `bottom` \\| `center` \\| `left-top` \\| `left-bottom` \\| `right-top` \\| `right-bottom` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right` \\| [number, number] | -         | ✓    |\n| r                 | 节点连接桩半径 <br> - 如果设置为 undefined，则连接桩被视为一个点，不在画布上显示但存在，边会优先连接到最近的连接桩 <br> - 如果设置为数字，则连接桩被视为一个圆，圆的半径由此处指定 | number                                                                                                                                                                                                 | -         |      |\n| linkToCenter      | 边是否连接到节点连接桩的中心 <br> - 若为 true，则边连接到节点连接桩的中心 <br> - 若为 false，则边连接到节点连接桩的边缘                                                            | boolean                                                                                                                                                                                                | false     |      |\n| cursor            | 节点连接桩鼠标移入样式，[配置项](#cursor)                                                                                                                                          | string                                                                                                                                                                                                 | `default` |      |\n| fill              | 节点连接桩填充颜色                                                                                                                                                                 | string                                                                                                                                                                                                 | -         |      |\n| fillOpacity       | 节点连接桩填充透明度                                                                                                                                                               | number                                                                                                                                                                                                 | 1         |      |\n| isBillboard       | 节点连接桩是否为Billboard 效果                                                                                                                                                     | boolean                                                                                                                                                                                                | -         |      |\n| isSizeAttenuation | 节点连接桩是否启用大小衰减                                                                                                                                                         | boolean                                                                                                                                                                                                | -         |      |\n| lineDash          | 节点连接桩描边虚线配置                                                                                                                                                             | number \\| string \\|(number \\| string )[]                                                                                                                                                               | -         |      |\n| lineDashOffset    | 节点连接桩描边虚线偏移量                                                                                                                                                           | number                                                                                                                                                                                                 | -         |      |\n| lineWidth         | 节点连接桩描边线宽                                                                                                                                                                 | number                                                                                                                                                                                                 | -         |      |\n| shadowBlur        | 节点连接桩阴影模糊程度                                                                                                                                                             | number                                                                                                                                                                                                 | -         |      |\n| shadowColor       | 节点连接桩阴影颜色                                                                                                                                                                 | string                                                                                                                                                                                                 | -         |      |\n| shadowOffsetX     | 节点连接桩阴影 X 方向偏移                                                                                                                                                          | number                                                                                                                                                                                                 | -         |      |\n| shadowOffsetY     | 节点连接桩阴影 Y 方向偏移                                                                                                                                                          | number                                                                                                                                                                                                 | -         |      |\n| stroke            | 节点连接桩描边颜色                                                                                                                                                                 | string                                                                                                                                                                                                 | -         |      |\n| strokeOpacity     | 节点连接桩描边透明度                                                                                                                                                               | number \\| string                                                                                                                                                                                       | 1         |      |\n| visibility        | 节点连接桩是否可见                                                                                                                                                                 | `visible` \\| `hidden`                                                                                                                                                                                  | `visible` |      |\n| zIndex            | 节点连接桩渲染层级                                                                                                                                                                 | number                                                                                                                                                                                                 | 2         |      |\n\n## State\n\n在一些交互行为中，比如点击选中一个节点或鼠标悬停激活一个边，仅仅是在该元素做了某些状态的标识。为了将这些状态反应到终端用户所见的视觉空间中，我们需要为不同的状态设置不同的图元素样式，以响应该图元素状态的变化。\n\nG6 提供了几种内置的状态，包括选中（selected）、高亮（highlight）、激活（active）、不活跃（inactive）和禁用（disabled）。此外，它还支持自定义状态，以满足更特定的需求。对于每个状态，开发者可以定义一套样式规则，这些规则会覆盖元素的默认样式。\n\n<img width=\"520\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*t2qvRp92itkAAAAAAAAAAAAADmJ7AQ/original\" />\n\n数据结构如下：\n\n```typescript\ntype NodeState = {\n  [state: string]: NodeStyle;\n};\n```\n\n例如，当节点处于 `focus` 状态时，可以为其添加一个宽度为 3 且颜色为橙色的描边。\n\n```js {4-7}\nconst graph = new Graph({\n  node: {\n    state: {\n      focus: {\n        lineWidth: 3, // 描边宽度\n        stroke: 'orange', // 描边颜色\n      },\n    },\n  },\n});\n```\n\n效果如下图所示：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 200,\n  height: 100,\n  autoFit: 'center',\n  data: {\n    nodes: [{ id: 'node1', states: ['focus'] }],\n  },\n  node: {\n    state: {\n      focus: {\n        lineWidth: 3,\n        stroke: 'orange',\n      },\n    },\n  },\n});\n\ngraph.render();\n```\n\n## Animation\n\n定义节点的动画效果，支持下列两种配置方式：\n\n1. 关闭节点全部动画\n\n```json\n{\n  \"node\": {\n    \"animation\": false\n  }\n}\n```\n\n2. 配置阶段动画\n\n阶段动画是指节点在进入画布、更新、离开画布时的动画效果。目前支持的阶段包括：\n\n- `enter`: 节点进入画布时的动画\n- `update`: 节点更新时的动画\n- `exit`: 节点离开画布时的动画\n- `show`: 节点从隐藏状态显示时的动画\n- `hide`: 节点隐藏时的动画\n- `collapse`: 节点收起时的动画\n- `expand`: 节点展开时的动画\n\n你可以参考 [动画范式](/manual/animation/animation#动画范式) 使用动画语法来配置节点，如：\n\n```json\n{\n  \"node\": {\n    \"animation\": {\n      \"update\": [\n        {\n          \"fields\": [\"x\", \"y\"], // 更新时只对 x 和 y 属性进行动画\n          \"duration\": 1000, // 动画持续时间\n          \"easing\": \"linear\" // 缓动函数\n        }\n      ],\n  }\n}\n```\n\n也可以使用内置的动画效果：\n\n```json\n{\n  \"node\": {\n    \"animation\": {\n      \"enter\": \"fade\", // 使用渐变动画\n      \"update\": \"translate\", // 使用平移动画\n      \"exit\": \"fade\" // 使用渐变动画\n    }\n  }\n}\n```\n\n你可以传入 false 来关闭特定阶段的动画：\n\n```json\n{\n  \"node\": {\n    \"animation\": {\n      \"enter\": false // 关闭节点入场动画\n    }\n  }\n}\n```\n\n## Palette\n\n定义节点的色板，即预定义节点颜色池，并根据规则进行分配，将颜色映射到 `fill` 属性。\n\n> 有关色板的定义，请参考 [色板](/manual/theme/palette)。\n\n| 属性   | 描述                                                                | 类型                          | 默认值  |\n| ------ | ------------------------------------------------------------------- | ----------------------------- | ------- |\n| color  | 色板颜色。如果色板注册过，可以直接指定其注册名，也接受一个颜色数组  | string \\| string[]            | -       |\n| field  | 指定元素数据中的分组字段。若不指定，默认取 id 作为分组字段          | string \\| ((datum) => string) | `id`    |\n| invert | 是否反转色板                                                        | boolean                       | false   |\n| type   | 指定当前色板类型。<br> - `group`: 离散色板 <br> - `value`: 连续色板 | `group` \\| `value`            | `group` |\n\n如将一组数据按 `category` 字段分配节点颜色，使得同类别的节点颜色相同：\n\n```json\n{\n  \"node\": {\n    \"palette\": {\n      \"type\": \"group\",\n      \"field\": \"category\",\n      \"color\": [\"#1783FF\", \"#F08F56\", \"#D580FF\", \"#00C9C9\", \"#7863FF\"]\n    }\n  }\n}\n```\n\n效果如下图所示：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 100,\n  data: {\n    nodes: new Array(10)\n      .fill(0)\n      .map((_, i) => ({ id: `node-${i}`, data: { category: ['A', 'B', 'C', 'D', 'E'][i % 5] } })),\n  },\n  layout: { type: 'grid', cols: 10 },\n  node: {\n    palette: {\n      type: 'group',\n      field: 'category',\n      color: ['#1783FF', '#F08F56', '#D580FF', '#00C9C9', '#7863FF'],\n    },\n  },\n});\n\ngraph.render();\n```\n\n也可以使用默认配置：\n\n```json\n{\n  \"node\": {\n    \"palette\": \"tableau\" // tableau 为色板名，默认根据 ID 分配颜色\n  }\n}\n```\n\n效果如下图所示：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 100,\n  data: {\n    nodes: new Array(10)\n      .fill(0)\n      .map((_, i) => ({ id: `node-${i}`, data: { category: ['A', 'B', 'C', 'D', 'E'][i % 5] } })),\n  },\n  layout: { type: 'grid', cols: 10 },\n  node: {\n    palette: 'tableau',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Circle.en.md",
    "content": "---\ntitle: Circle Node\norder: 2\n---\n\n## Overview\n\nA circle is a symmetrical geometric shape with a uniform radius.\n\nApplicable scenarios:\n\n- Used to represent the size or weight of a node (controlled by radius).\n- Suitable for representing non-directional relationships, such as user nodes in social networks.\n- Commonly used in flowcharts, network diagrams, topology diagrams, etc.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/nodes/circle.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseNode](/en/manual/element/node/base-node))\n\n## Example\n\n### Built-in Circle Node Effect\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'circle',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Circle.zh.md",
    "content": "---\ntitle: 圆形节点 Circle\norder: 2\n---\n\n## 概述\n\n圆形是一个对称的几何形状，具有均匀的半径。\n\n适用场景：\n\n- 用于表示节点的大小或权重（通过半径控制）。\n- 适合表示无方向性的关系，如社交网络中的用户节点。\n- 常用于流程图、网络图、拓扑图等。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/nodes/circle.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见[BaseNode](/manual/element/node/base-node)\n\n## 示例\n\n### 内置圆形节点效果\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'circle',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Diamond.en.md",
    "content": "---\ntitle: Diamond Node\norder: 3\n---\n\n## Overview\n\nA diamond is a geometric shape with four equal sides and diagonal symmetry.\n\nApplicable scenarios:\n\n- Used to represent decision nodes, conditional judgments, or critical paths.\n\n- Suitable for representing decision nodes or key steps in flowcharts.\n\n- Commonly used in flowcharts, decision trees, network diagrams, etc.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/nodes/diamond.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseNode](/en/manual/element/node/base-node)\n\n## Example\n\n### Built-in Diamond Node Effect\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'diamond',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Diamond.zh.md",
    "content": "---\ntitle: 菱形节点 Diamond\norder: 3\n---\n\n## 概述\n\n菱形是一个四边相等的几何形状，具有对角线对称性。\n\n适用场景：\n\n- 用于表示决策节点、条件判断或关键路径。\n\n- 适合表示流程图中的判断节点或关键步骤。\n\n- 常用于流程图、决策树、网络图等。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/nodes/diamond.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseNode](/manual/element/node/base-node)\n\n## 示例\n\n### 内置菱形节点效果\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'diamond',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Donut.en.md",
    "content": "---\ntitle: Donut Node\norder: 4\n---\n\n## Overview\n\nThe donut node is a ring-shaped geometric figure composed of two concentric circles.\n\nApplicable scenarios:\n\n- Used to represent proportional data, such as completion progress and ratio analysis.\n\n- Suitable for representing multi-layered data, such as nested ring charts.\n\n- Commonly used in data visualization, dashboards, progress charts, etc.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/nodes/donut.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseNode](/en/manual/element/node/base-node)\n\n| Attribute           | Description                         | Type                                    | Default   | Required |\n| ------------------- | ----------------------------------- | --------------------------------------- | --------- | -------- |\n| donutFill           | Fill color                          | string                                  | `#1783FF` |          |\n| donutFillOpacity    | Fill color opacity                  | number \\| string                        | 1         |          |\n| donutLineCap        | Stroke end style                    | `round` \\| `square` \\| `butt`           | `butt`    |          |\n| donutLineDash       | Stroke dash style                   | number[]                                | -         |          |\n| donutLineDashOffset | Stroke dash offset                  | number                                  | -         |          |\n| donutLineJoin       | Stroke join style                   | `round` \\| `bevel` \\| `miter`           | `miter`   |          |\n| donutLineWidth      | Stroke width                        | number                                  | 1         |          |\n| donutOpacity        | Opacity                             | number \\| string                        | 1         |          |\n| donutPalette        | Color or palette name               | string \\| string[]                      | `tableau` |          |\n| donuts              | Donut data                          | number[] \\| [DonutRound](#donutround)[] | -         |          |\n| donutShadowBlur     | Shadow blur                         | number                                  | -         |          |\n| donutShadowColor    | Shadow color                        | string                                  | -         |          |\n| donutShadowOffsetX  | Shadow offset in x-axis direction   | number \\| string                        | -         |          |\n| donutShadowOffsetY  | Shadow offset in y-axis direction   | number \\| string                        | -         |          |\n| donutShadowType     | Shadow type                         | `inner` \\| `outer`                      | `outer`   |          |\n| donutStroke         | Stroke color                        | string                                  | `#000`    |          |\n| donutStrokeOpacity  | Stroke color opacity                | number \\| string                        | 1         |          |\n| donutVisibility     | Visibility of the shape             | `visible` \\| `hidden`                   | `visible` |          |\n| innerR              | Inner ring radius, percentage or px | string \\| number                        | 50%       |          |\n\n### DonutRound\n\n| Attribute      | Description                       | Type                          | Default   | Required |\n| -------------- | --------------------------------- | ----------------------------- | --------- | -------- |\n| color          | Color                             | string                        | -         |          |\n| fill           | Fill color                        | string                        | `#1783FF` |          |\n| fillOpacity    | Fill color opacity                | number \\| string              | 1         |          |\n| lineCap        | Stroke end style                  | `round` \\| `square` \\| `butt` | `butt`    |          |\n| lineDash       | Stroke dash style                 | number[]                      | -         |          |\n| lineDashOffset | Stroke dash offset                | number                        | -         |          |\n| lineJoin       | Stroke join style                 | `round` \\| `bevel` \\| `miter` | `miter`   |          |\n| lineWidth      | Stroke width                      | number                        | 1         |          |\n| opacity        | Opacity                           | number \\| string              | 1         |          |\n| shadowBlur     | Shadow blur                       | number                        | -         |          |\n| shadowColor    | Shadow color                      | string                        | -         |          |\n| shadowOffsetX  | Shadow offset in x-axis direction | number \\| string              | -         |          |\n| shadowOffsetY  | Shadow offset in y-axis direction | number \\| string              | -         |          |\n| shadowType     | Shadow type                       | `inner` \\| `outer`            | `outer`   |          |\n| stroke         | Stroke color                      | string                        | `#000`    |          |\n| strokeOpacity  | Stroke color opacity              | number \\| string              | 1         |          |\n| value          | Value for ratio calculation       | number                        | -         | ✓        |\n| visibility     | Visibility of the shape           | `visible` \\| `hidden`         | `visible` |          |\n\n## Example\n\n### Built-in Donut Node Effect\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default', index: 0 },\n    { id: 'halo', index: 1 },\n    { id: 'badges', index: 2 },\n    { id: 'ports', index: 3 },\n    {\n      id: 'active',\n      states: ['active'],\n      index: 4,\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n      index: 5,\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n      index: 6,\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n      index: 7,\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n      index: 8,\n    },\n  ],\n};\nconst graph = new Graph({\n  container: 'container',\n  animation: false,\n  data,\n  node: {\n    type: 'donut',\n    style: {\n      size: 80,\n      fill: '#DB9D0D',\n      innerR: 20,\n      donuts: (item) => {\n        const { index } = item;\n        if (index === 0) return [1, 2, 3]; // donuts数据类型为number[]时，根据值的大小决定环的占比\n\n        if (index === 1) {\n          return [\n            { value: 50, color: 'red' },\n            { value: 150, color: 'green' },\n            { value: 100, color: 'blue' },\n          ];\n        }\n\n        if (index === 4) {\n          return [\n            { value: 150, fill: 'pink', stroke: '#fff', lineWidth: 1 },\n            { value: 250, stroke: '#fff', lineWidth: 1 },\n            { value: 200, stroke: '#fff', lineWidth: 1 },\n          ];\n        }\n\n        return [100, 200, 100, 200];\n      },\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Donut.zh.md",
    "content": "---\ntitle: 甜甜圈节点 Donut\norder: 4\n---\n\n## 概述\n\n甜甜圈节点是一个环形几何形状，由内外两个同心圆组成。\n\n适用场景：\n\n- 用于表示比例数据，如完成进度、占比分析。\n\n- 适合表示多层数据，如嵌套的环形图。\n\n- 常用于数据可视化、仪表盘、进度图等。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/nodes/donut.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseNode](/manual/element/node/base-node)\n\n| 属性                | 描述                           | 类型                                        | 默认值    | 必选 |\n| ------------------- | ------------------------------ | ------------------------------------------- | --------- | ---- |\n| donutFill           | 填充色                         | string                                      | `#1783FF` |      |\n| donutFillOpacity    | 填充色透明度                   | number \\| string                            | 1         |      |\n| donutLineCap        | 描边端点样式                   | `round` \\| `square` \\| `butt`               | `butt`    |      |\n| donutLineDash       | 描边虚线样式                   | number[]                                    | -         |      |\n| donutLineDashOffset | 描边虚线偏移量                 | number                                      | -         |      |\n| donutLineJoin       | 描边连接处样式                 | `round` \\| `bevel` \\| `miter`               | `miter`   |      |\n| donutLineWidth      | 描边宽度                       | number                                      | 1         |      |\n| donutOpacity        | 透明度                         | number \\| string                            | 1         |      |\n| donutPalette        | 颜色或者色板名                 | string &#124; string[]                      | `tableau` |      |\n| donuts              | 圆环数据                       | number[] &#124; [DonutRound](#donutround)[] | -         |      |\n| donutShadowBlur     | 阴影模糊度                     | number                                      | -         |      |\n| donutShadowColor    | 阴影颜色                       | string                                      | -         |      |\n| donutShadowOffsetX  | 阴影在 x 轴方向上的偏移量      | number \\| string                            | -         |      |\n| donutShadowOffsetY  | 阴影在 y 轴方向上的偏移量      | number \\| string                            | -         |      |\n| donutShadowType     | 阴影类型                       | `inner` \\| `outer`                          | `outer`   |      |\n| donutStroke         | 描边色                         | string                                      | `#000`    |      |\n| donutStrokeOpacity  | 描边色透明度                   | number \\| string                            | 1         |      |\n| donutVisibility     | 图形是否可见                   | `visible` \\| `hidden`                       | `visible` |      |\n| innerR              | 内环半径，使用百分比或者像素值 | string &#124; number                        | 50%       |      |\n\n### DonutRound\n\n| 属性           | 描述                      | 类型                          | 默认值    | 必选 |\n| -------------- | ------------------------- | ----------------------------- | --------- | ---- |\n| color          | 颜色                      | string                        | -         |      |\n| fill           | 填充色                    | string                        | `#1783FF` |      |\n| fillOpacity    | 填充色透明度              | number \\| string              | 1         |      |\n| lineCap        | 描边端点样式              | `round` \\| `square` \\| `butt` | `butt`    |      |\n| lineDash       | 描边虚线样式              | number[]                      | -         |      |\n| lineDashOffset | 描边虚线偏移量            | number                        | -         |      |\n| lineJoin       | 描边连接处样式            | `round` \\| `bevel` \\| `miter` | `miter`   |      |\n| lineWidth      | 描边宽度                  | number                        | 1         |      |\n| opacity        | 透明度                    | number \\| string              | 1         |      |\n| shadowBlur     | 阴影模糊度                | number                        | -         |      |\n| shadowColor    | 阴影颜色                  | string                        | -         |      |\n| shadowOffsetX  | 阴影在 x 轴方向上的偏移量 | number \\| string              | -         |      |\n| shadowOffsetY  | 阴影在 y 轴方向上的偏移量 | number \\| string              | -         |      |\n| shadowType     | 阴影类型                  | `inner` \\| `outer`            | `outer`   |      |\n| stroke         | 描边色                    | string                        | `#000`    |      |\n| strokeOpacity  | 描边色透明度              | number \\| string              | 1         |      |\n| value          | 数值，用于计算比例        | number                        | -         | ✓    |\n| visibility     | 图形是否可见              | `visible` \\| `hidden`         | `visible` |      |\n\n## 示例\n\n### 内置甜甜圈节点效果\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default', index: 0 },\n    { id: 'halo', index: 1 },\n    { id: 'badges', index: 2 },\n    { id: 'ports', index: 3 },\n    {\n      id: 'active',\n      states: ['active'],\n      index: 4,\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n      index: 5,\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n      index: 6,\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n      index: 7,\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n      index: 8,\n    },\n  ],\n};\nconst graph = new Graph({\n  container: 'container',\n  animation: false,\n  data,\n  node: {\n    type: 'donut',\n    style: {\n      size: 80,\n      fill: '#DB9D0D',\n      innerR: 20,\n      donuts: (item) => {\n        const { index } = item;\n        if (index === 0) return [1, 2, 3]; // donuts数据类型为number[]时，根据值的大小决定环的占比\n\n        if (index === 1) {\n          return [\n            { value: 50, color: 'red' },\n            { value: 150, color: 'green' },\n            { value: 100, color: 'blue' },\n          ];\n        }\n\n        if (index === 4) {\n          return [\n            { value: 150, fill: 'pink', stroke: '#fff', lineWidth: 1 },\n            { value: 250, stroke: '#fff', lineWidth: 1 },\n            { value: 200, stroke: '#fff', lineWidth: 1 },\n          ];\n        }\n\n        return [100, 200, 100, 200];\n      },\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Ellipse.en.md",
    "content": "---\ntitle: Ellipse Node\norder: 5\n---\n\n## Overview\n\nAn ellipse is a geometric shape with asymmetrical major and minor axes.\n\nApplicable scenarios:\n\n- Used to represent dynamic nodes, asymmetric relationships, or special shapes.\n\n- Suitable for representing flowcharts, network diagrams, or topology diagrams.\n\n- Commonly used in flowcharts, network diagrams, topology diagrams, etc.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/nodes/ellipse.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseNode](/en/manual/element/node/base-node)\n\n## Example\n\n### Built-in Ellipse Node Effect\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'ellipse',\n    style: {\n      size: [45, 35],\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Ellipse.zh.md",
    "content": "---\ntitle: 椭圆形节点 Ellipse\norder: 5\n---\n\n## 概述\n\n椭圆是一个长轴和短轴不对称的几何形状。\n\n适用场景：\n\n- 用于表示动态节点、非对称关系或特殊形状。\n\n- 适合表示流程图、网络图或拓扑图。\n\n- 常用于流程图、网络图、拓扑图等。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/nodes/ellipse.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseNode](/manual/element/node/base-node)\n\n## 示例\n\n### 内置椭圆形节点效果\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'ellipse',\n    style: {\n      size: [45, 35],\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Hexagon.en.md",
    "content": "---\ntitle: Hexagon Node\norder: 6\n---\n\n## Overview\n\nA hexagon is a geometric shape with six equal sides, featuring a honeycomb structure.\n\nApplicable scenarios:\n\n- Used to represent honeycomb networks, molecular structures, or tightly packed nodes.\n\n- Suitable for representing network topology, molecular diagrams, or game maps.\n\n- Commonly used in network diagrams, topology diagrams, game design, etc.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/nodes/hexagon.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseNode](/en/manual/element/node/base-node)\n\n| Attribute | Description                                                        | Type   | Default                                 | Required |\n| --------- | ------------------------------------------------------------------ | ------ | --------------------------------------- | -------- |\n| outerR    | Outer radius, the distance from the hexagon's center to any vertex | number | Half of the minimum of width and height |          |\n\n## Example\n\n### Built-in Hexagon Node Effect\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'hexagon',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      outerR: 30, // 外半径\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Hexagon.zh.md",
    "content": "---\ntitle: 六边形节点 Hexagon\norder: 6\n---\n\n## 概述\n\n六边形是一个六边相等的几何形状，具有蜂窝状结构。\n\n适用场景：\n\n- 用于表示蜂窝网络、分子结构或紧密排列的节点。\n\n- 适合表示网络拓扑、分子图或游戏地图。\n\n- 常用于网络图、拓扑图、游戏设计等。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/nodes/hexagon.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseNode](/manual/element/node/base-node)\n\n| 属性   | 描述                                     | 类型   | 默认值                   | 必选 |\n| ------ | ---------------------------------------- | ------ | ------------------------ | ---- |\n| outerR | 外半径，是指从六边形中心到任意顶点的距离 | number | 默认为宽高的最小值的一半 |      |\n\n## 示例\n\n### 内置六边形节点效果\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'hexagon',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      outerR: 30, // 外半径\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Html.en.md",
    "content": "---\ntitle: HTML Node\norder: 7\n---\n\n## Overview\n\nHTML node is a custom rectangular area used to display HTML content. It allows you to embed arbitrary HTML elements within graph nodes, providing great flexibility for creating complex custom nodes.\n\nUse Cases:\n\n- Used to represent complex custom nodes such as tables, charts, or rich text\n- Suitable for representing custom visual elements or interactive components\n- Commonly used in custom charts, UI design, dashboards, and other scenarios\n- When you need to embed interactive elements like forms and buttons in nodes\n\n## Framework Support\n\n> **💡 Tips**：\n>\n> - **React Projects**: Recommended to use [React Node](/en/manual/element/node/react-node) for better component-based development experience\n> - **Vue Projects**: Vue Node is not currently supported, community contributions are welcome\n> - **Native HTML**: The HTML node introduced in this document is suitable for native HTML development\n\n## Online Demo\n\n<embed src=\"@/common/api/elements/nodes/html.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific properties, we will list them below. For all common style properties, see [BaseNode](/en/manual/element/node/base-node)\n\n| Property  | Description                                                                                           | Type                        | Default | Required |\n| --------- | ----------------------------------------------------------------------------------------------------- | --------------------------- | ------- | -------- |\n| dx        | Horizontal offset. HTML container defaults to top-left corner as origin, use dx for horizontal offset | number                      | 0       |          |\n| dy        | Vertical offset. HTML container defaults to top-left corner as origin, use dy for vertical offset     | number                      | 0       |          |\n| innerHTML | HTML content, can be string or `HTMLElement`                                                          | string &#124; `HTMLElement` | -       | ✓        |\n\n## Examples\n\n### Basic HTML Node\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', data: { location: 'East', status: 'error', ip: '192.168.1.2' } },\n      { id: 'node-2', data: { location: 'West', status: 'overload', ip: '192.168.1.3' } },\n      { id: 'node-3', data: { location: 'South', status: 'running', ip: '192.168.1.4' } },\n    ],\n  },\n  node: {\n    type: 'html',\n    style: {\n      size: [240, 80],\n      dx: -120,\n      dy: -40,\n      innerHTML: (d) => {\n        const ICON_MAP = {\n          error: '&#10060;',\n          overload: '&#9889;',\n          running: '&#9989;',\n        };\n\n        const COLOR_MAP = {\n          error: '#f5222d',\n          overload: '#faad14',\n          running: '#52c41a',\n        };\n\n        const {\n          data: { location, status, ip },\n        } = d;\n        const color = COLOR_MAP[status];\n\n        return `\n<div \n  style=\"\n    width:100%; \n    height: 100%; \n    background: ${color}bb; \n    border: 1px solid ${color};\n    color: #fff;\n    user-select: none;\n    display: flex; \n    padding: 10px;\n    border-radius: 8px;\n    \"\n>\n  <div style=\"display: flex;flex-direction: column;flex: 1;\">\n    <div style=\"font-weight: bold; font-size: 14px;\">\n      ${location} Node\n    </div>\n    <div style=\"font-size: 12px; margin-top: 4px;\">\n      status: ${status} ${ICON_MAP[status]}\n    </div>\n  </div>\n  <div>\n    <span style=\"border: 1px solid white; padding: 2px 6px; border-radius: 4px; font-size: 12px;\">\n      ${ip}\n    </span>\n  </div>\n</div>`;\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['drag-element', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n### HTML Node with Interactive Buttons\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'server-1', data: { name: 'Web Server', cpu: 45, memory: 67, status: 'online' } },\n      { id: 'server-2', data: { name: 'Database', cpu: 78, memory: 89, status: 'warning' } },\n      { id: 'server-3', data: { name: 'Cache Server', cpu: 23, memory: 34, status: 'offline' } },\n    ],\n  },\n  node: {\n    type: 'html',\n    style: {\n      size: [280, 210],\n      dx: -140,\n      dy: -105,\n      innerHTML: (d) => {\n        const { data } = d;\n        const statusColors = {\n          online: '#52c41a',\n          warning: '#faad14',\n          offline: '#f5222d',\n        };\n\n        return `\n<div style=\"\n  width: 100%; \n  height: 100%; \n  background: #fff;\n  border: 2px solid ${statusColors[data.status]};\n  border-radius: 12px;\n  padding: 16px;\n  box-shadow: 0 4px 12px rgba(0,0,0,0.1);\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n\">\n  <div style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;\">\n    <h3 style=\"margin: 0; font-size: 16px; color: #333;\">${data.name}</h3>\n    <span style=\"\n      background: ${statusColors[data.status]};\n      color: white;\n      padding: 2px 8px;\n      border-radius: 12px;\n      font-size: 12px;\n      font-weight: bold;\n    \">${data.status.toUpperCase()}</span>\n  </div>\n  \n  <div style=\"margin-bottom: 12px;\">\n    <div style=\"display: flex; justify-content: space-between; margin-bottom: 4px;\">\n      <span style=\"font-size: 12px; color: #666;\">CPU</span>\n      <span style=\"font-size: 12px; color: #333;\">${data.cpu}%</span>\n    </div>\n    <div style=\"background: #f0f0f0; height: 6px; border-radius: 3px; overflow: hidden;\">\n      <div style=\"background: ${data.cpu > 70 ? '#f5222d' : '#52c41a'}; height: 100%; width: ${data.cpu}%; transition: width 0.3s;\"></div>\n    </div>\n  </div>\n  \n  <div style=\"margin-bottom: 12px;\">\n    <div style=\"display: flex; justify-content: space-between; margin-bottom: 4px;\">\n      <span style=\"font-size: 12px; color: #666;\">Memory</span>\n      <span style=\"font-size: 12px; color: #333;\">${data.memory}%</span>\n    </div>\n    <div style=\"background: #f0f0f0; height: 6px; border-radius: 3px; overflow: hidden;\">\n      <div style=\"background: ${data.memory > 80 ? '#f5222d' : '#1890ff'}; height: 100%; width: ${data.memory}%; transition: width 0.3s;\"></div>\n    </div>\n  </div>\n  \n  <div style=\"display: flex; gap: 8px;\">\n    <button \n      onclick=\"handleRestart('${d.id}')\"\n      style=\"\n        flex: 1;\n        padding: 6px 12px;\n        background: #1890ff;\n        color: white;\n        border: none;\n        border-radius: 6px;\n        font-size: 12px;\n        cursor: pointer;\n        transition: background 0.2s;\n      \"\n      onmouseover=\"this.style.background='#40a9ff'\"\n      onmouseout=\"this.style.background='#1890ff'\"\n    >Restart</button>\n    <button \n      onclick=\"handleMonitor('${d.id}')\"\n      style=\"\n        flex: 1;\n        padding: 6px 12px;\n        background: #52c41a;\n        color: white;\n        border: none;\n        border-radius: 6px;\n        font-size: 12px;\n        cursor: pointer;\n        transition: background 0.2s;\n      \"\n      onmouseover=\"this.style.background='#73d13d'\"\n      onmouseout=\"this.style.background='#52c41a'\"\n    >Monitor</button>\n  </div>\n</div>`;\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n    cols: 2,\n  },\n  behaviors: ['drag-element', 'zoom-canvas'],\n});\n\n// Global functions to handle button clicks\nwindow.handleRestart = (nodeId) => {\n  console.log(`Restarting server: ${nodeId}`);\n  alert(`Restarting server ${nodeId}...`);\n};\n\nwindow.handleMonitor = (nodeId) => {\n  console.log(`Opening monitoring panel: ${nodeId}`);\n  alert(`Opening monitoring panel for server ${nodeId}`);\n};\n\ngraph.render();\n```\n\n### Form Input HTML Node\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'form-1', data: { title: 'User Information', type: 'user-form' } },\n      { id: 'form-2', data: { title: 'Configuration Panel', type: 'config-form' } },\n    ],\n  },\n  node: {\n    type: 'html',\n    style: {\n      size: [300, 400],\n      dx: -150,\n      dy: -200,\n      innerHTML: (d) => {\n        const { data } = d;\n\n        return `\n<div style=\"\n  width: 100%; \n  height: 100%; \n  background: #fff;\n  border: 1px solid #d9d9d9;\n  border-radius: 8px;\n  padding: 20px;\n  box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n\">\n  <h3 style=\"margin: 0 0 16px 0; color: #333; font-size: 16px;\">${data.title}</h3>\n  \n  <div style=\"margin-bottom: 12px;\">\n    <label style=\"display: block; margin-bottom: 4px; font-size: 14px; color: #666;\">Name</label>\n    <input \n      type=\"text\" \n      placeholder=\"Enter name\"\n      style=\"\n        width: 100%;\n        padding: 8px 12px;\n        border: 1px solid #d9d9d9;\n        border-radius: 4px;\n        font-size: 14px;\n        box-sizing: border-box;\n      \"\n    />\n  </div>\n  \n  <div style=\"margin-bottom: 12px;\">\n    <label style=\"display: block; margin-bottom: 4px; font-size: 14px; color: #666;\">Email</label>\n    <input \n      type=\"email\" \n      placeholder=\"Enter email\"\n      style=\"\n        width: 100%;\n        padding: 8px 12px;\n        border: 1px solid #d9d9d9;\n        border-radius: 4px;\n        font-size: 14px;\n        box-sizing: border-box;\n      \"\n    />\n  </div>\n  \n  <div style=\"margin-bottom: 16px;\">\n    <label style=\"display: block; margin-bottom: 4px; font-size: 14px; color: #666;\">Role</label>\n    <select style=\"\n      width: 100%;\n      padding: 8px 12px;\n      border: 1px solid #d9d9d9;\n      border-radius: 4px;\n      font-size: 14px;\n      box-sizing: border-box;\n    \">\n      <option>Administrator</option>\n      <option>User</option>\n      <option>Guest</option>\n    </select>\n  </div>\n  \n  <div style=\"display: flex; gap: 8px;\">\n    <button \n      onclick=\"handleSave('${d.id}')\"\n      style=\"\n        flex: 1;\n        padding: 8px 16px;\n        background: #1890ff;\n        color: white;\n        border: none;\n        border-radius: 4px;\n        font-size: 14px;\n        cursor: pointer;\n      \"\n    >Save</button>\n    <button \n      onclick=\"handleCancel('${d.id}')\"\n      style=\"\n        flex: 1;\n        padding: 8px 16px;\n        background: #f5f5f5;\n        color: #333;\n        border: 1px solid #d9d9d9;\n        border-radius: 4px;\n        font-size: 14px;\n        cursor: pointer;\n      \"\n    >Cancel</button>\n  </div>\n</div>`;\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n    cols: 2,\n  },\n  behaviors: ['drag-element', 'zoom-canvas'],\n});\n\n// Global functions to handle form operations\nwindow.handleSave = (nodeId) => {\n  console.log(`Saving form: ${nodeId}`);\n  alert(`Form ${nodeId} saved`);\n};\n\nwindow.handleCancel = (nodeId) => {\n  console.log(`Canceling form: ${nodeId}`);\n  alert(`Form ${nodeId} operation canceled`);\n};\n\ngraph.render();\n```\n\n## Usage Notes\n\n### 1. Performance Optimization\n\n- HTML nodes have higher rendering costs compared to regular graphic nodes, recommend using when node count is small\n- Complex HTML structures will affect performance, recommend keeping structure simple\n- Avoid using too many animation effects in HTML\n\n### 2. Event Handling\n\n- Event handling in HTML nodes needs to be implemented through global functions or event delegation\n- Recommend mounting event handling functions to the `window` object to ensure accessibility in HTML strings\n- Be careful to prevent event bubbling from affecting graph interaction behavior\n\n### 3. Style Isolation\n\n- HTML node styles may be affected by global page styles\n- Recommend using inline styles or ensuring sufficient style specificity\n- Consider using CSS-in-JS or style namespaces to avoid style conflicts\n\n### 4. Responsive Design\n\n- HTML node dimensions are fixed and do not automatically adapt to content\n- Need to dynamically calculate node dimensions based on content or use responsive layouts\n- Consider display effects at different zoom levels\n\n### 5. Framework Integration Recommendations\n\n- **React Projects**: Recommended to use [React Node](/en/manual/element/node/react-node), which allows direct use of React components as node content\n- **Vue Projects**: Vue Node is not currently supported, community contributions are welcome if needed\n- **Native Projects**: HTML nodes are the best choice, providing maximum flexibility\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Html.zh.md",
    "content": "---\ntitle: HTML节点 Html\norder: 7\n---\n\n## 概述\n\nHTML 节点是一个自定义的矩形区域，用于显示 HTML 内容。它允许您在图形节点中嵌入任意的 HTML 元素，提供了极大的灵活性来创建复杂的自定义节点。\n\n适用场景：\n\n- 用于表示复杂的自定义节点，如表格、图表或富文本\n- 适合表示自定义的可视化元素或交互组件\n- 常用于自定义图表、UI 设计、仪表板等场景\n- 需要在节点中嵌入表单、按钮等交互元素时\n\n## 框架支持说明\n\n> **💡 提示**：\n>\n> - **React 项目**：推荐使用 [React Node](/manual/element/node/react-node) 来实现更好的组件化开发体验\n> - **Vue 项目**：目前暂不支持 Vue Node，欢迎社区共建贡献\n> - **原生 HTML**：本文档介绍的 HTML 节点适用于原生 HTML 开发\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/nodes/html.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseNode](/manual/element/node/base-node)\n\n| 属性      | 描述                                                            | 类型                        | 默认值 | 必选 |\n| --------- | --------------------------------------------------------------- | --------------------------- | ------ | ---- |\n| dx        | 横行偏移量。HTML 容器默认以左上角为原点，通过 dx 来进行横向偏移 | number                      | 0      |      |\n| dy        | 纵向偏移量。HTML 容器默认以左上角为原点，通过 dy 来进行纵向偏移 | number                      | 0      |      |\n| innerHTML | HTML 内容，可以为字符串或者 `HTMLElement`                       | string &#124; `HTMLElement` | -      | ✓    |\n\n## 示例\n\n### 基础HTML节点\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', data: { location: 'East', status: 'error', ip: '192.168.1.2' } },\n      { id: 'node-2', data: { location: 'West', status: 'overload', ip: '192.168.1.3' } },\n      { id: 'node-3', data: { location: 'South', status: 'running', ip: '192.168.1.4' } },\n    ],\n  },\n  node: {\n    type: 'html',\n    style: {\n      size: [240, 80],\n      dx: -120,\n      dy: -40,\n      innerHTML: (d) => {\n        const ICON_MAP = {\n          error: '&#10060;',\n          overload: '&#9889;',\n          running: '&#9989;',\n        };\n\n        const COLOR_MAP = {\n          error: '#f5222d',\n          overload: '#faad14',\n          running: '#52c41a',\n        };\n\n        const {\n          data: { location, status, ip },\n        } = d;\n        const color = COLOR_MAP[status];\n\n        return `\n<div \n  style=\"\n    width:100%; \n    height: 100%; \n    background: ${color}bb; \n    border: 1px solid ${color};\n    color: #fff;\n    user-select: none;\n    display: flex; \n    padding: 10px;\n    border-radius: 8px;\n    \"\n>\n  <div style=\"display: flex;flex-direction: column;flex: 1;\">\n    <div style=\"font-weight: bold; font-size: 14px;\">\n      ${location} Node\n    </div>\n    <div style=\"font-size: 12px; margin-top: 4px;\">\n      status: ${status} ${ICON_MAP[status]}\n    </div>\n  </div>\n  <div>\n    <span style=\"border: 1px solid white; padding: 2px 6px; border-radius: 4px; font-size: 12px;\">\n      ${ip}\n    </span>\n  </div>\n</div>`;\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['drag-element', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n### 带交互按钮的HTML节点\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'server-1', data: { name: 'Web Server', cpu: 45, memory: 67, status: 'online' } },\n      { id: 'server-2', data: { name: 'Database', cpu: 78, memory: 89, status: 'warning' } },\n      { id: 'server-3', data: { name: 'Cache Server', cpu: 23, memory: 34, status: 'offline' } },\n    ],\n  },\n  node: {\n    type: 'html',\n    style: {\n      size: [280, 210],\n      dx: -140,\n      dy: -105,\n      innerHTML: (d) => {\n        const { data } = d;\n        const statusColors = {\n          online: '#52c41a',\n          warning: '#faad14',\n          offline: '#f5222d',\n        };\n\n        return `\n<div style=\"\n  width: 100%;\n  height: 100%;\n  background: #fff;\n  border: 2px solid ${statusColors[data.status]};\n  border-radius: 12px;\n  padding: 16px;\n  box-shadow: 0 4px 12px rgba(0,0,0,0.1);\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n\">\n  <div style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;\">\n    <h3 style=\"margin: 0; font-size: 16px; color: #333;\">${data.name}</h3>\n    <span style=\"\n      background: ${statusColors[data.status]};\n      color: white;\n      padding: 2px 8px;\n      border-radius: 12px;\n      font-size: 12px;\n      font-weight: bold;\n    \">${data.status.toUpperCase()}</span>\n  </div>\n\n  <div style=\"margin-bottom: 12px;\">\n    <div style=\"display: flex; justify-content: space-between; margin-bottom: 4px;\">\n      <span style=\"font-size: 12px; color: #666;\">CPU</span>\n      <span style=\"font-size: 12px; color: #333;\">${data.cpu}%</span>\n    </div>\n    <div style=\"background: #f0f0f0; height: 6px; border-radius: 3px; overflow: hidden;\">\n      <div style=\"background: ${data.cpu > 70 ? '#f5222d' : '#52c41a'}; height: 100%; width: ${data.cpu}%; transition: width 0.3s;\"></div>\n    </div>\n  </div>\n\n  <div style=\"margin-bottom: 12px;\">\n    <div style=\"display: flex; justify-content: space-between; margin-bottom: 4px;\">\n      <span style=\"font-size: 12px; color: #666;\">Memory</span>\n      <span style=\"font-size: 12px; color: #333;\">${data.memory}%</span>\n    </div>\n    <div style=\"background: #f0f0f0; height: 6px; border-radius: 3px; overflow: hidden;\">\n      <div style=\"background: ${data.memory > 80 ? '#f5222d' : '#1890ff'}; height: 100%; width: ${data.memory}%; transition: width 0.3s;\"></div>\n    </div>\n  </div>\n\n  <div style=\"display: flex; gap: 8px;\">\n    <button\n      onclick=\"handleRestart('${d.id}')\"\n      style=\"\n        flex: 1;\n        padding: 6px 12px;\n        background: #1890ff;\n        color: white;\n        border: none;\n        border-radius: 6px;\n        font-size: 12px;\n        cursor: pointer;\n        transition: background 0.2s;\n      \"\n      onmouseover=\"this.style.background='#40a9ff'\"\n      onmouseout=\"this.style.background='#1890ff'\"\n    >重启</button>\n    <button\n      onclick=\"handleMonitor('${d.id}')\"\n      style=\"\n        flex: 1;\n        padding: 6px 12px;\n        background: #52c41a;\n        color: white;\n        border: none;\n        border-radius: 6px;\n        font-size: 12px;\n        cursor: pointer;\n        transition: background 0.2s;\n      \"\n      onmouseover=\"this.style.background='#73d13d'\"\n      onmouseout=\"this.style.background='#52c41a'\"\n    >监控</button>\n  </div>\n</div>`;\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n    cols: 2,\n  },\n  behaviors: ['drag-element', 'zoom-canvas'],\n});\n\n// 全局函数处理按钮点击\nwindow.handleRestart = (nodeId) => {\n  console.log(`重启服务器: ${nodeId}`);\n  alert(`正在重启服务器 ${nodeId}...`);\n};\n\nwindow.handleMonitor = (nodeId) => {\n  console.log(`打开监控面板: ${nodeId}`);\n  alert(`打开服务器 ${nodeId} 的监控面板`);\n};\n\ngraph.render();\n```\n\n### 表单输入HTML节点\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'form-1', data: { title: '用户信息', type: 'user-form' } },\n      { id: 'form-2', data: { title: '配置面板', type: 'config-form' } },\n    ],\n  },\n  node: {\n    type: 'html',\n    style: {\n      size: [300, 400],\n      dx: -150,\n      dy: -200,\n      innerHTML: (d) => {\n        const { data } = d;\n\n        return `\n<div style=\"\n  width: 100%; \n  height: 100%; \n  background: #fff;\n  border: 1px solid #d9d9d9;\n  border-radius: 8px;\n  padding: 20px;\n  box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n\">\n  <h3 style=\"margin: 0 0 16px 0; color: #333; font-size: 16px;\">${data.title}</h3>\n  \n  <div style=\"margin-bottom: 12px;\">\n    <label style=\"display: block; margin-bottom: 4px; font-size: 14px; color: #666;\">姓名</label>\n    <input \n      type=\"text\" \n      placeholder=\"请输入姓名\"\n      style=\"\n        width: 100%;\n        padding: 8px 12px;\n        border: 1px solid #d9d9d9;\n        border-radius: 4px;\n        font-size: 14px;\n        box-sizing: border-box;\n      \"\n    />\n  </div>\n  \n  <div style=\"margin-bottom: 12px;\">\n    <label style=\"display: block; margin-bottom: 4px; font-size: 14px; color: #666;\">邮箱</label>\n    <input \n      type=\"email\" \n      placeholder=\"请输入邮箱\"\n      style=\"\n        width: 100%;\n        padding: 8px 12px;\n        border: 1px solid #d9d9d9;\n        border-radius: 4px;\n        font-size: 14px;\n        box-sizing: border-box;\n      \"\n    />\n  </div>\n  \n  <div style=\"margin-bottom: 16px;\">\n    <label style=\"display: block; margin-bottom: 4px; font-size: 14px; color: #666;\">角色</label>\n    <select style=\"\n      width: 100%;\n      padding: 8px 12px;\n      border: 1px solid #d9d9d9;\n      border-radius: 4px;\n      font-size: 14px;\n      box-sizing: border-box;\n    \">\n      <option>管理员</option>\n      <option>用户</option>\n      <option>访客</option>\n    </select>\n  </div>\n  \n  <div style=\"display: flex; gap: 8px;\">\n    <button \n      onclick=\"handleSave('${d.id}')\"\n      style=\"\n        flex: 1;\n        padding: 8px 16px;\n        background: #1890ff;\n        color: white;\n        border: none;\n        border-radius: 4px;\n        font-size: 14px;\n        cursor: pointer;\n      \"\n    >保存</button>\n    <button \n      onclick=\"handleCancel('${d.id}')\"\n      style=\"\n        flex: 1;\n        padding: 8px 16px;\n        background: #f5f5f5;\n        color: #333;\n        border: 1px solid #d9d9d9;\n        border-radius: 4px;\n        font-size: 14px;\n        cursor: pointer;\n      \"\n    >取消</button>\n  </div>\n</div>`;\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n    cols: 2,\n  },\n  behaviors: ['drag-element', 'zoom-canvas'],\n});\n\n// 全局函数处理表单操作\nwindow.handleSave = (nodeId) => {\n  console.log(`保存表单: ${nodeId}`);\n  alert(`表单 ${nodeId} 已保存`);\n};\n\nwindow.handleCancel = (nodeId) => {\n  console.log(`取消表单: ${nodeId}`);\n  alert(`取消表单 ${nodeId} 操作`);\n};\n\ngraph.render();\n```\n\n## 使用注意事项\n\n### 1. 性能优化\n\n- HTML 节点相比普通图形节点有更高的渲染成本，建议在节点数量较少时使用\n- 复杂的 HTML 结构会影响性能，建议保持结构简洁\n- 避免在 HTML 中使用过多的动画效果\n\n### 2. 事件处理\n\n- HTML 节点中的事件处理需要通过全局函数或事件委托来实现\n- 建议将事件处理函数挂载到 `window` 对象上，确保在 HTML 字符串中可以访问\n- 注意防止事件冒泡影响图的交互行为\n\n### 3. 样式隔离\n\n- HTML 节点的样式可能会受到页面全局样式的影响\n- 建议使用内联样式或确保样式的特异性足够高\n- 考虑使用 CSS-in-JS 或样式命名空间来避免样式冲突\n\n### 4. 响应式设计\n\n- HTML 节点的尺寸是固定的，不会自动适应内容\n- 需要根据内容动态计算节点尺寸，或使用响应式布局\n- 考虑在不同缩放级别下的显示效果\n\n### 5. 框架集成建议\n\n- **React 项目**：推荐使用 [React Node](/manual/element/node/react-node)，可以直接使用 React 组件作为节点内容\n- **Vue 项目**：目前暂不支持 Vue Node，如有需求欢迎社区贡献\n- **原生项目**：HTML 节点是最佳选择，提供了最大的灵活性\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Image.en.md",
    "content": "---\ntitle: Image Node\norder: 8\n---\n\n## Overview\n\nThe image node is a rectangular area used to display images.\n\nApplicable scenarios:\n\n- Used to represent user avatars, product images, or icons.\n\n- Suitable for representing social networks, product catalogs, or icon collections.\n\n- Commonly used in social network graphs, product images, UI design, etc.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/nodes/image.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseNode](/en/manual/element/node/base-node)\n\n| Attribute | Description                          | Type   | Default | Required |\n| --------- | ------------------------------------ | ------ | ------- | -------- |\n| img       | Alias for the img attribute          | string | -       |          |\n| src       | Image source, i.e., image URL string | string | -       | ✓        |\n\n## Example\n\n### Built-in Image Node Effect\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'image',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      src: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*N4ZMS7gHsUIAAAAAAAAAAABkARQnAQ',\n      haloStroke: '#227eff',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n    state: {\n      inactive: {\n        fillOpacity: 0.5,\n      },\n      disabled: {\n        fillOpacity: 0.2,\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Image.zh.md",
    "content": "---\ntitle: 图片节点 Image\norder: 8\n---\n\n## 概述\n\n图片节点是一个矩形区域，用于显示图像。\n\n适用场景：\n\n- 用于表示用户头像、产品图片或图标。\n\n- 适合表示社交网络、产品目录或图标集合。\n\n- 常用于社交网络图、产品图、UI 设计等。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/nodes/image.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseNode](/manual/element/node/base-node)\n\n| 属性 | 描述                       | 类型   | 默认值 | 必选 |\n| ---- | -------------------------- | ------ | ------ | ---- |\n| img  | 该属性为 img 的别名        | string | -      |      |\n| src  | 图片来源，即图片地址字符串 | string | -      | ✓    |\n\n## 示例\n\n### 内置图片节点效果\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'image',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      src: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*N4ZMS7gHsUIAAAAAAAAAAABkARQnAQ',\n      haloStroke: '#227eff',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n    state: {\n      inactive: {\n        fillOpacity: 0.5,\n      },\n      disabled: {\n        fillOpacity: 0.2,\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Rect.en.md",
    "content": "---\ntitle: Rect Node\norder: 9\n---\n\n## Overview\n\nA rectangle is a geometric shape with four equal sides and defined boundaries.\n\nApplicable scenarios:\n\n- Used to represent modules, components, or containers.\n\n- Suitable for representing hierarchies, such as organizational charts, file directory trees.\n\n- Commonly used in flowcharts, architecture diagrams, UML diagrams, etc.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/nodes/rect.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseNode](/en/manual/element/node/base-node)\n\n## Example\n\n### Built-in Rect Node Effect\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Rect.zh.md",
    "content": "---\ntitle: 矩形节点 Rect\norder: 9\n---\n\n## 概述\n\n矩形是一个四边相等的几何形状，具有明确的边界。\n\n适用场景：\n\n- 用于表示模块、组件或容器。\n\n- 适合表示层次结构，如组织结构图、文件目录树。\n\n- 常用于流程图、架构图、UML 图等。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/nodes/rect.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseNode](/manual/element/node/base-node)\n\n## 示例\n\n### 内置矩形节点效果\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Star.en.md",
    "content": "---\ntitle: Star Node\norder: 10\n---\n\n## Overview\n\nA star is a polygonal geometric shape with prominent points.\n\nApplicable scenarios:\n\n- Used to represent important nodes, special markers, or decorative elements.\n\n- Suitable for representing flowcharts, network diagrams, or topology diagrams.\n\n- Commonly used in flowcharts, network diagrams, topology diagrams, etc.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/nodes/star.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseNode](/en/manual/element/node/base-node)\n\n| Attribute | Description                                                           | Type   | Default                            | Required |\n| --------- | --------------------------------------------------------------------- | ------ | ---------------------------------- | -------- |\n| innerR    | Inner radius, the distance from the star's center to the inner vertex | number | Default is 3/8 of the outer radius |\n\nStructure Description:\n\n<img width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*VKrvQpdqwXoAAAAAAAAAAAAAemJ7AQ/original\" />\n\n## Example\n\n### Built-in Star Node Effect\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'star',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Star.zh.md",
    "content": "---\ntitle: 五角形节点 Star\norder: 10\n---\n\n## 概述\n\n星形是一个多角几何形状，具有突出的角。\n\n适用场景：\n\n- 用于表示重要节点、特殊标记或装饰性元素。\n\n- 适合表示流程图、网络图或拓扑图。\n\n- 常用于流程图、网络图、拓扑图等。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/nodes/star.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseNode](/manual/element/node/base-node)\n\n| 属性   | 描述                                 | 类型   | 默认值             | 必选 |\n| ------ | ------------------------------------ | ------ | ------------------ | ---- |\n| innerR | 内半径，是指从星形中心到内顶点的距离 | number | 默认为外半径的 3/8 |\n\n结构说明：\n\n<img width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*VKrvQpdqwXoAAAAAAAAAAAAAemJ7AQ/original\" />\n\n## 示例\n\n### 内置五角形节点效果\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'star',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Triangle.en.md",
    "content": "---\ntitle: Triangle Node\norder: 11\n---\n\n## Overview\n\nA triangle is a geometric shape with three sides, having a clear directionality.\n\nApplicable scenarios:\n\n- Used to represent directional nodes, warnings, or prompts.\n\n- Suitable for indicating direction or hierarchy in flowcharts.\n\n- Commonly used in flowcharts, network diagrams, topology diagrams, etc.\n\n## Online Experience\n\n<embed src=\"@/common/api/elements/nodes/triangle.md\"></embed>\n\n## Style Configuration\n\n> If the element has specific attributes, we will list them below. For all general style attributes, see [BaseNode](/en/manual/element/node/base-node)\n\n| Attribute | Description               | Type                                | Default | Required |\n| --------- | ------------------------- | ----------------------------------- | ------- | -------- |\n| direction | Direction of the triangle | `up` \\| `left` \\| `right` \\| `down` | `up`    |\n\n## Example\n\n### Built-in Triangle Node Effect\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'triangle',\n    style: {\n      size: 40,\n      direction: (d) => (d.id === 'ports' ? 'left' : undefined),\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/Triangle.zh.md",
    "content": "---\ntitle: 三角形节点 Triangle\norder: 11\n---\n\n## 概述\n\n三角形是一个三边几何形状，具有明确的方向性。\n\n适用场景：\n\n- 用于表示方向性节点、警告或提示。\n\n- 适合表示流程图中的方向指示或层级关系。\n\n- 常用于流程图、网络图、拓扑图等。\n\n## 在线体验\n\n<embed src=\"@/common/api/elements/nodes/triangle.md\"></embed>\n\n## 样式配置\n\n> 如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见 [BaseNode](/manual/element/node/base-node)\n\n| 属性      | 描述         | 类型                                | 默认值 | 必选 |\n| --------- | ------------ | ----------------------------------- | ------ | ---- |\n| direction | 三角形的方向 | `up` \\| `left` \\| `right` \\| `down` | `up`   |\n\n## 示例\n\n### 内置三角形节点效果\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'triangle',\n    style: {\n      size: 40,\n      direction: (d) => (d.id === 'ports' ? 'left' : undefined),\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/custom-node.en.md",
    "content": "---\ntitle: Custom Node\norder: 12\n---\n\nG6 provides a series of [built-in nodes](/en/manual/element/node/base-node), including [circle (Circle Node)](/en/manual/element/node/circle), [diamond (Diamond Node)](/en/manual/element/node/diamond), [donut (Donut Node)](/en/manual/element/node/donut), [ellipse (Ellipse Node)](/en/manual/element/node/ellipse), [hexagon (Hexagon Node)](/en/manual/element/node/hexagon), [html (HTML Node)](/en/manual/element/node/html), [image (Image Node)](/en/manual/element/node/image), [rect (Rectangle Node)](/en/manual/element/node/rect), [star (Star Node)](/en/manual/element/node/star), and [triangle (Triangle Node)](/en/manual/element/node/triangle). These built-in nodes can meet most basic scenario requirements.\n\nHowever, in actual projects, you may encounter needs that these basic nodes cannot satisfy. In such cases, you need to create custom nodes. Don't worry, this is simpler than you might think!\n\n## Ways to Create Custom Nodes <Badge type=\"warning\">Choose the Right Approach</Badge>\n\nThere are mainly two approaches to creating custom nodes:\n\n### 1. Inherit from Existing Node Types <Badge type=\"success\">Recommended</Badge>\n\nThis is the most commonly used approach, where you can choose to inherit from one of the following types:\n\n- [`BaseNode`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/base-node.ts) - The most basic node class, providing core node functionality\n- [`Circle`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/circle.ts) - Circle node\n- [`Rect`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/rect.ts) - Rectangle node\n- [`Ellipse`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/ellipse.ts) - Ellipse node\n- [`Diamond`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/diamond.ts) - Diamond node\n- [`Triangle`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/triangle.ts) - Triangle node\n- [`Star`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/star.ts) - Star node\n- [`Image`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/image.ts) - Image node\n- [`Donut`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/donut.ts) - Donut node\n- [`Hexagon`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/hexagon.ts) - Hexagon node\n\n**Why choose this approach?**\n\n- 📌 **Less Code**: Reuse properties and methods of existing nodes, only focus on new features\n- 📌 **Rapid Development**: Suitable for most project needs, quickly achieve business goals\n- 📌 **Easy Maintenance**: Clear code structure with well-defined inheritance relationships\n\n:::tip{title=Get Started Now}\nIf you choose to inherit from existing node types (recommended), you can jump directly to [Create Your First Custom Node in Three Steps](#create-your-first-custom-node-in-three-steps) to start practicing. Most users will choose this approach!\n:::\n\n### 2. Build from Scratch Based on G Graphics System <Badge>Advanced Usage</Badge>\n\nIf existing node types don't meet your requirements, you can create nodes from scratch based on G's underlying graphics system.\n\n**Why choose this approach?**\n\n- 📌 **Maximum Freedom**: Complete control over every detail of the node, achieving any complex effects\n- 📌 **Special Requirements**: Highly customized scenarios that existing node types cannot satisfy\n- 📌 **Performance Optimization**: Performance optimization for specific scenarios\n\n:::warning{title=Important Notes}\nCustom nodes built from scratch require handling all details yourself, including graphics rendering, event response, state changes, etc., with higher development difficulty. You can refer directly to the [source code](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/base-node.ts) for implementation.\n:::\n\n## Create Your First Custom Node in Three Steps\n\nLet's start with a simple example - creating a **rectangle node with main and subtitle**:\n\n```js | ob { pin:false, inject: true }\nimport { Graph, register, Rect, ExtensionCategory } from '@antv/g6';\n\n// Step 1: Create custom node class\nclass DualLabelNode extends Rect {\n  // Subtitle style\n  getSubtitleStyle(attributes) {\n    return {\n      x: 0,\n      y: 45, // Place below the main title\n      text: attributes.subtitle || '',\n      fontSize: 12,\n      fill: '#666',\n      textAlign: 'center',\n      textBaseline: 'middle',\n    };\n  }\n\n  // Draw subtitle\n  drawSubtitleShape(attributes, container) {\n    const subtitleStyle = this.getSubtitleStyle(attributes);\n    this.upsert('subtitle', 'text', subtitleStyle, container);\n  }\n\n  // Render method\n  render(attributes = this.parsedAttributes, container) {\n    // 1. Render basic rectangle and main title\n    super.render(attributes, container);\n\n    // 2. Add subtitle\n    this.drawSubtitleShape(attributes, container);\n  }\n}\n\n// Step 2: Register custom node\nregister(ExtensionCategory.NODE, 'dual-label-node', DualLabelNode);\n\n// Step 3: Use custom node\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        style: { x: 100, y: 100 },\n        data: {\n          title: 'Node A', // Main title\n          subtitle: 'Your First Custom Node', // Subtitle\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'dual-label-node',\n    style: {\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n      radius: 5,\n      // Main title style\n      labelText: (d) => d.data.title,\n      labelFill: '#222',\n      labelFontSize: 14,\n      labelFontWeight: 500,\n      // Subtitle\n      subtitle: (d) => d.data.subtitle,\n    },\n  },\n});\n\ngraph.render();\n```\n\n### Step 1: Write Custom Node Class\n\nInherit from G6's `Rect` (rectangle node) and add a subtitle:\n\n```js\nimport { Rect, register, Graph, ExtensionCategory } from '@antv/g6';\n\n// Create custom node, inheriting from Rect\nclass DualLabelNode extends Rect {\n  // Subtitle style\n  getSubtitleStyle(attributes) {\n    return {\n      x: 0,\n      y: 45, // Place below the main title\n      text: attributes.subtitle || '',\n      fontSize: 12,\n      fill: '#666',\n      textAlign: 'center',\n      textBaseline: 'middle',\n    };\n  }\n\n  // Draw subtitle\n  drawSubtitleShape(attributes, container) {\n    const subtitleStyle = this.getSubtitleStyle(attributes);\n    this.upsert('subtitle', 'text', subtitleStyle, container);\n  }\n\n  // Render method\n  render(attributes = this.parsedAttributes, container) {\n    // 1. Render basic rectangle and main title\n    super.render(attributes, container);\n\n    // 2. Add subtitle\n    this.drawSubtitleShape(attributes, container);\n  }\n}\n```\n\n### Step 2: Register Custom Node\n\nUse the `register` method to register the node type so that G6 can recognize your custom node:\n\n```js\nregister(ExtensionCategory.NODE, 'dual-label-node', DualLabelNode);\n```\n\nThe `register` method requires three parameters:\n\n- Extension category: `ExtensionCategory.NODE` indicates this is a node type\n- Type name: `dual-label-node` is the name we give to this custom node, which will be used in configuration later\n- Class definition: `DualLabelNode` is the node class we just created\n\n### Step 3: Apply Custom Node\n\nUse the custom node in graph configuration:\n\n```js\nconst graph = new Graph({\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        style: { x: 100, y: 100 },\n        data: {\n          title: 'Node A', // Main title\n          subtitle: 'Your First Custom Node', // Subtitle\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'dual-label-node',\n    style: {\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n      radius: 8,\n      // Main title style\n      labelText: (d) => d.data.title,\n      labelFill: '#222',\n      labelFontSize: 14,\n      labelFontWeight: 500,\n      // Subtitle\n      subtitle: (d) => d.data.subtitle,\n    },\n  },\n});\n\ngraph.render();\n```\n\n🎉 Congratulations! You have created your first custom node. It looks simple, but this process contains the core concept of custom nodes: **inherit from a basic node type**, then **override the `render` method** to add custom content.\n\n## Understanding Data Flow: How to Access Data in Custom Nodes\n\nBefore creating complex custom nodes, understanding how data flows into custom nodes is very important. G6 provides multiple ways to access data for custom nodes:\n\n### Method 1: Through `attributes` Parameter (Recommended)\n\nThe first parameter `attributes` of the `render` method contains processed style attributes, including data-driven styles:\n\n```js\nclass CustomNode extends Rect {\n  render(attributes, container) {\n    // attributes contains all style attributes, including data-driven styles\n    console.log('All properties of current node:', attributes);\n\n    // If customData: (d) => d.data.someValue is defined in style\n    // Then you can access it through attributes.customData\n    const customValue = attributes.customData;\n\n    super.render(attributes, container);\n  }\n}\n```\n\n### Method 2: Through `this.context.graph` to Access Raw Data\n\nWhen you need to access the node's raw data, you can get it through the graph instance:\n\n```js\nclass CustomNode extends Rect {\n  // Convenient data access method\n  get nodeData() {\n    return this.context.graph.getNodeData(this.id);\n  }\n\n  get data() {\n    return this.nodeData.data || {};\n  }\n\n  render(attributes, container) {\n    // Get complete node data\n    const nodeData = this.nodeData;\n    console.log('Complete node data:', nodeData);\n\n    // Get business data from data field\n    const businessData = this.data;\n    console.log('Business data:', businessData);\n\n    super.render(attributes, container);\n  }\n}\n```\n\n### Complete Data Flow Process\n\nLet's understand how data flows from graph data to custom nodes through a specific example:\n\n```js | ob { inject: true }\nimport { Graph, register, Rect, ExtensionCategory } from '@antv/g6';\n\nclass DataFlowNode extends Rect {\n  // Method 2: Get raw data through graph\n  get nodeData() {\n    return this.context.graph.getNodeData(this.id);\n  }\n\n  get data() {\n    return this.nodeData.data || {};\n  }\n\n  render(attributes, container) {\n    // Method 1: Get processed styles from attributes\n    console.log('Get from attributes:', {\n      iconUrl: attributes.iconUrl,\n      userName: attributes.userName,\n    });\n\n    // Method 2: Get from raw data\n    console.log('Get from raw data:', {\n      icon: this.data.icon,\n      name: this.data.name,\n      role: this.data.role,\n    });\n\n    // Render basic rectangle\n    super.render(attributes, container);\n\n    // Use data to render custom content\n    if (attributes.iconUrl) {\n      this.upsert(\n        'icon',\n        'image',\n        {\n          x: -25,\n          y: -12,\n          width: 20,\n          height: 20,\n          src: attributes.iconUrl,\n        },\n        container,\n      );\n    }\n\n    if (attributes.userName) {\n      this.upsert(\n        'username',\n        'text',\n        {\n          x: 10,\n          y: 0,\n          text: attributes.userName,\n          fontSize: 10,\n          fill: '#666',\n          textAlign: 'center',\n          textBaseline: 'middle',\n        },\n        container,\n      );\n    }\n  }\n}\n\nregister(ExtensionCategory.NODE, 'data-flow-node', DataFlowNode);\n\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      {\n        id: 'user1',\n        style: { x: 100, y: 100 },\n        // This is the node's business data\n        data: {\n          name: 'Zhang San',\n          role: 'Developer',\n          icon: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'data-flow-node',\n    style: {\n      size: [80, 40],\n      fill: '#f0f9ff',\n      stroke: '#0ea5e9',\n      lineWidth: 1,\n      radius: 4,\n      // Map data from data field to style attributes\n      iconUrl: (d) => d.data.icon, // This becomes attributes.iconUrl\n      userName: (d) => d.data.name, // This becomes attributes.userName\n      // Main title uses role information\n      labelText: (d) => d.data.role,\n      labelFontSize: 12,\n      labelFill: '#0369a1',\n    },\n  },\n});\n\ngraph.render();\n```\n\n:::tip{title=Data Flow Summary}\n\n1. **Graph Data Definition**: Define business data in `data.nodes[].data`\n2. **Style Mapping**: Use functions in `node.style` to map data to style attributes\n3. **Node Access**: Access data in custom nodes through `attributes` or `this.context.graph`\n4. **Rendering Usage**: Use the obtained data to render custom graphics\n   :::\n\n## From Simple to Complex: Gradually Building Feature-Rich Nodes\n\nLet's gradually increase the complexity and functionality of nodes through practical examples.\n\n### Example 1: User Card Node with Icon and Badge\n\nThis example shows how to create a user card node containing avatar, name, and status badge:\n\n```js | ob { inject: true }\nimport { Graph, register, Rect, ExtensionCategory } from '@antv/g6';\n\nclass UserCardNode extends Rect {\n  get nodeData() {\n    return this.context.graph.getNodeData(this.id);\n  }\n\n  get data() {\n    return this.nodeData.data || {};\n  }\n\n  // Avatar style\n  getAvatarStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2 + 20,\n      y: -height / 2 + 15,\n      width: 30,\n      height: 30,\n      src: attributes.avatarUrl || '',\n      radius: 15, // Circular avatar\n    };\n  }\n\n  drawAvatarShape(attributes, container) {\n    if (!attributes.avatarUrl) return;\n\n    const avatarStyle = this.getAvatarStyle(attributes);\n    this.upsert('avatar', 'image', avatarStyle, container);\n  }\n\n  // Status badge style\n  getBadgeStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    const status = this.data.status || 'offline';\n    const colorMap = {\n      online: '#52c41a',\n      busy: '#faad14',\n      offline: '#8c8c8c',\n    };\n\n    return {\n      x: width / 2 - 8,\n      y: -height / 2 + 8,\n      r: 4,\n      fill: colorMap[status],\n      stroke: '#fff',\n      lineWidth: 2,\n    };\n  }\n\n  drawBadgeShape(attributes, container) {\n    const badgeStyle = this.getBadgeStyle(attributes);\n    this.upsert('badge', 'circle', badgeStyle, container);\n  }\n\n  // Username style\n  getUsernameStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2 + 55,\n      y: -height / 2 + 20,\n      text: attributes.username || '',\n      fontSize: 14,\n      fill: '#262626',\n      fontWeight: 'bold',\n      textAlign: 'left',\n      textBaseline: 'middle',\n    };\n  }\n\n  drawUsernameShape(attributes, container) {\n    if (!attributes.username) return;\n\n    const usernameStyle = this.getUsernameStyle(attributes);\n    this.upsert('username', 'text', usernameStyle, container);\n  }\n\n  // Role label style\n  getRoleStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2 + 55,\n      y: -height / 2 + 35,\n      text: attributes.userRole || '',\n      fontSize: 11,\n      fill: '#8c8c8c',\n      textAlign: 'left',\n      textBaseline: 'middle',\n    };\n  }\n\n  drawRoleShape(attributes, container) {\n    if (!attributes.userRole) return;\n\n    const roleStyle = this.getRoleStyle(attributes);\n    this.upsert('role', 'text', roleStyle, container);\n  }\n\n  render(attributes, container) {\n    // Render basic rectangle\n    super.render(attributes, container);\n\n    // Add various components\n    this.drawAvatarShape(attributes, container);\n    this.drawBadgeShape(attributes, container);\n    this.drawUsernameShape(attributes, container);\n    this.drawRoleShape(attributes, container);\n  }\n}\n\nregister(ExtensionCategory.NODE, 'user-card-node', UserCardNode);\n\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      {\n        id: 'user1',\n        style: { x: 100, y: 100 },\n        data: {\n          name: 'Zhang Xiaoming',\n          role: 'Frontend Engineer',\n          status: 'online',\n          avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Zhang',\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'user-card-node',\n    style: {\n      size: [140, 50],\n      fill: '#ffffff',\n      stroke: '#d9d9d9',\n      lineWidth: 1,\n      radius: 6,\n      // Data mapping\n      avatarUrl: (d) => d.data.avatar,\n      username: (d) => d.data.name,\n      userRole: (d) => d.data.role,\n    },\n  },\n});\n\ngraph.render();\n```\n\n### Example 2: Node with Clickable Action Button\n\nAdd a blue button to the node that triggers events (prints logs or executes callbacks) when clicked.\n\n```js | ob { inject: true }\nimport { Graph, register, Rect, ExtensionCategory } from '@antv/g6';\n\nclass ClickableNode extends Rect {\n  getButtonStyle(attributes) {\n    return {\n      x: 40,\n      y: -10,\n      width: 20,\n      height: 20,\n      radius: 10,\n      fill: '#1890ff',\n      cursor: 'pointer', // Mouse pointer becomes hand\n    };\n  }\n\n  drawButtonShape(attributes, container) {\n    const btnStyle = this.getButtonStyle(attributes, container);\n    const btn = this.upsert('button', 'rect', btnStyle, container);\n\n    // Add click event to button\n    if (!btn.__clickBound) {\n      btn.addEventListener('click', (e) => {\n        // Prevent event bubbling to avoid triggering node click event\n        e.stopPropagation();\n\n        // Execute business logic\n        console.log('Button clicked on node:', this.id);\n\n        // If there's a callback function in data, call it\n        if (typeof attributes.onButtonClick === 'function') {\n          attributes.onButtonClick(this.id, this.data);\n        }\n      });\n      btn.__clickBound = true; // Mark as bound to avoid duplicate binding\n    }\n  }\n\n  render(attributes, container) {\n    super.render(attributes, container);\n\n    // Add a button\n    this.drawButtonShape(attributes, container);\n  }\n}\n\nregister(ExtensionCategory.NODE, 'clickable-node', ClickableNode);\n\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        style: { x: 100, y: 100 },\n      },\n    ],\n  },\n  node: {\n    type: 'clickable-node', // Specify using our custom node\n    style: {\n      size: [60, 30],\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n      radius: 5,\n      onButtonClick: (id, data) => {},\n    },\n  },\n});\n\ngraph.render();\n```\n\n### Example 3: Node Responding to State Changes (Click to Change Color)\n\nCommon interactions require nodes and edges to provide feedback through style changes, such as when the mouse moves over a node, clicking to select nodes/edges, or activating interactions on edges through interaction. All these require changing the styles of nodes and edges. There are two ways to achieve this effect:\n\n1. Get the current state from `data.states` and handle state changes in the custom node class;\n2. Separate interaction state from raw data and node drawing logic, only update the node.\n\nWe recommend users use the second approach to implement node state adjustments, which can be achieved through the following steps:\n\n1. Implement custom node;\n2. Configure node state styles in graph configuration;\n3. Set node state through the `graph.setElementState()` method.\n\nBased on rect, extend a hole shape with default white fill color that turns orange when clicked. The sample code to achieve this effect is as follows:\n\n```js | ob { inject: true }\nimport { Rect, register, Graph, ExtensionCategory } from '@antv/g6';\n\n// 1. Define node class\nclass SelectableNode extends Rect {\n  getHoleStyle(attributes) {\n    return {\n      x: 20,\n      y: -10,\n      radius: 10,\n      width: 20,\n      height: 20,\n      fill: attributes.holeFill,\n    };\n  }\n\n  drawHoleShape(attributes, container) {\n    const holeStyle = this.getHoleStyle(attributes, container);\n\n    this.upsert('hole', 'rect', holeStyle, container);\n  }\n\n  render(attributes, container) {\n    super.render(attributes, container);\n\n    this.drawHoleShape(attributes, container);\n  }\n}\n\n// 2. Register node\nregister(ExtensionCategory.NODE, 'selectable-node', SelectableNode, true);\n\n// 3. Create graph instance\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],\n  },\n  node: {\n    type: 'selectable-node',\n    style: {\n      size: [120, 60],\n      radius: 6,\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n      holeFill: '#fff',\n    },\n    state: {\n      // Mouse selected state\n      selected: {\n        holeFill: 'orange',\n      },\n    },\n  },\n});\n\n// 4. Add node interaction\ngraph.on('node:click', (evt) => {\n  const nodeId = evt.target.id;\n\n  graph.setElementState(nodeId, ['selected']);\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/custom-node.zh.md",
    "content": "---\ntitle: 自定义节点\norder: 12\n---\n\nG6 提供了一系列 [内置节点](/manual/element/node/base-node)，包含 [circle（圆形节点）](/manual/element/node/circle)、[diamond（菱形节点）](/manual/element/node/diamond)、[donut（甜甜圈节点）](/manual/element/node/donut)、[ellipse（椭圆节点）](/manual/element/node/ellipse)、[hexagon（六边形节点）](/manual/element/node/hexagon)、[html（HTML节点）](/manual/element/node/html)、[image（图片节点）](/manual/element/node/image)、[rect（矩形节点）](/manual/element/node/rect)、[star（星形节点）](/manual/element/node/star) 和 [triangle（三角形节点）](/manual/element/node/triangle)。这些内置节点能够满足大部分基础场景需求。\n\n但在实际项目中，你可能会遇到这些基础节点无法满足的需求。这时，你需要创建自定义节点。别担心，这比你想象的要简单！\n\n## 自定义节点的方式 <Badge type=\"warning\">选择合适的方式</Badge>\n\n创建自定义节点的方式主要有两种途径：\n\n### 1. 继承现有节点类型 <Badge type=\"success\">推荐</Badge>\n\n这是最常用的方式，你可以选择继承以下类型之一：\n\n- [`BaseNode`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/base-node.ts) - 最基础的节点类，提供节点的核心功能\n- [`Circle`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/circle.ts) - 圆形节点\n- [`Rect`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/rect.ts) - 矩形节点\n- [`Ellipse`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/ellipse.ts) - 椭圆节点\n- [`Diamond`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/diamond.ts) - 菱形节点\n- [`Triangle`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/triangle.ts) - 三角形节点\n- [`Star`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/star.ts) - 星形节点\n- [`Image`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/image.ts) - 图片节点\n- [`Donut`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/donut.ts) - 甜甜圈节点\n- [`Hexagon`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/hexagon.ts) - 六边形节点\n\n**为什么选择这种方式？**\n\n- 📌 **代码量少**：复用现有节点的属性和方法，只需专注于新增功能\n- 📌 **开发迅速**：适合大多数项目需求，快速实现业务目标\n- 📌 **易于维护**：代码结构清晰，继承关系明确\n\n:::tip{title=立即开始}\n如果你选择继承现有节点类型（推荐），可以直接跳到 [三步创建你的第一个自定义节点](#三步创建你的第一个自定义节点) 开始实践。大部分用户都会选择这种方式！\n:::\n\n### 2. 基于 G 图形系统从零开发 <Badge>高级用法</Badge>\n\n如果现有节点类型都不满足需求，你可以基于 G 的底层图形系统从零创建节点。\n\n**为什么选择这种方式？**\n\n- 📌 **最大自由度**：完全控制节点的每个细节，实现任意复杂效果\n- 📌 **特殊需求**：现有节点类型无法满足的高度定制场景\n- 📌 **性能优化**：针对特定场景的性能优化\n\n:::warning{title=注意事项}\n从零开发的自定义节点需要自行处理所有细节，包括图形绘制、事件响应、状态变化等，开发难度较大。这里可以直接参考 [源码](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/nodes/base-node.ts) 进行实现。\n:::\n\n## 三步创建你的第一个自定义节点\n\n让我们从一个简单的例子开始 - 创建一个 **带有主副标题的矩形节点**：\n\n```js | ob { pin:false, inject: true }\nimport { Graph, register, Rect, ExtensionCategory } from '@antv/g6';\n\n// 第一步：创建自定义节点类\nclass DualLabelNode extends Rect {\n  // 副标题样式\n  getSubtitleStyle(attributes) {\n    return {\n      x: 0,\n      y: 45, // 放在主标题下方\n      text: attributes.subtitle || '',\n      fontSize: 12,\n      fill: '#666',\n      textAlign: 'center',\n      textBaseline: 'middle',\n    };\n  }\n\n  // 绘制副标题\n  drawSubtitleShape(attributes, container) {\n    const subtitleStyle = this.getSubtitleStyle(attributes);\n    this.upsert('subtitle', 'text', subtitleStyle, container);\n  }\n\n  // 渲染方法\n  render(attributes = this.parsedAttributes, container) {\n    // 1. 渲染基础矩形和主标题\n    super.render(attributes, container);\n\n    // 2. 添加副标题\n    this.drawSubtitleShape(attributes, container);\n  }\n}\n\n// 第二步：注册自定义节点\nregister(ExtensionCategory.NODE, 'dual-label-node', DualLabelNode);\n\n// 第三步：使用自定义节点\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        style: { x: 100, y: 100 },\n        data: {\n          title: '节点 A', // 主标题\n          subtitle: '你的第一个自定义节点', // 副标题\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'dual-label-node',\n    style: {\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n      radius: 5,\n      // 主标题样式\n      labelText: (d) => d.data.title,\n      labelFill: '#222',\n      labelFontSize: 14,\n      labelFontWeight: 500,\n      // 副标题\n      subtitle: (d) => d.data.subtitle,\n    },\n  },\n});\n\ngraph.render();\n```\n\n### 第一步：编写自定义节点类\n\n继承 G6 的 `Rect`（矩形节点），并添加一个副标题：\n\n```js\nimport { Rect, register, Graph, ExtensionCategory } from '@antv/g6';\n\n// 创建自定义节点，继承自 Rect\nclass DualLabelNode extends Rect {\n  // 副标题样式\n  getSubtitleStyle(attributes) {\n    return {\n      x: 0,\n      y: 45, // 放在主标题下方\n      text: attributes.subtitle || '',\n      fontSize: 12,\n      fill: '#666',\n      textAlign: 'center',\n      textBaseline: 'middle',\n    };\n  }\n\n  // 绘制副标题\n  drawSubtitleShape(attributes, container) {\n    const subtitleStyle = this.getSubtitleStyle(attributes);\n    this.upsert('subtitle', 'text', subtitleStyle, container);\n  }\n\n  // 渲染方法\n  render(attributes = this.parsedAttributes, container) {\n    // 1. 渲染基础矩形和主标题\n    super.render(attributes, container);\n\n    // 2. 添加副标题\n    this.drawSubtitleShape(attributes, container);\n  }\n}\n```\n\n### 第二步：注册自定义节点\n\n使用 `register` 方法注册节点类型，这样 G6 才能识别你的自定义节点：\n\n```js\nregister(ExtensionCategory.NODE, 'dual-label-node', DualLabelNode);\n```\n\n`register` 方法需要三个参数：\n\n- 扩展类别：`ExtensionCategory.NODE` 表示这是一个节点类型\n- 类型名称：`dual-label-node` 是我们给这个自定义节点起的名字，后续会在配置中使用\n- 类定义：`DualLabelNode` 是我们刚刚创建的节点类\n\n### 第三步：应用自定义节点\n\n在图配置中使用自定义节点：\n\n```js\nconst graph = new Graph({\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        style: { x: 100, y: 100 },\n        data: {\n          title: '节点 A', // 主标题\n          subtitle: '你的第一个自定义节点', // 副标题\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'dual-label-node',\n    style: {\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n      radius: 8,\n      // 主标题样式\n      labelText: (d) => d.data.title,\n      labelFill: '#222',\n      labelFontSize: 14,\n      labelFontWeight: 500,\n      // 副标题\n      subtitle: (d) => d.data.subtitle,\n    },\n  },\n});\n\ngraph.render();\n```\n\n🎉 恭喜！你已经创建了第一个自定义节点。它看起来很简单，但这个过程包含了自定义节点的核心思想：**继承一个基础节点类型**，然后 **重写 `render` 方法** 来添加自定义内容。\n\n## 理解数据流：如何在自定义节点中获取数据\n\n在创建复杂的自定义节点之前，理解数据如何流入自定义节点是非常重要的。G6 为自定义节点提供了多种数据获取方式：\n\n### 方式一：通过 `attributes` 参数（推荐）\n\n`render` 方法的第一个参数 `attributes` 包含了经过处理的样式属性，包括数据驱动的样式：\n\n```js\nclass CustomNode extends Rect {\n  render(attributes, container) {\n    // attributes 包含了所有样式属性，包括数据驱动的样式\n    console.log('当前节点的所有属性:', attributes);\n\n    // 如果在 style 中定义了 customData: (d) => d.data.someValue\n    // 那么可以通过 attributes.customData 获取\n    const customValue = attributes.customData;\n\n    super.render(attributes, container);\n  }\n}\n```\n\n### 方式二：通过 `this.context.graph` 获取原始数据\n\n当你需要访问节点的原始数据时，可以通过图实例获取：\n\n```js\nclass CustomNode extends Rect {\n  // 便捷的数据获取方法\n  get nodeData() {\n    return this.context.graph.getNodeData(this.id);\n  }\n\n  get data() {\n    return this.nodeData.data || {};\n  }\n\n  render(attributes, container) {\n    // 获取节点的完整数据\n    const nodeData = this.nodeData;\n    console.log('节点完整数据:', nodeData);\n\n    // 获取 data 字段中的业务数据\n    const businessData = this.data;\n    console.log('业务数据:', businessData);\n\n    super.render(attributes, container);\n  }\n}\n```\n\n### 数据传递的完整流程\n\n让我们通过一个具体例子来理解数据是如何从图数据传递到自定义节点的：\n\n```js | ob { inject: true }\nimport { Graph, register, Rect, ExtensionCategory } from '@antv/g6';\n\nclass DataFlowNode extends Rect {\n  // 方式二：通过 graph 获取原始数据\n  get nodeData() {\n    return this.context.graph.getNodeData(this.id);\n  }\n\n  get data() {\n    return this.nodeData.data || {};\n  }\n\n  render(attributes, container) {\n    // 方式一：从 attributes 获取处理后的样式\n    console.log('从 attributes 获取:', {\n      iconUrl: attributes.iconUrl,\n      userName: attributes.userName,\n    });\n\n    // 方式二：从原始数据获取\n    console.log('从原始数据获取:', {\n      icon: this.data.icon,\n      name: this.data.name,\n      role: this.data.role,\n    });\n\n    // 渲染基础矩形\n    super.render(attributes, container);\n\n    // 使用数据渲染自定义内容\n    if (attributes.iconUrl) {\n      this.upsert(\n        'icon',\n        'image',\n        {\n          x: -25,\n          y: -12,\n          width: 20,\n          height: 20,\n          src: attributes.iconUrl,\n        },\n        container,\n      );\n    }\n\n    if (attributes.userName) {\n      this.upsert(\n        'username',\n        'text',\n        {\n          x: 10,\n          y: 0,\n          text: attributes.userName,\n          fontSize: 10,\n          fill: '#666',\n          textAlign: 'center',\n          textBaseline: 'middle',\n        },\n        container,\n      );\n    }\n  }\n}\n\nregister(ExtensionCategory.NODE, 'data-flow-node', DataFlowNode);\n\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      {\n        id: 'user1',\n        style: { x: 100, y: 100 },\n        // 这里是节点的业务数据\n        data: {\n          name: '张三',\n          role: '开发者',\n          icon: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'data-flow-node',\n    style: {\n      size: [80, 40],\n      fill: '#f0f9ff',\n      stroke: '#0ea5e9',\n      lineWidth: 1,\n      radius: 4,\n      // 将 data 中的数据映射到样式属性\n      iconUrl: (d) => d.data.icon, // 这会变成 attributes.iconUrl\n      userName: (d) => d.data.name, // 这会变成 attributes.userName\n      // 主标题使用角色信息\n      labelText: (d) => d.data.role,\n      labelFontSize: 12,\n      labelFill: '#0369a1',\n    },\n  },\n});\n\ngraph.render();\n```\n\n:::tip{title=数据流总结}\n\n1. **图数据定义**：在 `data.nodes[].data` 中定义业务数据\n2. **样式映射**：在 `node.style` 中使用函数将数据映射到样式属性\n3. **节点获取**：在自定义节点中通过 `attributes` 或 `this.context.graph` 获取数据\n4. **渲染使用**：使用获取到的数据渲染自定义图形\n   :::\n\n## 从简单到复杂：逐步构建功能丰富的节点\n\n让我们通过实际例子，逐步增加节点的复杂度和功能。\n\n### 示例一：带图标和徽章的用户卡片节点\n\n这个例子展示如何创建一个包含头像、姓名、状态徽章的用户卡片节点：\n\n```js | ob { inject: true }\nimport { Graph, register, Rect, ExtensionCategory } from '@antv/g6';\n\nclass UserCardNode extends Rect {\n  get nodeData() {\n    return this.context.graph.getNodeData(this.id);\n  }\n\n  get data() {\n    return this.nodeData.data || {};\n  }\n\n  // 头像样式\n  getAvatarStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2 + 20,\n      y: -height / 2 + 15,\n      width: 30,\n      height: 30,\n      src: attributes.avatarUrl || '',\n      radius: 15, // 圆形头像\n    };\n  }\n\n  drawAvatarShape(attributes, container) {\n    if (!attributes.avatarUrl) return;\n\n    const avatarStyle = this.getAvatarStyle(attributes);\n    this.upsert('avatar', 'image', avatarStyle, container);\n  }\n\n  // 状态徽章样式\n  getBadgeStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    const status = this.data.status || 'offline';\n    const colorMap = {\n      online: '#52c41a',\n      busy: '#faad14',\n      offline: '#8c8c8c',\n    };\n\n    return {\n      x: width / 2 - 8,\n      y: -height / 2 + 8,\n      r: 4,\n      fill: colorMap[status],\n      stroke: '#fff',\n      lineWidth: 2,\n    };\n  }\n\n  drawBadgeShape(attributes, container) {\n    const badgeStyle = this.getBadgeStyle(attributes);\n    this.upsert('badge', 'circle', badgeStyle, container);\n  }\n\n  // 用户名样式\n  getUsernameStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2 + 55,\n      y: -height / 2 + 20,\n      text: attributes.username || '',\n      fontSize: 14,\n      fill: '#262626',\n      fontWeight: 'bold',\n      textAlign: 'left',\n      textBaseline: 'middle',\n    };\n  }\n\n  drawUsernameShape(attributes, container) {\n    if (!attributes.username) return;\n\n    const usernameStyle = this.getUsernameStyle(attributes);\n    this.upsert('username', 'text', usernameStyle, container);\n  }\n\n  // 角色标签样式\n  getRoleStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2 + 55,\n      y: -height / 2 + 35,\n      text: attributes.userRole || '',\n      fontSize: 11,\n      fill: '#8c8c8c',\n      textAlign: 'left',\n      textBaseline: 'middle',\n    };\n  }\n\n  drawRoleShape(attributes, container) {\n    if (!attributes.userRole) return;\n\n    const roleStyle = this.getRoleStyle(attributes);\n    this.upsert('role', 'text', roleStyle, container);\n  }\n\n  render(attributes, container) {\n    // 渲染基础矩形\n    super.render(attributes, container);\n\n    // 添加各个组件\n    this.drawAvatarShape(attributes, container);\n    this.drawBadgeShape(attributes, container);\n    this.drawUsernameShape(attributes, container);\n    this.drawRoleShape(attributes, container);\n  }\n}\n\nregister(ExtensionCategory.NODE, 'user-card-node', UserCardNode);\n\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      {\n        id: 'user1',\n        style: { x: 100, y: 100 },\n        data: {\n          name: '张小明',\n          role: '前端工程师',\n          status: 'online',\n          avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Zhang',\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'user-card-node',\n    style: {\n      size: [140, 50],\n      fill: '#ffffff',\n      stroke: '#d9d9d9',\n      lineWidth: 1,\n      radius: 6,\n      // 数据映射\n      avatarUrl: (d) => d.data.avatar,\n      username: (d) => d.data.name,\n      userRole: (d) => d.data.role,\n    },\n  },\n});\n\ngraph.render();\n```\n\n### 示例二：可点击操作按钮的节点\n\n给节点加一个蓝色按钮，点击后触发事件（打印日志或执行回调）。\n\n```js | ob { inject: true }\nimport { Graph, register, Rect, ExtensionCategory } from '@antv/g6';\n\nclass ClickableNode extends Rect {\n  getButtonStyle(attributes) {\n    return {\n      x: 40,\n      y: -10,\n      width: 20,\n      height: 20,\n      radius: 10,\n      fill: '#1890ff',\n      cursor: 'pointer', // 鼠标指针变为手型\n    };\n  }\n\n  drawButtonShape(attributes, container) {\n    const btnStyle = this.getButtonStyle(attributes, container);\n    const btn = this.upsert('button', 'rect', btnStyle, container);\n\n    // 为按钮添加点击事件\n    if (!btn.__clickBound) {\n      btn.addEventListener('click', (e) => {\n        // 阻止事件冒泡，避免触发节点的点击事件\n        e.stopPropagation();\n\n        // 执行业务逻辑\n        console.log('Button clicked on node:', this.id);\n\n        // 如果数据中有回调函数，则调用\n        if (typeof attributes.onButtonClick === 'function') {\n          attributes.onButtonClick(this.id, this.data);\n        }\n      });\n      btn.__clickBound = true; // 标记已绑定事件，避免重复绑定\n    }\n  }\n\n  render(attributes, container) {\n    super.render(attributes, container);\n\n    // 添加一个按钮\n    this.drawButtonShape(attributes, container);\n  }\n}\n\nregister(ExtensionCategory.NODE, 'clickable-node', ClickableNode);\n\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        style: { x: 100, y: 100 },\n      },\n    ],\n  },\n  node: {\n    type: 'clickable-node', // 指定使用我们的自定义节点\n    style: {\n      size: [60, 30],\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n      radius: 5,\n      onButtonClick: (id, data) => {},\n    },\n  },\n});\n\ngraph.render();\n```\n\n### 示例三：响应状态变化的节点（点击变色）\n\n常见的交互都需要节点和边通过样式变化做出反馈，例如鼠标移动到节点上、点击选中节点/边、通过交互激活边上的交互等，都需要改变节点和边的样式，有两种方式来实现这种效果：\n\n1. 从 `data.states` 获取当前状态，在自定义节点类中处理状态变化；\n2. 将交互状态同原始数据和绘制节点的逻辑分开，仅更新节点。\n\n我们推荐用户使用第二种方式来实现节点的状态调整，可以通过以下方式来实现：\n\n1. 实现自定义节点；\n2. 在图配置项中配置节点状态样式；\n3. 通过 `graph.setElementState()` 方法来设置节点状态。\n\n基于 rect 扩展出一个 hole 图形，默认填充色为白色，当鼠标点击时变成橙色，实现这一效果的示例代码如下：\n\n```js | ob { inject: true }\nimport { Rect, register, Graph, ExtensionCategory } from '@antv/g6';\n\n// 1. 定义节点类\nclass SelectableNode extends Rect {\n  getHoleStyle(attributes) {\n    return {\n      x: 20,\n      y: -10,\n      radius: 10,\n      width: 20,\n      height: 20,\n      fill: attributes.holeFill,\n    };\n  }\n\n  drawHoleShape(attributes, container) {\n    const holeStyle = this.getHoleStyle(attributes, container);\n\n    this.upsert('hole', 'rect', holeStyle, container);\n  }\n\n  render(attributes, container) {\n    super.render(attributes, container);\n\n    this.drawHoleShape(attributes, container);\n  }\n}\n\n// 2. 注册节点\nregister(ExtensionCategory.NODE, 'selectable-node', SelectableNode, true);\n\n// 3. 创建图实例\nconst graph = new Graph({\n  container: 'container',\n  height: 200,\n  data: {\n    nodes: [{ id: 'node-1', style: { x: 100, y: 100 } }],\n  },\n  node: {\n    type: 'selectable-node',\n    style: {\n      size: [120, 60],\n      radius: 6,\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n      holeFill: '#fff',\n    },\n    state: {\n      // 鼠标选中状态\n      selected: {\n        holeFill: 'orange',\n      },\n    },\n  },\n});\n\n// 4. 添加节点交互\ngraph.on('node:click', (evt) => {\n  const nodeId = evt.target.id;\n\n  graph.setElementState(nodeId, ['selected']);\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/overview.en.md",
    "content": "---\ntitle: Node Overview\norder: 0\n---\n\n## What is a Node\n\nA node is one of the basic elements in a graph, representing an entity or an abstract concept, such as a person, a place, an organization, etc. Nodes can contain attributes like ID, name, type, etc. In G6, nodes can have various shapes and styles, and support rich interactions and customization.\n\nYou can create any number of nodes in a graph and connect them with edges to represent relationships.\n\n## Node System\n\nThe G6 node system includes three main categories: built-in nodes, extended nodes, and custom nodes. **In most cases, built-in nodes are sufficient.**\n\n### Built-in Nodes\n\nG6 provides a variety of built-in node types, **which can be used directly without registration**:\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*TZt2S7Z0d-8AAAAAAAAAAAAADmJ7AQ/original\" />\n\n| Node Type      | Registration Name | Description                                     |\n| -------------- | ----------------- | ----------------------------------------------- |\n| Circle Node    | `circle`          | Commonly used for entities                      |\n| Rectangle Node | `rect`            | Suitable for more text and details              |\n| Ellipse Node   | `ellipse`         | A variant of the circle                         |\n| Diamond Node   | `diamond`         | Often used for decision points or special nodes |\n| Triangle Node  | `triangle`        | Can indicate direction or special marks         |\n| Hexagon Node   | `hexagon`         | Suitable for grid layouts and honeycomb charts  |\n| Star Node      | `star`            | Highlights important nodes                      |\n| Donut Node     | `donut`           | Can display proportions or progress             |\n| Image Node     | `image`           | Uses an image as the node body                  |\n| HTML Node      | `html`            | Supports custom HTML content                    |\n\n### 3D Nodes\n\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ShNXTp0u3vkAAAAAAAAAAAAADmJ7AQ/original\" />\n\n`@antv/g6-extension-3d` provides 3D nodes:\n\n- `Capsule` - Capsule-shaped node\n- `Cone` - Cone-shaped node\n- `Cube` - Cube-shaped node\n- `Cylinder` - Cylinder-shaped node\n- `Plane` - Plane node\n- `Sphere` - Sphere node\n- `Torus` - Torus node\n\n### React Nodes\n\n<image width=\"350\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*7jypQbkp00wAAAAAAAAAAAAADmJ7AQ/original\" />\n\n`@antv/g6-extension-react` provides React nodes, supporting the use of React components as the node body. For detailed tutorials, please refer to the [Using React to Define Nodes](/en/manual/element/node/react-node) document.\n\n### Custom Nodes\n\nWhen built-in and extended nodes cannot meet the requirements, G6 offers powerful customization capabilities:\n\n- Extend built-in nodes\n- Create entirely new node types\n\nUnlike built-in nodes, **custom nodes need to be registered before use**. For detailed tutorials, please refer to the [Custom Nodes](/en/manual/element/node/custom-node) document.\n\n## Data Structure\n\nWhen defining nodes, you need to add a `nodes` field to the graph's data object. Each node is an object with the following structure:\n\n| Property | Description                                                                                                                             | Type           | Default | Required |\n| -------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------- | -------- |\n| id       | Unique identifier for the node, used to distinguish different nodes                                                                     | string         | -       | ✓        |\n| type     | Node type, either a built-in node type name or a custom node name                                                                       | string         | -       |          |\n| data     | Node data, used to store custom data such as the node's name, description, etc. Can be accessed via callback functions in style mapping | object         | -       |          |\n| style    | Node style, including visual attributes like position, size, color, etc.                                                                | object         | -       |          |\n| states   | Initial states of the node, such as selected, active, hover, etc.                                                                       | string[]       | -       |          |\n| combo    | The ID of the combo to which the node belongs, used to organize hierarchical relationships. If none, it is null                         | string \\| null | -       |          |\n| children | Collection of child node IDs, used only in tree graph scenarios                                                                         | string[]       | -       |          |\n\nAn example of a data item in the `nodes` array:\n\n```json\n{\n  \"id\": \"node-1\",\n  \"type\": \"circle\",\n  \"data\": { \"name\": \"alice\", \"role\": \"Admin\" },\n  \"style\": { \"x\": 100, \"y\": 200, \"size\": 32, \"fill\": \"violet\" },\n  \"states\": [\"selected\"],\n  \"combo\": null\n}\n```\n\n## Configuration Methods\n\nThere are three ways to configure nodes, listed in order of priority from high to low:\n\n- Use `graph.setNode()` for dynamic configuration\n- Global configuration during graph instantiation\n- Dynamic properties in data\n\nThese configuration methods can be used simultaneously. When there are identical configuration items, the method with higher priority will override the one with lower priority.\n\n### Using `graph.setNode()`\n\nAfter creating the graph instance, you can use `graph.setNode()` to dynamically set the node's style mapping logic.\n\nThis method must be called before `graph.render()` to take effect and has the highest priority.\n\n```js\ngraph.setNode({\n  style: {\n    type: 'circle',\n    style: { size: 60, fill: '#7FFFD4', stroke: '#5CACEE', lineWidth: 2 },\n  },\n});\n\ngraph.render();\n```\n\n### Global Configuration During Graph Instantiation\n\nWhen instantiating the graph, you can configure node style mapping through `node`, which is a global configuration and will apply to all nodes.\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  node: {\n    type: 'circle',\n    style: { size: 60, fill: '#7FFFD4', stroke: '#5CACEE', lineWidth: 2 },\n  },\n});\n```\n\n### Dynamic Configuration in Data\n\nIf you need different configurations for different nodes, you can write the configuration into the node data. This configuration method can be directly written into the data in the following form:\n\n```typescript\nconst data = {\n  nodes: [\n    {\n      id: 'node-1',\n      type: 'circle',\n      style: { size: 60, fill: '#7FFFD4', stroke: '#5CACEE', lineWidth: 2 },\n    },\n  ],\n};\n```\n\n### Adjusting Priority\n\nIf you want the configuration in the data to have a higher priority than the global configuration, you can do so as follows:\n\n```js\nconst data = {\n  nodes: [\n    {\n      id: 'node-1',\n      type: 'circle',\n      style: { size: 60, fill: '#7FFFD4', stroke: '#5CACEE', lineWidth: 2 },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  node: {\n    type: 'circle',\n    style: {\n      stroke: (d) => d.style.stroke || '#5CACEE',\n      lineWidth: 2,\n    },\n  },\n});\n```\n\n### Dynamically Updating Nodes\n\nG6 supports dynamically updating the style and state of nodes at runtime:\n\n```typescript\n// Update the style of a single node\ngraph.updateNodeData([\n  {\n    id: 'node-1',\n    style: {\n      fill: 'red',\n      size: 80,\n    },\n  },\n]);\ngraph.draw();\n\n// Set node state\ngraph.setElementState('node-1', ['selected']);\n```\n\n:::warning{title=Note}\nWhen updating nodes, only the specified attributes will be updated, and unspecified attributes will remain unchanged.\n:::\n\nFor more node-related APIs, please refer to [API - Element Operations](/en/api/element).\n\n## Node States\n\nNodes can have different states, such as selected, highlighted, disabled, etc. You can define the display effect of nodes in different states by configuring state styles:\n\n```typescript\nconst graph = new Graph({\n  node: {\n    style: {\n      // Default style\n      fill: '#C6E5FF',\n    },\n    // State styles\n    state: {\n      selected: {\n        fill: '#ffa940',\n        stroke: '#ff7a00',\n        haloStroke: '#ff7a00',\n      },\n      highlight: {\n        stroke: '#1890ff',\n        lineWidth: 3,\n      },\n    },\n  },\n});\n```\n\nThe state system is the foundation for implementing node interaction effects. For more information on states, please refer to [Element States](/en/manual/element/state).\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/overview.zh.md",
    "content": "---\ntitle: 节点总览\norder: 0\n---\n\n## 什么是节点\n\n节点（Node）是图中的基本元素之一，表示图中的实体或者抽象概念，例如一个人、一个地点、一个组织等，节点可以包含一些属性，例如节点的 ID、名称、类型等。在 G6 中，节点可以具有多种形状和样式，并支持丰富的交互和自定义功能。\n\n你可以在图中创建任意数量的节点，并通过边连接它们以表示关系。\n\n## 节点体系\n\nG6 的节点体系包括三大类：内置节点、扩展节点和自定义节点。**大多数场景下，内置节点即可满足需求**。\n\n### 内置节点\n\nG6 提供了丰富的内置节点类型，**无需注册，直接配置即可使用**：\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*TZt2S7Z0d-8AAAAAAAAAAAAADmJ7AQ/original\" />\n\n| 节点类型   | 注册名称   | 描述                     |\n| ---------- | ---------- | ------------------------ |\n| 圆形节点   | `circle`   | 常用于表示普通实体       |\n| 矩形节点   | `rect`     | 适合展示更多文本和细节   |\n| 椭圆节点   | `ellipse`  | 类似圆形的变体           |\n| 菱形节点   | `diamond`  | 常用于决策点或特殊节点   |\n| 三角形节点 | `triangle` | 可用于指示方向或特殊标记 |\n| 六边形节点 | `hexagon`  | 适合网格布局和蜂窝图     |\n| 星形节点   | `star`     | 突出显示重要节点         |\n| 甜甜圈节点 | `donut`    | 可展示比例或进度信息     |\n| 图片节点   | `image`    | 使用图片作为节点主体     |\n| HTML节点   | `html`     | 支持自定义HTML内容       |\n\n### 3D 节点\n\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ShNXTp0u3vkAAAAAAAAAAAAADmJ7AQ/original\" />\n\n`@antv/g6-extension-3d` 提供了 3D 节点：\n\n- `Capsule` - 胶囊型节点\n- `Cone` - 圆锥型节点\n- `Cube` - 立方体节点\n- `Cylinder` - 圆柱型节点\n- `Plane` - 平面节点\n- `Sphere` - 球体节点\n- `Torus` - 圆环节点\n\n### React 节点\n\n<image width=\"350\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*7jypQbkp00wAAAAAAAAAAAAADmJ7AQ/original\" />\n\n`@antv/g6-extension-react` 提供了 React 节点，支持使用 React 组件作为节点的主体，详细教程请查看 [使用 React 定义节点](/manual/element/node/react-node) 文档。\n\n### 自定义节点\n\n当内置节点和扩展节点无法满足需求时，G6提供了强大的自定义能力：\n\n- 继承内置节点进行扩展\n- 创建全新的节点类型\n\n与内置节点不同，**自定义节点需要先注册后使用**。详细教程请参考 [自定义节点](/manual/element/node/custom-node) 文档。\n\n## 数据结构\n\n定义节点时，需要在图的数据对象中添加 `nodes` 字段。每个节点是一个对象，结构如下：\n\n| 属性     | 描述                                                                                         | 类型           | 默认值 | 必选 |\n| -------- | -------------------------------------------------------------------------------------------- | -------------- | ------ | ---- |\n| id       | 节点的唯一标识符，用于区分不同的节点                                                         | string         | -      | ✓    |\n| type     | 节点类型，内置节点类型名称或者自定义节点的名称                                               | string         | -      |      |\n| data     | 节点数据，用于存储节点的自定义数据，例如节点的名称、描述等。可以在样式映射中通过回调函数获取 | object         | -      |      |\n| style    | 节点样式，包括位置、大小、颜色等视觉属性                                                     | object         | -      |      |\n| states   | 节点初始状态，如选中、激活、悬停等                                                           | string[]       | -      |      |\n| combo    | 所属的组合 ID，用于组织节点的层级关系，如果没有则为 null                                     | string \\| null | -      |      |\n| children | 子节点 ID 集合，仅在树图场景下使用                                                           | string[]       | -      |      |\n\n`nodes` 数组中一个数据项的示例：\n\n```json\n{\n  \"id\": \"node-1\",\n  \"type\": \"circle\",\n  \"data\": { \"name\": \"alice\", \"role\": \"Admin\" },\n  \"style\": { \"x\": 100, \"y\": 200, \"size\": 32, \"fill\": \"violet\" },\n  \"states\": [\"selected\"],\n  \"combo\": null\n}\n```\n\n## 配置方法\n\n配置节点的方式有三种，按优先级从高到低如下：\n\n- 使用 `graph.setNode()` 动态配置\n- 实例化图时全局配置\n- 在数据中动态属性\n\n这几个配置方法可以同时使用。有相同的配置项时，优先级高的方式将会覆盖优先级低的。\n\n### 使用 `graph.setNode()`\n\n可在图实例创建后，使用 `graph.setNode()` 动态设置节点的样式映射逻辑。\n\n该方法需要在 `graph.render()` 之前调用才会生效，并拥有最高优先级。\n\n```js\ngraph.setNode({\n  style: {\n    type: 'circle',\n    style: { size: 60, fill: '#7FFFD4', stroke: '#5CACEE', lineWidth: 2 },\n  },\n});\n\ngraph.render();\n```\n\n### 实例化图时全局配置\n\n在实例化图时可以通过 `node` 配置节点样式映射，这里的配置是全局的配置，将会在所有节点上生效。\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  node: {\n    type: 'circle',\n    style: { size: 60, fill: '#7FFFD4', stroke: '#5CACEE', lineWidth: 2 },\n  },\n});\n```\n\n### 在数据中动态配置\n\n如果需要为不同节点进行不同的配置，可以将配置写入到节点数据中。这种配置方式可以通过下面代码的形式直接写入数据：\n\n```typescript\nconst data = {\n  nodes: [\n    {\n      id: 'node-1',\n      type: 'circle',\n      style: { size: 60, fill: '#7FFFD4', stroke: '#5CACEE', lineWidth: 2 },\n    },\n  ],\n};\n```\n\n### 调整优先级\n\n如果你想让数据中配置的优先级高于全局配置，你可以采取以下方式：\n\n```js\nconst data = {\n  nodes: [\n    {\n      id: 'node-1',\n      type: 'circle',\n      style: { size: 60, fill: '#7FFFD4', stroke: '#5CACEE', lineWidth: 2 },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  node: {\n    type: 'circle',\n    style: {\n      stroke: (d) => d.style.stroke || '#5CACEE',\n      lineWidth: 2,\n    },\n  },\n});\n```\n\n### 动态更新节点\n\nG6 支持在运行时动态更新节点的样式和状态：\n\n```typescript\n// 更新单个节点样式\ngraph.updateNodeData([\n  {\n    id: 'node-1',\n    style: {\n      fill: 'red',\n      size: 80,\n    },\n  },\n]);\ngraph.draw();\n\n// 设置节点状态\ngraph.setElementState('node-1', ['selected']);\n```\n\n:::warning{title=注意}\n更新节点时，只有指定的属性会被更新，未指定的属性保持不变。\n:::\n\n更多与节点相关的 API 请参考 [API - 元素操作](/api/element)。\n\n## 节点状态\n\n节点可以拥有不同的状态，例如选中、高亮、禁用等。可以通过配置状态样式来定义节点在不同状态下的显示效果：\n\n```typescript\nconst graph = new Graph({\n  node: {\n    style: {\n      // 默认样式\n      fill: '#C6E5FF',\n    },\n    // 状态样式\n    state: {\n      selected: {\n        fill: '#ffa940',\n        stroke: '#ff7a00',\n        haloStroke: '#ff7a00',\n      },\n      highlight: {\n        stroke: '#1890ff',\n        lineWidth: 3,\n      },\n    },\n  },\n});\n```\n\n状态系统是实现节点交互效果的基础，更多状态的介绍，请参考 [元素状态](/manual/element/state)。\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/react-node.en.md",
    "content": "---\ntitle: Define Nodes with React\norder: 13\n---\n\nIn G6, custom nodes typically require manipulating DOM or Canvas elements, but with the help of the `@antv/g6-extension-react` ecosystem library, you can directly use React components as node content, enhancing development efficiency and maintainability.\n\n## Choosing a Custom Node Solution\n\n### G6 Node\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*sEaLR7Q_hmoAAAAAAAAAAAAAemJ7AQ/fmt.avif\" width=\"300\" />\n\n✅ **Recommended Scenarios:**\n\n- Nodes are simple geometric shapes\n- Scenarios requiring efficient rendering of more than 2,000 nodes\n- Need to directly manipulate graphic instances for fine control\n\n> For detailed information on how to customize nodes using Canvas graphics, please refer to the [Custom Node](/en/manual/element/node/custom-node) documentation\n\n### React Node\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*9oz-R7bIkd0AAAAAAAAAAAAADmJ7AQ/original\" width=\"350\" />\n\n✅ **Recommended Scenarios:**\n\n- Business systems that need to integrate UI libraries like Ant Design\n- Nodes contain interactive logic such as form input, state switching\n- Scenarios where an existing React design system needs to be reused\n\n## Quick Start\n\n### Environment Preparation\n\nBefore starting, please ensure you have:\n\n- **Installed a React project**: Ensure a React project is installed and created.\n- **React version requirement**: Ensure the React version used is >=16.8.0.\n\n### Install Dependencies\n\nTo use `@antv/g6-extension-react`, run the following command:\n\n:::code-group\n\n```bash [npm]\nnpm install @antv/g6-extension-react\n```\n\n```bash [yarn]\nyarn add @antv/g6-extension-react\n```\n\n```bash [pnpm]\npnpm add @antv/g6-extension-react\n```\n\n:::\n\n### Component Integration\n\n#### 1. Register React Node Type\n\nRegister the React node type through the extension mechanism:\n\n```jsx\nimport { ExtensionCategory, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\n\nregister(ExtensionCategory.NODE, 'react-node', ReactNode);\n```\n\nThe `register` method requires three parameters:\n\n- Extension category: `ExtensionCategory.NODE` indicates this is a node type\n- Type name: `react-node` is the name we give to this custom node, which will be used in the configuration later\n- Class definition: ReactNode is the implementation class exported by `@antv/g6-extension-react`\n\n#### 2. Define Business Component\n\nDefine a simple React component as the content of the node:\n\n```jsx\nconst MyReactNode = () => {\n  return <div>node</div>;\n};\n```\n\n#### 3. Use the Component\n\nUse the custom React node in the graph configuration. Specify the node type and style in the graph configuration to use the custom React component.\n\n- `type`: Specify the node type as `react-node` (use the name given during registration)\n- `style.component`: Define the React component content of the node\n\n```jsx\nconst graph = new Graph({\n  node: {\n    type: 'react-node',\n    style: {\n      component: () => <MyReactNode />,\n    },\n  },\n});\n\ngraph.render();\n```\n\n## Advanced Features\n\n### State Management\n\nIn complex graph visualization scenarios, nodes need to dynamically respond to interaction states. We provide two complementary state management solutions:\n\n#### Respond to Built-in Interaction States\n\nG6 provides built-in interaction state management states, such as `hover-activate` and `click-select`. You can get the current node state through the `data.states` field in the node data and adjust the node style based on the state.\n\n**Example**: Change the background color when the node is hovered.\n\n```jsx\nimport { ExtensionCategory, register, Graph } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\n\nregister(ExtensionCategory.NODE, 'react-node', ReactNode);\n\nconst StatefulNode = ({ data }) => {\n  const isActive = data.states?.includes('active');\n\n  return (\n    <div\n      style={{\n        width: 100,\n        padding: 5,\n        border: '1px solid #eee',\n        boxShadow: isActive ? '0 0 8px rgba(24,144,255,0.8)' : 'none',\n        transform: `scale(${isActive ? 1.05 : 1})`,\n      }}\n    >\n      {data.data.label}\n    </div>\n  );\n};\n\nconst graph = new Graph({\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 200 }, data: { label: 'node1' } },\n      { id: 'node2', style: { x: 300, y: 200 }, data: { label: 'node2' } },\n    ],\n  },\n  node: {\n    type: 'react-node',\n    style: {\n      component: (data) => <StatefulNode data={data} />,\n    },\n  },\n  behaviors: ['hover-activate'],\n});\n\ngraph.render();\n```\n\n#### Custom Business State\n\nWhen you need to manage business-related states (such as approval status, risk level), you can extend node data to achieve this:\n\n**Example**: Add a `selected` variable through data to achieve style changes for node selection and deselection.\n\n```jsx\nimport { ExtensionCategory, register, Graph } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\n\nregister(ExtensionCategory.NODE, 'react-node', ReactNode);\n\nconst MyReactNode = ({ data, graph }) => {\n  const handleClick = () => {\n    graph.updateNodeData([{ id: data.id, data: { selected: !data.data.selected } }]);\n    graph.draw();\n  };\n\n  return (\n    <div\n      style={{\n        width: 200,\n        padding: 10,\n        border: '1px solid red',\n        borderColor: data.data.selected ? 'orange' : '#ddd', // Set border color based on selection state\n        cursor: 'pointer', // Add mouse pointer style\n      }}\n      onClick={handleClick}\n    >\n      Node\n    </div>\n  );\n};\n\nconst graph = new Graph({\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        style: { x: 100, y: 100 },\n        data: { selected: true },\n      },\n    ],\n  },\n  node: {\n    type: 'react-node',\n    style: {\n      component: (data) => <MyReactNode data={data} graph={graph} />,\n    },\n  },\n});\n\ngraph.render();\n```\n\n### Event Interaction\n\nAchieve two-way communication between nodes and graph instances, allowing nodes and graph instances to update each other.\n\n**Example**: Operate graph data through custom nodes and re-render the graph.\n\n```jsx\nconst IDCardNode = ({ id, selected, graph }) => {\n  const handleSelect = () => {\n    graph.updateNodeData([{ id, data: { selected: true } }]);\n    graph.draw();\n  };\n\n  return <Select onChange={handleSelect} style={{ background: selected ? 'orange' : '#eee' }} />;\n};\n\nconst graph = new Graph({\n  node: {\n    type: 'react-node',\n    style: {\n      component: ({ id, data }) => <IDCardNode id={id} selected={data.selected} graph={graph} />,\n    },\n  },\n});\n```\n\n## Real Cases\n\n```js | ob { inject: true }\nimport { DatabaseFilled } from '@ant-design/icons';\nimport { ExtensionCategory, Graph, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\nimport { Badge, Flex, Input, Tag, Typography } from 'antd';\nimport { useEffect, useRef } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nconst { Text } = Typography;\n\nregister(ExtensionCategory.NODE, 'react', ReactNode);\n\nconst Node = ({ data, onChange }) => {\n  const { status, type } = data.data;\n\n  return (\n    <Flex\n      style={{\n        width: '100%',\n        height: '100%',\n        background: '#fff',\n        padding: 10,\n        borderRadius: 5,\n        border: '1px solid gray',\n      }}\n      vertical\n    >\n      <Flex align=\"center\" justify=\"space-between\">\n        <Text>\n          <DatabaseFilled />\n          Server\n          <Tag>{type}</Tag>\n        </Text>\n        <Badge status={status} />\n      </Flex>\n      <Text type=\"secondary\">{data.id}</Text>\n      <Flex align=\"center\">\n        <Text style={{ flexShrink: 0 }}>\n          <Text type=\"danger\">*</Text>URL:\n        </Text>\n        <Input\n          style={{ borderRadius: 0, borderBottom: '1px solid #d9d9d9' }}\n          variant=\"borderless\"\n          value={data.data?.url}\n          onChange={(event) => {\n            const url = event.target.value;\n            onChange?.(url);\n          }}\n        />\n      </Flex>\n    </Flex>\n  );\n};\n\nexport const ReactNodeDemo = () => {\n  const containerRef = useRef();\n\n  useEffect(() => {\n    const graph = new Graph({\n      container: containerRef.current,\n      data: {\n        nodes: [\n          {\n            id: 'local-server-1',\n            data: { status: 'success', type: 'local', url: 'http://localhost:3000' },\n            style: { x: 50, y: 50 },\n          },\n          {\n            id: 'remote-server-1',\n            data: { status: 'warning', type: 'remote' },\n            style: { x: 350, y: 50 },\n          },\n        ],\n        edges: [{ source: 'local-server-1', target: 'remote-server-1' }],\n      },\n      node: {\n        type: 'react',\n        style: {\n          size: [240, 100],\n          component: (data) => <Node data={data} />,\n        },\n      },\n      behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  }, []);\n\n  return <div style={{ width: '100%', height: '100%' }} ref={containerRef}></div>;\n};\n\nconst root = createRoot(document.getElementById('container'));\nroot.render(<ReactNodeDemo />);\n```\n\n<br/>\n\n```js | ob { inject: true }\nimport { UserOutlined } from '@ant-design/icons';\nimport { ExtensionCategory, Graph, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\nimport { Avatar, Button, Card, Descriptions, Select, Space, Typography } from 'antd';\nimport React, { useEffect, useRef } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nconst { Title, Text } = Typography;\nconst { Option } = Select;\n\nregister(ExtensionCategory.NODE, 'react-node', ReactNode);\n\nconst IDCardNode = ({ id, data }) => {\n  const { name, idNumber, address, expanded, selected, graph } = data;\n\n  const toggleExpand = (e) => {\n    e.stopPropagation();\n    graph.updateNodeData([\n      {\n        id,\n        data: { expanded: !expanded },\n      },\n    ]);\n    graph.render();\n  };\n\n  const handleSelect = (value) => {\n    graph.updateNodeData([\n      {\n        id,\n        data: { selected: value !== 0 },\n      },\n    ]);\n    if (value === 2) {\n      // 获取与当前节点相连的所有节点\n      const connectedNodes = graph.getNeighborNodesData(id);\n\n      connectedNodes.forEach((node) => {\n        graph.updateNodeData([\n          {\n            id: node.id,\n            data: { selected: true },\n          },\n        ]);\n      });\n    }\n    graph.render();\n  };\n\n  const CardTitle = (\n    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>\n      <Space>\n        <Avatar shape=\"square\" size=\"small\" icon={<UserOutlined />} />\n        <Title level={5} style={{ margin: 0 }}>\n          {name}\n        </Title>\n\n        <Select\n          value={selected ? data.selectedOption || 1 : 0}\n          style={{ width: 150, marginRight: 8 }}\n          onChange={handleSelect}\n        >\n          <Option value={0}>None</Option>\n          <Option value={1}>Node</Option>\n          <Option value={2}>Connected</Option>\n        </Select>\n      </Space>\n      <Button type=\"link\" onClick={toggleExpand} style={{ padding: 0 }}>\n        {expanded ? 'fold' : 'expand'}\n      </Button>\n    </div>\n  );\n\n  return (\n    <Card\n      size=\"small\"\n      title={CardTitle}\n      style={{\n        width: 340,\n        padding: 10,\n        borderRadius: 8,\n        borderWidth: 2,\n        borderColor: selected ? 'orange' : '#eee', // 根据选中状态设置边框颜色\n        cursor: 'pointer',\n      }}\n    >\n      {expanded ? (\n        <Descriptions bordered column={1} style={{ width: '100%', textAlign: 'center' }}>\n          <Descriptions.Item label=\"ID Number\">{idNumber}</Descriptions.Item>\n          <Descriptions.Item label=\"Address\">{address}</Descriptions.Item>\n        </Descriptions>\n      ) : (\n        <Text style={{ textAlign: 'center' }}>IDCard Information</Text>\n      )}\n    </Card>\n  );\n};\n\n// 定义 Graph 数据\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n      data: {\n        name: 'Alice',\n        idNumber: 'IDUSAASD2131734',\n        address: '1234 Broadway, Apt 5B, New York, NY 10001',\n        expanded: false, // 初始状态为收缩\n        selected: false, // 初始状态为未选中\n        selectedOption: 1, // 初始选择本节点\n      },\n      style: { x: 50, y: 50 },\n    },\n    {\n      id: 'node2',\n      data: {\n        name: 'Bob',\n        idNumber: 'IDUSAASD1431920',\n        address: '3030 Chestnut St, Philadelphia, PA 19104',\n        expanded: false, // 初始状态为收缩\n        selected: false, // 初始状态为未选中\n        selectedOption: 0, // 初始不选择\n      },\n      style: { x: 700, y: 100 },\n    },\n    {\n      id: 'node3',\n      data: {\n        name: 'Charlie',\n        idNumber: 'IDUSAASD1431921',\n        address: '4040 Elm St, Chicago, IL 60611',\n        expanded: false,\n        selected: true,\n        selectedOption: 0,\n      },\n    },\n    {\n      id: 'node4',\n      data: {\n        name: 'David',\n        idNumber: 'IDUSAASD1431922',\n        address: '5050 Oak St, Houston, TX 77002',\n        expanded: false,\n        selected: false,\n        selectedOption: 0,\n      },\n    },\n    {\n      id: 'node5',\n      data: {\n        name: 'Eve',\n        idNumber: 'IDUSAASD1431923',\n        address: '6060 Pine St, Phoenix, AZ 85001',\n        expanded: false,\n        selected: false,\n        selectedOption: 0,\n      },\n    },\n  ],\n  edges: [\n    { source: 'node1', target: 'node2' },\n    { source: 'node2', target: 'node3' },\n    { source: 'node3', target: 'node4' },\n    { source: 'node4', target: 'node5' },\n  ],\n};\n\nexport const ReactNodeDemo = () => {\n  const containerRef = useRef();\n  const graphRef = useRef(null);\n\n  useEffect(() => {\n    // 创建 Graph 实例\n    const graph = new Graph({\n      autoFit: 'view',\n      container: containerRef.current,\n      data,\n      node: {\n        type: 'react-node',\n        style: {\n          size: (datum) => (datum.data.expanded ? [340, 236] : [340, 105]), // 调整大小以适应内容\n          component: (data) => <IDCardNode id={data.id} data={{ ...data.data, graph: graph }} />,\n        },\n      },\n      behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],\n      layout: {\n        type: 'snake',\n        cols: 2,\n        rowGap: 100,\n        colGap: 220,\n      },\n    });\n\n    // 渲染 Graph\n    graph.render();\n\n    // 保存 graph 实例\n    graphRef.current = graph;\n\n    return () => {\n      graph.destroy();\n    };\n  }, []);\n\n  return <div style={{ width: '100%', height: '100%' }} ref={containerRef}></div>;\n};\n\n// 渲染 React 组件到 DOM\nconst root = createRoot(document.getElementById('container'));\nroot.render(<ReactNodeDemo />);\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/react-node.zh.md",
    "content": "---\ntitle: 使用 React 定义节点\norder: 13\n---\n\n在 G6 中，自定义节点通常需要操作 DOM 或 Canvas 元素，但借助 `@antv/g6-extension-react` 一方生态库，可以直接使用 React 组件作为节点内容，提升开发效率与可维护性。\n\n## 自定义节点方案选择\n\n### G6 节点\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*sEaLR7Q_hmoAAAAAAAAAAAAAemJ7AQ/fmt.avif\" width=\"300\" />\n\n✅ **推荐场景：**\n\n- 节点只是简单的几何图形\n- 需要高效渲染超过 2,000 个节点的场景\n- 需要直接操作图形实例进行精细控制\n\n> 有关如何使用 Canvas 图形自定义节点的详细信息，请参阅 [自定义节点](/manual/element/node/custom-node) 文档\n\n### React Node\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*9oz-R7bIkd0AAAAAAAAAAAAADmJ7AQ/original\" width=\"350\" />\n\n✅ **推荐场景：**\n\n- 需要集成 Ant Design 等 UI 库的业务系统\n- 节点包含表单输入、状态切换等交互逻辑\n- 已有 React 设计系统需要复用的场景\n\n## 快速入门\n\n### 环境准备\n\n在开始之前，请确保您已经：\n\n- **安装 React 项目**：确保已安装并创建 React 项目。\n- **React 版本要求**：确保使用的 React 版本 >=16.8.0。\n\n### 安装依赖\n\n要使用 `@antv/g6-extension-react`，请运行以下命令：\n\n:::code-group\n\n```bash [npm]\nnpm install @antv/g6-extension-react\n```\n\n```bash [yarn]\nyarn add @antv/g6-extension-react\n```\n\n```bash [pnpm]\npnpm add @antv/g6-extension-react\n```\n\n:::\n\n### 组件集成\n\n#### 1. 注册 React 节点类型\n\n通过扩展机制注册 React 节点类型：\n\n```jsx\nimport { ExtensionCategory, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\n\nregister(ExtensionCategory.NODE, 'react-node', ReactNode);\n```\n\n`register` 方法需要三个参数：\n\n- 扩展类别：`ExtensionCategory.NODE` 表示这是一个节点类型\n- 类型名称：`react-node` 是我们给这个自定义节点起的名字，后续会在配置中使用\n- 类定义：ReactNode 是 `@antv/g6-extension-react` 导出的实现类\n\n#### 2. 定义业务组件\n\n定义一个简单的 React 组件作为节点的内容：\n\n```jsx\nconst MyReactNode = () => {\n  return <div>node</div>;\n};\n```\n\n#### 3. 使用组件\n\n在图配置中使用自定义的 React 节点。通过在图配置中指定节点类型和样式，来使用自定义的 React 组件。\n\n- `type`：指定节点类型为 `react-node` (使用与注册时起的名字)\n- `style.component`：定义节点的 React 组件内容\n\n```jsx\nconst graph = new Graph({\n  node: {\n    type: 'react-node',\n    style: {\n      component: () => <MyReactNode />,\n    },\n  },\n});\n\ngraph.render();\n```\n\n## 高级功能\n\n### 状态管理\n\n在复杂图可视化场景中，节点需要动态响应交互状态。我们提供两种互补的状态管理方案：\n\n#### 响应内置交互状态\n\nG6 提供内置的交互状态管理状态，如 `hover-activate` 和 `click-select`。可以通过节点数据中的 `data.states` 字段获取当前节点状态，并根据状态调整节点样式。\n\n**示例**：在节点被 hover 时改变背景颜色。\n\n```jsx\nimport { ExtensionCategory, register, Graph } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\n\nregister(ExtensionCategory.NODE, 'react-node', ReactNode);\n\nconst StatefulNode = ({ data }) => {\n  const isActive = data.states?.includes('active');\n\n  return (\n    <div\n      style={{\n        width: 100,\n        padding: 5,\n        border: '1px solid #eee',\n        boxShadow: isActive ? '0 0 8px rgba(24,144,255,0.8)' : 'none',\n        transform: `scale(${isActive ? 1.05 : 1})`,\n      }}\n    >\n      {data.data.label}\n    </div>\n  );\n};\n\nconst graph = new Graph({\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 200 }, data: { label: 'node1' } },\n      { id: 'node2', style: { x: 300, y: 200 }, data: { label: 'node2' } },\n    ],\n  },\n  node: {\n    type: 'react-node',\n    style: {\n      component: (data) => <StatefulNode data={data} />,\n    },\n  },\n  behaviors: ['hover-activate'],\n});\n\ngraph.render();\n```\n\n#### 自定义业务状态\n\n当需要管理业务相关状态（如审批状态、风险等级）时，可通过扩展节点数据实现：\n\n**示例**：通过 data 添加 `selected` 变量，实现节点选中和取消选中的样式变化。\n\n```jsx\nimport { ExtensionCategory, register, Graph } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\n\nregister(ExtensionCategory.NODE, 'react-node', ReactNode);\n\nconst MyReactNode = ({ data, graph }) => {\n  const handleClick = () => {\n    graph.updateNodeData([{ id: data.id, data: { selected: !data.data.selected } }]);\n    graph.draw();\n  };\n\n  return (\n    <div\n      style={{\n        width: 200,\n        padding: 10,\n        border: '1px solid red',\n        borderColor: data.data.selected ? 'orange' : '#ddd', // 根据选中状态设置边框颜色\n        cursor: 'pointer', // 添加鼠标指针样式\n      }}\n      onClick={handleClick}\n    >\n      Node\n    </div>\n  );\n};\n\nconst graph = new Graph({\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        style: { x: 100, y: 100 },\n        data: { selected: true },\n      },\n    ],\n  },\n  node: {\n    type: 'react-node',\n    style: {\n      component: (data) => <MyReactNode data={data} graph={graph} />,\n    },\n  },\n});\n\ngraph.render();\n```\n\n### 事件交互\n\n实现节点与图实例的双向通信，使节点和图实例可以相互更新。\n\n**示例**：通过自定义节点操作图数据，并重新渲染图形。\n\n```jsx\nconst IDCardNode = ({ id, selected, graph }) => {\n  const handleSelect = () => {\n    graph.updateNodeData([{ id, data: { selected: true } }]);\n    graph.draw();\n  };\n\n  return <Select onChange={handleSelect} style={{ background: selected ? 'orange' : '#eee' }} />;\n};\n\nconst graph = new Graph({\n  node: {\n    type: 'react-node',\n    style: {\n      component: ({ id, data }) => <IDCardNode id={id} selected={data.selected} graph={graph} />,\n    },\n  },\n});\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { DatabaseFilled } from '@ant-design/icons';\nimport { ExtensionCategory, Graph, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\nimport { Badge, Flex, Input, Tag, Typography } from 'antd';\nimport { useEffect, useRef } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nconst { Text } = Typography;\n\nregister(ExtensionCategory.NODE, 'react', ReactNode);\n\nconst Node = ({ data, onChange }) => {\n  const { status, type } = data.data;\n\n  return (\n    <Flex\n      style={{\n        width: '100%',\n        height: '100%',\n        background: '#fff',\n        padding: 10,\n        borderRadius: 5,\n        border: '1px solid gray',\n      }}\n      vertical\n    >\n      <Flex align=\"center\" justify=\"space-between\">\n        <Text>\n          <DatabaseFilled />\n          Server\n          <Tag>{type}</Tag>\n        </Text>\n        <Badge status={status} />\n      </Flex>\n      <Text type=\"secondary\">{data.id}</Text>\n      <Flex align=\"center\">\n        <Text style={{ flexShrink: 0 }}>\n          <Text type=\"danger\">*</Text>URL:\n        </Text>\n        <Input\n          style={{ borderRadius: 0, borderBottom: '1px solid #d9d9d9' }}\n          variant=\"borderless\"\n          value={data.data?.url}\n          onChange={(event) => {\n            const url = event.target.value;\n            onChange?.(url);\n          }}\n        />\n      </Flex>\n    </Flex>\n  );\n};\n\nexport const ReactNodeDemo = () => {\n  const containerRef = useRef();\n\n  useEffect(() => {\n    const graph = new Graph({\n      container: containerRef.current,\n      data: {\n        nodes: [\n          {\n            id: 'local-server-1',\n            data: { status: 'success', type: 'local', url: 'http://localhost:3000' },\n            style: { x: 50, y: 50 },\n          },\n          {\n            id: 'remote-server-1',\n            data: { status: 'warning', type: 'remote' },\n            style: { x: 350, y: 50 },\n          },\n        ],\n        edges: [{ source: 'local-server-1', target: 'remote-server-1' }],\n      },\n      node: {\n        type: 'react',\n        style: {\n          size: [240, 100],\n          component: (data) => <Node data={data} />,\n        },\n      },\n      behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  }, []);\n\n  return <div style={{ width: '100%', height: '100%' }} ref={containerRef}></div>;\n};\n\nconst root = createRoot(document.getElementById('container'));\nroot.render(<ReactNodeDemo />);\n```\n\n<br/>\n\n```js | ob { inject: true }\nimport { UserOutlined } from '@ant-design/icons';\nimport { ExtensionCategory, Graph, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\nimport { Avatar, Button, Card, Descriptions, Select, Space, Typography } from 'antd';\nimport React, { useEffect, useRef } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nconst { Title, Text } = Typography;\nconst { Option } = Select;\n\nregister(ExtensionCategory.NODE, 'react-node', ReactNode);\n\nconst IDCardNode = ({ id, data }) => {\n  const { name, idNumber, address, expanded, selected, graph } = data;\n\n  const toggleExpand = (e) => {\n    e.stopPropagation();\n    graph.updateNodeData([\n      {\n        id,\n        data: { expanded: !expanded },\n      },\n    ]);\n    graph.render();\n  };\n\n  const handleSelect = (value) => {\n    graph.updateNodeData([\n      {\n        id,\n        data: { selected: value !== 0 },\n      },\n    ]);\n    if (value === 2) {\n      // 获取与当前节点相连的所有节点\n      const connectedNodes = graph.getNeighborNodesData(id);\n\n      connectedNodes.forEach((node) => {\n        graph.updateNodeData([\n          {\n            id: node.id,\n            data: { selected: true },\n          },\n        ]);\n      });\n    }\n    graph.render();\n  };\n\n  const CardTitle = (\n    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>\n      <Space>\n        <Avatar shape=\"square\" size=\"small\" icon={<UserOutlined />} />\n        <Title level={5} style={{ margin: 0 }}>\n          {name}\n        </Title>\n\n        <Select\n          value={selected ? data.selectedOption || 1 : 0}\n          style={{ width: 150, marginRight: 8 }}\n          onChange={handleSelect}\n        >\n          <Option value={0}>None</Option>\n          <Option value={1}>Node</Option>\n          <Option value={2}>Connected</Option>\n        </Select>\n      </Space>\n      <Button type=\"link\" onClick={toggleExpand} style={{ padding: 0 }}>\n        {expanded ? 'fold' : 'expand'}\n      </Button>\n    </div>\n  );\n\n  return (\n    <Card\n      size=\"small\"\n      title={CardTitle}\n      style={{\n        width: 340,\n        padding: 10,\n        borderRadius: 8,\n        borderWidth: 2,\n        borderColor: selected ? 'orange' : '#eee', // 根据选中状态设置边框颜色\n        cursor: 'pointer',\n      }}\n    >\n      {expanded ? (\n        <Descriptions bordered column={1} style={{ width: '100%', textAlign: 'center' }}>\n          <Descriptions.Item label=\"ID Number\">{idNumber}</Descriptions.Item>\n          <Descriptions.Item label=\"Address\">{address}</Descriptions.Item>\n        </Descriptions>\n      ) : (\n        <Text style={{ textAlign: 'center' }}>IDCard Information</Text>\n      )}\n    </Card>\n  );\n};\n\n// 定义 Graph 数据\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n      data: {\n        name: 'Alice',\n        idNumber: 'IDUSAASD2131734',\n        address: '1234 Broadway, Apt 5B, New York, NY 10001',\n        expanded: false, // 初始状态为收缩\n        selected: false, // 初始状态为未选中\n        selectedOption: 1, // 初始选择本节点\n      },\n      style: { x: 50, y: 50 },\n    },\n    {\n      id: 'node2',\n      data: {\n        name: 'Bob',\n        idNumber: 'IDUSAASD1431920',\n        address: '3030 Chestnut St, Philadelphia, PA 19104',\n        expanded: false, // 初始状态为收缩\n        selected: false, // 初始状态为未选中\n        selectedOption: 0, // 初始不选择\n      },\n      style: { x: 700, y: 100 },\n    },\n    {\n      id: 'node3',\n      data: {\n        name: 'Charlie',\n        idNumber: 'IDUSAASD1431921',\n        address: '4040 Elm St, Chicago, IL 60611',\n        expanded: false,\n        selected: true,\n        selectedOption: 0,\n      },\n    },\n    {\n      id: 'node4',\n      data: {\n        name: 'David',\n        idNumber: 'IDUSAASD1431922',\n        address: '5050 Oak St, Houston, TX 77002',\n        expanded: false,\n        selected: false,\n        selectedOption: 0,\n      },\n    },\n    {\n      id: 'node5',\n      data: {\n        name: 'Eve',\n        idNumber: 'IDUSAASD1431923',\n        address: '6060 Pine St, Phoenix, AZ 85001',\n        expanded: false,\n        selected: false,\n        selectedOption: 0,\n      },\n    },\n  ],\n  edges: [\n    { source: 'node1', target: 'node2' },\n    { source: 'node2', target: 'node3' },\n    { source: 'node3', target: 'node4' },\n    { source: 'node4', target: 'node5' },\n  ],\n};\n\nexport const ReactNodeDemo = () => {\n  const containerRef = useRef();\n  const graphRef = useRef(null);\n\n  useEffect(() => {\n    // 创建 Graph 实例\n    const graph = new Graph({\n      autoFit: 'view',\n      container: containerRef.current,\n      data,\n      node: {\n        type: 'react-node',\n        style: {\n          size: (datum) => (datum.data.expanded ? [340, 236] : [340, 105]), // 调整大小以适应内容\n          component: (data) => <IDCardNode id={data.id} data={{ ...data.data, graph: graph }} />,\n        },\n      },\n      behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],\n      layout: {\n        type: 'snake',\n        cols: 2,\n        rowGap: 100,\n        colGap: 220,\n      },\n    });\n\n    // 渲染 Graph\n    graph.render();\n\n    // 保存 graph 实例\n    graphRef.current = graph;\n\n    return () => {\n      graph.destroy();\n    };\n  }, []);\n\n  return <div style={{ width: '100%', height: '100%' }} ref={containerRef}></div>;\n};\n\n// 渲染 React 组件到 DOM\nconst root = createRoot(document.getElementById('container'));\nroot.render(<ReactNodeDemo />);\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/vue-node.en.md",
    "content": "---\ntitle: Define Nodes with Vue\norder: 14\n---\n\nIn G6, custom nodes typically require manipulating DOM or Canvas elements, but with the help of the [`g6-extension-vue`](https://github.com/Child-qjj/g6-extension-vue) ecosystem library, you can directly use Vue components as node content, enhancing development efficiency and maintainability.\n\n## Choosing a Custom Node Solution\n\n### G6 Node\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*sEaLR7Q_hmoAAAAAAAAAAAAAemJ7AQ/fmt.avif\" width=\"300\" />\n\n✅ **Recommended Scenarios:**\n\n- Nodes are simple geometric shapes\n- Scenarios requiring efficient rendering of more than 2,000 nodes\n- Need to directly manipulate graphic instances for fine control\n\n> For detailed information on how to customize nodes using Canvas graphics, please refer to the [Custom Node](/en/manual/element/node/custom-node) documentation\n\n### Vue Node\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*9oz-R7bIkd0AAAAAAAAAAAAADmJ7AQ/original\" width=\"350\" />\n\n✅ **Recommended Scenarios:**\n\n- Business systems that need to integrate UI libraries like Element Plus / Ant Design Vue\n- Nodes contain interactive logic such as form input, state switching\n- Scenarios where an existing Vue design system needs to be reused\n\n## Quick Start\n\n### Environment Preparation\n\nBefore starting, please ensure you have:\n\n- **Installed a Vue project**: Ensure a Vue project is installed and created.\n- **Vue version requirement**: Ensure the Vue version used is >=2.6.0. (Vue 3 is recommended)\n\n### Install Dependencies\n\nTo use [`g6-extension-vue`](https://github.com/Child-qjj/g6-extension-vue), run the following command:\n\n:::code-group\n\n```bash [npm]\nnpm install g6-extension-vue\n```\n\n```bash [yarn]\nyarn add g6-extension-vue\n```\n\n```bash [pnpm]\npnpm add g6-extension-vue\n```\n\n:::\n\n### Component Integration\n\n#### 1. Register Vue Node Type\n\nRegister the Vue node type through the extension mechanism:\n\n```jsx\nimport { ExtensionCategory, register } from '@antv/g6';\nimport { VueNode } from 'g6-extension-vue';\n\nregister(ExtensionCategory.NODE, 'vue-node', VueNode);\n```\n\nThe `register` method requires three parameters:\n\n- Extension category: `ExtensionCategory.NODE` indicates this is a node type\n- Type name: `vue-node` is the name we give to this custom node, which will be used in the configuration later\n- Class definition: VueNode is the implementation class exported by `g6-extension-vue`\n\n#### 2. Define Business Component\n\nDefine a simple Vue component as the content of the node:\n\n```jsx\nimport { defineComponent, h } from 'vue';\n\nconst MyVueNode = defineComponent({\n  setup(props, { attrs, slots, expose }) {\n    return () => {\n      return h('div', 'vue node');\n    };\n  },\n});\n```\n\n#### 3. Use the Component\n\nUse the custom Vue node in the graph configuration. Specify the node type and style in the graph configuration to use the custom Vue component.\n\n- `type`: Specify the node type as `vue-node` (use the name given during registration)\n- `style.component`: Define the Vue component content of the node\n\n```jsx\nconst graph = new Graph({\n  node: {\n    type: 'vue-node',\n    style: {\n      component: () => <MyVueNode />,\n    },\n  },\n});\n\ngraph.render();\n```\n\n## Advanced Features\n\n### State Management\n\nIn complex graph visualization scenarios, nodes need to dynamically respond to interaction states. We provide two complementary state management solutions:\n\n#### Respond to Built-in Interaction States\n\nG6 provides built-in interaction state management states, such as `hover-activate` and `click-select`. You can get the current node state through the `data.states` field in the node data and adjust the node style based on the state.\n\n**Example**: Change the background color when the node is hovered.\n\n```jsx\nimport { ExtensionCategory, register, Graph } from '@antv/g6';\nimport { VueNode } from 'g6-extension-vue';\nimport { computed, defineComponent } from 'vue';\n\nregister(ExtensionCategory.NODE, 'vue-node', VueNode);\n\nconst StatefulNode = defineComponent({\n  setup(props, { attrs, slots, expose }) {\n    const isActive = computed(() => props.data.states?.includes('active'));\n    const label = computed(() => props.data.data?.label);\n\n    return (\n      <div\n        style={{\n          width: 100,\n          padding: 5,\n          border: '1px solid #eee',\n          boxShadow: isActive.value ? '0 0 8px rgba(24,144,255,0.8)' : 'none',\n          transform: `scale(${isActive.value ? 1.05 : 1})`,\n        }}\n      >\n        {label.value}\n      </div>\n    );\n  },\n});\n\nconst graph = new Graph({\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 200 }, data: { label: 'node1' } },\n      { id: 'node2', style: { x: 300, y: 200 }, data: { label: 'node2' } },\n    ],\n  },\n  node: {\n    type: 'vue-node',\n    style: {\n      component: (data) => <StatefulNode data={Object.assign({}, data)} />, // data is non-reactive, need to change reference to trigger Vue's props side effects\n    },\n  },\n  behaviors: ['hover-activate'],\n});\n\ngraph.render();\n```\n\n#### Custom Business State\n\nWhen you need to manage business-related states (such as approval status, risk level), you can extend node data to achieve this:\n\n**Example**: Add a `selected` variable through data to achieve style changes for node selection and deselection.\n\n```jsx\nimport { ExtensionCategory, register, Graph } from '@antv/g6';\nimport { VueNode } from 'g6-extension-vue';\nimport { defineComponent, computed } from 'vue';\n\nregister(ExtensionCategory.NODE, 'vue-node', VueNode);\n\nconst MyVueNode = defineComponent({\n  setup(props, { attrs, slots, expose }) {\n    const isSelected = computed(() => props.data.data.selected);\n\n    const handleClick = () => {\n      graph.updateNodeData([{ id: props.data.id, data: { selected: !isSelected.value } }]);\n      graph.draw();\n    };\n\n    return (\n      <div\n        style={{\n          width: 200,\n          padding: 10,\n          border: '1px solid red',\n          borderColor: isSelected.value ? 'orange' : '#ddd', // Set border color based on selection state\n          cursor: 'pointer', // Add mouse pointer style\n        }}\n        onClick={handleClick}\n      >\n        Node\n      </div>\n    );\n  },\n});\n\nconst graph = new Graph({\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        style: { x: 100, y: 100 },\n        data: { selected: true },\n      },\n    ],\n  },\n  node: {\n    type: 'vue-node',\n    style: {\n      component: (data) => <MyVueNode data={Object.assign({}, data)} graph={graph} />, // data is non-reactive, need to change reference to trigger Vue's props side effects\n    },\n  },\n});\n\ngraph.render();\n```\n\n### Event Interaction\n\nAchieve two-way communication between nodes and graph instances, allowing nodes and graph instances to update each other.\n\n**Example**: Operate graph data through custom nodes and re-render the graph.\n\n```jsx\nimport { ExtensionCategory, register, Graph } from '@antv/g6';\nimport { VueNode } from 'g6-extension-vue';\nimport { defineComponent, computed } from 'vue';\n\nregister(ExtensionCategory.NODE, 'vue-node', VueNode);\n\nconst IDCardNode = defineComponent({\n  setup(props, { attrs, slots, expose }) {\n    const isSelected = computed(() => props.data.data.selected);\n\n    const handleSelect = () => {\n      graph.updateNodeData([{ id: props.data.id, data: { selected: true } }]);\n      graph.draw();\n    };\n\n    return <Select onChange={handleSelect} style={{ background: isSelected.value ? 'orange' : '#eee' }} />;\n  },\n});\n\nconst graph = new Graph({\n  node: {\n    type: 'vue-node',\n    style: {\n      component: ({ id, data }) => <IDCardNode id={id} selected={isSelected.value} graph={graph} />,\n    },\n  },\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/node/vue-node.zh.md",
    "content": "---\ntitle: 使用 Vue 定义节点\norder: 14\n---\n\n在 G6 中，自定义节点通常需要操作 DOM 或 Canvas 元素，但借助 [`g6-extension-vue`](https://github.com/Child-qjj/g6-extension-vue) 社区生态库，可以直接使用 Vue 组件作为节点内容，提升开发效率与可维护性。\n\n### G6 节点\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*sEaLR7Q_hmoAAAAAAAAAAAAAemJ7AQ/fmt.avif\" width=\"300\" />\n\n✅ **推荐场景：**\n\n- 节点只是简单的几何图形\n- 需要高效渲染超过 2,000 个节点的场景\n- 需要直接操作图形实例进行精细控制\n\n> 有关如何使用 Canvas 图形自定义节点的详细信息，请参阅 [自定义节点](/manual/element/node/custom-node) 文档\n\n### Vue Node\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*9oz-R7bIkd0AAAAAAAAAAAAADmJ7AQ/original\" width=\"350\" />\n\n✅ **推荐场景：**\n\n- 需要集成 Element Plus / Ant Design Vue 等 UI 库的业务系统\n- 节点包含表单输入、状态切换等交互逻辑\n- 已有 Vue 设计系统需要复用的场景\n\n## 快速入门\n\n### 环境准备\n\n在开始之前，请确保您已经：\n\n- **安装 Vue 项目**：确保已安装并创建 Vue 项目。\n- **Vue 版本要求**：确保使用的 Vue 版本 >=2.6.0。(推荐使用 Vue 3)\n\n### 安装依赖\n\n要使用 [`g6-extension-vue`](https://github.com/Child-qjj/g6-extension-vue)，请运行以下命令：\n\n:::code-group\n\n```bash [npm]\nnpm install g6-extension-vue\n```\n\n```bash [yarn]\nyarn add g6-extension-vue\n```\n\n```bash [pnpm]\npnpm add g6-extension-vue\n```\n\n:::\n\n### 组件集成\n\n#### 1. 注册 Vue 节点类型\n\n通过扩展机制注册 Vue 节点类型：\n\n```jsx\nimport { ExtensionCategory, register } from '@antv/g6';\nimport { VueNode } from 'g6-extension-vue';\n\nregister(ExtensionCategory.NODE, 'vue-node', VueNode);\n```\n\n`register` 方法需要三个参数：\n\n- 扩展类别：`ExtensionCategory.NODE` 表示这是一个节点类型\n- 类型名称：`vue-node` 是我们给这个自定义节点起的名字，后续会在配置中使用\n- 类定义：VueNode 是 `g6-extension-vue` 导出的实现类\n\n#### 2. 定义业务组件\n\n定义一个简单的 Vue 组件作为节点的内容：\n\n```jsx\nimport { defineComponent, h } from 'vue';\n\nconst MyVueNode = defineComponent({\n  setup(props, { attrs, slots, expose }) {\n    return () => {\n      return h('div', 'vue node');\n    };\n  },\n});\n```\n\n#### 3. 使用组件\n\n在图配置中使用自定义的 Vue 节点。通过在图配置中指定节点类型和样式，来使用自定义的 Vue 组件。\n\n- `type`：指定节点类型为 `vue-node` (使用与注册时起的名字)\n- `style.component`：定义节点的 Vue 组件内容\n\n```jsx\nconst graph = new Graph({\n  node: {\n    type: 'vue-node',\n    style: {\n      component: () => <MyVueNode />,\n    },\n  },\n});\n\ngraph.render();\n```\n\n## 高级功能\n\n### 状态管理\n\n在复杂图可视化场景中，节点需要动态响应交互状态。我们提供两种互补的状态管理方案：\n\n#### 响应内置交互状态\n\nG6 提供内置的交互状态管理状态，如 `hover-activate` 和 `click-select`。可以通过节点数据中的 `data.states` 字段获取当前节点状态，并根据状态调整节点样式。\n\n**示例**：在节点被 hover 时改变背景颜色。\n\n```jsx\nimport { ExtensionCategory, register, Graph } from '@antv/g6';\nimport { VueNode } from 'g6-extension-vue';\nimport { computed, defineComponent } from 'vue';\n\nregister(ExtensionCategory.NODE, 'vue-node', VueNode);\n\nconst StatefulNode = defineComponent({\n  setup(props, { attrs, slots, expose }) {\n    const isActive = computed(() => props.data.states?.includes('active'));\n    const label = computed(() => props.data.data?.label);\n\n    return (\n      <div\n        style={{\n          width: 100,\n          padding: 5,\n          border: '1px solid #eee',\n          boxShadow: isActive.value ? '0 0 8px rgba(24,144,255,0.8)' : 'none',\n          transform: `scale(${isActive.value ? 1.05 : 1})`,\n        }}\n      >\n        {label.value}\n      </div>\n    );\n  },\n});\n\nconst graph = new Graph({\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 200 }, data: { label: 'node1' } },\n      { id: 'node2', style: { x: 300, y: 200 }, data: { label: 'node2' } },\n    ],\n  },\n  node: {\n    type: 'vue-node',\n    style: {\n      component: (data) => <StatefulNode data={Object.assign({}, data)} />, // data是非响应式数据，需要通过改变引用来触发Vue的props副作用\n    },\n  },\n  behaviors: ['hover-activate'],\n});\n\ngraph.render();\n```\n\n#### 自定义业务状态\n\n当需要管理业务相关状态（如审批状态、风险等级）时，可通过扩展节点数据实现：\n\n**示例**：通过 data 添加 `selected` 变量，实现节点选中和取消选中的样式变化。\n\n```jsx\nimport { ExtensionCategory, register, Graph } from '@antv/g6';\nimport { VueNode } from 'g6-extension-vue';\nimport { defineComponent, computed } from 'vue';\n\nregister(ExtensionCategory.NODE, 'vue-node', VueNode);\n\nconst MyVueNode = defineComponent({\n  setup(props, { attrs, slots, expose }) {\n    const isSelected = computed(() => props.data.data.selected);\n\n    const handleClick = () => {\n      graph.updateNodeData([{ id: props.data.id, data: { selected: !isSelected.value } }]);\n      graph.draw();\n    };\n\n    return (\n      <div\n        style={{\n          width: 200,\n          padding: 10,\n          border: '1px solid red',\n          borderColor: isSelected.value ? 'orange' : '#ddd', // 根据选中状态设置边框颜色\n          cursor: 'pointer', // 添加鼠标指针样式\n        }}\n        onClick={handleClick}\n      >\n        Node\n      </div>\n    );\n  },\n});\n\nconst graph = new Graph({\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        style: { x: 100, y: 100 },\n        data: { selected: true },\n      },\n    ],\n  },\n  node: {\n    type: 'vue-node',\n    style: {\n      component: (data) => <MyVueNode data={Object.assign({}, data)} graph={graph} />, // data是非响应式数据，需要通过改变引用来触发Vue的props副作用\n    },\n  },\n});\n\ngraph.render();\n```\n\n### 事件交互\n\n实现节点与图实例的双向通信，使节点和图实例可以相互更新。\n\n**示例**：通过自定义节点操作图数据，并重新渲染图形。\n\n```jsx\nimport { ExtensionCategory, register, Graph } from '@antv/g6';\nimport { VueNode } from 'g6-extension-vue';\nimport { defineComponent, computed } from 'vue';\n\nregister(ExtensionCategory.NODE, 'vue-node', VueNode);\n\nconst IDCardNode = defineComponent({\n  setup(props, { attrs, slots, expose }) {\n    const isSelected = computed(() => props.data.data.selected);\n\n    const handleSelect = () => {\n      graph.updateNodeData([{ id: props.data.id, data: { selected: true } }]);\n      graph.draw();\n    };\n\n    return <Select onChange={handleSelect} style={{ background: isSelected.value ? 'orange' : '#eee' }} />;\n  },\n});\n\nconst graph = new Graph({\n  node: {\n    type: 'vue-node',\n    style: {\n      component: ({ id, data }) => <IDCardNode id={id} selected={isSelected.value} graph={graph} />,\n    },\n  },\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/overview.en.md",
    "content": "---\ntitle: Element Overview\norder: 1\n---\n\n## Element System\n\nThe core of G6 charts is composed of three basic elements: **Node**, **Edge**, and **Combo**. These elements are the fundamental units for building complex graphical networks.\n\n<image width=\"400\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*2ZewT4T1p_4AAAAAAAAAAAAADmJ7AQ/original\" />\n\n### Node\n\n[Nodes](/en/manual/element/node/overview) represent entities or concepts in the graph, such as people, places, objects, etc. G6 provides a rich set of built-in node types:\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*TZt2S7Z0d-8AAAAAAAAAAAAADmJ7AQ/original\" />\n\nG6 also supports [defining nodes using React](/en/manual/element/node/react-node) or [custom nodes](/en/manual/element/node/custom-node) to meet specific needs.\n\n### Edge\n\n[Edges](/en/manual/element/edge/overview) represent the connections between nodes, such as friendships, transactions, etc. G6 has multiple built-in edge types:\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YKN7TasqOh4AAAAAAAAAAAAADmJ7AQ/original\" />\n\nWhen built-in edges do not meet the requirements, complex connection expressions can be achieved through [custom edges](/en/manual/element/edge/custom-edge).\n\n### Combo\n\n[Combos](/en/manual/element/combo/overview) are special elements that can contain nodes and other combos, used to represent collections, groups, or hierarchical relationships. G6 has two built-in combo types:\n\n- **Circle Combo**(`circle`): Suitable for compact grouping\n- **Rectangle Combo**(`rect`): Suitable for regular layout grouping\n\n<image width=\"450\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*zPAzSZ3XxpUAAAAAAAAAAAAADmJ7AQ/original\" />\n\nCombos support nesting, dragging, expanding/collapsing, and other interactions, and more complex container behaviors can be achieved through [custom combos](/en/manual/element/combo/custom-combo).\n\n## Element Composition Principle\n\nEach element is composed of one or more basic shapes. Shapes are the smallest graphical units in G6, including [rectangle](/en/manual/element/shape/properties#rectstyleprops), [circle](/en/manual/element/shape/properties#circlestyleprops), [text](/en/manual/element/shape/properties#textstyleprops), [path](/en/manual/element/shape/properties#pathstyleprops), etc.\n\nFor example:\n\n- A node may consist of a background shape (such as a circle) and a text label\n- An edge may consist of a path, arrow, and text label\n- A combo may consist of a container shape, title text, and expand/collapse button\n\nFor more information about shapes, see [Shape Overview](/en/manual/element/shape/overview) and [Shape Style Properties](/en/manual/element/shape/properties).\n\n## Element State\n\n[Element State](/en/manual/element/state) is a powerful mechanism for displaying visual changes of elements in different interactions or business scenarios. G6 provides a complete state management system:\n\n- **Preset States**: `selected`, `highlight`, `active`, etc.\n- **State Overlay**: Elements can have multiple states simultaneously, with styles overlaying according to priority\n- **Custom States**: Any state can be defined according to business needs\n\n<image width=\"500\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*yVbORYybrDQAAAAAAAAAAAAADmJ7AQ/original\" />\n\n## Configure Elements\n\nG6 5.x adopts a flat configuration structure, where all element configurations are at the same level, making it easy to find and manage:\n\n```typescript\n{\n  node: {\n    // Default node style\n    style: {\n      fill: 'orange',\n      labelText: 'node',\n    },\n    // Node styles in different states\n    state: {\n      selected: {\n        stroke: '#1890FF',\n        lineWidth: 2,\n      }\n    }\n  },\n  edge: {\n    // Default edge style\n    style: {\n      stroke: '#aaa',\n    },\n    // Edge styles in different states\n    state: {\n      highlight: {\n        stroke: 'red',\n      }\n    }\n  },\n  combo: {\n    // Default combo style\n    style: {\n      fill: 'lightblue',\n      stroke: 'blue',\n    }\n  }\n};\n```\n\nThere are three configuration methods, in order of priority from high to low:\n\n1. **Dynamic configuration using instance methods**: such as `graph.setNode()`, `graph.setEdge()`, `graph.setCombo()`\n2. **Global configuration when instantiating the graph**: specify configuration items in `new Graph()`\n3. **Configuration in data**: set in the data objects of nodes, edges, and combos\n\nIn editors like VSCode, you can see all configurable properties of elements and search based on keywords:\n\n<image width=\"800\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*oY_uTK80sIoAAAAAAAAAAAAADmJ7AQ/original\" />\n\n## Extension Capabilities\n\nG6 provides powerful extension capabilities to meet various customization needs:\n\n- **Custom Nodes**: [Custom Node Guide](/en/manual/element/node/custom-node)\n- **Custom Edges**: [Custom Edge Guide](/en/manual/element/edge/custom-edge)\n- **Custom Combos**: [Custom Combo Guide](/en/manual/element/combo/custom-combo)\n- **React Nodes**: [Define Nodes Using React](/en/manual/element/node/react-node)\n- **3D Extension**: Use 3D nodes through `@antv/g6-extension-3d`\n\n## Built-in Element Reference\n\n### Node Types\n\n- [Built-in Node Library](/en/manual/element/node/base-node)\n\n### Edge Types\n\n- [Built-in Edge Library](/en/manual/element/edge/base-edge)\n\n### Combo Types\n\n- [Built-in Combo Library](/en/manual/element/combo/base-combo)\n"
  },
  {
    "path": "packages/site/docs/manual/element/overview.zh.md",
    "content": "---\ntitle: 元素总览\norder: 1\n---\n\n## 元素体系\n\nG6 图表的核心是由三种基本元素构成：**节点(Node)**、**边(Edge)** 和 **组合(Combo)**。这些元素是构建复杂图形网络的基础单元。\n\n<image width=\"400\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*2ZewT4T1p_4AAAAAAAAAAAAADmJ7AQ/original\" />\n\n### 节点 (Node)\n\n[节点](/manual/element/node/overview) 表示图中的实体或概念，如人物、地点、对象等。G6 提供了丰富的内置节点类型：\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*TZt2S7Z0d-8AAAAAAAAAAAAADmJ7AQ/original\" />\n\nG6 还支持 [使用 React 定义节点](/manual/element/node/react-node) 或 [自定义节点](/manual/element/node/custom-node) 以满足特定需求。\n\n### 边 (Edge)\n\n[边](/manual/element/edge/overview)表示节点间的连接关系，如朋友关系、交易往来等。G6 内置多种边类型：\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YKN7TasqOh4AAAAAAAAAAAAADmJ7AQ/original\" />\n\n当内置边不满足需求时，可以通过 [自定义边](/manual/element/edge/custom-edge) 来实现复杂的连接表现。\n\n### 组合 (Combo)\n\n[组合](/manual/element/combo/overview)是一种特殊元素，可以包含节点和其他组合，用于表示集合、分组或层级关系。G6 内置两种组合类型：\n\n- **圆形组合**(`circle`)：适合紧凑型分组\n- **矩形组合**(`rect`)：适合规则布局的分组\n\n<image width=\"450\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*zPAzSZ3XxpUAAAAAAAAAAAAADmJ7AQ/original\" />\n\n组合支持嵌套、拖拽、展开/收起等交互，可以通过 [自定义组合](/manual/element/combo/custom-combo) 来实现更复杂的容器行为。\n\n## 元素构成原理\n\n每个元素由一个或多个基础图形(Shape)组成。图形是 G6 中的最小图形单元，包括 [矩形](/manual/element/shape/properties#rectstyleprops)、[圆形](/manual/element/shape/properties#circlestyleprops)、[文本](/manual/element/shape/properties#textstyleprops)、[路径](/manual/element/shape/properties#pathstyleprops)等。\n\n例如：\n\n- 一个节点可能由背景图形(如圆形)和文本标签组成\n- 一条边可能由路径、箭头和文本标签组成\n- 一个组合可能由容器图形、标题文本和展开/收起按钮组成\n\n要了解更多关于图形的信息，请参阅 [图形 Shape 总览](/manual/element/shape/overview) 和 [Shape 样式属性](/manual/element/shape/properties)。\n\n## 元素状态\n\n[元素状态](/manual/element/state) 是一种强大的机制，用于展示元素在不同交互或业务场景下的视觉变化。G6 提供了一套完整的状态管理系统：\n\n- **预设状态**：`selected`(选中)、`highlight`(高亮)、`active`(激活)等\n- **状态叠加**：元素可同时拥有多个状态，样式按优先级叠加\n- **自定义状态**：可根据业务需求定义任意状态\n\n<image width=\"500\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*yVbORYybrDQAAAAAAAAAAAAADmJ7AQ/original\" />\n\n## 配置元素\n\nG6 5.x 采用扁平化的配置结构，所有元素的配置都在同一层级，便于查找和管理：\n\n```typescript\n{\n  node: {\n    // 节点默认样式\n    style: {\n      fill: 'orange',\n      labelText: 'node',\n    },\n    // 节点在不同状态下的样式\n    state: {\n      selected: {\n        stroke: '#1890FF',\n        lineWidth: 2,\n      }\n    }\n  },\n  edge: {\n    // 边默认样式\n    style: {\n      stroke: '#aaa',\n    },\n    // 边在不同状态下的样式\n    state: {\n      highlight: {\n        stroke: 'red',\n      }\n    }\n  },\n  combo: {\n    // 组合默认样式\n    style: {\n      fill: 'lightblue',\n      stroke: 'blue',\n    }\n  }\n};\n```\n\n配置方式有三种，按优先级从高到低：\n\n1. **使用实例方法动态配置**：如 `graph.setNode()`、`graph.setEdge()`、`graph.setCombo()`\n2. **实例化图时全局配置**：在 `new Graph()` 时指定配置项\n3. **在数据中配置**：在节点、边、组合的数据对象中设置\n\n在 VSCode 等编辑器中，你可以看到元素的全部可配置属性，并基于关键字进行搜索：\n\n<image width=\"800\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*oY_uTK80sIoAAAAAAAAAAAAADmJ7AQ/original\" />\n\n## 扩展能力\n\nG6 提供了强大的扩展能力，满足各种自定义需求：\n\n- **自定义节点**：[自定义节点指南](/manual/element/node/custom-node)\n- **自定义边**：[自定义边指南](/manual/element/edge/custom-edge)\n- **自定义组合**：[自定义组合指南](/manual/element/combo/custom-combo)\n- **React 节点**：[使用 React 定义节点](/manual/element/node/react-node)\n- **3D 扩展**：通过 `@antv/g6-extension-3d` 使用 3D 节点\n\n## 内置元素参考\n\n### 节点类型\n\n- [内置节点库](/manual/element/node/base-node)\n\n### 边类型\n\n- [内置边库](/manual/element/edge/base-edge)\n\n### 组合类型\n\n- [内置组合库](/manual/element/combo/base-combo)\n"
  },
  {
    "path": "packages/site/docs/manual/element/shape/label-shape.en.md",
    "content": "---\ntitle: Design and Implementation of Composite Shape\norder: 3\n---\n\nG6 provides a flexible Shape mechanism, allowing developers to customize various graphics and efficiently reuse them in elements such as nodes, edges, and combos. This article uses Label as an example to explain how to customize a Shape and how to apply it in elements.\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*W3oqSYPZtWEAAAAAAAAAAAAAemJ7AQ/original\" width=\"80\" />\n\n## 1. Customization and Encapsulation of Shape\n\n### 1. Base Class Design of Shape\n\nAll Shapes inherit from `BaseShape`, which centrally manages the lifecycle (creation, update, destruction), property parsing, animation, event binding, etc. You only need to focus on implementing the `render` method.\n\n**Core Abstraction:**\n\n```js\nimport { CustomElement } from '@antv/g';\n\nabstract class BaseShape extends CustomElement {\n  // Lifecycle management, property parsing, animation, etc...\n  public abstract render(attributes, container): void;\n}\n```\n\n### 2. Hierarchical Structure of Composite Shape\n\nA node usually contains multiple child Shapes, for example:\n\n```\nNode\n├── keyShape (main shape)\n├── label (label, auxiliary information)\n│   ├── text\n│   └── rect\n├── icon\n│   ├── text\n│   └── image\n├── badge\n│   ├── text\n│   └── rect\n└── port\n│   ├── circle\n```\n\n<img width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Ot4bSbBx97EAAAAAAAAAAAAADmJ7AQ/original\" />\n\n### 3. Implementation of Label Shape\n\nLabel is a typical composite Shape, consisting of text (Text) and an optional background (Rect). The implementation idea is as follows:\n\n- **Property Separation**: The style properties of Label are divided into text style and background style, which are passed to Text and Rect respectively.\n- **Smart Layout**: The background automatically adjusts its size and position based on the text content, padding, border radius, etc.\n- **Reuse upsert**: The `upsert` method is used to automatically manage the creation, update, and destruction of child Shapes.\n\n**Main code snippet of Label:**\n\n```js\nimport { Text, Rect } from '@antv/g'; // Import atomic graphics\n\nexport class Label extends BaseShape {\n  public render(attributes = this.parsedAttributes, container= this): void {\n    this.upsert('text', Text, this.getTextStyle(attributes), container);\n    this.upsert('background', Rect, this.getBackgroundStyle(attributes), container);\n  }\n  // ... Omitted style extraction methods\n}\n```\n\n- `getTextStyle` and `getBackgroundStyle` extract the style properties for text and background respectively to avoid interference.\n- The `upsert` method ensures automatic CRUD of Shapes, greatly improving reusability and robustness.\n\n### 4. Complete Custom Shape Example\n\nBelow is an example of customizing a label with special decoration, demonstrating the complete definition, registration, and usage of a Shape:\n\n```js\nimport { BaseShape, ExtensionCategory, Circle } from 'g6';\nimport { Text, Rect, Circle } from '@antv/g';\n\nclass FancyLabel extends BaseShape {\n  render(attributes = this.parsedAttributes, container = this) {\n    // Main text\n    this.upsert('text', Text, this.getTextStyle(attributes), container);\n    // Background\n    this.upsert('background', Rect, this.getBackgroundStyle(attributes), container);\n    // Extra decoration: small dot on the left\n    this.upsert('dot', Circle, {\n      x: -8, y: 0, r: 3, fill: '#faad14',\n    }, container);\n  }\n  // ...implement getTextStyle/getBackgroundStyle\n}\n\n// Register custom Shape\nregister(ExtensionCategory.SHAPE, 'fancy-label-shape', FancyLabel);\n\n// Define custom node\nclass CustomCircle extends Circle {\n  public drawFancyLabelShape(attributes, container) {\n    this.upsert('fancy-label', 'fancy-label-shape', this.getFancyLabelStyle(attributes), container);\n  }\n\n  render(attributes = this.parsedAttributes, container) {\n    super.render(attributes, container);\n\n    this.drawFancyLabelShape(attributes, container);\n  }\n}\n\n// Register custom node\nregister(ExtensionCategory.Node, 'fancy-label-node', CustomCircle);\n```\n\n## 2. Prefix Separation of Style Properties\n\nIn G6, elements such as nodes, edges, and combos often contain multiple child Shapes (such as main shape, label, badge, port, etc.). To ensure that the style of each child Shape does not interfere with each other, G6 adopts a **prefix separation** design for style properties.\n\n### 1. Significance of Prefix Separation\n\n- **Decoupling**: Each child Shape only cares about its own style properties, avoiding style pollution.\n- **Easy Expansion**: Adding a new child Shape only requires defining a new prefix, without modifying the original logic.\n- **Intuitive Configuration**: When configuring nodes/edges/combos, users can clearly set the style of each part.\n\n### 2. Code Implementation\n\nTake Label as an example:\n\n```ts\nimport { RectStyleProps, TextStyleProps } from '@antv/g';\n\ntype PrefixKey<P extends string = string, K extends string = string> = `${P}${Capitalize<K>}`;\n\ntype Prefix<P extends string, T extends object> = {\n  [K in keyof T as K extends string ? PrefixKey<P, K> : never]?: T[K];\n};\n\ninterface LabelStyleProps extends TextStyleProps, Prefix<'background', RectStyleProps> {\n  background?: boolean;\n}\n```\n\n- `Prefix<'background', RectStyleProps>` means all properties starting with `background` belong to the label background style.\n- During rendering, tools such as `subStyleProps` and `subObject` are used to automatically extract prefixed styles and pass them to the corresponding Shape.\n\n**Label background style extraction example**\n\n```js\nprotected getBackgroundStyle(attributes: Required<LabelStyleProps>) {\n  if (attributes.background === false) return false;\n  const style = this.getGraphicStyle(attributes);\n  const backgroundStyle = subStyleProps<RectStyleProps>(style, 'background');\n// ...Omitted layout calculation\n  return backgroundStyle;\n}\n```\n\n**Style configuration example**\n\n```json\n{\n  \"text\": \"label\",\n  \"fontSize\": 12,\n  \"fontFamily\": \"system-ui, sans-serif\",\n  \"wordWrap\": true,\n  \"maxLines\": 1,\n  \"wordWrapWidth\": 128,\n  \"textOverflow\": \"...\",\n  \"textBaseline\": \"middle\",\n  \"background\": true,\n  \"backgroundOpacity\": 0.75,\n  \"backgroundZIndex\": -1,\n  \"backgroundLineWidth\": 0\n}\n```\n\n## 3. Relationship between Label and keyShape\n\n- **keyShape** is the main shape of a node/edge/combo, determining interaction picking, bounding box, main style, etc.\n- **Label**, icon, badge, port, etc. usually exist as auxiliary Shapes and are not used as keyShape.\n- When customizing a node, you can specify the keyShape via `drawKeyShape` or similar methods. Label is only responsible for displaying text information and does not affect the main interaction control of the node.\n\n## 4. How to Apply Custom Shape in Elements\n\nTake nodes as an example. The node base class `BaseNode` has built-in support for multiple child Shapes (keyShape, label, icon, badge, port, halo, etc.). You only need to focus on drawing the keyShape, and other child Shapes can be automatically managed through configuration and style prefixing.\n\n### 1. Node Rendering Process\n\n```js\nprotected drawLabelShape(attributes: Required<S>, container: Group): void {\n  const style = this.getLabelStyle(attributes);\n  this.upsert('label', Label, style, container);\n}\n\npublic render(attributes = this.parsedAttributes, container: Group = this) {\n  // 1. Draw keyShape (main shape)\n  this._drawKeyShape(attributes, container);\n  if (!this.getShape('key')) return;\n\n  // 2. Draw halo\n  this.drawHaloShape(attributes, container);\n\n  // 3. Draw icon\n  this.drawIconShape(attributes, container);\n\n  // 4. Draw badges\n  this.drawBadgeShapes(attributes, container);\n\n  // 5. Draw label\n  this.drawLabelShape(attributes, container);\n\n  // 6. Draw ports\n  this.drawPortShapes(attributes, container);\n}\n```\n\n- The style of each child Shape is automatically extracted by prefix separation and passed to the corresponding Shape instance.\n- You can flexibly control the display and style of each child Shape through configuration options.\n\n### 2. Example of Applying Label\n\nSuppose you want to add a label with a background to a node, just configure the label-related properties in the node data:\n\n```js\n{\n  label: true,\n  labelText: 'I am a label',\n  labelFill: '#333',\n  labelFontSize: 14,\n  labelBackground: true,\n  labelBackgroundFill: '#fffbe6',\n  labelBackgroundRadius: 6,\n  labelPadding: [4, 8],\n}\n```\n\n- `labelText`, `labelFill`, `labelFontSize`, etc. will be automatically extracted and passed to the text part of the Label.\n- `labelBackground`, `labelBackgroundFill`, `labelBackgroundRadius`, `labelPadding`, etc. will be automatically extracted and passed to the background part of the Label.\n\nYou do not need to manually manage the creation, update, or destruction of the Label. G6 will handle it automatically.\n\n## 5. Common Issues and Debugging Suggestions\n\n### 1. Why is the label style not effective?\n\n- Check whether the style property prefix is correct (such as `labelFill`, `labelBackgroundFill`).\n- Make sure the `label` configuration of the node/edge/combo is `true` and `labelText` is set.\n- Check if it is overridden by other styles.\n\n### 2. How to debug the rendering of custom Shape?\n\n- Use the browser console to view `shapeMap` and confirm whether each child Shape is created correctly.\n\n### 3. How to make Label respond to node states (such as hover, selected)?\n\n- Directly set node state styles in the graph configuration (recommended)\n\n```js\nconst graph = new Graph({\n  node: {\n    style: {\n      label: false,\n    },\n    state: {\n      hover: {\n        label: true,\n        labelText: 'show when hovered',\n      },\n    },\n  },\n});\n```\n\n- Or listen for state changes in the implementation of Label and dynamically adjust the style. You can get the current state value through data.\n\n---\n\nFor more details, it is recommended to read the source code [`base-shape.ts`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/shapes/base-shape.ts), [`base-node.ts`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/shapes/base-node.ts).\n"
  },
  {
    "path": "packages/site/docs/manual/element/shape/label-shape.zh.md",
    "content": "---\ntitle: 复合 Shape 的设计与实现\norder: 3\n---\n\nG6 提供了灵活的 Shape 机制，支持开发者自定义各种图形，并在节点、边、Combo 等元素中高效复用。本文将以 Label（标签）为例，讲解如何自定义 Shape、如何在元素中应用。\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*W3oqSYPZtWEAAAAAAAAAAAAAemJ7AQ/original\" width=\"80\" />\n\n## 一、Shape 的自定义与封装\n\n### 1. Shape 的基类设计\n\n所有 Shape 都继承自 `BaseShape`，它统一管理了 Shape 的生命周期（创建、更新、销毁）、属性解析、动画、事件绑定等。你只需关注如何实现 `render` 方法即可。\n\n**核心抽象：**\n\n```js\nimport { CustomElement } from '@antv/g';\n\nabstract class BaseShape extends CustomElement {\n  // 生命周期管理、属性解析、动画等...\n  public abstract render(attributes, container): void;\n}\n```\n\n### 2. 复合 Shape 层级结构示意\n\n一个节点通常包含多个子 Shape，例如：\n\n```\n节点（Node）\n├── keyShape（主图形）\n├── label（标签，辅助信息）\n│   ├── text（文本）\n│   └── rect（背景）\n├── icon（图标）\n│   ├── text（文本）\n│   └── image（图片）\n├── badge（徽标）\n│   ├── text（文本）\n│   └── rect（背景）\n└── port（锚点）\n│   ├── circle（圆形）\n```\n\n<img width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Ot4bSbBx97EAAAAAAAAAAAAADmJ7AQ/original\" />\n\n### 3. Label Shape 的实现\n\nLabel 是一个典型的复合 Shape，由文本（Text）和可选的背景（Rect）组成。其实现思路如下：\n\n- **属性分离**：Label 的样式属性分为文本样式和背景样式，分别传递给 Text 和 Rect。\n- **智能布局**：背景自动根据文本内容、内边距、圆角等动态调整尺寸和位置。\n- **复用 upsert**：通过 `upsert` 方法自动管理子 Shape 的创建、更新、销毁。\n\n**Label 主要代码片段：**\n\n```js\nimport { Text, Rect } from '@antv/g'; // 引入原子图形\n\nexport class Label extends BaseShape {\n  public render(attributes = this.parsedAttributes, container= this): void {\n    this.upsert('text', Text, this.getTextStyle(attributes), container);\n    this.upsert('background', Rect, this.getBackgroundStyle(attributes), container);\n  }\n  // ... 省略样式提取方法\n}\n```\n\n- `getTextStyle`、`getBackgroundStyle` 分别提取文本和背景的样式属性，避免相互干扰。\n- `upsert` 方法保证了 Shape 的自动增删改查，极大提升了复用性和健壮性。\n\n### 4. 完整自定义 Shape 示例\n\n下面以自定义一个带特殊装饰的标签为例，演示 Shape 的完整定义、注册与使用：\n\n```js\nimport { BaseShape, ExtensionCategory, Circle } from 'g6';\nimport { Text, Rect, Circle } from '@antv/g';\n\nclass FancyLabel extends BaseShape {\n  render(attributes = this.parsedAttributes, container = this) {\n    // 主文本\n    this.upsert('text', Text, this.getTextStyle(attributes), container);\n    // 背景\n    this.upsert('background', Rect, this.getBackgroundStyle(attributes), container);\n    // 额外装饰：左侧小圆点\n    this.upsert('dot', Circle, {\n      x: -8, y: 0, r: 3, fill: '#faad14',\n    }, container);\n  }\n  // ...实现 getTextStyle/getBackgroundStyle\n}\n\n// 注册自定义 Shape\nregister(ExtensionCategory.SHAPE, 'fancy-label-shape', FancyLabel);\n\n// 定义自定义节点\nclass CustomCircle extends Circle {\n  public drawFancyLabelShape(attributes, container) {\n    this.upsert('fancy-label', 'fancy-label-shape', this.getFancyLabelStyle(attributes), container);\n  }\n\n  render(attributes = this.parsedAttributes, container) {\n    super.render(attributes, container);\n\n    this.drawFancyLabelShape(attributes, container);\n  }\n}\n\n// 注册自定义节点\nregister(ExtensionCategory.Node, 'fancy-label-node', CustomCircle);\n```\n\n## 二、样式属性的前缀分离\n\nG6 中节点、边、Combo 等元素往往包含多个子 Shape（如主图形、标签、徽标、锚点等）。为了让每个子 Shape 的样式互不干扰，G6 采用了**样式属性前缀分离**的设计。\n\n### 1. 前缀分离的意义\n\n- **解耦**：每个子 Shape 只关心属于自己的样式属性，避免样式污染。\n- **易扩展**：新增子 Shape 只需定义新的前缀，无需修改原有逻辑。\n- **配置直观**：用户在配置节点/边/Combo 时，可以一目了然地设置各部分样式。\n\n### 2. 代码实现\n\n以 Label 为例：\n\n```ts\nimport { RectStyleProps, TextStyleProps } from '@antv/g';\n\ntype PrefixKey<P extends string = string, K extends string = string> = `${P}${Capitalize<K>}`;\n\ntype Prefix<P extends string, T extends object> = {\n  [K in keyof T as K extends string ? PrefixKey<P, K> : never]?: T[K];\n};\n\ninterface LabelStyleProps extends TextStyleProps, Prefix<'background', RectStyleProps> {\n  background?: boolean;\n}\n```\n\n- `Prefix<'background', RectStyleProps>` 表示所有以 `background` 开头的属性都属于标签背景样式。\n- 在实际渲染时，通过 `subStyleProps`、`subObject` 等工具函数，自动提取带前缀的样式，传递给对应的 Shape。\n\n**Label 背景样式提取示例**\n\n```js\nprotected getBackgroundStyle(attributes: Required<LabelStyleProps>) {\n  if (attributes.background === false) return false;\n  const style = this.getGraphicStyle(attributes);\n  const backgroundStyle = subStyleProps<RectStyleProps>(style, 'background');\n// ...省略布局计算\n  return backgroundStyle;\n}\n```\n\n**样式配置示例**\n\n```json\n{\n  \"text\": \"label\",\n  \"fontSize\": 12,\n  \"fontFamily\": \"system-ui, sans-serif\",\n  \"wordWrap\": true,\n  \"maxLines\": 1,\n  \"wordWrapWidth\": 128,\n  \"textOverflow\": \"...\",\n  \"textBaseline\": \"middle\",\n  \"background\": true,\n  \"backgroundOpacity\": 0.75,\n  \"backgroundZIndex\": -1,\n  \"backgroundLineWidth\": 0\n}\n```\n\n## 三、Label 与 keyShape 的关系\n\n- **keyShape** 是节点/边/Combo 的主图形，决定交互拾取、包围盒、主样式等。\n- **Label**、icon、badge、port 等通常作为辅助 Shape 存在，不会作为 keyShape。\n- 你可以在自定义节点时通过 `drawKeyShape` 或类似方法指定 keyShape，Label 只负责展示文本信息，不影响节点的交互主控。\n\n## 四、如何在元素中应用自定义 Shape\n\n以节点为例，节点基类 `BaseNode` 已经内置了对多种子 Shape 的支持（keyShape、label、icon、badge、port、halo 等）。你只需专注于 keyShape 的绘制，其他子 Shape 可以通过配置和样式前缀自动管理。\n\n### 1. 节点渲染流程\n\n```js\nprotected drawLabelShape(attributes: Required<S>, container: Group): void {\n  const style = this.getLabelStyle(attributes);\n  this.upsert('label', Label, style, container);\n}\n\npublic render(attributes = this.parsedAttributes, container: Group = this) {\n  // 1. 绘制 keyShape（主图形）\n  this._drawKeyShape(attributes, container);\n  if (!this.getShape('key')) return;\n\n  // 2. 绘制 halo\n  this.drawHaloShape(attributes, container);\n\n  // 3. 绘制 icon\n  this.drawIconShape(attributes, container);\n\n  // 4. 绘制 badges\n  this.drawBadgeShapes(attributes, container);\n\n  // 5. 绘制 label\n  this.drawLabelShape(attributes, container);\n\n  // 6. 绘制 ports\n  this.drawPortShapes(attributes, container);\n}\n```\n\n- 每个子 Shape 的样式都通过前缀分离自动提取，传递给对应的 Shape 实例。\n- 你可以通过配置项灵活控制每个子 Shape 的显示与样式。\n\n### 2. 应用 Label 的示例\n\n假设你要为节点添加带背景的标签，只需在节点数据中配置 label 相关属性：\n\n```js\n{\n  label: true,\n  labelText: '我是标签',\n  labelFill: '#333',\n  labelFontSize: 14,\n  labelBackground: true,\n  labelBackgroundFill: '#fffbe6',\n  labelBackgroundRadius: 6,\n  labelPadding: [4, 8],\n}\n```\n\n- `labelText`、`labelFill`、`labelFontSize` 等会被自动提取并传递给 Label 的文本部分。\n- `labelBackground`、`labelBackgroundFill`、`labelBackgroundRadius`、`labelPadding` 等会被自动提取并传递给 Label 的背景部分。\n\n你无需手动管理 Label 的创建、更新、销毁，G6 会自动完成。\n\n## 五、常见问题与调试建议\n\n### 1. 为什么 label 样式没有生效？\n\n- 检查样式属性前缀是否正确（如 `labelFill`、`labelBackgroundFill`）。\n- 确认节点/边/Combo 的 `label` 配置为 `true`，且 `labelText` 已设置。\n- 检查是否被其他样式覆盖。\n\n### 2. 如何调试自定义 Shape 的渲染？\n\n- 使用浏览器控制台查看 `shapeMap`，确认各子 Shape 是否被正确创建。\n\n### 3. 如何让 Label 响应节点状态（如 hover、selected）？\n\n- 直接在图配置中设置节点状态样式（推荐）\n\n```js\nconst graph = new Graph({\n  node: {\n    style: {\n      label: false,\n    },\n    state: {\n      hover: {\n        label: true,\n        labelText: 'show when hovered',\n      },\n    },\n  },\n});\n```\n\n- 或者在 Label 的实现中监听状态变化，动态调整样式。可以通过 data 获取到当前的状态值\n\n---\n\n如需更深入了解，建议阅读源码 [`base-shape.ts`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/shapes/base-shape.ts)、[`base-node.ts`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/shapes/base-node.ts)。\n"
  },
  {
    "path": "packages/site/docs/manual/element/shape/overview.en.md",
    "content": "---\ntitle: Shape and KeyShape\norder: 1\n---\n\n## Shape\n\nA Shape in G6 refers to a graphical element, such as a circle, rectangle, or path. Shapes are generally associated with nodes, edges, or combos in G6. **💡 Every node/edge/combo in G6 is composed of one or more shapes. The style configuration of nodes, edges, and combos is reflected on their corresponding shapes.**\n\nFor example, in the images below: the node on the left contains a single circular shape; the node in the middle contains a circle and a text shape; the node on the right contains five circles (the blue-green main circle and four anchor points at the top, bottom, left, and right) and a text shape. Each node/edge/combo has its unique key shape (keyShape). In the examples below, the keyShape for all three nodes is the blue-green circle. The keyShape is mainly used for interaction detection and automatic style updates with [element states](/en/manual/element/state), see [keyShape](#keyshape).\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*OcaaTIIu_4cAAAAAAAAAAABkARQnAQ' width=50 alt='img'/><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*r5M0Sowd1R8AAAAAAAAAAABkARQnAQ' width=50 alt='img'/><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*pHoETad75CIAAAAAAAAAAABkARQnAQ' width=50 alt='img'/>\n\n> (Left) A node with only one circular shape, whose keyShape is the circle. (Middle) A node with a circle and a text shape, whose keyShape is the circle. (Right) A node with a main circle, text, and four small circles at the top, bottom, left, and right, whose keyShape is the main circle.\n\nG6 uses different combinations of shapes to design various built-in nodes/edges/combos. Built-in nodes include 'circle', 'rect', 'ellipse', ... (see [Built-in Nodes](/en/manual/element/node/base-node)); built-in edges include 'line', 'polyline', 'cubic', ... (see [Built-in Edges](/en/manual/element/edge/base-edge)); built-in combos include 'circle', 'rect', ... (see [Built-in Combos](/en/manual/element/combo/base-combo)).\n\nIn addition to using built-in nodes/edges/combos, G6 also allows users to customize nodes/edges/combos by combining shapes as needed. See [Custom Node](/en/manual/element/node/custom-node), [Custom Edge](/en/manual/element/edge/custom-edge), and [Custom Combo](/en/manual/element/combo/custom-combo) for details.\n\n## KeyShape\n\nIn G6, each node, edge, or combo consists of one or more shapes, but one of them is called the keyShape, which is the \"key graphical element\" of the item:\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*TZt2S7Z0d-8AAAAAAAAAAAAADmJ7AQ/original\" />\n\n> The key graphical element of the node is the colored area in the image above.\n\n### Bounding Box Determination\n\n**Determines the bounding box of a node/combo**, which is used to calculate the connection point of related edges (the intersection with the edge). If the keyShape is different, the intersection calculation between the node and the edge will also differ.\n\n#### Example\n\nIn this example, a node consists of a rect shape and a circle shape with a gray stroke and transparent fill.\n\n- When the node's keyShape is the circle:\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*CY7cSaMs4U0AAAAAAAAAAABkARQnAQ' width=220 alt='img'/>\n\n- When the node's keyShape is the rect:\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*upWTQLTvxGEAAAAAAAAAAABkARQnAQ' width=250 alt='img'/>\n\n## Shape Lifecycle\n\n> If you need to [customize nodes](/en/manual/element/node/custom-node), [customize edges](/en/manual/element/edge/custom-edge), or [customize combos](/en/manual/element/combo/custom-combo), you need to understand the lifecycle of shapes. If you use built-in nodes/edges/combos, you can skip this section.\n\nThe shape lifecycle includes:\n\n- **Initial Rendering**: Draws the shape for the first time based on data and style configuration.\n- **Update**: Automatically updates the appearance of the shape when data or style changes.\n- **Operation**: Responds to interaction states (such as selected, active, hover, etc.) and dynamically adjusts the style.\n- **Destruction**: Cleans up when the shape is removed (usually managed automatically by the Graph, so users don't need to worry).\n\nWhen customizing shapes, the most common requirement is \"how to efficiently manage the creation, update, and destruction of shapes.\" For this, G6 provides a very useful method in BaseShape:\n\n### Principle and Advantages of upsert\n\nupsert is a combination of \"update\" and \"insert\", meaning \"update if exists, insert if not\". Its function can be simply understood as:\n\n- **Automatic Judgment**: You only need to describe the desired appearance of the shape. `upsert` will automatically determine whether the shape already exists. If not, it will create it; if it exists, it will update it; if it needs to be deleted, it will remove it automatically.\n- **Simplified Logic**: Developers do not need to manually manage the CRUD of shapes, avoiding duplicate code and state confusion.\n- **Improved Robustness**: Whether it's the initial rendering, data changes, or state switching, upsert ensures that the shape always stays in sync with the data and configuration.\n\n**Type Definition:**\n\n```js\n/**\n * Create, update, or delete a shape\n * @param className Shape name\n * @param Ctor Shape type\n * @param style Shape style. Pass false to delete the shape\n * @param container Container\n * @param hooks Hooks\n * @returns Shape instance\n */\nupsert<T extends DisplayObject>(\n  className: string,\n  Ctor: string | { new (...args: any[]): T },\n  style: T['attributes'] | false,\n  container: DisplayObject,\n  hooks?: UpsertHooks,\n): T | undefined {}\n```\n\nYou only need to describe \"what kind of shape you want now\" without worrying about whether it is being created, updated, or deleted. upsert will handle it for you. This makes customizing and managing complex composite shapes very simple and safe.\n"
  },
  {
    "path": "packages/site/docs/manual/element/shape/overview.zh.md",
    "content": "---\ntitle: 图形 Shape 与 KeyShape\norder: 1\n---\n\n## 图形 Shape\n\nShape 指 G6 中的图形、形状，可以是圆形、矩形、路径等。它一般与 G6 中的节点、边、Combo 相关。**💡 G6 中的每一种节点/边/Combo 都是由一个或多个 Shape 组合而成。节点、边、Combo 的样式配置都会被体现到对应的图形上。**\n\n例如下图（左）的节点包含了一个圆形图形；下图（中）的节点含有一个圆形和一个文本图形；下图（右）的节点中含有 5 个圆形（蓝绿色的圆和上下左右四个锚点）、一个文本图形。但每种节点/边/Combo 都会有自己的唯一关键图形 keyShape，下图中三个节点的 keyShape 都是蓝绿色的圆，keyShape 主要用于交互检测、样式随 [元素状态](/manual/element/state) 自动更新等，见 [keyShape](#keyshape)。\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*OcaaTIIu_4cAAAAAAAAAAABkARQnAQ' width=50 alt='img'/><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*r5M0Sowd1R8AAAAAAAAAAABkARQnAQ' width=50 alt='img'/><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*pHoETad75CIAAAAAAAAAAABkARQnAQ' width=50 alt='img'/>\n\n> （左）只含有一个圆形图形的节点，keyShape 是该圆形。（中）含有圆形和文本图形的节点，keyShape 是圆形。（右）含有主要圆形、文本、上下左右四个小圆形的节点，keyShape 是圆形。\n\nG6 使用不同的 shape 组合，设计了多种内置的节点/边/ Combo 。G6 内置节点的有 'circle'， 'rect'，'ellipse'，...（详见 [内置节点](/manual/element/node/base-node)）；内置边的有 'line'，'polyline'，'cubic'，...（详见 [内置边](/manual/element/edge/base-edge)）；内置 Combo 有 'circle'，'rect'，（详见 [内置 Combo](/manual/element/combo/base-combo)）。\n\n除了使用内置的节点/边/ Combo 外，G6 还允许用户通过自己搭配和组合 shape 进行节点/边/ Combo 的自定义，详见 [自定义节点](/manual/element/node/custom-node)，[自定义边](/manual/element/edge/custom-edge)，[自定义 Combo](/manual/element/combo/custom-combo)。\n\n## KeyShape\n\n在 G6 中，每个节点、边、Combo 都由一个或多个 Shape 组成，但其中有一个 Shape 被称为 keyShape，它是该元素的“关键图形”：\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*TZt2S7Z0d-8AAAAAAAAAAAAADmJ7AQ/original\" />\n\n> 节点的关键图形就是上图的颜色区域\n\n### 包围盒确定\n\n**确定节点 / Combo 的包围盒（Bounding Box）** ，从而计算相关边的连入点（与相关边的交点）。若 keyShape 不同，节点与边的交点计算结果不同。\n\n#### 示例  \n\n本例中的一个节点由一个 rect 图形和一个带灰色描边、填充透明的 circle 图形构成。\n\n- 当节点的 keyShape 为 circle 时：\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*CY7cSaMs4U0AAAAAAAAAAABkARQnAQ' width=220 alt='img'/>\n\n- 当节点的 keyShape 为 rect 时：\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*upWTQLTvxGEAAAAAAAAAAABkARQnAQ' width=250 alt='img'/>\n\n## Shape 的生命周期\n\n> 当用户需要 [自定义节点](/manual/element/node/custom-node)、[自定义边](/manual/element/edge/custom-edge)、[自定义 Combo](/manual/element/combo/custom-combo) 时，需要了解 Shape 的生命周期。使用内置节点/边/Combo 则可以跳过这一部分内容。\n\n从整体来看，Shape 的生命周期分为：\n\n- **初始化渲染**：首次根据数据和样式配置绘制出 Shape。\n- **更新**：当数据或样式发生变化时，自动更新 Shape 的表现。\n- **操作**：响应交互状态（如 selected、active、hover 等），动态调整样式。\n- **销毁**：Shape 被移除时的清理（通常由 Graph 自动管理，用户无需关心）。\n\n在自定义 Shape 时，最常见的需求就是“如何高效地管理 Shape 的创建、更新和销毁”。G6 为此在 BaseShape 中设计了一个非常实用的方法：\n\n### upsert 的原理与优势\n\nupsert 是“update” 和 “insert” 的合成词，意思是“有则更新，无则创建”。它的作用可以简单理解为：\n\n- **自动判断**：你只需描述希望 Shape 呈现的样子，`upsert` 会自动判断当前 Shape 是否已存在。如果不存在则创建，如果已存在则更新，如果需要删除则自动移除。\n- **简化逻辑**：开发者无需手动管理 Shape 的增删改查，避免了重复代码和状态混乱。\n- **提升健壮性**：无论是初次渲染、数据变更还是状态切换，upsert 都能保证 Shape 始终与数据和配置保持同步。\n\n**类型定义：**\n\n```js\n/**\n * 创建、更新或删除图形\n * @param className 图形名称\n * @param Ctor 图形类型\n * @param style 图形样式。若要删除图形，传入 false\n * @param container 容器\n * @param hooks 钩子函数\n * @returns 图形实例\n */\nupsert<T extends DisplayObject>(\n  className: string,\n  Ctor: string | { new (...args: any[]): T },\n  style: T['attributes'] | false,\n  container: DisplayObject,\n  hooks?: UpsertHooks,\n): T | undefined {}\n```\n\n你只需要描述“我现在想要什么样的图形”，不用关心它是新建、更新还是删除，upsert 都会帮你处理好。这让自定义和管理复杂的复合 Shape 变得非常简单和安全。\n"
  },
  {
    "path": "packages/site/docs/manual/element/shape/properties.en.md",
    "content": "---\ntitle: Atomic Shapes and Their Properties\norder: 2\n---\n\nElements (nodes/edges) in G6 are composed of **one or more [shapes](/en/manual/element/shape/overview)**, mainly added via `upsert` in the `render` method when customizing nodes or edges. G6 supports the following shapes:\n\n1. [Circle](#circlestyleprops)\n2. [Ellipse](#ellipsestyleprops)\n3. [Rect](#rectstyleprops)\n4. [HTML Element](#htmlstyleprops)\n5. [Image](#imagestyleprops)\n6. [Line](#linestyleprops)\n7. [Path](#pathstyleprops)\n8. [Polygon](#polygonstyleprops)\n9. [Polyline](#polylinestyleprops)\n10. [Text](#textstyleprops)\n\n## Common Properties of All Shapes\n\n### BaseShapeStyle\n\n| Property       | Description                                                                                      | Type                                     | Required |\n| -------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------- | -------- |\n| x              | x coordinate                                                                                     | number                                   | ✓        |\n| y              | y coordinate                                                                                     | number                                   | ✓        |\n| width          | Width                                                                                            | number                                   | ✓        |\n| height         | Height                                                                                           | number                                   | ✓        |\n| fill           | Fill color                                                                                       | string \\| Pattern \\| null                |          |\n| stroke         | Stroke color                                                                                     | string \\| Pattern \\| null                |          |\n| opacity        | Overall opacity                                                                                  | number \\| string                         |          |\n| fillOpacity    | Fill opacity                                                                                     | number \\| string                         |          |\n| strokeOpacity  | Stroke opacity                                                                                   | number \\| string                         |          |\n| lineWidth      | Line width                                                                                       | number \\| string                         |          |\n| lineCap        | Line cap style                                                                                   | `butt` \\| `round` \\| `square`            |          |\n| lineJoin       | Line join style                                                                                  | `miter` \\| `round` \\| `bevel`            |          |\n| lineDash       | Dash array                                                                                       | number \\| string \\| (string \\| number)[] |          |\n| lineDashOffset | Dash offset                                                                                      | number                                   |          |\n| shadowBlur     | Shadow blur                                                                                      | number                                   |          |\n| shadowColor    | Shadow color                                                                                     | string                                   |          |\n| shadowOffsetX  | Shadow X offset                                                                                  | number                                   |          |\n| shadowOffsetY  | Shadow Y offset                                                                                  | number                                   |          |\n| cursor         | Mouse cursor, supports all [CSS cursor](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor) | string                                   |          |\n| zIndex         | Render z-index                                                                                   | number                                   |          |\n| visibility     | Visibility                                                                                       | `visible` \\| `hidden`                    |          |\n\n**Example:**\n\n```js\nconst shape = BaseShape.upsert(\n  // Specify the shape key, which must be unique within the same custom element type\n  'shape',\n  'circle',\n  {\n    cx: 100,\n    cy: 100,\n    r: 50,\n    fill: 'blue',\n  },\n  container,\n);\n```\n\n## Common Methods of All Shapes\n\n### attr()\n\nSet or get the drawing attributes of the instance.\n\n### attr(name)\n\nGet the value of an attribute.\n\n```js\nconst width = shape.attr('width');\n```\n\n### attr(name, value)\n\nUpdate a single drawing attribute.\n\n### attr({...})\n\nBatch update drawing attributes.\n\n```js\nshape.attr({\n  fill: '#999',\n  stroke: '#666',\n});\n```\n\n## Circle Shape\n\n### CircleStyleProps\n\n| Property          | Description                               | Type             | Required |\n| ----------------- | ----------------------------------------- | ---------------- | -------- |\n| cx                | Center x coordinate                       | number \\| string | ✓        |\n| cy                | Center y coordinate                       | number \\| string | ✓        |\n| cz                | Center z coordinate                       | number \\| string |          |\n| r                 | Radius                                    | number \\| string | ✓        |\n| isBillboard       | Billboard mode (always faces camera)      | boolean          |          |\n| isSizeAttenuation | Size attenuation (size changes with view) | boolean          |          |\n\n**Example:**\n\n```js\nBaseShape.upsert(\n  'shape',\n  'circle',\n  {\n    cx: 100,\n    cy: 100,\n    r: 50,\n    fill: 'blue',\n  },\n  container,\n);\n```\n\n## Rect Shape\n\n### RectStyleProps\n\n| Property          | Description       | Type                         | Required |\n| ----------------- | ----------------- | ---------------------------- | -------- |\n| x                 | Rect x coordinate | number \\| string             |          |\n| y                 | Rect y coordinate | number \\| string             |          |\n| z                 | Rect z coordinate | number                       |          |\n| width             | Rect width        | number \\| string             | ✓        |\n| height            | Rect height       | number \\| string             | ✓        |\n| isBillboard       | Billboard mode    | boolean                      |          |\n| isSizeAttenuation | Size attenuation  | boolean                      |          |\n| radius            | Border radius     | number \\| string \\| number[] |          |\n\n**Example:**\n\n```js\nBaseShape.upsert(\n  'shape',\n  'rect',\n  {\n    x: 100,\n    y: 100,\n    width: 100,\n    height: 100,\n    radius: 8,\n    fill: 'blue',\n  },\n  container,\n);\n```\n\n## Ellipse Shape\n\n### EllipseStyleProps\n\n| Property          | Description         | Type             | Required |\n| ----------------- | ------------------- | ---------------- | -------- |\n| cx                | Center x coordinate | number \\| string | ✓        |\n| cy                | Center y coordinate | number \\| string | ✓        |\n| cz                | Center z coordinate | number \\| string |          |\n| rx                | X-axis radius       | number \\| string | ✓        |\n| ry                | Y-axis radius       | number \\| string | ✓        |\n| isBillboard       | Billboard mode      | boolean          |          |\n| isSizeAttenuation | Size attenuation    | boolean          |          |\n\n**Example:**\n\n```js\nBaseShape.upsert(\n  'shape',\n  'ellipse',\n  {\n    cx: 100,\n    cy: 100,\n    rx: 50,\n    ry: 80,\n    fill: 'blue',\n  },\n  container,\n);\n```\n\n## HTML DOM\n\n### HTMLStyleProps\n\n| Property  | Description       | Type                  | Required |\n| --------- | ----------------- | --------------------- | -------- |\n| x         | HTML x coordinate | number \\| string      |          |\n| y         | HTML y coordinate | number \\| string      |          |\n| innerHTML | HTML content      | string \\| HTMLElement | ✓        |\n| width     | HTML width        | number \\| string      |          |\n| height    | HTML height       | number \\| string      |          |\n\n**Example:**\n\n```js\nBaseShape.upsert(\n  'shape',\n  'html',\n  {\n    x: 100,\n    y: 100,\n    innerHTML: <div>content</div>,\n  },\n  container,\n);\n```\n\n## Image Shape\n\n### ImageStyleProps\n\n| Property          | Description                      | Type                       | Required |\n| ----------------- | -------------------------------- | -------------------------- | -------- |\n| x                 | Image x coordinate               | number \\| string           |          |\n| y                 | Image y coordinate               | number \\| string           |          |\n| z                 | Image z coordinate               | number                     |          |\n| src               | Image source or HTMLImageElement | string \\| HTMLImageElement | ✓        |\n| width             | Image width                      | number \\| string           |          |\n| height            | Image height                     | number \\| string           |          |\n| isBillboard       | Billboard mode                   | boolean                    |          |\n| isSizeAttenuation | Size attenuation                 | boolean                    |          |\n| billboardRotation | Billboard rotation angle         | number                     |          |\n| keepAspectRatio   | Keep original aspect ratio       | boolean                    |          |\n\n**Example:**\n\n```js\nBaseShape.upsert(\n  'shape',\n  'image',\n  {\n    x: 100,\n    y: 100,\n    src: 'http://',\n  },\n  container,\n);\n```\n\n## Line Shape\n\n### LineStyleProps\n\n| Property          | Description         | Type                  | Required |\n| ----------------- | ------------------- | --------------------- | -------- |\n| x1                | Start x coordinate  | number                | ✓        |\n| y1                | Start y coordinate  | number                | ✓        |\n| x2                | End x coordinate    | number                | ✓        |\n| y2                | End y coordinate    | number                | ✓        |\n| z1                | Start z coordinate  | number                |          |\n| z2                | End z coordinate    | number                |          |\n| isBillboard       | Billboard mode      | boolean               |          |\n| isSizeAttenuation | Size attenuation    | boolean               |          |\n| markerStart       | Marker at start     | DisplayObject \\| null |          |\n| markerEnd         | Marker at end       | DisplayObject \\| null |          |\n| markerStartOffset | Start marker offset | number                |          |\n| markerEndOffset   | End marker offset   | number                |          |\n\n**Example:**\n\n```js\nBaseShape.upsert(\n  'shape',\n  'line',\n  {\n    x1: 100,\n    y1: 100,\n    x2: 150,\n    y2: 150,\n    stroke: 'blue',\n  },\n  container,\n);\n```\n\n## Path Shape\n\n### PathStyleProps\n\n| Property          | Description          | Type                   | Required |\n| ----------------- | -------------------- | ---------------------- | -------- |\n| d                 | Path string or array | string \\| PathArray    | ✓        |\n| markerStart       | Marker at start      | DisplayObject \\| null  |          |\n| markerEnd         | Marker at end        | DisplayObject \\| null  |          |\n| markerMid         | Marker at middle     | DisplayObject \\| null  |          |\n| markerStartOffset | Start marker offset  | number                 |          |\n| markerEndOffset   | End marker offset    | number                 |          |\n| isBillboard       | Billboard mode       | boolean                |          |\n| isSizeAttenuation | Size attenuation     | boolean                |          |\n| fillRule          | Fill rule            | `nonzero` \\| `evenodd` |          |\n\n**Example:**\n\n```js\nBaseShape.upsert(\n  'shape',\n  'path',\n  {\n    d: 'M 0,0 L 20,10 L 20,-10 Z',\n    stroke: 'blue',\n  },\n  container,\n);\n```\n\n## Polygon Shape\n\n### PolygonStyleProps\n\n| Property          | Description             | Type                                             | Required |\n| ----------------- | ----------------------- | ------------------------------------------------ | -------- |\n| points            | Array of polygon points | ([number, number] \\| [number, number, number])[] | ✓        |\n| markerStart       | Marker at start         | DisplayObject \\| null                            |          |\n| markerEnd         | Marker at end           | DisplayObject \\| null                            |          |\n| markerMid         | Marker at middle        | DisplayObject \\| null                            |          |\n| markerStartOffset | Start marker offset     | number                                           |          |\n| markerEndOffset   | End marker offset       | number                                           |          |\n| isClosed          | Is polygon closed       | boolean                                          |          |\n| isBillboard       | Billboard mode          | boolean                                          |          |\n| isSizeAttenuation | Size attenuation        | boolean                                          |          |\n\n**Example:**\n\n```js\nBaseShape.upsert(\n  'shape',\n  'polygon',\n  {\n    points: [\n      [30, 30],\n      [40, 20],\n      [30, 50],\n      [60, 100],\n    ],\n    fill: 'red',\n  },\n  container,\n);\n```\n\n## Polyline Shape\n\n### PolylineStyleProps\n\n| Property          | Description              | Type                                             | Required |\n| ----------------- | ------------------------ | ------------------------------------------------ | -------- |\n| points            | Array of polyline points | ([number, number] \\| [number, number, number])[] | ✓        |\n| markerStart       | Marker at start          | DisplayObject \\| null                            |          |\n| markerEnd         | Marker at end            | DisplayObject \\| null                            |          |\n| markerMid         | Marker at middle         | DisplayObject \\| null                            |          |\n| markerStartOffset | Start marker offset      | number                                           |          |\n| markerEndOffset   | End marker offset        | number                                           |          |\n| isBillboard       | Billboard mode           | boolean                                          |          |\n| isSizeAttenuation | Size attenuation         | boolean                                          |          |\n\n**Example:**\n\n```js\nBaseShape.upsert(\n  'shape',\n  'polyline',\n  {\n    points: [\n      [30, 30],\n      [40, 20],\n      [30, 50],\n      [60, 100],\n    ],\n    fill: 'red',\n  },\n  container,\n);\n```\n\n## Text\n\n### TextStyleProps\n\n| Property            | Description              | Type                                                                        | Required |\n| ------------------- | ------------------------ | --------------------------------------------------------------------------- | -------- |\n| x                   | Text x coordinate        | number \\| string                                                            |          |\n| y                   | Text y coordinate        | number \\| string                                                            |          |\n| z                   | Text z coordinate        | number \\| string                                                            |          |\n| text                | Text content             | number \\| string                                                            | ✓        |\n| fontSize            | Font size                | number \\| string                                                            |          |\n| fontFamily          | Font family              | string                                                                      |          |\n| fontStyle           | Font style               | `normal` \\| `italic` \\| `oblique`                                           |          |\n| fontWeight          | Font weight              | `normal` \\| `bold` \\| `bolder` \\| `lighter` \\| number                       |          |\n| fontVariant         | Font variant             | `normal` \\| `small-caps` \\| string                                          |          |\n| textAlign           | Text horizontal align    | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`               |          |\n| textBaseline        | Text baseline            | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom' |          |\n| textOverflow        | Text overflow            | `clip` \\| `ellipsis` \\| string                                              |          |\n| lineHeight          | Line height              | number \\| string                                                            |          |\n| letterSpacing       | Letter spacing           | number \\| string                                                            |          |\n| maxLines            | Max lines                | number                                                                      |          |\n| textPath            | Text path                | Path                                                                        |          |\n| textPathSide        | Text path side           | `left` \\| `right`                                                           |          |\n| textPathStartOffset | Text path start offset   | number \\| string                                                            |          |\n| textDecorationLine  | Text decoration line     | string                                                                      |          |\n| textDecorationColor | Text decoration color    | string                                                                      |          |\n| textDecorationStyle | Text decoration style    | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                       |          |\n| isBillboard         | Billboard mode           | boolean                                                                     |          |\n| billboardRotation   | Billboard rotation angle | number                                                                      |          |\n| isSizeAttenuation   | Size attenuation         | boolean                                                                     |          |\n| wordWrap            | Word wrap                | boolean                                                                     |          |\n| wordWrapWidth       | Word wrap width          | number                                                                      |          |\n| dx                  | X offset                 | number \\| string                                                            |          |\n| dy                  | Y offset                 | number \\| string                                                            |          |\n\n**Example:**\n\n```js\nBaseShape.upsert(\n  'shape',\n  'text',\n  {\n    x: 100,\n    y: 100,\n    text: 'text',\n  },\n  container,\n);\n```\n\n**Display in multiply line:**\n\n```js\n{\n  wordWrap: true,\n  wordWrapWidth: 100,\n  maxLines: 4,\n  textOverflow: 'ellipsis',\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/shape/properties.zh.md",
    "content": "---\ntitle: 原子 Shape 以及其属性\norder: 2\n---\n\nG6 中的元素（节点/边）是由**一个或多个 [图形 Shape](/manual/element/shape/overview)** 组成，主要通过自定义节点或自定义边时在 `render` 方法中使用 `upsert` 添加，G6 中支持以下的图形 Shape：\n\n1. [Circle - 圆形](#circlestyleprops)\n2. [Ellipse - 椭圆](#ellipsestyleprops)\n3. [Rect - 矩形](#rectstyleprops)\n4. [HTML - HTML元素](#htmlstyleprops)\n5. [Image - 图片](#imagestyleprops)\n6. [Line - 线](#linestyleprops)\n7. [Path - 路径](#pathstyleprops)\n8. [Polygon - 多边形](#polygonstyleprops)\n9. [Polyline - 折线](#polylinestyleprops)\n10. [Text - 文本](#textstyleprops)\n\n## 各图形 Shape 的通用属性\n\n### BaseShapeStyle\n\n| 属性           | 描述                                                                                          | 类型                                     | 必选 |\n| -------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------- | ---- |\n| x              | x 坐标                                                                                        | number                                   | ✓    |\n| y              | y 坐标                                                                                        | number                                   | ✓    |\n| width          | 宽度                                                                                          | number                                   | ✓    |\n| height         | 高度                                                                                          | number                                   | ✓    |\n| fill           | 填充颜色                                                                                      | string \\| Pattern \\| null                |      |\n| stroke         | 描边颜色                                                                                      | string \\| Pattern \\| null                |      |\n| opacity        | 整体透明度                                                                                    | number \\| string                         |      |\n| fillOpacity    | 填充透明度                                                                                    | number \\| string                         |      |\n| strokeOpacity  | 描边透明度                                                                                    | number \\| string                         |      |\n| lineWidth      | 线宽度                                                                                        | number \\| string                         |      |\n| lineCap        | 线段端点样式                                                                                  | `butt` \\| `round` \\| `square`            |      |\n| lineJoin       | 线段连接处样式                                                                                | `miter` \\| `round` \\| `bevel`            |      |\n| lineDash       | 虚线配置                                                                                      | number \\| string \\| (string \\| number)[] |      |\n| lineDashOffset | 虚线偏移量                                                                                    | number                                   |      |\n| shadowBlur     | 阴影模糊程度                                                                                  | number                                   |      |\n| shadowColor    | 阴影颜色                                                                                      | string                                   |      |\n| shadowOffsetX  | 阴影 X 方向偏移                                                                               | number                                   |      |\n| shadowOffsetY  | 阴影 Y 方向偏移                                                                               | number                                   |      |\n| cursor         | 鼠标样式，[CSS 的 cursor](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor) 选项都支持 | string                                   |      |\n| zIndex         | 渲染层级                                                                                      | number                                   |      |\n| visibility     | 可见性                                                                                        | `visible` \\| `hidden`                    |      |\n\n**示例：**\n\n```js\nconst shape = BaseShape.upsert(\n  // 指定图形 key，需要保证在同一个自定义元素类型中保持唯一性\n  'shape',\n  'circle',\n  {\n    cx: 100,\n    cy: 100,\n    r: 50,\n    fill: 'blue',\n  },\n  container,\n);\n```\n\n## 各图形 Shape 的通用方法\n\n### attr()\n\n设置或获取实例的绘图属性。\n\n### attr(name)\n\n获取实例的属性值。\n\n```js\nconst width = shape.attr('width');\n```\n\n### attr(name, value)\n\n更新实例的单个绘图属性。\n\n### attr({...})\n\n批量更新实例绘图属性。\n\n```js\nshape.attr({\n  fill: '#999',\n  stroke: '#666',\n});\n```\n\n## 圆图形 Circle\n\n### CircleStyleProps\n\n| 属性              | 描述                               | 类型             | 必选 |\n| ----------------- | ---------------------------------- | ---------------- | ---- |\n| cx                | 圆心 x 坐标                        | number \\| string | ✓    |\n| cy                | 圆心 y 坐标                        | number \\| string | ✓    |\n| cz                | 圆心 z 坐标                        | number \\| string |      |\n| r                 | 圆的半径                           | number \\| string | ✓    |\n| isBillboard       | 是否启用公告牌模式（始终面向相机） | boolean          |      |\n| isSizeAttenuation | 是否启用大小衰减（随视距变化大小） | boolean          |      |\n\n**示例：**\n\n```js\nBaseShape.upsert(\n  // 指定图形 key，需要保证在同一个自定义元素类型中保持唯一性\n  'shape',\n  'circle',\n  {\n    cx: 100,\n    cy: 100,\n    r: 50,\n    fill: 'blue',\n  },\n  container,\n);\n```\n\n## 矩形图形 Rect\n\n### RectStyleProps\n\n| 属性              | 描述               | 类型                         | 必选 |\n| ----------------- | ------------------ | ---------------------------- | ---- |\n| x                 | 矩形 x 坐标        | number \\| string             |      |\n| y                 | 矩形 y 坐标        | number \\| string             |      |\n| z                 | 矩形 z 坐标        | number                       |      |\n| width             | 矩形宽度           | number \\| string             | ✓    |\n| height            | 矩形高度           | number \\| string             | ✓    |\n| isBillboard       | 是否启用公告牌模式 | boolean                      |      |\n| isSizeAttenuation | 是否启用大小衰减   | boolean                      |      |\n| radius            | 矩形圆角半径       | number \\| string \\| number[] |      |\n\n**示例：**\n\n```js\nBaseShape.upsert(\n  // 指定图形 key，需要保证在同一个自定义元素类型中保持唯一性\n  'shape',\n  'rect',\n  {\n    x: 100,\n    y: 100,\n    width: 100,\n    height: 100,\n    radius: 8,\n    fill: 'blue',\n  },\n  container,\n);\n```\n\n## 椭圆图形 Ellipse\n\n### EllipseStyleProps\n\n| 属性              | 描述               | 类型             | 必选 |\n| ----------------- | ------------------ | ---------------- | ---- |\n| cx                | 椭圆中心 x 坐标    | number \\| string | ✓    |\n| cy                | 椭圆中心 y 坐标    | number \\| string | ✓    |\n| cz                | 椭圆中心 z 坐标    | number \\| string |      |\n| rx                | 椭圆 x 轴半径      | number \\| string | ✓    |\n| ry                | 椭圆 y 轴半径      | number \\| string | ✓    |\n| isBillboard       | 是否启用公告牌模式 | boolean          |      |\n| isSizeAttenuation | 是否启用大小衰减   | boolean          |      |\n\n**示例：**\n\n```js\nBaseShape.upsert(\n  // 指定图形 key，需要保证在同一个自定义元素类型中保持唯一性\n  'shape',\n  'ellipse',\n  {\n    cx: 100,\n    cy: 100,\n    rx: 50,\n    ry: 80,\n    fill: 'blue',\n  },\n  container,\n);\n```\n\n## HTML DOM\n\n### HTMLStyleProps\n\n| 属性      | 描述             | 类型                  | 必选 |\n| --------- | ---------------- | --------------------- | ---- |\n| x         | HTML 元素 x 坐标 | number \\| string      |      |\n| y         | HTML 元素 y 坐标 | number \\| string      |      |\n| innerHTML | HTML 内容        | string \\| HTMLElement | ✓    |\n| width     | HTML 元素宽度    | number \\| string      |      |\n| height    | HTML 元素高度    | number \\| string      |      |\n\n**示例：**\n\n```js\nBaseShape.upsert(\n  // 指定图形 key，需要保证在同一个自定义元素类型中保持唯一性\n  'shape',\n  'html',\n  {\n    x: 100,\n    y: 100,\n    innerHTML: <div>content</div>,\n  },\n  container,\n);\n```\n\n## 图片图形 Image\n\n### ImageStyleProps\n\n| 属性              | 描述                         | 类型                       | 必选 |\n| ----------------- | ---------------------------- | -------------------------- | ---- |\n| x                 | 图片 x 坐标                  | number \\| string           |      |\n| y                 | 图片 y 坐标                  | number \\| string           |      |\n| z                 | 图片 z 坐标                  | number                     |      |\n| src               | 图片资源路径或 HTML 图片元素 | string \\| HTMLImageElement | ✓    |\n| width             | 图片宽度                     | number \\| string           |      |\n| height            | 图片高度                     | number \\| string           |      |\n| isBillboard       | 是否启用公告牌模式           | boolean                    |      |\n| isSizeAttenuation | 是否启用大小衰减             | boolean                    |      |\n| billboardRotation | 公告牌模式下的旋转角度       | number                     |      |\n| keepAspectRatio   | 是否保持图片原有宽高比       | boolean                    |      |\n\n**示例：**\n\n```js\nBaseShape.upsert(\n  // 指定图形 key，需要保证在同一个自定义元素类型中保持唯一性\n  'shape',\n  'image',\n  {\n    x: 100,\n    y: 100,\n    src: 'http://',\n  },\n  container,\n);\n```\n\n## 直线 Line\n\n### LineStyleProps\n\n| 属性              | 描述               | 类型                  | 必选 |\n| ----------------- | ------------------ | --------------------- | ---- |\n| x1                | 线段起点 x 坐标    | number                | ✓    |\n| y1                | 线段起点 y 坐标    | number                | ✓    |\n| x2                | 线段终点 x 坐标    | number                | ✓    |\n| y2                | 线段终点 y 坐标    | number                | ✓    |\n| z1                | 线段起点 z 坐标    | number                |      |\n| z2                | 线段终点 z 坐标    | number                |      |\n| isBillboard       | 是否启用公告牌模式 | boolean               |      |\n| isSizeAttenuation | 是否启用大小衰减   | boolean               |      |\n| markerStart       | 线段起点的标记     | DisplayObject \\| null |      |\n| markerEnd         | 线段终点的标记     | DisplayObject \\| null |      |\n| markerStartOffset | 起点标记的偏移量   | number                |      |\n| markerEndOffset   | 终点标记的偏移量   | number                |      |\n\n**示例：**\n\n```js\nBaseShape.upsert(\n  // 指定图形 key，需要保证在同一个自定义元素类型中保持唯一性\n  'shape',\n  'line',\n  {\n    x1: 100,\n    y1: 100,\n    x2: 150,\n    y2: 150,\n    stroke: 'blue',\n  },\n  container,\n);\n```\n\n## 路径 Path\n\n### PathStyleProps\n\n| 属性              | 描述                 | 类型                   | 必选 |\n| ----------------- | -------------------- | ---------------------- | ---- |\n| d                 | 路径定义字符串或数组 | string \\| PathArray    | ✓    |\n| markerStart       | 路径起点的标记       | DisplayObject \\| null  |      |\n| markerEnd         | 路径终点的标记       | DisplayObject \\| null  |      |\n| markerMid         | 路径中间点的标记     | DisplayObject \\| null  |      |\n| markerStartOffset | 起点标记的偏移量     | number                 |      |\n| markerEndOffset   | 终点标记的偏移量     | number                 |      |\n| isBillboard       | 是否启用公告牌模式   | boolean                |      |\n| isSizeAttenuation | 是否启用大小衰减     | boolean                |      |\n| fillRule          | 填充规则             | `nonzero` \\| `evenodd` |      |\n\n**示例：**\n\n```js\nBaseShape.upsert(\n  // 指定图形 key，需要保证在同一个自定义元素类型中保持唯一性\n  'shape',\n  'path',\n  {\n    d: 'M 0,0 L 20,10 L 20,-10 Z',\n    stroke: 'blue',\n  },\n  container,\n);\n```\n\n## 多边形图形 Polygon\n\n### PolygonStyleProps\n\n| 属性              | 描述               | 类型                                             | 必选 |\n| ----------------- | ------------------ | ------------------------------------------------ | ---- |\n| points            | 多边形的顶点数组   | ([number, number] \\| [number, number, number])[] | ✓    |\n| markerStart       | 多边形起点的标记   | DisplayObject \\| null                            |      |\n| markerEnd         | 多边形终点的标记   | DisplayObject \\| null                            |      |\n| markerMid         | 多边形中间点的标记 | DisplayObject \\| null                            |      |\n| markerStartOffset | 起点标记的偏移量   | number                                           |      |\n| markerEndOffset   | 终点标记的偏移量   | number                                           |      |\n| isClosed          | 是否闭合多边形     | boolean                                          |      |\n| isBillboard       | 是否启用公告牌模式 | boolean                                          |      |\n| isSizeAttenuation | 是否启用大小衰减   | boolean                                          |      |\n\n**示例：**\n\n```js\nBaseShape.upsert(\n  // 指定图形 key，需要保证在同一个自定义元素类型中保持唯一性\n  'shape',\n  'polygon',\n  {\n    points: [\n      [30, 30],\n      [40, 20],\n      [30, 50],\n      [60, 100],\n    ],\n    fill: 'red',\n  },\n  container,\n);\n```\n\n## 折线 Polyline\n\n### PolylineStyleProps\n\n| 属性              | 描述               | 类型                                             | 必选 |\n| ----------------- | ------------------ | ------------------------------------------------ | ---- |\n| points            | 折线的顶点数组     | ([number, number] \\| [number, number, number])[] | ✓    |\n| markerStart       | 折线起点的标记     | DisplayObject \\| null                            |      |\n| markerEnd         | 折线终点的标记     | DisplayObject \\| null                            |      |\n| markerMid         | 折线中间点的标记   | DisplayObject \\| null                            |      |\n| markerStartOffset | 起点标记的偏移量   | number                                           |      |\n| markerEndOffset   | 终点标记的偏移量   | number                                           |      |\n| isBillboard       | 是否启用公告牌模式 | boolean                                          |      |\n| isSizeAttenuation | 是否启用大小衰减   | boolean                                          |      |\n\n**示例：**\n\n```js\nBaseShape.upsert(\n  // 指定图形 key，需要保证在同一个自定义元素类型中保持唯一性\n  'shape',\n  'polyline',\n  {\n    points: [\n      [30, 30],\n      [40, 20],\n      [30, 50],\n      [60, 100],\n    ],\n    fill: 'red',\n  },\n  container,\n);\n```\n\n## 文字 Text\n\n### TextStyleProps\n\n| 属性                | 描述               | 类型                                                                        | 必选 |\n| ------------------- | ------------------ | --------------------------------------------------------------------------- | ---- |\n| x                   | 文本 x 坐标        | number \\| string                                                            |      |\n| y                   | 文本 y 坐标        | number \\| string                                                            |      |\n| z                   | 文本 z 坐标        | number \\| string                                                            |      |\n| text                | 文本内容           | number \\| string                                                            | ✓    |\n| fontSize            | 字体大小           | number \\| string                                                            |      |\n| fontFamily          | 字体族             | string                                                                      |      |\n| fontStyle           | 字体样式           | `normal` \\| `italic` \\| `oblique`                                           |      |\n| fontWeight          | 字体粗细           | `normal` \\| `bold` \\| `bolder` \\| `lighter` \\| number                       |      |\n| fontVariant         | 字体变种           | `normal` \\| `small-caps` \\| string                                          |      |\n| textAlign           | 文本水平对齐方式   | `start` \\| `center` \\| `middle` \\| `end` \\| `left` \\| `right`               |      |\n| textBaseline        | 文本基线           | `top` \\| `hanging` \\| `middle` \\| `alphabetic` \\| `ideographic` \\| `bottom' |      |\n| textOverflow        | 文本溢出处理方式   | `clip` \\| `ellipsis` \\| string                                              |      |\n| lineHeight          | 行高               | number \\| string                                                            |      |\n| letterSpacing       | 字间距             | number \\| string                                                            |      |\n| maxLines            | 最大行数           | number                                                                      |      |\n| textPath            | 文本路径           | Path                                                                        |      |\n| textPathSide        | 文本路径侧边       | `left` \\| `right`                                                           |      |\n| textPathStartOffset | 文本路径起始偏移   | number \\| string                                                            |      |\n| textDecorationLine  | 文本装饰线         | string                                                                      |      |\n| textDecorationColor | 文本装饰线颜色     | string                                                                      |      |\n| textDecorationStyle | 文本装饰线样式     | `solid` \\| `double` \\| `dotted` \\| `dashed` \\| `wavy`                       |      |\n| isBillboard         | 是否启用公告牌模式 | boolean                                                                     |      |\n| billboardRotation   | 公告牌旋转角度     | number                                                                      |      |\n| isSizeAttenuation   | 是否启用大小衰减   | boolean                                                                     |      |\n| wordWrap            | 是否自动换行       | boolean                                                                     |      |\n| wordWrapWidth       | 自动换行宽度       | number                                                                      |      |\n| dx                  | X 方向偏移         | number \\| string                                                            |      |\n| dy                  | Y 方向偏移         | number \\| string                                                            |      |\n\n**示例：**\n\n```js\nBaseShape.upsert(\n  // 指定图形 key，需要保证在同一个自定义元素类型中保持唯一性\n  'shape',\n  'text',\n  {\n    x: 100,\n    y: 100,\n    text: 'text',\n  },\n  container,\n);\n```\n\n多行文字显示：\n\n```js\n{\n  wordWrap: true,\n  wordWrapWidth: 100,\n  maxLines: 4,\n  textOverflow: 'ellipsis',\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/state.en.md",
    "content": "---\ntitle: Element State\norder: 2\n---\n\n## What is Element State\n\n<image width=\"500px\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*yVbORYybrDQAAAAAAAAAAAAADmJ7AQ/original\" />\n\nElement State refers to the visual representation of elements (nodes, edges, combos) in a graph under different interaction scenarios. For example, when a user clicks on a node, the node might enter a \"selected\" state and change color; when the mouse hovers over an edge, the edge might enter a \"highlight\" state and become bold.\n\n**Simply put, states allow elements to dynamically change their appearance based on user operations or business logic.**\n\n### Characteristics of States\n\n- **Multiple State Coexistence**: An element can have multiple states simultaneously, such as being both \"selected\" and \"highlighted\"\n- **Style Stacking**: Styles from multiple states are stacked together, with later-set state styles having higher priority\n- **Complete Customization**: Besides built-in states, you can create any custom states that meet your business requirements\n\n## Built-in State Types\n\nG6 provides some commonly used built-in states that you can use directly:\n\n| State Name  | Description     | Typical Use Cases                    |\n| ----------- | --------------- | ------------------------------------ |\n| `selected`  | Selected state  | When user clicks to select elements  |\n| `active`    | Active state    | Currently interacting element        |\n| `highlight` | Highlight state | Elements that need emphasis          |\n| `inactive`  | Inactive state  | Dimmed display of unfocused elements |\n| `disabled`   | Disabled state  | Non-interactive elements             |\n\n> 💡 **Tip**: These built-in states are not mandatory. You can completely define your own state names according to business requirements.\n\n## Configuring State Styles\n\n### Basic Configuration\n\nConfigure corresponding styles for different states when creating a graph instance:\n\n```javascript\nconst graph = new Graph({\n  // Node state style configuration\n  node: {\n    // Default style (style when no state is applied)\n    style: {\n      fill: '#C6E5FF',\n      stroke: '#5B8FF9',\n      lineWidth: 1,\n    },\n    // Styles for various states\n    state: {\n      selected: {\n        fill: '#95D6FB',\n        stroke: '#1890FF',\n        lineWidth: 2,\n        shadowColor: '#1890FF',\n        shadowBlur: 10,\n      },\n      highlight: {\n        stroke: '#FF6A00',\n        lineWidth: 2,\n      },\n      disabled: {\n        fill: '#ECECEC',\n        stroke: '#BFBFBF',\n        opacity: 0.5,\n      },\n    },\n  },\n\n  // Edge state style configuration\n  edge: {\n    style: {\n      stroke: '#E2E2E2',\n      lineWidth: 1,\n    },\n    state: {\n      selected: {\n        stroke: '#1890FF',\n        lineWidth: 2,\n      },\n      highlight: {\n        stroke: '#FF6A00',\n        lineWidth: 3,\n      },\n    },\n  },\n\n  // Combo state style configuration\n  combo: {\n    style: {\n      fill: '#F0F0F0',\n      stroke: '#D9D9D9',\n    },\n    state: {\n      selected: {\n        stroke: '#1890FF',\n        lineWidth: 2,\n      },\n    },\n  },\n});\n```\n\n### Custom States\n\nYou can create any custom states that meet your business requirements:\n\n```javascript\nconst graph = new Graph({\n  node: {\n    style: {\n      fill: '#C6E5FF',\n      stroke: '#5B8FF9',\n    },\n    state: {\n      // Custom state: error\n      error: {\n        fill: '#FFEBE6',\n        stroke: '#FF4D4F',\n        lineWidth: 2,\n        lineDash: [4, 4], // Dashed border\n      },\n      // Custom state: success\n      success: {\n        fill: '#F6FFED',\n        stroke: '#52C41A',\n        lineWidth: 2,\n      },\n      // Custom state: warning\n      warning: {\n        fill: '#FFFBE6',\n        stroke: '#FAAD14',\n        lineWidth: 2,\n        // Add icon\n        icon: {\n          show: true,\n          text: '⚠️',\n          fontSize: 16,\n        },\n      },\n    },\n  },\n});\n```\n\n## Setting Element States\n\n### Setting Initial States in Data\n\nSet initial states for elements in data:\n\n```javascript\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n      states: ['selected'], // Initially in selected state\n    },\n    {\n      id: 'node2',\n      states: ['disabled'], // Initially in disabled state\n    },\n    {\n      id: 'node3',\n      states: ['highlight', 'active'], // Initially has multiple states\n    },\n  ],\n  edges: [\n    {\n      source: 'node1',\n      target: 'node2',\n      states: ['highlight'], // Initial state of the edge\n    },\n  ],\n};\n\ngraph.setData(data);\n```\n\n### Dynamic State Setting\n\nDynamically change element states through API:\n\n```javascript\n// Set a single state for a single element\ngraph.setElementState('node1', 'selected');\n\n// Set multiple states for a single element\ngraph.setElementState('node2', ['highlight', 'active']);\n\n// Batch set states for multiple elements\ngraph.setElementState({\n  node1: ['selected'],\n  node2: ['highlight'],\n  edge1: ['active'],\n});\n```\n\n### State Stacking Effect\n\nWhen an element has multiple states, styles are stacked in order:\n\n```javascript\n// Assume a node has both selected and highlight states\ngraph.setElementState('node1', ['selected', 'highlight']);\n\n// Final style = default style + selected state style + highlight state style\n// If there are style conflicts, later state styles will override earlier ones\n```\n\n## Clearing Element States\n\n### Clear All States\n\nRestore elements to default state (no states):\n\n```javascript\n// Clear all states of a single element\ngraph.setElementState('node1', []);\n\n// Batch clear states of multiple elements\ngraph.setElementState({\n  node1: [],\n  node2: [],\n  edge1: [],\n});\n```\n\n### Clear Specific States\n\nIf an element has multiple states, you can clear only some of them:\n\n```javascript\n// Assume node1 currently has ['selected', 'highlight', 'active'] three states\n// Now only want to keep 'selected' state, clear other states\ngraph.setElementState('node1', ['selected']);\n\n// Or get current states, then filter out unwanted states\nconst currentStates = graph.getElementState('node1');\nconst newStates = currentStates.filter((state) => state !== 'highlight');\ngraph.setElementState('node1', newStates);\n```\n\n### Clear Specific States from All Elements\n\n```javascript\n// Clear 'highlight' state from all nodes\nconst allNodes = graph.getNodeData();\nconst stateUpdates = {};\n\nallNodes.forEach((node) => {\n  const currentStates = graph.getElementState(node.id);\n  const newStates = currentStates.filter((state) => state !== 'highlight');\n  stateUpdates[node.id] = newStates;\n});\n\ngraph.setElementState(stateUpdates);\n```\n\n## Querying Element States\n\n### Get Element States\n\n```javascript\n// Get all states of a specified element\nconst states = graph.getElementState('node1');\nconsole.log(states); // For example: ['selected', 'highlight']\n\n// If element has no states, returns empty array\nconsole.log(states); // []\n```\n\n### Find Elements with Specific States\n\n```javascript\n// Get all node data in 'selected' state\nconst selectedNodes = graph.getElementDataByState('node', 'selected');\n\n// Get all edge data in 'highlight' state\nconst highlightEdges = graph.getElementDataByState('edge', 'highlight');\n```\n\n### Check if Element is in Specific State\n\n```javascript\n// Check if element is in specific state\nconst states = graph.getElementState('node1');\nconst isSelected = states.includes('selected');\nconst isHighlight = states.includes('highlight');\n\nconsole.log('Is node selected:', isSelected);\nconsole.log('Is node highlighted:', isHighlight);\n```\n"
  },
  {
    "path": "packages/site/docs/manual/element/state.zh.md",
    "content": "---\ntitle: 元素状态\norder: 2\n---\n\n## 什么是元素状态\n\n<image width=\"500px\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*yVbORYybrDQAAAAAAAAAAAAADmJ7AQ/original\" />\n\n元素状态(State)是指图中元素（节点、边、组合）在不同交互场景下的视觉表现形式。比如当用户点击一个节点时，节点可能会变成\"选中\"状态并改变颜色；当鼠标悬停在边上时，边可能会进入\"高亮\"状态并加粗显示。\n\n**简单来说，状态让元素能够根据用户的操作或业务逻辑动态改变外观。**\n\n### 状态的特点\n\n- **多状态共存**：一个元素可以同时拥有多个状态，比如既是\"选中\"又是\"高亮\"\n- **样式叠加**：多个状态的样式会叠加在一起，后设置的状态样式优先级更高\n- **完全自定义**：除了内置状态，您可以创建任何符合业务需求的自定义状态\n\n## 内置状态类型\n\nG6 提供了一些常用的内置状态，您可以直接使用：\n\n| 状态名      | 说明       | 典型使用场景       |\n| ----------- | ---------- | ------------------ |\n| `selected`  | 选中状态   | 用户点击选择元素时 |\n| `active`    | 激活状态   | 当前正在交互的元素 |\n| `highlight` | 高亮状态   | 需要强调显示的元素 |\n| `inactive`  | 非活跃状态 | 淡化显示非关注元素 |\n| `disabled`   | 禁用状态   | 不可交互的元素     |\n\n> 💡 **提示**：这些内置状态并非必须使用，您完全可以根据业务需求定义自己的状态名称。\n\n## 配置状态样式\n\n### 基础配置\n\n在创建图实例时，为不同状态配置相应的样式：\n\n```javascript\nconst graph = new Graph({\n  // 节点的状态样式配置\n  node: {\n    // 默认样式（无状态时的样式）\n    style: {\n      fill: '#C6E5FF',\n      stroke: '#5B8FF9',\n      lineWidth: 1,\n    },\n    // 各种状态下的样式\n    state: {\n      selected: {\n        fill: '#95D6FB',\n        stroke: '#1890FF',\n        lineWidth: 2,\n        shadowColor: '#1890FF',\n        shadowBlur: 10,\n      },\n      highlight: {\n        stroke: '#FF6A00',\n        lineWidth: 2,\n      },\n      disabled: {\n        fill: '#ECECEC',\n        stroke: '#BFBFBF',\n        opacity: 0.5,\n      },\n    },\n  },\n\n  // 边的状态样式配置\n  edge: {\n    style: {\n      stroke: '#E2E2E2',\n      lineWidth: 1,\n    },\n    state: {\n      selected: {\n        stroke: '#1890FF',\n        lineWidth: 2,\n      },\n      highlight: {\n        stroke: '#FF6A00',\n        lineWidth: 3,\n      },\n    },\n  },\n\n  // 组合的状态样式配置\n  combo: {\n    style: {\n      fill: '#F0F0F0',\n      stroke: '#D9D9D9',\n    },\n    state: {\n      selected: {\n        stroke: '#1890FF',\n        lineWidth: 2,\n      },\n    },\n  },\n});\n```\n\n### 自定义状态\n\n您可以创建任何符合业务需求的自定义状态：\n\n```javascript\nconst graph = new Graph({\n  node: {\n    style: {\n      fill: '#C6E5FF',\n      stroke: '#5B8FF9',\n    },\n    state: {\n      // 自定义状态：错误\n      error: {\n        fill: '#FFEBE6',\n        stroke: '#FF4D4F',\n        lineWidth: 2,\n        lineDash: [4, 4], // 虚线边框\n      },\n      // 自定义状态：成功\n      success: {\n        fill: '#F6FFED',\n        stroke: '#52C41A',\n        lineWidth: 2,\n      },\n      // 自定义状态：警告\n      warning: {\n        fill: '#FFFBE6',\n        stroke: '#FAAD14',\n        lineWidth: 2,\n        // 添加图标\n        icon: {\n          show: true,\n          text: '⚠️',\n          fontSize: 16,\n        },\n      },\n    },\n  },\n});\n```\n\n## 设置元素状态\n\n### 数据中设置初始状态\n\n在数据中为元素设置初始状态：\n\n```javascript\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n      states: ['selected'], // 初始为选中状态\n    },\n    {\n      id: 'node2',\n      states: ['disabled'], // 初始为禁用状态\n    },\n    {\n      id: 'node3',\n      states: ['highlight', 'active'], // 初始有多个状态\n    },\n  ],\n  edges: [\n    {\n      source: 'node1',\n      target: 'node2',\n      states: ['highlight'], // 边的初始状态\n    },\n  ],\n};\n\ngraph.setData(data);\n```\n\n### 动态设置状态\n\n通过 API 动态改变元素状态：\n\n```javascript\n// 设置单个元素的单个状态\ngraph.setElementState('node1', 'selected');\n\n// 设置单个元素的多个状态\ngraph.setElementState('node2', ['highlight', 'active']);\n\n// 批量设置多个元素的状态\ngraph.setElementState({\n  node1: ['selected'],\n  node2: ['highlight'],\n  edge1: ['active'],\n});\n```\n\n### 状态的叠加效果\n\n当元素有多个状态时，样式会按顺序叠加：\n\n```javascript\n// 假设节点同时有 selected 和 highlight 状态\ngraph.setElementState('node1', ['selected', 'highlight']);\n\n// 最终样式 = 默认样式 + selected状态样式 + highlight状态样式\n// 如果有样式冲突，后面的状态样式会覆盖前面的\n```\n\n## 清除元素状态\n\n### 清除所有状态\n\n将元素恢复到默认状态（无任何状态）：\n\n```javascript\n// 清除单个元素的所有状态\ngraph.setElementState('node1', []);\n\n// 批量清除多个元素的状态\ngraph.setElementState({\n  node1: [],\n  node2: [],\n  edge1: [],\n});\n```\n\n### 清除特定状态\n\n如果元素有多个状态，您可以只清除其中的某些状态：\n\n```javascript\n// 假设 node1 当前有 ['selected', 'highlight', 'active'] 三个状态\n// 现在只想保留 'selected' 状态，清除其他状态\ngraph.setElementState('node1', ['selected']);\n\n// 或者获取当前状态，然后过滤掉不需要的状态\nconst currentStates = graph.getElementState('node1');\nconst newStates = currentStates.filter((state) => state !== 'highlight');\ngraph.setElementState('node1', newStates);\n```\n\n### 清除所有元素的特定状态\n\n```javascript\n// 清除所有节点的 'highlight' 状态\nconst allNodes = graph.getNodeData();\nconst stateUpdates = {};\n\nallNodes.forEach((node) => {\n  const currentStates = graph.getElementState(node.id);\n  const newStates = currentStates.filter((state) => state !== 'highlight');\n  stateUpdates[node.id] = newStates;\n});\n\ngraph.setElementState(stateUpdates);\n```\n\n## 查询元素状态\n\n### 获取元素状态\n\n```javascript\n// 获取指定元素的所有状态\nconst states = graph.getElementState('node1');\nconsole.log(states); // 例如：['selected', 'highlight']\n\n// 如果元素没有任何状态，返回空数组\nconsole.log(states); // []\n```\n\n### 查找特定状态的元素\n\n```javascript\n// 获取所有处于 'selected' 状态的节点数据\nconst selectedNodes = graph.getElementDataByState('node', 'selected');\n\n// 获取所有处于 'highlight' 状态的边数据\nconst highlightEdges = graph.getElementDataByState('edge', 'highlight');\n```\n\n### 判断元素是否处于某状态\n\n```javascript\n// 检查元素是否处于特定状态\nconst states = graph.getElementState('node1');\nconst isSelected = states.includes('selected');\nconst isHighlight = states.includes('highlight');\n\nconsole.log('节点是否选中:', isSelected);\nconsole.log('节点是否高亮:', isHighlight);\n```\n"
  },
  {
    "path": "packages/site/docs/manual/extension/3d.en.md",
    "content": "---\ntitle: g6-extension-3d\n---\n"
  },
  {
    "path": "packages/site/docs/manual/extension/3d.zh.md",
    "content": "---\ntitle: g6-extension-3d\n---\n"
  },
  {
    "path": "packages/site/docs/manual/faq.en.md",
    "content": "---\ntitle: FAQ\norder: 13\n---\n\n### What is the Difference Between Extension and Plugin?\n\n`Extension` is a concept in G6 that collectively refers to all types of registrable content, including elements, behaviors, layouts, and plugins, among others.\n\n`Plugin` represents a flexible extension mechanism provided by G6 and is a special type of `Extension`.\n\n### Set Text Overflow Ellipsis\n\nTaking `label` as an example, you can set `labelWordWrap` and `labelWordWrapWidth` to achieve text overflow ellipsis.\n\n```typescript {3-4}\n{\n  labelText: 'This is a long text',\n  labelWordWrap: true,\n  labelWordWrapWidth: 50,\n}\n```\n\n### Key Press Not Working\n\nSome plugins or behaviors support configuring key press triggers. Please use standard key names, such as `Control`, `Shift`, `Alt`, `Meta`, as well as letters, numbers, symbols, and so on.\n\n### Canvas Not Updating After Data Update\n\nEnsure that you call `graph.draw()` or `graph.render()` to update the canvas after updating the data.\n\n> G6 merges differences and updates the canvas uniformly after `draw` or `render` for multiple data updates to improve performance.\n\n### How to Resolve Interaction Conflicts\n\nWhen multiple interactions conflict with each other, you can set the enable timing of interactions to avoid multiple interactions being triggered simultaneously.\n\nTaking `drag-canvas` and `brush-select` as an example, if you directly configure these two interactions, dragging on the canvas will cause interaction exceptions. You can disable the `drag-canvas` interaction when the `shift` key is pressed.\n\n```typescript {4}\nbehaviors: [\n  {\n    type: 'drag-canvas',\n    enable: (event) => event.shiftKey === false,\n  },\n  {\n    type: 'brush-select',\n  },\n];\n```\n\nAt this point, when the `shift` key is pressed, the `drag-canvas` interaction will be disabled, and the `brush-select` interaction will not be affected.\n\n### Difference Between `draw` and `render`\n\nBoth `draw` and `render` execute drawing operations, but `render` additionally performs **layout** and **auto fit** operations based on `draw`.\n\nYou can simply understand it as: `render` = `draw` + `layout` + `fitView`/`fitCenter`.\n\n### Style in Data Not Effective\n\nReason 1: The style in the data is overridden by the style in the style mapping.\n\n```typescript {5}\n{\n  data: [{ id: 'node-1', style: { fill: 'orange' } }],\n  node: {\n    style: {\n      fill: 'pink', // No matter what the style in the data is, it will be overridden by the style here\n    }\n  }\n}\n```\n\nSolution: Use a callback method to prioritize obtaining styles from the data to improve data priority.\n\n```typescript {5}\n{\n  node: {\n    style: (data) => {\n      return {\n        fill: data.style?.fill || 'pink',\n      };\n    };\n  }\n}\n```\n\n### Residual content in the canvas\n\nWhen using the Canvas renderer for drawing, residual content may appear in the canvas, which is referred to as \"dirty rectangles\". This phenomenon occurs because the underlying rendering engine improves performance by only drawing the parts that have changed each time, rather than clearing the entire canvas.\n\nHowever, when the graphics in the canvas change, there may be cases where some graphics are not correctly cleared, resulting in residual content.\n\nYou can solve this issue by:\n\n1. Using the SVG or WebGL renderer;\n2. Checking whether there are illegal values in the element of the nodes, such as null, NaN, and so on;\n3. Using integers as much as possible for numeric style values, such as r, width, height, fontSize, and so on.\n\n### Use Vanilla JavaScript Object Data\n\nPlease avoid using Vue reactive data, Immer.js, and other wrapped objects as the data source for G6, as these objects will be deeply monitored internally, and even freeze the data object, causing G6 to fail to operate normally.\n\n### G6 project startup Warning during compilation Type mapping points to non-existent path\n\n```shell\nWARNING in ./node_modules/@antv/util/esm/path/util/segment-cubic-factory.js\nModule Warning (from ./node_modules/source-map-loader/dist/cjs.js):\nFailed to parse source map from '/Users/xxx/workspace/antv-g6-learn/node_modules/@antv/util/esm/path/util/src/path/util/segment-cubic-factory.ts' file: Error: ENOENT: no such file or directory, open '/Users/xxx/workspace/antv-g6-learn/node_modules/@antv/util/esm/path/util/src/path/util/segment-cubic-factory.ts'\n\nWARNING in ./node_modules/@antv/util/esm/path/util/segment-line-factory.js\nModule Warning (from ./node_modules/source-map-loader/dist/cjs.js):\nFailed to parse source map from '/Users/xxx/workspace/antv-g6-learn/node_modules/@antv/util/esm/path/util/src/path/util/segment-line-factory.ts' file: Error: ENOENT: no such file or directory, open '/Users/xxx/workspace/antv-g6-learn/node_modules/@antv/util/esm/path/util/src/path/util/segment-line-factory.ts'\n\nWARNING in ./node_modules/@antv/util/esm/path/util/segment-quad-factory.js\nModule Warning (from ./node_modules/source-map-loader/dist/cjs.js):\nFailed to parse source map from '/Users/xxx/workspace/antv-g6-learn/node_modules/@antv/util/esm/path/util/src/path/util/segment-quad-factory.ts' file: Error: ENOENT: no such file or directory, open '/Users/xxx/workspace/antv-g6-learn/node_modules/@antv/util/esm/path/util/src/path/util/segment-quad-factory.ts'\n```\n\n> Explanation: [@antv/util](https://github.com/antvis/util) is a tool library that AntV relies on at the bottom.\n\nFrom the partial warning message above, we can see that there is a problem with the type declaration file of the `@antv/util` tool library that G6 depends on.**This warning does not affect the normal operation of the project**.\n\nThis message will only appear in TypeScript projects. The following are ways to turn it off:\n\n1. Turn off TypeScript sourcemap\n\nCreate a `.env` file in the root directory of the project and add the following content:\n\ntext\nGENERATE_SOURCEMAP=false\ntext\n\n2. Disable sourcemapping for specific modules\n\nDisabling sourcemapping directly is too simple and crude, and is not user-friendly for developers who may have debugging needs. Therefore, sourcemapping can be disabled for specific modules by configuring the build tool separately.\n\na. webpack configuration\n\n```javascript\nmodule.exports = {\n  // ...其他配置\n  module: {\n    rules: [\n      {\n        test: /node_modules\\/@antv\\/util\\/esm\\/path\\/util\\/.+\\.js$/,\n        use: ['source-map-loader'],\n        enforce: 'pre',\n      },\n    ],\n  },\n  ignoreWarnings: [/Failed to parse source map/],\n};\n```\n\nb. vite configuration\n\n```javascript\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n  build: {\n    rollupOptions: {\n      onwarn(warning, warn) {\n        // Ignore warnings for specific modules\n        if (warning.code === 'MODULE_LEVEL_DIRECTIVE' && warning.message.includes('@antv/util')) {\n          return;\n        }\n        // For other warnings, use the default warning handling\n        warn(warning);\n      },\n    },\n  },\n});\n```\n\n### Manually configuring the color palette does not take effect\n\n> In v5, the built-in colors are: export type BuiltInPalette = 'spectral' | 'oranges' | 'greens' | 'blues';\n\nThe solution is as follows:\n\n```typescript {10}\nconst graph = new Graph({\n  container: '#ID',\n  width: number,\n  height: number,\n  data,\n  node: {\n    palette: {\n      field: 'color',\n      // right\n      color: ['red', 'green', 'blue'],\n\n      // error\n      // color: 'red'\n    },\n  },\n});\n```\n\n### grid-line plugin does not take effect\n\n> In v5, built-in plugins include `bubble-sets` `edge-filter-lens` `grid-line` `background` `contextmenu` `fisheye` `fullscreen` `history` `hull` `legend` `minimap` `snapline` `timebar` `toolbar` `tooltip` >`watermark`. [Detailed reference](https://github.com/antvis/G6/blob/6e2355020c20b3a1e2e5ca0e0ee97aeb81f932b3/packages/g6/src/registry/build-in.ts#L189)\n\nActual reason: The parent container of the `graph` instance, `<div ref={containerRef} />`, does not have a height set, so the G6 Graph may not be able to calculate the correct size.**If you want to enable the `grid-line` canvas plugin, you need to set the width and height of the parent element, which is not valid in the graph configuration**.\n\n### v5 cannot use the tree layout\n\nUse `new Graph({xxx})` uniformly.\n\n> In v5, built-in layouts include `antv-dagre` `combo-combined` `compact-box` `force-atlas2` `circular` `concentric` `d3-force` `dagre` `dendrogram` `force` `fruchterman` `grid` `indented` `mds` `mindmap` `radial` `random`. [Specific reference](https://github.com/antvis/G6/blob/6e2355020c20b3a1e2e5ca0e0ee97aeb81f932b3/packages/g6/src/registry/build-in.ts#L147)\n\nv5 merges the graph and tree graph, no longer creates the tree graph layout by instantiating `G6.TreeGraph`, and removes this method. For details, see [Features - Merge Graph and Tree Graph](https://g6.antv.antgroup.com/manual/feature#-%E5%90%88%E5%B9%B6%E5%9B%BE%E4%B8%8E%E6%A0%91%E5%9B%BE)\n\n### edge does not have a connection at the center of the node\n\nis configured [portLinkToCenter](https://g6.antv.antgroup.com/api/elements/nodes/base-node#portlinktocenter) to `true`.\n\n```typescript {6}\nconst graph = new Graph({\n  container: xxx,\n  node: {\n    type: 'rect',\n    style: {\n      portLinkToCenter: true,\n    },\n  },\n  edge: {\n    type: 'xxx',\n  },\n});\n```\n\n### How to dynamically set the width of a node based on the length of the label content\n\nImplementation solution: [#6347](https://github.com/antvis/G6/pull/6347#issuecomment-2357515570)\n\n```typescript\nconst measureTextWidth = memoize(\n  (text: string, font: any = {}): TextMetrics => {\n    const { fontSize, fontFamily = 'sans-serif', fontWeight, fontStyle, fontVariant } = font;\n    const ctx = getCanvasContext();\n    // @see https://developer.mozilla.org/zh-CN/docs/Web/CSS/font\n    ctx.font = [fontStyle, fontWeight, fontVariant, `${fontSize}px`, fontFamily].join(' ');\n    return ctx.measureText(isString(text) ? text : '').width;\n  },\n  (text: string, font = {}) => [text, ...values(font)].join(''),\n);\n\nconst graph = new G6.Graph({\n    node: {\n          style: { size: d => [measureTextWidth(d.label, {...}) , xxx] },\n    }\n})\n```\n\n### NodeEvent node event object type is not complete\n\nYou can manually specify the `IPointerEvent` type. For details, see [#6346](https://github.com/antvis/G6/issues/6346)\n\n```typescript {4}\nimport { NodeEvent } from '@antv/g6';\nimport type { IPointerEvent } from '@antv/g6';\n\ngraph.on(NodeEvent.CLICK, (event: IPointerEvent) => {\n  // handler\n});\n```\n\n### Remove the parent combo of the node\n\nUpdate the node data, set the `combo` value to `null`.\n\n```typescript\ngraph.updateNodeData([{ id: 'node-id', combo: null }]);\n```\n"
  },
  {
    "path": "packages/site/docs/manual/faq.zh.md",
    "content": "---\ntitle: 常见问题\norder: 13\n---\n\n### Extension 和 Plugin 有什么区别？\n\n`Extension` 是 G6 中的一个概念，是所有可注册内容的统称，包含元素、交互、布局、插件等。\n\n`Plugin` 是 G6 提供的灵活扩展机制，是一种特殊的 `Extension`。\n\n### 设置文本超出省略\n\n以 label 为例，设置 `labelWordWrap` 和 `labelWordWrapWidth` 即可实现文本超出省略。\n\n```typescript {3-4}\n{\n  labelText: 'This is a long text',\n  labelWordWrap: true,\n  labelWordWrapWidth: 50,\n}\n```\n\n### 按键不生效\n\n一些插件或交互支持配置触发按键，请使用标准按键名：如 `Control`, `Shift`, `Alt`, `Meta`，以及字母、数字、符号等。\n\n### 更新数据后画布不更新\n\n请确保数据更新后调用 `graph.draw()` 或者 `graph.render()` 更新画布。\n\n> G6 对于多次数据更新，会在 `draw` 或 `render` 后合并差异并统一更新画布，以提高性能。\n\n### 交互有冲突如何解决\n\n当多个交互之间存在冲突时，你可以设置交互的启用时机来避免多个交互被同时触发。\n\n以 `drag-canvas` 和 `brush-select` 为例，如果直接配置这两个交互，当指针在画布上进行拖拽时，会导致交互异常。可以设置为在按下 `shift` 键时禁用 `drag-canvas` 交互。\n\n```typescript {4}\nbehaviors: [\n  {\n    type: 'drag-canvas',\n    enable: (event) => event.shiftKey === false,\n  },\n  {\n    type: 'brush-select',\n  },\n];\n```\n\n此时，当按下 `shift` 键时，`drag-canvas` 交互会被禁用，`brush-select` 交互会不会受到影响。\n\n### draw 和 render 的区别\n\n`draw` 和 `render` 都会执行绘制操作，但 `render` 会在 `draw` 的基础上额外进行**布局**、**视图自适应**操作。\n\n可以简单理解为：`render` = `draw` + `layout` + `fitView`/`fitCenter`。\n\n### 数据中的样式不生效\n\n原因一：被样式映射中的样式覆盖\n\n```typescript {5}\n{\n  data: [{ id: 'node-1', style: { fill: 'orange' } }],\n  node: {\n    style: {\n      fill: 'pink', // 无论数据中的样式如何，都会被这里的样式覆盖\n    }\n  }\n}\n```\n\n解决方式：使用回调方法，优先从数据中获取样式以提高数据优先级\n\n```typescript {5}\n{\n  node: {\n    style: (data) => {\n      return {\n        fill: data.style?.fill || 'pink',\n      };\n    };\n  }\n}\n```\n\n### 画布中出现残影\n\n在使用 Canvas 渲染器进行绘制时，可能会出现残影现象，这些图形被称为“脏矩形”。该现象出现的原因是底层渲染引擎为了提高性能，每次绘制时只会绘制发生变化的部分，而不会清空整个画布。\n\n但是，当画布中的图形发生变化时，可能会出现部分图形未被正确清除的情况，从而导致残影现象。\n\n可以通过以下方式解决：\n\n1. 使用 SVG 或 WebGL 渲染器；\n2. 检查节点中的图形样式中是否存在非法值，例如 null、NaN 等；\n3. 尽量使用整数作为数值型的样式值，例如 r、width、height、fontSize 等；\n\n### 使用原生 JavaScript 对象数据\n\n请避免使用 Vue 响应式数据、Immer.js 等包装过的对象作为 G6 的数据源，因为这些对象会在内部进行深度监听，甚至冻结数据对象，导致 G6 无法正常操作数据。\n\n### G6 项目启动在编译时警告类型映射指向不存在路径\n\n```shell\nWARNING in ./node_modules/@antv/util/esm/path/util/segment-cubic-factory.js\nModule Warning (from ./node_modules/source-map-loader/dist/cjs.js):\nFailed to parse source map from '/Users/xxx/workspace/antv-g6-learn/node_modules/@antv/util/esm/path/util/src/path/util/segment-cubic-factory.ts' file: Error: ENOENT: no such file or directory, open '/Users/xxx/workspace/antv-g6-learn/node_modules/@antv/util/esm/path/util/src/path/util/segment-cubic-factory.ts'\n\nWARNING in ./node_modules/@antv/util/esm/path/util/segment-line-factory.js\nModule Warning (from ./node_modules/source-map-loader/dist/cjs.js):\nFailed to parse source map from '/Users/xxx/workspace/antv-g6-learn/node_modules/@antv/util/esm/path/util/src/path/util/segment-line-factory.ts' file: Error: ENOENT: no such file or directory, open '/Users/xxx/workspace/antv-g6-learn/node_modules/@antv/util/esm/path/util/src/path/util/segment-line-factory.ts'\n\nWARNING in ./node_modules/@antv/util/esm/path/util/segment-quad-factory.js\nModule Warning (from ./node_modules/source-map-loader/dist/cjs.js):\nFailed to parse source map from '/Users/xxx/workspace/antv-g6-learn/node_modules/@antv/util/esm/path/util/src/path/util/segment-quad-factory.ts' file: Error: ENOENT: no such file or directory, open '/Users/xxx/workspace/antv-g6-learn/node_modules/@antv/util/esm/path/util/src/path/util/segment-quad-factory.ts'\n```\n\n> 解释: [@antv/util](https://github.com/antvis/util) 是 AntV 底层依赖的工具库。\n\n从上面部分警告信息中我们可以得知是 G6 依赖的 `@antv/util` 工具库的类型声明文件存在问题，**该警告不影响项目正常运行**。\n\n该信息只会在 TypeScript 项目中出现，关闭办法如下:\n\n1. 关闭TypeScript 的sourcemap源码映射\n\n在项目根目录下创建`.env`文件，并添加以下内容:\n\n```text\nGENERATE_SOURCEMAP=false\n```\n\n2. 单独禁用指定模块的源码映射\n\n直接禁用sourcemap映射的方式过于简单粗暴，对于部分可能有调试需求的开发者不太友好，所以也可以通过在构建工具单独配置，单独禁用这些特定模块的源码映射。\n\na. webpack配置\n\n```javascript\nmodule.exports = {\n  // ...其他配置\n  module: {\n    rules: [\n      {\n        test: /node_modules\\/@antv\\/util\\/esm\\/path\\/util\\/.+\\.js$/,\n        use: ['source-map-loader'],\n        enforce: 'pre',\n      },\n    ],\n  },\n  ignoreWarnings: [/Failed to parse source map/],\n};\n```\n\nb. vite配置\n\n```javascript\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n  build: {\n    rollupOptions: {\n      onwarn(warning, warn) {\n        // 忽略特定模块的警告\n        if (warning.code === 'MODULE_LEVEL_DIRECTIVE' && warning.message.includes('@antv/util')) {\n          return;\n        }\n        // 对于其他警告,使用默认的警告处理\n        warn(warning);\n      },\n    },\n  },\n});\n```\n\n### 手动配置色板颜色不生效\n\n> 在 v5 中内置颜色有：export type BuiltInPalette = 'spectral' | 'oranges' | 'greens' | 'blues';\n\n解决办法如下:\n\n```typescript {10}\nconst graph = new Graph({\n  container: '#ID',\n  width: number,\n  height: number,\n  data,\n  node: {\n    palette: {\n      field: 'color',\n      // right\n      color: ['red', 'green', 'blue'],\n\n      // error\n      // color: 'red'\n    },\n  },\n});\n```\n\n### grid-line 插件不生效\n\n> 在 v5 中内置插件有`bubble-sets` `edge-filter-lens` `grid-line` `background` `contextmenu` `fisheye` `fullscreen` `history` `hull` `legend` `minimap` `snapline` `timebar` `toolbar` `tooltip` >`watermark`. [具体参考](https://github.com/antvis/G6/blob/6e2355020c20b3a1e2e5ca0e0ee97aeb81f932b3/packages/g6/src/registry/build-in.ts#L189)\n\n实际原因: `graph`实例的父容器`<div ref={containerRef} />`本身没有设置高度，G6 Graph图 可能无法正确计算出合适的大小。**如果要启用`grid-line`画布插件，需要给父元素 div 设置宽高，在 graph 配置中是无效的**。\n\n### v5无法使用树图布局\n\n统一使用`new Graph({xxx})`。\n\n> 在 v5 中内置布局有`antv-dagre` `combo-combined` `compact-box` `force-atlas2` `circular` `concentric` `d3-force` `dagre` `dendrogram` `force` `fruchterman` `grid` `indented` `mds` `mindmap` `radial` `random`. [具体参考](https://github.com/antvis/G6/blob/6e2355020c20b3a1e2e5ca0e0ee97aeb81f932b3/packages/g6/src/registry/build-in.ts#L147)\n\nv5合并了图和树图，不再通过实例化`G6.TreeGraph`创建树图布局，并且移除该方式。具体参考[特性-合并图与树图](/manual/whats-new/feature#🌲-合并图与树图)\n\n### edge 没有连接在 node 的边缘中心\n\n配置[portLinkToCenter](https://g6.antv.antgroup.com/api/elements/nodes/base-node#portlinktocenter)为 `true`。\n\n```typescript {6}\nconst graph = new Graph({\n  container: xxx,\n  node: {\n    type: 'rect',\n    style: {\n      portLinkToCenter: true,\n    },\n  },\n  edge: {\n    type: 'xxx',\n  },\n});\n```\n\n### 如何根据label内容长度动态设置node宽度\n\n参考方案：[#6347](https://github.com/antvis/G6/pull/6347#issuecomment-2357515570)\n\n```typescript\nconst measureTextWidth = memoize(\n  (text: string, font: any = {}): TextMetrics => {\n    const { fontSize, fontFamily = 'sans-serif', fontWeight, fontStyle, fontVariant } = font;\n    const ctx = getCanvasContext();\n    // @see https://developer.mozilla.org/zh-CN/docs/Web/CSS/font\n    ctx.font = [fontStyle, fontWeight, fontVariant, `${fontSize}px`, fontFamily].join(' ');\n    return ctx.measureText(isString(text) ? text : '').width;\n  },\n  (text: string, font = {}) => [text, ...values(font)].join(''),\n);\n\nconst graph = new G6.Graph({\n    node: {\n          style: { size: d => [measureTextWidth(d.label, {...}) , xxx] },\n    }\n})\n```\n\n### NodeEvent节点事件对象类型不齐全问题\n\n可以手动指定`IPointerEvent`类型。具体参考[#6346](https://github.com/antvis/G6/issues/6346)\n\n```typescript {4}\nimport { NodeEvent } from '@antv/g6';\nimport type { IPointerEvent } from '@antv/g6';\n\ngraph.on(NodeEvent.CLICK, (event: IPointerEvent) => {\n  // handler\n});\n```\n\n### 解除节点所在组合\n\n更新节点数据，`combo` 值设置为 `null`。\n\n```typescript\ngraph.updateNodeData([{ id: 'node-id', combo: null }]);\n```\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/3d.en.md",
    "content": "---\ntitle: Use 3D\norder: 5\n---\n\n## Install Dependencies\n\nBefore using 3D capabilities, please install the 3D extension package first:\n\n```bash\nnpm install @antv/g6-extension-3d --save\n```\n\nThe extension package exports the following:\n\n- renderer: 3D renderer\n\n**Elements**\n\n- Capsule: capsule node\n- Cone: cone node\n- Cube: cube node\n- Cylinder: cylinder node\n- Sphere: sphere node\n- Torus: torus node\n- Line3D: 3D line\n\n**Layout**\n\n- D3Force3DLayout: 3D force-directed layout\n\n**Behaviors**\n\n- DragCanvas3D: drag canvas\n- ObserveCanvas3D: observe canvas\n- RollCanvas3D: rotate canvas\n- ZoomCanvas3D: zoom canvas\n\n**Plugin**\n\n- Light: light source\n\n## Register Extensions\n\nThe following extensions are required:\n\n- renderer\n- at least one 3D node\n- Line3D\n- Light\n\n> The renderer does not need to be registered, it can be passed in during the instantiation of Graph.\n\nRegister as follows:\n\n```ts\nimport { register, ExtensionCategory } from '@antv/g6';\nimport { Sphere, Line3D, Light } from '@antv/g6-extension-3d';\n\nregister(ExtensionCategory.NODE, 'sphere', Sphere);\nregister(ExtensionCategory.EDGE, 'line3d', Line3D);\nregister(ExtensionCategory.PLUGIN, 'light', Light);\n```\n\n## Create 3D Graph\n\nAfter completing the above steps, you can create a 3D graph:\n\n```ts\nimport { Graph } from '@antv/g6';\nimport { renderer } from '@antv/g6-extension-3d';\n\nconst graph = new Graph({\n  // ... other options\n  // use 3d renderer\n  renderer,\n  node: {\n    type: 'sphere', // use 3d node\n    style: {\n      materialType: 'phong', // use Phong material\n    },\n  },\n  edge: {\n    type: 'line3d', // use 3D edge\n  },\n  plugins: [\n    {\n      type: 'light', // Add light source\n      // configure directional light\n      directional: {\n        direction: [0, 0, 1],\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\nYou can also refer to:\n\n- [Lite Solar System](/examples/feature/default/#lite-solar-system)\n- [3D Force Layout](/examples/layout/force-directed/#3d-force)\n\n## Tips\n\n`@antv/g6` has a built-in registered plugin for CameraSetting for camera configuration, refer to [plugin](https://github.com/antvis/G6/blob/v5/packages/g6/src/plugins/camera-setting.ts).\n\n```typescript\n{\n  plugins: [\n    {\n      type: 'camera-setting',\n      projectionMode: 'perspective',\n      near: 0.1,\n      far: 1000,\n      fov: 45,\n      aspect: 1,\n    },\n  ];\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/3d.zh.md",
    "content": "---\ntitle: 使用 3D\norder: 5\n---\n\n## 安装依赖\n\n在使用 3D 能力之前，请首先安装 3D 扩展包：\n\n```bash\nnpm install @antv/g6-extension-3d --save\n```\n\n该扩展包导出了以下内容：\n\n- renderer：3D 渲染器\n\n**元素**\n\n- Capsule：胶囊节点\n- Cone：圆锥节点\n- Cube：立方体节点\n- Cylinder：圆柱节点\n- Sphere：球体节点\n- Torus：圆环节点\n- Line3D：3D 线\n\n**布局**\n\n- D3Force3DLayout：3D 力导向布局\n\n**交互**\n\n- DragCanvas3D：拖拽画布\n- ObserveCanvas3D：观察画布\n- RollCanvas3D：旋转画布\n- ZoomCanvas3D：缩放画布\n\n**插件**\n\n- Light：光源\n\n## 注册扩展\n\n其中下列扩展是必须的：\n\n- renderer\n- 至少一个 3D 节点\n- Line3D\n- Light\n\n> renderer 无需注册，实例化 Graph 过程中传入即可。\n\n使用如下方式进行注册：\n\n```ts\nimport { register, ExtensionCategory } from '@antv/g6';\nimport { Sphere, Line3D, Light } from '@antv/g6-extension-3d';\n\nregister(ExtensionCategory.NODE, 'sphere', Sphere);\nregister(ExtensionCategory.EDGE, 'line3d', Line3D);\nregister(ExtensionCategory.PLUGIN, 'light', Light);\n```\n\n## 创建 3D 图\n\n完成上述步骤后，即可创建 3D 图：\n\n```ts\nimport { Graph } from '@antv/g6';\nimport { renderer } from '@antv/g6-extension-3d';\n\nconst graph = new Graph({\n  // ... 其他配置\n  // 使用 3D 渲染器\n  renderer,\n  node: {\n    type: 'sphere', // 使用 3D 节点\n    style: {\n      materialType: 'phong', // 使用 Phong 材质\n    },\n  },\n  edge: {\n    type: 'line3d', // 使用 3D 边\n  },\n  plugins: [\n    {\n      type: 'light', // 添加光源\n      // 配置方向光\n      directional: {\n        direction: [0, 0, 1],\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n你可以参考以下示例：\n\n- [精简太阳系](/examples/feature/default/#lite-solar-system)\n- [3D 力导向布局](/examples/layout/force-directed/#3d-force)\n\n## 其他\n\n`@antv/g6` 中内置注册了 `CameraSetting` 插件，可用于配置相机，具体可参考[插件](https://github.com/antvis/G6/blob/v5/packages/g6/src/plugins/camera-setting.ts)。\n\n```typescript\n{\n  plugins: [\n    {\n      type: 'camera-setting',\n      projectionMode: 'perspective',\n      near: 0.1,\n      far: 1000,\n      fov: 45,\n      aspect: 1,\n    },\n  ];\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/bundle.en.md",
    "content": "---\ntitle: Bundle Project\norder: 6\n---\n\nIn general, if you are using modern build tools such as Webpack, Rollup, or ESBuild, you can easily build projects that depend on `@antv/g6`.\n\nSome build tools, such as Vite, use ESBuild as the underlying tool, so you can refer to ESBuild's configuration.\n\nBelow are some example configurations that you can refer to when building your project. If you find that these configurations do not work properly, make sure that your build tool version is up to date.\n\n## Bundle Project with Webpack\n\n1. Make sure that `webpack` and `webpack-cli` are installed in your project:\n\n```bash\nnpm install webpack webpack-cli --save-dev\n```\n\n2. Refer to the following `webpack.config.js` for configuration:\n\n```js\nconst path = require('path');\n\nmodule.exports = {\n  entry: './src/index.ts',\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: 'index.js',\n  },\n  mode: 'production',\n};\n```\n\n3. Run the build command:\n\n```bash\nnpx webpack\n```\n\n> The above configuration works with `\"webpack\": \"^5.94.0\"`, `\"webpack-cli\": \"^5.1.4\"`.\n\n:::error{title=Webpack4}\n⚠️ It is strongly recommended that projects use Webpack 5. If you are using Webpack 4, follow the steps below to configure:\n\n1. Install the necessary dependencies: `babel-loader` (<9), `@babel/preset-env`, `@open-wc/webpack-import-meta-loader`\n\n> If you are using TypeScript, you also need to install `ts-loader`.\n\n```bash\nnpm install babel-loader@8 @babel/preset-env @open-wc/webpack-import-meta-loader --save-dev\n```\n\n2. Modify the `webpack.config.js` configuration:\n\n<embed src=\"@/common/manual/feature/webpack4.md\"></embed>\n:::\n> This configuration is for a standard webpack project. If you are using vue-cli, you should REMOVE the `mode: 'production'` option as it is unnecessary.\n\n## Bundle Project with Rollup\n\n1. First, make sure that `rollup` and the necessary plugins are installed in your project:\n\n- `@rollup/plugin-commonjs`: Used to load CommonJS modules\n- `@rollup/plugin-node-resolve`: Used to load Node.js modules\n\n```bash\nnpm install rollup @rollup/plugin-commonjs @rollup/plugin-node-resolve --save-dev\n```\n\n2. Refer to the following `rollup.config.js` for configuration:\n\n```js\nconst commonjs = require('@rollup/plugin-commonjs');\nconst resolve = require('@rollup/plugin-node-resolve');\n\nmodule.exports = {\n  input: 'src/index.ts',\n  output: {\n    file: 'dist/index.js',\n    format: 'umd',\n    name: 'project',\n  },\n  plugins: [resolve(), commonjs()],\n};\n```\n\n3. Run the build command:\n\n```bash\nnpx rollup -c\n```\n\n## Bundle Project with ESBuild\n\n1. First, make sure that `esbuild` is installed in your project:\n\n```bash\nnpm install esbuild --save-dev\n```\n\n2. Run the build command:\n\n```bash\nnpx esbuild src/index.ts --bundle --outfile=dist/index.js\n```\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/bundle.zh.md",
    "content": "---\ntitle: 项目打包\norder: 6\n---\n\n通常情况下，如果你使用的现代构建工具，如 Webpack、Rollup 或 ESBuild，你可以很容易地构建依赖于 `@antv/g6` 的项目。\n\n一些构建工具例如 Vite，其底层是使用 ESBuild，因此你可以参考 ESBuild 的配置。\n\n下面是一些示例配置，你可以参考这些配置来构建你的项目。如果你发现这些配置无法正常工作，请确保你的构建工具版本较新。\n\n## 使用 Webpack 打包项目\n\n1. 确保你的项目中已经安装了 `webpack` 和 `webpack-cli`：\n\n```bash\nnpm install webpack webpack-cli --save-dev\n```\n\n2. 参考下面的 `webpack.config.js` 进行配置：\n\n```js\nconst path = require('path');\n\nmodule.exports = {\n  entry: './src/index.ts',\n  output: {\n    path: path.resolve(__dirname, 'dist'),\n    filename: 'index.js',\n  },\n  mode: 'production',\n};\n```\n\n3. 执行构建命令：\n\n```bash\nnpx webpack\n```\n\n> 上述配置在：\"webpack\": \"^5.94.0\"，\"webpack-cli\": \"^5.1.4\" 可以正常工作。\n\n:::error{title=Webpack4}\n⚠️ 强烈建议项目使用 Webpack 5，如果你使用的是 Webpack 4，按以下步骤配置：\n\n1. 安装相关依赖：babel-loader(<9)、@babel/preset-env、@open-wc/webpack-import-meta-loader\n\n> 如果你使用的 typescript，还需要安装 ts-loader\n\n```bash\nnpm install babel-loader@8 @babel/preset-env @open-wc/webpack-import-meta-loader --save-dev\n```\n\n2. 修改 `webpack.config.js` 配置：\n\n<embed src=\"@/common/manual/feature/webpack4.md\"></embed>\n:::\n> 如果你使用的是 vue-cli，请移除 mode: 'production' 配置，否则可能会影响开发模式下的构建性能。\n\n## 使用 Rollup 打包项目\n\n1. 首先，确保你的项目中已经安装了 `rollup` 及必要的插件：\n\n- `@rollup/plugin-commonjs`：用于加载 CommonJS 模块\n- `@rollup/plugin-node-resolve`：用于加载 Node.js 模块\n\n```bash\nnpm install rollup @rollup/plugin-commonjs @rollup/plugin-node-resolve --save-dev\n```\n\n2. 参考下面的 `rollup.config.js` 进行配置：\n\n```js\nconst commonjs = require('@rollup/plugin-commonjs');\nconst resolve = require('@rollup/plugin-node-resolve');\n\nmodule.exports = {\n  input: 'src/index.ts',\n  output: {\n    file: 'dist/index.js',\n    format: 'umd',\n    name: 'project',\n  },\n  plugins: [resolve(), commonjs()],\n};\n```\n\n3. 执行构建命令：\n\n```bash\nnpx rollup -c\n```\n\n## 使用 ESBuild 打包项目\n\n1. 首先，确保你的项目中已经安装了 `esbuild`：\n\n```bash\nnpm install esbuild --save-dev\n```\n\n2. 执行构建命令：\n\n```bash\nnpx esbuild src/index.ts --bundle --outfile=dist/index.js\n```\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/coordinate.en.md",
    "content": "---\ntitle: coordinate\norder: 2\n---\n\n## Overview\n\nThere are three coordinate systems in G6 5.0: Canvas, Viewport, and Client.\n\n### Canvas Coordinate System\n\nThe coordinate system used when drawing G6 elements is not affected by camera zooming or panning. To change the position of an element, you need to directly modify the element's position properties (x/y/z).\n\nThe canvas space is theoretically infinite. In the initial state (no panning, zoom ratio is 1), the origin of the canvas coordinate system is located at the upper-left corner of the viewport.\n\n### Viewport Coordinate System\n\nThe viewport coordinate system is the projection of the camera coordinate system. When the camera pans or zooms, the position of elements in the canvas will also change in the viewport coordinate system.\n\nThe size of the viewport is the size of the canvas DOM container. The origin of the viewport coordinate system is located at the upper-left corner of the viewport, with the x-axis pointing to the right and the y-axis pointing down.\n\n![viewport](https://developer.mozilla.org/en-US/Web/API/Canvas_API/Tutorial/Drawing_shapes/canvas_default_grid.png)\n\n### Client Coordinate System\n\nThe client coordinate system has the browser's upper-left corner as the origin, with the x-axis pointing to the right and the y-axis pointing down.\n\nThe following figure describes the relationship between the viewport coordinate system and the client coordinate system:\n\n<img width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*HOcfToHFDIYAAAAAAAAAAAAADmJ7AQ/original\" />\n\n## Coordinate System Conversion\n\nG6 provides methods for coordinate system conversion, making it easy to convert between different coordinate systems.\n\n- Canvas coordinate system to viewport coordinate system: [getViewportByCanvas](/en/api/coordinate#graphgetviewportbycanvaspoint)\n- Client coordinate system to canvas coordinate system: [getCanvasByClient](/en/api/coordinate#graphgetcanvasbyclientpoint)\n- Viewport coordinate system to canvas coordinate system: [getCanvasByViewport](/en/api/coordinate#graphgetcanvasbyviewportpoint)\n- Canvas coordinate system to client coordinate system: [getClientByCanvas](/en/api/coordinate#graphgetclientbycanvaspoint)\n\nOther related APIs are also provided:\n\n- Get the viewport center in viewport coordinates: [getCanvasCenter](/en/api/viewport#graphgetcanvascenter)\n- Get the viewport center in canvas coordinates: [getViewportCenter](/en/api/viewport#graphgetviewportcenter)\n- Get the position of the graph origin in the viewport coordinate system: [getPosition](/en/api/viewport#graphgetposition)\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/coordinate.zh.md",
    "content": "---\ntitle: 坐标系\norder: 2\n---\n\n## 概述\n\n在 G6 5.0 中主要会涉及三个坐标系：画布坐标系(Canvas)、视口坐标系(Viewport)和浏览器坐标系(Client)。\n\n### 画布坐标系\n\nG6 元素绘制时所使用的坐标系，其不受相机缩放、平移的影响，要改变一个元素的位置，需要直接修改元素的位置属性(x/y/z)。\n\n画布空间理论上是无限大的，在初始状态下（无平移、缩放倍率为1），画布坐标系的原点位于视口左上角位置。\n\n### 视口坐标系\n\n视口坐标系是相机坐标系的投影，当相机发生平移、缩放时，画布中元素位置在视口坐标系中的位置也会发生变化。\n\n视口的大小即画布 DOM 容器的大小，视口坐标系的原点位于视口左上角位置，x 轴正方向向右，y 轴正方向向下。\n\n![viewport](https://developer.mozilla.org/en-US/Web/API/Canvas_API/Tutorial/Drawing_shapes/canvas_default_grid.png)\n\n### 浏览器坐标系\n\n浏览器坐标系以浏览器左上角为原点，x 轴正方向向右，y 轴正方向向下。\n\n下图描述了视口坐标系和浏览器坐标系之间的关系：\n\n<img width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*HOcfToHFDIYAAAAAAAAAAAAADmJ7AQ/original\" />\n\n## 坐标系转换\n\nG6 提供了坐标系转换的方法，可以方便地在不同坐标系之间进行转换。\n\n- 画布坐标系转视口坐标系：[getViewportByCanvas](/api/coordinate#graphgetviewportbycanvaspoint)\n- 浏览器坐标系转画布坐标系：[getCanvasByClient](/api/coordinate#graphgetcanvasbyclientpoint)\n- 视口坐标系转画布坐标系：[getCanvasByViewport](/api/coordinate#graphgetcanvasbyviewportpoint)\n- 画布坐标系转浏览器坐标系：[getClientByCanvas](/api/coordinate#graphgetclientbycanvaspoint)\n\n另外还提供了其他相关 API：\n\n- 获取视口中心的视口坐标：[getCanvasCenter](/api/viewport#graphgetcanvascenter)\n- 获取视口中心的画布坐标：[getViewportCenter](/api/viewport#graphgetviewportcenter)\n- 获取图原点在视口坐标系中的位置：[getPosition](/api/viewport#graphgetposition)\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/download-image.en.md",
    "content": "---\ntitle: download-image\norder: 3\n---\n\nG6 5.0 only provides an API for exporting the canvas as a Base64 image ([toDataURL](/en/api/export-image#graphtodataurloptions)). If you need to download the image, you can use the following method:\n\n```typescript\nasync function downloadImage() {\n  const dataURL = await graph.toDataURL();\n  const [head, content] = dataURL.split(',');\n  const contentType = head.match(/:(.*?);/)![1];\n\n  const bstr = atob(content);\n  let length = bstr.length;\n  const u8arr = new Uint8Array(length);\n\n  while (length--) {\n    u8arr[length] = bstr.charCodeAt(length);\n  }\n\n  const blob = new Blob([u8arr], { type: contentType });\n\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement('a');\n  a.href = url;\n  a.download = 'graph.png';\n  a.click();\n}\n```\n\n:::warning{title=note}\nThe exported image may not contain the complete canvas content; the export range only includes the content within the Graph canvas. Some plugins use custom containers, canvases, etc., and this content will not appear in the exported image.\n:::\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/download-image.zh.md",
    "content": "---\ntitle: 下载图片\norder: 3\n---\n\nG6 5.0 仅提供导出画布为 Base64 图片的 API([toDataURL](/api/export-image#graphtodataurloptions))，如果需要下载图片，可以使用以下方法：\n\n```typescript\nasync function downloadImage() {\n  const dataURL = await graph.toDataURL();\n  const [head, content] = dataURL.split(',');\n  const contentType = head.match(/:(.*?);/)![1];\n\n  const bstr = atob(content);\n  let length = bstr.length;\n  const u8arr = new Uint8Array(length);\n\n  while (length--) {\n    u8arr[length] = bstr.charCodeAt(length);\n  }\n\n  const blob = new Blob([u8arr], { type: contentType });\n\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement('a');\n  a.href = url;\n  a.download = 'graph.png';\n  a.click();\n}\n```\n\n:::warning{title=注意}\n导出的图片内容可能不会包含完整的画布内容，导出范围仅包含 Graph 画布中的内容。部分插件使用了自定义的容器、画布等，这部分内容不会出现在导出的图片中。\n:::\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/event.en.md",
    "content": "---\ntitle: Event\norder: 0\n---\n\n## Overview\n\nThe event system in G6 is encapsulated based on the event system of [G](https://g.antv.antgroup.com/en/api/event/intro), providing a wider range of event types and more convenient methods for event binding and unbinding.\n\n## Event Types\n\nThe event types in G6 are mainly divided into the following categories:\n\n1. Graph events\n2. Canvas events\n3. Element events\n\n### Graph Events\n\nGraph events refer to events associated with the entire graph instance, such as the graph's rendering completion event, the graph's update event, etc. The complete list of graph events can be found at [GraphEvent](/en/api/event#graph-lifecycle-events-graphevent).\n\n#### Listening to Graph Events\n\nListening to graph events is consistent with the default event listening method. For example, to listen to the graph's rendering completion event:\n\n```typescript\nimport { Graph, GraphEvent } from '@antv/g6';\n\nconst graph = new Graph({\n  // ...\n});\n\ngraph.on(GraphEvent.AFTER_RENDER, () => {\n  // event handler\n});\n```\n\n### Canvas Events\n\nCanvas events refer to events associated with the canvas, such as the canvas's click event, the canvas's drag event, etc. The complete list of canvas events can be found at [CanvasEvent](/en/api/event#canvas-events-canvasevent).\n\n#### Listening to Canvas Events\n\nFor example, to listen to the canvas's click event:\n\n```typescript\nimport { Graph, CanvasEvent } from '@antv/g6';\n\nconst graph = new Graph({\n  // ...\n});\n\ngraph.on(CanvasEvent.CLICK, (event) => {\n  // event handler\n});\n```\n\n### Element Events\n\nElement events primarily refer to events that are triggered on element objects, such as a node's drag event, an edge's click event, etc. Elements are categorized into three types: nodes (`node`), edges (`edge`), and combos (`combo`). The complete list of corresponding events can be found at: [NodeEvent](/en/api/event#node-events-nodeevent), [EdgeEvent](/en/api/event#edge-events-edgeevent), [ComboEvent](/en/api/event#combo-events-comboevent).\n\n#### Listening to Element Events\n\nSimilar to canvas events, for example, to listen to a node's drag event and an edge's click event:\n\n```ts\nimport { Graph, NodeEvent, EdgeEvent, ComboEvent } from '@antv/g6';\n\nconst graph = new Graph({\n  // ...\n});\n\ngraph.on(NodeEvent.DRAG, (event) => {\n  // event handler\n});\n\ngraph.on(EdgeEvent.CLICK, (event) => {\n  // event handler\n});\n\ngraph.on(ComboEvent.CLICK, (event) => {\n  // event handler\n});\n```\n\n## Event Listening and Unlistening\n\nG6 provides the following APIs for event listening and unlistening:\n\n### on\n\nAdd an event listener\n\n```typescript\nconst handler = (event) => {\n  // event handler\n};\n\ngraph.on('event_name', handler);\n```\n\n### off\n\nRemove an event listener\n\n```typescript\ngraph.off('event_name', handler);\n```\n\nWhen no arguments are passed, it will remove all event listeners:\n\n```typescript\ngraph.off();\n```\n\n### once\n\nAdd a one-time event listener, which means the event listener will be automatically removed after the event is triggered\n\n```typescript\ngraph.once('event_name', handler);\n```\n\n### emit\n\nIf you want to manually trigger an event, you can use the `emit` method:\n\n```typescript\ngraph.emit('event_name', {\n  // event data\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/event.zh.md",
    "content": "---\ntitle: 事件\norder: 0\n---\n\n## 概述\n\nG6 中的事件系统是在 [G](https://g.antv.antgroup.com/api/event/intro) 的事件系统基础上进行了封装，提供了更多的事件类型，以及更加方便的事件绑定和解绑方法。\n\n## 事件类型\n\nG6 中的事件类型主要分为以下几类：\n\n1. 图(Graph)事件\n2. 画布(Canvas)事件\n3. 元素(Element)事件\n\n### 图事件\n\n图事件是指与整个图实例相关的事件，例如图的渲染完成事件、图的更新事件等。图事件的完整列表见[GraphEvent](/api/event#图表生命周期事件-graphevent)。\n\n#### 监听图事件\n\n图事件的监听与默认的事件监听方式一致，例如监听图的渲染完成事件：\n\n```typescript\nimport { Graph, GraphEvent } from '@antv/g6';\n\nconst graph = new Graph({\n  // ...\n});\n\ngraph.on(GraphEvent.AFTER_RENDER, () => {\n  // event handler\n});\n```\n\n### 画布事件\n\n画布事件是指与画布相关的事件，例如画布的点击事件、画布的拖拽事件等。画布事件的完整列表见[CanvasEvent](/api/event#画布事件-canvasevent)。\n\n#### 监听画布事件\n\n例如监听画布的点击事件：\n\n```typescript\nimport { Graph, CanvasEvent } from '@antv/g6';\n\nconst graph = new Graph({\n  // ...\n});\n\ngraph.on(CanvasEvent.CLICK, (event) => {\n  // event handler\n});\n```\n\n### 元素事件\n\n元素事件主要指在元素对象上触发的事件，例如节点的拖拽事件、边的点击事件等。元素分为节点(`node`)、边(`edge`)、组合(`combo`)三类，对应的事件完整列表分别见：[NodeEvent](/api/event#节点事件-nodeevent) 、[EdgeEvent](/api/event#边事件-edgeevent)、[ComboEvent](/api/event#combo事件-comboevent)。\n\n#### 监听元素事件\n\n与画布事件类似，例如监听节点的拖拽和边的点击事件：\n\n```ts\nimport { Graph, NodeEvent, EdgeEvent, ComboEvent } from '@antv/g6';\n\nconst graph = new Graph({\n  // ...\n});\n\ngraph.on(NodeEvent.DRAG, (event) => {\n  // event handler\n});\n\ngraph.on(EdgeEvent.CLICK, (event) => {\n  // event handler\n});\n\ngraph.on(ComboEvent.CLICK, (event) => {\n  // event handler\n});\n```\n\n## 事件监听与解除\n\nG6 提供以下 API 用于事件监听和解除：\n\n### on\n\n添加事件监听\n\n```typescript\nconst handler = (event) => {\n  // event handler\n};\n\ngraph.on('event_name', handler);\n```\n\n### off\n\n移除事件监听\n\n```typescript\ngraph.off('event_name', handler);\n```\n\n当不传入任何参数时，会移除所有事件监听：\n\n```typescript\ngraph.off();\n```\n\n### once\n\n添加一次性事件监听，即事件触发后会自动移除事件监听\n\n```typescript\ngraph.once('event_name', handler);\n```\n\n### emit\n\n如果你想手动触发一个事件，可以使用 `emit` 方法：\n\n```typescript\ngraph.emit('event_name', {\n  // event data\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/iconfont.en.md",
    "content": "---\ntitle: Using Iconfont\norder: 4\n---\n\n## Overview\n\nWhy use iconfont? It offers great compatibility, a wide variety of icons, and multicolor options. For more details, please visit the [Alibaba Iconfont Platform](https://www.iconfont.cn).\n\n![iconfont](https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*rJ3lQa0HR-wAAAAAAAAAAABkARQnAQ)\n\n## Usage\n\n### Download Font Icons\n\nFirst, you need to download the desired font icons from the [iconfont](https://www.iconfont.cn) website. Create a project, select the required icons, and then download the generated icon files.\n\n### Add Font Icons\n\nAfter downloading, place the directory containing the icon font files (typically including `.eot`, `.woff`, `.ttf`, and `.svg` files, as well as the `iconfont.css` stylesheet) into your project.\n\nYou can choose how to include them. Below is an example of how to include them in HTML:\n\n```html\n<head>\n  <style>\n    @import 'path-to-iconfont/iconfont.css';\n  </style>\n</head>\n```\n\n### Using the Font\n\n```js\n{\n  node: {\n    style: {\n      iconFontFamily: 'iconfont', // Corresponds to the `font-family` value in iconfont.css\n      iconText: '\\ue7f1', // Corresponds to the `content` value in iconfont.css, make sure to add `u`\n      iconFill: '#7863FF'\n    }\n  }\n}\n```\n\n### Utility Function `getIcon()`\n\nTo make it easier to retrieve icons, you can create a utility function `getIcon`. This function reads the icon information from the `iconfont.json` file and returns the corresponding Unicode character. Note that manually concatenating Unicode (`\\\\u${icon.unicode}`) won't work. Refer to [MDN String.fromCodePoint](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint) for details.\n\nFirst, ensure you have an `iconfont.json` file containing the detailed icon information.\n\n```js\nimport fonts from 'path-to-iconfont/iconfont.json';\n\nconst icons = fonts.glyphs.map((icon) => {\n  return {\n    name: icon.font_class,\n    unicode: String.fromCodePoint(icon.unicode_decimal), // `\\\\u${icon.unicode}`,\n  };\n});\n\nconst getIcon = (type) => {\n  const matchIcon = icons.find((icon) => {\n    return icon.name === type;\n  }) || { unicode: '', name: 'default' };\n  return matchIcon.unicode;\n};\n```\n\nUse it in your project:\n\n```js\n{\n  node: {\n    style: {\n      iconFontFamily: \"iconfont\",\n      iconText: getIcon('logo')\n    }\n  }\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/iconfont.zh.md",
    "content": "---\ntitle: 使用 iconfont\norder: 4\n---\n\n## 概述\n\n为什么使用 iconfont？ 兼容性好、种类多、多色等。在此不做过多介绍，请直接移步 [阿里巴巴-iconfont 平台](https://www.iconfont.cn)。\n\n![iconfont](https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*rJ3lQa0HR-wAAAAAAAAAAABkARQnAQ)\n\n## 使用\n\n### 下载字体图标\n\n首先，你需要从 [iconfont](https://www.iconfont.cn) 网站下载所需的图标字体。创建一个项目并选择所需的图标，然后下载生成的图标文件。\n\n### 添加字体图标\n\n下载完成后，将包含图标字体文件的目录（通常包括 `.eot`、`.woff`、`.ttf` 和 `.svg` 文件，以及 `iconfont.css` 样式文件）放入你的项目中。\n\n引入方式可自行选择，下面为在 HTML 中引入的例子：\n\n```html\n<head>\n  <style>\n    @import 'path-to-iconfont/iconfont.css';\n  </style>\n</head>\n```\n\n### 使用字体\n\n```js\n{\n  node: {\n    style: {\n      iconFontFamily: 'iconfont', // 对应 iconfont.css 中的 `font-family` 属性值\n      iconText: '\\ue7f1', // 对应 iconfont.css 中的 `content` 属性值，注意加 `u`\n      iconFill: '#7863FF'\n    }\n  }\n}\n```\n\n### 工具函数 `getIcon()`\n\n为了更方便地获取图标，可以创建一个工具函数 `getIcon`。该函数会从图标文件 `iconfont.json` 中读取图标信息并返回相应的 Unicode 字符。这里注意，手动拼接 unicode 是不行的（`\\\\u${icon.unicode}`）。详细参考 [MDN String.fromCodePoint](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint)。\n\n首先，确保你有一个 `iconfont.json` 文件，包含图标的详细信息。\n\n```js\nimport fonts from 'path-to-iconfont/iconfont.json';\n\nconst icons = fonts.glyphs.map((icon) => {\n  return {\n    name: icon.font_class,\n    unicode: String.fromCodePoint(icon.unicode_decimal), // `\\\\u${icon.unicode}`,\n  };\n});\n\nconst getIcon = (type: string) => {\n  const matchIcon = icons.find((icon) => {\n    return icon.name === type;\n  }) || { unicode: '', name: 'default' };\n  return matchIcon.unicode;\n};\n```\n\n在项目中使用：\n\n```js\n{\n  node: {\n    style: {\n      iconFontFamily: \"iconfont\",\n      iconText: getIcon('logo')\n    }\n  }\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/renderer.en.md",
    "content": "---\ntitle: renderer\norder: 1\n---\n\nG6 uses Canvas as the default renderer, but also supports rendering with SVG and WebGL. To switch to the SVG or WebGL renderer, simply pass the `renderer` parameter during initialization.\n\n## Using the SVG Renderer\n\n1. Install the renderer dependency:\n\n```bash\nnpm install @antv/g-svg\n```\n\n2. Configure the renderer:\n\n```javascript\nimport { Renderer as SVGRenderer } from '@antv/g-svg';\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  // ... other options\n  // All canvases will use the SVG renderer here\n  renderer: () => new SVGRenderer(),\n});\n```\n\n## Using the WebGL Renderer\n\n1. Install the renderer dependency:\n\n```bash\nnpm install @antv/g-webgl\n```\n\n2. Configure the renderer:\n\n```javascript\nimport { Renderer as WebGLRenderer } from '@antv/g-webgl';\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  // ... other options\n  // All canvases will use the WebGL renderer here\n  renderer: () => new WebGLRenderer(),\n});\n```\n\n## Using Different Renderers for Different Layers\n\nG6 uses layered canvases for rendering, so `renderer` is a callback function that takes the canvas type as a parameter and returns the renderer instance. If you want to use different renderers on different canvases, you can configure it like this:\n\n```javascript\nimport { Renderer as SVGRenderer } from '@antv/g-svg';\nimport { Renderer as WebGLRenderer } from '@antv/g-webgl';\n\nconst graph = new Graph({\n  // ... other options\n  renderer: (layer) => {\n    // The main canvas uses the WebGL renderer, and the other canvases use the SVG renderer\n    if (layer === 'main') return new WebGLRenderer();\n    return new SVGRenderer();\n  },\n});\n```\n\n## Switch Renderers Dynamically\n\nG6 does not provide a API to switch the renderer, but you can still update the `renderer` option through the `setOptions` method.\n\n```javascript\nimport { Renderer as SVGRenderer } from '@antv/g-svg';\nimport { Renderer as WebGLRenderer } from '@antv/g-webgl';\n\n// Use the WebGL renderer by default\nconst graph = new Graph({\n  // ... other options\n  renderer: () => new WebGLRenderer(),\n});\n\nawait graph.render();\n\n// Switch to the SVG renderer\ngraph.setOptions({\n  renderer: () => new SVGRenderer(),\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/further-reading/renderer.zh.md",
    "content": "---\ntitle: 渲染器\norder: 1\n---\n\nG6 默认使用 Canvas 作为渲染器，但也支持通过 SVG 和 WebGL 进行渲染，要切换到 SVG 或 WebGL 渲染器，只需在初始化时传入 `renderer` 参数即可。\n\n## 使用 SVG 渲染器\n\n1. 安装渲染器依赖：\n\n```bash\nnpm install @antv/g-svg\n```\n\n2. 配置渲染器：\n\n```javascript\nimport { Renderer as SVGRenderer } from '@antv/g-svg';\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  // ... other options\n  // 这里所有的画布都会使用 SVG 渲染器\n  renderer: () => new SVGRenderer(),\n});\n```\n\n## 使用 WebGL 渲染器\n\n1. 安装渲染器依赖：\n\n```bash\nnpm install @antv/g-webgl\n```\n\n2. 配置渲染器：\n\n```javascript\nimport { Renderer as WebGLRenderer } from '@antv/g-webgl';\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  // ... other options\n  // 这里所有的画布都会使用 WebGL 渲染器\n  renderer: () => new WebGLRenderer(),\n});\n```\n\n## 分层使用不同的渲染器\n\nG6 采用了分层画布进行渲染，因此 `renderer` 是一个回调函数，参数是画布类型，返回渲染器实例，如果你想在不同的画布上使用不同的渲染器，可以这样配置：\n\n```javascript\nimport { Renderer as SVGRenderer } from '@antv/g-svg';\nimport { Renderer as WebGLRenderer } from '@antv/g-webgl';\n\nconst graph = new Graph({\n  // ... other options\n  renderer: (layer) => {\n    // 主画布使用 WebGL 渲染器，其他画布使用 SVG 渲染器\n    if (layer === 'main') return new WebGLRenderer();\n    return new SVGRenderer();\n  },\n});\n```\n\n## 动态切换渲染器\n\nG6 没有提供单独的 API 来切换渲染器，但你仍可以通过 `setOptions` 方法来更新 `renderer` 参数：\n\n```javascript\nimport { Renderer as SVGRenderer } from '@antv/g-svg';\nimport { Renderer as WebGLRenderer } from '@antv/g-webgl';\n\n// 初始化时使用 WebGL 渲染器\nconst graph = new Graph({\n  // ... other options\n  renderer: () => new WebGLRenderer(),\n});\n\nawait graph.render();\n\n// 切换到 SVG 渲染器\ngraph.setOptions({\n  renderer: () => new SVGRenderer(),\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/getting-started/installation.en.md",
    "content": "---\ntitle: Installation\norder: 1\n---\n\n## npm\n\n```bash\n# npm\nnpm install @antv/g6 --save\n\n# pnpm\npnpm add @antv/g6\n\n# yarn\nyarn add @antv/g6\n```\n\nImport G6 in the code:\n\n```js\nimport { Graph } from '@antv/g6';\n```\n\nVisit [G6 npm Example](https://codesandbox.io/p/sandbox/using-g6-from-npm-d9spnr) to view the complete example code.\n\n## CDN\n\n`@antv/g6` Available on multiple CDNs:\n\n- unpkg: https://unpkg.com/@antv/g6@5/dist/g6.min.js\n- jsDelivr: https://cdn.jsdelivr.net/npm/@antv/g6@5/dist/g6.min.js\n- npmmirror: https://registry.npmmirror.com/@antv/g6/5/files/dist/g6.min.js\n\nImport G6 using a `script` tag:\n\n```html\n<script src=\"https://unpkg.com/@antv/g6@5/dist/g6.min.js\"></script>\n```\n\nVisit [G6 CDN Example](https://codesandbox.io/p/sandbox/using-g6-from-cdn-xt9ty6) to view the complete example code.\n"
  },
  {
    "path": "packages/site/docs/manual/getting-started/installation.zh.md",
    "content": "---\ntitle: 安装\norder: 1\n---\n\n## npm\n\n```bash\n# npm\nnpm install @antv/g6 --save\n\n# pnpm\npnpm add @antv/g6\n\n# yarn\nyarn add @antv/g6\n```\n\n在代码中引入 G6：\n\n```js\nimport { Graph } from '@antv/g6';\n```\n\n访问 [G6 npm 示例](https://codesandbox.io/p/sandbox/using-g6-from-npm-d9spnr) 查看完整示例代码。\n\n## CDN\n\n`@antv/g6` 在多个 CDN 上提供：\n\n- unpkg: https://unpkg.com/@antv/g6@latest/dist/g6.min.js\n- jsDelivr: https://cdn.jsdelivr.net/npm/@antv/g6@5/dist/g6.min.js\n- npmmirror: https://registry.npmmirror.com/@antv/g6/5/files/dist/g6.min.js\n\n使用 `script` 标签引入 G6：\n\n```html\n<script src=\"https://unpkg.com/@antv/g6@5/dist/g6.min.js\"></script>\n```\n\n访问 [G6 CDN 示例](https://codesandbox.io/p/sandbox/using-g6-from-cdn-xt9ty6) 查看完整示例代码。\n"
  },
  {
    "path": "packages/site/docs/manual/getting-started/integration/angular.en.md",
    "content": "---\ntitle: angular\norder: 2\n---\n\nRefer to the example below, you can use G6 in Angular, and you can also view the [Live Example](https://stackblitz.com/edit/g6-in-angular?file=src%2Fmain.ts)。\n\n<embed src=\"@/common/angular-snippet\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/getting-started/integration/angular.zh.md",
    "content": "---\ntitle: 在 Angular 中使用\norder: 2\n---\n\n参考下面的示例，你可以在 Angular 中使用 G6，也可以查看 [在线示例](https://stackblitz.com/edit/g6-in-angular?file=src%2Fmain.ts)。\n\n<embed src=\"@/common/angular-snippet\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/getting-started/integration/react.en.md",
    "content": "---\ntitle: react\norder: 0\n---\n\n## Non-Strict Mode\n\nRefer to the example below, you can use G6 in React, and you can also view the [Live Example](https://stackblitz.com/edit/g6-in-react?file=src/App.tsx) 。\n\n<embed src=\"@/common/react-snippet\"></embed>\n\n## Strict Mode\n\nIn strict mode, React will update twice, causing G6 to create and destroy the Graph instance repeatedly. You can refer to the following example to solve this problem:\n\n<embed src=\"@/common/react-snippet-strict\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/getting-started/integration/react.zh.md",
    "content": "---\ntitle: 在 React 中使用\norder: 0\n---\n\n:::info{title=建议}\n如果你需要更完善的 React 与 G6 集成解决方案，可以使用 AntV 官方封装库 [`@antv/graphin`](https://github.com/antvis/graphin)。\n:::\n\n## 非严格模式\n\n参考下面的示例，你可以在 React 中使用 G6，也可以查看 [在线示例](https://stackblitz.com/edit/g6-in-react?file=src/App.tsx) 。\n\n<embed src=\"@/common/react-snippet\"></embed>\n\n## 严格模式\n\n在严格模式下，React 会二次更新导致 G6 重复创建 Graph 实例并销毁，可以参考如下示例解决：\n\n<embed src=\"@/common/react-snippet-strict\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/getting-started/integration/vue.en.md",
    "content": "---\ntitle: vue\norder: 1\n---\n\n:::warning\nPlease do not pass Vue reactive data directly to the G6 instance, which may cause G6 to fail to render correctly, or even cause the page to crash.\n:::\n\nRefer to the example below, you can use G6 in Vue, and you can also view the [Live Example](https://stackblitz.com/edit/g6-in-vue?file=src/App.vue)。\n\n<embed src=\"@/common/vue-snippet\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/getting-started/integration/vue.zh.md",
    "content": "---\ntitle: 在 Vue 中使用\norder: 1\n---\n\n:::warning{title=注意}\n请不要将 Vue 响应式数据直接传递给 G6 实例，这可能会导致 G6 无法正确渲染，甚至导致页面崩溃。\n:::\n\n参考下面的示例，你可以在 Vue 中使用 G6，也可以查看 [在线示例](https://stackblitz.com/edit/g6-in-vue?file=src/App.vue)。\n\n<embed src=\"@/common/vue-snippet\"></embed>\n"
  },
  {
    "path": "packages/site/docs/manual/getting-started/quick-start.en.md",
    "content": "---\ntitle: Quick Start\norder: 0\n---\n\n## Online Experience with G6\n\nVisit [Chart Examples](/en/examples) to experience G6 online without any environment setup.\n\n## Creating a Simple Graph\n\nIn this example, we will create a simple graph using G6 based on an HTML page.\n\nCopy the following code into an HTML file and then open this file in a browser:\n\n```html\n<!-- Prepare a container -->\n<div id=\"container\" style=\"width: 500px; height: 500px\"></div>\n\n<!-- Import G6's JS file -->\n<script src=\"https://unpkg.com/@antv/g6@5/dist/g6.min.js\"></script>\n\n<script>\n  const { Graph } = G6;\n\n  fetch('https://assets.antv.antgroup.com/g6/graph.json')\n    .then((res) => res.json())\n    .then((data) => {\n      const graph = new Graph({\n        container: 'container',\n        autoFit: 'view',\n        data,\n        node: {\n          style: {\n            size: 10,\n          },\n          palette: {\n            field: 'group',\n            color: 'tableau',\n          },\n        },\n        layout: {\n          type: 'd3-force',\n          manyBody: {},\n          x: {},\n          y: {},\n        },\n        behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n      });\n\n      graph.render();\n    });\n</script>\n```\n\nYou will get a graph as shown below:\n\n<embed src=\"@/common/manual/getting-started/quick-start/simple-graph.md\"></embed>\n\nLet's analyze the following code snippet:\n\n1. First, we create a `div` element to serve as the container for the graph:\n\n```html\n<div id=\"container\" style=\"width: 500px; height: 500px\"></div>\n```\n\n2. Then, include the G6's JS file:\n\n```html\n<script src=\"https://unpkg.com/@antv/g6@5/dist/g6.min.js\"></script>\n```\n\n3. Use the `fetch` method to obtain the graph's data:\n\n```js\nfetch('https://assets.antv.antgroup.com/g6/graph.json').then((res) => res.json());\n```\n\n4. Finally, create an instance of the graph, pass in the configuration object, and call the `render` method to render the graph:\n\n```js\nconst { Graph } = G6;\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'view',\n  data,\n  node: {\n    style: {\n      size: 10,\n    },\n    palette: {\n      field: 'group',\n      color: 'tableau',\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    manyBody: {},\n    x: {},\n    y: {},\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\nIf you are using frameworks such as React, Vue, Angular, etc., you can refer to:\n\n- [Using G6 in React](./integration/react)\n- [Using G6 in Vue](./integration/vue)\n- [Using G6 in Angular](./integration/angular)\n"
  },
  {
    "path": "packages/site/docs/manual/getting-started/quick-start.zh.md",
    "content": "---\ntitle: 快速开始\norder: 0\n---\n\n## 在线体验 G6\n\n访问 [图表示例](/examples) 无需任何环境配置即可在线体验 G6。\n\n## 创建一个简单的图\n\n在本例子中，我们将基于 HTML 页面使用 G6 创建一个简单的图。\n\n将下面的代码复制到一个 HTML 文件中，然后在浏览器中打开这个文件：\n\n```html\n<!-- 准备一个容器 -->\n<div id=\"container\" style=\"width: 500px; height: 500px\"></div>\n\n<!-- 引入 G6 的 JS 文件 -->\n<script src=\"https://unpkg.com/@antv/g6@5/dist/g6.min.js\"></script>\n\n<script>\n  const { Graph } = G6;\n\n  fetch('https://assets.antv.antgroup.com/g6/graph.json')\n    .then((res) => res.json())\n    .then((data) => {\n      const graph = new Graph({\n        container: 'container',\n        autoFit: 'view',\n        data,\n        node: {\n          style: {\n            size: 10,\n          },\n          palette: {\n            field: 'group',\n            color: 'tableau',\n          },\n        },\n        layout: {\n          type: 'd3-force',\n          manyBody: {},\n          x: {},\n          y: {},\n        },\n        behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n      });\n\n      graph.render();\n    });\n</script>\n```\n\n会得到如下所示的图：\n\n<embed src=\"@/common/manual/getting-started/quick-start/simple-graph.md\"></embed>\n\n下面分析一下这段代码：\n\n1. 首先我们创建一个 `div` 元素作为图的容器：\n\n```html\n<div id=\"container\" style=\"width: 500px; height: 500px\"></div>\n```\n\n2. 然后引入 G6 的 JS 文件：\n\n```html\n<script src=\"https://unpkg.com/@antv/g6@5/dist/g6.min.js\"></script>\n```\n\n3. 使用 `fetch` 方法获取图的数据：\n\n```js\nfetch('https://assets.antv.antgroup.com/g6/graph.json').then((res) => res.json());\n```\n\n4. 最后创建一个图实例，传入配置对象，并调用 `render` 方法渲染图：\n\n```js\nconst { Graph } = G6;\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'view',\n  data,\n  node: {\n    style: {\n      size: 10,\n    },\n    palette: {\n      field: 'group',\n      color: 'tableau',\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    manyBody: {},\n    x: {},\n    y: {},\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n如果你使用 React、Vue、Angular 等框架，可以查看：\n\n- [在 React 中使用 G6](./integration/react)\n- [在 Vue 中使用 G6](./integration/vue)\n- [在 Angular 中使用 G6](./integration/angular)\n"
  },
  {
    "path": "packages/site/docs/manual/getting-started/step-by-step.en.md",
    "content": "---\ntitle: Step-by-step guide\norder: 3\n---\n\nThis tutorial will guide you through the development of a G6 chart from scratch, and along the way, you will learn and understand the main concepts of G6.\n\n## Create Application\n\nWe will use Vite to create a simple front-end application.\n\n### Initialization\n\nFirst, create an empty directory:\n\n```bash\nmkdir g6-tutorial\n\ncd g6-tutorial\n```\n\nInitialize the project:\n\n```bash\nnpm init -y\n```\n\nInstall G6:\n\n```bash\nnpm install @antv/g6 --save\n```\n\nVite is a new type of front-end build tool that is based on ESModule and can quickly start up projects.\n\nInstall Vite:\n\n```bash\nnpm install vite --save-dev\n```\n\nAdd a start script to the `package.json`:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"vite\"\n  }\n}\n```\n\n### Create Files\n\nCreate the `index.html` and `main.ts` files with the following content:\n\n**index.html**:\n\n```html\n<!doctype html>\n<html>\n  <head>\n    <title>@antv/g6 Tutorial</title>\n  </head>\n  <body>\n    <div id=\"container\"></div>\n    <script type=\"module\" src=\"main.ts\"></script>\n  </body>\n</html>\n```\n\n**main.ts**：\n\n```typescript\nalert('Hello, G6!');\n```\n\n### Start project\n\n```bash\nnpm run dev\n```\n\nOpen a web browser and visit the address output in the terminal (typically: http://127.0.0.1:5173/), and you will see a pop-up displaying \"Hello, G6!\".\n\n## Creating a Simple Graph\n\nNext, we will create a simple chart using G6.\n\n### Preparing the Data\n\nG6 uses JSON-formatted data to describe the graph, which usually includes nodes and edges. We will use the following prepared data:\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 50, y: 50 } },\n    { id: 'node-2', style: { x: 150, y: 50 } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2' }],\n};\n```\n\nThe data includes two nodes and one edge. The `id` attribute for nodes is mandatory, and the position of each node is set in the `style`. The `source` and `target` attributes of the edge represent the `id` of the starting node and the ending node, respectively.\n\n### Creating and Drawing the Graph\n\nCreate an instance of the Graph, pass in a configuration object that includes the container and data, and then call the `render` method to draw the Graph:\n\n```typescript\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 50, y: 50 } },\n      { id: 'node-2', style: { x: 150, y: 50 } },\n    ],\n    edges: [{ source: 'node-1', target: 'node-2' }],\n  },\n});\n\ngraph.render();\n```\n\nAs shown below, you can see that the chart has been successfully drawn:\n\n<embed src=\"@/common/manual/getting-started/step-by-step/create-chart.md\"></embed>\n\n### Element\n\nNext, we will introduce how to configure the style and types of elements in the canvas.\n\nG6 provides various mechanisms to configure element styles, which can be done in the data itself or within the chart instance. In the previous example, we configured the position of the nodes in the data. Next, we will configure the styles of nodes and edges in the graph configuration options:\n\n<!-- TODO -->\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 50, y: 50 } },\n      { id: 'node-2', style: { x: 150, y: 50 } },\n    ],\n    edges: [{ source: 'node-1', target: 'node-2' }],\n  },\n  node: {\n    style: {\n      fill: 'pink',\n    },\n  },\n  edge: {\n    style: {\n      stroke: 'lightgreen',\n    },\n  },\n});\n\ngraph.render();\n```\n\nAs the code shows, we have configured the fill color of the nodes to be pink and the stroke color of the edges to be light green within the chart instance. You can see the effect in the example below:\n\n<embed src=\"@/common/manual/getting-started/step-by-step/elements-1.md\"></embed>\n\nThe key parts are the `node.style` and `edge.style` options, which are used to configure the styles of nodes and edges, respectively.\n\n> In the subsequent code examples, we will only display the parts of the options. For the complete code of this project, please refer to the [Complete Example](https://codesandbox.io/s/g6-tutorial).\n\nNext, we will demonstrate more types of nodes by setting the node type:\n\n```js\n{\n  node: {\n    type: (datum) => datum.id === 'node-1' ? 'circle' : 'rect',\n    style: {\n      fill: 'pink',\n      size: 20\n    }\n  }\n}\n```\n\nIn the code above, we set the `type` attribute of the node, which can be a string or a function. When `type` is a function, the argument of the function is the current node's data object, and the return value is the type of the node.\n\n> Similarly, each attribute under the `style` style of an element can also be a function, with the argument being the current element's data object.\n\n> You can even set the entire `style` property as a function, allowing you to dynamically set the element's style based on the data object.\n\nThe circular node (`circle`) is the default node type in G6. Here, we set the type of the first node to a circle and the type of the second node to a rectangle.\n\nAt the same time, we also set the size of the nodes to 20, so the first node is a circle with a radius of 10, and the second node is a square with a side length of 20.\n\n> If you want to set the size of the rectangular node to 20x10, you can set `size` to an array `[20, 10]`.\n\nYou can see the effect in the example below:\n\n<embed src=\"@/common/manual/getting-started/step-by-step/elements-2.md\"></embed>\n\n### Behaviors\n\nThe chart provided in the previous example is static. Next, we will add some interactive behaviors.\n\nG6 offers a variety of interactive behaviors. We will add a few commonly used Behaviors to allow users to drag, zoom the canvas, and drag nodes.\n\n```js\n{\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'];\n}\n```\n\nTry dragging nodes and the canvas in the example below, and use the scroll wheel to zoom in and out on the canvas:\n\n<embed src=\"@/common/manual/getting-started/step-by-step/behaviors.md\"></embed>\n\n### Layout\n\nIn the previous example, we manually set the positions of the nodes. However, this can become very difficult when there are many nodes.\n\nLayout algorithms can automatically adjust the positions of nodes based on certain rules. G6 provides a variety of layout algorithms, such as tree layout, force-directed layout, and so on.\n\nFirst, generate a set of data that does not include position information:\n\n```js\nconst data = {\n  nodes: Array.from({ length: 10 }).map((_, i) => ({ id: `node-${i}` })),\n  edges: Array.from({ length: 9 }).map((_, i) => ({ source: `node-0`, target: `node-${i + 1}` })),\n};\n```\n\nBy default, if a node does not have position information, G6 will place the node at the top-left corner, that is, at the coordinates `(0, 0)`.\n\nNext, we will use the `d3-force` layout algorithm, which is a force-directed layout algorithm that can simulate the forces of attraction and repulsion between nodes, allowing the nodes to automatically adjust to suitable positions.\n\n```js\n{\n  layout: {\n    type: 'd3-force',\n  },\n}\n```\n\nView the example below, and you can see that the nodes have automatically adjusted to suitable positions:\n\n<details>\n<summary>Complete Code</summary>\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: Array.from({ length: 10 }).map((_, i) => ({ id: `node-${i}` })),\n    edges: Array.from({ length: 9 }).map((_, i) => ({ source: `node-0`, target: `node-${i + 1}` })),\n  },\n  node: {\n    style: {\n      size: 20,\n      fill: 'pink',\n    },\n  },\n  edge: {\n    style: {\n      stroke: 'lightgreen',\n    },\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n  layout: {\n    type: 'd3-force',\n  },\n});\n\ngraph.render();\n```\n\n</details>\n\n<embed src=\"@/common/manual/getting-started/step-by-step/layout.md\"></embed>\n\n### Palette\n\nSimilarly, when there are many nodes, manually setting the color of each node can become difficult. G6 provides a palette mechanism that makes it easy to assign colors to elements.\n\nPalettes typically assign colors to elements based on a specific field in the data, such as the type of node, the weight of an edge, etc.\n\nBelow, we add a `category` field in the data:\n\n```js\nconst data = {\n  nodes: Array.from({ length: 10 }).map((_, i) => ({\n    id: `node-${i}`,\n    data: { category: i === 0 ? 'central' : 'around' },\n  })),\n  edges: Array.from({ length: 9 }).map((_, i) => ({ source: `node-0`, target: `node-${i + 1}` })),\n};\n```\n\nThen, use the `tableau` palette to set the colors for the nodes, where the `field` attribute specifies the field in the data, and the `color` attribute specifies the name of the palette.\n\n```js\n{\n  node: {\n    palette: {\n      field: 'category',\n      color: 'tableau',\n    }\n  }\n}\n```\n\n> It is important to note that the `fill` style in `node.style` should be removed, as its priority is higher than the colors assigned by the palette.\n\n<embed src=\"@/common/manual/getting-started/step-by-step/palette.md\"></embed>\n\n### Plugins\n\nThe plugin mechanism is an important feature of G6, which allows you to extend the functionality of G6 through plugins. G6 provides a wealth of built-in plugins, such as `tooltip`, `legend`, etc., and also supports user-defined plugins.\n\nBelow, we will use the `grid-line` plugin to add grid lines to the canvas:\n\n```js\n{\n  plugins: ['grid-line'],\n}\n```\n\nYou can see that grid lines have been added to the canvas:\n\n<embed src=\"@/common/manual/getting-started/step-by-step/plugins-1.md\"></embed>\n\nThe plugin configuration mentioned above used a shorthand form. Most plugins support the passing of additional parameters. For example, the `grid-line` plugin allows you to configure the `follow` property to specify whether the grid lines should follow the canvas when it is dragged.\n\n```js\n{\n  plugins: [{ type: 'grid-line', follow: true }];\n}\n```\n\nTry dragging the canvas in the example below, and you will see that the grid lines move along with the canvas:\n\n<embed src=\"@/common/manual/getting-started/step-by-step/plugins-2.md\"></embed>\n\n## Summary\n\nIn this tutorial, we created a G6 chart from scratch and became acquainted with the main concepts of G6. We learned how to create a simple chart, how to configure the style and types of elements, how to add interactive behaviors, how to use layout algorithms, how to use palettes, and how to use plugins.\n\nFor a more detailed introduction to the concepts of G6, you can refer to [Core Concepts](/en/manual/graph/graph).\n\nDetailed explanations of options such as elements, layouts, and plugins can be found in the [API](/en/api/data).\n"
  },
  {
    "path": "packages/site/docs/manual/getting-started/step-by-step.zh.md",
    "content": "---\ntitle: 详细教程\norder: 3\n---\n\n本教程将引导你从头开始完成一个 G6 图表开发，并在过程中了解和学习 G6 的主要概念。\n\n## 创建应用\n\n我们将使用 Vite 来创建一个简单的前端应用。\n\n### 初始化\n\n首先创建一个空目录：\n\n```bash\nmkdir g6-tutorial\n\ncd g6-tutorial\n```\n\n初始化项目：\n\n```bash\nnpm init -y\n```\n\n安装 G6：\n\n```bash\nnpm install @antv/g6 --save\n```\n\nVite 是一个新型的前端构建工具，它基于 ESModule，可以快速启动项目。\n\n安装 Vite：\n\n```bash\nnpm install vite --save-dev\n```\n\n在 `package.json` 中添加启动脚本：\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"vite\"\n  }\n}\n```\n\n### 创建文件\n\n创建 `index.html` 和 `main.ts` 文件，内容如下：\n\n**index.html**：\n\n```html\n<!doctype html>\n<html>\n  <head>\n    <title>@antv/g6 Tutorial</title>\n  </head>\n  <body>\n    <div id=\"container\"></div>\n    <script type=\"module\" src=\"main.ts\"></script>\n  </body>\n</html>\n```\n\n**main.ts**：\n\n```typescript\nalert('Hello, G6!');\n```\n\n### 启动项目\n\n```bash\nnpm run dev\n```\n\n打开浏览器访问终端中输出的地址（通常为：http://127.0.0.1:5173/ ），你将看到一个弹窗显示 \"Hello, G6!\"。\n\n## 创建一个简单的图表\n\n接下来，我们将使用 G6 创建一个简单的图表。\n\n### 准备数据\n\nG6 使用 JSON 格式的数据来描述图，通常包括节点和边。我们将使用下面准备的数据：\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 50, y: 50 } },\n    { id: 'node-2', style: { x: 150, y: 50 } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2' }],\n};\n```\n\n数据中包括两个节点和一条边，节点的 `id` 属性是必须的，并在 `style` 设置了每个节点的位置。边的 `source` 和 `target` 属性分别表示边的起始节点 `id` 和结束节点 `id`。\n\n### 创建并绘制图表\n\n创建一个图表实例，传入一个配置对象，包括容器和数据，然后调用 `render` 方法渲染图表：\n\n```typescript\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 50, y: 50 } },\n      { id: 'node-2', style: { x: 150, y: 50 } },\n    ],\n    edges: [{ source: 'node-1', target: 'node-2' }],\n  },\n});\n\ngraph.render();\n```\n\n如下所示，可以看到图表已经顺利绘制出来：\n\n<embed src=\"@/common/manual/getting-started/step-by-step/create-chart.md\"></embed>\n\n### 元素\n\n接下来将介绍如何配置画布中的元素样式和种类。\n\nG6 提供了多种机制来配置元素样式，可以在数据中进行配置，也可以在图表实例中进行配置。前面的示例中，我们在数据中配置了节点的位置，接下来我们在图配置项中配置节点和边的样式：\n\n<!-- TODO -->\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 50, y: 50 } },\n      { id: 'node-2', style: { x: 150, y: 50 } },\n    ],\n    edges: [{ source: 'node-1', target: 'node-2' }],\n  },\n  node: {\n    style: {\n      fill: 'pink',\n    },\n  },\n  edge: {\n    style: {\n      stroke: 'lightgreen',\n    },\n  },\n});\n\ngraph.render();\n```\n\n正如代码所示，我们在图表实例中配置了节点的填充颜色为粉色，边的描边颜色为浅绿色。你可以在下面的示例中看到效果：\n\n<embed src=\"@/common/manual/getting-started/step-by-step/elements-1.md\"></embed>\n\n其中的关键部分是 `node.style` 和 `edge.style` 配置项，分别用来配置节点和边的样式。\n\n> 在后续的代码示例中，我们仅展示配置项的部分代码，本项目的完整代码请查看 [完整示例](https://codesandbox.io/s/g6-tutorial)。\n\n下面我们将通过设置节点的类型来展示更多的节点种类：\n\n```js\n{\n  node: {\n    type: (datum) => datum.id === 'node-1' ? 'circle' : 'rect',\n    style: {\n      fill: 'pink',\n      size: 20\n    }\n  }\n}\n```\n\n上面的代码中，我们设置了节点的 `type` 属性，其值可以是一个字符串，也可以是一个函数。当 `type` 是一个函数时，函数的参数是当前节点的数据对象，函数的返回值是节点的类型。\n\n> 同样的，元素中 `style` 样式下的每个属性都可以是一个函数，函数的参数是当前元素的数据对象。\n\n> 你甚至可以将整个 `style` 属性设置为一个函数，这样你可以根据数据对象动态设置元素的样式。\n\n圆形节点(`circle`)是 G6 的默认节点类型，这里我们将第一个节点的类型设置为圆形，第二个节点的类型设置为矩形。\n\n同时我们还将节点的大小设置为 20，因此第一个节点是一个半径为 10 的圆形，第二个节点是一个边长为 20 的正方形。\n\n> 如果你想将矩形节点的大小设置为 20x10，可以将 `size` 设置为一个数组 `[20, 10]`。\n\n你可以在下面的示例中看到效果：\n\n<embed src=\"@/common/manual/getting-started/step-by-step/elements-2.md\"></embed>\n\n### 交互\n\n在上面的例子中提供的图表是静态的，接下来我们将添加一些交互行为。\n\nG6 提供了多种交互行为，我们添加几个常用的交互，使得用户可以拖拽、缩放画布，拖拽节点。\n\n```js\n{\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'];\n}\n```\n\n尝试在下面的示例中拖拽节点和画布，并使用滚轮缩放画布：\n\n<embed src=\"@/common/manual/getting-started/step-by-step/behaviors.md\"></embed>\n\n### 布局\n\n在上面的示例中，我们手动设置了节点的位置。但当节点数量较多时，这会变得非常困难。\n\n布局算法可以基于一定的规则自动调整节点的位置，G6 提供了多种布局算法，例如树形布局、力导向布局等。\n\n首先生成一组不包括位置信息的数据：\n\n```js\nconst data = {\n  nodes: Array.from({ length: 10 }).map((_, i) => ({ id: `node-${i}` })),\n  edges: Array.from({ length: 9 }).map((_, i) => ({ source: `node-0`, target: `node-${i + 1}` })),\n};\n```\n\n默认情况下，如果节点没有位置信息，G6 会将节点放置在左上角，即 `(0, 0)`。\n\n接下来我们使用 `d3-force` 布局算法，它是一种力导向布局算法，可以模拟节点之间的引力和斥力，使得节点自动调整到合适的位置。\n\n```js\n{\n  layout: {\n    type: 'd3-force',\n  },\n}\n```\n\n查看下面的示例，可以看到节点已经自动调整到合适的位置：\n\n<details>\n<summary>完整代码</summary>\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: Array.from({ length: 10 }).map((_, i) => ({ id: `node-${i}` })),\n    edges: Array.from({ length: 9 }).map((_, i) => ({ source: `node-0`, target: `node-${i + 1}` })),\n  },\n  node: {\n    style: {\n      size: 20,\n      fill: 'pink',\n    },\n  },\n  edge: {\n    style: {\n      stroke: 'lightgreen',\n    },\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n  layout: {\n    type: 'd3-force',\n  },\n});\n\ngraph.render();\n```\n\n</details>\n\n<embed src=\"@/common/manual/getting-started/step-by-step/layout.md\"></embed>\n\n### 色板\n\n同样的，当节点数量较多时，手动设置节点的颜色也会变得困难。G6 提供了色板机制，可以便捷地为元素设置颜色。\n\n色板通常会基于数据的某个字段为元素设置颜色，例如节点的类型、边的权重等。\n\n下面我们在数据中的添加 `category` 字段：\n\n```js\nconst data = {\n  nodes: Array.from({ length: 10 }).map((_, i) => ({\n    id: `node-${i}`,\n    data: { category: i === 0 ? 'central' : 'around' },\n  })),\n  edges: Array.from({ length: 9 }).map((_, i) => ({ source: `node-0`, target: `node-${i + 1}` })),\n};\n```\n\n然后使用 `tableau` 色板为节点设置颜色，`field` 属性指定了数据中的字段，`color` 属性指定了色板的名称。\n\n```js\n{\n  node: {\n    palette: {\n      field: 'category',\n      color: 'tableau',\n    }\n  }\n}\n```\n\n> 需要注意将 `node.style` 中的 `fill` 样式移除，因为其优先级高于色板分配的颜色。\n\n<embed src=\"@/common/manual/getting-started/step-by-step/palette.md\"></embed>\n\n### 插件\n\n插件机制是 G6 的一个重要特性，可以通过插件扩展 G6 的功能。G6 提供了丰富的内置插件，例如 `tooltip`、`legend` 等，也支持用户自定义插件。\n\n下面我们将使用 `grid-line` 插件为画布添加网格线：\n\n```js\n{\n  plugins: ['grid-line'],\n}\n```\n\n可以看到画布已经添加了网格线：\n\n<embed src=\"@/common/manual/getting-started/step-by-step/plugins-1.md\"></embed>\n\n上面的插件配置项中使用了简写形式，大部分的插件都支持传递额外的参数，例如 `grid-line` 插件可以配置 `follow` 属性来指定拖拽画布时网格线是否跟随画布移动。\n\n```js\n{\n  plugins: [{ type: 'grid-line', follow: true }];\n}\n```\n\n尝试在下面的示例中拖拽画布，可以看到网格线跟随画布移动：\n\n<embed src=\"@/common/manual/getting-started/step-by-step/plugins-2.md\"></embed>\n\n## 小结\n\n在本教程中，我们从头开始创建了一个 G6 图表，并了解了 G6 的主要概念。我们学习了如何创建一个简单的图表，如何配置元素的样式和种类，如何添加交互行为，如何使用布局算法，如何使用色板，如何使用插件。\n\n关于 G6 更加详细的概念介绍可以在 [核心概念](/manual/graph/graph) 中查看。\n\n图的 API 详细说明可以在 [API](/api/graph) 中查看。\n"
  },
  {
    "path": "packages/site/docs/manual/graph/extension.en.md",
    "content": "---\ntitle: extension\norder: 9\n---\n\n## Concept\n\nExtension is an important concept in G6, it is a general term for all expandable parts in G6, including the following types:\n\n- Animation\n- Behavior\n- Element\n  - Node\n  - Edge\n  - Combo\n- Layout\n- Palette\n- Plugin\n- Theme\n- Transform\n\n## Register Extension\n\nG6 provides the `register` function for registering extensions, for example:\n\n```typescript\nimport { register, ExtensionCategory } from '@antv/g6';\nimport { CustomNode } from './my-custom-node';\n\n// # Registering Nodes\nregister(ExtensionCategory.NODE, 'custom-node', CustomNode);\n```\n\nThe first parameter of the `register` function is the type of the extension, the second parameter is the name of the extension, and the third parameter is the implementation of the extension(refer to the custom-related sections in the documentation for each extension type).\n\nDifferent types of extensions **can** use the same extension name, but when registering extensions of the same type, only the first registration will take effect.\n\n```typescript\n// ✅\nregister(ExtensionCategory.NODE, 'custom-name', CustomNode);\nregister(ExtensionCategory.COMBO, 'custom-name', CustomCombo);\n\n// ❌\nregister(ExtensionCategory.NODE, 'custom-name', CustomNode);\nregister(ExtensionCategory.NODE, 'custom-name', CustomNode);\n```\n\n## Use Extension\n\nThe configuration location for different types of extensions varies, but all are used by specifying the name that was used during registration, for example:\n\n- Using node extensions: `options.node.type`\n- Using edge extensions: `options.edge.type`\n- Using combo extensions: `options.combo.type`\n- Using behavior extensions: `options.behaviors`\n- Using layout extensions: `options.layout.type`\n- Using plugin extensions: `options.plugins`\n- Using theme extensions: `options.theme`\n- Using data transform extensions: `options.transform`\n- Using palette extensions: `options.node.palette`, `options.edge.palette`, etc.\n- Using animation extensions: `options.node.animate`, `options.edge.animate`, etc.\n\n## Get Extension\n\nG6 provides the `getExtension` and `getExtensions` methods to obtain a single extension and all extensions of a specified type, respectively, for example:\n\n```typescript\nimport { getExtension, getExtensions, ExtensionCategory } from '@antv/g6';\n\n// To get the implementation of the node extension registered with the name 'custom-node'\ngetExtension(ExtensionCategory.NODE, 'custom-node');\n\n// Retrieve all registered node extension implementations\ngetExtensions(ExtensionCategory.NODE);\n```\n"
  },
  {
    "path": "packages/site/docs/manual/graph/extension.zh.md",
    "content": "---\ntitle: Extension - 扩展\norder: 9\n---\n\n## 概念\n\n扩展 (Extension) 是 G6 中的一个重要概念，它是 G6 中所有可扩展部分的统称，包括以下几种：\n\n- 动画 (Animation)\n- 交互 (Behavior)\n- 元素 (Element)\n- 节点 (Node)\n  - 边 (Edge)\n  - 组合 (Combo)\n- 布局 (Layout)\n- 色板 (Palette)\n- 插件 (Plugin)\n- 主题 (Theme)\n- 数据转换 (Transform)\n\n## 注册扩展\n\nG6 提供了 `register` 函数用于注册扩展，例如：\n\n```typescript\nimport { register, ExtensionCategory } from '@antv/g6';\nimport { CustomNode } from './my-custom-node';\n\n// 注册节点\nregister(ExtensionCategory.NODE, 'custom-node', CustomNode);\n```\n\n`register` 函数的第一个参数是扩展的类型，第二个参数是扩展的名称，第三个参数是扩展的实现(扩展实现参考各扩展类型所在文档的自定义相关章节)。\n\n不同的扩展类型之间**可以**使用相同的扩展名称，但同一类型的扩展重复注册时仅会在第一次注册时生效。\n\n<!-- TODO: 详细的参数签名见：[API 文档](/api/reference/g6/register) -->\n\n```typescript\n// ✅\nregister(ExtensionCategory.NODE, 'custom-name', CustomNode);\nregister(ExtensionCategory.COMBO, 'custom-name', CustomCombo);\n\n// ❌\nregister(ExtensionCategory.NODE, 'custom-name', CustomNode);\nregister(ExtensionCategory.NODE, 'custom-name', CustomNode);\n```\n\n## 使用扩展\n\n不同的扩展类型的配置位置有所不同，但都是通过指定注册时所使用的名称来使用扩展，例如：\n\n- 使用节点扩展：`options.node.type`\n- 使用边扩展：`options.edge.type`\n- 使用组合扩展：`options.combo.type`\n- 使用交互扩展：`options.behaviors`\n- 使用布局扩展：`options.layout.type`\n- 使用插件扩展：`options.plugins`\n- 使用主题扩展：`options.theme`\n- 使用数据转换扩展：`options.transform`\n- 使用色板扩展：`options.node.palette` `options.edge.palette` 等\n- 使用动画扩展：`options.node.animate` `options.edge.animate` 等\n\n## 获取扩展\n\nG6 提供了 `getExtension` 和 `getExtensions` 方法分别用于获取指定扩展类型下的单个扩展和所有扩展，例如：\n\n```typescript\nimport { getExtension, getExtensions, ExtensionCategory } from '@antv/g6';\n\n// 获取注册的名称为 'custom-node' 的节点扩展实现\ngetExtension(ExtensionCategory.NODE, 'custom-node');\n\n// 获取所有注册的节点扩展实现\ngetExtensions(ExtensionCategory.NODE);\n```\n"
  },
  {
    "path": "packages/site/docs/manual/graph/extensions.en.md",
    "content": "---\n\ntitle: Built-in Extensions\norder: 4\n\nThe G6 built-in extensions and registered types are as follows:\n\n## Animations\n\n| Extension     | Registration Type |\n| ------------- | ----------------- |\n| ComboCollapse | 'combo-collapse'  |\n| ComboExpand   | 'combo-expand'    |\n| NodeCollapse  | 'node-collapse'   |\n| NodeExpand    | 'node-expand'     |\n| PathIn        | 'path-in'         |\n| PathOut       | 'path-out'        |\n| Fade          | 'fade'            |\n| Translate     | 'translate'       |\n\nUsage:\n\nIn `GraphOptions.[node|edge|combo].animation.[stage]`, for example:\n\n```ts\nconst graph = new Graph({\n  // ... other options\n  node: {\n    animation: {\n      update: 'translate', // Only use translation animation in the update stage\n    },\n  },\n});\n```\n\n## Behaviors\n\n| Extension                 | Registration Type             | Description                                    |\n| ------------------------- | ----------------------------- | ---------------------------------------------- |\n| BrushSelect               | 'brush-select'                | /                                              |\n| ClickSelect               | 'click-select'                | /                                              |\n| CollapseExpand            | 'collapse-expand'             | /                                              |\n| CreateEdge                | 'create-edge'                 | /                                              |\n| DragCanvas                | 'drag-canvas'                 | /                                              |\n| DragElementForce          | 'drag-element-force'          | Drag element when use d3-force layout          |\n| DragElement               | 'drag-element'                | /                                              |\n| FixElementSize            | 'fix-element-size'            | Keep the size of element during zooming canvas |\n| FocusElement              | 'focus-element'               | /                                              |\n| HoverActivate             | 'hover-activate'              | /                                              |\n| LassoSelect               | 'lasso-select'                | /                                              |\n| OptimizeViewportTransform | 'optimize-viewport-transform' | Hide elements during manipulate the canvas     |\n| ScrollCanvas              | 'scroll-canvas'               | /                                              |\n| ZoomCanvas                | 'zoom-canvas'                 | /                                              |\n\nUsage:\n\nIn `GraphOptions.behaviors`, for example:\n\n```ts\nconst graph = new Graph({\n  // ... other options\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n});\n```\n\n## Elements\n\n### Nodes\n\n| Extension | Registration Type |\n| --------- | ----------------- |\n| circle    | Circle            |\n| diamond   | Diamond           |\n| ellipse   | Ellipse           |\n| hexagon   | Hexagon           |\n| html      | HTML              |\n| image     | Image             |\n| rect      | Rect              |\n| star      | Star              |\n| donut     | Donut             |\n| triangle  | Triangle          |\n\nUsage:\n\n1. In `GraphOptions.data.nodes[number].type`;\n2. In `GraphOptions.node.type`;\n\n```ts\nconst graph = new Graph({\n  // ... other options\n  data: {\n    nodes: [{ id: 'node-1', type: 'circle' }],\n  },\n  node: {\n    type: 'circle',\n  },\n});\n```\n\n### Edges\n\n| Extension       | Registration Type  | Description                   |\n| --------------- | ------------------ | ----------------------------- |\n| Cubic           | 'cubic'            | Cubic Bezier Curve            |\n| Line            | 'line'             | /                             |\n| Polyline        | 'polyline'         | /                             |\n| Quadratic       | 'quadratic'        | Quadratic Bezier Curve        |\n| CubicHorizontal | 'cubic-horizontal' | Horizontal Cubic Bezier Curve |\n| CubicVertical   | 'cubic-vertical'   | Vertical Cubic Bezier Curve   |\n| CubicRadial     | 'cubic-radial'     | Radial Cubic Bezier Curve     |\n\nUsage(like `Nodes`):\n\n1. In `GraphOptions.data.edges[number].type`;\n2. In `GraphOptions.edge.type`;\n\n### Combos\n\n| Extension   | Registration Type |\n| ----------- | ----------------- |\n| CircleCombo | 'circle'          |\n| RectCombo   | 'rect'            |\n\nUsage(like `Nodes`):\n\n1. In `GraphOptions.data.combos[number].type`;\n2. In `GraphOptions.combo.type`;\n\n## Layouts\n\n| Extension           | Registration Type | Description                     |\n| ------------------- | ----------------- | ------------------------------- |\n| AntVDagreLayout     | 'antv-dagre'      | /                               |\n| ComboCombinedLayout | 'combo-combined'  | /                               |\n| CompactBoxLayout    | 'compact-box'     | /                               |\n| ForceAtlas2Layout   | 'force-atlas2'    | /                               |\n| CircularLayout      | 'circular'        | /                               |\n| ConcentricLayout    | 'concentric'      | /                               |\n| D3ForceLayout       | 'd3-force'        | /                               |\n| DagreLayout         | 'dagre'           | /                               |\n| DendrogramLayout    | 'dendrogram'      | /                               |\n| ForceLayout         | 'force'           | /                               |\n| FruchtermanLayout   | 'fruchterman'     | /                               |\n| GridLayout          | 'grid'            | /                               |\n| IndentedLayout      | 'indented'        | /                               |\n| MDSLayout           | 'mds'             | Multidimensional Scaling Layout |\n| MindmapLayout       | 'mindmap'         | /                               |\n| RadialLayout        | 'radial'          | /                               |\n| RandomLayout        | 'random'          | /                               |\n\nUsage:\n\nIn `GraphOptions.layout`, for example:\n\n```ts\nconst graph = new Graph({\n  // ... other options\n  layout: {\n    type: 'force',\n  },\n});\n```\n\n## Palettes\n\n<embed src=\"@/common/manual/getting-started/extensions/palettes.md\"></embed>\n\nUsage:\n\nIn `GraphOptions.[node|edge|combo].palette`, for example:\n\n```ts\nconst graph = new Graph({\n  // ... other options\n  node: {\n    palette: 'tableau',\n  },\n});\n```\n\n## Themes\n\n| Registration Type |\n| ----------------- |\n| dark              |\n| light             |\n\nUsage:\n\nIn `GraphOptions.theme`, for example:\n\n```ts\nconst graph = new Graph({\n  // ... other options\n  theme: 'dark',\n});\n```\n\n## Plugins\n\n| Extension      | Registration Type  |\n| -------------- | ------------------ |\n| BubbleSets     | 'bubble-sets'      |\n| EdgeFilterLens | 'edge-filter-lens' |\n| GridLine       | 'grid-line'        |\n| Background     | 'background'       |\n| Contextmenu    | 'contextmenu'      |\n| Fisheye        | 'fisheye'          |\n| Fullscreen     | 'fullscreen'       |\n| History        | 'history'          |\n| Hull           | 'hull'             |\n| Legend         | 'legend'           |\n| Minimap        | 'minimap'          |\n| Snapline       | 'snapline'         |\n| Timebar        | 'timebar'          |\n| Toolbar        | 'toolbar'          |\n| Tooltip        | 'tooltip'          |\n| Watermark      | 'watermark'        |\n\nUsage:\n\nIn `GraphOptions.plugins`, for example:\n\n```ts\nconst graph = new Graph({\n  // ... other options\n  plugins: ['minimap', 'contextmenu'],\n});\n```\n\n## Transforms\n\n| Extension            | Registration Type        | Description |\n| -------------------- | ------------------------ | ----------- |\n| ProcessParallelEdges | 'process-parallel-edges' | /           |\n| PlaceRadialLabels    | 'place-radial-labels'    | 径向标签    |\n\nUsage:\n\nIn `GraphOptions.transform`, for example:\n\n```ts\nconst graph = new Graph({\n  // ... other options\n  transform: ['process-parallel-edges', 'place-radial-labels'],\n});\n```\n\n## Shapes\n\n| Registration Type |\n| ----------------- |\n| circle            |\n| ellipse           |\n| group             |\n| html              |\n| image             |\n| line              |\n| path              |\n| polygon           |\n| polyline          |\n| rect              |\n| text              |\n| label             |\n| badge             |\n\nUsage:\n\nIn the [upsert](http://localhost:8000/en/manual/custom-extension/element#methods) method of the element class when customizing the shape, pass the second parameter:\n\n```ts\nthis.upsert('shape-key', 'text', { text: 'label', fontSize: 16 }, this);\n```\n"
  },
  {
    "path": "packages/site/docs/manual/graph/extensions.zh.md",
    "content": "---\ntitle: 内置扩展\norder: 4\n---\n\nG6 内置扩展及注册扩展类型如下：\n\n## 动画\n\n| 扩展          | 注册类型         | 描述     |\n| ------------- | ---------------- | -------- |\n| ComboCollapse | 'combo-collapse' | 组合收起 |\n| ComboExpand   | 'combo-expand'   | 组合展开 |\n| NodeCollapse  | 'node-collapse'  | 节点收起 |\n| NodeExpand    | 'node-expand'    | 节点展开 |\n| PathIn        | 'path-in'        | 路径进入 |\n| PathOut       | 'path-out'       | 路径退出 |\n| Fade          | 'fade'           | 渐变     |\n| Translate     | 'translate'      | 平移     |\n\n配置方式：\n\n在 `GraphOptions.[node|edge|combo].animation.[stage]` 中使用，示例：\n\n```ts\nconst graph = new Graph({\n  // ... 其他配置\n  node: {\n    animation: {\n      update: 'translate', // 更新阶段仅使用平移动画\n    },\n  },\n});\n```\n\n## 交互\n\n| 扩展                      | 注册类型                      | 描述                   |\n| ------------------------- | ----------------------------- | ---------------------- |\n| BrushSelect               | 'brush-select'                | 框选                   |\n| ClickSelect               | 'click-select'                | 点击选中               |\n| CollapseExpand            | 'collapse-expand'             | 展开/收起元素          |\n| CreateEdge                | 'create-edge'                 | 创建边                 |\n| DragCanvas                | 'drag-canvas'                 | 拖拽画布               |\n| DragElementForce          | 'drag-element-force'          | 力导向拖拽元素         |\n| DragElement               | 'drag-element'                | 拖拽元素               |\n| FixElementSize            | 'fix-element-size'            | 缩放画布时固定元素大小 |\n| FocusElement              | 'focus-element'               | 聚焦元素               |\n| HoverActivate             | 'hover-activate'              | 悬停激活               |\n| LassoSelect               | 'lasso-select'                | 套索选择               |\n| OptimizeViewportTransform | 'optimize-viewport-transform' | 操作画布时隐藏元素     |\n| ScrollCanvas              | 'scroll-canvas'               | 滚动画布               |\n| ZoomCanvas                | 'zoom-canvas'                 | 缩放画布               |\n\n配置方式：\n\n在 `GraphOptions.behaviors` 中配置，示例：\n\n```ts\nconst graph = new Graph({\n  // ... 其他配置\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n});\n```\n\n## 元素\n\n### 节点\n\n| 扩展     | 注册类型 | 描述       |\n| -------- | -------- | ---------- |\n| circle   | Circle   | 圆形节点   |\n| diamond  | Diamond  | 菱形节点   |\n| ellipse  | Ellipse  | 椭圆节点   |\n| hexagon  | Hexagon  | 六边形节点 |\n| html     | HTML     | HTML节点   |\n| image    | Image    | 图片节点   |\n| rect     | Rect     | 矩形节点   |\n| star     | Star     | 星形节点   |\n| donut    | Donut    | 甜甜圈节点 |\n| triangle | Triangle | 三角形节点 |\n\n配置方式：\n\n1. 在 `GraphOptions.data.nodes[number].type` 中配置；\n2. 在 `GraphOptions.node.type` 中配置；\n\n```ts\nconst graph = new Graph({\n  // ... 其他配置\n  data: {\n    nodes: [{ id: 'node-1', type: 'circle' }],\n  },\n  node: {\n    type: 'circle',\n  },\n});\n```\n\n### 边\n\n| 扩展            | 注册类型           | 描述               |\n| --------------- | ------------------ | ------------------ |\n| Cubic           | 'cubic'            | 三次贝塞尔曲线     |\n| Line            | 'line'             | 直线               |\n| Polyline        | 'polyline'         | 折线               |\n| Quadratic       | 'quadratic'        | 二次贝塞尔曲线     |\n| CubicHorizontal | 'cubic-horizontal' | 水平三次贝塞尔曲线 |\n| CubicVertical   | 'cubic-vertical'   | 垂直三次贝塞尔曲线 |\n| CubicRadial     | 'cubic-radial'     | 径向三次贝塞尔曲线 |\n\n配置方式同 `节点`：\n\n1. 在 `GraphOptions.data.edges[number].type` 中配置；\n2. 在 `GraphOptions.edge.type` 中配置；\n\n### 组合\n\n| 扩展        | 注册类型 | 描述     |\n| ----------- | -------- | -------- |\n| CircleCombo | 'circle' | 圆形组合 |\n| RectCombo   | 'rect'   | 矩形组合 |\n\n配置方式同 `节点`：\n\n1. 在 `GraphOptions.data.combos[number].type` 中配置；\n2. 在 `GraphOptions.combo.type` 中配置；\n\n## 布局\n\n| 扩展                | 注册类型         | 描述                   |\n| ------------------- | ---------------- | ---------------------- |\n| AntVDagreLayout     | 'antv-dagre'     | AntV Dagre 布局        |\n| ComboCombinedLayout | 'combo-combined' | 组合布局               |\n| CompactBoxLayout    | 'compact-box'    | 紧凑树                 |\n| ForceAtlas2Layout   | 'force-atlas2'   | ForceAlas2 力导向布局  |\n| CircularLayout      | 'circular'       | 环形布局               |\n| ConcentricLayout    | 'concentric'     | 同心圆布局             |\n| D3ForceLayout       | 'd3-force'       | D3 力导向布局          |\n| DagreLayout         | 'dagre'          | Dagre 布局             |\n| DendrogramLayout    | 'dendrogram'     | 生态树                 |\n| ForceLayout         | 'force'          | 力导向布局             |\n| FruchtermanLayout   | 'fruchterman'    | Fruchterman 力导向布局 |\n| GridLayout          | 'grid'           | 网格布局               |\n| IndentedLayout      | 'indented'       | 缩进树                 |\n| MDSLayout           | 'mds'            | 高维数据降维布局       |\n| MindmapLayout       | 'mindmap'        | 脑图树                 |\n| RadialLayout        | 'radial'         | 径向布局               |\n| RandomLayout        | 'random'         | 随机布局               |\n\n配置方式：\n\n在 `GraphOptions.layout` 中配置，示例：\n\n```ts\nconst graph = new Graph({\n  // ... 其他配置\n  layout: {\n    type: 'force',\n  },\n});\n```\n\n## 色板\n\n<embed src=\"@/common/manual/getting-started/extensions/palettes.md\"></embed>\n\n配置方式：\n\n在 `GraphOptions.[node|edge|combo].palette` 中配置，示例：\n\n```ts\nconst graph = new Graph({\n  // ... 其他配置\n  node: {\n    palette: 'tableau',\n  },\n});\n```\n\n## 主题\n\n| 注册类型 | 描述     |\n| -------- | -------- |\n| dark     | 深色主题 |\n| light    | 浅色主题 |\n\n配置方式：\n\n在 `GraphOptions.theme` 中配置，示例：\n\n```ts\nconst graph = new Graph({\n  // ... 其他配置\n  theme: 'dark',\n});\n```\n\n## 插件\n\n| 扩展           | 注册类型           | 描述       |\n| -------------- | ------------------ | ---------- |\n| BubbleSets     | 'bubble-sets'      | 气泡集     |\n| EdgeFilterLens | 'edge-filter-lens' | 边过滤镜   |\n| GridLine       | 'grid-line'        | 网格线     |\n| Background     | 'background'       | 背景       |\n| Contextmenu    | 'contextmenu'      | 上下文菜单 |\n| Fisheye        | 'fisheye'          | 鱼眼放大镜 |\n| Fullscreen     | 'fullscreen'       | 全屏展示   |\n| History        | 'history'          | 历史记录   |\n| Hull           | 'hull'             | 轮廓包围   |\n| Legend         | 'legend'           | 图例       |\n| Minimap        | 'minimap'          | 小地图     |\n| Snapline       | 'snapline'         | 对齐线     |\n| Timebar        | 'timebar'          | 时间条     |\n| Toolbar        | 'toolbar'          | 工具栏     |\n| Tooltip        | 'tooltip'          | 提示框     |\n| Watermark      | 'watermark'        | 水印       |\n\n配置方式：\n\n在 `GraphOptions.plugins` 中配置，示例：\n\n```ts\nconst graph = new Graph({\n  // ... 其他配置\n  plugins: ['minimap', 'contextmenu'],\n});\n```\n\n## 数据转换\n\n| 扩展                 | 注册类型                 | 描述     |\n| -------------------- | ------------------------ | -------- |\n| ProcessParallelEdges | 'process-parallel-edges' | 平行边   |\n| PlaceRadialLabels    | 'place-radial-labels'    | 径向标签 |\n\n配置方式：\n\n在 `GraphOptions.transforms` 中配置，示例：\n\n```ts\nconst graph = new Graph({\n  // ... 其他配置\n  transform: ['process-parallel-edges', 'place-radial-labels'],\n});\n```\n\n## 图形\n\n| 注册类型 | 描述   |\n| -------- | ------ |\n| circle   | 圆形   |\n| ellipse  | 椭圆   |\n| group    | 分组   |\n| html     | HTML   |\n| image    | 图片   |\n| line     | 直线   |\n| path     | 路径   |\n| polygon  | 多边形 |\n| polyline | 折线   |\n| rect     | 矩形   |\n| text     | 文本   |\n| label    | 标签   |\n| badge    | 徽标   |\n\n使用方式：\n\n自定义图形时，元素类成员方法 [upsert](/manual/element/node/custom-node) 方法第二个参数传入：\n\n```ts\nthis.upsert('shape-key', 'text', { text: 'label', fontSize: 16 }, this);\n```\n"
  },
  {
    "path": "packages/site/docs/manual/graph/graph.en.md",
    "content": "---\ntitle: Graph\norder: 0\n---\n\n## Overview\n\n### Definition of Graph\n\nIn Chinese, the character \"图\" (Graph) can often be used to represent many different concepts, such as image,shape,and chart or diagram,etc.\n\nIn Graph Theory, a graph is a mathematical structure used to model pairwise relationships between objects, which we typically represent with nodes (or Vertex) and edges (or Link) to denote the objects and the relationships between them.\n\nThe \"graph\" in G6:\n\n- Conceptually, it is the \"graph\" from Graph Theory, a data structure composed of nodes and edges.\n- Visually, a \"graph\" is a figure composed of a set of graphical elements representing nodes and edges.\n- In terms of code implementation, a \"graph\" is a class capable of transforming data into a graphical display.\n\n### Types of Graph\n\nGraph Theory categorizes graphs into many different types based on their structure and properties, such as:\n\n- Directed Graph and Undirected Graph\n- Weighted Graph and Unweighted Graph\n- Simple Graph and Multigraph\n- Cyclic Graph and Acyclic Graph\n- Connected Graph and Disconnected Graph\n- Complete Graph and Non-Complete Graph\n- Sparse Graph and Dense Graph\n- ...\n\nIn G6, we provide a universal graph representation capable of depicting the various types of graphs mentioned above, for example:\n\n- Directed Graph and Undirected Graph: Defined by the start and end points of the edges.\n- Weighted Graph and Unweighted Graph: Defined by the `weight` data on the edges.\n- Simple Graph and Multigraph: Defined by the uniqueness of the edges.\n- ...\n\n### Use Scenarios\n\nGraphs are a very versatile data structure that can be used to represent a variety of scenarios, such as:\n\n- Social Networks\n- Knowledge Graphs\n- Traffic Networks\n- Power Grids\n\nIn G6, we provide a rich expression capability for graphs that can meet the needs of different scenarios. We also offer a wealth of interactive and animated effects to make the graphs more vivid and intuitive.\n\n## Use G6 Graph\n\nTo create a Graph with G6, you first need to import the `@antv/g6` library, and then instantiate the Graph class.\n\n> For installation instructions, refer to: [Getting Started - Installation](/en/manual/getting-started/installation)\n\nThe Graph class accepts an instantiation argument object, known as **options** (Options, in visualization theory it is referred to as: `Specification`), which is used to configure the graph's data, element styles, layout, interactions, etc.\n\n```typescript\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  // ... other other options\n});\n```\n\n:::warning{title=note}\nThe instantiation process only configures the basic information of the graph. To render the graph onto the page, you still need to call the `render` method.\n:::\n\n- To learn how to quickly create a graph, please refer to [Quick Start](/en/manual/getting-started/quick-start).\n- For more detailed information about the configuration options, please refer to [Options](/en/manual/graph/option).\n- To gain an in-depth understanding of the concepts within the configuration options, please read the rest of the content in this section.\n"
  },
  {
    "path": "packages/site/docs/manual/graph/graph.zh.md",
    "content": "---\ntitle: Graph - 图\norder: 0\n---\n\n## 什么是 Graph\n\n中文字“图”在大家的传统认知里指的是图画、图像，而图论与可视化中的“图”—— Graph 则有着更精确的定位：主体（objects）与关系（relationships）的组成。它甚至不局限于视觉，主体与关系的数据也可以称为图。\n\n> —— 摘自 [AntV 专栏](https://zhuanlan.zhihu.com/aiux-antv) 文章：[Graph Visualization · 知多少 之 《HelloWorld 图可视化》](https://zhuanlan.zhihu.com/p/83685690)。\n\n在 G6 中，Graph 对象是图的载体，它包含了图上的所有元素（节点、边等），同时挂载了图的相关操作（如交互监听、元素操作、渲染等）。\n\nGraph 对象的完整生命周期包括：\n\n1. **创建**: 通过 `new Graph(options)` 实例化\n2. **初始化**: 在创建时进行内部初始化\n3. **渲染**: 调用 `graph.render()` 进行首次渲染\n4. **更新**: 通过各种 API 更新图的数据和配置\n5. **销毁**: 调用 `graph.destroy()` 销毁实例并释放资源\n\n## 使用 G6 Graph\n\n要使用 G6 创建 Graph，首先需要引入 `@antv/g6` 库，然后实例化 Graph 类。\n\n> 安装教程参考：[开始使用 - 安装](/manual/getting-started/installation)\n\nGraph 类接收一个实例化参数对象，称之为**配置项**(Options，在可视化理论中将其称为：`Specification`)，用于配置图的数据、元素样式、布局、交互等。\n\n```typescript\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container', // 通过 DOM ID 指定容器\n  width: 800, // 画布宽高（若容器已设尺寸可省略）\n  height: 600,\n  data: {\n    // 初始数据\n    nodes: [{ id: 'start', data: { label: 'Hello G6!' } }],\n  },\n});\n```\n\n:::warning{title=注意}\n实例化过程仅是配置图的基本信息，要将图渲染到页面上，还需要调用 `render` 方法\n:::\n\n## 图配置项\n\n通过下表速查图的配置项，更多类型定义说明和详细用法请参考 [API - 图配置项](/manual/graph/option)。\n\n| 属性             | 类型                               | 默认值      | 描述                                                          |\n| ---------------- | ---------------------------------- | ----------- | ------------------------------------------------------------- |\n| container        | string \\| HTMLElement \\| Canvas    | -           | 图容器，可以是 DOM 元素 ID、DOM 元素实例或 Canvas 实例        |\n| width            | number                             | 容器宽度    | 画布宽度(像素)                                                |\n| height           | number                             | 容器高度    | 画布高度(像素)                                                |\n| autoFit          | 'view' \\| 'center' \\| object       | -           | 自动适配策略，'view'(适应视图)或'center'(居中)                |\n| autoResize       | boolean                            | false       | 是否在窗口大小变化时自动调整画布大小                          |\n| background       | string                             | -           | 画布背景色，也作为导出图片时的背景色                          |\n| canvas           | CanvasConfig                       | -           | 画布配置                                                      |\n| cursor           | Cursor                             | `'default'` | 指针样式                                                      |\n| devicePixelRatio | number                             | 2           | 设备像素比                                                    |\n| padding          | number \\| number[]                 | -           | 画布内边距，在自适应时会根据内边距进行适配                    |\n| renderer         | (layer: string) => IRenderer       | -           | 手动指定渲染器                                                |\n| rotation         | number                             | 0           | 旋转角度(弧度)                                                |\n| zoom             | number                             | 1           | 缩放比例                                                      |\n| zoomRange        | [number, number]                   | [0.01, 10]  | 缩放比例的限制范围                                            |\n| x                | number                             | -           | 视口 x 坐标                                                   |\n| y                | number                             | -           | 视口 y 坐标                                                   |\n| data             | GraphData                          | -           | 图数据，详见 [数据](/manual/data)                             |\n| node             | NodeOptions                        | -           | 节点全局配置，详见 [节点](/manual/element/node/overview)      |\n| edge             | EdgeOptions                        | -           | 边全局配置，详见 [边](/manual/element/edge/overview)          |\n| combo            | ComboOptions                       | -           | 组合全局配置，详见 [组合](/manual/element/combo/overview)     |\n| animation        | boolean \\| AnimationEffectTiming   | -           | 全局动画配置，详见 [动画](/manual/animation/animation)        |\n| theme            | string \\| false                    | `'light'`   | 主题配置，支持 `'light'`、`'dark'` 或自定义主题名             |\n| layout           | LayoutOptions \\| LayoutOptions[]   | -           | 布局配置，详见 [布局](/manual/layout/overview)                |\n| behaviors        | (string \\| CustomBehaviorOption)[] | -           | 交互行为配置，详见 [交互](/manual/behavior/overview)          |\n| plugins          | (string \\| CustomPluginOption)[]   | -           | 插件配置，详见 [插件](/manual/plugin/overview)                |\n| transforms       | TransformOptions                   | -           | 数据转换器配置，详见 [数据转换器](/manual/transform/overview) |\n\n## 图属性\n\n图实例提供了一些只读属性，用于获取图的状态信息：\n\n| 属性      | 类型    | 描述                     |\n| --------- | ------- | ------------------------ |\n| destroyed | boolean | 当前图实例是否已被销毁   |\n| rendered  | boolean | 当前图实例是否已完成渲染 |\n\n## 参考示例\n\n### 完整的创建和配置示例\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 200,\n  width: 300,\n  height: 200,\n  padding: 30,\n  autoResize: true,\n\n  // 视口配置\n  zoom: 0.8,\n  autoFit: 'view',\n  padding: 20,\n\n  // 主题配置\n  theme: 'dark',\n\n  // 节点配置\n  node: {\n    style: {\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n    },\n  },\n\n  // 边配置\n  edge: {\n    style: {\n      stroke: '#A4D3EE',\n      lineWidth: 1.5,\n      endArrow: true,\n    },\n  },\n\n  // 布局配置\n  layout: {\n    type: 'force',\n    preventOverlap: true,\n    linkDistance: 100,\n  },\n\n  // 交互行为\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n\n  // 初始数据\n  data: {\n    nodes: [\n      { id: 'node1', data: { label: '节点1' } },\n      { id: 'node2', data: { label: '节点2' } },\n    ],\n    edges: [{ source: 'node1', target: 'node2', data: { label: '关系' } }],\n  },\n});\n\ngraph.render();\n```\n\n```typescript\nimport { Graph } from '@antv/g6';\n\n// 创建图实例\nconst graph = new Graph({\n  // 基础配置\n  container: 'container',\n  width: 300,\n  height: 200,\n  padding: 30,\n  autoResize: true,\n\n  // 视口配置\n  zoom: 0.8,\n  autoFit: 'view',\n  padding: 20,\n\n  // 主题配置\n  theme: 'dark',\n\n  // 节点配置\n  node: {\n    style: {\n      fill: '#7FFFD4',\n      stroke: '#5CACEE',\n      lineWidth: 2,\n    },\n  },\n\n  // 边配置\n  edge: {\n    style: {\n      stroke: '#A4D3EE',\n      lineWidth: 1.5,\n      endArrow: true,\n    },\n  },\n\n  // 布局配置\n  layout: {\n    type: 'force',\n    preventOverlap: true,\n    linkDistance: 100,\n  },\n\n  // 交互行为\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n\n  // 初始数据\n  data: {\n    nodes: [\n      { id: 'node1', data: { label: '节点1' } },\n      { id: 'node2', data: { label: '节点2' } },\n    ],\n    edges: [{ source: 'node1', target: 'node2', data: { label: '关系' } }],\n  },\n});\n\n// 渲染图\ngraph.render();\n```\n\n- 要了解如何快速创建一个图，请参考[快速上手](/manual/getting-started/quick-start)。\n- 要深入了解配置项中个部分的概念，请阅读本章节的其他内容。\n"
  },
  {
    "path": "packages/site/docs/manual/graph/option.en.md",
    "content": "---\ntitle: Options\norder: 0\n---\n\n## autoFit\n\n> _{ type: 'view'; options?: [FitViewOptions](#fitviewoptions); animation?: [ViewportAnimationEffectTiming](#viewportanimationeffecttiming); } \\| { type: 'center'; animation?: [ViewportAnimationEffectTiming](#viewportanimationeffecttiming); } \\| 'view' \\| 'center'_\n\nWhether to automatically fit the canvas. ⚠️ **Note**: Each time `render` is executed, it will adapt according to `autoFit`.\n\nTwo basic adaptation modes:\n\n- `'view'` - Automatically scale to ensure all content is visible within the view\n- `'center'` - Center the content without changing the zoom level\n\nMore precise adaptation control can be achieved through object form:\n\n```javascript\nconst graph = new Graph({\n  autoFit: {\n    type: 'view', // Adaptation type: 'view' or 'center'\n    options: {\n      // Only applicable to 'view' type\n      when: 'overflow', // When to adapt: 'overflow' (only when content overflows) or 'always' (always adapt)\n      direction: 'x', // Adaptation direction: 'x', 'y', or 'both'\n    },\n    animation: {\n      // Adaptation animation effect\n      duration: 1000, // Animation duration (milliseconds)\n      easing: 'ease-in-out', // Animation easing function\n    },\n  },\n});\n```\n\n#### FitViewOptions\n\n| Property  | Description                                                                                                                                                             | Type                       | Default    | Required |\n| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ---------- | -------- |\n| when      | Adaptation occurs under the following conditions <br/> - `'overflow'` adapt only when content overflows <br/> - `'always'` always adapt                                 | `'overflow'` \\| `'always'` | `'always'` |          |\n| direction | Adapt only in the specified direction <br/> - `'x'` adapt only in x direction <br/> - `'y'` adapt only in y direction <br/> - `'both'` adapt in both x and y directions | `'x'` \\| `'y'` \\| `'both'` | `'both'`   |          |\n\n#### ViewportAnimationEffectTiming\n\n```typescript\ntype ViewportAnimationEffectTiming =\n  | boolean // true to enable default animation, false to disable animation\n  | {\n      easing?: string; // Animation easing function: 'ease-in-out', 'ease-in', 'ease-out', 'linear'\n      duration?: number; // Animation duration (milliseconds)\n    };\n```\n\n## autoResize\n\n> _boolean_ **Default:** `false`\n\nWhether to automatically resize the canvas.\n\nImplemented based on the `window.onresize` event. When the browser window size changes, the canvas will automatically resize to fit the container.\n\n## background\n\n> _string_\n\nCanvas background color.\n\nThis color is used as the background color when exporting images. Any valid CSS color value can be used, such as hexadecimal, RGB, RGBA, etc.\n\n## canvas\n\n> [CanvasConfig](#canvasconfig)\n\nCanvas configuration. Related configuration items under GraphOptions (such as `container`, `width`, `height`, `devicePixelRatio`, `background`, `cursor`) are shortcut configuration items and will be converted to canvas configuration items.\n\n#### CanvasConfig\n\n| Property         | Description                                                                                | Type                                                                           | Default | Required |\n| ---------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------- | -------- |\n| container        | Canvas container                                                                           | string \\| HTMLElement                                                          | -       |          |\n| devicePixelRatio | Device pixel ratio                                                                         | number                                                                         | -       |          |\n| width            | Canvas width                                                                               | number                                                                         | -       |          |\n| height           | Canvas height                                                                              | number                                                                         | -       |          |\n| cursor           | Cursor style, same as [GraphOptions.cursor](#cursor)                                       | string                                                                         | -       |          |\n| background       | Canvas background color                                                                    | string                                                                         | -       |          |\n| renderer         | Renderer, same as [GraphOptions.renderer](#renderer)                                       | (layer: `'background'` \\| `'main'` \\| `'label'` \\| `'transient'`) => IRenderer | -       |          |\n| enableMultiLayer | Whether to enable multi-layer. Non-dynamic parameter, effective only during initialization | boolean                                                                        | -       |          |\n\n## container\n\n> _string \\|_ _HTMLElement_ _\\|_ Canvas\n\nCanvas container, can be one of the following three assignments:\n\n- ID string of the DOM element, such as `'container'`\n- HTML element object, such as `document.getElementById('container')`\n- Canvas instance, such as `new Canvas(options)`, where `options` is of type [CanvasConfig](#canvasconfig).\n\n## cursor\n\n> string\n\nCursor style, controls the cursor shape when hovering over the canvas. Any valid CSS cursor value can be used.\n\nSupported values include: `'auto'`, `'default'`, `'none'`, `'context-menu'`, `'help'`, `'pointer'`, `'progress'`, `'wait'`, `'cell'`, `'crosshair'`, `'text'`, `'vertical-text'`, `'alias'`, `'copy'`, `'move'`, `'no-drop'`, `'not-allowed'`, `'grab'`, `'grabbing'`, `'all-scroll'`, `'col-resize'`, `'row-resize'`, `'n-resize'`, `'e-resize'`, `'s-resize'`, `'w-resize'`, `'ne-resize'`, `'nw-resize'`, `'se-resize'`, `'sw-resize'`, `'ew-resize'`, `'ns-resize'`, `'nesw-resize'`, `'nwse-resize'`, `'zoom-in'`, `'zoom-out'.\n\nCursor values are referenced from [MDN - cursor](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor).\n\n## devicePixelRatio\n\n> _number_\n\nDevice pixel ratio.\n\nUsed for high-definition screens, the default is [window.devicePixelRatio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio).\n\n## width\n\n> _number_\n\nCanvas width. If not set, the container width will be automatically obtained.\n\n## height\n\n> _number_\n\nCanvas height. If not set, the container height will be automatically obtained.\n\n## renderer\n\n> _(layer: 'background' \\| 'main' \\| 'label' \\| 'transient') =>_ _IRenderer_\n\nManually specify the renderer\n\nG6 uses a layered rendering approach, divided into four layers: `background`, `main`, `label`, `transient`. Users can set the renderer for each layer of the canvas through this configuration item.\n\n**Example**: Use SVG renderer for rendering\n\n```javascript\nimport { Renderer as SVGRenderer } from '@antv/g-svg';\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  renderer: () => new SVGRenderer(),\n});\n```\n\n## padding\n\n> _number \\| number[]_\n\nCanvas padding\n\nUsually, during adaptation, it will be adapted according to the padding. It can be a single value (same padding on all sides) or an array form (specify the padding for top, right, bottom, left in order).\n\n**Example:**\n\n```javascript\n// Single value\nconst graph1 = new Graph({\n  padding: 20, // 20 pixels of padding on all sides\n});\n\n// Array form\nconst graph2 = new Graph({\n  padding: [20, 40, 20, 40], // Padding for top, right, bottom, left\n});\n```\n\n## rotation\n\n> _number_ **Default:** `0`\n\nRotation angle (in radians)\n\n## x\n\n> _number_\n\nViewport x coordinate, sets the initial horizontal position of the viewport.\n\n## y\n\n> _number_\n\nViewport y coordinate, sets the initial vertical position of the viewport.\n\n## zoom\n\n> _number_ **Default:** `1`\n\nSets the initial zoom level of the viewport, 1 means 100% (original size).\n\n## zoomRange\n\n> _[number, number]_ **Default:** `[0.01, 10]`\n\nZoom range, limits the minimum and maximum scale that users can zoom.\n\n## animation\n\n> _boolean \\| [AnimationEffectTiming](#animationeffecttiming)_\n\nEnable or disable global animation\n\nWhen configured as an animation option, animation will be enabled, and this animation configuration will be used as the base configuration for global animation.\n\n#### AnimationEffectTiming\n\n| Property   | Description                    | Type                                                                | Default     | Required |\n| ---------- | ------------------------------ | ------------------------------------------------------------------- | ----------- | -------- |\n| delay      | Animation delay time           | number                                                              | -           |          |\n| direction  | Animation direction            | `'alternate'` \\| `'alternate-reverse'` \\| `'normal'` \\| `'reverse'` | `'forward'` |          |\n| duration   | Animation duration             | number                                                              | -           |          |\n| easing     | Animation easing function      | string                                                              | -           |          |\n| fill       | Fill mode after animation ends | `'auto'` \\| `'backwards'` \\| `'both'` \\| `'forwards'` \\| `'none'`   | `'none'`    |          |\n| iterations | Animation iteration count      | number                                                              | -           |          |\n\n**Example:**\n\n```javascript\n// Simple enable\nconst graph1 = new Graph({\n  animation: true,\n});\n\n// Detailed configuration\nconst graph2 = new Graph({\n  animation: {\n    duration: 500, // Animation duration (milliseconds)\n    easing: 'ease-in-out', // Easing function\n  },\n});\n```\n\n## data\n\n> [GraphData](#graphdata)\n\nData.\n\n#### GraphData\n\n| Property | Description | Type                      | Default | Required |\n| -------- | ----------- | ------------------------- | ------- | -------- |\n| nodes    | Node data   | [NodeData](#nodedata)[]   | -       | ✓        |\n| edges    | Edge data   | [EdgeData](#edgedata)[]   | -       | ✓        |\n| combos   | Combo data  | [ComboData](#combodata)[] | -       | ✓        |\n\n#### NodeData\n\n| Property | Description                                                                                                                                         | Type           | Default | Required |\n| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------- | -------- |\n| id       | Unique identifier for the node, used to distinguish different nodes                                                                                 | string         | -       | ✓        |\n| type     | Node type, built-in node type name or custom node name                                                                                              | string         | -       |          |\n| data     | Node data, used to store custom data for the node, such as node name, description, etc. Can be accessed in style mapping through callback functions | object         | -       |          |\n| style    | Node style, including visual attributes such as position, size, color, etc.                                                                         | object         | -       |          |\n| states   | Initial state of the node, such as selected, activated, hovered, etc.                                                                               | string[]       | -       |          |\n| combo    | ID of the combo to which it belongs, used to organize the hierarchical relationship of nodes, if none, it is null                                   | string \\| null | -       |          |\n| children | Collection of child node IDs, used only in tree graph scenarios                                                                                     | string[]       | -       |          |\n\n#### EdgeData\n\n| Property | Description                                                                                                    | Type     | Default | Required |\n| -------- | -------------------------------------------------------------------------------------------------------------- | -------- | ------- | -------- |\n| source   | Starting node ID of the edge                                                                                   | string   | -       | ✓        |\n| target   | Target node ID of the edge                                                                                     | string   | -       | ✓        |\n| id       | Unique identifier for the edge                                                                                 | string   | -       |          |\n| type     | Edge type, built-in edge type name or custom edge name                                                         | string   | -       |          |\n| data     | Edge data, used to store custom data for the edge, can be accessed in style mapping through callback functions | object   | -       |          |\n| style    | Edge style, including visual attributes such as line color, width, arrow, etc.                                 | object   | -       |          |\n| states   | Initial state of the edge                                                                                      | string[] | -       |          |\n\n#### ComboData\n\n| Property | Description                                                                                                      | Type           | Default | Required |\n| -------- | ---------------------------------------------------------------------------------------------------------------- | -------------- | ------- | -------- |\n| id       | Unique identifier for the combo                                                                                  | string         | -       | ✓        |\n| type     | Combo type, built-in combo type name or custom combo name                                                        | string         | -       |          |\n| data     | Combo data, used to store custom data for the combo, can be accessed in style mapping through callback functions | object         | -       |          |\n| style    | Combo style                                                                                                      | object         | -       |          |\n| states   | Initial state of the combo                                                                                       | string[]       | -       |          |\n| combo    | Parent combo ID. If there is no parent combo, it is null                                                         | string \\| null | -       |          |\n\n**Example:**\n\n```javascript\nconst graph = new Graph({\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 100 } },\n      { id: 'node2', style: { x: 200, y: 200 } },\n    ],\n    edges: [{ id: 'edge1', source: 'node1', target: 'node2' }],\n    combos: [{ id: 'combo1', style: { x: 150, y: 150 } }],\n  },\n});\n```\n\n- Read [Data](/en/manual/data) to learn more about graph data, including but not limited to data formats, how to manipulate data, etc.\n\n## node\n\n> [NodeOptions](#nodeoptions)\n\nNode configuration options.\n\n#### NodeOptions\n\n| Property  | Description                                                                      | Type                                                     | Default  | Required |\n| --------- | -------------------------------------------------------------------------------- | -------------------------------------------------------- | -------- | -------- |\n| type      | Node type, built-in node type name or custom node name                           | [Type](/en/manual/element/node/base-node#type)           | `circle` |          |\n| style     | Node style, including color, size, etc.                                          | [Style](/en/manual/element/node/base-node#style)         | -        |          |\n| state     | Define the style of the node in different states                                 | [State](/en/manual/element/node/base-node#state)         | -        |          |\n| palette   | Define the color palette of the node, used to map colors based on different data | [Palette](/en/manual/element/node/base-node#palette)     | -        |          |\n| animation | Define the animation effect of the node                                          | [Animation](/en/manual/element/node/base-node#animation) | -        |          |\n\nSee [Node](/en/manual/element/node/base-node) for details\n\n**Example:**\n\n```javascript\nconst graph = new Graph({\n  node: {\n    type: 'circle', // Node type\n    style: {\n      fill: '#e6f7ff', // Fill color\n      stroke: '#91d5ff', // Border color\n      lineWidth: 1, // Border width\n      r: 20, // Radius\n      labelText: (d) => d.id, // Label text\n    },\n    // Node state style\n    state: {\n      hover: {\n        lineWidth: 2,\n        stroke: '#69c0ff',\n      },\n      selected: {\n        fill: '#bae7ff',\n        stroke: '#1890ff',\n        lineWidth: 2,\n      },\n    },\n  },\n});\n```\n\n## edge\n\n> [EdgeOptions](#edgeoptions)\n\nEdge configuration options\n\n#### EdgeOptions\n\n| Property  | Description                                                                      | Type                                                     | Default | Required |\n| --------- | -------------------------------------------------------------------------------- | -------------------------------------------------------- | ------- | -------- |\n| type      | Edge type, built-in edge type name or custom edge name                           | [Type](/en/manual/element/edge/base-edge#type)           | `line`  |          |\n| style     | Edge style, including color, size, etc.                                          | [Style](/en/manual/element/edge/base-edge#style)         | -       |          |\n| state     | Define the style of the edge in different states                                 | [State](/en/manual/element/edge/base-edge#state)         | -       |          |\n| palette   | Define the color palette of the edge, used to map colors based on different data | [Palette](/en/manual/element/edge/base-edge#palette)     | -       |          |\n| animation | Define the animation effect of the edge                                          | [Animation](/en/manual/element/edge/base-edge#animation) | -       |          |\n\nSee [Edge](/en/manual/element/edge/base-edge) for details\n\n**Example:**\n\n```javascript\nconst graph = new Graph({\n  edge: {\n    type: 'polyline', // Edge type\n    style: {\n      stroke: '#91d5ff', // Edge color\n      lineWidth: 2, // Edge width\n      endArrow: true, // Whether there is an arrow\n    },\n    // Edge state style\n    state: {\n      selected: {\n        stroke: '#1890ff',\n        lineWidth: 3,\n      },\n    },\n  },\n});\n```\n\n## combo\n\n> [ComboOptions](#combooptions)\n\nCombo configuration options\n\n| Property  | Description                                                                       | Type                                                       | Default  | Required |\n| --------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------- | -------- | -------- |\n| type      | Combo type, built-in combo type name or custom combo name                         | [Type](/en/manual/element/combo/base-combo#type)           | `circle` |          |\n| style     | Combo style, including color, size, etc.                                          | [Style](/en/manual/element/combo/base-combo#style)         | -        |          |\n| state     | Define the style of the combo in different states                                 | [State](/en/manual/element/combo/base-combo#state)         | -        |          |\n| palette   | Define the color palette of the combo, used to map colors based on different data | [Palette](/en/manual/element/combo/base-combo#palette)     | -        |          |\n| animation | Define the animation effect of the combo                                          | [Animation](/en/manual/element/combo/base-combo#animation) | -        |          |\n\nSee [Combo](/en/manual/element/combo/base-combo) for details\n\n**Example:**\n\n```javascript\nconst graph = new Graph({\n  combo: {\n    type: 'circle', // Combo type\n    style: {\n      fill: '#f0f0f0', // Background color\n      stroke: '#d9d9d9', // Border color\n      lineWidth: 1, // Border width\n    },\n    // Combo state style\n    state: {\n      selected: {\n        stroke: '#1890ff',\n        lineWidth: 2,\n      },\n    },\n  },\n});\n```\n\n## layout\n\n> _CustomLayoutOptions \\| CustomLayoutOptions[]_\n\nLayout configuration options, can be an object (normal layout) or an array (pipeline layout).\n\n**Example**:\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'force', // Force-directed layout\n    preventOverlap: true, // Prevent node overlap\n    nodeStrength: -50, // Repulsion between nodes\n    edgeStrength: 0.5, // Elastic coefficient of edges\n    iterations: 200, // Number of iterations\n    animation: true, // Enable layout animation\n  },\n});\n```\n\n## theme\n\n> _false \\| 'light' \\| 'dark' \\| string_\n\nSet the theme of the chart, can be the built-in `'light'`, `'dark'` theme, or the name of a custom theme. Set to `false` to use no theme.\n\n## behaviors\n\n> _(string \\| [CustomExtensionOptions](#customextensionoptions) \\| ((this:Graph) =>CustomExtensionOptions))[]_\n\nConfigure the interaction behaviors of the chart, can be a string (using default configuration), an object (custom configuration), or a function (dynamic configuration, the graph instance can be accessed within the function).\n\n**Example:**\n\n```javascript\nconst graph = new Graph({\n  behaviors: [\n    'drag-canvas', // Enable canvas dragging with default configuration\n    'zoom-canvas', // Enable canvas zooming with default configuration\n    {\n      type: 'drag-element', // Custom configuration for dragging elements\n      key: 'drag-node-only',\n      enable: (event) => event.targetType === 'node', // Only allow dragging nodes\n    },\n    function () {\n      console.log(this); // Output graph instance\n      return {\n        type: 'hover-activate',\n      };\n    },\n  ],\n});\n```\n\n- View [Interaction Overview](/en/manual/behavior/overview) to learn more about interaction principles\n- Browse [Built-in Interactions](/en/manual/behavior/auto-adapt-label) to get a list of all built-in interactions and their configuration options\n\n## plugins\n\n> _(string \\| [CustomExtensionOptions](#customextensionoptions) \\| ((this:Graph) =>CustomExtensionOptions))[]_\n\nSet the plugins of the chart, can be a string (using default configuration), an object (custom configuration), or a function (dynamic configuration, the graph instance can be accessed within the function).\n\n**Example:**\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  plugins: [\n    'minimap', // Enable minimap with default configuration\n    {\n      type: 'grid', // Enable grid background\n      key: 'grid-plugin',\n      line: {\n        stroke: '#d9d9d9',\n        lineWidth: 1,\n      },\n    },\n    {\n      type: 'toolbar', // Enable toolbar\n      key: 'graph-toolbar',\n      position: 'top-right', // Position\n    },\n  ],\n});\n```\n\n- View [Plugin Overview](/en/manual/plugin/overview) to learn more about plugin principles\n- Browse [Built-in Plugins](/en/manual/plugin/background) to get a list of all built-in plugins and their configuration options\n\n## transforms\n\n> _(string \\| [CustomExtensionOptions](#customextensionoptions) \\| ((this:Graph) =>CustomExtensionOptions))[]_\n\nConfigure data processing, used to process data before rendering, does not affect the original data. Can be a string (using default configuration), an object (custom configuration), or a function (dynamic configuration, the graph instance can be accessed within the function).\n\n**Example:**\n\n```javascript\nconst graph = new Graph({\n  transforms: [\n    'process-parallel-edges', // Process parallel edges with default configuration\n    {\n      type: 'map-node-size', // Map node size based on node data\n      field: 'value', // Use the value of the field\n      max: 50, // Maximum radius\n      min: 20, // Minimum radius\n    },\n  ],\n});\n```\n\n- View [Data Processing Overview](/en/manual/transform/overview) to learn more about data processing principles\n- Browse [Built-in Data Processing](/en/manual/transform/map-node-size) to get a list of all built-in data processing and their configuration options\n\n#### CustomExtensionOptions\n\n```typescript\ninterface CustomExtensionOption extends Record<string, any> {\n  /** Extension type */\n  type: string;\n  /** Extension key, i.e., unique identifier */\n  key?: string;\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/graph/option.zh.md",
    "content": "---\ntitle: Options 配置项\norder: 0\n---\n\n## autoFit\n\n> _{ type: 'view'; options?: [FitViewOptions](#fitviewoptions); animation?: [ViewportAnimationEffectTiming](#viewportanimationeffecttiming); } \\| { type: 'center'; animation?: [ViewportAnimationEffectTiming](#viewportanimationeffecttiming); } \\| 'view' \\| 'center'_\n\n是否自动适应画布。⚠️ **注意**：每次执行 `render` 时，都会根据 `autoFit` 进行自适应。\n\n两种基本自适应模式：\n\n- `'view'` - 自动缩放，确保所有内容都在视图内可见\n- `'center'` - 内容居中显示，但不改变缩放比例\n\n还可通过对象形式实现更精细的自适应控制：\n\n```javascript\nconst graph = new Graph({\n  autoFit: {\n    type: 'view', // 自适应类型：'view' 或 'center'\n    options: {\n      // 仅适用于 'view' 类型\n      when: 'overflow', // 何时适配：'overflow'(仅当内容溢出时) 或 'always'(总是适配)\n      direction: 'x', // 适配方向：'x'、'y' 或 'both'\n    },\n    animation: {\n      // 自适应动画效果\n      duration: 1000, // 动画持续时间(毫秒)\n      easing: 'ease-in-out', // 动画缓动函数\n    },\n  },\n});\n```\n\n#### FitViewOptions\n\n| 属性      | 描述                                                                                                           | 类型                       | 默认值     | 必选 |\n| --------- | -------------------------------------------------------------------------------------------------------------- | -------------------------- | ---------- | ---- |\n| when      | 在以下情况下进行适配 <br/> - `'overflow'` 仅当图内容超出视口时进行适配 <br/> - `'always'` 总是进行适配         | `'overflow`' \\| `'always'` | `'always'` |      |\n| direction | 仅对指定方向进行适配 <br/> - `'x'` 仅适配 x 方向 <br/> - `'y'` 仅适配 y 方向 <br/> - `'both'` 适配 x 和 y 方向 | `'x`' \\| `'y`' \\| `'both'` | `'both'`   |      |\n\n#### ViewportAnimationEffectTiming\n\n```typescript\ntype ViewportAnimationEffectTiming =\n  | boolean // true 启用默认动画，false 禁用动画\n  | {\n      easing?: string; // 动画缓动函数：'ease-in-out'、'ease-in'、'ease-out'、'linear'\n      duration?: number; // 动画持续时间(毫秒)\n    };\n```\n\n## autoResize\n\n> _boolean_ **默认值:** `false`\n\n是否自动调整画布大小。\n\n基于 `window.onresize` 事件实现。当浏览器窗口大小变化时，画布将自动调整大小以适应容器。\n\n## background\n\n> _string_\n\n画布背景色。\n\n该颜色作为导出图片时的背景色。可以使用任何有效的 CSS 颜色值，如十六进制、RGB、RGBA 等。\n\n## canvas\n\n> [CanvasConfig](#canvasconfig)\n\n画布配置。GraphOptions 下相关配置项（如 `container`、`width`、`height`、`devicePixelRatio`、`background`、`cursor`）为快捷配置项，会被转换为 canvas 配置项。\n\n#### CanvasConfig\n\n| 属性             | 描述                                                   | 类型                                                                           | 默认值 | 必填 |\n| ---------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------ | ------ | ---- |\n| container        | 画布容器                                               | string \\| HTMLElement                                                          | -      |      |\n| devicePixelRatio | 设备像素比                                             | number                                                                         | -      |      |\n| width            | 画布宽度                                               | number                                                                         | -      |      |\n| height           | 画布高度                                               | number                                                                         | -      |      |\n| cursor           | 指针样式，与 [GraphOptions.cursor](#cursor) 配置相同   | string                                                                         | -      |      |\n| background       | 画布背景色                                             | string                                                                         | -      |      |\n| renderer         | 渲染器，与 [GraphOptions.renderer](#renderer) 配置相同 | (layer: `'background'` \\| `'main'` \\| `'label'` \\| `'transient'`) => IRenderer | -      |      |\n| enableMultiLayer | 是否启用多图层。非动态参数，仅在初始化时生效           | boolean                                                                        | -      |      |\n\n## container\n\n> _string \\|_ _HTMLElement_ _\\|_ Canvas\n\n画布容器，可以是以下三种赋值之一：\n\n- DOM 元素的 ID 字符串，如 `'container'`\n- HTML 元素对象，如 `document.getElementById('container')`\n- Canvas 实例，如 `new Canvas(options)`，其中 `options` 为 [CanvasConfig](#canvasconfig) 类型。\n\n## cursor\n\n> string\n\n指针样式，控制鼠标悬停在画布上时的光标形状。可以使用任何有效的 CSS cursor 值。\n\n支持的值有： `'auto'`、`'default'`、`'none'`、`'context-menu'`、`'help'`、`'pointer'`、`'progress'`、`'wait'`、`'cell'`、`'crosshair'`、`'text'`、`'vertical-text'`、`'alias'`、`'copy'`、`'move'`、`'no-drop'`、`'not-allowed'`、`'grab'`、`'grabbing'`、`'all-scroll'`、`'col-resize'`、`'row-resize'`、`'n-resize'`、`'e-resize'`、`'s-resize'`、`'w-resize'`、`'ne-resize'`、`'nw-resize'`、`'se-resize'`、`'sw-resize'`、`'ew-resize'`、`'ns-resize'`、`'nesw-resize'`、`'nwse-resize'`、`'zoom-in'`、`'zoom-out'`。\n\n这里的 Cursor 值参考 [MDN - cursor](https://developer.mozilla.org/zh-CN/docs/Web/CSS/cursor)。\n\n## devicePixelRatio\n\n> _number_\n\n设备像素比。\n\n用于高清屏的设备像素比，默认为 [window.devicePixelRatio](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/devicePixelRatio)。\n\n## width\n\n> _number_\n\n画布宽度。如果未设置，则会自动获取容器宽度。\n\n## height\n\n> _number_\n\n画布高度。如果未设置，则会自动获取容器高度。\n\n## renderer\n\n> _(layer: 'background' \\| 'main' \\| 'label' \\| 'transient') =>_ _IRenderer_\n\n手动指定渲染器\n\nG6 采用了分层渲染的方式，分为 `background`、`main`、`label`、`transient` 四层，用户可以通过该配置项分别设置每层画布的渲染器。\n\n**示例**: 使用 SVG 渲染器进行渲染\n\n```javascript\nimport { Renderer as SVGRenderer } from '@antv/g-svg';\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  renderer: () => new SVGRenderer(),\n});\n```\n\n## padding\n\n> _number \\| number[]_\n\n画布内边距\n\n通常在自适应时，会根据内边距进行适配。可以是单个数值（四边相同）或者数组形式（按顺序指定上、右、下、左的内边距）。\n\n**示例：**\n\n```javascript\n// 单个数值\nconst graph1 = new Graph({\n  padding: 20, // 四边均为 20 像素的内边距\n});\n\n// 数组形式\nconst graph2 = new Graph({\n  padding: [20, 40, 20, 40], // 上、右、下、左的内边距\n});\n```\n\n## rotation\n\n> _number_ **默认值:** `0`\n\n旋转角度（以弧度为单位）\n\n## x\n\n> _number_\n\n视口 x 坐标，设置视口的初始水平位置。\n\n## y\n\n> _number_\n\n视口 y 坐标，设置视口的初始垂直位置。\n\n## zoom\n\n> _number_ **默认值:** `1`\n\n设置视口的初始缩放级别，1 表示 100%（原始大小）。\n\n## zoomRange\n\n> _[number, number]_ **默认值:** `[0.01, 10]`\n\n缩放范围，限制用户可以缩放的最小和最大比例。\n\n## animation\n\n> _boolean \\| [AnimationEffectTiming](#animationeffecttiming)_\n\n启用或关闭全局动画\n\n为动画配置项时，会启用动画，并将该动画配置作为全局动画的基础配置。\n\n#### AnimationEffectTiming\n\n| 属性       | 描述                 | 类型                                                                | 默认值      | 必选 |\n| ---------- | -------------------- | ------------------------------------------------------------------- | ----------- | ---- |\n| delay      | 动画延迟时间         | number                                                              | -           |      |\n| direction  | 动画方向             | `'alternate'` \\| `'alternate-reverse'` \\| `'normal'` \\| `'reverse'` | `'forward'` |      |\n| duration   | 动画持续时间         | number                                                              | -           |      |\n| easing     | 动画缓动函数         | string                                                              | -           |      |\n| fill       | 动画结束后的填充模式 | `'auto'` \\| `'backwards'` \\| `'both'` \\| `'forwards'` \\| `'none'`   | `'none'`    |      |\n| iterations | 动画迭代次数         | number                                                              | -           |      |\n\n**示例：**\n\n```javascript\n// 简单启用\nconst graph1 = new Graph({\n  animation: true,\n});\n\n// 详细配置\nconst graph2 = new Graph({\n  animation: {\n    duration: 500, // 动画持续时间（毫秒）\n    easing: 'ease-in-out', // 缓动函数\n  },\n});\n```\n\n## data\n\n> [GraphData](#graphdata)\n\n数据。\n\n#### GraphData\n\n| 属性   | 描述     | 类型                      | 默认值 | 必选 |\n| ------ | -------- | ------------------------- | ------ | ---- |\n| nodes  | 节点数据 | [NodeData](#nodedata)[]   | -      | ✓    |\n| edges  | 边数据   | [EdgeData](#edgedata)[]   | -      | ✓    |\n| combos | 组合数据 | [ComboData](#combodata)[] | -      | ✓    |\n\n#### NodeData\n\n| 属性     | 描述                                                                                         | 类型           | 默认值 | 必选 |\n| -------- | -------------------------------------------------------------------------------------------- | -------------- | ------ | ---- |\n| id       | 节点的唯一标识符，用于区分不同的节点                                                         | string         | -      | ✓    |\n| type     | 节点类型，内置节点类型名称或者自定义节点的名称                                               | string         | -      |      |\n| data     | 节点数据，用于存储节点的自定义数据，例如节点的名称、描述等。可以在样式映射中通过回调函数获取 | object         | -      |      |\n| style    | 节点样式，包括位置、大小、颜色等视觉属性                                                     | object         | -      |      |\n| states   | 节点初始状态，如选中、激活、悬停等                                                           | string[]       | -      |      |\n| combo    | 所属的组合 ID，用于组织节点的层级关系，如果没有则为 null                                     | string \\| null | -      |      |\n| children | 子节点 ID 集合，仅在树图场景下使用                                                           | string[]       | -      |      |\n\n#### EdgeData\n\n| 属性   | 描述                                                             | 类型     | 默认值 | 必选 |\n| ------ | ---------------------------------------------------------------- | -------- | ------ | ---- |\n| source | 边起始节点 ID                                                    | string   | -      | ✓    |\n| target | 边目标节点 ID                                                    | string   | -      | ✓    |\n| id     | 边的唯一标识符                                                   | string   | -      |      |\n| type   | 边类型，内置边类型名称或者自定义边的名称                         | string   | -      |      |\n| data   | 边数据，用于存储边的自定义数据，可以在样式映射中通过回调函数获取 | object   | -      |      |\n| style  | 边样式，包括线条颜色、宽度、箭头等视觉属性                       | object   | -      |      |\n| states | 边初始状态                                                       | string[] | -      |      |\n\n#### ComboData\n\n| 属性   | 描述                                                                 | 类型           | 默认值 | 必选 |\n| ------ | -------------------------------------------------------------------- | -------------- | ------ | ---- |\n| id     | 组合的唯一标识符                                                     | string         | -      | ✓    |\n| type   | 组合类型，内置组合类型名称或者自定义组合名称                         | string         | -      |      |\n| data   | 组合数据，用于存储组合的自定义数据，可以在样式映射中通过回调函数获取 | object         | -      |      |\n| style  | 组合样式                                                             | object         | -      |      |\n| states | 组合初始状态                                                         | string[]       | -      |      |\n| combo  | 组合的父组合 ID。如果没有父组合，则为 null                           | string \\| null | -      |      |\n\n**示例：**\n\n```javascript\nconst graph = new Graph({\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 100 } },\n      { id: 'node2', style: { x: 200, y: 200 } },\n    ],\n    edges: [{ id: 'edge1', source: 'node1', target: 'node2' }],\n    combos: [{ id: 'combo1', style: { x: 150, y: 150 } }],\n  },\n});\n```\n\n- 阅读 [数据](/manual/data) 深入了解图数据，包括不限于数据格式、如何操作数据等。\n\n## node\n\n> [NodeOptions](#nodeoptions)\n\n节点配置项。\n\n#### NodeOptions\n\n| 属性      | 描述                                         | 类型                                                  | 默认值   | 必选 |\n| --------- | -------------------------------------------- | ----------------------------------------------------- | -------- | ---- |\n| type      | 节点类型，内置节点类型名称或自定义节点的名称 | [Type](/manual/element/node/base-node#type)           | `circle` |      |\n| style     | 节点样式，包括颜色、大小等                   | [Style](/manual/element/node/base-node#style)         | -        |      |\n| state     | 定义节点在不同状态下的样式                   | [State](/manual/element/node/base-node#state)         | -        |      |\n| palette   | 定义节点的色板，用于根据不同数据映射颜色     | [Palette](/manual/element/node/base-node#palette)     | -        |      |\n| animation | 定义节点的动画效果                           | [Animation](/manual/element/node/base-node#animation) | -        |      |\n\n详见 [Node](/manual/element/node/base-node)\n\n**示例：**\n\n```javascript\nconst graph = new Graph({\n  node: {\n    type: 'circle', // 节点类型\n    style: {\n      fill: '#e6f7ff', // 填充色\n      stroke: '#91d5ff', // 边框色\n      lineWidth: 1, // 边框宽度\n      r: 20, // 半径\n      labelText: (d) => d.id, // 标签文本\n    },\n    // 节点状态样式\n    state: {\n      hover: {\n        lineWidth: 2,\n        stroke: '#69c0ff',\n      },\n      selected: {\n        fill: '#bae7ff',\n        stroke: '#1890ff',\n        lineWidth: 2,\n      },\n    },\n  },\n});\n```\n\n## edge\n\n> [EdgeOptions](#edgeoptions)\n\n边配置项\n\n#### EdgeOptions\n\n| 属性      | 描述                                   | 类型                                                  | 默认值 | 必选 |\n| --------- | -------------------------------------- | ----------------------------------------------------- | ------ | ---- |\n| type      | 边类型，内置边类型名称或自定义边的名称 | [Type](/manual/element/edge/base-edge#type)           | `line` |      |\n| style     | 边样式，包括颜色、大小等               | [Style](/manual/element/edge/base-edge#style)         | -      |      |\n| state     | 定义边在不同状态下的样式               | [State](/manual/element/edge/base-edge#state)         | -      |      |\n| palette   | 定义边的色板，用于根据不同数据映射颜色 | [Palette](/manual/element/edge/base-edge#palette)     | -      |      |\n| animation | 定义边的动画效果                       | [Animation](/manual/element/edge/base-edge#animation) | -      |      |\n\n详见 [Edge](/manual/element/edge/base-edge)\n\n**示例：**\n\n```javascript\nconst graph = new Graph({\n  edge: {\n    type: 'polyline', // 边类型\n    style: {\n      stroke: '#91d5ff', // 边的颜色\n      lineWidth: 2, // 边的宽度\n      endArrow: true, // 是否有箭头\n    },\n    // 边的状态样式\n    state: {\n      selected: {\n        stroke: '#1890ff',\n        lineWidth: 3,\n      },\n    },\n  },\n});\n```\n\n## combo\n\n> [ComboOptions](#combooptions)\n\n组合配置项\n\n| 属性      | 描述                                         | 类型                                                    | 默认值   | 必选 |\n| --------- | -------------------------------------------- | ------------------------------------------------------- | -------- | ---- |\n| type      | 组合类型，内置组合类型名称或自定义组合的名称 | [Type](/manual/element/combo/base-combo#type)           | `circle` |      |\n| style     | 组合样式，包括颜色、大小等                   | [Style](/manual/element/combo/base-combo#style)         | -        |      |\n| state     | 定义组合在不同状态下的样式                   | [State](/manual/element/combo/base-combo#state)         | -        |      |\n| palette   | 定义组合的色板，用于根据不同数据映射颜色     | [Palette](/manual/element/combo/base-combo#palette)     | -        |      |\n| animation | 定义组合的动画效果                           | [Animation](/manual/element/combo/base-combo#animation) | -        |      |\n\n详见 [Combo](/manual/element/combo/base-combo)\n\n**示例：**\n\n```javascript\nconst graph = new Graph({\n  combo: {\n    type: 'circle', // 组合类型\n    style: {\n      fill: '#f0f0f0', // 背景色\n      stroke: '#d9d9d9', // 边框色\n      lineWidth: 1, // 边框宽度\n    },\n    // 组合状态样式\n    state: {\n      selected: {\n        stroke: '#1890ff',\n        lineWidth: 2,\n      },\n    },\n  },\n});\n```\n\n## layout\n\n> _CustomLayoutOptions \\| CustomLayoutOptions[]_\n\n布局配置项，可以是对象（普通布局）或数组（流水线布局）。\n\n**示例**:\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'force', // 力导向布局\n    preventOverlap: true, // 防止节点重叠\n    nodeStrength: -50, // 节点之间的斥力\n    edgeStrength: 0.5, // 边的弹性系数\n    iterations: 200, // 迭代次数\n    animation: true, // 启用布局动画\n  },\n});\n```\n\n## theme\n\n> _false \\| 'light' \\| 'dark' \\| string_\n\n设置图表的主题，可以是内置的 `'light'`、`'dark'` 主题，也可以是自定义主题的名称。设为 `false` 则不使用任何主题。\n\n## behaviors\n\n> _(string \\| [CustomExtensionOptions](#customextensionoptions) \\| ((this:Graph) =>CustomExtensionOptions))[]_\n\n配置图表的交互行为，可以是字符串（使用默认配置）、对象（自定义配置）或函数（动态配置、函数内可访问图实例）。\n\n**示例：**\n\n```javascript\nconst graph = new Graph({\n  behaviors: [\n    'drag-canvas', // 使用默认配置启用画布拖拽\n    'zoom-canvas', // 使用默认配置启用画布缩放\n    {\n      type: 'drag-element', // 自定义配置拖拽元素\n      key: 'drag-node-only',\n      enable: (event) => event.targetType === 'node', // 只允许拖拽节点\n    },\n    function () {\n      console.log(this); // 输出 graph 实例\n      return {\n        type: 'hover-activate',\n      };\n    },\n  ],\n});\n```\n\n- 查看 [交互总览](/manual/behavior/overview) 深入了解交互原理\n- 浏览 [内置交互](/manual/behavior/auto-adapt-label) 获取所有内置交互列表及其配置选项\n\n## plugins\n\n> _(string \\| [CustomExtensionOptions](#customextensionoptions) \\| ((this:Graph) =>CustomExtensionOptions))[]_\n\n设置图表的插件，可以是字符串（使用默认配置）、对象（自定义配置）或函数（动态配置、函数内可访问图实例）。\n\n**示例：**\n\n```javascript\nconst graph = new Graph({\n  container: 'container',\n  plugins: [\n    'minimap', // 启用小地图，使用默认配置\n    {\n      type: 'grid', // 启用网格背景\n      key: 'grid-plugin',\n      line: {\n        stroke: '#d9d9d9',\n        lineWidth: 1,\n      },\n    },\n    {\n      type: 'toolbar', // 启用工具栏\n      key: 'graph-toolbar',\n      position: 'top-right', // 位置\n    },\n  ],\n});\n```\n\n- 查看 [插件总览](/manual/plugin/overview) 深入了解插件原理\n- 浏览 [内置插件](/manual/plugin/background) 获取所有内置插件列表及其配置项\n\n## transforms\n\n> _(string \\| [CustomExtensionOptions](#customextensionoptions) \\| ((this:Graph) =>CustomExtensionOptions))[]_\n\n配置数据处理，用于在渲染前对数据进行处理，不会影响原始数据。可以是字符串（使用默认配置）、对象（自定义配置）或函数（动态配置、函数内可访问图实例）。\n\n**示例：**\n\n```javascript\nconst graph = new Graph({\n  transforms: [\n    'process-parallel-edges', // 处理平行边，使用默认配置\n    {\n      type: 'map-node-size', // 根据节点数据映射节点大小\n      field: 'value', // 使用 value 字段的值\n      max: 50, // 最大半径\n      min: 20, // 最小半径\n    },\n  ],\n});\n```\n\n- 查看 [数据处理总览](/manual/transform/overview) 深入了解数据处理原理\n- 浏览 [内置数据处理](/manual/transform/map-node-size) 获取所有内置数据处理列表及其配置项\n\n#### CustomExtensionOptions\n\n```typescript\ninterface CustomExtensionOption extends Record<string, any> {\n  /** 拓展类型 */\n  type: string;\n  /** 拓展 key，即唯一标识 */\n  key?: string;\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/introduction.en.md",
    "content": "---\ntitle: Introduction\norder: 0\n---\n\n![](https://user-images.githubusercontent.com/6113694/45008751-ea465300-b036-11e8-8e2a-166cbb338ce2.png)\n\n[![Build Status](https://github.com/antvis/g6/workflows/build/badge.svg?branch=v5)](https://github.com/antvis//actions)\n[![Coverage Status](https://img.shields.io/coveralls/github/antvis/G6/v5.svg)](https://coveralls.io/github/antvis/G6?branch=v5)\n![typescript](https://img.shields.io/badge/language-typescript-red.svg)\n![MIT](https://img.shields.io/badge/license-MIT-000000.svg)\n[![npm package](https://img.shields.io/npm/v/@antv/g6.svg)](https://www.npmjs.com/package/@antv/g6)\n[![NPM downloads](http://img.shields.io/npm/dm/@antv/g6.svg)](https://npmjs.org/package/@antv/g6)\n[![Percentage of issues still open](http://isitmaintained.com/badge/open/antvis/g6.svg)](http://isitmaintained.com/project/antvis/g6 'Percentage of issues still open')\n\n<h3 style=\"text-align: center;\">AntV G6</h3>\n\n<h3 style=\"text-align: center;\"><image width=\"500\" src=\"https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*zTjwQaXokeQAAAAAAAAAAABkARQnAQ\" /></h3>\n\n<p style=\"text-align: center;\">G6 is a graph visualization engine. It provides capabilities for graph drawing, layout, analysis, interaction, animation, and other aspects of graph visualization. It aims to offer developers a set of tools that are easy to use, professionally reliable, and highly customizable for graph visualization development.</p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">Rich Elements</h4>\n\n<p style=\"text-align: center;\">Built-in 10+ elements to meet the needs of common scenarios.</p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">Flexible Interactions</h4>\n\n<p style=\"text-align: center;\">Built-in 10+ interactions that can be freely combined according to different scenarios.</p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">High-Performance Layout Algorithms</h4>\n\n<p style=\"text-align: center;\">High-performance layouts, built-in 10+ common graph layouts, with support for WebGPU and WASM computational acceleration.</p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">Multicolor Themes for Various Scenarios</h4>\n\n<p style=\"text-align: center;\"><img height=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*QjJoSbD7GTwAAAAAAAAAAAAADmJ7AQ/original\"></p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">3D Scenes</h4>\n\n<p style=\"text-align: center;\"><img width=\"400\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*IUOnSbLisyoAAAAAAAAAAAAADmJ7AQ/original\"></p>\n\n<p style=\"text-align: center;\"><img width=\"400\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*BK0OSYplirUAAAAAAAAAAAAADmJ7AQ/original\"></p>\n\n<p style=\"text-align: center;\">Supports 3D elements and layouts to create immersive graph visualization scenarios.</p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">High Customizability</h4>\n\n<p style=\"text-align: center;\">Elements, layouts, interactions, and plugins are all customizable, enabling every creative idea you have.</p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">Start Your G6 Journey in Just 3 Minutes</h4>\n\n<p style=\"text-align: center;\">Click to enter 👉 <a href=\"/manual/getting-started/quick-start\">Quick Start</a></p>\n\n<div>\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YXHtRZUKAZcAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*VChnTLySxScAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*tqlbS7ukmYUAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*I5uDQZWTzMsAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*hnLoRJR8EvMAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*8LqvQJ09-EEAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*UgMZS6vrUlgAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*SDQKSb8gcxgAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ZC1CT7q0fM4AAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YMxxTZwt54UAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*h3eWT4loiTwAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*XTcoRKPMDloAAAAAAAAAAAAADmJ7AQ/original\" />\n</div>\n"
  },
  {
    "path": "packages/site/docs/manual/introduction.zh.md",
    "content": "---\ntitle: 简介\norder: 0\nsidebar: false\n---\n\n![](https://user-images.githubusercontent.com/6113694/45008751-ea465300-b036-11e8-8e2a-166cbb338ce2.png)\n\n[![Build Status](https://github.com/antvis/g6/workflows/build/badge.svg?branch=v5)](https://github.com/antvis//actions)\n[![Coverage Status](https://img.shields.io/coveralls/github/antvis/G6/v5.svg)](https://coveralls.io/github/antvis/G6?branch=v5)\n![typescript](https://img.shields.io/badge/language-typescript-red.svg)\n![MIT](https://img.shields.io/badge/license-MIT-000000.svg)\n[![npm package](https://img.shields.io/npm/v/@antv/g6.svg)](https://www.npmjs.com/package/@antv/g6)\n[![NPM downloads](http://img.shields.io/npm/dm/@antv/g6.svg)](https://npmjs.org/package/@antv/g6)\n[![Percentage of issues still open](http://isitmaintained.com/badge/open/antvis/g6.svg)](http://isitmaintained.com/project/antvis/g6 'Percentage of issues still open')\n\n<h3 style=\"text-align: center;\">AntV G6</h3>\n\n<h3 style=\"text-align: center;\"><image width=\"500\" src=\"https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*zTjwQaXokeQAAAAAAAAAAABkARQnAQ\" /></h3>\n\n<p style=\"text-align: center;\">G6 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化能力。旨在为开发者提供一套简单易用、专业可靠、可高度定制的图可视化开发工具</p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">丰富元素</h4>\n\n<p style=\"text-align: center;\">内置 10+ 元素，满足常规场景需求</p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">灵活交互</h4>\n\n<p style=\"text-align: center;\">内置 10+ 交互，并可根据不同场景自由组合</p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">高性能布局算法</h4>\n\n<p style=\"text-align: center;\">高性能布局，内置 10+ 常用的图布局，支持 WebGPU、 WASM 计算加速</p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">多色主题，适用多种场景</h4>\n\n<p style=\"text-align: center;\"><image height=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*QjJoSbD7GTwAAAAAAAAAAAAADmJ7AQ/original\"></p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">3D 场景</h4>\n\n<p style=\"text-align: center;\"><image width=\"400\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*IUOnSbLisyoAAAAAAAAAAAAADmJ7AQ/original\"></p>\n\n<p style=\"text-align: center;\"><image width=\"400\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*BK0OSYplirUAAAAAAAAAAAAADmJ7AQ/original\"></p>\n\n<p style=\"text-align: center;\">支持 3D 元素、布局，打造沉浸式的图可视化场景</p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">高可定制性</h4>\n\n<p style=\"text-align: center;\">元素、布局、交互、插件统统可定制，实现你的每一个创意</p>\n\n<h4 style=\"text-align: center; color: #678ff3;\">仅需 3 分钟，开启 G6 之旅</h4>\n\n<p style=\"text-align: center;\">点击进入👉 <a href=\"/manual/getting-started/quick-start\">快速开始</a></p>\n\n<div>\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YXHtRZUKAZcAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*VChnTLySxScAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*tqlbS7ukmYUAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*I5uDQZWTzMsAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*hnLoRJR8EvMAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*8LqvQJ09-EEAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*UgMZS6vrUlgAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*SDQKSb8gcxgAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ZC1CT7q0fM4AAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YMxxTZwt54UAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*h3eWT4loiTwAAAAAAAAAAAAADmJ7AQ/original\" />\n<image width=\"200\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*XTcoRKPMDloAAAAAAAAAAAAADmJ7AQ/original\" />\n</div>\n"
  },
  {
    "path": "packages/site/docs/manual/layout/AntvDagreLayout.en.md",
    "content": "---\ntitle: AntvDagre Layout\norder: 2\n---\n\n## Overview\n\nAntvDagre builds upon the original [dagre](https://github.com/dagrejs/dagre/wiki) layout and adds more useful options, such as `nodeOrder`, `edgeLabelSpace`, and more. The `dagre` layout itself is a hierarchical layout suitable for directed acyclic graphs (DAGs), which can automatically handle node direction and spacing, and supports both horizontal and vertical layouts. See more Dagre layout [examples](/en/examples#layout-dagre), [source code](https://github.com/dagrejs/dagre/blob/master/lib/layout.js), and [official documentation](https://github.com/dagrejs/dagre/wiki).\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*2uMmRo5wYPUAAAAAAAAAAABkARQnAQ' width=350 alt='Dagre Layout'/>\n\n## Configuration\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'antv-dagre',\n    rankdir: 'TB',\n    align: 'UL',\n    nodesep: 50,\n    ranksep: 50,\n    controlPoints: false,\n  },\n});\n```\n\n## Options\n\n> For more native `dagre` options, refer to the [official documentation](https://github.com/dagrejs/dagre/wiki#configuring-the-layout). Here, only some core and new options are listed.\n\n<img src=\"https://img.alicdn.com/imgextra/i3/O1CN01OpQHBZ1HcpZuWZLS7_!!6000000000779-0-tps-1274-1234.jpg\" width=\"400\" alt=\"Dagre Layout Options Diagram\" />\n\n| Property       | Description                                                                                                                                                                                                              | Type                                                | Default                    | Required |\n| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------- | -------------------------- | -------- |\n| type           | Layout type                                                                                                                                                                                                              | `antv-dagre`                                        | -                          | ✓        |\n| rankdir        | Layout direction, options                                                                                                                                                                                                | `TB` \\| `BT` \\| `LR` \\| `RL`                        | `TB`                       |          |\n| align          | Node alignment, options                                                                                                                                                                                                  | `UL` \\| `UR` \\| `DL` \\| `DR`                        | `UL`                       |          |\n| nodesep        | Node spacing (px). For `TB` or `BT`, it's horizontal spacing; for `LR` or `RL`, it's vertical spacing.                                                                                                                   | number                                              | 50                         |          |\n| nodesepFunc    | Callback for node spacing (px), allows different spacing for different nodes. For `TB` or `BT`, it's horizontal spacing; for `LR` or `RL`, it's vertical spacing. Takes precedence over nodesep.                         | (d?: Node) => number                                |                            |          |\n| ranksep        | Rank spacing (px). For `TB` or `BT`, it's vertical spacing between ranks; for `LR` or `RL`, it's horizontal spacing.                                                                                                     | number                                              | 100                        |          |\n| ranksepFunc    | Callback for rank spacing (px), allows different spacing for different ranks. For `TB` or `BT`, it's vertical spacing; for `LR` or `RL`, it's horizontal spacing. Takes precedence over ranksep.                         | (d?: Node) => number                                |                            |          |\n| ranker         | Algorithm for assigning ranks to nodes: `longest-path`, `tight-tree`, or `network-simplex`                                                                                                                               | `network-simplex` \\| `tight-tree` \\| `longest-path` | `network-simplex`          |          |\n| nodeSize       | Specify node size for all or each node, used for collision detection. If a single number is returned, width and height are the same; if an array, e.g. `[width, height]`.                                                | Size                                                | ((nodeData: Node) => Size) |          |\n| controlPoints  | Whether to keep edge control points. Only effective when using built-in polyline edges (`type: 'polyline-edge'`) or any edge that uses `style.controlPoints` as control points. Adds `style.controlPoints` to edge data. | boolean                                             | false                      |          |\n| begin          | Top-left alignment position of the layout                                                                                                                                                                                | [number, number] \\| [number, number, number]        |                            |          |\n| sortByCombo    | Whether to sort nodes in the same rank by their parentId to prevent Combo overlap                                                                                                                                        | boolean                                             | false                      |          |\n| edgeLabelSpace | Whether to reserve space for edge labels                                                                                                                                                                                 | boolean                                             | true                       |          |\n| nodeOrder      | Reference array for node order in the same rank, stores node ids. If not specified, dagre's default order is used.                                                                                                       | string[]                                            |                            |          |\n| radial         | Whether to use radial layout based on `dagre`                                                                                                                                                                            | boolean                                             | false                      |          |\n| focusNode      | Focus node, only effective when `radial` is true                                                                                                                                                                         | ID \\| Node \\| null                                  |                            |          |\n| preset         | Reference node positions for layout calculation, usually for smooth transitions when switching data. In G6, if updating data, the existing layout result is used as input.                                               | OutNode[]                                           |                            |          |\n\n### align\n\n> _DagreAlign_ **Default:** `UL`\n\nNode alignment: U = upper, D = down, L = left, R = right\n\n- `UL`: align to upper left\n- `UR`: align to upper right\n- `DL`: align to lower left\n- `DR`: align to lower right\n\n### rankdir\n\n> _DagreRankdir_ **Default:** `TB`\n\nLayout direction. T = top, B = bottom, L = left, R = right\n\n- `TB`: top to bottom\n- `BT`: bottom to top\n- `LR`: left to right\n- `RL`: right to left\n\n### ranker\n\n> _`network-simplex` \\| `tight-tree` \\| `longest-path`_\n\nLayout mode\n\n### ranksep\n\n> _number_ **Default:** 50\n\nRank spacing (px)\n\nFor 'TB' or 'BT', it's vertical spacing; for 'LR' or 'RL', it's horizontal spacing. `ranksepFunc` has higher priority.\n\n### ranksepFunc\n\n> _(d?: Node) => number_\n\nCallback for rank spacing (px)\n\nFor 'TB' or 'BT', it's vertical spacing; for 'LR' or 'RL', it's horizontal spacing. Takes precedence over nodesep if set.\n\n### nodesep\n\n> _number_ **Default:** 50\n\nNode spacing (px)\n\nFor 'TB' or 'BT', it's horizontal spacing; for 'LR' or 'RL', it's vertical spacing. `nodesepFunc` has higher priority.\n\n### nodesepFunc\n\n> _(d?: Node) => number_\n\nCallback for node spacing (px), allows different spacing for different nodes\n\nFor 'TB' or 'BT', it's horizontal spacing; for 'LR' or 'RL', it's vertical spacing. Takes precedence over nodesep if set.\n\n### begin\n\n> _[number, number] \\| [number, number, number]_ **Default:** undefined\n\nTop-left alignment position of the layout\n\n### controlPoints\n\n> _boolean_ **Default:** false\n\nWhether to keep edge control points. Only effective when using built-in polyline edges (`type: 'polyline-edge'`) or any edge that uses `style.controlPoints` as control points. Adds `style.controlPoints` to edge data.\n\n### edgeLabelSpace\n\n> _boolean_ **Default:** true\n\nWhether to reserve space for edge labels\n\nThis affects whether a dummy node is added in the middle of the edge.\n\n### focusNode\n\n> _ID \\| Node \\| null_\n\nFocus node, only effective when `radial` is true\n\n- ID: node id\n- Node: node instance\n- null: cancel focus\n\n### nodeOrder\n\n> _string[]_ **Default:** undefined\n\nReference array for node order in the same rank, stores node ids\n\nIf not specified, dagre's default order is used.\n\n### nodeSize\n\n> _Size \\| ((nodeData: Node) => Size)_ **Default:** undefined\n\nSpecify node size for all or each node.\n\nUsed for collision detection to prevent node overlap\n\n### preset\n\n> _OutNode[]_ **Default:** undefined\n\nReference node positions for layout calculation\n\nUsually for smooth transitions when switching data. In G6, if updating data, the existing layout result is used as input.\n\n### radial\n\n> _boolean_\n\nWhether to use radial layout based on dagre\n\n### sortByCombo\n\n> _boolean_ **Default:** false\n\nWhether to sort nodes in the same rank by their parentId to prevent Combo overlap\n\nRecommended to enable when using Combo\n\n## Suitable Scenarios\n\n- **Flowcharts**: Suitable for displaying flowcharts, node direction and spacing are automatically handled\n- **Dependency Graphs**: Display dependencies between packages or modules\n- **Task Scheduling Graphs**: Show dependencies and execution order between tasks\n\n## Related Documentation\n\n> The following documents can help you better understand the Dagre layout\n\n- [Graph Layout Algorithms: In-depth Dagre Layout](https://mp.weixin.qq.com/s/EdyTfFUH7fyMefNSBXI2nA)\n- [In-depth Dagre Layout Algorithm](https://www.yuque.com/antv/g6-blog/xxp5nl)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/AntvDagreLayout.zh.md",
    "content": "---\ntitle: AntV Dagre 布局 AntvDagre\norder: 2\n---\n\n## 概述\n\nAntvDagre 在原先[dagre](https://github.com/dagrejs/dagre/wiki)布局的基础上增加了更多有用的设置项，比如`nodeOrder`、`edgeLabelSpace`等等。 `dagre`布局本身一种层次化布局，适用于有向无环图（DAG）的布局场景，能够自动处理节点之间的方向和间距，支持水平和垂直布局。参考更多 Dagre 布局[样例](/examples#layout-dagre)或[源码](https://github.com/dagrejs/dagre/blob/master/lib/layout.js)以及[官方文档](https://github.com/dagrejs/dagre/wiki)。\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*2uMmRo5wYPUAAAAAAAAAAABkARQnAQ' width=350 alt='Dagre布局'/>\n\n## 配置方式\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'antv-dagre',\n    rankdir: 'TB',\n    align: 'UL',\n    nodesep: 50,\n    ranksep: 50,\n    controlPoints: false,\n  },\n});\n```\n\n## 配置项\n\n> 更多`dagre`原生配置项可参考[官方文档](https://github.com/dagrejs/dagre/wiki#configuring-the-layout)，这里仅列出部分核心配置和新增的配置\n\n<img src=\"https://img.alicdn.com/imgextra/i3/O1CN01OpQHBZ1HcpZuWZLS7_!!6000000000779-0-tps-1274-1234.jpg\" width=\"400\" alt=\"Dagre 布局配置项图解\" />\n\n| 属性           | 描述                                                                                                                                                                                                                                  | 类型                                                | 默认值                     | 必选 |\n| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------------------------- | ---- |\n| type           | 布局类型                                                                                                                                                                                                                              | `antv-dagre`                                        | -                          | ✓    |\n| rankdir        | 布局方向，可选值                                                                                                                                                                                                                      | `TB` \\| `BT` \\| `LR` \\| `RL`                        | `TB`                       |      |\n| align          | 节点对齐方式，可选值                                                                                                                                                                                                                  | `UL` \\| `UR` \\| `DL` \\| `DR`                        | `UL`                       |      |\n| nodesep        | 节点间距（px）。在rankdir 为 `TB` 或 `BT` 时是节点的水平间距；在rankdir 为 `LR` 或 `RL` 时代表节点的竖直方向间距                                                                                                                      | number                                              | 50                         |      |\n| nodesepFunc    | 节点间距（px）的回调函数，通过该参数可以对不同节点设置不同的节点间距。在rankdir 为 `TB` 或 `BT` 时是节点的水平间距；在rankdir 为 `LR` 或 `RL` 时代表节点的竖直方向间距。优先级高于 nodesep，即若设置了 nodesepFunc，则 nodesep 不生效 | (d?: Node) => number                                |                            |      |\n| ranksep        | 层间距（px）。在rankdir 为 `TB` 或 `BT` 时是竖直方向相邻层间距；在rankdir 为 `LR` 或 `RL` 时代表水平方向相邻层间距                                                                                                                    | number                                              | 100                        |      |\n| ranksepFunc    | 层间距（px）的回调函数，通过该参数可以对不同层级设置不同的节点间距。在rankdir 为 `TB` 或 `BT` 时是节点的水平间距；在rankdir 为 `LR` 或 `RL` 时代表节点的竖直方向间距。优先级高于 ranksep，即若设置了 ranksepFunc，则 ranksep 不生效   | (d?: Node) => number                                |                            |      |\n| ranker         | 为每个节点分配等级的算法，共支持三种算法，分别是：`longest-path` 最长路径算法、`tight-tree` 紧凑树算法、`network-simplex` 网络单形法                                                                                                  | `network-simplex` \\| `tight-tree` \\| `longest-path` | `network-simplex`          |      |\n| nodeSize       | 统一指定或为每个节点指定节点大小，用于防止节点重叠时的碰撞检测。如果仅返回单个number，则表示节点的宽度和高度相同；如果返回一个数组，则形如：`[width, height]`                                                                         | Size                                                | ((nodeData: Node) => Size) |      |\n| controlPoints  | 是否保留边的控制点，仅在边配置中使用了内置折线（type: 'polyline-edge'） 时，或任何将自定义消费了 `style.controlPoints` 字段作为控制点位置的边时生效。本质上就是给边数据增加了 `style.controlPoints`                                   | boolean                                             | false                      |      |\n| begin          | 布局的左上角对齐位置                                                                                                                                                                                                                  | [number, number] \\| [number, number, number]        |                            |      |\n| sortByCombo    | 同一层节点是否根据每个节点数据中的 parentId 进行排序，以防止 Combo 重叠置                                                                                                                                                             | boolean                                             | false                      |      |\n| edgeLabelSpace | 是否为边的label留位置                                                                                                                                                                                                                 | boolean                                             | true                       |      |\n| nodeOrder      | 同层节点顺序的参考数组，存放节点 id 值，若未指定，则将按照 dagre 本身机制排列同层节点顺序                                                                                                                                             | string[]                                            |                            |      |\n| radial         | 是否基于 `dagre` 进行辐射布局                                                                                                                                                                                                         | boolean                                             | false                      |      |\n| focusNode      | 关注的节点，注意，仅在`radial` 为 true 时生效                                                                                                                                                                                         | ID \\| Node \\| null                                  |                            |      |\n| preset         | 布局计算时参考的节点位置，一般用于切换数据时保证重新布局的连续性。在 G6 中，若是更新数据，则将自动使用已存在的布局结果数据作为输入                                                                                                    | OutNode[]                                           |                            |\n\n### align\n\n> _DagreAlign_ **Default:** `UL`\n\n节点对齐方式 U：upper（上）；D：down（下）；L：left（左）；R：right（右）\n\n- `UL`:对齐到左上角\n- `UR`:对齐到右上角\n- `DL`:对齐到左下角\n- `DR`:对齐到右下角\n\n### rankdir\n\n> _DagreRankdir_ **Default:** `TB`\n\n布局的方向。T：top（上）；B：bottom（下）；L：left（左）；R：right（右）\n\n- `TB`:从上至下布局\n- `BT`:从下至上布局\n- `LR`:从左至右布局\n- `RL`:从右至左布局\n\n### ranker\n\n> _`network-simplex` \\| `tight-tree` \\| `longest-path`_\n\n布局的模式\n\n### ranksep\n\n> _number_ **Default:** 50\n\n层间距（px）\n\n在 rankdir 为 'TB' 或 'BT' 时是竖直方向相邻层间距；在 rankdir 为 'LR' 或 'RL' 时代表水平方向相邻层间距。ranksepFunc 拥有更高的优先级\n\n### ranksepFunc\n\n> _(d?: Node) => number_\n\n层间距（px）的回调函数\n\n在 rankdir 为 'TB' 或 'BT' 时是竖直方向相邻层间距；在 rankdir 为 'LR' 或 'RL' 时代表水平方向相邻层间距。优先级高于 nodesep，即若设置了 nodesepFunc，则 nodesep 不生效\n\n### nodesep\n\n> _number_ **Default:** 50\n\n节点间距（px）\n\n在 rankdir 为 'TB' 或 'BT' 时是节点的水平间距；在 rankdir 为 'LR' 或 'RL' 时代表节点的竖直方向间距。nodesepFunc 拥有更高的优先级\n\n### nodesepFunc\n\n> _(d?: Node) => number_\n\n节点间距（px）的回调函数，通过该参数可以对不同节点设置不同的节点间距\n\n在 rankdir 为 'TB' 或 'BT' 时是节点的水平间距；在 rankdir 为 'LR' 或 'RL' 时代表节点的竖直方向间距。优先级高于 nodesep，即若设置了 nodesepFunc，则 nodesep 不生效\n\n### begin\n\n> _[number, number] \\| [number, number, number]_ **Default:** undefined\n\n布局的左上角对齐位置\n\n### controlPoints\n\n> _boolean_ **Default:** false\n\n是否保留边的控制点，仅在边配置中使用了内置折线（type: 'polyline-edge'） 时，或任何将自定义消费了 `style.controlPoints` 字段作为控制点位置的边时生效。本质上就是给边数据增加了 `style.controlPoints`\n\n### edgeLabelSpace\n\n> _boolean_ **Default:** true\n\n是否为边的label留位置\n\n这会影响是否在边中间添加dummy node\n\n### focusNode\n\n> _ID \\| Node \\| null_\n\n关注的节点，注意，仅在`radial` 为 true 时生效\n\n- ID: 节点 id\n- Node: 节点实例\n- null: 取消关注\n\n### nodeOrder\n\n> _string[]_ **Default:** undefined\n\n同层节点顺序的参考数组，存放节点 id 值\n\n若未指定，则将按照 dagre 本身机制排列同层节点顺序\n\n### nodeSize\n\n> _Size \\| ((nodeData: Node) => Size)_ **Default:** undefined\n\n统一指定或为每个节点指定节点大小。\n\n用于防止节点重叠时的碰撞检测\n\n### preset\n\n> _OutNode[]_ **Default:** undefined\n\n布局计算时参考的节点位置\n\n一般用于切换数据时保证重新布局的连续性。在 G6 中，若是更新数据，则将自动使用已存在的布局结果数据作为输入\n\n### radial\n\n> _boolean_\n\n是否基于 dagre 进行辐射布局\n\n### sortByCombo\n\n> _boolean_ **Default:** false\n\n同一层节点是否根据每个节点数据中的 parentId 进行排序，以防止 Combo 重叠\n\n建议在有 Combo 的情况下配置\n\n## 布局适用场景\n\n- **流程图**：适合展示流程图，节点之间的方向和间距会自动处理；\n- **依赖关系图**：展示软件包或模块之间的依赖关系；\n- **任务调度图**：展示任务之间的依赖关系和执行顺序。\n\n## 相关文档\n\n> 以下文档可以帮助你更好地理解 Dagre 布局\n\n- [图布局算法｜详解 Dagre 布局](https://mp.weixin.qq.com/s/EdyTfFUH7fyMefNSBXI2nA)\n- [深入解读Dagre布局算法](https://www.yuque.com/antv/g6-blog/xxp5nl)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/BaseLayout.en.md",
    "content": "---\ntitle: Common Layout Configuration Options\norder: 1\n---\n\nThis article introduces the common attribute configurations for built-in layouts.\n\n## General Configuration\n\n| Property               | Description                                                                             | Type                          | Default    | Required |\n| ---------------------- | --------------------------------------------------------------------------------------- | ----------------------------- | ---------- | -------- |\n| type                   | Layout type, name of built-in or custom layout                                          | [Type](#Type)                 | -          | ✓        |\n| isLayoutInvisibleNodes | Whether invisible nodes participate in the layout (takes effect when preLayout is true) | boolean                       | false      |          |\n| nodeFilter             | Nodes participating in the layout                                                       | (node: NodeData) => boolean   | () => true |          |\n| comboFilter            | Combos participating in the layout                                                      | (combo: ComboData) => boolean | () => true |          |\n| preLayout              | Use pre-layout, calculate layout before initializing elements                           | boolean                       | false      |          |\n| enableWorker           | Whether to run the layout in a WebWorker                                                | boolean                       | -          |          |\n| iterations             | Number of iterations for iterative layout                                               | number                        | -          |          |\n\n### Type\n\nSpecifies the layout type, either the name of a built-in layout type or a custom layout.\n\n```js {4}\nconst graph = new Graph({\n  // Other configurations...\n  layout: {\n    type: 'antv-dagre',\n  },\n});\n```\n\nOptional values include:\n\n- `antv-dagre`: [Custom layout based on dagre](/en/manual/layout/antv-dagre-layout)\n- `circular`: [Circular layout](/en/manual/layout/circular-layout)\n- `combo-combined`: [Layout suitable for combinations](/en/manual/layout/combo-combined-layout)\n- `concentric`: [Concentric layout](/en/manual/layout/concentric-layout)\n- `d3-force`: [Force-directed layout based on D3](/en/manual/layout/d3-force-layout)\n- `d3-force-3d`: [3D Force-directed layout](/en/manual/layout/d3-force3-d-layout)\n- `dagre`: [Dagre layout](/en/manual/layout/dagre-layout)\n- `fishbone`: [Fishbone layout](/en/manual/layout/fishbone)\n- `force`: [Force-directed layout](/en/manual/layout/force-layout)\n- `force-atlas2`: [ForceAtlas2 layout](/en/manual/layout/force-atlas2-layout)\n- `fruchterman`: [Fruchterman layout](/en/manual/layout/fruchterman-layout)\n- `grid`: [Grid layout](/en/manual/layout/grid-layout)\n- `mds`: [MDS layout for high-dimensional data](/en/manual/layout/mds-layout)\n- `radial`: [Radial layout](/en/manual/layout/radial-layout)\n- `random`: [Random layout](/en/manual/layout/random-layout)\n- `snake`: [Snake layout](/en/manual/layout/snake)\n- `compact-box`: [Compact box tree layout](/en/manual/layout/compact-box-layout)\n- `dendrogram`: [Dendrogram layout](/en/manual/layout/dendrogram-layout)\n- `mindmap`: [Mindmap layout](/en/manual/layout/mindmap-layout)\n- `indented`: [Indented tree layout](/en/manual/layout/indented-layout)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/BaseLayout.zh.md",
    "content": "---\ntitle: 布局通用配置项\norder: 1\n---\n\n本文介绍内置布局通用属性配置。\n\n## 通用配置\n\n| 属性                   | 描述                                                  | 类型                          | 默认值     | 必选 |\n| ---------------------- | ----------------------------------------------------- | ----------------------------- | ---------- | ---- |\n| type                   | 布局类型，内置布局或自定义布局的名称                  | [Type](#Type)                 | -          | ✓    |\n| isLayoutInvisibleNodes | 不可见节点是否参与布局（当 preLayout 为 true 时生效） | boolean                       | false      |      |\n| nodeFilter             | 参与该布局的节点                                      | (node: NodeData) => boolean   | () => true |      |\n| comboFilter            | 参与该布局的combo元素                                 | (combo: ComboData) => boolean | () => true |      |\n| preLayout              | 使用前布局，在初始化元素前计算布局                    | boolean                       | false      |      |\n| enableWorker           | 是否在 WebWorker 中运行布局                           | boolean                       | -          |      |\n| iterations             | 迭代布局的迭代次数                                    | number                        | -          |      |\n\n### Type\n\n指定布局类型，内置布局类型名称或自定义布局的名称。\n\n```js {4}\nconst graph = new Graph({\n  // 其他配置...\n  layout: {\n    type: 'antv-dagre',\n  },\n});\n```\n\n可选值有：\n\n- `antv-dagre`：[基于 dagre 定制的布局](/manual/layout/antv-dagre-layout)\n- `circular`：[环形布局](/manual/layout/circular-layout)\n- `combo-combined`：[适用于存在组合的布局](/manual/layout/combo-combined-layout)\n- `concentric`：[同心圆布局](/manual/layout/concentric-layout)\n- `d3-force`[基于 D3 的力导向布局](/manual/layout/d3-force-layout)\n- `d3-force-3d`：[3D力导向布局](/manual/layout/d3-force3-d-layout)\n- `dagre`：[dagre 布局](/manual/layout/dagre-layout)\n- `fishbone`：[鱼骨布局](/manual/layout/fishbone)\n- `force`：[力导向布局](/manual/layout/force-layout)\n- `force-atlas2`：[ForceAtlas2 布局](/manual/layout/force-atlas2-layout)\n- `fruchterman`：[Fruchterman 布局](/manual/layout/fruchterman-layout)\n- `grid`：[网格布局](/manual/layout/grid-layout)\n- `mds`：[高维数据降维算法布局](/manual/layout/mds-layout)\n- `radial`：[径向布局](/manual/layout/radial-layout)\n- `random`：[随机布局](/manual/layout/random-layout)\n- `snake`：[蛇形布局](/manual/layout/snake)\n- `compact-box`：[紧凑树布局](/manual/layout/compact-box-layout)\n- `dendrogram`：[树状布局](/manual/layout/dendrogram-layout)\n- `mindmap`：[思维导图布局](/manual/layout/mindmap-layout)\n- `indented`：[缩进树布局](/manual/layout/indented-layout)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/CircularLayout.en.md",
    "content": "---\ntitle: Circular Layout\norder: 3\n---\n\n## Overview\n\nCircular layout arranges nodes evenly or at intervals on a circle, and also supports spiral layouts by configuring different startRadius and endRadius. See more circular layout [examples](en/examples#layout-circular) or [source code](https://github.com/antvis/layout/blob/v5/packages/layout/src/circular.ts).\n\n## Usage Scenarios\n\n**Circular layout**:\n\n- Suitable for networks with equal relationships and no hierarchical structure\n\n**Spiral layout**:\n\n- Suitable for implicit hierarchies or time series graphs (such as organizational charts, propagation networks)\n\n## Basic Usage\n\nOther settings use the default configuration (layout width and height default to the entire canvas container)\n\n```js\nconst graph = new Graph({\n  // other configurations\n  layout: {\n    type: 'circular',\n  },\n});\n```\n\n## Options\n\n| Property    | Description                                                                                                       | Type                                          | Default                                   | Required |\n| ----------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | ----------------------------------------- | -------- |\n| type        | Layout type                                                                                                       | circular                                      | -                                         | ✓        |\n| angleRatio  | How many 2\\*PI between the first and last node                                                                    | number                                        | 1                                         |          |\n| center      | Center of the layout                                                                                              | [number, number]\\|[number, number, number]    | [`layout width` / 2, `layout height` / 2] |          |\n| clockwise   | Whether to arrange clockwise                                                                                      | boolean                                       | true                                      |          |\n| divisions   | Number of segments on the ring (segments will be evenly distributed, effective when endRadius - startRadius != 0) | number                                        | 1                                         |          |\n| nodeSize    | Node size (diameter), used for collision detection                                                                | Size \\| ((nodeData: Node) => Size)            | 10                                        |          |\n| nodeSpacing | Minimum spacing between rings, used to adjust radius                                                              | number \\| ((nodeData: Node) => number)        | 10                                        |          |\n| ordering    | Node ordering on the ring, [see details](#ordering)                                                               | `topology` \\| `topology-directed` \\| `degree` | -                                         |          |\n| radius      | Circle radius, if set, spiral layout configs `startRadius` and `endRadius` are ignored, [see details](#radius)    | number                                        | -                                         |          |\n| startAngle  | Start angle of the layout                                                                                         | number                                        | 0                                         |          |\n| endAngle    | End angle of the layout                                                                                           | number                                        | 2 \\* Math.PI                              |          |\n| startRadius | Start radius for spiral layout, [usage](#spiral-layout)                                                           | number                                        | -                                         |          |\n| endRadius   | End radius for spiral layout                                                                                      | number                                        | -                                         |          |\n| width       | Layout width                                                                                                      | number                                        | canvas width                              |          |\n| height      | Layout height                                                                                                     | number                                        | canvas height                             |          |\n\n### ordering\n\nNode ordering on the ring\n\n- `topology`: topological order\n- `topology-directed`: topological order (directed graph)\n- `degree`: order by degree\n\nIf not set (`null`), the order in the array is used directly\n\n### radius\n\nIf radius, startRadius, and endRadius are not set, the default is `Math.min(layout width, layout height) / 2`, i.e., fills the entire layout area\n\n## Code Examples\n\n### Basic Circular Layout\n\n```javascript\nconst graph = new Graph({\n  // other configurations\n  layout: {\n    type: 'circular',\n  },\n});\n```\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelFill: '#fff',\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'circular',\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n\n### Spiral Layout\n\n```javascript\nconst graph = new Graph({\n  // other configurations\n  layout: {\n    type: 'circular',\n    startRadius: 10,\n    endRadius: 300,\n  },\n});\n```\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'center',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelFill: '#fff',\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'circular',\n        startRadius: 10,\n        endRadius: 300,\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/CircularLayout.zh.md",
    "content": "---\ntitle: 环形布局 Circular\norder: 3\n---\n\n## 概述\n\n环形布局是一种把节点均匀或者按间隔放置在圆上的布局，也支持通过配置 startRadius 和 endRadius 为不一样的值实现螺旋状布局。参考更多环形布局[样例](/examples#layout-circular)或[源码](https://github.com/antvis/layout/blob/v5/packages/layout/src/circular.ts)。\n\n## 使用场景\n\n**环形布局**:\n\n- 适用于平等关系网络、无层级结构的图\n\n**螺旋状布局**:\n\n- 适用于隐式层级或时间序列图（如组织架构、传播网络）\n\n## 基本用法\n\n其余均使用默认配置（布局宽高默认是整个画布容器）\n\n```js\nconst graph = new Graph({\n  // 其他配置\n  layout: {\n    type: 'circular',\n  },\n});\n```\n\n## 配置项\n\n| 属性        | 描述                                                                                | 类型                                          | 默认值                           | 必选 |\n| ----------- | ----------------------------------------------------------------------------------- | --------------------------------------------- | -------------------------------- | ---- |\n| type        | 布局类型                                                                            | circular                                      | -                                | ✓    |\n| angleRatio  | 从第一个节点到最后节点之间相隔多少个 2\\*PI                                          | number                                        | 1                                |      |\n| center      | 布局的中心                                                                          | [number, number]\\|[number, number, number]    | [`布局宽度` / 2, `布局高度` / 2] |      |\n| clockwise   | 是否顺时针排列                                                                      | boolean                                       | true                             |      |\n| divisions   | 节点在环上的分段数（几个段将均匀分布，在 endRadius - startRadius != 0 时生效）      | number                                        | 1                                |      |\n| nodeSize    | 节点大小（直径）。用于防止节点重叠时的碰撞检测                                      | Size \\| ((nodeData: Node) => Size)            | 10                               |      |\n| nodeSpacing | 环与环之间最小间距，用于调整半径                                                    | number \\| ((nodeData: Node) => number)        | 10                               |      |\n| ordering    | 节点在环上排序的依据，[说明](#ordering)                                             | `topology` \\| `topology-directed` \\| `degree` | -                                |      |\n| radius      | 圆的半径，设置了则螺旋状布局的配置`startRadius`、`endRadius`不生效，[说明](#radius) | number                                        | -                                |      |\n| startAngle  | 布局的开始角度                                                                      | number                                        | 0                                |      |\n| endAngle    | 布局的结束角度                                                                      | number                                        | 2 \\* Math.PI                     |      |\n| startRadius | 螺旋状布局的开始半径，[用法](#螺旋状布局)                                           | number                                        | -                                |      |\n| endRadius   | 螺旋状布局的结束半径                                                                | number                                        | -                                |      |\n| width       | 布局的宽度                                                                          | number                                        | 画布宽度                         |      |\n| height      | 布局的高度                                                                          | number                                        | 画布高度                         |      |\n\n### ordering\n\n节点在环上排序的依据\n\n- `topology`: 按照拓扑排序\n- `topology-directed`: 按照拓扑排序（有向图）\n- `degree`: 按照度数大小排序\n\n不配置（`null`）则直接使用数组中的顺序\n\n### radius\n\n如果radius、startRadius、endRadius都没配置，则默认为最终计算出来的`Math.min(布局宽度, 布局高度) / 2`，即布满整个布局区域\n\n## 代码示例\n\n### 基础环形布局\n\n```javascript\nconst graph = new Graph({\n  // 其他配置\n  layout: {\n    type: 'circular',\n  },\n});\n```\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelFill: '#fff',\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'circular',\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n\n### 螺旋状布局\n\n```javascript\nconst graph = new Graph({\n  // 其他配置\n  layout: {\n    type: 'circular',\n    startRadius: 10,\n    endRadius: 300,\n  },\n});\n```\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'center',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelFill: '#fff',\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'circular',\n        startRadius: 10,\n        endRadius: 300,\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/ComboCombinedLayout.en.md",
    "content": "---\ntitle: ComboCombined Layout\norder: 4\n---\n\n## Overview\n\nComboCombined composite layout is suitable for graph data with composite group structures. It supports flexible configuration of the layout for elements inside combos as well as the layout between the outermost combos and nodes. By default, the internal elements use the Concentric layout, and the outer layout uses the gForce force-directed layout, balancing layout effect and overall stability. See more ComboCombined layout [examples](/en/examples#layout-combo-layout) and [source code](https://github.com/antvis/layout/blob/v5/packages/layout/src/combo-combined.ts).\n\n## Usage Scenarios\n\n- User profile analysis: Analyze user behavior and product relationships, use user interest circles as combos, display specific products and behavior tags as internal nodes, and help operators identify user consumption paths.\n- Supply chain management graph: Divide suppliers, manufacturers, warehouses, and distributors into combos by role or region, display resources, personnel, or equipment as internal nodes, and clearly show the internal structure of each link in the supply chain.\n\n## Options\n\n| Property     | Description                                                                                                                                        | Type                                                                                       | Default          | Required |\n| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------- | -------- |\n| type         | Layout type                                                                                                                                        | `combo-combined`                                                                           | -                | ✓        |\n| center       | Layout center                                                                                                                                      | [`PointTuple`](https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L829) | Graph center     |          |\n| comboPadding | Padding value inside the combo, used only for force calculation, not for rendering. It is recommended to set the same value as the visual padding. | `((d?: unknown) => number)` \\| `number` \\| `number[]` \\| `undefined`                       | 10               |          |\n| innerLayout  | Layout algorithm for elements inside the combo, [see below](#innerlayout)                                                                          | [`Layout`](https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L881)     | ConcentricLayout |          |\n| nodeSize     | Node size (diameter), used for collision detection. If not specified, it is calculated from the node's size property, or defaults to 10.           | `number` \\| `number[]` \\| (d?: [NodeData](/en/manual/data#节点数据nodedata)) => number     | 10               |          |\n| outerLayout  | Layout algorithm for the outermost layer, [see below](#outerlayout)                                                                                | [`Layout`](https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L866)     | ForceLayout      |          |\n| spacing      | Minimum spacing between node/combo edges when preventNodeOverlap or preventOverlap is `true`. Can be a callback for different nodes.               | `number` \\| (d?: [NodeData](/en/manual/data#节点数据nodedata)) => number                   | -                |          |\n| treeKey      | treeKey                                                                                                                                            | `string`                                                                                   | -                |          |\n\n### innerLayout\n\n> _`Layout<any>`_ **Default:** `ConcentricLayout`\n\nThe layout algorithm for elements inside the combo. Must use a synchronous layout algorithm. Default is [ConcentricLayout](https://github.com/antvis/layout/blob/v5/packages/layout/src/concentric.ts). [More layouts](https://github.com/antvis/layout/tree/v5/packages/layout)\n\n**Example**:\n\n```ts\nimport { ConcentricLayout } from '@antv/layout';\n\nnew Graph({\n  layout: {\n    type: 'combo-combined',\n    /**\n     * See more ConcentricLayout options:\n     * https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L397\n     */\n    innerLayout: new ConcentricLayout({\n      sortBy: 'id',\n      nodeSize: 20,\n      clockwise: true,\n    }),\n  },\n});\n```\n\n### outerLayout\n\n> _`Layout<any>`_ **Default:** `ForceLayout`\n\nThe layout algorithm for the outermost layer. Default is [ForceLayout](https://github.com/antvis/layout/blob/v5/packages/layout/src/force/index.ts). [More layouts](https://github.com/antvis/layout/tree/v5/packages/layout)\n\n**Example**\n\n```ts\nimport { ForceLayout } from '@antv/layout';\n\nnew Graph({\n  layout: {\n    type: 'combo-combined',\n    /**\n     * See more ForceLayout options:\n     * https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L950\n     */\n    outerLayout: new ForceLayout({\n      gravity: 1,\n      factor: 2,\n      linkDistance: (edge: any, source: any, target: any) => {\n        const nodeSize = ((source.size?.[0] || 30) + (target.size?.[0] || 30)) / 2;\n        return Math.min(nodeSize * 1.5, 70);\n      },\n    }),\n  },\n});\n```\n\n## Example Code\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/combo.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'combo-combined',\n        comboPadding: 2,\n      },\n      node: {\n        style: {\n          size: 20,\n          labelText: (d) => d.id,\n        },\n        palette: {\n          type: 'group',\n          field: (d) => d.combo,\n        },\n      },\n      edge: {\n        style: (model) => {\n          const { size, color } = model.data;\n          return {\n            stroke: color || '#99ADD1',\n            lineWidth: size || 1,\n          };\n        },\n      },\n      behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n      autoFit: 'view',\n    });\n\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/ComboCombinedLayout.zh.md",
    "content": "---\ntitle: 复合布局 ComboCombined\norder: 4\n---\n\n## 概述\n\nComboCombined 复合布局适用于复合分组结构的图数据展示场景，支持灵活配置 Combo 内部元素的布局以及最外层 Combo 和节点之间的布局。 默认情况内部元素采用 Concentric 同心圆布局，外部布局采用 gForce 力导向布局，兼顾布局效果与整体稳定性。参考更多 ComboCombined 复合布局[样例](/examples#layout-combo-layout)和[源码](https://github.com/antvis/layout/blob/v5/packages/layout/src/combo-combined.ts)\n\n## 使用场景\n\n- 用户画像分析: 分析用户行为与商品关系，将用户兴趣圈层作为 Combo，内部节点展示具体商品和行为标签，帮助运营人员识别用户消费路径。\n- 供应链管理图：供应商、制造商、仓储、分销商按角色或区域划分 Combo，内部节点展示资源、人员或设备，清晰展示供应链各环节内部结构。\n\n## 配置项\n\n| 属性         | 描述                                                                                                                                         | 类型                                                                                       | 默认值           | 必选 |\n| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------- | ---- |\n| type         | 布局类型                                                                                                                                     | `combo-combined`                                                                           | -                | ✓    |\n| center       | 布局中心                                                                                                                                     | [`PointTuple`](https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L829) | 图中心           |      |\n| comboPadding | Combo 内部的 padding 值，不用于渲染，仅用于计算力。推荐设置为与视图上 Combo 内部 padding 值相同的值                                          | `((d?: unknown) => number)` \\| `number` \\| `number[]` \\| `undefined`                       | 10               |      |\n| innerLayout  | Combo 内部的布局算法, [说明](#innerlayout)                                                                                                   | [`Layout`](https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L881)     | ConcentricLayout |      |\n| nodeSize     | 节点大小（直径）。用于碰撞检测。若不指定，则根据传入的节点的 size 属性计算。若即不指定，节点中也没有 size，则默认大小为 10                   | `number` \\| `number[]` \\| (d?: [NodeData](/manual/data#节点数据nodedata)) => number        | 10               |      |\n| outerLayout  | 最外层的布局算法, [说明](#outerlayout)                                                                                                       | [`Layout`](https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L866)     | ForceLayout      |      |\n| spacing      | preventNodeOverlap 或 preventOverlap 为 `true` 时生效, 防止重叠时节点 / Combo 边缘间距的最小值。可以是回调函数, 为不同节点设置不同的最小间距 | `number` \\| (d?: [NodeData](/manual/data#节点数据nodedata)) => number                      | -                |      |\n| treeKey      | treeKey                                                                                                                                      | `string`                                                                                   | -                |      |\n\n### innerLayout\n\n> _`Layout<any>`_ **Default:** `ConcentricLayout`\n\nCombo 内部的布局算法，需要使用同步的布局算法，默认为 [ConcentricLayout](https://github.com/antvis/layout/blob/v5/packages/layout/src/concentric.ts)，[更多布局算法](https://github.com/antvis/layout/tree/v5/packages/layout)\n\n**示例**:\n\n```ts\nimport { ConcentricLayout } from '@antv/layout';\n\nnew Graph({\n  layout: {\n    type: 'combo-combined',\n    /**\n     * 查看更多 ConcentricLayout 配置参数:\n     * https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L397\n     */\n    innerLayout: new ConcentricLayout({\n      sortBy: 'id',\n      nodeSize: 20,\n      clockwise: true,\n    }),\n  },\n});\n```\n\n### outerLayout\n\n> _`Layout<any>`_ **Default:** `ForceLayout`\n\n最外层的布局算法，默认为 [ForceLayout](https://github.com/antvis/layout/blob/v5/packages/layout/src/force/index.ts)，[更多布局算法](https://github.com/antvis/layout/tree/v5/packages/layout)\n\n**示例**\n\n```ts\nimport { ForceLayout } from '@antv/layout';\n\nnew Graph({\n  layout: {\n    type: 'combo-combined',\n    /**\n     * 查看更多 ForceLayout 配置参数:\n     * https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L950\n     */\n    outerLayout: new ForceLayout({\n      gravity: 1,\n      factor: 2,\n      linkDistance: (edge: any, source: any, target: any) => {\n        const nodeSize = ((source.size?.[0] || 30) + (target.size?.[0] || 30)) / 2;\n        return Math.min(nodeSize * 1.5, 70);\n      },\n    }),\n  },\n});\n```\n\n## 示例代码\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/combo.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'combo-combined',\n        comboPadding: 2,\n      },\n      node: {\n        style: {\n          size: 20,\n          labelText: (d) => d.id,\n        },\n        palette: {\n          type: 'group',\n          field: (d) => d.combo,\n        },\n      },\n      edge: {\n        style: (model) => {\n          const { size, color } = model.data;\n          return {\n            stroke: color || '#99ADD1',\n            lineWidth: size || 1,\n          };\n        },\n      },\n      behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n      autoFit: 'view',\n    });\n\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/CompactBoxLayout.en.md",
    "content": "---\ntitle: CompactBox Layout\norder: 5\n---\n\n## Overview\n\nThe CompactBox layout is suitable for visualizing structured tree data. It is evolved from the classic [Reingold–Tilford tidy layout algorithm](http://emr.cs.iit.edu/~reingold/tidier-drawings.pdf), and considers the bounding box of each tree node during layout, effectively maintaining the compactness and hierarchical clarity of the tree structure. See more CompactBox layout [examples](/en/examples#layout-compact-box) and [source code](https://github.com/antvis/hierarchy/blob/master/src/compact-box.js).\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*z-ESRoHTpvIAAAAAAAAAAABkARQnAQ' width=650 alt='CompactBox Tidy Tree Layout Example'/>\n\n## Usage Scenarios\n\n- Decision trees: The compact tree layout can visually and intuitively display each decision path.\n- Knowledge graphs: Show hierarchical relationships and connections between concepts. The compact layout can present complex knowledge networks in limited space.\n\n## Configuration\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'compact-box',\n    direction: 'LR',\n    getHeight: () => 16,\n    getWidth: () => 16,\n    getVGap: () => 16,\n    getHGap: () => 40,\n  },\n});\n```\n\n## Options\n\n| Property  | Description                                                                                                   | Type                                                         | Default | Required |\n| --------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ------- | -------- |\n| type      | Layout type                                                                                                   | `compact-box`                                                | -       | ✓        |\n| direction | Layout direction, [options](#direction)                                                                       | `LR` \\| `RL` \\| `TB` \\| `BT` \\| `H` \\| `V`                   | `LR`    |          |\n| getSide   | Set whether the node is on the left or right of the root. Only works for `H` direction. [See below](#getside) | (d: { data?: [NodeData](/en/manual/data#节点数据nodedata) }) => string |         |          |\n| getId     | Callback for node id                                                                                          | (d?: [NodeData](/en/manual/data#节点数据nodedata)) => string |         |          |\n| getWidth  | Callback for node width                                                                                       | (d?: [NodeData](/en/manual/data#节点数据nodedata)) => number |         |          |\n| getHeight | Callback for node height                                                                                      | (d?: [NodeData](/en/manual/data#节点数据nodedata)) => number |         |          |\n| getHGap   | Callback for horizontal gap                                                                                   | (d?: [NodeData](/en/manual/data#节点数据nodedata)) => number |         |          |\n| getVGap   | Callback for vertical gap                                                                                     | (d?: [NodeData](/en/manual/data#节点数据nodedata)) => number |         |          |\n| radial    | Whether to enable radial layout, [see below](#radial)                                                         | boolean                                                      | false   |          |\n\n### direction\n\n> `LR` \\| `RL` \\| `TB` \\| `BT` \\| `H` \\| `V` **Default:** `LR`\n\nTree layout direction\n\n- `TB`: Root at the top, layout downwards\n- `BT`: Root at the bottom, layout upwards\n- `LR`: Root at the left, layout to the right\n- `RL`: Root at the right, layout to the left\n- `H`: Root in the middle, horizontal symmetric layout. You can use `getSide` to specify the left/right logic for each node\n- `V`: Root in the middle, vertical symmetric layout\n\n### getSide\n\n> _(d: { data?: [NodeData](/en/manual/data#节点数据nodedata) }) => string_\n\nSet whether the node is on the left or right of the root. Only works for `H` direction. If not set, the algorithm will automatically assign left/right. See [getSide auto logic](https://github.com/antvis/hierarchy/blob/d786901874f59d96c47e2a5dfe17b373eefd72e3/src/layout/separate-root.js#L11).\n\nExample:\n\n```javascript\n({ data }) => {\n  // data is a node\n  if (data.id === 'test-child-id') return 'right';\n  return 'left';\n};\n```\n\n### getId\n\n> _(d?: [NodeData](/en/manual/data#节点数据nodedata)) => string_\n\nCallback for node id\n\nExample:\n\n```javascript\n(d) => {\n  // d is a node\n  return d.id + '_node';\n};\n```\n\n### getWidth\n\n> _(d?: [NodeData](/en/manual/data#节点数据nodedata)) => number_\n\nCallback for node width\n\nExample:\n\n```javascript\n(d) => {\n  // d is a node\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getHeight\n\n> _(d?: [NodeData](/en/manual/data#节点数据nodedata)) => number_\n\nCallback for node height\n\nExample:\n\n```javascript\n(d) => {\n  // d is a node\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getHGap\n\n> _(d?: [NodeData](/en/manual/data#节点数据nodedata)) => number_\n\nCallback for horizontal gap\n\nExample:\n\n```javascript\n(d) => {\n  // d is a node\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getVGap\n\n> _(d?: [NodeData](/en/manual/data#节点数据nodedata)) => number_\n\nCallback for vertical gap\n\nExample:\n\n```javascript\n(d) => {\n  // d is a node\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### radial\n\n> _boolean_\n\nWhether to use radial layout. If `radial` is `true`, it is recommended to set `direction` to `'LR'` or `'RL'`.\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*E0c8TIYRPYoAAAAAAAAAAABkARQnAQ' width=200 alt='img'/>\n\n## Example Code\n\n```js | ob { inject: true }\nimport { Graph, treeToGraphData } from '@antv/g6';\n\n/**\n * If the node is a leaf node\n * @param {*} d - node data\n * @returns {boolean} - whether the node is a leaf node\n */\nfunction isLeafNode(d) {\n  return !d.children || d.children.length === 0;\n}\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: (d) => (isLeafNode(d) ? 'right' : 'left'),\n          labelBackground: true,\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'compact-box',\n        direction: 'LR',\n        getHeight: function getHeight() {\n          return 32;\n        },\n        getWidth: function getWidth() {\n          return 32;\n        },\n        getVGap: function getVGap() {\n          return 10;\n        },\n        getHGap: function getHGap() {\n          return 100;\n        },\n      },\n    });\n\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/CompactBoxLayout.zh.md",
    "content": "---\ntitle: 紧凑树布局 CompactBox\norder: 5\n---\n\n## 概述\n\n紧凑树布局适用于结构化树形数据的展示，基于经典的 [Reingold–Tilford tidy 布局算法](http://emr.cs.iit.edu/~reingold/tidier-drawings.pdf) 演进而来，通过布局时综合考虑每个树节点的包围盒，有效保持树结构的紧凑性与层次清晰。参考更多 CompactBox 布局[样例](/examples#layout-compact-box)和[源码](https://github.com/antvis/hierarchy/blob/master/src/compact-box.js)\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*z-ESRoHTpvIAAAAAAAAAAABkARQnAQ' width=650 alt='CompactBox 紧凑树布局示例'/>\n\n## 使用场景\n\n- 决策树: 通过紧凑树布局可简单直观的图形化展示每个决策路径\n- 知识图谱: 展示概念之间的层级关系和连接，紧凑布局可以在有限空间内呈现复杂的知识网络\n\n## 配置方式\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'compact-box',\n    direction: 'LR',\n    getHeight: () => 16,\n    getWidth: () => 16,\n    getVGap: () => 16,\n    getHGap: () => 40,\n  },\n});\n```\n\n## 配置项\n\n| 属性      | 描述                                                                                                    | 类型                                                      | 默认值 | 必选 |\n| --------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ------ | ---- |\n| type      | 布局类型                                                                                                | `compact-box`                                             | -      | ✓    |\n| direction | 布局方向，[可选值](#direction)                                                                          | `LR` \\| `RL` \\| `TB` \\| `BT` \\| `H` \\| `V`                | `LR`   |      |\n| getSide   | 设置节点排布在根节点的左侧/右侧，如未设置，则算法自动分配左侧/右侧。注意：该参数仅在 `H` 布局方向上生效 | (d: { data?: [NodeData](/manual/data#节点数据nodedata) }) => string |        |      |\n| getId     | 节点 id 的回调函数                                                                                      | (d?: [NodeData](/manual/data#节点数据nodedata)) => string |        |      |\n| getWidth  | 计算每个节点的宽度                                                                                      | (d?: [NodeData](/manual/data#节点数据nodedata)) => number |        |      |\n| getHeight | 计算每个节点的高度                                                                                      | (d?: [NodeData](/manual/data#节点数据nodedata)) => number |        |      |\n| getHGap   | 计算每个节点的水平间隙                                                                                  | (d?: [NodeData](/manual/data#节点数据nodedata)) => number |        |      |\n| getVGap   | 计算每个节点的垂直间隙                                                                                  | (d?: [NodeData](/manual/data#节点数据nodedata)) => number |        |      |\n| radial    | 是否启用辐射状布局，[说明](#radial)                                                                     | boolean                                                   | false  |      |\n\n### direction\n\n> `LR` \\| `RL` \\| `TB` \\| `BT` \\| `H` \\| `V` **Default:** `LR`\n\n树布局方向\n\n- `TB`：根节点在上，往下布局\n\n  <img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*KrAqTrFbNjMAAAAAAAAAAABkARQnAQ' width=150 alt='垂直布局'/>\n\n- `BT`：根节点在下，往上布局\n\n  <img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*vNmOTJ4q0uwAAAAAAAAAAABkARQnAQ' width=150 alt='垂直布局'/>\n\n- `LR`：根节点在左，往右布局\n\n  <img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*ffD6S74MXw4AAAAAAAAAAABkARQnAQ' width=150 alt='水平布局'/>\n\n- `RL`：根节点在右，往左布局\n\n  <img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*vTg2SJbtj_sAAAAAAAAAAABkARQnAQ' width=150 alt='水平布局'/>\n\n- `H`：根节点在中间，水平对称布局。可传入 `getSide` 方法指定每个节点的左右分布逻辑\n\n  <img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*0GsIQISvieYAAAAAAAAAAABkARQnAQ' width=150 alt='水平布局'/>\n\n- `V`：根节点在中间，垂直对称布局\n\n  <img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*E0c8TIYRPYoAAAAAAAAAAABkARQnAQ' width=150 alt='垂直布局'/>\n\n### getSide\n\n> _(d: { data?: [NodeData](/manual/data#节点数据nodedata) }) => string_\n\n设置节点排布在根节点的左侧/右侧。注意：该参数仅在 `direction` 为 `H` 时生效。如未设置，会默认将子节点前半部分放置在右侧，后半部分放置在左侧，参考 [getSide自动计算逻辑](https://github.com/antvis/hierarchy/blob/d786901874f59d96c47e2a5dfe17b373eefd72e3/src/layout/separate-root.js#L11)。\n\n示例：\n\n```javascript\n({ data }) => {\n  // data 是一个节点\n  if (data.id === 'test-child-id') return 'right';\n  return 'left';\n};\n```\n\n### getId\n\n> _(d?: [NodeData](/manual/data#节点数据nodedata)) => string_\n\n节点 id 的回调函数\n\n示例：\n\n```javascript\n(d) => {\n  // d 是一个节点\n  return d.id + '_node';\n};\n```\n\n### getWidth\n\n> _(d?: [NodeData](/manual/data#节点数据nodedata)) => number_\n\n每个节点的宽度\n\n示例：\n\n```javascript\n(d) => {\n  // d 是一个节点\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getHeight\n\n> _(d?: [NodeData](/manual/data#节点数据nodedata)) => number_\n\n每个节点的高度\n\n示例：\n\n```javascript\n(d) => {\n  // d 是一个节点\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getHGap\n\n> _(d?: [NodeData](/manual/data#节点数据nodedata)) => number_\n\n每个节点的水平间隙\n\n示例：\n\n```javascript\n(d) => {\n  // d 是一个节点\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getVGap\n\n> _(d?: [NodeData](/manual/data#节点数据nodedata)) => number_\n\n每个节点的垂直间隙\n\n示例：\n\n```javascript\n(d) => {\n  // d 是一个节点\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### radial\n\n> _boolean_\n\n是否按照辐射状布局。若 `radial` 为 `true`，建议 `direction` 设置为 `'LR'` 或 `'RL'`\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*E0c8TIYRPYoAAAAAAAAAAABkARQnAQ' width=200 alt='img'/>\n\n## 代码示例\n\n```js | ob { inject: true }\nimport { Graph, treeToGraphData } from '@antv/g6';\n\n/**\n * If the node is a leaf node\n * @param {*} d - node data\n * @returns {boolean} - whether the node is a leaf node\n */\nfunction isLeafNode(d) {\n  return !d.children || d.children.length === 0;\n}\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: (d) => (isLeafNode(d) ? 'right' : 'left'),\n          labelBackground: true,\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'compact-box',\n        direction: 'LR',\n        getHeight: function getHeight() {\n          return 32;\n        },\n        getWidth: function getWidth() {\n          return 32;\n        },\n        getVGap: function getVGap() {\n          return 10;\n        },\n        getHGap: function getHGap() {\n          return 100;\n        },\n      },\n    });\n\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/ConcentricLayout.en.md",
    "content": "---\ntitle: Concentric Layout\norder: 6\n---\n\n## Overview\n\nThe concentric layout arranges nodes in layers according to a certain sorting rule, with each layer of nodes placed around a common center. See more concentric layout [examples](/en/examples#layout-concentric) or [source code](https://github.com/antvis/layout/blob/v5/packages/layout/src/circular.ts).\n\n## Usage Scenarios\n\n- Layered data visualization, such as permission structures, organizational charts, etc., with the center as the top-level role and outer rings as lower-level nodes.\n- Visualization of ranking analysis results, with high-importance nodes in the center and low-importance nodes on the periphery, quickly expressing the relative influence of nodes in the graph.\n\n## Options\n\n| Property       | Description                                                                                                                                                                 | Type                                               | Default          | Required |\n| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ---------------- | -------- |\n| type           | Layout type                                                                                                                                                                 | `concentric`                                       | -                | ✓        |\n| center         | Center position of the circular layout, defaults to the center of the container                                                                                             | [number, number] \\| [number, number, number]       | -                |          |\n| clockwise      | Whether to arrange nodes clockwise                                                                                                                                          | boolean                                            | false            |          |\n| equidistant    | Whether the distance between rings is equal                                                                                                                                 | boolean                                            | false            |          |\n| width          | Layout width, defaults to container width                                                                                                                                   | number                                             | -                |          |\n| height         | Layout height, defaults to container height                                                                                                                                 | number                                             | -                |          |\n| sortBy         | The property to sort by (node attribute name). The higher the value, the closer to the center. If set to 'degree', nodes with higher degree are placed closer to the center | string                                             | `degree`         |          |\n| maxLevelDiff   | Maximum attribute difference in the same layer. If undefined, set to maxValue / 4, where maxValue is the maximum value of the sorting property                              | number                                             | undefined        |          |\n| nodeSize       | Node size (diameter), used for collision detection                                                                                                                          | number \\| number[] \\| ((nodeData: Node) => number) | 30               |          |\n| nodeSpacing    | Minimum spacing between rings, used to adjust the radius                                                                                                                    | number \\| number[] \\| ((node?: Node) => number)    | 10               |          |\n| preventOverlap | Whether to prevent overlap. Must be used with nodeSize or data.size. Only works if node size is set in data or in this layout config.                                       | boolean                                            | false            |          |\n| startAngle     | The angle (in radians) to start laying out nodes                                                                                                                            | number                                             | 3 / 2 \\* Math.PI |          |\n| sweep          | The angle difference between the first and last node in the same layer. If undefined, set to 2 _Math.PI_ (1 - 1 / level.nodes )                                             | number                                             | undefined        |          |\n\n## Example Code\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 500,\n  height: 250,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      { id: 'center', data: { label: 'Center', level: 0 } },\n      { id: 'level1-0', data: { label: 'L1-0', level: 1 } },\n      { id: 'level1-1', data: { label: 'L1-1', level: 1 } },\n      { id: 'level1-2', data: { label: 'L1-2', level: 1 } },\n      { id: 'level1-3', data: { label: 'L1-3', level: 1 } },\n      { id: 'level1-4', data: { label: 'L1-4', level: 1 } },\n      { id: 'level1-5', data: { label: 'L1-5', level: 1 } },\n      { id: 'level2-0', data: { label: 'L2-0', level: 2 } },\n      { id: 'level2-1', data: { label: 'L2-1', level: 2 } },\n      { id: 'level2-2', data: { label: 'L2-2', level: 2 } },\n      { id: 'level2-3', data: { label: 'L2-3', level: 2 } },\n      { id: 'level2-4', data: { label: 'L2-4', level: 2 } },\n      { id: 'level2-5', data: { label: 'L2-5', level: 2 } },\n      { id: 'level2-6', data: { label: 'L2-6', level: 2 } },\n      { id: 'level2-7', data: { label: 'L2-7', level: 2 } },\n      { id: 'level2-8', data: { label: 'L2-8', level: 2 } },\n      { id: 'level2-9', data: { label: 'L2-9', level: 2 } },\n      { id: 'level2-10', data: { label: 'L2-10', level: 2 } },\n      { id: 'level2-11', data: { label: 'L2-11', level: 2 } },\n    ],\n    edges: [\n      { id: 'e-center-level1-0', source: 'center', target: 'level1-0' },\n      { id: 'e-center-level1-1', source: 'center', target: 'level1-1' },\n      { id: 'e-center-level1-2', source: 'center', target: 'level1-2' },\n      { id: 'e-center-level1-3', source: 'center', target: 'level1-3' },\n      { id: 'e-center-level1-4', source: 'center', target: 'level1-4' },\n      { id: 'e-center-level1-5', source: 'center', target: 'level1-5' },\n      { id: 'e-level1-0-level2-0', source: 'level1-0', target: 'level2-0' },\n      { id: 'e-level1-0-level2-1', source: 'level1-0', target: 'level2-1' },\n      { id: 'e-level1-1-level2-2', source: 'level1-1', target: 'level2-2' },\n      { id: 'e-level1-1-level2-3', source: 'level1-1', target: 'level2-3' },\n      { id: 'e-level1-2-level2-4', source: 'level1-2', target: 'level2-4' },\n      { id: 'e-level1-2-level2-5', source: 'level1-2', target: 'level2-5' },\n      { id: 'e-level1-3-level2-6', source: 'level1-3', target: 'level2-6' },\n      { id: 'e-level1-3-level2-7', source: 'level1-3', target: 'level2-7' },\n      { id: 'e-level1-4-level2-8', source: 'level1-4', target: 'level2-8' },\n      { id: 'e-level1-4-level2-9', source: 'level1-4', target: 'level2-9' },\n      { id: 'e-level1-5-level2-10', source: 'level1-5', target: 'level2-10' },\n      { id: 'e-level1-5-level2-11', source: 'level1-5', target: 'level2-11' },\n    ],\n  },\n  layout: {\n    type: 'concentric',\n    nodeSize: 32,\n    sortBy: 'degree',\n    preventOverlap: true,\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  animation: false,\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/ConcentricLayout.zh.md",
    "content": "---\ntitle: 同心圆布局 Concentric\norder: 6\n---\n\n## 概述\n\n同心圆布局是一种将节点根据某种排序规则分层，并以圆心为中心、沿圆周排列每层节点的布局方式。参考更多同心圆布局[样例](/examples#layout-concentric)或[源码](https://github.com/antvis/layout/blob/v5/packages/layout/src/circular.ts)。\n\n## 使用场景\n\n- 分层数据可视化，如权限控制结构、组织架构图等，中心是顶级角色，外圈为下级节点。\n- 排序分析结果可视化，高重要度放中心，低重要度放外围，快速表达图中节点的相对影响力。\n\n## 配置项\n\n| 属性           | 描述                                                                                                                                                                                                        | 类型                                               | 默认值           | 必选 |\n| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ---------------- | ---- |\n| type           | 布局类型                                                                                                                                                                                                    | `concentric`                                       | -                | ✓    |\n| center         | 圆形布局的中心位置，默认为当前容器的中心位置                                                                                                                                                                | [number, number] \\| [number, number, number]       | -                |      |\n| clockwise      | 是否按照顺时针排列                                                                                                                                                                                          | boolean                                            | false            |\n| equidistant    | 环与环之间的距离是否相等                                                                                                                                                                                    | boolean                                            | false            |      |\n| width          | 布局的宽度，默认使用容器宽度                                                                                                                                                                                | number                                             | -                |      |\n| height         | 布局的高度，默认使用容器高度                                                                                                                                                                                | number                                             | -                |      |\n| sortBy         | 指定排序的依据（节点属性名）<br>数值越高则该节点被放置得越中心。若为 degree，则会计算节点的度数，度数越高，节点将被放置得越中心                                                                             | string                                             | `degree`         |      |\n| maxLevelDiff   | 同一层节点的最大属性差值<br>若为 undefined，则将会被设置为 maxValue / 4 ，其中 maxValue 为最大的排序依据的属性值。例如，若 sortBy 为 'degree'，则 maxValue 为所有节点中度数最大的节点的度数                 | number                                             | undefined        |      |\n| nodeSize       | 节点大小（直径）。用于防止节点重叠时的碰撞检测                                                                                                                                                              | number \\| number[] \\| ((nodeData: Node) => number) | 30               |      |\n| nodeSpacing    | 环与环之间最小间距，用于调整半径                                                                                                                                                                            | number \\| number[] \\| ((node?: Node) => number)    | 10               |      |\n| preventOverlap | 是否防止重叠<br>必须配合 nodeSize 属性或节点数据中的 data.size 属性，只有在数据中设置了 data.size 或在该布局中配置了与当前图节点大小相同的 nodeSize 值，才能够进行节点重叠的碰撞检测                        | boolean                                            | false            |      |\n| startAngle     | 开始布局节点的弧度                                                                                                                                                                                          | number                                             | 3 / 2 \\* Math.PI |      |\n| sweep          | 同一层中第一个节点与最后一个节点之间的弧度差<br>若为 undefined ，则将会被设置为 2 \\* Math.PI \\* (1 - 1 / \\|level.nodes\\|) ，其中 level.nodes 为该算法计算出的每一层的节点，\\|level.nodes\\| 代表该层节点数量 | number                                             | undefined        |      |\n\n## 代码示例\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 500,\n  height: 250,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      { id: 'center', data: { label: '中心', level: 0 } },\n      { id: 'level1-0', data: { label: 'L1-0', level: 1 } },\n      { id: 'level1-1', data: { label: 'L1-1', level: 1 } },\n      { id: 'level1-2', data: { label: 'L1-2', level: 1 } },\n      { id: 'level1-3', data: { label: 'L1-3', level: 1 } },\n      { id: 'level1-4', data: { label: 'L1-4', level: 1 } },\n      { id: 'level1-5', data: { label: 'L1-5', level: 1 } },\n      { id: 'level2-0', data: { label: 'L2-0', level: 2 } },\n      { id: 'level2-1', data: { label: 'L2-1', level: 2 } },\n      { id: 'level2-2', data: { label: 'L2-2', level: 2 } },\n      { id: 'level2-3', data: { label: 'L2-3', level: 2 } },\n      { id: 'level2-4', data: { label: 'L2-4', level: 2 } },\n      { id: 'level2-5', data: { label: 'L2-5', level: 2 } },\n      { id: 'level2-6', data: { label: 'L2-6', level: 2 } },\n      { id: 'level2-7', data: { label: 'L2-7', level: 2 } },\n      { id: 'level2-8', data: { label: 'L2-8', level: 2 } },\n      { id: 'level2-9', data: { label: 'L2-9', level: 2 } },\n      { id: 'level2-10', data: { label: 'L2-10', level: 2 } },\n      { id: 'level2-11', data: { label: 'L2-11', level: 2 } },\n    ],\n    edges: [\n      { id: 'e-center-level1-0', source: 'center', target: 'level1-0' },\n      { id: 'e-center-level1-1', source: 'center', target: 'level1-1' },\n      { id: 'e-center-level1-2', source: 'center', target: 'level1-2' },\n      { id: 'e-center-level1-3', source: 'center', target: 'level1-3' },\n      { id: 'e-center-level1-4', source: 'center', target: 'level1-4' },\n      { id: 'e-center-level1-5', source: 'center', target: 'level1-5' },\n\n      { id: 'e-level1-0-level2-0', source: 'level1-0', target: 'level2-0' },\n      { id: 'e-level1-0-level2-1', source: 'level1-0', target: 'level2-1' },\n      { id: 'e-level1-1-level2-2', source: 'level1-1', target: 'level2-2' },\n      { id: 'e-level1-1-level2-3', source: 'level1-1', target: 'level2-3' },\n      { id: 'e-level1-2-level2-4', source: 'level1-2', target: 'level2-4' },\n      { id: 'e-level1-2-level2-5', source: 'level1-2', target: 'level2-5' },\n      { id: 'e-level1-3-level2-6', source: 'level1-3', target: 'level2-6' },\n      { id: 'e-level1-3-level2-7', source: 'level1-3', target: 'level2-7' },\n      { id: 'e-level1-4-level2-8', source: 'level1-4', target: 'level2-8' },\n      { id: 'e-level1-4-level2-9', source: 'level1-4', target: 'level2-9' },\n      { id: 'e-level1-5-level2-10', source: 'level1-5', target: 'level2-10' },\n      { id: 'e-level1-5-level2-11', source: 'level1-5', target: 'level2-11' },\n    ],\n  },\n  layout: {\n    type: 'concentric',\n    nodeSize: 32,\n    sortBy: 'degree',\n    preventOverlap: true,\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  animation: false,\n});\n\ngraph.render();\n```\n\n<details><summary>展开查看完整代码</summary>\n\n```javascript\nimport { Graph } from '@antv/g6';\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'view',\n  data: {\n    nodes: [\n      { id: 'center', data: { label: '中心', level: 0 } },\n\n      { id: 'level1-0', data: { label: 'L1-0', level: 1 } },\n      { id: 'level1-1', data: { label: 'L1-1', level: 1 } },\n      { id: 'level1-2', data: { label: 'L1-2', level: 1 } },\n      { id: 'level1-3', data: { label: 'L1-3', level: 1 } },\n      { id: 'level1-4', data: { label: 'L1-4', level: 1 } },\n      { id: 'level1-5', data: { label: 'L1-5', level: 1 } },\n\n      { id: 'level2-0', data: { label: 'L2-0', level: 2 } },\n      { id: 'level2-1', data: { label: 'L2-1', level: 2 } },\n      { id: 'level2-2', data: { label: 'L2-2', level: 2 } },\n      { id: 'level2-3', data: { label: 'L2-3', level: 2 } },\n      { id: 'level2-4', data: { label: 'L2-4', level: 2 } },\n      { id: 'level2-5', data: { label: 'L2-5', level: 2 } },\n      { id: 'level2-6', data: { label: 'L2-6', level: 2 } },\n      { id: 'level2-7', data: { label: 'L2-7', level: 2 } },\n      { id: 'level2-8', data: { label: 'L2-8', level: 2 } },\n      { id: 'level2-9', data: { label: 'L2-9', level: 2 } },\n      { id: 'level2-10', data: { label: 'L2-10', level: 2 } },\n      { id: 'level2-11', data: { label: 'L2-11', level: 2 } },\n    ],\n    edges: [\n      { id: 'e-center-level1-0', source: 'center', target: 'level1-0' },\n      { id: 'e-center-level1-1', source: 'center', target: 'level1-1' },\n      { id: 'e-center-level1-2', source: 'center', target: 'level1-2' },\n      { id: 'e-center-level1-3', source: 'center', target: 'level1-3' },\n      { id: 'e-center-level1-4', source: 'center', target: 'level1-4' },\n      { id: 'e-center-level1-5', source: 'center', target: 'level1-5' },\n\n      { id: 'e-level1-0-level2-0', source: 'level1-0', target: 'level2-0' },\n      { id: 'e-level1-0-level2-1', source: 'level1-0', target: 'level2-1' },\n      { id: 'e-level1-1-level2-2', source: 'level1-1', target: 'level2-2' },\n      { id: 'e-level1-1-level2-3', source: 'level1-1', target: 'level2-3' },\n      { id: 'e-level1-2-level2-4', source: 'level1-2', target: 'level2-4' },\n      { id: 'e-level1-2-level2-5', source: 'level1-2', target: 'level2-5' },\n      { id: 'e-level1-3-level2-6', source: 'level1-3', target: 'level2-6' },\n      { id: 'e-level1-3-level2-7', source: 'level1-3', target: 'level2-7' },\n      { id: 'e-level1-4-level2-8', source: 'level1-4', target: 'level2-8' },\n      { id: 'e-level1-4-level2-9', source: 'level1-4', target: 'level2-9' },\n      { id: 'e-level1-5-level2-10', source: 'level1-5', target: 'level2-10' },\n      { id: 'e-level1-5-level2-11', source: 'level1-5', target: 'level2-11' },\n    ],\n  },\n  layout: {\n    type: 'concentric',\n    nodeSize: 32,\n    sortBy: 'degree',\n    preventOverlap: true,\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  animation: false,\n});\n\ngraph.render();\n```\n\n</details>\n"
  },
  {
    "path": "packages/site/docs/manual/layout/D3Force3DLayout.en.md",
    "content": "---\ntitle: 3D Force-Directed Layout\norder: 7\n---\n\n## Overview\n\nThe D3Force3D layout is a 3D extension based on [d3-force](https://d3js.org/d3-force), which simulates physical forces in three-dimensional space to achieve automatic layout. Compared to 2D layouts, it adds force effects in the Z-axis direction, allowing richer data relationships to be displayed in 3D space.\n\n<img width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*4mbSTJLOXkgAAAAAAAAAAAAADmJ7AQ/original\" alt=\"3D Force-Directed Layout Illustration\" />\n\n## Core Concepts\n\n### Force System\n\nD3Force3D extends the traditional 2D force-directed layout with the following forces:\n\n- **3D Centering Force**: Pulls nodes toward the center point in 3D space\n- **3D Collision Force**: Prevents node overlap in 3D space\n- **3D Radial Force**: Attracts nodes to a sphere in 3D space\n- **3D Axis Forces**: Applies forces along the X, Y, and Z axes\n\n### Iteration System\n\nThe layout is computed through iterations, mainly involving the following parameters:\n\n- **alpha**: The current energy value of the iteration, controlling node movement speed\n- **alphaDecay**: The decay rate of the energy value\n- **alphaMin**: The minimum energy value; iteration stops below this value\n- **velocityDecay**: The velocity decay factor\n\n## Options\n\n| Property        | Description                                                      | Type                                                                       | Default       | Required |\n| --------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------- | -------- |\n| type            | Layout type                                                      | string                                                                     | `d3-force-3d` | ✓        |\n| nodeSize        | Node size (diameter), used for collision detection               | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -             |          |\n| iterations      | Number of force iterations; higher means more precise but slower | number                                                                     | -             |          |\n| numDimensions   | Number of dimensions (2 or 3)                                    | number                                                                     | 3             |          |\n| forceSimulation | Custom force simulation method                                   | Simulation<NodeDatum, EdgeDatum>                                           | -             |          |\n| onTick          | Callback for each iteration                                      | (data: LayoutMapping) => void                                              | -             |          |\n| randomSource    | Random number generator                                          | () => number                                                               | -             |          |\n\n### Iteration Control\n\n| Property      | Description                   | Type   | Default | Required |\n| ------------- | ----------------------------- | ------ | ------- | -------- |\n| alpha         | Current convergence threshold | number | 1       |          |\n| alphaDecay    | Convergence decay rate (0-1)  | number | 0.028   |          |\n| alphaMin      | Stop iteration threshold      | number | 0.001   |          |\n| alphaTarget   | Target convergence threshold  | number | 0       |          |\n| velocityDecay | Velocity decay factor         | number | 0.4     |          |\n\n### Force Model Options\n\n#### Centering Force (center)\n\n| Property        | Description         | Type   | Default | Required |\n| --------------- | ------------------- | ------ | ------- | -------- |\n| center.x        | Center x coordinate | number | 0       |          |\n| center.y        | Center y coordinate | number | 0       |          |\n| center.z        | Center z coordinate | number | 0       |          |\n| center.strength | Force strength      | number | 1       |          |\n\n#### Collision Force (collide)\n\n| Property           | Description          | Type                                                                       | Default | Required |\n| ------------------ | -------------------- | -------------------------------------------------------------------------- | ------- | -------- |\n| collide.radius     | Collision radius     | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | 10      |          |\n| collide.strength   | Force strength       | number                                                                     | 1       |          |\n| collide.iterations | Collision iterations | number                                                                     | 1       |          |\n\n#### Link Force (link)\n\n| Property        | Description           | Type                                                                       | Default | Required |\n| --------------- | --------------------- | -------------------------------------------------------------------------- | ------- | -------- |\n| link.id         | Edge id generator     | (edge: EdgeDatum, index: number, edges: EdgeDatum[]) => string             | edge.id |          |\n| link.distance   | Ideal edge length     | number \\| ((edge: EdgeDatum, index: number, edges: EdgeDatum[]) => number) | 30      |          |\n| link.strength   | Force strength        | number \\| ((edge: EdgeDatum, index: number, edges: EdgeDatum[]) => number) | 1       |          |\n| link.iterations | Link force iterations | number                                                                     | 1       |          |\n\n#### Many-Body Force (manyBody)\n\n| Property             | Description                  | Type                                                                       | Default  | Required |\n| -------------------- | ---------------------------- | -------------------------------------------------------------------------- | -------- | -------- |\n| manyBody.strength    | Force strength               | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -30      |          |\n| manyBody.theta       | Barnes-Hut accuracy          | number                                                                     | 0.9      |          |\n| manyBody.distanceMin | Minimum interaction distance | number                                                                     | 1        |          |\n| manyBody.distanceMax | Maximum interaction distance | number                                                                     | Infinity |          |\n\n#### Radial Force (radial)\n\n| Property        | Description     | Type                                                                       | Default | Required |\n| --------------- | --------------- | -------------------------------------------------------------------------- | ------- | -------- |\n| radial.strength | Force strength  | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | 0.1     |          |\n| radial.radius   | Target radius   | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | 100     |          |\n| radial.x        | Sphere center x | number                                                                     | 0       |          |\n| radial.y        | Sphere center y | number                                                                     | 0       |          |\n| radial.z        | Sphere center z | number                                                                     | 0       |          |\n\n#### Axis Forces (x, y, z)\n\nEach axis can be configured separately:\n\n| Property   | Description           | Type                                                                       | Default | Required |\n| ---------- | --------------------- | -------------------------------------------------------------------------- | ------- | -------- |\n| x.strength | X-axis force strength | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -       |          |\n| x.x        | Target x coordinate   | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -       |          |\n| y.strength | Y-axis force strength | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -       |          |\n| y.y        | Target y coordinate   | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -       |          |\n| z.strength | Z-axis force strength | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -       |          |\n| z.z        | Target z coordinate   | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -       |          |\n"
  },
  {
    "path": "packages/site/docs/manual/layout/D3Force3DLayout.zh.md",
    "content": "---\ntitle: 3D D3力导向布局 D3Force3D\norder: 7\n---\n\n## 概述\n\nD3Force3D 布局是基于 [d3-force](https://d3js.org/d3-force) 的三维扩展版本，通过在三维空间中模拟物理力的作用来实现自动布局。相比二维布局，它增加了 Z 轴方向的力作用，能够在三维空间中展现更丰富的数据关系。\n\n<img width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*4mbSTJLOXkgAAAAAAAAAAAAADmJ7AQ/original\" alt=\"3D 力导向布局示意图\" />\n\n## 核心概念\n\n### 力系统\n\nD3Force3D 在传统二维力导向布局的基础上，扩展了以下力的作用：\n\n- **三维中心力**：将节点拉向三维空间的中心点\n- **三维碰撞力**：在三维空间中防止节点重叠\n- **三维径向力**：将节点吸引到三维空间中的球面上\n- **三维坐标力**：分别在 X、Y、Z 三个方向上施加作用力\n\n### 迭代系统\n\n布局计算通过迭代来实现，主要涉及以下参数：\n\n- **alpha**：当前迭代的活力值，控制节点移动速度\n- **alphaDecay**：活力值的衰减率\n- **alphaMin**：最小活力值，低于此值停止迭代\n- **velocityDecay**：速度衰减因子\n\n## 配置项\n\n| 属性            | 描述                                         | 类型                                                                       | 默认值        | 必选 |\n| --------------- | -------------------------------------------- | -------------------------------------------------------------------------- | ------------- | ---- |\n| type            | 布局类型                                     | string                                                                     | `d3-force-3d` | ✓    |\n| nodeSize        | 节点大小（直径），用于碰撞检测防止节点重叠   | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -             |      |\n| iterations      | 力的迭代次数，值越大布局越精确但性能消耗越大 | number                                                                     | -             |      |\n| numDimensions   | 维度数量（2 或 3）                           | number                                                                     | 3             |      |\n| forceSimulation | 自定义力模拟方法                             | Simulation<NodeDatum, EdgeDatum>                                           | -             |      |\n| onTick          | 每次迭代的回调函数                           | (data: LayoutMapping) => void                                              | -             |      |\n| randomSource    | 随机数生成函数                               | () => number                                                               | -             |      |\n\n### 迭代控制\n\n| 属性          | 描述                  | 类型   | 默认值 | 必选 |\n| ------------- | --------------------- | ------ | ------ | ---- |\n| alpha         | 当前迭代收敛阈值      | number | 1      |      |\n| alphaDecay    | 收敛阈值衰减率（0-1） | number | 0.028  |      |\n| alphaMin      | 停止迭代的阈值        | number | 0.001  |      |\n| alphaTarget   | 目标收敛阈值          | number | 0      |      |\n| velocityDecay | 速度衰减因子          | number | 0.4    |      |\n\n### 力模型配置\n\n#### 中心力（center）\n\n| 属性            | 描述          | 类型   | 默认值 | 必选 |\n| --------------- | ------------- | ------ | ------ | ---- |\n| center.x        | 中心点 x 坐标 | number | 0      |      |\n| center.y        | 中心点 y 坐标 | number | 0      |      |\n| center.z        | 中心点 z 坐标 | number | 0      |      |\n| center.strength | 力的强度      | number | 1      |      |\n\n#### 碰撞力（collide）\n\n| 属性               | 描述               | 类型                                                                       | 默认值 | 必选 |\n| ------------------ | ------------------ | -------------------------------------------------------------------------- | ------ | ---- |\n| collide.radius     | 碰撞半径           | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | 10     |      |\n| collide.strength   | 力的强度           | number                                                                     | 1      |      |\n| collide.iterations | 碰撞检测的迭代次数 | number                                                                     | 1      |      |\n\n#### 链接力（link）\n\n| 属性            | 描述             | 类型                                                                       | 默认值  | 必选 |\n| --------------- | ---------------- | -------------------------------------------------------------------------- | ------- | ---- |\n| link.id         | 边的 id 生成函数 | (edge: EdgeDatum, index: number, edges: EdgeDatum[]) => string             | edge.id |      |\n| link.distance   | 理想边长         | number \\| ((edge: EdgeDatum, index: number, edges: EdgeDatum[]) => number) | 30      |      |\n| link.strength   | 力的强度         | number \\| ((edge: EdgeDatum, index: number, edges: EdgeDatum[]) => number) | 1       |      |\n| link.iterations | 链接力的迭代次数 | number                                                                     | 1       |      |\n\n#### 多体力（manyBody）\n\n| 属性                 | 描述                      | 类型                                                                       | 默认值   | 必选 |\n| -------------------- | ------------------------- | -------------------------------------------------------------------------- | -------- | ---- |\n| manyBody.strength    | 力的强度                  | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -30      |      |\n| manyBody.theta       | Barnes-Hut 算法的精度参数 | number                                                                     | 0.9      |      |\n| manyBody.distanceMin | 最小作用距离              | number                                                                     | 1        |      |\n| manyBody.distanceMax | 最大作用距离              | number                                                                     | Infinity |      |\n\n#### 径向力（radial）\n\n| 属性            | 描述        | 类型                                                                       | 默认值 | 必选 |\n| --------------- | ----------- | -------------------------------------------------------------------------- | ------ | ---- |\n| radial.strength | 力的强度    | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | 0.1    |      |\n| radial.radius   | 目标半径    | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | 100    |      |\n| radial.x        | 球心 x 坐标 | number                                                                     | 0      |      |\n| radial.y        | 球心 y 坐标 | number                                                                     | 0      |      |\n| radial.z        | 球心 z 坐标 | number                                                                     | 0      |      |\n\n#### 坐标力（x、y、z）\n\n每个方向的力可以单独配置：\n\n| 属性       | 描述             | 类型                                                                       | 默认值 | 必选 |\n| ---------- | ---------------- | -------------------------------------------------------------------------- | ------ | ---- |\n| x.strength | X 轴方向的力强度 | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -      |      |\n| x.x        | 目标 x 坐标      | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -      |      |\n| y.strength | Y 轴方向的力强度 | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -      |      |\n| y.y        | 目标 y 坐标      | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -      |      |\n| z.strength | Z 轴方向的力强度 | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -      |      |\n| z.z        | 目标 z 坐标      | number \\| ((node: NodeDatum, index: number, nodes: NodeDatum[]) => number) | -      |      |\n"
  },
  {
    "path": "packages/site/docs/manual/layout/D3ForceLayout.en.md",
    "content": "---\ntitle: D3 Force-Directed Layout\norder: 8\n---\n\n## Overview\n\nThe D3Force layout is a force-directed layout based on [d3-force](https://d3js.org/d3-force). It simulates physical forces (such as attraction, repulsion, collision, etc.) to make the graph reach a stable state with minimal energy.\n\nThe main features of this layout are:\n\n1. **Automatic arrangement**: No need to manually set node positions, the system will automatically find suitable positions\n2. **Real-time adjustment**: When you drag a node, other nodes will adjust their positions in real time\n3. **Flexible configuration**:\n   - You can adjust the attraction and repulsion between nodes\n   - You can set the ideal length of edges\n   - You can fix the positions of important nodes\n4. **Animation effect**: Nodes move with smooth animation, making changes more natural\n\n<img alt=\"D3Force layout diagram\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-_sFS5IRGGcAAAAAAAAAAAAADmJ7AQ/original\" />\n\n## Core Concepts\n\n### Force System\n\nThe D3Force layout simulates five different forces to achieve automatic layout. Imagine a physical world where these forces act simultaneously and eventually reach equilibrium:\n\n<img width=\"350\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*p5L2S6gtZ2AAAAAAAAAAAAAAemJ7AQ/original\" alt=\"force\" />\n\n> Note: The arrows of different colors in the diagram represent different types of forces. In the actual layout, these forces are invisible and also affected by other forces.\n\n- **Link Force**: Imagine nodes connected by rubber bands, which pull connected nodes to a suitable distance. The tightness of the rubber band is the force strength, and the ideal length is the distance we set.\n- **Many-Body Force**: Similar to magnets, it allows all nodes to attract or repel each other. When the force strength is negative, nodes repel each other (like like poles of magnets); when positive, they attract (like opposite poles). This force determines the density of the graph.\n- **Center Force**: Like all nodes are tied to the center of the canvas by an invisible string. This force prevents nodes from drifting too far and keeps the graph centered.\n- **Collision Force**: Treats nodes as solid balls. When nodes get too close, they automatically bounce apart. This force mainly prevents node overlap and improves readability.\n- **Radial Force**: Imagine an invisible ring that attracts nodes to the ring. By setting the radius and force strength, nodes can form a beautiful circular layout.\n\n### Iteration System\n\nLayout calculation is an iterative process with two key concepts:\n\n#### Alpha Value (Energy)\n\nLike the \"energy\" of the layout, it determines how vigorously nodes move:\n\n- **Initial state**: Alpha = 1, nodes move vigorously\n- **During calculation**: Alpha gradually decreases, node movement slows\n- **End state**: When Alpha < alphaMin, nodes stop moving\n\n#### Iterations\n\nControls the number of times forces are applied in each calculation:\n\n- **Effect**: The larger the value, the more precise the layout, but the slower the computation\n- **Adjustment**:\n  - Simple graphs: use the default value\n  - Complex graphs: increase the number of iterations as needed\n  - Real-time interaction: use fewer iterations\n\n> Tip: Iterations and alpha value work together. Increasing iterations makes each step more precise, while alpha controls the overall progress.\n\n## Options\n\n| Property        | Description                                        | Type                                       | Default    | Required |\n| --------------- | -------------------------------------------------- | ------------------------------------------ | ---------- | -------- |\n| type            | Layout type                                        | string                                     | 'd3-force' | ✓        |\n| nodeSize        | Node size (diameter), for collision detection      | number \\| ((node, index, nodes) => number) | -          |          |\n| iterations      | Number of force iterations, higher is more precise | number                                     | -          |          |\n| onTick          | Callback for each iteration, for real-time results | (data: LayoutMapping) => void              | -          |          |\n| forceSimulation | Custom force simulation, defaults to d3.js method  | Simulation<NodeDatum, EdgeDatum>           | -          |          |\n| randomSource    | Function to generate random numbers                | () => number                               | -          |          |\n\n### Iteration Control\n\n| Property      | Description                                          | Type   | Default | Required |\n| ------------- | ---------------------------------------------------- | ------ | ------- | -------- |\n| alpha         | Current convergence threshold, controls activity     | number | 1       |          |\n| alphaMin      | Minimum threshold to stop, when alpha < this, stop   | number | 0.001   |          |\n| alphaDecay    | Decay rate of alpha, [0, 1], 0.028 ≈ 300 iterations  | number | 0.028   |          |\n| alphaTarget   | Target alpha, system tries to converge to this value | number | 0       |          |\n| velocityDecay | Velocity decay factor, higher means slower movement  | number | 0.4     |          |\n\n### Force Model Options\n\n#### Link Force (link)\n\n| Property        | Description                                  | Type                                       | Default     | Required |\n| --------------- | -------------------------------------------- | ------------------------------------------ | ----------- | -------- |\n| link.id         | Function to generate edge id                 | (edge, index, edges) => string             | (e) => e.id |          |\n| link.distance   | Ideal edge length                            | number \\| ((edge, index, edges) => number) | 30          |          |\n| link.strength   | Force strength, higher means closer to ideal | number \\| ((edge, index, edges) => number) | 1           |          |\n| link.iterations | Number of link force iterations              | number                                     | 1           |          |\n\n#### Many-Body Force (manyBody)\n\n| Property             | Description                                                     | Type                                       | Default  | Required |\n| -------------------- | --------------------------------------------------------------- | ------------------------------------------ | -------- | -------- |\n| manyBody.strength    | Force strength, negative for repulsion, positive for attraction | number \\| ((node, index, nodes) => number) | -30      |          |\n| manyBody.theta       | Barnes-Hut accuracy, smaller is more accurate                   | number                                     | 0.9      |          |\n| manyBody.distanceMin | Minimum distance, prevents excessive force                      | number                                     | 1        |          |\n| manyBody.distanceMax | Maximum distance, beyond which no force is applied              | number                                     | Infinity |          |\n\n#### Center Force (center)\n\n| Property        | Description                                   | Type   | Default | Required |\n| --------------- | --------------------------------------------- | ------ | ------- | -------- |\n| center.x        | Center x coordinate                           | number | 0       |          |\n| center.y        | Center y coordinate                           | number | 0       |          |\n| center.strength | Force strength, higher means closer to center | number | 1       |          |\n\n#### Collision Force (collide)\n\n| Property           | Description                                     | Type                                       | Default | Required |\n| ------------------ | ----------------------------------------------- | ------------------------------------------ | ------- | -------- |\n| collide.radius     | Collision radius, nodes repel if closer         | number \\| ((node, index, nodes) => number) | 10      |          |\n| collide.strength   | Force strength, higher means stronger repulsion | number                                     | 1       |          |\n| collide.iterations | Number of collision iterations                  | number                                     | 1       |          |\n\n#### Radial Force (radial)\n\n| Property        | Description                                   | Type                                       | Default | Required |\n| --------------- | --------------------------------------------- | ------------------------------------------ | ------- | -------- |\n| radial.strength | Force strength, higher means closer to radius | number \\| ((node, index, nodes) => number) | 0.1     |          |\n| radial.radius   | Target radius, nodes are attracted to circle  | number \\| ((node, index, nodes) => number) | 100     |          |\n| radial.x        | Center x coordinate                           | number                                     | 0       |          |\n| radial.y        | Center y coordinate                           | number                                     | 0       |          |\n\n#### X Axis Force (x)\n\n| Property   | Description                               | Type                                       | Default | Required |\n| ---------- | ----------------------------------------- | ------------------------------------------ | ------- | -------- |\n| x.strength | Force strength in x direction             | number \\| ((node, index, nodes) => number) | -       |          |\n| x.x        | Target x coordinate, nodes attracted here | number \\| ((node, index, nodes) => number) | -       |          |\n\n#### Y Axis Force (y)\n\n| Property   | Description                               | Type                                       | Default | Required |\n| ---------- | ----------------------------------------- | ------------------------------------------ | ------- | -------- |\n| y.strength | Force strength in y direction             | number \\| ((node, index, nodes) => number) | -       |          |\n| y.y        | Target y coordinate, nodes attracted here | number \\| ((node, index, nodes) => number) | -       |          |\n\n## Code Examples\n\n### Prevent Node Overlap\n\n```js\n{\n  layout: {\n    type: 'd3-force',\n    collide: {\n      // Prevent nodes from overlapping by specifying a collision radius for each node.\n      radius: (d) => d.size / 2,\n    },\n  },\n}\n```\n\nSee [Example - Prevent Node Overlap in Force-Directed Layout](/en/examples/layout/force-directed/#prevent-overlap)\n\n### Team Clustering Layout\n\nThis example shows how to use force-directed layout for team clustering, where nodes of different teams automatically cluster together.\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 500,\n  height: 250,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      // Team A\n      { id: 'A1', team: 'A', label: 'A1', size: 30 },\n      { id: 'A2', team: 'A', label: 'A2', size: 20 },\n      { id: 'A3', team: 'A', label: 'A3', size: 20 },\n      { id: 'A4', team: 'A', label: 'A4', size: 20 },\n      // Team B\n      { id: 'B1', team: 'B', label: 'B1', size: 30 },\n      { id: 'B2', team: 'B', label: 'B2', size: 20 },\n      { id: 'B3', team: 'B', label: 'B3', size: 20 },\n      { id: 'B4', team: 'B', label: 'B4', size: 20 },\n      // Team C\n      { id: 'C1', team: 'C', label: 'C1', size: 30 },\n      { id: 'C2', team: 'C', label: 'C2', size: 20 },\n      { id: 'C3', team: 'C', label: 'C3', size: 20 },\n      { id: 'C4', team: 'C', label: 'C4', size: 20 },\n    ],\n    edges: [\n      // Team A internal connections\n      { source: 'A1', target: 'A2' },\n      { source: 'A1', target: 'A3' },\n      { source: 'A1', target: 'A4' },\n      // Team B internal connections\n      { source: 'B1', target: 'B2' },\n      { source: 'B1', target: 'B3' },\n      { source: 'B1', target: 'B4' },\n      // Team C internal connections\n      { source: 'C1', target: 'C2' },\n      { source: 'C1', target: 'C3' },\n      { source: 'C1', target: 'C4' },\n      // Few connections between teams\n      { source: 'A1', target: 'B1' },\n      { source: 'B1', target: 'C1' },\n    ],\n  },\n  node: {\n    style: {\n      size: (d) => d.size,\n      fill: (d) => {\n        // Different colors for different teams\n        const colors = {\n          A: '#FF6B6B',\n          B: '#4ECDC4',\n          C: '#45B7D1',\n        };\n        return colors[d.team];\n      },\n      labelText: (d) => d.label,\n      labelPlacement: 'center',\n      labelFill: '#fff',\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#aaa',\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    // Configure link force - nodes within the same team are closer\n    link: {\n      distance: (d) => {\n        // Shorter distance within the same team\n        if (d.source.team === d.target.team) return 50;\n        // Longer distance between teams\n        return 200;\n      },\n      strength: (d) => {\n        // Stronger connection within the same team\n        if (d.source.team === d.target.team) return 0.7;\n        // Weaker connection between teams\n        return 0.1;\n      },\n    },\n    // Configure many-body force - control repulsion between nodes\n    manyBody: {\n      strength: (d) => {\n        // Team leader nodes (ending with 1) have stronger repulsion\n        if (d.label.endsWith('1')) return -100;\n        return -30;\n      },\n    },\n    // Configure collision force - prevent node overlap\n    collide: {\n      radius: 35,\n      strength: 0.8,\n    },\n    // Configure center force - keep the graph centered\n    center: {\n      strength: 0.05,\n    },\n  },\n  behaviors: ['drag-element-force'],\n});\n\ngraph.render();\n```\n\n<details><summary>Show full code</summary>\n\n```javascript\nimport { Graph } from '@antv/g6';\n\n// Create mock data with nodes from different teams\nconst data = {\n  nodes: [\n    // Team A\n    { id: 'A1', team: 'A', label: 'A1', size: 30 },\n    { id: 'A2', team: 'A', label: 'A2', size: 20 },\n    { id: 'A3', team: 'A', label: 'A3', size: 20 },\n    { id: 'A4', team: 'A', label: 'A4', size: 20 },\n    // Team B\n    { id: 'B1', team: 'B', label: 'B1', size: 30 },\n    { id: 'B2', team: 'B', label: 'B2', size: 20 },\n    { id: 'B3', team: 'B', label: 'B3', size: 20 },\n    { id: 'B4', team: 'B', label: 'B4', size: 20 },\n    // Team C\n    { id: 'C1', team: 'C', label: 'C1', size: 30 },\n    { id: 'C2', team: 'C', label: 'C2', size: 20 },\n    { id: 'C3', team: 'C', label: 'C3', size: 20 },\n    { id: 'C4', team: 'C', label: 'C4', size: 20 },\n  ],\n  edges: [\n    // Team A internal connections\n    { source: 'A1', target: 'A2' },\n    { source: 'A1', target: 'A3' },\n    { source: 'A1', target: 'A4' },\n    // Team B internal connections\n    { source: 'B1', target: 'B2' },\n    { source: 'B1', target: 'B3' },\n    { source: 'B1', target: 'B4' },\n    // Team C internal connections\n    { source: 'C1', target: 'C2' },\n    { source: 'C1', target: 'C3' },\n    { source: 'C1', target: 'C4' },\n    // Few connections between teams\n    { source: 'A1', target: 'B1' },\n    { source: 'B1', target: 'C1' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      size: (d) => d.size,\n      fill: (d) => {\n        // Different colors for different teams\n        const colors = {\n          A: '#FF6B6B',\n          B: '#4ECDC4',\n          C: '#45B7D1',\n        };\n        return colors[d.team];\n      },\n      labelText: (d) => d.label,\n      labelPlacement: 'center',\n      labelFill: '#fff',\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#aaa',\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    // Configure link force - nodes within the same team are closer\n    link: {\n      distance: (d) => {\n        // Shorter distance within the same team\n        if (d.source.team === d.target.team) return 50;\n        // Longer distance between teams\n        return 200;\n      },\n      strength: (d) => {\n        // Stronger connection within the same team\n        if (d.source.team === d.target.team) return 0.7;\n        // Weaker connection between teams\n        return 0.1;\n      },\n    },\n    // Configure many-body force - control repulsion between nodes\n    manyBody: {\n      strength: (d) => {\n        // Team leader nodes (ending with 1) have stronger repulsion\n        if (d.label.endsWith('1')) return -100;\n        return -30;\n      },\n    },\n    // Configure collision force - prevent node overlap\n    collide: {\n      radius: 35,\n      strength: 0.8,\n    },\n    // Configure center force - keep the graph centered\n    center: {\n      strength: 0.05,\n    },\n  },\n  behaviors: ['drag-element-force'],\n});\n\ngraph.render();\n```\n\n</details>\n\nMain configuration notes:\n\n- `link.distance`: Shorter within teams, longer between teams\n- `link.strength`: Stronger within teams, weaker between teams\n- `manyBody.strength`: Controls repulsion between nodes\n- `collide`: Prevents node overlap\n- `center`: Keeps the layout centered\n\nSee also [Customize parameters for different nodes](/en/examples/layout/force-directed/#functional-params).\n"
  },
  {
    "path": "packages/site/docs/manual/layout/D3ForceLayout.zh.md",
    "content": "---\ntitle: D3力导向布局 D3Force\norder: 8\n---\n\n## 概述\n\nD3Force 布局是基于 [d3-force](https://d3js.org/d3-force) 实现的力导向布局。它通过模拟物理力的作用（如引力、斥力、碰撞等），使图布局达到一个能量最小的稳定状态。\n\n这种布局的主要特点是：\n\n1. **自动排列**：不需要手动设置节点位置，系统会自动找到合适的位置\n2. **实时调整**：当你拖动某个节点时，其他节点会实时跟随调整位置\n3. **灵活配置**：\n   - 可以调整节点间的吸引力和排斥力\n   - 可以设置边的理想长度\n   - 可以固定某些重要节点的位置\n4. **动画效果**：节点移动时会有平滑的动画，让变化更自然\n\n<img alt=\"D3Force 布局示意图\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-_sFS5IRGGcAAAAAAAAAAAAADmJ7AQ/original\" />\n\n## 核心概念\n\n### 力系统 Force System\n\nD3Force 布局通过模拟五种不同的力来实现自动布局。想象一个物理世界，这些力同时作用，最终达到平衡：\n\n<img width=\"350\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*p5L2S6gtZ2AAAAAAAAAAAAAAemJ7AQ/original\" alt=\"force\" />\n\n> 注：图中不同颜色的箭头代表不同类型的力，实际布局中这些力是无形的，同时也会受其他力影响。\n\n- **链接力**（Link Force）：想象节点之间连着橡皮筋，可以把相连的节点拉到合适的距离。橡皮筋的松紧度就是力的强度（strength），理想长度就是我们设置的距离（distance）。\n- **多体力**（Many-Body Force）：类似磁铁的效果，可以让所有节点互相吸引或排斥。力的强度为负值时节点会互相排斥（像相同磁极），为正值时会互相吸引（像相反磁极）。这个力决定了图的疏密程度。\n- **中心力**（Center Force）：就像所有节点都被一根看不见的绳子拴在画布中心。这个力可以防止节点飘得太远，让整个图保持在画布的中心位置。\n- **碰撞力**（Collision Force）：让节点变成有实体大小的小球，当节点太近时会自动弹开。这个力主要用来防止节点重叠，提高图的可读性。\n- **径向力**（Radial Force）：想象有一个看不见的圆环，这个力会把节点吸引到圆环上。通过设置圆的半径和力的强度，可以让节点形成漂亮的环形布局。\n\n### 迭代系统（Iteration System）\n\n布局计算是一个反复调整的过程，包含两个关键概念：\n\n#### Alpha 值（活力值）\n\n就像布局的\"能量\"，决定节点移动的剧烈程度：\n\n- **初始状态**：Alpha = 1，节点移动剧烈\n- **计算过程**：Alpha 值逐渐降低，节点移动变缓\n- **结束状态**：当 Alpha < alphaMin 时，节点停止移动\n\n#### 迭代次数（Iterations）\n\n控制每次计算时力的作用次数：\n\n- **作用**：值越大，布局越精确，但计算越慢\n- **调节**：\n  - 简单图：使用默认值即可\n  - 复杂图：可以适当增加迭代次数\n  - 实时交互：建议使用较小的迭代次数\n\n> 提示：迭代次数（iterations）和活力值（alpha）是相互配合的。增加迭代次数可以让每一步计算更精确，而活力值则控制整体计算的进度。\n\n## 配置项\n\n| 属性            | 描述                                          | 类型                                       | 默认值     | 必选 |\n| --------------- | --------------------------------------------- | ------------------------------------------ | ---------- | ---- |\n| type            | 布局类型                                      | string                                     | 'd3-force' | ✓    |\n| nodeSize        | 节点大小（直径），用于碰撞检测防止节点重叠    | number \\| ((node, index, nodes) => number) | -          |      |\n| iterations      | 力的迭代次数，值越大布局越精确但性能消耗越大  | number                                     | -          |      |\n| onTick          | 每次迭代的回调函数，用于实时获取布局结果      | (data: LayoutMapping) => void              | -          |      |\n| forceSimulation | 自定义力模拟方法，若不指定则使用 d3.js 的方法 | Simulation<NodeDatum, EdgeDatum>           | -          |      |\n| randomSource    | 用于生成随机数的函数                          | () => number                               | -          |      |\n\n### 迭代控制\n\n| 属性          | 描述                                                   | 类型   | 默认值 | 必选 |\n| ------------- | ------------------------------------------------------ | ------ | ------ | ---- |\n| alpha         | 当前迭代的收敛阈值，控制布局的活跃程度                 | number | 1      |      |\n| alphaMin      | 停止迭代的最小阈值，当 alpha 小于该值时停止迭代        | number | 0.001  |      |\n| alphaDecay    | 收敛阈值的衰减率，范围 [0, 1]，0.028 对应约 300 次迭代 | number | 0.028  |      |\n| alphaTarget   | 目标收敛阈值，系统会尝试将 alpha 收敛到该值            | number | 0      |      |\n| velocityDecay | 速度衰减因子，值越大节点运动越缓慢                     | number | 0.4    |      |\n\n### 力模型配置\n\n#### 链接力（link）\n\n| 属性            | 描述                               | 类型                                       | 默认值      | 必选 |\n| --------------- | ---------------------------------- | ------------------------------------------ | ----------- | ---- |\n| link.id         | 边的 id 生成函数                   | (edge, index, edges) => string             | (e) => e.id |      |\n| link.distance   | 理想边长，边会趋向于该长度         | number \\| ((edge, index, edges) => number) | 30          |      |\n| link.strength   | 力的强度，值越大边长越接近理想边长 | number \\| ((edge, index, edges) => number) | 1           |      |\n| link.iterations | 链接力的迭代次数                   | number                                     | 1           |      |\n\n#### 多体力（manyBody）\n\n| 属性                 | 描述                                                  | 类型                                       | 默认值   | 必选 |\n| -------------------- | ----------------------------------------------------- | ------------------------------------------ | -------- | ---- |\n| manyBody.strength    | 力的强度，负值为斥力，正值为引力                      | number \\| ((node, index, nodes) => number) | -30      |      |\n| manyBody.theta       | Barnes-Hut 算法的精度参数，值越小越精确但性能消耗越大 | number                                     | 0.9      |      |\n| manyBody.distanceMin | 最小作用距离，防止力过大                              | number                                     | 1        |      |\n| manyBody.distanceMax | 最大作用距离，超过该距离的节点不产生力                | number                                     | Infinity |      |\n\n#### 中心力（center）\n\n| 属性            | 描述                               | 类型   | 默认值 | 必选 |\n| --------------- | ---------------------------------- | ------ | ------ | ---- |\n| center.x        | 中心点 x 坐标                      | number | 0      |      |\n| center.y        | 中心点 y 坐标                      | number | 0      |      |\n| center.strength | 力的强度，值越大节点越趋向于中心点 | number | 1      |      |\n\n#### 碰撞力（collide）\n\n| 属性               | 描述                                   | 类型                                       | 默认值 | 必选 |\n| ------------------ | -------------------------------------- | ------------------------------------------ | ------ | ---- |\n| collide.radius     | 碰撞半径，小于该距离的节点会产生排斥力 | number \\| ((node, index, nodes) => number) | 10     |      |\n| collide.strength   | 力的强度，值越大排斥效果越明显         | number                                     | 1      |      |\n| collide.iterations | 碰撞检测的迭代次数                     | number                                     | 1      |      |\n\n#### 径向力（radial）\n\n| 属性            | 描述                                   | 类型                                       | 默认值 | 必选 |\n| --------------- | -------------------------------------- | ------------------------------------------ | ------ | ---- |\n| radial.strength | 力的强度，值越大节点越趋向于目标半径   | number \\| ((node, index, nodes) => number) | 0.1    |      |\n| radial.radius   | 目标半径，节点会被吸引到该半径的圆周上 | number \\| ((node, index, nodes) => number) | 100    |      |\n| radial.x        | 圆心 x 坐标                            | number                                     | 0      |      |\n| radial.y        | 圆心 y 坐标                            | number                                     | 0      |      |\n\n#### X 轴力（x）\n\n| 属性       | 描述                                | 类型                                       | 默认值 | 必选 |\n| ---------- | ----------------------------------- | ------------------------------------------ | ------ | ---- |\n| x.strength | X 轴方向的力强度                    | number \\| ((node, index, nodes) => number) | -      |      |\n| x.x        | 目标 x 坐标，节点会被吸引到这个位置 | number \\| ((node, index, nodes) => number) | -      |      |\n\n#### Y 轴力（y）\n\n| 属性       | 描述                                | 类型                                       | 默认值 | 必选 |\n| ---------- | ----------------------------------- | ------------------------------------------ | ------ | ---- |\n| y.strength | Y 轴方向的力强度                    | number \\| ((node, index, nodes) => number) | -      |      |\n| y.y        | 目标 y 坐标，节点会被吸引到这个位置 | number \\| ((node, index, nodes) => number) | -      |      |\n\n## 代码示例\n\n### 防止节点重叠\n\n```js\n{\n  layout: {\n    type: 'd3-force',\n    collide: {\n      // Prevent nodes from overlapping by specifying a collision radius for each node.\n      radius: (d) => d.size / 2,\n    },\n  },\n}\n```\n\n效果见 [示例 - 力导向布局防止节点重叠](/examples/layout/force-directed/#prevent-overlap)\n\n### 团队聚类布局\n\n该示例展示了如何使用力导向布局实现团队聚类效果，不同团队的节点会自动聚集在一起。\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 500,\n  height: 250,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      // 团队 A\n      { id: 'A1', team: 'A', label: 'A1', size: 30 },\n      { id: 'A2', team: 'A', label: 'A2', size: 20 },\n      { id: 'A3', team: 'A', label: 'A3', size: 20 },\n      { id: 'A4', team: 'A', label: 'A4', size: 20 },\n      // 团队 B\n      { id: 'B1', team: 'B', label: 'B1', size: 30 },\n      { id: 'B2', team: 'B', label: 'B2', size: 20 },\n      { id: 'B3', team: 'B', label: 'B3', size: 20 },\n      { id: 'B4', team: 'B', label: 'B4', size: 20 },\n      // 团队 C\n      { id: 'C1', team: 'C', label: 'C1', size: 30 },\n      { id: 'C2', team: 'C', label: 'C2', size: 20 },\n      { id: 'C3', team: 'C', label: 'C3', size: 20 },\n      { id: 'C4', team: 'C', label: 'C4', size: 20 },\n    ],\n    edges: [\n      // 团队 A 内部连接\n      { source: 'A1', target: 'A2' },\n      { source: 'A1', target: 'A3' },\n      { source: 'A1', target: 'A4' },\n      // 团队 B 内部连接\n      { source: 'B1', target: 'B2' },\n      { source: 'B1', target: 'B3' },\n      { source: 'B1', target: 'B4' },\n      // 团队 C 内部连接\n      { source: 'C1', target: 'C2' },\n      { source: 'C1', target: 'C3' },\n      { source: 'C1', target: 'C4' },\n      // 团队间的少量连接\n      { source: 'A1', target: 'B1' },\n      { source: 'B1', target: 'C1' },\n    ],\n  },\n  node: {\n    style: {\n      size: (d) => d.size,\n      fill: (d) => {\n        // 不同团队使用不同颜色\n        const colors = {\n          A: '#FF6B6B',\n          B: '#4ECDC4',\n          C: '#45B7D1',\n        };\n        return colors[d.team];\n      },\n      labelText: (d) => d.label,\n      labelPlacement: 'center',\n      labelFill: '#fff',\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#aaa',\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    // 配置链接力 - 团队内部节点更靠近\n    link: {\n      distance: (d) => {\n        // 同一团队内的连接距离更短\n        if (d.source.team === d.target.team) return 50;\n        // 不同团队间的连接距离更长\n        return 200;\n      },\n      strength: (d) => {\n        // 同一团队内的连接强度更大\n        if (d.source.team === d.target.team) return 0.7;\n        // 不同团队间的连接强度更小\n        return 0.1;\n      },\n    },\n    // 配置多体力 - 控制节点间的排斥力\n    manyBody: {\n      strength: (d) => {\n        // 团队领导节点（编号1）的排斥力更强\n        if (d.label.endsWith('1')) return -100;\n        return -30;\n      },\n    },\n    // 配置碰撞力 - 防止节点重叠\n    collide: {\n      radius: 35,\n      strength: 0.8,\n    },\n    // 配置中心力 - 保持图形在画布中心\n    center: {\n      strength: 0.05,\n    },\n  },\n  behaviors: ['drag-element-force'],\n});\n\ngraph.render();\n```\n\n<details><summary>展开查看完整代码</summary>\n\n```javascript\nimport { Graph } from '@antv/g6';\n\n// 创建模拟数据，包含不同团队的节点\nconst data = {\n  nodes: [\n    // 团队 A\n    { id: 'A1', team: 'A', label: 'A1', size: 30 },\n    { id: 'A2', team: 'A', label: 'A2', size: 20 },\n    { id: 'A3', team: 'A', label: 'A3', size: 20 },\n    { id: 'A4', team: 'A', label: 'A4', size: 20 },\n    // 团队 B\n    { id: 'B1', team: 'B', label: 'B1', size: 30 },\n    { id: 'B2', team: 'B', label: 'B2', size: 20 },\n    { id: 'B3', team: 'B', label: 'B3', size: 20 },\n    { id: 'B4', team: 'B', label: 'B4', size: 20 },\n    // 团队 C\n    { id: 'C1', team: 'C', label: 'C1', size: 30 },\n    { id: 'C2', team: 'C', label: 'C2', size: 20 },\n    { id: 'C3', team: 'C', label: 'C3', size: 20 },\n    { id: 'C4', team: 'C', label: 'C4', size: 20 },\n  ],\n  edges: [\n    // 团队 A 内部连接\n    { source: 'A1', target: 'A2' },\n    { source: 'A1', target: 'A3' },\n    { source: 'A1', target: 'A4' },\n    // 团队 B 内部连接\n    { source: 'B1', target: 'B2' },\n    { source: 'B1', target: 'B3' },\n    { source: 'B1', target: 'B4' },\n    // 团队 C 内部连接\n    { source: 'C1', target: 'C2' },\n    { source: 'C1', target: 'C3' },\n    { source: 'C1', target: 'C4' },\n    // 团队间的少量连接\n    { source: 'A1', target: 'B1' },\n    { source: 'B1', target: 'C1' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      size: (d) => d.size,\n      fill: (d) => {\n        // 不同团队使用不同颜色\n        const colors = {\n          A: '#FF6B6B',\n          B: '#4ECDC4',\n          C: '#45B7D1',\n        };\n        return colors[d.team];\n      },\n      labelText: (d) => d.label,\n      labelPlacement: 'center',\n      labelFill: '#fff',\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#aaa',\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    // 配置链接力 - 团队内部节点更靠近\n    link: {\n      distance: (d) => {\n        // 同一团队内的连接距离更短\n        if (d.source.team === d.target.team) return 50;\n        // 不同团队间的连接距离更长\n        return 200;\n      },\n      strength: (d) => {\n        // 同一团队内的连接强度更大\n        if (d.source.team === d.target.team) return 0.7;\n        // 不同团队间的连接强度更小\n        return 0.1;\n      },\n    },\n    // 配置多体力 - 控制节点间的排斥力\n    manyBody: {\n      strength: (d) => {\n        // 团队领导节点（编号1）的排斥力更强\n        if (d.label.endsWith('1')) return -100;\n        return -30;\n      },\n    },\n    // 配置碰撞力 - 防止节点重叠\n    collide: {\n      radius: 35,\n      strength: 0.8,\n    },\n    // 配置中心力 - 保持图形在画布中心\n    center: {\n      strength: 0.05,\n    },\n  },\n  behaviors: ['drag-element-force'],\n});\n\ngraph.render();\n```\n\n</details>\n\n主要配置说明：\n\n- `link.distance`：团队内部距离短，团队间距离长\n- `link.strength`：团队内部连接强度大，团队间连接强度小\n- `manyBody.strength`：控制节点间排斥力\n- `collide`：防止节点重叠\n- `center`：保持整体布局在画布中心\n\n还可以参考 [定制不同节点的参数](/examples/layout/force-directed/#functional-params) 示例。\n"
  },
  {
    "path": "packages/site/docs/manual/layout/DagreLayout.en.md",
    "content": "---\ntitle: Dagre Layout\norder: 9\n---\n\n# Dagre Layout\n\n## Overview\n\nDagre is a hierarchical layout suitable for directed acyclic graphs (DAGs). It can automatically handle the direction and spacing between nodes and supports both horizontal and vertical layouts. See more Dagre layout [examples](/en/examples#layout-dagre), [source code](https://github.com/dagrejs/dagre/blob/master/lib/layout.js), and [official documentation](https://github.com/dagrejs/dagre/wiki).\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*2uMmRo5wYPUAAAAAAAAAAABkARQnAQ' width=350 alt='Dagre Layout'/>\n\n## Configuration\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'dagre',\n    rankdir: 'TB',\n    align: 'UL',\n    nodesep: 50,\n    ranksep: 50,\n    controlPoints: false,\n  },\n});\n```\n\n## Options\n\n> For more options, refer to the [official documentation](https://github.com/dagrejs/dagre/wiki#configuring-the-layout)\n\n<img src=\"https://img.alicdn.com/imgextra/i3/O1CN01OpQHBZ1HcpZuWZLS7_!!6000000000779-0-tps-1274-1234.jpg\" width=\"400\" alt=\"Dagre Layout Options Illustration\" />\n\n| Property      | Description                                                                                                                                | Type                                                | Default           | Required |\n| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------- | ----------------- | -------- |\n| type          | Layout type                                                                                                                                | `dagre`                                             | -                 | ✓        |\n| rankdir       | Layout direction, options                                                                                                                  | `TB` \\| `BT` \\| `LR` \\| `RL`                        | `TB`              |          |\n| align         | Node alignment, options                                                                                                                    | `UL` \\| `UR` \\| `DL` \\| `DR`                        | `UL`              |          |\n| nodesep       | Node spacing (px). For `TB` or `BT`, it's the horizontal spacing; for `LR` or `RL`, it's the vertical spacing                              | number                                              | 50                |          |\n| ranksep       | Rank spacing (px). For `TB` or `BT`, it's the vertical spacing between ranks; for `LR` or `RL`, it's the horizontal spacing between ranks  | number                                              | 100               |          |\n| ranker        | Algorithm for assigning ranks to nodes: `longest-path`, `tight-tree`, or `network-simplex`                                                 | `network-simplex` \\| `tight-tree` \\| `longest-path` | `network-simplex` |          |\n| nodeSize      | G6 custom property, specify node size for all or each node. If a single number, width and height are the same; if array: `[width, height]` | number \\| number[] \\| () => (number \\| number[])    |                   |          |\n| controlPoints | Whether to retain edge control points                                                                                                      | boolean                                             | false             |          |\n\n### rankdir\n\n> `TB` | `BT` | `LR` | `RL`, **Default**: `TB`\n\nLayout direction\n\n- `TB`: Top to Bottom;\n\n<img src='https://img.alicdn.com/imgextra/i3/O1CN01ulI3Se1DeQUfhQ29v_!!6000000000241-0-tps-1092-1218.jpg' width=170 alt='Top to Bottom Layout'/>\n\n- `BT`: Bottom to Top;\n\n<img src='https://img.alicdn.com/imgextra/i1/O1CN01IfytBS1EOE6NXVprx_!!6000000000341-0-tps-1004-1236.jpg' width=170 alt='Bottom to Top Layout'/>\n\n- `LR`: Left to Right;\n\n<img src='https://img.alicdn.com/imgextra/i2/O1CN01tpEdMJ1MsTBKpoP6r_!!6000000001490-0-tps-1452-786.jpg' width=170 alt='Left to Right Layout'/>\n\n- `RL`: Right to Left.\n\n<img src='https://img.alicdn.com/imgextra/i4/O1CN01Lw8JHC27j71xd0wl9_!!6000000007832-0-tps-1460-848.jpg' width=170 alt='Right to Left Layout'/>\n\n### align\n\n> `UL` | `UR` | `DL` | `DR`, **Default**: `UL`\n\nNode alignment\n\n- `UL`: Upper Left\n- `UR`: Upper Right\n- `DL`: Down Left\n- `DR`: Down Right\n\n### nodesep\n\n> number, **Default**: 50\n\nNode spacing (px). For `TB` or `BT`, it's the horizontal spacing; for `LR` or `RL`, it's the vertical spacing\n\n### ranksep\n\n> number, **Default**: 50\n\nRank spacing (px). For `TB` or `BT`, it's the vertical spacing between ranks; for `LR` or `RL`, it's the horizontal spacing between ranks\n\n### ranker\n\n> `network-simplex` | `tight-tree` | `longest-path`, **Default**: `network-simplex`\n\nAlgorithm for assigning ranks to nodes, supports three algorithms:\n\n- `longest-path`: Uses DFS to recursively find the longest path for each node. Simple and fast, but may result in many long edges.\n- `tight-tree`: An optimization algorithm to reduce the number of long edges. It first uses `longest-path` to compute initial ranks, then adjusts slack edges to build a feasible tree.\n- `network-simplex`: Based on [A Technique for Drawing Directed Graphs](https://www.graphviz.org/documentation/TSE93.pdf), iteratively modifies node ranks to minimize slack edges.\n\n### nodeSize\n\n> number \\| number[] \\| () => (number \\| number[])\n\nG6 custom property, specify node size for all or each node. If a single number, width and height are the same; if array: `[width, height]`\n\n```js\n(d) => {\n  // d is a node\n  if (d.id === 'testId') return 20;\n  return [10, 20];\n};\n```\n\n### controlPoints\n\n> boolean, **Default**: false\n\nWhether to retain edge control points.\n\n## Applicable Scenarios\n\n- **Flowcharts**: Suitable for displaying flowcharts, automatically handling direction and spacing between nodes.\n- **Dependency Graphs**: Display dependencies between packages or modules.\n- **Task Scheduling Graphs**: Show dependencies and execution order between tasks.\n\n## Related Documentation\n\n> The following documents can help you better understand Dagre layout\n\n- [Graph Layout Algorithms｜Detailed Dagre Layout](https://mp.weixin.qq.com/s/EdyTfFUH7fyMefNSBXI2nA)\n- [In-depth Interpretation of Dagre Layout Algorithm](https://www.yuque.com/antv/g6-blog/xxp5nl)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/DagreLayout.zh.md",
    "content": "---\ntitle: Dagre 布局\norder: 9\n---\n\n## 概述\n\nDagre 是一种层次化布局，适用于有向无环图（DAG）的布局场景，能够自动处理节点之间的方向和间距，支持水平和垂直布局。参考更多 Dagre 布局[样例](/examples#layout-dagre)或[源码](https://github.com/dagrejs/dagre/blob/master/lib/layout.js)以及[官方文档](https://github.com/dagrejs/dagre/wiki)。\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*2uMmRo5wYPUAAAAAAAAAAABkARQnAQ' width=350 alt='Dagre布局'/>\n\n## 配置方式\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'dagre',\n    rankdir: 'TB',\n    align: 'UL',\n    nodesep: 50,\n    ranksep: 50,\n    controlPoints: false,\n  },\n});\n```\n\n## 配置项\n\n> 更多配置项可参考[官方文档](https://github.com/dagrejs/dagre/wiki#configuring-the-layout)\n\n<img src=\"https://img.alicdn.com/imgextra/i3/O1CN01OpQHBZ1HcpZuWZLS7_!!6000000000779-0-tps-1274-1234.jpg\" width=\"400\" alt=\"Dagre 布局配置项图解\" />\n\n| 属性          | 描述                                                                                                                                          | 类型                                                | 默认值            | 必选 |\n| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | ----------------- | ---- |\n| type          | 布局类型                                                                                                                                      | `dagre`                                             | -                 | ✓    |\n| rankdir       | 布局方向，可选值                                                                                                                              | `TB` \\| `BT` \\| `LR` \\| `RL`                        | `TB`              |      |\n| align         | 节点对齐方式，可选值                                                                                                                          | `UL` \\| `UR` \\| `DL` \\| `DR`                        | `UL`              |      |\n| nodesep       | 节点间距（px）。在rankdir 为 `TB` 或 `BT` 时是节点的水平间距；在rankdir 为 `LR` 或 `RL` 时代表节点的竖直方向间距                              | number                                              | 50                |      |\n| ranksep       | 层间距（px）。在rankdir 为 `TB` 或 `BT` 时是竖直方向相邻层间距；在rankdir 为 `LR` 或 `RL` 时代表水平方向相邻层间距                            | number                                              | 100               |      |\n| ranker        | 为每个节点分配等级的算法，共支持三种算法，分别是：`longest-path` 最长路径算法、`tight-tree` 紧凑树算法、`network-simplex` 网络单形法          | `network-simplex` \\| `tight-tree` \\| `longest-path` | `network-simplex` |      |\n| nodeSize      | G6自定义属性，统一指定或为每个节点指定节点大小。如果仅返回单个number，则表示节点的宽度和高度相同；如果返回一个数组，则形如：`[width, height]` | number \\| number[] \\| () => (number \\| number[])    |                   |      |\n| controlPoints | 是否保留边的控制点                                                                                                                            | boolean                                             | false             |      |\n\n### rankdir\n\n> `TB` | `BT` | `LR` | `RL`， **Default**: `TB`\n\n布局方向\n\n- `TB`：从上到下；\n\n<img src='https://img.alicdn.com/imgextra/i3/O1CN01ulI3Se1DeQUfhQ29v_!!6000000000241-0-tps-1092-1218.jpg' width=170 alt='：从上到下布局'/>\n\n- `BT`：从下到上；\n\n<img src='https://img.alicdn.com/imgextra/i1/O1CN01IfytBS1EOE6NXVprx_!!6000000000341-0-tps-1004-1236.jpg' width=170 alt='从下到上布局'/>\n\n- `LR`：从左到右；\n\n<img src='https://img.alicdn.com/imgextra/i2/O1CN01tpEdMJ1MsTBKpoP6r_!!6000000001490-0-tps-1452-786.jpg' width=170 alt='从左到右布局'/>\n\n- `RL`：从右到左。\n\n<img src='https://img.alicdn.com/imgextra/i4/O1CN01Lw8JHC27j71xd0wl9_!!6000000007832-0-tps-1460-848.jpg' width=170 alt='水平布局'/>\n\n### align\n\n> `UL` | `UR` | `DL` | `DR`， **Default**: `UL`\n\n节点对齐方式\n\n- `UL`：左上对齐\n- `UR`：右上对齐\n- `DL`：左下对齐\n- `DR`：右下对齐\n\n### nodesep\n\n> number， **Default**: 50\n\n节点间距（px）。在rankdir 为 `TB` 或 `BT` 时是节点的水平间距；在rankdir 为 `LR` 或 `RL` 时代表节点的竖直方向间距\n\n### ranksep\n\n> number， **Default**: 50\n\n层间距（px）。在rankdir 为 `TB` 或 `BT` 时是竖直方向相邻层间距；在rankdir 为 `LR` 或 `RL` 时代表水平方向相邻层间距\n\n### ranker\n\n> `network-simplex` | `tight-tree` | `longest-path`， **Default**: `network-simplex`\n\n为每个节点分配层级的算法，共支持三种算法，分别是：\n\n- `longest-path`： 最长路径算法，使用DFS深度优先搜索，递归查找每个节点的最长路径。优点是计算简单速度快，但会导致长边过多；\n- `tight-tree`： 紧凑树算法，一种优化算法，目的是减少长边的数量。先用最长路径算法`longest-path`计算出初始层级，然后调整松弛边的长度，从而构建可行树。\n- `network-simplex`： 网络单形法，参考算法[A Technique for Drawing Directed Graphs](https://www.graphviz.org/documentation/TSE93.pdf)，核心思想是迭代修改节点的层级，缩小松弛边。\n\n### nodeSize\n\n> number \\| number[] \\| () => (number \\| number[])\n\nG6自定义属性，统一指定或为每个节点指定节点大小。如果仅返回单个number，则表示节点的宽度和高度相同；如果返回一个数组，则形如：`[width, height]`\n\n```js\n(d) => {\n  // d 是一个节点\n  if (d.id === 'testId') return 20;\n  return [10, 20];\n};\n```\n\n### controlPoints\n\n> boolean， **Default**: false\n\n是否保留边的控制点。\n\n## 布局适用场景\n\n- **流程图**：适合展示流程图，节点之间的方向和间距会自动处理；\n- **依赖关系图**：展示软件包或模块之间的依赖关系；\n- **任务调度图**：展示任务之间的依赖关系和执行顺序。\n\n## 相关文档\n\n> 以下文档可以帮助你更好地理解Dagre 布局\n\n- [图布局算法｜详解 Dagre 布局](https://mp.weixin.qq.com/s/EdyTfFUH7fyMefNSBXI2nA)\n- [深入解读Dagre布局算法](https://www.yuque.com/antv/g6-blog/xxp5nl)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/DendrogramLayout.en.md",
    "content": "---\ntitle: Dendrogram Layout\norder: 10\n---\n\n## Overview\n\nThe dendrogram layout is suitable for visualizing hierarchical clustering data. Its feature is that all child nodes are laid out on the same level, node size is not considered, and each node is treated as 1px.\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*zX7tSLqBvwcAAAAAAAAAAABkARQnAQ' width=400 alt='Dendrogram Layout Example'/>\n\n## Configuration\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'dendrogram',\n    direction: 'LR',\n    nodeSep: 30,\n    rankSep: 250,\n    radial: false,\n  },\n});\n```\n\n## Options\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*tTShQLD_dGoAAAAAAAAAAAAAemJ7AQ/original\" width=\"400\" alt=\"Dendrogram Layout Options Illustration\" />\n\n| Property  | Description                                            | Type                                       | Default | Required |\n| --------- | ------------------------------------------------------ | ------------------------------------------ | ------- | -------- |\n| type      | Layout type                                            | `dendrogram`                               | -       | ✓        |\n| direction | Layout direction, [options](#direction)                | `LR` \\| `RL` \\| `TB` \\| `BT` \\| `H` \\| `V` | `LR`    |          |\n| nodeSep   | Node spacing, distance between nodes on the same level | number                                     | 20      |          |\n| rankSep   | Rank spacing, distance between different levels        | number                                     | 200     |          |\n| radial    | Whether to enable radial layout, [see below](#radial)  | boolean                                    | false   |          |\n\n### direction\n\nTree layout direction options:\n\n- `TB`: Root at the top, layout downward\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*krAnRrLTEnEAAAAAAAAAAABkARQnAQ' width=115 alt='TB direction'/>\n\n- `BT`: Root at the bottom, layout upward\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*0HRyS64i7QoAAAAAAAAAAABkARQnAQ' width=115 alt='BT direction'/>\n\n- `LR`: Root at the left, layout to the right\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*T5KZTJdA2OUAAAAAAAAAAABkARQnAQ' width=55 alt='LR direction'/>\n\n- `RL`: Root at the right, layout to the left\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*q7QJQ5RbQ5kAAAAAAAAAAABkARQnAQ' width=55 alt='RL direction'/>\n\n- `H`: Root in the middle, horizontal symmetric layout\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*tzIfRJ5CuR8AAAAAAAAAAABkARQnAQ' width=85 alt='H direction'/>\n\n- `V`: Root in the middle, vertical symmetric layout\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*B9sjToOzCiAAAAAAAAAAAABkARQnAQ' width=115 alt='V direction'/>\n\n### radial\n\nWhether to enable radial layout mode. When enabled, nodes are distributed radially around the root node.\n\nIf `radial` is set to `true`, it is recommended to set `direction` to `'LR'` or `'RL'` for best results.\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*AhopQI5j-bcAAAAAAAAAAABkARQnAQ' width=175 alt='Radial Layout'/>\n"
  },
  {
    "path": "packages/site/docs/manual/layout/DendrogramLayout.zh.md",
    "content": "---\ntitle: 生态树 Dendrogram\norder: 10\n---\n\n## 概述\n\n生态树布局适用于层次聚类数据的可视化，其特点是所有子节点布局在同一层级，不考虑节点大小，每个节点被当成 1px 处理。\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*zX7tSLqBvwcAAAAAAAAAAABkARQnAQ' width=400 alt='生态树布局示例'/>\n\n## 配置方式\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'dendrogram',\n    direction: 'LR',\n    nodeSep: 30,\n    rankSep: 250,\n    radial: false,\n  },\n});\n```\n\n## 配置项\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*tTShQLD_dGoAAAAAAAAAAAAAemJ7AQ/original\" width=\"400\" alt=\"生态树配置项图解\" />\n\n| 属性      | 描述                                           | 类型                                       | 默认值 | 必选 |\n| --------- | ---------------------------------------------- | ------------------------------------------ | ------ | ---- |\n| type      | 布局类型                                       | `dendrogram`                               | -      | ✓    |\n| direction | 布局方向，[可选值](#direction)                 | `LR` \\| `RL` \\| `TB` \\| `BT` \\| `H` \\| `V` | `LR`   |      |\n| nodeSep   | 节点间距，即同一层级节点之间的距离，单位为像素 | number                                     | 20     |      |\n| rankSep   | 层级间距，即不同层级之间的距离，单位为像素     | number                                     | 200    |      |\n| radial    | 是否启用辐射状布局，[说明](#radial)            | boolean                                    | false  |      |\n\n### direction\n\n树布局的方向，有以下选项：\n\n- `TB`：根节点在上，往下布局\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*krAnRrLTEnEAAAAAAAAAAABkARQnAQ' width=115 alt='TB方向'/>\n\n- `BT`：根节点在下，往上布局\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*0HRyS64i7QoAAAAAAAAAAABkARQnAQ' width=115 alt='BT方向'/>\n\n- `LR`：根节点在左，往右布局\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*T5KZTJdA2OUAAAAAAAAAAABkARQnAQ' width=55 alt='LR方向'/>\n\n- `RL`：根节点在右，往左布局\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*q7QJQ5RbQ5kAAAAAAAAAAABkARQnAQ' width=55 alt='RL方向'/>\n\n- `H`：根节点在中间，水平对称布局\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*tzIfRJ5CuR8AAAAAAAAAAABkARQnAQ' width=85 alt='H方向'/>\n\n- `V`：根节点在中间，垂直对称布局\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*B9sjToOzCiAAAAAAAAAAAABkARQnAQ' width=115 alt='V方向'/>\n\n### radial\n\n是否启用辐射状布局模式。启用后，节点将以根节点为中心呈辐射状分布。\n\n若 `radial` 设置为 `true`，建议将 `direction` 设置为 `'LR'` 或 `'RL'` 以获得最佳效果。\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*AhopQI5j-bcAAAAAAAAAAABkARQnAQ' width=175 alt='辐射状布局'/>\n"
  },
  {
    "path": "packages/site/docs/manual/layout/Fishbone.en.md",
    "content": "---\ntitle: Fishbone Layout\norder: 11\n---\n\n## Overview\n\nFishbone layout is a graphical layout specifically designed for representing hierarchical data. By simulating the shape of a fishbone, it arranges data nodes according to their hierarchy, making the hierarchical relationships of the data clearer and more intuitive. Fishbone layout is especially suitable for datasets that need to show causality, hierarchy, or classification information.\n\n## Use Cases\n\n- Displaying hierarchical data, such as organizational structures or classification systems\n- Showing problem analysis processes, such as fault analysis or quality analysis\n- Displaying decision processes, such as decision trees or factor analysis\n\n## Online Demo\n\n<embed src=\"@/common/api/layout/fishbone.md\"></embed>\n\n## Basic Usage\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'fishbone',\n    direction: 'LR',\n    hGap: 50,\n    vGap: 50,\n    getRibSep: () => 60,\n  },\n});\n```\n\n## Options\n\n| Property               | Description                                                                                      | Type                                                                                                                                   | Default  | Required |\n| ---------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------- |\n| type                   | Layout type                                                                                      | `fishbone`                                                                                                                             | -        | ✓        |\n| direction              | Layout direction, `RL` (right to left, head on right), `LR` (left to right, head on left)        | `RL` \\| `LR`                                                                                                                           | `RL`     |          |\n| hGap                   | Horizontal gap                                                                                   | number                                                                                                                                 | -        |          |\n| vGap                   | Vertical gap                                                                                     | number                                                                                                                                 | -        |          |\n| getRibSep              | Function to get rib gap                                                                          | (node: NodeData) => number                                                                                                             | () => 60 |          |\n| width                  | Layout width                                                                                     | number                                                                                                                                 | -        |          |\n| height                 | Layout height                                                                                    | number                                                                                                                                 | -        |          |\n| nodeSize               | Node size                                                                                        | number \\| [number, number] \\| [number, number, number] \\| ((node: NodeData) => number \\| [number, number] \\| [number, number, number]) | -        |          |\n| isLayoutInvisibleNodes | Whether invisible nodes participate in layout (effective when preLayout is true)                 | boolean                                                                                                                                | -        |          |\n| nodeFilter             | Nodes to participate in this layout                                                              | (node: NodeData) => boolean                                                                                                            | -        |          |\n| preLayout              | Use pre-layout, calculate layout before initializing elements (not suitable for pipeline layout) | boolean                                                                                                                                | -        |          |\n\n## Code Example\n\n### Basic Usage\n\nThe simplest configuration:\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nconst graph = new Graph({\n  layout: {\n    type: 'fishbone',\n  },\n  autoFit: 'view',\n  data: treeToGraphData({\n    nodes: [\n      { id: 'root', data: { label: 'Root' } },\n      { id: 'child1', data: { label: 'Child 1' } },\n      { id: 'child2', data: { label: 'Child 2' } },\n      { id: 'child3', data: { label: 'Child 3' } },\n    ],\n    edges: [\n      { id: 'e1', source: 'root', target: 'child1' },\n      { id: 'e2', source: 'root', target: 'child2' },\n      { id: 'e3', source: 'root', target: 'child3' },\n    ],\n  }),\n  edge: {\n    type: 'polyline',\n    style: {\n      lineWidth: 3,\n    },\n  },\n  behaviors: ['drag-canvas'],\n});\n```\n\nResult:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 400,\n  layout: {\n    type: 'fishbone',\n  },\n  autoFit: 'view',\n  data: {\n    nodes: [\n      {\n        id: 'Quality',\n        depth: 0,\n        children: ['Machine', 'Method', 'Material', 'Man Power', 'Measurement', 'Milieu'],\n      },\n      {\n        id: 'Machine',\n        depth: 1,\n        children: ['Mill', 'Mixer', 'Metal Lathe'],\n      },\n      {\n        id: 'Mill',\n        depth: 2,\n      },\n      {\n        id: 'Mixer',\n        depth: 2,\n      },\n      {\n        id: 'Metal Lathe',\n        depth: 2,\n        children: ['Milling'],\n      },\n      {\n        id: 'Milling',\n        depth: 3,\n      },\n      {\n        id: 'Method',\n        depth: 1,\n      },\n      {\n        id: 'Material',\n        depth: 1,\n        children: ['Masonite', 'Marscapone', 'Meat'],\n      },\n      {\n        id: 'Masonite',\n        depth: 2,\n        children: ['spearMint', 'pepperMint', 'test1'],\n      },\n      {\n        id: 'spearMint',\n        depth: 3,\n      },\n      {\n        id: 'pepperMint',\n        depth: 3,\n        children: ['test3'],\n      },\n      {\n        id: 'test3',\n        depth: 4,\n      },\n      {\n        id: 'test1',\n        depth: 3,\n        children: ['test4'],\n      },\n      {\n        id: 'test4',\n        depth: 4,\n      },\n      {\n        id: 'Marscapone',\n        depth: 2,\n        children: ['Malty', 'Minty'],\n      },\n      {\n        id: 'Malty',\n        depth: 3,\n      },\n      {\n        id: 'Minty',\n        depth: 3,\n      },\n      {\n        id: 'Meat',\n        depth: 2,\n        children: ['Mutton'],\n      },\n      {\n        id: 'Mutton',\n        depth: 3,\n      },\n      {\n        id: 'Man Power',\n        depth: 1,\n        children: ['Manager', \"Master's Student\", 'Magician', 'Miner', 'Magister', 'Massage Artist'],\n      },\n      {\n        id: 'Manager',\n        depth: 2,\n      },\n      {\n        id: \"Master's Student\",\n        depth: 2,\n      },\n      {\n        id: 'Magician',\n        depth: 2,\n      },\n      {\n        id: 'Miner',\n        depth: 2,\n      },\n      {\n        id: 'Magister',\n        depth: 2,\n        children: ['Malpractice'],\n      },\n      {\n        id: 'Malpractice',\n        depth: 3,\n      },\n      {\n        id: 'Massage Artist',\n        depth: 2,\n        children: ['Masseur', 'Masseuse'],\n      },\n      {\n        id: 'Masseur',\n        depth: 3,\n      },\n      {\n        id: 'Masseuse',\n        depth: 3,\n      },\n      {\n        id: 'Measurement',\n        depth: 1,\n        children: ['Malleability'],\n      },\n      {\n        id: 'Malleability',\n        depth: 2,\n      },\n      {\n        id: 'Milieu',\n        depth: 1,\n        children: ['Marine'],\n      },\n      {\n        id: 'Marine',\n        depth: 2,\n      },\n    ],\n    edges: [\n      {\n        source: 'Quality',\n        target: 'Machine',\n      },\n      {\n        source: 'Quality',\n        target: 'Method',\n      },\n      {\n        source: 'Quality',\n        target: 'Material',\n      },\n      {\n        source: 'Quality',\n        target: 'Man Power',\n      },\n      {\n        source: 'Quality',\n        target: 'Measurement',\n      },\n      {\n        source: 'Quality',\n        target: 'Milieu',\n      },\n      {\n        source: 'Machine',\n        target: 'Mill',\n      },\n      {\n        source: 'Machine',\n        target: 'Mixer',\n      },\n      {\n        source: 'Machine',\n        target: 'Metal Lathe',\n      },\n      {\n        source: 'Metal Lathe',\n        target: 'Milling',\n      },\n      {\n        source: 'Material',\n        target: 'Masonite',\n      },\n      {\n        source: 'Material',\n        target: 'Marscapone',\n      },\n      {\n        source: 'Material',\n        target: 'Meat',\n      },\n      {\n        source: 'Masonite',\n        target: 'spearMint',\n      },\n      {\n        source: 'Masonite',\n        target: 'pepperMint',\n      },\n      {\n        source: 'Masonite',\n        target: 'test1',\n      },\n      {\n        source: 'pepperMint',\n        target: 'test3',\n      },\n      {\n        source: 'test1',\n        target: 'test4',\n      },\n      {\n        source: 'Marscapone',\n        target: 'Malty',\n      },\n      {\n        source: 'Marscapone',\n        target: 'Minty',\n      },\n      {\n        source: 'Meat',\n        target: 'Mutton',\n      },\n      {\n        source: 'Man Power',\n        target: 'Manager',\n      },\n      {\n        source: 'Man Power',\n        target: \"Master's Student\",\n      },\n      {\n        source: 'Man Power',\n        target: 'Magician',\n      },\n      {\n        source: 'Man Power',\n        target: 'Miner',\n      },\n      {\n        source: 'Man Power',\n        target: 'Magister',\n      },\n      {\n        source: 'Man Power',\n        target: 'Massage Artist',\n      },\n      {\n        source: 'Magister',\n        target: 'Malpractice',\n      },\n      {\n        source: 'Massage Artist',\n        target: 'Masseur',\n      },\n      {\n        source: 'Massage Artist',\n        target: 'Masseuse',\n      },\n      {\n        source: 'Measurement',\n        target: 'Malleability',\n      },\n      {\n        source: 'Milieu',\n        target: 'Marine',\n      },\n    ],\n  },\n  edge: {\n    type: 'polyline',\n    style: {\n      lineWidth: 3,\n    },\n  },\n  behaviors: ['drag-canvas'],\n});\n\ngraph.render();\n```\n\n## Real Case\n\n```js | ob { inject: true }\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nconst data = {\n  id: 'Quality',\n  children: [\n    {\n      id: 'Machine',\n      children: [{ id: 'Mill' }, { id: 'Mixer' }, { id: 'Metal Lathe', children: [{ id: 'Milling' }] }],\n    },\n    { id: 'Method' },\n    {\n      id: 'Material',\n      children: [\n        {\n          id: 'Masonite',\n          children: [\n            { id: 'spearMint' },\n            { id: 'pepperMint', children: [{ id: 'test3' }] },\n            { id: 'test1', children: [{ id: 'test4' }] },\n          ],\n        },\n        {\n          id: 'Marscapone',\n          children: [{ id: 'Malty' }, { id: 'Minty' }],\n        },\n        { id: 'Meat', children: [{ id: 'Mutton' }] },\n      ],\n    },\n    {\n      id: 'Man Power',\n      children: [\n        { id: 'Manager' },\n        { id: \"Master's Student\" },\n        { id: 'Magician' },\n        { id: 'Miner' },\n        { id: 'Magister', children: [{ id: 'Malpractice' }] },\n        {\n          id: 'Massage Artist',\n          children: [{ id: 'Masseur' }, { id: 'Masseuse' }],\n        },\n      ],\n    },\n    {\n      id: 'Measurement',\n      children: [{ id: 'Malleability' }],\n    },\n    {\n      id: 'Milieu',\n      children: [{ id: 'Marine' }],\n    },\n  ],\n};\n\nexport const layoutFishbone = async (context) => {\n  const graph = new Graph({\n    ...context,\n    container: 'container',\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    node: {\n      type: 'rect',\n      style: {\n        size: [32, 32],\n        // fill: () => randomColor(),\n        label: false,\n        labelFill: '#262626',\n        labelFontFamily: 'Gill Sans',\n        labelMaxLines: 2,\n        labelMaxWidth: '100%',\n        labelPlacement: 'center',\n        labelText: (d) => d.id,\n        labelWordWrap: true,\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        lineWidth: 3,\n      },\n    },\n    layout: {\n      type: 'fishbone',\n      vGap: 48,\n      hGap: 48,\n      direction: 'RL',\n    },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n    animation: false,\n  });\n\n  await graph.render();\n\n  layoutFishbone.form = (panel) => {\n    const config = {\n      type: 'fishbone',\n      direction: 'RL',\n    };\n\n    return [\n      panel\n        .add(config, 'direction', ['LR', 'RL'])\n        .name('Direction')\n        .onChange((value) => {\n          graph.setLayout((prev) => ({ ...prev, direction: value }));\n          graph.render();\n        }),\n    ];\n  };\n\n  return graph;\n};\n\nlayoutFishbone();\n```\n\n- [Fishbone Layout](/en/examples/layout/fishbone/#basic)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/Fishbone.zh.md",
    "content": "---\ntitle: 鱼骨布局 Fishbone\norder: 11\n---\n\n## 概述\n\n鱼骨布局是一种专门用于表示层次结构数据的图形布局方式。它通过模拟鱼骨的形状，将数据节点按照层次结构排列，使得数据的层次关系更加清晰直观。鱼骨布局特别适用于需要展示因果关系、层次结构或分类信息的数据集。\n\n## 使用场景\n\n- 需要展示层次结构数据，如组织结构、分类体系\n- 需要展示问题分析过程，如故障分析、质量分析\n- 需要展示决策过程，如决策树、影响因素分析\n\n## 在线体验\n\n<embed src=\"@/common/api/layout/fishbone.md\"></embed>\n\n## 基本用法\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'fishbone',\n    direction: 'LR',\n    hGap: 50,\n    vGap: 50,\n    getRibSep: () => 60,\n  },\n});\n```\n\n## 配置项\n\n| 属性                   | 描述                                                       | 类型                                                                                                                                   | 默认值   | 必选 |\n| ---------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---- |\n| type                   | 布局类型                                                   | `fishbone`                                                                                                                             | -        | ✓    |\n| direction              | 排布方向，`RL` 从右到左，鱼头在右；`LR` 从左到右，鱼头在左 | `RL` \\| `LR`                                                                                                                           | `RL`     |      |\n| hGap                   | 水平间距                                                   | number                                                                                                                                 | -        |      |\n| vGap                   | 垂直间距                                                   | number                                                                                                                                 | -        |      |\n| getRibSep              | 获取鱼骨间距                                               | (node: NodeData) => number                                                                                                             | () => 60 |      |\n| width                  | 布局宽度                                                   | number                                                                                                                                 | -        |      |\n| height                 | 布局高度                                                   | number                                                                                                                                 | -        |      |\n| nodeSize               | 节点大小                                                   | number \\| [number, number] \\| [number, number, number] \\| ((node: NodeData) => number \\| [number, number] \\| [number, number, number]) | -        |      |\n| isLayoutInvisibleNodes | 不可见节点是否参与布局，当 preLayout 为 true 时生效        | boolean                                                                                                                                | -        |      |\n| nodeFilter             | 参与该布局的节点                                           | (node: NodeData) => boolean                                                                                                            | -        |      |\n| preLayout              | 使用前布局，在初始化元素前计算布局，不适用于流水线布局     | boolean                                                                                                                                | -        |      |\n\n## 代码示例\n\n### 基础用法\n\n最简单的配置方式：\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nconst graph = new Graph({\n  layout: {\n    type: 'fishbone',\n  },\n  autoFit: 'view',\n  data: treeToGraphData({\n    nodes: [\n      { id: 'root', data: { label: 'Root' } },\n      { id: 'child1', data: { label: 'Child 1' } },\n      { id: 'child2', data: { label: 'Child 2' } },\n      { id: 'child3', data: { label: 'Child 3' } },\n    ],\n    edges: [\n      { id: 'e1', source: 'root', target: 'child1' },\n      { id: 'e2', source: 'root', target: 'child2' },\n      { id: 'e3', source: 'root', target: 'child3' },\n    ],\n  }),\n  edge: {\n    type: 'polyline',\n    style: {\n      lineWidth: 3,\n    },\n  },\n  behaviors: ['drag-canvas'],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 400,\n  layout: {\n    type: 'fishbone',\n  },\n  autoFit: 'view',\n  data: {\n    nodes: [\n      {\n        id: 'Quality',\n        depth: 0,\n        children: ['Machine', 'Method', 'Material', 'Man Power', 'Measurement', 'Milieu'],\n      },\n      {\n        id: 'Machine',\n        depth: 1,\n        children: ['Mill', 'Mixer', 'Metal Lathe'],\n      },\n      {\n        id: 'Mill',\n        depth: 2,\n      },\n      {\n        id: 'Mixer',\n        depth: 2,\n      },\n      {\n        id: 'Metal Lathe',\n        depth: 2,\n        children: ['Milling'],\n      },\n      {\n        id: 'Milling',\n        depth: 3,\n      },\n      {\n        id: 'Method',\n        depth: 1,\n      },\n      {\n        id: 'Material',\n        depth: 1,\n        children: ['Masonite', 'Marscapone', 'Meat'],\n      },\n      {\n        id: 'Masonite',\n        depth: 2,\n        children: ['spearMint', 'pepperMint', 'test1'],\n      },\n      {\n        id: 'spearMint',\n        depth: 3,\n      },\n      {\n        id: 'pepperMint',\n        depth: 3,\n        children: ['test3'],\n      },\n      {\n        id: 'test3',\n        depth: 4,\n      },\n      {\n        id: 'test1',\n        depth: 3,\n        children: ['test4'],\n      },\n      {\n        id: 'test4',\n        depth: 4,\n      },\n      {\n        id: 'Marscapone',\n        depth: 2,\n        children: ['Malty', 'Minty'],\n      },\n      {\n        id: 'Malty',\n        depth: 3,\n      },\n      {\n        id: 'Minty',\n        depth: 3,\n      },\n      {\n        id: 'Meat',\n        depth: 2,\n        children: ['Mutton'],\n      },\n      {\n        id: 'Mutton',\n        depth: 3,\n      },\n      {\n        id: 'Man Power',\n        depth: 1,\n        children: ['Manager', \"Master's Student\", 'Magician', 'Miner', 'Magister', 'Massage Artist'],\n      },\n      {\n        id: 'Manager',\n        depth: 2,\n      },\n      {\n        id: \"Master's Student\",\n        depth: 2,\n      },\n      {\n        id: 'Magician',\n        depth: 2,\n      },\n      {\n        id: 'Miner',\n        depth: 2,\n      },\n      {\n        id: 'Magister',\n        depth: 2,\n        children: ['Malpractice'],\n      },\n      {\n        id: 'Malpractice',\n        depth: 3,\n      },\n      {\n        id: 'Massage Artist',\n        depth: 2,\n        children: ['Masseur', 'Masseuse'],\n      },\n      {\n        id: 'Masseur',\n        depth: 3,\n      },\n      {\n        id: 'Masseuse',\n        depth: 3,\n      },\n      {\n        id: 'Measurement',\n        depth: 1,\n        children: ['Malleability'],\n      },\n      {\n        id: 'Malleability',\n        depth: 2,\n      },\n      {\n        id: 'Milieu',\n        depth: 1,\n        children: ['Marine'],\n      },\n      {\n        id: 'Marine',\n        depth: 2,\n      },\n    ],\n    edges: [\n      {\n        source: 'Quality',\n        target: 'Machine',\n      },\n      {\n        source: 'Quality',\n        target: 'Method',\n      },\n      {\n        source: 'Quality',\n        target: 'Material',\n      },\n      {\n        source: 'Quality',\n        target: 'Man Power',\n      },\n      {\n        source: 'Quality',\n        target: 'Measurement',\n      },\n      {\n        source: 'Quality',\n        target: 'Milieu',\n      },\n      {\n        source: 'Machine',\n        target: 'Mill',\n      },\n      {\n        source: 'Machine',\n        target: 'Mixer',\n      },\n      {\n        source: 'Machine',\n        target: 'Metal Lathe',\n      },\n      {\n        source: 'Metal Lathe',\n        target: 'Milling',\n      },\n      {\n        source: 'Material',\n        target: 'Masonite',\n      },\n      {\n        source: 'Material',\n        target: 'Marscapone',\n      },\n      {\n        source: 'Material',\n        target: 'Meat',\n      },\n      {\n        source: 'Masonite',\n        target: 'spearMint',\n      },\n      {\n        source: 'Masonite',\n        target: 'pepperMint',\n      },\n      {\n        source: 'Masonite',\n        target: 'test1',\n      },\n      {\n        source: 'pepperMint',\n        target: 'test3',\n      },\n      {\n        source: 'test1',\n        target: 'test4',\n      },\n      {\n        source: 'Marscapone',\n        target: 'Malty',\n      },\n      {\n        source: 'Marscapone',\n        target: 'Minty',\n      },\n      {\n        source: 'Meat',\n        target: 'Mutton',\n      },\n      {\n        source: 'Man Power',\n        target: 'Manager',\n      },\n      {\n        source: 'Man Power',\n        target: \"Master's Student\",\n      },\n      {\n        source: 'Man Power',\n        target: 'Magician',\n      },\n      {\n        source: 'Man Power',\n        target: 'Miner',\n      },\n      {\n        source: 'Man Power',\n        target: 'Magister',\n      },\n      {\n        source: 'Man Power',\n        target: 'Massage Artist',\n      },\n      {\n        source: 'Magister',\n        target: 'Malpractice',\n      },\n      {\n        source: 'Massage Artist',\n        target: 'Masseur',\n      },\n      {\n        source: 'Massage Artist',\n        target: 'Masseuse',\n      },\n      {\n        source: 'Measurement',\n        target: 'Malleability',\n      },\n      {\n        source: 'Milieu',\n        target: 'Marine',\n      },\n    ],\n  },\n  edge: {\n    type: 'polyline',\n    style: {\n      lineWidth: 3,\n    },\n  },\n  behaviors: ['drag-canvas'],\n});\n\ngraph.render();\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nconst data = {\n  id: 'Quality',\n  children: [\n    {\n      id: 'Machine',\n      children: [{ id: 'Mill' }, { id: 'Mixer' }, { id: 'Metal Lathe', children: [{ id: 'Milling' }] }],\n    },\n    { id: 'Method' },\n    {\n      id: 'Material',\n      children: [\n        {\n          id: 'Masonite',\n          children: [\n            { id: 'spearMint' },\n            { id: 'pepperMint', children: [{ id: 'test3' }] },\n            { id: 'test1', children: [{ id: 'test4' }] },\n          ],\n        },\n        {\n          id: 'Marscapone',\n          children: [{ id: 'Malty' }, { id: 'Minty' }],\n        },\n        { id: 'Meat', children: [{ id: 'Mutton' }] },\n      ],\n    },\n    {\n      id: 'Man Power',\n      children: [\n        { id: 'Manager' },\n        { id: \"Master's Student\" },\n        { id: 'Magician' },\n        { id: 'Miner' },\n        { id: 'Magister', children: [{ id: 'Malpractice' }] },\n        {\n          id: 'Massage Artist',\n          children: [{ id: 'Masseur' }, { id: 'Masseuse' }],\n        },\n      ],\n    },\n    {\n      id: 'Measurement',\n      children: [{ id: 'Malleability' }],\n    },\n    {\n      id: 'Milieu',\n      children: [{ id: 'Marine' }],\n    },\n  ],\n};\n\nexport const layoutFishbone = async (context) => {\n  const graph = new Graph({\n    ...context,\n    container: 'container',\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    node: {\n      type: 'rect',\n      style: {\n        size: [32, 32],\n        // fill: () => randomColor(),\n        label: false,\n        labelFill: '#262626',\n        labelFontFamily: 'Gill Sans',\n        labelMaxLines: 2,\n        labelMaxWidth: '100%',\n        labelPlacement: 'center',\n        labelText: (d) => d.id,\n        labelWordWrap: true,\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        lineWidth: 3,\n      },\n    },\n    layout: {\n      type: 'fishbone',\n      vGap: 48,\n      hGap: 48,\n      direction: 'RL',\n    },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n    animation: false,\n  });\n\n  await graph.render();\n\n  layoutFishbone.form = (panel) => {\n    const config = {\n      type: 'fishbone',\n      direction: 'RL',\n    };\n\n    return [\n      panel\n        .add(config, 'direction', ['LR', 'RL'])\n        .name('Direction')\n        .onChange((value) => {\n          graph.setLayout((prev) => ({ ...prev, direction: value }));\n          graph.render();\n        }),\n    ];\n  };\n\n  return graph;\n};\n\nlayoutFishbone();\n```\n\n- [Fishbone布局](/examples/layout/fishbone/#basic)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/ForceAtlas2Layout.en.md",
    "content": "---\ntitle: ForceAtlas2 Force-directed Layout\norder: 12\n---\n\n## Overview\n\nForceAtlas2 is a force-directed layout algorithm that optimizes node positions by simulating forces in a physical system. This layout is especially suitable for visualizing large-scale network data, effectively revealing relationships and cluster structures among nodes.\n\n## Use Cases\n\n- Social network analysis: Display user relationship networks, with node degree reflecting user influence\n- Knowledge graphs: Show associations between concepts, discover knowledge domains through clustering\n- System architecture diagrams: Show dependencies between system components, highlight core components via hub mode\n\n## Online Demo\n\n<embed src=\"@/common/api/layout/force-atlas2.md\"></embed>\n\n## Basic Usage\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'force-atlas2',\n    preventOverlap: true,\n    kr: 20,\n    center: [250, 250],\n  },\n});\n```\n\n## Options\n\n| Property       | Description                                                                                                                                                                   | Type                            | Default  | Required |\n| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | -------- | -------- |\n| type           | Layout type, must be `force-atlas2`                                                                                                                                           | `force-atlas2`                  | -        | ✓        |\n| barnesHut      | Whether to enable quadtree acceleration. When enabled, improves performance for large graphs but may affect layout quality. By default, enabled if node count > 250.          | boolean                         | -        |          |\n| dissuadeHubs   | Whether to enable hub mode. If true, nodes with higher in-degree are more likely to be placed at the center than those with high out-degree                                   | boolean                         | false    |          |\n| height         | Layout height. Defaults to container height                                                                                                                                   | number                          | -        |          |\n| kg             | Gravity coefficient. The larger the value, the more concentrated the layout is at the center                                                                                  | number                          | 1        |          |\n| kr             | Repulsion coefficient. Adjusts the compactness of the layout. The larger the value, the looser the layout                                                                     | number                          | 5        |          |\n| ks             | Controls the speed of node movement during iteration                                                                                                                          | number                          | 0.1      |          |\n| ksmax          | Maximum node movement speed during iteration                                                                                                                                  | number                          | 10       |          |\n| mode           | Clustering mode. In `linlog` mode, clusters are more compact                                                                                                                  | `normal` \\| `linlog`            | `normal` |          |\n| nodeSize       | Node size (diameter). Used for repulsion calculation when `preventOverlap` is enabled. If not set, uses `data.size` in node data                                              | Size \\| ((node?: Node) => Size) | -        |          |\n| onTick         | Callback for each iteration                                                                                                                                                   | (data: LayoutMapping) => void   | -        |          |\n| preventOverlap | Whether to prevent node overlap. When enabled, layout considers node size to avoid overlap. Node size is specified by `nodeSize` or `data.size` in node data                  | boolean                         | false    |          |\n| prune          | Whether to enable auto-pruning. By default, enabled if node count > 100. Pruning speeds up convergence but may reduce layout quality. Set to false to disable auto-activation | boolean                         | -        |          |\n| tao            | Tolerance for stopping oscillation when layout is near convergence                                                                                                            | number                          | 0.1      |          |\n| width          | Layout width. Defaults to container width                                                                                                                                     | number                          | -        |          |\n| center         | Layout center, format [x, y]. Each node is attracted to this point, gravity controlled by `kg`. If not set, uses canvas center                                                | [number, number]                | -        |          |\n\n## Code Example\n\n### Basic Usage\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  layout: {\n    type: 'force-atlas2',\n    preventOverlap: true,\n    kr: 20,\n  },\n  autoFit: 'view',\n  data: {\n    nodes: [\n      { id: 'node1' },\n      { id: 'node2' },\n      { id: 'node3' },\n      { id: 'node4' },\n      { id: 'node5' },\n      { id: 'node6' },\n      { id: 'node7' },\n      { id: 'node8' },\n      { id: 'node9' },\n      { id: 'node10' },\n      { id: 'node11' },\n      { id: 'node12' },\n      { id: 'node13' },\n      { id: 'node14' },\n      { id: 'node15' },\n    ],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n      { source: 'node5', target: 'node6' },\n      { source: 'node6', target: 'node7' },\n      { source: 'node7', target: 'node8' },\n      { source: 'node8', target: 'node9' },\n      { source: 'node9', target: 'node10' },\n      { source: 'node10', target: 'node11' },\n      { source: 'node11', target: 'node12' },\n      { source: 'node12', target: 'node13' },\n      { source: 'node13', target: 'node14' },\n      { source: 'node14', target: 'node15' },\n      { source: 'node15', target: 'node1' },\n      { source: 'node1', target: 'node8' },\n      { source: 'node2', target: 'node9' },\n      { source: 'node3', target: 'node10' },\n      { source: 'node4', target: 'node11' },\n      { source: 'node5', target: 'node12' },\n      { source: 'node6', target: 'node13' },\n      { source: 'node7', target: 'node14' },\n    ],\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n});\n```\n\nResult:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 300,\n  layout: {\n    type: 'force-atlas2',\n    preventOverlap: true,\n    kr: 20,\n  },\n  data: {\n    nodes: [\n      { id: 'node1' },\n      { id: 'node2' },\n      { id: 'node3' },\n      { id: 'node4' },\n      { id: 'node5' },\n      { id: 'node6' },\n      { id: 'node7' },\n      { id: 'node8' },\n      { id: 'node9' },\n      { id: 'node10' },\n      { id: 'node11' },\n      { id: 'node12' },\n      { id: 'node13' },\n      { id: 'node14' },\n      { id: 'node15' },\n    ],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n      { source: 'node5', target: 'node6' },\n      { source: 'node6', target: 'node7' },\n      { source: 'node7', target: 'node8' },\n      { source: 'node8', target: 'node9' },\n      { source: 'node9', target: 'node10' },\n      { source: 'node10', target: 'node11' },\n      { source: 'node11', target: 'node12' },\n      { source: 'node12', target: 'node13' },\n      { source: 'node13', target: 'node14' },\n      { source: 'node14', target: 'node15' },\n      { source: 'node15', target: 'node1' },\n      { source: 'node1', target: 'node8' },\n      { source: 'node2', target: 'node9' },\n      { source: 'node3', target: 'node10' },\n      { source: 'node4', target: 'node11' },\n      { source: 'node5', target: 'node12' },\n      { source: 'node6', target: 'node13' },\n      { source: 'node7', target: 'node14' },\n    ],\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n## Real Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      layout: {\n        type: 'force-atlas2',\n        preventOverlap: true,\n        kr: 20,\n        center: [250, 250],\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n      autoResize: true,\n      zoomRange: [0.1, 5],\n    });\n\n    graph.render();\n  });\n```\n\n- [ForceAtlas2 Layout](/en/examples/layout/force-directed/#atlas2)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/ForceAtlas2Layout.zh.md",
    "content": "---\ntitle: ForceAtlas2 力导向布局\norder: 12\n---\n\n## 概述\n\nForceAtlas2 是一种基于力导向的布局算法，它通过模拟物理系统中的力来优化节点位置。该布局特别适用于大规模网络数据的可视化，能够有效地展示节点之间的关系和聚类结构。\n\n## 使用场景\n\n- 社交网络分析：展示用户之间的关系网络，通过节点度数反映用户影响力\n- 知识图谱：展示概念之间的关联关系，通过聚类效果发现知识领域\n- 系统架构图：展示系统组件之间的依赖关系，通过 hub 模式突出核心组件\n\n## 在线体验\n\n<embed src=\"@/common/api/layout/force-atlas2.md\"></embed>\n\n## 基本用法\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'force-atlas2',\n    preventOverlap: true,\n    kr: 20,\n    center: [250, 250],\n  },\n});\n```\n\n## 配置项\n\n| 属性           | 描述                                                                                                                                                                 | 类型                            | 默认值   | 必选 |\n| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | -------- | ---- |\n| type           | 布局类型，必须为 `force-atlas2`                                                                                                                                      | `force-atlas2`                  | -        | ✓    |\n| barnesHut      | 是否开启四叉树加速，开启后可以提升大规模图的布局性能，但可能会影响布局质量。默认情况下为 undefined，当节点数量大于 250 时它将会被激活。设置为 false 则不会自动被激活 | boolean                         | -        |      |\n| dissuadeHubs   | 是否开启 hub 模式。若为 true，相比与出度大的节点，入度大的节点将会有更高的优先级被放置在中心位置                                                                     | boolean                         | false    |      |\n| height         | 布局高度，默认使用容器高度                                                                                                                                           | number                          | -        |      |\n| kg             | 重力系数，`kg` 越大，布局越聚集在中心                                                                                                                                | number                          | 1        |      |\n| kr             | 斥力系数，可用于调整布局的紧凑程度。kr 越大，布局越松散                                                                                                              | number                          | 5        |      |\n| ks             | 控制迭代过程中，节点移动的速度                                                                                                                                       | number                          | 0.1      |      |\n| ksmax          | 迭代过程中，最大的节点移动的速度上限                                                                                                                                 | number                          | 10       |      |\n| mode           | 聚类模式，`linlog` 模式下，聚类将更加紧凑                                                                                                                            | `normal` \\| `linlog`            | `normal` |      |\n| nodeSize       | 节点大小（直径）。当开启 `preventOverlap` 时，用于计算节点之间的斥力。如果不设置，则使用节点数据中的 data.size 属性                                                  | Size \\| ((node?: Node) => Size) | -        |      |\n| onTick         | 每一次迭代的回调函数                                                                                                                                                 | (data: LayoutMapping) => void   | -        |      |\n| preventOverlap | 是否防止节点重叠。开启后，布局会考虑节点大小，避免节点重叠。节点大小通过 `nodeSize` 配置指定，如果没有设置 `nodeSize`，则通过节点数据中的 data.size 属性指定         | boolean                         | false    |      |\n| prune          | 是否开启自动剪枝模式。默认情况下为 undefined，当节点数量大于 100 时它将会被激活。注意，剪枝能够提高收敛速度，但可能会降低图的布局质量。设置为 false 则不会自动被激活 | boolean                         | -        |      |\n| tao            | 迭代接近收敛时停止震荡的容忍度                                                                                                                                       | number                          | 0.1      |      |\n| width          | 布局宽度，默认使用容器宽度                                                                                                                                           | number                          | -        |      |\n| center         | 布局中心点，用于指定重力的中心，格式为 [x, y]。每个节点都会受到一个指向该中心点的重力，重力大小由 `kg` 参数控制。如果不设置，则使用画布中心点                        | [number, number]                | -        |      |\n\n## 代码示例\n\n### 基础用法\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  layout: {\n    type: 'force-atlas2',\n    preventOverlap: true,\n    kr: 20,\n  },\n  autoFit: 'view',\n  data: {\n    nodes: [\n      { id: 'node1' },\n      { id: 'node2' },\n      { id: 'node3' },\n      { id: 'node4' },\n      { id: 'node5' },\n      { id: 'node6' },\n      { id: 'node7' },\n      { id: 'node8' },\n      { id: 'node9' },\n      { id: 'node10' },\n      { id: 'node11' },\n      { id: 'node12' },\n      { id: 'node13' },\n      { id: 'node14' },\n      { id: 'node15' },\n    ],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n      { source: 'node5', target: 'node6' },\n      { source: 'node6', target: 'node7' },\n      { source: 'node7', target: 'node8' },\n      { source: 'node8', target: 'node9' },\n      { source: 'node9', target: 'node10' },\n      { source: 'node10', target: 'node11' },\n      { source: 'node11', target: 'node12' },\n      { source: 'node12', target: 'node13' },\n      { source: 'node13', target: 'node14' },\n      { source: 'node14', target: 'node15' },\n      { source: 'node15', target: 'node1' },\n      { source: 'node1', target: 'node8' },\n      { source: 'node2', target: 'node9' },\n      { source: 'node3', target: 'node10' },\n      { source: 'node4', target: 'node11' },\n      { source: 'node5', target: 'node12' },\n      { source: 'node6', target: 'node13' },\n      { source: 'node7', target: 'node14' },\n    ],\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 300,\n  layout: {\n    type: 'force-atlas2',\n    preventOverlap: true,\n    kr: 20,\n  },\n  data: {\n    nodes: [\n      { id: 'node1' },\n      { id: 'node2' },\n      { id: 'node3' },\n      { id: 'node4' },\n      { id: 'node5' },\n      { id: 'node6' },\n      { id: 'node7' },\n      { id: 'node8' },\n      { id: 'node9' },\n      { id: 'node10' },\n      { id: 'node11' },\n      { id: 'node12' },\n      { id: 'node13' },\n      { id: 'node14' },\n      { id: 'node15' },\n    ],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n      { source: 'node5', target: 'node6' },\n      { source: 'node6', target: 'node7' },\n      { source: 'node7', target: 'node8' },\n      { source: 'node8', target: 'node9' },\n      { source: 'node9', target: 'node10' },\n      { source: 'node10', target: 'node11' },\n      { source: 'node11', target: 'node12' },\n      { source: 'node12', target: 'node13' },\n      { source: 'node13', target: 'node14' },\n      { source: 'node14', target: 'node15' },\n      { source: 'node15', target: 'node1' },\n      { source: 'node1', target: 'node8' },\n      { source: 'node2', target: 'node9' },\n      { source: 'node3', target: 'node10' },\n      { source: 'node4', target: 'node11' },\n      { source: 'node5', target: 'node12' },\n      { source: 'node6', target: 'node13' },\n      { source: 'node7', target: 'node14' },\n    ],\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      layout: {\n        type: 'force-atlas2',\n        preventOverlap: true,\n        kr: 20,\n        center: [250, 250],\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n      autoResize: true,\n      zoomRange: [0.1, 5],\n    });\n\n    graph.render();\n  });\n```\n\n- [ForceAtlas2布局](/examples/layout/force-directed/#atlas2)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/ForceLayout.en.md",
    "content": "---\ntitle: Force-directed Layout\norder: 13\n---\n\n## Overview\n\nForce-directed layout is a graph layout algorithm based on physical simulation that determines node positions by simulating attraction and repulsion forces between nodes. This layout is particularly suitable for displaying complex relationship networks, such as social networks and knowledge graphs.\n\nThe force-directed layout automatically calculates and adjusts node positions to maintain appropriate distances between connected nodes while minimizing edge crossings. During the layout process, it simulates a physical system where nodes repel each other like charged particles, and edges connect nodes like springs.\n\nKey features of force-directed layout include:\n\n1. **Automatic Arrangement**: No need to manually set node positions, the system automatically finds suitable positions\n2. **Real-time Adjustment**: When you drag a node, other nodes will adjust their positions in real-time\n3. **Flexible Configuration**:\n   - Can adjust attraction and repulsion forces between nodes\n   - Can set edge lengths\n   - Can prevent node overlap\n4. **Animation Effects**: Smooth animations during node movement make changes more natural\n\n<img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/pdZUQIReZ_gAAAAAAAAAAAAADpdRAQFr/original' alt='Force-directed Layout Example'/>\n\n## Core Concepts\n\n### Basic Principles of Force-directed Layout\n\nForce-directed layout is a graph layout algorithm based on physical simulation that models nodes and edges as a physical system:\n\n- Nodes are treated as physical particles\n- Edges are treated as springs\n- The entire system reaches its lowest energy state through physical simulation\n\n### Detailed Core Forces\n\n#### Node Repulsion\n\n- **Physical Model**: Coulomb's Law\n- **Function**: Prevents node overlap and ensures more uniform node distribution, where `factor` and `coulombDisScale` control the overall strength and range of repulsion.\n- **Formula**:\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/7udvQ5K8VvMAAAAAAAAAAAAADpdRAQFr/original' alt='Repulsion Force'/>\n\n  - k: Repulsion coefficient (`factor` / `coulombDisScale²`)\n  - q1,q2: Node strength (`nodeStrength`)\n  - r: Distance between nodes\n\n#### Edge Attraction\n\n- **Physical Model**: Hooke's Law\n- **Function**: Simulates edge tension, moving nodes along edge directions, where `edgeStrength` and `linkDistance` control edge \"stiffness\" and length.\n- **Formula**:\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/WY15QYfpMSAAAAAAAAAAAAAADpdRAQFr/original' alt='Edge Attraction'/>\n\n  - ka: Edge attraction strength (`edgeStrength`)\n  - L: Edge length (`linkDistance`)\n  - r: Actual edge length\n\n#### Centripetal Force\n\n- **Physical Model**: Newton's Universal Law of Gravitation\n- **Function**: Attracts nodes toward the canvas center or cluster centers, where `gravity` and `center` control gravity strength and center point position\n- **Formula**:\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/R-26R4Zc09kAAAAAQDAAAAgADpdRAQFr/original' alt='Centripetal Force'/>\n\n  - G: Gravitational constant (`gravity`)\n  - xc: Center point coordinates (`center`)\n  - mass: Node mass (`nodeSize`)\n\n#### Interaction of Three Forces\n\n- **Physical Model**: Force interactions, generating acceleration\n- **Function**: Repulsion, edge attraction, and centripetal force work together, affecting node movement through acceleration superposition, ultimately reaching the lowest energy state.\n- **Formula**:\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/R-26R4Zc09kAAAAAQDAAAAgADpdRAQFr/original' alt='Force Interactions'/>\n\n### Physical System\n\n#### Node Velocity Formula\n\n- **Formula**:\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/4Nk0Q44tWGIAAAAAAAAAAAAADpdRAQFr/original' alt='Node Velocity Formula'/>\n\n  - v: Velocity\n  - a: Acceleration\n  - dt: Time step (`interval`)\n  - damping: Damping coefficient (`damping`)\n\n- **Function**:\n  1. Controls node movement stability\n  2. Damping coefficient prevents system oscillation\n  3. Time step affects displacement per iteration\n\n#### Node Position Formula\n\n- **Formula**:\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/orF2RoAlHwAAAAAAAAAAAAAADpdRAQFr/original' alt='Node Position Formula'/>\n\n  - x: Node position\n  - v: Node velocity\n  - dt: Time step (`interval`)\n\n- **Function**:\n  1. Updates node position based on velocity\n  2. Ensures motion continuity\n  3. Prevents node overlap through `preventOverlap`\n\n#### Cluster Center Calculation\n\n- **Formula**:\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/2jc-TrgcG20AAAAAQDAAAAgADpdRAQFr/original' alt='Cluster Center Calculation'/>\n\n  - n: Number of nodes in cluster\n  - (xi​,yi​): Position of each node\n\n- **Function**:\n  1. Calculates cluster center\n  2. Centripetal force pulls nodes toward their cluster center\n  3. Cluster center can change dynamically\n\n#### Cluster Strength Calculation\n\n- **Formula**:\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/sVEtTLyM3rwAAAAAAAAAAAAADpdRAQFr/original' alt='Cluster Strength Calculation'/>\n\n  - s: Cluster strength (`clusterNodeStrength`)\n  - xc​: Cluster center\n\n- **Function**:\n  1. Controls cluster compactness\n  2. Higher cluster strength means tighter clusters\n  3. Can be dynamically adjusted based on node properties\n\n#### Mass Effect on Forces\n\n- **Formula**:\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/5ckVQ6gHQygAAAAAQBAAAAgADpdRAQFr/original' alt='Mass Effect on Forces'/>\n\n  - a: Acceleration\n  - F: Force (repulsion, edge attraction, centripetal force)\n  - mass: Node mass\n\n- **Function**:\n  1. Nodes with larger mass move less\n  2. Nodes with smaller mass move more\n  3. Mass calculation can be customized through `getMass`\n\n#### Energy Calculation\n\n- **Formula**:\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/M84ERKphqf0AAAAAAAAAAAAADpdRAQFr/original' alt='Energy Calculation'/>\n\n  - m: Node mass\n  - v: Node velocity\n\n- **Function**:\n  1. Monitors layout convergence\n  2. System stabilizes when energy approaches zero\n\n#### System Convergence Condition\n\n- **Formula**:\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/oBHdRLKIEAcAAAAAQFAAAAgADpdRAQFr/original' alt='System Convergence Condition'/>\n\n- **Function**:\n  1. Controls iteration count\n  2. Stops when movement is below threshold\n  3. Can choose between mean, maximum, or minimum through `distanceThresholdMode`\n\n### Force Interaction Diagram\n\n```mermaid\ngraph TD\n    A[Input] --> B[Initialize Parameters];\n    B --> C[Build Layout Calculation];\n    C --> D[Iterative Calculation];\n    D --> E{Converged?};\n    E -->|Yes| F[Output Layout];\n    E -->|No| G[Calculate Repulsion];\n    G --> H[Calculate Edge Attraction];\n    H --> I[Calculate Centripetal Force];\n    I --> J[Update Velocity];\n    J --> K[Update Position];\n    K --> D;\n```\n\n<img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/BOu7Rbuz4MoAAAAAQjAAAAgADpdRAQFr/original' alt='Force Simulation Diagram'/>\n\n## Configuration Options\n\nBased on the physical characteristics of force-directed layout, the following configuration options are available:\n\n### Basic Configuration\n\n| Property              | Description                                                                                                                                                                                                                                        | Default Value | Required |\n| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- |\n| type                  | Layout type                                                                                                                                                                                                                                        | `force`       | ✓        |\n| dimensions            | Layout dimensions, 2 for 2D layout, 3 for 3D layout                                                                                                                                                                                                | 2             |          |\n| width                 | Layout width                                                                                                                                                                                                                                       | Canvas width  |          |\n| height                | Layout height                                                                                                                                                                                                                                      | Canvas height |          |\n| center                | Layout center point                                                                                                                                                                                                                                | Graph center  |          |\n| maxIteration          | Maximum iteration count, if 0 will auto-adjust                                                                                                                                                                                                     | 0             |          |\n| minMovement           | Stop iteration when average movement distance is less than 0.4                                                                                                                                                                                     | 0.4           |          |\n| distanceThresholdMode | Movement distance calculation mode: mean: stop when average movement distance is less than `minMovement`; max: stop when maximum movement distance is less than `minMovement`; min: stop when minimum movement distance is less than `minMovement` | `mean`        |          |\n| maxDistance           | Maximum distance                                                                                                                                                                                                                                   |               |          |\n\n### Force-related Configuration\n\n#### Repulsion Configuration\n\n| Property        | Description                                                                                         | Default Value | Required |\n| --------------- | --------------------------------------------------------------------------------------------------- | ------------- | -------- |\n| nodeStrength    | Node force, positive values represent attraction between nodes, negative values represent repulsion | 1000          |          |\n| factor          | Repulsion coefficient, larger values mean stronger repulsion                                        | 1             |          |\n| coulombDisScale | Coulomb coefficient, a factor for repulsion, larger values mean stronger repulsion between nodes    | 0.005         |          |\n\n#### Edge Attraction Configuration\n\n| Property     | Description                                                                                                    | Default Value | Required |\n| ------------ | -------------------------------------------------------------------------------------------------------------- | ------------- | -------- |\n| edgeStrength | Edge force (attraction) strength, fixed force or callback function to dynamically return different edge forces | 500           |          |\n| linkDistance | Edge length, fixed length or callback function to dynamically return different edge lengths                    | 200           |          |\n\n#### Centripetal Force Configuration\n\n| Property           | Description                                                                                                                                                                                                                                                          | Default Value | Required |\n| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- |\n| gravity            | Center force strength, the force attracting all nodes to the center. Larger values mean more compact layout                                                                                                                                                          | 10            |          |\n| centripetalOptions | Centripetal force configuration, including center and strength for leaf nodes, isolated nodes, and other nodes. leaf: leaf node centripetal force; single: single node centripetal force; others: other node centripetal force; center: custom center point function | [0, 0]        |          |\n\n#### Clustering Configuration\n\n| Property            | Description                                                                                                                                                                                                                                                                                                                                                   | Default Value | Required |\n| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- |\n| clustering          | Whether to cluster all nodes. If true, will use the field specified by nodeClusterBy in node data for clustering. centripetalOptions.single, centripetalOptions.leaf, and centripetalOptions.others will use the value returned by getClusterNodeStrength; leaf and centripetalOptions.center will use the average center of all nodes in the current cluster | `false`       |          |\n| nodeClusterBy       | Specifies the field name in node data for clustering. Takes effect when clustering is true. Automatically generates centripetalOptions, can be used with clusterNodeStrength                                                                                                                                                                                  |               |          |\n| clusterNodeStrength | Used with clustering and nodeClusterBy to specify the strength of the cluster centripetal force                                                                                                                                                                                                                                                               |               |          |\n| leafCluster         | Whether to cluster leaf nodes. If true, centripetalOptions.single will be 100; centripetalOptions.leaf will use the value returned by getClusterNodeStrength; getClusterNodeStrength.center will return the average center of all leaf nodes                                                                                                                  | false         |          |\n\n#### Performance and Optimization Configuration\n\n| Property        | Description                                                                                                                                                                                                                                                               | Default Value | Required |\n| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- |\n| damping         | Damping coefficient, range [0, 1]. Larger values mean slower speed decrease                                                                                                                                                                                               | 0.9           |          |\n| maxSpeed        | Maximum movement length per iteration                                                                                                                                                                                                                                     | 200           |          |\n| interval        | Controls the movement speed of each node per iteration                                                                                                                                                                                                                    | 0.02          |          |\n| preventOverlap  | Whether to prevent overlap. Must be used with nodeSize or data.size in node data. Only when data.size is set in the data or nodeSize is configured in the layout with the same value as the node size in the graph, collision detection for node overlap can be performed | true          |          |\n| nodeSize        | Node size (diameter). Used for collision detection to prevent node overlap. Fixed size or callback function to dynamically return node size                                                                                                                               |               |          |\n| nodeSpacing     | Takes effect when preventOverlap is true. Minimum spacing between node edges to prevent overlap. Can be a callback to set different spacing for different nodes                                                                                                           |               |          |\n| collideStrength | Strength of anti-overlap force, range [0, 1]                                                                                                                                                                                                                              | 1             |          |\n\n#### Other Configuration\n\n| Property  | Description                                                                                                                                                                              | Default Value | Required |\n| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- |\n| getMass   | Callback for the mass of each node. The parameter is the node's internal data, and the return value is the mass                                                                          |               |          |\n| getCenter | Callback for the x, y, and strength of the centripetal force for each node. If not specified, no extra centripetal force is applied                                                      |               |          |\n| onTick    | Callback for each iteration                                                                                                                                                              |               |          |\n| monitor   | Callback for monitoring each iteration. energy indicates the convergence energy of the layout. May incur extra computation if configured; if not configured, no computation is performed |               |          |\n\n## Code Examples\n\n### Basic Usage\n\n```js\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'force',\n    // Prevent node overlap\n    preventOverlap: true,\n    // Node size\n    nodeSize: 20,\n    // Layout width\n    width: 800,\n    // Layout height\n    height: 600,\n  },\n});\n```\n\n### Preventing Node Overlap\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'force',\n    // Prevent node overlap\n    preventOverlap: true,\n    // Node size\n    nodeSize: 20,\n  },\n});\n```\n\n### Force-directed Layout\n\nThis example demonstrates how to create a basic force-directed graph using force-directed layout.\n\n```js\nimport { Graph, NodeEvent } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', label: 'Node 1', size: 30 },\n    { id: 'node2', label: 'Node 2', size: 20 },\n    { id: 'node3', label: 'Node 3', size: 20 },\n    { id: 'node4', label: 'Node 4', size: 20 },\n    { id: 'node5', label: 'Node 5', size: 30 },\n    { id: 'node6', label: 'Node 6', size: 20 },\n  ],\n  edges: [\n    { source: 'node1', target: 'node2' },\n    { source: 'node1', target: 'node3' },\n    { source: 'node2', target: 'node4' },\n    { source: 'node3', target: 'node4' },\n    { source: 'node4', target: 'node5' },\n    { source: 'node5', target: 'node6' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  autoFit: 'view',\n  modes: {\n    default: ['drag-canvas', 'zoom-canvas'],\n  },\n  layout: {\n    type: 'force',\n    // Prevent node overlap\n    preventOverlap: true,\n    // Node size\n    nodeSize: 20,\n    // Centripetal force\n    gravity: 0.9,\n    // Iteration count\n    iterations: 100,\n  },\n  node: {\n    style: {\n      size: (d) => d.size,\n      fill: '#9EC9FF',\n      stroke: '#69C8FF',\n      label: (d) => d.label,\n      labelPlacement: 'center',\n      labelFill: '#333',\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#e2e2e2',\n    },\n  },\n});\n\ngraph.on(NodeEvent.CLICK, async (e) => {\n  const nodeId = e.target.id;\n  graph.updateNodeData([{ id: nodeId, size: 200 }]);\n  await graph.render();\n});\n\ngraph.render();\n```\n\nKey configuration explanations:\n\n- `preventOverlap`: Enable node overlap detection\n- `nodeSize`: Set node size\n- `gravity`: Set node centripetal force\n- `iterations`: Set layout calculation precision\n\nYou can also refer to [View Examples](https://g6.antv.antgroup.com/examples/layout/force-directed/#force) for more usage examples.\n"
  },
  {
    "path": "packages/site/docs/manual/layout/ForceLayout.zh.md",
    "content": "---\ntitle: Force 力导向布局\norder: 13\n---\n\n## 概述\n\n力导向布局是一种基于物理模拟的图布局算法，它通过模拟节点间的引力和斥力来确定节点的位置。这种布局方式特别适合展示复杂的关系网络，如社交网络、知识图谱等。\n\n力导向布局会自动计算并调整节点位置，使得相连的节点保持适当的距离，同时尽量减少边的交叉。布局过程中会模拟物理系统，节点会像带电粒子一样相互排斥，边则像弹簧一样连接节点。\n\n力导向布局的主要特点包括：\n\n1. **自动排列**：不需要手动设置节点位置，系统会自动找到合适的位置\n2. **实时调整**：当你拖动某个节点时，其他节点会实时跟随调整位置\n3. **灵活配置**：\n   - 可以调整节点间的吸引力和排斥力\n   - 可以设置边的长度\n   - 可以防止节点重叠\n4. **动画效果**：节点移动时会有平滑的动画，让变化更自然\n\n<img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/pdZUQIReZ_gAAAAAAAAAAAAADpdRAQFr/original' alt='力导向布局示例'/>\n\n## 核心概念\n\n### Force 力导向布局基本原理\n\n力导向布局是一种基于物理模拟的图布局算法，它将图中的节点和边模拟为物理系统：\n\n- 节点被视为物理粒子\n- 边被视为弹簧\n- 整个系统通过物理模拟达到能量最低状态\n\n### 核心力详解\n\n#### 斥力（Node Repulsion）\n\n- **物理模型**：库伦定律（Coulomb's Law）\n- **作用**：防止节点重叠，让节点分布更均匀，其中 `factor` 和 `coulombDisScale` 控制斥力的总体强度和范围。\n- **公式**：\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/7udvQ5K8VvMAAAAAAAAAAAAADpdRAQFr/original' alt='斥力'/>\n\n  - k: 斥力系数（`factor` / `coulombDisScale²`）\n  - q1,q2: 节点强度(`nodeStrength`)\n  - r: 节点间距离\n\n#### 边拉力（Edge Attraction）\n\n- **物理模型**：胡克定律（Hooke's Law）\n- **作用**：模拟边的拉力，使节点沿着边的方向移动，其中 `edgeStrength` 和 `linkDistance` 控制边的“硬度”和长度。\n- **公式**：\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/WY15QYfpMSAAAAAAAAAAAAAADpdRAQFr/original' alt='边拉力'/>\n\n  - ka: 边拉力强度（`edgeStrength`）\n  - L: 边的长度（`linkDistance`）\n  - r: 实际边长度\n\n#### 向心力（Gravity）\n\n- **物理模型**：牛顿万有引力定律（Newton's Universal Law of Gravitational）\n- **作用**：使节点向画布中心或者聚类中心聚集，其中 `gravity` 和 `center` 控制重力强度和中心点位置\n- **公式**：\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/R-26R4Zc09kAAAAAQDAAAAgADpdRAQFr/original' alt='向心力'/>\n\n  - G: 万有引力常数（`gravity`）\n  - xc: 中心点坐标（`center`）\n  - mass: 节点质量（`nodeSize`）\n\n#### 三种力的相互作用\n\n- **物理模型**：力的相互作用，产生加速度\n- **作用**：斥力、边拉力、向心力共同作用，通过加速度叠加影响节点运动，最终达到能量最低状态。\n- **公式**：\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/R-26R4Zc09kAAAAAQDAAAAgADpdRAQFr/original' alt='力的相互作用'/>\n\n### 物理系统\n\n#### 节点运动速度公式\n\n- **公式**：\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/4Nk0Q44tWGIAAAAAAAAAAAAADpdRAQFr/original' alt='节点运动速度公式'/>\n\n  - v: 速度\n  - a: 加速度\n  - dt: 时间步长（`interval`）\n  - damping: 阻尼系数（`damping`）\n\n- **作用**：\n  1. 控制节点移动的稳定性\n  2. 阻尼系数防止系统震荡\n  3. 时间步长影响每次迭代的位移\n\n#### 节点位置公式\n\n- **公式**：\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/orF2RoAlHwAAAAAAAAAAAAAADpdRAQFr/original' alt='节点位置公式'/>\n\n  - x: 节点位置\n  - v: 节点速度\n  - dt: 时间步长（`interval`）\n\n- **作用**：\n  1. 根据速度更新节点位置\n  2. 确保运动连续性\n  3. 通过 `preventOverlap` 防止节点重叠\n\n#### 聚类中心计算\n\n- **公式**：\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/2jc-TrgcG20AAAAAQDAAAAgADpdRAQFr/original' alt='聚类中心计算'/>\n\n  - n: 聚类内节点数量\n  - (xi​,yi​): 每个节点的位置\n\n- **作用**：\n  1. 计算聚类中心\n  2. 向心力将节点拉向所属聚类中心\n  3. 聚类中心可动态变化\n\n#### 聚类强度计算\n\n- **公式**：\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/sVEtTLyM3rwAAAAAAAAAAAAADpdRAQFr/original' alt='聚类强度计算'/>\n\n  - s: 聚类强度（`clusterNodeStrength`）\n  - xc​: 聚类中心\n\n- **作用**：\n  1. 控制聚类的紧密程度\n  2. 聚类强度越大，聚类越紧凑\n  3. 可根据节点属性动态调整\n\n#### 质量对力的影响\n\n- **公式**：\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/5ckVQ6gHQygAAAAAQBAAAAgADpdRAQFr/original' alt='质量对力的影响'/>\n\n  - a: 加速度\n  - F: 力（斥力、边拉力、向心力）\n  - mass: 节点质量\n\n- **作用**：\n  1. 质量大的节点移动较小\n  2. 质量小的节点移动较大\n  3. 通过 `getMass` 可自定义质量计算\n\n#### 能量计算\n\n- **公式**：\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/M84ERKphqf0AAAAAAAAAAAAADpdRAQFr/original' alt='能量计算'/>\n\n  - m: 节点质量\n  - v: 节点速度\n\n- **作用**：\n  1. 监控布局收敛情况\n  2. 能量趋近于零时系统趋于稳定\n\n#### 系统收敛条件\n\n- **公式**：\n  <img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/oBHdRLKIEAcAAAAAQFAAAAgADpdRAQFr/original' alt='系统收敛条件'/>\n\n- **作用**：\n  1. 控制迭代次数\n  2. 移动量小于阈值时停止\n  3. 通过 `distanceThresholdMode` 可选择平均值、最大值或最小值\n\n### 力相互作用图\n\n<img src='https://mdn.alipayobjects.com/huamei_4greni/afts/img/2lI1RruANXoAAAAAAAAAAAAADpdRAQFr/original' alt='力作用模拟图'/>\n\n## 配置项\n\n根据上述力导向布局的物理特性，有以下配置项：\n\n### 基础配置\n\n| 属性                  | 描述                                                                                                                                                         | 默认值   | 必选 |\n| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ---- |\n| type                  | 布局类型                                                                                                                                                     | `force`  | ✓    |\n| dimensions            | 布局维度，2表示2D布局，3表示3D布局                                                                                                                           | 2        |      |\n| width                 | 布局宽度                                                                                                                                                     | 画布宽度 |      |\n| height                | 布局高度                                                                                                                                                     | 画布高度 |      |\n| center                | 布局的中心点                                                                                                                                                 | 图中心   |      |\n| maxIteration          | 最大迭代次数，若为 0 则将自动调整                                                                                                                            | 0        |      |\n| minMovement           | 当平均移动距离小于0.4时停止迭代                                                                                                                              | 0.4      |      |\n| distanceThresholdMode | 移动距离的计算模式：mean: 平均移动距离小于 `minMovement` 时停止迭代；max: 最大移动距离小于时 `minMovement` 时停止迭代；min: 最小移动距离小于时 `minMovement` | `mean`   |      |\n| maxDistance           | 最大距离                                                                                                                                                     |          |      |\n\n### 力相关配置\n\n#### 斥力配置\n\n| 属性            | 描述                                                               | 默认值 | 必选 |\n| --------------- | ------------------------------------------------------------------ | ------ | ---- |\n| nodeStrength    | 节点作用力，正数代表节点之间的引力作用，负数代表节点之间的斥力作用 | 1000   |      |\n| factor          | 斥力系数，数值越大，斥力越大                                       | 1      |      |\n| coulombDisScale | 库伦系数，斥力的一个系数，数字越大，节点之间的斥力越大             | 0.005  |      |\n\n#### 边拉力配置\n\n| 属性         | 描述                                                               | 默认值 | 必选 |\n| ------------ | ------------------------------------------------------------------ | ------ | ---- |\n| edgeStrength | 边的作用力（引力）大小，固定作用力或回调函数动态返回不同边的作用力 | 500    |      |\n| linkDistance | 边的长度，固定长度或回调函数动态返回不同边的长度                   | 200    |      |\n\n#### 向心力配置\n\n| 属性               | 描述                                                                                                                                                         | 默认值 | 必选 |\n| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | ---- |\n| gravity            | 向中心力大小，指所有节点被吸引到 center 的力。数字越大，布局越紧凑                                                                                           | 10     |      |\n| centripetalOptions | 向心力配置，包括叶子节点、离散点、其他节点的向心中心及向心力大小。leaf: 叶子节点向心力；single: 单点向心力；others: 其他节点向心力；center: 自定义中心点函数 | [0, 0] |      |\n\n#### 聚类配置\n\n| 属性                | 描述                                                                                                                                                                                                                                                                                                      | 默认值  | 必选 |\n| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ---- |\n| clustering          | 是否需要全部节点聚类，若为 `true`，将使用 `nodeClusterBy` 配置的节点数据中的字段作为聚类依据。 `centripetalOptions.single`、`centripetalOptions.leaf`、`centripetalOptions.others` 将使用 `getClusterNodeStrength` 返回值；`leaf`、`centripetalOptions.center` 将使用当前节点所属聚类中所有节点的平均中心 | `false` |      |\n| nodeClusterBy       | 指定节点数据中的字段名称作为节点聚类的依据，`clustering` 为 true 时生效，自动生成 `centripetalOptions`，可配合 `clusterNodeStrength` 使用                                                                                                                                                                 |         |      |\n| clusterNodeStrength | 配合 `clustering` 和 `nodeClusterBy` 使用，指定聚类向心力的大小                                                                                                                                                                                                                                           |         |      |\n| leafCluster         | 是否需要叶子节点聚类，若为 `true`，则 `centripetalOptions.single` 将为 100；`centripetalOptions.leaf` 将使用 `getClusterNodeStrength` 返回值；`getClusterNodeStrength.center` 将为叶子节点返回当前所有叶子节点的平均中心                                                                                  | false   |      |\n\n#### 性能与优化配置\n\n| 属性            | 描述                                                                                                                                                                                           | 默认值 | 必选 |\n| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---- |\n| damping         | 阻尼系数，取值范围 [0, 1]。数字越大，速度降低得越慢                                                                                                                                            | 0.9    |      |\n| maxSpeed        | 一次迭代的最大移动长度                                                                                                                                                                         | 200    |      |\n| interval        | 控制每个迭代节点的移动速度                                                                                                                                                                     | 0.02   |      |\n| preventOverlap  | 是否防止重叠，必须配合下面属性 `nodeSize` 或节点数据中的 `data.size` 属性，只有在数据中设置了 `data.size` 或在该布局中配置了与当前图节点大小相同的 `nodeSize` 值，才能够进行节点重叠的碰撞检测 | true   |      |\n| nodeSize        | 节点大小（直径）。用于防止节点重叠时的碰撞检测，固定大小或者回调函数动态返回节点大小                                                                                                           |        |      |\n| nodeSpacing     | `preventOverlap` 为 `true` 时生效, 防止重叠时节点边缘间距的最小值。可以是回调函数, 为不同节点设置不同的最小间距                                                                                |        |      |\n| collideStrength | 防止重叠的力强度，范围 [0, 1]                                                                                                                                                                  | 1      |      |\n\n#### 其他配置\n\n| 属性      | 描述                                                                                                                                                                                                | 默认值 | 必选 |\n| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---- |\n| getMass   | 获取节点质量的回调函数，入参为节点内部流转数据，用于计算节点质量大小                                                                                                                                |        |      |\n| getCenter | 每个节点中心力的 x、y、强度的回调函数，若不指定，则没有额外中心力                                                                                                                                   |        |      |\n| onTick    | 每一次迭代的回调函数                                                                                                                                                                                |        |      |\n| monitor   | 每个迭代的监控信息回调，energy 表示布局的收敛能量。若配置可能带来额外的计算能量性能消耗，不配置则不计算。入参为迭代监控信息 `{ energy: number; nodes: Node[]; edges: Edge[]; iterations: number; }` |        |      |\n\n## 代码示例\n\n### 基础用法\n\n```js\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'force',\n    // 防止节点重叠\n    preventOverlap: true,\n    // 节点大小\n    nodeSize: 20,\n    // 布局宽度\n    width: 800,\n    // 布局高度\n    height: 600,\n  },\n});\n```\n\n### 防止节点重叠\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'force',\n    // 防止节点重叠\n    preventOverlap: true,\n    // 节点大小\n    nodeSize: 20,\n  },\n});\n```\n\n### 力导向布局\n\n该示例展示了如何使用力导向布局创建一个基础的力导向图。\n\n```js\nimport { Graph, NodeEvent } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', label: 'Node 1', size: 30 },\n    { id: 'node2', label: 'Node 2', size: 20 },\n    { id: 'node3', label: 'Node 3', size: 20 },\n    { id: 'node4', label: 'Node 4', size: 20 },\n    { id: 'node5', label: 'Node 5', size: 30 },\n    { id: 'node6', label: 'Node 6', size: 20 },\n  ],\n  edges: [\n    { source: 'node1', target: 'node2' },\n    { source: 'node1', target: 'node3' },\n    { source: 'node2', target: 'node4' },\n    { source: 'node3', target: 'node4' },\n    { source: 'node4', target: 'node5' },\n    { source: 'node5', target: 'node6' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  autoFit: 'view',\n  modes: {\n    default: ['drag-canvas', 'zoom-canvas'],\n  },\n  layout: {\n    type: 'force',\n    // 防止节点重叠\n    preventOverlap: true,\n    // 节点大小\n    nodeSize: 20,\n    // 向心力\n    gravity: 0.9,\n    // 迭代次数\n    iterations: 100,\n  },\n  node: {\n    style: {\n      size: (d) => d.size,\n      fill: '#9EC9FF',\n      stroke: '#69C8FF',\n      label: (d) => d.label,\n      labelPlacement: 'center',\n      labelFill: '#333',\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#e2e2e2',\n    },\n  },\n});\n\ngraph.on(NodeEvent.CLICK, async (e) => {\n  const nodeId = e.target.id;\n  graph.updateNodeData([{ id: nodeId, size: 200 }]);\n  await graph.render();\n});\n\ngraph.render();\n```\n\n主要配置说明：\n\n- `preventOverlap`: 开启节点重叠检测\n- `nodeSize`: 设置节点大小\n- `gravity`: 设置节点向心力\n- `iterations`: 设置布局计算的精确程度\n\n还可以参考 [查看示例](https://g6.antv.antgroup.com/examples/layout/force-directed/#force) 获取更多用法。\n"
  },
  {
    "path": "packages/site/docs/manual/layout/FruchtermanLayout.en.md",
    "content": "---\ntitle: Fruchterman Force-directed Layout\norder: 14\n---\n\n## Overview\n\nThe Fruchterman layout is a force-directed layout based on the algorithm from [Graph Drawing by Force-directed Placement](https://www.mathe2.uni-bayreuth.de/axel/papers/reingold:graph_drawing_by_force_directed_placement.pdf). By flexibly configuring parameters to simulate physical forces, the layout automatically reaches a stable equilibrium state with minimal energy. It supports both basic uniform distribution and cluster layouts. See more Fruchterman force-directed layout [examples](/en/examples#layout-fruchterman) and [source code](https://github.com/antvis/layout/blob/v5/packages/layout/src/fruchterman.ts).\n\n## Use Cases\n\n- Basic uniform distribution: Suitable for displaying network graphs with evenly distributed nodes and clear overall structure, such as network topology and knowledge graphs.\n- Cluster layout: Suitable for visualizing data with internal aggregation or grouping, such as community structure display and association group analysis.\n\n## Options\n\n| Property | Description                                                                                                                                                                                        | Type                                                                                                       | Default          | Required |\n| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------- | -------- |\n| type     | Layout type                                                                                                                                                                                        | `'fruchterman'`                                                                                            | -                | ✓        |\n| height   | Layout height                                                                                                                                                                                      | `number`                                                                                                   | container height |          |\n| width    | Layout width                                                                                                                                                                                       | `number`                                                                                                   | container width  |          |\n| gravity  | Central force, i.e., the force attracting all nodes to the [center](https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L915). The larger the value, the more compact the layout | `number`                                                                                                   | 10               |          |\n| speed    | Node movement speed per iteration. Too high a speed may cause strong oscillation                                                                                                                   | `number`                                                                                                   | 5                |          |\n| onTick   | Callback for each iteration                                                                                                                                                                        | (data: [LayoutMapping](https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L69)) => void | -                |          |\n\n### Cluster Layout\n\n| Property       | Description                                                                               | Type      | Default     | Required |\n| -------------- | ----------------------------------------------------------------------------------------- | --------- | ----------- | -------- |\n| clustering     | Whether to use cluster layout                                                             | `boolean` | `false`     |          |\n| nodeClusterBy  | Field name in node data for clustering, effective when `clustering` is true               | `string`  | `'cluster'` |          |\n| clusterGravity | Gravity within clusters, affects cluster compactness, effective when `clustering` is true | `number`  | 10          |          |\n\n## Example Code\n\n### Basic Layout\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 500,\n  height: 250,\n  data: {\n    nodes: [\n      { id: '0' },\n      { id: '1' },\n      { id: '2' },\n      { id: '3' },\n      { id: '4' },\n      { id: '5' },\n      { id: '6' },\n      { id: '7' },\n      { id: '8' },\n      { id: '9' },\n      { id: '10' },\n    ],\n    edges: [\n      { source: '0', target: '1' },\n      { source: '0', target: '2' },\n      { source: '0', target: '3' },\n      { source: '0', target: '4' },\n      { source: '0', target: '7' },\n      { source: '0', target: '8' },\n      { source: '0', target: '9' },\n      { source: '0', target: '10' },\n      { source: '2', target: '3' },\n      { source: '4', target: '5' },\n      { source: '4', target: '6' },\n      { source: '5', target: '6' },\n      { source: '9', target: '10' },\n    ],\n  },\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n  },\n  layout: {\n    type: 'fruchterman',\n    gravity: 5,\n    speed: 5,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n<details><summary>Show full code</summary>\n\n```javascript\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0' },\n    { id: '1' },\n    { id: '2' },\n    { id: '3' },\n    { id: '4' },\n    { id: '5' },\n    { id: '6' },\n    { id: '7' },\n    { id: '8' },\n    { id: '9' },\n    { id: '10' },\n  ],\n  edges: [\n    { source: '0', target: '1' },\n    { source: '0', target: '2' },\n    { source: '0', target: '3' },\n    { source: '0', target: '4' },\n    { source: '0', target: '7' },\n    { source: '0', target: '8' },\n    { source: '0', target: '9' },\n    { source: '0', target: '10' },\n    { source: '2', target: '3' },\n    { source: '4', target: '5' },\n    { source: '4', target: '6' },\n    { source: '5', target: '6' },\n    { source: '9', target: '10' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n  },\n  layout: {\n    type: 'fruchterman',\n    gravity: 5,\n    speed: 5,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n</details>\n\n### Cluster Layout\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 500,\n  height: 250,\n  data: {\n    nodes: [\n      { id: '0', data: { cluster: 'a' } },\n      { id: '1', data: { cluster: 'a' } },\n      { id: '2', data: { cluster: 'a' } },\n      { id: '3', data: { cluster: 'a' } },\n      { id: '4', data: { cluster: 'a' } },\n      { id: '5', data: { cluster: 'b' } },\n      { id: '6', data: { cluster: 'b' } },\n      { id: '7', data: { cluster: 'b' } },\n      { id: '8', data: { cluster: 'c' } },\n      { id: '9', data: { cluster: 'c' } },\n      { id: '10', data: { cluster: 'c' } },\n    ],\n    edges: [\n      { source: '0', target: '1' },\n      { source: '0', target: '2' },\n      { source: '0', target: '4' },\n      { source: '0', target: '6' },\n      { source: '2', target: '3' },\n      { source: '2', target: '4' },\n      { source: '3', target: '4' },\n      { source: '5', target: '6' },\n      { source: '6', target: '7' },\n      { source: '7', target: '8' },\n      { source: '8', target: '9' },\n      { source: '8', target: '10' },\n    ],\n  },\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => `${d.data.cluster}-${d.id}`,\n    },\n    palette: {\n      type: 'group',\n      field: 'cluster',\n    },\n  },\n  edge: {\n    style: {\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'fruchterman',\n    gravity: 6,\n    speed: 5,\n\n    // Cluster layout parameters\n    clustering: true,\n    nodeClusterBy: 'cluster',\n    clusterGravity: 3,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n<details><summary>Show full code</summary>\n\n```javascript\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0', data: { cluster: 'a' } },\n    { id: '1', data: { cluster: 'a' } },\n    { id: '2', data: { cluster: 'a' } },\n    { id: '3', data: { cluster: 'a' } },\n    { id: '4', data: { cluster: 'a' } },\n    { id: '5', data: { cluster: 'b' } },\n    { id: '6', data: { cluster: 'b' } },\n    { id: '7', data: { cluster: 'b' } },\n    { id: '8', data: { cluster: 'c' } },\n    { id: '9', data: { cluster: 'c' } },\n    { id: '10', data: { cluster: 'c' } },\n  ],\n  edges: [\n    { source: '0', target: '1' },\n    { source: '0', target: '2' },\n    { source: '0', target: '4' },\n    { source: '0', target: '6' },\n    { source: '2', target: '3' },\n    { source: '2', target: '4' },\n    { source: '3', target: '4' },\n    { source: '5', target: '6' },\n    { source: '6', target: '7' },\n    { source: '7', target: '8' },\n    { source: '8', target: '9' },\n    { source: '8', target: '10' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => `${d.data.cluster}-${d.id}`,\n    },\n    palette: {\n      type: 'group',\n      field: 'cluster',\n    },\n  },\n  edge: {\n    style: {\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'fruchterman',\n    gravity: 6,\n    speed: 5,\n\n    // Cluster layout parameters\n    clustering: true,\n    nodeClusterBy: 'cluster',\n    clusterGravity: 3,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n</details>\n"
  },
  {
    "path": "packages/site/docs/manual/layout/FruchtermanLayout.zh.md",
    "content": "---\ntitle: Fruchterman 力导向布局\norder: 14\n---\n\n## 概述\n\nFruchterman 布局是基于 [Graph Drawing by Force-directed Placement](https://www.mathe2.uni-bayreuth.de/axel/papers/reingold:graph_drawing_by_force_directed_placement.pdf) 算法实现的一种力导向布局，通过灵活的参数配置模拟物理作用，使整个布局自动达到能量最小的稳定平衡状态，支持基础均匀分布和聚类布局。参考更多 Fruchterman 力导向布局[样例](/examples#layout-fruchterman)和[源码](https://github.com/antvis/layout/blob/v5/packages/layout/src/fruchterman.ts)\n\n## 使用场景\n\n- 基础均匀分布: 适用于展示节点均匀分布，整体结构清晰的网络关系图, 比如网络拓扑、知识图谱。\n- 聚类布局: 适用于具有内部聚合特性或分组的数据可视化展示, 比如社区结构展示、关联组分析。\n\n## 配置项\n\n| 属性    | 描述                                                                                                                                           | 类型                                                                                                       | 默认值   | 必选 |\n| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -------- | ---- |\n| type    | 布局类型                                                                                                                                       | `'fruchterman'`                                                                                            | -        | ✓    |\n| height  | 布局的高度                                                                                                                                     | `number`                                                                                                   | 容器高度 |      |\n| width   | 布局的宽度                                                                                                                                     | `number`                                                                                                   | 容器宽度 |      |\n| gravity | 中心力大小，指所有节点被吸引到 [center](https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L915) 的力。数字越大，布局越紧凑 | `number`                                                                                                   | 10       |      |\n| speed   | 每次迭代节点移动的速度。速度太快可能会导致强烈震荡                                                                                             | `number`                                                                                                   | 5        |      |\n| onTick  | 每一次迭代的回调函数                                                                                                                           | (data: [LayoutMapping](https://github.com/antvis/layout/blob/v5/packages/layout/src/types.ts#L69)) => void | -        |      |\n\n### 聚类布局\n\n| 属性           | 描述                                                                       | 类型      | 默认值      | 必选 |\n| -------------- | -------------------------------------------------------------------------- | --------- | ----------- | ---- |\n| clustering     | 是否按照聚类布局                                                           | `boolean` | `false`     |      |\n| nodeClusterBy  | 聚类布局依据的节点数据 `data` 中的字段名，在 `clustering` 为 `true` 时生效 | `string`  | `'cluster'` |      |\n| clusterGravity | 聚类内部的重力大小，影响聚类的紧凑程度，在 `clustering` 为 `true` 时生效   | `number`  | 10          |      |\n\n## 示例代码\n\n### 基本布局\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 500,\n  height: 250,\n  data: {\n    nodes: [\n      { id: '0' },\n      { id: '1' },\n      { id: '2' },\n      { id: '3' },\n      { id: '4' },\n      { id: '5' },\n      { id: '6' },\n      { id: '7' },\n      { id: '8' },\n      { id: '9' },\n      { id: '10' },\n    ],\n    edges: [\n      { source: '0', target: '1' },\n      { source: '0', target: '2' },\n      { source: '0', target: '3' },\n      { source: '0', target: '4' },\n      { source: '0', target: '7' },\n      { source: '0', target: '8' },\n      { source: '0', target: '9' },\n      { source: '0', target: '10' },\n      { source: '2', target: '3' },\n      { source: '4', target: '5' },\n      { source: '4', target: '6' },\n      { source: '5', target: '6' },\n      { source: '9', target: '10' },\n    ],\n  },\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n  },\n  layout: {\n    type: 'fruchterman',\n    gravity: 5,\n    speed: 5,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n<details><summary>展开查看完整代码</summary>\n\n```javascript\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0' },\n    { id: '1' },\n    { id: '2' },\n    { id: '3' },\n    { id: '4' },\n    { id: '5' },\n    { id: '6' },\n    { id: '7' },\n    { id: '8' },\n    { id: '9' },\n    { id: '10' },\n  ],\n  edges: [\n    { source: '0', target: '1' },\n    { source: '0', target: '2' },\n    { source: '0', target: '3' },\n    { source: '0', target: '4' },\n    { source: '0', target: '7' },\n    { source: '0', target: '8' },\n    { source: '0', target: '9' },\n    { source: '0', target: '10' },\n    { source: '2', target: '3' },\n    { source: '4', target: '5' },\n    { source: '4', target: '6' },\n    { source: '5', target: '6' },\n    { source: '9', target: '10' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n  },\n  layout: {\n    type: 'fruchterman',\n    gravity: 5,\n    speed: 5,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n</details>\n\n### 聚类布局\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 500,\n  height: 250,\n  data: {\n    nodes: [\n      { id: '0', data: { cluster: 'a' } },\n      { id: '1', data: { cluster: 'a' } },\n      { id: '2', data: { cluster: 'a' } },\n      { id: '3', data: { cluster: 'a' } },\n      { id: '4', data: { cluster: 'a' } },\n      { id: '5', data: { cluster: 'b' } },\n      { id: '6', data: { cluster: 'b' } },\n      { id: '7', data: { cluster: 'b' } },\n      { id: '8', data: { cluster: 'c' } },\n      { id: '9', data: { cluster: 'c' } },\n      { id: '10', data: { cluster: 'c' } },\n    ],\n    edges: [\n      { source: '0', target: '1' },\n      { source: '0', target: '2' },\n      { source: '0', target: '4' },\n      { source: '0', target: '6' },\n      { source: '2', target: '3' },\n      { source: '2', target: '4' },\n      { source: '3', target: '4' },\n      { source: '5', target: '6' },\n      { source: '6', target: '7' },\n      { source: '7', target: '8' },\n      { source: '8', target: '9' },\n      { source: '8', target: '10' },\n    ],\n  },\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => `${d.data.cluster}-${d.id}`,\n    },\n    palette: {\n      type: 'group',\n      field: 'cluster',\n    },\n  },\n  edge: {\n    style: {\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'fruchterman',\n    gravity: 6,\n    speed: 5,\n\n    // 聚类布局参数\n    clustering: true,\n    nodeClusterBy: 'cluster',\n    clusterGravity: 3,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n<details><summary>展开查看完整代码</summary>\n\n```javascript\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0', data: { cluster: 'a' } },\n    { id: '1', data: { cluster: 'a' } },\n    { id: '2', data: { cluster: 'a' } },\n    { id: '3', data: { cluster: 'a' } },\n    { id: '4', data: { cluster: 'a' } },\n    { id: '5', data: { cluster: 'b' } },\n    { id: '6', data: { cluster: 'b' } },\n    { id: '7', data: { cluster: 'b' } },\n    { id: '8', data: { cluster: 'c' } },\n    { id: '9', data: { cluster: 'c' } },\n    { id: '10', data: { cluster: 'c' } },\n  ],\n  edges: [\n    { source: '0', target: '1' },\n    { source: '0', target: '2' },\n    { source: '0', target: '4' },\n    { source: '0', target: '6' },\n    { source: '2', target: '3' },\n    { source: '2', target: '4' },\n    { source: '3', target: '4' },\n    { source: '5', target: '6' },\n    { source: '6', target: '7' },\n    { source: '7', target: '8' },\n    { source: '8', target: '9' },\n    { source: '8', target: '10' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => `${d.data.cluster}-${d.id}`,\n    },\n    palette: {\n      type: 'group',\n      field: 'cluster',\n    },\n  },\n  edge: {\n    style: {\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'fruchterman',\n    gravity: 6,\n    speed: 5,\n\n    // 聚类布局参数\n    clustering: true,\n    nodeClusterBy: 'cluster',\n    clusterGravity: 3,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n</details>\n"
  },
  {
    "path": "packages/site/docs/manual/layout/GridLayout.en.md",
    "content": "---\ntitle: Grid Layout\norder: 15\n---\n\n## Overview\n\nThe grid layout arranges nodes in a grid pattern, suitable for scenarios where nodes need to be arranged neatly. This layout supports automatic calculation of the number of rows and columns, or you can specify them manually. It also supports preventing node overlap.\n\n## Use Cases\n\n- Visualizing data in a matrix or table format\n\n## Online Demo\n\n```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: Array.from({ length: 25 }, (_, i) => ({\n        id: `node-${i}`,\n        data: {\n          value: Math.random() * 100,\n        },\n      })),\n      edges: Array.from({ length: 20 }, (_, i) => ({\n        id: `edge-${i}`,\n        source: `node-${Math.floor(Math.random() * 25)}`,\n        target: `node-${Math.floor(Math.random() * 25)}`,\n      })),\n    },\n    autoFit: 'view',\n    node: {\n      style: {\n        size: 20,\n        label: true,\n        labelText: (datum) => datum.id,\n        labelBackground: true,\n        icon: false,\n      },\n      palette: {\n        type: 'group',\n        field: (datum) => datum.data.value,\n        color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n      },\n    },\n    edge: {\n      style: {\n        stroke: '#bfbfbf',\n      },\n    },\n    behaviors: ['drag-canvas'],\n    layout: {\n      type: 'grid',\n      cols: 5,\n      rows: 5,\n      width: 400,\n      height: 400,\n      preventOverlap: true,\n      nodeSize: 30,\n      condense: false,\n    },\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const options = {\n      type: 'grid',\n      cols: 5,\n      rows: 5,\n      width: 400,\n      height: 400,\n      preventOverlap: true,\n      nodeSize: 30,\n      condense: false,\n    };\n\n    const optionFolder = gui.addFolder('Grid Layout Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'cols', 2, 10, 1);\n    optionFolder.add(options, 'rows', 2, 10, 1);\n    optionFolder.add(options, 'width', 200, 600, 50);\n    optionFolder.add(options, 'height', 200, 600, 50);\n    optionFolder.add(options, 'preventOverlap');\n    optionFolder.add(options, 'nodeSize', 10, 50, 5);\n    optionFolder.add(options, 'condense');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.setLayout({\n        type: 'grid',\n        [property]: value,\n      });\n      graph.layout();\n    });\n  },\n);\n```\n\n## Configuration\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'grid',\n    begin: [0, 0],\n    cols: 5,\n    rows: 5,\n    width: 300,\n    height: 300,\n    preventOverlap: true,\n    nodeSize: 30,\n    condense: false,\n  },\n});\n```\n\n## Options\n\n| Property              | Description                                                                                                                     | Type                                             | Default   | Required |\n| --------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | --------- | -------- |\n| type                  | Layout type                                                                                                                     | `grid`                                           | -         | ✓        |\n| begin                 | Grid start position (top-left corner), default is `[0, 0]`                                                                      | [number, number]                                 | [0, 0]    |          |\n| cols                  | Number of columns. If undefined, the algorithm calculates it automatically based on node count, layout space, and rows (if set) | number                                           | undefined |          |\n| rows                  | Number of rows. If undefined, the algorithm calculates it automatically based on node count, layout space, and cols (if set)    | number                                           | 10        |          |\n| width                 | Layout area width. In G6, the container width is used as the default value                                                      | number                                           | 300       |          |\n| height                | Layout area height. In G6, the container height is used as the default value                                                    | number                                           | 300       |          |\n| condense              | If false, uses all available canvas space; if true, uses the minimum canvas space                                               | boolean                                          | false     |          |\n| nodeSize              | Node size (diameter), used for collision detection when preventing overlap                                                      | Size \\| ((nodeData: Node) => Size)               | -         |          |\n| nodeSpacing           | Node spacing, used to adjust the gap between nodes                                                                              | ((node?: Node) => number) \\| number              | -         |          |\n| position              | Specify the row and column for each node                                                                                        | (node?: Node) => { row?: number; col?: number; } | undefined |          |\n| preventOverlap        | Whether to prevent node overlap. Requires nodeSize or size property in node data                                                | boolean                                          | false     |          |\n| preventOverlapPadding | Padding when preventing overlap. Effective when preventOverlap is true                                                          | number                                           | 10        |          |\n| sortBy                | Sort basis (node property name). Higher values are placed more centrally. If undefined, degree is used for sorting              | string                                           | undefined |          |\n\n### preventOverlap\n\n> _boolean_ **Default:** `false`\n\nWhether to prevent overlap\n\nMust be used with nodeSize or the size property in node data. Only when data has data.size or nodeSize is set in the layout, collision detection for node overlap can be performed.\n\n### preventOverlapPadding\n\n> _number_ **Default:** `10`\n\nPadding when preventing overlap. Effective when preventOverlap is true.\n\n### sortBy\n\n> _string_ **Default:** `undefined`\n\nSort basis (node property name). Higher values are placed more centrally. If undefined, degree is used for sorting. In G6, the container width is used as the default value for grid layout width. When used alone, the default is 300.\n\n## Code Examples\n\n### Basic Usage\n\nThe simplest configuration:\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'grid',\n    cols: 5,\n    rows: 5,\n  },\n  data: {\n    nodes: Array.from({ length: 25 }, (_, i) => ({\n      id: `node-${i}`,\n      data: {\n        value: Math.random() * 100,\n      },\n    })),\n    edges: Array.from({ length: 20 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 25)}`,\n      target: `node-${Math.floor(Math.random() * 25)}`,\n    })),\n  },\n});\n```\n\nResult:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 400,\n  layout: {\n    type: 'grid',\n    cols: 5,\n    rows: 5,\n  },\n  data: {\n    nodes: Array.from({ length: 25 }, (_, i) => ({\n      id: `node-${i}`,\n      data: {\n        value: Math.random() * 100,\n      },\n    })),\n    edges: Array.from({ length: 20 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 25)}`,\n      target: `node-${Math.floor(Math.random() * 25)}`,\n    })),\n  },\n  node: {\n    style: {\n      size: 20,\n      label: true,\n      labelText: (datum) => datum.id,\n      labelBackground: true,\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#bfbfbf',\n    },\n  },\n});\n\ngraph.render();\n```\n\n### Custom Configuration\n\nYou can customize the grid layout in various ways:\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'grid',\n    begin: [50, 50], // Start layout from [50, 50]\n    cols: 4, // 4 columns\n    rows: 6, // 6 rows\n    width: 400, // Layout area width\n    height: 600, // Layout area height\n    preventOverlap: true, // Prevent node overlap\n    nodeSize: 30, // Node size\n    condense: true, // Use minimum space\n    sortBy: 'value', // Sort by value property\n  },\n  data: {\n    nodes: Array.from({ length: 24 }, (_, i) => ({\n      id: `node-${i}`,\n      data: {\n        value: Math.random() * 100, // Property for sorting\n      },\n    })),\n    edges: Array.from({ length: 20 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 24)}`,\n      target: `node-${Math.floor(Math.random() * 24)}`,\n    })),\n  },\n});\n```\n\nResult:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 400,\n  layout: {\n    type: 'grid',\n    begin: [50, 50],\n    cols: 4,\n    rows: 6,\n    width: 400,\n    height: 600,\n    preventOverlap: true,\n    nodeSize: 30,\n    condense: true,\n    sortBy: 'value',\n  },\n  data: {\n    nodes: Array.from({ length: 24 }, (_, i) => ({\n      id: `node-${i}`,\n      data: {\n        value: Math.random() * 100,\n      },\n    })),\n    edges: Array.from({ length: 20 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 24)}`,\n      target: `node-${Math.floor(Math.random() * 24)}`,\n    })),\n  },\n  node: {\n    style: {\n      size: 20,\n      label: true,\n      labelText: (datum) => datum.id,\n      labelBackground: true,\n    },\n    palette: {\n      type: 'group',\n      field: (datum) => datum.data.value,\n      color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#bfbfbf',\n    },\n  },\n});\n\ngraph.render();\n```\n\n### Specify Node Position\n\nYou can specify the position for specific nodes using the `position` property:\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'grid',\n    cols: 5,\n    rows: 5,\n    position: (node) => {\n      // Specify position for specific nodes\n      if (node.id === 'node-0') return { row: 0, col: 0 }; // Top-left\n      if (node.id === 'node-1') return { row: 0, col: 4 }; // Top-right\n      if (node.id === 'node-2') return { row: 4, col: 0 }; // Bottom-left\n      if (node.id === 'node-3') return { row: 4, col: 4 }; // Bottom-right\n      return undefined; // Other nodes are auto-arranged\n    },\n  },\n  data: {\n    nodes: Array.from({ length: 25 }, (_, i) => ({\n      id: `node-${i}`,\n    })),\n    edges: Array.from({ length: 20 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 25)}`,\n      target: `node-${Math.floor(Math.random() * 25)}`,\n    })),\n  },\n});\n```\n\nResult:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 400,\n  layout: {\n    type: 'grid',\n    cols: 5,\n    rows: 5,\n    position: (node) => {\n      if (node.id === 'node-0') return { row: 0, col: 0 };\n      if (node.id === 'node-1') return { row: 0, col: 4 };\n      if (node.id === 'node-2') return { row: 4, col: 0 };\n      if (node.id === 'node-3') return { row: 4, col: 4 };\n      return undefined;\n    },\n  },\n  data: {\n    nodes: Array.from({ length: 25 }, (_, i) => ({\n      id: `node-${i}`,\n    })),\n    edges: Array.from({ length: 20 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 25)}`,\n      target: `node-${Math.floor(Math.random() * 25)}`,\n    })),\n  },\n  node: {\n    style: {\n      size: 20,\n      label: true,\n      labelText: (datum) => datum.id,\n      labelBackground: true,\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#bfbfbf',\n    },\n  },\n});\n\ngraph.render();\n```\n\n## Real Cases\n\n- [Grid Layout](/en/examples/layout/grid/#basic)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/GridLayout.zh.md",
    "content": "---\ntitle: 网格布局 Grid\norder: 15\n---\n\n## 概述\n\n网格布局将节点按照网格形式排列，适用于需要整齐排列节点的场景。该布局支持自动计算行列数，也可以手动指定行列数，并支持防止节点重叠。\n\n## 使用场景\n\n- 在数据可视化中需要展示矩阵或表格形式的数据关系\n\n## 在线体验\n\n```js | ob { pin: false }\ncreateGraph(\n  {\n    data: {\n      nodes: Array.from({ length: 25 }, (_, i) => ({\n        id: `node-${i}`,\n        data: {\n          value: Math.random() * 100,\n        },\n      })),\n      edges: Array.from({ length: 20 }, (_, i) => ({\n        id: `edge-${i}`,\n        source: `node-${Math.floor(Math.random() * 25)}`,\n        target: `node-${Math.floor(Math.random() * 25)}`,\n      })),\n    },\n    autoFit: 'view',\n    node: {\n      style: {\n        size: 20,\n        label: true,\n        labelText: (datum) => datum.id,\n        labelBackground: true,\n        icon: false,\n      },\n      palette: {\n        type: 'group',\n        field: (datum) => datum.data.value,\n        color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n      },\n    },\n    edge: {\n      style: {\n        stroke: '#bfbfbf',\n      },\n    },\n    behaviors: ['drag-canvas'],\n    layout: {\n      type: 'grid',\n      cols: 5,\n      rows: 5,\n      width: 400,\n      height: 400,\n      preventOverlap: true,\n      nodeSize: 30,\n      condense: false,\n    },\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    const options = {\n      type: 'grid',\n      cols: 5,\n      rows: 5,\n      width: 400,\n      height: 400,\n      preventOverlap: true,\n      nodeSize: 30,\n      condense: false,\n    };\n\n    const optionFolder = gui.addFolder('Grid Layout Options');\n    optionFolder.add(options, 'type').disable(true);\n    optionFolder.add(options, 'cols', 2, 10, 1);\n    optionFolder.add(options, 'rows', 2, 10, 1);\n    optionFolder.add(options, 'width', 200, 600, 50);\n    optionFolder.add(options, 'height', 200, 600, 50);\n    optionFolder.add(options, 'preventOverlap');\n    optionFolder.add(options, 'nodeSize', 10, 50, 5);\n    optionFolder.add(options, 'condense');\n\n    optionFolder.onChange(({ property, value }) => {\n      graph.setLayout({\n        type: 'grid',\n        [property]: value,\n      });\n      graph.layout();\n    });\n  },\n);\n```\n\n## 配置方式\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'grid',\n    begin: [0, 0],\n    cols: 5,\n    rows: 5,\n    width: 300,\n    height: 300,\n    preventOverlap: true,\n    nodeSize: 30,\n    condense: false,\n  },\n});\n```\n\n## 配置项\n\n| 属性                  | 描述                                                                                                                             | 类型                                             | 默认值    | 必选 |\n| --------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | --------- | ---- |\n| type                  | 布局类型                                                                                                                         | `grid`                                           | -         | ✓    |\n| begin                 | 网格开始位置（左上角），默认为 `[0, 0]`                                                                                          | [number, number]                                 | [0, 0]    |      |\n| cols                  | 网格的列数，为 undefined 时算法根据节点数量、布局空间、rows（若指定）自动计算                                                    | number                                           | undefined |      |\n| rows                  | 网格的行数，为 undefined 时算法根据节点数量、布局空间、cols（若指定）自动计算                                                    | number                                           | 10        |      |\n| width                 | 布局区域宽度，在 G6 中使用当前容器的宽度作为默认值                                                                               | number                                           | 300       |      |\n| height                | 布局区域高度，在 G6 中使用当前容器的高度作为默认值                                                                               | number                                           | 300       |      |\n| condense              | 为 false 时表示利用所有可用画布空间，为 true 时表示利用最小的画布空间                                                            | boolean                                          | false     |      |\n| nodeSize              | 节点大小（直径），用于防止节点重叠时的碰撞检测                                                                                   | Size \\| ((nodeData: Node) => Size)               | -         |      |\n| nodeSpacing           | 节点间距，用于调整节点之间的间隔                                                                                                 | ((node?: Node) => number) \\| number              | -         |      |\n| position              | 指定每个节点所在的行和列                                                                                                         | (node?: Node) => { row?: number; col?: number; } | undefined |      |\n| preventOverlap        | 是否防止节点重叠，需要配合 nodeSize 或节点数据中的 size 属性使用                                                                 | boolean                                          | false     |      |\n| preventOverlapPadding | 避免重叠时节点的间距 padding，preventOverlap 为 true 时生效                                                                      | number                                           | 10        |      |\n| sortBy                | 指定排序的依据（节点属性名），数值越高则该节点被放置得越中心。若为 undefined，则会计算节点的度数，度数越高，节点将被放置得越中心 | string                                           | undefined |      |\n\n### preventOverlap\n\n> _boolean_ **Default:** `false`\n\n是否防止重叠\n\n必须配合下面属性 nodeSize 或节点数据中的 data.size 属性，只有在数据中设置了 data.size 或在该布局中配置了与当前图节点大小相同的 nodeSize 值，才能够进行节点重叠的碰撞检测\n\n### preventOverlapPadding\n\n> _number_ **Default:** `10`\n\n避免重叠时节点的间距 padding，preventOverlap 为 true 时生效\n\n### sortBy\n\n> _string_ **Default:** `undefined`\n\n指定排序的依据（节点属性名），数值越高则该节点被放置得越中心。若为 undefined，则会计算节点的度数，度数越高，节点将被放置得越中心\n\n在 G6 中使用当前容器的宽度作为 grid 布局 width 的默认值。单独使用此布局时默认值为 300\n\n## 代码示例\n\n### 基础用法\n\n最简单的配置方式：\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'grid',\n    cols: 5,\n    rows: 5,\n  },\n  data: {\n    nodes: Array.from({ length: 25 }, (_, i) => ({\n      id: `node-${i}`,\n      data: {\n        value: Math.random() * 100,\n      },\n    })),\n    edges: Array.from({ length: 20 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 25)}`,\n      target: `node-${Math.floor(Math.random() * 25)}`,\n    })),\n  },\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 400,\n  layout: {\n    type: 'grid',\n    cols: 5,\n    rows: 5,\n  },\n  data: {\n    nodes: Array.from({ length: 25 }, (_, i) => ({\n      id: `node-${i}`,\n      data: {\n        value: Math.random() * 100,\n      },\n    })),\n    edges: Array.from({ length: 20 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 25)}`,\n      target: `node-${Math.floor(Math.random() * 25)}`,\n    })),\n  },\n  node: {\n    style: {\n      size: 20,\n      label: true,\n      labelText: (datum) => datum.id,\n      labelBackground: true,\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#bfbfbf',\n    },\n  },\n});\n\ngraph.render();\n```\n\n### 自定义配置\n\n可以通过多种方式自定义网格布局：\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'grid',\n    begin: [50, 50], // 从坐标 [50, 50] 开始布局\n    cols: 4, // 指定 4 列\n    rows: 6, // 指定 6 行\n    width: 400, // 布局区域宽度\n    height: 600, // 布局区域高度\n    preventOverlap: true, // 防止节点重叠\n    nodeSize: 30, // 节点大小\n    condense: true, // 使用最小空间\n    sortBy: 'value', // 按 value 属性排序\n  },\n  data: {\n    nodes: Array.from({ length: 24 }, (_, i) => ({\n      id: `node-${i}`,\n      data: {\n        value: Math.random() * 100, // 用于排序的属性\n      },\n    })),\n    edges: Array.from({ length: 20 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 24)}`,\n      target: `node-${Math.floor(Math.random() * 24)}`,\n    })),\n  },\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 400,\n  layout: {\n    type: 'grid',\n    begin: [50, 50],\n    cols: 4,\n    rows: 6,\n    width: 400,\n    height: 600,\n    preventOverlap: true,\n    nodeSize: 30,\n    condense: true,\n    sortBy: 'value',\n  },\n  data: {\n    nodes: Array.from({ length: 24 }, (_, i) => ({\n      id: `node-${i}`,\n      data: {\n        value: Math.random() * 100,\n      },\n    })),\n    edges: Array.from({ length: 20 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 24)}`,\n      target: `node-${Math.floor(Math.random() * 24)}`,\n    })),\n  },\n  node: {\n    style: {\n      size: 20,\n      label: true,\n      labelText: (datum) => datum.id,\n      labelBackground: true,\n    },\n    palette: {\n      type: 'group',\n      field: (datum) => datum.data.value,\n      color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#bfbfbf',\n    },\n  },\n});\n\ngraph.render();\n```\n\n### 指定节点位置\n\n可以通过 `position` 属性为特定节点指定位置：\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'grid',\n    cols: 5,\n    rows: 5,\n    position: (node) => {\n      // 为特定节点指定位置\n      if (node.id === 'node-0') return { row: 0, col: 0 }; // 左上角\n      if (node.id === 'node-1') return { row: 0, col: 4 }; // 右上角\n      if (node.id === 'node-2') return { row: 4, col: 0 }; // 左下角\n      if (node.id === 'node-3') return { row: 4, col: 4 }; // 右下角\n      return undefined; // 其他节点自动布局\n    },\n  },\n  data: {\n    nodes: Array.from({ length: 25 }, (_, i) => ({\n      id: `node-${i}`,\n    })),\n    edges: Array.from({ length: 20 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 25)}`,\n      target: `node-${Math.floor(Math.random() * 25)}`,\n    })),\n  },\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 400,\n  layout: {\n    type: 'grid',\n    cols: 5,\n    rows: 5,\n    position: (node) => {\n      if (node.id === 'node-0') return { row: 0, col: 0 };\n      if (node.id === 'node-1') return { row: 0, col: 4 };\n      if (node.id === 'node-2') return { row: 4, col: 0 };\n      if (node.id === 'node-3') return { row: 4, col: 4 };\n      return undefined;\n    },\n  },\n  data: {\n    nodes: Array.from({ length: 25 }, (_, i) => ({\n      id: `node-${i}`,\n    })),\n    edges: Array.from({ length: 20 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 25)}`,\n      target: `node-${Math.floor(Math.random() * 25)}`,\n    })),\n  },\n  node: {\n    style: {\n      size: 20,\n      label: true,\n      labelText: (datum) => datum.id,\n      labelBackground: true,\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#bfbfbf',\n    },\n  },\n});\n\ngraph.render();\n```\n\n## 实际案例\n\n- [Grid布局](/examples/layout/grid/#basic)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/IndentedLayout.en.md",
    "content": "---\ntitle: Indented Tree\norder: 16\n---\n\n# Indented Tree Layout\n\n## Overview\n\nIndented tree layout represents the hierarchy of tree nodes through indentation in the horizontal direction. Each element occupies a row or column, commonly used in file directory structures, organizational charts, and other scenarios. This layout provides a clear structure for displaying hierarchical relationships.\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*NBUzRonaOYMAAAAAAAAAAABkARQnAQ' width=175 alt='Indented Tree Layout'/>\n\n## Use Cases\n\n- File directory structure visualization\n- Organizational charts\n- Classification system display\n- Tree-like data where hierarchical relationships need to be emphasized\n\n## Configuration Items\n\n> IndentedLayout supports common layout configuration items and specific configuration items, as shown below.\n\n| Property               | Description                                                         | Type                            | Default    | Required |\n| ---------------------- | ------------------------------------------------------------------- | ------------------------------- | ---------- | -------- |\n| type                   | Layout type, must be 'indented'                                     | 'indented'                      | -          | ✓        |\n| direction              | Layout direction, see details below                                 | 'LR' \\| 'RL' \\| 'H'             | 'LR'       |          |\n| indent                 | Column spacing, fixed value or function                             | number \\| (d?: Node) => number  | 20         |          |\n| getWidth               | Get each node's width, effective when direction='H'                 | (d?: Node) => number            | -          |          |\n| getHeight              | Get each node's height                                              | (d?: Node) => number            | -          |          |\n| getSide                | Node placement on left/right side of root, overrides direction='H'  | (d?: Node) => 'left' \\| 'right' | -          |          |\n| dropCap                | Whether the first child of each node starts on the next line        | boolean                         | true       |          |\n| isLayoutInvisibleNodes | Whether invisible nodes participate in layout (when preLayout=true) | boolean                         | false      |          |\n| nodeFilter             | Nodes participating in this layout                                  | (node: NodeData) => boolean     | () => true |          |\n| preLayout              | Use pre-layout, calculate layout before initializing elements       | boolean                         | false      |          |\n| enableWorker           | Whether to run layout in WebWorker                                  | boolean                         | -          |          |\n| iterations             | Number of iterations for iterative layout                           | number                          | -          |          |\n\n### Complex Type Explanations\n\n- **direction**\n\n  - `'LR'`: Root node on the left, layout to the right\n    <img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*mq6YSIKrAt0AAAAAAAAAAABkARQnAQ' width=110 alt='LR'/>\n  - `'RL'`: Root node on the right, layout to the left\n    <img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*VGEnRbpvxlUAAAAAAAAAAABkARQnAQ' width=90 alt='RL'/>\n  - `'H'`: Root node in the middle, horizontal symmetric layout\n    <img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*Vek6RqtUXNcAAAAAAAAAAABkARQnAQ' width=160 alt='H'/>\n\n- **indent**\n\n  - Fixed value: Consistent indentation for all levels\n  - Function: (d?: Node) => number, customize indentation based on node\n  - Example:\n    ```js\n    (d) => {\n      if (d.parent?.id === 'testId') return d.parent.x + 50;\n      return 100;\n    };\n    ```\n\n- **getWidth/getHeight**\n\n  - Used to customize each node's width/height, often for content adaptation\n  - Example:\n    ```js\n    (d) => (d.id === 'testId' ? 50 : 100);\n    ```\n\n- **getSide**\n  - Specifies which side of the root node a node should be placed, only effective when direction='H'\n  - Example:\n    ```js\n    (d) => (d.id === 'testId' ? 'left' : 'right');\n    ```\n\n## Example Code\n\n> For more examples, see [Online Demo](https://g6.antv.antgroup.com/en/examples/layout/indented)\n\n### Automatic Child Node Distribution\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Kc63QoxgLNYAAAAAAAAAAAAADmJ7AQ/original\" width=\"300\" />\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data: treeToGraphData(data),\n      autoFit: 'view',\n      layout: {\n        type: 'indented',\n        direction: 'H',\n        indent: 80,\n        getHeight: () => 16,\n        getWidth: () => 32,\n      },\n    });\n    graph.render();\n  });\n```\n\n### Right Side Child Node Distribution\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*3PioQ4TAMx8AAAAAAAAAAAAADmJ7AQ/original\" width=\"300\" />\n\n```js\n// ... code as above, layout.direction: 'LR'\n```\n\n### Left Side Child Node Distribution\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*o6uzQ5nmXJkAAAAAAAAAAAAADmJ7AQ/original\" width=\"300\" />\n\n```js\n// ... code as above, layout.direction: 'RL'\n```\n\n### Custom Child Node Distribution\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Kc63QoxgLNYAAAAAAAAAAAAADmJ7AQ/original\" width=\"300\" />\n\n```js\nlayout: {\n  type: 'indented',\n  direction: 'H',\n  indent: 80,\n  getHeight: () => 16,\n  getWidth: () => 32,\n  getSide: (d) => {\n    if (d.id === 'Regression' || d.id === 'Classification') return 'left';\n    return 'right';\n  },\n}\n```\n\n### No Line Break for First Child Node\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*bC-pRrO7srwAAAAAAAAAAAAADmJ7AQ/original\" width=\"300\" />\n\n```js\nlayout: {\n  type: 'indented',\n  direction: 'LR',\n  indent: 80,\n  getHeight: () => 16,\n  getWidth: () => 32,\n  dropCap: false,\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/IndentedLayout.zh.md",
    "content": "---\ntitle: 缩进树 Indented\norder: 16\n---\n\n## 概述\n\nIndented（缩进树）布局是一种通过水平方向的缩进量来表示树节点层级的布局方式。每个元素占据一行或一列，常用于文件目录结构、组织架构等场景。该布局结构清晰，便于展示层级关系。\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*NBUzRonaOYMAAAAAAAAAAABkARQnAQ' width=175 alt='Indented 缩进树布局示意图'/>\n\n## 使用场景\n\n- 文件目录结构可视化\n- 组织架构树\n- 分类体系展示\n- 需要突出层级关系的树状数据\n\n## 配置项\n\n> IndentedLayout 支持通用布局配置项和专有配置项，详见下表。\n\n| 属性                   | 描述                                                 | 类型                            | 默认值     | 必选 |\n| ---------------------- | ---------------------------------------------------- | ------------------------------- | ---------- | ---- |\n| type                   | 布局类型，需为 'indented'                            | 'indented'                      | -          | ✓    |\n| direction              | 布局方向，根节点在左/右/中间，详见下方说明           | 'LR' \\| 'RL' \\| 'H'             | 'LR'       |      |\n| indent                 | 列间间距，支持固定值或函数                           | number \\| (d?: Node) => number  | 20         |      |\n| getWidth               | 获取每个节点宽度，仅 direction='H' 时生效            | (d?: Node) => number            | -          |      |\n| getHeight              | 获取每个节点高度                                     | (d?: Node) => number            | -          |      |\n| getSide                | 节点排布在根节点的左/右侧，设置后 direction='H' 失效 | (d?: Node) => 'left' \\| 'right' | -          |      |\n| dropCap                | 每个节点的第一个子节点是否换行                       | boolean                         | true       |      |\n| isLayoutInvisibleNodes | 不可见节点是否参与布局（preLayout=true 时生效）      | boolean                         | false      |      |\n| nodeFilter             | 参与该布局的节点                                     | (node: NodeData) => boolean     | () => true |      |\n| preLayout              | 使用前布局，在初始化元素前计算布局                   | boolean                         | false      |      |\n| enableWorker           | 是否在 WebWorker 中运行布局                          | boolean                         | -          |      |\n| iterations             | 迭代布局的迭代次数                                   | number                          | -          |      |\n\n### 复杂类型说明\n\n- **direction**\n\n  - `'LR'`：根节点在左，向右布局\n    <img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*mq6YSIKrAt0AAAAAAAAAAABkARQnAQ' width=110 alt='LR'/>\n  - `'RL'`：根节点在右，向左布局\n    <img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*VGEnRbpvxlUAAAAAAAAAAABkARQnAQ' width=90 alt='RL'/>\n  - `'H'`：根节点在中间，水平对称布局\n    <img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*Vek6RqtUXNcAAAAAAAAAAABkARQnAQ' width=160 alt='H'/>\n\n- **indent**\n\n  - 固定数值：所有层级缩进一致\n  - 函数：(d?: Node) => number，可根据节点自定义缩进\n  - 示例：\n    ```js\n    (d) => {\n      if (d.parent?.id === 'testId') return d.parent.x + 50;\n      return 100;\n    };\n    ```\n\n- **getWidth/getHeight**\n\n  - 用于自定义每个节点的宽度/高度，常用于自适应内容\n  - 示例：\n    ```js\n    (d) => (d.id === 'testId' ? 50 : 100);\n    ```\n\n- **getSide**\n  - 指定节点在根节点的哪一侧，仅 direction='H' 时生效\n  - 示例：\n    ```js\n    (d) => (d.id === 'testId' ? 'left' : 'right');\n    ```\n\n## 示例代码\n\n> 更多示例可参考 [在线 Demo](https://g6.antv.antgroup.com/examples/layout/indented)\n\n### 子节点自动分布\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Kc63QoxgLNYAAAAAAAAAAAAADmJ7AQ/original\" width=\"300\" />\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data: treeToGraphData(data),\n      autoFit: 'view',\n      layout: {\n        type: 'indented',\n        direction: 'H',\n        indent: 80,\n        getHeight: () => 16,\n        getWidth: () => 32,\n      },\n    });\n    graph.render();\n  });\n```\n\n### 子节点右侧分布\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*3PioQ4TAMx8AAAAAAAAAAAAADmJ7AQ/original\" width=\"300\" />\n\n```js\n// ... 代码同上，layout.direction: 'LR'\n```\n\n### 子节点左侧分布\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*o6uzQ5nmXJkAAAAAAAAAAAAADmJ7AQ/original\" width=\"300\" />\n\n```js\n// ... 代码同上，layout.direction: 'RL'\n```\n\n### 自定义子节点分布\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Kc63QoxgLNYAAAAAAAAAAAAADmJ7AQ/original\" width=\"300\" />\n\n```js\nlayout: {\n  type: 'indented',\n  direction: 'H',\n  indent: 80,\n  getHeight: () => 16,\n  getWidth: () => 32,\n  getSide: (d) => {\n    if (d.id === 'Regression' || d.id === 'Classification') return 'left';\n    return 'right';\n  },\n}\n```\n\n### 首子节点不换行\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*bC-pRrO7srwAAAAAAAAAAAAADmJ7AQ/original\" width=\"300\" />\n\n```js\nlayout: {\n  type: 'indented',\n  direction: 'LR',\n  indent: 80,\n  getHeight: () => 16,\n  getWidth: () => 32,\n  dropCap: false,\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/MdsLayout.en.md",
    "content": "---\ntitle: MDS High-dimensional Data Dimensionality Reduction Layout\norder: 17\n---\n\n# MDS High-dimensional Data Dimensionality Reduction Layout\n\n## Overview\n\nMDS (Multidimensional Scaling) is a classic dimensionality reduction algorithm. In G6, the MDS layout constructs a distance matrix between nodes and restores their relative distances in high-dimensional space as much as possible in 2D space. It is suitable for graph visualization scenarios that show similarity, distance, or structural relationships between nodes.\n\n## Use Cases\n\n- Data dimensionality reduction visualization\n- Displaying distance relationships between nodes\n\n## Configuration\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'mds',\n    center: [300, 300],\n    linkDistance: 100,\n  },\n});\n```\n\n## Options\n\n| Option           | Description                            | Type              | Default | Required |\n| :--------------- | :------------------------------------- | :---------------- | :------ | :------- |\n| **type**         | Layout type                            | `mds`             | -       | Yes      |\n| **center**       | Center position of the circular layout | `[number,number]` | [0,0]   | No       |\n| **linkDistance** | Ideal length of edges (spring length)  | `number`          | 50      | No       |\n\n**center**\n\nThe center coordinates of the layout. All nodes will be symmetrically distributed around this point.\n\n**linkDistance**\n\n> number Default: 50\n\nThe ideal distance between nodes. The larger the value, the more dispersed the nodes.\n\n## Code Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: Array.from({ length: 25 }, (_, i) => ({\n    id: `node-${i}`,\n    data: {\n      value: Math.random() * 100,\n    },\n  })),\n  edges: Array.from({ length: 20 }, (_, i) => ({\n    id: `edge-${i}`,\n    source: `node-${Math.floor(Math.random() * 25)}`,\n    target: `node-${Math.floor(Math.random() * 25)}`,\n  })),\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  autoFit: 'view',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  layout: {\n    type: 'mds',\n    nodeSize: 32,\n    linkDistance: 100,\n  },\n  behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/MdsLayout.zh.md",
    "content": "---\ntitle: 高维数据降维布局 MDS\norder: 17\n---\n\n## 概述\n\nMDS（多维尺度分析，Multidimensional Scaling）是一种经典的降维算法。在 G6 中，MDS 布局通过构造节点间的距离矩阵，在二维空间中尽可能还原它们在高维空间中的相对距离。适用于展示节点之间相似度、距离或结构关系的图可视化场景。\n\n## 使用场景\n\n- 数据降维可视化\n- 展示节点之间的距离关系\n\n## 配置方式\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'mds',\n    center: [300, 300],\n    linkDistance: 100,\n  },\n});\n```\n\n## 配置项\n\n| 配置项           | 描述                               | 类型              | 默认值 | 必选 |\n| :--------------- | :--------------------------------- | :---------------- | :----- | :--- |\n| **type**         | 布局类型                           | `mds`             | -      | 是   |\n| **center**       | 圆形布局的中心位置                 | `[number,number]` | [0,0]  | 否   |\n| **linkDistance** | 边的理想长度（弹簧未受力时的长度） | `number`          | 50     | 否   |\n\n**center**\n\n布局的中心点坐标，所有节点会围绕该点对称分布。\n\n**linkDistance**\n\n> number Default: 50\n\n节点之间的理想距离，越大则节点间距离越分散。\n\n## 代码示例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: Array.from({ length: 25 }, (_, i) => ({\n    id: `node-${i}`,\n    data: {\n      value: Math.random() * 100,\n    },\n  })),\n  edges: Array.from({ length: 20 }, (_, i) => ({\n    id: `edge-${i}`,\n    source: `node-${Math.floor(Math.random() * 25)}`,\n    target: `node-${Math.floor(Math.random() * 25)}`,\n  })),\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  autoFit: 'view',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  layout: {\n    type: 'mds',\n    nodeSize: 32,\n    linkDistance: 100,\n  },\n  behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/MindmapLayout.en.md",
    "content": "---\ntitle: Mindmap Tree\norder: 18\n---\n\n## Overview\n\nThe mindmap tree layout is suitable for hierarchical layouts of tree structures, supporting expansion on both left and right sides. Nodes at the same depth will be placed on the same layer. Note: the layout **does** take node size into account. See more mindmap layout [examples](/en/examples#layout-mindmap) or [source code](https://github.com/antvis/hierarchy/blob/master/src/mindmap.js).\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*J1l5RofvbP0AAAAAAAAAAABkARQnAQ' width=350 alt='img'/>\n\n## Configuration\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'mindmap',\n    direction: 'H',\n    preLayout: false,\n    getHeight: () => 32,\n    getWidth: () => 32,\n    getVGap: () => 16,\n    getHGap: () => 72,\n  },\n});\n```\n\n## Options\n\n<img src=\"https://img.alicdn.com/imgextra/i4/O1CN014J5e691gxm5GSrwD2_!!6000000004209-0-tps-1163-832.jpg\" width=\"400\" alt=\"Mindmap tree options diagram\" />\n\n| Property  | Description                                                                                             | Type                                | Default | Required |\n| --------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------- | ------- | -------- |\n| type      | Layout type                                                                                             | `mindmap`                           | -       | ✓        |\n| direction | Layout direction, [options](#direction)                                                                 | `H` \\| `LR` \\| `RL` \\| `TB` \\| `BT` | `LR`    |          |\n| getHeight | Function to calculate the height of each node                                                           | (d?: Node) => number                |         | ✓        |\n| getWidth  | Function to calculate the width of each node                                                            | (d?: Node) => number                |         | ✓        |\n| getVGap   | Vertical gap for each node. Note: the actual vertical gap between two nodes is twice the vgap           | (d?: Node) => number                |         |          |\n| getHGap   | Horizontal gap for each node. Note: the actual horizontal gap between two nodes is twice the hgap       | (d?: Node) => number                |         |          |\n| getSide   | Set whether the node is placed on the left or right of the root. Only effective when `direction` is `H` | (d?: Node) => string                |         |          |\n\n### direction\n\n> `H` \\| `LR` \\| `RL` \\| `TB` \\| `BT` **Default:** `'LR'`\n\nTree layout direction\n\n- `'H'`: horizontal — The children of the root node are divided into two parts and placed on the left and right sides of the root node. You can pass the `getSide` method to specify the left/right distribution logic for each node. If not provided, the first half will be placed on the right, and the second half on the left by default.\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*J1l5RofvbP0AAAAAAAAAAABkARQnAQ' width=170 alt='Horizontal layout'/>\n\n- `'LR' | 'TB'`: Children are placed on the right side of the root node.\n\n<img src='https://img.alicdn.com/imgextra/i2/O1CN01SWsfai28ZmZu2ehyh_!!6000000007947-0-tps-1390-1254.jpg' width=150 alt='Vertical layout'/>\n\n- `'RL'`: Children are placed on the left side of the root node.\n\n<img src='https://img.alicdn.com/imgextra/i1/O1CN01DFh7iu26fcORrjGfT_!!6000000007689-0-tps-1396-1254.jpg' width=150 alt='Vertical layout'/>\n\n- `BT`: Children are placed on the right side of the root node, then the entire graph is rotated 180° along the X axis.\n\n<img src='https://img.alicdn.com/imgextra/i2/O1CN01zppRLx1Igmbtv4EyJ_!!6000000000923-0-tps-1388-1282.jpg' width=150 alt='Vertical layout'/>\n\n### getWidth\n\n> _(d?: Node) => number_\n\nWidth of each node\n\nExample:\n\n```javascript\n(d) => {\n  // d is a node\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getHeight\n\n> _(d?: Node) => number_\n\nHeight of each node\n\nExample:\n\n```javascript\n(d) => {\n  // d is a node\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getHGap\n\n> _(d?: Node) => number_\n\nHorizontal gap for each node\n\nExample:\n\n```javascript\n(d) => {\n  // d is a node\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getVGap\n\n> _(d?: Node) => number_\n\nVertical gap for each node\n\nExample:\n\n```javascript\n(d) => {\n  // d is a node\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getSide\n\n> _(d?: Node) => string_\n\nSet whether the node is placed on the left or right of the root. Note: only effective when `direction` is `H`. If not set, the first half of the children will be placed on the right, and the second half on the left by default. See [getSide auto calculation logic](https://github.com/antvis/hierarchy/blob/d786901874f59d96c47e2a5dfe17b373eefd72e3/src/layout/separate-root.js#L11).\n\nExample:\n\n```javascript\n(d) => {\n  // d is a node\n  if (d.id === 'test-child-id') return 'right';\n  return 'left';\n};\n```\n\n### Suitable Scenarios\n\n- Data lineage graph: `direction='H'` is suitable for rendering upstream and downstream lineage of a specified node, with upstream on the left and downstream on the right of the central node.\n- Mind map: Build custom mind map components.\n"
  },
  {
    "path": "packages/site/docs/manual/layout/MindmapLayout.zh.md",
    "content": "---\ntitle: 脑图树 Mindmap\norder: 18\n---\n\n## 概述\n\n脑图树布局适用于树状结构的层次化布局，支持左右两侧展开，深度相同的节点将会被放置在同一层。需要注意：布局**会**考虑节点的大小。参考更多脑图布局[样例](/examples#layout-mindmap)或[源码](https://github.com/antvis/hierarchy/blob/master/src/mindmap.js)。\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*J1l5RofvbP0AAAAAAAAAAABkARQnAQ' width=350 alt='img'/>\n\n## 配置方式\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'mindmap',\n    direction: 'H',\n    preLayout: false,\n    getHeight: () => 32,\n    getWidth: () => 32,\n    getVGap: () => 16,\n    getHGap: () => 72,\n  },\n});\n```\n\n## 配置项\n\n<img src=\"https://img.alicdn.com/imgextra/i4/O1CN014J5e691gxm5GSrwD2_!!6000000004209-0-tps-1163-832.jpg\" width=\"400\" alt=\"脑图树配置项图解\" />\n\n| 属性      | 描述                                                                                                  | 类型                                | 默认值 | 必选 |\n| --------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------- | ------ | ---- |\n| type      | 布局类型                                                                                              | `mindmap`                           | -      | ✓    |\n| direction | 布局方向，[可选值](#direction)                                                                        | `H` \\| `LR` \\| `RL` \\| `TB` \\| `BT` | `LR`   |      |\n| getHeight | 计算每个节点的高度                                                                                    | (d?: Node) => number                |        | ✓    |\n| getWidth  | 计算每个节点的宽度                                                                                    | (d?: Node) => number                |        | ✓    |\n| getVGap   | 每个节点的垂直间隙，注意实际两个节点间的垂直间隙是2倍的vgap                                           | (d?: Node) => number                |        |      |\n| getHGap   | 每个节点的水平间隙，注意实际两个节点间的水平间隙是2倍的hgap                                           | (d?: Node) => number                |        |      |\n| getSide   | 设置节点排布在根节点的左侧/右侧，如未设置，则算法自动分配左侧/右侧。注意：该参数仅在`H`布局方向上生效 | (d?: Node) => string                |        |      |\n\n### direction\n\n> `H` \\| `LR` \\| `RL` \\| `TB` \\| `BT` **Default:** `'LR'`\n\n树布局的方向\n\n- `'H'`：horizontal（水平）—— 根节点的子节点分成两部分横向放置在根节点左右两侧。可传入`getSide`方法指定每个节点的左右分布逻辑，不传则默认将前半部分放置在右侧，后半部分放置在左侧。\n\n<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*J1l5RofvbP0AAAAAAAAAAABkARQnAQ' width=170 alt='水平布局'/>\n\n- `'LR' | 'TB'`：将子节点排布在根节点的右侧；\n\n<img src='https://img.alicdn.com/imgextra/i2/O1CN01SWsfai28ZmZu2ehyh_!!6000000007947-0-tps-1390-1254.jpg' width=150 alt='竖直布局'/>\n\n- `'RL'`：将子节点排布在根节点的左侧；\n\n<img src='https://img.alicdn.com/imgextra/i1/O1CN01DFh7iu26fcORrjGfT_!!6000000007689-0-tps-1396-1254.jpg' width=150 alt='竖直布局'/>\n\n- `BT`：将子节点排布在根节点右侧，然后将整个图沿X轴旋转180°；\n\n<img src='https://img.alicdn.com/imgextra/i2/O1CN01zppRLx1Igmbtv4EyJ_!!6000000000923-0-tps-1388-1282.jpg' width=150 alt='竖直布局'/>\n\n### getWidth\n\n> _(d?: Node) => number_\n\n每个节点的宽度\n\n示例：\n\n```javascript\n(d) => {\n  // d 是一个节点\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getHeight\n\n> _(d?: Node) => number_\n\n每个节点的高度\n\n示例：\n\n```javascript\n(d) => {\n  // d 是一个节点\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getHGap\n\n> _(d?: Node) => number_\n\n每个节点的水平间隙\n\n示例：\n\n```javascript\n(d) => {\n  // d 是一个节点\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getVGap\n\n> _(d?: Node) => number_\n\n每个节点的垂直间隙\n\n示例：\n\n```javascript\n(d) => {\n  // d 是一个节点\n  if (d.id === 'testId') return 50;\n  return 100;\n};\n```\n\n### getSide\n\n> _(d?: Node) => string_\n\n设置节点排布在根节点的左侧/右侧。注意：该参数仅在`direction`为`H`时生效。如未设置，会默认将子节点前半部分放置在右侧，后半部分放置在左侧，参考[getSide自动计算逻辑](https://github.com/antvis/hierarchy/blob/d786901874f59d96c47e2a5dfe17b373eefd72e3/src/layout/separate-root.js#L11)。\n\n示例：\n\n```javascript\n(d) => {\n  // d 是一个节点\n  if (d.id === 'test-child-id') return 'right';\n  return 'left';\n};\n```\n\n### 布局适用场景\n\n- 数据血缘图：`direction='H'`很适合渲染血缘图中查看指定节点的上下游血缘的场景，上游分布在中心节点的左侧，下游分布在右侧；\n- 思维导图：构建自定义的思维导图组件。\n"
  },
  {
    "path": "packages/site/docs/manual/layout/RadialLayout.en.md",
    "content": "---\ntitle: Radial Layout\norder: 19\n---\n\n## Overview\n\nRadial layout is a graph layout algorithm that arranges nodes in concentric circles by layers. It is commonly used to display hierarchical relationships, community structures, and more. This layout supports advanced features such as node overlap prevention and group sorting, making it suitable for visualizing various network structures.\n\n## Use Cases\n\n- Displaying hierarchical structures (e.g., organizational charts, family trees)\n- Community structure analysis\n- Scenarios that need to highlight the central node and its radiating relationships\n- Complex networks requiring node grouping and sorting\n\n## Online Demo\n\n<embed src=\"@/common/api/layouts/radial.md\"></embed>\n\n## Configuration\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'radial',\n    nodeSize: 32,\n    unitRadius: 100,\n    linkDistance: 200,\n  },\n  // other configurations...\n});\n```\n\n## Options\n\n| Property                   | Description                                              | Type                                 | Default  | Required |\n| -------------------------- | -------------------------------------------------------- | ------------------------------------ | -------- | -------- |\n| type                       | Layout type                                              | string                               | `radial` | ✓        |\n| center                     | Center coordinates                                       | [number, number]                     | -        |          |\n| focusNode                  | Radiating center node                                    | string \\| Node \\| null               | null     |          |\n| height                     | Canvas height                                            | number                               | -        |          |\n| width                      | Canvas width                                             | number                               | -        |          |\n| nodeSize                   | Node size (diameter)                                     | number                               | -        |          |\n| nodeSpacing                | Minimum node spacing (effective when preventing overlap) | number \\| (nodeData: Node) => number | 10       |          |\n| linkDistance               | Edge length                                              | number                               | 50       |          |\n| unitRadius                 | Radius per circle                                        | number \\| null                       | 100      |          |\n| maxIteration               | Maximum number of iterations                             | number                               | 1000     |          |\n| maxPreventOverlapIteration | Max iterations for overlap prevention                    | number                               | 200      |          |\n| preventOverlap             | Whether to prevent node overlap                          | boolean                              | false    |          |\n| sortBy                     | Field for sorting nodes in the same layer                | string                               | -        |          |\n| sortStrength               | Sorting strength for nodes in the same layer             | number                               | 10       |          |\n| strictRadial               | Strictly place nodes in the same layer on the same ring  | boolean                              | true     |          |\n\n## Code Example\n\n### Basic Usage\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/radial.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'center',\n      layout: {\n        type: 'radial',\n        nodeSize: 32,\n        unitRadius: 100,\n        linkDistance: 200,\n      },\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelPlacement: 'center',\n          labelText: (d) => d.id,\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n    graph.render();\n  });\n```\n\nResult:\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*d3P-RK4YCDYAAAAAAAAAAAAADmJ7AQ/original\" alt=\"Basic Radial Layout\" style=\"max-width: 600px;\" />\n\n## Real Cases\n\n- [Basic Radial Layout](/en/examples/layout/radial/#basic)\n- [Strict Overlap Prevention Radial Layout](/en/examples/layout/radial/#strict-prevent-overlap)\n- [Non-strict Overlap Prevention Radial Layout](/en/examples/layout/radial/#non-strict-prevent-overlap)\n- [Cluster Sorting](/en/examples/layout/radial/#cluster-sort)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/RadialLayout.zh.md",
    "content": "---\ntitle: 径向布局 Radial\norder: 19\n---\n\n## 概述\n\n径向（Radial）布局是一种将节点以同心圆方式分层排列的图布局算法，常用于展示层级关系、社群结构等。该布局支持节点防重叠、分组排序等高级特性，适用于多种网络结构的可视化。\n\n## 使用场景\n\n- 展示层级结构（如组织架构、家谱等）\n- 社群结构分析\n- 需要突出中心节点及其辐射关系的场景\n- 需要节点分组、排序的复杂网络\n\n## 在线体验\n\n<embed src=\"@/common/api/layouts/radial.md\"></embed>\n\n## 配置方式\n\n```js\nconst graph = new Graph({\n  layout: {\n    type: 'radial',\n    nodeSize: 32,\n    unitRadius: 100,\n    linkDistance: 200,\n  },\n  // 其他配置...\n});\n```\n\n## 配置项\n\n| 属性                       | 描述                                         | 类型                                 | 默认值   | 必选 |\n| -------------------------- | -------------------------------------------- | ------------------------------------ | -------- | ---- |\n| type                       | 布局类型                                     | string                               | `radial` | ✓    |\n| center                     | 圆心坐标                                     | [number, number]                     | -        |      |\n| focusNode                  | 辐射中心节点                                 | string \\| Node \\| null               | null     |      |\n| height                     | 画布高度                                     | number                               | -        |      |\n| width                      | 画布宽度                                     | number                               | -        |      |\n| nodeSize                   | 节点大小（直径）                             | number                               | -        |      |\n| nodeSpacing                | 节点最小间距（防重叠时生效）                 | number \\| (nodeData: Node) => number | 10       |      |\n| linkDistance               | 边长度                                       | number                               | 50       |      |\n| unitRadius                 | 每圈半径                                     | number \\| null                       | 100      |      |\n| maxIteration               | 最大迭代次数                                 | number                               | 1000     |      |\n| maxPreventOverlapIteration | 防重叠最大迭代次数                           | number                               | 200      |      |\n| preventOverlap             | 是否防止节点重叠                             | boolean                              | false    |      |\n| sortBy                     | 同层节点排序字段                             | string                               | -        |      |\n| sortStrength               | 同层节点排序强度                             | number                               | 10       |      |\n| strictRadial               | 是否严格每层节点在同一圆环上（防重叠时生效） | boolean                              | true     |      |\n\n## 代码示例\n\n### 基本用法\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/radial.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'center',\n      layout: {\n        type: 'radial',\n        nodeSize: 32,\n        unitRadius: 100,\n        linkDistance: 200,\n      },\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelPlacement: 'center',\n          labelText: (d) => d.id,\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n    graph.render();\n  });\n```\n\n效果如下：\n\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*d3P-RK4YCDYAAAAAAAAAAAAADmJ7AQ/original\" alt=\"基本 Radial 辐射布局\" style=\"max-width: 600px;\" />\n\n## 实际案例\n\n- [基本 Radial 辐射布局](/examples/layout/radial/#basic)\n- [防止节点重叠的严格辐射布局](/examples/layout/radial/#strict-prevent-overlap)\n- [防止节点重叠的非严格辐射布局](/examples/layout/radial/#non-strict-prevent-overlap)\n- [排序聚类](/examples/layout/radial/#cluster-sort)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/RandomLayout.en.md",
    "content": "---\ntitle: Random Layout\norder: 20\n---\n\n## Overview\n\n**Random Layout** is a layout method based on simple rules. Its core logic is to generate random coordinates for each node within a specified layout area (defined by the layout center, width, and height). The coordinates are completely random, and there is no node collision prevention.\n\n## Use Cases\n\nThe use cases for random layout are very limited. It is only recommended for the following scenarios:\n\n- **Initial Data Display**:\n\n  During early development, when debugging data loading logic or quickly verifying data structure, random layout can be used for preliminary validation.\n\nFor final business delivery, it is recommended to use layouts that better reflect business value, such as [AntVDagreLayout](/en/manual/layout/antv-dagre-layout), [ForceLayout](/en/manual/layout/force-layout), or [custom layouts](/en/manual/layout/custom-layout).\n\n## Basic Usage\n\nAll other configurations use defaults (the layout width and height default to the entire canvas container).\n\n```js\nconst graph = new Graph({\n  // other configurations\n  layout: {\n    type: 'random',\n  },\n});\n```\n\n## Options\n\n| Property | Description   | Type                                         | Default                                   | Required |\n| -------- | ------------- | -------------------------------------------- | ----------------------------------------- | -------- |\n| type     | Layout type   | random                                       | -                                         | ✓        |\n| center   | Layout center | [number, number] \\| [number, number, number] | [`layout width` / 2, `layout height` / 2] |          |\n| height   | Layout height | number                                       | canvas height                             |          |\n| width    | Layout width  | number                                       | canvas width                              |          |\n\n## Example\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  autoFit: 'view',\n  data: {\n    nodes: Array.from({ length: 50 }).map((_, i) => ({\n      id: `${i}`,\n    })),\n  },\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n  layout: {\n    type: 'random',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/RandomLayout.zh.md",
    "content": "---\ntitle: 随机布局 Random\norder: 20\n---\n\n## 概述\n\n**随机布局（Random Layout）** 是一种基于简单规则的布局方式，其核心逻辑是为每个节点在指定布局区域（由布局中心点、宽度和高度定义）内生成随机坐标（完全随机，无节点防碰撞）。\n\n## 使用场景\n\n随机布局的使用场景非常局限，只推荐以下场景使用：\n\n- **数据初步展示**:\n\n  在开发初期调试数据加载逻辑或需要快速验证数据结构的情况下，可使用随机布局先行验证\n\n业务最终交付推荐使用更能体现业务价值的布局方式，比如 [AntVDagreLayout](/manual/layout/antv-dagre-layout) 、[ForceLayout](/manual/layout/force-layout) 或者[自定义布局](/manual/layout/custom-layout)等。\n\n## 基本用法\n\n其余均使用默认配置（布局宽高默认是整个画布容器）\n\n```js\nconst graph = new Graph({\n  // 其他配置\n  layout: {\n    type: 'random',\n  },\n});\n```\n\n## 配置项\n\n| 属性   | 描述       | 类型                                         | 默认值                           | 必选 |\n| ------ | ---------- | -------------------------------------------- | -------------------------------- | ---- |\n| type   | 布局类型   | random                                       | -                                | ✓    |\n| center | 布局的中心 | [number, number] \\| [number, number, number] | [`布局宽度` / 2, `布局高度` / 2] |      |\n| height | 布局的高度 | number                                       | 画布高度                         |      |\n| width  | 布局的宽度 | number                                       | 画布宽度                         |      |\n\n## 示例\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  autoFit: 'view',\n  data: {\n    nodes: Array.from({ length: 50 }).map((_, i) => ({\n      id: `${i}`,\n    })),\n  },\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n  layout: {\n    type: 'random',\n  },\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/Snake.en.md",
    "content": "---\ntitle: Snake Layout\norder: 21\n---\n\n## Overview\n\nSnake Layout is a special type of graph layout that can more efficiently display long chain structures in a limited space. Note that the graph data must ensure that nodes are linearly arranged from the source node to the sink node, forming a clear path.\n\nNodes are arranged in an S-shape: the first node is at the start of the first row, subsequent nodes are arranged to the right in the first row until the end. At the end of the row, the next row's nodes are arranged from right to left. This process repeats until all nodes are placed.\n\n## Use Cases\n\nSuitable for scenarios that require compact presentation of linear relationships:\n\n- **Long process visualization**\n\n  Perfect for scenarios with many process steps, such as approval flows, production line procedures, logistics routes, etc.\n\n- **Hierarchical structures in limited space**\n\n  When the hierarchy is too long but the canvas is limited, rows can be folded to save space. For example, API call dependencies (client → gateway → serviceA → serviceB → database, snake layout compresses 5 layers into 2 rows), or file directory trees (deeply nested folder structures, e.g., src/components/utils/helpers/..., using snake layout to fold subdirectories horizontally).\n\n## Online Demo\n\n<embed src=\"@/common/api/layouts/snake.md\"></embed>\n\n## Options\n\n> If the layout has specific properties, they are listed below. For common layout options, see [Base Layout Options](/en/manual/layout/base-layout)\n\n| Property                | Description                                                 | Type                                               | Default                                                    | Required |\n| ----------------------- | ----------------------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------- | -------- |\n| type                    | Layout type                                                 | snake                                              | -                                                          | ✓        |\n| [clockwise](#clockwise) | Whether nodes are arranged clockwise                        | boolean                                            | true                                                       |          |\n| colGap                  | Gap size between columns                                    | number                                             | Automatically calculated by canvas width and total columns |          |\n| cols                    | Number of columns                                           | number                                             | 5                                                          |          |\n| nodeSize                | Node size                                                   | Size \\| ((node: NodeData) => Size)                 | -                                                          |          |\n| padding                 | Padding, i.e., distance from layout area to canvas boundary | number \\| number[]                                 | 0                                                          |          |\n| rowGap                  | Gap size between rows                                       | number                                             | Automatically calculated by canvas height and total rows   |          |\n| sortBy                  | Node sorting method                                         | (nodeA: NodeData, nodeB: NodeData) => -1 \\| 0 \\| 1 | Default is the path order in the graph                     |          |\n\n### clockwise\n\n- When arranged clockwise, nodes start from the top left, the first row is arranged left to right, the second row right to left, and so on, forming an S-shaped path.\n- When arranged counterclockwise, nodes start from the top right, the first row is arranged right to left, the second row left to right, and so on, forming a reversed S-shaped path.\n\n## Real Cases\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(16).fill(0).map((_, i) => ({ id: `${i}` })),\n  edges: new Array(15).fill(0).map((_, i) => ({ source: `${i}`, target: `${i + 1}` })),\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n  },\n  layout: {\n    type: 'snake',\n    padding: 50,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/Snake.zh.md",
    "content": "---\ntitle: 蛇形布局 Snake\norder: 21\n---\n\n## 概览\n\n蛇形布局（Snake Layout）是一种特殊的图形布局方式，能够在较小的空间内更有效地展示长链结构。需要注意的是，其图数据需要确保节点按照从源节点到汇节点的顺序进行线性排列，形成一条明确的路径。\n\n节点按 S 字型排列，第一个节点位于第一行的起始位置，接下来的节点在第一行向右排列，直到行末尾。到达行末尾后，下一行的节点从右向左反向排列。这个过程重复进行，直到所有节点排列完毕。\n\n## 使用场景\n\n适合需要紧凑呈现线性关系的场景：\n\n- **长流程可视化**\n\n  完美适配流程步骤过多的场景，如审批流程、生产线工序、物流运输路径等。\n\n- **有限空间内的层级结构**\n\n  层级结构过长但画布受限，需通过折叠行节省空间，比如 API 调用依赖（客户端 → 网关 → 服务A → 服务B → 数据库，蛇形布局将 5 层压缩为 2 行）、文件目录树（深度嵌套的文件夹结构，如 src/components/utils/helpers/... ，用蛇形布局横向折叠子目录）。\n\n## 在线体验\n\n<embed src=\"@/common/api/layouts/snake.md\"></embed>\n\n## 配置项\n\n> 如果布局有其特定的属性，我们将在下面列出。对于所有布局的通用属性，见[布局通用配置项](/manual/layout/base-layout)\n\n| 属性                    | 描述                               | 类型                                               | 默认值                                 | 必选 |\n| ----------------------- | ---------------------------------- | -------------------------------------------------- | -------------------------------------- | ---- |\n| type                    | 布局类型                           | snake                                              | -                                      | ✓    |\n| [clockwise](#clockwise) | 节点排布方向是否顺时针             | boolean                                            | true                                   |      |\n| colGap                  | 节点列之间的间隙大小               | number                                             | 默认将根据画布宽度和节点总列数自动计算 |      |\n| cols                    | 节点列数                           | number                                             | 5                                      |      |\n| nodeSize                | 节点尺寸                           | Size \\| ((node: NodeData) => Size)                 | -                                      |      |\n| padding                 | 内边距，即布局区域与画布边界的距离 | number \\| number[]                                 | 0                                      |      |\n| rowGap                  | 节点行之间的间隙大小               | number                                             | 默认将根据画布高度和节点总行数自动计算 |      |\n| sortBy                  | 节点排序方法                       | (nodeA: NodeData, nodeB: NodeData) => -1 \\| 0 \\| 1 | 默认按照在图中的路径顺序进行展示       |      |\n\n### clockwise\n\n- 在顺时针排布时，节点从左上角开始，第一行从左到右排列，第二行从右到左排列，依次类推，形成 S 型路径。\n\n- 在逆时针排布时，节点从右上角开始，第一行从右到左排列，第二行从左到右排列，依次类推，形成反向 S 型路径。\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(16).fill(0).map((_, i) => ({ id: `${i}` })),\n  edges: new Array(15).fill(0).map((_, i) => ({ source: `${i}`, target: `${i + 1}` })),\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n  },\n  layout: {\n    type: 'snake',\n    padding: 50,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/layout/custom-layout.en.md",
    "content": "---\ntitle: Custom Layout\norder: 22\n---\n\n## Overview\n\nIn G6, layouts are divided into two types: 'iterative layout' and 'non-iterative layout'. An iterative layout refers to a layout algorithm that iterates multiple times until convergence, while a non-iterative layout refers to a layout algorithm that executes only once.\n\n## Implement Layout\n\n### Non-Iterative Layout\n\nImplementing a non-iterative layout is relatively straightforward; you only need to implement the `execute` method in `BaseLayout`. Below is a simple implementation of a custom layout:\n\n```typescript\nimport { BaseLayout } from '@antv/g6';\nimport type { GraphData } from '@antv/g6';\n\nclass DiagonalLayout extends BaseLayout {\n  id = 'diagonal-layout';\n\n  async execute(data: GraphData): Promise<GraphData> {\n    const { nodes = [] } = data;\n    return {\n      nodes: nodes.map((node, index) => ({\n        id: node.id,\n        style: {\n          x: 50 * index + 25,\n          y: 50 * index + 25,\n        },\n      })),\n    };\n  }\n}\n```\n\nIn the code above, we implemented a simple layout algorithm that arranges nodes along a diagonal line starting from the top-left corner.\n\n<embed src=\"@/common/manual/custom-extension/layout/non-iterative-layout.md\"></embed>\n\n:::info{title=info}\n\nThe `execute` method returns a GraphData object, which only needs to contain the basic information of the elements (such as id, source, target) and the properties added by the layout (such as x, y, control points of edges, etc.), and does not need to contain other unnecessary information.\nIf you only need to layout the nodes, you can return only the node information and do not need to return the edge information.\n:::\n\n### Iterative Layout\n\nThe implementation of an iterative layout also requires inheriting from `BaseLayout`, but in addition to `execute`, you also need to implement the `tick` and `stop` methods. The `tick` method is used to iterate the layout to a specified number of rounds, and the `stop` method is used to stop the layout iteration.\n\nIn addition, in iterative layouts, you need to call `options.onTick` each time the layout iterates to trigger the graph update.\n\n```typescript\ntype onTick = (data: GraphData) => void;\n```\n\nBelow is a simple implementation of an iterative layout:\n\n```typescript\nimport { BaseLayout } from '@antv/g6';\nimport type { GraphData, BaseLayoutOptions } from '@antv/g6';\n\ninterface TickTockLayoutOptions extends BaseLayoutOptions {\n  onTick: (data: GraphData) => void;\n}\n\nclass TickTockLayout extends BaseLayout<TickTockLayoutOptions> {\n  public id = 'custom-layout';\n\n  private tickCount = 0;\n\n  private data?: GraphData;\n\n  private timer?: number;\n\n  private resolve?: () => void;\n\n  private promise?: Promise<void>;\n\n  async execute(data: GraphData, options: TickTockLayoutOptions): Promise<GraphData> {\n    const { onTick } = { ...this.options, ...options };\n\n    this.tickCount = 0;\n    this.data = data;\n\n    this.promise = new Promise((resolve) => {\n      this.resolve = resolve;\n    });\n\n    this.timer = window.setInterval(() => {\n      onTick(this.simulateTick());\n      if (this.tickCount === 10) this.stop();\n    }, 200);\n\n    await this.promise;\n\n    return this.simulateTick();\n  }\n\n  simulateTick = () => {\n    const x = this.tickCount++ % 2 === 0 ? 50 : 150;\n\n    return {\n      nodes: (this?.data?.nodes || []).map((node, index) => ({\n        id: node.id,\n        style: { x, y: (index + 1) * 30 },\n      })),\n    };\n  };\n\n  tick = () => {\n    return this.simulateTick();\n  };\n\n  stop = () => {\n    clearInterval(this.timer);\n    this.resolve?.();\n  };\n}\n```\n\nIn this example, we have implemented a simple iterative layout algorithm that toggles the x-coordinate of the nodes between 50 and 150 every 200 milliseconds and arranges them in order along the y-axis according to the sequence of the nodes.\n\n<embed src=\"@/common/manual/custom-extension/layout/iterative-layout.md\"></embed>\n\n## Register Layout\n\nYou can register through the `register` method provided by G6. For more details, please refer to [Register Layout](/en/manual/core-concept/layout#register-layout)\n\n## Configure Layout\n\nThe type and parameters of the layout can be configured in `options.layout`. For more details, please refer to [Configure Layout](/en/manual/core-concept/layout#configure-layout)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/custom-layout.zh.md",
    "content": "---\ntitle: 自定义布局\norder: 22\n---\n\n## 概述\n\nG6 中布局分为`迭代布局`和`非迭代布局`两种，迭代布局是指布局算法会迭代多次直到收敛，非迭代布局是指布局算法只执行一次。\n\n## 实现布局\n\n### 非迭代布局\n\n实现一个非迭代布局相对简单，只需要实现 `BaseLayout` 中的 `execute` 方法即可，下面是一个简单的自定义布局实现：\n\n```typescript\nimport { BaseLayout } from '@antv/g6';\nimport type { GraphData } from '@antv/g6';\n\nclass DiagonalLayout extends BaseLayout {\n  id = 'diagonal-layout';\n\n  async execute(data: GraphData): Promise<GraphData> {\n    const { nodes = [] } = data;\n    return {\n      nodes: nodes.map((node, index) => ({\n        id: node.id,\n        style: {\n          x: 50 * index + 25,\n          y: 50 * index + 25,\n        },\n      })),\n    };\n  }\n}\n```\n\n在上面的代码中，我们实现了一个简单的布局算法，将节点从左上角沿对角线排列。\n\n<embed src=\"@/common/manual/custom-extension/layout/non-iterative-layout.md\"></embed>\n\n:::info{title=提示}\n`execute` 方法返回的是一个 GraphData 对象，里面仅需要包含元素的基本信息（如 id、source、target）以及布局新增的属性（如 x、y、边的控制点等），不需要包含其他无用的信息。\n如果仅需要对节点进行布局，可以只返回节点信息，不需要返回边的信息。\n:::\n\n### 迭代布局\n\n迭代布局的实现同样需要继承 `BaseLayout`，但是除 `execute` 外还需要实现 `tick` 和 `stop` 方法，`tick` 方法用于将布局迭代到指定轮次，`stop` 方法用于停止布局迭代。\n\n此外，迭代布局中需要在每次迭代调用 `options.onTick` 以触发图的更新。\n\n```typescript\ntype onTick = (data: GraphData) => void;\n```\n\n下面是一个简单的迭代布局实现：\n\n```typescript\nimport { BaseLayout } from '@antv/g6';\nimport type { GraphData, BaseLayoutOptions } from '@antv/g6';\n\ninterface TickTockLayoutOptions extends BaseLayoutOptions {\n  onTick: (data: GraphData) => void;\n}\n\nclass TickTockLayout extends BaseLayout<TickTockLayoutOptions> {\n  public id = 'custom-layout';\n\n  private tickCount = 0;\n\n  private data?: GraphData;\n\n  private timer?: number;\n\n  private resolve?: () => void;\n\n  private promise?: Promise<void>;\n\n  async execute(data: GraphData, options: TickTockLayoutOptions): Promise<GraphData> {\n    const { onTick } = { ...this.options, ...options };\n\n    this.tickCount = 0;\n    this.data = data;\n\n    this.promise = new Promise((resolve) => {\n      this.resolve = resolve;\n    });\n\n    this.timer = window.setInterval(() => {\n      onTick(this.simulateTick());\n      if (this.tickCount === 10) this.stop();\n    }, 200);\n\n    await this.promise;\n\n    return this.simulateTick();\n  }\n\n  simulateTick = () => {\n    const x = this.tickCount++ % 2 === 0 ? 50 : 150;\n\n    return {\n      nodes: (this?.data?.nodes || []).map((node, index) => ({\n        id: node.id,\n        style: { x, y: (index + 1) * 30 },\n      })),\n    };\n  };\n\n  tick = () => {\n    return this.simulateTick();\n  };\n\n  stop = () => {\n    clearInterval(this.timer);\n    this.resolve?.();\n  };\n}\n```\n\n在这个例子中，我们实现了一个简单的迭代布局算法，每 200ms 将节点的 x 坐标在 50 和 150 之间切换，并按照节点顺序在 y 方向上排列。\n\n<embed src=\"@/common/manual/custom-extension/layout/iterative-layout.md\"></embed>\n\n## 注册布局\n\n通过 G6 提供的 register 方法注册即可，详见[注册布局](/manual/layout/overview#注册布局)\n\n## 配置布局\n\n可在 `options.layout` 中配置布局的类型和参数，详见[配置布局](/manual/layout/overview#配置布局)\n"
  },
  {
    "path": "packages/site/docs/manual/layout/overview.en.md",
    "content": "---\ntitle: Layout Overview\norder: 0\n---\n\n## Overview\n\nGraph layout refers to the process of arranging elements in a graph according to certain rules, such as force-directed layout based on charge elasticity models, grid layout with sequential arrangement, and tree layout based on hierarchical structures.\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*WIhlToluHaEAAAAAAAAAAAAADmJ7AQ/original\" />\n\n## Layout Types\n\nG6 provides a variety of layout algorithms, allowing users to choose the appropriate one based on their needs:\n\n- [AntVDagreLayout](/en/manual/layout/antv-dagre-layout): Custom layout based on dagre\n- [CircularLayout](/en/manual/layout/circular-layout): Circular layout\n- [ComboCombinedLayout](/en/manual/layout/combo-combined-layout): Layout suitable for combinations\n- [ConcentricLayout](/en/manual/layout/concentric-layout): Concentric layout\n- [D3Force3DLayout](/en/manual/layout/d3-force3-d-layout): [3D Force-directed](https://github.com/vasturiano/d3-force-3d) layout\n- [D3ForceLayout](/en/manual/layout/d3-force-layout): Force-directed layout based on [D3](https://d3js.org/d3-force)\n- [DagreLayout](/en/manual/layout/dagre-layout): [dagre](https://github.com/dagrejs/dagre) layout\n- [FishboneLayout](/en/manual/layout/fishbone): Fishbone layout\n- [ForceAtlas2Layout](/en/manual/layout/force-atlas2-layout): [ForceAtlas2](https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0098679) layout\n- [ForceLayout](/en/manual/layout/force-layout): Force-directed layout\n- [FruchtermanLayout](/en/manual/layout/fruchterman-layout): [Fruchterman](https://www.sciencedirect.com/topics/computer-science/reingold-layout) layout\n- [GridLayout](/en/manual/layout/grid-layout): Grid layout\n- [MDSLayout](/en/manual/layout/mds-layout): High-dimensional data dimensionality reduction layout\n- [RadialLayout](/en/manual/layout/radial-layout): Radial layout\n- [RandomLayout](/en/manual/layout/random-layout): Random layout\n- [SnakeLayout](/en/manual/layout/snake): Snake layout\n- [CompactBoxLayout](/en/manual/layout/compact-box-layout): Compact tree layout\n- [DendrogramLayout](/en/manual/layout/dendrogram-layout): Dendrogram layout\n- [MindmapLayout](/en/manual/layout/mindmap-layout): Mindmap layout\n- [IndentedLayout](/en/manual/layout/indented-layout): Indented tree layout\n\nAmong them, `CompactBox Layout`, `Dendrogram Layout`, `Mindmap Layout`, and `Indented Layout` are types of tree layouts suitable for tree-structured graphs.\n\n## Register Layout\n\nYou can directly use built-in layouts, but if you want to use other layouts, you need to register them first:\n\n```typescript\nimport { register, ExtensionCategory } from '@antv/g6';\nimport { CustomLayout } from 'package-name/or/path-to-your-custom-layout';\n\nregister(ExtensionCategory.LAYOUT, 'custom-layout', CustomLayout);\n```\n\n## Configure Layout\n\nThe `layout` configuration item can specify the graph's layout algorithm, for example:\n\n```typescript\n{\n  layout: {\n    // Specify the layout algorithm to use\n    type: 'force',\n    // Configuration items for the layout algorithm\n    gravity: 10\n    // ...\n  }\n}\n```\n\nYou can also use `graph.setLayout` to update the layout configuration after the graph is instantiated.\n\n## Layout Acceleration\n\nG6 provides accelerated versions for some layout algorithms, including executing layout algorithms in Web Workers, providing [WASM](https://webassembly.org/) versions of layout algorithms, and GPU-accelerated layout algorithms. They can be used as follows:\n\n### Execute Layout Algorithms in Web Workers\n\nExcept for tree layouts, all built-in layout algorithms in G6 support execution in Web Workers. Simply set `enableWorker` to `true`:\n\n```typescript\n{\n  layout: {\n    type: 'force',\n    enableWorker: true,\n    // ...\n  }\n}\n```\n\n### Use WASM Version Layout Algorithms\n\nCurrently supported WASM version layout algorithms include: `Fruchterman Layout`, `ForceAtlas Layout`, `Force Layout`, `Dagre Layout`.\n\nFirst, install `@antv/layout-wasm`:\n\n```bash\nnpm install @antv/layout-wasm --save\n```\n\nImport and register the layout algorithm:\n\n```typescript\nimport { register, Graph, ExtensionCategory } from '@antv/g6';\nimport { FruchtermanLayout, initThreads, supportsThreads } from '@antv/layout-wasm';\n\nregister(ExtensionCategory.LAYOUT, 'fruchterman-wasm', FruchtermanLayout);\n```\n\nInitialize threads:\n\n```typescript\nconst supported = await supportsThreads();\nconst threads = await initThreads(supported);\n```\n\nInitialize the graph and pass in the layout configuration:\n\n```typescript\nconst graph = new Graph({\n  // ... other configurations\n  layout: {\n    type: 'fruchterman-wasm',\n    threads,\n    // ... other configurations\n  },\n});\n```\n\n### Use GPU-Accelerated Layout\n\nCurrently supported GPU-accelerated layout algorithms include: `Fruchterman Layout`, `GForce Layout`.\n\nFirst, install `@antv/layout-gpu`:\n\n```bash\nnpm install @antv/layout-gpu --save\n```\n\nImport and register the layout algorithm:\n\n```typescript\nimport { register, Graph, ExtensionCategory } from '@antv/g6';\nimport { FruchtermanLayout } from '@antv/layout-gpu';\n\nregister(ExtensionCategory.LAYOUT, 'fruchterman-gpu', FruchtermanLayout);\n```\n\nInitialize the graph and pass in the layout configuration:\n\n```typescript\nconst graph = new Graph({\n  // ... other configurations\n  layout: {\n    type: 'fruchterman-gpu',\n    // ... other configurations\n  },\n});\n```\n\n## Execute Layout\n\nUsually, after calling `graph.render()`, G6 will automatically execute the layout algorithm.\n\nIf you need to manually execute the layout algorithm, G6 provides the following APIs:\n\n- [layout](/api/layout#graphlayoutlayoutoptions): Execute layout algorithm\n- [setLayout](/api/layout#graphsetlayoutlayout): Set layout algorithm\n- [stopLayout](/api/layout#graphstoplayout): Stop layout algorithm\n\n## Custom Layout\n\nIf the built-in layout algorithms cannot meet your needs, you can customize layout algorithms. For details, please refer to [Custom Layout](/manual/layout/custom-layout).\n"
  },
  {
    "path": "packages/site/docs/manual/layout/overview.zh.md",
    "content": "---\ntitle: 布局总览\norder: 0\n---\n\n## 概述\n\n图布局是指将图中的元素按照一定的规则进行排列的过程，例如基于电荷弹性模型的力导向布局、逐次排布的网格布局、基于层次结构的树布局等。\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*WIhlToluHaEAAAAAAAAAAAAADmJ7AQ/original\" />\n\n## 布局类型\n\nG6 提供了多种布局算法，用户可以根据自己的需求选择合适的布局算法：\n\n- [AntVDagreLayout](/manual/layout/antv-dagre-layout)：基于 dagre 定制的布局\n- [CircularLayout](/manual/layout/circular-layout)：环形布局\n- [ComboCombinedLayout](/manual/layout/combo-combined-layout)：适用于存在组合的布局\n- [ConcentricLayout](/manual/layout/concentric-layout)：同心圆布局\n- [D3Force3DLayout](/manual/layout/d3-force3-d-layout)：[3D 力导向](https://github.com/vasturiano/d3-force-3d)布局\n- [D3ForceLayout](/manual/layout/d3-force-layout)：基于 [D3](https://d3js.org/d3-force) 的力导向布局\n- [DagreLayout](/manual/layout/dagre-layout)：[dagre](https://github.com/dagrejs/dagre) 布局\n- [FishboneLayout](/manual/layout/fishbone)：鱼骨布局\n- [ForceAtlas2Layout](/manual/layout/force-atlas2-layout)：[ForceAtlas2](https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0098679) 布局\n- [ForceLayout](/manual/layout/force-layout)：力导向布局\n- [FruchtermanLayout](/manual/layout/fruchterman-layout)：[Fruchterman](https://www.sciencedirect.com/topics/computer-science/reingold-layout) 布局\n- [GridLayout](/manual/layout/grid-layout)：网格布局\n- [MDSLayout](/manual/layout/mds-layout)：高维数据降维算法布局\n- [RadialLayout](/manual/layout/radial-layout)：径向布局\n- [RandomLayout](/manual/layout/random-layout)：随机布局\n- [SnakeLayout](/manual/layout/snake)：蛇形布局\n- [CompactBoxLayout](/manual/layout/compact-box-layout): 紧凑树布局\n- [DendrogramLayout](/manual/layout/dendrogram-layout): 树状布局\n- [MindmapLayout](/manual/layout/mindmap-layout): 思维导图布局\n- [IndentedLayout](/manual/layout/indented-layout): 缩进树布局\n\n其中 `CompactBox Layout`、`Dendrogram Layout`、`Mindmap Layout`、`Indented Layout` 是树布局的一种，适用于树状结构的图。\n\n## 注册布局\n\n你可以直接使用内置布局，如果想要使用其他布局，需要先进行注册：\n\n```typescript\nimport { register, ExtensionCategory } from '@antv/g6';\nimport { CustomLayout } from 'package-name/or/path-to-your-custom-layout';\n\nregister(ExtensionCategory.LAYOUT, 'custom-layout', CustomLayout);\n```\n\n## 配置布局\n\n通过 `layout` 配置项可以指定图的布局算法，例如：\n\n```typescript\n{\n  layout: {\n    // 指定要使用的布局算法\n    type: 'force',\n    // 布局算法的配置项\n    gravity: 10\n    // ...\n  }\n}\n```\n\n也可在图实例化之后使用 `graph.setLayout` 来更新布局配置。\n\n## 布局加速\n\nG6 对一些布局算法提供了加速版本，包括：在 Web Worker 中执行布局算法、提供 [WASM](https://webassembly.org/) 版本的布局算法、GPU 加速的布局算法等。可按照下列方式使用：\n\n### 在 Web Worker 中执行布局算法\n\n除树布局外，G6 的所有内置布局算法都支持在 Web Worker 中执行。只需将 `enableWorker` 设置为 `true` 即可：\n\n```typescript\n{\n  layout: {\n    type: 'force',\n    enableWorker: true,\n    // ...\n  }\n}\n```\n\n### 使用 WASM 版本布局算法\n\n目前支持 WASM 版本的布局算法有：`Fruchterman Layout` `ForceAtlas Layout` `Force Layout` `Dagre Layout`。\n\n首先安装 `@antv/layout-wasm`：\n\n```bash\nnpm install @antv/layout-wasm --save\n```\n\n引入并注册布局算法：\n\n```typescript\nimport { register, Graph, ExtensionCategory } from '@antv/g6';\nimport { FruchtermanLayout, initThreads, supportsThreads } from '@antv/layout-wasm';\n\nregister(ExtensionCategory.LAYOUT, 'fruchterman-wasm', FruchtermanLayout);\n```\n\n初始化线程：\n\n```typescript\nconst supported = await supportsThreads();\nconst threads = await initThreads(supported);\n```\n\n初始化图并传入布局配置：\n\n```typescript\nconst graph = new Graph({\n  // ... 其他配置\n  layout: {\n    type: 'fruchterman-wasm',\n    threads,\n    // ... 其他配置\n  },\n});\n```\n\n### 使用 GPU 加速布局\n\n目前支持 GPU 加速的布局算法有：`Fruchterman Layout` `GForce Layout`。\n\n首先安装 `@antv/layout-gpu`：\n\n```bash\nnpm install @antv/layout-gpu --save\n```\n\n引入并注册布局算法：\n\n```typescript\nimport { register, Graph, ExtensionCategory } from '@antv/g6';\nimport { FruchtermanLayout } from '@antv/layout-gpu';\n\nregister(ExtensionCategory.LAYOUT, 'fruchterman-gpu', FruchtermanLayout);\n```\n\n初始化图并传入布局配置：\n\n```typescript\nconst graph = new Graph({\n  // ... 其他配置\n  layout: {\n    type: 'fruchterman-gpu',\n    // ... 其他配置\n  },\n});\n```\n\n## 执行布局\n\n通常，在调用 `graph.render()` 后，G6 会自动执行布局算法。\n\n如果需要手动执行布局算法，G6 提供了以下 API：\n\n- [layout](/api/layout#graphlayoutlayoutoptions)：执行布局算法\n- [setLayout](/api/layout#graphsetlayoutlayout)：设置布局算法\n- [stopLayout](/api/layout#graphstoplayout)：停止布局算法\n\n## 自定义布局\n\n如果内置布局算法无法满足需求，可以自定义布局算法，具体请参考[自定义布局](/manual/layout/custom-layout)。\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Background.en.md",
    "content": "---\ntitle: Background\norder: 1\n---\n\n## Overview\n\nSupport setting a background image for the canvas to make the canvas more hierarchical and narrative.\n\n## Use Cases\n\nThis plugin is mainly used for:\n\n- Setting a unified brand background color or image for charts\n- Distinguishing different functional areas through the background\n- Enhancing the visual hierarchy and aesthetics of charts\n\n## Basic Usage\n\nBelow is a simple example of initializing the Background plugin:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'background',\n      key: 'my-background', // Specify an identifier for the plugin for dynamic updates\n      backgroundColor: '#f0f2f5', // Set background color\n      backgroundImage: 'url(https://example.com/bg.png)', // Set background image\n    },\n  ],\n});\n```\n\n## Configuration Options\n\nThe configuration options for the Background plugin inherit all CSS style properties ([CSSStyleDeclaration](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration)), so you can use any valid CSS property to configure the background. Here are some common configurations:\n\n| Property           | Description                   | Type   | Default Value     | Required |\n| ------------------ | ----------------------------- | ------ | ----------------- | -------- |\n| type               | Plugin type                   | string | `background`      | ✓        |\n| key                | Unique identifier for updates | string | -                 |          |\n| width              | Background width              | string | `100%`            |          |\n| height             | Background height             | string | `100%`            |          |\n| backgroundColor    | Background color              | string | -                 |          |\n| backgroundImage    | Background image              | string | -                 |          |\n| backgroundSize     | Background size               | string | `cover`           |          |\n| backgroundPosition | Background position           | string | -                 |          |\n| backgroundRepeat   | Background repeat             | string | -                 |          |\n| opacity            | Background opacity            | string | -                 |          |\n| transition         | Transition animation          | string | `background 0.5s` |          |\n| zIndex             | Stacking order                | string | -1                |          |\n\n> Note: The `zIndex` is set to -1 by default to prevent the background from covering other plugin DOM elements, such as grid lines.\n\n## Code Examples\n\n### Basic Background Color\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 200,\n  plugins: [\n    {\n      type: 'background',\n      width: '300px',\n      height: '200px',\n      backgroundColor: '#f0f2f5',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### Using a Background Image\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 200,\n  plugins: [\n    {\n      type: 'background',\n      width: '300px',\n      height: '200px',\n      backgroundImage:\n        'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*0Qq0ToQm1rEAAAAAAAAAAAAADmJ7AQ/original)',\n      backgroundRepeat: 'no-repeat',\n      backgroundSize: 'cover',\n      opacity: 0.2,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### Gradient Background\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 200,\n  plugins: [\n    {\n      type: 'background',\n      width: '300px',\n      height: '200px',\n      background: 'linear-gradient(45deg, #1890ff, #722ed1)',\n      opacity: '0.8',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### Dynamically Updating the Background\n\n```js\n// Initial configuration\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'background',\n      key: 'my-background',\n      backgroundColor: '#f0f2f5',\n    },\n  ],\n});\n\n// Subsequent updates\ngraph.updatePlugin({\n  key: 'my-background',\n  backgroundColor: '#e6f7ff',\n  transition: 'background 1s ease',\n});\n```\n\n## FAQs\n\n### 1. Background conflicts with other plugins?\n\nBy default, the `zIndex` of the background plugin is set to `-1` to ensure it is below other elements. If there are still conflicts, you can adjust the `zIndex` value:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'background',\n      zIndex: '-2', // Lower z-index to avoid conflicts\n    },\n  ],\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Background.zh.md",
    "content": "---\ntitle: 背景 Background\norder: 1\n---\n\n## 概述\n\n支持为图画布设置一个背景图片，让画布更有层次感、叙事性。\n\n## 使用场景\n\n这一插件主要用于：\n\n- 为图表设置统一的品牌背景色或图片\n- 通过背景区分不同的功能区域\n- 增强图表的视觉层次感和美观度\n\n## 基本用法\n\n以下是一个简单的 Background 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'background',\n      key: 'my-background', // 为插件指定标识符，方便动态更新\n      backgroundColor: '#f0f2f5', // 设置背景色\n      backgroundImage: 'url(https://example.com/bg.png)', // 设置背景图\n    },\n  ],\n});\n```\n\n## 配置项\n\nBackground 插件的配置项继承了所有 CSS 样式属性（[CSSStyleDeclaration](https://developer.mozilla.org/zh-CN/docs/Web/API/CSSStyleDeclaration)），因此你可以使用任何合法的 CSS 属性来配置背景。以下是一些常用配置：\n\n| 属性               | 描述                         | 类型   | 默认值            | 必选 |\n| ------------------ | ---------------------------- | ------ | ----------------- | ---- |\n| type               | 插件类型                     | string | `background`      | ✓    |\n| key                | 插件唯一标识符，用于后续更新 | string | -                 |      |\n| width              | 背景宽度                     | string | `100%`            |      |\n| height             | 背景高度                     | string | `100%`            |      |\n| backgroundColor    | 背景颜色                     | string | -                 |      |\n| backgroundImage    | 背景图片                     | string | -                 |      |\n| backgroundSize     | 背景尺寸                     | string | `cover`           |      |\n| backgroundPosition | 背景位置                     | string | -                 |      |\n| backgroundRepeat   | 背景重复方式                 | string | -                 |      |\n| opacity            | 背景透明度                   | string | -                 |      |\n| transition         | 过渡动画                     | string | `background 0.5s` |      |\n| zIndex             | 层叠顺序                     | string | -1                |      |\n\n> 注意：`zIndex` 默认为 -1，这是为了避免背景覆盖其他插件的 DOM 元素，如网格线。\n\n## 代码示例\n\n### 基础背景色\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 200,\n  plugins: [\n    {\n      type: 'background',\n      width: '300px',\n      height: '200px',\n      backgroundColor: '#f0f2f5',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### 使用背景图片\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 200,\n  plugins: [\n    {\n      type: 'background',\n      width: '300px',\n      height: '200px',\n      backgroundImage:\n        'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*0Qq0ToQm1rEAAAAAAAAAAAAADmJ7AQ/original)',\n      backgroundRepeat: 'no-repeat',\n      backgroundSize: 'cover',\n      opacity: 0.2,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### 渐变背景\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 200,\n  plugins: [\n    {\n      type: 'background',\n      width: '300px',\n      height: '200px',\n      background: 'linear-gradient(45deg, #1890ff, #722ed1)',\n      opacity: '0.8',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### 动态更新背景\n\n```js\n// 初始化时配置\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'background',\n      key: 'my-background',\n      backgroundColor: '#f0f2f5',\n    },\n  ],\n});\n\n// 后续更新\ngraph.updatePlugin({\n  key: 'my-background',\n  backgroundColor: '#e6f7ff',\n  transition: 'background 1s ease',\n});\n```\n\n## 常见问题\n\n### 1. 背景与其他插件冲突？\n\n默认情况下，背景插件的 `zIndex` 设为 `-1`，以确保它位于其他元素之下。如果仍有冲突，可以调整 `zIndex` 值：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'background',\n      zIndex: '-2', // 降低 z-index 避免冲突\n    },\n  ],\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/BubbleSets.en.md",
    "content": "---\ntitle: BubbleSets\norder: 2\n---\n\n## Overview\n\nThe BubbleSets plugin represents sets and their relationships by creating bubble shapes, helping users intuitively understand logical relationships such as intersections and unions between sets. It is a tool to enhance data visualization effects, especially suitable for displaying complex data set relationships.\n\n## Use Cases\n\nThe BubbleSets plugin is mainly suitable for the following scenarios:\n\n- Displaying relationships between sets (e.g., intersections, unions)\n- Enhancing the expressive ability of data visualization\n- Identifying specific sets of nodes or edges in complex network graphs\n\n## Basic Usage\n\nBelow is a simple example of initializing the BubbleSets plugin:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node1', 'node2'], // List of node IDs to be enclosed\n      label: true, // Whether to display labels\n    },\n  ],\n});\n```\n\n## Live Demo\n\n<embed src=\"@/common/api/plugins/bubble-sets.md\"></embed>\n\n## Configuration Options\n\n| Property                 | Description                                                                              | Type                                                           | Default Value | Required |\n| ------------------------ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------- | -------- |\n| type                     | Plugin type                                                                              | string                                                         | `bubble-sets` | ✓        |\n| key                      | Unique identifier for the plugin for subsequent updates                                  | string                                                         | -             |          |\n| members                  | Member elements, including nodes and edges, [example](#members)                          | string[]                                                       | -             | ✓        |\n| avoidMembers             | Elements to avoid, not included when drawing contours (currently supports setting nodes) | string[]                                                       | -             |          |\n| label                    | Whether to display labels                                                                | boolean                                                        | true          |          |\n| labelPlacement           | Label position                                                                           | `left` \\| `right` \\| `top` \\| `bottom` \\| `center` \\| `bottom` | `bottom`      |          |\n| labelBackground          | Whether to display background                                                            | boolean                                                        | false         |          |\n| labelPadding             | Label padding                                                                            | number \\| number[]                                             | 0             |          |\n| labelCloseToPath         | Whether the label is close to the contour, [example](#labelclosetopath)                  | boolean                                                        | true          |          |\n| labelAutoRotate          | Whether the label rotates with the contour, [example](#labelautorotate)                  | boolean                                                        | true          |          |\n| labelOffsetX             | Label x-axis offset                                                                      | number                                                         | 0             |          |\n| labelOffsetY             | Label y-axis offset                                                                      | number                                                         | 0             |          |\n| labelMaxWidth            | Maximum width of the text, automatically ellipsized if exceeded                          | number                                                         | -             |          |\n| maxRoutingIterations     | Maximum number of iterations for calculating paths between members                       | number                                                         | 100           |          |\n| maxMarchingIterations    | Maximum number of iterations for calculating contours                                    | number                                                         | 20            |          |\n| pixelGroup               | Number of pixels per potential area group, used to improve speed                         | number                                                         | 4             |          |\n| edgeR0                   | Edge radius parameter R0                                                                 | number                                                         | -             |          |\n| edgeR1                   | Edge radius parameter R1                                                                 | number                                                         | -             |          |\n| nodeR0                   | Node radius parameter R0                                                                 | number                                                         | -             |          |\n| nodeR1                   | Node radius parameter R1                                                                 | number                                                         | -             |          |\n| morphBuffer              | Morph buffer size                                                                        | number                                                         |               |          |\n| threshold                | Threshold                                                                                | number                                                         | -             |          |\n| memberInfluenceFactor    | Member influence factor                                                                  | number                                                         | -             |          |\n| edgeInfluenceFactor      | Edge influence factor                                                                    | number                                                         | -             |          |\n| nonMemberInfluenceFactor | Non-member influence factor                                                              | number                                                         | -             |          |\n| virtualEdges             | Whether to use virtual edges                                                             | boolean                                                        | -             |          |\n\n### members\n\nMember elements, including nodes and edges.\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2'],\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge-0',\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-1',\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-2',\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        id: 'edge-3',\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        id: 'edge-4',\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        id: 'edge-5',\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n  plugins: [\n    {\n      type: 'bubble-sets',\n      key: 'bubble-sets-a',\n      members: ['node-0', 'node-1', 'node-2'],\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### labelCloseToPath\n\nExample: Do not let the label stick to the contour\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      label: true, // Display label\n      labelText: 'cluster-a',\n      labelCloseToPath: false,\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge-0',\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-1',\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-2',\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        id: 'edge-3',\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        id: 'edge-4',\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        id: 'edge-5',\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      key: 'bubble-sets-a',\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      label: true, // Display label\n      labelText: 'cluster-a',\n      labelCloseToPath: false,\n    },\n  ],\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n### labelAutoRotate\n\nExample: Do not let the label rotate with the contour\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      label: true, // Display label\n      labelText: 'cluster-a',\n      labelAutoRotate: false,\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge-0',\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-1',\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-2',\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        id: 'edge-3',\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        id: 'edge-4',\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        id: 'edge-5',\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      key: 'bubble-sets-a',\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      label: true, // Display label\n      labelText: 'cluster-a',\n      labelAutoRotate: false,\n    },\n  ],\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n## Usage Examples\n\n### Basic BubbleSets\n\nThe simplest way is to use the preset configuration directly:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge-0',\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-1',\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-2',\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        id: 'edge-3',\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        id: 'edge-4',\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        id: 'edge-5',\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n  plugins: [\n    {\n      type: 'bubble-sets',\n      key: 'bubble-sets-a',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### Custom BubbleSets Style\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      fill: '#7e3feb', // Bubble fill color\n      fillOpacity: 0.1, // Fill opacity\n      stroke: '#7e3feb', // Border color\n      strokeOpacity: 1, // Border opacity\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge-0',\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-1',\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-2',\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        id: 'edge-3',\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        id: 'edge-4',\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        id: 'edge-5',\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      fill: '#7e3feb', // Bubble fill color\n      fillOpacity: 0.1, // Fill opacity\n      stroke: '#7e3feb', // Border color\n      strokeOpacity: 1, // Border opacity\n    },\n  ],\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n### Label Configuration\n\nYou can configure the position, background, offset, and other properties of the label to enhance the visualization effect.\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      label: true, // Display label\n      labelPlacement: 'top', // Label position\n      labelBackground: true, // Display label background\n      labelPadding: 5, // Label padding\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge-0',\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-1',\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-2',\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        id: 'edge-3',\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        id: 'edge-4',\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        id: 'edge-5',\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      key: 'bubble-sets-a',\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      label: true, // Display label\n      labelText: 'cluster-a',\n      labelPlacement: 'top', // Label position\n      labelBackground: true, // Display label background\n      labelPadding: 5, // Label padding\n    },\n  ],\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n## Practical Examples\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/collection.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const groupedNodesByCluster = data.nodes.reduce((acc, node) => {\n      const cluster = node.data.cluster;\n      acc[cluster] ||= [];\n      acc[cluster].push(node.id);\n      return acc;\n    }, {});\n\n    const createStyle = (baseColor) => ({\n      fill: baseColor,\n      stroke: baseColor,\n      labelFill: '#fff',\n      labelPadding: 2,\n      labelBackgroundFill: baseColor,\n      labelBackgroundRadius: 5,\n    });\n\n    const graph = new Graph({\n      container: 'container',\n      data,\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n      node: {\n        palette: { field: 'cluster' },\n      },\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        linkDistance: (d) => {\n          if (d.source === 'node0' || d.target === 'node0') {\n            return 200;\n          }\n          return 80;\n        },\n      },\n      plugins: [\n        {\n          key: 'bubble-sets-a',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['a'],\n          labelText: 'cluster-a',\n          ...createStyle('#1783FF'),\n        },\n        {\n          key: 'bubble-sets-b',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['b'],\n          labelText: 'cluster-b',\n          ...createStyle('#00C9C9'),\n        },\n        {\n          key: 'bubble-sets-c',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['c'],\n          labelText: 'cluster-c',\n          ...createStyle('#F08F56'),\n        },\n        {\n          key: 'bubble-sets-d',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['d'],\n          labelText: 'cluster-d',\n          ...createStyle('#D580FF'),\n        },\n      ],\n      autoFit: 'center',\n    });\n\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/BubbleSets.zh.md",
    "content": "---\ntitle: 气泡集 BubbleSets\norder: 2\n---\n\n## 概述\n\nBubbleSets 插件通过创建气泡形状来表示集合及其关系，帮助用户直观地理解集合间的交集、并集等逻辑关系。它是一种增强数据可视化效果的工具，特别适用于展示复杂的数据集合关系。\n\n## 使用场景\n\nBubbleSets 插件主要适用于以下场景：\n\n- 展示集合间的关系（如交集、并集）\n- 增强数据可视化的表达能力\n- 在复杂网络图中标识特定节点或边的集合\n\n## 基本用法\n\n以下是一个简单的 BubbleSets 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node1', 'node2'], // 需要包裹的节点 ID 列表\n      label: true, // 是否显示标签\n    },\n  ],\n});\n```\n\n## 在线体验\n\n<embed src=\"@/common/api/plugins/bubble-sets.md\"></embed>\n\n## 配置项\n\n| 属性                     | 描述                                                             | 类型                                                           | 默认值        | 必选 |\n| ------------------------ | ---------------------------------------------------------------- | -------------------------------------------------------------- | ------------- | ---- |\n| type                     | 插件类型                                                         | string                                                         | `bubble-sets` | ✓    |\n| key                      | 插件唯一标识符，用于后续更新                                     | string                                                         | -             |      |\n| members                  | 成员元素，包括节点和边，[示例](#members)                         | string[]                                                       | -             | ✓    |\n| avoidMembers             | 需要避开的元素，在绘制轮廓时不会包含这些元素（目前支持设置节点） | string[]                                                       | -             |      |\n| label                    | 是否显示标签                                                     | boolean                                                        | true          |      |\n| labelPlacement           | 标签位置                                                         | `left` \\| `right` \\| `top` \\| `bottom` \\| `center` \\| `bottom` | `bottom`      |      |\n| labelBackground          | 是否显示背景                                                     | boolean                                                        | false         |      |\n| labelPadding             | 标签内边距                                                       | number \\| number[]                                             | 0             |      |\n| labelCloseToPath         | 标签是否贴合轮廓，[示例](#labelclosetopath)                      | boolean                                                        | true          |      |\n| labelAutoRotate          | 标签是否跟随轮廓旋转，[示例](#labelautorotate)                   | boolean                                                        | true          |      |\n| labelOffsetX             | 标签 x 轴偏移量                                                  | number                                                         | 0             |      |\n| labelOffsetY             | 标签 y 轴偏移量                                                  | number                                                         | 0             |      |\n| labelMaxWidth            | 文本的最大宽度，超出会自动省略                                   | number                                                         | -             |      |\n| maxRoutingIterations     | 计算成员之间路径的最大迭代次数                                   | number                                                         | 100           |      |\n| maxMarchingIterations    | 计算轮廓的最大迭代次数                                           | number                                                         | 20            |      |\n| pixelGroup               | 每个潜在区域组的像素数，用于提高速度                             | number                                                         | 4             |      |\n| edgeR0                   | 边的半径参数 R0                                                  | number                                                         | -             |      |\n| edgeR1                   | 边的半径参数 R1                                                  | number                                                         | -             |      |\n| nodeR0                   | 节点的半径参数 R0                                                | number                                                         | -             |      |\n| nodeR1                   | 节点的半径参数 R1                                                | number                                                         | -             |      |\n| morphBuffer              | 形态缓冲区大小                                                   | number                                                         |               |      |\n| threshold                | 阈值                                                             | number                                                         | -             |      |\n| memberInfluenceFactor    | 成员影响因子                                                     | number                                                         | -             |      |\n| edgeInfluenceFactor      | 边影响因子                                                       | number                                                         | -             |      |\n| nonMemberInfluenceFactor | 非成员影响因子                                                   | number                                                         | -             |      |\n| virtualEdges             | 是否使用虚拟边                                                   | boolean                                                        | -             |      |\n\n### members\n\n成员元素，包括节点和边。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2'],\n    },\n  ],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge-0',\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-1',\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-2',\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        id: 'edge-3',\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        id: 'edge-4',\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        id: 'edge-5',\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n  plugins: [\n    {\n      type: 'bubble-sets',\n      key: 'bubble-sets-a',\n      members: ['node-0', 'node-1', 'node-2'],\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### labelCloseToPath\n\n示例：不让 label 贴合轮廓\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      label: true, // 显示标签\n      labelText: 'cluster-a',\n      labelCloseToPath: false,\n    },\n  ],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge-0',\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-1',\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-2',\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        id: 'edge-3',\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        id: 'edge-4',\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        id: 'edge-5',\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      key: 'bubble-sets-a',\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      label: true, // 显示标签\n      labelText: 'cluster-a',\n      labelCloseToPath: false,\n    },\n  ],\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n### labelAutoRotate\n\n示例：不让label标签跟随轮廓旋转\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      label: true, // 显示标签\n      labelText: 'cluster-a',\n      labelAutoRotate: false,\n    },\n  ],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge-0',\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-1',\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-2',\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        id: 'edge-3',\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        id: 'edge-4',\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        id: 'edge-5',\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      key: 'bubble-sets-a',\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      label: true, // 显示标签\n      labelText: 'cluster-a',\n      labelAutoRotate: false,\n    },\n  ],\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n## 使用示例\n\n### 基础 BubbleSets\n\n最简单的方式是直接使用预设配置：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n    },\n  ],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge-0',\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-1',\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-2',\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        id: 'edge-3',\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        id: 'edge-4',\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        id: 'edge-5',\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n  plugins: [\n    {\n      type: 'bubble-sets',\n      key: 'bubble-sets-a',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### 自定义 BubbleSets 样式\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      fill: '#7e3feb', // 气泡填充颜色\n      fillOpacity: 0.1, // 填充透明度\n      stroke: '#7e3feb', // 边框颜色\n      strokeOpacity: 1, // 边框透明度\n    },\n  ],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge-0',\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-1',\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-2',\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        id: 'edge-3',\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        id: 'edge-4',\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        id: 'edge-5',\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      fill: '#7e3feb', // 气泡填充颜色\n      fillOpacity: 0.1, // 填充透明度\n      stroke: '#7e3feb', // 边框颜色\n      strokeOpacity: 1, // 边框透明度\n    },\n  ],\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n### 标签配置\n\n您可以配置标签的位置、背景、偏移量等属性，以增强可视化效果。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      label: true, // 显示标签\n      labelPlacement: 'top', // 标签位置\n      labelBackground: true, // 显示标签背景\n      labelPadding: 5, // 标签内边距\n    },\n  ],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge-0',\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-1',\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        id: 'edge-2',\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        id: 'edge-3',\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        id: 'edge-4',\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        id: 'edge-5',\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      key: 'bubble-sets-a',\n      type: 'bubble-sets',\n      members: ['node-0', 'node-1', 'node-2', 'node-3'],\n      label: true, // 显示标签\n      labelText: 'cluster-a',\n      labelPlacement: 'top', // 标签位置\n      labelBackground: true, // 显示标签背景\n      labelPadding: 5, // 标签内边距\n    },\n  ],\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/collection.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const groupedNodesByCluster = data.nodes.reduce((acc, node) => {\n      const cluster = node.data.cluster;\n      acc[cluster] ||= [];\n      acc[cluster].push(node.id);\n      return acc;\n    }, {});\n\n    const createStyle = (baseColor) => ({\n      fill: baseColor,\n      stroke: baseColor,\n      labelFill: '#fff',\n      labelPadding: 2,\n      labelBackgroundFill: baseColor,\n      labelBackgroundRadius: 5,\n    });\n\n    const graph = new Graph({\n      container: 'container',\n      data,\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n      node: {\n        palette: { field: 'cluster' },\n      },\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        linkDistance: (d) => {\n          if (d.source === 'node0' || d.target === 'node0') {\n            return 200;\n          }\n          return 80;\n        },\n      },\n      plugins: [\n        {\n          key: 'bubble-sets-a',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['a'],\n          labelText: 'cluster-a',\n          ...createStyle('#1783FF'),\n        },\n        {\n          key: 'bubble-sets-b',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['b'],\n          labelText: 'cluster-b',\n          ...createStyle('#00C9C9'),\n        },\n        {\n          key: 'bubble-sets-c',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['c'],\n          labelText: 'cluster-c',\n          ...createStyle('#F08F56'),\n        },\n        {\n          key: 'bubble-sets-d',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['d'],\n          labelText: 'cluster-d',\n          ...createStyle('#D580FF'),\n        },\n      ],\n      autoFit: 'center',\n    });\n\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Contextmenu.en.md",
    "content": "---\ntitle: Contextmenu\norder: 3\n---\n\n## Overview\n\nThe context menu, also known as the right-click menu, is a menu that appears when a user clicks on a specific area. It supports triggering custom events before and after clicking. Through the context menu, specific element operations can be integrated, making it convenient to control a particular item when needed.\n\n## Use Cases\n\nThis plugin is mainly used for:\n\n- Various interactions with elements: viewing nodes, viewing edges, deleting nodes, etc.\n\n## Basic Usage\n\nBelow is a simple example of initializing the Contextmenu plugin:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'contextmenu',\n      // Enable right-click menu only on nodes, by default all elements are enabled\n      enable: (e) => e.targetType === 'node',\n      getItems: () => {\n        return [{ name: 'View Details', value: 'detail' }];\n      },\n      onClick: (value) => {\n        if (value === 'detail') console.log('Display node details');\n      },\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Property       | Description                                                                                                               | Type                                                                              | Default Value    | Required |\n| -------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ---------------- | -------- |\n| className      | Additional class name for the menu DOM                                                                                    | string                                                                            | `g6-contextmenu` |          |\n| trigger        | How to trigger the right-click menu: `contextmenu` for right-click, `click` for click                                     | `click` \\| `contextmenu`                                                          | `contextmenu`    |          |\n| offset         | Offset of the menu display in X and Y directions                                                                          | [number, number]                                                                  | [4, 4]           |          |\n| onClick        | Callback method triggered after the menu is clicked, [example](#onclick)                                                  | (value: string, target: HTMLElement, current: Element) => void                    | -                |          |\n| getItems       | Returns the list of menu items, supports `Promise` type return value. It is a shortcut configuration for `getContent`     | (event: IElementEvent) => [Item](#item)[] \\| Promise<[Item](#item)[]>             | -                |          |\n| getContent     | Returns the content of the menu, supports `Promise` type return value, can also use `getItems` for shortcut configuration | (event: IElementEvent) => HTMLElement \\| string \\| Promise<HTMLElement \\| string> | -                |          |\n| loadingContent | Menu content used when `getContent` returns a `Promise`                                                                   | HTMLElement \\| string                                                             | -                |          |\n| enable         | Whether it is available, determines whether the right-click menu is supported by parameters, by default all are available | boolean \\| (event: IElementEvent) => boolean                                      | true             |          |\n\n### Item\n\nEach menu item (Item) contains the following properties:\n\n| Property | Description                          | Type     | Required |\n| -------- | ------------------------------------ | -------- | -------- |\n| name     | Name displayed for the menu item     | `string` | ✓        |\n| value    | Value corresponding to the menu item | `string` | ✓        |\n\n### onClick\n\nThis function is triggered after clicking a menu item, and the function has three parameters:\n\n- value: Corresponds to the value of the menu item\n- target: The DOM node of the menu item container\n- current: The element that triggered the menu item, for example, if it is a node, you can use `current` to get the node information (id), or to modify the element\n\n## Code Examples\n\n### Basic Right-click Menu\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  layout: { type: 'grid' },\n  plugins: [\n    {\n      type: 'contextmenu',\n      trigger: 'contextmenu', // 'click' or 'contextmenu'\n      onClick: (value, target, current) => {\n        alert('You have clicked the「' + value + '」item');\n      },\n      getItems: () => {\n        return [\n          { name: 'View Details', value: 'detail' },\n          { name: 'Delete', value: 'delete' },\n        ];\n      },\n    },\n  ],\n});\n```\n\n### Edge Right-click Menu\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  layout: { type: 'grid' },\n  plugins: [\n    {\n      type: 'contextmenu',\n      trigger: 'contextmenu',\n      getItems: () => {\n        return [{ name: 'Change Start Point', value: 'change' }];\n      },\n      onClick: (value) => {\n        if (value === 'change') console.log('Execute change start point operation here');\n      },\n      // Enable right-click menu only on edges\n      enable: (e) => e.targetType === 'edge',\n    },\n  ],\n});\n```\n\n### Asynchronous Loading of Menu Items\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  layout: { type: 'grid' },\n  plugins: [\n    {\n      type: 'contextmenu',\n      trigger: 'contextmenu',\n      getItems: async () => {\n        // Toolbar configuration can be obtained from the server or other asynchronous sources\n        const response = await fetch('/api/contextmenu-config');\n        const items = await response.json();\n        return items;\n      },\n      // Enable right-click menu only on nodes\n      enable: (e) => e.targetType === 'node',\n    },\n  ],\n});\n```\n\n### Dynamic Control of Menu Items\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  layout: { type: 'grid' },\n  plugins: [\n    {\n      type: 'contextmenu',\n      trigger: 'contextmenu',\n      getItems: (e) => {\n        if (e.target.id === 'node-1') {\n          return [\n            {\n              name: 'Delete Node',\n              value: 'delete',\n            },\n          ];\n        }\n        if (e.target.type === 'edge') {\n          return [\n            {\n              name: 'Move Edge',\n              value: 'move',\n            },\n          ];\n        }\n        return [];\n      },\n    },\n  ],\n});\n```\n\n## Practical Examples\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'contextmenu',\n      trigger: 'contextmenu', // 'click' or 'contextmenu'\n      onClick: (v) => {\n        alert('You have clicked the「' + v + '」item');\n      },\n      getItems: () => {\n        return [\n          { name: '展开一度关系', value: 'spread' },\n          { name: '查看详情', value: 'detail' },\n        ];\n      },\n      enable: (e) => e.targetType === 'node',\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Contextmenu.zh.md",
    "content": "---\ntitle: 上下文菜单 Contextmenu\norder: 3\n---\n\n## 概述\n\n上下文菜单（Contextmenu），也被称为右键菜单，是当用户在某个特定区域上点击后出现的一个菜单。支持在点击前后，触发自定义事件。通过上下文菜单，可以将一些具体元素的操作集成在其中，方便对某一项来进行需要时的单独控制。\n\n## 使用场景\n\n这一插件主要用于：\n\n- 元素的各种交互：查看节点、查看边、删除节点等等交互操作\n\n## 基本用法\n\n以下是一个简单的 Contextmenu 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'contextmenu',\n      // 只在节点上开启右键菜单，默认全部元素都开启\n      enable: (e) => e.targetType === 'node',\n      getItems: () => {\n        return [{ name: '查看详情', value: 'detail' }];\n      },\n      onClick: (value) => {\n        if (value === 'detail') console.log('展示节点详情');\n      },\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 属性           | 描述                                                                            | 类型                                                                              | 默认值           | 必选 |\n| -------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ---------------- | ---- |\n| className      | 给菜单的 DOM 追加的类名                                                         | string                                                                            | `g6-contextmenu` |      |\n| trigger        | 如何触发右键菜单：`contextmenu` 表示右键触发，`click` 表示点击触发              | `click` \\| `contextmenu`                                                          | `contextmenu`    |      |\n| offset         | 菜单显式 X、Y 方向的偏移量                                                      | [number, number]                                                                  | [4, 4]           |      |\n| onClick        | 当菜单被点击后，触发的回调方法，[示例](#onclick)                                | (value: string, target: HTMLElement, current: Element) => void                    | -                |      |\n| getItems       | 返回菜单的项目列表，支持 `Promise` 类型的返回值。是 `getContent` 的快捷配置     | (event: IElementEvent) => [Item](#item)[] \\| Promise<[Item](#item)[]>             | -                |      |\n| getContent     | 返回菜单的内容，支持 `Promise` 类型的返回值，也可以使用 `getItems` 进行快捷配置 | (event: IElementEvent) => HTMLElement \\| string \\| Promise<HTMLElement \\| string> | -                |      |\n| loadingContent | 当 `getContent` 返回一个 `Promise` 时，使用的菜单内容                           | HTMLElement \\| string                                                             | -                |      |\n| enable         | 是否可用，通过参数判断是否支持右键菜单，默认是全部可用                          | boolean \\| (event: IElementEvent) => boolean                                      | true             |      |\n\n### Item\n\n每个菜单项目 (Item) 包含以下属性：\n\n| 属性  | 描述             | 类型     | 必选 |\n| ----- | ---------------- | -------- | ---- |\n| name  | 菜单项显示的名字 | `string` | ✓    |\n| value | 菜单项对应的值   | `string` | ✓    |\n\n### onClick\n\n点击菜单项后会触发该函数，函数有三个参数：\n\n- value: 对应菜单项的 value\n- target: 对应菜单项容器的 dom 节点\n- current: 对应触发菜单项的元素，例如是节点则可以通过 `current` 来获取到节点的信息(id)等，或者来对元素进行修改\n\n## 代码示例\n\n### 基础右键菜单\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  layout: { type: 'grid' },\n  plugins: [\n    {\n      type: 'contextmenu',\n      trigger: 'contextmenu', // 'click' or 'contextmenu'\n      onClick: (value, target, current) => {\n        alert('You have clicked the「' + v + '」item');\n      },\n      getItems: () => {\n        return [\n          { name: '查看详情', value: 'detail' },\n          { name: '删除', value: 'delete' },\n        ];\n      },\n    },\n  ],\n});\n```\n\n### 边的右键菜单\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  layout: { type: 'grid' },\n  plugins: [\n    {\n      type: 'contextmenu',\n      trigger: 'contextmenu',\n      getItems: () => {\n        return [{ name: '变更起点', value: 'change' }];\n      },\n      onClick: (value) => {\n        if (value === 'change') console.log('这里执行变更起点操作');\n      },\n      // 仅在边上开启右键菜单\n      enable: (e) => e.targetType === 'edge',\n    },\n  ],\n});\n```\n\n### 异步加载菜单项\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  layout: { type: 'grid' },\n  plugins: [\n    {\n      type: 'contextmenu',\n      trigger: 'contextmenu',\n      getItems: async () => {\n        // 可以从服务器或其他异步源获取工具栏配置\n        const response = await fetch('/api/contextmenu-config');\n        const items = await response.json();\n        return items;\n      },\n      // 仅在边上开启右键菜单\n      enable: (e) => e.targetType === 'node',\n    },\n  ],\n});\n```\n\n### 动态控制菜单项\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  layout: { type: 'grid' },\n  plugins: [\n    {\n      type: 'contextmenu',\n      trigger: 'contextmenu',\n      getItems: (e) => {\n        if (e.target.id === 'node-1') {\n          return [\n            {\n              name: '删除节点',\n              value: 'delete',\n            },\n          ];\n        }\n        if (e.target.type === 'edge') {\n          return [\n            {\n              name: '移动边',\n              value: 'move',\n            },\n          ];\n        }\n        return [];\n      },\n    },\n  ],\n});\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'contextmenu',\n      trigger: 'contextmenu', // 'click' or 'contextmenu'\n      onClick: (v) => {\n        alert('You have clicked the「' + v + '」item');\n      },\n      getItems: () => {\n        return [\n          { name: '展开一度关系', value: 'spread' },\n          { name: '查看详情', value: 'detail' },\n        ];\n      },\n      enable: (e) => e.targetType === 'node',\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/EdgeBundling.en.md",
    "content": "---\ntitle: EdgeBundling\norder: 4\n---\n\n## Overview\n\nEdge bundling is a graph visualization technique used to reduce visual clutter in complex network graphs and to reveal high-level patterns and structures in the graph. Its purpose is to bundle adjacent edges together.\n\nThe edge bundling plugin provided in G6 is based on the implementation of the [FEDB (Force-Directed Edge Bundling for Graph Visualization)](https://classes.engineering.wustl.edu/cse557/readings/holten-edgebundling.pdf) paper: modeling edges as flexible springs that can attract each other and bundling them through a self-organizing process.\n\n## Use Cases\n\nThe edge bundling plugin is mainly suitable for the following scenarios:\n\n- Reducing visual clutter in complex network graphs\n- Revealing high-level patterns and structures in the graph\n- Improving the readability and aesthetics of large-scale graph data\n\n## Basic Usage\n\nBelow is a simple example of initializing the EdgeBundling plugin:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-bundling',\n      bundleThreshold: 0.6,\n      cycles: 6,\n      divisions: 3,\n      divRate: 2,\n      iterations: 90,\n      iterRate: 2 / 3,\n      K: 0.1,\n      lambda: 0.1,\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Property        | Description                                                                                                                                                                                            | Type   | Default Value   | Required |\n| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | --------------- | -------- |\n| type            | Plugin type, used to identify the plugin as an edge bundling plugin                                                                                                                                    | string | `edge-bundling` | ✓        |\n| key             | Unique identifier for the plugin, can be used to get the plugin instance or update plugin options                                                                                                      | string | -               |          |\n| bundleThreshold | Edge compatibility threshold, determines which edges should be bundled together, the larger the value, the fewer edges are bundled, [example](#bundlethreshold)                                        | number | 0.6             |          |\n| cycles          | Number of simulation cycles, controls the number of execution rounds of the edge bundling simulation                                                                                                   | number | 6               |          |\n| divisions       | Initial number of cut points, in subsequent cycles, the number of cut points will gradually increase according to divRate, affecting the degree of edge subdivision                                    | number | 1               |          |\n| divRate         | Growth rate of cut points, determines the growth rate of cut points in each cycle                                                                                                                      | number | 2               |          |\n| iterations      | Specifies the number of iterations executed in the first cycle, in subsequent cycles, the number of iterations will gradually decrease according to iterRate, affecting the accuracy of the simulation | number | 90              |          |\n| iterRate        | Iteration decrement rate, controls the reduction ratio of iterations in each cycle                                                                                                                     | number | 2/3             |          |\n| K               | Edge strength, affects the attraction and repulsion between edges, [example](#k)                                                                                                                       | number | 0.1             |          |\n| lambda          | Initial step size, in subsequent cycles, the step size will double increment, affecting the magnitude of node movement during edge bundling                                                            | number | 0.1             |          |\n\n### bundleThreshold\n\nEdge compatibility threshold, determines which edges should be bundled together. The larger the value, the fewer edges are bundled, and vice versa.\n\n- A lower bundleThreshold value (e.g., 0.4) will cause more edges to be bundled together, forming a more pronounced bundling effect.\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-bundling',\n      bundleThreshold: 0.4, // Lower edge compatibility threshold\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*_0iOSZnijrMAAAAAAAAAAAAAemJ7AQ/original\" width=\"240\" alt=\"Lower edge compatibility threshold\">\n\n- A higher bundleThreshold value (e.g., 0.8) will cause fewer edges to be bundled together, maintaining more independent edges.\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-bundling',\n      bundleThreshold: 0.8, // Higher edge compatibility threshold\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*WNHMT4L4AfkAAAAAAAAAAAAAemJ7AQ/original\" width=\"240\" alt=\"Higher edge compatibility threshold\">\n\n### K\n\nEdge strength, affects the attraction and repulsion between edges. A higher K value will make the attraction between edges stronger, resulting in a tighter bundling effect.\n\n- A lower K value (e.g., 0.05) will make the attraction between edges weaker, resulting in a weaker bundling effect.\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-bundling',\n      K: 0.05, // Lower edge strength\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*wlHVSb2515gAAAAAAAAAAAAAemJ7AQ/original\" width=\"240\" alt=\"Lower edge strength\">\n\n- A higher K value (e.g., 0.2) will make the attraction between edges stronger, resulting in a more pronounced bundling effect.\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-bundling',\n      K: 0.2, // Higher edge strength\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*4DAMQLvtrk4AAAAAAAAAAAAAemJ7AQ/original\" width=\"240\" alt=\"Higher edge strength\">\n\n## Code Examples\n\n### Basic Edge Bundling\n\nThe simplest way is to use the preset configuration directly:\n\n```js\nconst graph = new Graph({\n  // Other configurations...\n  plugins: ['edge-bundling'],\n});\n```\n\n### Custom Styles\n\nYou can customize the parameters of edge bundling as needed:\n\n```js\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'edge-bundling',\n      bundleThreshold: 0.8, // Higher edge compatibility threshold\n      cycles: 8, // More simulation cycles\n      K: 0.2, // Stronger edge strength\n    },\n  ],\n});\n```\n\n### Dynamic Update of Edge Bundling\n\nUse the key identifier to dynamically update edge bundling properties at runtime:\n\n```js\n// Initial configuration\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'edge-bundling',\n      key: 'my-edge-bundling',\n      bundleThreshold: 0.6,\n    },\n  ],\n});\n\n// Subsequent dynamic update\ngraph.updatePlugin({\n  key: 'my-edge-bundling',\n  bundleThreshold: 0.8, // Update edge compatibility threshold\n  cycles: 10, // Update number of simulation cycles\n});\n```\n\n## Practical Examples\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      layout: {\n        type: 'circular',\n      },\n      node: { style: { size: 20 } },\n      behaviors: ['drag-canvas', 'drag-element'],\n      plugins: [\n        {\n          key: 'edge-bundling',\n          type: 'edge-bundling',\n          bundleThreshold: 0.1,\n        },\n      ],\n    });\n\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/EdgeBundling.zh.md",
    "content": "---\ntitle: 边绑定 EdgeBundling\norder: 4\n---\n\n## 概述\n\n边绑定（Edge Bundling）是一种图可视化技术，用于减少复杂网络图中的视觉混乱，并展示图中的高级别模式和结构。其目的是将相邻的边捆绑在一起。\n\nG6 中提供的边绑定插件是基于 [FEDB（Force-Directed Edge Bundling for Graph Visualization）](https://classes.engineering.wustl.edu/cse557/readings/holten-edgebundling.pdf)论文的实现：将边建模为可以相互吸引的柔性弹簧，通过自组织的方式进行捆绑。\n\n## 使用场景\n\n边绑定插件主要适用于以下场景：\n\n- 减少复杂网络图中的视觉混乱\n- 揭示图中的高级别模式和结构\n- 提高大规模图数据的可读性和美观性\n\n## 基本用法\n\n以下是一个简单的 EdgeBundling 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-bundling',\n      bundleThreshold: 0.6,\n      cycles: 6,\n      divisions: 3,\n      divRate: 2,\n      iterations: 90,\n      iterRate: 2 / 3,\n      K: 0.1,\n      lambda: 0.1,\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 属性            | 描述                                                                                               | 类型   | 默认值          | 必选 |\n| --------------- | -------------------------------------------------------------------------------------------------- | ------ | --------------- | ---- |\n| type            | 插件类型，用于标识该插件为边捆绑插件                                                               | string | `edge-bundling` | ✓    |\n| key             | 插件的唯一标识，可用于获取插件实例或更新插件选项                                                   | string | -               |      |\n| bundleThreshold | 边兼容性阈值，该值决定了哪些边应该被绑定在一起，值越大，绑定的边越少，[示例](#bundlethreshold)     | number | 0.6             |      |\n| cycles          | 模拟周期数，控制边捆绑模拟的执行轮数                                                               | number | 6               |      |\n| divisions       | 初始切割点数，在后续的周期中，切割点数将根据 divRate 逐步递增，影响边的细分程度                    | number | 1               |      |\n| divRate         | 切割点数增长率，决定了每一轮周期中切割点数的增长幅度                                               | number | 2               |      |\n| iterations      | 指定在第一个周期中执行的迭代次数，在后续的周期中，迭代次数将根据 iterRate 逐步递减，影响模拟的精度 | number | 90              |      |\n| iterRate        | 迭代次数递减率，控制每一轮周期中迭代次数的减少比例                                                 | number | 2\\/3            |      |\n| K               | 边的强度，影响边之间的吸引力和排斥力，[示例](#k)                                                   | number | 0.1             |      |\n| lambda          | 初始步长，在后续的周期中，步长将双倍递增，影响边捆绑过程中节点移动的幅度                           | number | 0.1             |      |\n\n### bundleThreshold\n\n边兼容性阈值，该值决定了哪些边应该被绑定在一起。值越大，绑定的边越少，反之则绑定的边越多。\n\n- 较低的 bundleThreshold 值（如 0.4）会使更多的边被绑定在一起，形成更明显的捆绑效果。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-bundling',\n      bundleThreshold: 0.4, // 较低的边兼容性阈值\n    },\n  ],\n});\n```\n\n效果如下：\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*_0iOSZnijrMAAAAAAAAAAAAAemJ7AQ/original\" width=\"240\" alt=\"较低的边兼容性阈值\">\n\n- 较高的 bundleThreshold 值（如 0.8）会使较少的边被绑定在一起，保持更多的独立边。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-bundling',\n      bundleThreshold: 0.8, // 较高的边兼容性阈值\n    },\n  ],\n});\n```\n\n效果如下：\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*WNHMT4L4AfkAAAAAAAAAAAAAemJ7AQ/original\" width=\"240\" alt=\"较高的边兼容性阈值\">\n\n### K\n\n边的强度，影响边之间的吸引力和排斥力。较高的 K 值会使边之间的吸引力更强，从而形成更紧密的捆绑效果。\n\n- 较低的 K 值（如 0.05）会使边之间的吸引力较弱，边的捆绑效果较弱。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-bundling',\n      K: 0.05, // 较低的边强度\n    },\n  ],\n});\n```\n\n效果如下：\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*wlHVSb2515gAAAAAAAAAAAAAemJ7AQ/original\" width=\"240\" alt=\"较低的边强度\">\n\n- 较高的 K 值（如 0.2）会使边之间的吸引力较强，边的捆绑效果更明显。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-bundling',\n      K: 0.2, // 较高的边强度\n    },\n  ],\n});\n```\n\n效果如下：\n<img src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*4DAMQLvtrk4AAAAAAAAAAAAAemJ7AQ/original\" width=\"240\" alt=\"较高的边强度\">\n\n## 代码示例\n\n### 基础边绑定\n\n最简单的方式是直接使用预设配置：\n\n```js\nconst graph = new Graph({\n  // 其他配置...\n  plugins: ['edge-bundling'],\n});\n```\n\n### 自定义样式\n\n您可以根据需要自定义边绑定的参数：\n\n```js\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'edge-bundling',\n      bundleThreshold: 0.8, // 更高的边兼容性阈值\n      cycles: 8, // 更多模拟周期\n      K: 0.2, // 更强的边强度\n    },\n  ],\n});\n```\n\n### 动态更新边绑定\n\n使用 key 标识符可以在运行时动态更新边绑定属性：\n\n```js\n// 初始化配置\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'edge-bundling',\n      key: 'my-edge-bundling',\n      bundleThreshold: 0.6,\n    },\n  ],\n});\n\n// 后续动态更新\ngraph.updatePlugin({\n  key: 'my-edge-bundling',\n  bundleThreshold: 0.8, // 更新边兼容性阈值\n  cycles: 10, // 更新模拟周期数\n});\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      layout: {\n        type: 'circular',\n      },\n      node: { style: { size: 20 } },\n      behaviors: ['drag-canvas', 'drag-element'],\n      plugins: [\n        {\n          key: 'edge-bundling',\n          type: 'edge-bundling',\n          bundleThreshold: 0.1,\n        },\n      ],\n    });\n\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/EdgeFilterLens.en.md",
    "content": "---\ntitle: EdgeFilterLens\norder: 5\n---\n\n## Overview\n\nThe Edge Filter Lens plugin allows you to keep the edges of interest within the lens range, while other edges will not be displayed in that range. This is an important visualization exploration tool that can help users focus on edge relationships in specific areas.\n\n## Use Cases\n\n- Need to focus on viewing edge relationships in local areas\n- Highlight connections between specific nodes in complex networks\n\n## Basic Usage\n\nBelow is a simple example of initializing the EdgeFilterLens plugin:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-filter-lens',\n      trigger: 'pointermove', // Follow mouse movement\n      r: 60, // Set lens radius\n      nodeType: 'both', // Edge display condition\n    },\n  ],\n});\n```\n\n## Online Experience\n\n<embed src=\"@/common/api/plugins/edge-filter-lens.md\"></embed>\n\n## Configuration Options\n\n| Property       | Description                                                                                                                                                                                                                                                                                                                                                                                 | Type                                                                                                                                                                    | Default Value                               | Required |\n| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | -------- |\n| type           | Plugin type                                                                                                                                                                                                                                                                                                                                                                                 | string                                                                                                                                                                  | `edge-filter-lens`                          | ✓        |\n| key            | Unique identifier for the plugin, can be used to get the plugin instance or update plugin options                                                                                                                                                                                                                                                                                           | string                                                                                                                                                                  | -                                           |          |\n| trigger        | Method to move the lens:<br/>- `pointermove`: The lens always follows the mouse movement <br/>- `click`: Move the lens to the click position when clicking on the canvas <br/>- `drag`: Move the lens by dragging                                                                                                                                                                           | `pointermove` \\| `click` \\| `drag`                                                                                                                                      | `pointermove`                               |          |\n| r              | Radius of the lens                                                                                                                                                                                                                                                                                                                                                                          | number                                                                                                                                                                  | 60                                          |          |\n| maxR           | Maximum radius of the lens                                                                                                                                                                                                                                                                                                                                                                  | number                                                                                                                                                                  | Half of the smaller dimension of the canvas |          |\n| minR           | Minimum radius of the lens                                                                                                                                                                                                                                                                                                                                                                  | number                                                                                                                                                                  | 0                                           |          |\n| scaleRBy       | Method to scale the lens radius: `wheel`: Scale the lens radius by the wheel                                                                                                                                                                                                                                                                                                                | `wheel`                                                                                                                                                                 | -                                           |          |\n| nodeType       | Edge display condition:<br/> - `both`: The edge is displayed only when both the source and target nodes are in the lens <br/> - `source`: The edge is displayed only when the source node is in the lens<br/> - `target`: The edge is displayed only when the target node is in the lens <br/> - `either`: The edge is displayed as long as either the source or target node is in the lens | `both` \\| `source` \\| `target` \\| `either`                                                                                                                              | `both`                                      |          |\n| filter         | Filter out elements that are never displayed in the lens                                                                                                                                                                                                                                                                                                                                    | (id: string, elementType: `node` \\| `edge` \\| `combo`) => boolean                                                                                                       | () => true                                  |          |\n| style          | Style of the lens, [configuration options](#style)                                                                                                                                                                                                                                                                                                                                          | object                                                                                                                                                                  |                                             |          |\n| nodeStyle      | Style of nodes in the lens                                                                                                                                                                                                                                                                                                                                                                  | [NodeStyle](/en/manual/element/node/base-node#style) \\| ((datum: [NodeData](/en/manual/data#节点数据nodedata)) => [NodeStyle](/en/manual/element/node/base-node#style)) | `{ label: false }`                          |          |\n| edgeStyle      | Style of edges in the lens                                                                                                                                                                                                                                                                                                                                                                  | [EdgeStyle](/en/manual/element/edge/base-edge#style) \\| ((datum: [EdgeData](/en/manual/data#边数据edgedata)) => [EdgeStyle](/en/manual/element/edge/base-edge#style))   | `{ label: true }`                           |          |\n| preventDefault | Whether to prevent default events                                                                                                                                                                                                                                                                                                                                                           | boolean                                                                                                                                                                 | true                                        |          |\n\n### style\n\nStyle properties of the circular lens.\n\n| Property      | Description        | Type                          | Default Value |\n| ------------- | ------------------ | ----------------------------- | ------------- |\n| fill          | Fill color         | string \\| Pattern \\| null     | `#fff`        |\n| stroke        | Stroke color       | string \\| Pattern \\| null     | `#000`        |\n| opacity       | Overall opacity    | number \\| string              | 1             |\n| fillOpacity   | Fill opacity       | number \\| string              | 0.8           |\n| strokeOpacity | Stroke opacity     | number \\| string              | -             |\n| lineWidth     | Line width         | number \\| string              | 2             |\n| lineCap       | Line cap style     | `butt` \\| `round` \\| `square` | -             |\n| lineJoin      | Line join style    | `miter` \\| `round` \\| `bevel` | -             |\n| shadowColor   | Shadow color       | string                        | -             |\n| shadowBlur    | Shadow blur degree | number                        | -             |\n| shadowOffsetX | Shadow X offset    | number                        | -             |\n| shadowOffsetY | Shadow Y offset    | number                        | -             |\n\nFor complete style properties, refer to [Element - Node - Built-in Node - General Style Properties - style](/en/manual/element/node/base-node#style)\n\n## Code Examples\n\n### Basic Usage\n\nThe simplest configuration method:\n\n```js\nconst graph = new Graph({\n  plugins: ['edge-filter-lens'],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 300,\n  data: {\n    nodes: [\n      // Upper evacuation area\n      { id: 'node1', style: { x: 150, y: 60, label: 'Node 1' } },\n      { id: 'node2', style: { x: 100, y: 40, label: 'Node 2' } },\n      { id: 'node3', style: { x: 200, y: 35, label: 'Node 3' } },\n      { id: 'node4', style: { x: 150, y: 30, label: 'Node 4' } },\n\n      // Middle area\n      { id: 'node5', style: { x: 220, y: 140, label: 'Node 5' } },\n      { id: 'node6', style: { x: 280, y: 160, label: 'Node 6' } },\n      { id: 'node7', style: { x: 220, y: 120, label: 'Node 7' } },\n      { id: 'node8', style: { x: 260, y: 100, label: 'Node 8' } },\n      { id: 'node9', style: { x: 240, y: 130, label: 'Node 9' } },\n      { id: 'node10', style: { x: 300, y: 110, label: 'Node 10' } },\n\n      // Lower area\n      { id: 'node11', style: { x: 240, y: 200, label: 'Node 11' } },\n      { id: 'node12', style: { x: 280, y: 220, label: 'Node 12' } },\n      { id: 'node13', style: { x: 300, y: 190, label: 'Node 13' } },\n      { id: 'node14', style: { x: 320, y: 210, label: 'Node 14' } },\n    ],\n    edges: [\n      // Upper connections\n      { id: 'edge1', source: 'node1', target: 'node2' },\n      { id: 'edge2', source: 'node2', target: 'node3' },\n      { id: 'edge3', source: 'node3', target: 'node4' },\n\n      // Middle connections\n      { id: 'edge4', source: 'node5', target: 'node6' },\n      { id: 'edge5', source: 'node6', target: 'node7' },\n      { id: 'edge6', source: 'node7', target: 'node8' },\n      { id: 'edge7', source: 'node8', target: 'node9' },\n      { id: 'edge8', source: 'node9', target: 'node10' },\n\n      // Lower connections\n      { id: 'edge9', source: 'node11', target: 'node12' },\n      { id: 'edge10', source: 'node12', target: 'node13' },\n      { id: 'edge11', source: 'node13', target: 'node14' },\n\n      // Cross-region connections\n      { id: 'edge12', source: 'node4', target: 'node8' },\n      { id: 'edge13', source: 'node7', target: 'node11' },\n      { id: 'edge14', source: 'node10', target: 'node13' },\n    ],\n  },\n  node: {\n    style: {\n      size: 20,\n    },\n  },\n  plugins: ['edge-filter-lens'],\n});\n\ngraph.render();\n```\n\n### Custom Styles\n\nYou can customize the appearance and behavior of the lens:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-filter-lens',\n      r: 80,\n      style: {\n        fill: '#f0f5ff', // Fill color of the lens area\n        fillOpacity: 0.6, // Opacity of the fill area\n        stroke: '#7e3feb', // Change lens border to purple\n        strokeOpacity: 0.8, // Opacity of the border\n        lineWidth: 1.5, // Line width of the border\n      },\n      nodeStyle: {\n        size: 24, // Enlarge nodes\n        fill: '#7e3feb', // Purple fill\n        stroke: '#5719c9', // Dark purple stroke\n        lineWidth: 1, // Thin border\n        label: true, // Show label\n        labelFill: '#ffffff', // White text\n        labelFontSize: 14, // Enlarge text\n        labelFontWeight: 'bold', // Bold text\n      },\n      edgeStyle: {\n        stroke: '#8b9baf', // Gray edge\n        lineWidth: 2, // Thicken edge line\n        label: true, // Show label\n        labelFill: '#5719c9', // Dark purple text\n        opacity: 0.8, // Appropriate opacity\n      },\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 300,\n  data: {\n    nodes: [\n      // Upper evacuation area\n      { id: 'node1', style: { x: 150, y: 60, label: 'Node 1' } },\n      { id: 'node2', style: { x: 100, y: 40, label: 'Node 2' } },\n      { id: 'node3', style: { x: 200, y: 35, label: 'Node 3' } },\n      { id: 'node4', style: { x: 150, y: 30, label: 'Node 4' } },\n\n      // Middle area\n      { id: 'node5', style: { x: 220, y: 140, label: 'Node 5' } },\n      { id: 'node6', style: { x: 280, y: 160, label: 'Node 6' } },\n      { id: 'node7', style: { x: 220, y: 120, label: 'Node 7' } },\n      { id: 'node8', style: { x: 260, y: 100, label: 'Node 8' } },\n      { id: 'node9', style: { x: 240, y: 130, label: 'Node 9' } },\n      { id: 'node10', style: { x: 300, y: 110, label: 'Node 10' } },\n\n      // Lower area\n      { id: 'node11', style: { x: 240, y: 200, label: 'Node 11' } },\n      { id: 'node12', style: { x: 280, y: 220, label: 'Node 12' } },\n      { id: 'node13', style: { x: 300, y: 190, label: 'Node 13' } },\n      { id: 'node14', style: { x: 320, y: 210, label: 'Node 14' } },\n    ],\n    edges: [\n      // Upper connections\n      { id: 'edge1', source: 'node1', target: 'node2' },\n      { id: 'edge2', source: 'node2', target: 'node3' },\n      { id: 'edge3', source: 'node3', target: 'node4' },\n\n      // Middle connections\n      { id: 'edge4', source: 'node5', target: 'node6' },\n      { id: 'edge5', source: 'node6', target: 'node7' },\n      { id: 'edge6', source: 'node7', target: 'node8' },\n      { id: 'edge7', source: 'node8', target: 'node9' },\n      { id: 'edge8', source: 'node9', target: 'node10' },\n\n      // Lower connections\n      { id: 'edge9', source: 'node11', target: 'node12' },\n      { id: 'edge10', source: 'node12', target: 'node13' },\n      { id: 'edge11', source: 'node13', target: 'node14' },\n\n      // Cross-region connections\n      { id: 'edge12', source: 'node4', target: 'node8' },\n      { id: 'edge13', source: 'node7', target: 'node11' },\n      { id: 'edge14', source: 'node10', target: 'node13' },\n    ],\n  },\n  node: {\n    style: {\n      size: 20,\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#91d5ff',\n      lineWidth: 1,\n    },\n  },\n  plugins: [\n    {\n      type: 'edge-filter-lens',\n      r: 80,\n      style: {\n        fill: '#f0f5ff', // Fill color of the lens area\n        fillOpacity: 0.6, // Opacity of the fill area\n        stroke: '#7e3feb', // Change lens border to purple\n        strokeOpacity: 0.8, // Opacity of the border\n        lineWidth: 1.5, // Line width of the border\n      },\n      nodeStyle: {\n        size: 24, // Enlarge nodes\n        fill: '#7e3feb', // Purple fill\n        stroke: '#5719c9', // Dark purple stroke\n        lineWidth: 1, // Thin border\n        label: true, // Show label\n        labelFill: '#ffffff', // White text\n        labelFontSize: 14, // Enlarge text\n        labelFontWeight: 'bold', // Bold text\n      },\n      edgeStyle: {\n        stroke: '#8b9baf', // Gray edge\n        lineWidth: 2, // Thicken edge line\n        label: true, // Show label\n        labelFill: '#5719c9', // Dark purple text\n        opacity: 0.8, // Appropriate opacity\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n## Practical Examples\n\n- [Edge Filter Lens](/en/examples/plugin/edge-filter-lens/#basic)\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/EdgeFilterLens.zh.md",
    "content": "---\ntitle: 边过滤镜 EdgeFilterLens\norder: 5\n---\n\n## 概述\n\n边过滤镜插件可以将关注的边保留在过滤镜范围内，其他边将在该范围内不显示。这是一个重要的可视化探索工具，可以帮助用户聚焦于特定区域的边关系。\n\n## 使用场景\n\n- 需要聚焦查看局部区域的边关系\n- 在复杂网络中突出显示特定节点之间的连接\n\n## 基本用法\n\n以下是一个简单的 EdgeFilterLens 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-filter-lens',\n      trigger: 'pointermove', // 跟随鼠标移动\n      r: 60, // 设置透镜半径\n      nodeType: 'both', // 边的显示条件\n    },\n  ],\n});\n```\n\n## 在线体验\n\n<embed src=\"@/common/api/plugins/edge-filter-lens.md\"></embed>\n\n## 配置项\n\n| 属性           | 描述                                                                                                                                                                                                                                                       | 类型                                                                                                                                                           | 默认值               | 必选 |\n| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | ---- |\n| type           | 插件类型                                                                                                                                                                                                                                                   | string                                                                                                                                                         | `edge-filter-lens`   | ✓    |\n| key            | 插件的唯一标识，可用于获取插件实例或更新插件选项                                                                                                                                                                                                           | string                                                                                                                                                         | -                    |      |\n| trigger        | 移动透镜的方式：<br/>- `pointermove`：透镜始终跟随鼠标移动 <br/>- `click`：点击画布时移动透镜到点击位置 <br/>- `drag`：通过拖拽方式移动透镜                                                                                                                | `pointermove` \\| `click` \\| `drag`                                                                                                                             | `pointermove`        |      |\n| r              | 透镜的半径                                                                                                                                                                                                                                                 | number                                                                                                                                                         | 60                   |      |\n| maxR           | 透镜的最大半径                                                                                                                                                                                                                                             | number                                                                                                                                                         | 画布宽高最小值的一半 |      |\n| minR           | 透镜的最小半径                                                                                                                                                                                                                                             | number                                                                                                                                                         | 0                    |      |\n| scaleRBy       | 缩放透镜半径的方式：`wheel`：通过滚轮缩放透镜的半径                                                                                                                                                                                                        | `wheel`                                                                                                                                                        | -                    |      |\n| nodeType       | 边显示的条件：<br/> - `both`：只有起始节点和目标节点都在透镜中时，边才会显示 <br/> - `source`：只有起始节点在透镜中时，边才会显示<br/> - `target`：只有目标节点在透镜中时，边才会显示 <br/> - `either`：只要起始节点或目标节点有一个在透镜中时，边就会显示 | `both` \\| `source` \\| `target` \\| `either`                                                                                                                     | `both`               |      |\n| filter         | 过滤出始终不在透镜中显示的元素                                                                                                                                                                                                                             | (id: string, elementType: `node` \\| `edge` \\| `combo`) => boolean                                                                                              | () => true           |      |\n| style          | 透镜的样式，[配置项](#style)                                                                                                                                                                                                                               | object                                                                                                                                                         |                      |      |\n| nodeStyle      | 在透镜中节点的样式                                                                                                                                                                                                                                         | [NodeStyle](/manual/element/node/base-node#style) \\| ((datum: [NodeData](/manual/data#节点数据nodedata)) => [NodeStyle](/manual/element/node/base-node#style)) | `{ label: false }`   |      |\n| edgeStyle      | 在透镜中边的样式                                                                                                                                                                                                                                           | [EdgeStyle](/manual/element/edge/base-edge#style) \\| ((datum: [EdgeData](/manual/data#边数据edgedata)) => [EdgeStyle](/manual/element/edge/base-edge#style))   | `{ label: true }`    |      |\n| preventDefault | 是否阻止默认事件                                                                                                                                                                                                                                           | boolean                                                                                                                                                        | true                 |      |\n\n### style\n\n圆形透镜的样式属性。\n\n| 属性          | 描述            | 类型                          | 默认值 |\n| ------------- | --------------- | ----------------------------- | ------ |\n| fill          | 填充颜色        | string \\| Pattern \\| null     | `#fff` |\n| stroke        | 描边颜色        | string \\| Pattern \\| null     | `#000` |\n| opacity       | 整体透明度      | number \\| string              | 1      |\n| fillOpacity   | 填充透明度      | number \\| string              | 0.8    |\n| strokeOpacity | 描边透明度      | number \\| string              | -      |\n| lineWidth     | 线宽度          | number \\| string              | 2      |\n| lineCap       | 线段端点样式    | `butt` \\| `round` \\| `square` | -      |\n| lineJoin      | 线段连接处样式  | `miter` \\| `round` \\| `bevel` | -      |\n| shadowColor   | 阴影颜色        | string                        | -      |\n| shadowBlur    | 阴影模糊程度    | number                        | -      |\n| shadowOffsetX | 阴影 X 方向偏移 | number                        | -      |\n| shadowOffsetY | 阴影 Y 方向偏移 | number                        | -      |\n\n完整样式属性参考 [元素 -节点 - 内置节点 - 通用样式属性 - style](/manual/element/node/base-node#style)\n\n## 代码示例\n\n### 基础用法\n\n最简单的配置方式：\n\n```js\nconst graph = new Graph({\n  plugins: ['edge-filter-lens'],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 300,\n  data: {\n    nodes: [\n      // 上部疏散区域\n      { id: 'node1', style: { x: 150, y: 60, label: 'Node 1' } },\n      { id: 'node2', style: { x: 100, y: 40, label: 'Node 2' } },\n      { id: 'node3', style: { x: 200, y: 35, label: 'Node 3' } },\n      { id: 'node4', style: { x: 150, y: 30, label: 'Node 4' } },\n\n      // 中部区域\n      { id: 'node5', style: { x: 220, y: 140, label: 'Node 5' } },\n      { id: 'node6', style: { x: 280, y: 160, label: 'Node 6' } },\n      { id: 'node7', style: { x: 220, y: 120, label: 'Node 7' } },\n      { id: 'node8', style: { x: 260, y: 100, label: 'Node 8' } },\n      { id: 'node9', style: { x: 240, y: 130, label: 'Node 9' } },\n      { id: 'node10', style: { x: 300, y: 110, label: 'Node 10' } },\n\n      // 下部区域\n      { id: 'node11', style: { x: 240, y: 200, label: 'Node 11' } },\n      { id: 'node12', style: { x: 280, y: 220, label: 'Node 12' } },\n      { id: 'node13', style: { x: 300, y: 190, label: 'Node 13' } },\n      { id: 'node14', style: { x: 320, y: 210, label: 'Node 14' } },\n    ],\n    edges: [\n      // 上部连接\n      { id: 'edge1', source: 'node1', target: 'node2' },\n      { id: 'edge2', source: 'node2', target: 'node3' },\n      { id: 'edge3', source: 'node3', target: 'node4' },\n\n      // 中部连接\n      { id: 'edge4', source: 'node5', target: 'node6' },\n      { id: 'edge5', source: 'node6', target: 'node7' },\n      { id: 'edge6', source: 'node7', target: 'node8' },\n      { id: 'edge7', source: 'node8', target: 'node9' },\n      { id: 'edge8', source: 'node9', target: 'node10' },\n\n      // 下部连接\n      { id: 'edge9', source: 'node11', target: 'node12' },\n      { id: 'edge10', source: 'node12', target: 'node13' },\n      { id: 'edge11', source: 'node13', target: 'node14' },\n\n      // 跨区域连接\n      { id: 'edge12', source: 'node4', target: 'node8' },\n      { id: 'edge13', source: 'node7', target: 'node11' },\n      { id: 'edge14', source: 'node10', target: 'node13' },\n    ],\n  },\n  node: {\n    style: {\n      size: 20,\n    },\n  },\n  plugins: ['edge-filter-lens'],\n});\n\ngraph.render();\n```\n\n### 自定义样式\n\n可以自定义透镜的外观和行为：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'edge-filter-lens',\n      r: 80,\n      style: {\n        fill: '#f0f5ff', // 透镜区域的填充颜色\n        fillOpacity: 0.6, // 填充区域的透明度\n        stroke: '#7e3feb', // 透镜边框改为紫色\n        strokeOpacity: 0.8, // 边框的透明度\n        lineWidth: 1.5, // 边框的线宽\n      },\n      nodeStyle: {\n        size: 24, // 放大节点\n        fill: '#7e3feb', // 紫色填充\n        stroke: '#5719c9', // 深紫色描边\n        lineWidth: 1, // 细边框\n        label: true, // 显示标签\n        labelFill: '#ffffff', // 白色文字\n        labelFontSize: 14, // 放大文字\n        labelFontWeight: 'bold', // 文字加粗\n      },\n      edgeStyle: {\n        stroke: '#8b9baf', // 灰色边\n        lineWidth: 2, // 加粗边线\n        label: true, // 显示标签\n        labelFill: '#5719c9', // 深紫色文字\n        opacity: 0.8, // 适当的透明度\n      },\n    },\n  ],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 300,\n  data: {\n    nodes: [\n      // 上部疏散区域\n      { id: 'node1', style: { x: 150, y: 60, label: 'Node 1' } },\n      { id: 'node2', style: { x: 100, y: 40, label: 'Node 2' } },\n      { id: 'node3', style: { x: 200, y: 35, label: 'Node 3' } },\n      { id: 'node4', style: { x: 150, y: 30, label: 'Node 4' } },\n\n      // 中部区域\n      { id: 'node5', style: { x: 220, y: 140, label: 'Node 5' } },\n      { id: 'node6', style: { x: 280, y: 160, label: 'Node 6' } },\n      { id: 'node7', style: { x: 220, y: 120, label: 'Node 7' } },\n      { id: 'node8', style: { x: 260, y: 100, label: 'Node 8' } },\n      { id: 'node9', style: { x: 240, y: 130, label: 'Node 9' } },\n      { id: 'node10', style: { x: 300, y: 110, label: 'Node 10' } },\n\n      // 下部区域\n      { id: 'node11', style: { x: 240, y: 200, label: 'Node 11' } },\n      { id: 'node12', style: { x: 280, y: 220, label: 'Node 12' } },\n      { id: 'node13', style: { x: 300, y: 190, label: 'Node 13' } },\n      { id: 'node14', style: { x: 320, y: 210, label: 'Node 14' } },\n    ],\n    edges: [\n      // 上部连接\n      { id: 'edge1', source: 'node1', target: 'node2' },\n      { id: 'edge2', source: 'node2', target: 'node3' },\n      { id: 'edge3', source: 'node3', target: 'node4' },\n\n      // 中部连接\n      { id: 'edge4', source: 'node5', target: 'node6' },\n      { id: 'edge5', source: 'node6', target: 'node7' },\n      { id: 'edge6', source: 'node7', target: 'node8' },\n      { id: 'edge7', source: 'node8', target: 'node9' },\n      { id: 'edge8', source: 'node9', target: 'node10' },\n\n      // 下部连接\n      { id: 'edge9', source: 'node11', target: 'node12' },\n      { id: 'edge10', source: 'node12', target: 'node13' },\n      { id: 'edge11', source: 'node13', target: 'node14' },\n\n      // 跨区域连接\n      { id: 'edge12', source: 'node4', target: 'node8' },\n      { id: 'edge13', source: 'node7', target: 'node11' },\n      { id: 'edge14', source: 'node10', target: 'node13' },\n    ],\n  },\n  node: {\n    style: {\n      size: 20,\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#91d5ff',\n      lineWidth: 1,\n    },\n  },\n  plugins: [\n    {\n      type: 'edge-filter-lens',\n      r: 80,\n      style: {\n        fill: '#f0f5ff', // 透镜区域的填充颜色\n        fillOpacity: 0.6, // 填充区域的透明度\n        stroke: '#7e3feb', // 透镜边框改为紫色\n        strokeOpacity: 0.8, // 边框的透明度\n        lineWidth: 1.5, // 边框的线宽\n      },\n      nodeStyle: {\n        size: 24, // 放大节点\n        fill: '#7e3feb', // 紫色填充\n        stroke: '#5719c9', // 深紫色描边\n        lineWidth: 1, // 细边框\n        label: true, // 显示标签\n        labelFill: '#ffffff', // 白色文字\n        labelFontSize: 14, // 放大文字\n        labelFontWeight: 'bold', // 文字加粗\n      },\n      edgeStyle: {\n        stroke: '#8b9baf', // 灰色边\n        lineWidth: 2, // 加粗边线\n        label: true, // 显示标签\n        labelFill: '#5719c9', // 深紫色文字\n        opacity: 0.8, // 适当的透明度\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n## 实际案例\n\n- [边过滤镜](/examples/plugin/edge-filter-lens/#basic)\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Fisheye.en.md",
    "content": "---\ntitle: Fisheye\norder: 6\n---\n\n## Overview\n\nThe Fisheye plugin is designed for focus+context exploration scenarios. It can magnify the area of interest while ensuring that the context and the relationship between the context and the focus center are not lost. It is an important visualization exploration tool.\n\n## Use Cases\n\n- Highlight certain areas during presentations\n- Magnify details locally without losing the overall view\n\n## Basic Usage\n\nBelow is a simple example of initializing the Fisheye plugin:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fisheye',\n      trigger: 'drag', // Move fisheye by dragging\n      d: 1.5, // Set distortion factor\n      r: 120, // Set fisheye radius\n      showDPercent: true, // Show distortion degree\n    },\n  ],\n});\n```\n\n## Online Experience\n\n<embed src=\"@/common/api/plugins/fisheye.md\"></embed>\n\n## Configuration Options\n\n| Property       | Description                                                                                                                                                                                                                   | Type                                                                                                                                                                    | Default Value                               | Required |\n| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | -------- |\n| type           | Plugin type                                                                                                                                                                                                                   | string                                                                                                                                                                  | `fisheye`                                   | ✓        |\n| key            | Unique identifier for the plugin, can be used to get the plugin instance or update plugin options                                                                                                                             | string                                                                                                                                                                  | -                                           |          |\n| trigger        | Method to move the fisheye:<br/>- `pointermove`: The fisheye always follows the mouse movement <br/>- `click`: Move the fisheye to the click position when clicking on the canvas <br/>- `drag`: Move the fisheye by dragging | `pointermove` \\| `drag` \\| `click`                                                                                                                                      | `pointermove`                               |          |\n| r              | Radius of the fisheye                                                                                                                                                                                                         | number                                                                                                                                                                  | 120                                         |          |\n| maxR           | Maximum adjustable radius of the fisheye                                                                                                                                                                                      | number                                                                                                                                                                  | Half of the smaller dimension of the canvas |          |\n| minR           | Minimum adjustable radius of the fisheye                                                                                                                                                                                      | number                                                                                                                                                                  | 0                                           |          |\n| d              | Distortion factor                                                                                                                                                                                                             | number                                                                                                                                                                  | 1.5                                         |          |\n| maxD           | Maximum adjustable distortion factor of the fisheye                                                                                                                                                                           | number                                                                                                                                                                  | 5                                           |          |\n| minD           | Minimum adjustable distortion factor of the fisheye                                                                                                                                                                           | number                                                                                                                                                                  | 0                                           |          |\n| scaleRBy       | Method to adjust the fisheye radius:<br/>- `'wheel'`: Adjust by wheel <br/>- `'drag'`: Adjust by dragging                                                                                                                     | `wheel` \\| `drag`                                                                                                                                                       | -                                           |          |\n| scaleDBy       | Method to adjust the fisheye distortion factor:<br/>- `'wheel'`: Adjust by wheel <br/>- `'drag'`: Adjust by dragging                                                                                                          | `wheel` \\| `drag`                                                                                                                                                       | -                                           |          |\n| showDPercent   | Whether to show the distortion factor value in the fisheye                                                                                                                                                                    | boolean                                                                                                                                                                 | true                                        |          |\n| style          | Style of the fisheye, [configuration options](#style)                                                                                                                                                                         | object                                                                                                                                                                  | -                                           |          |\n| nodeStyle      | Style of nodes in the fisheye                                                                                                                                                                                                 | [NodeStyle](/en/manual/element/node/base-node#style) \\| ((datum: [NodeData](/en/manual/data#节点数据nodedata)) => [NodeStyle](/en/manual/element/node/base-node#style)) | `{ label: true }`                           |          |\n| preventDefault | Whether to prevent default events                                                                                                                                                                                             | boolean                                                                                                                                                                 | true                                        |          |\n\n### style\n\nCircular style properties for configuring the appearance of the fisheye.\n\n| Property      | Description        | Type                          | Default Value |\n| ------------- | ------------------ | ----------------------------- | ------------- |\n| fill          | Fill color         | string \\| Pattern \\| null     | `#ccc`        |\n| stroke        | Stroke color       | string \\| Pattern \\| null     | `#000`        |\n| opacity       | Overall opacity    | number \\| string              | -             |\n| fillOpacity   | Fill opacity       | number \\| string              | 0.1           |\n| strokeOpacity | Stroke opacity     | number \\| string              | -             |\n| lineWidth     | Line width         | number \\| string              | 2             |\n| lineCap       | Line cap style     | `butt` \\| `round` \\| `square` | -             |\n| lineJoin      | Line join style    | `miter` \\| `round` \\| `bevel` | -             |\n| shadowColor   | Shadow color       | string                        | -             |\n| shadowBlur    | Shadow blur degree | number                        | -             |\n| shadowOffsetX | Shadow X offset    | number                        | -             |\n| shadowOffsetY | Shadow Y offset    | number                        | -             |\n\nFor complete style properties, refer to [Element - Node - Built-in Node - General Style Properties - style](/en/manual/element/node/base-node#style)\n\n### Zoom Control\n\n`scaleRBy` and `scaleDBy` can be used to control the adjustment method of the fisheye's radius and distortion factor respectively:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fisheye',\n      // Adjust radius by wheel\n      scaleRBy: 'wheel',\n      // Adjust distortion factor by dragging\n      scaleDBy: 'drag',\n      // Set range for radius and distortion factor\n      minR: 50,\n      maxR: 200,\n      minD: 1,\n      maxD: 3,\n    },\n  ],\n});\n```\n\nNote: When `trigger`, `scaleRBy`, and `scaleDBy` are all set to `'drag'`, the priority order is `trigger` > `scaleRBy` > `scaleDBy`, and only the highest priority configuration item will bind the drag event. Similarly, if `scaleRBy` and `scaleDBy` are both set to `'wheel'`, only `scaleRBy` will bind the wheel event.\n\n## Code Examples\n\n### Basic Usage\n\nThe simplest configuration method:\n\n```js\nconst graph = new Graph({\n  plugins: ['fisheye'],\n});\n```\n\n### Custom Styles\n\nYou can customize the appearance and behavior of the fisheye:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fisheye',\n      r: 150,\n      d: 2,\n      style: {\n        fill: '#2f54eb', // Fill color of the fisheye area\n        fillOpacity: 0.2, // Opacity of the fill area\n        stroke: '#1d39c4', // Color of the fisheye border\n        strokeOpacity: 0.8, // Opacity of the border\n        lineWidth: 1.5, // Line width of the border\n        shadowColor: '#1d39c4', // Shadow color\n        shadowBlur: 10, // Shadow blur radius\n        shadowOffsetX: 0, // Horizontal shadow offset\n        shadowOffsetY: 0, // Vertical shadow offset\n        cursor: 'pointer', // Cursor style when hovering\n      },\n      nodeStyle: {\n        // Basic node style\n        size: 40, // Node size\n        fill: '#d6e4ff', // Node fill color\n        stroke: '#2f54eb', // Node border color\n        lineWidth: 2, // Node border width\n        shadowColor: '#2f54eb', // Node shadow color\n        shadowBlur: 5, // Node shadow blur radius\n        cursor: 'pointer', // Cursor style when hovering\n\n        // Label style\n        label: true, // Show label\n        labelFontSize: 14, // Label font size\n        labelFontWeight: 'bold', // Label font weight\n        labelFill: '#1d39c4', // Label text color\n        labelBackground: true, // Show label background\n        labelBackgroundFill: '#fff', // Label background fill color\n        labelBackgroundStroke: '#1d39c4', // Label background border color\n        labelBackgroundOpacity: 0.8, // Label background opacity\n        labelBackgroundPadding: [4, 8, 4, 8], // Label background padding [top, right, bottom, left]\n\n        // Icon style\n        icon: true, // Show icon\n        iconFontFamily: 'iconfont', // Icon font\n        iconText: '\\ue6f6', // Icon Unicode\n        iconFill: '#1d39c4', // Icon color\n        iconSize: 16, // Icon size\n        iconFontWeight: 'normal', // Icon font weight\n      },\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 300,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 150, y: 100 } },\n      { id: 'node-2', style: { x: 250, y: 100 } },\n      { id: 'node-3', style: { x: 200, y: 180 } },\n      { id: 'node-4', style: { x: 120, y: 180 } },\n      { id: 'node-5', style: { x: 280, y: 180 } },\n    ],\n    edges: [\n      { id: 'edge-1', source: 'node-1', target: 'node-2' },\n      { id: 'edge-2', source: 'node-1', target: 'node-3' },\n      { id: 'edge-3', source: 'node-2', target: 'node-3' },\n      { id: 'edge-4', source: 'node-3', target: 'node-4' },\n      { id: 'edge-5', source: 'node-3', target: 'node-5' },\n    ],\n  },\n  node: {\n    style: {\n      size: 30,\n      fill: '#e6f7ff',\n      stroke: '#1890ff',\n      lineWidth: 1,\n      label: false,\n      icon: false,\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#91d5ff',\n      lineWidth: 1,\n    },\n  },\n  plugins: [\n    {\n      type: 'fisheye',\n      key: 'fisheye',\n      r: 100,\n      d: 2,\n      style: {\n        fill: '#2f54eb', // Fill color of the fisheye area\n        fillOpacity: 0.2, // Opacity of the fill area\n        stroke: '#1d39c4', // Color of the fisheye border\n        strokeOpacity: 0.8, // Opacity of the border\n        lineWidth: 1.5, // Line width of the border\n        shadowColor: '#1d39c4', // Shadow color\n        shadowBlur: 10, // Shadow blur radius\n        shadowOffsetX: 0, // Horizontal shadow offset\n        shadowOffsetY: 0, // Vertical shadow offset\n        cursor: 'pointer', // Cursor style when hovering\n      },\n      nodeStyle: {\n        // Basic node style\n        size: 40, // Node size\n        fill: '#d6e4ff', // Node fill color\n        stroke: '#2f54eb', // Node border color\n        lineWidth: 2, // Node border width\n        shadowColor: '#2f54eb', // Node shadow color\n        shadowBlur: 5, // Node shadow blur radius\n        cursor: 'pointer', // Cursor style when hovering\n\n        // Label style\n        label: true, // Show label\n        labelFontSize: 14, // Label font size\n        labelFontWeight: 'bold', // Label font weight\n        labelFill: '#1d39c4', // Label text color\n        labelBackground: true, // Show label background\n        labelBackgroundFill: '#fff', // Label background fill color\n        labelBackgroundStroke: '#1d39c4', // Label background border color\n        labelBackgroundOpacity: 0.8, // Label background opacity\n        labelBackgroundPadding: [4, 8, 4, 8], // Label background padding [top, right, bottom, left]\n\n        // Icon style\n        icon: true, // Show icon\n        iconFontFamily: 'iconfont', // Icon font\n        iconText: '\\ue6f6', // Icon Unicode\n        iconFill: '#1d39c4', // Icon color\n        iconSize: 16, // Icon size\n        iconFontWeight: 'normal', // Icon font weight\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n## Practical Examples\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nfetch('https://assets.antv.antgroup.com/g6/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          size: (datum) => datum.id.length * 2 + 10,\n          label: false,\n          labelText: (datum) => datum.id,\n          labelBackground: true,\n          icon: false,\n          iconFontFamily: 'iconfont',\n          iconText: '\\ue6f6',\n          iconFill: '#fff',\n        },\n        palette: {\n          type: 'group',\n          field: (datum) => datum.id,\n          color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n        },\n      },\n      edge: {\n        style: {\n          stroke: '#e2e2e2',\n        },\n      },\n      plugins: [{ key: 'fisheye', type: 'fisheye', nodeStyle: { label: true, icon: true } }],\n    });\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Fisheye.zh.md",
    "content": "---\ntitle: 鱼眼放大镜 Fisheye\norder: 6\n---\n\n## 概述\n\n鱼眼放大镜插件是为 focus+context 的探索场景设计的，它能够在放大关注区域的同时，保证上下文以及上下文与关注中心的关系不丢失，是一个重要的可视化探索工具。\n\n## 使用场景\n\n- 在演示过程中需要突出展示某些区域内容\n- 需要局部放大查看细节时，同时又不想失去整体视图\n\n## 基本用法\n\n以下是一个简单的 Fisheye 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fisheye',\n      trigger: 'drag', // 通过拖拽移动鱼眼\n      d: 1.5, // 设置畸变因子\n      r: 120, // 设置鱼眼半径\n      showDPercent: true, // 显示畸变程度\n    },\n  ],\n});\n```\n\n## 在线体验\n\n<embed src=\"@/common/api/plugins/fisheye.md\"></embed>\n\n## 配置项\n\n| 属性           | 描述                                                                                                                                                                                          | 类型                                                                                                                                                           | 默认值                 | 必选 |\n| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ---- |\n| type           | 插件类型                                                                                                                                                                                      | string                                                                                                                                                         | `fisheye`              | ✓    |\n| key            | 插件的唯一标识，可用于获取插件实例或更新插件选项                                                                                                                                              | string                                                                                                                                                         | -                      |      |\n| trigger        | 用于控制鱼眼放大镜的移动方式，支持以下三种配置：<br/>- `pointermove`：鱼眼放大镜始终跟随鼠标移动 <br/>- `click`：点击画布时移动鱼眼放大镜到点击位置 <br/>- `drag`：通过拖拽方式移动鱼眼放大镜 | `pointermove` \\| `drag` \\| `click`                                                                                                                             | `pointermove`          |      |\n| r              | 鱼眼放大镜半径                                                                                                                                                                                | number                                                                                                                                                         | 120                    |      |\n| maxR           | 鱼眼放大镜可调整的最大半径                                                                                                                                                                    | number                                                                                                                                                         | 画布宽高的最小值的一半 |      |\n| minR           | 鱼眼放大镜可调整的最小半径                                                                                                                                                                    | number                                                                                                                                                         | 0                      |      |\n| d              | 畸变因子                                                                                                                                                                                      | number                                                                                                                                                         | 1.5                    |      |\n| maxD           | 鱼眼放大镜可调整的最大畸变因子                                                                                                                                                                | number                                                                                                                                                         | 5                      |      |\n| minD           | 鱼眼放大镜可调整的最小畸变因子                                                                                                                                                                | number                                                                                                                                                         | 0                      |      |\n| scaleRBy       | 调整鱼眼放大镜范围半径的方式：<br/>- `'wheel'`：滚轮调整 <br/>- `'drag'`：拖拽调整                                                                                                            | `wheel` \\| `drag`                                                                                                                                              | -                      |      |\n| scaleDBy       | 调整鱼眼放大镜畸变因子的方式：<br/>- `'wheel'`：滚轮调整 <br/>- `'drag'`：拖拽调整                                                                                                            | `wheel` \\| `drag`                                                                                                                                              | -                      |      |\n| showDPercent   | 是否在鱼眼放大镜中显示畸变因子数值                                                                                                                                                            | boolean                                                                                                                                                        | true                   |      |\n| style          | 鱼眼放大镜样式，[配置项](#style)                                                                                                                                                              | object                                                                                                                                                         | -                      |      |\n| nodeStyle      | 在鱼眼放大镜中的节点样式                                                                                                                                                                      | [NodeStyle](/manual/element/node/base-node#style) \\| ((datum: [NodeData](/manual/data#节点数据nodedata)) => [NodeStyle](/manual/element/node/base-node#style)) | `{ label: true }`      |      |\n| preventDefault | 是否阻止默认事件                                                                                                                                                                              | boolean                                                                                                                                                        | true                   |      |\n\n### style\n\n圆形样式属性，用于配置鱼眼放大镜的外观。\n\n| 属性          | 描述            | 类型                          | 默认值 |\n| ------------- | --------------- | ----------------------------- | ------ |\n| fill          | 填充颜色        | string \\| Pattern \\| null     | `#ccc` |\n| stroke        | 描边颜色        | string \\| Pattern \\| null     | `#000` |\n| opacity       | 整体透明度      | number \\| string              | -      |\n| fillOpacity   | 填充透明度      | number \\| string              | 0.1    |\n| strokeOpacity | 描边透明度      | number \\| string              | -      |\n| lineWidth     | 线宽度          | number \\| string              | 2      |\n| lineCap       | 线段端点样式    | `butt` \\| `round` \\| `square` | -      |\n| lineJoin      | 线段连接处样式  | `miter` \\| `round` \\| `bevel` | -      |\n| shadowColor   | 阴影颜色        | string                        | -      |\n| shadowBlur    | 阴影模糊程度    | number                        | -      |\n| shadowOffsetX | 阴影 X 方向偏移 | number                        | -      |\n| shadowOffsetY | 阴影 Y 方向偏移 | number                        | -      |\n\n完整样式属性参考 [元素 -节点 - 内置节点 - 通用样式属性 - style](/manual/element/node/base-node#style)\n\n### 缩放控制\n\n通过 `scaleRBy` 和 `scaleDBy` 可以分别控制鱼眼放大镜的半径和畸变因子的调整方式：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fisheye',\n      // 通过滚轮调整半径\n      scaleRBy: 'wheel',\n      // 通过拖拽调整畸变因子\n      scaleDBy: 'drag',\n      // 设置半径和畸变因子的范围\n      minR: 50,\n      maxR: 200,\n      minD: 1,\n      maxD: 3,\n    },\n  ],\n});\n```\n\n注意：当 `trigger`、`scaleRBy` 和 `scaleDBy` 同时设置为 `'drag'` 时，优先级顺序为 `trigger` > `scaleRBy` > `scaleDBy`，只会为优先级最高的配置项绑定拖拽事件。同理，如果 `scaleRBy` 和 `scaleDBy` 同时设置为 `'wheel'`，只会为 `scaleRBy` 绑定滚轮事件。\n\n## 代码示例\n\n### 基础用法\n\n最简单的配置方式：\n\n```js\nconst graph = new Graph({\n  plugins: ['fisheye'],\n});\n```\n\n### 自定义样式\n\n可以自定义鱼眼放大镜的外观和行为：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fisheye',\n      r: 150,\n      d: 2,\n      style: {\n        fill: '#2f54eb', // 鱼眼区域的填充颜色\n        fillOpacity: 0.2, // 填充区域的透明度\n        stroke: '#1d39c4', // 鱼眼边框的颜色\n        strokeOpacity: 0.8, // 边框的透明度\n        lineWidth: 1.5, // 边框的线宽\n        shadowColor: '#1d39c4', // 阴影颜色\n        shadowBlur: 10, // 阴影的模糊半径\n        shadowOffsetX: 0, // 阴影的水平偏移\n        shadowOffsetY: 0, // 阴影的垂直偏移\n        cursor: 'pointer', // 鼠标悬停时的指针样式\n      },\n      nodeStyle: {\n        // 节点基础样式\n        size: 40, // 节点大小\n        fill: '#d6e4ff', // 节点填充颜色\n        stroke: '#2f54eb', // 节点边框颜色\n        lineWidth: 2, // 节点边框宽度\n        shadowColor: '#2f54eb', // 节点阴影颜色\n        shadowBlur: 5, // 节点阴影模糊半径\n        cursor: 'pointer', // 鼠标悬停时的指针样式\n\n        // 标签样式\n        label: true, // 是否显示标签\n        labelFontSize: 14, // 标签字体大小\n        labelFontWeight: 'bold', // 标签字体粗细\n        labelFill: '#1d39c4', // 标签文字颜色\n        labelBackground: true, // 是否显示标签背景\n        labelBackgroundFill: '#fff', // 标签背景填充颜色\n        labelBackgroundStroke: '#1d39c4', // 标签背景边框颜色\n        labelBackgroundOpacity: 0.8, // 标签背景透明度\n        labelBackgroundPadding: [4, 8, 4, 8], // 标签背景内边距 [上,右,下,左]\n\n        // 图标样式\n        icon: true, // 是否显示图标\n        iconFontFamily: 'iconfont', // 图标字体\n        iconText: '\\ue6f6', // 图标的 Unicode 编码\n        iconFill: '#1d39c4', // 图标颜色\n        iconSize: 16, // 图标大小\n        iconFontWeight: 'normal', // 图标字体粗细\n      },\n    },\n  ],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 300,\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 150, y: 100 } },\n      { id: 'node-2', style: { x: 250, y: 100 } },\n      { id: 'node-3', style: { x: 200, y: 180 } },\n      { id: 'node-4', style: { x: 120, y: 180 } },\n      { id: 'node-5', style: { x: 280, y: 180 } },\n    ],\n    edges: [\n      { id: 'edge-1', source: 'node-1', target: 'node-2' },\n      { id: 'edge-2', source: 'node-1', target: 'node-3' },\n      { id: 'edge-3', source: 'node-2', target: 'node-3' },\n      { id: 'edge-4', source: 'node-3', target: 'node-4' },\n      { id: 'edge-5', source: 'node-3', target: 'node-5' },\n    ],\n  },\n  node: {\n    style: {\n      size: 30,\n      fill: '#e6f7ff',\n      stroke: '#1890ff',\n      lineWidth: 1,\n      label: false,\n      icon: false,\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#91d5ff',\n      lineWidth: 1,\n    },\n  },\n  plugins: [\n    {\n      type: 'fisheye',\n      key: 'fisheye',\n      r: 100,\n      d: 2,\n      style: {\n        fill: '#2f54eb', // 鱼眼区域的填充颜色\n        fillOpacity: 0.2, // 填充区域的透明度\n        stroke: '#1d39c4', // 鱼眼边框的颜色\n        strokeOpacity: 0.8, // 边框的透明度\n        lineWidth: 1.5, // 边框的线宽\n        shadowColor: '#1d39c4', // 阴影颜色\n        shadowBlur: 10, // 阴影的模糊半径\n        shadowOffsetX: 0, // 阴影的水平偏移\n        shadowOffsetY: 0, // 阴影的垂直偏移\n        cursor: 'pointer', // 鼠标悬停时的指针样式\n      },\n      nodeStyle: {\n        // 节点基础样式\n        size: 40, // 节点大小\n        fill: '#d6e4ff', // 节点填充颜色\n        stroke: '#2f54eb', // 节点边框颜色\n        lineWidth: 2, // 节点边框宽度\n        shadowColor: '#2f54eb', // 节点阴影颜色\n        shadowBlur: 5, // 节点阴影模糊半径\n        cursor: 'pointer', // 鼠标悬停时的指针样式\n\n        // 标签样式\n        label: true, // 是否显示标签\n        labelFontSize: 14, // 标签字体大小\n        labelFontWeight: 'bold', // 标签字体粗细\n        labelFill: '#1d39c4', // 标签文字颜色\n        labelBackground: true, // 是否显示标签背景\n        labelBackgroundFill: '#fff', // 标签背景填充颜色\n        labelBackgroundStroke: '#1d39c4', // 标签背景边框颜色\n        labelBackgroundOpacity: 0.8, // 标签背景透明度\n        labelBackgroundPadding: [4, 8, 4, 8], // 标签背景内边距 [上,右,下,左]\n\n        // 图标样式\n        icon: true, // 是否显示图标\n        iconFontFamily: 'iconfont', // 图标字体\n        iconText: '\\ue6f6', // 图标的 Unicode 编码\n        iconFill: '#1d39c4', // 图标颜色\n        iconSize: 16, // 图标大小\n        iconFontWeight: 'normal', // 图标字体粗细\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nfetch('https://assets.antv.antgroup.com/g6/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          size: (datum) => datum.id.length * 2 + 10,\n          label: false,\n          labelText: (datum) => datum.id,\n          labelBackground: true,\n          icon: false,\n          iconFontFamily: 'iconfont',\n          iconText: '\\ue6f6',\n          iconFill: '#fff',\n        },\n        palette: {\n          type: 'group',\n          field: (datum) => datum.id,\n          color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n        },\n      },\n      edge: {\n        style: {\n          stroke: '#e2e2e2',\n        },\n      },\n      plugins: [{ key: 'fisheye', type: 'fisheye', nodeStyle: { label: true, icon: true } }],\n    });\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Fullscreen.en.md",
    "content": "---\ntitle: Fullscreen\norder: 7\n---\n\n## Overview\n\nThe Fullscreen plugin allows users to expand the graph visualization content to the entire screen, providing a broader view and a better immersive experience.\n\n## Use Cases\n\nThe Fullscreen plugin is mainly suitable for the following scenarios:\n\n- Provide a broader view for viewing complex graph data\n- Enhance immersive experience, focusing on graph visualization content\n- Display graph data in presentations or reports\n\n## Basic Usage\n\nBelow is a simple example of initializing the Fullscreen plugin:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fullscreen',\n      autoFit: true,\n      trigger: {\n        request: 'F', // Use shortcut key F to enter fullscreen\n        exit: 'Esc', // Use shortcut key Esc to exit fullscreen\n      },\n      onEnter: () => {\n        console.log('Entered fullscreen mode');\n      },\n      onExit: () => {\n        console.log('Exited fullscreen mode');\n      },\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Property | Description                                                                                                      | Type                                 | Default Value | Required |\n| -------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------- | -------- |\n| type     | Plugin type                                                                                                      | string                               | `fullscreen`  | ✓        |\n| key      | Unique identifier for the plugin, can be used to get the plugin instance or update plugin options                | string                               | -             |          |\n| autoFit  | Whether to auto-fit the canvas size, the canvas size will automatically adapt to the screen size when fullscreen | boolean                              | true          |          |\n| trigger  | Method to trigger fullscreen, [example](#trigger)                                                                | { request?: string; exit?: string; } | -             |          |\n| onEnter  | Callback after entering fullscreen                                                                               | () => void                           | -             |          |\n| onExit   | Callback after exiting fullscreen                                                                                | () => void                           | -             |          |\n\n### trigger\n\nThe trigger property is used to control the method of triggering fullscreen. It supports two configuration methods:\n\n#### Shortcut Key Configuration\n\nUse keyboard shortcuts to trigger fullscreen and exit fullscreen.\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fullscreen',\n      trigger: {\n        request: 'F', // Use shortcut key F to enter fullscreen\n        exit: 'Esc', // Use shortcut key Esc to exit fullscreen\n      },\n    },\n  ],\n});\n```\n\n#### Custom Trigger\n\nControl fullscreen by calling the request and exit methods.\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fullscreen',\n      key: 'my-fullscreen',\n    },\n  ],\n});\n\n// Enter fullscreen\ngraph.getPluginInstance('my-fullscreen').request();\n\n// Exit fullscreen\ngraph.getPluginInstance('my-fullscreen').exit();\n```\n\n### autoFit\n\nWhether to auto-fit the canvas size, the canvas size will automatically adapt to the screen size when fullscreen.\n\n- When set to true, the canvas will automatically resize to fit the entire screen.\n- When set to false, the canvas size remains unchanged.\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fullscreen',\n      autoFit: true,\n    },\n  ],\n});\n```\n\n## API\n\n### Fullscreen.request()\n\nThis method is used to enter fullscreen mode programmatically. It can be called on the plugin instance to expand the graph visualization to the entire screen.\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fullscreen',\n      key: 'my-fullscreen',\n    },\n  ],\n});\n\n// Enter fullscreen\ngraph.getPluginInstance('my-fullscreen').request();\n```\n\n### Fullscreen.exit()\n\nThis method is used to exit fullscreen mode programmatically. It can be called on the plugin instance to revert the graph visualization back to its original size.\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fullscreen',\n      key: 'my-fullscreen',\n    },\n  ],\n});\n\n// Exit fullscreen\ngraph.getPluginInstance('my-fullscreen').exit();\n```\n\n## Practical Examples\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  data: { nodes: Array.from({ length: 20 }).map((_, i) => ({ id: `node${i}` })) },\n  autoFit: 'center',\n  background: '#fff',\n  plugins: [\n    {\n      type: 'fullscreen',\n      key: 'fullscreen',\n    },\n    function () {\n      const graph = this;\n      return {\n        type: 'toolbar',\n        key: 'toolbar',\n        position: 'top-left',\n        onClick: (item) => {\n          const fullscreenPlugin = graph.getPluginInstance('fullscreen');\n          if (item === 'request-fullscreen') {\n            fullscreenPlugin.request();\n          }\n          if (item === 'exit-fullscreen') {\n            fullscreenPlugin.exit();\n          }\n        },\n        getItems: () => {\n          return [\n            { id: 'request-fullscreen', value: 'request-fullscreen' },\n            { id: 'exit-fullscreen', value: 'exit-fullscreen' },\n          ];\n        },\n      };\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Fullscreen.zh.md",
    "content": "---\ntitle: 全屏展示 Fullscreen\norder: 7\n---\n\n## 概述\n\n全屏展示插件允许用户将图可视化内容扩展到整个屏幕，提供更广阔的视图和更好的沉浸式体验。\n\n## 使用场景\n\n全屏展示插件主要适用于以下场景：\n\n- 提供更广阔的视图，便于查看复杂图数据\n- 增强沉浸式体验，专注于图可视化内容\n- 在演示或报告中展示图数据\n\n## 基本用法\n\n以下是一个简单的 Fullscreen 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fullscreen',\n      autoFit: true,\n      trigger: {\n        request: 'F', // 使用快捷键 F 进入全屏\n        exit: 'Esc', // 使用快捷键 Esc 退出全屏\n      },\n      onEnter: () => {\n        console.log('进入全屏模式');\n      },\n      onExit: () => {\n        console.log('退出全屏模式');\n      },\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 属性    | 描述                                                 | 类型                                 | 默认值       | 必选 |\n| ------- | ---------------------------------------------------- | ------------------------------------ | ------------ | ---- |\n| type    | 插件类型                                             | string                               | `fullscreen` | ✓    |\n| key     | 插件的唯一标识，可用于获取插件实例或更新插件选项     | string                               | -            |      |\n| autoFit | 是否自适应画布尺寸，全屏后画布尺寸会自动适应屏幕尺寸 | boolean                              | true         |      |\n| trigger | 触发全屏的方式，[示例](#trigger)                     | { request?: string; exit?: string; } | -            |      |\n| onEnter | 进入全屏后的回调                                     | () => void                           | -            |      |\n| onExit  | 退出全屏后的回调                                     | () => void                           | -            |      |\n\n### trigger\n\ntrigger 属性用于控制触发全屏的方式。它支持两种配置方式：\n\n#### 快捷键配置\n\n使用键盘快捷键来触发全屏和退出全屏。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fullscreen',\n      trigger: {\n        request: 'F', // 使用快捷键 F 进入全屏\n        exit: 'Esc', // 使用快捷键 Esc 退出全屏\n      },\n    },\n  ],\n});\n```\n\n#### 自定义触发\n\n通过调用 request 和 exit 方法来控制全屏。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fullscreen',\n      key: 'my-fullscreen',\n    },\n  ],\n});\n\n// 进入全屏\ngraph.getPluginInstance('my-fullscreen').request();\n\n// 退出全屏\ngraph.getPluginInstance('my-fullscreen').exit();\n```\n\n### autoFit\n\n是否自适应画布尺寸，全屏后画布尺寸会自动适应屏幕尺寸。\n\n- 设置为 true 时，画布会自动调整大小以适应整个屏幕。\n- 设置为 false 时，画布大小保持不变。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fullscreen',\n      autoFit: true,\n    },\n  ],\n});\n```\n\n## API\n\n### Fullscreen.request()\n\n这个方法可以让你通过代码进入全屏模式。调用插件实例上的这个方法，就能把图形内容扩展到整个屏幕。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fullscreen',\n      key: 'my-fullscreen',\n    },\n  ],\n});\n\n// 进入全屏\ngraph.getPluginInstance('my-fullscreen').request();\n```\n\n### Fullscreen.exit()\n\n这个方法可以让你通过代码退出全屏模式。调用插件实例上的这个方法，就能把图形内容恢复到原来的大小。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'fullscreen',\n      key: 'my-fullscreen',\n    },\n  ],\n});\n\n// 退出全屏\ngraph.getPluginInstance('my-fullscreen').exit();\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  data: { nodes: Array.from({ length: 20 }).map((_, i) => ({ id: `node${i}` })) },\n  autoFit: 'center',\n  background: '#fff',\n  plugins: [\n    {\n      type: 'fullscreen',\n      key: 'fullscreen',\n    },\n    function () {\n      const graph = this;\n      return {\n        type: 'toolbar',\n        key: 'toolbar',\n        position: 'top-left',\n        onClick: (item) => {\n          const fullscreenPlugin = graph.getPluginInstance('fullscreen');\n          if (item === 'request-fullscreen') {\n            fullscreenPlugin.request();\n          }\n          if (item === 'exit-fullscreen') {\n            fullscreenPlugin.exit();\n          }\n        },\n        getItems: () => {\n          return [\n            { id: 'request-fullscreen', value: 'request-fullscreen' },\n            { id: 'exit-fullscreen', value: 'exit-fullscreen' },\n          ];\n        },\n      };\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/GridLine.en.md",
    "content": "---\ntitle: GridLine\norder: 8\n---\n\n## Overview\n\nThe GridLine plugin provides visual auxiliary lines for the canvas, helping users precisely position and align graphic elements. It is an indispensable tool in graphic drawing.\n\n## Use Cases\n\nThe GridLine plugin is mainly suitable for the following scenarios:\n\n- Assisting users in precise drawing and element alignment\n- Providing visual references to enhance spatial awareness\n- Building a structured reference system when designing and editing graphics\n\n## Basic Usage\n\nBelow is a simple example of initializing the GridLine plugin:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'grid-line',\n      key: 'my-grid-line', // Specify a unique identifier for dynamic updates\n      size: 20,\n      stroke: '#0001',\n      follow: true,\n    },\n  ],\n});\n```\n\n## Online Experience\n\n<embed src=\"@/common/api/plugins/grid-line.md\"></embed>\n\n## Configuration Options\n\n| Property        | Description                                                                                         | Type                                              | Default     | Required |\n| --------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ----------- | -------- |\n| type            | Plugin type                                                                                         | string                                            | `grid-line` | ✓        |\n| key             | Unique identifier for the plugin, used to get the plugin instance or update plugin options          | string                                            | -           |          |\n| border          | Whether to display the border                                                                       | boolean                                           | true        |          |\n| borderLineWidth | Border line width                                                                                   | number                                            | 1           |          |\n| borderStroke    | Border color, see [CSS border-color](https://developer.mozilla.org/en-US/docs/Web/CSS/border-color) | string                                            | `#eee`      |          |\n| borderStyle     | Border style, see [CSS border-style](https://developer.mozilla.org/en-US/docs/Web/CSS/border-style) | string                                            | `solid`     |          |\n| follow          | Whether to follow canvas movements                                                                  | boolean \\| {translate ?: boolean, zoom?: boolean} | false       |          |\n| lineWidth       | Grid line width                                                                                     | number \\| string                                  | 1           |          |\n| size            | Grid unit size in pixels                                                                            | number                                            | 20          |          |\n| stroke          | Grid line color                                                                                     | string                                            | `#eee`      |          |\n\n### follow\n\nThe `follow` property controls whether the grid lines follow the canvas transformations. It supports two configuration methods:\n\n1. **Boolean Configuration**: When set to `true`, the grid lines follow both canvas translation and zoom; when set to `false`, they remain static.\n\n```js\n// Enable both translation and zoom following\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'grid-line',\n      follow: true,\n    },\n  ],\n});\n```\n\n2. **Object Configuration**: Allows more precise control over the grid line following behavior.\n\n```js\n// Follow translation only, not zoom\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'grid-line',\n      follow: {\n        translate: true, // Follow translation\n        zoom: false, // Do not follow zoom\n      },\n    },\n  ],\n});\n\n// Follow zoom only, not translation\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'grid-line',\n      follow: {\n        translate: false, // Do not follow translation\n        zoom: true, // Follow zoom\n      },\n    },\n  ],\n});\n```\n\nWhen grid lines follow zoom, they maintain a relative position to the canvas content, making alignment references more precise. Following translation allows the grid to move with the canvas content, enhancing the visual experience of spatial continuity.\n\n## Code Examples\n\n### Basic Grid Line\n\nThe simplest way is to use the preset configuration directly:\n\n```js\nconst graph = new Graph({\n  // Other configurations...\n  plugins: ['grid-line'],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  data: { nodes: [{ id: 'node-1', style: { x: 150, y: 75 } }] },\n  behaviors: ['drag-canvas'],\n  plugins: ['grid-line'],\n});\n\ngraph.render();\n```\n\n### Custom Style\n\nYou can customize the grid line style as needed:\n\n```js\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'grid-line',\n      stroke: '#1890ff33', // Blue semi-transparent grid line\n      lineWidth: 2,\n      size: 40, // Larger grid unit\n      borderStroke: '#1890ff', // Blue border\n      borderLineWidth: 2,\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  data: { nodes: [{ id: 'node-1', style: { x: 150, y: 75 } }] },\n  behaviors: ['drag-canvas'],\n  plugins: [\n    {\n      type: 'grid-line',\n      stroke: '#1890ff33', // Blue semi-transparent grid line\n      lineWidth: 2,\n      size: 40, // Larger grid\n      borderStroke: '#1890ff', // Blue border\n      borderLineWidth: 2,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### Follow Movement\n\nEnabling the follow option allows the grid to move with the canvas, enhancing user experience:\n\n```js\nconst graph = new Graph({\n  // Other configurations...\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n  plugins: [\n    {\n      type: 'grid-line',\n      follow: true, // Grid follows canvas movement\n    },\n  ],\n});\n```\n\nTry dragging/zooming the canvas to observe the grid following effect:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  data: { nodes: [{ id: 'node-1', style: { x: 150, y: 75 } }] },\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n  plugins: [\n    {\n      type: 'grid-line',\n      follow: true, // Grid follows canvas movement\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### Dynamic Grid Update\n\nUse the key identifier to dynamically update grid properties at runtime:\n\n```js\n// Initial configuration\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'grid-line',\n      key: 'my-grid',\n      size: 20,\n    },\n  ],\n});\n\n// Subsequent dynamic updates\ngraph.updatePlugin({\n  key: 'my-grid',\n  size: 40, // Update grid size\n  stroke: '#ff4d4f', // Update grid color\n});\n```\n\n## Cases\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['drag-canvas'],\n  plugins: [{ key: 'grid-line', type: 'grid-line', follow: false }],\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  gui\n    .add({ follow: false }, 'follow')\n    .name('Follow')\n    .onChange((value) => {\n      graph.updatePlugin({\n        key: 'grid-line',\n        follow: value,\n      });\n    });\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/GridLine.zh.md",
    "content": "---\ntitle: 网格线 GridLine\norder: 8\n---\n\n## 概述\n\n网格线插件为画布提供可视化辅助线，帮助用户精确定位和对齐图形元素，是图形绘制中不可或缺的辅助工具。\n\n## 使用场景\n\n网格线插件主要适用于以下场景：\n\n- 辅助用户精确绘图和元素对齐\n- 提供视觉参考，增强空间感知\n- 在设计和编辑图形时构建结构化的参考系统\n\n## 基本用法\n\n以下是一个简单的 GridLine 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'grid-line',\n      key: 'my-grid-line', // 指定唯一标识符，便于后续动态更新\n      size: 20,\n      stroke: '#0001',\n      follow: true,\n    },\n  ],\n});\n```\n\n## 在线体验\n\n<embed src=\"@/common/api/plugins/grid-line.md\"></embed>\n\n## 配置项\n\n| 属性            | 描述                                                                                                     | 类型                                               | 默认值      | 必选 |\n| --------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ----------- | ---- |\n| type            | 插件类型                                                                                                 | string                                             | `grid-line` | ✓    |\n| key             | 插件的唯一标识，可用于获取插件实例或更新插件选项                                                         | string                                             | -           |      |\n| border          | 是否显示边框                                                                                             | boolean                                            | true        |      |\n| borderLineWidth | 边框线宽                                                                                                 | number                                             | 1           |      |\n| borderStroke    | 边框颜色，详细属性参考 [CSS border-color](https://developer.mozilla.org/zh-CN/docs/Web/CSS/border-color) | string                                             | `#eee`      |      |\n| borderStyle     | 边框样式，详细属性参考 [CSS border-style](https://developer.mozilla.org/zh-CN/docs/Web/CSS/border-style) | string                                             | `solid`     |      |\n| follow          | 是否跟随画布移动                                                                                         | boolean \\｜ {translate ?: boolean, zoom?: boolean} | false       |      |\n| lineWidth       | 网格线宽度                                                                                               | number \\| string                                   | 1           |      |\n| size            | 网格单元大小，单位为像素                                                                                 | number                                             | 20          |      |\n| stroke          | 网格线颜色                                                                                               | string                                             | `#eee`      |      |\n\n### follow\n\n`follow` 属性用于控制网格线是否跟随画布的变换操作。它支持两种配置方式：\n\n1. **布尔值配置**：当设置为 `true` 时，网格线会同时跟随画布的平移和缩放；设置为 `false` 时则保持静态。\n\n```js\n// 同时启用跟随平移和缩放\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'grid-line',\n      follow: true,\n    },\n  ],\n});\n```\n\n2. **对象配置**：可以更精细地控制网格线的跟随行为。\n\n```js\n// 仅跟随平移，不跟随缩放\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'grid-line',\n      follow: {\n        translate: true, // 跟随平移\n        zoom: false, // 不跟随缩放\n      },\n    },\n  ],\n});\n\n// 仅跟随缩放，不跟随平移\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'grid-line',\n      follow: {\n        translate: false, // 不跟随平移\n        zoom: true, // 跟随缩放\n      },\n    },\n  ],\n});\n```\n\n当网格线跟随缩放时，它会保持与画布内容的相对位置关系，使得对齐参考更加精准。跟随平移则让网格随着画布内容一起移动，增强空间连续性的视觉体验。\n\n## 代码示例\n\n### 基础网格线\n\n最简单的方式是直接使用预设配置：\n\n```js\nconst graph = new Graph({\n  // 其他配置...\n  plugins: ['grid-line'],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  data: { nodes: [{ id: 'node-1', style: { x: 150, y: 75 } }] },\n  behaviors: ['drag-canvas'],\n  plugins: ['grid-line'],\n});\n\ngraph.render();\n```\n\n### 自定义样式\n\n您可以根据需要自定义网格线的样式：\n\n```js\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'grid-line',\n      stroke: '#1890ff33', // 蓝色半透明网格线\n      lineWidth: 2,\n      size: 40, // 更大的网格单元\n      borderStroke: '#1890ff', // 蓝色边框\n      borderLineWidth: 2,\n    },\n  ],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  data: { nodes: [{ id: 'node-1', style: { x: 150, y: 75 } }] },\n  behaviors: ['drag-canvas'],\n  plugins: [\n    {\n      type: 'grid-line',\n      stroke: '#1890ff33', // 蓝色半透明网格线\n      lineWidth: 2,\n      size: 40, // 更大的网格\n      borderStroke: '#1890ff', // 蓝色边框\n      borderLineWidth: 2,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### 跟随移动\n\n启用 follow 选项可以让网格跟随画布移动，增强用户体验：\n\n```js\nconst graph = new Graph({\n  // 其他配置...\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n  plugins: [\n    {\n      type: 'grid-line',\n      follow: true, // 网格跟随画布移动\n    },\n  ],\n});\n```\n\n试着拖拽/缩放画布，观察网格的跟随效果：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  data: { nodes: [{ id: 'node-1', style: { x: 150, y: 75 } }] },\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n  plugins: [\n    {\n      type: 'grid-line',\n      follow: true, // 网格跟随画布移动\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### 动态更新网格\n\n使用 key 标识符可以在运行时动态更新网格属性：\n\n```js\n// 初始化配置\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'grid-line',\n      key: 'my-grid',\n      size: 20,\n    },\n  ],\n});\n\n// 后续动态更新\ngraph.updatePlugin({\n  key: 'my-grid',\n  size: 40, // 更新网格大小\n  stroke: '#ff4d4f', // 更新网格颜色\n});\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['drag-canvas'],\n  plugins: [{ key: 'grid-line', type: 'grid-line', follow: false }],\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  gui\n    .add({ follow: false }, 'follow')\n    .name('Follow')\n    .onChange((value) => {\n      graph.updatePlugin({\n        key: 'grid-line',\n        follow: value,\n      });\n    });\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/History.en.md",
    "content": "---\ntitle: History\norder: 9\n---\n\n## Overview\n\nThis plugin is used to implement the **Undo** and **Redo** functions in graph editing. By recording the historical state stack of user operations, it supports backtracking or restoring operations during graph interactions. The plugin provides users with comprehensive configuration options and APIs.\n\n## Usage Scenarios\n\nThe history plugin is suitable for all scenarios involving graph editing.\n\n## Online Experience\n\n<embed src=\"@/common/api/plugins/history.md\"></embed>\n\n## Basic Usage\n\nAdd this plugin in the graph configuration:\n\n**1. Quick Configuration (Static)**\n\nDeclare directly using a string. This method is simple but only supports default configurations and cannot be dynamically modified after configuration:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  plugins: ['history'],\n});\n```\n\n**2. Object Configuration (Recommended)**\n\nConfigure using an object form, supporting custom parameters, and allowing dynamic updates at runtime:\n\n```javascript\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'history',\n      key: 'history-1',\n      stackSize: 10,\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Property         | Description                                                                                                                                                                                                   | Type                                                           | Default Value | Required |\n| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------- | -------- |\n| afterAddCommand  | Called after a command is added to the `Undo/Redo` queue. `revert` is `true` for undo operations and `false` for redo operations                                                                              | (cmd: [Command](#command), revert: boolean) => void            | -             |          |\n| beforeAddCommand | Called before a command is added to the `Undo/Redo` queue. If this method returns `false`, the command will not be added to the queue. `revert` is `true` for undo operations and `false` for redo operations | (cmd: [Command](#command), revert: boolean) => boolean \\| void | -             |          |\n| executeCommand   | Callback function when executing a command                                                                                                                                                                    | (cmd: [Command](#command)) => void                             | -             |          |\n| stackSize        | Maximum length of history records to be recorded                                                                                                                                                              | number                                                         | 0 (unlimited) |          |\n\n### Parameter Type Description\n\n#### `Command`\n\n```typescript\n// Single history command\ninterface Command {\n  current: CommandData; // Current data\n  original: CommandData; // Original data\n  animation: boolean; // Whether to enable animation\n}\n// Single history command data\ninterface CommandData {\n  add: GraphData; // Added data\n  update: GraphData; // Updated data\n  remove: GraphData; // Removed data\n}\n// Graph data\ninterface GraphData {\n  nodes?: NodeData[]; // Node data\n  edges?: EdgeData[]; // Edge data\n  combos?: ComboData[]; // Combo data\n}\n```\n\n## API\n\nThe history plugin provides the following APIs for users to use as needed. For how to call plugin methods, please refer to the [Plugin Overview Document](/en/manual/plugin/overview#calling-plugin-methods)\n\n### History.canRedo()\n\nDetermines whether a **redo** operation can be performed. If there are records in the redo stack, it returns `true`; otherwise, it returns `false`.\n\n```typescript\ncanRedo(): boolean;\n```\n\n**Example:**\n\n```typescript\nconst canRedo = historyInstance.canRedo();\nif (canRedo) {\n  console.log('Redo operation can be performed');\n} else {\n  console.log('Redo stack is empty, cannot redo');\n}\n```\n\n### History.canUndo()\n\nDetermines whether an **undo** operation can be performed. If there are records in the undo stack, it returns `true`; otherwise, it returns `false`.\n\n```typescript\ncanUndo(): boolean;\n```\n\n**Example:**\n\n```typescript\nconst canUndo = historyInstance.canUndo();\nif (canUndo) {\n  console.log('Undo operation can be performed');\n} else {\n  console.log('Undo stack is empty, cannot undo');\n}\n```\n\n### History.clear()\n\nClears the history records, including the undo and redo stacks.\n\n```typescript\nclear(): void;\n```\n\n**Example:**\n\n```typescript\nhistoryInstance.clear();\nconsole.log('History records cleared');\n```\n\n### History.on()\n\nListens to history events, allowing users to execute custom logic when specific events occur.\n\n```typescript\non(event: Loosen<HistoryEvent>, handler: (e: { cmd?: Command | null }) => void): void;\n```\n\nParameter Type Description:\n\n- HistoryEvent\n\n  ```typescript\n  enum HistoryEvent {\n    UNDO = 'undo', // When a command is undone\n    REDO = 'redo', // When a command is redone\n    CANCEL = 'cancel', // When a command is canceled\n    ADD = 'add', // When a command is added to the queue\n    CLEAR = 'clear', // When the history queue is cleared\n    CHANGE = 'change', // When the history queue changes\n  }\n  ```\n\n- Command\n\n  Please refer to the previous [Command](#parameter-type-description) type description\n\nExample:\n\n```typescript\nhistoryInstance.on(HistoryEvent.UNDO, () => {\n  console.log('Undo operation executed');\n});\n```\n\n### History.redo()\n\nPerforms a **redo** operation and returns the plugin instance. If the redo stack is empty, no operation is performed.\n\n```typescript\nredo(): History;\n```\n\n**Example:**\n\n```typescript\nhistoryInstance.redo();\nconsole.log('Redo operation executed');\n```\n\n### History.undo()\n\nPerforms an **undo** operation and returns the plugin instance. If the undo stack is empty, no operation is performed.\n\n```typescript\nundo(): History;\n```\n\n**Example:**\n\n```typescript\nhistoryInstance.undo();\nconsole.log('Undo operation executed');\n```\n\n### History.undoAndCancel()\n\nPerforms an undo operation without recording it in the history and returns the plugin instance. Note that this operation will clear the **redo** stack.\n\n```typescript\nundoAndCancel(): History;\n```\n\n**Example:**\n\n```typescript\nhistoryInstance.undoAndCancel();\nconsole.log('Undo and cancel operation executed');\n```\n\n## History Modes\n\nThis plugin supports two history modes:\n\n### Default Mode\n\nIn default mode, every time a **render** is triggered (for example, after updating element data, the user actively executes the `graph.draw()` method to trigger rendering), the plugin records the data **before** and **after** rendering and stacks it as an operation record.\n\n### Custom Mode\n\n#### Scenario Description\n\nIn actual needs, a user's graph editing operation may involve **multiple renders**. For example, in one editing operation, first display nodes A and B, then display the connection from A to B. This involves two renders (i.e., the user needs to perform `graph.draw()` twice). In this scenario, the default mode will stack two history records, which are:\n\n- Display nodes A and B\n- Display the connection from A to B\n\nObviously, in actual business, one operation should only require one undo.\n\nBut here, when undoing this operation, the user needs to call the `undo` method twice, which means two undos are required.\n\n#### Scenario Support\n\nTo support such scenarios, G6 provides a batch controller (`BatchController`, [refer to the source code](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/batch.ts)), which is provided in the graph instance context.\n\nThe history plugin implements custom operation records based on this batch controller. The code example is as follows:\n\n```typescript\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'history',\n      key: 'history',\n    },\n  ],\n});\n\ngraph.context.batch.startBatch(); // Start batch operation\ngraph.addNodeData(...); // Display nodes A and B\ngraph.draw(); // First render trigger\ngraph.addEdgeData(...); // Display the connection from A to B\ngraph.draw(); // Second render trigger\ngraph.context.batch.endBatch(); // End batch operation\n```\n\nIn the example:\n\n- By calling the `startBatch` method of the batch controller instance, the history plugin is informed that batch operations are now being performed. Before the batch operation ends, no matter how many renders are triggered, no history records should be stacked (the history plugin will record the change data for each render trigger).\n- After completing the last data change, call the `endBatch()` method. The history plugin listens for the completion of the batch operation and stacks this batch operation as a history record.\n\nFinally, the user only needs to perform one `undo` to undo.\n\n## Code Examples\n\nBelow are some common cases with corresponding code references.\n\n### Undo and Redo Button States\n\nIn actual business scenarios, you may need to customize the toolbar of the canvas, which involves the enable and disable states of the undo and redo buttons.\n\n```typescript\nconst canUndo = false;\nconst canRedo = false;\n\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'history',\n      key: 'history',\n    },\n  ],\n});\nconst historyInstance = graph.getPluginInstance('history');\n\nhistoryInstance.on(HistoryEvent.CHANGE, () => {\n  canUndo = historyInstance.canUndo();\n  canRedo = historyInstance.canRedo();\n});\n```\n\nIn the example, by listening to the `HistoryEvent.CHANGE` event, which is triggered when the history queue changes, it is determined in real-time whether undo and redo operations can be performed.\n\n### Determine Whether a Command is Allowed to Enter the Queue\n\nHere is a simple scenario: only the operation of removing elements is allowed to enter the history queue.\n\n```typescript\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'history',\n      key: 'history',\n      beforeAddCommand: (cmd) => {\n        return (\n          cmd.current.remove?.nodes?.length > 0 ||\n          cmd.current.remove?.combos?.length > 0 ||\n          cmd.current.remove?.edges?.length > 0\n        );\n      },\n    },\n  ],\n});\n```\n\nIn the example, the configuration option [beforeAddCommand](#beforeAddCommand) is used to determine whether there are elements removed in `cmd.current.remove`.\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/History.zh.md",
    "content": "---\ntitle: 历史记录 History\norder: 9\n---\n\n## 概述\n\n该插件用于实现图编辑的 **撤销（Undo）** 和 **重做（Redo）** 功能，通过记录用户操作的历史状态堆栈，支持在图交互过程中进行回溯或恢复操作。该插件为用户提供了完善的配置项和 API 。\n\n## 使用场景\n\n历史记录插件适用于所有涉及到图编辑的场景。\n\n## 在线体验\n\n<embed src=\"@/common/api/plugins/history.md\"></embed>\n\n## 基本用法\n\n在图配置中添加这一插件：\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  plugins: ['history'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'history',\n      key: 'history-1',\n      stackSize: 10,\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 属性             | 描述                                                                                                                                                                | 类型                                                           | 默认值      | 必选 |\n| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ----------- | ---- |\n| afterAddCommand  | 当一个命令被添加到 `Undo/Redo` 队列后被调用。`revert` 为 `true` 时表示撤销操作，为 `false` 时表示重做操作                                                           | (cmd: [Command](#command), revert: boolean) => void            | -           |      |\n| beforeAddCommand | 当一个命令被添加到 `Undo/Redo` 队列前被调用，如果该方法返回 `false`，那么这个命令将不会被添加到队列中。`revert` 为 `true` 时表示撤销操作，为 `false` 时表示重做操作 | (cmd: [Command](#command), revert: boolean) => boolean \\| void | -           |      |\n| executeCommand   | 执行命令时的回调函数                                                                                                                                                | (cmd: [Command](#command)) => void                             | -           |      |\n| stackSize        | 最多记录该数据长度的历史记录                                                                                                                                        | number                                                         | 0（不限制） |      |\n\n### 参数类型说明\n\n#### `Command`\n\n```typescript\n// 单条历史记录命令\ninterface Command {\n  current: CommandData; // 当前数据\n  original: CommandData; // 原始数据\n  animation: boolean; // 是否开启动画\n}\n// 单条历史记录命令数据\ninterface CommandData {\n  add: GraphData; // 新增的数据\n  update: GraphData; // 更新的数据\n  remove: GraphData; // 移除的数据\n}\n// 图数据\ninterface GraphData {\n  nodes?: NodeData[]; // 节点数据\n  edges?: EdgeData[]; // 边数据\n  combos?: ComboData[]; // Combo 数据\n}\n```\n\n## API\n\nhistory 插件提供了以下 API 供用户按需使用，调用插件方法的方式请参考 [插件总览文档](/manual/plugin/overview#调用插件方法)\n\n### History.canRedo()\n\n判断是否可以进行**重做**操作。如果重做堆栈中有记录，则返回 `true`，否则返回 `false`。\n\n```typescript\ncanRedo(): boolean;\n```\n\n**示例：**\n\n```typescript\nconst canRedo = historyInstance.canRedo();\nif (canRedo) {\n  console.log('可以进行重做操作');\n} else {\n  console.log('重做堆栈为空，无法重做');\n}\n```\n\n### History.canUndo()\n\n判断是否可以进行**撤销**操作。如果撤销堆栈中有记录，则返回 `true`，否则返回 `false`。\n\n```typescript\ncanUndo(): boolean;\n```\n\n**示例：**\n\n```typescript\nconst canUndo = historyInstance.canUndo();\nif (canUndo) {\n  console.log('可以进行撤销操作');\n} else {\n  console.log('撤销堆栈为空，无法撤销');\n}\n```\n\n### History.clear()\n\n清空历史记录，包括撤销和重做堆栈。\n\n```typescript\nclear(): void;\n```\n\n**示例：**\n\n```typescript\nhistoryInstance.clear();\nconsole.log('历史记录已清空');\n```\n\n### History.on()\n\n监听历史记录事件，允许用户在特定事件发生时执行自定义逻辑。\n\n```typescript\non(event: Loosen/<HistoryEvent/>, handler: (e: { cmd?: Command | null }) => void): void;\n```\n\n参数类型说明：\n\n- HistoryEvent\n\n  ```typescript\n  enum HistoryEvent {\n    UNDO = 'undo', // 当命令被撤销时\n    REDO = 'redo', // 当命令被重做时\n    CANCEL = 'cancel', // 当命令被取消时\n    ADD = 'add', // 当命令被添加到队列时\n    CLEAR = 'clear', // 当历史队列被清空时\n    CHANGE = 'change', // 当历史队列发生变化时\n  }\n  ```\n\n- Command\n\n  请参考前面的 [Command](#参数类型说明) 类型说明\n\n示例：\n\n```typescript\nhistoryInstance.on(HistoryEvent.UNDO, () => {\n  console.log('执行了撤销操作');\n});\n```\n\n### History.redo()\n\n执行**重做**操作，并返回插件实例。如果重做堆栈为空，则不执行任何操作。\n\n```typescript\nredo(): History;\n```\n\n**示例：**\n\n```typescript\nhistoryInstance.redo();\nconsole.log('执行了重做操作');\n```\n\n### History.undo()\n\n执行**撤销**操作，并返回插件实例。如果撤销堆栈为空，则不执行任何操作。\n\n```typescript\nundo(): History;\n```\n\n**示例：**\n\n```typescript\nhistoryInstance.undo();\nconsole.log('执行了撤销操作');\n```\n\n### History.undoAndCancel()\n\n执行撤销操作且不计入历史记录，并返回插件实例。注意，执行该操作会清空**重做**栈。\n\n```typescript\nundoAndCancel(): History;\n```\n\n**示例：**\n\n```typescript\nhistoryInstance.undoAndCancel();\nconsole.log('执行了撤销并取消操作');\n```\n\n## 历史记录模式\n\n该插件支持两种历史记录模式：\n\n### 默认模式\n\n默认模式下，每一次触发**渲染后**（比如更新元素数据后，用户主动执行 `graph.draw()` 方法触发渲染），插件会把**渲染前**和**渲染后**的数据记录下来并作为一次操作记录入栈。\n\n### 自定义模式\n\n#### 场景描述\n\n实际需求中，用户的一次图编辑操作可能涉及到**多次渲染**，比如，一次编辑操作中，首先把节点 A、B 展示出来，然后展示 A->B 的连线，这里就涉及到两次渲染（即用户需要进行两次 `graph.draw()` ），这种场景下，默认模式会入栈两次历史记录，分别是：\n\n- 展示节点 A 和 B\n- 展示 A->B 的连线\n\n显然，实际业务中，一次操作，也应该只需一次撤销。\n\n但这里在撤销本次操作时，用户需要调用两次 `undo` 方法，也就是需要进行两次撤销。\n\n#### 场景支持\n\n为了支持这样的场景，G6 提供了一个批量控制器（ `BatchController`，[可参考源码](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/batch.ts)），在图实例上下文中提供了这个批量控制器实例。\n\n历史记录插件则基于这个批量控制器，来实现自定义操作记录，代码示例如下：\n\n```typescript\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'history',\n      key: 'history',\n    },\n  ],\n});\n\ngraph.context.batch.startBatch(); // 开始批量操作\ngraph.addNodeData(...); // 把节点 A、B 展示出来\ngraph.draw(); // 第一次触发渲染\ngraph.addEdgeData(...); // 把 A->B 连线展示出来\ngraph.draw(); // 第二次触发渲染\ngraph.context.batch.endBatch(); // 结束批量操作\n```\n\n示例中：\n\n- 通过调用批量控制器实例的 `startBatch` 方法，告诉历史记录插件，现在开始进行批量操作，在批量操作没有结束前，不管触发多少次渲染，都不能进行历史记录入栈（历史记录插件会把每次触发渲染的变更数据记录下来）\n- 在完成最后一次数据变更后，调用 `endBatch()` 方法，历史记录插件监听到批量操作完成，则把本次批量操作作为一次历史记录入栈\n\n最终，用户只需要进行一次 `undo` 即可撤销。\n\n## 代码示例\n\n下面列举一些常见的案例，并给出相应的代码参考\n\n### 撤销、重做按钮状态\n\n实际业务场景中，可能需要自定义画布的工具栏，也就涉及到撤销和重做按钮的启禁用状态\n\n```typescript\nconst canUndo = false;\nconst canRedo = false;\n\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'history',\n      key: 'history',\n    },\n  ],\n});\nconst historyInstance = graph.getPluginInstance('history');\n\nhistoryInstance.on(HistoryEvent.CHANGE, () => {\n  canUndo = historyInstance.canUndo();\n  canRedo = historyInstance.canRedo();\n});\n```\n\n示例中通过监听 `HistoryEvent.CHANGE` 事件，这个事件在历史队列发生变化时会触发，每次发生变化后，实时判断当前是否可以进行撤销和重做操作\n\n### 判断是否允许命令进入队列\n\n这里实现一个简单的场景：只有移除元素的操作才允许进入历史记录队列\n\n```typescript\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'history',\n      key: 'history',\n      beforeAddCommand: (cmd) => {\n        return (\n          cmd.current.remove?.nodes?.length > 0 ||\n          cmd.current.remove?.combos?.length > 0 ||\n          cmd.current.remove?.edges?.length > 0\n        );\n      },\n    },\n  ],\n});\n```\n\n示例中通过配置项 [beforeAddCommand](#beforeAddCommand) 来实现，判断 `cmd.current.remove` 里面是否存在被移除的元素\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Hull.en.md",
    "content": "---\ntitle: Hull\norder: 10\n---\n\n## Overview\n\nHull is used to process and represent the convex or concave polygon bounding box of a set of points. It can wrap a set of nodes in a minimal geometric shape, helping users better understand and analyze datasets.\n\n- **Convex Hull**: This is a convex polygon that contains all the points and has no indentations.\n- **Concave Hull**: This is a concave polygon that also contains all the points but may have indentations. The degree of indentation is controlled by the concavity parameter.\n\n## Usage Scenarios\n\nThe hull plugin is mainly applicable to the following scenarios:\n\n- Wrapping node collections in data visualization\n- Providing visual references to enhance spatial awareness\n- Identifying the collection relationship of specific nodes in complex network graphs\n\n## Basic Usage\n\nBelow is a simple example of initializing the Hull plugin:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'hull',\n      key: 'my-hull', // Specify a unique identifier for subsequent dynamic updates\n      members: ['node-1', 'node-2'], // List of node IDs to be wrapped\n      concavity: Infinity, // Default to convex hull\n    },\n  ],\n});\n```\n\n## Online Experience\n\n<embed src=\"@/common/api/plugins/hull.md\"></embed>\n\n## Configuration Options\n\n| Property         | Description                                                                                              | Type                                               | Default Value | Required |\n| ---------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------- | -------- |\n| type             | Plugin type                                                                                              | string                                             | `hull`        | ✓        |\n| key              | Unique identifier for the plugin, used for subsequent updates                                            | string                                             | -             |          |\n| members          | Elements within the Hull, including nodes and edges                                                      | string[]                                           | -             | ✓        |\n| concavity        | Concavity, the larger the value, the smaller the concavity; default is Infinity representing Convex Hull | number                                             | Infinity      |          |\n| corner           | Corner type, options are `rounded` \\| `smooth` \\| `sharp`                                                | string                                             | `rounded`     |          |\n| padding          | Padding                                                                                                  | number                                             | `10`          |          |\n| label            | Whether to display the label                                                                             | boolean                                            | true          |          |\n| labelPlacement   | Label position                                                                                           | `left` \\| `right` \\| `top` \\| `bottom` \\| `center` | `bottom`      |          |\n| labelBackground  | Whether to display the background                                                                        | boolean                                            | false         |          |\n| labelPadding     | Label padding                                                                                            | number \\| number[]                                 | 0             |          |\n| labelCloseToPath | Whether the label is close to the hull                                                                   | boolean                                            | true          |          |\n| labelAutoRotate  | Whether the label rotates with the hull, effective only when closeToPath is true                         | boolean                                            | true          |          |\n| labelOffsetX     | X-axis offset                                                                                            | number                                             | 0             |          |\n| labelOffsetY     | Y-axis offset                                                                                            | number                                             | 0             |          |\n| labelMaxWidth    | Maximum width of the text, exceeding will automatically ellipsis                                         | number                                             | 0             |          |\n\nFor complete label styles, see [this link](https://g6.antv.antgroup.com/manual/element/node/base-node#%E6%A0%87%E7%AD%BE%E6%A0%B7%E5%BC%8F)\n\n### concavity\n\nThe concavity attribute is used to control the concavity of the Hull. When set to Infinity, a convex hull is generated; otherwise, a concave hull is generated.\n\n```js\n// Convex hull example\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'hull',\n      concavity: Infinity, // Convex hull\n      members: ['node-1', 'node-2'],\n    },\n  ],\n});\n\n// Concave hull example\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'hull',\n      concavity: 50, // Concave hull\n      members: ['node-1', 'node-2'],\n    },\n  ],\n});\n```\n\n## Code Examples\n\n### Basic Hull\n\nThe simplest way is to use the preset configuration directly:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'hull',\n      members: ['node-1', 'node-2'], // List of node IDs to be wrapped\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      type: 'hull',\n      members: ['node-1', 'node-2'], // List of node IDs to be wrapped\n    },\n  ],\n  behaviors: ['zoom-canvas', 'drag-canvas'],\n});\n\ngraph.render();\n```\n\n### Custom Styles\n\nYou can customize the style of the Hull as needed, such as adjusting color, transparency, and other properties.\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'hull',\n      members: ['node-1', 'node-2', 'node-3'],\n      stroke: '#ff000033', // Red semi-transparent border\n      fill: '#7e3feb', // Light purple fill\n      fillOpacity: 0.2,\n      lineWidth: 2,\n      padding: 15, // Larger padding\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      type: 'hull',\n      members: ['node-1', 'node-2', 'node-3'],\n      stroke: '#ff000033', // Red semi-transparent border\n      fill: '#7e3feb', // Light purple fill\n      fillOpacity: 0.2,\n      lineWidth: 2,\n      padding: 15, // Larger padding\n    },\n  ],\n  behaviors: ['zoom-canvas', 'drag-canvas'],\n});\n\ngraph.render();\n```\n\n### Label Configuration\n\nYou can configure the position, background, offset, and other properties of the label to enhance the visual effect.\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'hull',\n      members: ['node-1', 'node-2'],\n      label: true, // Display label\n      labelText: 'hull-a',\n      labelPlacement: 'top', // Label position\n      labelBackground: true, // Display label background\n      labelPadding: 5, // Label padding\n    },\n  ],\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      type: 'hull',\n      members: ['node-1', 'node-2'],\n      label: true, // Display label\n      labelText: 'hull-a',\n      labelPlacement: 'top', // Label position\n      labelBackground: true, // Display label background\n      labelPadding: 5, // Label padding\n    },\n  ],\n  behaviors: ['zoom-canvas', 'drag-canvas'],\n});\n\ngraph.render();\n```\n\n## Practical Cases\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/collection.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const groupedNodesByCluster = data.nodes.reduce((acc, node) => {\n      const cluster = node.data.cluster;\n      acc[cluster] ||= [];\n      acc[cluster].push(node.id);\n      return acc;\n    }, {});\n\n    const createStyle = (baseColor) => ({\n      fill: baseColor,\n      stroke: baseColor,\n      labelFill: '#fff',\n      labelPadding: 2,\n      labelBackgroundFill: baseColor,\n      labelBackgroundRadius: 5,\n    });\n\n    const graph = new Graph({\n      container: 'container',\n      data,\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n      node: {\n        palette: { field: 'cluster' },\n      },\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        linkDistance: (d) => {\n          if (d.source === 'node0' || d.target === 'node0') {\n            return 200;\n          }\n          return 80;\n        },\n      },\n      plugins: [\n        {\n          key: 'hull-a',\n          type: 'hull',\n          members: groupedNodesByCluster['a'],\n          labelText: 'cluster-a',\n          ...createStyle('#1783FF'),\n        },\n        {\n          key: 'hull-b',\n          type: 'hull',\n          members: groupedNodesByCluster['b'],\n          labelText: 'cluster-b',\n          ...createStyle('#00C9C9'),\n        },\n        {\n          key: 'hull-c',\n          type: 'hull',\n          members: groupedNodesByCluster['c'],\n          labelText: 'cluster-c',\n          ...createStyle('#F08F56'),\n        },\n        {\n          key: 'hull-d',\n          type: 'hull',\n          members: groupedNodesByCluster['d'],\n          labelText: 'cluster-d',\n          ...createStyle('#D580FF'),\n        },\n      ],\n      autoFit: 'center',\n    });\n\n    graph.render();\n  });\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Hull.zh.md",
    "content": "---\ntitle: 轮廓包围 Hull\norder: 10\n---\n\n## 概述\n\n轮廓包围（Hull）用于处理和表示一组点的凸多边形或凹多边形包围盒。它可以将一组节点包裹在一个最小的几何形状中，帮助用户更好地理解和分析数据集。\n\n- **凸包（Convex Hull）**：这是一个凸多边形，它包含所有的点，并且没有任何凹陷。\n- **凹包（Concave Hull）**：这是一个凹多边形，它同样包含所有的点，但是可能会有凹陷。凹包的凹陷程度由 concavity 参数控制。\n\n## 使用场景\n\n轮廓包围插件主要适用于以下场景：\n\n- 数据可视化中的节点集合包裹\n- 提供视觉参考，增强空间感知\n- 在复杂网络图中标识特定节点的集合关系\n\n## 基本用法\n\n以下是一个简单的 Hull 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'hull',\n      key: 'my-hull', // 指定唯一标识符，便于后续动态更新\n      members: ['node-1', 'node-2'], // 需要包裹的节点 ID 列表\n      concavity: Infinity, // 默认为凸包\n    },\n  ],\n});\n```\n\n## 在线体验\n\n<embed src=\"@/common/api/plugins/hull.md\"></embed>\n\n## 配置项\n\n| 属性             | 描述                                                       | 类型                                               | 默认值    | 必选 |\n| ---------------- | ---------------------------------------------------------- | -------------------------------------------------- | --------- | ---- |\n| type             | 插件类型                                                   | string                                             | `hull`    | ✓    |\n| key              | 插件唯一标识符，用于后续更新                               | string                                             | -         |      |\n| members          | Hull 内的元素，包括节点和边                                | string[]                                           | -         | ✓    |\n| concavity        | 凹度，数值越大凹度越小；默认为 Infinity 代表为 Convex Hull | number                                             | Infinity  |      |\n| corner           | 拐角类型，可选值为 `rounded` \\| `smooth` \\| `sharp`        | string                                             | `rounded` |      |\n| padding          | 内边距                                                     | number                                             | `10`      |      |\n| label            | 是否显示标签                                               | boolean                                            | true      |      |\n| labelPlacement   | 标签位置                                                   | `left` \\| `right` \\| `top` \\| `bottom` \\| `center` | `bottom`  |      |\n| labelBackground  | 是否显示背景                                               | boolean                                            | false     |      |\n| labelPadding     | 标签内边距                                                 | number \\| number[]                                 | 0         |      |\n| labelCloseToPath | 标签是否贴合轮廓                                           | boolean                                            | true      |      |\n| labelAutoRotate  | 标签是否跟随轮廓旋转，仅在 closeToPath 为 true 时生效      | boolean                                            | true      |      |\n| labelOffsetX     | x 轴偏移量                                                 | number                                             | 0         |      |\n| labelOffsetY     | y 轴偏移量                                                 | number                                             | 0         |      |\n| labelMaxWidth    | 文本的最大宽度，超出会自动省略                             | number                                             | 0         |      |\n\n完整的标签样式见[此链接](https://g6.antv.antgroup.com/manual/element/node/base-node#%E6%A0%87%E7%AD%BE%E6%A0%B7%E5%BC%8F)\n\n### concavity\n\nconcavity 属性用于控制 Hull 的凹度。当设置为 Infinity 时，生成的是凸包；否则会生成凹包。\n\n```js\n// 凸包示例\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'hull',\n      concavity: Infinity, // 凸包\n      members: ['node-1', 'node-2'],\n    },\n  ],\n});\n\n// 凹包示例\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'hull',\n      concavity: 50, // 凹包\n      members: ['node-1', 'node-2'],\n    },\n  ],\n});\n```\n\n## 代码示例\n\n### 基础 Hull\n\n最简单的方式是直接使用预设配置：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'hull',\n      members: ['node-1', 'node-2'], // 需要包裹的节点 ID 列表\n    },\n  ],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      type: 'hull',\n      members: ['node-1', 'node-2'], // 需要包裹的节点 ID 列表\n    },\n  ],\n  behaviors: ['zoom-canvas', 'drag-canvas'],\n});\n\ngraph.render();\n```\n\n### 自定义样式\n\n您可以根据需要自定义 Hull 的样式，例如调整颜色、透明度等属性。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'hull',\n      members: ['node-1', 'node-2', 'node-3'],\n      stroke: '#ff000033', // 红色半透明边框\n      fill: '#7e3feb', // 浅紫色填充\n      fillOpacity: 0.2,\n      lineWidth: 2,\n      padding: 15, // 更大的内边距\n    },\n  ],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'view',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      type: 'hull',\n      members: ['node-1', 'node-2', 'node-3'],\n      stroke: '#ff000033', // 红色半透明边框\n      fill: '#7e3feb', // 浅紫色填充\n      fillOpacity: 0.2,\n      lineWidth: 2,\n      padding: 15, // 更大的内边距\n    },\n  ],\n  behaviors: ['zoom-canvas', 'drag-canvas'],\n});\n\ngraph.render();\n```\n\n### 标签配置\n\n您可以配置标签的位置、背景、偏移量等属性，以增强可视化效果。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'hull',\n      members: ['node-1', 'node-2'],\n      label: true, // 显示标签\n      labelText: 'hull-a',\n      labelPlacement: 'top', // 标签位置\n      labelBackground: true, // 显示标签背景\n      labelPadding: 5, // 标签内边距\n    },\n  ],\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 300,\n  height: 150,\n  autoFit: 'center',\n  data: {\n    nodes: [\n      {\n        id: 'node-0',\n        data: { cluster: 'a' },\n        style: { x: 555, y: 151 },\n      },\n      {\n        id: 'node-1',\n        data: { cluster: 'a' },\n        style: { x: 532, y: 323 },\n      },\n      {\n        id: 'node-2',\n        data: { cluster: 'a' },\n        style: { x: 473, y: 227 },\n      },\n      {\n        id: 'node-3',\n        data: { cluster: 'a' },\n        style: { x: 349, y: 212 },\n      },\n      {\n        id: 'node-4',\n        data: { cluster: 'b' },\n        style: { x: 234, y: 201 },\n      },\n      {\n        id: 'node-5',\n        data: { cluster: 'b' },\n        style: { x: 338, y: 333 },\n      },\n      {\n        id: 'node-6',\n        data: { cluster: 'b' },\n        style: { x: 365, y: 91 },\n      },\n    ],\n    edges: [\n      {\n        source: 'node-0',\n        target: 'node-2',\n      },\n      {\n        source: 'node-1',\n        target: 'node-2',\n      },\n      {\n        source: 'node-2',\n        target: 'node-3',\n      },\n      {\n        source: 'node-3',\n        target: 'node-4',\n      },\n      {\n        source: 'node-3',\n        target: 'node-5',\n      },\n      {\n        source: 'node-3',\n        target: 'node-6',\n      },\n    ],\n  },\n  plugins: [\n    {\n      type: 'hull',\n      members: ['node-1', 'node-2'],\n      label: true, // 显示标签\n      labelText: 'hull-a',\n      labelPlacement: 'top', // 标签位置\n      labelBackground: true, // 显示标签背景\n      labelPadding: 5, // 标签内边距\n    },\n  ],\n  behaviors: ['zoom-canvas', 'drag-canvas'],\n});\n\ngraph.render();\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/collection.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const groupedNodesByCluster = data.nodes.reduce((acc, node) => {\n      const cluster = node.data.cluster;\n      acc[cluster] ||= [];\n      acc[cluster].push(node.id);\n      return acc;\n    }, {});\n\n    const createStyle = (baseColor) => ({\n      fill: baseColor,\n      stroke: baseColor,\n      labelFill: '#fff',\n      labelPadding: 2,\n      labelBackgroundFill: baseColor,\n      labelBackgroundRadius: 5,\n    });\n\n    const graph = new Graph({\n      container: 'container',\n      data,\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n      node: {\n        palette: { field: 'cluster' },\n      },\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        linkDistance: (d) => {\n          if (d.source === 'node0' || d.target === 'node0') {\n            return 200;\n          }\n          return 80;\n        },\n      },\n      plugins: [\n        {\n          key: 'hull-a',\n          type: 'hull',\n          members: groupedNodesByCluster['a'],\n          labelText: 'cluster-a',\n          ...createStyle('#1783FF'),\n        },\n        {\n          key: 'hull-b',\n          type: 'hull',\n          members: groupedNodesByCluster['b'],\n          labelText: 'cluster-b',\n          ...createStyle('#00C9C9'),\n        },\n        {\n          key: 'hull-c',\n          type: 'hull',\n          members: groupedNodesByCluster['c'],\n          labelText: 'cluster-c',\n          ...createStyle('#F08F56'),\n        },\n        {\n          key: 'hull-d',\n          type: 'hull',\n          members: groupedNodesByCluster['d'],\n          labelText: 'cluster-d',\n          ...createStyle('#D580FF'),\n        },\n      ],\n      autoFit: 'center',\n    });\n\n    graph.render();\n  });\n```\n\n```\n\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Legend.en.md",
    "content": "---\ntitle: Legend\norder: 11\n---\n\n## Overview\n\nThe Legend plugin is used to display classification information of elements in the graph, supporting the display of classification information for nodes, edges, and combos. Through the legend, users can quickly perceive the classification information of related elements in the graph and quickly locate elements by clicking on the corresponding legend items, improving user browsing efficiency.\n\n## Usage Scenarios\n\nThis plugin is mainly used for:\n\n- Quickly classifying elements through the legend\n- Quickly highlighting and locating corresponding elements through the legend\n\n## Basic Usage\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  // Other configurations...\n  plugins: [\n    {\n      type: 'legend', // Plugin type is legend\n      nodeField: 'cluster', // Array field name for node grouping\n      edgeField: 'cluster', // Array field name for edge grouping\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Property          | Description                                                                                                                                                                                    | Type                                                                                        | Default Value | Required |\n| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------- | -------- |\n| type              | Plugin type                                                                                                                                                                                    | string                                                                                      | `legend`      | ✓        |\n| key               | Unique identifier for the plugin, used for subsequent updates                                                                                                                                  | string                                                                                      | -             |          |\n| trigger           | How the legend item triggers the corresponding item highlight: <br/>- `hover`: Triggered when the mouse enters the legend item <br/>- `click`: Triggered when the mouse clicks the legend item | `hover` \\| `click`                                                                          | `hover`       |          |\n| position          | Relative position of the legend on the canvas, [optional values](#cardinalplacement)                                                                                                           | [CardinalPlacement](#cardinalplacement)                                                     | `bottom`      |          |\n| container         | Container to which the legend is mounted, if not provided, it is mounted to the container where the Graph is located                                                                           | HTMLElement \\| string                                                                       | -             |          |\n| className         | Legend canvas class name, not effective when an external container is passed                                                                                                                   | string                                                                                      | -             |          |\n| containerStyle    | Style of the legend container, not effective when an external container is passed                                                                                                              | [CSSStyleDeclaration](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration) | -             |          |\n| nodeField         | Node classification identifier                                                                                                                                                                 | string \\| (item: ElementDatum) => string                                                    | -             |          |\n| edgeField         | Edge classification identifier                                                                                                                                                                 | string \\| (item: ElementDatum) => string                                                    | -             |          |\n| comboField        | Combo classification identifier                                                                                                                                                                | string \\| (item: ElementDatum) => string                                                    | -             |          |\n| orientation       | Layout direction of legend items: <br/>- `horizontal`: Horizontal direction <br/>- `vertical`: Vertical direction                                                                              | `horizontal` \\| `vertical`                                                                  | 'horizontal'  |          |\n| layout            | Layout method: <br/>- `flex`: Flexible layout <br/>- `grid`: Grid layout                                                                                                                       | `flex` \\| `grid`                                                                            | `flex`        |          |\n| showTitle         | Whether to display the title                                                                                                                                                                   | boolean                                                                                     | false         |\n| titleText         | Title content                                                                                                                                                                                  | string                                                                                      | \"\"            |\n| x                 | Relative horizontal position of the legend on the canvas, higher priority than position                                                                                                        | number                                                                                      | -             |          |\n| y                 | Relative vertical position of the legend on the canvas, higher priority than position                                                                                                          | number                                                                                      | -             |          |\n| width             | Width of the legend                                                                                                                                                                            | number                                                                                      | 240           |          |\n| height            | Height of the legend                                                                                                                                                                           | number                                                                                      | 160           |          |\n| itemSpacing       | Spacing between the text of the legend item and the corresponding marker                                                                                                                       | number                                                                                      | 4             |          |\n| rowPadding        | Spacing between each row in the legend                                                                                                                                                         | number                                                                                      | 10            |          |\n| colPadding        | Spacing between each column in the legend                                                                                                                                                      | number                                                                                      | 10            |          |\n| itemMarkerSize    | Size of the legend item marker                                                                                                                                                                 | number                                                                                      | 16            |          |\n| itemLabelFontSize | Font size of the legend item text                                                                                                                                                              | number                                                                                      | 16            |          |\n| gridCol           | Maximum number of columns allowed for legend items when width permits                                                                                                                          | number                                                                                      | -             |          |\n| gridRow           | Maximum number of rows allowed for legend items when height permits                                                                                                                            | number                                                                                      | -             |          |\n\n### CardinalPlacement\n\nThe `position` property supports the following values:\n\n- `'top-left'`: Top left corner\n- `'top-right'`: Top right corner\n- `'bottom-left'`: Bottom left corner\n- `'bottom-right'`: Bottom right corner\n- `'left-top'`: Left side near the top\n- `'left-bottom'`: Left side near the bottom\n- `'right-top'`: Right side near the top\n- `'right-bottom'`: Right side near the bottom\n\n## Code Examples\n\n### Basic Legend\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'legend', // Plugin type is legend\n      nodeField: 'cluster', // Array field name for node grouping\n      edgeField: 'cluster', // Array field name for edge grouping\n    },\n  ],\n});\n```\n\n### Custom Legend Position\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  // Other configurations...\n  plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n      // You can quickly specify the position through position\n      // position: \"top-left\",\n      // Or you can more flexibly control the position of the legend through x, y\n      x: 20,\n      y: 20,\n    },\n  ],\n});\n```\n\n### Custom Legend Item Layout\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  // Other configurations...\n  plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n      layout: 'flex',\n      // Control to display only one row\n      gridRow: 1,\n      // Control to display 10 columns in one row, a page button will be displayed when the column width is insufficient\n      gridCol: 10,\n    },\n  ],\n});\n```\n\n## Common Issues\n\n### 1. Setting orientation is ineffective?\n\n`orientation` mainly controls the direction of the layout, and the specific display of **multiple columns in one row** or **multiple rows in one column** is mainly controlled by `gridRow` and `gridCol`. For example, if you want it to look like a vertical legend item, you can configure it like this:\n\n```js\n   plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n      layout: \"flex\",\n      // Control to display 1 column in one row\n      gridCol:1,\n      // Control to display up to 20 rows\n      gridRow: 20,\n    },\n  ],\n```\n\nThis way, it becomes a legend with only one column, conforming to the visual vertical arrangement.\n\n### 2. How to dynamically update the toolbar?\n\nYou can use the `updatePlugin` method to dynamically update the toolbar:\n\n```js\nconst graph = new Graph({\n  data,\n  // Other configurations...\n  plugins: [\n    {\n      type: 'legend',\n      key: 'my-legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n    },\n  ],\n});\n\n// Update legend position\ngraph.updatePlugin({\n  key: 'my-legend',\n  position: 'bottom-right',\n});\n```\n\n## Practical Cases\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n    { id: 'node-3', type: 'triangle', data: { cluster: 'node-type3' } },\n    { id: 'node-4', type: 'diamond', data: { cluster: 'node-type4' } },\n  ],\n  edges: [\n    { source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } },\n    { source: 'node-1', target: 'node-4', data: { cluster: 'edge-type2' } },\n    { source: 'node-3', target: 'node-4' },\n    { source: 'node-2', target: 'node-4', data: { cluster: 'edge-type3' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: { size: 32 },\n    palette: {\n      field: 'cluster',\n    },\n  },\n  layout: {\n    type: 'force',\n  },\n  plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Legend.zh.md",
    "content": "---\ntitle: 图例 Legend\norder: 11\n---\n\n## 概述\n\n图例（Legend）插件用于展示图中元素的分类信息，支持节点、边、组合的分类信息展示。通过图例，用户可以快速感知到图中相关元素的分类信息，也可以通过点击对应图例项来快速定位到元素，提高用户的浏览效率。\n\n## 使用场景\n\n这一插件主要用于：\n\n- 通过图例快速对元素进行分类\n- 通过图例快速高亮定位到对应元素\n\n## 基本用法\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  // 其他配置...\n  plugins: [\n    {\n      type: 'legend', // 插件类型为 legend\n      nodeField: 'cluster', // 用于节点分组的数组字段名称\n      edgeField: 'cluster', // 用于边分组的数组字段名称\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 属性              | 描述                                                                                                  | 类型                                                                                        | 默认值       | 必选 |\n| ----------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------ | ---- |\n| type              | 插件类型                                                                                              | string                                                                                      | `legend`     | ✓    |\n| key               | 插件唯一标识符，用于后续更新                                                                          | string                                                                                      | -            |      |\n| trigger           | 图例项触发对应项高亮的方式：<br/>- `hover`：鼠标移入图例项时触发 <br/>- `click`：鼠标点击图例项时触发 | `hover` \\| `click`                                                                          | `hover`      |      |\n| position          | 图例在画布中的相对位置，[可选值](#cardinalplacement)                                                  | [CardinalPlacement](#cardinalplacement)                                                     | `bottom`     |      |\n| container         | 图例挂载的容器，无则挂载到 Graph 所在容器                                                             | HTMLElement \\| string                                                                       | -            |      |\n| className         | 图例画布类名，传入外置容器时不生效                                                                    | string                                                                                      | -            |      |\n| containerStyle    | 图例的容器样式，传入外置容器时不生效                                                                  | [CSSStyleDeclaration](https://developer.mozilla.org/zh-CN/docs/Web/API/CSSStyleDeclaration) | -            |      |\n| nodeField         | 节点分类标识                                                                                          | string \\| (item: ElementDatum) => string                                                    | -            |      |\n| edgeField         | 边分类标识                                                                                            | string \\| (item: ElementDatum) => string                                                    | -            |      |\n| comboField        | 组合分类标识                                                                                          | string \\| (item: ElementDatum) => string                                                    | -            |      |\n| orientation       | 图例项的布局方向：<br/>- `horizontal`：水平方向 <br/>- `vertical`：垂直方向                           | `horizontal` \\| `vertical`                                                                  | 'horizontal' |      |\n| layout            | 布局方式：<br/>- `flex`：弹性布局 <br/>- `grid`：网格布局                                             | `flex` \\| `grid`                                                                            | `flex`       |      |\n| showTitle         | 是否显示标题                                                                                          | boolean                                                                                     | false        |\n| titleText         | 标题内容                                                                                              | string                                                                                      | \"\"           |\n| x                 | 图例在画布中的相对的横向位置，优先级高于position                                                      | number                                                                                      | -            |      |\n| y                 | 图例在画布中的相对的纵向位置，优先级高于position                                                      | number                                                                                      | -            |      |\n| width             | 图例的宽度                                                                                            | number                                                                                      | 240          |      |\n| height            | 图例的高度                                                                                            | number                                                                                      | 160          |      |\n| itemSpacing       | 图例项的文本和对应标记之间的间距                                                                      | number                                                                                      | 4            |      |\n| rowPadding        | 图例中每行之间的间距                                                                                  | number                                                                                      | 10           |      |\n| colPadding        | 图例中每列之间的间距                                                                                  | number                                                                                      | 10           |      |\n| itemMarkerSize    | 图例项标记的大小                                                                                      | number                                                                                      | 16           |      |\n| itemLabelFontSize | 图例项文本的字体大小                                                                                  | number                                                                                      | 16           |      |\n| gridCol           | 图例项在宽度允许情况下的最大列数                                                                      | number                                                                                      | -            |      |\n| gridRow           | 图例项在高度允许情况下的最大行数                                                                      | number                                                                                      | -            |      |\n\n### CardinalPlacement\n\n`position` 属性支持以下值：\n\n- `'top-left'`：左上角\n- `'top-right'`：右上角\n- `'bottom-left'`：左下角\n- `'bottom-right'`：右下角\n- `'left-top'`：左侧靠上\n- `'left-bottom'`：左侧靠下\n- `'right-top'`：右侧靠上\n- `'right-bottom'`：右侧靠下\n\n## 代码示例\n\n### 基础图例\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'legend', // 插件类型为 legend\n      nodeField: 'cluster', // 用于节点分组的数组字段名称\n      edgeField: 'cluster', // 用于边分组的数组字段名称\n    },\n  ],\n});\n```\n\n### 自定义图例位置\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  // 其他配置...\n  plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n      // 可以通过 position 快捷的来指定位置\n      // position: \"top-left\",\n      // 也可以通过x,y来更加灵活的控制图例的位置\n      x: 20,\n      y: 20,\n    },\n  ],\n});\n```\n\n### 自定义图例项布局\n\n```js\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } }],\n};\n\nconst graph = new Graph({\n  data,\n  // 其他配置...\n  plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n      layout: 'flex',\n      // 控制只显示一行\n      gridRow: 1,\n      // 控制一行显示10列，当列宽不足时会显示翻页按钮\n      gridCol: 10,\n    },\n  ],\n});\n```\n\n## 常见问题\n\n### 1. 设置了 orientation 无效？\n\n`orientation`主要控制布局的方向，具体展示**一行多列**还是**一列多行**，主要通过 `gridRow` 以及`gridCol`来控制，例如想要看起来像是竖向的图例项，则可以通过这样配置:\n\n```js\n   plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n      layout: \"flex\",\n      // 控制一行显示1列\n      gridCol:1,\n      // 控制显示最多20行\n      gridRow: 20,\n    },\n  ],\n```\n\n这样就变成了只有一列的图例，符合视觉上的竖向排列。\n\n### 2. 如何动态更新工具栏？\n\n可以使用 `updatePlugin` 方法动态更新工具栏：\n\n```js\nconst graph = new Graph({\n  data,\n  // 其他配置...\n  plugins: [\n    {\n      type: 'legend',\n      key: 'my-legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n    },\n  ],\n});\n\n// 更新图例位置\ngraph.updatePlugin({\n  key: 'my-legend',\n  position: 'bottom-right',\n});\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n    { id: 'node-3', type: 'triangle', data: { cluster: 'node-type3' } },\n    { id: 'node-4', type: 'diamond', data: { cluster: 'node-type4' } },\n  ],\n  edges: [\n    { source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } },\n    { source: 'node-1', target: 'node-4', data: { cluster: 'edge-type2' } },\n    { source: 'node-3', target: 'node-4' },\n    { source: 'node-2', target: 'node-4', data: { cluster: 'edge-type3' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: { size: 32 },\n    palette: {\n      field: 'cluster',\n    },\n  },\n  layout: {\n    type: 'force',\n  },\n  plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Minimap.en.md",
    "content": "---\ntitle: Minimap\norder: 12\n---\n\n## Overview\n\nThe main function of the Minimap is to provide users with an overall layout of the current graph content in the form of a thumbnail, allowing quick positioning of graph operation locations.\n\n**⚠️ Note**, The Minimap plugin is currently incompatible with React Node rendering mechanism. When using Minimap functionality, it is recommended to implement node rendering through [built-in nodes](/en/manual/element/node/overview) or [custom nodes](/en/manual/element/node/custom-node).\n\n## Usage Scenarios\n\nThe Minimap plugin is mainly applicable to the following scenarios:\n\n- Providing a global view for quick area positioning\n- Navigation and interaction assistance, allowing quick positioning to the target location through the minimap\n\n## Basic Usage\n\nBelow is a simple example of initializing the Minimap plugin:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      key: 'minimap',\n      type: 'minimap',\n      size: [240, 160],\n    },\n  ],\n});\n```\n\n## Online Experience\n\n<embed src=\"@/common/api/plugins/minimap.md\"></embed>\n\n## Configuration Options\n\n| Property       | Description                                                                                                             | Type                                                                                                                                                                                                   | Default Value  | Required |\n| -------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | -------- |\n| type           | Plugin type                                                                                                             | string                                                                                                                                                                                                 | `minimap`      | ✓        |\n| key            | Unique identifier for the plugin, used for subsequent updates                                                           | string                                                                                                                                                                                                 | -              |          |\n| className      | Class name of the thumbnail canvas, not effective when an external container is passed                                  | string                                                                                                                                                                                                 |                |          |\n| container      | Container to which the thumbnail is mounted, if not provided, it is mounted to the container where the Graph is located | HTMLElement \\| string                                                                                                                                                                                  |                |          |\n| containerStyle | Style of the thumbnail container, not effective when an external container is passed                                    | Partial\\<CSSStyleDeclaration\\>                                                                                                                                                                         |                |          |\n| delay          | Delay update time (milliseconds) for performance optimization                                                           | number                                                                                                                                                                                                 | 128            |          |\n| filter         | Filter for filtering out elements that do not need to be displayed                                                      | (id: string, elementType: `node` \\| `edge` \\| `combo`) => boolean                                                                                                                                      |                |          |\n| maskStyle      | Style of the mask                                                                                                       | Partial\\<CSSStyleDeclaration\\>                                                                                                                                                                         |                |          |\n| padding        | Padding                                                                                                                 | number \\| number[]                                                                                                                                                                                     | 10             |          |\n| position       | Position of the thumbnail relative to the canvas                                                                        | [number, number] \\| `left` \\| `right` \\| `top` \\| `bottom` \\| `left-top` \\| `left-bottom` \\| `right-top` \\| `right-bottom` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right` \\| `center` | `right-bottom` |          |\n| renderer       | Renderer, default is Canvas renderer                                                                                    | IRenderer                                                                                                                                                                                              |                |          |\n| shape          | Method for generating element thumbnails                                                                                | `key` \\| ((id: string, elementType: `node` \\| `edge` \\| `combo`, element: DisplayObject) => DisplayObject)                                                                                             | `key`          |          |\n| size           | Width and height                                                                                                        | [number, number]                                                                                                                                                                                       | [240, 160]     |          |\n\n### containerStyle\n\nSet the style of the thumbnail container, not effective when an external container is passed. Inherits all CSS style properties (CSSStyleDeclaration), and you can use any valid CSS property to configure the style of the thumbnail container.\n\nBelow are some common configurations:\n\n| Property     | Description                | Type   | Default Value    | Required |\n| ------------ | -------------------------- | ------ | ---------------- | -------- |\n| border       | Container border style     | string | `1px solid #ddd` | ✓        |\n| background   | Container background color | string | `#fff`           | ✓        |\n| borderRadius | Container border radius    | string | -                |          |\n| boxShadow    | Container shadow effect    | string | -                |          |\n| padding      | Container padding          | string | -                |          |\n| margin       | Container margin           | string | -                |          |\n| opacity      | Opacity                    | string | -                |          |\n\n### maskStyle\n\nSpecify the style of the mask. Inherits all CSS style properties (CSSStyleDeclaration), and you can use any valid CSS property to configure the style of the thumbnail container.\n\nBelow are some common configurations:\n\n| Property     | Description                | Type   | Default Value        | Required |\n| ------------ | -------------------------- | ------ | -------------------- | -------- |\n| border       | Container border style     | string | `1px solid #ddd`     | ✓        |\n| background   | Container background color | string | `rgba(0, 0, 0, 0.1)` | ✓        |\n| borderRadius | Container border radius    | string | -                    | -        |\n| boxShadow    | Container shadow effect    | string | -                    | -        |\n| padding      | Container padding          | string | -                    | -        |\n| margin       | Container margin           | string | -                    | -        |\n| opacity      | Opacity                    | string | -                    | -        |\n\n### position\n\nPosition of the thumbnail relative to the canvas, the thumbnail position configuration supports array form and preset value form.\n\n- Array form [number, number] represents relative position, with a value range of 0~1. For example: [0, 0] represents the top left corner of the canvas, [1, 1] represents the bottom right corner of the canvas.\n- Preset value form is used to set the fixed position of the thumbnail on the canvas, optional values are: `left` \\| `right` \\| `top` \\| `bottom` \\| `left-top` \\| `left-bottom` \\| `right-top` \\| `right-bottom` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right` \\| `center`\n\n```js\nconst graph = new Graph({\n  plugins:[\n    {\n      ... // Other configurations\n      key: 'minimap',\n      type: 'minimap',\n      position: 'right-bottom'  // Modify the position of the minimap here\n    }\n  ]\n})\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  data: {\n    nodes: Array.from({ length: 50 }).map((_, i) => ({\n      id: `node-${i}`,\n      x: Math.random() * 500,\n      y: Math.random() * 300,\n    })),\n    edges: Array.from({ length: 100 }).map((_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 50)}`,\n      target: `node-${Math.floor(Math.random() * 50)}`,\n    })),\n  },\n  node: { style: { fill: '#7e3feb' } },\n  edge: { style: { stroke: '#8b9baf' } },\n  layout: { type: 'force' },\n  behaviors: ['drag-canvas'],\n  plugins: [{ type: 'minimap', key: 'minimap', size: [240, 160], position: 'right-bottom' }],\n});\n\ngraph.render();\n```\n\n### size\n\nSet the width and height of the minimap, default value is [240, 160]\n\n```js\nconst graph = new Graph({\n  plugins:[\n    {\n      ... // Other configurations\n      key: 'minimap',\n      type: 'minimap',\n      size: [200, 120]  // Set the width and height of the minimap\n    }\n  ]\n})\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  data: {\n    nodes: Array.from({ length: 50 }).map((_, i) => ({\n      id: `node-${i}`,\n      x: Math.random() * 500,\n      y: Math.random() * 300,\n    })),\n    edges: Array.from({ length: 100 }).map((_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 50)}`,\n      target: `node-${Math.floor(Math.random() * 50)}`,\n    })),\n  },\n  node: { style: { fill: '#7e3feb' } },\n  edge: { style: { stroke: '#8b9baf' } },\n  layout: { type: 'force' },\n  behaviors: ['drag-canvas'],\n  plugins: [{ type: 'minimap', key: 'minimap', size: [200, 120], position: 'right-bottom' }],\n});\n\ngraph.render();\n```\n\n## Practical Cases\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: { nodes: Array.from({ length: 20 }).map((_, i) => ({ id: `node${i}` })) },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'minimap',\n      size: [240, 160],\n    },\n  ],\n  node: {\n    palette: 'spectral',\n  },\n  layout: {\n    type: 'circular',\n  },\n  autoFit: 'view',\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Minimap.zh.md",
    "content": "---\ntitle: 小地图 Minimap\norder: 12\n---\n\n## 概述\n\nMinimap（小地图）的主要作用是为用户提供以缩略图形式展示当前图内容的整体布局，可以快速定位图操作位置。\n\n**⚠️ 需要注意**，Minimap 插件当前不兼容 React Node 渲染机制，在需要使用 Minimap 功能的场景中，建议通过 [内置节点](/manual/element/node/overview) 或者[自定义节点](/manual/element/node/custom-node) 实现节点渲染。\n\n## 使用场景\n\nMinimap（小地图）插件主要适用于以下场景：\n\n- 提供全局视野，快速定位区域\n- 导航与交互辅助，通过操作小地图可以快速定位到目标位置\n\n## 基本用法\n\n以下是一个简单的 Minimap 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      key: 'minimap',\n      type: 'minimap',\n      size: [240, 160],\n    },\n  ],\n});\n```\n\n## 在线体验\n\n<embed src=\"@/common/api/plugins/minimap.md\"></embed>\n\n## 配置项\n\n| 属性           | 描述                                        | 类型                                                                                                                                                                                                   | 默认值         | 必选 |\n| -------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- | ---- |\n| type           | 插件类型                                    | string                                                                                                                                                                                                 | `minimap`      | ✓    |\n| key            | 插件唯一标识符，用于后续更新                | string                                                                                                                                                                                                 | -              |      |\n| className      | 缩略图画布类名，传入外置容器时不生效        | string                                                                                                                                                                                                 |                |      |\n| container      | 缩略图挂载的容器，无则挂载到 Graph 所在容器 | HTMLElement \\| string                                                                                                                                                                                  |                |      |\n| containerStyle | 缩略图的容器样式，传入外置容器时不生效      | Partial\\<CSSStyleDeclaration\\>                                                                                                                                                                         |                |      |\n| delay          | 延迟更新时间(毫秒)，用于性能优化            | number                                                                                                                                                                                                 | 128            |      |\n| filter         | 过滤器，用于过滤不必显示的元素              | (id: string, elementType: `node` \\| `edge` \\| `combo`) => boolean                                                                                                                                      |                |      |\n| maskStyle      | 遮罩的样式                                  | Partial\\<CSSStyleDeclaration\\>                                                                                                                                                                         |                |      |\n| padding        | 内边距                                      | number \\| number[]                                                                                                                                                                                     | 10             |      |\n| position       | 缩略图相对于画布的位置                      | [number, number] \\| `left` \\| `right` \\| `top` \\| `bottom` \\| `left-top` \\| `left-bottom` \\| `right-top` \\| `right-bottom` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right` \\| `center` | `right-bottom` |      |\n| renderer       | 渲染器，默认使用 Canvas 渲染器              | IRenderer                                                                                                                                                                                              |                |      |\n| shape          | 元素缩略图形的生成方法                      | `key` \\| ((id: string, elementType: `node` \\| `edge` \\| `combo`, element: DisplayObject) => DisplayObject)                                                                                             | `key`          |      |\n| size           | 宽度和高度                                  | [number, number]                                                                                                                                                                                       | [240, 160]     |      |\n\n### containerStyle\n\n设置缩略图的容器样式，传入外置容器时不生效。继承了所有 CSS 样式属性（CSSStyleDeclaration），你可以使用任何合法的 CSS 属性来配置缩略图容器的样式。\n\n以下是一些常用配置：\n\n| 属性         | 描述         | 类型   | 默认值           | 必选 |\n| ------------ | ------------ | ------ | ---------------- | ---- |\n| border       | 容器边框样式 | string | `1px solid #ddd` | ✓    |\n| background   | 容器背景颜色 | string | `#fff`           | ✓    |\n| borderRadius | 容器圆角大小 | string | -                |      |\n| boxShadow    | 容器阴影效果 | string | -                |      |\n| padding      | 容器内边距   | string | -                |      |\n| margin       | 容器外边距   | string | -                |      |\n| opacity      | 透明度       | string | -                |      |\n\n### maskStyle\n\n指定遮罩的样式。继承了所有 CSS 样式属性（CSSStyleDeclaration），你可以使用任何合法的 CSS 属性来配置缩略图容器的样式。\n\n以下是一些常用配置：\n\n| 属性         | 描述         | 类型   | 默认值               | 必选 |\n| ------------ | ------------ | ------ | -------------------- | ---- |\n| border       | 容器边框样式 | string | `1px solid #ddd`     | ✓    |\n| background   | 容器背景颜色 | string | `rgba(0, 0, 0, 0.1)` | ✓    |\n| borderRadius | 容器圆角大小 | string | -                    | -    |\n| boxShadow    | 容器阴影效果 | string | -                    | -    |\n| padding      | 容器内边距   | string | -                    | -    |\n| margin       | 容器外边距   | string | -                    | -    |\n| opacity      | 透明度       | string | -                    | -    |\n\n### position\n\n缩略图相对于画布的位置，缩略图位置配置支持数组形式和预设值形式。\n\n- 数组形式 [number, number] 表示相对位置，取值范围为 0~1。举例：[0, 0] 代表画布左上角，[1, 1] 代表画布右下角。\n- 预设值形式用于设定缩略图所在画布固定方位，可选值有：`left` \\| `right` \\| `top` \\| `bottom` \\| `left-top` \\| `left-bottom` \\| `right-top` \\| `right-bottom` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right` \\| `center`\n\n```js\nconst graph = new Graph({\n  plugins:[\n    {\n      ... // 其他配置\n      key: 'minimap',\n      type: 'minimap',\n      position: 'right-bottom'  // 这里进行修改minimap所在位置\n    }\n  ]\n})\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  data: {\n    nodes: Array.from({ length: 50 }).map((_, i) => ({\n      id: `node-${i}`,\n      x: Math.random() * 500,\n      y: Math.random() * 300,\n    })),\n    edges: Array.from({ length: 100 }).map((_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 50)}`,\n      target: `node-${Math.floor(Math.random() * 50)}`,\n    })),\n  },\n  node: { style: { fill: '#7e3feb' } },\n  edge: { style: { stroke: '#8b9baf' } },\n  layout: { type: 'force' },\n  behaviors: ['drag-canvas'],\n  plugins: [{ type: 'minimap', key: 'minimap', size: [240, 160], position: 'right-bottom' }],\n});\n\ngraph.render();\n```\n\n### size\n\n设置小地图的宽度和高度，默认值为 [240, 160]\n\n```js\nconst graph = new Graph({\n  plugins:[\n    {\n      ... // 其他配置\n      key: 'minimap',\n      type: 'minimap',\n      size: [200, 120]  // minimap的宽度和高度的设置\n    }\n  ]\n})\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 300,\n  data: {\n    nodes: Array.from({ length: 50 }).map((_, i) => ({\n      id: `node-${i}`,\n      x: Math.random() * 500,\n      y: Math.random() * 300,\n    })),\n    edges: Array.from({ length: 100 }).map((_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${Math.floor(Math.random() * 50)}`,\n      target: `node-${Math.floor(Math.random() * 50)}`,\n    })),\n  },\n  node: { style: { fill: '#7e3feb' } },\n  edge: { style: { stroke: '#8b9baf' } },\n  layout: { type: 'force' },\n  behaviors: ['drag-canvas'],\n  plugins: [{ type: 'minimap', key: 'minimap', size: [200, 120], position: 'right-bottom' }],\n});\n\ngraph.render();\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: { nodes: Array.from({ length: 20 }).map((_, i) => ({ id: `node${i}` })) },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'minimap',\n      size: [240, 160],\n    },\n  ],\n  node: {\n    palette: 'spectral',\n  },\n  layout: {\n    type: 'circular',\n  },\n  autoFit: 'view',\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Snapline.en.md",
    "content": "---\ntitle: Snapline\norder: 13\n---\n\n## Overview\n\nThe Snapline plugin provides intelligent alignment guidelines for the canvas, automatically displaying guide lines when moving nodes and supporting automatic snapping. It helps users achieve precise alignment and is an important tool for improving efficiency and accuracy in graphic editing.\n\n## Use Cases\n\nThe Snapline plugin is mainly suitable for the following scenarios:\n\n- When manually adjusting node positions and precise alignment with other nodes is needed\n- When dragging multiple nodes while maintaining their alignment relationships\n- When creating standardized graphic layouts to ensure consistency in node spacing and positioning\n- When improving node layout efficiency through automatic snapping functionality\n\n## Basic Usage\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'snapline',\n      key: 'my-snapline', // Specify unique identifier\n      tolerance: 5, // Alignment snap threshold\n      offset: 20, // Guide line extension distance\n      autoSnap: true, // Enable automatic snapping\n    },\n  ],\n});\n```\n\n## Live Demo\n\n<embed src=\"@/common/api/plugins/snapline.md\"></embed>\n\n## Options\n\n| Property            | Description                                                                                                                                                                                            | Type                                      | Default                 | Required |\n| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------- | ----------------------- | -------- |\n| type                | Plugin type                                                                                                                                                                                            | string                                    | `'snapline'`            | ✓        |\n| key                 | Plugin unique identifier                                                                                                                                                                               | string                                    | -                       |          |\n| tolerance           | The alignment accuracy, that is, when the distance between the moved node and the target position is less than tolerance, the alignment line is displayed                                              | number                                    | 5                       |          |\n| offset              | The extension distance of the snapline                                                                                                                                                                 | number                                    | 20                      |          |\n| autoSnap            | Whether to enable automatic snapping                                                                                                                                                                   | boolean                                   | true                    |          |\n| shape               | Specifies which shape on the element to use as the reference shape:<br/>- `'key'`: uses the key shape of the element as the reference shape<br/>- `Function`: receives the element and returns a shape | string \\| ((node: Node) => DisplayObject) | `'key'`                 |          |\n| verticalLineStyle   | Vertical snapline style                                                                                                                                                                                | BaseStyleProps                            | `{ stroke: '#1783FF' }` |          |\n| horizontalLineStyle | Horizontal snapline style                                                                                                                                                                              | BaseStyleProps                            | `{ stroke: '#1783FF' }` |          |\n| filter              | Filter nodes that do not need to participate in alignment                                                                                                                                              | (node: Node) => boolean                   | `() => true`            |          |\n\n### shape\n\nThe `shape` property specifies the reference shape for elements and supports the following configurations:\n\n```js\n// Use the key shape as reference\n{\n  type: 'snapline',\n  shape: 'key'\n}\n\n// Use custom function to return reference shape\n{\n  type: 'snapline',\n  shape: (node) => {\n    return node.getShape('custom-shape');\n  }\n}\n```\n\n### Snapline Style Configuration\n\n| Property       | Description             | Type                                     | Default     |\n| -------------- | ----------------------- | ---------------------------------------- | ----------- |\n| stroke         | Line color              | string \\| Pattern \\| null                | `'#1783FF'` |\n| opacity        | Overall opacity         | number \\| string                         | 1           |\n| strokeOpacity  | Stroke opacity          | number \\| string                         | 1           |\n| lineWidth      | Line width              | number \\| string                         | 1           |\n| lineCap        | Line end style          | `'butt'` \\| `'round'` \\| `'square'`      | `'butt'`    |\n| lineJoin       | Line join style         | `'miter'` \\| `'round'` \\| `'bevel'`      | `'miter'`   |\n| lineDash       | Dash line configuration | number \\| string \\| (string \\| number)[] | -           |\n| lineDashOffset | Dash line offset        | number                                   | 0           |\n| shadowBlur     | Shadow blur             | number                                   | 0           |\n| shadowColor    | Shadow color            | string                                   | -           |\n| shadowOffsetX  | Shadow X offset         | number                                   | 0           |\n| shadowOffsetY  | Shadow Y offset         | number                                   | 0           |\n| cursor         | Mouse cursor style      | string                                   | `'default'` |\n| zIndex         | Rendering level         | number                                   | 0           |\n\nExample configuration:\n\n```js\n{\n  type: 'snapline',\n  horizontalLineStyle: {\n    stroke: '#F08F56',\n    strokeOpacity: 0.8,\n    lineWidth: 2,\n    lineDash: [4, 4],\n    lineDashOffset: 0,\n    opacity: 1,\n    cursor: 'move',\n  },\n  verticalLineStyle: {\n    stroke: '#17C76F',\n    strokeOpacity: 0.8,\n    lineWidth: 2,\n    lineDash: [4, 4],\n    lineDashOffset: 0,\n    opacity: 1,\n    cursor: 'move',\n  },\n}\n```\n\n## Code Examples\n\n### Basic Snapline\n\nThe simplest usage:\n\n```js\nconst graph = new Graph({\n  plugins: ['snapline'],\n});\n```\n\n### Custom Configuration\n\nYou can customize the snapline behavior according to your needs:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'snapline',\n      tolerance: 8, // Larger snap range\n      offset: 30, // Longer extension lines\n      horizontalLineStyle: {\n        stroke: '#1890ff',\n        lineWidth: 2,\n      },\n      filter: (node) => node.id !== 'node-0', // Filter nodes by id, exclude from alignment\n    },\n  ],\n});\n```\n\n## Live Example\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'snapline',\n      key: 'snapline',\n      verticalLineStyle: { stroke: '#F08F56', lineWidth: 2 },\n      horizontalLineStyle: { stroke: '#17C76F', lineWidth: 2 },\n      autoSnap: false,\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Snapline.zh.md",
    "content": "---\ntitle: 对齐线 Snapline\norder: 13\n---\n\n## 概述\n\n对齐线插件为画布提供智能对齐参考线，在移动节点时自动显示辅助线并支持自动吸附，帮助用户实现精确对齐，是图形编辑中提升效率和精确度的重要工具。\n\n## 使用场景\n\n对齐线插件主要适用于以下场景：\n\n- 手动调整节点位置时,需要与其他节点精确对齐\n- 拖拽移动多个节点时,保持它们之间的对齐关系\n- 制作规范的图形布局时,确保节点间距和位置的一致性\n- 通过自动吸附功能提高节点排版效率\n\n## 基本用法\n\n以下是一个简单的 Snapline 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'snapline',\n      key: 'my-snapline', // 指定唯一标识符\n      tolerance: 5, // 对齐吸附阈值\n      offset: 20, // 对齐线延伸距离\n      autoSnap: true, // 启用自动吸附\n    },\n  ],\n});\n```\n\n## 在线体验\n\n<embed src=\"@/common/api/plugins/snapline.md\"></embed>\n\n## 配置项\n\n| 属性                | 描述                                                                                                                    | 类型                                                                                                                            | 默认值                  | 必选 |\n| ------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ---- |\n| type                | 插件类型                                                                                                                | string                                                                                                                          | `'snapline'`            | ✓    |\n| key                 | 插件唯一标识符                                                                                                          | string                                                                                                                          | -                       |      |\n| tolerance           | 对齐精度，即触发对齐的距离阈值                                                                                          | number                                                                                                                          | 5                       |      |\n| offset              | 对齐线头尾的延伸距离                                                                                                    | number                                                                                                                          | 20                      |      |\n| autoSnap            | 是否启用自动吸附                                                                                                        | boolean                                                                                                                         | true                    |      |\n| shape               | 指定元素的参照图形：<br/>- `'key'`: 使用元素的主图形作为参照图形<br/>- `Function`: 传入函数时接收元素对象，返回一个图形 | string \\| ((node: Node) => DisplayObject)                                                                                       | `'key'`                 |      |\n| verticalLineStyle   | 垂直对齐线样式                                                                                                          | 支持[BaseStyleProps](/manual/element/shape/properties#baseshapestyle) 下的部分配置，详见[「对齐线样式配置项」](#对齐线样式配置) | `{ stroke: '#1783FF' }` |      |\n| horizontalLineStyle | 水平对齐线样式                                                                                                          | 支持[BaseStyleProps](/manual/element/shape/properties#baseshapestyle) 下的部分配置，详见[「对齐线样式配置项」](#对齐线样式配置) | `{ stroke: '#1783FF' }` |      |\n| filter              | 过滤不需要参与对齐的节点                                                                                                | (node: Node) => boolean                                                                                                         | `() => true`            |      |\n\n### shape\n\n`shape` 属性用于指定元素的参照图形，支持以下配置方式：\n\n```js\n// 使用主图形作为参照\n{\n  type: 'snapline',\n  shape: 'key'\n}\n\n// 使用自定义函数返回参照图形\n{\n  type: 'snapline',\n  shape: (node) => {\n    return node.getShape('custom-shape');\n  }\n}\n```\n\n### 对齐线样式配置\n\n| 配置项         | 说明            | 类型                                     | 默认值      |\n| -------------- | --------------- | ---------------------------------------- | ----------- |\n| stroke         | 线条颜色        | string \\| Pattern \\| null                | `'#1783FF'` |\n| opacity        | 整体透明度      | number \\| string                         | 1           |\n| strokeOpacity  | 描边透明度      | number \\| string                         | 1           |\n| lineWidth      | 线宽度          | number \\| string                         | 1           |\n| lineCap        | 线段端点样式    | `'butt'` \\| `'round'` \\| `'square'`      | `'butt'`    |\n| lineJoin       | 线段连接处样式  | `'miter'` \\| `'round'` \\| `'bevel'`      | `'miter'`   |\n| lineDash       | 虚线配置        | number \\| string \\| (string \\| number)[] | -           |\n| lineDashOffset | 虚线偏移量      | number                                   | 0           |\n| shadowBlur     | 阴影模糊程度    | number                                   | 0           |\n| shadowColor    | 阴影颜色        | string                                   | -           |\n| shadowOffsetX  | 阴影 X 方向偏移 | number                                   | 0           |\n| shadowOffsetY  | 阴影 Y 方向偏移 | number                                   | 0           |\n| cursor         | 鼠标样式        | string                                   | `'default'` |\n| zIndex         | 渲染层级        | number                                   | 0           |\n\n示例配置\n\n```js\n{\n  type: 'snapline',\n  horizontalLineStyle: {\n    stroke: '#F08F56',\n    strokeOpacity: 0.8,\n    lineWidth: 2,\n    lineDash: [4, 4],\n    lineDashOffset: 0,\n    opacity: 1,\n    cursor: 'move',\n  },\n  verticalLineStyle: {\n    stroke: '#17C76F',\n    strokeOpacity: 0.8,\n    lineWidth: 2,\n    lineDash: [4, 4],\n    lineDashOffset: 0,\n    opacity: 1,\n    cursor: 'move',\n  },\n}\n```\n\n## 代码示例\n\n### 基础对齐线\n\n最简单的使用方式：\n\n```js\nconst graph = new Graph({\n  plugins: ['snapline'],\n});\n```\n\n### 自定义配置\n\n可以根据需要自定义对齐线的行为：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'snapline',\n      tolerance: 8, // 更大的吸附范围\n      offset: 30, // 更长的延伸线\n      horizontalLineStyle: {\n        stroke: '#1890ff',\n        lineWidth: 2,\n      },\n      filter: (node) => node.id !== 'node-0', // 根据id过滤节点，不参与对齐\n    },\n  ],\n});\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'snapline',\n      key: 'snapline',\n      verticalLineStyle: { stroke: '#F08F56', lineWidth: 2 },\n      horizontalLineStyle: { stroke: '#17C76F', lineWidth: 2 },\n      autoSnap: false,\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Timebar.en.md",
    "content": "---\ntitle: Timebar\norder: 14\n---\n\n## Overview\n\nThe Timebar plugin is an important tool for exploring time-series data. It can display the time distribution of data in the form of a timeline or trend chart, and supports interactions such as time interval filtering and dynamic playback, helping users better understand the changes in data over time.\n\n## Use Cases\n\n- Need to display and analyze the time distribution of time-series data\n- Need to filter and explore graph data through the time dimension\n- Need to dynamically display the process of data changing over time\n\n## Basic Usage\n\nBelow is a simple example of initializing the Timebar plugin:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'timebar',\n      data: timeData, // Time data\n      width: 450, // Timebar width\n      height: 60, // Timebar height\n      position: 'bottom', // Position\n      loop: false, // Whether to loop playback\n    },\n  ],\n});\n```\n\n## Online Experience\n\n<embed src=\"@/common/api/plugins/timebar.md\"></embed>\n\n## Configuration Options\n\n| Property       | Description                                                                                                                                                                              | Type                                               | Default Value | Required |\n| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------- | -------- |\n| type           | Plugin type                                                                                                                                                                              | string                                             | `timebar`     | ✓        |\n| key            | Unique identifier for the plugin, can be used to get the plugin instance or update plugin options                                                                                        | string                                             | -             |          |\n| className      | Additional class name for the toolbar DOM                                                                                                                                                | string                                             | `g6-timebar`  |          |\n| x              | X position (position will be invalid if set)                                                                                                                                             | number                                             | -             |          |\n| y              | Y position (position will be invalid if set)                                                                                                                                             | number                                             | -             |          |\n| width          | Timebar width                                                                                                                                                                            | number                                             | 450           |          |\n| height         | Timebar height                                                                                                                                                                           | number                                             | 60            |          |\n| position       | Timebar position                                                                                                                                                                         | `bottom` \\| `top`                                  | `bottom`      |          |\n| padding        | Padding                                                                                                                                                                                  | number \\| number[]                                 | 10            |          |\n| data           | Time data                                                                                                                                                                                | number[] \\| { time: number; value: number }[]      | -             | ✓        |\n| timebarType    | Timebar display type                                                                                                                                                                     | `time` \\| `chart`                                  | `time`        |          |\n| elementTypes   | Filter element types                                                                                                                                                                     | (`node` \\| `edge` \\| `combo`)[]                    | [`node`]      |          |\n| mode           | Control element filtering method, supports the following two configurations: <br/>- `modify`: filter by modifying graph data <br/>- `visibility`: filter by modifying element visibility | `modify` \\| `visibility`                           | `modify`      |          |\n| values         | Current time value                                                                                                                                                                       | number \\| [number, number] \\| Date \\| [Date, Date] | -             |          |\n| loop           | Whether to loop playback                                                                                                                                                                 | boolean                                            | false         |          |\n| getTime        | Method to get element time                                                                                                                                                               | (datum: ElementDatum) => number                    | -             |          |\n| labelFormatter | Custom time formatting in chart mode                                                                                                                                                     | (time: number \\| Date) => string                   | -             |          |\n| onChange       | Callback when the time interval changes                                                                                                                                                  | (values: number \\| [number, number]) => void       | -             |          |\n| onReset        | Callback when reset                                                                                                                                                                      | () => void                                         | -             |          |\n| onSpeedChange  | Callback when playback speed changes                                                                                                                                                     | (speed: number) => void                            | -             |          |\n| onPlay         | Callback when playback starts                                                                                                                                                            | () => void                                         | -             |          |\n| onPause        | Callback when paused                                                                                                                                                                     | () => void                                         | -             |          |\n| onBackward     | Callback when moving backward                                                                                                                                                            | () => void                                         | -             |          |\n| onForward      | Callback when moving forward                                                                                                                                                             | () => void                                         | -             |          |\n\n### timebarType\n\nThe `timebarType` property is used to control the display type of the timebar, supporting the following two configurations:\n\n- `time`: Displayed as a timeline, refer to [Time Mode Example](/examples/plugin/timebar/#timer)\n- `chart`: Displayed as a trend chart, at this time the `data` configuration item under `timebar` needs to pass an additional `value` field as chart data, refer to [Chart Mode Example](/examples/plugin/timebar/#chart)\n\n## Code Examples\n\n### Basic Usage\n\nThe simplest configuration method:\n\n```js\nconst graph = new Graph({\n  layout: { type: 'grid', cols: 5 },\n  plugins: [\n    {\n      type: 'timebar',\n      data: [\n        {\n          time: new Date('2023-08-01').getTime(),\n          value: 5,\n        },\n        {\n          time: new Date('2023-08-02').getTime(),\n          value: 10,\n        },\n        {\n          time: new Date('2023-08-03').getTime(),\n          value: 15,\n        },\n      ],\n    },\n  ],\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        label: 'Node 1',\n        // By default, elementTypes=['node'], so nodes need to set data.timestamp to display sequentially according to the timeline\n        data: {\n          timestamp: new Date('2023-08-01').getTime(),\n        },\n      },\n      {\n        id: 'node2',\n        label: 'Node 2',\n        data: {\n          timestamp: new Date('2023-08-02').getTime(),\n        },\n      },\n      {\n        id: 'node3',\n        label: 'Node 3',\n        data: {\n          timestamp: new Date('2023-08-03').getTime(),\n        },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge1',\n        source: 'node1',\n        target: 'node2',\n        // Scenario 1: By default, elementTypes = ['node']\n        // - Edges do not need to set data.timestamp, the display/hide of edges depends entirely on whether the two connected nodes are visible\n\n        // Scenario 2: If elementTypes includes 'edge', for example, elementTypes = ['node', 'edge']\n        // - At this time, edges must set data.timestamp, and the display of edges is controlled by it\n        // data: {\n        //   timestamp: new Date('2023-08-01').getTime()\n        // }\n      },\n      {\n        id: 'edge2',\n        source: 'node2',\n        target: 'node3',\n      },\n      {\n        id: 'edge3',\n        source: 'node3',\n        target: 'node1',\n      },\n    ],\n  },\n});\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 400,\n  width: 600,\n  height: 400,\n  layout: { type: 'grid', cols: 5 },\n  plugins: [\n    {\n      type: 'timebar',\n      data: [\n        {\n          time: new Date('2023-08-01').getTime(),\n          value: 5,\n        },\n        {\n          time: new Date('2023-08-02').getTime(),\n          value: 10,\n        },\n        {\n          time: new Date('2023-08-03').getTime(),\n          value: 15,\n        },\n      ],\n    },\n  ],\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        label: 'Node 1',\n        data: {\n          timestamp: new Date('2023-08-01').getTime(),\n        },\n      },\n      {\n        id: 'node2',\n        label: 'Node 2',\n        data: {\n          timestamp: new Date('2023-08-02').getTime(),\n        },\n      },\n      {\n        id: 'node3',\n        label: 'Node 3',\n        data: {\n          timestamp: new Date('2023-08-03').getTime(),\n        },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge1',\n        source: 'node1',\n        target: 'node2',\n      },\n      {\n        id: 'edge2',\n        source: 'node2',\n        target: 'node3',\n      },\n      {\n        id: 'edge3',\n        source: 'node3',\n        target: 'node1',\n      },\n    ],\n  },\n});\n\ngraph.render();\n```\n\n### Custom Styles\n\n`width`, `height`, `padding`, `className` can customize the display effect of the timebar, but note that `className` only acts on the outer DOM container and cannot affect the internal Canvas rendering content of the timebar (timeline, chart, play button, etc.).\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'timebar',\n      className: 'custom-timebar', // Note: Since the content is Canvas rendered, CSS styles cannot affect the internal content of the timebar\n      width: 400, // Set timebar width\n      height: 80, // Set timebar height\n      padding: [20, 20, 10, 20], // Set padding [top, right, bottom, left]\n      position: 'bottom', // Keep position at the bottom\n      data: timeData,\n      // labelFormatter: (time) => {\n      //   return new Date(time).toLocaleDateString();\n      // }\n    },\n  ],\n});\n```\n\nCSS can only set the style of the timebar container:\n\n```css\n.custom-timebar {\n  background-color: #f0f0f0;\n  border: 1px solid #ccc;\n  border-radius: 5px;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n```\n\nThe effect is as follows:\n\n```js | ob { pin: false }\ncreateGraph(\n  {\n    data: () => {\n      return {\n        nodes: [\n          {\n            id: 'node1',\n            style: { x: 100, y: 100, label: 'Node 1' },\n            data: {\n              timestamp: new Date('2023-08-01').getTime(),\n            },\n          },\n          {\n            id: 'node2',\n            style: { x: 200, y: 100, label: 'Node 2' },\n            data: {\n              timestamp: new Date('2023-08-01').getTime() + 3600 * 24 * 1000,\n            },\n          },\n          {\n            id: 'node3',\n            style: { x: 150, y: 200, label: 'Node 3' },\n            data: {\n              timestamp: new Date('2023-08-01').getTime() + 3600 * 24 * 1000 * 2,\n            },\n          },\n        ],\n        edges: [\n          { id: 'edge1', source: 'node1', target: 'node2' },\n          { id: 'edge2', source: 'node2', target: 'node3' },\n          { id: 'edge3', source: 'node3', target: 'node1' },\n        ],\n      };\n    },\n    node: {\n      style: {\n        size: 20,\n        label: true,\n      },\n    },\n    edge: {\n      style: {\n        stroke: '#91d5ff',\n        lineWidth: 1,\n      },\n    },\n    plugins: [\n      {\n        type: 'timebar',\n        className: 'custom-timebar',\n        width: 400,\n        height: 80,\n        padding: [20, 20, 10, 20],\n        position: 'bottom',\n        data: [\n          {\n            time: new Date('2023-08-01').getTime(),\n            value: 5,\n          },\n          {\n            time: new Date('2023-08-01').getTime() + 3600 * 24 * 1000,\n            value: 10,\n          },\n          {\n            time: new Date('2023-08-01').getTime() + 3600 * 24 * 1000 * 2,\n            value: 15,\n          },\n        ],\n        labelFormatter: (time) => {\n          return new Date(time).toLocaleDateString();\n        },\n      },\n    ],\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    gui?.hide();\n    const style = document.createElement('style');\n    style.innerHTML = `\n      .custom-timebar {\n        background-color: #f0f0f0;\n        border: 1px solid #ccc;\n        border-radius: 5px;\n        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n      }\n    `;\n    document.head.appendChild(style);\n  },\n);\n```\n\n## Real Cases\n\n- [Time Mode](/examples/plugin/timebar/#timer)\n- [Chart Mode](/examples/plugin/timebar/#chart)\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Timebar.zh.md",
    "content": "---\ntitle: 时间条 Timebar\norder: 14\n---\n\n## 概述\n\n时间条插件是一个用于时序数据探索的重要工具，它能够通过时间轴或趋势图的形式展示数据的时间分布，并支持时间区间筛选、动态播放等交互方式，帮助用户更好地理解数据随时间的变化。\n\n## 使用场景\n\n- 需要展示和分析时序数据的时间分布\n- 需要通过时间维度筛选和探索图数据\n- 需要动态展示数据随时间变化的过程\n\n## 基本用法\n\n以下是一个简单的 Timebar 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'timebar',\n      data: timeData, // 时间数据\n      width: 450, // 时间条宽度\n      height: 60, // 时间条高度\n      position: 'bottom', // 位置\n      loop: false, // 是否循环播放\n    },\n  ],\n});\n```\n\n## 在线体验\n\n<embed src=\"@/common/api/plugins/timebar.md\"></embed>\n\n## 配置项\n\n| 属性           | 描述                                                                                                                           | 类型                                               | 默认值       | 必选 |\n| -------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | ------------ | ---- |\n| type           | 插件类型                                                                                                                       | string                                             | `timebar`    | ✓    |\n| key            | 插件的唯一标识，可用于获取插件实例或更新插件选项                                                                               | string                                             | -            |      |\n| className      | 给工具栏的 DOM 追加的类名                                                                                                      | string                                             | `g6-timebar` |      |\n| x              | X 位置（设置后 position 会失效）                                                                                               | number                                             | -            |      |\n| y              | Y 位置（设置后 position 会失效）                                                                                               | number                                             | -            |      |\n| width          | 时间条宽度                                                                                                                     | number                                             | 450          |      |\n| height         | 时间条高度                                                                                                                     | number                                             | 60           |      |\n| position       | 时间条位置                                                                                                                     | `bottom` \\| `top`                                  | `bottom`     |      |\n| padding        | 边距                                                                                                                           | number \\| number[]                                 | 10           |      |\n| data           | 时间数据                                                                                                                       | number[] \\| { time: number; value: number }[]      | -            | ✓    |\n| timebarType    | 时间条展示类型                                                                                                                 | `time` \\| `chart`                                  | `time`       |      |\n| elementTypes   | 筛选元素类型                                                                                                                   | (`node` \\| `edge` \\| `combo`)[]                    | [`node`]     |      |\n| mode           | 控制元素的筛选方式，支持以下两种配置： <br/>- `modify`：通过修改图数据进行筛选 <br/>- `visibility`：通过修改元素可见性进行筛选 | `modify` \\| `visibility`                           | `modify`     |      |\n| values         | 当前时间值                                                                                                                     | number \\| [number, number] \\| Date \\| [Date, Date] | -            |      |\n| loop           | 是否循环播放                                                                                                                   | boolean                                            | false        |      |\n| getTime        | 获取元素时间的方法                                                                                                             | (datum: ElementDatum) => number                    | -            |      |\n| labelFormatter | 图表模式下自定义时间格式化                                                                                                     | (time: number \\| Date) => string                   | -            |      |\n| onChange       | 时间区间变化时的回调                                                                                                           | (values: number \\| [number, number]) => void       | -            |      |\n| onReset        | 重置时的回调                                                                                                                   | () => void                                         | -            |      |\n| onSpeedChange  | 播放速度变化时的回调                                                                                                           | (speed: number) => void                            | -            |      |\n| onPlay         | 开始播放时的回调                                                                                                               | () => void                                         | -            |      |\n| onPause        | 暂停时的回调                                                                                                                   | () => void                                         | -            |      |\n| onBackward     | 后退时的回调                                                                                                                   | () => void                                         | -            |      |\n| onForward      | 前进时的回调                                                                                                                   | () => void                                         | -            |      |\n\n### timebarType\n\n`timebarType` 属性用于控制时间条的展示类型，支持以下两种配置：\n\n- `time`：显示为时间轴形式，参考 [时间模式示例](/examples/plugin/timebar/#timer)\n- `chart`：显示为趋势图形式，此时`timebar`下的`data`配置项，每个数组项需要额外传入 `value` 字段作为图表数据，参考 [图表模式示例](/examples/plugin/timebar/#chart)\n\n## 代码示例\n\n### 基础用法\n\n最简单的配置方式：\n\n```js\nconst graph = new Graph({\n  layout: { type: 'grid', cols: 5 },\n  plugins: [\n    {\n      type: 'timebar',\n      data: [\n        {\n          time: new Date('2023-08-01').getTime(),\n          value: 5,\n        },\n        {\n          time: new Date('2023-08-02').getTime(),\n          value: 10,\n        },\n        {\n          time: new Date('2023-08-03').getTime(),\n          value: 15,\n        },\n      ],\n    },\n  ],\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        label: '节点1',\n        // 默认情况下 elementTypes=['node']，所以节点需要设置 data.timestamp，才能按照时间轴内的时间依次展示\n        data: {\n          timestamp: new Date('2023-08-01').getTime(),\n        },\n      },\n      {\n        id: 'node2',\n        label: '节点2',\n        data: {\n          timestamp: new Date('2023-08-02').getTime(),\n        },\n      },\n      {\n        id: 'node3',\n        label: '节点3',\n        data: {\n          timestamp: new Date('2023-08-03').getTime(),\n        },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge1',\n        source: 'node1',\n        target: 'node2',\n        // 场景一：默认情况 elementTypes = ['node']\n        // - 边不需要设置 data.timestamp，边的显示/隐藏完全取决于其连接的两个节点是否可见\n\n        // 场景二：如果elementTypes包含了'edge'，比如 elementTypes = ['node', 'edge']\n        // - 此时必须为边设置 data.timestamp，边的显示受其控制\n        // data: {\n        //   timestamp: new Date('2023-08-01').getTime()\n        // }\n      },\n      {\n        id: 'edge2',\n        source: 'node2',\n        target: 'node3',\n      },\n      {\n        id: 'edge3',\n        source: 'node3',\n        target: 'node1',\n      },\n    ],\n  },\n});\n```\n\n效果如下：\n\n```js | ob { pin: false, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  width: 600,\n  height: 400,\n  width: 600,\n  height: 400,\n  layout: { type: 'grid', cols: 5 },\n  plugins: [\n    {\n      type: 'timebar',\n      data: [\n        {\n          time: new Date('2023-08-01').getTime(),\n          value: 5,\n        },\n        {\n          time: new Date('2023-08-02').getTime(),\n          value: 10,\n        },\n        {\n          time: new Date('2023-08-03').getTime(),\n          value: 15,\n        },\n      ],\n    },\n  ],\n  data: {\n    nodes: [\n      {\n        id: 'node1',\n        label: '节点1',\n        data: {\n          timestamp: new Date('2023-08-01').getTime(),\n        },\n      },\n      {\n        id: 'node2',\n        label: '节点2',\n        data: {\n          timestamp: new Date('2023-08-02').getTime(),\n        },\n      },\n      {\n        id: 'node3',\n        label: '节点3',\n        data: {\n          timestamp: new Date('2023-08-03').getTime(),\n        },\n      },\n    ],\n    edges: [\n      {\n        id: 'edge1',\n        source: 'node1',\n        target: 'node2',\n      },\n      {\n        id: 'edge2',\n        source: 'node2',\n        target: 'node3',\n      },\n      {\n        id: 'edge3',\n        source: 'node3',\n        target: 'node1',\n      },\n    ],\n  },\n});\n\ngraph.render();\n```\n\n### 自定义样式\n\n`width`、`height`、`padding`、`className` 可自定义时间条的展示效果，但需要注意 `className` 仅作用于外层 DOM 容器，无法影响时间条内部的 Canvas 渲染内容（时间轴、图表、播放按钮等）。\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'timebar',\n      className: 'custom-timebar', // 注意：由于内容是 Canvas 渲染，CSS 样式无法影响到时间条的内部内容\n      width: 400, // 设置时间条宽度\n      height: 80, // 设置时间条高度\n      padding: [20, 20, 10, 20], // 设置内边距 [上, 右, 下, 左]\n      position: 'bottom', // 位置保持在底部\n      data: timeData,\n      // labelFormatter: (time) => {\n      //   return new Date(time).toLocaleDateString();\n      // }\n    },\n  ],\n});\n```\n\n通过 CSS 只能设置时间条容器的样式：\n\n```css\n.custom-timebar {\n  background-color: #f0f0f0;\n  border: 1px solid #ccc;\n  border-radius: 5px;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n```\n\n效果如下：\n\n```js | ob { pin: false }\ncreateGraph(\n  {\n    data: () => {\n      return {\n        nodes: [\n          {\n            id: 'node1',\n            style: { x: 100, y: 100, label: 'Node 1' },\n            data: {\n              timestamp: new Date('2023-08-01').getTime(),\n            },\n          },\n          {\n            id: 'node2',\n            style: { x: 200, y: 100, label: 'Node 2' },\n            data: {\n              timestamp: new Date('2023-08-01').getTime() + 3600 * 24 * 1000,\n            },\n          },\n          {\n            id: 'node3',\n            style: { x: 150, y: 200, label: 'Node 3' },\n            data: {\n              timestamp: new Date('2023-08-01').getTime() + 3600 * 24 * 1000 * 2,\n            },\n          },\n        ],\n        edges: [\n          { id: 'edge1', source: 'node1', target: 'node2' },\n          { id: 'edge2', source: 'node2', target: 'node3' },\n          { id: 'edge3', source: 'node3', target: 'node1' },\n        ],\n      };\n    },\n    node: {\n      style: {\n        size: 20,\n        label: true,\n      },\n    },\n    edge: {\n      style: {\n        stroke: '#91d5ff',\n        lineWidth: 1,\n      },\n    },\n    plugins: [\n      {\n        type: 'timebar',\n        className: 'custom-timebar',\n        width: 400,\n        height: 80,\n        padding: [20, 20, 10, 20],\n        position: 'bottom',\n        data: [\n          {\n            time: new Date('2023-08-01').getTime(),\n            value: 5,\n          },\n          {\n            time: new Date('2023-08-01').getTime() + 3600 * 24 * 1000,\n            value: 10,\n          },\n          {\n            time: new Date('2023-08-01').getTime() + 3600 * 24 * 1000 * 2,\n            value: 15,\n          },\n        ],\n        labelFormatter: (time) => {\n          return new Date(time).toLocaleDateString();\n        },\n      },\n    ],\n  },\n  { width: 600, height: 400 },\n  (gui, graph) => {\n    gui?.hide();\n    const style = document.createElement('style');\n    style.innerHTML = `\n      .custom-timebar {\n        background-color: #f0f0f0;\n        border: 1px solid #ccc;\n        border-radius: 5px;\n        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n      }\n    `;\n    document.head.appendChild(style);\n  },\n);\n```\n\n## 实际案例\n\n- [时间模式](/examples/plugin/timebar/#timer)\n- [图表模式](/examples/plugin/timebar/#chart)\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Title.en.md",
    "content": "---\ntitle: Title\norder: 15\n---\n\n## Overview\n\nTitle indicates the name of the image and conveys the brief content of the image.\n\n## Basic Usage\n\nThe following is a simple example of initializing the Title plugin:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      key: 'title',\n      type: 'title',\n      title: 'This is a title',\n      subTitle: 'This is a subtitle',\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Option    | Description                                                   | Type                          | Default      | Required |\n| --------- | ------------------------------------------------------------- | ----------------------------- | ------------ | -------- |\n| type      | Plugin type                                                   | string                        | `title`      | ✓        |\n| key       | Unique identifier for the plugin, used for subsequent updates | string                        | -            |          |\n| title     | title content [style config](#title)                          | `TitleStyle`                  | -            | ✓        |\n| subtitle  | subtitle content [style config](#subtitle)                    | `SubTitleStyle`               | -            |          |\n| spacing   | Vertical spacing between main title and subtitle              | number                        | 8            |          |\n| className | Class name of the title canvas                                | string                        | -            |          |\n| align     | Graph title alignment                                         | `left` \\| `center` \\| `right` | `left`       |          |\n| size      | Height of the title plugin                                    | number                        | 44           |          |\n| padding   | Padding                                                       | number \\| number[]            | [16,24,0,24] |          |\n\n### size\n\n<description> _number_ **optional** </description>\n\nUsed to configure the space height of the title plugin. Default is `44`。\n\n### align\n\n<description> _string_ **optional** </description>\n\nUsed to configure the horizontal alignment of the title plugin. Default is `left`. You can choose `left`, `center`, or `right`, representing left-aligned, center-aligned, and right-aligned respectively.\n\n```js | ob { pin: false }\ncreateGraph(\n  {\n    data: { nodes: Array.from({ length: 12 }).map((_, i) => ({ id: `node${i}` })) },\n    node: {\n      palette: 'spectral',\n      style: { labelText: 'Ciallo' },\n    },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n    plugins: [\n      {\n        key: 'title',\n        type: 'title',\n        title: 'This is a title This is a title',\n        subtitle: 'This is a sub-',\n      },\n    ],\n    layout: { type: 'circular' },\n    autoFit: 'view',\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = { align: 'left' };\n    const optionFolder = gui.addFolder('Align Options');\n    optionFolder.add(options, 'align', ['left', 'center', 'right']);\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'title',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n\n### spacing\n\n<description> _number_ **optional** </description>\n\nUsed to configure the spacing between the chart main title and subtitle. Default is `8`. Appropriate spacing can make the chart look more harmonious overall.\n\n### title\n\nThe title, specifically the main title, can be customized with the following configurations for various title styles.\n\n| Attr               | Desc                                               | Type       | Default               |\n| ------------------ | -------------------------------------------------- | ---------- | --------------------- |\n| title              | Title text content                                 | `string`   | -                     |\n| titleFontSize      | Title text size                                    | `number`   | 16                    |\n| titleFontFamily    | Title text font                                    | `string`   | system-ui, sans-serif |\n| titleFontWeight    | Title font weight                                  | `number`   | bold                  |\n| titleLineHeight    | Title text line height                             | `number`   | 16                    |\n| titleTextAlign     | Horizontal alignment of content in title text line | `string`   | left                  |\n| titleTextBaseline  | Vertical baseline of title text                    | `string`   | top                   |\n| titleFill          | Fill color of title text                           | `string`   | #1D2129               |\n| titleFillOpacity   | Fill transparency of title text                    | `number`   | 0.9                   |\n| titleStroke        | Stroke color of title text                         | `string`   | transparent           |\n| titleStrokeOpacity | Stroke transparency of title text                  | `number`   | 1                     |\n| titleLineWidth     | Stroke width of title text                         | `number`   | 0                     |\n| titleLineDash      | Dash style of title text                           | `number[]` | []                    |\n| titleOpacity       | Overall transparency of title text                 | `number`   | 1                     |\n| titleShadowColor   | Shadow color of title text                         | `string`   | transparent           |\n| titleShadowBlur    | Gaussian blur coefficient of title text shadow     | `number`   | 0                     |\n| titleShadowOffsetX | Horizontal offset of title text shadow             | `number`   | 0                     |\n| titleShadowOffsetY | Vertical offset of title text shadow               | `number`   | 0                     |\n| titleCursor        | Mouse style of title text                          | `string`   | default               |\n| titleDx            | Horizontal offset of title text                    | `number`   | 0                     |\n| titleDy            | Vertical offset of title text                      | `number`   | 0                     |\n\n### subtitle\n\nThe subtitle, which can be customized with the following configurations for various subtitle styles.\n\n| Attr                  | Desc                                            | Type       | Default               |\n| --------------------- | ----------------------------------------------- | ---------- | --------------------- |\n| subtitle              | Subtitle text content                           | `string`   | -                     |\n| subtitleFontSize      | Subtitle text size                              | `number`   | 12                    |\n| subtitleFontFamily    | Subtitle text font                              | `string`   | system-ui, sans-serif |\n| subtitleFontWeight    | Subtitle font weight                            | `number`   | normal                |\n| subtitleLineHeight    | Subtitle text line height                       | `number`   | 12                    |\n| subtitleTextAlign     | Subtitle text line content horizontal alignment | `string`   | left                  |\n| subtitleTextBaseline  | Subtitle text vertical baseline                 | `string`   | top                   |\n| subtitleFill          | Subtitle text fill color                        | `string`   | #1D2129               |\n| subtitleFillOpacity   | Subtitle text fill transparency                 | `number`   | 0.65                  |\n| subtitleStroke        | Subtitle text stroke color                      | `string`   | transparent           |\n| subtitleStrokeOpacity | Subtitle text stroke transparency               | `number`   | 1                     |\n| subtitleLineWidth     | Subtitle text stroke width                      | `number`   | 0                     |\n| subtitleLineDash      | Subtitle text dashed line style                 | `number[]` | []                    |\n| subtitleOpacity       | Subtitle text overall transparency              | `number`   | 1                     |\n| subtitleShadowColor   | Subtitle text shadow color                      | `string`   | transparent           |\n| subtitleShadowBlur    | Subtitle text shadow Gaussian blur coefficient  | `number`   | 0                     |\n| subtitleShadowOffsetX | Subtitle text shadow horizontal offset          | `number`   | 0                     |\n| subtitleShadowOffsetY | Subtitle text shadow vertical offset            | `number`   | 0                     |\n| subtitleCursor        | Subtitle text mouse style                       | `string`   | default               |\n| subtitleDx            | Subtitle text horizontal offset                 | `number`   | 0                     |\n| subtitleDy            | Subtitle text vertical offset                   | `number`   | 0                     |\n\n## Try it\n\nFeel free to modify this example and try different configurations\n\n```js | ob { pin: true, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: { nodes: Array.from({ length: 12 }).map((_, i) => ({ id: `node${i}` })) },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n  plugins: [\n    {\n      key: 'title',\n      type: 'title',\n\n      align: 'center', // Alignment of title\n      spacing: 4, // Spacing between main title and subtitle\n      size: 60, // Height of title, default is 44\n\n      // title\n      title: 'This is a title This is a title', // Title text\n      titleFontSize: 28, // Main title font size\n      titleFontFamily: 'sans-serif', // Main title font\n      titleFontWeight: 600, // Main title font weight\n      titleFill: '#fff', // Main title text color\n      titleFillOpacity: 1, // Main title text transparency\n      titleStroke: '#000', // Main title text stroke color\n      titleLineWidth: 2, // Main title text stroke line width\n      titleStrokeOpacity: 1, // Main title text stroke transparency\n\n      // subtitle\n      subtitle: 'This is a sub-', // Subtitle text\n      subtitleFontSize: 16, // Subtitle font size\n      subtitleFontFamily: 'Arial', // Subtitle font\n      subtitleFontWeight: 300, // Subtitle font weight\n      subtitleFill: '#2989FF', // Subtitle text color\n      subtitleFillOpacity: 1, // Subtitle text transparency\n      subtitleStroke: '#000', // Subtitle text stroke color\n      subtitleLineWidth: 1, // Subtitle text stroke line width\n      subtitleStrokeOpacity: 0.5, // Subtitle text stroke transparency\n    },\n  ],\n  node: {\n    palette: 'spectral',\n    style: { labelText: 'Ciallo' },\n  },\n  layout: {\n    type: 'circular',\n  },\n  autoFit: 'view',\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Title.zh.md",
    "content": "---\ntitle: 标题 Title\norder: 15\n---\n\n## 概述\n\nTitle（标题）表明了这张图的名称，传达图的简略内容\n\n## 基本用法\n\n以下是一个简单的 Title 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      key: 'title',\n      type: 'title',\n      title: '这是一个标题',\n      subTitle: '这是一个副标题',\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 属性      | 描述                             | 类型                          | 默认值       | 必选 |\n| --------- | -------------------------------- | ----------------------------- | ------------ | ---- |\n| type      | 插件类型                         | string                        | `title`      | ✓    |\n| key       | 插件唯一标识符，用于后续更新     | string                        | -            |      |\n| title     | 标题内容 [样式配置](#title)      | `TitleStyle`                  | -            | ✓    |\n| subtitle  | 副标题内容 [样式配置](#subtitle) | `SubTitleStyle`               | -            |      |\n| spacing   | 主标题、副标题之间的上下间距     | number                        | 8            |      |\n| className | 标题画布类名                     | string                        | -            |      |\n| align     | 标题相对于画布的位置             | `left` \\| `center` \\| `right` | `left`       |      |\n| size      | 整个标题插件的高度               | number                        | 44           |      |\n| padding   | 内边距                           | number \\| number[]            | [16,24,0,24] |      |\n\n### size\n\n<description> _number_ **optional** </description>\n\n用于配置标题的空间高度大小，默认为 `44`。\n\n### align\n\n<description> _string_ **optional** </description>\n\n用于配置标题的的左右对齐方式，默认为 `left`，可以选择使用 `left`，`center`，`right`，分别代表着居左对齐、居中对齐、居右对齐。\n\n```js | ob { pin: false }\ncreateGraph(\n  {\n    data: { nodes: Array.from({ length: 12 }).map((_, i) => ({ id: `node${i}` })) },\n    node: {\n      palette: 'spectral',\n      style: { labelText: '你好' },\n    },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n    plugins: [\n      {\n        key: 'title',\n        type: 'title',\n        title: '这是一个标题这是一个标题',\n        subtitle: '这是一个副标',\n      },\n    ],\n    layout: { type: 'circular' },\n    autoFit: 'view',\n  },\n  { width: 600, height: 300 },\n  (gui, graph) => {\n    const options = { align: 'left' };\n    const optionFolder = gui.addFolder('Align Options');\n    optionFolder.add(options, 'align', ['left', 'center', 'right']);\n    optionFolder.onChange(({ property, value }) => {\n      graph.updatePlugin({\n        key: 'title',\n        [property]: value,\n      });\n      graph.render();\n    });\n  },\n);\n```\n\n### spacing\n\n<description> _number_ **optional** </description>\n\n用于配置主标题和副标题之间的间距，默认为 `8`，合适的间距，可以让看起来整体更协调。\n\n### title\n\n的标题，具体来说是主标题，可以用以下的配置来定制标题的各种样式。\n\n| 属性               | 描述                           | 类型       | 默认值                |\n| ------------------ | ------------------------------ | ---------- | --------------------- |\n| title              | 标题文字内容                   | `string`   | -                     |\n| titleFontSize      | 标题文字大小                   | `number`   | 16                    |\n| titleFontFamily    | 标题文字字体                   | `string`   | system-ui, sans-serif |\n| titleFontWeight    | 标题字体粗细                   | `number`   | bold                  |\n| titleLineHeight    | 标题文字的行高                 | `number`   | 16                    |\n| titleTextAlign     | 标题文字行内内容的水平对齐方式 | `string`   | left                  |\n| titleTextBaseline  | 标题文字垂直方向的基线         | `string`   | top                   |\n| titleFill          | 标题文字的填充色               | `string`   | #1D2129               |\n| titleFillOpacity   | 标题文字的填充透明度           | `number`   | 0.9                   |\n| titleStroke        | 标题文字的描边颜色             | `string`   | transparent           |\n| titleStrokeOpacity | 标题文字的描边透明度           | `number`   | 1                     |\n| titleLineWidth     | 标题文字描边宽度               | `number`   | 0                     |\n| titleLineDash      | 标题文字虚线样式               | `number[]` | []                    |\n| titleOpacity       | 标题文字整体透明度             | `number`   | 1                     |\n| titleShadowColor   | 标题文字阴影颜色               | `string`   | transparent           |\n| titleShadowBlur    | 标题文字阴影的高斯模糊系数     | `number`   | 0                     |\n| titleShadowOffsetX | 标题文字阴影水平偏移量         | `number`   | 0                     |\n| titleShadowOffsetY | 标题文字阴影垂直偏移量         | `number`   | 0                     |\n| titleCursor        | 标题文字鼠标样式               | `string`   | default               |\n| titleDx            | 标题文字在水平方向的偏移量     | `number`   | 0                     |\n| titleDy            | 标题文字在垂直方向的偏移量     | `number`   | 0                     |\n\n### subtitle\n\n的副标题，可以用以下的配置来定制副标题的各种样式。\n\n| 属性                  | 描述                             | 类型       | 默认值                |\n| --------------------- | -------------------------------- | ---------- | --------------------- |\n| subtitle              | 副标题文字内容                   | `string`   | -                     |\n| subtitleFontSize      | 副标题文字大小                   | `number`   | 12                    |\n| subtitleFontFamily    | 副标题文字字体                   | `string`   | system-ui, sans-serif |\n| subtitleFontWeight    | 副标题字体粗细                   | `number`   | normal                |\n| subtitleLineHeight    | 副标题文字的行高                 | `number`   | 12                    |\n| subtitleTextAlign     | 副标题文字行内内容的水平对齐方式 | `string`   | left                  |\n| subtitleTextBaseline  | 副标题文字垂直方向的基线         | `string`   | top                   |\n| subtitleFill          | 副标题文字的填充色               | `string`   | #1D2129               |\n| subtitleFillOpacity   | 副标题文字的填充透明度           | `number`   | 0.65                  |\n| subtitleStroke        | 副标题文字的描边颜色             | `string`   | transparent           |\n| subtitleStrokeOpacity | 副标题文字的描边透明度           | `number`   | 1                     |\n| subtitleLineWidth     | 副标题文字描边宽度               | `number`   | 0                     |\n| subtitleLineDash      | 副标题文字虚线样式               | `number[]` | []                    |\n| subtitleOpacity       | 副标题文字整体透明度             | `number`   | 1                     |\n| subtitleShadowColor   | 副标题文字阴影颜色               | `string`   | transparent           |\n| subtitleShadowBlur    | 副标题文字阴影的高斯模糊系数     | `number`   | 0                     |\n| subtitleShadowOffsetX | 副标题文字阴影水平偏移量         | `number`   | 0                     |\n| subtitleShadowOffsetY | 副标题文字阴影垂直偏移量         | `number`   | 0                     |\n| subtitleCursor        | 副标题文字鼠标样式               | `string`   | default               |\n| subtitleDx            | 副标题文字在水平方向的偏移量     | `number`   | 0                     |\n| subtitleDy            | 副标题文字在垂直方向的偏移量     | `number`   | 0                     |\n\n## 尝试一下\n\n你可以在这个例子里随意修改并尝试不同的配置\n\n```js | ob { pin: true, inject: true }\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: { nodes: Array.from({ length: 12 }).map((_, i) => ({ id: `node${i}` })) },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n  plugins: [\n    {\n      key: 'title',\n      type: 'title',\n\n      align: 'center', // 标题的对齐方式\n      spacing: 4, // 主标题和副标题之间的间距\n      size: 60, // 标题的高度，默认为 44\n\n      // 标题\n      title: '这是一个标题这是一个标题', // 标题的文本\n      titleFontSize: 28, // 主标题的字体大小\n      titleFontFamily: 'sans-serif', // 主标题的字体\n      titleFontWeight: 600, // 主标题的字体粗细\n      titleFill: '#fff', // 主标题的文字颜色\n      titleFillOpacity: 1, // 主标题的文字透明度\n      titleStroke: '#000', // 主标题的文字描边颜色\n      titleLineWidth: 2, // 主标题的文字描边线宽\n      titleStrokeOpacity: 1, // 主标题的文字描边透明度\n\n      // 副标题\n      subtitle: '这是一个副标', // 副标题的文本\n      subtitleFontSize: 16, // 副标题的字体大小\n      subtitleFontFamily: 'Arial', // 副标题的字体\n      subtitleFontWeight: 300, // 副标题的字体粗细\n      subtitleFill: '#2989FF', // 副标题的文字颜色\n      subtitleFillOpacity: 1, // 副标题的文字透明度\n      subtitleStroke: '#000', // 副标题的文字描边颜色\n      subtitleLineWidth: 1, // 副标题的文字描边线宽\n      subtitleStrokeOpacity: 0.5, // 副标题的文字描边透明度\n    },\n  ],\n  node: {\n    palette: 'spectral',\n    style: { labelText: '你好' },\n  },\n  layout: {\n    type: 'circular',\n  },\n  autoFit: 'view',\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Toolbar.en.md",
    "content": "---\ntitle: Toolbar\norder: 15\n---\n\n## Overview\n\nThe Toolbar is a plugin in G6 used to provide a collection of operation buttons, supporting common chart operations such as zoom in, zoom out, auto-fit, and reset. Through the toolbar, users can quickly access common functions of the chart, improving operational efficiency and user experience.\n\n## Use Cases\n\nThis plugin is mainly used for:\n\n- Quickly accessing common functions of the chart\n\n## Basic Usage\n\nBelow is a simple example of initializing the Toolbar plugin:\n\n```js\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'toolbar',\n      getItems: () => [\n        { id: 'zoom-in', value: 'zoom-in' },\n        { id: 'zoom-out', value: 'zoom-out' },\n        { id: 'auto-fit', value: 'auto-fit' },\n      ],\n      onClick: (value) => {\n        // Handle button click events\n        if (value === 'zoom-in') {\n          graph.zoomTo(1.1);\n        } else if (value === 'zoom-out') {\n          graph.zoomTo(0.9);\n        } else if (value === 'auto-fit') {\n          graph.fitView();\n        }\n      },\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Property  | Description                                                                  | Type                                                                                        | Default Value | Required |\n| --------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------- | -------- |\n| type      | Plugin type                                                                  | string                                                                                      | `toolbar`     | ✓        |\n| key       | Unique identifier for the plugin, used for updates                           | string                                                                                      | -             |          |\n| className | Additional CSS class name for the toolbar DOM element                        | string                                                                                      | -             |          |\n| position  | Toolbar position relative to the canvas, [optional values](#position)        | string                                                                                      | `'top-left'`  |          |\n| style     | Custom style for the toolbar DOM element, [common values](#style-attributes) | [CSSStyleDeclaration](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration) | -             |          |\n| getItems  | Returns the list of toolbar items                                            | () => [ToolbarItem](#toolbaritem)[] \\| Promise<[ToolbarItem](#toolbaritem)[]>               | -             | ✓        |\n| onClick   | Callback function after a toolbar item is clicked                            | (value: string, target: Element) => void                                                    | -             |          |\n\n### position\n\nThe `position` parameter supports the following values:\n\n- `'top-left'`: Top left corner\n- `'top-right'`: Top right corner\n- `'bottom-left'`: Bottom left corner\n- `'bottom-right'`: Bottom right corner\n- `'left-top'`: Left side, top\n- `'left-bottom'`: Left side, bottom\n- `'right-top'`: Right side, top\n- `'right-bottom'`: Right side, bottom\n\n### style Attributes\n\n| Attribute       | Description      | Type   | Default Value       |\n| --------------- | ---------------- | ------ | ------------------- |\n| backgroundColor | Background color | string | `#fff`              |\n| border          | Border           | string | `1px solid #e8e8e8` |\n| borderRadius    | Border radius    | string | `4px`               |\n| height          | Height           | string | `auto`              |\n| margin          | Margin           | string | `12px`              |\n| opacity         | Opacity          | number | 0.9                 |\n| padding         | Padding          | string | `8px`               |\n| width           | Width            | string | `auto`              |\n\n### ToolbarItem\n\nEach toolbar item (ToolbarItem) includes the following attributes:\n\n| Attribute | Description                                                | Type     | Required |\n| --------- | ---------------------------------------------------------- | -------- | -------- |\n| id        | Icon ID of the item, see [Built-in Icons](#built-in-icons) | `string` | ✓        |\n| value     | Value returned when the item is clicked                    | `string` | ✓        |\n\n### Built-in Icons\n\nThe Toolbar provides the following built-in icon IDs:\n\n- `'zoom-in'`: Zoom in\n- `'zoom-out'`: Zoom out\n- `'redo'`: Redo\n- `'undo'`: Undo\n- `'edit'`: Edit\n- `'delete'`: Delete\n- `'auto-fit'`: Auto-fit view\n- `'export'`: Export chart\n- `'reset'`: Reset view\n- `'request-fullscreen'`: Request fullscreen\n- `'exit-fullscreen'`: Exit fullscreen\n\n### Custom Icons\n\nIn addition to using built-in icons, you can also use custom icons by introducing third-party icon libraries (such as Alibaba iconfont):\n\n```js\n// Introduce iconfont script\nconst iconFont = document.createElement('script');\niconFont.src = '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js'; // Replace with your iconfont script URL\ndocument.head.appendChild(iconFont);\n\n// Use custom icons in the toolbar\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'toolbar',\n      getItems: () => [\n        { id: 'icon-xinjian', value: 'new' }, // Use icons from iconfont\n        { id: 'icon-fenxiang', value: 'share' },\n        { id: 'icon-chexiao', value: 'undo' },\n      ],\n      onClick: (value) => {\n        // Handle click events\n      },\n    },\n  ],\n});\n```\n\n> Note: Custom icon IDs usually start with `icon-` and need to correspond to the icon names in the introduced iconfont.\n\n## Code Examples\n\n### Basic Toolbar\n\n```js\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'toolbar',\n      position: 'top-right',\n      getItems: () => [\n        { id: 'zoom-in', value: 'zoom-in' },\n        { id: 'zoom-out', value: 'zoom-out' },\n        { id: 'undo', value: 'undo' },\n        { id: 'redo', value: 'redo' },\n        { id: 'auto-fit', value: 'fit' },\n      ],\n      onClick: (value) => {\n        // redo, undo need to be used with the history plugin\n        const history = graph.getPluginInstance('history');\n        switch (value) {\n          case 'zoom-in':\n            graph.zoomTo(1.1);\n            break;\n          case 'zoom-out':\n            graph.zoomTo(0.9);\n            break;\n          case 'undo':\n            history?.undo();\n            break;\n          case 'redo':\n            history?.redo();\n            break;\n          case 'fit':\n            graph.fitView();\n            break;\n          default:\n            break;\n        }\n      },\n    },\n  ],\n});\n```\n\n### Custom Styles\n\n```js\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'toolbar',\n      className: 'my-custom-toolbar',\n      style: {\n        backgroundColor: '#f5f5f5',\n        padding: '8px',\n        boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',\n        borderRadius: '8px',\n        border: '1px solid #e8e8e8',\n        opacity: '0.9',\n        marginTop: '12px',\n        marginLeft: '12px',\n      },\n      getItems: () => [\n        { id: 'zoom-in', value: 'zoom-in' },\n        { id: 'zoom-out', value: 'zoom-out' },\n      ],\n      onClick: (value) => {\n        // Handle click events\n      },\n    },\n  ],\n});\n```\n\n> Common style attributes include:\n>\n> - `backgroundColor`: Background color\n> - `padding`: Padding\n> - `margin`/`marginTop`/`marginLeft`, etc.: Margin\n> - `border`: Border\n> - `borderRadius`: Border radius\n> - `boxShadow`: Shadow effect\n> - `opacity`: Opacity\n> - `width`/`height`: Width and height (default is content adaptive)\n> - `zIndex`: Layer (default is 100)\n> - `display`: Display mode (default is flex)\n\nThe toolbar container is set to `display: flex` by default, and child items use row layout by default (or change according to the direction configured by position). You can customize its appearance and position through `style`.\n\n### Asynchronous Loading of Toolbar Items\n\n```js\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'toolbar',\n      getItems: async () => {\n        // Toolbar configuration can be obtained from the server or other asynchronous sources\n        const response = await fetch('/api/toolbar-config');\n        const items = await response.json();\n        return items;\n      },\n      onClick: (value) => {\n        // Handle click events\n      },\n    },\n  ],\n});\n```\n\n## Common Issues\n\n### 1. Toolbar icons not displaying?\n\n- Check if the correct built-in icon ID is used\n- Ensure CSS styles are not overridden or conflicting\n\n### 2. How to use with other plugins?\n\nThe toolbar is often used in conjunction with other plugins (such as history):\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'history',\n      key: 'history',\n    },\n    {\n      type: 'toolbar',\n      getItems: () => [\n        { id: 'undo', value: 'undo' },\n        { id: 'redo', value: 'redo' },\n      ],\n      onClick: (value) => {\n        const history = graph.getPluginInstance('history');\n        if (value === 'undo') {\n          history.undo();\n        } else if (value === 'redo') {\n          history.redo();\n        }\n      },\n    },\n  ],\n});\n```\n\n### 3. How to dynamically update the toolbar?\n\nYou can use the `updatePlugin` method to dynamically update the toolbar:\n\n```js\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'toolbar',\n      key: 'my-toolbar',\n    },\n  ],\n});\n\n// Update toolbar position\ngraph.updatePlugin({\n  key: 'my-toolbar',\n  position: 'bottom-right',\n});\n```\n\n## Real Cases\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'toolbar',\n      position: 'top-left',\n      onClick: (item) => {\n        alert('item clicked:' + item);\n      },\n      getItems: () => {\n        // G6 内置了 9 个 icon，分别是 zoom-in、zoom-out、redo、undo、edit、delete、auto-fit、export、reset\n        return [\n          { id: 'zoom-in', value: 'zoom-in' },\n          { id: 'zoom-out', value: 'zoom-out' },\n          { id: 'redo', value: 'redo' },\n          { id: 'undo', value: 'undo' },\n          { id: 'edit', value: 'edit' },\n          { id: 'delete', value: 'delete' },\n          { id: 'auto-fit', value: 'auto-fit' },\n          { id: 'export', value: 'export' },\n          { id: 'reset', value: 'reset' },\n        ];\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Toolbar.zh.md",
    "content": "---\ntitle: 工具栏 Toolbar\norder: 15\n---\n\n## 概述\n\nToolbar 是 G6 中用于提供操作按钮集合的插件，支持放大、缩小、自适应、重置等常用图表操作。通过工具栏，用户可以快速访问图表的常用功能，提高操作效率和用户体验。\n\n## 使用场景\n\n这一插件主要用于：\n\n- 快速访问图表的常用功能\n\n## 基本用法\n\n以下是一个简单的 Toolbar 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'toolbar',\n      getItems: () => [\n        { id: 'zoom-in', value: 'zoom-in' },\n        { id: 'zoom-out', value: 'zoom-out' },\n        { id: 'auto-fit', value: 'auto-fit' },\n      ],\n      onClick: (value) => {\n        // 处理按钮点击事件\n        if (value === 'zoom-in') {\n          graph.zoomTo(1.1);\n        } else if (value === 'zoom-out') {\n          graph.zoomTo(0.9);\n        } else if (value === 'auto-fit') {\n          graph.fitView();\n        }\n      },\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 属性      | 描述                                             | 类型                                                                                        | 默认值       | 必选 |\n| --------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------- | ------------ | ---- |\n| type      | 插件类型                                         | string                                                                                      | `toolbar`    | ✓    |\n| key       | 插件唯一标识符，用于后续更新                     | string                                                                                      | -            |      |\n| className | 工具栏DOM元素的额外CSS类名                       | string                                                                                      | -            |      |\n| position  | 工具栏位置，相对于画布，[可选值](#position)      | string                                                                                      | `'top-left'` |      |\n| style     | 工具栏DOM元素的自定义样式，[常用值](#style-属性) | [CSSStyleDeclaration](https://developer.mozilla.org/zh-CN/docs/Web/API/CSSStyleDeclaration) | -            |      |\n| getItems  | 返回工具栏项目列表                               | () => [ToolbarItem](#toolbaritem)[] \\| Promise<[ToolbarItem](#toolbaritem)[]>               | -            | ✓    |\n| onClick   | 工具栏项目点击后的回调函数                       | (value: string, target: Element) => void                                                    | -            |      |\n\n### position\n\n`position` 参数支持以下值：\n\n- `'top-left'`：左上角\n- `'top-right'`：右上角\n- `'bottom-left'`：左下角\n- `'bottom-right'`：右下角\n- `'left-top'`：左侧靠上\n- `'left-bottom'`：左侧靠下\n- `'right-top'`：右侧靠上\n- `'right-bottom'`：右侧靠下\n\n### style 属性\n\n| 属性            | 描述     | 类型   | 默认值              |\n| --------------- | -------- | ------ | ------------------- |\n| backgroundColor | 背景颜色 | string | `#fff`              |\n| border          | 边框     | string | `1px solid #e8e8e8` |\n| borderRadius    | 圆角     | string | `4px`               |\n| height          | 高度     | string | `auto`              |\n| margin          | 外边距   | string | `12px`              |\n| opacity         | 透明度   | number | 0.9                 |\n| padding         | 内边距   | string | `8px`               |\n| width           | 宽度     | string | `auto`              |\n\n### ToolbarItem\n\n每个工具栏项目 (ToolbarItem) 包含以下属性：\n\n| 属性  | 描述                                            | 类型     | 必选 |\n| ----- | ----------------------------------------------- | -------- | ---- |\n| id    | 项目的图标ID，内置图标ID见[内置图标](#内置图标) | `string` | ✓    |\n| value | 点击项目时返回的值                              | `string` | ✓    |\n\n### 内置图标\n\nToolbar 提供以下内置图标 ID：\n\n- `'zoom-in'`：放大\n- `'zoom-out'`：缩小\n- `'redo'`：重做\n- `'undo'`：撤销\n- `'edit'`：编辑\n- `'delete'`：删除\n- `'auto-fit'`：自适应视图\n- `'export'`：导出图表\n- `'reset'`：重置视图\n- `'request-fullscreen'`：请求全屏\n- `'exit-fullscreen'`：退出全屏\n\n### 自定义图标\n\n除了使用内置图标外，还可以通过引入第三方图标库（如阿里巴巴 iconfont）来使用自定义图标：\n\n```js\n// 引入 iconfont 脚本\nconst iconFont = document.createElement('script');\niconFont.src = '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js'; // 替换为你的 iconfont 脚本地址\ndocument.head.appendChild(iconFont);\n\n// 在工具栏中使用自定义图标\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'toolbar',\n      getItems: () => [\n        { id: 'icon-xinjian', value: 'new' }, // 使用 iconfont 中的图标\n        { id: 'icon-fenxiang', value: 'share' },\n        { id: 'icon-chexiao', value: 'undo' },\n      ],\n      onClick: (value) => {\n        // 处理点击事件\n      },\n    },\n  ],\n});\n```\n\n> 注意：自定义图标的 ID 通常以 `icon-` 开头，需要与引入的 iconfont 中的图标名称对应。\n\n## 代码示例\n\n### 基础工具栏\n\n```js\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'toolbar',\n      position: 'top-right',\n      getItems: () => [\n        { id: 'zoom-in', value: 'zoom-in' },\n        { id: 'zoom-out', value: 'zoom-out' },\n        { id: 'undo', value: 'undo' },\n        { id: 'redo', value: 'redo' },\n        { id: 'auto-fit', value: 'fit' },\n      ],\n      onClick: (value) => {\n        // redo、undo 需要配合 history 插件使用\n        const history = graph.getPluginInstance('history');\n        switch (value) {\n          case 'zoom-in':\n            graph.zoomTo(1.1);\n            break;\n          case 'zoom-out':\n            graph.zoomTo(0.9);\n            break;\n          case 'undo':\n            history?.undo();\n            break;\n          case 'redo':\n            history?.redo();\n            break;\n          case 'fit':\n            graph.fitView();\n            break;\n          default:\n            break;\n        }\n      },\n    },\n  ],\n});\n```\n\n### 自定义样式\n\n```js\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'toolbar',\n      className: 'my-custom-toolbar',\n      style: {\n        backgroundColor: '#f5f5f5',\n        padding: '8px',\n        boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',\n        borderRadius: '8px',\n        border: '1px solid #e8e8e8',\n        opacity: '0.9',\n        marginTop: '12px',\n        marginLeft: '12px',\n      },\n      getItems: () => [\n        { id: 'zoom-in', value: 'zoom-in' },\n        { id: 'zoom-out', value: 'zoom-out' },\n      ],\n      onClick: (value) => {\n        // 处理点击事件\n      },\n    },\n  ],\n});\n```\n\n> 常用的样式属性包括：\n>\n> - `backgroundColor`：背景颜色\n> - `padding`：内边距\n> - `margin`/`marginTop`/`marginLeft` 等：外边距\n> - `border`：边框\n> - `borderRadius`：圆角\n> - `boxShadow`：阴影效果\n> - `opacity`：透明度\n> - `width`/`height`：宽高（默认自适应内容）\n> - `zIndex`：层级（默认为 100）\n> - `display`：显示方式（默认为 flex）\n\n工具栏容器默认设置 `display: flex`，子项目默认使用行布局（或根据 position 配置的方向改变）。你可以通过 `style` 自定义其外观和位置。\n\n### 异步加载工具栏项\n\n```js\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'toolbar',\n      getItems: async () => {\n        // 可以从服务器或其他异步源获取工具栏配置\n        const response = await fetch('/api/toolbar-config');\n        const items = await response.json();\n        return items;\n      },\n      onClick: (value) => {\n        // 处理点击事件\n      },\n    },\n  ],\n});\n```\n\n## 常见问题\n\n### 1. 工具栏图标不显示？\n\n- 检查是否使用了正确的内置图标 ID\n- 确保 CSS 样式未被覆盖或冲突\n\n### 2. 如何结合其他插件使用？\n\n工具栏常常与其他插件（如历史记录）配合使用：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'history',\n      key: 'history',\n    },\n    {\n      type: 'toolbar',\n      getItems: () => [\n        { id: 'undo', value: 'undo' },\n        { id: 'redo', value: 'redo' },\n      ],\n      onClick: (value) => {\n        const history = graph.getPluginInstance('history');\n        if (value === 'undo') {\n          history.undo();\n        } else if (value === 'redo') {\n          history.redo();\n        }\n      },\n    },\n  ],\n});\n```\n\n### 3. 如何动态更新工具栏？\n\n可以使用 `updatePlugin` 方法动态更新工具栏：\n\n```js\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'toolbar',\n      key: 'my-toolbar',\n    },\n  ],\n});\n\n// 更新工具栏位置\ngraph.updatePlugin({\n  key: 'my-toolbar',\n  position: 'bottom-right',\n});\n```\n\n## 实际案例\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'toolbar',\n      position: 'top-left',\n      onClick: (item) => {\n        alert('item clicked:' + item);\n      },\n      getItems: () => {\n        // G6 内置了 9 个 icon，分别是 zoom-in、zoom-out、redo、undo、edit、delete、auto-fit、export、reset\n        return [\n          { id: 'zoom-in', value: 'zoom-in' },\n          { id: 'zoom-out', value: 'zoom-out' },\n          { id: 'redo', value: 'redo' },\n          { id: 'undo', value: 'undo' },\n          { id: 'edit', value: 'edit' },\n          { id: 'delete', value: 'delete' },\n          { id: 'auto-fit', value: 'auto-fit' },\n          { id: 'export', value: 'export' },\n          { id: 'reset', value: 'reset' },\n        ];\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Tooltip.en.md",
    "content": "---\ntitle: Tooltip\norder: 16\n---\n\n## Overview\n\nThe Tooltip plugin is used to display additional information when users hover over or click on elements in the graph. It helps users better understand the data in the graph and improves the interactive experience.\n\n## Use Cases\n\n- **Detailed Information Display**: When users need to understand detailed information about elements, use Tooltip to display this information\n- **Data Visualization Assistance**: In data visualization, Tooltip can display detailed information about data points in charts, helping users better understand the data\n- **Interactive Feedback**: Provide immediate visual feedback for user mouse operations\n\n## Basic Usage\n\nThe simplest Tooltip plugin configuration:\n\n```js\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'tooltip',\n    },\n  ],\n});\n```\n\n## Configuration Options\n\n| Property     | Description               | Type                                                                                                                                          | Default Value                         | Required |\n| ------------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | -------- |\n| type         | Plugin type               | string                                                                                                                                        | `tooltip`                             | ✓        |\n| key          | Identifier                | string                                                                                                                                        | -                                     |          |\n| position     | Tooltip position          | `top` \\| `bottom` \\| `left` \\| `right` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right`                                        | `top-right`                           |          |\n| enable       | Whether plugin is enabled | boolean \\| ((event: [IElementEvent](/en/api/event#event-object-properties), items: NodeData \\| EdgeData \\| ComboData[]) => boolean)           | true                                  |          |\n| getContent   | Custom content            | (event: [IElementEvent](/en/api/event#event-object-properties), items: NodeData \\| EdgeData \\| ComboData[]) => Promise<HTMLElement \\| string> | -                                     |          |\n| onOpenChange | Show/hide callback        | (open: boolean) => void                                                                                                                       | -                                     |          |\n| trigger      | Trigger behavior          | `hover` \\| `click`                                                                                                                            | `hover`                               |\n| container    | Custom render container   | string \\| HTMLElement                                                                                                                         | -                                     |          |\n| offset       | Offset distance           | [number,number]                                                                                                                               | [10,10]                               |          |\n| enterable    | Whether pointer can enter | boolean                                                                                                                                       | false                                 |          |\n| title        | Title                     | string                                                                                                                                        | -                                     |\n| style        | Style object              | Record<string,any>                                                                                                                            | {'.tooltip': { visibility: 'hidden'}} |          |\n\n## Detailed Configuration\n\n### enable - Conditional Enable\n\nControls whether the plugin is enabled, supports passing functions to dynamically adjust enable logic.\n\n**Example: Enable Tooltip only for nodes**\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 }, data: { name: 'Server Node' } },\n    { id: 'node2', style: { x: 200, y: 100 }, data: { name: 'Database Node' } },\n  ],\n  edges: [{ source: 'node1', target: 'node2', data: { type: 'Connection' } }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data,\n  plugins: [\n    {\n      type: 'tooltip',\n      // Enable only for nodes, not for edges\n      enable: (e) => e.targetType === 'node',\n      getContent: (e, items) => {\n        return `<div>Node: ${items[0].data.name}</div>`;\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### getContent - Custom Content\n\nCustomize Tooltip content rendering, supports returning HTMLElement or string.\n\n**Example: Dynamically render custom HTML content**\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n      style: { x: 100, y: 100 },\n      data: { name: 'Server A', type: 'Application Server', status: 'Running', cpu: '45%', memory: '2.1GB' },\n    },\n    {\n      id: 'node2',\n      style: { x: 250, y: 100 },\n      data: { name: 'Database B', type: 'MySQL Database', status: 'Normal', connections: 23, size: '500MB' },\n    },\n  ],\n  edges: [{ source: 'node1', target: 'node2', data: { bandwidth: '1Gbps', latency: '5ms' } }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data,\n  plugins: [\n    {\n      type: 'tooltip',\n      getContent: (e, items) => {\n        const item = items[0];\n\n        if (e.targetType === 'node') {\n          return `\n            <div>\n              <h4 style=\"margin: 0 0 8px 0; color: #333; border-bottom: 1px solid #eee; padding-bottom: 4px;\">\n                ${item.data.name}\n              </h4>\n              <div style=\"margin: 4px 0; color: #666;\">\n                <strong>Type:</strong> ${item.data.type}\n              </div>\n              <div style=\"margin: 4px 0; color: #666;\">\n                <strong>Status:</strong>\n                <span style=\"color: ${item.data.status === 'Running' || item.data.status === 'Normal' ? '#52c41a' : '#ff4d4f'}\">\n                  ${item.data.status}\n                </span>\n              </div>\n              ${item.data.cpu ? `<div style=\"margin: 4px 0; color: #666;\"><strong>CPU:</strong> ${item.data.cpu}</div>` : ''}\n              ${item.data.memory ? `<div style=\"margin: 4px 0; color: #666;\"><strong>Memory:</strong> ${item.data.memory}</div>` : ''}\n              ${item.data.connections ? `<div style=\"margin: 4px 0; color: #666;\"><strong>Connections:</strong> ${item.data.connections}</div>` : ''}\n              ${item.data.size ? `<div style=\"margin: 4px 0; color: #666;\"><strong>Size:</strong> ${item.data.size}</div>` : ''}\n            </div>\n          `;\n        } else if (e.targetType === 'edge') {\n          return `\n            <div>\n              <h4 style=\"margin: 0 0 8px 0; color: #333;\">Connection Info</h4>\n              <div style=\"margin: 4px 0; color: #666;\"><strong>Bandwidth:</strong> ${item.data.bandwidth}</div>\n              <div style=\"margin: 4px 0; color: #666;\"><strong>Latency:</strong> ${item.data.latency}</div>\n            </div>\n          `;\n        }\n\n        return 'No information available';\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### trigger - Trigger Mode\n\nControls the trigger behavior of Tooltip.\n\n**Available values:**\n\n- `hover`: Trigger when mouse enters element (default)\n- `click`: Trigger when mouse clicks element\n\n**Example: Click-triggered Tooltip**\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 }, data: { name: 'Click me' } },\n    { id: 'node2', style: { x: 200, y: 100 }, data: { name: 'Click me too' } },\n  ],\n  edges: [{ source: 'node1', target: 'node2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 350,\n  height: 200,\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.data.name,\n    },\n  },\n  plugins: [\n    {\n      type: 'tooltip',\n      trigger: 'click',\n      getContent: (e, items) => {\n        return `\n          <div>\n            <div style=\"color: #0369a1; font-weight: bold; margin-bottom: 4px;\">\n              Click Triggered 🖱️\n            </div>\n            <div style=\"color: #0c4a6e;\">\n              Element ID: ${items[0].id}<br/>\n              Name: ${items[0].data?.name || 'Unnamed'}\n            </div>\n          </div>\n        `;\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### position - Display Position\n\nControls the display position of Tooltip relative to mouse position.\n\n**Available values:**\n\n- `top`: Top\n- `bottom`: Bottom\n- `left`: Left\n- `right`: Right\n- `top-left`: Top left\n- `top-right`: Top right (default)\n- `bottom-left`: Bottom left\n- `bottom-right`: Bottom right\n\n**Example: Tooltips at different positions**\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 }, data: { label: 'TOP' } },\n    { id: 'node2', style: { x: 250, y: 100 }, data: { label: 'BOTTOM' } },\n    { id: 'node3', style: { x: 100, y: 250 }, data: { label: 'LEFT' } },\n    { id: 'node4', style: { x: 250, y: 250 }, data: { label: 'RIGHT' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 400,\n  data,\n  node: { style: { labelText: (d) => d.data.label } },\n  plugins: [\n    {\n      key: 'tooltip-top',\n      type: 'tooltip',\n      position: 'top',\n      enable: (e, items) => items[0].id === 'node1',\n      getContent: () => `Display at top ⬆️`,\n      style: {\n        '.tooltip': {\n          background: ' #fff2e8',\n          border: '1px solid #ffa940',\n          borderRadius: 4,\n        },\n      },\n    },\n    {\n      key: 'tooltip-bottom',\n      type: 'tooltip',\n      position: 'bottom',\n      enable: (e, items) => items[0].id === 'node2',\n      getContent: () => `Display at bottom ⬇️`,\n      style: {\n        '.tooltip': {\n          background: '#f6ffed',\n          border: '1px solid #73d13d',\n          borderRadius: 4,\n        },\n      },\n    },\n    {\n      key: 'tooltip-left',\n      type: 'tooltip',\n      position: 'left',\n      enable: (e, items) => items[0].id === 'node3',\n      getContent: () => `Display at left ⬅️`,\n      style: {\n        '.tooltip': {\n          background: '#fff1f0',\n          border: '1px solid #ff7875',\n          borderRadius: 4,\n        },\n      },\n    },\n    {\n      key: 'tooltip-right',\n      type: 'tooltip',\n      position: 'right',\n      enable: (e, items) => items[0].id === 'node4',\n      getContent: () => `Display at right ➡️`,\n      style: {\n        '.tooltip': {\n          background: '#f0f5ff',\n          border: '1px solid #597ef7',\n          borderRadius: 4,\n        },\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### offset - Offset\n\nSet the offset for Tooltip display position, with mouse position as the base point.\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 }, data: { label: 'Default offset' } },\n    { id: 'node2', style: { x: 250, y: 100 }, data: { label: 'Custom offset' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 200,\n  data,\n  plugins: [\n    {\n      key: 'tooltip-default',\n      type: 'tooltip',\n      enable: (e, items) => items[0].id === 'node1',\n      getContent: () => `Default offset [10,10]`,\n    },\n    {\n      key: 'tooltip-custom',\n      type: 'tooltip',\n      offset: [30, -10], // Offset 30px to the right, 10px up\n      enable: (e, items) => items[0].id === 'node2',\n      getContent: () => `Custom offset [30,-10]`,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### enterable - Mouse Enterable\n\nControls whether the mouse pointer can enter the tooltip box, commonly used for scenarios requiring interaction within the Tooltip.\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 }, data: { name: 'User A', email: 'usera@example.com' } },\n    { id: 'node2', style: { x: 250, y: 100 }, data: { name: 'User B', email: 'userb@example.com' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data,\n  plugins: [\n    {\n      type: 'tooltip',\n      enterable: true,\n      position: 'right',\n      getContent: (e, items) => {\n        const item = items[0];\n        return `\n          <div>\n            <h4 style=\"margin: 0 0 12px 0; color: #333;\">User Actions</h4>\n            <div style=\"margin-bottom: 8px; color: #666;\">\n              <strong>Name:</strong> ${item.data.name}\n            </div>\n            <div style=\"margin-bottom: 12px; color: #666;\">\n              <strong>Email:</strong> ${item.data.email}\n            </div>\n            <div style=\"display: flex; gap: 8px;\">\n              <button onclick=\"alert('Send message to ${item.data.name}')\"\n                      style=\"padding: 4px 12px; background: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;\">\n                Message\n              </button>\n              <button onclick=\"alert('View ${item.data.name} details')\"\n                      style=\"padding: 4px 12px; background: #52c41a; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;\">\n                Details\n              </button>\n            </div>\n          </div>\n        `;\n      },\n      style: {\n        '.tooltip': {\n          background: '#fff',\n          borderRadius: '8px',\n          boxShadow: '0 4px 20px rgba(0,0,0,0.15)',\n          minWidth: '200px',\n        },\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### style - Style Customization\n\nCustomize Tooltip styles.\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 }, data: { theme: 'dark', name: 'Dark Theme' } },\n    { id: 'node2', style: { x: 250, y: 100 }, data: { theme: 'light', name: 'Light Theme' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data,\n  plugins: [\n    {\n      key: 'tooltip-dark',\n      type: 'tooltip',\n      enable: (e, items) => items[0].data.theme === 'dark',\n      style: {\n        '.tooltip': {\n          background: '#1f1f1f',\n          color: '#fff',\n          border: '1px solid #333',\n          borderRadius: '8px',\n          fontSize: '14px',\n          fontFamily: 'Arial, sans-serif',\n          boxShadow: '0 4px 20px rgba(0,0,0,0.3)',\n        },\n      },\n      getContent: (e, items) => {\n        return `<div>🌙 ${items[0].data.name}</div>`;\n      },\n    },\n    {\n      key: 'tooltip-light',\n      type: 'tooltip',\n      enable: (e, items) => items[0].data.theme === 'light',\n      style: {\n        '.tooltip': {\n          background: '#ffffff',\n          color: '#333',\n          border: '1px solid #d9d9d9',\n          borderRadius: '8px',\n          fontSize: '14px',\n          fontFamily: 'Arial, sans-serif',\n          boxShadow: '0 2px 8px rgba(0,0,0,0.15)',\n        },\n      },\n      getContent: (e, items) => {\n        return `<div>☀️ ${items[0].data.name}</div>`;\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n## Practical Examples\n\n- [Basic Tooltip](/en/examples/plugin/tooltip/#basic)\n- [Click-triggered Tooltip](/en/examples/plugin/tooltip/#click)\n- [Different tooltips for hover and click on the same element](/en/examples/plugin/tooltip/#dual)\n- [Custom styled Tooltip](/en/examples/plugin/tooltip/#custom-style)\n- [Asynchronous content loading Tooltip](/en/examples/plugin/tooltip/#async)\n\n## API\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Tooltip.zh.md",
    "content": "---\ntitle: 提示框 Tooltip\norder: 16\n---\n\n## 概述\n\nTooltip 插件用于在用户将鼠标悬停或点击图中的元素时，显示额外的信息。它可以帮助用户更好地理解图中的数据，提高交互体验。\n\n## 使用场景\n\n- **详细信息展示**：当用户需要了解元素的详细信息时，使用 Tooltip 提示框来展示这些信息\n- **数据可视化辅助**：在数据可视化中，Tooltip 可以显示图表中数据点的详细信息，帮助用户更好地理解数据\n- **交互反馈**：为用户的鼠标操作提供即时的视觉反馈\n\n## 基本使用\n\n最简单的 Tooltip 插件配置：\n\n```js\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'tooltip',\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 属性         | 描述                    | 类型                                                                                                                            | 默认值                                | 必选 |\n| ------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ---- |\n| type         | 插件类型                | string                                                                                                                          | `tooltip`                             | ✓    |\n| key          | 标识符                  | string                                                                                                                          | -                                     |      |\n| position     | 气泡框位置              | `top` \\| `bottom` \\| `left` \\| `right` \\| `top-left` \\| `top-right` \\| `bottom-left` \\| `bottom-right`                          | `top-right`                           |      |\n| enable       | 插件是否启用            | boolean \\| ((event: [IElementEvent](/api/event#事件对象属性), items: NodeData \\| EdgeData \\| ComboData[]) => boolean)           | true                                  |      |\n| getContent   | 自定义内容              | (event: [IElementEvent](/api/event#事件对象属性), items: NodeData \\| EdgeData \\| ComboData[]) => Promise<HTMLElement \\| string> | -                                     |      |\n| onOpenChange | 显示隐藏的回调          | (open: boolean) => void                                                                                                         | -                                     |      |\n| trigger      | 触发行为                | `hover` \\| `click`                                                                                                              | `hover`                               |\n| container    | tooltip自定义渲染的容器 | string \\| HTMLElement                                                                                                           | -                                     |      |\n| offset       | 偏移距离                | [number,number]                                                                                                                 | [10,10]                               |      |\n| enterable    | 指针是否可以进入        | boolean                                                                                                                         | false                                 |      |\n| title        | 标题                    | string                                                                                                                          | -                                     |\n| style        | 样式对象                | Record<string,any>                                                                                                              | {'.tooltip': { visibility: 'hidden'}} |      |\n\n## 详细配置说明\n\n### enable - 条件启用\n\n控制插件是否启用，支持传入函数动态调整启用逻辑。\n\n**示例：只对节点启用 Tooltip**\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 }, data: { name: '服务器节点' } },\n    { id: 'node2', style: { x: 200, y: 100 }, data: { name: '数据库节点' } },\n  ],\n  edges: [{ source: 'node1', target: 'node2', data: { type: '连接线' } }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data,\n  plugins: [\n    {\n      type: 'tooltip',\n      // 只对节点启用，边不显示tooltip\n      enable: (e) => e.targetType === 'node',\n      getContent: (e, items) => {\n        return `<div>节点: ${items[0].data.name}</div>`;\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### getContent - 自定义内容\n\n自定义渲染 Tooltip 内容，支持返回 HTMLElement 或 string。\n\n**示例：动态渲染自定义 HTML 内容**\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n      style: { x: 100, y: 100 },\n      data: { name: '服务器A', type: '应用服务器', status: '运行中', cpu: '45%', memory: '2.1GB' },\n    },\n    {\n      id: 'node2',\n      style: { x: 250, y: 100 },\n      data: { name: '数据库B', type: 'MySQL数据库', status: '正常', connections: 23, size: '500MB' },\n    },\n  ],\n  edges: [{ source: 'node1', target: 'node2', data: { bandwidth: '1Gbps', latency: '5ms' } }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data,\n  plugins: [\n    {\n      type: 'tooltip',\n      getContent: (e, items) => {\n        const item = items[0];\n\n        if (e.targetType === 'node') {\n          return `\n            <div>\n              <h4 style=\"margin: 0 0 8px 0; color: #333; border-bottom: 1px solid #eee; padding-bottom: 4px;\">\n                ${item.data.name}\n              </h4>\n              <div style=\"margin: 4px 0; color: #666;\">\n                <strong>类型:</strong> ${item.data.type}\n              </div>\n              <div style=\"margin: 4px 0; color: #666;\">\n                <strong>状态:</strong>\n                <span style=\"color: ${item.data.status === '运行中' || item.data.status === '正常' ? '#52c41a' : '#ff4d4f'}\">\n                  ${item.data.status}\n                </span>\n              </div>\n              ${item.data.cpu ? `<div style=\"margin: 4px 0; color: #666;\"><strong>CPU:</strong> ${item.data.cpu}</div>` : ''}\n              ${item.data.memory ? `<div style=\"margin: 4px 0; color: #666;\"><strong>内存:</strong> ${item.data.memory}</div>` : ''}\n              ${item.data.connections ? `<div style=\"margin: 4px 0; color: #666;\"><strong>连接数:</strong> ${item.data.connections}</div>` : ''}\n              ${item.data.size ? `<div style=\"margin: 4px 0; color: #666;\"><strong>大小:</strong> ${item.data.size}</div>` : ''}\n            </div>\n          `;\n        } else if (e.targetType === 'edge') {\n          return `\n            <div>\n              <h4 style=\"margin: 0 0 8px 0; color: #333;\">连接信息</h4>\n              <div style=\"margin: 4px 0; color: #666;\"><strong>带宽:</strong> ${item.data.bandwidth}</div>\n              <div style=\"margin: 4px 0; color: #666;\"><strong>延迟:</strong> ${item.data.latency}</div>\n            </div>\n          `;\n        }\n\n        return '暂无信息';\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### trigger - 触发方式\n\n控制 Tooltip 的触发行为。\n\n**可选值：**\n\n- `hover`：鼠标移入元素时触发（默认）\n- `click`：鼠标点击元素时触发\n\n**示例：点击触发 Tooltip**\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 }, data: { name: '点击我' } },\n    { id: 'node2', style: { x: 200, y: 100 }, data: { name: '也点击我' } },\n  ],\n  edges: [{ source: 'node1', target: 'node2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 350,\n  height: 200,\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.data.name,\n    },\n  },\n  plugins: [\n    {\n      type: 'tooltip',\n      trigger: 'click',\n      getContent: (e, items) => {\n        return `\n          <div>\n            <div style=\"color: #0369a1; font-weight: bold; margin-bottom: 4px;\">\n              点击触发 🖱️\n            </div>\n            <div style=\"color: #0c4a6e;\">\n              元素ID: ${items[0].id}<br/>\n              名称: ${items[0].data?.name || '未命名'}\n            </div>\n          </div>\n        `;\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### position - 显示位置\n\n控制 Tooltip 相对于鼠标位置的显示位置。\n\n**可选值：**\n\n- `top`: 顶部\n- `bottom`: 底部\n- `left`: 左侧\n- `right`: 右侧\n- `top-left`: 顶部靠左\n- `top-right`: 顶部靠右（默认）\n- `bottom-left`: 底部靠左\n- `bottom-right`: 底部靠右\n\n**示例：不同位置的 Tooltip**\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 }, data: { label: 'TOP' } },\n    { id: 'node2', style: { x: 250, y: 100 }, data: { label: 'BOTTOM' } },\n    { id: 'node3', style: { x: 100, y: 250 }, data: { label: 'LEFT' } },\n    { id: 'node4', style: { x: 250, y: 250 }, data: { label: 'RIGHT' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 400,\n  data,\n  node: { style: { labelText: (d) => d.data.label } },\n  plugins: [\n    {\n      key: 'tooltip-top',\n      type: 'tooltip',\n      position: 'top',\n      enable: (e, items) => items[0].id === 'node1',\n      getContent: () => `顶部显示 ⬆️`,\n      style: {\n        '.tooltip': {\n          background: ' #fff2e8',\n          border: '1px solid #ffa940',\n          borderRadius: 4,\n        },\n      },\n    },\n    {\n      key: 'tooltip-bottom',\n      type: 'tooltip',\n      position: 'bottom',\n      enable: (e, items) => items[0].id === 'node2',\n      getContent: () => `底部显示 ⬇️`,\n      style: {\n        '.tooltip': {\n          background: '#f6ffed',\n          border: '1px solid #73d13d',\n          borderRadius: 4,\n        },\n      },\n    },\n    {\n      key: 'tooltip-left',\n      type: 'tooltip',\n      position: 'left',\n      enable: (e, items) => items[0].id === 'node3',\n      getContent: () => `左侧显示 ⬅️`,\n      style: {\n        '.tooltip': {\n          background: '#fff1f0',\n          border: '1px solid #ff7875',\n          borderRadius: 4,\n        },\n      },\n    },\n    {\n      key: 'tooltip-right',\n      type: 'tooltip',\n      position: 'right',\n      enable: (e, items) => items[0].id === 'node4',\n      getContent: () => `右侧显示 ➡️`,\n      style: {\n        '.tooltip': {\n          background: '#f0f5ff',\n          border: '1px solid #597ef7',\n          borderRadius: 4,\n        },\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### offset - 偏移量\n\n设置 Tooltip 显示位置的偏移量，以鼠标位置为基点。\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 }, data: { label: '默认偏移' } },\n    { id: 'node2', style: { x: 250, y: 100 }, data: { label: '自定义偏移' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 200,\n  data,\n  plugins: [\n    {\n      key: 'tooltip-default',\n      type: 'tooltip',\n      enable: (e, items) => items[0].id === 'node1',\n      getContent: () => `默认偏移 [10,10]`,\n    },\n    {\n      key: 'tooltip-custom',\n      type: 'tooltip',\n      offset: [30, -10], // 向右偏移30px，向上偏移10px\n      enable: (e, items) => items[0].id === 'node2',\n      getContent: () => `自定义偏移 [30,-10]`,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### enterable - 鼠标可进入\n\n控制鼠标指针是否可以进入气泡框，常用于需要在 Tooltip 内进行交互的场景。\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 }, data: { name: '用户A', email: 'usera@example.com' } },\n    { id: 'node2', style: { x: 250, y: 100 }, data: { name: '用户B', email: 'userb@example.com' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data,\n  plugins: [\n    {\n      type: 'tooltip',\n      enterable: true,\n      position: 'right',\n      getContent: (e, items) => {\n        const item = items[0];\n        return `\n          <div>\n            <h4 style=\"margin: 0 0 12px 0; color: #333;\">用户操作</h4>\n            <div style=\"margin-bottom: 8px; color: #666;\">\n              <strong>姓名:</strong> ${item.data.name}\n            </div>\n            <div style=\"margin-bottom: 12px; color: #666;\">\n              <strong>邮箱:</strong> ${item.data.email}\n            </div>\n            <div style=\"display: flex; gap: 8px;\">\n              <button onclick=\"alert('发送消息给 ${item.data.name}')\"\n                      style=\"padding: 4px 12px; background: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;\">\n                发消息\n              </button>\n              <button onclick=\"alert('查看 ${item.data.name} 的详情')\"\n                      style=\"padding: 4px 12px; background: #52c41a; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;\">\n                详情\n              </button>\n            </div>\n          </div>\n        `;\n      },\n      style: {\n        '.tooltip': {\n          background: '#fff',\n          borderRadius: '8px',\n          boxShadow: '0 4px 20px rgba(0,0,0,0.15)',\n          minWidth: '200px',\n        },\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### style - 样式自定义\n\n自定义 Tooltip 的样式。\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 100, y: 100 }, data: { theme: 'dark', name: '深色主题' } },\n    { id: 'node2', style: { x: 250, y: 100 }, data: { theme: 'light', name: '浅色主题' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 400,\n  height: 200,\n  data,\n  plugins: [\n    {\n      key: 'tooltip-dark',\n      type: 'tooltip',\n      enable: (e, items) => items[0].data.theme === 'dark',\n      style: {\n        '.tooltip': {\n          background: '#1f1f1f',\n          color: '#fff',\n          border: '1px solid #333',\n          borderRadius: '8px',\n          fontSize: '14px',\n          fontFamily: 'Arial, sans-serif',\n          boxShadow: '0 4px 20px rgba(0,0,0,0.3)',\n        },\n      },\n      getContent: (e, items) => {\n        return `<div>🌙 ${items[0].data.name}</div>`;\n      },\n    },\n    {\n      key: 'tooltip-light',\n      type: 'tooltip',\n      enable: (e, items) => items[0].data.theme === 'light',\n      style: {\n        '.tooltip': {\n          background: '#ffffff',\n          color: '#333',\n          border: '1px solid #d9d9d9',\n          borderRadius: '8px',\n          fontSize: '14px',\n          fontFamily: 'Arial, sans-serif',\n          boxShadow: '0 2px 8px rgba(0,0,0,0.15)',\n        },\n      },\n      getContent: (e, items) => {\n        return `<div>☀️ ${items[0].data.name}</div>`;\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n## 实际案例\n\n- [基础提示框](/examples/plugin/tooltip/#basic)\n- [点击触发 Tooltip](/examples/plugin/tooltip/#click)\n- [鼠标移入和点击同一元素时显示不同的提示框](/examples/plugin/tooltip/#dual)\n- [自定义样式的 Tooltip](/examples/plugin/tooltip/#custom-style)\n- [异步加载内容的 Tooltip](/examples/plugin/tooltip/#async)\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Watermark.en.md",
    "content": "---\ntitle: Watermark\norder: 17\n---\n\n## Overview\n\nThe watermark plugin supports using text and images as watermarks. The principle is to add a `background-image` attribute to the div of the Graph container, and then control the position and style of the watermark through CSS. For text watermarks, a hidden canvas is used to convert the text into an image.\n\n## Use Cases\n\n- Add copyright or ownership marks to charts\n- Mark the status of charts during presentations or previews\n- Add anti-leakage marks to sensitive data\n\n## Basic Usage\n\nBelow is a simple example of initializing the Watermark plugin:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'watermark',\n      text: 'G6 Graph', // Watermark text\n      opacity: 0.2, // Opacity\n      rotate: Math.PI / 12, // Rotation angle\n    },\n  ],\n});\n```\n\n## Online Experience\n\n<embed src=\"@/common/api/plugins/watermark.md\"></embed>\n\n## Configuration Options\n\n| Property             | Description                                              | Type                                                                        | Default Value | Required |\n| -------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------- | ------------- | -------- |\n| type                 | Plugin type                                              | string                                                                      | `watermark`   | ✓        |\n| width                | Width of a single watermark                              | number                                                                      | 200           |          |\n| height               | Height of a single watermark                             | number                                                                      | 100           |          |\n| opacity              | Opacity of the watermark                                 | number                                                                      | 0.2           |          |\n| rotate               | Rotation angle of the watermark                          | number                                                                      | Math.PI / 12  |          |\n| imageURL             | Image watermark URL, higher priority than text watermark | string                                                                      | -             |          |\n| text                 | Watermark text content                                   | string                                                                      | -             |          |\n| textFill             | Color of the text watermark                              | string                                                                      | `#000`        |          |\n| textFontSize         | Font size of the text watermark                          | number                                                                      | 16            |          |\n| textFontFamily       | Font of the text watermark                               | string                                                                      | -             |          |\n| textFontWeight       | Font weight of the text watermark                        | string                                                                      | -             |          |\n| textFontVariant      | Font variant of the text watermark                       | string                                                                      | -             |          |\n| textAlign            | Text alignment of the watermark                          | `center` \\| `end` \\| `left` \\| `right` \\| `start`                           | `center`      |          |\n| textBaseline         | Baseline alignment of the text watermark                 | `alphabetic` \\| `bottom` \\| `hanging` \\| `ideographic` \\| `middle` \\| `top` | `middle`      |          |\n| backgroundRepeat     | Repeat mode of the watermark                             | string                                                                      | `repeat`      |          |\n| backgroundAttachment | Background attachment behavior of the watermark          | string                                                                      | -             |          |\n| backgroundBlendMode  | Background blend mode of the watermark                   | string                                                                      | -             |          |\n| backgroundClip       | Background clip of the watermark                         | string                                                                      | -             |          |\n| backgroundColor      | Background color of the watermark                        | string                                                                      | -             |          |\n| backgroundImage      | Background image of the watermark                        | string                                                                      | -             |          |\n| backgroundOrigin     | Background origin of the watermark                       | string                                                                      | -             |          |\n| backgroundPosition   | Background position of the watermark                     | string                                                                      | -             |          |\n| backgroundPositionX  | Horizontal position of the watermark background          | string                                                                      | -             |          |\n| backgroundPositionY  | Vertical position of the watermark background            | string                                                                      | -             |          |\n| backgroundSize       | Background size of the watermark                         | string                                                                      | -             |          |\n\n## Code Examples\n\n### Text Watermark\n\nThe simplest text watermark configuration:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'watermark',\n      text: 'G6 Graph',\n    },\n  ],\n});\n```\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'watermark',\n      text: 'G6: Graph Visualization',\n      textFontSize: 14,\n      textFontFamily: 'Microsoft YaHei',\n      fill: 'rgba(0, 0, 0, 0.1)',\n      rotate: Math.PI / 12,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### Image Watermark\n\nUse an image as a watermark:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'watermark',\n      imageURL: 'https://example.com/logo.png',\n      width: 100,\n      height: 50,\n      opacity: 0.1,\n    },\n  ],\n});\n```\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'watermark',\n      width: 200,\n      height: 100,\n      rotate: Math.PI / 12,\n      imageURL: 'https://gw.alipayobjects.com/os/s/prod/antv/assets/image/logo-with-text-73b8a.svg',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### Custom Styles\n\nYou can customize the style and position of the watermark:\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'watermark',\n      text: 'G6 Graph',\n      textFontSize: 20, // Set font size\n      textFontFamily: 'Arial', // Set font\n      textFontWeight: 'bold', // Set font weight\n      textFill: '#1890ff', // Set text color\n      rotate: Math.PI / 6, // Set rotation angle\n      opacity: 0.15, // Set opacity\n      width: 180, // Set watermark width\n      height: 100, // Set watermark height\n      backgroundRepeat: 'space', // Set repeat mode\n      backgroundPosition: 'center', // Set position\n      textAlign: 'center', // Set text alignment\n      textBaseline: 'middle', // Set baseline alignment\n    },\n  ],\n});\n```\n\n## Real Cases\n\n- [Text Watermark](/examples/plugin/watermark/#text)\n- [Image Watermark](/examples/plugin/watermark/#repeat)\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/Watermark.zh.md",
    "content": "---\ntitle: 水印 Watermark\norder: 17\n---\n\n## 概述\n\n水印插件支持使用文本和图片作为水印，实现原理是在 Graph 容器的 div 上加上 `background-image` 属性，然后通过 CSS 来控制水印的位置和样式。对于文本水印，会使用隐藏 canvas 将文本转换为图片的方式来实现。\n\n## 使用场景\n\n- 为图表添加版权或所有权标识\n- 在演示或预览时标记图表的状态\n- 为敏感数据添加防泄露标记\n\n## 基本用法\n\n以下是一个简单的 Watermark 插件初始化示例：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'watermark',\n      text: 'G6 Graph', // 水印文本\n      opacity: 0.2, // 透明度\n      rotate: Math.PI / 12, // 旋转角度\n    },\n  ],\n});\n```\n\n## 在线体验\n\n<embed src=\"@/common/api/plugins/watermark.md\"></embed>\n\n## 配置项\n\n| 属性                 | 描述                               | 类型                                                                        | 默认值       | 必选 |\n| -------------------- | ---------------------------------- | --------------------------------------------------------------------------- | ------------ | ---- |\n| type                 | 插件类型                           | string                                                                      | `watermark`  | ✓    |\n| width                | 单个水印的宽度                     | number                                                                      | 200          |      |\n| height               | 单个水印的高度                     | number                                                                      | 100          |      |\n| opacity              | 水印的透明度                       | number                                                                      | 0.2          |      |\n| rotate               | 水印的旋转角度                     | number                                                                      | Math.PI / 12 |      |\n| imageURL             | 图片水印的地址，优先级高于文本水印 | string                                                                      | -            |      |\n| text                 | 水印文本内容                       | string                                                                      | -            |      |\n| textFill             | 文本水印的颜色                     | string                                                                      | `#000`       |      |\n| textFontSize         | 文本水印的字体大小                 | number                                                                      | 16           |      |\n| textFontFamily       | 文本水印的字体                     | string                                                                      | -            |      |\n| textFontWeight       | 文本水印的字体粗细                 | string                                                                      | -            |      |\n| textFontVariant      | 文本水印的字体变体                 | string                                                                      | -            |      |\n| textAlign            | 文本水印的对齐方式                 | `center` \\| `end` \\| `left` \\| `right` \\| `start`                           | `center`     |      |\n| textBaseline         | 文本水印的基线对齐方式             | `alphabetic` \\| `bottom` \\| `hanging` \\| `ideographic` \\| `middle` \\| `top` | `middle`     |      |\n| backgroundRepeat     | 水印的重复方式                     | string                                                                      | `repeat`     |      |\n| backgroundAttachment | 水印的背景定位行为                 | string                                                                      | -            |      |\n| backgroundBlendMode  | 水印的背景混合模式                 | string                                                                      | -            |      |\n| backgroundClip       | 水印的背景裁剪                     | string                                                                      | -            |      |\n| backgroundColor      | 水印的背景颜色                     | string                                                                      | -            |      |\n| backgroundImage      | 水印的背景图片                     | string                                                                      | -            |      |\n| backgroundOrigin     | 水印的背景原点                     | string                                                                      | -            |      |\n| backgroundPosition   | 水印的背景位置                     | string                                                                      | -            |      |\n| backgroundPositionX  | 水印的背景水平位置                 | string                                                                      | -            |      |\n| backgroundPositionY  | 水印的背景垂直位置                 | string                                                                      | -            |      |\n| backgroundSize       | 水印的背景大小                     | string                                                                      | -            |      |\n\n## 代码示例\n\n### 文本水印\n\n最简单的文本水印配置：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'watermark',\n      text: 'G6 Graph',\n    },\n  ],\n});\n```\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'watermark',\n      text: 'G6: Graph Visualization',\n      textFontSize: 14,\n      textFontFamily: 'Microsoft YaHei',\n      fill: 'rgba(0, 0, 0, 0.1)',\n      rotate: Math.PI / 12,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### 图片水印\n\n使用图片作为水印：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'watermark',\n      imageURL: 'https://example.com/logo.png',\n      width: 100,\n      height: 50,\n      opacity: 0.1,\n    },\n  ],\n});\n```\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'watermark',\n      width: 200,\n      height: 100,\n      rotate: Math.PI / 12,\n      imageURL: 'https://gw.alipayobjects.com/os/s/prod/antv/assets/image/logo-with-text-73b8a.svg',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n### 自定义样式\n\n可以自定义水印的样式和位置：\n\n```js\nconst graph = new Graph({\n  plugins: [\n    {\n      type: 'watermark',\n      text: 'G6 Graph',\n      textFontSize: 20, // 设置字体大小\n      textFontFamily: 'Arial', // 设置字体\n      textFontWeight: 'bold', // 设置字体粗细\n      textFill: '#1890ff', // 设置文字颜色\n      rotate: Math.PI / 6, // 设置旋转角度\n      opacity: 0.15, // 设置透明度\n      width: 180, // 设置水印宽度\n      height: 100, // 设置水印高度\n      backgroundRepeat: 'space', // 设置重复方式\n      backgroundPosition: 'center', // 设置位置\n      textAlign: 'center', // 设置文本对齐\n      textBaseline: 'middle', // 设置基线对齐\n    },\n  ],\n});\n```\n\n## 实际案例\n\n- [文本水印](/examples/plugin/watermark/#text)\n- [图片水印](/examples/plugin/watermark/#repeat)\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/custom-plugin.en.md",
    "content": "---\ntitle: Custom Plugin\norder: 20\n---\n\n## Overview\n\nCustom plugins can implement additional features, such as adding extra components, rendering logic, etc. Custom plugins can effectively achieve functional decoupling, better manage and orchestrate code, and facilitate subsequent maintenance.\n\n## Use Cases\n\n### Add extra components, rendering logic, etc.\n\n- **Extra components**: Such as built-in plugins like `Tooltip`, `Minimap`, `Snapline`, `Grid`, `Context Menu`, `Watermark`, etc.\n- **Rendering logic**: Such as built-in plugins like `Edge Bundling`, and `Remote Data Loading` ([Example](#remote-data-loading)), etc.\n\n### When built-in plugins cannot meet the requirements\n\nWhen built-in plugins cannot fully meet business needs, users can also make adjustments and modifications through custom plugins (inheriting built-in plugins).\n\n_(If the features supported by built-in plugins are more general, or if there are bugs in built-in plugins, you are welcome to submit issues or PRs on [Github](https://github.com/antvis/G6))_\n\n## Custom Plugin Examples\n\nLike interactions, the implementation of plugins is also quite flexible, and you can implement your plugin in your preferred style.\n\nHere are a few simple custom plugin implementations:\n\n### Remote Data Loading\n\nAutomatically load remote data during graph instantiation:\n\n```typescript\nimport { BasePlugin } from '@antv/g6';\nimport type { BasePluginOptions, RuntimeContext } from '@antv/g6';\n\ninterface RemoteDataSourceOptions extends BasePluginOptions {}\n\nclass RemoteDataSource extends BasePlugin<RemoteDataSourceOptions> {\n  constructor(context: RuntimeContext, options: RemoteDataSourceOptions) {\n    super(context, options);\n    this.loadData();\n  }\n\n  private async loadData() {\n    // mock remote data\n    const data = {\n      nodes: [\n        { id: 'node-1', x: 100, y: 100 },\n        { id: 'node-2', x: 200, y: 200 },\n      ],\n      edges: [{ source: 'node-1', target: 'node-2' }],\n    };\n\n    const { graph } = this.context;\n    graph.setData(data);\n    await graph.render();\n  }\n}\n```\n\n- In this example, we simulate a data loading plugin. After using this plugin, there is no need to pass data when instantiating the Graph, as the plugin will automatically load remote data.\n\n- `BasePlugin` is the base class for all plugins, and each custom plugin needs to inherit this base class.\n\n<embed src=\"@/common/manual/custom-extension/plugin/implement-plugin.md\"></embed>\n\n### Automatically Enable or Disable Animation Based on Node Count\n\n```typescript\nimport type { BasePluginOptions, RuntimeContext } from '@antv/g6';\nimport { BasePlugin, GraphEvent } from '@antv/g6';\n\ninterface AutoSwitchAnimationOptions extends BasePluginOptions {\n  maxLength: number; // Disable global animation when the number of nodes reaches this value\n}\n\nclass AutoSwitchAnimation extends BasePlugin<AutoSwitchAnimationOptions> {\n  static defaultOptions: Partial<AutoSwitchAnimationOptions> = {\n    maxLength: 1000,\n  };\n  constructor(context: RuntimeContext, options: AutoSwitchAnimationOptions) {\n    super(context, options);\n    this.bindEvents();\n  }\n  private bindEvents() {\n    const { graph } = this.context;\n    graph.on(GraphEvent.BEFORE_RENDER, this.switchAnimation);\n  }\n  private switchAnimation() {\n    const { graph } = this.context;\n    graph.setOptions({\n      animation: graph.getNodeData().length < this.options.maxLength,\n    });\n  }\n  private unbindEvents() {\n    const { graph } = this.context;\n    graph.off(GraphEvent.BEFORE_RENDER, this.switchAnimation);\n  }\n  destroy() {\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n```\n\n- In this example, we listen to the `GraphEvent.BEFORE_RENDER` event and determine whether the current number of nodes exceeds a specified value. If so, global animation is disabled; otherwise, it is enabled.\n- `maxLength` is a defined configuration item that can be passed in when initializing the graph instance. [Plugin Configuration](#configure-plugin)\n\n## Register Plugin\n\nRegister using the method provided by G6\n\n```typescript\nimport { register, ExtensionCategory } from '@antv/g6';\nimport { MyCustomPlugin } from './my-custom-plugin';\n\nregister(ExtensionCategory.PLUGIN, 'my-custom-plugin', MyCustomPlugin);\n```\n\n## Configure Plugin\n\n- You can pass the plugin type name or configuration parameter object in `plugins`, see [Configure Plugin](/manual/plugin/overview#configuration-method)\n\n- For example, the previous [Automatically Enable or Disable Animation Based on Node Count](#automatically-enable-or-disable-animation-based-on-node-count) is configured as follows:\n\n  ```typescript\n  const graph = new Graph({\n    // Other configurations\n    plugins: [\n      {\n        type: 'auto-switch-animation',\n        maxLength: 500,\n      },\n    ],\n  });\n  ```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/custom-plugin.zh.md",
    "content": "---\ntitle: 自定义插件\norder: 20\n---\n\n## 概述\n\n自定义插件可以实现一些额外的功能，例如添加额外的组件、渲染逻辑等。通过自定义插件可以很好地实现功能解耦，更好地进行管理、编排代码以及后续维护。\n\n## 使用场景\n\n### 添加额外的组件、渲染逻辑等\n\n- **额外的组件**：如内置插件的`提示框`、`小地图`、`对齐线`、`网格线`、`上下文菜单`、`水印`等\n- **渲染逻辑**：如内置插件的`边绑定`，以及`远程数据加载`（[示例](#远程数据加载)）等\n\n### 内置插件无法满足需求\n\n当内置插件无法完全满足业务需求时，用户也可以通过自定义插件（继承内置插件）进行调整和修改。\n\n_（如果需要内置插件支持的特性是较通用的，或者内置插件存在 Bug ，这种时候欢迎大家到 [Github](https://github.com/antvis/G6) 提 Issue 或者 PR ）_\n\n## 自定义插件示例\n\n与交互类似，插件的实现也相当灵活，你可以以你喜欢的风格实现你的插件。\n\n下面列举几个简单的自定义插件实现：\n\n### 远程数据加载\n\n在图实例化过程中自动加载远程数据：\n\n```typescript\nimport { BasePlugin } from '@antv/g6';\nimport type { BasePluginOptions, RuntimeContext } from '@antv/g6';\n\ninterface RemoteDataSourceOptions extends BasePluginOptions {}\n\nclass RemoteDataSource extends BasePlugin<RemoteDataSourceOptions> {\n  constructor(context: RuntimeContext, options: RemoteDataSourceOptions) {\n    super(context, options);\n    this.loadData();\n  }\n\n  private async loadData() {\n    // mock remote data\n    const data = {\n      nodes: [\n        { id: 'node-1', x: 100, y: 100 },\n        { id: 'node-2', x: 200, y: 200 },\n      ],\n      edges: [{ source: 'node-1', target: 'node-2' }],\n    };\n\n    const { graph } = this.context;\n    graph.setData(data);\n    await graph.render();\n  }\n}\n```\n\n- 在这个例子中，我们模拟实现了一个数据加载插件，在使用该插件后，实例化 Graph 时不用再传入数据，该插件会自动加载远程数据。\n\n- `BasePlugin` 是所有插件的基类，每个自定义插件都需要继承这个基类实现。\n\n<embed src=\"@/common/manual/custom-extension/plugin/implement-plugin.md\"></embed>\n\n### 自动判断节点数量开启或关闭动画\n\n```typescript\nimport type { BasePluginOptions, RuntimeContext } from '@antv/g6';\nimport { BasePlugin, GraphEvent } from '@antv/g6';\n\ninterface AutoSwitchAnimationOptions extends BasePluginOptions {\n  maxLength: number; // 节点数量达到这个值后关闭全局动画\n}\n\nclass AutoSwitchAnimation extends BasePlugin<AutoSwitchAnimationOptions> {\n  static defaultOptions: Partial<AutoSwitchAnimationOptions> = {\n    maxLength: 1000,\n  };\n  constructor(context: RuntimeContext, options: AutoSwitchAnimationOptions) {\n    super(context, options);\n    this.bindEvents();\n  }\n  private bindEvents() {\n    const { graph } = this.context;\n    graph.on(GraphEvent.BEFORE_RENDER, this.switchAnimation);\n  }\n  private switchAnimation() {\n    const { graph } = this.context;\n    graph.setOptions({\n      animation: graph.getNodeData().length < this.options.maxLength,\n    });\n  }\n  private unbindEvents() {\n    const { graph } = this.context;\n    graph.off(GraphEvent.BEFORE_RENDER, this.switchAnimation);\n  }\n  destroy() {\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n```\n\n- 在这个例子中，我们监听 `GraphEvent.BEFORE_RENDER` 事件，在响应中判断当前节点数量是否大于指定值，是的话则关闭全局动画，否则开启\n- `maxLength` 是定义的配置项，可在初始化画布实例时传入具体配置，[插件配置](#配置插件)\n\n## 注册插件\n\n通过 G6 提供的 register 方法注册即可\n\n```typescript\nimport { register, ExtensionCategory } from '@antv/g6';\nimport { MyCustomPlugin } from './my-custom-plugin';\n\nregister(ExtensionCategory.PLUGIN, 'my-custom-plugin', MyCustomPlugin);\n```\n\n## 配置插件\n\n- 可在 `plugins` 中传入插件类型名称或配置参数对象，详见[配置插件](/manual/plugin/overview#配置方式)\n\n- 比如前面的[自动判断节点数量开启或关闭动画](#自动判断节点数量开启或关闭动画)，配置如下：\n\n  ```typescript\n  const graph = new Graph({\n    // 其他配置\n    plugins: [\n      {\n        type: 'auto-switch-animation',\n        maxLength: 500,\n      },\n    ],\n  });\n  ```\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/overview.en.md",
    "content": "---\ntitle: Plugin Overview\norder: 0\n---\n\n## What is a Plugin\n\n<image width=\"200px\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*sa3jRqp83K4AAAAAAAAAAAAADmJ7AQ/original\" />\n\nA Plugin is the most flexible extension mechanism in G6, allowing users to extend G6's functionality, such as adding graphical components to the canvas or implementing undo/redo features.\n\nMost customization needs can be achieved through plugins. G6 comes with some built-in plugins, such as: [Tooltip](/en/manual/plugin/tooltip), [Grid](/en/manual/plugin/grid-line), [History](/en/manual/plugin/history).\n\n## Built-in Plugins\n\nG6 provides a rich set of built-in plugins covering various common functional scenarios:\n\n| Category                     | Plugin Name                                                                                         | Registration Type  | Description                                                            |\n| ---------------------------- | --------------------------------------------------------------------------------------------------- | ------------------ | ---------------------------------------------------------------------- |\n| **Visual Style Enhancement** |                                                                                                     |                    |                                                                        |\n|                              | [Grid Line](/en/manual/plugin/grid-line)                                                            | `grid-line`        | Displays grid reference lines on the canvas                            |\n|                              | [Background](/en/manual/plugin/background)                                                          | `background`       | Adds background images or colors to the canvas                         |\n|                              | [Watermark](/en/manual/plugin/watermark)                                                            | `watermark`        | Adds a watermark to the canvas to protect copyright                    |\n|                              | [Hull](/en/manual/plugin/hull)                                                                      | `hull`             | Creates an outline for a specified set of nodes                        |\n|                              | [Bubble Sets](/en/manual/plugin/bubble-sets)                                                        | `bubble-sets`      | Creates smooth bubble-like element outlines                            |\n|                              | [Snapline](/en/manual/plugin/snapline)                                                              | `snapline`         | Displays alignment reference lines when dragging elements              |\n| **Navigation and Overview**  |                                                                                                     |                    |                                                                        |\n|                              | [Minimap](/en/manual/plugin/minimap)                                                                | `minimap`          | Displays a thumbnail preview of the graph, supporting navigation       |\n|                              | [Fullscreen](/en/manual/plugin/fullscreen)                                                          | `fullscreen`       | Supports full-screen display and exit for charts                       |\n|                              | [Timebar](/en/manual/plugin/timebar)                                                                | `timebar`          | Provides filtering and playback control for temporal data              |\n| **Interactive Controls**     |                                                                                                     |                    |                                                                        |\n|                              | [Toolbar](/en/manual/plugin/toolbar)                                                                | `toolbar`          | Provides a collection of common operation buttons                      |\n|                              | [Context Menu](/en/manual/plugin/contextmenu)                                                       | `contextmenu`      | Displays a menu of selectable operations on right-click                |\n|                              | [Tooltip](/en/manual/plugin/tooltip)                                                                | `tooltip`          | Displays detailed information about elements on hover                  |\n|                              | [Legend](/en/manual/plugin/legend)                                                                  | `legend`           | Displays categories and corresponding style descriptions of chart data |\n| **Data Exploration**         |                                                                                                     |                    |                                                                        |\n|                              | [Fisheye](/en/manual/plugin/fisheye)                                                                | `fisheye`          | Provides a focus + context exploration experience                      |\n|                              | [Edge Filter Lens](/en/manual/plugin/edge-filter-lens)                                              | `edge-filter-lens` | Filters and displays edges within a specified area                     |\n|                              | [Edge Bundling](/en/manual/plugin/edge-bundling)                                                    | `edge-bundling`    | Bundles edges with similar paths together to reduce visual clutter     |\n| **Advanced Features**        |                                                                                                     |                    |                                                                        |\n|                              | [History](/en/manual/plugin/history)                                                                | `history`          | Supports undo/redo operations                                          |\n|                              | [Camera Setting](/enhttps://github.com/antvis/G6/blob/v5/packages/g6/src/plugins/camera-setting.ts) | `camera-setting`   | Configures camera parameters in a 3D scene                             |\n\nFor detailed configuration of each plugin, refer to the [Built-in Plugin Documentation](/en/manual/plugin/grid-line).\n\n## Configuration Methods\n\n### Basic Configuration\n\nSpecify the required plugins through the `plugins` array when initializing the graph instance:\n\n```javascript {}5\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  // Other configurations...\n  plugins: ['grid', 'minimap', 'tooltip'],\n});\n```\n\n### Configuring Plugin Parameters\n\nFor plugins that require custom parameters, you can configure properties using the `object` form:\n\n```javascript {5-9}\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    'grid',\n    {\n      type: 'tooltip',\n      key: 'my-tooltip', // Specify a key for the plugin for future updates\n      getContent: (e) => `<div>Node: ${e.target.id}</div>`,\n    },\n  ],\n});\n```\n\n### Dynamically Updating Plugins\n\nG6 supports dynamic management of plugins during the runtime of the graph instance to meet complex interaction needs:\n\nUse the [getPlugins](/en/api/plugin#graphgetplugins) method to get the current list of plugins:\n\n```javascript\n// Get the list of plugins\nconst plugins = graph.getPlugins();\n// console.log(plugins) 👉 ['minimap', 'grid']\n```\n\nYou can adjust plugins using the [setPlugins](/en/api/plugin#graphsetpluginsplugins) method:\n\n```javascript\n// Add a new plugin\ngraph.setPlugins((plugins) => [...plugins, 'minimap']);\n\n// Remove a plugin\ngraph.setPlugins((plugins) => plugins.filter((p) => p !== 'grid'));\n```\n\nYou can update the configuration of a plugin using the [updatePlugin](/en/api/plugin#graphupdatepluginplugin) method:\n\n```javascript {6,14}\nconst graph = new Graph({\n  // Other configurations...\n  plugins: [\n    {\n      type: 'tooltip',\n      key: 'my-tooltip',\n      getContent: (e) => `<div>Node: ${e.target.id}</div>`,\n    },\n  ],\n});\n\n// Update a single plugin\ngraph.updatePlugin({\n  key: 'my-tooltip',\n  getContent: (e) => `<div>Updated content: ${e.target.id}</div>`,\n});\n```\n\n:::warning{title=Note}\nWhen using the `updatePlugin` method, you need to specify a unique `key` for the plugin during initialization.\n:::\n\n### Uninstalling Plugins\n\nUse the [setPlugins](/en/api/plugin#graphsetpluginsplugins) method to uninstall plugins by setting the plugin configuration list to empty:\n\n```javascript\n// Uninstall all plugins\ngraph.setPlugins([]);\n```\n\n### Calling Plugin Methods\n\nSome plugins provide API methods for users to call, such as the `history` plugin providing `undo` and `redo` methods, allowing users to implement undo and redo operations by calling these methods.\n\nTo call these methods, you need to first get the plugin instance, which can be obtained through the [getPluginInstance](/en/api/plugin#graphgetplugininstancekey) method:\n\n```javascript\n// Configure the plugin\nconst graph = new Graph({\n  plugins: [{ type: 'history', key: 'my-history' }],\n});\n\n// Get the plugin instance\nconst history = graph.getPluginInstance('my-history');\n\n// Call plugin methods\nhistory.undo();\nhistory.redo();\n```\n\n:::warning{title=Note}\nThe `graph.getPluginInstance` method takes the plugin key value as a parameter, so if you need to get the plugin instance, you need to configure the corresponding plugin in the form of an `object` and pass in the `key` value.\n:::\n\nFor more plugin-related APIs, please refer to the [Plugin API Documentation](/en/api/plugin).\n\n## Custom Plugins\n\nWhen built-in plugins cannot meet your needs, you can:\n\n- Inherit and extend existing plugins\n- Create brand new custom plugins\n\nCustom plugins need to be registered before use. For detailed tutorials, please refer to the [Custom Plugin](/en/manual/plugin/custom-plugin) documentation.\n\n```javascript\nimport { register, ExtensionCategory } from '@antv/g6';\nimport { MyCustomPlugin } from './my-custom-plugin';\n\n// Register custom plugin\nregister(ExtensionCategory.PLUGIN, 'my-custom-plugin', MyCustomPlugin);\n\n// Use custom plugin\nconst graph = new Graph({\n  plugins: ['my-custom-plugin'],\n});\n```\n\nBy reasonably combining and configuring plugins, you can build graph visualization applications with rich features and excellent interactive experiences.\n"
  },
  {
    "path": "packages/site/docs/manual/plugin/overview.zh.md",
    "content": "---\ntitle: 插件总览\norder: 0\n---\n\n## 什么是插件\n\n<image width=\"200px\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*sa3jRqp83K4AAAAAAAAAAAAADmJ7AQ/original\" />\n\n插件(Plugin)是 G6 中最灵活的扩展机制，用户可以通过插件来扩展 G6 的功能，例如在画布中额外挂载图形组件、实现撤销重做等功能。\n\n绝大多数的定制需求都可以通过插件来实现，G6 内置了一些常用的插件，例如：[Tooltip](/manual/plugin/tooltip)、[Grid](/manual/plugin/grid-line)、[History](/manual/plugin/history)。\n\n## 内置插件\n\nG6 提供了丰富的内置插件，涵盖多种常见功能场景：\n\n| 分类             | 插件名称                                                                                   | 注册类型           | 功能描述                               |\n| ---------------- | ------------------------------------------------------------------------------------------ | ------------------ | -------------------------------------- |\n| **视觉样式增强** |                                                                                            |                    |                                        |\n|                  | [网格线](/manual/plugin/grid-line)                                                         | `grid-line`        | 在画布上显示网格参考线                 |\n|                  | [背景](/manual/plugin/background)                                                          | `background`       | 为画布添加背景图片或颜色               |\n|                  | [水印](/manual/plugin/watermark)                                                           | `watermark`        | 为画布添加水印，保护版权               |\n|                  | [轮廓包围](/manual/plugin/hull)                                                            | `hull`             | 为指定节点集合创建轮廓                 |\n|                  | [气泡集](/manual/plugin/bubble-sets)                                                       | `bubble-sets`      | 创建平滑气泡状的元素集合轮廓           |\n|                  | [对齐线](/manual/plugin/snapline)                                                          | `snapline`         | 拖动元素时显示对齐参考线               |\n| **导航与概览**   |                                                                                            |                    |                                        |\n|                  | [缩略图](/manual/plugin/minimap)                                                           | `minimap`          | 显示图的缩略预览，支持导航             |\n|                  | [全屏](/manual/plugin/fullscreen)                                                          | `fullscreen`       | 支持图表全屏显示和退出                 |\n|                  | [时间轴](/manual/plugin/timebar)                                                           | `timebar`          | 提供时序数据的筛选和播放控制           |\n| **交互控件**     |                                                                                            |                    |                                        |\n|                  | [工具栏](/manual/plugin/toolbar)                                                           | `toolbar`          | 提供常用操作按钮集合                   |\n|                  | [上下文菜单](/manual/plugin/contextmenu)                                                   | `contextmenu`      | 右键点击时显示可选操作菜单             |\n|                  | [提示框](/manual/plugin/tooltip)                                                           | `tooltip`          | 悬停时显示元素详细信息                 |\n|                  | [图例](/manual/plugin/legend)                                                              | `legend`           | 显示图表数据的类别和对应样式说明       |\n| **数据探索**     |                                                                                            |                    |                                        |\n|                  | [鱼眼放大镜](/manual/plugin/fisheye)                                                       | `fisheye`          | 提供焦点+上下文的探索体验              |\n|                  | [边过滤镜](/manual/plugin/edge-filter-lens)                                                | `edge-filter-lens` | 在指定区域内筛选显示边                 |\n|                  | [边绑定](/manual/plugin/edge-bundling)                                                     | `edge-bundling`    | 将相似路径的边捆绑在一起，减少视觉混乱 |\n| **高级功能**     |                                                                                            |                    |                                        |\n|                  | [历史记录](/manual/plugin/history)                                                         | `history`          | 支持撤销/重做操作                      |\n|                  | [相机设置](https://github.com/antvis/G6/blob/v5/packages/g6/src/plugins/camera-setting.ts) | `camera-setting`   | 配置3D场景下的相机参数                 |\n\n各插件的详细配置可参考 [内置插件文档](/manual/plugin/grid-line)。\n\n## 配置方式\n\n### 基本配置\n\n在图实例初始化时，通过 `plugins` 数组指定需要的插件：\n\n```javascript {}5\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  // 其他配置...\n  plugins: ['grid', 'minimap', 'tooltip'],\n});\n```\n\n### 配置插件参数\n\n对于需要自定义参数的插件，可以使用 `object` 形式配置属性：\n\n```javascript {5-9}\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    'grid',\n    {\n      type: 'tooltip',\n      key: 'my-tooltip', // 为插件指定key，便于后续更新\n      getContent: (e) => `<div>节点：${e.target.id}</div>`,\n    },\n  ],\n});\n```\n\n### 动态更新插件\n\nG6 支持在图实例运行期间动态管理插件，满足复杂交互需求：\n\n通过 [getPlugins](/api/plugin#graphgetplugins) 方法获取当前插件列表：\n\n```javascript\n// 获取插件列表\nconst plugins = graph.getPlugins();\n// console.log(plugins) 👉 ['minimap', 'grid']\n```\n\n可以通过 [setPlugins](/api/plugin#graphsetpluginsplugins) 方法调整插件：\n\n```javascript\n// 添加新插件\ngraph.setPlugins((plugins) => [...plugins, 'minimap']);\n\n// 移除插件\ngraph.setPlugins((plugins) => plugins.filter((p) => p !== 'grid'));\n```\n\n可以通过 [updatePlugin](/api/plugin#graphupdatepluginplugin) 方法更新插件的配置：\n\n```javascript {6,14}\nconst graph = new Graph({\n  // 其他配置...\n  plugins: [\n    {\n      type: 'tooltip',\n      key: 'my-tooltip',\n      getContent: (e) => `<div>节点：${e.target.id}</div>`,\n    },\n  ],\n});\n\n// 更新单个插件\ngraph.updatePlugin({\n  key: 'my-tooltip',\n  getContent: (e) => `<div>更新的内容：${e.target.id}</div>`,\n});\n```\n\n:::warning{title=注意}\n使用 `updatePlugin` 方法时，需要在初始化时为插件指定唯一的 `key`。\n:::\n\n### 卸载插件\n\n使用 [setPlugins](/api/plugin#graphsetpluginsplugins) 方法同样可以卸载插件，将插件配置列表置为空即可：\n\n```javascript\n// 卸载所有插件\ngraph.setPlugins([]);\n```\n\n### 调用插件方法\n\n一些插件提供了可供用户调用的 API 方法，例如 `history` 插件提供了 `undo` 和 `redo` 方法，用户可以通过调用这些方法来实现撤销和重做操作。\n\n要调用这些方法，需要先获取到插件实例，可通过 [getPluginInstance](/api/plugin#graphgetplugininstancekey) 方法获取：\n\n```javascript\n// 配置插件\nconst graph = new Graph({\n  plugins: [{ type: 'history', key: 'my-history' }],\n});\n\n// 获取插件实例\nconst history = graph.getPluginInstance('my-history');\n\n// 调用插件方法\nhistory.undo();\nhistory.redo();\n```\n\n:::warning{title=注意}\n`graph.getPluginInstance` 方法接收插件 key 值作为参数，因此如果需要获取插件实例，需要将对应插件配置为 `object` 的形式，并传入 `key` 值。\n:::\n\n更多与插件相关的 API 请参考 [插件 API 文档](/api/plugin)。\n\n## 自定义插件\n\n当内置插件无法满足需求时，你可以：\n\n- 继承和扩展现有插件\n- 创建全新的自定义插件\n\n自定义插件需要先注册后使用。详细教程请参考 [自定义插件](/manual/plugin/custom-plugin) 文档。\n\n```javascript\nimport { register, ExtensionCategory } from '@antv/g6';\nimport { MyCustomPlugin } from './my-custom-plugin';\n\n// 注册自定义插件\nregister(ExtensionCategory.PLUGIN, 'my-custom-plugin', MyCustomPlugin);\n\n// 使用自定义插件\nconst graph = new Graph({\n  plugins: ['my-custom-plugin'],\n});\n```\n\n通过合理组合和配置插件，你可以构建出功能丰富、交互体验出色的图可视化应用。\n"
  },
  {
    "path": "packages/site/docs/manual/theme/custom-palette.en.md",
    "content": "---\ntitle: Custom Palette\norder: 4\n---\n\n## Overview\n\n[Core Concepts - Palette](/en/manual/core-concept/palette) mentions that G6 supports discrete palettes and continuous palettes. A discrete palette is essentially an array of colors, while a continuous palette is a color interpolator.\n\nTherefore, customizing a palette also adopts these two methods, and the following sections will introduce how to customize discrete and continuous palettes, respectively.\n\n## Implement Palette\n\n### Discrete Palette\n\nYou can simply define a string array that contains color values. Supported color values include: RGB color values, hexadecimal color values, and color names. Below is an example of a discrete palette:\n\n```typescript\nconst hex = ['#FF0000', '#00FF00', '#0000FF'];\n\nconst color = ['red', 'green', 'blue'];\n\nconst rgb = ['rgb(255, 0, 0)', 'rgb(0, 255, 0)', 'rgb(0, 0, 255)'];\n```\n\n### Continuous Palette\n\nA continuous palette requires the definition of a color interpolator. The interpolator is a function that accepts a numerical value as a parameter and returns a color value. Below is an example of a continuous palette:\n\n```typescript\nconst color = (value: number) => `rgb(${value * 255}, 0, 0)`;\n```\n\n## Register Palette\n\nYou can register a palette using the `register` method provided by G6. For more details, please refer to [Register Palette](/en/manual/core-concept/palette#register-palette)\n\n## Use Without Registration\n\nIn addition to registration, you can also bypass the registration mechanism and directly pass the palette value at the location where the palette is needed, for example:\n\n```typescript\n{\n  node: {\n    palette: {\n      type: 'group',\n      field: 'category',\n      color: ['#5B8FF9', '#61DDAA', '#F6BD16'], // Pass in a color array.\n    }\n  },\n  edge: {\n    palette: {\n      type: 'value',\n      field: 'value',\n      color: (value) => `rgb(${value * 255}, 0, 0)`, // Pass in an interpolator\n    }\n  }\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/theme/custom-palette.zh.md",
    "content": "---\ntitle: 自定义色板\norder: 4\n---\n\n## 概述\n\n[色板](/manual/theme/palette) 中提到，G6 支持离散色板和连续色板，其中离散色板本质上是一个颜色数组，而连续色板是一个颜色插值器。\n\n因此自定义色板也采用这两种方式，下面分别介绍如何自定义离散色板和连续色板。\n\n## 实现色板\n\n### 离散色板\n\n直接定义一个包含颜色值的字符串数组即可，颜值值支持：RGB 色值、16 进制、颜色名，下面是一组离散色板示例：\n\n```typescript\nconst hex = ['#FF0000', '#00FF00', '#0000FF'];\n\nconst color = ['red', 'green', 'blue'];\n\nconst rgb = ['rgb(255, 0, 0)', 'rgb(0, 255, 0)', 'rgb(0, 0, 255)'];\n```\n\n### 连续色板\n\n连续色板需要定义一个颜色插值器，插值器是一个函数，接受一个数值参数，返回一个颜色值，下面是一个连续色板示例：\n\n```typescript\nconst color = (value: number) => `rgb(${value * 255}, 0, 0)`;\n```\n\n## 注册色板\n\n通过 G6 提供的 register 方法注册即可，详见[注册色板](/manual/theme/palette#注册色板)\n\n## 非注册方式使用\n\n除此之外，你也可以在需要使用色板的位置跳过注册机制直接传入色板值，例如：\n\n```typescript\n{\n  node: {\n    palette: {\n      type: 'group',\n      field: 'category',\n      color: ['#5B8FF9', '#61DDAA', '#F6BD16'], // 传入颜色数组\n    }\n  },\n  edge: {\n    palette: {\n      type: 'value',\n      field: 'value',\n      color: (value) => `rgb(${value * 255}, 0, 0)`, // 传入插值器\n    }\n  }\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/theme/custom-theme.en.md",
    "content": "---\ntitle: Custom Theme\norder: 2\n---\n\n## Overview\n\nIn G6, the theme is a subset of Graph Options and includes configurations related to the canvas and element styles. A theme can help you quickly switch between different graph styles.\n\n## Custom Theme\n\nFor element styles, the configurations within a theme are static and do not support the use of callback functions to dynamically calculate styles. Additionally, `type` is also not supported for configuration within a theme. A theme includes the following configurations:\n\n- `background`: Canvas background color\n- `node`: Node style\n- `edge`: Edge style\n- `combo`: Combo style\n\nBelow is a simple example of a theme configuration:\n\n```typescript\nconst theme = {\n  background: '#fff',\n  node: {\n    style: {\n      fill: '#e1f3fe',\n      lineWidth: 0,\n    },\n    selected: {\n      style: {\n        fill: '#3b71d6',\n        lineWidth: 1,\n      },\n    },\n  },\n  edge: {\n    // ...\n  },\n  combo: {\n    // ...\n  },\n};\n```\n\n❌ Incorrect Example\n\n```typescript\nconst theme = {\n  node: {\n    // ❌ The theme does not support configuring element types\n    type: 'rect',\n    style: {\n      // ❌ The theme does not support callback functions\n      fill: (d) => d.style.color,\n    },\n  },\n};\n```\n\n:::warning{title=Note}\nFor element state styles, please ensure that every property in the state style has a corresponding default style in the default style, otherwise it may result in the inability to clear the state style.\n:::\n\n## Register Theme\n\nYou can register a theme using the `register` method provided by G6. Here is an example:\n\n```typescript\nimport { register, ExtensionCategory } from '@antv/g6';\n\nregister(ExtensionCategory.THEME, 'custom-theme', theme);\n```\n\n## Configure Theme\n\nTo enable and configure a theme, you need to pass the `theme` option when instantiating the `Graph`:\n\n```typescript\n{\n  theme: 'custom-theme',\n}\n```\n\n### Switch Theme\n\nAfter the `Graph` instance is created, you can switch themes by using the [setTheme](/en/api/theme#graphsetthemetheme) method:\n\n```typescript\ngraph.setTheme('dark');\n```\n\nAdditionally, you can also obtain the current theme by using the `getTheme` method:\n\n```typescript\ngraph.getTheme();\n// => 'dark'\n```\n"
  },
  {
    "path": "packages/site/docs/manual/theme/custom-theme.zh.md",
    "content": "---\ntitle: 自定义主题\norder: 2\n---\n\n除了使用内置主题外，G6 还支持创建自定义主题来满足特定的视觉需求。本文将介绍如何创建和使用自定义主题。\n\n## 创建自定义主题\n\n一个自定义主题需要遵循主题的基本结构，包含画布背景色和元素样式配置：\n\n```javascript\nconst customTheme = {\n  // 1. 画布背景色\n  background: '#f0f0f0',\n\n  // 2. 节点配置\n  node: {\n    // 调色板配置\n    palette: {\n      type: 'group',\n      color: ['#1783FF', '#00C9C9' /* 自定义颜色... */],\n    },\n    // 基础样式\n    style: {\n      fill: '#fff',\n      stroke: '#d9d9d9',\n      lineWidth: 1,\n      // ... 其他节点样式\n    },\n    // 状态样式\n    state: {\n      selected: {\n        fill: '#e8f3ff',\n        stroke: '#1783FF',\n      },\n      // ... 其他状态样式\n    },\n  },\n\n  // 3. 边配置\n  edge: {\n    style: {\n      stroke: '#d9d9d9',\n      lineWidth: 1,\n      // ... 其他边样式\n    },\n    state: {\n      // ... 状态样式\n    },\n  },\n\n  // 4. Combo 配置\n  combo: {\n    style: {\n      fill: '#f7f7f7',\n      stroke: '#d9d9d9',\n      // ... 其他 Combo 样式\n    },\n    state: {\n      // ... 状态样式\n    },\n  },\n};\n```\n\n## 使用限制\n\n在创建自定义主题时，需要注意以下限制：\n\n1. **仅支持静态值**\n\n   ```javascript\n   // ❌ 错误示例：不支持回调函数\n   const theme = {\n     node: {\n       style: {\n         fill: (d) => d.style.color,\n       },\n     },\n   };\n   ```\n\n2. **不支持配置元素类型**\n\n   ```javascript\n   // ❌ 错误示例：不支持在主题中配置元素类型\n   const theme = {\n     node: {\n       type: 'rect',\n       style: {\n         fill: '#fff',\n       },\n     },\n   };\n   ```\n\n3. **状态样式需要对应默认样式**\n   ```javascript\n   // ✅ 正确示例：状态样式的属性在默认样式中都有定义\n   const theme = {\n     node: {\n       style: {\n         fill: '#fff',\n         stroke: '#000',\n       },\n       state: {\n         selected: {\n           fill: '#e8f3ff',\n           stroke: '#1783FF',\n         },\n       },\n     },\n   };\n   ```\n\n## 应用自定义主题\n\n先注册主题，然后通过名称引用：\n\n```javascript\n// 1. 注册主题\nimport { register, ExtensionCategory } from '@antv/g6';\nregister(ExtensionCategory.THEME, 'custom-theme', customTheme);\n\n// 2. 使用主题\nconst graph = new Graph({\n  theme: 'custom-theme',\n  // ... 其他配置\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/theme/overview.en.md",
    "content": "---\ntitle: Theme Overview\norder: 1\n---\n\n## Overview\n"
  },
  {
    "path": "packages/site/docs/manual/theme/overview.zh.md",
    "content": "---\ntitle: 主题总览\norder: 1\n---\n\n## 概述\n\nG6 中的主题是 Graph Options 的子集，它包含了关于画布和元素样式的配置。多主题可以帮助你快速地切换不同的图样式。\n\n<image width=\"350\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*gASzQbsbAaIAAAAAAAAAAAAADmJ7AQ/original\"></image>\n\n## 主题结构\n\n一个主题由以下四个部分组成：\n\n1. **画布背景色 (background)**\n\n   - 控制整个画布的背景颜色\n\n2. **节点配置 (node)**\n\n   - 基础样式：填充色、描边、标签等静态视觉属性\n   - [调色板](/manual/theme/palette)：用于节点分组的颜色配置\n   - 状态样式：不同状态下的样式配置（选中、激活、禁用等）\n   - 动画配置：节点的动画效果配置\n\n3. **边配置 (edge)**\n\n   - 基础样式：线条样式、箭头、标签等静态视觉属性\n   - [调色板](/manual/theme/palette)：用于边分组的颜色配置\n   - 状态样式：不同状态下的样式配置\n   - 动画配置：边的动画效果配置\n\n4. **Combo 配置 (combo)**\n   - 基础样式：填充、描边、折叠按钮等静态视觉属性\n   - 状态样式：不同状态下的样式配置\n   - 动画配置：Combo 的动画效果配置\n\n> 注意：主题中的样式配置仅支持静态值，不支持回调函数形式的动态配置。如需动态样式，请使用图的配置项。\n\n## 内置主题\n\nG6 默认提供两种内置主题：\n\n### 亮色主题（默认）\n\n<img width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*SPCES62UzzAAAAAAAAAAAAAAemJ7AQ/original\" alt=\"亮色主题\" />\n\n<details><summary>查看亮色主题完整配置项</summary>\n\n```js\nconst lightTheme = {\n  background: '#ffffff',\n  node: {\n    palette: {\n      type: 'group',\n      color: [\n        '#1783FF',\n        '#00C9C9',\n        '#F08F56',\n        '#D580FF',\n        '#7863FF',\n        '#DB9D0D',\n        '#60C42D',\n        '#FF80CA',\n        '#2491B3',\n        '#17C76F',\n      ],\n    },\n    style: {\n      donutOpacity: 1,\n      badgeBackgroundOpacity: 1,\n      badgeFill: '#fff',\n      badgeFontSize: 8,\n      badgePadding: [0, 4],\n      badgePalette: ['#7E92B5', '#F4664A', '#FFBE3A'],\n      fill: '#1783ff',\n      fillOpacity: 1,\n      halo: false,\n      iconFill: '#fff',\n      iconOpacity: 1,\n      labelBackground: false,\n      labelBackgroundFill: '#ffffff',\n      labelBackgroundLineWidth: 0,\n      labelBackgroundOpacity: 0.75,\n      labelFill: '#000000',\n      labelFillOpacity: 0.85,\n      labelLineHeight: 16,\n      labelPadding: [0, 2],\n      labelFontSize: 12,\n      labelFontWeight: 400,\n      labelOpacity: 1,\n      labelOffsetY: 2,\n      lineWidth: 0,\n      portFill: '#1783ff',\n      portLineWidth: 1,\n      portStroke: '#000000',\n      portStrokeOpacity: 0.65,\n      size: 32,\n      stroke: '#000000',\n      strokeOpacity: 1,\n      zIndex: 2,\n    },\n    state: {\n      selected: {\n        halo: true,\n        haloLineWidth: 24,\n        haloStrokeOpacity: 0.25,\n        labelFontSize: 12,\n        labelFontWeight: 'bold',\n        lineWidth: 4,\n        stroke: '#000000',\n      },\n      active: {\n        halo: true,\n        haloLineWidth: 12,\n        haloStrokeOpacity: 0.15,\n      },\n      highlight: {\n        labelFontWeight: 'bold',\n        lineWidth: 4,\n        stroke: '#000000',\n        strokeOpacity: 0.85,\n      },\n      inactive: {\n        badgeBackgroundOpacity: 0.25,\n        donutOpacity: 0.25,\n        fillOpacity: 0.25,\n        iconOpacity: 0.85,\n        labelFill: '#000000',\n        labelFillOpacity: 0.25,\n        strokeOpacity: 0.25,\n      },\n      disabled: {\n        badgeBackgroundOpacity: 0.25,\n        donutOpacity: 0.06,\n        fill: '#1B324F',\n        fillOpacity: 0.06,\n        iconFill: '#1B324F',\n        iconOpacity: 0.25,\n        labelFill: '#000000',\n        labelFillOpacity: 0.25,\n        strokeOpacity: 0.06,\n      },\n    },\n    animation: {\n      enter: 'fade',\n      exit: 'fade',\n      show: 'fade',\n      hide: 'fade',\n      expand: 'node-expand',\n      collapse: 'node-collapse',\n      update: [{ fields: ['x', 'y', 'fill', 'stroke'] }],\n      translate: [{ fields: ['x', 'y'] }],\n    },\n  },\n  edge: {\n    palette: {\n      type: 'group',\n      color: [\n        '#99ADD1',\n        '#1783FF',\n        '#00C9C9',\n        '#F08F56',\n        '#D580FF',\n        '#7863FF',\n        '#DB9D0D',\n        '#60C42D',\n        '#FF80CA',\n        '#2491B3',\n        '#17C76F',\n      ],\n    },\n    style: {\n      badgeBackgroundFill: '#99ADD1',\n      badgeFill: '#fff',\n      badgeFontSize: 8,\n      badgeOffsetX: 10,\n      fillOpacity: 1,\n      halo: false,\n      haloLineWidth: 12,\n      haloStrokeOpacity: 1,\n      increasedLineWidthForHitTesting: 2,\n      labelBackground: false,\n      labelBackgroundFill: '#ffffff',\n      labelBackgroundLineWidth: 0,\n      labelBackgroundOpacity: 0.75,\n      labelBackgroundPadding: [4, 4, 4, 4],\n      labelFill: '#000000',\n      labelFontSize: 12,\n      labelFontWeight: 400,\n      labelOpacity: 1,\n      labelPlacement: 'center',\n      labelTextBaseline: 'middle',\n      lineWidth: 1,\n      stroke: '#99ADD1',\n      strokeOpacity: 1,\n      zIndex: 1,\n    },\n    state: {\n      selected: {\n        halo: true,\n        haloStrokeOpacity: 0.25,\n        labelFontSize: 14,\n        labelFontWeight: 'bold',\n        lineWidth: 3,\n      },\n      active: {\n        halo: true,\n        haloStrokeOpacity: 0.15,\n      },\n      highlight: {\n        labelFontWeight: 'bold',\n        lineWidth: 3,\n      },\n      inactive: {\n        stroke: '#1B324F',\n        fillOpacity: 0.08,\n        labelOpacity: 0.25,\n        strokeOpacity: 0.08,\n        badgeBackgroundOpacity: 0.25,\n      },\n      disabled: {\n        stroke: '#d9d9d9',\n        fillOpacity: 0.45,\n        strokeOpacity: 0.45,\n        labelOpacity: 0.25,\n        badgeBackgroundOpacity: 0.45,\n      },\n    },\n    animation: {\n      enter: 'fade',\n      exit: 'fade',\n      expand: 'path-in',\n      collapse: 'path-out',\n      show: 'fade',\n      hide: 'fade',\n      update: [{ fields: ['sourceNode', 'targetNode'] }, { fields: ['stroke'], shape: 'key' }],\n      translate: [{ fields: ['sourceNode', 'targetNode'] }],\n    },\n  },\n  combo: {\n    style: {\n      collapsedMarkerFill: '#ffffff',\n      collapsedMarkerFontSize: 12,\n      collapsedMarkerFillOpacity: 1,\n      collapsedSize: 32,\n      collapsedFillOpacity: 1,\n      fill: '#99ADD1',\n      halo: false,\n      haloLineWidth: 12,\n      haloStroke: '#99ADD1',\n      haloStrokeOpacity: 0.25,\n      labelBackground: false,\n      labelBackgroundFill: '#ffffff',\n      labelBackgroundLineWidth: 0,\n      labelBackgroundOpacity: 0.75,\n      labelBackgroundPadding: [2, 4, 2, 4],\n      labelFill: '#000000',\n      labelFontSize: 12,\n      labelFontWeight: 400,\n      labelOpacity: 1,\n      lineDash: 0,\n      lineWidth: 1,\n      fillOpacity: 0.04,\n      strokeOpacity: 1,\n      padding: 10,\n      stroke: '#99ADD1',\n    },\n    state: {\n      selected: {\n        halo: true,\n        labelFontSize: 14,\n        labelFontWeight: 700,\n        lineWidth: 4,\n      },\n      active: {\n        halo: true,\n      },\n      highlight: {\n        labelFontWeight: 700,\n        lineWidth: 4,\n      },\n      inactive: {\n        fillOpacity: 0.65,\n        labelOpacity: 0.25,\n        strokeOpacity: 0.65,\n      },\n      disabled: {\n        fill: '#d9d9d9',\n        fillOpacity: 0.25,\n        labelOpacity: 0.25,\n        stroke: '#d9d9d9',\n        strokeOpacity: 0.25,\n      },\n    },\n    animation: {\n      enter: 'fade',\n      exit: 'fade',\n      show: 'fade',\n      hide: 'fade',\n      expand: 'combo-expand',\n      collapse: 'combo-collapse',\n      update: [{ fields: ['x', 'y'] }, { fields: ['fill', 'stroke', 'lineWidth'], shape: 'key' }],\n      translate: [{ fields: ['x', 'y'] }],\n    },\n  },\n};\n```\n\n</details>\n\n### 暗色主题\n\n<img width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*qTlLSoAbaXYAAAAAAAAAAAAAemJ7AQ/original\" alt=\"暗色主题\" />\n\n<details><summary>查看暗色主题完整配置项</summary>\n\n```js\nconst darkTheme = {\n  background: '#000000',\n  node: {\n    palette: {\n      type: 'group',\n      color: [\n        '#1783FF',\n        '#00C9C9',\n        '#F08F56',\n        '#D580FF',\n        '#7863FF',\n        '#DB9D0D',\n        '#60C42D',\n        '#FF80CA',\n        '#2491B3',\n        '#17C76F',\n      ],\n    },\n    style: {\n      donutOpacity: 1,\n      badgeBackgroundOpacity: 1,\n      badgeFill: '#fff',\n      badgeFontSize: 8,\n      badgePadding: [0, 4],\n      badgePalette: ['#7E92B5', '#F4664A', '#FFBE3A'],\n      fill: '#1783ff',\n      fillOpacity: 1,\n      halo: false,\n      iconFill: '#fff',\n      iconOpacity: 1,\n      labelBackground: false,\n      labelBackgroundFill: '#000000',\n      labelBackgroundLineWidth: 0,\n      labelBackgroundOpacity: 0.75,\n      labelFill: '#ffffff',\n      labelFillOpacity: 0.85,\n      labelLineHeight: 16,\n      labelPadding: [0, 2],\n      labelFontSize: 12,\n      labelFontWeight: 400,\n      labelOpacity: 1,\n      labelOffsetY: 2,\n      lineWidth: 0,\n      portFill: '#1783ff',\n      portLineWidth: 1,\n      portStroke: '#d0e4ff',\n      portStrokeOpacity: 0.65,\n      size: 32,\n      stroke: '#d0e4ff',\n      strokeOpacity: 1,\n      zIndex: 2,\n    },\n    state: {\n      selected: {\n        halo: true,\n        haloLineWidth: 24,\n        haloStrokeOpacity: 0.45,\n        labelFontSize: 12,\n        labelFontWeight: 'bold',\n        lineWidth: 4,\n        stroke: '#d0e4ff',\n      },\n      active: {\n        halo: true,\n        haloLineWidth: 12,\n        haloStrokeOpacity: 0.25,\n      },\n      highlight: {\n        labelFontWeight: 'bold',\n        lineWidth: 4,\n        stroke: '#d0e4ff',\n        strokeOpacity: 0.85,\n      },\n      inactive: {\n        badgeBackgroundOpacity: 0.45,\n        donutOpacity: 0.45,\n        fillOpacity: 0.45,\n        iconOpacity: 0.45,\n        labelFill: '#ffffff',\n        labelFillOpacity: 0.45,\n        strokeOpacity: 0.45,\n      },\n      disabled: {\n        badgeBackgroundOpacity: 0.25,\n        donutOpacity: 0.25,\n        fill: '#D0E4FF',\n        fillOpacity: 0.25,\n        iconFill: '#D0E4FF',\n        iconOpacity: 0.25,\n        labelFill: '#ffffff',\n        labelFillOpacity: 0.25,\n        strokeOpacity: 0.25,\n      },\n    },\n    animation: {\n      enter: 'fade',\n      exit: 'fade',\n      show: 'fade',\n      hide: 'fade',\n      expand: 'node-expand',\n      collapse: 'node-collapse',\n      update: [{ fields: ['x', 'y', 'fill', 'stroke'] }],\n      translate: [{ fields: ['x', 'y'] }],\n    },\n  },\n  edge: {\n    palette: {\n      type: 'group',\n      color: [\n        '#637088',\n        '#0F55A6',\n        '#008383',\n        '#9C5D38',\n        '#8B53A6',\n        '#4E40A6',\n        '#8F6608',\n        '#3E801D',\n        '#A65383',\n        '#175E75',\n        '#0F8248',\n      ],\n    },\n    style: {\n      badgeBackgroundFill: '#637088',\n      badgeFill: '#fff',\n      badgeFontSize: 8,\n      badgeOffsetX: 10,\n      fillOpacity: 1,\n      halo: false,\n      haloLineWidth: 12,\n      haloStrokeOpacity: 1,\n      increasedLineWidthForHitTesting: 2,\n      labelBackground: false,\n      labelBackgroundFill: '#000000',\n      labelBackgroundLineWidth: 0,\n      labelBackgroundOpacity: 0.75,\n      labelBackgroundPadding: [4, 4, 4, 4],\n      labelFill: '#ffffff',\n      labelFontSize: 12,\n      labelFontWeight: 400,\n      labelOpacity: 1,\n      labelPlacement: 'center',\n      labelTextBaseline: 'middle',\n      lineWidth: 1,\n      stroke: '#637088',\n      strokeOpacity: 1,\n      zIndex: 1,\n    },\n    state: {\n      selected: {\n        halo: true,\n        haloStrokeOpacity: 0.25,\n        labelFontSize: 14,\n        labelFontWeight: 'bold',\n        lineWidth: 3,\n      },\n      active: {\n        halo: true,\n        haloStrokeOpacity: 0.15,\n      },\n      highlight: {\n        labelFontWeight: 'bold',\n        lineWidth: 3,\n      },\n      inactive: {\n        stroke: '#D0E4FF',\n        fillOpacity: 0.08,\n        labelOpacity: 0.25,\n        strokeOpacity: 0.08,\n        badgeBackgroundOpacity: 0.25,\n      },\n      disabled: {\n        stroke: '#637088',\n        fillOpacity: 0.45,\n        strokeOpacity: 0.45,\n        labelOpacity: 0.25,\n        badgeBackgroundOpacity: 0.45,\n      },\n    },\n    animation: {\n      enter: 'fade',\n      exit: 'fade',\n      expand: 'path-in',\n      collapse: 'path-out',\n      show: 'fade',\n      hide: 'fade',\n      update: [{ fields: ['sourceNode', 'targetNode'] }, { fields: ['stroke'], shape: 'key' }],\n      translate: [{ fields: ['sourceNode', 'targetNode'] }],\n    },\n  },\n  combo: {\n    style: {\n      collapsedMarkerFill: '#000000',\n      collapsedMarkerFontSize: 12,\n      collapsedMarkerFillOpacity: 1,\n      collapsedSize: 32,\n      collapsedFillOpacity: 1,\n      fill: '#fdfdfd',\n      halo: false,\n      haloLineWidth: 12,\n      haloStroke: '#99add1',\n      haloStrokeOpacity: 0.25,\n      labelBackground: false,\n      labelBackgroundFill: '#000000',\n      labelBackgroundLineWidth: 0,\n      labelBackgroundOpacity: 0.75,\n      labelBackgroundPadding: [2, 4, 2, 4],\n      labelFill: '#ffffff',\n      labelFontSize: 12,\n      labelFontWeight: 400,\n      labelOpacity: 1,\n      lineDash: 0,\n      lineWidth: 1,\n      fillOpacity: 0.04,\n      strokeOpacity: 1,\n      padding: 10,\n      stroke: '#99add1',\n    },\n    state: {\n      selected: {\n        halo: true,\n        labelFontSize: 14,\n        labelFontWeight: 700,\n        lineWidth: 4,\n      },\n      active: {\n        halo: true,\n      },\n      highlight: {\n        labelFontWeight: 700,\n        lineWidth: 4,\n      },\n      inactive: {\n        fillOpacity: 0.65,\n        labelOpacity: 0.25,\n        strokeOpacity: 0.65,\n      },\n      disabled: {\n        fill: '#d0e4ff',\n        fillOpacity: 0.25,\n        labelOpacity: 0.25,\n        stroke: '#969696',\n        strokeOpacity: 0.25,\n      },\n    },\n    animation: {\n      enter: 'fade',\n      exit: 'fade',\n      show: 'fade',\n      hide: 'fade',\n      expand: 'combo-expand',\n      collapse: 'combo-collapse',\n      update: [{ fields: ['x', 'y'] }, { fields: ['fill', 'stroke', 'lineWidth'], shape: 'key' }],\n      translate: [{ fields: ['x', 'y'] }],\n    },\n  },\n};\n```\n\n</details>\n\n## 使用主题\n\n### 配置主题\n\n在创建图时通过 `theme` 选项指定要使用的主题：\n\n```javascript\nconst graph = new Graph({\n  theme: 'light', // 或 'dark'\n  // ... 其他配置\n});\n```\n\n### 切换主题\n\n创建图后，可以通过 `setTheme` 方法动态切换主题：\n\n```javascript\n// 切换到暗色主题\ngraph.setTheme('dark');\n\n// 获取当前主题\nconst currentTheme = graph.getTheme(); // 'dark'\n```\n\n## 样式优先级\n\n在 G6 中，元素的最终样式由多个层级的样式合并而成，按优先级从低到高排序：\n\n**⭐️ 主题默认样式** < 调色板样式 < 数据样式 < 图的默认样式 < **⭐️ 主题状态样式** < 图的状态样式\n\n详细说明：\n\n1. **主题默认样式**：主题系统提供的基础样式\n2. **调色板样式**：基于主题调色板配置的自动着色样式\n3. **数据样式**：在数据中定义的样式\n4. **图的默认样式**：通过图的配置项设置的样式\n5. **主题状态样式**：主题中定义的状态样式\n6. **图的状态样式**：通过图的配置项设置的状态样式\n\n更多关于自定义主题的内容，请参考[自定义主题](/manual/theme/custom-theme)。\n"
  },
  {
    "path": "packages/site/docs/manual/theme/palette.en.md",
    "content": "---\ntitle: Palette\norder: 3\n---\n\n## Overview\n\nA palette refers to a set of predefined color collections that help users more conveniently select colors. In G6, a palette is a common option that allows users to configure the colors of elements such as nodes, edges, and links through the palette.\n\nPalettes are divided into two types: `discrete palette` and `continuous palette`.\n\nA discrete palette is an array of colors used to map discrete values within elements to different colors, such as the type of nodes, the relationship of edges, etc. Below is a simple example of a discrete palette:\n\n```typescript\n['#5B8FF9', '#61DDAA', '#F6BD16', '#F6903D', '#F08BB4'];\n```\n\nA continuous palette is an interpolator that takes a value between 0 and 1 and returns the corresponding color. It is used to map continuous values within elements to different colors, such as the degree of nodes, the weight of edges, etc. Below is a simple example of a continuous palette:\n\n```typescript\n(value: number) => `rgb(${value * 255}, 0, 0)`;\n```\n\n## Register Palette\n\nYou can directly use the built-in palettes, but if you want to use other palettes, you need to register them first:\n\n```typescript\nimport { register, ExtensionCategory } from '@antv/g6';\nimport { CustomPalette } from 'package-name/or/path-to-your-custom-palette';\n\nregister(ExtensionCategory.PALETTE, 'custom-palette', CustomPalette);\n```\n\n:::warning{title=note}\n\nDuring the process of registering a palette, there is no distinction made between discrete and continuous palettes. It is necessary to ensure the consistency between the palette type and the data type when using the palette.\n:::\n\n### Built-in Palettes\n\nCurrently, G6 has 5 sets of commonly used discrete palettes that users can directly utilize:\n\n- spectral\n\n<div style=\"display: flex; width: 600px; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(158, 1, 66);\"></div><div style=\"background: rgb(213, 62, 79);\"></div><div style=\"background: rgb(244, 109, 67);\"></div><div style=\"background: rgb(253, 174, 97);\"></div><div style=\"background: rgb(254, 224, 139);\"></div><div style=\"background: rgb(255, 255, 191);\"></div><div style=\"background: rgb(230, 245, 152);\"></div><div style=\"background: rgb(171, 221, 164);\"></div><div style=\"background: rgb(102, 194, 165);\"></div><div style=\"background: rgb(50, 136, 189);\"></div><div style=\"background: rgb(94, 79, 162);\"></div></div>\n\n- tableau\n\n<div style=\"display: flex; width: 600px; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(78, 121, 167);\"></div><div style=\"background: rgb(242, 142, 44);\"></div><div style=\"background: rgb(225, 87, 89);\"></div><div style=\"background: rgb(118, 183, 178);\"></div><div style=\"background: rgb(89, 161, 79);\"></div><div style=\"background: rgb(237, 201, 73);\"></div><div style=\"background: rgb(175, 122, 161);\"></div><div style=\"background: rgb(255, 157, 167);\"></div><div style=\"background: rgb(156, 117, 95);\"></div><div style=\"background: rgb(186, 176, 171);\"></div></div>\n\n- oranges\n\n<div style=\"display: flex; width: 600px; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(255, 245, 235);\"></div><div style=\"background: rgb(254, 230, 206);\"></div><div style=\"background: rgb(253, 208, 162);\"></div><div style=\"background: rgb(253, 174, 107);\"></div><div style=\"background: rgb(253, 141, 60);\"></div><div style=\"background: rgb(241, 105, 19);\"></div><div style=\"background: rgb(217, 72, 1);\"></div><div style=\"background: rgb(166, 54, 3);\"></div><div style=\"background: rgb(127, 39, 4);\"></div></div>\n\n- greens\n\n<div style=\"display: flex; width: 600px; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(247, 252, 245);\"></div><div style=\"background: rgb(229, 245, 224);\"></div><div style=\"background: rgb(199, 233, 192);\"></div><div style=\"background: rgb(161, 217, 155);\"></div><div style=\"background: rgb(116, 196, 118);\"></div><div style=\"background: rgb(65, 171, 93);\"></div><div style=\"background: rgb(35, 139, 69);\"></div><div style=\"background: rgb(0, 109, 44);\"></div><div style=\"background: rgb(0, 68, 27);\"></div></div>\n\n- blues\n\n<div style=\"display: flex; width: 600px; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(247, 251, 255);\"></div><div style=\"background: rgb(222, 235, 247);\"></div><div style=\"background: rgb(198, 219, 239);\"></div><div style=\"background: rgb(158, 202, 225);\"></div><div style=\"background: rgb(107, 174, 214);\"></div><div style=\"background: rgb(66, 146, 198);\"></div><div style=\"background: rgb(33, 113, 181);\"></div><div style=\"background: rgb(8, 81, 156);\"></div><div style=\"background: rgb(8, 48, 107);\"></div></div>\n\n## Configure Palette\n\nCurrently, the configuration of palettes is mainly focused on elements, taking nodes as an example:\n\n### Discrete Palette\n\n1. Default Configuration: By directly setting the value of `palette` to the name of the palette, each node will be assigned a different color by default\n\n```typescript\n{\n  node: {\n    palette: 'spectral', // spectral is the Palette Name\n  }\n}\n```\n\n<embed src=\"@/common/manual/core-concept/palette/default-config.md\"></embed>\n\n> When the number of elements exceeds the number of colors in the palette, the colors in the palette will be reused in a cyclic manner.\n\n2. Standard Configuration: The attributes for configuring a discrete palette include: `type: 'group'`, `field`, `color`, `invert`.\n\nAmong them, `type: 'group'` explicitly specifies that the current palette type is a discrete palette; `field` designates the field for grouping in the element data; `color` is the name of the palette; `invert` indicates whether to invert the palette.\n\nGiven a set of example data:\n\n```json\n{\n  \"nodes\": [\n    { \"id\": \"node-1\", \"data\": { \"category\": \"A\" } },\n    { \"id\": \"node-2\", \"data\": { \"category\": \"B\" } },\n    { \"id\": \"node-3\", \"data\": { \"category\": \"C\" } },\n    { \"id\": \"node-4\", \"data\": { \"category\": \"A\" } },\n    { \"id\": \"node-5\", \"data\": { \"category\": \"B\" } },\n    { \"id\": \"node-6\", \"data\": { \"category\": \"C\" } }\n  ]\n}\n```\n\nIn the data, `node-1` and `node-4` belong to category A, `node-2` and `node-5` belong to category B, `node-3` and `node-6` belong to category C.\n\nConfigure the color of the nodes in such a way that nodes of the same category have the same color:\n\n```typescript\n{\n  node: {\n    palette: {\n      type: 'group', // Specify the palette type as a categorical palette.\n      field: 'category', // Specify the grouping field in the data.\n      color: 'tableau', // Use a Tableau-like palette.\n    }\n  }\n}\n```\n\n<embed src=\"@/common/manual/core-concept/palette/standard-config.md\"></embed>\n\n### Continuous Palette\n\nA continuous palette only supports standard configuration methods, with configuration properties including: `type: 'value'`, `field`, `color`, `invert`.\n\nGiven a set of example data:\n\n```json\n{\n  \"nodes\": [\n    { \"id\": \"node-1\", \"data\": { \"value\": 0 } },\n    { \"id\": \"node-2\", \"data\": { \"value\": 20 } },\n    { \"id\": \"node-3\", \"data\": { \"value\": 40 } },\n    { \"id\": \"node-4\", \"data\": { \"value\": 60 } },\n    { \"id\": \"node-5\", \"data\": { \"value\": 80 } },\n    { \"id\": \"node-6\", \"data\": { \"value\": 100 } }\n  ]\n}\n```\n\nNow, create an interpolator that maps the maximum value to red (`rgb(255, 0, 0)`) and the minimum value to black (`rgb(0, 0, 0)`):\n\n```typescript\n(value) => `rgb(${value * 255}, 0, 0)`;\n```\n\nConfigure the following so that the color of the nodes is mapped to different colors based on the value of the `value` field in the data:\n\n```typescript\n{\n  node: {\n    palette: {\n      type: 'value', // Specify the palette type as a continuous palette\n      field: 'value', // Specify the numerical field in the data\n      color: (value) => `rgb(${value * 255}, 0, 0)`, // Use an interpolator\n    }\n  }\n}\n```\n\n<embed src=\"@/common/manual/core-concept/palette/continuous-palette.md\"></embed>\n\n:::warning{title=note}\n\nThe built-in continuous palette does not support specifying a value range. If there is a need for more complex color mapping, it can be customized within the style mapping.\n:::\n\n## Custom Palette\n\nIf the built-in palette does not meet your requirements, you can customize the palette. For details, please refer to [Custom Palette](/en/manual/custom-extension/palette).\n\n## Priority\n\nThe palette generates styles based on the type of element. For nodes and combos, the color is mapped to the `fill` attribute; for edges, the color is mapped to the `stroke` attribute.\n\nIf both a palette and a style mapping are configured, the style mapping will override the palette colors. In the following example, the color of the nodes is always red:\n\n```typescript\n{\n  node: {\n    style: {\n      fill: 'red',\n    },\n    palette: 'spectral',\n  }\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/theme/palette.zh.md",
    "content": "---\ntitle: 色板\norder: 3\n---\n\n## 概述\n\n色板(Palette)是指一组预定义的颜色集合，用于帮助用户更方便的选择颜色。在 G6 中，色板是一种常见的配置项，用户可以通过色板来配置节点、边、连线等元素的颜色。\n\n色板分为`离散色板`和`连续色板`两种类型。\n\n离散色板是一组颜色数组，用于将元素中的离散值映射到不同的颜色上，例如节点的类型、边的关系等。下面是一个简单的离散色板示例：\n\n```typescript\n['#5B8FF9', '#61DDAA', '#F6BD16', '#F6903D', '#F08BB4'];\n```\n\n连续色板是一个插值器，输入 0~1 的值，返回对应的颜色，用于将元素中的连续值映射到不同的颜色上，例如节点的度数、边的权重等。下面是一个简单的连续色板示例：\n\n```typescript\n(value: number) => `rgb(${value * 255}, 0, 0)`;\n```\n\n## 注册色板\n\n你可以直接使用内置色板，如果想使用其他色板，需要先进行注册：\n\n```typescript\nimport { register, ExtensionCategory } from '@antv/g6';\nimport { CustomPalette } from 'package-name/or/path-to-your-custom-palette';\n\nregister(ExtensionCategory.PALETTE, 'custom-palette', CustomPalette);\n```\n\n:::warning{title=注意}\n在注册色板过程中并不会区分离散色板和连续色板，使用色板过程中需要自行保证色板类型和数据类型的一致性。\n:::\n\n### 内置色板\n\n目前 G6 内置了 5 套常用的离散色板，用户可以直接使用：\n\n- spectral\n\n<div style=\"display: flex; width: 600px; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(158, 1, 66);\"></div><div style=\"background: rgb(213, 62, 79);\"></div><div style=\"background: rgb(244, 109, 67);\"></div><div style=\"background: rgb(253, 174, 97);\"></div><div style=\"background: rgb(254, 224, 139);\"></div><div style=\"background: rgb(255, 255, 191);\"></div><div style=\"background: rgb(230, 245, 152);\"></div><div style=\"background: rgb(171, 221, 164);\"></div><div style=\"background: rgb(102, 194, 165);\"></div><div style=\"background: rgb(50, 136, 189);\"></div><div style=\"background: rgb(94, 79, 162);\"></div></div>\n\n- tableau\n\n<div style=\"display: flex; width: 600px; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(78, 121, 167);\"></div><div style=\"background: rgb(242, 142, 44);\"></div><div style=\"background: rgb(225, 87, 89);\"></div><div style=\"background: rgb(118, 183, 178);\"></div><div style=\"background: rgb(89, 161, 79);\"></div><div style=\"background: rgb(237, 201, 73);\"></div><div style=\"background: rgb(175, 122, 161);\"></div><div style=\"background: rgb(255, 157, 167);\"></div><div style=\"background: rgb(156, 117, 95);\"></div><div style=\"background: rgb(186, 176, 171);\"></div></div>\n\n- oranges\n\n<div style=\"display: flex; width: 600px; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(255, 245, 235);\"></div><div style=\"background: rgb(254, 230, 206);\"></div><div style=\"background: rgb(253, 208, 162);\"></div><div style=\"background: rgb(253, 174, 107);\"></div><div style=\"background: rgb(253, 141, 60);\"></div><div style=\"background: rgb(241, 105, 19);\"></div><div style=\"background: rgb(217, 72, 1);\"></div><div style=\"background: rgb(166, 54, 3);\"></div><div style=\"background: rgb(127, 39, 4);\"></div></div>\n\n- greens\n\n<div style=\"display: flex; width: 600px; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(247, 252, 245);\"></div><div style=\"background: rgb(229, 245, 224);\"></div><div style=\"background: rgb(199, 233, 192);\"></div><div style=\"background: rgb(161, 217, 155);\"></div><div style=\"background: rgb(116, 196, 118);\"></div><div style=\"background: rgb(65, 171, 93);\"></div><div style=\"background: rgb(35, 139, 69);\"></div><div style=\"background: rgb(0, 109, 44);\"></div><div style=\"background: rgb(0, 68, 27);\"></div></div>\n\n- blues\n\n<div style=\"display: flex; width: 600px; height: 20px;\"><style>div{flex-grow:1}</style><div style=\"background: rgb(247, 251, 255);\"></div><div style=\"background: rgb(222, 235, 247);\"></div><div style=\"background: rgb(198, 219, 239);\"></div><div style=\"background: rgb(158, 202, 225);\"></div><div style=\"background: rgb(107, 174, 214);\"></div><div style=\"background: rgb(66, 146, 198);\"></div><div style=\"background: rgb(33, 113, 181);\"></div><div style=\"background: rgb(8, 81, 156);\"></div><div style=\"background: rgb(8, 48, 107);\"></div></div>\n\n## 配置色板\n\n目前开放色板配置的地方主要以元素为主，以节点为例：\n\n### 离散色板\n\n1. 默认配置，直接配置 `palette` 的值为色板名，会默认为每个节点分配不同的颜色\n\n```typescript\n{\n  node: {\n    palette: 'spectral', // spectral 为色板名\n  }\n}\n```\n\n<embed src=\"@/common/manual/core-concept/palette/default-config.md\"></embed>\n\n> 当元素数量超过色板颜色数量时，会循环使用色板中的颜色\n\n2. 标准配置，离散色板配置属性包括：`type: 'group'`，`field`，`color`，`invert`\n\n其中 `type: 'group'` 显式指定了当前色板类型为离散色板；`field` 指定元素数据中的分组字段；`color` 为色板名；`invert` 为是否反转色板。\n\n给定一组示例数据：\n\n```json\n{\n  \"nodes\": [\n    { \"id\": \"node-1\", \"data\": { \"category\": \"A\" } },\n    { \"id\": \"node-2\", \"data\": { \"category\": \"B\" } },\n    { \"id\": \"node-3\", \"data\": { \"category\": \"C\" } },\n    { \"id\": \"node-4\", \"data\": { \"category\": \"A\" } },\n    { \"id\": \"node-5\", \"data\": { \"category\": \"B\" } },\n    { \"id\": \"node-6\", \"data\": { \"category\": \"C\" } }\n  ]\n}\n```\n\n数据中 `node-1`，`node-4` 属于 A 类别，`node-2`，`node-5` 属于 B 类别，`node-3`，`node-6` 属于 C 类别。\n\n通过以下方式配置节点的颜色，使得同类别的节点颜色相同：\n\n```typescript\n{\n  node: {\n    palette: {\n      type: 'group', // 指定色板类型为分类色板\n      field: 'category', // 指定数据中的分组字段\n      color: 'tableau', // 使用 tableau 色板\n    }\n  }\n}\n```\n\n<embed src=\"@/common/manual/core-concept/palette/standard-config.md\"></embed>\n\n### 连续色板\n\n连续色板只支持标准方式配置，配置属性包括：`type: 'value'`，`field`，`color`，`invert`。\n\n给定一组示例数据：\n\n```json\n{\n  \"nodes\": [\n    { \"id\": \"node-1\", \"data\": { \"value\": 0 } },\n    { \"id\": \"node-2\", \"data\": { \"value\": 20 } },\n    { \"id\": \"node-3\", \"data\": { \"value\": 40 } },\n    { \"id\": \"node-4\", \"data\": { \"value\": 60 } },\n    { \"id\": \"node-5\", \"data\": { \"value\": 80 } },\n    { \"id\": \"node-6\", \"data\": { \"value\": 100 } }\n  ]\n}\n```\n\n现在创建一个插值器，将最大值映射为红色(`rgb(255, 0, 0)`)，最小值映射为黑色(`rgb(0, 0, 0)`)：\n\n```typescript\n(value) => `rgb(${value * 255}, 0, 0)`;\n```\n\n通过以下配置使得节点的颜色根据数据中的 `value` 字段的值映射到不同的颜色：\n\n```typescript\n{\n  node: {\n    palette: {\n      type: 'value', // 指定色板类型为连续色板\n      field: 'value', // 指定数据中的数值字段\n      color: (value) => `rgb(${value * 255}, 0, 0)`, // 使用插值器\n    }\n  }\n}\n```\n\n<embed src=\"@/common/manual/core-concept/palette/continuous-palette.md\"></embed>\n\n:::warning{title=注意}\n内置连续色板不支持指定值域范围，如果有更复杂的颜色映射需求，可以在样式映射中自定义\n:::\n\n## 自定义色板\n\n如果内置色板无法满足需求，可以自定义色板，具体请参考[自定义色板](/manual/theme/custom-palette)。\n\n## 优先级\n\n色板会基于元素类型生成样式，对于节点和组合，会将颜色映射到 `fill` 属性；对于边，会将颜色映射到 `stroke` 属性。\n\n如果同时配置了色板和样式映射，样式映射会覆盖色板颜色。下面的例子中，节点的颜色始终为红色：\n\n```typescript\n{\n  node: {\n    style: {\n      fill: 'red',\n    },\n    palette: 'spectral',\n  }\n}\n```\n"
  },
  {
    "path": "packages/site/docs/manual/transform/MapNodeSize.en.md",
    "content": "---\ntitle: MapNodeSize\norder: 1\n---\n\nIn graph visualization, the size of a node is usually used to convey the importance or influence of the node. By adjusting the size of the node based on the centrality of the node, we can more intuitively show the importance of each node in the network, helping users better understand and analyze complex network structures.\n\n## Options\n\n### centrality\n\n> [NodeCentralityOptions](#nodecentralityoptions) _\\| ((graphData:_ [GraphData](/manual/core-concept/data#图数据graphdata)_) =>_ _Map**&lt;**string, number>)_ **Default:** `type: 'eigenvector'`\n\nThe method of measuring the node centrality\n\n- `'degree'`: Degree centrality, measures centrality by the degree (number of connected edges) of a node. Nodes with high degree centrality usually have more direct connections and may play important roles in the network\n- `'betweenness'`: Betweenness centrality, measures centrality by the number of times a node appears in all shortest paths. Nodes with high betweenness centrality usually act as bridges in the network, controlling the flow of information\n- `'closeness'`: Closeness centrality, measures centrality by the reciprocal of the average shortest path length from a node to all other nodes. Nodes with high closeness centrality usually can reach other nodes in the network more quickly\n- `'eigenvector'`: Eigenvector centrality, measures centrality by the degree of connection between a node and other central nodes. Nodes with high eigenvector centrality usually connect to other important nodes\n- `'pagerank'`: PageRank centrality, measures centrality by the number of times a node is referenced by other nodes, commonly used in directed graphs. Nodes with high PageRank centrality usually have high influence in the network, similar to the page ranking algorithm\n- Custom centrality calculation method: `(graphData: GraphData) => Map<ID, number>`, where `graphData` is the graph data, and `Map<ID, number>` is the mapping from node ID to centrality value\n\n#### NodeCentralityOptions\n\n```typescript\ntype NodeCentralityOptions =\n  | { type: 'degree'; direction?: 'in' | 'out' | 'both' }\n  | { type: 'betweenness'; directed?: boolean; weightPropertyName?: string }\n  | { type: 'closeness'; directed?: boolean; weightPropertyName?: string }\n  | { type: 'eigenvector'; directed?: boolean }\n  | { type: 'pagerank'; epsilon?: number; linkProb?: number };\n```\n\n### mapLabelSize\n\n> _boolean \\| [number, number]_ **Default:** `false`\n\nWhether to map label size synchronously\n\n### maxSize\n\n> _number \\| [number, number] \\| Float32Array \\| [number, number, number]_ **Default:** `80`\n\nThe maximum size of the node\n\n### minSize\n\n> _number \\| [number, number] \\| Float32Array \\| [number, number, number]_ **Default:** `20`\n\nThe minimum size of the node\n\n### scale\n\n> _'linear' \\| 'log' \\| 'pow' \\| 'sqrt' \\| ((value: number, domain: [number, number], range: [number, number]) => number)_ **Default:** `'log'`\n\nScale type\n\n- `'linear'`: Linear scale, maps a value from one range to another range linearly, commonly used for cases where the difference in centrality values is small\n\n- `'log'`: Logarithmic scale, maps a value from one range to another range logarithmically, commonly used for cases where the difference in centrality values is large\n\n- `'pow'`: Power-law scale, maps a value from one range to another range using power law, commonly used for cases where the difference in centrality values is large\n\n- `'sqrt'`: Square root scale, maps a value from one range to another range using square root, commonly used for cases where the difference in centrality values is large\n\n- Custom scale: `(value: number, domain: [number, number], range: [number, number]) => number`，where `value` is the value to be mapped, `domain` is the input range, and `range` is the output range\n"
  },
  {
    "path": "packages/site/docs/manual/transform/MapNodeSize.zh.md",
    "content": "---\ntitle: 动态调整节点大小 MapNodeSize\norder: 1\n---\n\n## 概述\n\n在图可视化中，节点的大小通常用于传达节点的重要性或影响力。通过根据节点中心性调整节点的大小，我们可以更直观地展示网络中各个节点的重要性，从而帮助用户更好地理解和分析复杂的网络结构。\n\n## 使用场景\n\n需要通过节点大小来突出节点的重要性和影响力时，可使用此数据处理。\n\n以下为常见的场景：\n\n- **社交网络分析**：比如分析社交媒体平台中用户的活跃度与影响力，通过节点大小突出高互动用户。\n\n- **金融风险传导网络**：比如识别金融系统中承担关键资金流转职能的机构，预防系统性风险。\n\n- **交通枢纽规划**：比如优化城市地铁网络设计，识别换乘压力点。\n\n## 配置项\n\n| 属性         | 描述                                                       | 类型                                                                                                                               | 默认值               | 必选 |\n| ------------ | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------- | ---- |\n| type         | 数据处理类型                                               | map-node-size                                                                                                                      | -                    | ✓    |\n| centrality   | 节点中心性的度量方法，[配置项](#centrality)                | [NodeCentralityOptions](#nodecentralityoptions) \\| ((graphData: [GraphData](/manual/data#图数据graphdata)) => Map<string, number>) | `{ type: 'degree' }` |      |\n| mapLabelSize | 是否同步调整标签大小                                       | boolean \\| [number, number]                                                                                                        | false                |      |\n| maxSize      | 节点最大尺寸                                               | number \\| [number, number] \\| [number, number, number]                                                                             | 80                   |      |\n| minSize      | 节点最小尺寸                                               | number \\| [number, number] \\| [number, number, number]                                                                             | 20                   |      |\n| scale        | 插值函数，用于将节点中心性映射到节点大小，[配置项](#scale) | `linear` \\| `log` \\| `pow` \\| `sqrt` \\| ((value: number, domain: [number, number], range: [number, number]) => number)             | `log`                |      |\n\n### centrality\n\n节点中心性的度量方法\n\n- `'degree'`：度中心性，通过节点的度数（连接的边的数量）来衡量其重要性。度中心性高的节点通常具有较多的直接连接，在网络中可能扮演着重要的角色\n- `'betweenness'`：介数中心性，通过节点在所有最短路径中出现的次数来衡量其重要性。介数中心性高的节点通常在网络中起到桥梁作用，控制着信息的流动\n- `'closeness'`：接近中心性，通过节点到其他所有节点的最短路径长度总和的倒数来衡量其重要性。接近中心性高的节点通常能够更快地到达网络中的其他节点\n- `'eigenvector'`：特征向量中心性，通过节点与其他中心节点的连接程度来衡量其重要性。特征向量中心性高的节点通常连接着其他重要节点\n- `'pagerank'`：PageRank 中心性，通过节点被其他节点引用的次数来衡量其重要性，常用于有向图。PageRank 中心性高的节点通常在网络中具有较高的影响力，类似于网页排名算法\n- 自定义中心性计算方法：`(graphData: GraphData) => Map<ID, number>`，其中 `graphData` 为图数据，`Map<ID, number>` 为节点 ID 到中心性值的映射\n\n**示例：**\n\n```typescript {6-9}\nconst graph = new Graph({\n  // 其他配置...\n  transforms: [\n    {\n      type: 'map-node-size',\n      centrality: {\n        type: 'degree',\n        direction: 'both',\n      },\n    },\n  ],\n});\n```\n\n效果如下（可切换度量方法查看不同效果，示例中节点 label 为`${节点 id } - ${节点大小}`）：\n\n<embed src=\"@/common/api/transforms/map-node-size-centrality.md\"></embed>\n\n#### NodeCentralityOptions\n\n```typescript\ntype NodeCentralityOptions =\n  | { type: 'degree'; direction?: 'in' | 'out' | 'both' }\n  | { type: 'betweenness'; directed?: boolean; weightPropertyName?: string }\n  | { type: 'closeness'; directed?: boolean; weightPropertyName?: string }\n  | { type: 'eigenvector'; directed?: boolean }\n  | { type: 'pagerank'; epsilon?: number; linkProb?: number };\n```\n\n`direction`：表示统计哪些方向的边，`in` -入边、 `out` -出边、 `both` -入边和出边都考虑进去\n\n`directed`：是否为有向图\n\n`weightPropertyName`：边的权重属性名\n\n`epsilon`：PageRank 算法的收敛容差\n\n`linkProb`：PageRank 算法的阻尼系数，指任意时刻，用户访问到某节点后继续访问该节点链接的下一个节点的概率，经验值 0.85\n\n### scale\n\n- `'linear'`：线性插值函数，将一个值从一个范围线性映射到另一个范围，常用于处理中心性值的差异较小的情况\n- `'log'`：对数插值函数，将一个值从一个范围对数映射到另一个范围，常用于处理中心性值的差异较大的情况\n- `'pow'`：幂律插值函数，将一个值从一个范围幂律映射到另一个范围，常用于处理中心性值的差异较大的情况\n- `'sqrt'`：平方根插值函数，将一个值从一个范围平方根映射到另一个范围，常用于处理中心性值的差异较大的情况\n- 自定义插值函数：`(value: number, domain: [number, number], range: [number, number]) => number`，其中 `value` 为需要映射的值，`domain` 为输入值的范围，`range` 为输出值的范围\n\n**示例：**\n\n```typescript {9}\nconst graph = new Graph({\n  // 其他配置...\n  transforms: [\n    {\n      type: 'map-node-size',\n      centrality: {\n        type: 'degree',\n      },\n      scale: 'linear',\n    },\n  ],\n});\n```\n\n效果如下（该示例为基于度中心性 `degree` ，可切换插值函数查看不同效果，示例中节点 label 为`${节点 id } - ${节点大小}`）：\n\n<embed src=\"@/common/api/transforms/map-node-size-scale.md\"></embed>\n\n## 实际案例\n\n- [场景案例：独角兽和他们的投资者](/examples/feature/default/#unicorns-investors)\n"
  },
  {
    "path": "packages/site/docs/manual/transform/PlaceRadialLabels.en.md",
    "content": "---\ntitle: PlaceRadialLabels\norder: 3\n---\n\n## Options\n\n### offset\n\n> _number_\n\nOffset\n"
  },
  {
    "path": "packages/site/docs/manual/transform/PlaceRadialLabels.zh.md",
    "content": "---\ntitle: 径向标签 PlaceRadialLabels\norder: 2\n---\n\n**参考示例**：\n\n- [径向生态树](/examples/scene-case/tree-graph/#radial-dendrogram)\n- [径向紧凑树](/examples/scene-case/tree-graph/#radial-compact-tree)\n\n## 配置项\n\n### type\n\n> _`place-radial-labels` \\| string_\n\n此数据处理已内置，你可以通过 `type: 'place-radial-labels'` 来使用它。\n\n### offset\n\n> _number_\n\n偏移量\n"
  },
  {
    "path": "packages/site/docs/manual/transform/ProcessParallelEdges.en.md",
    "content": "---\ntitle: ProcessParallelEdges\norder: 3\n---\n\nParallel Edges refer to multiple edges existing between two nodes in a graph structure. These edges share the same source and target nodes but may represent different relationships or attributes. To avoid edge overlap and confusion, two methods are provided for handling parallel edges: (1) Bundle Mode: Bundles parallel edges together and separates them from other edges by altering their curvature; (2) Merge Mode: Merges parallel edges into a single aggregated edge.\n\n## Options\n\n### distance\n\n> _number_\n\nThe distance between edges, only valid for bundling mode\n\n### edges\n\n> _string[]_\n\nThe edges to be handled, all edges by default\n\n### <Badge type=\"success\">Required</Badge> mode\n\n> _'bundle' \\| 'merge'_ **Default:** `'bundle'`\n\nProcessing mode\n\n- '`merge`': Merge parallel edges into one edge which is suitable for cases where parallel edges do not need to be distinguished\n\n- '`bundle`': Each edge will be bundled with all other parallel edges and separated from them by varying the curvature. If the number of parallel edges in a group is odd, the central edge will be drawn as a straight line, and the others will be drawn as curves\n\n### style\n\n> _PathStyleProps_ _\\| ((prev:_ [EdgeData](/api/graph/option#edgedata)_[]) =>_ _PathStyleProps)_\n\nThe style of the merged edge, only valid for merging mode\n"
  },
  {
    "path": "packages/site/docs/manual/transform/ProcessParallelEdges.zh.md",
    "content": "---\ntitle: 平行边 ProcessParallelEdges\norder: 3\n---\n\n## 概述\n\n平行边（Parallel Edges）是指在图结构中，两个节点之间存在多条边。这些边共享相同的源节点和目标节点，但可能代表不同的关系或属性。为了避免边的重叠和混淆，提供了两种处理平行边的方式：\n\n- 捆绑模式（bundle）：将平行边捆绑在一起，通过改变曲率与其他边分开\n- 合并模式（merge）：将平行边合并为一条聚合\n\n## 使用场景\n\n下面为常见使用场景举例：\n\n- 双向数据流，比如客户端发送请求，服务端返回响应\n\n- 多依赖关系，即一个节点通过多种方式依赖另一个节点，比如微服务架构中，服务 A 调用服务 B的两个不同 API\n- 多链路，比如高可用架构中，主链路（实线，状态正常）与备份链路（灰色虚线，状态待机）同时展示\n\n## 基本用法\n\n**1. 快速配置（静态）**\n\n使用字符串形式直接声明，这种方式简洁但仅支持默认配置，且配置后不可动态修改：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  transforms: ['process-parallel-edges'],\n});\n```\n\n**2. 对象配置（推荐）**\n\n使用对象形式进行配置，支持自定义参数，且可以在运行时动态更新配置：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  transforms: [\n    {\n      type: 'process-parallel-edges',\n      key: 'process-parallel-edges',\n      mode: 'bundle', // 默认就是捆绑模式了\n      distance: 30, // 配置捆绑模式下边之间的距离为30\n    },\n  ],\n});\n```\n\n## 配置项\n\n| 属性     | 描述                                               | 类型                                                                                    | 默认值                  | 必选 |\n| -------- | -------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------- | ---- |\n| type     | 数据处理类型                                       | process-parallel-edges                                                                  | -                       | ✓    |\n| distance | 边之间的距离，仅在捆绑模式下有效                   | number                                                                                  | 15                      |      |\n| edges    | 考虑要处理的边，默认为全部边                       | string[]                                                                                | -                       |      |\n| mode     | 处理模式，[配置项](#mode)                          | `'bundle'`                                                                              | `'merge'` \\| `'bundle'` |      |\n| style    | 合并边的样式，仅在合并模式下有效，[配置项](#style) | PathStyleProps \\| ((prev: [EdgeData](/manual/data#边数据edgedata)[]) => PathStyleProps) | -                       |      |\n\n### mode\n\n提供了两种处理模式：\n\n- `'merge'`: 将平行边合并为一条边，适用于不需要区分平行边的情况，[示例](#合并模式)\n\n`merge` 使用的是内置的直线（ [`line`](/manual/element/edge/Line) ）来展示合并边。\n\n- `'bundle'`: 每条边都会与其他所有平行边捆绑在一起，并通过改变曲率与其他边分开。如果一组平行边的数量是奇数，那么中心的边将被绘制为直线，其他的边将被绘制为曲线，[示例](#捆绑模式)\n\n`bundle` 使用的是内置的二次贝塞尔曲线（ [`quadratic`](/manual/element/edge/Quadratic) ）来实现，数据处理过程会强制把每个 `edgeDatum.type` 改为 `quadratic`，并计算每条线的曲率。\n\n:::warning{title=注意}\n\n禁止在创建 Graph 实例时配置 `edge.type` 即默认的边类型，因为它的优先级比 `edgeDatum.type` 要高，会导致 `bundle` 模式的处理无法生效。\n\n:::\n\n### style\n\n合并边的样式，仅在合并模式下有效\n\n#### PathStyleProps\n\n下表列出了常用的属性，更多属性请参考 [PathStyleProps](https://g.antv.antgroup.com/api/basic/path#pathstyleprops)。\n\n| 属性           | 描述                      | 类型                          | 默认值    | 必选 |\n| -------------- | ------------------------- | ----------------------------- | --------- | ---- |\n| fill           | 填充色                    | string                        | `#1783FF` |\n| fillOpacity    | 填充色透明度              | number \\| string              | 1         |\n| lineCap        | 描边端点样式              | `round` \\| `square` \\| `butt` | `butt`    |\n| lineDash       | 描边虚线样式              | number[]                      | -         |\n| lineDashOffset | 描边虚线偏移量            | number                        | -         |\n| lineJoin       | 描边连接处样式            | `round` \\| `bevel` \\| `miter` | `miter`   |\n| lineWidth      | 描边宽度                  | number                        | 1         |\n| opacity        | 透明度                    | number \\| string              | 1         |\n| shadowBlur     | 阴影模糊度                | number                        | -         |\n| shadowColor    | 阴影颜色                  | string                        | -         |\n| shadowOffsetX  | 阴影在 x 轴方向上的偏移量 | number \\| string              | -         |\n| shadowOffsetY  | 阴影在 y 轴方向上的偏移量 | number \\| string              | -         |\n| shadowType     | 阴影类型                  | `inner` \\| `outer`            | `outer`   |\n| stroke         | 描边色                    | string                        | `#000`    |\n| strokeOpacity  | 描边色透明度              | number \\| string              | 1         |\n| visibility     | 图形是否可见              | `visible` \\| `hidden`         | `visible` |\n\n## 代码示例\n\n### 捆绑模式\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'A', style: { x: 50, y: 350 } },\n    { id: 'B', style: { x: 250, y: 150 } },\n    { id: 'C', style: { x: 450, y: 350 } },\n  ],\n  edges: [\n    { source: 'A', target: 'C' },\n    { source: 'C', target: 'A' },\n    ...Array.from({ length: 10 }).map((_, i) => ({\n      id: `edge:A-B${i}`,\n      source: 'A',\n      target: 'B',\n      data: {\n        label: `A->B:${i}`,\n      },\n    })),\n    ...Array.from({ length: 5 }).map((_, i) => ({\n      id: `edge:B-C${i}`,\n      source: 'B',\n      target: 'C',\n      data: {\n        label: `B->C:${i}`,\n      },\n    })),\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'center',\n  data,\n  node: {\n    style: {\n      ports: [{ placement: 'center' }],\n      labelText: (d) => d.id,\n    },\n  },\n  edge: {\n    style: {\n      labelText: (d) => d?.data?.label || `${d.source}->${d.target}`,\n    },\n  },\n  behaviors: ['drag-element'],\n  transforms: ['process-parallel-edges'],\n});\n\ngraph.render();\n```\n\n### 合并模式\n\n下面是一个简单的合并模式的例子，需要注意：\n\n- 不需要合并（即两个节点间只有一条边）的边，合并样式不会在这条边上生效，比如例子中的 **A->C**\n- 合并样式实际上是赋值给 `datum.style` ，也就是优先级会比实例化 Graph 时配置的默认样式低（ `edge.style` ），所以例子中合并样式的 `startArrow` 没有生效\n\n```js | ob { inject: true }\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'A', style: { x: 50, y: 350 } },\n    { id: 'B', style: { x: 250, y: 150 } },\n    { id: 'C', style: { x: 450, y: 350 } },\n  ],\n  edges: [\n    { source: 'A', target: 'B' },\n    { source: 'B', target: 'A' },\n    { id: 'B-C:1', source: 'B', target: 'C' },\n    { id: 'B-C:2', source: 'B', target: 'C' },\n    { source: 'A', target: 'C' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'center',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  edge: {\n    style: {\n      labelText: (d) => d?.data?.label || `${d.source}->${d.target}`,\n      startArrow: false,\n    },\n  },\n  transforms: [\n    {\n      type: 'process-parallel-edges',\n      mode: 'merge',\n      style: {\n        halo: true,\n        haloOpacity: 0.2,\n        haloStroke: 'red',\n        startArrow: true,\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n"
  },
  {
    "path": "packages/site/docs/manual/transform/custom-transform.en.md",
    "content": "---\ntitle: Custom Transform\norder: 4\n---\n"
  },
  {
    "path": "packages/site/docs/manual/transform/custom-transform.zh.md",
    "content": "---\ntitle: 自定义数据处理\norder: 4\n---\n\n## 概述\n\n自定义数据处理允许用户在业务实现过程中，把额外的数据处理封装起来，在渲染前或者布局后对数据进行进一步处理。用户通过自定义数据处理，实现部分数据处理解耦，更好地进行管理、编排代码以及提高代码可维护性。\n\n## 开始前\n\n用户在进行自定义数据处理前，需要仔细阅读并掌握数据处理器的 [实现原理和执行时机](/manual/transform/overview#实现原理) 。\n\n## 代码示例\n\n接下来将讲述两个可能的业务场景，并通过自定义数据处理来实现：\n\n### 不展示游离节点\n\n- **需求**\n\n  游离节点，即没有连线的节点，画布渲染时不展示游离节点\n\n- **实现**\n\n  ```typescript\n  import type { DrawData, DrawContext } from '@antv/g6';\n  import { Graph, BaseTransform, register, ExtensionCategory } from '@antv/g6';\n\n  class HideFreeNode extends BaseTransform {\n    public beforeDraw(input: DrawData, context: DrawContext): DrawData {\n      const { model } = this.context;\n      const { add, update, remove } = input;\n\n      add.nodes.forEach((nodeData, nodeId) => {\n        // 获取节点的相关连线\n        const edges = model.getRelatedEdgesData(nodeId);\n        // 没有任何连线的的节点则从add里面移除，添加到remove里面\n        if (!edges.length) {\n          add.nodes.delete(nodeId);\n          remove.nodes.set(nodeId, nodeData);\n        }\n      });\n\n      return input;\n    }\n  }\n  ```\n\n  <embed src=\"@/common/manual/custom-extension/transform/hide-free-node.md\"></embed>\n\n- **说明**\n\n  示例中总共有6个节点，id为1-6，id为4的节点没有连线，因此被移除了。\n\n  通过 `getRelatedEdgesData` 获取节点的相关连线，没有则把该节点放到 `remove.nodes` 里面去，并从 `add.nodes` 里面移除。\n\n### 环形布局径向label\n\n- **需求**\n\n  使用 [环形布局](/manual/layout/circular-layout) 时，节点 label 的也需要像内置数据处理器 [PlaceRadialLabels](/manual/transform/place-radial-labels) 一样实现径向展示（但 PlaceRadialLabels 只支持径向布局，环形布局不是径向布局）\n\n- **实现**\n\n  ```typescript\n  import type { RuntimeContext, DrawContext, Point, TransformArray, Vector2, Vector3 } from '@antv/g6';\n  import { Graph, BaseTransform, register, ExtensionCategory, BaseTransformOptions } from '@antv/g6';\n\n  // 目前circular布局没有暴露方法可以获取布局中心，这里简单处理先固定一个，配置circular布局时center与这里保持一致即可\n  const circularCenter = [300, 300];\n\n  // 下面的函数 G6 没有暴露出来，先自行声明\n  function subtract(a: Vector2 | Vector3, b: Vector2 | Vector3): Vector2 | Vector3 {\n    return a.map((v, i) => v - b[i]) as Vector2 | Vector3;\n  }\n  function rad(a: Vector2 | Vector3): number {\n    const [x, y] = a;\n    if (!x && !y) return 0;\n    return Math.atan2(y, x);\n  }\n  function rad2deg(rad: number): number {\n    return rad * (180 / Math.PI);\n  }\n\n  interface CircularRadialLabelsOptions extends BaseTransformOptions {\n    offset?: number; // 偏移量\n  }\n\n  class CircularRadialLabels extends BaseTransform<CircularRadialLabelsOptions> {\n    static defaultOptions = {\n      offset: 5,\n    };\n    constructor(context: RuntimeContext, options: CircularRadialLabelsOptions) {\n      super(context, Object.assign({}, CircularRadialLabels.defaultOptions, options));\n    }\n    get center(): Point {\n      return circularCenter;\n    }\n    public afterLayout() {\n      const { graph, model } = this.context;\n      const data = model.getData();\n      data.nodes?.forEach((datum) => {\n        const radian = rad(subtract([datum.style.x, datum.style.y], this.center));\n        const isLeft = Math.abs(radian) > Math.PI / 2;\n        const isLeaf = !datum.children || datum.children.length === 0;\n        const nodeId = datum.id;\n        const node = this.context.element?.getElement(nodeId);\n        if (!node || !node.isVisible()) return;\n\n        const nodeHalfWidth = graph.getElementRenderStyle(nodeId).size / 2;\n        const offset = (isLeaf ? 1 : -1) * (nodeHalfWidth + this.options.offset);\n\n        const labelTransform: TransformArray = [\n          ['translate', offset * Math.cos(radian), offset * Math.sin(radian)],\n          ['rotate', isLeft ? rad2deg(radian) + 180 : rad2deg(radian)],\n        ];\n\n        model.updateNodeData([\n          {\n            id: datum.id,\n            style: {\n              labelTextAlign: isLeft === isLeaf ? 'right' : 'left',\n              labelTextBaseline: 'middle',\n              labelTransform,\n            },\n          },\n        ]);\n      });\n\n      graph.draw();\n    }\n  }\n  ```\n\n  <embed src=\"@/common/manual/custom-extension/transform/circular-radial-labels.md\"></embed>\n\n- **说明**\n  上面的实现基本是参考内置数据处理器 [PlaceRadialLabels](/manual/transform/place-radial-labels) 来实现的，区别是这里的实现是通过拿到布局中心来计算偏移和旋转，具体可参考 PlaceRadialLabels 的 [源码](https://github.com/antvis/G6/blob/v5/packages/g6/src/transforms/place-radial-labels.ts)\n"
  },
  {
    "path": "packages/site/docs/manual/transform/overview.en.md",
    "content": "---\ntitle: Data Transformation Overview\norder: 0\n---\n\n## Overview\n"
  },
  {
    "path": "packages/site/docs/manual/transform/overview.zh.md",
    "content": "---\ntitle: 数据处理总览\norder: 0\n---\n\n## 什么是数据处理\n\n数据处理（ `transform` ），也叫数据转换器，是 G6 提供的支持在 **渲染前( `beforeDraw` )** 或者 **布局后( `afterLayout` )** 对绘制数据进行转化处理的机制，用户可以通过数据处理很方便地对数据处理逻辑进行封装解耦。\n\n## 实现原理\n\n### 基类\n\n所有的数据处理器都是基于 [BaseTransform](https://github.com/antvis/G6/blob/v5/packages/g6/src/transforms/base-transform.ts) 这个基类进行实现，里面定义了两个基类方法 `beforeDraw` 和 `afterLayout` ：\n\n```typescript\nexport abstract class BaseTransform<T extends BaseTransformOptions = BaseTransformOptions> extends BaseExtension<T> {\n  public beforeDraw(data: DrawData, context: DrawContext): DrawData {\n    return data;\n  }\n\n  public afterLayout(type: 'pre', data: DrawData): void;\n  public afterLayout(type: 'post', data?: undefined): void;\n  public afterLayout(type: 'pre' | 'post', data?: DrawData) {}\n}\n```\n\n以下是这两个方法里核心的参数类型说明：\n\n- **DrawData**\n\n  ```typescript\n  type ProcedureData = {\n    nodes: Map<ID, NodeData>;\n    edges: Map<ID, EdgeData>;\n    combos: Map<ID, ComboData>;\n  };\n\n  type DrawData = {\n    add: ProcedureData; // 本次渲染需要新增的元素\n    update: ProcedureData; // 本次渲染需要更新的元素\n    remove: ProcedureData; // 本次渲染需要移除的元素\n  };\n  ```\n\n- **pre | post**\n\n  pre：绘制前进行的布局（只会在首次布局触发）\n\n  post：完成绘制后进行的布局\n\n### 执行时机\n\n- **beforeDraw**\n\n  下面是每次渲染时数据处理的执行流程/时机：\n\n  <img width=\"300px\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Pb3kRI2yHo8AAAAAAAAAAAAAemJ7AQ/original\"/>\n\n  **详细说明：**\n\n  1. G6 在每次渲染前计算出 `add`、`update`、`remove`，分别对应为需要新增、更新、移除的元素，以下简称为 `DrawData`\n  2. 此时数据处理介入，按配置顺序执行每个数据处理的 `beforeDraw` 方法，参数则为 `DrawData`\n  3. 数据处理器中，对 `DrawData` 里面的元素数据进行改动，即可以按需对 `add`、`update`、`remove` 里面的元素数据进行修改、移除或者插入元素数据等，最终把改动后的 `DrawData` 返回给渲染主体逻辑\n  4. 在执行数据处理后，执行对应的新增、更新、移除元素的操作，完成渲染\n\n:::info{title=提示}\n触发渲染的场景分为主动和被动，列举如下：\n\n- **主动：** 用户主动调用 `graph.render()` 、 `graph.draw()` 或者在自定义插件、交互等实例里面通过上下文拿到元素控制器（ [ElementController](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/element.ts) ）实例调用 `this.context.element.draw()`，等（ `graph.render()` 和 `graph.draw()` 也是调用元素控制器的 `draw` 方法）\n- **被动：** 部分内置交互和插件有触发渲染，布局执行后也有触发渲染更新元素位置，等\n\n:::\n\n- **afterLayout**：在执行完布局计算并开始更新节点位置后，执行数据处理\n\n## 内置数据处理\n\n- **G6 提供给用户的内置数据处理如下：**\n\n各数据处理详细配置可参考 [内置数据处理文档](/manual/transform/map-node-size)。\n\n| 数据处理名称                                        | 注册类型                 | 功能描述                                             | 执行时机   |\n| --------------------------------------------------- | ------------------------ | ---------------------------------------------------- | ---------- |\n| [动态调整节点大小](/manual/transform/map-node-size) | `map-node-size`          | 根据节点中心性调整节点的大小                         | beforeDraw |\n| [径向标签](/manual/transform/place-radial-labels)   | `place-radial-labels`    | 根据径向布局自动调整节点标签样式，包括位置和旋转角度 | afterDraw  |\n| [平行边](/manual/transform/process-parallel-edges)  | `process-parallel-edges` | 处理平行边，即多条边共享同一源节点和目标节点         | beforeDraw |\n\n- **G6 内嵌的数据处理如下：**\n\n除了提供给用户选用的数据处理外， G6 也封装并使用了以下数据处理机制来实现基础功能。以下数据处理不开放给用户配置使用，默认必带（列举出来供用户有需要时点击查看并参考源码）：\n\n| 数据处理名称                                                                                                            | 注册类型                | 功能描述                                                                                                                                                      | 执行时机   |\n| ----------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |\n| [调整combo绘制顺序](https://github.com/antvis/G6/blob/v5/packages/g6/src/transforms/arrange-draw-order.ts)              | `arrange-draw-order`    | combo 嵌套时，优先绘制子 combo                                                                                                                                | beforeDraw |\n| [处理组合的展开收起](https://github.com/antvis/G6/blob/v5/packages/g6/src/transforms/collapse-expand-combo.ts)          | `collapse-expand-combo` | 收起时，移除 combo 内部元素、销毁内部边，外部边则连到收起的 combo 上；<br />展开时，反之；                                                                    | beforeDraw |\n| [处理（树图）节点的收起和展开](https://github.com/antvis/G6/blob/v5/packages/g6/src/transforms/collapse-expand-node.ts) | `collapse-expand-node`  | 绘制前，处理（树图）节点的收起和展开                                                                                                                          | beforeDraw |\n| [获取边的实际端点](https://github.com/antvis/G6/blob/v5/packages/g6/src/transforms/get-edge-actual-ends.ts)             | `get-edge-actual-ends`  | 配合`collapse-expand-combo`实现收起时，combo 外部连到内部节点的边改为连到收起的 combo 上（`collapse-expand-combo`只是在收起时判断并标记了这些边需要更新端点） | beforeDraw |\n| [更新节点、combo相关边](https://github.com/antvis/G6/blob/v5/packages/g6/src/transforms/update-related-edge.ts)         | `update-related-edges`  | 如果更新了节点/combo，则把连接的边也一起更新了                                                                                                                | beforeDraw |\n\n:::warning{title=注意}\n\n上面 G6 为实现自身基础功能使用的数据处理仅供参考，不可改动。如有需要在这些数据处理基础上做特殊处理，可通过 [自定义数据处理](#自定义数据处理) 实现。\n\n:::\n\n## 配置方式\n\n### 基本配置\n\n在图实例初始化时，通过 `transforms` 数组指定需要的数据处理：\n\n```javascript\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  // 其他配置...\n  transforms: ['process-parallel-edges'],\n});\n```\n\n### 配置数据处理参数\n\n对于需要自定义参数的数据处理，可以使用 `object` 形式配置属性：\n\n```javascript\nconst graph = new Graph({\n  // 其他配置...\n  transforms: [\n    'place-radial-labels',\n    {\n      type: 'process-parallel-edges',\n      key: 'process-parallel-edges-1',\n      mode: 'bundle',\n      distance: 30,\n    },\n  ],\n});\n```\n\n### 动态更新数据处理\n\nG6 支持在图实例运行期间动态管理数据处理：\n\n- 可以通过 [setTransforms](/api/transform#graphsettransformstransforms) 方法调整数据处理器：\n\n```javascript\n// 添加新的数据处理器\ngraph.setTransforms((transforms) => [...transforms, 'place-radial-labels']);\n\n// 移除数据处理器\ngraph.setTransforms((transforms) => transforms.filter((t) => t !== 'place-radial-labels'));\n```\n\n- 可以通过 [updateTransform](/api/transform#graphupdatetransformtransform) 方法更新数据处理的配置：\n\n```javascript\n// 更新单个数据处理器\ngraph.updateTransform({\n  key: 'process-parallel-edges-1',\n  distance: 100,\n});\n```\n\n:::warning{title=注意}\n使用`updateTransform`方法时，需要在初始化时为数据处理指定唯一的`key`。\n:::\n\n### 卸载数据处理\n\n使用 [setTransforms](/api/transform#graphsettransformstransforms) 方法同样可以卸载数据处理，将数据处理配置列表置为空即可：\n\n```javascript\n// 卸载所有数据处理器\ngraph.setTransforms([]);\n```\n\n## 自定义数据处理\n\n当内置数据处理器无法满足需求时，你可以：\n\n- 继承和扩展现有数据处理\n- 创建全新的自定义数据处理\n\n自定义数据处理需要先注册后使用。详细教程请参考 [自定义数据处理](/manual/transform/custom-transform) 文档。\n\n```javascript\nimport { register, ExtensionCategory } from '@antv/g6';\nimport { MyCustomTransform } from './my-custom-transform';\n\n// 注册自定义数据处理器\nregister(ExtensionCategory.TRANSFORM, 'my-custom-transform', MyCustomTransform);\n\n// 使用自定义数据处理\nconst graph = new Graph({\n  transforms: ['my-custom-transform'],\n});\n```\n"
  },
  {
    "path": "packages/site/docs/manual/whats-new/feature.en.md",
    "content": "---\ntitle: Feature\norder: 1\n---\n\n## 🏖️ Brand New Design Specification for Graphs\n\nG6 version 5.0 has redesigned the Options specification. While ensuring comprehensive capabilities, it optimizes the options structure to be more intuitive and easier to understand.\n\nYou only need to quickly grasp the basic [core concepts](/en/manual/graph/graph) to get started with G6 quickly and achieve graph visualization without delay.\n\n**😰 The 4.0 Options** had a complex nested structure and was less semantically capable\n\n```typescript\n{\n  defaultNode: {\n    size: 30,\n    style: {\n      fill: 'steelblue',\n      stroke: '#666',\n      lineWidth: 1\n    },\n    labelCfg: {\n      style: {\n        fill: '#fff',\n      }\n    }\n  },\n  nodeStateStyles: {\n    hover: {\n      fill: 'lightsteelblue'\n    }\n  },\n  modes: {\n    default: ['zoom-canvas', 'drag-canvas', 'drag-node'],\n  },\n}\n```\n\n**😄 The 5.0 Options** has a clear structure and is easy to understand\n\n```typescript\n{\n  node: {\n    style: {\n      size: 30,\n      fill: 'steelblue',\n      stroke: '#666',\n      lineWidth: 1\n      labelFill: '#fff',\n    },\n    state: {\n      hover: {\n        fill: 'lightsteelblue'\n      }\n    }\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n}\n```\n\n## 🔨 Brand New API Design\n\nG6 5.0 features a cleaner, easy-to-use API design that is more in line with modern front-end frameworks.\n\n## 🌲 Merging Graphs with Tree Graphs\n\nTree graphs are essentially a type of directed acyclic graph. G6 5.0 has integrated the design of graphs and tree graphs, reducing the cost of understanding and usage.\n\nNow, you can directly use `Graph` to instantiate and draw tree graphs in G6, without the need to use `TreeGraph`. You simply need to specify the layout as a tree graph layout.\n\nAdditionally, G6 provides the `treeToGraphData` utility method to help you quickly convert tree graph data into graph data.\n\n<embed src=\"@/common/manual/feature/treeToGraphData.md\"></embed>\n\n## 🌆 Multi-Renderer Support\n\nG6 5.0 employs the next-generation @antv/g rendering engine, which has been newly designed. It offers support for multiple renderers such as `Canvas`, `SVG`, and `WebGL`. Additionally, it supports the mixed use of different renderers on layered canvases.\n\n```typescript\nimport { Renderer } from '@antv/g-webgl';\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  // ... other configurations\n  // Use the WebGL renderer\n  renderer: () => new Renderer(),\n});\n```\n\n## 🚀 High-Performance Layouts\n\nG6 5.0 has adopted a brand-new layout engine, with some layouts implemented in Rust, providing higher performance for layout calculations. Additionally, there is support for WebGPU acceleration in certain layouts.\n\n> 🚀 To utilize high-performance layouts, you will need to install the `@antv/layout-wasm` package\n\n```typescript\nimport { FruchtermanLayout } from '@antv/layout-gpu';\nimport { Graph, register, ExtensionCategory } from '@antv/g6';\n\nregister(ExtensionCategory.LAYOUT, 'fruchterman-gpu', FruchtermanLayout);\n\nconst graph = new Graph({\n  // ... other configurations\n  layout: {\n    type: 'fruchterman-gpu',\n    // ... Other Layout Configurations\n  },\n});\n```\n\n## 🎨 Multiple Themes Mechanism\n\nG6 5.0 comes with two built-in themes: light and dark, and allows for flexible customization based on the use case. For details, please refer to [Custom Theme](/en/manual/custom-extension/theme).\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*gASzQbsbAaIAAAAAAAAAAAAADmJ7AQ/original\"></image>\n\n## 🌍 3D Large Graphs\n\nG6 5.0 provides 3D rendering, layout, interaction capabilities, and can be used by import 3d elements, renderer, and behaviors from `@antv/g6-extension-3D` registration, see: [Using 3D](/manual/further-reading/3d).\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ZQoEQLKazPIAAAAAAAAAAAAADmJ7AQ/original\"></image>\n\n## 💪 Plugin Optimization and Enhancement\n\nG6 5.0 has optimized and enhanced existing plugins, decoupling Graph from plugins, and providing richer capabilities while optimizing configurations.\n\nPlease visit [Plugin](/en/api/plugins/bubble-sets) to experience the capabilities of more plugins.\n\n## 💼 Optimized Package Size\n\nThanks to the well-modularized design and extension registration mechanism of G6 5.0, modules that are not used will not be packaged into the final build file, reducing the package size.\n\nCompared to 4.0, the UMD package size has been reduced from 1.8 MB to 0.96 MB, a reduction of nearly 50%.\n"
  },
  {
    "path": "packages/site/docs/manual/whats-new/feature.zh.md",
    "content": "---\ntitle: 新版本特性\norder: 1\n---\n\n## 🏖️ 全新设计图配置范式\n\nG6 5.0 重新设计了图配置范式，在保证能力完善的基础上，优化配置项结构，更加直观、易于理解。\n\n仅需快速了解基本[核心概念](/manual/graph/graph)，即可快速上手 G6，快速实现图可视化。\n\n**😰 4.0 配置项** 嵌套结构复杂，语义化能力较弱\n\n```typescript\n{\n  defaultNode: {\n    size: 30,\n    style: {\n      fill: 'steelblue',\n      stroke: '#666',\n      lineWidth: 1\n    },\n    labelCfg: {\n      style: {\n        fill: '#fff',\n      }\n    }\n  },\n  nodeStateStyles: {\n    hover: {\n      fill: 'lightsteelblue'\n    }\n  },\n  modes: {\n    default: ['zoom-canvas', 'drag-canvas', 'drag-node'],\n  },\n}\n```\n\n**😄 5.0 配置项** 结构清晰，易于理解\n\n```typescript\n{\n  node: {\n    style: {\n      size: 30,\n      fill: 'steelblue',\n      stroke: '#666',\n      lineWidth: 1\n      labelFill: '#fff',\n    },\n    state: {\n      hover: {\n        fill: 'lightsteelblue'\n      }\n    }\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n}\n```\n\n## 🔨 全新 API 设计\n\nG6 5.0 采用了更加简洁、易用的 API 设计，更加符合现代前端框架的设计风格。\n\n## 🌲 合并图与树图\n\n树图本质上是一种单向无环图，G6 5.0 融合了图与树图的设计，降低了理解和使用成本。\n\n现在，你可以在 G6 中直接使用 `Graph` 来实例化绘制树图，而不需要再使用 `TreeGraph`，仅需指定布局为树图布局即可。\n\n此外，G6 提供了 `treeToGraphData` 工具方法，帮助你快速将树图数据转换为图数据。\n\n<embed src=\"@/common/manual/feature/treeToGraphData.md\"></embed>\n\n## 🌆 多渲染器支持\n\nG6 5.0 采用了新一代设计的 @antv/g 渲染引擎，提供了 `Canvas` `SVG` `WebGL` 多种渲染器支持，并且分层画布支持不同渲染器的混合使用。\n\n```typescript\nimport { Renderer } from '@antv/g-webgl';\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  // ... 其他配置\n  // 使用 WebGL 渲染器\n  renderer: () => new Renderer(),\n});\n```\n\n## 🚀 高性能布局\n\nG6 5.0 采用了全新布局引擎，部分布局提供 Rust 实现，提供了更高性能的布局计算。另有布局支持 WebGPU 加速。\n\n> 🚀 高性能布局需要安装 `@antv/layout-wasm` 包\n\n```typescript\nimport { ForceAtlas2Layout, initThreads, supportsThreads } from '@antv/layout-wasm';\nimport { Graph, register, ExtensionCategory } from '@antv/g6';\n\nregister(ExtensionCategory.LAYOUT, 'forceatlas2-wasm', ForceAtlas2Layout);\n\nconst supported = await supportsThreads();\nconst threads = await initThreads(supported);\n\nconst graph = new Graph({\n  // ... 其他配置\n  layout: {\n    type: 'forceatlas2-wasm',\n    threads,\n    // ... 其他布局配置\n  },\n});\n```\n\n> GPU 加速布局需要安装 `@antv/layout-gpu` 包\n\n```typescript\nimport { FruchtermanLayout } from '@antv/layout-gpu';\nimport { Graph, register, ExtensionCategory } from '@antv/g6';\n\nregister(ExtensionCategory.LAYOUT, 'fruchterman-gpu', FruchtermanLayout);\n\nconst graph = new Graph({\n  // ... 其他配置\n  layout: {\n    type: 'fruchterman-gpu',\n    // ... 其他布局配置\n  },\n});\n```\n\n## 🎨 多主题机制\n\nG6 5.0 内置了亮色、暗色两套主题，并可基于使用场景进行灵活定制，具体可参考[自定义主题](/manual/theme/custom-theme)。\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*gASzQbsbAaIAAAAAAAAAAAAADmJ7AQ/original\"></image>\n\n## 🌍 3D 大图\n\nG6 5.0 提供了 3D 大图渲染、布局、交互能力，从 `@antv/g6-extension-3d` 中引入 3D 元素、渲染器、交互等注册即可使用，详见：[使用 3D](/manual/further-reading/3d)。\n\n<image width=\"300\" src=\"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ZQoEQLKazPIAAAAAAAAAAAAADmJ7AQ/original\"></image>\n\n## 💪 插件优化增强\n\nG6 5.0 对现有插件进行了优化增强，解除了 Graph 与插件之间的耦合，并优化配置的同时提供了更加丰富了能力。\n\n敬请前往[插件](/manual/plugin/overview)体验更多插件的能力。\n\n## 💼 优化包体积\n\n得益于 G6 5.0 良好的模块化的设计以及扩展注册机制，对于未使用的模块，不会被打包到最终的构建文件中，减小了包体积。\n\n与 4.0 相比，UMD 包体积从 1.8 MB 减小到 0.96 MB，减小了近 50%。\n"
  },
  {
    "path": "packages/site/docs/manual/whats-new/upgrade.en.md",
    "content": "---\ntitle: Upgrade To 5.0\norder: 6\n---\n\nThis document will guide you through the process of upgrading from G6 version `4.x` to `5.x`. If you are using version `3.x`, please upgrade to version `4.x` first.\n\n## Preparation Before Upgrade\n\n1. Please ensure that your current git branch is clean and there is no uncommitted code.\n2. Refer to the [Installation](./getting-started/installation) document to install version `5.x` and remove the dependencies for version `4.x`.\n\n## Start Upgrade\n\n### Data\n\nThe data format in the new version has changed as follows:\n\n1. All style attributes in `nodes`, `edges`, and `combos` need to be placed within `style`, and data attributes should be stored in `data`:\n\n```typescript\n// 4.x\nconst data = {\n  nodes: [\n    { id: 'node1', label: 'node1', size: 20 },\n    { id: 'node2', label: 'node2', size: 20 },\n  ],\n  edges: [{ source: 'node1', target: 'node2' }],\n};\n\n// 5.x\nconst data = {\n  nodes: [\n    // The label is a non-stylistic attribute, placed in the data, and can be accessed in the style mapping function\n    // The `size` is a stylistic attribute, placed within the `style`\n    { id: 'node1', data: { label: 'node1' }, style: { size: 20 } },\n    { id: 'node2', data: { label: 'node2' }, style: { size: 20 } },\n  ],\n  edges: [{ source: 'node1', target: 'node2' }],\n};\n```\n\nSince we have redesigned and implemented the elements, please refer to the corresponding documentation to modify the new element options:\n\n- [Node](/en/api/elements/nodes/base-node)\n- [Edge](/en/api/elements/edges/base-edge)\n- [Combo](/en/api/elements/combos/base-combo)\n\n2. If you need to specify the element type in the data, you can use the `type` attribute:\n\n```typescript\n{\n  nodes: [\n    // Specify the node type as rect\n    { id: 'node1', type: 'rect' },\n  ];\n}\n```\n\n### Options\n\n<Badge type=\"warning\">Change</Badge> **fitView / fitCenter / fitViewPadding**\n\n- The `fitView` and `fitCenter` options have been merged into `autoFit`.\n- To use `fitView`, you can configure it as `autoFit: 'view'`\n- To use `fitCenter`, you can configure it as `autoFit: 'center'`\n- You can also pass an object for full configuration:\n\n```js\nautoFit: {\n  type: 'view',\n  options: {\n    // ...\n  }\n}\n```\n\n- The `fitViewPadding` has been changed to `padding`.\n\n<Badge type=\"error\">Removed</Badge> **linkCenter**\n\nIn version 5.x, the edge connection mechanism will attempt to connect to nodes/Combos in the following order:\n\n1. Connect Port\n2. Outline\n3. Center\n\n<Badge type=\"error\">Removed</Badge> **groupByTypes**\n\n<Badge type=\"error\">Removed</Badge> **autoPaint**\n\nPlease manually call the `render` or `draw` method to perform rendering.\n\n<Badge type=\"warning\">Changed</Badge> **modes**\n\nIn version 5.x, interaction modes have been removed. You can switch the currently enabled behaviors by setting `behaviors`.\n\n```typescript\n// 4.x\n{\n  modes: {\n    default: ['drag-canvas', 'zoom-canvas'],\n    preview: ['drag-canvas'],\n  },\n}\n\ngraph.setMode('preview');\n```\n\n```typescript\n\n// 5.x\n{\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n}\n\ngraph.setBehaviors(['drag-canvas']);\n\n```\n\n<Badge type=\"warning\">Change</Badge> **defaultNode / defaultEdge / defaultCombo**\n\nThe element styles have been moved to `[element].style`, for example, `defaultNode` has been changed to `node.style`:\n\n```typescript\n// 4.x\n{\n  defaultNode: {\n    size: 20,\n    fill: 'red',\n  }\n}\n\n// 5.x\n{\n  node: {\n    style: {\n      size: 20,\n      fill: 'red',\n    }\n  }\n}\n```\n\n<Badge type=\"warning\">Change</Badge> **nodeStateStyles / edgeStateStyles / comboStateStyle**\n\nElement state styles have been moved to `[element].state`, for example, `nodeStateStyles` has been changed to `node.stateStyles`:\n\n```typescript\n// 4.x\n{\n  nodeStateStyles: {\n    selected: {\n      fill: 'red',\n    }\n  }\n}\n\n// 5.x\n{\n  node: {\n    state: {\n      selected: {\n        fill: 'red',\n      }\n    }\n  }\n}\n```\n\n<Badge type=\"warning\">Change</Badge> **animate / animateCfg**\n\n- The `animate` options has been changed to `animation`\n- `animate` and `animateCfg` have been merged into `animation`\n\n```typescript\n// 4.x\n{\n  animate: true,\n}\n\n// 5.x\n{\n  animation: true,\n}\n{\n  animation: {\n    duration: 500,\n    easing: 'easeLinear',\n  }\n}\n```\n\n<Badge type=\"warning\">Change</Badge> **minZoom / maxZoom**\n\n- The `minZoom` and `maxZoom` options have been merged into `zoomRange`\n\n```typescript\n// 4.x\n{\n  minZoom: 0.5,\n  maxZoom: 2,\n}\n\n// 5.x\n{\n  zoomRange: [0.5, 2],\n}\n```\n\n<Badge type=\"warning\">Change</Badge> **renderer**\n\nG6 5.x supports multi-layer canvases and defaults to using the `canvas` renderer.\n\nThe `renderer` no longer supports the string type and has been changed to a callback function:\n\n```typescript\n// 4.x\nvar options = {\n  renderer: 'svg',\n};\n\n// 5.x\nimport { Renderer } from '@antv/g-svg';\n\n{\n  renderer: () => new Renderer(),\n}\n```\n\n<Badge type=\"error\">Removed</Badge> **enabledStack / maxStep**\n\nThe built-in undo and redo functionality has been removed in version 5.x. For related capabilities, please use a plugin to implement.\n\n### API\n\n<Badge type=\"warning\">Change</Badge> **data / save / read / changeData**\n\nVersion 5.x offers a completely new data API. For details, see [Data API](/en/api/data).\n\n- The `data` and `changeData` methods from 4.x are replaced by `setData` in 5.x.\n- The `save` method from 4.x is replaced by `getData` in 5.x.\n- The `read` method from 4.x is replaced by `setData` + `render` in 5.x.\n\n<Badge type=\"warning\">Change</Badge> **get / set**\n\nTo access Graph options, please use `getOptions` or the `getXxx` API, such as `getZoomRange`, `getBehaviors`, etc. The `set` method is analogous.\n\n<Badge type=\"warning\">Change</Badge> **getContainer**\n\nDirect API to obtain the container is not currently supported, but you can obtain it through `graph.getCanvas().getContainer()`.\n\n> In most cases, you do not need to directly manipulate the container.\n\n<Badge type=\"error\">Removed</Badge> **getGroup**\n\n<Badge type=\"warning\">Change</Badge> **getMinZoom / getMaxZoom**\n\nUse `getZoomRange` to obtain the values.\n\n<Badge type=\"warning\">Change</Badge> **setMinZoom / setMaxZoom**\n\nUse the `setZoomRange` method to set the values.\n\n<Badge type=\"warning\">Change</Badge> **getWidth / getHeight**\n\nUse `getSize` to get the dimensions.\n\n<Badge type=\"warning\">Change</Badge> **changeSize**\n\nUse `setSize` to set the dimensions.\n\n<Badge type=\"warning\">Change</Badge> **zoom**\n\nChanged to `zoomBy`.\n\n<Badge type=\"warning\">Change</Badge> **translate**\n\nChanged to `translateBy`.\n\n<Badge type=\"warning\">Change</Badge> **moveTo**\n\nChanged to `translateTo`.\n\n<Badge type=\"warning\">Change</Badge> **focusItem**\n\nChanged to `focusElement`.\n\n<Badge type=\"error\">Removed</Badge> **addItem / updateItem / removeItem**\n\nTo add or remove elements, use the methods `addData` / `updateData` / `removeData` to manipulate data.\n\n<Badge type=\"error\">Removed</Badge> **refreshItem**\n\n<Badge type=\"error\">Removed</Badge> **refreshPositions**\n\n<Badge type=\"error\">Removed</Badge> **updateCombo**\n\n<Badge type=\"error\">Removed</Badge> **updateCombos**\n\n<Badge type=\"error\">Removed</Badge> **updateComboTree**\n\n<Badge type=\"warning\">Change</Badge> **node / edge / combo**\n\nUse the `setNode` / `setEdge` / `setCombo` methods as alternatives.\n\n<Badge type=\"warning\">Change</Badge> **showItem / hideItem**\n\nUse the `setElementVisibility` method as an alternative.\n\n<Badge type=\"error\">Removed</Badge> **getNodes / getEdges / getCombos / getComboChildren / getNeighbors / find / findById / findAll / findAllByState**\n\nIn version 5.x, direct retrieval of element instances is not supported.\n\n- To obtain element data, use the methods `getData`, `getNodeData`, `getEdgeData`, `getComboData`, which support searching by element ID.\n- To obtain child node data, use the `getChildrenData` method.\n- To obtain neighbor node data, use the `getNeighborNodesData` method.\n- To find element data based on state, use the `getElementDataByState` method.\n\n<Badge type=\"warning\">Change</Badge> **collapseCombo / expandCombo**\n\nUse the `collapseElement` / `expandElement` methods as alternatives.\n\n<Badge type=\"error\">Removed</Badge> **collapseExpandCombo**\n\n<Badge type=\"error\">Removed</Badge> **createCombo**\n\nCombos can now be added using the `addData` / `addComboData` methods.\n\n<Badge type=\"error\">Removed</Badge> **uncombo**\n\nCombos can now be removed using the `removeData` / `removeComboData` methods.\n\n<Badge type=\"warning\">Change</Badge> **setItemState**\n\nUse the `setElementState` method as an alternative.\n\n<Badge type=\"error\">Removed</Badge> **clearItemStates**\n\n- To clear all states of a single element: `graph.setElementState(id, [])`\n- To clear all states of multiple elements: `graph.setElementState({ id1: [], id2: [] })`\n\n<Badge type=\"error\">Removed</Badge> **priorityState**\n\nWhen using `setElementState`, the state that appears later in the array has a higher priority.\n\n<Badge type=\"error\">Removed</Badge> **setMode**\n\nUse `setBehaviors` to set the current behaviors.\n\n<Badge type=\"error\">Removed</Badge> **setCurrentMode**\n\n<Badge type=\"warning\">Change</Badge> **layout**\n\nDoes not support parameters. To configure the layout, please use `setLayout`.\n\n<Badge type=\"warning\">Change</Badge> **updateLayout**\n\nChanged to `setLayout`.\n\n<Badge type=\"error\">Removed</Badge> **destroyLayout**\n\n<Badge type=\"warning\">Change</Badge> **addBehaviors / removeBehaviors**\n\nReplaced with `setBehaviors`.\n\n<Badge type=\"error\">Removed</Badge> **createHull / getHulls / removeHull / removeHulls**\n\n- For multiple `Hull` instances, you need to configure multiple `hull` plugins in `plugins`, such as:\n\n```typescript\n{\n  plugins: ['hull', 'hull'],\n};\n```\n\n- Operations to retrieve, update, and remove `Hull` are implemented through `setPlugins`, `updatePlugin`.\n\n<Badge>Not yet available</Badge> **getNodeDegree**\n\n<Badge>Not yet available</Badge> **getShortestPathMatrix**\n\n<Badge>Not yet available</Badge> **getAdjMatrix**\n\n<Badge type=\"error\">Removed</Badge> **pushStack / getUndoStack / getRedoStack / getStackData / clearStack**\n\nAll undo and redo related APIs should be called after obtaining the corresponding plugin, for example:\n\n```typescript\n// 'history' is the key configured for use with the plugin\nconst history = graph.getPluginInstance('history');\n\nhistory.redo();\n```\n\n<Badge type=\"error\">Removed</Badge> **positionsAnimate / stopAnimate / isAnimating**\n\nAnimation-related information is now emitted through events:\n\n- Animation start event: `beforeanimate`\n- Animation end event: `afteranimate`\n- To stop an animation:\n\n```typescript\ngraph.on('beforeanimate', (event) => {\n  event.animation.stop();\n});\n```\n\n<Badge type=\"warning\">Change</Badge> **getPointByClient / getClientByPoint / getPointByCanvas / getCanvasByPoint / getGraphCenterPoint / getViewPortCenterPoint**\n\nG6 5.x uses a different coordinate system than 4.x. For details, see [Coordinate](/en/manual/further-reading/coordinate).\n\n<Badge type=\"error\">Removed</Badge> **setTextWaterMarker / setImageWaterMarker**\n\nFor watermark functionality, please refer to the [Watermark](/en/api/plugins/watermark)plugin.\n\n<Badge type=\"warning\">Change</Badge> **toFullDataURL**\n\nReplaced with `toDataURL`, specify the parameter as: `mode: 'overall'`\n\n```typescript\ngraph.toDataURL({ mode: 'overall' });\n```\n\n<Badge type=\"error\">Removed</Badge> **downloadFullImage / downloadImage**\n\nOnly the capability to export as a `DataURL` is provided. If you need to download an image, please refer to the following example code:\n\n```typescript\nasync function downloadImage() {\n  const dataURL = await graph.toDataURL();\n  const [head, content] = dataURL.split(',');\n  const contentType = head.match(/:(.*?);/)![1];\n\n  const bstr = atob(content);\n  let length = bstr.length;\n  const u8arr = new Uint8Array(length);\n\n  while (length--) {\n    u8arr[length] = bstr.charCodeAt(length);\n  }\n\n  const blob = new Blob([u8arr], { type: contentType });\n\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement('a');\n  a.href = url;\n  a.download = 'graph.png';\n  a.click();\n}\n```\n\n<Badge type=\"error\">Removed</Badge> **clear**\n\nUse `setData` + `draw` to clear data and the canvas.\n\n### Extension Registration\n\nUnlike G6 4.x, G6 5.x uses a unified extension registration function (register). You can refer to the [Extension Register](/en/manual/core-concept/extension#register-extension) to register G6 extensions.\n\nThe following G6 4.x registration functions have been deprecated:\n\n- registerNode\n- registerEdge\n- registerCombo\n- registerLayout\n- registerBehavior\n\n### Events\n\nCompared to G6 4.x, G6 5.x has the following differences in events:\n\n- The `mouse` and `touch` events have been removed and are unified under the `pointer` event.\n- The naming convention for lifecycle events is usually in the format of: `before/after` + `object/property` + `action`, for example: `beforeelementcreate` is triggered before an element is created.\n- The following events have been removed:\n  - afteractivaterelations\n  - afteradditem\n  - aftercreateedge\n  - aftergraphrefresh\n  - aftergraphrefreshposition\n  - afteritemrefresh\n  - aftermodechange\n  - afterremoveitem\n  - afterupdateitem\n  - beforeadditem\n  - beforecreateedge\n  - beforegraphrefresh\n  - beforegraphrefreshposition\n  - beforeitemrefresh\n  - beforemodechange\n  - beforeremoveitem\n  - beforeupdateitem\n  - dragnodeend\n  - nodeselectchange\n  - stackchange\n  - tooltipchange\n- The following element change events have been removed, but you can still access them through `beforeelementupdate` and `afterelementupdate`:\n  - afteritemstatechange\n  - afteritemstatesclear\n  - afteritemvisibilitychange\n  - beforeitemstatechange\n  - beforeitemstatesclear\n  - beforeitemvisibilitychange\n- The following events have been changed:\n  - The `graphstatechange` event has been changed to `beforeelementstatechange` / `afterelementstatechange`.\n  - The `viewportchange` event has been changed to `beforetransform` / `aftertransform`.\n\nFor a complete list of events, please refer to [Event](/en/api/reference/g6#event).\n"
  },
  {
    "path": "packages/site/docs/manual/whats-new/upgrade.zh.md",
    "content": "---\ntitle: 升级到 5.0\norder: 6\n---\n\n本文档将引导你从 G6 `4.x` 版本升级到 `5.x` 版本。如果你使用的是 `3.x` 版本，请先升级到 `4.x` 版本。\n\n## 升级前准备\n\n1. 请确保当前 git 分支是干净的，没有未提交的代码。\n2. 参考 [安装](/manual/getting-started/installation) 文档安装 `5.x` 版本，并移除 `4.x` 版本依赖。\n\n## 开始升级\n\n### 数据\n\n新版本的数据格式有所变化，具体如下：\n\n1. `nodes` `edges` `combos` 中所有样式属性都需要放在 `style` 中，`data` 中存放数据属性：\n\n```typescript\n// 4.x\nconst data = {\n  nodes: [\n    { id: 'node1', label: 'node1', size: 20 },\n    { id: 'node2', label: 'node2', size: 20 },\n  ],\n  edges: [{ source: 'node1', target: 'node2' }],\n};\n\n// 5.x\nconst data = {\n  nodes: [\n    // label 为非样式属性，放在 data 中，可在样式映射函数中访问\n    // size 为样式属性，放在 style 中\n    { id: 'node1', data: { label: 'node1' }, style: { size: 20 } },\n    { id: 'node2', data: { label: 'node2' }, style: { size: 20 } },\n  ],\n  edges: [{ source: 'node1', target: 'node2' }],\n};\n```\n\n由于我们重新设计实现了元素，新的元素配置项请参考相应文档进行修改：\n\n- [Node](/manual/element/node/overview)\n- [Edge](/manual/element/edge/overview)\n- [Combo](/manual/element/combo/overview)\n\n2. 如果要在数据中指定元素类型，可以使用 `type` 属性：\n\n```typescript\n{\n  nodes: [\n    // 指定节点类型为 rect\n    { id: 'node1', type: 'rect' },\n  ];\n}\n```\n\n### 配置项\n\n<Badge type=\"warning\">变更</Badge> **fitView / fitCenter / fitViewPadding**\n\n- `fitView` 和 `fitCenter` 配置项已经合并为 `autoFit`\n- 若要使用 `fitView`，可以配置为 `autoFit: 'view'`\n- 若要使用 `fitCenter`，可以配置为 `autoFit: 'center'`\n- 也可以传入对象进行完整配置：\n\n```js\nautoFit: {\n  type: 'view',\n  options: {\n    // ...\n  }\n}\n```\n\n- `fitViewPadding` 已变更为 `padding`\n\n<Badge type=\"error\">移除</Badge> **linkCenter**\n\n5.x 的边连接机制会按照如下顺序依次尝试连接到节点/Combo：\n\n1. 连接桩\n2. 轮廓\n3. 中心\n\n<Badge type=\"error\">移除</Badge> **groupByTypes**\n\n<Badge type=\"error\">移除</Badge> **autoPaint**\n\n请手动调用 `render` 或 `draw` 方法进行绘制。\n\n<Badge type=\"warning\">变更</Badge> **modes**\n\n5.x 已经移除交互模式，你可以通过设置 `behaviors` 来切换当前启用的交互行为。\n\n```typescript\n// 4.x\n{\n  modes: {\n    default: ['drag-canvas', 'zoom-canvas'],\n    preview: ['drag-canvas'],\n  },\n}\n\ngraph.setMode('preview');\n```\n\n```typescript\n\n// 5.x\n{\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n}\n\ngraph.setBehaviors(['drag-canvas']);\n\n```\n\n<Badge type=\"warning\">变更</Badge> **defaultNode / defaultEdge / defaultCombo**\n\n元素样式已移至 `[element].style` 中，如 `defaultNode` 变更为 `node.style`：\n\n```typescript\n// 4.x\n{\n  defaultNode: {\n    size: 20,\n    fill: 'red',\n  }\n}\n\n// 5.x\n{\n  node: {\n    style: {\n      size: 20,\n      fill: 'red',\n    }\n  }\n}\n```\n\n<Badge type=\"warning\">变更</Badge> **nodeStateStyles / edgeStateStyles / comboStateStyle**\n\n元素状态样式已移至 `[element].state` 中，如 `nodeStateStyles` 变更为 `node.stateStyles`：\n\n```typescript\n// 4.x\n{\n  nodeStateStyles: {\n    selected: {\n      fill: 'red',\n    }\n  }\n}\n\n// 5.x\n{\n  node: {\n    state: {\n      selected: {\n        fill: 'red',\n      }\n    }\n  }\n}\n```\n\n<Badge type=\"warning\">变更</Badge> **animate / animateCfg**\n\n- `animate` 配置项已变更为 `animation`\n- `animate` 和 `animateCfg` 已合并为 `animation`\n\n```typescript\n// 4.x\n{\n  animate: true,\n}\n\n// 5.x\n{\n  animation: true,\n}\n{\n  animation: {\n    duration: 500,\n    easing: 'easeLinear',\n  }\n}\n```\n\n<Badge type=\"warning\">变更</Badge> **minZoom / maxZoom**\n\n`minZoom` 和 `maxZoom` 已合并为 `zoomRange`\n\n```typescript\n// 4.x\n{\n  minZoom: 0.5,\n  maxZoom: 2,\n}\n\n// 5.x\n{\n  zoomRange: [0.5, 2],\n}\n```\n\n<Badge type=\"warning\">变更</Badge> **renderer**\n\nG6 5.x 支持多层画布，默认使用 `canvas` 渲染。\n\nrenderer 不再支持字符串类型，变更为回调函数：\n\n```typescript\n// 4.x\nvar options = {\n  renderer: 'svg',\n};\n\n// 5.x\nimport { Renderer } from '@antv/g-svg';\n\n{\n  renderer: () => new Renderer(),\n}\n```\n\n<Badge type=\"error\">移除</Badge> **enabledStack / maxStep**\n\n5.x 已移除内置撤销重做功能，相关能力请使用插件实现。\n\n### API\n\n<Badge type=\"warning\">变更</Badge> **data / save / read / changeData**\n\n5.x 提供了全新的数据 API，详见 [数据 API](/api/data)。\n\n- 4.x `data` `changeData` 方法使用 5.x `setData` 替代\n- 4.x `save` 方法使用 5.x `getData` 替代\n- 4.x `read` 方法使用 5.x `setData` + `render` 替代\n\n<Badge type=\"warning\">变更</Badge> **get / set**\n\n若要访问 Graph options，请使用 `getOptions` 或者 `getXxx` API，例如 `getZoomRange` `getBehaviors` 等。 `set` 同理。\n\n<Badge type=\"warning\">变更</Badge> **getContainer**\n\n暂不支持直接获取容器的 API，但可以通过 `graph.getCanvas().getContainer()` 获取。\n\n> 绝大部分情况下，你都不需要直接操作容器。\n\n<Badge type=\"error\">移除</Badge> **getGroup**\n\n<Badge type=\"warning\">变更</Badge> **getMinZoom / getMaxZoom**\n\n使用 `getZoomRange` 获取。\n\n<Badge type=\"warning\">变更</Badge> **setMinZoom / setMaxZoom**\n\n使用 `setZoomRange` 方法设置。\n\n<Badge type=\"warning\">变更</Badge> **getWidth / getHeight**\n\n使用 `getSize` 获取。\n\n<Badge type=\"warning\">变更</Badge> **changeSize**\n\n使用 `setSize` 设置。\n\n<Badge type=\"warning\">变更</Badge> **zoom**\n\n变更为 `zoomBy`。\n\n<Badge type=\"warning\">变更</Badge> **translate**\n\n变更为 `translateBy`。\n\n<Badge type=\"warning\">变更</Badge> **moveTo**\n\n变更为 `translateTo`。\n\n<Badge type=\"warning\">变更</Badge> **focusItem**\n\n变更为 `focusElement`。\n\n<Badge type=\"error\">移除</Badge> **addItem / updateItem / removeItem**\n\n通过 `addData` / `updateData` / `removeData` 方法操作数据来添加或删除元素。\n\n<Badge type=\"error\">移除</Badge> **refreshItem**\n\n<Badge type=\"error\">移除</Badge> **refreshPositions**\n\n<Badge type=\"error\">移除</Badge> **updateCombo**\n\n<Badge type=\"error\">移除</Badge> **updateCombos**\n\n<Badge type=\"error\">移除</Badge> **updateComboTree**\n\n<Badge type=\"warning\">变更</Badge> **node / edge / combo**\n\n使用 `setNode` / `setEdge` / `setCombo` 方法替代。\n\n<Badge type=\"warning\">变更</Badge> **showItem / hideItem**\n\n使用 `setElementVisibility` 方法替代。\n\n<Badge type=\"error\">移除</Badge> **getNodes / getEdges / getCombos / getComboChildren /getNeighbors /find /findById / findAll /findAllByState**\n\n5.x 不支持直接获取元素实例。\n\n- 若要获取元素数据，使用 `getData` `getNodeData` `getEdgeData` `getComboData` 方法，支持传入元素 id 进行查找。\n- 获取子节点数据，使用 `getChildrenData` 方法。\n- 获取邻居节点数据，使用 `getNeighborNodesData` 方法。\n- 基于状态查找元素数据，使用 `getElementDataByState`。\n\n<Badge type=\"warning\">变更</Badge> **collapseCombo / expandCombo**\n\n使用 `collapseElement` / `expandElement` 方法替代。\n\n<Badge type=\"error\">移除</Badge> **collapseExpandCombo**\n\n<Badge type=\"error\">移除</Badge> **createCombo**\n\n通过 `addData` / `addComboData` 方法添加 Combo。\n\n<Badge type=\"error\">移除</Badge> **uncombo**\n\n通过 `removeData` / `removeComboData` 方法移除 Combo。\n\n<Badge type=\"warning\">变更</Badge> **setItemState**\n\n使用 `setElementState` 方法替代。\n\n<Badge type=\"error\">移除</Badge> **clearItemStates**\n\n- 清除单个元素所有状态：`graph.setElementState(id, [])`\n- 清除多个元素所有状态：`graph.setElementState({ id1: [], id2: [] })`\n\n<Badge type=\"error\">移除</Badge> **priorityState**\n\n`setElementState` 时状态数组中靠后的状态优先级更高。\n\n<Badge type=\"error\">移除</Badge> **setMode**\n\n使用 `setBehaviors` 来设置当前交互。\n\n<Badge type=\"error\">移除</Badge> **setCurrentMode**\n\n<Badge type=\"warning\">变更</Badge> **layout**\n\n不支持参数，如需配置布局，请使用 `setLayout`。\n\n<Badge type=\"warning\">变更</Badge> **updateLayout**\n\n变更为 `setLayout`。\n\n<Badge type=\"error\">移除</Badge> **destroyLayout**\n\n<Badge type=\"warning\">变更</Badge> **addBehaviors / removeBehaviors**\n\n使用 `setBehaviors` 替代。\n\n<Badge type=\"error\">移除</Badge> **createHull / getHulls / removeHull / removeHulls**\n\n- 多个 `Hull` 需在 `plugins` 中配置多个 `hull` 插件，如：\n\n```typescript\n{\n  plugins: ['hull', 'hull'],\n};\n```\n\n- `Hull` 的获取、更新、移除操作通过 `setPlugins`, `updatePlugin` 实现。\n\n<Badge>暂未提供</Badge> **getNodeDegree**\n\n<Badge>暂未提供</Badge> **getShortestPathMatrix**\n\n<Badge>暂未提供</Badge> **getAdjMatrix**\n\n<Badge type=\"error\">移除</Badge> **pushStack / getUndoStack / getRedoStack / getStackData / clearStack**\n\n所有撤销重做相关 API 请获取到对应插件后调用 API，例：\n\n```typescript\n// 'history' 为使用插件时配置的 key\nconst history = graph.getPluginInstance('history');\n\nhistory.redo();\n```\n\n<Badge type=\"error\">移除</Badge> **positionsAnimate / stopAnimate / isAnimating**\n\n动画相关信息通过事件抛出：\n\n- 动画开始事件：`beforeanimate`\n- 动画结束事件：`afteranimate`\n- 停止动画：\n\n```typescript\ngraph.on('beforeanimate', (event) => {\n  event.animation.stop();\n});\n```\n\n<Badge type=\"warning\">变更</Badge> **getPointByClient / getClientByPoint / getPointByCanvas / getCanvasByPoint / getGraphCenterPoint / getViewPortCenterPoint**\n\nG6 5.x 采用了与 4.x 不同的坐标系，详见 [坐标系](/manual/further-reading/coordinate)。\n\n<Badge type=\"error\">移除</Badge> **setTextWaterMarker / setImageWaterMarker**\n\n要使用水印功能，请参考 [水印](/manual/plugin/watermark)插件。\n\n<Badge type=\"warning\">变更</Badge> **toFullDataURL**\n\n使用 `toDataURL` 替代，指定参数为：`mode: 'overall'`\n\n```typescript\ngraph.toDataURL({ mode: 'overall' });\n```\n\n<Badge type=\"error\">移除</Badge> **downloadFullImage / downloadImage**\n\n仅提供导出为 `DataURL` 的能力，如需下载图片，请参考如下实例代码：\n\n```typescript\nasync function downloadImage() {\n  const dataURL = await graph.toDataURL();\n  const [head, content] = dataURL.split(',');\n  const contentType = head.match(/:(.*?);/)![1];\n\n  const bstr = atob(content);\n  let length = bstr.length;\n  const u8arr = new Uint8Array(length);\n\n  while (length--) {\n    u8arr[length] = bstr.charCodeAt(length);\n  }\n\n  const blob = new Blob([u8arr], { type: contentType });\n\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement('a');\n  a.href = url;\n  a.download = 'graph.png';\n  a.click();\n}\n```\n\n<Badge type=\"error\">移除</Badge> **clear**\n\n使用 `setData` + `draw` 清空数据和画布。\n\n### 扩展注册\n\n与 G6 4.x 不同，G6 5.x 使用的统一的扩展注册函数(register)，你可以参考 [注册扩展](/manual/graph/extension#注册扩展) 来注册 G6 扩展。\n\n下列 G6 4.x 的注册函数已经废除：\n\n- registerNode\n- registerEdge\n- registerCombo\n- registerLayout\n- registerBehavior\n\n### 事件\n\n与 G6 4.x 相比，G6 5.x 的事件但存下如下差异：\n\n- 移除了 `mouse` 和 `touch` 事件，统一使用 `pointer` 事件\n- 生命周期事件名命名格式通常为： `before/after` + `对象/属性` + `操作`，例如：`beforeelementcreate` 表示在创建元素前触发\n- 下列事件已被移除：\n  - afteractivaterelations\n  - afteradditem\n  - aftercreateedge\n  - aftergraphrefresh\n  - aftergraphrefreshposition\n  - afteritemrefresh\n  - aftermodechange\n  - afterremoveitem\n  - afterupdateitem\n  - beforeadditem\n  - beforecreateedge\n  - beforegraphrefresh\n  - beforegraphrefreshposition\n  - beforeitemrefresh\n  - beforemodechange\n  - beforeremoveitem\n  - beforeupdateitem\n  - dragnodeend\n  - nodeselectchange\n  - stackchange\n  - tooltipchange\n- 下列元素变更事件被移除，但你仍可通过 `beforeelementupdate` 和 `afterelementupdate` 获取：\n  - afteritemstatechange\n  - afteritemstatesclear\n  - afteritemvisibilitychange\n  - beforeitemstatechange\n  - beforeitemstatesclear\n  - beforeitemvisibilitychange\n- 下列事件有所变更：\n  - graphstatechange 事件变更为 beforeelementstatechange / afterelementstatechange\n  - viewportchange 事件变更为 beforetransform / aftertransform\n\n完整的事件列表请参考 [事件](/api/event)。\n"
  },
  {
    "path": "packages/site/examples/algorithm/case/demo/label-propagation.js",
    "content": "import { labelPropagation } from '@antv/algorithm';\nimport { Graph } from '@antv/g6';\n\nconst colors = [\n  '#5F95FF',\n  '#61DDAA',\n  '#65789B',\n  '#F6BD16',\n  '#7262FD',\n  '#78D3F8',\n  '#9661BC',\n  '#F6903D',\n  '#008685',\n  '#F08BB4',\n];\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n      layout: {\n        type: 'force',\n        linkDistance: 50,\n        animation: false,\n      },\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      gui.add(\n        {\n          Cluster: () => {\n            const clusteredData = labelPropagation(data, false);\n            const result = clusteredData.clusters\n              .map((cluster, i) => {\n                const color = colors[i % colors.length];\n                const nodes = cluster.nodes.map((node) => ({\n                  id: node.id,\n                  style: {\n                    fill: color,\n                  },\n                }));\n                return nodes;\n              })\n              .flat();\n            graph.updateNodeData(result);\n            graph.draw();\n          },\n        },\n        'Cluster',\n      );\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/algorithm/case/demo/louvain.js",
    "content": "import { louvain } from '@antv/algorithm';\nimport { Graph } from '@antv/g6';\n\nconst colors = [\n  '#5F95FF',\n  '#61DDAA',\n  '#65789B',\n  '#F6BD16',\n  '#7262FD',\n  '#78D3F8',\n  '#9661BC',\n  '#F6903D',\n  '#008685',\n  '#F08BB4',\n];\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n      layout: {\n        type: 'force',\n        linkDistance: 50,\n        animation: false,\n      },\n    });\n    graph.render();\n\n    window.addPanel((gui) => {\n      gui.add(\n        {\n          Cluster: () => {\n            const clusteredData = louvain(data, false);\n            const result = clusteredData.clusters\n              .map((cluster, i) => {\n                const color = colors[i % colors.length];\n                const nodes = cluster.nodes.map((node) => ({\n                  id: node.id,\n                  style: {\n                    fill: color,\n                  },\n                }));\n                return nodes;\n              })\n              .flat();\n            graph.updateNodeData(result);\n            graph.draw();\n          },\n        },\n        'Cluster',\n      );\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/algorithm/case/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"pattern-matching.js\",\n      \"title\": {\n        \"zh\": \"图模式匹配\",\n        \"en\": \"Graph pattern matching\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*a9eSQa4f4Y4AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"shortest-path.js\",\n      \"title\": {\n        \"zh\": \"最短路径\",\n        \"en\": \"Shortest path\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*wQy2TZkb-VgAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"label-propagation.js\",\n      \"title\": {\n        \"zh\": \"LP 自动聚类\",\n        \"en\": \"LP automatic clustering\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*1QHsR4wHCKwAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"louvain.js\",\n      \"title\": {\n        \"zh\": \"LOUVAIN 自动聚类\",\n        \"en\": \"LOUVAIN automatic clustering\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*1QHsR4wHCKwAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/algorithm/case/demo/pattern-matching.js",
    "content": "import { GADDI } from '@antv/algorithm';\nimport { Graph } from '@antv/g6';\n\nconst pattern = {\n  nodes: [\n    {\n      id: 'pn0',\n      cluster: 'nodeType-0',\n    },\n    {\n      id: 'pn1',\n      cluster: 'nodeType-1',\n    },\n    {\n      id: 'pn2',\n      cluster: 'nodeType-2',\n    },\n  ],\n  edges: [\n    { source: 'pn1', target: 'pn0', cluster: 'edgeType-1' },\n    { source: 'pn1', target: 'pn2', cluster: 'edgeType-0' },\n    { source: 'pn2', target: 'pn0', cluster: 'edgeType-2' },\n  ],\n};\n\nfetch('https://assets.antv.antgroup.com/g6/gaddi.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n      node: {\n        style: {\n          labelPlacement: 'center',\n          labelText: (d) => d.label,\n          stroke: '#5F95FF',\n          lineWidth: 1,\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n          color: ['#5F95FF', '#61DDAA', '#65789B'],\n        },\n      },\n      edge: {\n        style: {\n          endArrow: true,\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n          color: ['#5F95FF', '#61DDAA', '#65789B'],\n        },\n      },\n      plugins: [\n        {\n          type: 'legend',\n          nodeField: 'cluster',\n          position: 'bottom',\n        },\n        {\n          key: 'hull-0',\n          type: 'hull',\n          members: [],\n        },\n        {\n          key: 'hull-1',\n          type: 'hull',\n          members: [],\n        },\n      ],\n    });\n    graph.render();\n\n    window.addPanel((gui) => {\n      gui.add(\n        {\n          match: () => {\n            const matches = GADDI(data, pattern, true, undefined, undefined, 'cluster', 'cluster');\n            matches.forEach((match, i) => {\n              graph.updatePlugin({\n                key: `hull-${i}`,\n                members: match.nodes.map((node) => node.id),\n              });\n            });\n            graph.render();\n          },\n        },\n        'match',\n      );\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/algorithm/case/demo/shortest-path.js",
    "content": "import { findShortestPath } from '@antv/algorithm';\nimport { CanvasEvent, Graph } from '@antv/g6';\n\nconst format = ({ nodes, edges }) => {\n  return {\n    nodes: nodes.map((node) => ({\n      ...node,\n      style: {\n        x: node.x,\n        y: node.y,\n      },\n    })),\n    edges,\n  };\n};\n\nfetch('https://gw.alipayobjects.com/os/bmw-prod/b0ca4b15-bd0c-43ec-ae41-c810374a1d55.json')\n  .then((res) => res.json())\n  .then(format)\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      animation: false,\n      data,\n      node: {\n        style: {\n          size: 12,\n        },\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', { type: 'click-select', multiple: true }],\n      autoFit: 'view',\n    });\n\n    graph.render();\n\n    const resetStates = () => {\n      graph.setElementState(Object.fromEntries([...data.nodes, ...data.edges].map((element) => [element.id, []])));\n    };\n\n    graph.on(CanvasEvent.CLICK, () => {\n      resetStates();\n    });\n\n    window.addPanel((gui) => {\n      gui.add(\n        {\n          Help: () => {\n            alert(\"Press 'shift' to select source and target nodes \\n按住 'shift' 选取起点和终点\");\n          },\n        },\n        'Help',\n      );\n      gui.add(\n        {\n          Search: () => {\n            const nodes = graph.getElementDataByState('node', 'selected');\n            if (nodes.length !== 2) {\n              alert('Please select 2 nodes!\\n请选择两个节点！');\n              return;\n            }\n            const [source, target] = nodes;\n            const { length, path } = findShortestPath(data, source.id, target.id);\n            if (length === Infinity) {\n              alert('No path found!\\n未找到路径！');\n              return;\n            }\n\n            const states = {};\n            data.nodes.forEach(({ id }) => {\n              if (path.includes(id)) states[id] = 'highlight';\n              else states[id] = 'inactive';\n            });\n\n            data.edges.forEach(({ id, source, target }) => {\n              const sourceIndex = path.indexOf(source);\n              const targetIndex = path.indexOf(target);\n              if (sourceIndex === -1 || targetIndex === -1) return;\n              if (Math.abs(sourceIndex - targetIndex) === 1) states[id] = 'highlight';\n              else states[id] = 'inactive';\n            });\n\n            graph.setElementState(states);\n            graph.frontElement(path);\n          },\n        },\n        'Search',\n      );\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/algorithm/case/index.en.md",
    "content": "---\ntitle: Graph Algorithm Practice\norder: 5\n---\n"
  },
  {
    "path": "packages/site/examples/algorithm/case/index.zh.md",
    "content": "---\ntitle: 图算法应用\norder: 5\n---\n"
  },
  {
    "path": "packages/site/examples/animation/basic/demo/combo-collapse-expand.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', combo: 'combo-2', style: { x: 120, y: 100 } },\n      { id: 'node-2', combo: 'combo-1', style: { x: 300, y: 200 } },\n      { id: 'node-3', combo: 'combo-1', style: { x: 200, y: 300 } },\n    ],\n    edges: [\n      { id: 'edge-1', source: 'node-1', target: 'node-2' },\n      { id: 'edge-2', source: 'node-2', target: 'node-3' },\n    ],\n    combos: [\n      {\n        id: 'combo-1',\n        type: 'rect',\n        combo: 'combo-2',\n        style: {\n          collapsed: true,\n        },\n      },\n      { id: 'combo-2' },\n    ],\n  },\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  combo: {\n    style: {\n      labelText: (d) => d.id,\n      lineDash: 0,\n      collapsedLineDash: [5, 5],\n    },\n  },\n  behaviors: [{ type: 'drag-element' }, 'collapse-expand'],\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = {\n    collapse: () => {\n      graph.collapseElement('combo-1');\n    },\n    expand: () => {\n      graph.expandElement('combo-1');\n    },\n  };\n  gui.add(config, 'collapse');\n  gui.add(config, 'expand');\n});\n"
  },
  {
    "path": "packages/site/examples/animation/basic/demo/enter-edge-path-in.js",
    "content": "import { ExtensionCategory, Graph, Line, register } from '@antv/g6';\n\nclass PathInLine extends Line {\n  onCreate() {\n    const shape = this.shapeMap.key;\n    const length = shape.getTotalLength();\n    shape.animate([{ lineDash: [0, length] }, { lineDash: [length, 0] }], {\n      duration: 500,\n      fill: 'both',\n    });\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'path-in-line', PathInLine);\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 50, y: 50 } },\n      { id: 'node-1', style: { x: 200, y: 50 } },\n    ],\n  },\n  edge: {\n    type: 'path-in-line',\n    animation: {\n      // disable default enter and exit animation\n      enter: false,\n      exit: false,\n    },\n  },\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = {\n    connect: () => {\n      const edge = graph.getEdgeData('edge-1');\n      if (edge) {\n        alert('The edge already exists.');\n        return;\n      }\n\n      graph.addEdgeData([{ id: 'edge-1', source: 'node-0', target: 'node-1' }]);\n      graph.draw();\n    },\n    disconnect: () => {\n      const edge = graph.getEdgeData('edge-1');\n      if (edge) {\n        graph.removeEdgeData(['edge-1']);\n        graph.draw();\n      }\n    },\n  };\n  gui.add(config, 'connect');\n  gui.add(config, 'disconnect');\n});\n"
  },
  {
    "path": "packages/site/examples/animation/basic/demo/enter.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 50, y: 50 } },\n      { id: 'node-1', style: { x: 200, y: 50 } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  node: {\n    animation: {\n      enter: [\n        {\n          fields: ['opacity'],\n          duration: 1000,\n          easing: 'linear',\n        },\n      ],\n    },\n  },\n  edge: {\n    animation: {\n      enter: [\n        {\n          fields: ['opacity'],\n          duration: 1000,\n          easing: 'linear',\n        },\n      ],\n    },\n  },\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = {\n    index: 2,\n    duration: 1000,\n    easing: 'linear',\n    add: () => {\n      const { index } = config;\n      const y = 50 + 25 * index;\n      graph.addData({\n        nodes: [\n          { id: `node-${index + 1}`, style: { x: 50, y } },\n          { id: `node-${index + 2}`, style: { x: 200, y } },\n        ],\n        edges: [{ source: `node-${index + 1}`, target: `node-${index + 2}` }],\n      });\n      graph.draw();\n      config.index += 2;\n    },\n  };\n\n  const updateMapper = (key, value) => {\n    const { node, edge } = graph.getOptions();\n    node.animation.enter[0][key] = value;\n    edge.animation.enter[0][key] = value;\n    graph.setNode(node);\n    graph.setEdge(edge);\n  };\n\n  gui.add(config, 'duration', 500, 5000, 100).onChange((duration) => {\n    updateMapper('duration', duration);\n  });\n  // see: https://g.antv.antgroup.com/en/api/animation/waapi#easing-1\n  gui.add(config, 'easing', ['linear', 'ease-in-sine', 'ease-in-cubic']).onChange((easing) => {\n    updateMapper('easing', easing);\n  });\n  gui.add(config, 'add').name('Add Element');\n});\n"
  },
  {
    "path": "packages/site/examples/animation/basic/demo/exit.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: Array.from({ length: 10 }, (_, i) => ({\n      id: `node-${i}`,\n      style: { x: i % 2 === 0 ? 50 : 200, y: 25 + 50 * Math.floor(i / 2) },\n    })),\n    edges: Array.from({ length: 5 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${i * 2}`,\n      target: `node-${i * 2 + 1}`,\n    })),\n  },\n  node: {\n    animation: {\n      exit: [\n        {\n          fields: ['opacity'],\n          duration: 1000,\n          easing: 'linear',\n        },\n      ],\n    },\n  },\n  edge: {\n    animation: {\n      exit: [\n        {\n          fields: ['opacity'],\n          duration: 1000,\n          easing: 'linear',\n        },\n      ],\n    },\n  },\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = {\n    index: 4,\n    duration: 1000,\n    easing: 'linear',\n    remove: () => {\n      const { index } = config;\n      if (index === -1) return;\n      graph.removeData({\n        nodes: [`node-${index * 2}`, `node-${index * 2 + 1}`],\n        edges: [`edge-${index}`],\n      });\n      graph.draw();\n      config.index--;\n    },\n  };\n  const updateMapper = (key, value) => {\n    const { node, edge } = graph.getOptions();\n    node.animation.exit[0][key] = value;\n    edge.animation.exit[0][key] = value;\n    graph.setNode(node);\n    graph.setEdge(edge);\n  };\n  gui.add(config, 'duration', 500, 5000, 100).onChange((duration) => {\n    updateMapper('duration', duration);\n  });\n  // see: https://g.antv.antgroup.com/en/api/animation/waapi#easing-1\n  gui.add(config, 'easing', ['linear', 'ease-in-sine', 'ease-in-cubic']).onChange((easing) => {\n    updateMapper('easing', easing);\n  });\n  gui.add(config, 'remove').name('Remove Element');\n});\n"
  },
  {
    "path": "packages/site/examples/animation/basic/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"enter.js\",\n      \"title\": {\n        \"zh\": \"入场动画\",\n        \"en\": \"Enter Animation\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*f9JTTKw-VNwAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"enter-edge-path-in.js\",\n      \"title\": {\n        \"zh\": \"边路径入场\",\n        \"en\": \"Edge Path In\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*R895Tq952I0AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"update.js\",\n      \"title\": {\n        \"zh\": \"更新动画\",\n        \"en\": \"Update Animation\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*C1wjRrz-gC8AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"exit.js\",\n      \"title\": {\n        \"zh\": \"退场动画\",\n        \"en\": \"Exit Animation\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*UGu1Qolzzc4AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"combo-collapse-expand.js\",\n      \"title\": {\n        \"zh\": \"组合展开/收起\",\n        \"en\": \"Combo Collapse/Expand\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*wzK3T6oBEg0AAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/animation/basic/demo/update.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 50, y: 50 } },\n      { id: 'node-1', style: { x: 200, y: 50 } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  node: {\n    animation: {\n      update: [{ fields: ['x', 'y', 'size'] }, { fields: ['fill'], shape: 'key' }],\n    },\n  },\n  edge: {\n    animation: {\n      update: [{ fields: ['sourceNode', 'targetNode'] }, { fields: ['stroke', 'lineWidth'], shape: 'key' }],\n    },\n  },\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const colors = ['red', 'blue', 'green', 'yellow', 'black', 'purple', 'orange', 'gray'];\n  let [nextOffsetY, nextSize, nextLineWidth] = [50, 50, 5];\n  const config = {\n    color: () => {\n      const color = colors[Math.floor(Math.random() * colors.length)];\n      graph.updateData({\n        nodes: [\n          { id: 'node-0', style: { fill: color } },\n          { id: 'node-1', style: { fill: color } },\n        ],\n        edges: [{ source: 'node-0', target: 'node-1', style: { stroke: color } }],\n      });\n      graph.draw();\n    },\n    position: () => {\n      const offsetY = nextOffsetY;\n      graph.translateElementBy({\n        'node-0': [0, offsetY],\n        'node-1': [0, offsetY],\n      });\n      nextOffsetY = -nextOffsetY;\n    },\n    size: () => {\n      const size = nextSize;\n      const lineWidth = nextLineWidth;\n      graph.updateData({\n        nodes: [\n          { id: 'node-0', style: { size } },\n          { id: 'node-1', style: { size } },\n        ],\n        edges: [{ source: 'node-0', target: 'node-1', style: { lineWidth } }],\n      });\n      graph.draw();\n      [nextSize, nextLineWidth] = [nextSize === 50 ? 16 : 50, nextLineWidth === 5 ? 1 : 5];\n    },\n  };\n  gui.add(config, 'color').name('fill & stroke');\n  gui.add(config, 'position').name('position');\n  gui.add(config, 'size').name('size & lineWidth');\n});\n"
  },
  {
    "path": "packages/site/examples/animation/basic/index.en.md",
    "content": "---\ntitle: Basic Animation\norder: 0\n---\n"
  },
  {
    "path": "packages/site/examples/animation/basic/index.zh.md",
    "content": "---\ntitle: 基本动画\norder: 0\n---\n"
  },
  {
    "path": "packages/site/examples/animation/persistence/demo/ant-line.js",
    "content": "import { ExtensionCategory, Graph, Line, register } from '@antv/g6';\n\nclass AntLine extends Line {\n  onCreate() {\n    const shape = this.shapeMap.key;\n    shape.animate([{ lineDashOffset: -20 }, { lineDashOffset: 0 }], {\n      duration: 500,\n      iterations: Infinity,\n    });\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'ant-line', AntLine);\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 50, y: 50 } },\n      { id: 'node-2', style: { x: 200, y: 50 } },\n      { id: 'node-3', style: { x: 125, y: 150 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-2', target: 'node-3' },\n      { source: 'node-3', target: 'node-1' },\n    ],\n  },\n  edge: {\n    type: 'ant-line',\n    style: {\n      lineDash: [10, 10],\n    },\n  },\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = {\n    lineDash: 10,\n  };\n  gui.add(config, 'lineDash', 1, 20, 1).onChange((lineDash) => {\n    graph.setEdge({\n      type: 'ant-line',\n      style: {\n        lineDash: [lineDash, lineDash],\n      },\n    });\n    graph.draw();\n  });\n});\n"
  },
  {
    "path": "packages/site/examples/animation/persistence/demo/breathing-circle.js",
    "content": "import { Circle, ExtensionCategory, Graph, register } from '@antv/g6';\n\nclass BreathingCircle extends Circle {\n  onCreate() {\n    const halo = this.shapeMap.halo;\n    halo.animate([{ lineWidth: 0 }, { lineWidth: 20 }], {\n      duration: 1000,\n      iterations: Infinity,\n      direction: 'alternate',\n    });\n  }\n}\n\nregister(ExtensionCategory.NODE, 'breathing-circle', BreathingCircle);\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }],\n  },\n  node: {\n    type: 'breathing-circle',\n    style: {\n      size: 50,\n      halo: true,\n    },\n    palette: ['#3875f6', '#efb041', '#ec5b56', '#72c240'],\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/animation/persistence/demo/fly-marker.js",
    "content": "import { Circle } from '@antv/g';\nimport { Renderer } from '@antv/g-svg';\nimport { CubicHorizontal, ExtensionCategory, Graph, register, subStyleProps } from '@antv/g6';\n\nclass FlyMarkerCubic extends CubicHorizontal {\n  getMarkerStyle(attributes) {\n    return { r: 5, fill: '#c3d5f9', offsetPath: this.shapeMap.key, ...subStyleProps(attributes, 'marker') };\n  }\n\n  onCreate() {\n    const marker = this.upsert('marker', Circle, this.getMarkerStyle(this.attributes), this);\n    marker.animate([{ offsetDistance: 0 }, { offsetDistance: 1 }], {\n      duration: 3000,\n      iterations: Infinity,\n    });\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'fly-marker-cubic', FlyMarkerCubic);\n\nconst graph = new Graph({\n  container: 'container',\n  renderer: () => new Renderer(),\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 50, y: 50 } },\n      { id: 'node-1', style: { x: 200, y: 200 } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  edge: {\n    type: 'fly-marker-cubic',\n    style: {\n      lineDash: [10, 10],\n    },\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/animation/persistence/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"ant-line.js\",\n      \"title\": {\n        \"zh\": \"蚂蚁线\",\n        \"en\": \"Ant Line\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*EonQSohWxtYAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"fly-marker.js\",\n      \"title\": {\n        \"zh\": \"飞行标记\",\n        \"en\": \"Fly Marker\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*CN6ZQ4M8c80AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"breathing-circle.js\",\n      \"title\": {\n        \"zh\": \"呼吸效果\",\n        \"en\": \"Fly Marker\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*b-o-R5irgzsAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"ripple-circle.js\",\n      \"title\": {\n        \"zh\": \"涟漪效果\",\n        \"en\": \"Ripple\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*blf6TZXDzckAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/animation/persistence/demo/path-in.js",
    "content": "import { ExtensionCategory, Graph, Line, register } from '@antv/g6';\n\nclass PathInLine extends Line {\n  onCreate() {\n    const shape = this.shapeMap.key;\n    const length = shape.getTotalLength();\n    console.log('shape', shape);\n    shape.animate([{ lineDash: [0, length] }, { lineDash: [length, 0] }], {\n      duration: 500,\n      fill: 'both',\n    });\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'path-in-line', PathInLine);\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 50, y: 50 } },\n      { id: 'node-1', style: { x: 200, y: 50 } },\n    ],\n  },\n  edge: {\n    type: 'path-in-line',\n    animation: {\n      enter: false,\n    },\n  },\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  gui.add(\n    {\n      connect: () => {\n        graph.addEdgeData([{ source: 'node-0', target: 'node-1' }]);\n        graph.draw();\n      },\n    },\n    'connect',\n  );\n});\n"
  },
  {
    "path": "packages/site/examples/animation/persistence/demo/ripple-circle.js",
    "content": "import { Circle as CircleGeometry } from '@antv/g';\nimport { Renderer } from '@antv/g-svg';\nimport { Circle, ExtensionCategory, Graph, register } from '@antv/g6';\n\nclass RippleCircle extends Circle {\n  onCreate() {\n    const { fill } = this.attributes;\n    const r = this.shapeMap.key.style.r;\n    const length = 5;\n    const fillOpacity = 0.5;\n\n    Array.from({ length }).map((_, index) => {\n      const ripple = this.upsert(\n        `ripple-${index}`,\n        CircleGeometry,\n        {\n          r,\n          fill,\n          fillOpacity,\n        },\n        this,\n      );\n      ripple.animate(\n        [\n          { r, fillOpacity },\n          { r: r + length * 5, fillOpacity: 0 },\n        ],\n        {\n          duration: 1000 * length,\n          iterations: Infinity,\n          delay: 1000 * index,\n          easing: 'ease-cubic',\n        },\n      );\n    });\n  }\n}\n\nregister(ExtensionCategory.NODE, 'ripple-circle', RippleCircle);\n\nconst graph = new Graph({\n  container: 'container',\n  renderer: () => new Renderer(),\n  data: {\n    nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }],\n  },\n  node: {\n    type: 'ripple-circle',\n    animation: {\n      enter: false,\n    },\n    style: {\n      size: 50,\n    },\n    palette: ['#3875f6', '#efb041', '#ec5b56', '#72c240'],\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/animation/persistence/index.en.md",
    "content": "---\ntitle: Persistence Animation\norder: 1\n---\n"
  },
  {
    "path": "packages/site/examples/animation/persistence/index.zh.md",
    "content": "---\ntitle: 持续动画\norder: 1\n---\n"
  },
  {
    "path": "packages/site/examples/animation/viewport/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"zoom.js\",\n      \"title\": {\n        \"zh\": \"视口缩放\",\n        \"en\": \"Zoom Viewport\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*zZPpRoMmlvMAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"translate.js\",\n      \"title\": {\n        \"zh\": \"视口平移\",\n        \"en\": \"Translate Viewport\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*zZPpRoMmlvMAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"rotate.js\",\n      \"title\": {\n        \"zh\": \"视口旋转\",\n        \"en\": \"Rotate Viewport\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-yI2QqHqPQEAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/animation/viewport/demo/rotate.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/force.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'force',\n      },\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      const animation = {\n        duration: 500,\n        easing: 'linear',\n      };\n      const config = {\n        clockwise: () => graph.rotateBy(-10, animation),\n        anticlockwise: () => graph.rotateBy(10, animation),\n      };\n      gui.add(config, 'clockwise').name('🔁 Clockwise');\n      gui.add(config, 'anticlockwise').name('🔄 Anti-clockwise');\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/animation/viewport/demo/translate.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/force.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'force',\n      },\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      const animation = {\n        duration: 500,\n        easing: 'linear',\n      };\n      const config = {\n        Up: () => graph.translateBy([0, -50], animation),\n        Down: () => graph.translateBy([0, 50], animation),\n        Left: () => graph.translateBy([-50, 0], animation),\n        Right: () => graph.translateBy([50, 0], animation),\n      };\n      gui.add(config, 'Up').name('⬆️ Up');\n      gui.add(config, 'Down').name('⬇️ Down');\n      gui.add(config, 'Left').name('⬅️ Left');\n      gui.add(config, 'Right').name('➡️ Right');\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/animation/viewport/demo/zoom.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/force.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'force',\n      },\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      const animation = {\n        duration: 500,\n        easing: 'linear',\n      };\n      const config = {\n        zoomIn: () => {\n          graph.zoomBy(1.2, animation);\n        },\n        zoomOut: () => {\n          graph.zoomBy(0.8, animation);\n        },\n      };\n      gui.add(config, 'zoomIn');\n      gui.add(config, 'zoomOut');\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/animation/viewport/index.en.md",
    "content": "---\ntitle: Viewport Animation\norder: 2\n---\n"
  },
  {
    "path": "packages/site/examples/animation/viewport/index.zh.md",
    "content": "---\ntitle: 视口动画\norder: 2\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/auto-adapt-label/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 200, y: 100, labelText: '短标签' } },\n    { id: 'node2', style: { x: 360, y: 100, labelText: '中等长度的标签' } },\n    { id: 'node3', style: { x: 280, y: 220, labelText: '这是一个非常非常长的标签，需要自适应显示' } },\n  ],\n  edges: [\n    { source: 'node1', target: 'node2' },\n    { source: 'node1', target: 'node3' },\n    { source: 'node2', target: 'node3' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  behaviors: [\n    'zoom-canvas',\n    'drag-canvas',\n    {\n      key: 'auto-adapt-label',\n      type: 'auto-adapt-label',\n      padding: 0,\n      throttle: 200,\n    },\n  ],\n  plugins: [{ type: 'grid-line', size: 30 }],\n  animation: true,\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/auto-adapt-label/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": \"智能标签控制\",\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*FIj7T4ZtyoAAAAAAAAAAAAAAemJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/behavior/auto-adapt-label/index.en.md",
    "content": "---\ntitle: Intelligent tag control\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/auto-adapt-label/index.zh.md",
    "content": "---\ntitle: 智能标签控制\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/canvas/demo/drag.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: ['drag-canvas'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/canvas/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"drag.js\",\n      \"title\": {\n        \"zh\": \"拖拽画布\",\n        \"en\": \"Drag Canvas\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*0f81RI8DTF0AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"scroll-xy.js\",\n      \"title\": {\n        \"zh\": \"XY 轴滚动\",\n        \"en\": \"Scroll XY\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*VoBZR7Z62KEAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"scroll-y.js\",\n      \"title\": {\n        \"zh\": \"Y 轴滚动\",\n        \"en\": \"Scroll Y\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*c5DCSoAYsZQAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"zoom.js\",\n      \"title\": {\n        \"zh\": \"缩放画布\",\n        \"en\": \"Zoom Canvas\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*1AXoRY9Bw-0AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"optimize.js\",\n      \"title\": {\n        \"zh\": \"拖拽缩放画布时隐藏元素\",\n        \"en\": \"Hide Elements When Dragging and Zooming\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*AZ4IRJkIZr8AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"scroll-and-zoom.js\",\n      \"title\": {\n        \"zh\": \"滚动和缩放画布\",\n        \"en\": \"Scroll Canvas and Zoom Canvas\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*VoBZR7Z62KEAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/behavior/canvas/demo/optimize.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      labelText: (datum) => datum.id,\n    },\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'scroll-canvas', 'optimize-viewport-transform'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/canvas/demo/scroll-and-zoom.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: [\n    'scroll-canvas',\n    {\n      key: 'custom-zoom-canvas',\n      type: 'zoom-canvas',\n      enable: (event) => {\n        return event.ctrlKey; // ctrlKey 为 true 时，是双指捏合或扩张操作，false 时是双指滑动操作\n      },\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/canvas/demo/scroll-xy.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: ['scroll-canvas'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/canvas/demo/scroll-y.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: [{ type: 'scroll-canvas', direction: 'y' }],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/canvas/demo/zoom.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: ['zoom-canvas'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/canvas/index.en.md",
    "content": "---\ntitle: Canvas Manipulation\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/canvas/index.zh.md",
    "content": "---\ntitle: 画布操作\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/combo/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo1', style: { x: 350, y: 200 } },\n      { id: 'node2', combo: 'combo1', style: { x: 350, y: 250 } },\n      { id: 'node3', combo: 'combo3', style: { x: 100, y: 200 } },\n    ],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'combo1', target: 'node3' },\n    ],\n    combos: [{ id: 'combo1', combo: 'combo2' }, { id: 'combo2' }, { id: 'combo3', style: { collapsed: true } }],\n  },\n  behaviors: ['collapse-expand', 'drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/combo/demo/collapse-expand.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo1', style: { x: 300, y: 100 } },\n      { id: 'node2', combo: 'combo1', style: { x: 300, y: 150 } },\n      { id: 'node3', combo: 'combo2', style: { x: 100, y: 100 } },\n      { id: 'node4', combo: 'combo2', style: { x: 50, y: 150 } },\n      { id: 'node5', combo: 'combo2', style: { x: 150, y: 150 } },\n    ],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node3', target: 'node5' },\n    ],\n    combos: [\n      { id: 'combo1', style: { labelText: '双击折叠', collapsed: true } },\n      { id: 'combo2', style: { labelText: '单击折叠', collapsed: false } },\n    ],\n  },\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      trigger: 'dblclick',\n      enable: (event) => event.targetType === 'combo' && event.target.id === 'combo1',\n    },\n    {\n      type: 'collapse-expand',\n      trigger: 'click',\n      enable: (event) => event.targetType === 'combo' && event.target.id === 'combo2',\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/combo/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"展开/收起\",\n        \"en\": \"Expand/Collapse\"\n      },\n      \"screenshot\": \"https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*Rvx9SYSHGsIAAAAAAAAAAABkARQnAQ\"\n    },\n    {\n      \"filename\": \"collapse-expand.js\",\n      \"title\": {\n        \"zh\": \"展开/收起的触发方式\",\n        \"en\": \"Trigger of Expand/Collapse\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*zrL2RJtAKkAAAAAAAAAAAAAAemJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/behavior/combo/index.en.md",
    "content": "---\ntitle: Combo Behaviors\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/combo/index.zh.md",
    "content": "---\ntitle: 组合交互\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/create-edge/demo/between-combos.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n      { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n      { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n    ],\n    combos: [{ id: 'combo1' }, { id: 'combo2', style: { ports: [{ placement: 'center' }] } }],\n  },\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'drag',\n      style: {\n        lineWidth: 2,\n        lineDash: [2, 3],\n      },\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/create-edge/demo/by-click.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'click',\n      style: {\n        stroke: 'red',\n        lineWidth: 2,\n      },\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/create-edge/demo/by-drag.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'drag',\n      style: {\n        fill: 'red',\n        lineWidth: 2,\n      },\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/create-edge/demo/custom-edge-style.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'click',\n      onCreate: (edge) => {\n        const { style, ...rest } = edge;\n        return {\n          ...rest,\n          style: {\n            ...style,\n            stroke: 'red',\n            lineWidth: 2,\n            endArrow: true,\n          },\n        };\n      },\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/create-edge/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"by-drag.js\",\n      \"title\": {\n        \"zh\": \"通过拖拽创建边\",\n        \"en\": \"Create Edge by Drag\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*DW16QJNmRm8AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"by-click.js\",\n      \"title\": {\n        \"zh\": \"通过点击创建边\",\n        \"en\": \"Create Edge by Click\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*DW16QJNmRm8AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"between-combos.js\",\n      \"title\": {\n        \"zh\": \"创建组合间连边\",\n        \"en\": \"Create Edge Between Combos\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Uwn-RZXSXLQAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"custom-edge-style.js\",\n      \"title\": {\n        \"zh\": \"自定义边样式\",\n        \"en\": \"Custom Edge Style\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ccvHTKQ19oMAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/behavior/create-edge/index.en.md",
    "content": "---\ntitle: Create Edge\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/create-edge/index.zh.md",
    "content": "---\ntitle: 创建边\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/fix-element-size/demo/autosize-label.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node0', size: 50, label: '0', style: { x: 326, y: 268 } },\n    { id: 'node1', size: 30, label: '1', style: { x: 280, y: 384 } },\n    { id: 'node2', size: 30, label: '2', style: { x: 234, y: 167 } },\n    { id: 'node3', size: 30, label: '3', style: { x: 391, y: 368 } },\n    { id: 'node4', size: 30, label: '4', style: { x: 444, y: 209 } },\n    { id: 'node5', size: 30, label: '5', style: { x: 378, y: 157 } },\n    { id: 'node6', size: 15, label: '6', style: { x: 229, y: 400 } },\n    { id: 'node7', size: 15, label: '7', style: { x: 281, y: 440 } },\n    { id: 'node8', size: 15, label: '8', style: { x: 188, y: 119 } },\n    { id: 'node9', size: 15, label: '9', style: { x: 287, y: 157 } },\n    { id: 'node10', size: 15, label: '10', style: { x: 185, y: 200 } },\n    { id: 'node11', size: 15, label: '11', style: { x: 238, y: 110 } },\n    { id: 'node12', size: 15, label: '12', style: { x: 239, y: 221 } },\n    { id: 'node13', size: 15, label: '13', style: { x: 176, y: 160 } },\n    { id: 'node14', size: 15, label: '14', style: { x: 389, y: 423 } },\n    { id: 'node15', size: 15, label: '15', style: { x: 441, y: 341 } },\n    { id: 'node16', size: 15, label: '16', style: { x: 442, y: 398 } },\n  ],\n  edges: [\n    { source: 'node0', target: 'node1', label: '0-1' },\n    { source: 'node0', target: 'node2', label: '0-2' },\n    { source: 'node0', target: 'node3', label: '0-3' },\n    { source: 'node0', target: 'node4', label: '0-4' },\n    { source: 'node0', target: 'node5', label: '0-5' },\n    { source: 'node1', target: 'node6', label: '1-6' },\n    { source: 'node1', target: 'node7', label: '1-7' },\n    { source: 'node2', target: 'node8', label: '2-8' },\n    { source: 'node2', target: 'node9', label: '2-9' },\n    { source: 'node2', target: 'node10', label: '2-10' },\n    { source: 'node2', target: 'node11', label: '2-11' },\n    { source: 'node2', target: 'node12', label: '2-12' },\n    { source: 'node2', target: 'node13', label: '2-13' },\n    { source: 'node3', target: 'node14', label: '3-14' },\n    { source: 'node3', target: 'node15', label: '3-15' },\n    { source: 'node3', target: 'node16', label: '3-16' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n      labelMaxWidth: '200%',\n      labelWordWrap: true,\n      size: (d) => d.size,\n    },\n  },\n  edge: {\n    style: {\n      labelText: (d) => `${d.source}-${d.target}`,\n      labelWordWrap: true,\n      labelMaxLines: 2,\n      labelMaxWidth: '60%',\n    },\n  },\n  behaviors: [\n    {\n      type: 'fix-element-size',\n      key: 'fix-element-size',\n      enable: true,\n      node: [\n        {\n          shape: (shapes) =>\n            shapes.find((shape) => shape.parentElement?.className === 'label' && shape.className === 'text'),\n          fields: ['fontSize', 'lineHeight'],\n        },\n      ],\n      edge: [\n        {\n          shape: (shapes) =>\n            shapes.find((shape) => shape.parentElement?.className === 'label' && shape.className === 'text'),\n          fields: ['fontSize', 'lineHeight'],\n        },\n      ],\n    },\n    'zoom-canvas',\n    'drag-canvas',\n  ],\n  autoFit: 'center',\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/fix-element-size/demo/fix-font-size.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node0', size: 50, label: '0', style: { x: 326, y: 268 } },\n    { id: 'node1', size: 30, label: '1', style: { x: 280, y: 384 } },\n    { id: 'node2', size: 30, label: '2', style: { x: 234, y: 167 } },\n    { id: 'node3', size: 30, label: '3', style: { x: 391, y: 368 } },\n    { id: 'node4', size: 30, label: '4', style: { x: 444, y: 209 } },\n    { id: 'node5', size: 30, label: '5', style: { x: 378, y: 157 } },\n    { id: 'node6', size: 15, label: '6', style: { x: 229, y: 400 } },\n    { id: 'node7', size: 15, label: '7', style: { x: 281, y: 440 } },\n    { id: 'node8', size: 15, label: '8', style: { x: 188, y: 119 } },\n    { id: 'node9', size: 15, label: '9', style: { x: 287, y: 157 } },\n    { id: 'node10', size: 15, label: '10', style: { x: 185, y: 200 } },\n    { id: 'node11', size: 15, label: '11', style: { x: 238, y: 110 } },\n    { id: 'node12', size: 15, label: '12', style: { x: 239, y: 221 } },\n    { id: 'node13', size: 15, label: '13', style: { x: 176, y: 160 } },\n    { id: 'node14', size: 15, label: '14', style: { x: 389, y: 423 } },\n    { id: 'node15', size: 15, label: '15', style: { x: 441, y: 341 } },\n    { id: 'node16', size: 15, label: '16', style: { x: 442, y: 398 } },\n  ],\n  edges: [\n    { source: 'node0', target: 'node1', label: '0-1' },\n    { source: 'node0', target: 'node2', label: '0-2' },\n    { source: 'node0', target: 'node3', label: '0-3' },\n    { source: 'node0', target: 'node4', label: '0-4' },\n    { source: 'node0', target: 'node5', label: '0-5' },\n    { source: 'node1', target: 'node6', label: '1-6' },\n    { source: 'node1', target: 'node7', label: '1-7' },\n    { source: 'node2', target: 'node8', label: '2-8' },\n    { source: 'node2', target: 'node9', label: '2-9' },\n    { source: 'node2', target: 'node10', label: '2-10' },\n    { source: 'node2', target: 'node11', label: '2-11' },\n    { source: 'node2', target: 'node12', label: '2-12' },\n    { source: 'node2', target: 'node13', label: '2-13' },\n    { source: 'node3', target: 'node14', label: '3-14' },\n    { source: 'node3', target: 'node15', label: '3-15' },\n    { source: 'node3', target: 'node16', label: '3-16' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelBackground: true,\n      labelBackgroundFill: '#FFB6C1',\n      labelBackgroundRadius: 4,\n      labelFontFamily: 'Arial',\n      labelPadding: [0, 4],\n      labelText: (d) => d.id,\n      size: (d) => d.size,\n    },\n  },\n  behaviors: [\n    'zoom-canvas',\n    'drag-canvas',\n    {\n      key: 'fix-element-size',\n      type: 'fix-element-size',\n      enable: true,\n      node: { shape: 'label' },\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/fix-element-size/demo/fix-size.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node0', size: 50, label: '0', style: { x: 326, y: 268 }, states: ['selected'] },\n    { id: 'node1', size: 30, label: '1', style: { x: 280, y: 384 }, states: ['selected'] },\n    { id: 'node2', size: 30, label: '2', style: { x: 234, y: 167 } },\n    { id: 'node3', size: 30, label: '3', style: { x: 391, y: 368 } },\n    { id: 'node4', size: 30, label: '4', style: { x: 444, y: 209 } },\n    { id: 'node5', size: 30, label: '5', style: { x: 378, y: 157 } },\n    { id: 'node6', size: 15, label: '6', style: { x: 229, y: 400 } },\n    { id: 'node7', size: 15, label: '7', style: { x: 281, y: 440 } },\n    { id: 'node8', size: 15, label: '8', style: { x: 188, y: 119 } },\n    { id: 'node9', size: 15, label: '9', style: { x: 287, y: 157 } },\n    { id: 'node10', size: 15, label: '10', style: { x: 185, y: 200 } },\n    { id: 'node11', size: 15, label: '11', style: { x: 238, y: 110 } },\n    { id: 'node12', size: 15, label: '12', style: { x: 239, y: 221 } },\n    { id: 'node13', size: 15, label: '13', style: { x: 176, y: 160 } },\n    { id: 'node14', size: 15, label: '14', style: { x: 389, y: 423 } },\n    { id: 'node15', size: 15, label: '15', style: { x: 441, y: 341 } },\n    { id: 'node16', size: 15, label: '16', style: { x: 442, y: 398 } },\n  ],\n  edges: [\n    { source: 'node0', target: 'node1', label: '0-1', states: ['selected'] },\n    { source: 'node0', target: 'node2', label: '0-2' },\n    { source: 'node0', target: 'node3', label: '0-3' },\n    { source: 'node0', target: 'node4', label: '0-4' },\n    { source: 'node0', target: 'node5', label: '0-5' },\n    { source: 'node1', target: 'node6', label: '1-6' },\n    { source: 'node1', target: 'node7', label: '1-7' },\n    { source: 'node2', target: 'node8', label: '2-8' },\n    { source: 'node2', target: 'node9', label: '2-9' },\n    { source: 'node2', target: 'node10', label: '2-10' },\n    { source: 'node2', target: 'node11', label: '2-11' },\n    { source: 'node2', target: 'node12', label: '2-12' },\n    { source: 'node2', target: 'node13', label: '2-13' },\n    { source: 'node3', target: 'node14', label: '3-14' },\n    { source: 'node3', target: 'node15', label: '3-15' },\n    { source: 'node3', target: 'node16', label: '3-16' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.label,\n      size: (d) => d.size,\n      lineWidth: 1,\n    },\n  },\n  edge: { style: { labelText: (d) => d.label } },\n  behaviors: [\n    'zoom-canvas',\n    'drag-canvas',\n    {\n      key: 'fix-element-size',\n      type: 'fix-element-size',\n      enable: (event) => event.data.scale < 1,\n      state: 'selected',\n      reset: true,\n    },\n    { type: 'click-select', key: 'click-select', multiple: true },\n  ],\n  autoFit: 'center',\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/fix-element-size/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"fix-size.js\",\n      \"title\": {\n        \"zh\": \"缩放过程中固定选中元素\",\n        \"en\": \"Fix Element Size While Zooming\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ipfbT61guFYAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"autosize-label.js\",\n      \"title\": {\n        \"zh\": \"缩放过程中动态调整标签的最大长度\",\n        \"en\": \"Auto Size Label While Zooming\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ggOXQqiwhboAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"fix-font-size.js\",\n      \"title\": {\n        \"zh\": \"缩放过程中字号保持\",\n        \"en\": \"Fix Font Size While Zooming\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*nLPOTaYUEO4AAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/behavior/fix-element-size/index.en.md",
    "content": ""
  },
  {
    "path": "packages/site/examples/behavior/fix-element-size/index.zh.md",
    "content": "---\ntitle: 缩放画布时固定元素大小\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/focus/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', combo: 'combo1', style: { x: 110, y: 150 } },\n    { id: 'node2', combo: 'combo1', style: { x: 190, y: 150 } },\n    { id: 'node3', combo: 'combo2', style: { x: 150, y: 260 } },\n  ],\n  edges: [{ source: 'node1', target: 'node2' }],\n  combos: [{ id: 'combo1', combo: 'combo2' }, { id: 'combo2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  node: {\n    style: { labelText: (d) => d.id },\n  },\n  data,\n  behaviors: ['collapse-expand', 'focus-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/focus/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": \"点击聚焦\",\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ph6tS4k6S-oAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/behavior/focus/index.en.md",
    "content": "---\ntitle: Focus Element\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/focus/index.zh.md",
    "content": "---\ntitle: 元素聚焦\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/highlight-element/demo/activate-relations.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst format = (data) => {\n  const { nodes, edges } = data;\n  return {\n    nodes: nodes.map(({ id, ...node }) => ({ id, data: node })),\n    edges: edges.map(({ id, source, target, ...edge }) => ({ id, source, target, data: edge })),\n  };\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/xiaomi.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: format(data),\n      behaviors: [\n        {\n          type: 'hover-activate',\n          degree: 1, // 👈🏻 Activate relations.\n        },\n      ],\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        nodeSize: 24,\n      },\n      animation: false,\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/behavior/highlight-element/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst format = (data) => {\n  const { nodes, edges } = data;\n  return {\n    nodes: nodes.map(({ id, ...node }) => ({ id, data: node })),\n    edges: edges.map(({ id, source, target, ...edge }) => ({ id, source, target, data: edge })),\n  };\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/xiaomi.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: format(data),\n      behaviors: ['hover-activate'],\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        nodeSize: 24,\n      },\n      animation: false,\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/behavior/highlight-element/demo/config-params.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst format = (data) => {\n  const { nodes, edges } = data;\n  return {\n    nodes: nodes.map(({ id, ...node }) => ({ id, data: node })),\n    edges: edges.map(({ id, source, target, ...edge }) => ({ id, source, target, data: edge })),\n  };\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/xiaomi.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: format(data),\n      node: {\n        style: { size: 30 },\n        state: {\n          highlight: {\n            fill: '#D580FF',\n            halo: true,\n            lineWidth: 0,\n          },\n          dim: {\n            fill: '#99ADD1',\n          },\n        },\n      },\n      edge: {\n        state: {\n          highlight: {\n            stroke: '#D580FF',\n          },\n        },\n      },\n      behaviors: [\n        {\n          type: 'hover-activate',\n          enable: (event) => event.targetType === 'node',\n          degree: 1, // 👈🏻 Activate relations.\n          state: 'highlight',\n          inactiveState: 'dim',\n          onHover: (event) => {\n            event.view.setCursor('pointer');\n          },\n          onHoverEnd: (event) => {\n            event.view.setCursor('default');\n          },\n        },\n      ],\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        nodeSize: 24,\n      },\n      animation: false,\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/behavior/highlight-element/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"高亮元素\",\n        \"en\": \"Hover Element\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*7cgmToibajkAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"activate-relations.js\",\n      \"title\": {\n        \"zh\": \"高亮相邻节点\",\n        \"en\": \"Activate Relations\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*7cgmToibajkAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"config-params.js\",\n      \"title\": {\n        \"zh\": \"定制参数\",\n        \"en\": \"Configurations for Activate Relations\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*AdibQby3Ma0AAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/behavior/highlight-element/index.en.md",
    "content": "---\ntitle: Highlight Element\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/highlight-element/index.zh.md",
    "content": "---\ntitle: 高亮元素\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/inner-event/demo/basic.js",
    "content": "import { Circle as CircleGeometry } from '@antv/g';\nimport { CanvasEvent, Circle, ExtensionCategory, Graph, NodeEvent, register } from '@antv/g6';\n\nclass LightNode extends Circle {\n  render(attributes, container) {\n    super.render(attributes, container);\n    this.upsert('light', CircleGeometry, { r: 8, fill: '#0f0', cx: 0, cy: -25 }, container);\n  }\n}\n\nregister(ExtensionCategory.NODE, 'light', LightNode);\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 150 } },\n      { id: 'node2', style: { x: 300, y: 150 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  node: {\n    type: 'light',\n    style: {\n      size: 100,\n      labelText: (d) => d.style.labelText || 'Click the Light',\n      labelPlacement: 'center',\n      labelBackground: true,\n      labelBackgroundFill: '#fff',\n      labelBackgroundFillOpacity: 0.8,\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n\ngraph.on(NodeEvent.CLICK, (event) => {\n  const { target, originalTarget } = event;\n  if (originalTarget.className === 'light') {\n    graph.updateNodeData([{ id: target.id, states: ['selected'], style: { labelText: 'Clicked!' } }]);\n    graph.draw();\n  }\n});\n\ngraph.on(CanvasEvent.CLICK, () => {\n  const selectedIds = graph.getElementDataByState('node', 'selected').map((node) => node.id);\n  graph.updateNodeData(selectedIds.map((id) => ({ id, states: [], style: { labelText: 'Click the Light' } })));\n  graph.draw();\n});\n"
  },
  {
    "path": "packages/site/examples/behavior/inner-event/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"指定图形响应事件\",\n        \"en\": \"Specify the shape response event\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ZPt0SKT5JXQAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/behavior/inner-event/index.en.md",
    "content": "---\ntitle: Inner Element Event\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/inner-event/index.zh.md",
    "content": "---\ntitle: 元素内部事件\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/select/demo/brush-combo.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  animation: false,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  data: {\n    nodes: [\n      { id: 'node-1', combo: 'combo1', style: { x: 250, y: 150, lineWidth: 0 } },\n      { id: 'node-2', combo: 'combo1', style: { x: 350, y: 150, lineWidth: 0 } },\n      { id: 'node-3', combo: 'combo2', style: { x: 250, y: 300, lineWidth: 0 } },\n    ],\n    edges: [\n      { target: 'node-1', source: 'node-2' },\n      { target: 'node-1', source: 'node-3' },\n    ],\n    combos: [{ id: 'combo1', combo: 'combo2' }, { id: 'combo2' }],\n  },\n  behaviors: [\n    {\n      type: 'brush-select',\n      immediately: true,\n      mode: 'default',\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/select/demo/brush.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 200, y: 250 } },\n      { id: 'node-2', style: { x: 250, y: 200 } },\n      { id: 'node-3', style: { x: 300, y: 250 } },\n      { id: 'node-4', style: { x: 250, y: 300 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-2', target: 'node-3' },\n      { source: 'node-3', target: 'node-4' },\n      { source: 'node-4', target: 'node-1' },\n    ],\n  },\n  behaviors: [\n    {\n      key: 'brush-select',\n      type: 'brush-select',\n      enable: true,\n      animation: false,\n      mode: 'default', // union intersect diff default\n      state: 'selected', // 'active', 'selected', 'inactive', ...\n      trigger: [], // ['Shift', 'Alt', 'Control', 'Drag', 'Meta', ...]\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2],\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/select/demo/click.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      active: {\n        fill: '#0b0',\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'selected', // 选中的状态\n      neighborState: 'active', // 相邻节点附着状态\n      unselectedState: 'inactive', // 未选中节点状态\n      multiple: true,\n      trigger: ['shift'],\n    },\n    'drag-element',\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/select/demo/lasso.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 200, y: 250 } },\n      { id: 'node-2', style: { x: 250, y: 200 } },\n      { id: 'node-3', style: { x: 300, y: 250 } },\n      { id: 'node-4', style: { x: 250, y: 300 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-2', target: 'node-3' },\n      { source: 'node-3', target: 'node-4' },\n      { source: 'node-4', target: 'node-1' },\n    ],\n  },\n  behaviors: [\n    {\n      key: 'lasso-select',\n      type: 'lasso-select',\n      enable: true,\n      animation: false,\n      mode: 'default', // union intersect diff default\n      state: 'selected', // 'active', 'selected', 'inactive', ...\n      trigger: [], // ['Shift', 'Alt', 'Control', 'Drag', 'Meta', ...]\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2],\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/behavior/select/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"click.js\",\n      \"title\": {\n        \"zh\": \"点选\",\n        \"en\": \"Click Select\"\n      },\n      \"screenshot\": \"https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*F0RGQ67xbGYAAAAAAAAAAAAAARQnAQ\"\n    },\n    {\n      \"filename\": \"brush.js\",\n      \"title\": {\n        \"zh\": \"框选\",\n        \"en\": \"Brush Select\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*OeBHQZKfEwsAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"brush-combo.js\",\n      \"title\": {\n        \"zh\": \"框选组合\",\n        \"en\": \"Brush Combo\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*hE19RZL7iYQAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"lasso.js\",\n      \"title\": {\n        \"zh\": \"拉索选择\",\n        \"en\": \"Lasso Select\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*GSdRQ5TO9IAAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/behavior/select/index.en.md",
    "content": "---\ntitle: Select\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/select/index.zh.md",
    "content": "---\ntitle: 选中\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/update-label/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"update.js\",\n      \"title\": {\n        \"zh\": \"更新标签\",\n        \"en\": \"Update Label\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*DSVuQ6oH_m8AAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/behavior/update-label/demo/update.js",
    "content": "import { EdgeEvent, Graph, NodeEvent } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node-1',\n      style: { x: 100, y: 150, labelText: 'Hover me!' },\n    },\n    {\n      id: 'node-2',\n      style: { x: 300, y: 150, labelText: 'Hover me!' },\n    },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', style: { labelText: 'Hover me!' } }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n\ngraph.on(NodeEvent.POINTER_ENTER, (event) => {\n  const { target } = event;\n  graph.updateNodeData([\n    { id: target.id, style: { labelText: 'Hovered', fill: 'lightgreen', labelFill: 'lightgreen' } },\n  ]);\n  graph.draw();\n});\n\ngraph.on(EdgeEvent.POINTER_ENTER, (event) => {\n  const { target } = event;\n  graph.updateEdgeData([\n    { id: target.id, style: { labelText: 'Hovered', stroke: 'lightgreen', labelFill: 'lightgreen', lineWidth: 3 } },\n  ]);\n  graph.draw();\n});\n\ngraph.on(NodeEvent.POINTER_OUT, (event) => {\n  const { target } = event;\n  graph.updateNodeData([{ id: target.id, style: { labelText: 'Hover me!', fill: '#5B8FF9', labelFill: 'black' } }]);\n  graph.draw();\n});\n\ngraph.on(EdgeEvent.POINTER_OUT, (event) => {\n  const { target } = event;\n  graph.updateEdgeData([\n    { id: target.id, style: { labelText: 'Hover me!', stroke: '#5B8FF9', labelFill: 'black', lineWidth: 1 } },\n  ]);\n  graph.draw();\n});\n"
  },
  {
    "path": "packages/site/examples/behavior/update-label/index.en.md",
    "content": "---\ntitle: Update Label\n---\n"
  },
  {
    "path": "packages/site/examples/behavior/update-label/index.zh.md",
    "content": "---\ntitle: 更新标签\n---\n"
  },
  {
    "path": "packages/site/examples/element/combo/demo/circle.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n    { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n    { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n  ],\n  edges: [],\n  combos: [{ id: 'combo1', combo: 'combo2' }, { id: 'combo2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  combo: {\n    type: 'circle',\n  },\n  behaviors: ['drag-element', 'collapse-expand'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/combo/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"circle.js\",\n      \"title\": \"圆形\",\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Kbk1S5pzSY0AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"rect.js\",\n      \"title\": \"矩形\",\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*PKtgSZzmb3YAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/element/combo/demo/rect.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n    { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n    { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n  ],\n  edges: [],\n  combos: [{ id: 'combo1', combo: 'combo2' }, { id: 'combo2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  combo: {\n    type: 'rect',\n    style: {\n      padding: 20,\n    },\n  },\n  behaviors: ['drag-element', 'collapse-expand'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/combo/index.en.md",
    "content": "---\ntitle: Combos\norder: 2\n---\n"
  },
  {
    "path": "packages/site/examples/element/combo/index.zh.md",
    "content": "---\ntitle: 组合\norder: 2\n---\n"
  },
  {
    "path": "packages/site/examples/element/custom-combo/demo/extra-button.js",
    "content": "import { Circle, Path } from '@antv/g';\nimport { Renderer } from '@antv/g-svg';\nimport { CircleCombo, ExtensionCategory, Graph, register } from '@antv/g6';\n\nconst collapse = (x, y, r) => {\n  return [\n    ['M', x - r, y],\n    ['a', r, r, 0, 1, 0, r * 2, 0],\n    ['a', r, r, 0, 1, 0, -r * 2, 0],\n    ['M', x - r + 4, y],\n    ['L', x + r - 4, y],\n  ];\n};\n\nconst expand = (x, y, r) => {\n  return [\n    ['M', x - r, y],\n    ['a', r, r, 0, 1, 0, r * 2, 0],\n    ['a', r, r, 0, 1, 0, -r * 2, 0],\n    ['M', x - r + 4, y],\n    ['L', x - r + 2 * r - 4, y],\n    ['M', x - r + r, y - r + 4],\n    ['L', x, y + r - 4],\n  ];\n};\n\nclass CircleComboWithExtraButton extends CircleCombo {\n  render(attributes, container) {\n    super.render(attributes, container);\n    this.drawButton(attributes);\n  }\n\n  drawButton(attributes) {\n    const { collapsed } = attributes;\n    const [, height] = this.getKeySize(attributes);\n    const btnR = 8;\n    const y = height / 2 + btnR;\n    const d = collapsed ? expand(0, y, btnR) : collapse(0, y, btnR);\n\n    const hitArea = this.upsert('hit-area', Circle, { cy: y, r: 10, fill: '#fff', cursor: 'pointer' }, this);\n    this.upsert('button', Path, { stroke: '#3d81f7', d, cursor: 'pointer' }, hitArea);\n  }\n\n  onCreate() {\n    this.shapeMap['hit-area'].addEventListener('click', () => {\n      const id = this.id;\n      const collapsed = !this.attributes.collapsed;\n      const { graph } = this.context;\n      if (collapsed) graph.collapseElement(id);\n      else graph.expandElement(id);\n    });\n  }\n}\n\nregister(ExtensionCategory.COMBO, 'circle-combo-with-extra-button', CircleComboWithExtraButton);\n\nconst graph = new Graph({\n  container: 'container',\n  renderer: () => new Renderer(),\n  data: {\n    nodes: [\n      { id: 'node-0', combo: 'combo-0', style: { x: 100, y: 100 } },\n      { id: 'node-1', combo: 'combo-0', style: { x: 150, y: 100 } },\n      { id: 'node-2', style: { x: 250, y: 100 } },\n    ],\n    edges: [{ source: 'node-1', target: 'node-2' }],\n    combos: [{ id: 'combo-0' }],\n  },\n  combo: {\n    type: 'circle-combo-with-extra-button',\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/custom-combo/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"extra-button.js\",\n      \"title\": {\n        \"zh\": \"额外按钮\",\n        \"en\": \"Extra Button\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YBe_Ta8leloAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/element/custom-combo/index.en.md",
    "content": "---\ntitle: Custom Combo\norder: 6\n---\n"
  },
  {
    "path": "packages/site/examples/element/custom-combo/index.zh.md",
    "content": "---\ntitle: 自定义组合\norder: 6\n---\n"
  },
  {
    "path": "packages/site/examples/element/custom-edge/demo/custom-arrow.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(6).fill(0).map((_, i) => ({ id: `node${i + 1}` })),\n  edges: [\n    {\n      id: 'custom-arrow-1',\n      source: 'node1',\n      target: 'node2',\n      style: {\n        endArrowD: 'M-14,0 L-4,-4 L0,-14 L4,-4 L14,0 L4,4 L0,14 L-4,4 Z',\n        endArrowOffset: 14,\n      },\n    },\n    {\n      id: 'custom-arrow-2',\n      source: 'node3',\n      target: 'node4',\n      style: {\n        endArrowD: 'M 3,-5 L 3,5 L 15,10 L 15,-10 Z',\n        endArrowOffset: 10,\n      },\n    },\n    {\n      id: 'image-arrow',\n      source: 'node5',\n      target: 'node6',\n      style: {\n        endArrowSrc: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*N4ZMS7gHsUIAAAAAAAAAAABkARQnAQ',\n        endArrowSize: 28,\n        endArrowTransform: [['rotate', 90]],\n      },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  data,\n  edge: {\n    style: {\n      stroke: '#F6BD16',\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'grid',\n    cols: 2,\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/custom-edge/demo/custom-path.js",
    "content": "import { BaseEdge, ExtensionCategory, Graph, register } from '@antv/g6';\n\nclass PolylineEdge extends BaseEdge {\n  getKeyPath(attributes) {\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes);\n\n    return [\n      ['M', sourcePoint[0], sourcePoint[1]],\n      ['L', targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], sourcePoint[1]],\n      ['L', targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], targetPoint[1]],\n      ['L', targetPoint[0], targetPoint[1]],\n    ];\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'custom-polyline', PolylineEdge);\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 100, y: 100, ports: [{ key: 'right', placement: [1, 0.5] }] } },\n      { id: 'node-1', style: { x: 250, y: 200, ports: [{ key: 'left', placement: [0, 0.5] }] } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  edge: {\n    type: 'custom-polyline',\n    style: {\n      startArrow: true,\n      endArrow: true,\n      stroke: '#F6BD16',\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/custom-edge/demo/extra-label.js",
    "content": "import { Text } from '@antv/g';\nimport { Renderer } from '@antv/g-svg';\nimport { ExtensionCategory, Graph, Line, register, subStyleProps } from '@antv/g6';\n\nclass LabelEdge extends Line {\n  render(attributes, container) {\n    super.render(attributes);\n    this.drawEndLabel(attributes, container, 'start');\n    this.drawEndLabel(attributes, container, 'end');\n  }\n\n  drawEndLabel(attributes, container, type) {\n    const key = type === 'start' ? 'startLabel' : 'endLabel';\n    const [x, y] = this.getEndpoints(attributes)[type === 'start' ? 0 : 1];\n\n    const fontStyle = {\n      x,\n      y,\n      dx: type === 'start' ? 15 : -15,\n      fontSize: 16,\n      fill: 'gray',\n      textBaseline: 'middle',\n      textAlign: type,\n    };\n    const style = subStyleProps(attributes, key);\n    const text = style.text;\n    this.upsert(`label-${type}`, Text, text ? { ...fontStyle, ...style } : false, container);\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'extra-label-edge', LabelEdge);\n\nconst graph = new Graph({\n  container: 'container',\n  renderer: () => new Renderer(),\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 100, y: 100 } },\n      { id: 'node-1', style: { x: 300, y: 100 } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  edge: {\n    type: 'extra-label-edge',\n    style: {\n      startArrow: true,\n      endArrow: true,\n      stroke: '#F6BD16',\n      startLabelText: 'start',\n      endLabelText: 'end',\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/custom-edge/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"custom-path.js\",\n      \"title\": {\n        \"zh\": \"自定义路径\",\n        \"en\": \"Custom Path\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*MPQTSap6ZusAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"extra-label.js\",\n      \"title\": {\n        \"zh\": \"额外标签\",\n        \"en\": \"Extra Label\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*RnE9SL9xkBIAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"custom-arrow.js\",\n      \"title\": {\n        \"zh\": \"自定义箭头\",\n        \"en\": \"Custom Arrow\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*6wrVRoF3S98AAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/element/custom-edge/index.en.md",
    "content": "---\ntitle: Custom Edge\norder: 5\n---\n"
  },
  {
    "path": "packages/site/examples/element/custom-edge/index.zh.md",
    "content": "---\ntitle: 自定义边\norder: 5\n---\n"
  },
  {
    "path": "packages/site/examples/element/custom-node/demo/g2-activity-chart.js",
    "content": "import { Circle as CircleGeometry } from '@antv/g';\nimport { renderToMountedElement, stdlib } from '@antv/g2';\nimport { Circle, ExtensionCategory, Graph, register } from '@antv/g6';\n\nclass ActivityChart extends Circle {\n  onCreate() {\n    const { value } = this.attributes;\n    const radius = this.shapeMap.key.style.r;\n    const activeRadius = radius / 4;\n    const activeSize = radius / 2;\n\n    const group = this.upsert(\n      'chart-container',\n      CircleGeometry,\n      {\n        r: radius,\n        fill: '#fff',\n      },\n      this.shapeMap.key,\n    );\n\n    renderToMountedElement(\n      // @antv/g2 Specification\n      // https://g2.antv.antgroup.com/examples/general/radial/#apple-activity\n      {\n        x: -radius,\n        y: -radius,\n        width: radius * 2,\n        height: radius * 2,\n        type: 'view',\n        data: value,\n        margin: 0,\n        coordinate: { type: 'radial', innerRadius: 0.2 },\n        children: [\n          {\n            type: 'interval',\n            encode: { x: 'name', y: 1, size: activeSize, color: 'color' },\n            scale: { color: { type: 'identity' } },\n            style: { fillOpacity: 0.25 },\n            animate: false,\n            tooltip: false,\n          },\n          {\n            type: 'interval',\n            encode: { x: 'name', y: 'percent', color: 'color', size: activeSize },\n            style: {\n              radius: activeRadius,\n              shadowColor: 'rgba(0,0,0,0.45)',\n              shadowBlur: 20,\n              shadowOffsetX: -2,\n              shadowOffsetY: -5,\n            },\n            animate: {\n              enter: { type: 'fadeIn', duration: 1000 },\n            },\n            axis: false,\n            tooltip: false,\n          },\n          {\n            type: 'image',\n            encode: { x: 'name', y: 0, src: (d) => d.icon, size: 6 },\n            style: { transform: [['translateX', 6]] },\n          },\n        ],\n      },\n      {\n        group,\n        library: stdlib(),\n      },\n    );\n  }\n}\n\nconst people = ['Aaron', 'Rebecca', 'Emily', 'Liam', 'Olivia', 'Ethan', 'Sophia', 'Mason'];\n\nconst mockData = () => {\n  const getRandomPercent = () => +(Math.random() * 0.9 + 0.1).toFixed(1);\n\n  return [\n    {\n      name: 'activity1',\n      percent: getRandomPercent(),\n      color: '#1ad5de',\n      icon: 'https://gw.alipayobjects.com/zos/antfincdn/ck11Y6aRrz/shangjiantou.png',\n    },\n    {\n      name: 'activity2',\n      percent: getRandomPercent(),\n      color: '#a0ff03',\n      icon: 'https://gw.alipayobjects.com/zos/antfincdn/zY2JB7hhrO/shuangjiantou.png',\n    },\n    {\n      name: 'activity3',\n      percent: getRandomPercent(),\n      color: '#e90b3a',\n      icon: 'https://gw.alipayobjects.com/zos/antfincdn/%24qBxSxdK05/jiantou.png',\n    },\n  ];\n};\n\nregister(ExtensionCategory.NODE, 'activity-chart', ActivityChart);\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'view',\n  data: {\n    nodes: people.map((name, i) => ({\n      id: name,\n      style: { value: mockData() },\n    })),\n    edges: people.map((_, i) => {\n      return {\n        id: `edge${i}`,\n        source: people[i],\n        target: people[(i + 1) % 5],\n      };\n    }),\n  },\n  node: {\n    type: 'activity-chart',\n    style: {\n      size: 50,\n      labelText: (d) => d.id,\n      fillOpacity: 0,\n    },\n    animation: {\n      enter: false,\n    },\n  },\n  layout: {\n    type: 'force',\n    preventOverlap: true,\n    animated: false,\n  },\n  behaviors: ['zoom-canvas', 'drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/custom-node/demo/g2-bar-chart.js",
    "content": "import { Rect as RectGeometry } from '@antv/g';\nimport { renderToMountedElement, stdlib } from '@antv/g2';\nimport { ExtensionCategory, Graph, Rect, register } from '@antv/g6';\n\nclass BarChart extends Rect {\n  onCreate() {\n    const [width, height] = this.getSize();\n    const group = this.upsert(\n      'chart-container',\n      RectGeometry,\n      {\n        transform: `translate(${-width / 2}, ${-height / 2})`,\n        width,\n        height,\n        fill: '#fff',\n        stroke: '#697b8c',\n        radius: 10,\n        shadowColor: '#697b8c',\n        shadowBlur: 10,\n        shadowOffsetX: 5,\n        shadowOffsetY: 5,\n      },\n      this.shapeMap.key,\n    );\n\n    const { name, value } = this.attributes;\n    renderToMountedElement(\n      // @antv/g2 Specification\n      // https://g2.antv.antgroup.com/examples/general/interval/#column\n      {\n        width,\n        height,\n        data: { value },\n        title: name,\n        type: 'interval',\n        axis: {\n          x: { title: false },\n          y: { title: false },\n        },\n        scale: {\n          y: { domain: [0, 100] },\n        },\n        encode: {\n          x: 'subject',\n          y: 'score',\n          color: 'subject',\n        },\n        legend: { color: false },\n      },\n      {\n        group,\n        library: stdlib(),\n      },\n    );\n  }\n}\n\nregister(ExtensionCategory.NODE, 'bar-chart', BarChart);\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      {\n        id: 'Jack',\n        data: {\n          value: [\n            { subject: 'Math', score: 95 },\n            { subject: 'Chinese', score: 70 },\n            { subject: 'English', score: 75 },\n            { subject: 'Geography', score: 80 },\n            { subject: 'Physics', score: 90 },\n            { subject: 'Chemistry', score: 85 },\n            { subject: 'Biology', score: 70 },\n          ],\n        },\n      },\n      {\n        id: 'Aaron',\n        data: {\n          value: [\n            { subject: 'Math', score: 70 },\n            { subject: 'Chinese', score: 90 },\n            { subject: 'English', score: 90 },\n            { subject: 'Geography', score: 60 },\n            { subject: 'Physics', score: 70 },\n            { subject: 'Chemistry', score: 65 },\n            { subject: 'Biology', score: 80 },\n          ],\n        },\n      },\n      {\n        id: 'Rebecca',\n        data: {\n          value: [\n            { subject: 'Math', score: 60 },\n            { subject: 'Chinese', score: 95 },\n            { subject: 'English', score: 100 },\n            { subject: 'Geography', score: 80 },\n            { subject: 'Physics', score: 60 },\n            { subject: 'Chemistry', score: 90 },\n            { subject: 'Biology', score: 85 },\n          ],\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'bar-chart',\n    style: {\n      size: 250,\n      fillOpacity: 0,\n      name: (d) => d.id,\n      value: (d) => d.data.value,\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/custom-node/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"react-node.jsx\",\n      \"title\": {\n        \"zh\": \"React 节点\",\n        \"en\": \"React node\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*9oz-R7bIkd0AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"reactnode-idcard.jsx\",\n      \"title\": {\n        \"zh\": \"React 节点 身份证\",\n        \"en\": \"React node IDCard\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*nKs0R7O2njIAAAAAAAAAAAAAemJ7AQ/original\"\n    },\n    {\n      \"filename\": \"g2-bar-chart.js\",\n      \"title\": {\n        \"zh\": \"G2 柱状图\",\n        \"en\": \"G2 bar chart\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*VcCDR5myr70AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"g2-activity-chart.js\",\n      \"title\": {\n        \"zh\": \"G2 活动图\",\n        \"en\": \"G2 activity Chart\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*GVyoQKk2WIIAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/element/custom-node/demo/react-node.jsx",
    "content": "import { DatabaseFilled } from '@ant-design/icons';\nimport { ExtensionCategory, Graph, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\nimport { Badge, Flex, Input, Tag, Typography } from 'antd';\nimport { useEffect, useRef } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nconst { Text } = Typography;\n\nregister(ExtensionCategory.NODE, 'react', ReactNode);\n\nconst Node = ({ data, onChange }) => {\n  const { status, type } = data.data;\n\n  return (\n    <Flex\n      style={{\n        width: '100%',\n        height: '100%',\n        background: '#fff',\n        padding: 10,\n        borderRadius: 5,\n        border: '1px solid gray',\n      }}\n      vertical\n    >\n      <Flex align=\"center\" justify=\"space-between\">\n        <Text>\n          <DatabaseFilled />\n          Server\n          <Tag>{type}</Tag>\n        </Text>\n        <Badge status={status} />\n      </Flex>\n      <Text type=\"secondary\">{data.id}</Text>\n      <Flex align=\"center\">\n        <Text style={{ flexShrink: 0 }}>\n          <Text type=\"danger\">*</Text>URL:\n        </Text>\n        <Input\n          style={{ borderRadius: 0, borderBottom: '1px solid #d9d9d9' }}\n          variant=\"borderless\"\n          value={data.data?.url}\n          onChange={(event) => {\n            const url = event.target.value;\n            onChange?.(url);\n          }}\n        />\n      </Flex>\n    </Flex>\n  );\n};\n\nexport const ReactNodeDemo = () => {\n  const containerRef = useRef();\n\n  useEffect(() => {\n    const graph = new Graph({\n      container: containerRef.current,\n      data: {\n        nodes: [\n          {\n            id: 'local-server-1',\n            data: { status: 'success', type: 'local', url: 'http://localhost:3000' },\n            style: { x: 50, y: 50 },\n          },\n          {\n            id: 'remote-server-1',\n            data: { status: 'warning', type: 'remote' },\n            style: { x: 350, y: 50 },\n          },\n        ],\n        edges: [{ source: 'local-server-1', target: 'remote-server-1' }],\n      },\n      node: {\n        type: 'react',\n        style: {\n          size: [240, 100],\n          component: (data) => <Node data={data} />,\n        },\n      },\n      behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  }, []);\n\n  return <div style={{ width: '100%', height: '100%' }} ref={containerRef}></div>;\n};\n\nconst root = createRoot(document.getElementById('container'));\nroot.render(<ReactNodeDemo />);\n"
  },
  {
    "path": "packages/site/examples/element/custom-node/demo/reactnode-idcard.jsx",
    "content": "import { UserOutlined } from '@ant-design/icons';\nimport { ExtensionCategory, Graph, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\nimport { Avatar, Button, Card, Descriptions, Select, Space, Typography } from 'antd';\nimport React, { useEffect, useRef } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nconst { Title, Text } = Typography;\nconst { Option } = Select;\n\nregister(ExtensionCategory.NODE, 'react-node', ReactNode);\n\nconst IDCardNode = ({ id, data }) => {\n  const { name, idNumber, address, expanded, selected, graph } = data;\n\n  const toggleExpand = (e) => {\n    e.stopPropagation();\n    graph.updateNodeData([\n      {\n        id,\n        data: { expanded: !expanded },\n      },\n    ]);\n    graph.render();\n  };\n\n  const handleSelect = (value) => {\n    graph.updateNodeData([\n      {\n        id,\n        data: { selected: value !== 0 },\n      },\n    ]);\n    if (value === 2) {\n      // 获取与当前节点相连的所有节点\n      const connectedNodes = graph.getNeighborNodesData(id);\n\n      connectedNodes.forEach((node) => {\n        graph.updateNodeData([\n          {\n            id: node.id,\n            data: { selected: true },\n          },\n        ]);\n      });\n    }\n    graph.render();\n  };\n\n  const CardTitle = (\n    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>\n      <Space>\n        <Avatar shape=\"square\" size=\"small\" icon={<UserOutlined />} />\n        <Title level={5} style={{ margin: 0 }}>\n          {name}\n        </Title>\n\n        <Select\n          value={selected ? data.selectedOption || 1 : 0}\n          style={{ width: 150, marginRight: 8 }}\n          onChange={handleSelect}\n        >\n          <Option value={0}>None</Option>\n          <Option value={1}>Node</Option>\n          <Option value={2}>Connected</Option>\n        </Select>\n      </Space>\n      <Button type=\"link\" onClick={toggleExpand} style={{ padding: 0 }}>\n        {expanded ? 'fold' : 'expand'}\n      </Button>\n    </div>\n  );\n\n  return (\n    <Card\n      size=\"small\"\n      title={CardTitle}\n      style={{\n        width: 340,\n        padding: 10,\n        borderRadius: 8,\n        borderWidth: 2,\n        borderColor: selected ? 'orange' : '#eee', // 根据选中状态设置边框颜色\n        cursor: 'pointer',\n      }}\n    >\n      {expanded ? (\n        <Descriptions bordered column={1} style={{ width: '100%', textAlign: 'center' }}>\n          <Descriptions.Item label=\"ID Number\">{idNumber}</Descriptions.Item>\n          <Descriptions.Item label=\"Address\">{address}</Descriptions.Item>\n        </Descriptions>\n      ) : (\n        <Text style={{ textAlign: 'center' }}>IDCard Information</Text>\n      )}\n    </Card>\n  );\n};\n\n// 定义 Graph 数据\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n      data: {\n        name: 'Alice',\n        idNumber: 'IDUSAASD2131734',\n        address: '1234 Broadway, Apt 5B, New York, NY 10001',\n        expanded: false, // 初始状态为收缩\n        selected: false, // 初始状态为未选中\n        selectedOption: 1, // 初始选择本节点\n      },\n      style: { x: 50, y: 50 },\n    },\n    {\n      id: 'node2',\n      data: {\n        name: 'Bob',\n        idNumber: 'IDUSAASD1431920',\n        address: '3030 Chestnut St, Philadelphia, PA 19104',\n        expanded: false, // 初始状态为收缩\n        selected: false, // 初始状态为未选中\n        selectedOption: 0, // 初始不选择\n      },\n      style: { x: 700, y: 100 },\n    },\n    {\n      id: 'node3',\n      data: {\n        name: 'Charlie',\n        idNumber: 'IDUSAASD1431921',\n        address: '4040 Elm St, Chicago, IL 60611',\n        expanded: false,\n        selected: true,\n        selectedOption: 0,\n      },\n    },\n    {\n      id: 'node4',\n      data: {\n        name: 'David',\n        idNumber: 'IDUSAASD1431922',\n        address: '5050 Oak St, Houston, TX 77002',\n        expanded: false,\n        selected: false,\n        selectedOption: 0,\n      },\n    },\n    {\n      id: 'node5',\n      data: {\n        name: 'Eve',\n        idNumber: 'IDUSAASD1431923',\n        address: '6060 Pine St, Phoenix, AZ 85001',\n        expanded: false,\n        selected: false,\n        selectedOption: 0,\n      },\n    },\n  ],\n  edges: [\n    { source: 'node1', target: 'node2' },\n    { source: 'node2', target: 'node3' },\n    { source: 'node3', target: 'node4' },\n    { source: 'node4', target: 'node5' },\n  ],\n};\n\nexport const ReactNodeDemo = () => {\n  const containerRef = useRef();\n  const graphRef = useRef(null);\n\n  useEffect(() => {\n    // 创建 Graph 实例\n    const graph = new Graph({\n      autoFit: 'view',\n      container: containerRef.current,\n      data,\n      node: {\n        type: 'react-node',\n        style: {\n          size: (datum) => (datum.data.expanded ? [340, 236] : [340, 105]), // 调整大小以适应内容\n          component: (data) => <IDCardNode id={data.id} data={{ ...data.data, graph: graph }} />,\n        },\n      },\n      behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],\n      layout: {\n        type: 'snake',\n        cols: 2,\n        rowGap: 100,\n        colGap: 220,\n      },\n    });\n\n    // 渲染 Graph\n    graph.render();\n\n    // 保存 graph 实例\n    graphRef.current = graph;\n\n    return () => {\n      graph.destroy();\n    };\n  }, []);\n\n  return <div style={{ width: '100%', height: '100%' }} ref={containerRef}></div>;\n};\n\n// 渲染 React 组件到 DOM\nconst root = createRoot(document.getElementById('container'));\nroot.render(<ReactNodeDemo />);\n"
  },
  {
    "path": "packages/site/examples/element/custom-node/index.en.md",
    "content": "---\ntitle: Custom Node\norder: 4\n---\n"
  },
  {
    "path": "packages/site/examples/element/custom-node/index.zh.md",
    "content": "---\ntitle: 自定义节点\norder: 4\n---\n"
  },
  {
    "path": "packages/site/examples/element/edge/demo/arrows.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(16).fill(0).map((_, i) => ({ id: `node${i + 1}` })),\n  edges: [\n    'default-arrow',\n    'triangle-arrow',\n    'simple-arrow',\n    'vee-arrow',\n    'circle-arrow',\n    'rect-arrow',\n    'diamond-arrow',\n    'triangleRect-arrow',\n  ].map((id, i) => ({\n    id,\n    source: `node${i * 2 + 1}`,\n    target: `node${i * 2 + 2}`,\n  })),\n};\n\nconst graph = new Graph({\n  data,\n  edge: {\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      endArrowType: (d) => d.id.split('-')[0],\n    },\n  },\n  layout: {\n    type: 'grid',\n    cols: 2,\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/edge/demo/cubic.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'cubic',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      badge: true,\n      badgeText: '\\ue603',\n      badgeFontFamily: 'iconfont',\n      badgeBackgroundWidth: 12,\n      badgeBackgroundHeight: 12,\n    },\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 220,\n    linkDistance: 220,\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/edge/demo/horizontal-cubic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      port: true,\n      ports: [{ placement: 'right' }, { placement: 'left' }],\n    },\n  },\n  edge: {\n    type: 'cubic-horizontal',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'antv-dagre',\n    rankdir: 'LR',\n    nodesep: 20,\n    ranksep: 120,\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/edge/demo/line.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'line',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      badge: true,\n      badgeText: '\\ue603',\n      badgeFontFamily: 'iconfont',\n      badgeBackgroundWidth: 12,\n      badgeBackgroundHeight: 12,\n    },\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 220,\n    linkDistance: 220,\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/edge/demo/loop-curve.js",
    "content": "import { Graph, idOf } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3-ports' }, { id: 'node4-ports' }],\n  edges: [\n    {\n      id: 'loop-1',\n      source: 'node1',\n      target: 'node1',\n      style: { placement: 'top' },\n    },\n    {\n      id: 'loop-2',\n      source: 'node1',\n      target: 'node1',\n      style: { placement: 'right' },\n    },\n    {\n      id: 'loop-3',\n      source: 'node1',\n      target: 'node1',\n      style: { placement: 'bottom' },\n    },\n    {\n      id: 'loop-4',\n      source: 'node1',\n      target: 'node1',\n      style: { placement: 'left' },\n    },\n    {\n      id: 'loop-5',\n      source: 'node2',\n      target: 'node2',\n      style: { placement: 'top-right' },\n    },\n    {\n      id: 'loop-6',\n      source: 'node2',\n      target: 'node2',\n      style: { placement: 'bottom-right' },\n    },\n    {\n      id: 'loop-7',\n      source: 'node2',\n      target: 'node2',\n      style: { placement: 'bottom-left' },\n    },\n    {\n      id: 'loop-8',\n      source: 'node2',\n      target: 'node2',\n      style: { placement: 'top-left' },\n    },\n    {\n      id: 'loop-9',\n      source: 'node3-ports',\n      target: 'node3-ports',\n      style: { sourcePort: 'port1', targetPort: 'port2' },\n    },\n    {\n      id: 'loop-10',\n      source: 'node4-ports',\n      target: 'node4-ports',\n      style: { sourcePort: 'port2', targetPort: 'port2' },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      size: [80, 30],\n      port: (d) => idOf(d).includes('ports'),\n      portR: 3,\n      ports: [\n        {\n          key: 'port1',\n          placement: [0.7, 0],\n        },\n        {\n          key: 'port2',\n          placement: 'right',\n        },\n      ],\n    },\n  },\n  edge: {\n    type: 'line',\n    style: {\n      sourcePort: (d) => d.style.sourcePort,\n      targetPort: (d) => d.style.targetPort,\n      endArrow: true,\n      loopPlacement: (d) => d.style.placement,\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/edge/demo/loop-polyline.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3-ports' }, { id: 'node4-ports' }],\n  edges: [\n    {\n      id: 'loop-1',\n      source: 'node1',\n      target: 'node1',\n      placement: 'top',\n    },\n    {\n      id: 'loop-2',\n      source: 'node1',\n      target: 'node1',\n      placement: 'right',\n    },\n    {\n      id: 'loop-3',\n      source: 'node1',\n      target: 'node1',\n      placement: 'bottom',\n    },\n    {\n      id: 'loop-4',\n      source: 'node1',\n      target: 'node1',\n      placement: 'left',\n    },\n    {\n      id: 'loop-5',\n      source: 'node2',\n      target: 'node2',\n      placement: 'top-right',\n    },\n    {\n      id: 'loop-6',\n      source: 'node2',\n      target: 'node2',\n      placement: 'bottom-right',\n    },\n    {\n      id: 'loop-7',\n      source: 'node2',\n      target: 'node2',\n      placement: 'bottom-left',\n    },\n    {\n      id: 'loop-8',\n      source: 'node2',\n      target: 'node2',\n      placement: 'top-left',\n    },\n    {\n      id: 'loop-9',\n      source: 'node3-ports',\n      target: 'node3-ports',\n      style: { sourcePort: 'port1', targetPort: 'port2' },\n    },\n    {\n      id: 'loop-10',\n      source: 'node4-ports',\n      target: 'node4-ports',\n      style: { sourcePort: 'port2', targetPort: 'port2' },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      size: [80, 30],\n      port: (d) => d.id.includes('ports'),\n      portR: 3,\n      ports: [\n        {\n          key: 'port1',\n          placement: [0.7, 0],\n        },\n        {\n          key: 'port2',\n          placement: 'right',\n        },\n      ],\n    },\n  },\n  edge: {\n    type: 'polyline',\n    style: {\n      sourcePort: (d) => d.style.sourcePort,\n      targetPort: (d) => d.style.targetPort,\n      endArrow: true,\n      loopPlacement: (d) => d.placement,\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/edge/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"line.js\",\n      \"title\": {\n        \"zh\": \"直线\",\n        \"en\": \"Line\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ax2RRKUqXlcAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"polyline.js\",\n      \"title\": {\n        \"zh\": \"折线-带控制点\",\n        \"en\": \"Polyline with Control Points\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*zw8VRou1mRAAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"polyline-orth.js\",\n      \"title\": {\n        \"zh\": \"正交线\",\n        \"en\": \"Orthogonal Line\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ldO6S4NOfKQAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"polyline-orth-with-cps.js\",\n      \"title\": {\n        \"zh\": \"正交线-带控制点\",\n        \"en\": \"Orthogonal Line with Control Points\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*C9apQa9NioMAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n\n    {\n      \"filename\": \"quadratic.js\",\n      \"title\": {\n        \"zh\": \"二次贝塞尔曲线\",\n        \"en\": \"Quadratic\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*RRelQ7VrFc4AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"cubic.js\",\n      \"title\": {\n        \"zh\": \"三次贝塞尔曲线\",\n        \"en\": \"Cubic\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*cVd-TLQWujYAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"vertical-cubic.js\",\n      \"title\": {\n        \"zh\": \"三次贝塞尔曲线-垂直\",\n        \"en\": \"Vertical Cubic\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*wrDlQKxNHNEAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"horizontal-cubic.js\",\n      \"title\": {\n        \"zh\": \"三次贝塞尔曲线-水平\",\n        \"en\": \"Horizontal Cubic\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*6Gj2R50AJ8AAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"loop-polyline.js\",\n      \"title\": {\n        \"zh\": \"折线自环边\",\n        \"en\": \"Polyline Loop\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*eCLSSIiRDVEAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"loop-curve.js\",\n      \"title\": {\n        \"zh\": \"曲线自环边\",\n        \"en\": \"Curve Loop\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*OReBSqIkp_cAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"arrows.js\",\n      \"title\": {\n        \"zh\": \"箭头\",\n        \"en\": \"Arrows\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*VlxQQoEpTIgAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/element/edge/demo/polyline-orth-with-cps.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 200, y: 200 } },\n    { id: 'node-2', style: { x: 350, y: 120 } },\n    { id: 'node-cp', style: { x: 300, y: 190, size: 5, fill: 'rgb(244, 109, 67)' } },\n  ],\n  edges: [\n    {\n      id: 'edge-1',\n      source: 'node-1',\n      target: 'node-2',\n      style: { controlPoints: [[300, 190]] },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'polyline',\n    style: {\n      router: {\n        type: 'orth',\n      },\n      controlPoints: (d) => d.style.controlPoints,\n    },\n  },\n  behaviors: [{ type: 'drag-element' }],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/edge/demo/polyline-orth.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 200, y: 200 } },\n    { id: 'node-2', style: { x: 350, y: 120 } },\n  ],\n  edges: [\n    {\n      id: 'edge-1',\n      source: 'node-1',\n      target: 'node-2',\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'polyline',\n    style: {\n      router: {\n        type: 'orth',\n      },\n    },\n  },\n  behaviors: [{ type: 'drag-element' }],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/edge/demo/polyline.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 200, y: 200 } },\n    { id: 'node-2', style: { x: 350, y: 120 } },\n  ],\n  edges: [\n    {\n      id: 'edge-1',\n      source: 'node-1',\n      target: 'node-2',\n      controlPoints: [[300, 190]],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'polyline',\n    style: {\n      controlPoints: (d) => d.controlPoints,\n    },\n  },\n  behaviors: [{ type: 'drag-element' }],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/edge/demo/quadratic.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'quadratic',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      badge: true,\n      badgeText: '\\ue603',\n      badgeFontFamily: 'iconfont',\n      badgeBackgroundWidth: 12,\n      badgeBackgroundHeight: 12,\n    },\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 220,\n    linkDistance: 220,\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/edge/demo/vertical-cubic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      port: true,\n      ports: [{ placement: 'top' }, { placement: 'bottom' }],\n    },\n  },\n  edge: {\n    type: 'cubic-vertical',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'antv-dagre',\n    begin: [50, 50],\n    rankdir: 'TB',\n    nodesep: 20,\n    ranksep: 120,\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/edge/index.en.md",
    "content": "---\ntitle: Edges\norder: 1\n---\n"
  },
  {
    "path": "packages/site/examples/element/edge/index.zh.md",
    "content": "---\ntitle: 边\norder: 1\n---\n"
  },
  {
    "path": "packages/site/examples/element/label/demo/background.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 150, y: 100 } },\n    { id: 'node2', style: { x: 250, y: 200 } },\n    { id: 'node3', style: { x: 450, y: 200 } },\n  ],\n  edges: [\n    { source: 'node1', target: 'node2' },\n    { source: 'node1', target: 'node3' },\n    { source: 'node2', target: 'node3' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n      labelPosition: 'bottom',\n      labelFill: '#e66465',\n      labelFontSize: 12,\n      labelFontStyle: 'italic',\n      labelBackground: true,\n      labelBackgroundFill: 'linear-gradient(#e66465, #9198e5)',\n      labelBackgroundStroke: '#9ec9ff',\n      labelBackgroundRadius: 2,\n    },\n  },\n  edge: {\n    style: {\n      labelText: (d) => d.id,\n      labelPosition: 'center',\n      labelTextBaseline: 'top',\n      labelDy: 5,\n      labelFontSize: 12,\n      labelFontWeight: 'bold',\n      labelFill: '#1890ff',\n      labelBackground: true,\n      labelBackgroundFill: 'linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n      labelBackgroundStroke: '#9ec9ff',\n      labelBackgroundRadius: 2,\n    },\n  },\n  layout: {\n    type: 'force',\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/label/demo/copy.js",
    "content": "import { Graph, NodeEvent } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n      data: {\n        label: 'Click to copy this label which is too long to be displayed',\n      },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      x: 200,\n      y: 200,\n      size: 150,\n      labelPlacement: 'center',\n      labelText: (d) => d.data.label,\n      labelWordWrap: true,\n      labelMaxWidth: '90%',\n      labelBackground: true,\n      labelBackgroundFill: '#eee',\n      labelBackgroundFillOpacity: 0.5,\n      labelBackgroundRadius: 4,\n      labelPointerEvents: 'none',\n      labelBackgroundPointerEvents: 'none',\n    },\n  },\n  behaviors: ['drag-element'],\n  plugins: [\n    {\n      type: 'tooltip',\n      getContent: (e, items) => {\n        let result = `<h4>Node Label:</h4>`;\n        items.forEach((item) => {\n          result += `<p>${item.data.label}</p>`;\n        });\n        return result;\n      },\n    },\n  ],\n});\n\ngraph.render();\n\ngraph.on('node:click', (e) => {\n  const node = graph.getNodeData(e.target.id);\n  const label = node?.data?.label;\n\n  navigator.clipboard.writeText(label);\n  alert('copied to clipboard!');\n});\n\ngraph.on(NodeEvent.POINTER_ENTER, (e) => {\n  graph.setElementState({ [e.target.id]: 'active' });\n});\n\ngraph.on(NodeEvent.POINTER_OUT, (e) => {\n  graph.setElementState({ [e.target.id]: [] });\n});\n"
  },
  {
    "path": "packages/site/examples/element/label/demo/ellipsis.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 100, y: 150, size: 100 } },\n    { id: 'node-2', style: { x: 400, y: 150, size: 150 } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelPlacement: 'center',\n      labelText: 'This label is too long to be displayed',\n      labelWordWrap: true, // enable label ellipsis\n      labelMaxWidth: '90%',\n      labelBackground: true,\n      labelBackgroundFill: '#eee',\n      labelBackgroundFillOpacity: 0.5,\n      labelBackgroundRadius: 4,\n    },\n  },\n  edge: {\n    style: {\n      labelOffsetY: -4,\n      labelTextBaseline: 'bottom',\n      labelText: 'This label is too long to be displayed',\n      labelWordWrap: true,\n      labelMaxWidth: '80%',\n      labelBackground: true,\n      labelBackgroundFill: 'red',\n      labelBackgroundFillOpacity: 0.5,\n      labelBackgroundRadius: 4,\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/label/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"copy.js\",\n      \"title\": {\n        \"zh\": \"复制\",\n        \"en\": \"Copy Content\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*a-keS4q-nU4AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"ellipsis.js\",\n      \"title\": {\n        \"zh\": \"文本省略\",\n        \"en\": \"Text Ellipsis\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-0P0R6AAnuMAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"word-wrap.js\",\n      \"title\": {\n        \"zh\": \"文本换行\",\n        \"en\": \"Word Wrap\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*N9eHSoiH3bEAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"background.js\",\n      \"title\": {\n        \"zh\": \"背景颜色\",\n        \"en\": \"Background\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*NG7xT4mkRL4AAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/element/label/demo/word-wrap.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 100, y: 150, size: 100 } },\n    { id: 'node-2', style: { x: 400, y: 150, size: 150 } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  fitCenter: true,\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      labelPlacement: 'bottom',\n      labelText: 'This label is too long to be displayed',\n      labelMaxWidth: '90%',\n      labelBackground: true,\n      labelBackgroundFill: '#eee',\n      labelBackgroundFillOpacity: 0.5,\n      labelBackgroundRadius: 4,\n      labelWordWrap: true,\n      labelMaxLines: 4,\n    },\n  },\n  edge: {\n    style: {\n      labelOffsetY: -4,\n      labelTextBaseline: 'bottom',\n      labelText: 'This label is too long to be displayed',\n      labelMaxWidth: '80%',\n      labelBackground: true,\n      labelBackgroundFill: 'red',\n      labelBackgroundFillOpacity: 0.5,\n      labelBackgroundRadius: 4,\n      labelWordWrap: true,\n      labelMaxLines: 4,\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/label/index.en.md",
    "content": "---\ntitle: Label\norder: 3\n---\n"
  },
  {
    "path": "packages/site/examples/element/label/index.zh.md",
    "content": "---\ntitle: 标签\norder: 3\n---\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/3d-node.js",
    "content": "import { ExtensionCategory, Graph, register } from '@antv/g6';\nimport {\n  Capsule,\n  Cone,\n  Cube,\n  Cylinder,\n  Light,\n  ObserveCanvas3D,\n  Plane,\n  Sphere,\n  Torus,\n  renderer,\n} from '@antv/g6-extension-3d';\n\nregister(ExtensionCategory.PLUGIN, '3d-light', Light);\nregister(ExtensionCategory.NODE, 'sphere', Sphere);\nregister(ExtensionCategory.NODE, 'plane', Plane);\nregister(ExtensionCategory.NODE, 'cylinder', Cylinder);\nregister(ExtensionCategory.NODE, 'cone', Cone);\nregister(ExtensionCategory.NODE, 'cube', Cube);\nregister(ExtensionCategory.NODE, 'capsule', Capsule);\nregister(ExtensionCategory.NODE, 'torus', Torus);\nregister(ExtensionCategory.BEHAVIOR, 'observe-canvas-3d', ObserveCanvas3D);\n\nconst nodes = [\n  {\n    id: 'node-1',\n    type: 'sphere',\n    style: {\n      texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*cdTdTI2bNl8AAAAAAAAAAAAADmJ7AQ/original',\n    },\n  },\n  { id: 'node-2', type: 'plane', style: { size: 50 } },\n  { id: 'node-3', type: 'cylinder' },\n  { id: 'node-4', type: 'cone' },\n  {\n    id: 'node-5',\n    type: 'cube',\n    style: {\n      texture: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*8TlCRIsKeUkAAAAAAAAAAAAAARQnAQ',\n    },\n  },\n  { id: 'node-6', type: 'capsule' },\n  { id: 'node-7', type: 'torus' },\n];\n\nconst graph = new Graph({\n  container: 'container',\n  renderer,\n  data: {\n    nodes,\n  },\n  node: {\n    style: {\n      materialType: 'phong',\n      labelText: (d) => d.id,\n      x: (d) => 100 + (nodes.findIndex((n) => n.id === d.id) % 5) * 100,\n      y: (d) => 100 + Math.floor(nodes.findIndex((n) => n.id === d.id) / 5) * 100,\n    },\n  },\n  plugins: [\n    {\n      type: '3d-light',\n      directional: {\n        direction: [0, 0, 1],\n      },\n    },\n  ],\n  behaviors: ['observe-canvas-3d'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/circle.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'circle',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/diamond.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'diamond',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/donut.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default', index: 0 },\n    { id: 'halo', index: 1 },\n    { id: 'badges', index: 2 },\n    { id: 'ports', index: 3 },\n    {\n      id: 'active',\n      states: ['active'],\n      index: 4,\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n      index: 5,\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n      index: 6,\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n      index: 7,\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n      index: 8,\n    },\n  ],\n};\nconst graph = new Graph({\n  container: 'container',\n  animation: false,\n  data,\n  node: {\n    type: 'donut',\n    style: {\n      size: 80,\n      fill: '#DB9D0D',\n      innerR: 20,\n      donuts: (item) => {\n        const { index } = item;\n        if (index === 0) return [1, 2, 3]; // donuts数据类型为number[]时，根据值的大小决定环的占比\n\n        if (index === 1) {\n          return [\n            { value: 50, color: 'red' },\n            { value: 150, color: 'green' },\n            { value: 100, color: 'blue' },\n          ];\n        }\n\n        if (index === 4) {\n          return [\n            { value: 150, fill: 'pink', stroke: '#fff', lineWidth: 1 },\n            { value: 250, stroke: '#fff', lineWidth: 1 },\n            { value: 200, stroke: '#fff', lineWidth: 1 },\n          ];\n        }\n\n        return [100, 200, 100, 200];\n      },\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/ellipse.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'ellipse',\n    style: {\n      size: [45, 35],\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/hexagon.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'hexagon',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      outerR: 30, // 外半径\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/html.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst ICON_MAP = {\n  error: '&#10060;',\n  overload: '&#9889;',\n  running: '&#9989;',\n};\n\nconst COLOR_MAP = {\n  error: '#f5222d',\n  overload: '#faad14',\n  running: '#52c41a',\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', data: { location: 'East', status: 'error', ip: '192.168.1.2' } },\n      { id: 'node-2', data: { location: 'West', status: 'overload', ip: '192.168.1.3' } },\n      { id: 'node-3', data: { location: 'South', status: 'running', ip: '192.168.1.4' } },\n    ],\n  },\n  node: {\n    type: 'html',\n    style: {\n      size: [240, 80],\n      dx: -120,\n      dy: -40,\n      innerHTML: (d) => {\n        const {\n          data: { location, status, ip },\n        } = d;\n        const color = COLOR_MAP[status];\n\n        return `\n<div \n  style=\"\n    width:100%; \n    height: 100%; \n    background: ${color}bb; \n    border: 1px solid ${color};\n    color: #fff;\n    user-select: none;\n    display: flex; \n    padding: 10px;\n    \"\n>\n  <div style=\"display: flex;flex-direction: column;flex: 1;\">\n    <div style=\"font-weight: bold;\">\n      ${location} Node\n    </div>\n    <div>\n      status: ${status} ${ICON_MAP[status]}\n    </div>\n  </div>\n  <div>\n    <span style=\"border: 1px solid white; padding: 2px;\">\n      ${ip}\n    </span>\n  </div>\n</div>`;\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['drag-element', 'zoom-canvas'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/image.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'image',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      src: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*N4ZMS7gHsUIAAAAAAAAAAABkARQnAQ',\n      haloStroke: '#227eff',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n    state: {\n      inactive: {\n        fillOpacity: 0.5,\n      },\n      disabled: {\n        fillOpacity: 0.2,\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"circle.js\",\n      \"title\": {\n        \"zh\": \"圆形\",\n        \"en\": \"Circle\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*SuPdRLp1PQgAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"rect.js\",\n      \"title\": {\n        \"zh\": \"矩形\",\n        \"en\": \"Rect\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*etLSQYZnJAAAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"rounded-rect.js\",\n      \"title\": {\n        \"zh\": \"圆角矩形\",\n        \"en\": \"Radius Rect\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*r03cSIy59-UAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"diamond.js\",\n      \"title\": {\n        \"zh\": \"菱形\",\n        \"en\": \"Diamond\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*oUSlSZt6rCoAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"hexagon.js\",\n      \"title\": {\n        \"zh\": \"六边形\",\n        \"en\": \"Hexagon\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*muosSr4ft8QAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"triangle.js\",\n      \"title\": {\n        \"zh\": \"三角形\",\n        \"en\": \"Triangle\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*BW_sSbWVQowAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"image.js\",\n      \"title\": {\n        \"zh\": \"图片\",\n        \"en\": \"Image\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*NPG3SL_n-CYAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"ellipse.js\",\n      \"title\": {\n        \"zh\": \"椭圆\",\n        \"en\": \"Ellipse\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Vdq4Rb3ESOoAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"star.js\",\n      \"title\": {\n        \"zh\": \"星形\",\n        \"en\": \"Star\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YSVjSLyYNwIAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"html.js\",\n      \"title\": {\n        \"zh\": \"HTML节点\",\n        \"en\": \"HTML节点\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*TvSOSINFbBIAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"donut.js\",\n      \"title\": {\n        \"zh\": \"甜甜圈\",\n        \"en\": \"Donut\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*h3eWT4loiTwAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"3d-node.js\",\n      \"title\": {\n        \"zh\": \"3D 节点\",\n        \"en\": \"3D Node\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*rLPZSqW7LskAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"port.js\",\n      \"title\": {\n        \"zh\": \"连接桩\",\n        \"en\": \"Port\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*AvaIRIuptcwAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/port.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', style: { x: 80, y: 200, size: 30 } },\n    {\n      id: 'node-2',\n      type: 'rect',\n      style: {\n        x: 250,\n        y: 200,\n        size: 50,\n        port: true,\n        ports: [\n          { key: 'port-1', placement: [0, 0.15] },\n          { key: 'port-2', placement: [0, 0.5] },\n          { key: 'port-3', placement: [0, 0.85] },\n        ],\n      },\n    },\n  ],\n  edges: [\n    { id: 'edge-1', source: 'node-1', target: 'node-2', style: { targetPort: 'port-1' } },\n    { id: 'edge-2', source: 'node-1', target: 'node-2', style: { targetPort: 'port-2' } },\n    { id: 'edge-3', source: 'node-1', target: 'node-2', style: { targetPort: 'port-3' } },\n  ],\n};\n\nconst graph = new Graph({\n  data,\n  edge: {\n    style: {\n      endArrow: true,\n    },\n  },\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = { show: false, position: 'outline' };\n  gui.add(config, 'position', ['outline', 'center']).onChange((value) => {\n    graph.updateNodeData([{ id: 'node-2', style: { portLinkToCenter: value === 'center' } }]);\n    graph.draw();\n  });\n  gui\n    .add(config, 'show')\n    .onChange((value) => {\n      graph.updateNodeData([{ id: 'node-2', style: { portR: value ? 5 : 0 } }]);\n      graph.draw();\n    })\n    .name('show ports');\n});\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/rect.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/rounded-rect.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      radius: 4, // 👈🏻 Set the radius.\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/star.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'star',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/node/demo/triangle.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'triangle',\n    style: {\n      size: 40,\n      direction: (d) => (d.id === 'ports' ? 'left' : undefined),\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/element/node/index.en.md",
    "content": "---\ntitle: Nodes\norder: 0\n---\n"
  },
  {
    "path": "packages/site/examples/element/node/index.zh.md",
    "content": "---\ntitle: 节点\norder: 0\n---\n"
  },
  {
    "path": "packages/site/examples/examples.md",
    "content": "## algorithm / case\n\n### Graph pattern matching\n\n**文件路径**: `algorithm/case/demo/pattern-matching.js`\n\n```js\nimport { GADDI } from '@antv/algorithm';\nimport { Graph } from '@antv/g6';\n\nconst pattern = {\n  nodes: [\n    {\n      id: 'pn0',\n      cluster: 'nodeType-0',\n    },\n    {\n      id: 'pn1',\n      cluster: 'nodeType-1',\n    },\n    {\n      id: 'pn2',\n      cluster: 'nodeType-2',\n    },\n  ],\n  edges: [\n    { source: 'pn1', target: 'pn0', cluster: 'edgeType-1' },\n    { source: 'pn1', target: 'pn2', cluster: 'edgeType-0' },\n    { source: 'pn2', target: 'pn0', cluster: 'edgeType-2' },\n  ],\n};\n\nfetch('https://assets.antv.antgroup.com/g6/gaddi.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n      node: {\n        style: {\n          labelPlacement: 'center',\n          labelText: (d) => d.label,\n          stroke: '#5F95FF',\n          lineWidth: 1,\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n          color: ['#5F95FF', '#61DDAA', '#65789B'],\n        },\n      },\n      edge: {\n        style: {\n          endArrow: true,\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n          color: ['#5F95FF', '#61DDAA', '#65789B'],\n        },\n      },\n      plugins: [\n        {\n          type: 'legend',\n          nodeField: 'cluster',\n          position: 'bottom',\n        },\n        {\n          key: 'hull-0',\n          type: 'hull',\n          members: [],\n        },\n        {\n          key: 'hull-1',\n          type: 'hull',\n          members: [],\n        },\n      ],\n    });\n    graph.render();\n\n    window.addPanel((gui) => {\n      gui.add(\n        {\n          match: () => {\n            const matches = GADDI(data, pattern, true, undefined, undefined, 'cluster', 'cluster');\n            matches.forEach((match, i) => {\n              graph.updatePlugin({\n                key: `hull-${i}`,\n                members: match.nodes.map((node) => node.id),\n              });\n            });\n            graph.render();\n          },\n        },\n        'match',\n      );\n    });\n  });\n```\n\n---\n\n### Shortest path\n\n**文件路径**: `algorithm/case/demo/shortest-path.js`\n\n```js\nimport { findShortestPath } from '@antv/algorithm';\nimport { CanvasEvent, Graph } from '@antv/g6';\n\nconst format = ({ nodes, edges }) => {\n  return {\n    nodes: nodes.map((node) => ({\n      ...node,\n      style: {\n        x: node.x,\n        y: node.y,\n      },\n    })),\n    edges,\n  };\n};\n\nfetch('https://gw.alipayobjects.com/os/bmw-prod/b0ca4b15-bd0c-43ec-ae41-c810374a1d55.json')\n  .then((res) => res.json())\n  .then(format)\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      animation: false,\n      data,\n      node: {\n        style: {\n          size: 12,\n        },\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', { type: 'click-select', multiple: true }],\n      autoFit: 'view',\n    });\n\n    graph.render();\n\n    const resetStates = () => {\n      graph.setElementState(Object.fromEntries([...data.nodes, ...data.edges].map((element) => [element.id, []])));\n    };\n\n    graph.on(CanvasEvent.CLICK, () => {\n      resetStates();\n    });\n\n    window.addPanel((gui) => {\n      gui.add(\n        {\n          Help: () => {\n            alert(\"Press 'shift' to select source and target nodes \\n按住 'shift' 选取起点和终点\");\n          },\n        },\n        'Help',\n      );\n      gui.add(\n        {\n          Search: () => {\n            const nodes = graph.getElementDataByState('node', 'selected');\n            if (nodes.length !== 2) {\n              alert('Please select 2 nodes!\\n请选择两个节点！');\n              return;\n            }\n            const [source, target] = nodes;\n            const { length, path } = findShortestPath(data, source.id, target.id);\n            if (length === Infinity) {\n              alert('No path found!\\n未找到路径！');\n              return;\n            }\n\n            const states = {};\n            data.nodes.forEach(({ id }) => {\n              if (path.includes(id)) states[id] = 'highlight';\n              else states[id] = 'inactive';\n            });\n\n            data.edges.forEach(({ id, source, target }) => {\n              const sourceIndex = path.indexOf(source);\n              const targetIndex = path.indexOf(target);\n              if (sourceIndex === -1 || targetIndex === -1) return;\n              if (Math.abs(sourceIndex - targetIndex) === 1) states[id] = 'highlight';\n              else states[id] = 'inactive';\n            });\n\n            graph.setElementState(states);\n            graph.frontElement(path);\n          },\n        },\n        'Search',\n      );\n    });\n  });\n```\n\n---\n\n### LP automatic clustering\n\n**文件路径**: `algorithm/case/demo/label-propagation.js`\n\n```js\nimport { labelPropagation } from '@antv/algorithm';\nimport { Graph } from '@antv/g6';\n\nconst colors = [\n  '#5F95FF',\n  '#61DDAA',\n  '#65789B',\n  '#F6BD16',\n  '#7262FD',\n  '#78D3F8',\n  '#9661BC',\n  '#F6903D',\n  '#008685',\n  '#F08BB4',\n];\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n      layout: {\n        type: 'force',\n        linkDistance: 50,\n        animation: false,\n      },\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      gui.add(\n        {\n          Cluster: () => {\n            const clusteredData = labelPropagation(data, false);\n            const result = clusteredData.clusters\n              .map((cluster, i) => {\n                const color = colors[i % colors.length];\n                const nodes = cluster.nodes.map((node) => ({\n                  id: node.id,\n                  style: {\n                    fill: color,\n                  },\n                }));\n                return nodes;\n              })\n              .flat();\n            graph.updateNodeData(result);\n            graph.draw();\n          },\n        },\n        'Cluster',\n      );\n    });\n  });\n```\n\n---\n\n### LOUVAIN automatic clustering\n\n**文件路径**: `algorithm/case/demo/louvain.js`\n\n```js\nimport { louvain } from '@antv/algorithm';\nimport { Graph } from '@antv/g6';\n\nconst colors = [\n  '#5F95FF',\n  '#61DDAA',\n  '#65789B',\n  '#F6BD16',\n  '#7262FD',\n  '#78D3F8',\n  '#9661BC',\n  '#F6903D',\n  '#008685',\n  '#F08BB4',\n];\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n      layout: {\n        type: 'force',\n        linkDistance: 50,\n        animation: false,\n      },\n    });\n    graph.render();\n\n    window.addPanel((gui) => {\n      gui.add(\n        {\n          Cluster: () => {\n            const clusteredData = louvain(data, false);\n            const result = clusteredData.clusters\n              .map((cluster, i) => {\n                const color = colors[i % colors.length];\n                const nodes = cluster.nodes.map((node) => ({\n                  id: node.id,\n                  style: {\n                    fill: color,\n                  },\n                }));\n                return nodes;\n              })\n              .flat();\n            graph.updateNodeData(result);\n            graph.draw();\n          },\n        },\n        'Cluster',\n      );\n    });\n  });\n```\n\n---\n\n## animation / basic\n\n### Enter Animation\n\n**文件路径**: `animation/basic/demo/enter.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 50, y: 50 } },\n      { id: 'node-1', style: { x: 200, y: 50 } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  node: {\n    animation: {\n      enter: [\n        {\n          fields: ['opacity'],\n          duration: 1000,\n          easing: 'linear',\n        },\n      ],\n    },\n  },\n  edge: {\n    animation: {\n      enter: [\n        {\n          fields: ['opacity'],\n          duration: 1000,\n          easing: 'linear',\n        },\n      ],\n    },\n  },\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = {\n    index: 2,\n    duration: 1000,\n    easing: 'linear',\n    add: () => {\n      const { index } = config;\n      const y = 50 + 25 * index;\n      graph.addData({\n        nodes: [\n          { id: `node-${index + 1}`, style: { x: 50, y } },\n          { id: `node-${index + 2}`, style: { x: 200, y } },\n        ],\n        edges: [{ source: `node-${index + 1}`, target: `node-${index + 2}` }],\n      });\n      graph.draw();\n      config.index += 2;\n    },\n  };\n\n  const updateMapper = (key, value) => {\n    const { node, edge } = graph.getOptions();\n    node.animation.enter[0][key] = value;\n    edge.animation.enter[0][key] = value;\n    graph.setNode(node);\n    graph.setEdge(edge);\n  };\n\n  gui.add(config, 'duration', 500, 5000, 100).onChange((duration) => {\n    updateMapper('duration', duration);\n  });\n  // see: https://g.antv.antgroup.com/en/api/animation/waapi#easing-1\n  gui.add(config, 'easing', ['linear', 'ease-in-sine', 'ease-in-cubic']).onChange((easing) => {\n    updateMapper('easing', easing);\n  });\n  gui.add(config, 'add').name('Add Element');\n});\n```\n\n---\n\n### Edge Path In\n\n**文件路径**: `animation/basic/demo/enter-edge-path-in.js`\n\n```js\nimport { ExtensionCategory, Graph, Line, register } from '@antv/g6';\n\nclass PathInLine extends Line {\n  onCreate() {\n    const shape = this.shapeMap.key;\n    const length = shape.getTotalLength();\n    shape.animate([{ lineDash: [0, length] }, { lineDash: [length, 0] }], {\n      duration: 500,\n      fill: 'both',\n    });\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'path-in-line', PathInLine);\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 50, y: 50 } },\n      { id: 'node-1', style: { x: 200, y: 50 } },\n    ],\n  },\n  edge: {\n    type: 'path-in-line',\n    animation: {\n      // disable default enter and exit animation\n      enter: false,\n      exit: false,\n    },\n  },\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = {\n    connect: () => {\n      const edge = graph.getEdgeData('edge-1');\n      if (edge) {\n        alert('The edge already exists.');\n        return;\n      }\n\n      graph.addEdgeData([{ id: 'edge-1', source: 'node-0', target: 'node-1' }]);\n      graph.draw();\n    },\n    disconnect: () => {\n      const edge = graph.getEdgeData('edge-1');\n      if (edge) {\n        graph.removeEdgeData(['edge-1']);\n        graph.draw();\n      }\n    },\n  };\n  gui.add(config, 'connect');\n  gui.add(config, 'disconnect');\n});\n```\n\n---\n\n### Update Animation\n\n**文件路径**: `animation/basic/demo/update.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 50, y: 50 } },\n      { id: 'node-1', style: { x: 200, y: 50 } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  node: {\n    animation: {\n      update: [{ fields: ['x', 'y', 'size'] }, { fields: ['fill'], shape: 'key' }],\n    },\n  },\n  edge: {\n    animation: {\n      update: [{ fields: ['sourceNode', 'targetNode'] }, { fields: ['stroke', 'lineWidth'], shape: 'key' }],\n    },\n  },\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const colors = ['red', 'blue', 'green', 'yellow', 'black', 'purple', 'orange', 'gray'];\n  let [nextOffsetY, nextSize, nextLineWidth] = [50, 50, 5];\n  const config = {\n    color: () => {\n      const color = colors[Math.floor(Math.random() * colors.length)];\n      graph.updateData({\n        nodes: [\n          { id: 'node-0', style: { fill: color } },\n          { id: 'node-1', style: { fill: color } },\n        ],\n        edges: [{ source: 'node-0', target: 'node-1', style: { stroke: color } }],\n      });\n      graph.draw();\n    },\n    position: () => {\n      const offsetY = nextOffsetY;\n      graph.translateElementBy({\n        'node-0': [0, offsetY],\n        'node-1': [0, offsetY],\n      });\n      nextOffsetY = -nextOffsetY;\n    },\n    size: () => {\n      const size = nextSize;\n      const lineWidth = nextLineWidth;\n      graph.updateData({\n        nodes: [\n          { id: 'node-0', style: { size } },\n          { id: 'node-1', style: { size } },\n        ],\n        edges: [{ source: 'node-0', target: 'node-1', style: { lineWidth } }],\n      });\n      graph.draw();\n      [nextSize, nextLineWidth] = [nextSize === 50 ? 16 : 50, nextLineWidth === 5 ? 1 : 5];\n    },\n  };\n  gui.add(config, 'color').name('fill & stroke');\n  gui.add(config, 'position').name('position');\n  gui.add(config, 'size').name('size & lineWidth');\n});\n```\n\n---\n\n### Exit Animation\n\n**文件路径**: `animation/basic/demo/exit.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: Array.from({ length: 10 }, (_, i) => ({\n      id: `node-${i}`,\n      style: { x: i % 2 === 0 ? 50 : 200, y: 25 + 50 * Math.floor(i / 2) },\n    })),\n    edges: Array.from({ length: 5 }, (_, i) => ({\n      id: `edge-${i}`,\n      source: `node-${i * 2}`,\n      target: `node-${i * 2 + 1}`,\n    })),\n  },\n  node: {\n    animation: {\n      exit: [\n        {\n          fields: ['opacity'],\n          duration: 1000,\n          easing: 'linear',\n        },\n      ],\n    },\n  },\n  edge: {\n    animation: {\n      exit: [\n        {\n          fields: ['opacity'],\n          duration: 1000,\n          easing: 'linear',\n        },\n      ],\n    },\n  },\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = {\n    index: 4,\n    duration: 1000,\n    easing: 'linear',\n    remove: () => {\n      const { index } = config;\n      if (index === -1) return;\n      graph.removeData({\n        nodes: [`node-${index * 2}`, `node-${index * 2 + 1}`],\n        edges: [`edge-${index}`],\n      });\n      graph.draw();\n      config.index--;\n    },\n  };\n  const updateMapper = (key, value) => {\n    const { node, edge } = graph.getOptions();\n    node.animation.exit[0][key] = value;\n    edge.animation.exit[0][key] = value;\n    graph.setNode(node);\n    graph.setEdge(edge);\n  };\n  gui.add(config, 'duration', 500, 5000, 100).onChange((duration) => {\n    updateMapper('duration', duration);\n  });\n  // see: https://g.antv.antgroup.com/en/api/animation/waapi#easing-1\n  gui.add(config, 'easing', ['linear', 'ease-in-sine', 'ease-in-cubic']).onChange((easing) => {\n    updateMapper('easing', easing);\n  });\n  gui.add(config, 'remove').name('Remove Element');\n});\n```\n\n---\n\n### Combo Collapse/Expand\n\n**文件路径**: `animation/basic/demo/combo-collapse-expand.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', combo: 'combo-2', style: { x: 120, y: 100 } },\n      { id: 'node-2', combo: 'combo-1', style: { x: 300, y: 200 } },\n      { id: 'node-3', combo: 'combo-1', style: { x: 200, y: 300 } },\n    ],\n    edges: [\n      { id: 'edge-1', source: 'node-1', target: 'node-2' },\n      { id: 'edge-2', source: 'node-2', target: 'node-3' },\n    ],\n    combos: [\n      {\n        id: 'combo-1',\n        type: 'rect',\n        combo: 'combo-2',\n        style: {\n          collapsed: true,\n        },\n      },\n      { id: 'combo-2' },\n    ],\n  },\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  combo: {\n    style: {\n      labelText: (d) => d.id,\n      lineDash: 0,\n      collapsedLineDash: [5, 5],\n    },\n  },\n  behaviors: [{ type: 'drag-element' }, 'collapse-expand'],\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = {\n    collapse: () => {\n      graph.collapseElement('combo-1');\n    },\n    expand: () => {\n      graph.expandElement('combo-1');\n    },\n  };\n  gui.add(config, 'collapse');\n  gui.add(config, 'expand');\n});\n```\n\n---\n\n## animation / persistence\n\n### Ant Line\n\n**文件路径**: `animation/persistence/demo/ant-line.js`\n\n```js\nimport { ExtensionCategory, Graph, Line, register } from '@antv/g6';\n\nclass AntLine extends Line {\n  onCreate() {\n    const shape = this.shapeMap.key;\n    shape.animate([{ lineDashOffset: -20 }, { lineDashOffset: 0 }], {\n      duration: 500,\n      iterations: Infinity,\n    });\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'ant-line', AntLine);\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 50, y: 50 } },\n      { id: 'node-2', style: { x: 200, y: 50 } },\n      { id: 'node-3', style: { x: 125, y: 150 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-2', target: 'node-3' },\n      { source: 'node-3', target: 'node-1' },\n    ],\n  },\n  edge: {\n    type: 'ant-line',\n    style: {\n      lineDash: [10, 10],\n    },\n  },\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = {\n    lineDash: 10,\n  };\n  gui.add(config, 'lineDash', 1, 20, 1).onChange((lineDash) => {\n    graph.setEdge({\n      type: 'ant-line',\n      style: {\n        lineDash: [lineDash, lineDash],\n      },\n    });\n    graph.draw();\n  });\n});\n```\n\n---\n\n### Fly Marker\n\n**文件路径**: `animation/persistence/demo/fly-marker.js`\n\n```js\nimport { Circle } from '@antv/g';\nimport { Renderer } from '@antv/g-svg';\nimport { CubicHorizontal, ExtensionCategory, Graph, register, subStyleProps } from '@antv/g6';\n\nclass FlyMarkerCubic extends CubicHorizontal {\n  getMarkerStyle(attributes) {\n    return { r: 5, fill: '#c3d5f9', offsetPath: this.shapeMap.key, ...subStyleProps(attributes, 'marker') };\n  }\n\n  onCreate() {\n    const marker = this.upsert('marker', Circle, this.getMarkerStyle(this.attributes), this);\n    marker.animate([{ offsetDistance: 0 }, { offsetDistance: 1 }], {\n      duration: 3000,\n      iterations: Infinity,\n    });\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'fly-marker-cubic', FlyMarkerCubic);\n\nconst graph = new Graph({\n  container: 'container',\n  renderer: () => new Renderer(),\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 50, y: 50 } },\n      { id: 'node-1', style: { x: 200, y: 200 } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  edge: {\n    type: 'fly-marker-cubic',\n    style: {\n      lineDash: [10, 10],\n    },\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Fly Marker\n\n**文件路径**: `animation/persistence/demo/breathing-circle.js`\n\n```js\nimport { Circle, ExtensionCategory, Graph, register } from '@antv/g6';\n\nclass BreathingCircle extends Circle {\n  onCreate() {\n    const halo = this.shapeMap.halo;\n    halo.animate([{ lineWidth: 0 }, { lineWidth: 20 }], {\n      duration: 1000,\n      iterations: Infinity,\n      direction: 'alternate',\n    });\n  }\n}\n\nregister(ExtensionCategory.NODE, 'breathing-circle', BreathingCircle);\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }],\n  },\n  node: {\n    type: 'breathing-circle',\n    style: {\n      size: 50,\n      halo: true,\n    },\n    palette: ['#3875f6', '#efb041', '#ec5b56', '#72c240'],\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Ripple\n\n**文件路径**: `animation/persistence/demo/ripple-circle.js`\n\n```js\nimport { Circle as CircleGeometry } from '@antv/g';\nimport { Renderer } from '@antv/g-svg';\nimport { Circle, ExtensionCategory, Graph, register } from '@antv/g6';\n\nclass RippleCircle extends Circle {\n  onCreate() {\n    const { fill } = this.attributes;\n    const r = this.shapeMap.key.style.r;\n    const length = 5;\n    const fillOpacity = 0.5;\n\n    Array.from({ length }).map((_, index) => {\n      const ripple = this.upsert(\n        `ripple-${index}`,\n        CircleGeometry,\n        {\n          r,\n          fill,\n          fillOpacity,\n        },\n        this,\n      );\n      ripple.animate(\n        [\n          { r, fillOpacity },\n          { r: r + length * 5, fillOpacity: 0 },\n        ],\n        {\n          duration: 1000 * length,\n          iterations: Infinity,\n          delay: 1000 * index,\n          easing: 'ease-cubic',\n        },\n      );\n    });\n  }\n}\n\nregister(ExtensionCategory.NODE, 'ripple-circle', RippleCircle);\n\nconst graph = new Graph({\n  container: 'container',\n  renderer: () => new Renderer(),\n  data: {\n    nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }],\n  },\n  node: {\n    type: 'ripple-circle',\n    animation: {\n      enter: false,\n    },\n    style: {\n      size: 50,\n    },\n    palette: ['#3875f6', '#efb041', '#ec5b56', '#72c240'],\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n## animation / viewport\n\n### Zoom Viewport\n\n**文件路径**: `animation/viewport/demo/zoom.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/force.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'force',\n      },\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      const animation = {\n        duration: 500,\n        easing: 'linear',\n      };\n      const config = {\n        zoomIn: () => {\n          graph.zoomBy(1.2, animation);\n        },\n        zoomOut: () => {\n          graph.zoomBy(0.8, animation);\n        },\n      };\n      gui.add(config, 'zoomIn');\n      gui.add(config, 'zoomOut');\n    });\n  });\n```\n\n---\n\n### Translate Viewport\n\n**文件路径**: `animation/viewport/demo/translate.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/force.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'force',\n      },\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      const animation = {\n        duration: 500,\n        easing: 'linear',\n      };\n      const config = {\n        Up: () => graph.translateBy([0, -50], animation),\n        Down: () => graph.translateBy([0, 50], animation),\n        Left: () => graph.translateBy([-50, 0], animation),\n        Right: () => graph.translateBy([50, 0], animation),\n      };\n      gui.add(config, 'Up').name('⬆️ Up');\n      gui.add(config, 'Down').name('⬇️ Down');\n      gui.add(config, 'Left').name('⬅️ Left');\n      gui.add(config, 'Right').name('➡️ Right');\n    });\n  });\n```\n\n---\n\n### Rotate Viewport\n\n**文件路径**: `animation/viewport/demo/rotate.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/force.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'force',\n      },\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      const animation = {\n        duration: 500,\n        easing: 'linear',\n      };\n      const config = {\n        clockwise: () => graph.rotateBy(-10, animation),\n        anticlockwise: () => graph.rotateBy(10, animation),\n      };\n      gui.add(config, 'clockwise').name('🔁 Clockwise');\n      gui.add(config, 'anticlockwise').name('🔄 Anti-clockwise');\n    });\n  });\n```\n\n---\n\n## behavior / auto-adapt-label\n\n### basic.js\n\n**文件路径**: `behavior/auto-adapt-label/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 200, y: 100, labelText: '短标签' } },\n    { id: 'node2', style: { x: 360, y: 100, labelText: '中等长度的标签' } },\n    { id: 'node3', style: { x: 280, y: 220, labelText: '这是一个非常非常长的标签，需要自适应显示' } },\n  ],\n  edges: [\n    { source: 'node1', target: 'node2' },\n    { source: 'node1', target: 'node3' },\n    { source: 'node2', target: 'node3' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  behaviors: [\n    'zoom-canvas',\n    'drag-canvas',\n    {\n      key: 'auto-adapt-label',\n      type: 'auto-adapt-label',\n      padding: 0,\n      throttle: 200,\n    },\n  ],\n  plugins: [{ type: 'grid-line', size: 30 }],\n  animation: true,\n});\n\ngraph.render();\n```\n\n---\n\n## behavior / canvas\n\n### Drag Canvas\n\n**文件路径**: `behavior/canvas/demo/drag.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: ['drag-canvas'],\n});\n\ngraph.render();\n```\n\n---\n\n### Scroll XY\n\n**文件路径**: `behavior/canvas/demo/scroll-xy.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: ['scroll-canvas'],\n});\n\ngraph.render();\n```\n\n---\n\n### Scroll Y\n\n**文件路径**: `behavior/canvas/demo/scroll-y.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: [{ type: 'scroll-canvas', direction: 'y' }],\n});\n\ngraph.render();\n```\n\n---\n\n### Zoom Canvas\n\n**文件路径**: `behavior/canvas/demo/zoom.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  behaviors: ['zoom-canvas'],\n});\n\ngraph.render();\n```\n\n---\n\n### Hide Elements When Dragging and Zooming\n\n**文件路径**: `behavior/canvas/demo/optimize.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      labelText: (datum) => datum.id,\n    },\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'scroll-canvas', 'optimize-viewport-transform'],\n});\n\ngraph.render();\n```\n\n---\n\n## behavior / combo\n\n### Expand/Collapse\n\n**文件路径**: `behavior/combo/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo1', style: { x: 350, y: 200 } },\n      { id: 'node2', combo: 'combo1', style: { x: 350, y: 250 } },\n      { id: 'node3', combo: 'combo3', style: { x: 100, y: 200 } },\n    ],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'combo1', target: 'node3' },\n    ],\n    combos: [{ id: 'combo1', combo: 'combo2' }, { id: 'combo2' }, { id: 'combo3', style: { collapsed: true } }],\n  },\n  behaviors: ['collapse-expand', 'drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n### Trigger of Expand/Collapse\n\n**文件路径**: `behavior/combo/demo/collapse-expand.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo1', style: { x: 300, y: 100 } },\n      { id: 'node2', combo: 'combo1', style: { x: 300, y: 150 } },\n      { id: 'node3', combo: 'combo2', style: { x: 100, y: 100 } },\n      { id: 'node4', combo: 'combo2', style: { x: 50, y: 150 } },\n      { id: 'node5', combo: 'combo2', style: { x: 150, y: 150 } },\n    ],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node3', target: 'node5' },\n    ],\n    combos: [\n      { id: 'combo1', style: { labelText: '双击折叠', collapsed: true } },\n      { id: 'combo2', style: { labelText: '单击折叠', collapsed: false } },\n    ],\n  },\n  behaviors: [\n    {\n      type: 'collapse-expand',\n      trigger: 'dblclick',\n      enable: (event) => event.targetType === 'combo' && event.target.id === 'combo1',\n    },\n    {\n      type: 'collapse-expand',\n      trigger: 'click',\n      enable: (event) => event.targetType === 'combo' && event.target.id === 'combo2',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n## behavior / create-edge\n\n### Create Edge by Drag\n\n**文件路径**: `behavior/create-edge/demo/by-drag.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'drag',\n      style: {\n        fill: 'red',\n        lineWidth: 2,\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n### Create Edge by Click\n\n**文件路径**: `behavior/create-edge/demo/by-click.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'click',\n      style: {\n        stroke: 'red',\n        lineWidth: 2,\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n### Create Edge Between Combos\n\n**文件路径**: `behavior/create-edge/demo/between-combos.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  data: {\n    nodes: [\n      { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n      { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n      { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n    ],\n    combos: [{ id: 'combo1' }, { id: 'combo2', style: { ports: [{ placement: 'center' }] } }],\n  },\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'drag',\n      style: {\n        lineWidth: 2,\n        lineDash: [2, 3],\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n### Custom Edge Style\n\n**文件路径**: `behavior/create-edge/demo/custom-edge-style.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node1', target: 'node3' },\n      { source: 'node1', target: 'node4' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: [\n    {\n      type: 'create-edge',\n      trigger: 'click',\n      onCreate: (edge) => {\n        const { style, ...rest } = edge;\n        return {\n          ...rest,\n          style: {\n            ...style,\n            stroke: 'red',\n            lineWidth: 2,\n            endArrow: true,\n          },\n        };\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n## behavior / fix-element-size\n\n### Fix Element Size While Zooming\n\n**文件路径**: `behavior/fix-element-size/demo/fix-size.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node0', size: 50, label: '0', style: { x: 326, y: 268 }, states: ['selected'] },\n    { id: 'node1', size: 30, label: '1', style: { x: 280, y: 384 }, states: ['selected'] },\n    { id: 'node2', size: 30, label: '2', style: { x: 234, y: 167 } },\n    { id: 'node3', size: 30, label: '3', style: { x: 391, y: 368 } },\n    { id: 'node4', size: 30, label: '4', style: { x: 444, y: 209 } },\n    { id: 'node5', size: 30, label: '5', style: { x: 378, y: 157 } },\n    { id: 'node6', size: 15, label: '6', style: { x: 229, y: 400 } },\n    { id: 'node7', size: 15, label: '7', style: { x: 281, y: 440 } },\n    { id: 'node8', size: 15, label: '8', style: { x: 188, y: 119 } },\n    { id: 'node9', size: 15, label: '9', style: { x: 287, y: 157 } },\n    { id: 'node10', size: 15, label: '10', style: { x: 185, y: 200 } },\n    { id: 'node11', size: 15, label: '11', style: { x: 238, y: 110 } },\n    { id: 'node12', size: 15, label: '12', style: { x: 239, y: 221 } },\n    { id: 'node13', size: 15, label: '13', style: { x: 176, y: 160 } },\n    { id: 'node14', size: 15, label: '14', style: { x: 389, y: 423 } },\n    { id: 'node15', size: 15, label: '15', style: { x: 441, y: 341 } },\n    { id: 'node16', size: 15, label: '16', style: { x: 442, y: 398 } },\n  ],\n  edges: [\n    { source: 'node0', target: 'node1', label: '0-1', states: ['selected'] },\n    { source: 'node0', target: 'node2', label: '0-2' },\n    { source: 'node0', target: 'node3', label: '0-3' },\n    { source: 'node0', target: 'node4', label: '0-4' },\n    { source: 'node0', target: 'node5', label: '0-5' },\n    { source: 'node1', target: 'node6', label: '1-6' },\n    { source: 'node1', target: 'node7', label: '1-7' },\n    { source: 'node2', target: 'node8', label: '2-8' },\n    { source: 'node2', target: 'node9', label: '2-9' },\n    { source: 'node2', target: 'node10', label: '2-10' },\n    { source: 'node2', target: 'node11', label: '2-11' },\n    { source: 'node2', target: 'node12', label: '2-12' },\n    { source: 'node2', target: 'node13', label: '2-13' },\n    { source: 'node3', target: 'node14', label: '3-14' },\n    { source: 'node3', target: 'node15', label: '3-15' },\n    { source: 'node3', target: 'node16', label: '3-16' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.label,\n      size: (d) => d.size,\n      lineWidth: 1,\n    },\n  },\n  edge: { style: { labelText: (d) => d.label } },\n  behaviors: [\n    'zoom-canvas',\n    'drag-canvas',\n    {\n      key: 'fix-element-size',\n      type: 'fix-element-size',\n      enable: (event) => event.data.scale < 1,\n      state: 'selected',\n      reset: true,\n    },\n    { type: 'click-select', key: 'click-select', multiple: true },\n  ],\n  autoFit: 'center',\n});\n\ngraph.render();\n```\n\n---\n\n### Auto Size Label While Zooming\n\n**文件路径**: `behavior/fix-element-size/demo/autosize-label.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node0', size: 50, label: '0', style: { x: 326, y: 268 } },\n    { id: 'node1', size: 30, label: '1', style: { x: 280, y: 384 } },\n    { id: 'node2', size: 30, label: '2', style: { x: 234, y: 167 } },\n    { id: 'node3', size: 30, label: '3', style: { x: 391, y: 368 } },\n    { id: 'node4', size: 30, label: '4', style: { x: 444, y: 209 } },\n    { id: 'node5', size: 30, label: '5', style: { x: 378, y: 157 } },\n    { id: 'node6', size: 15, label: '6', style: { x: 229, y: 400 } },\n    { id: 'node7', size: 15, label: '7', style: { x: 281, y: 440 } },\n    { id: 'node8', size: 15, label: '8', style: { x: 188, y: 119 } },\n    { id: 'node9', size: 15, label: '9', style: { x: 287, y: 157 } },\n    { id: 'node10', size: 15, label: '10', style: { x: 185, y: 200 } },\n    { id: 'node11', size: 15, label: '11', style: { x: 238, y: 110 } },\n    { id: 'node12', size: 15, label: '12', style: { x: 239, y: 221 } },\n    { id: 'node13', size: 15, label: '13', style: { x: 176, y: 160 } },\n    { id: 'node14', size: 15, label: '14', style: { x: 389, y: 423 } },\n    { id: 'node15', size: 15, label: '15', style: { x: 441, y: 341 } },\n    { id: 'node16', size: 15, label: '16', style: { x: 442, y: 398 } },\n  ],\n  edges: [\n    { source: 'node0', target: 'node1', label: '0-1' },\n    { source: 'node0', target: 'node2', label: '0-2' },\n    { source: 'node0', target: 'node3', label: '0-3' },\n    { source: 'node0', target: 'node4', label: '0-4' },\n    { source: 'node0', target: 'node5', label: '0-5' },\n    { source: 'node1', target: 'node6', label: '1-6' },\n    { source: 'node1', target: 'node7', label: '1-7' },\n    { source: 'node2', target: 'node8', label: '2-8' },\n    { source: 'node2', target: 'node9', label: '2-9' },\n    { source: 'node2', target: 'node10', label: '2-10' },\n    { source: 'node2', target: 'node11', label: '2-11' },\n    { source: 'node2', target: 'node12', label: '2-12' },\n    { source: 'node2', target: 'node13', label: '2-13' },\n    { source: 'node3', target: 'node14', label: '3-14' },\n    { source: 'node3', target: 'node15', label: '3-15' },\n    { source: 'node3', target: 'node16', label: '3-16' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n      labelMaxWidth: '200%',\n      labelWordWrap: true,\n      size: (d) => d.size,\n    },\n  },\n  edge: {\n    style: {\n      labelText: (d) => `${d.source}-${d.target}`,\n      labelWordWrap: true,\n      labelMaxLines: 2,\n      labelMaxWidth: '60%',\n    },\n  },\n  behaviors: [\n    {\n      type: 'fix-element-size',\n      key: 'fix-element-size',\n      enable: true,\n      node: [\n        {\n          shape: (shapes) =>\n            shapes.find((shape) => shape.parentElement?.className === 'label' && shape.className === 'text'),\n          fields: ['fontSize', 'lineHeight'],\n        },\n      ],\n      edge: [\n        {\n          shape: (shapes) =>\n            shapes.find((shape) => shape.parentElement?.className === 'label' && shape.className === 'text'),\n          fields: ['fontSize', 'lineHeight'],\n        },\n      ],\n    },\n    'zoom-canvas',\n    'drag-canvas',\n  ],\n  autoFit: 'center',\n});\n\ngraph.render();\n```\n\n---\n\n### Fix Font Size While Zooming\n\n**文件路径**: `behavior/fix-element-size/demo/fix-font-size.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node0', size: 50, label: '0', style: { x: 326, y: 268 } },\n    { id: 'node1', size: 30, label: '1', style: { x: 280, y: 384 } },\n    { id: 'node2', size: 30, label: '2', style: { x: 234, y: 167 } },\n    { id: 'node3', size: 30, label: '3', style: { x: 391, y: 368 } },\n    { id: 'node4', size: 30, label: '4', style: { x: 444, y: 209 } },\n    { id: 'node5', size: 30, label: '5', style: { x: 378, y: 157 } },\n    { id: 'node6', size: 15, label: '6', style: { x: 229, y: 400 } },\n    { id: 'node7', size: 15, label: '7', style: { x: 281, y: 440 } },\n    { id: 'node8', size: 15, label: '8', style: { x: 188, y: 119 } },\n    { id: 'node9', size: 15, label: '9', style: { x: 287, y: 157 } },\n    { id: 'node10', size: 15, label: '10', style: { x: 185, y: 200 } },\n    { id: 'node11', size: 15, label: '11', style: { x: 238, y: 110 } },\n    { id: 'node12', size: 15, label: '12', style: { x: 239, y: 221 } },\n    { id: 'node13', size: 15, label: '13', style: { x: 176, y: 160 } },\n    { id: 'node14', size: 15, label: '14', style: { x: 389, y: 423 } },\n    { id: 'node15', size: 15, label: '15', style: { x: 441, y: 341 } },\n    { id: 'node16', size: 15, label: '16', style: { x: 442, y: 398 } },\n  ],\n  edges: [\n    { source: 'node0', target: 'node1', label: '0-1' },\n    { source: 'node0', target: 'node2', label: '0-2' },\n    { source: 'node0', target: 'node3', label: '0-3' },\n    { source: 'node0', target: 'node4', label: '0-4' },\n    { source: 'node0', target: 'node5', label: '0-5' },\n    { source: 'node1', target: 'node6', label: '1-6' },\n    { source: 'node1', target: 'node7', label: '1-7' },\n    { source: 'node2', target: 'node8', label: '2-8' },\n    { source: 'node2', target: 'node9', label: '2-9' },\n    { source: 'node2', target: 'node10', label: '2-10' },\n    { source: 'node2', target: 'node11', label: '2-11' },\n    { source: 'node2', target: 'node12', label: '2-12' },\n    { source: 'node2', target: 'node13', label: '2-13' },\n    { source: 'node3', target: 'node14', label: '3-14' },\n    { source: 'node3', target: 'node15', label: '3-15' },\n    { source: 'node3', target: 'node16', label: '3-16' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelBackground: true,\n      labelBackgroundFill: '#FFB6C1',\n      labelBackgroundRadius: 4,\n      labelFontFamily: 'Arial',\n      labelPadding: [0, 4],\n      labelText: (d) => d.id,\n      size: (d) => d.size,\n    },\n  },\n  behaviors: [\n    'zoom-canvas',\n    'drag-canvas',\n    {\n      key: 'fix-element-size',\n      type: 'fix-element-size',\n      enable: true,\n      node: { shape: 'label' },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n## behavior / focus\n\n### basic.js\n\n**文件路径**: `behavior/focus/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', combo: 'combo1', style: { x: 110, y: 150 } },\n    { id: 'node2', combo: 'combo1', style: { x: 190, y: 150 } },\n    { id: 'node3', combo: 'combo2', style: { x: 150, y: 260 } },\n  ],\n  edges: [{ source: 'node1', target: 'node2' }],\n  combos: [{ id: 'combo1', combo: 'combo2' }, { id: 'combo2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  node: {\n    style: { labelText: (d) => d.id },\n  },\n  data,\n  behaviors: ['collapse-expand', 'focus-element'],\n});\n\ngraph.render();\n```\n\n---\n\n## behavior / highlight-element\n\n### Hover Element\n\n**文件路径**: `behavior/highlight-element/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst format = (data) => {\n  const { nodes, edges } = data;\n  return {\n    nodes: nodes.map(({ id, ...node }) => ({ id, data: node })),\n    edges: edges.map(({ id, source, target, ...edge }) => ({ id, source, target, data: edge })),\n  };\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/xiaomi.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: format(data),\n      behaviors: ['hover-activate'],\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        nodeSize: 24,\n      },\n      animation: false,\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Activate Relations\n\n**文件路径**: `behavior/highlight-element/demo/activate-relations.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst format = (data) => {\n  const { nodes, edges } = data;\n  return {\n    nodes: nodes.map(({ id, ...node }) => ({ id, data: node })),\n    edges: edges.map(({ id, source, target, ...edge }) => ({ id, source, target, data: edge })),\n  };\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/xiaomi.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: format(data),\n      behaviors: [\n        {\n          type: 'hover-activate',\n          degree: 1, // 👈🏻 Activate relations.\n        },\n      ],\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        nodeSize: 24,\n      },\n      animation: false,\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Configurations for Activate Relations\n\n**文件路径**: `behavior/highlight-element/demo/config-params.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst format = (data) => {\n  const { nodes, edges } = data;\n  return {\n    nodes: nodes.map(({ id, ...node }) => ({ id, data: node })),\n    edges: edges.map(({ id, source, target, ...edge }) => ({ id, source, target, data: edge })),\n  };\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/xiaomi.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: format(data),\n      node: {\n        style: { size: 30 },\n        state: {\n          highlight: {\n            fill: '#D580FF',\n            halo: true,\n            lineWidth: 0,\n          },\n          dim: {\n            fill: '#99ADD1',\n          },\n        },\n      },\n      edge: {\n        state: {\n          highlight: {\n            stroke: '#D580FF',\n          },\n        },\n      },\n      behaviors: [\n        {\n          type: 'hover-activate',\n          enable: (event) => event.targetType === 'node',\n          degree: 1, // 👈🏻 Activate relations.\n          state: 'highlight',\n          inactiveState: 'dim',\n          onHover: (event) => {\n            event.view.setCursor('pointer');\n          },\n          onHoverEnd: (event) => {\n            event.view.setCursor('default');\n          },\n        },\n      ],\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        nodeSize: 24,\n      },\n      animation: false,\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## behavior / inner-event\n\n### Specify the shape response event\n\n**文件路径**: `behavior/inner-event/demo/basic.js`\n\n```js\nimport { Circle as CircleGeometry } from '@antv/g';\nimport { CanvasEvent, Circle, ExtensionCategory, Graph, NodeEvent, register } from '@antv/g6';\n\nclass LightNode extends Circle {\n  render(attributes, container) {\n    super.render(attributes, container);\n    this.upsert('light', CircleGeometry, { r: 8, fill: '#0f0', cx: 0, cy: -25 }, container);\n  }\n}\n\nregister(ExtensionCategory.NODE, 'light', LightNode);\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node1', style: { x: 100, y: 150 } },\n      { id: 'node2', style: { x: 300, y: 150 } },\n    ],\n    edges: [{ source: 'node1', target: 'node2' }],\n  },\n  node: {\n    type: 'light',\n    style: {\n      size: 100,\n      labelText: (d) => d.style.labelText || 'Click the Light',\n      labelPlacement: 'center',\n      labelBackground: true,\n      labelBackgroundFill: '#fff',\n      labelBackgroundFillOpacity: 0.8,\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n\ngraph.on(NodeEvent.CLICK, (event) => {\n  const { target, originalTarget } = event;\n  if (originalTarget.className === 'light') {\n    graph.updateNodeData([{ id: target.id, states: ['selected'], style: { labelText: 'Clicked!' } }]);\n    graph.draw();\n  }\n});\n\ngraph.on(CanvasEvent.CLICK, () => {\n  const selectedIds = graph.getElementDataByState('node', 'selected').map((node) => node.id);\n  graph.updateNodeData(selectedIds.map((id) => ({ id, states: [], style: { labelText: 'Click the Light' } })));\n  graph.draw();\n});\n```\n\n---\n\n## behavior / select\n\n### Click Select\n\n**文件路径**: `behavior/select/demo/click.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  layout: {\n    type: 'grid',\n  },\n  data: {\n    nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3' }, { id: 'node4' }, { id: 'node5' }],\n    edges: [\n      { source: 'node1', target: 'node2' },\n      { source: 'node2', target: 'node3' },\n      { source: 'node3', target: 'node4' },\n      { source: 'node4', target: 'node5' },\n    ],\n  },\n  node: {\n    style: {\n      fill: '#E4504D',\n    },\n    state: {\n      active: {\n        fill: '#0b0',\n      },\n    },\n  },\n  behaviors: [\n    {\n      type: 'click-select',\n      degree: 1,\n      state: 'active',\n      unselectedState: 'inactive',\n      multiple: true,\n      trigger: ['shift'],\n    },\n    'drag-element',\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n### Brush Select\n\n**文件路径**: `behavior/select/demo/brush.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 200, y: 250 } },\n      { id: 'node-2', style: { x: 250, y: 200 } },\n      { id: 'node-3', style: { x: 300, y: 250 } },\n      { id: 'node-4', style: { x: 250, y: 300 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-2', target: 'node-3' },\n      { source: 'node-3', target: 'node-4' },\n      { source: 'node-4', target: 'node-1' },\n    ],\n  },\n  behaviors: [\n    {\n      key: 'brush-select',\n      type: 'brush-select',\n      enable: true,\n      animation: false,\n      mode: 'default', // union intersect diff default\n      state: 'selected', // 'active', 'selected', 'inactive', ...\n      trigger: [], // ['Shift', 'Alt', 'Control', 'Drag', 'Meta', ...]\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2],\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n### Brush Combo\n\n**文件路径**: `behavior/select/demo/brush-combo.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  animation: false,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  data: {\n    nodes: [\n      { id: 'node-1', combo: 'combo1', style: { x: 250, y: 150, lineWidth: 0 } },\n      { id: 'node-2', combo: 'combo1', style: { x: 350, y: 150, lineWidth: 0 } },\n      { id: 'node-3', combo: 'combo2', style: { x: 250, y: 300, lineWidth: 0 } },\n    ],\n    edges: [\n      { target: 'node-1', source: 'node-2' },\n      { target: 'node-1', source: 'node-3' },\n    ],\n    combos: [{ id: 'combo1', combo: 'combo2' }, { id: 'combo2' }],\n  },\n  behaviors: [\n    {\n      type: 'brush-select',\n      immediately: true,\n      mode: 'default',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n### Lasso Select\n\n**文件路径**: `behavior/select/demo/lasso.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', style: { x: 200, y: 250 } },\n      { id: 'node-2', style: { x: 250, y: 200 } },\n      { id: 'node-3', style: { x: 300, y: 250 } },\n      { id: 'node-4', style: { x: 250, y: 300 } },\n    ],\n    edges: [\n      { source: 'node-1', target: 'node-2' },\n      { source: 'node-2', target: 'node-3' },\n      { source: 'node-3', target: 'node-4' },\n      { source: 'node-4', target: 'node-1' },\n    ],\n  },\n  behaviors: [\n    {\n      key: 'lasso-select',\n      type: 'lasso-select',\n      enable: true,\n      animation: false,\n      mode: 'default', // union intersect diff default\n      state: 'selected', // 'active', 'selected', 'inactive', ...\n      trigger: [], // ['Shift', 'Alt', 'Control', 'Drag', 'Meta', ...]\n      style: {\n        width: 0,\n        height: 0,\n        lineWidth: 4,\n        lineDash: [2, 2],\n        fill: 'linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%),linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%),linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n        stroke: 'pink',\n        fillOpacity: 0.2,\n        zIndex: 2,\n        pointerEvents: 'none',\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n## behavior / update-label\n\n### Update Label\n\n**文件路径**: `behavior/update-label/demo/update.js`\n\n```js\nimport { EdgeEvent, Graph, NodeEvent } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node-1',\n      style: { x: 100, y: 150, labelText: 'Hover me!' },\n    },\n    {\n      id: 'node-2',\n      style: { x: 300, y: 150, labelText: 'Hover me!' },\n    },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2', style: { labelText: 'Hover me!' } }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n\ngraph.on(NodeEvent.POINTER_ENTER, (event) => {\n  const { target } = event;\n  graph.updateNodeData([\n    { id: target.id, style: { labelText: 'Hovered', fill: 'lightgreen', labelFill: 'lightgreen' } },\n  ]);\n  graph.draw();\n});\n\ngraph.on(EdgeEvent.POINTER_ENTER, (event) => {\n  const { target } = event;\n  graph.updateEdgeData([\n    { id: target.id, style: { labelText: 'Hovered', stroke: 'lightgreen', labelFill: 'lightgreen', lineWidth: 3 } },\n  ]);\n  graph.draw();\n});\n\ngraph.on(NodeEvent.POINTER_OUT, (event) => {\n  const { target } = event;\n  graph.updateNodeData([{ id: target.id, style: { labelText: 'Hover me!', fill: '#5B8FF9', labelFill: 'black' } }]);\n  graph.draw();\n});\n\ngraph.on(EdgeEvent.POINTER_OUT, (event) => {\n  const { target } = event;\n  graph.updateEdgeData([\n    { id: target.id, style: { labelText: 'Hover me!', stroke: '#5B8FF9', labelFill: 'black', lineWidth: 1 } },\n  ]);\n  graph.draw();\n});\n```\n\n---\n\n## element / combo\n\n### circle.js\n\n**文件路径**: `element/combo/demo/circle.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n    { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n    { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n  ],\n  edges: [],\n  combos: [{ id: 'combo1', combo: 'combo2' }, { id: 'combo2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  combo: {\n    type: 'circle',\n  },\n  behaviors: ['drag-element', 'collapse-expand'],\n});\n\ngraph.render();\n```\n\n---\n\n### rect.js\n\n**文件路径**: `element/combo/demo/rect.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', combo: 'combo1', style: { x: 250, y: 150 } },\n    { id: 'node2', combo: 'combo1', style: { x: 350, y: 150 } },\n    { id: 'node3', combo: 'combo2', style: { x: 250, y: 300 } },\n  ],\n  edges: [],\n  combos: [{ id: 'combo1', combo: 'combo2' }, { id: 'combo2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  combo: {\n    type: 'rect',\n    style: {\n      padding: 20,\n    },\n  },\n  behaviors: ['drag-element', 'collapse-expand'],\n});\n\ngraph.render();\n```\n\n---\n\n## element / custom-combo\n\n### Extra Button\n\n**文件路径**: `element/custom-combo/demo/extra-button.js`\n\n```js\nimport { Circle, Path } from '@antv/g';\nimport { Renderer } from '@antv/g-svg';\nimport { CircleCombo, ExtensionCategory, Graph, register } from '@antv/g6';\n\nconst collapse = (x, y, r) => {\n  return [\n    ['M', x - r, y],\n    ['a', r, r, 0, 1, 0, r * 2, 0],\n    ['a', r, r, 0, 1, 0, -r * 2, 0],\n    ['M', x - r + 4, y],\n    ['L', x + r - 4, y],\n  ];\n};\n\nconst expand = (x, y, r) => {\n  return [\n    ['M', x - r, y],\n    ['a', r, r, 0, 1, 0, r * 2, 0],\n    ['a', r, r, 0, 1, 0, -r * 2, 0],\n    ['M', x - r + 4, y],\n    ['L', x - r + 2 * r - 4, y],\n    ['M', x - r + r, y - r + 4],\n    ['L', x, y + r - 4],\n  ];\n};\n\nclass CircleComboWithExtraButton extends CircleCombo {\n  render(attributes, container) {\n    super.render(attributes, container);\n    this.drawButton(attributes);\n  }\n\n  drawButton(attributes) {\n    const { collapsed } = attributes;\n    const [, height] = this.getKeySize(attributes);\n    const btnR = 8;\n    const y = height / 2 + btnR;\n    const d = collapsed ? expand(0, y, btnR) : collapse(0, y, btnR);\n\n    const hitArea = this.upsert('hit-area', Circle, { cy: y, r: 10, fill: '#fff', cursor: 'pointer' }, this);\n    this.upsert('button', Path, { stroke: '#3d81f7', d, cursor: 'pointer' }, hitArea);\n  }\n\n  onCreate() {\n    this.shapeMap['hit-area'].addEventListener('click', () => {\n      const id = this.id;\n      const collapsed = !this.attributes.collapsed;\n      const { graph } = this.context;\n      if (collapsed) graph.collapseElement(id);\n      else graph.expandElement(id);\n    });\n  }\n}\n\nregister(ExtensionCategory.COMBO, 'circle-combo-with-extra-button', CircleComboWithExtraButton);\n\nconst graph = new Graph({\n  container: 'container',\n  renderer: () => new Renderer(),\n  data: {\n    nodes: [\n      { id: 'node-0', combo: 'combo-0', style: { x: 100, y: 100 } },\n      { id: 'node-1', combo: 'combo-0', style: { x: 150, y: 100 } },\n      { id: 'node-2', style: { x: 250, y: 100 } },\n    ],\n    edges: [{ source: 'node-1', target: 'node-2' }],\n    combos: [{ id: 'combo-0' }],\n  },\n  combo: {\n    type: 'circle-combo-with-extra-button',\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n## element / custom-edge\n\n### Custom Path\n\n**文件路径**: `element/custom-edge/demo/custom-path.js`\n\n```js\nimport { BaseEdge, ExtensionCategory, Graph, register } from '@antv/g6';\n\nclass PolylineEdge extends BaseEdge {\n  getKeyPath(attributes) {\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes);\n\n    return [\n      ['M', sourcePoint[0], sourcePoint[1]],\n      ['L', targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], sourcePoint[1]],\n      ['L', targetPoint[0] / 2 + (1 / 2) * sourcePoint[0], targetPoint[1]],\n      ['L', targetPoint[0], targetPoint[1]],\n    ];\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'custom-polyline', PolylineEdge);\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 100, y: 100, ports: [{ key: 'right', placement: [1, 0.5] }] } },\n      { id: 'node-1', style: { x: 250, y: 200, ports: [{ key: 'left', placement: [0, 0.5] }] } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  edge: {\n    type: 'custom-polyline',\n    style: {\n      startArrow: true,\n      endArrow: true,\n      stroke: '#F6BD16',\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n### Extra Label\n\n**文件路径**: `element/custom-edge/demo/extra-label.js`\n\n```js\nimport { Text } from '@antv/g';\nimport { Renderer } from '@antv/g-svg';\nimport { ExtensionCategory, Graph, Line, register, subStyleProps } from '@antv/g6';\n\nclass LabelEdge extends Line {\n  render(attributes, container) {\n    super.render(attributes);\n    this.drawEndLabel(attributes, container, 'start');\n    this.drawEndLabel(attributes, container, 'end');\n  }\n\n  drawEndLabel(attributes, container, type) {\n    const key = type === 'start' ? 'startLabel' : 'endLabel';\n    const [x, y] = this.getEndpoints(attributes)[type === 'start' ? 0 : 1];\n\n    const fontStyle = {\n      x,\n      y,\n      dx: type === 'start' ? 15 : -15,\n      fontSize: 16,\n      fill: 'gray',\n      textBaseline: 'middle',\n      textAlign: type,\n    };\n    const style = subStyleProps(attributes, key);\n    const text = style.text;\n    this.upsert(`label-${type}`, Text, text ? { ...fontStyle, ...style } : false, container);\n  }\n}\n\nregister(ExtensionCategory.EDGE, 'extra-label-edge', LabelEdge);\n\nconst graph = new Graph({\n  container: 'container',\n  renderer: () => new Renderer(),\n  data: {\n    nodes: [\n      { id: 'node-0', style: { x: 100, y: 100 } },\n      { id: 'node-1', style: { x: 300, y: 100 } },\n    ],\n    edges: [{ source: 'node-0', target: 'node-1' }],\n  },\n  edge: {\n    type: 'extra-label-edge',\n    style: {\n      startArrow: true,\n      endArrow: true,\n      stroke: '#F6BD16',\n      startLabelText: 'start',\n      endLabelText: 'end',\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n### Custom Arrow\n\n**文件路径**: `element/custom-edge/demo/custom-arrow.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(6).fill(0).map((_, i) => ({ id: `node${i + 1}` })),\n  edges: [\n    {\n      id: 'custom-arrow-1',\n      source: 'node1',\n      target: 'node2',\n      style: {\n        endArrowD: 'M-14,0 L-4,-4 L0,-14 L4,-4 L14,0 L4,4 L0,14 L-4,4 Z',\n        endArrowOffset: 14,\n      },\n    },\n    {\n      id: 'custom-arrow-2',\n      source: 'node3',\n      target: 'node4',\n      style: {\n        endArrowD: 'M 3,-5 L 3,5 L 15,10 L 15,-10 Z',\n        endArrowOffset: 10,\n      },\n    },\n    {\n      id: 'image-arrow',\n      source: 'node5',\n      target: 'node6',\n      style: {\n        endArrowSrc: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*N4ZMS7gHsUIAAAAAAAAAAABkARQnAQ',\n        endArrowSize: 28,\n        endArrowTransform: [['rotate', 90]],\n      },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  data,\n  edge: {\n    style: {\n      stroke: '#F6BD16',\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'grid',\n    cols: 2,\n  },\n});\n\ngraph.render();\n```\n\n---\n\n## element / custom-node\n\n### G2 bar chart\n\n**文件路径**: `element/custom-node/demo/g2-bar-chart.js`\n\n```js\nimport { Rect as RectGeometry } from '@antv/g';\nimport { renderToMountedElement, stdlib } from '@antv/g2';\nimport { ExtensionCategory, Graph, Rect, register } from '@antv/g6';\n\nclass BarChart extends Rect {\n  onCreate() {\n    const [width, height] = this.getSize();\n    const group = this.upsert(\n      'chart-container',\n      RectGeometry,\n      {\n        transform: `translate(${-width / 2}, ${-height / 2})`,\n        width,\n        height,\n        fill: '#fff',\n        stroke: '#697b8c',\n        radius: 10,\n        shadowColor: '#697b8c',\n        shadowBlur: 10,\n        shadowOffsetX: 5,\n        shadowOffsetY: 5,\n      },\n      this.shapeMap.key,\n    );\n\n    const { name, value } = this.attributes;\n    renderToMountedElement(\n      // @antv/g2 Specification\n      // https://g2.antv.antgroup.com/examples/general/interval/#column\n      {\n        width,\n        height,\n        data: { value },\n        title: name,\n        type: 'interval',\n        axis: {\n          x: { title: false },\n          y: { title: false },\n        },\n        scale: {\n          y: { domain: [0, 100] },\n        },\n        encode: {\n          x: 'subject',\n          y: 'score',\n          color: 'subject',\n        },\n        legend: { color: false },\n      },\n      {\n        group,\n        library: stdlib(),\n      },\n    );\n  }\n}\n\nregister(ExtensionCategory.NODE, 'bar-chart', BarChart);\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      {\n        id: 'Jack',\n        data: {\n          value: [\n            { subject: 'Math', score: 95 },\n            { subject: 'Chinese', score: 70 },\n            { subject: 'English', score: 75 },\n            { subject: 'Geography', score: 80 },\n            { subject: 'Physics', score: 90 },\n            { subject: 'Chemistry', score: 85 },\n            { subject: 'Biology', score: 70 },\n          ],\n        },\n      },\n      {\n        id: 'Aaron',\n        data: {\n          value: [\n            { subject: 'Math', score: 70 },\n            { subject: 'Chinese', score: 90 },\n            { subject: 'English', score: 90 },\n            { subject: 'Geography', score: 60 },\n            { subject: 'Physics', score: 70 },\n            { subject: 'Chemistry', score: 65 },\n            { subject: 'Biology', score: 80 },\n          ],\n        },\n      },\n      {\n        id: 'Rebecca',\n        data: {\n          value: [\n            { subject: 'Math', score: 60 },\n            { subject: 'Chinese', score: 95 },\n            { subject: 'English', score: 100 },\n            { subject: 'Geography', score: 80 },\n            { subject: 'Physics', score: 60 },\n            { subject: 'Chemistry', score: 90 },\n            { subject: 'Biology', score: 85 },\n          ],\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'bar-chart',\n    style: {\n      size: 250,\n      fillOpacity: 0,\n      name: (d) => d.id,\n      value: (d) => d.data.value,\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n### G2 activity Chart\n\n**文件路径**: `element/custom-node/demo/g2-activity-chart.js`\n\n```js\nimport { Circle as CircleGeometry } from '@antv/g';\nimport { renderToMountedElement, stdlib } from '@antv/g2';\nimport { Circle, ExtensionCategory, Graph, register } from '@antv/g6';\n\nclass ActivityChart extends Circle {\n  onCreate() {\n    const { value } = this.attributes;\n    const radius = this.shapeMap.key.style.r;\n    const activeRadius = radius / 4;\n    const activeSize = radius / 2;\n\n    const group = this.upsert(\n      'chart-container',\n      CircleGeometry,\n      {\n        r: radius,\n        fill: '#fff',\n      },\n      this.shapeMap.key,\n    );\n\n    renderToMountedElement(\n      // @antv/g2 Specification\n      // https://g2.antv.antgroup.com/examples/general/radial/#apple-activity\n      {\n        x: -radius,\n        y: -radius,\n        width: radius * 2,\n        height: radius * 2,\n        type: 'view',\n        data: value,\n        margin: 0,\n        coordinate: { type: 'radial', innerRadius: 0.2 },\n        children: [\n          {\n            type: 'interval',\n            encode: { x: 'name', y: 1, size: activeSize, color: 'color' },\n            scale: { color: { type: 'identity' } },\n            style: { fillOpacity: 0.25 },\n            animate: false,\n            tooltip: false,\n          },\n          {\n            type: 'interval',\n            encode: { x: 'name', y: 'percent', color: 'color', size: activeSize },\n            style: {\n              radius: activeRadius,\n              shadowColor: 'rgba(0,0,0,0.45)',\n              shadowBlur: 20,\n              shadowOffsetX: -2,\n              shadowOffsetY: -5,\n            },\n            animate: {\n              enter: { type: 'fadeIn', duration: 1000 },\n            },\n            axis: false,\n            tooltip: false,\n          },\n          {\n            type: 'image',\n            encode: { x: 'name', y: 0, src: (d) => d.icon, size: 6 },\n            style: { transform: [['translateX', 6]] },\n          },\n        ],\n      },\n      {\n        group,\n        library: stdlib(),\n      },\n    );\n  }\n}\n\nconst people = ['Aaron', 'Rebecca', 'Emily', 'Liam', 'Olivia', 'Ethan', 'Sophia', 'Mason'];\n\nconst mockData = () => {\n  const getRandomPercent = () => +(Math.random() * 0.9 + 0.1).toFixed(1);\n\n  return [\n    {\n      name: 'activity1',\n      percent: getRandomPercent(),\n      color: '#1ad5de',\n      icon: 'https://gw.alipayobjects.com/zos/antfincdn/ck11Y6aRrz/shangjiantou.png',\n    },\n    {\n      name: 'activity2',\n      percent: getRandomPercent(),\n      color: '#a0ff03',\n      icon: 'https://gw.alipayobjects.com/zos/antfincdn/zY2JB7hhrO/shuangjiantou.png',\n    },\n    {\n      name: 'activity3',\n      percent: getRandomPercent(),\n      color: '#e90b3a',\n      icon: 'https://gw.alipayobjects.com/zos/antfincdn/%24qBxSxdK05/jiantou.png',\n    },\n  ];\n};\n\nregister(ExtensionCategory.NODE, 'activity-chart', ActivityChart);\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'view',\n  data: {\n    nodes: people.map((name, i) => ({\n      id: name,\n      style: { value: mockData() },\n    })),\n    edges: people.map((_, i) => {\n      return {\n        id: `edge${i}`,\n        source: people[i],\n        target: people[(i + 1) % 5],\n      };\n    }),\n  },\n  node: {\n    type: 'activity-chart',\n    style: {\n      size: 50,\n      labelText: (d) => d.id,\n      fillOpacity: 0,\n    },\n    animation: {\n      enter: false,\n    },\n  },\n  layout: {\n    type: 'force',\n    preventOverlap: true,\n    animated: false,\n  },\n  behaviors: ['zoom-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n## element / edge\n\n### Line\n\n**文件路径**: `element/edge/demo/line.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'line',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      badge: true,\n      badgeText: '\\ue603',\n      badgeFontFamily: 'iconfont',\n      badgeBackgroundWidth: 12,\n      badgeBackgroundHeight: 12,\n    },\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 220,\n    linkDistance: 220,\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Polyline with Control Points\n\n**文件路径**: `element/edge/demo/polyline.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 200, y: 200 } },\n    { id: 'node-2', style: { x: 350, y: 120 } },\n  ],\n  edges: [\n    {\n      id: 'edge-1',\n      source: 'node-1',\n      target: 'node-2',\n      controlPoints: [[300, 190]],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'polyline',\n    style: {\n      controlPoints: (d) => d.controlPoints,\n    },\n  },\n  behaviors: [{ type: 'drag-element' }],\n});\n\ngraph.render();\n```\n\n---\n\n### Orthogonal Line\n\n**文件路径**: `element/edge/demo/polyline-orth.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 200, y: 200 } },\n    { id: 'node-2', style: { x: 350, y: 120 } },\n  ],\n  edges: [\n    {\n      id: 'edge-1',\n      source: 'node-1',\n      target: 'node-2',\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'polyline',\n    style: {\n      router: {\n        type: 'orth',\n      },\n    },\n  },\n  behaviors: [{ type: 'drag-element' }],\n});\n\ngraph.render();\n```\n\n---\n\n### Orthogonal Line with Control Points\n\n**文件路径**: `element/edge/demo/polyline-orth-with-cps.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 200, y: 200 } },\n    { id: 'node-2', style: { x: 350, y: 120 } },\n    { id: 'node-cp', style: { x: 300, y: 190, size: 5, fill: 'rgb(244, 109, 67)' } },\n  ],\n  edges: [\n    {\n      id: 'edge-1',\n      source: 'node-1',\n      target: 'node-2',\n      style: { controlPoints: [[300, 190]] },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'polyline',\n    style: {\n      router: {\n        type: 'orth',\n      },\n      controlPoints: (d) => d.style.controlPoints,\n    },\n  },\n  behaviors: [{ type: 'drag-element' }],\n});\n\ngraph.render();\n```\n\n---\n\n### Quadratic\n\n**文件路径**: `element/edge/demo/quadratic.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'quadratic',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      badge: true,\n      badgeText: '\\ue603',\n      badgeFontFamily: 'iconfont',\n      badgeBackgroundWidth: 12,\n      badgeBackgroundHeight: 12,\n    },\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 220,\n    linkDistance: 220,\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Cubic\n\n**文件路径**: `element/edge/demo/cubic.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  edge: {\n    type: 'cubic',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      badge: true,\n      badgeText: '\\ue603',\n      badgeFontFamily: 'iconfont',\n      badgeBackgroundWidth: 12,\n      badgeBackgroundHeight: 12,\n    },\n  },\n  layout: {\n    type: 'radial',\n    unitRadius: 220,\n    linkDistance: 220,\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Vertical Cubic\n\n**文件路径**: `element/edge/demo/vertical-cubic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      port: true,\n      ports: [{ placement: 'top' }, { placement: 'bottom' }],\n    },\n  },\n  edge: {\n    type: 'cubic-vertical',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'antv-dagre',\n    begin: [50, 50],\n    rankdir: 'TB',\n    nodesep: 20,\n    ranksep: 120,\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Horizontal Cubic\n\n**文件路径**: `element/edge/demo/horizontal-cubic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n    },\n    {\n      id: 'node2',\n    },\n    {\n      id: 'node3',\n    },\n    {\n      id: 'node4',\n    },\n    {\n      id: 'node5',\n    },\n    {\n      id: 'node6',\n    },\n  ],\n  edges: [\n    {\n      id: 'line-default',\n      source: 'node1',\n      target: 'node2',\n    },\n    {\n      id: 'line-active',\n      source: 'node1',\n      target: 'node3',\n      states: ['active'],\n    },\n    {\n      id: 'line-selected',\n      source: 'node1',\n      target: 'node4',\n      states: ['selected'],\n    },\n    {\n      id: 'line-highlight',\n      source: 'node1',\n      target: 'node5',\n      states: ['highlight'],\n    },\n    {\n      id: 'line-inactive',\n      source: 'node1',\n      target: 'node6',\n      states: ['inactive'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      port: true,\n      ports: [{ placement: 'right' }, { placement: 'left' }],\n    },\n  },\n  edge: {\n    type: 'cubic-horizontal',\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n    },\n  },\n  layout: {\n    type: 'antv-dagre',\n    rankdir: 'LR',\n    nodesep: 20,\n    ranksep: 120,\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Polyline Loop\n\n**文件路径**: `element/edge/demo/loop-polyline.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3-ports' }, { id: 'node4-ports' }],\n  edges: [\n    {\n      id: 'loop-1',\n      source: 'node1',\n      target: 'node1',\n      placement: 'top',\n    },\n    {\n      id: 'loop-2',\n      source: 'node1',\n      target: 'node1',\n      placement: 'right',\n    },\n    {\n      id: 'loop-3',\n      source: 'node1',\n      target: 'node1',\n      placement: 'bottom',\n    },\n    {\n      id: 'loop-4',\n      source: 'node1',\n      target: 'node1',\n      placement: 'left',\n    },\n    {\n      id: 'loop-5',\n      source: 'node2',\n      target: 'node2',\n      placement: 'top-right',\n    },\n    {\n      id: 'loop-6',\n      source: 'node2',\n      target: 'node2',\n      placement: 'bottom-right',\n    },\n    {\n      id: 'loop-7',\n      source: 'node2',\n      target: 'node2',\n      placement: 'bottom-left',\n    },\n    {\n      id: 'loop-8',\n      source: 'node2',\n      target: 'node2',\n      placement: 'top-left',\n    },\n    {\n      id: 'loop-9',\n      source: 'node3-ports',\n      target: 'node3-ports',\n      style: { sourcePort: 'port1', targetPort: 'port2' },\n    },\n    {\n      id: 'loop-10',\n      source: 'node4-ports',\n      target: 'node4-ports',\n      style: { sourcePort: 'port2', targetPort: 'port2' },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      size: [80, 30],\n      port: (d) => d.id.includes('ports'),\n      portR: 3,\n      ports: [\n        {\n          key: 'port1',\n          placement: [0.7, 0],\n        },\n        {\n          key: 'port2',\n          placement: 'right',\n        },\n      ],\n    },\n  },\n  edge: {\n    type: 'polyline',\n    style: {\n      sourcePort: (d) => d.style.sourcePort,\n      targetPort: (d) => d.style.targetPort,\n      endArrow: true,\n      loopPlacement: (d) => d.placement,\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Curve Loop\n\n**文件路径**: `element/edge/demo/loop-curve.js`\n\n```js\nimport { Graph, idOf } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node1' }, { id: 'node2' }, { id: 'node3-ports' }, { id: 'node4-ports' }],\n  edges: [\n    {\n      id: 'loop-1',\n      source: 'node1',\n      target: 'node1',\n      style: { placement: 'top' },\n    },\n    {\n      id: 'loop-2',\n      source: 'node1',\n      target: 'node1',\n      style: { placement: 'right' },\n    },\n    {\n      id: 'loop-3',\n      source: 'node1',\n      target: 'node1',\n      style: { placement: 'bottom' },\n    },\n    {\n      id: 'loop-4',\n      source: 'node1',\n      target: 'node1',\n      style: { placement: 'left' },\n    },\n    {\n      id: 'loop-5',\n      source: 'node2',\n      target: 'node2',\n      style: { placement: 'top-right' },\n    },\n    {\n      id: 'loop-6',\n      source: 'node2',\n      target: 'node2',\n      style: { placement: 'bottom-right' },\n    },\n    {\n      id: 'loop-7',\n      source: 'node2',\n      target: 'node2',\n      style: { placement: 'bottom-left' },\n    },\n    {\n      id: 'loop-8',\n      source: 'node2',\n      target: 'node2',\n      style: { placement: 'top-left' },\n    },\n    {\n      id: 'loop-9',\n      source: 'node3-ports',\n      target: 'node3-ports',\n      style: { sourcePort: 'port1', targetPort: 'port2' },\n    },\n    {\n      id: 'loop-10',\n      source: 'node4-ports',\n      target: 'node4-ports',\n      style: { sourcePort: 'port2', targetPort: 'port2' },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      size: [80, 30],\n      port: (d) => idOf(d).includes('ports'),\n      portR: 3,\n      ports: [\n        {\n          key: 'port1',\n          placement: [0.7, 0],\n        },\n        {\n          key: 'port2',\n          placement: 'right',\n        },\n      ],\n    },\n  },\n  edge: {\n    type: 'line',\n    style: {\n      sourcePort: (d) => d.style.sourcePort,\n      targetPort: (d) => d.style.targetPort,\n      endArrow: true,\n      loopPlacement: (d) => d.style.placement,\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Arrows\n\n**文件路径**: `element/edge/demo/arrows.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(16).fill(0).map((_, i) => ({ id: `node${i + 1}` })),\n  edges: [\n    'default-arrow',\n    'triangle-arrow',\n    'simple-arrow',\n    'vee-arrow',\n    'circle-arrow',\n    'rect-arrow',\n    'diamond-arrow',\n    'triangleRect-arrow',\n  ].map((id, i) => ({\n    id,\n    source: `node${i * 2 + 1}`,\n    target: `node${i * 2 + 2}`,\n  })),\n};\n\nconst graph = new Graph({\n  data,\n  edge: {\n    style: {\n      labelText: (d) => d.id,\n      labelBackground: true,\n      endArrow: true,\n      endArrowType: (d) => d.id.split('-')[0],\n    },\n  },\n  layout: {\n    type: 'grid',\n    cols: 2,\n  },\n});\n\ngraph.render();\n```\n\n---\n\n## element / label\n\n### Copy Content\n\n**文件路径**: `element/label/demo/copy.js`\n\n```js\nimport { Graph, NodeEvent } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: 'node1',\n      data: {\n        label: 'Click to copy this label which is too long to be displayed',\n      },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      x: 200,\n      y: 200,\n      size: 150,\n      labelPlacement: 'center',\n      labelText: (d) => d.data.label,\n      labelWordWrap: true,\n      labelMaxWidth: '90%',\n      labelBackground: true,\n      labelBackgroundFill: '#eee',\n      labelBackgroundFillOpacity: 0.5,\n      labelBackgroundRadius: 4,\n      labelPointerEvents: 'none',\n      labelBackgroundPointerEvents: 'none',\n    },\n  },\n  behaviors: ['drag-element'],\n  plugins: [\n    {\n      type: 'tooltip',\n      getContent: (e, items) => {\n        let result = `<h4>Node Label:</h4>`;\n        items.forEach((item) => {\n          result += `<p>${item.data.label}</p>`;\n        });\n        return result;\n      },\n    },\n  ],\n});\n\ngraph.render();\n\ngraph.on('node:click', (e) => {\n  const node = graph.getNodeData(e.target.id);\n  const label = node?.data?.label;\n\n  navigator.clipboard.writeText(label);\n  alert('copied to clipboard!');\n});\n\ngraph.on(NodeEvent.POINTER_ENTER, (e) => {\n  graph.setElementState({ [e.target.id]: 'active' });\n});\n\ngraph.on(NodeEvent.POINTER_OUT, (e) => {\n  graph.setElementState({ [e.target.id]: [] });\n});\n```\n\n---\n\n### Text Ellipsis\n\n**文件路径**: `element/label/demo/ellipsis.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 100, y: 150, size: 100 } },\n    { id: 'node-2', style: { x: 400, y: 150, size: 150 } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelPlacement: 'center',\n      labelText: 'This label is too long to be displayed',\n      labelWordWrap: true, // enable label ellipsis\n      labelMaxWidth: '90%',\n      labelBackground: true,\n      labelBackgroundFill: '#eee',\n      labelBackgroundFillOpacity: 0.5,\n      labelBackgroundRadius: 4,\n    },\n  },\n  edge: {\n    style: {\n      labelOffsetY: -4,\n      labelTextBaseline: 'bottom',\n      labelText: 'This label is too long to be displayed',\n      labelWordWrap: true,\n      labelMaxWidth: '80%',\n      labelBackground: true,\n      labelBackgroundFill: 'red',\n      labelBackgroundFillOpacity: 0.5,\n      labelBackgroundRadius: 4,\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n### Word Wrap\n\n**文件路径**: `element/label/demo/word-wrap.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', style: { x: 100, y: 150, size: 100 } },\n    { id: 'node-2', style: { x: 400, y: 150, size: 150 } },\n  ],\n  edges: [{ source: 'node-1', target: 'node-2' }],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  fitCenter: true,\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      labelPlacement: 'bottom',\n      labelText: 'This label is too long to be displayed',\n      labelMaxWidth: '90%',\n      labelBackground: true,\n      labelBackgroundFill: '#eee',\n      labelBackgroundFillOpacity: 0.5,\n      labelBackgroundRadius: 4,\n      labelWordWrap: true,\n      labelMaxLines: 4,\n    },\n  },\n  edge: {\n    style: {\n      labelOffsetY: -4,\n      labelTextBaseline: 'bottom',\n      labelText: 'This label is too long to be displayed',\n      labelMaxWidth: '80%',\n      labelBackground: true,\n      labelBackgroundFill: 'red',\n      labelBackgroundFillOpacity: 0.5,\n      labelBackgroundRadius: 4,\n      labelWordWrap: true,\n      labelMaxLines: 4,\n    },\n  },\n  behaviors: ['drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n### Background\n\n**文件路径**: `element/label/demo/background.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node1', style: { x: 150, y: 100 } },\n    { id: 'node2', style: { x: 250, y: 200 } },\n    { id: 'node3', style: { x: 450, y: 200 } },\n  ],\n  edges: [\n    { source: 'node1', target: 'node2' },\n    { source: 'node1', target: 'node3' },\n    { source: 'node2', target: 'node3' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n      labelPosition: 'bottom',\n      labelFill: '#e66465',\n      labelFontSize: 12,\n      labelFontStyle: 'italic',\n      labelBackground: true,\n      labelBackgroundFill: 'linear-gradient(#e66465, #9198e5)',\n      labelBackgroundStroke: '#9ec9ff',\n      labelBackgroundRadius: 2,\n    },\n  },\n  edge: {\n    style: {\n      labelText: (d) => d.id,\n      labelPosition: 'center',\n      labelTextBaseline: 'top',\n      labelDy: 5,\n      labelFontSize: 12,\n      labelFontWeight: 'bold',\n      labelFill: '#1890ff',\n      labelBackground: true,\n      labelBackgroundFill: 'linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)',\n      labelBackgroundStroke: '#9ec9ff',\n      labelBackgroundRadius: 2,\n    },\n  },\n  layout: {\n    type: 'force',\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n## element / node\n\n### Circle\n\n**文件路径**: `element/node/demo/circle.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'circle',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Rect\n\n**文件路径**: `element/node/demo/rect.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Radius Rect\n\n**文件路径**: `element/node/demo/rounded-rect.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      radius: 4, // 👈🏻 Set the radius.\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Diamond\n\n**文件路径**: `element/node/demo/diamond.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'diamond',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Hexagon\n\n**文件路径**: `element/node/demo/hexagon.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'hexagon',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      outerR: 30, // 外半径\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Triangle\n\n**文件路径**: `element/node/demo/triangle.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'triangle',\n    style: {\n      size: 40,\n      direction: (d) => (d.id === 'ports' ? 'left' : undefined),\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Image\n\n**文件路径**: `element/node/demo/image.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'image',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      src: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*N4ZMS7gHsUIAAAAAAAAAAABkARQnAQ',\n      haloStroke: '#227eff',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n    state: {\n      inactive: {\n        fillOpacity: 0.5,\n      },\n      disabled: {\n        fillOpacity: 0.2,\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Ellipse\n\n**文件路径**: `element/node/demo/ellipse.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'ellipse',\n    style: {\n      size: [45, 35],\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### Star\n\n**文件路径**: `element/node/demo/star.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default' },\n    { id: 'halo' },\n    { id: 'badges' },\n    { id: 'ports' },\n    {\n      id: 'active',\n      states: ['active'],\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    type: 'star',\n    style: {\n      size: 40,\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### HTML节点\n\n**文件路径**: `element/node/demo/html.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst ICON_MAP = {\n  error: '&#10060;',\n  overload: '&#9889;',\n  running: '&#9989;',\n};\n\nconst COLOR_MAP = {\n  error: '#f5222d',\n  overload: '#faad14',\n  running: '#52c41a',\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: 'node-1', data: { location: 'East', status: 'error', ip: '192.168.1.2' } },\n      { id: 'node-2', data: { location: 'West', status: 'overload', ip: '192.168.1.3' } },\n      { id: 'node-3', data: { location: 'South', status: 'running', ip: '192.168.1.4' } },\n    ],\n  },\n  node: {\n    type: 'html',\n    style: {\n      size: [240, 80],\n      dx: -120,\n      dy: -40,\n      innerHTML: (d) => {\n        const {\n          data: { location, status, ip },\n        } = d;\n        const color = COLOR_MAP[status];\n\n        return `\n<div \n  style=\"\n    width:100%; \n    height: 100%; \n    background: ${color}bb; \n    border: 1px solid ${color};\n    color: #fff;\n    user-select: none;\n    display: flex; \n    padding: 10px;\n    \"\n>\n  <div style=\"display: flex;flex-direction: column;flex: 1;\">\n    <div style=\"font-weight: bold;\">\n      ${location} Node\n    </div>\n    <div>\n      status: ${status} ${ICON_MAP[status]}\n    </div>\n  </div>\n  <div>\n    <span style=\"border: 1px solid white; padding: 2px;\">\n      ${ip}\n    </span>\n  </div>\n</div>`;\n      },\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['drag-element', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n---\n\n### Donut\n\n**文件路径**: `element/node/demo/donut.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst data = {\n  nodes: [\n    { id: 'default', index: 0 },\n    { id: 'halo', index: 1 },\n    { id: 'badges', index: 2 },\n    { id: 'ports', index: 3 },\n    {\n      id: 'active',\n      states: ['active'],\n      index: 4,\n    },\n    {\n      id: 'selected',\n      states: ['selected'],\n      index: 5,\n    },\n    {\n      id: 'highlight',\n      states: ['highlight'],\n      index: 6,\n    },\n    {\n      id: 'inactive',\n      states: ['inactive'],\n      index: 7,\n    },\n    {\n      id: 'disabled',\n      states: ['disabled'],\n      index: 8,\n    },\n  ],\n};\nconst graph = new Graph({\n  container: 'container',\n  animation: false,\n  data,\n  node: {\n    type: 'donut',\n    style: {\n      size: 80,\n      fill: '#DB9D0D',\n      innerR: 20,\n      donuts: (item) => {\n        const { index } = item;\n        if (index === 0) return [1, 2, 3]; // donuts数据类型为number[]时，根据值的大小决定环的占比\n\n        if (index === 1) {\n          return [\n            { value: 50, color: 'red' },\n            { value: 150, color: 'green' },\n            { value: 100, color: 'blue' },\n          ];\n        }\n\n        if (index === 4) {\n          return [\n            { value: 150, fill: 'pink', stroke: '#fff', lineWidth: 1 },\n            { value: 250, stroke: '#fff', lineWidth: 1 },\n            { value: 200, stroke: '#fff', lineWidth: 1 },\n          ];\n        }\n\n        return [100, 200, 100, 200];\n      },\n      labelText: (d) => d.id,\n      iconFontFamily: 'iconfont',\n      iconText: '\\ue602',\n      halo: (d) => (d.id === 'halo' ? true : false),\n      badges: (d) =>\n        d.id === 'badges'\n          ? [\n              {\n                text: 'A',\n                placement: 'right-top',\n              },\n              {\n                text: 'Important',\n                placement: 'right',\n              },\n              {\n                text: 'Notice',\n                placement: 'right-bottom',\n              },\n            ]\n          : [],\n      badgeFontSize: 8,\n      badgePadding: [1, 4],\n      portR: 3,\n      ports: (d) =>\n        d.id === 'ports'\n          ? [{ placement: 'left' }, { placement: 'right' }, { placement: 'top' }, { placement: 'bottom' }]\n          : [],\n    },\n  },\n  layout: {\n    type: 'grid',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n### 3D Node\n\n**文件路径**: `element/node/demo/3d-node.js`\n\n```js\nimport { ExtensionCategory, Graph, register } from '@antv/g6';\nimport {\n  Capsule,\n  Cone,\n  Cube,\n  Cylinder,\n  Light,\n  ObserveCanvas3D,\n  Plane,\n  Sphere,\n  Torus,\n  renderer,\n} from '@antv/g6-extension-3d';\n\nregister(ExtensionCategory.PLUGIN, '3d-light', Light);\nregister(ExtensionCategory.NODE, 'sphere', Sphere);\nregister(ExtensionCategory.NODE, 'plane', Plane);\nregister(ExtensionCategory.NODE, 'cylinder', Cylinder);\nregister(ExtensionCategory.NODE, 'cone', Cone);\nregister(ExtensionCategory.NODE, 'cube', Cube);\nregister(ExtensionCategory.NODE, 'capsule', Capsule);\nregister(ExtensionCategory.NODE, 'torus', Torus);\nregister(ExtensionCategory.BEHAVIOR, 'observe-canvas-3d', ObserveCanvas3D);\n\nconst nodes = [\n  {\n    id: 'node-1',\n    type: 'sphere',\n    style: {\n      texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*cdTdTI2bNl8AAAAAAAAAAAAADmJ7AQ/original',\n    },\n  },\n  { id: 'node-2', type: 'plane', style: { size: 50 } },\n  { id: 'node-3', type: 'cylinder' },\n  { id: 'node-4', type: 'cone' },\n  {\n    id: 'node-5',\n    type: 'cube',\n    style: {\n      texture: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*8TlCRIsKeUkAAAAAAAAAAAAAARQnAQ',\n    },\n  },\n  { id: 'node-6', type: 'capsule' },\n  { id: 'node-7', type: 'torus' },\n];\n\nconst graph = new Graph({\n  container: 'container',\n  renderer,\n  data: {\n    nodes,\n  },\n  node: {\n    style: {\n      materialType: 'phong',\n      labelText: (d) => d.id,\n      x: (d) => 100 + (nodes.findIndex((n) => n.id === d.id) % 5) * 100,\n      y: (d) => 100 + Math.floor(nodes.findIndex((n) => n.id === d.id) / 5) * 100,\n    },\n  },\n  plugins: [\n    {\n      type: '3d-light',\n      directional: {\n        direction: [0, 0, 1],\n      },\n    },\n  ],\n  behaviors: ['observe-canvas-3d'],\n});\n\ngraph.render();\n```\n\n---\n\n### Port\n\n**文件路径**: `element/node/demo/port.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', style: { x: 80, y: 200, size: 30 } },\n    {\n      id: 'node-2',\n      type: 'rect',\n      style: {\n        x: 250,\n        y: 200,\n        size: 50,\n        port: true,\n        ports: [\n          { key: 'port-1', placement: [0, 0.15] },\n          { key: 'port-2', placement: [0, 0.5] },\n          { key: 'port-3', placement: [0, 0.85] },\n        ],\n      },\n    },\n  ],\n  edges: [\n    { id: 'edge-1', source: 'node-1', target: 'node-2', style: { targetPort: 'port-1' } },\n    { id: 'edge-2', source: 'node-1', target: 'node-2', style: { targetPort: 'port-2' } },\n    { id: 'edge-3', source: 'node-1', target: 'node-2', style: { targetPort: 'port-3' } },\n  ],\n};\n\nconst graph = new Graph({\n  data,\n  edge: {\n    style: {\n      endArrow: true,\n    },\n  },\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = { show: false, position: 'outline' };\n  gui.add(config, 'position', ['outline', 'center']).onChange((value) => {\n    graph.updateNodeData([{ id: 'node-2', style: { portLinkToCenter: value === 'center' } }]);\n    graph.draw();\n  });\n  gui\n    .add(config, 'show')\n    .onChange((value) => {\n      graph.updateNodeData([{ id: 'node-2', style: { portR: value ? 5 : 0 } }]);\n      graph.draw();\n    })\n    .name('show ports');\n});\n```\n\n---\n\n## feature / default\n\n### Switch Theme\n\n**文件路径**: `feature/default/demo/theme.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst themes = {\n  '🌞 Light': {\n    theme: 'light',\n    node: {\n      style: { size: 4 },\n      palette: {\n        type: 'group',\n        field: 'cluster',\n      },\n    },\n    plugins: [{ type: 'background', background: '#fff' }],\n  },\n  '🌚 Dark': {\n    theme: 'dark',\n    node: {\n      style: { size: 4 },\n      palette: {\n        type: 'group',\n        field: 'cluster',\n      },\n    },\n    plugins: [{ type: 'background', background: '#000' }],\n  },\n  '🌎 Blue': {\n    theme: 'light',\n    node: {\n      style: { size: 4 },\n      palette: {\n        type: 'group',\n        field: 'cluster',\n        color: 'blues',\n        invert: true,\n      },\n    },\n    plugins: [{ type: 'background', background: '#f3faff' }],\n  },\n  '🌕 Yellow': {\n    background: '#fcf9f1',\n    theme: 'light',\n    node: {\n      style: { size: 4 },\n      palette: {\n        type: 'group',\n        field: 'cluster',\n        color: ['#ffe7ba', '#ffd591', '#ffc069', '#ffa940', '#fa8c16', '#d46b08', '#ad4e00', '#873800', '#612500'],\n      },\n    },\n    plugins: [{ type: 'background', background: '#fcf9f1' }],\n  },\n};\n\nfetch('https://assets.antv.antgroup.com/g6/20000.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      animation: false,\n      padding: 20,\n      autoFit: 'view',\n      theme: 'light',\n      data,\n      node: {\n        style: { size: 4 },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas'],\n      plugins: [{ type: 'background', background: '#fff' }],\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      gui.add({ theme: '🌞 Light' }, 'theme', Object.keys(themes)).onChange((theme) => {\n        graph.setOptions(themes[theme]);\n        graph.draw();\n      });\n    });\n  });\n```\n\n---\n\n### Lite Solar System\n\n**文件路径**: `feature/default/demo/lite-solar-system.js`\n\n```js\nimport { ExtensionCategory, Graph, register } from '@antv/g6';\nimport { Light, Sphere, renderer } from '@antv/g6-extension-3d';\n\nregister(ExtensionCategory.PLUGIN, 'light', Light);\nregister(ExtensionCategory.NODE, 'sphere', Sphere);\n\nconst graph = new Graph({\n  renderer,\n  data: {\n    nodes: [\n      {\n        id: 'sum',\n        style: {\n          x: 300,\n          y: 300,\n          radius: 100,\n          texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-mZfQr8LtPUAAAAAAAAAAAAADmJ7AQ/original',\n        },\n      },\n      {\n        id: 'mars',\n        style: {\n          x: 430,\n          y: 300,\n          z: 0,\n          radius: 20,\n          texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*mniGTZktpecAAAAAAAAAAAAADmJ7AQ/original',\n        },\n      },\n      {\n        id: 'earth',\n        style: {\n          x: 500,\n          y: 300,\n          z: 0,\n          radius: 30,\n          texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*cdTdTI2bNl8AAAAAAAAAAAAADmJ7AQ/original',\n        },\n      },\n      {\n        id: 'jupiter',\n        style: {\n          x: 600,\n          y: 300,\n          z: 0,\n          radius: 50,\n          texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*t_mQSZYAT70AAAAAAAAAAAAADmJ7AQ/original',\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'sphere',\n    style: {\n      materialShininess: 0,\n      labelText: (d) => d.id,\n      labelFill: '#fff',\n    },\n  },\n  plugins: [\n    {\n      type: '3d-light',\n      directional: {\n        direction: [0, 0, 1],\n      },\n    },\n    {\n      type: 'background',\n      backgroundImage:\n        'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*M_OaRrzIZOEAAAAAAAAAAAAADmJ7AQ/original)',\n      backgroundPosition: 'center',\n    },\n  ],\n});\n\ngraph.draw().then(() => {\n  const element = graph.context.element;\n\n  const sum = element.getElement('sum');\n  const mars = element.getElement('mars');\n  const earth = element.getElement('earth');\n  const jupiter = element.getElement('jupiter');\n\n  const setRotation = (element, speed) => {\n    setInterval(() => {\n      element.rotate(0, -speed, 0);\n    }, 30);\n  };\n  setRotation(sum, 0.1);\n  setRotation(mars, 0.8);\n  setRotation(earth, 1);\n  setRotation(jupiter, 0.5);\n\n  const setRevolution = (element, center, speed) => {\n    setInterval(() => {\n      const [x, y, z] = element.getPosition();\n      const [cx, , cz] = center;\n      const angle = (speed * Math.PI) / 180;\n\n      const newX = (x - cx) * Math.cos(angle) + (z - cz) * Math.sin(angle) + cx;\n      const newZ = -(x - cx) * Math.sin(angle) + (z - cz) * Math.cos(angle) + cz;\n\n      element.setPosition(newX, y, newZ);\n    }, 30);\n  };\n\n  setRevolution(mars, [300, 300, 0], 1.5);\n  setRevolution(earth, [300, 300, 0], 1);\n  setRevolution(jupiter, [300, 300, 0], 0.5);\n});\n```\n\n---\n\n### 3D Massive Data\n\n**文件路径**: `feature/default/demo/3d-massive.js`\n\n```js\nimport { CameraSetting, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { Light, Line3D, ObserveCanvas3D, Sphere, ZoomCanvas3D, renderer } from '@antv/g6-extension-3d';\n\nregister(ExtensionCategory.PLUGIN, '3d-light', Light);\nregister(ExtensionCategory.NODE, 'sphere', Sphere);\nregister(ExtensionCategory.EDGE, 'line3d', Line3D);\nregister(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting);\nregister(ExtensionCategory.BEHAVIOR, 'zoom-canvas-3d', ZoomCanvas3D);\nregister(ExtensionCategory.BEHAVIOR, 'observe-canvas-3d', ObserveCanvas3D);\n\nfetch('https://assets.antv.antgroup.com/g6/eva-3d-data.json')\n  .then((res) => res.json())\n  .then(({ nodes, edges }) => {\n    const degree = new Map();\n    edges.forEach(({ source, target }) => {\n      if (!degree.has(source)) degree.set(source, 0);\n      if (!degree.has(target)) degree.set(target, 0);\n      degree.set(source, degree.get(source) + 1);\n      degree.set(target, degree.get(target) + 1);\n    });\n    nodes.forEach((node) => {\n      const { id } = node;\n      Object.assign(node.data, { degree: degree.get(id) ?? 0 });\n      return node;\n    });\n\n    return { nodes, edges };\n  })\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      renderer,\n      data,\n      animation: false,\n      node: {\n        type: 'sphere',\n        style: {\n          materialType: 'phong',\n          size: (d) => 50 + d.data.degree,\n          x: (d) => d.data.x,\n          y: (d) => d.data.y,\n          z: (d) => d.data.z,\n        },\n        palette: {\n          color: 'tableau',\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      edge: {\n        type: 'line3d',\n        style: {\n          lineWidth: 0.4,\n          opacity: 0.4,\n          stroke: '#fff',\n        },\n      },\n      behaviors: ['observe-canvas-3d', 'zoom-canvas-3d'],\n      plugins: [\n        {\n          type: 'camera-setting',\n          projectionMode: 'orthographic',\n          near: 1,\n          far: 10000,\n          fov: 45,\n          aspect: 1,\n        },\n        {\n          type: '3d-light',\n          directional: {\n            direction: [0, 0, 1],\n          },\n        },\n        {\n          type: 'background',\n          background: '#000',\n        },\n      ],\n    });\n\n    graph.draw().then(() => {\n      const camera = graph.getCanvas().getCamera();\n      let frame;\n      let counter = 0;\n      const tick = () => {\n        if (counter < 80) {\n          camera.dolly(4);\n        }\n        camera.rotate(0.4, 0);\n        counter++;\n\n        frame = requestAnimationFrame(tick);\n        if (counter > 160 && frame) {\n          cancelAnimationFrame(frame);\n        }\n      };\n\n      tick();\n    });\n  });\n```\n\n---\n\n### Information Density\n\n**文件路径**: `feature/default/demo/unicorns-investors.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\n/**\n * Inspired by https://graphcommons.com/graphs/be8bc972-5b26-4f5c-837d-a34704f33a9e\n */\nfetch('https://assets.antv.antgroup.com/g6/unicorns-investors.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const size = (node) => Math.max(...node.style.size);\n\n    const graph = new Graph({\n      data,\n      autoFit: 'view',\n      node: {\n        style: {\n          fillOpacity: 1,\n          label: true,\n          labelText: (d) => d.data?.name,\n          labelBackground: true,\n          icon: true,\n          iconText: (d) => (d.data?.type === 'Investor' ? '💰' : '🦄️'),\n          fill: (d) => (d.data?.type === 'Investor' ? '#6495ED' : '#FFA07A'),\n        },\n        state: {\n          inactive: {\n            fillOpacity: 0.3,\n            icon: false,\n            label: false,\n          },\n        },\n      },\n      edge: {\n        style: {\n          label: false,\n          labelText: (d) => d.data?.type,\n          labelBackground: true,\n        },\n        state: {\n          active: {\n            label: true,\n          },\n          inactive: {\n            strokeOpacity: 0,\n          },\n        },\n      },\n      layout: {\n        type: 'd3-force',\n        link: { distance: (edge) => size(edge.source) + size(edge.target) },\n        collide: { radius: (node) => size(node) },\n        manyBody: { strength: (node) => -4 * size(node) },\n        animation: false,\n      },\n      transforms: [\n        {\n          type: 'map-node-size',\n          scale: 'linear',\n          maxSize: 60,\n          minSize: 20,\n          mapLabelSize: [12, 24],\n        },\n      ],\n      behaviors: [\n        'drag-canvas',\n        'zoom-canvas',\n        function () {\n          return {\n            key: 'hover-activate',\n            type: 'hover-activate',\n            enable: (e) => e.targetType === 'node',\n            degree: 1,\n            inactiveState: 'inactive',\n            onHover: (e) => {\n              this.frontElement(e.target.id);\n              e.view.setCursor('pointer');\n            },\n            onHoverEnd: (e) => {\n              e.view.setCursor('default');\n            },\n          };\n        },\n        {\n          type: 'fix-element-size',\n          enable: true,\n        },\n        'auto-adapt-label',\n      ],\n      animation: false,\n    });\n\n    graph.render();\n  });\n\nconst container = document.getElementById('container');\nconst descriptionDiv = document.createElement('div');\ndescriptionDiv.innerHTML = 'Network Map of 🦄 Unicorns and Their 💰Investors - 1086 nodes, 1247 edges';\ncontainer.appendChild(descriptionDiv);\n```\n\n---\n\n## layout / circular\n\n### Basic Circular Layout\n\n**文件路径**: `layout/circular/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelFill: '#fff',\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'circular',\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Degree Ordered\n\n**文件路径**: `layout/circular/demo/degree.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelFill: '#fff',\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'circular',\n        ordering: 'degree',\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Spiral Layout\n\n**文件路径**: `layout/circular/demo/spiral.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'center',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelFill: '#fff',\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'circular',\n        startRadius: 10,\n        endRadius: 300,\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Divided Circular Layout\n\n**文件路径**: `layout/circular/demo/division.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'center',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelFill: '#fff',\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'circular',\n        divisions: 5,\n        radius: 200,\n        startAngle: Math.PI / 4,\n        endAngle: Math.PI,\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## layout / combo-layout\n\n### Combo Combined Layout\n\n**文件路径**: `layout/combo-layout/demo/combo-combined.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/combo.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'combo-combined',\n        comboPadding: 2,\n      },\n      node: {\n        style: {\n          size: 20,\n          labelText: (d) => d.id,\n        },\n        palette: {\n          type: 'group',\n          field: (d) => d.combo,\n        },\n      },\n      edge: {\n        style: (model) => {\n          const { size, color } = model.data;\n          return {\n            stroke: color || '#99ADD1',\n            lineWidth: size || 1,\n          };\n        },\n      },\n      behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n      autoFit: 'view',\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## layout / compact-box\n\n### CompactBox Layout\n\n**文件路径**: `layout/compact-box/demo/basic.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\n/**\n * If the node is a leaf node\n * @param {*} d - node data\n * @returns {boolean} - whether the node is a leaf node\n */\nfunction isLeafNode(d) {\n  return !d.children || d.children.length === 0;\n}\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: (d) => (isLeafNode(d) ? 'right' : 'left'),\n          labelBackground: true,\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'compact-box',\n        direction: 'LR',\n        getHeight: function getHeight() {\n          return 32;\n        },\n        getWidth: function getWidth() {\n          return 32;\n        },\n        getVGap: function getVGap() {\n          return 10;\n        },\n        getHGap: function getHGap() {\n          return 100;\n        },\n      },\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Top to Bottom CompactBox\n\n**文件路径**: `layout/compact-box/demo/vertical.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\n/**\n * If the node is a leaf node\n * @param {*} d - node data\n * @returns {boolean} - whether the node is a leaf node\n */\nfunction isLeafNode(d) {\n  return !d.children || d.children.length === 0;\n}\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n      node: {\n        style: (d) => {\n          const style = {\n            labelText: d.id,\n            labelPlacement: 'right',\n            labelOffsetX: 2,\n            labelBackground: true,\n            ports: [{ placement: 'top' }, { placement: 'bottom' }],\n          };\n          if (isLeafNode(d)) {\n            Object.assign(style, {\n              labelTransform: [\n                ['rotate', 90],\n                ['translate', 18],\n              ],\n              labelBaseline: 'center',\n              labelTextAlign: 'left',\n            });\n          }\n          return style;\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-vertical',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'compact-box',\n        direction: 'TB',\n        getHeight: function getHeight() {\n          return 16;\n        },\n        getWidth: function getWidth() {\n          return 16;\n        },\n        getVGap: function getVGap() {\n          return 80;\n        },\n        getHGap: function getHGap() {\n          return 20;\n        },\n      },\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Radial Layout\n\n**文件路径**: `layout/compact-box/demo/radial.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelBackground: true,\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'compact-box',\n        radial: true,\n        direction: 'RL',\n        getId: function getId(d) {\n          return d.id;\n        },\n        getHeight: () => {\n          return 26;\n        },\n        getWidth: () => {\n          return 26;\n        },\n        getVGap: () => {\n          return 20;\n        },\n        getHGap: () => {\n          return 40;\n        },\n      },\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## layout / concentric\n\n### Concentric Layout\n\n**文件路径**: `layout/concentric/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/basement_prod/8dacf27e-e1bc-4522-b6d3-4b6d9b9ed7df.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      edge: {\n        type: 'line',\n      },\n      layout: {\n        type: 'concentric',\n        nodeSize: 32,\n        maxLevelDiff: 0.5,\n        sortBy: 'degree',\n        preventOverlap: true,\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n      animation: false,\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## layout / custom\n\n### Bi-graph Layout\n\n**文件路径**: `layout/custom/demo/bi-graph.js`\n\n```js\nimport { BaseLayout, ExtensionCategory, Graph, register } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0', data: { cluster: 'A' } },\n    { id: '1', data: { cluster: 'A' } },\n    { id: '2', data: { cluster: 'A' } },\n    { id: '3', data: { cluster: 'A' } },\n    { id: '4', data: { cluster: 'A' } },\n    { id: '5', data: { cluster: 'A' } },\n    { id: '6', data: { cluster: 'B' } },\n    { id: '7', data: { cluster: 'B' } },\n    { id: '8', data: { cluster: 'B' } },\n    { id: '9', data: { cluster: 'B' } },\n  ],\n  edges: [\n    { source: '0', target: '6' },\n    { source: '0', target: '7' },\n    { source: '0', target: '9' },\n    { source: '1', target: '6' },\n    { source: '1', target: '9' },\n    { source: '1', target: '7' },\n    { source: '2', target: '8' },\n    { source: '2', target: '9' },\n    { source: '2', target: '6' },\n    { source: '3', target: '8' },\n    { source: '4', target: '6' },\n    { source: '4', target: '7' },\n    { source: '5', target: '9' },\n  ],\n};\n\nclass BiLayout extends BaseLayout {\n  id = 'bi-layout';\n\n  async execute(data, options) {\n    const { sep = 100, nodeSep = 20, nodeSize = 32 } = { ...this.options, ...options };\n\n    const [A, B] = data.nodes.reduce(\n      (acc, curr) => {\n        acc[curr.data.cluster === 'A' ? 0 : 1].push(curr);\n        return acc;\n      },\n      [[], []],\n    );\n\n    return {\n      nodes: [\n        ...A.map((node, i) => ({\n          id: node.id,\n          style: {\n            x: i * (nodeSep + nodeSize),\n            y: 0,\n          },\n        })),\n        ...B.map((node, i) => ({\n          id: node.id,\n          style: {\n            x: i * (nodeSep + nodeSize),\n            y: sep,\n          },\n        })),\n      ],\n    };\n  }\n}\n\nregister(ExtensionCategory.LAYOUT, 'bi', BiLayout);\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  autoFit: 'center',\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n    palette: {\n      type: 'group',\n      field: 'cluster',\n      color: ['#1783FF', '#D580FF'],\n    },\n  },\n  layout: {\n    type: 'bi',\n    sep: 300,\n    nodeSep: 20,\n    nodeSize: 32,\n    preLayout: true,\n  },\n  behaviors: ['drag-canvas', 'drag-element', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n---\n\n### Arc Diagram\n\n**文件路径**: `layout/custom/demo/arc.js`\n\n```js\nimport { BaseEdge, BaseLayout, ExtensionCategory, Graph, register } from '@antv/g6';\n\nclass ArcLayout extends BaseLayout {\n  async execute(data, options) {\n    const { nodeSep = 20, nodeSize } = { ...this.options, ...options };\n    const { nodes } = data;\n    return {\n      nodes: nodes.map((node, index) => ({\n        id: node.id,\n        style: {\n          x: index * (nodeSep + nodeSize),\n          y: 0,\n        },\n      })),\n    };\n  }\n}\n\nclass ArcEdge extends BaseEdge {\n  getKeyPath(attributes) {\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes);\n    const [sx, sy] = sourcePoint;\n    const [tx] = targetPoint;\n    const r = Math.abs(tx - sx) / 2;\n\n    return [\n      ['M', sx, sy],\n      ['A', r, r, 0, 0, sx < tx ? 1 : 0, tx, sy],\n    ];\n  }\n}\n\nregister(ExtensionCategory.LAYOUT, 'arc', ArcLayout);\nregister(ExtensionCategory.EDGE, 'arc', ArcEdge);\n\nconst palette = {\n  analytics: 'rgb(158, 1, 66)',\n  data: 'rgb(213, 62, 79)',\n  animate: 'rgb(244, 109, 67)',\n  display: 'rgb(253, 174, 97)',\n  flex: 'rgb(254, 224, 139)',\n  physics: 'rgb(230, 245, 152)',\n  query: 'rgb(171, 221, 164)',\n  scale: 'rgb(102, 194, 165)',\n  util: 'rgb(50, 136, 189)',\n  vis: 'rgb(94, 79, 162)',\n};\n\nfetch('https://gw.alipayobjects.com/os/basement_prod/70cde3be-22e8-4291-98f1-4d5a5b75b62f.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const getCluster = (id) => data.nodes.find((node) => node.id === id).cluster;\n\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      node: {\n        style: {\n          size: 20,\n          fill: (d) => palette[d.cluster],\n          ports: [{ position: 'top' }],\n          labelText: (d) => d.name,\n          labelTextAlign: 'start',\n          labelTextBaseline: 'middle',\n          labelTransform: [['rotate', 90]],\n        },\n      },\n      edge: {\n        type: 'arc',\n        style: {\n          stroke: (d) => `linear-gradient(${palette[getCluster(d.source)]}, ${palette[getCluster(d.source)]})`,\n          strokeOpacity: 0.5,\n        },\n      },\n      layout: {\n        type: 'arc',\n        nodeSize: 20,\n        preLayout: true,\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## layout / dagre\n\n### Dagre Layout\n\n**文件路径**: `layout/dagre/demo/antv-dagre.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0' },\n    { id: '1' },\n    { id: '2' },\n    { id: '3' },\n    { id: '4' },\n    { id: '5' },\n    { id: '6' },\n    { id: '7' },\n    { id: '8' },\n    { id: '9' },\n  ],\n  edges: [\n    { source: '0', target: '1' },\n    { source: '0', target: '2' },\n    { source: '1', target: '4' },\n    { source: '0', target: '3' },\n    { source: '3', target: '4' },\n    { source: '4', target: '5' },\n    { source: '4', target: '6' },\n    { source: '5', target: '7' },\n    { source: '5', target: '8' },\n    { source: '8', target: '9' },\n    { source: '2', target: '9' },\n    { source: '3', target: '9' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'view',\n  data,\n  layout: {\n    type: 'antv-dagre',\n    nodeSize: [60, 30],\n    nodesep: 60,\n    ranksep: 40,\n    controlPoints: true,\n  },\n  node: {\n    type: 'rect',\n    style: {\n      size: [60, 30],\n      radius: 8,\n      labelText: (d) => d.id,\n      labelBackground: true,\n    },\n  },\n  edge: {\n    type: 'polyline',\n  },\n  behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = { layout: 'default' };\n  const layouts = {\n    default: { type: 'antv-dagre', nodesep: 100, ranksep: 70, controlPoints: true },\n    LR: { type: 'antv-dagre', rankdir: 'LR', align: 'DL', nodesep: 50, ranksep: 70, controlPoints: true },\n    'LR&UL': { type: 'antv-dagre', rankdir: 'LR', align: 'UL', controlPoints: true, nodesep: 50, ranksep: 70 },\n  };\n\n  gui.add(config, 'layout', Object.keys(layouts)).onChange(async (layout) => {\n    graph.setLayout(layouts[layout]);\n    await graph.layout();\n    graph.fitCenter();\n  });\n});\n```\n\n---\n\n### Dagre with Combos\n\n**文件路径**: `layout/dagre/demo/antv-dagre-combo.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/dagre-combo.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'center',\n      data,\n      node: {\n        type: 'rect',\n        style: {\n          size: [60, 30],\n          radius: 8,\n          labelText: (d) => d.id,\n          labelBackground: true,\n          ports: [{ placement: 'top' }, { placement: 'bottom' }],\n        },\n        palette: {\n          field: (d) => d.combo,\n        },\n      },\n      edge: {\n        type: 'cubic-vertical',\n        style: {\n          endArrow: true,\n        },\n      },\n      combo: {\n        type: 'rect',\n        style: {\n          radius: 8,\n          labelText: (d) => d.id,\n        },\n      },\n      layout: {\n        type: 'antv-dagre',\n        ranksep: 50,\n        nodesep: 5,\n        sortByCombo: true,\n      },\n      behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Dagre.js Layout\n\n**文件路径**: `layout/dagre/demo/dagre.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'kspacey', data: { label: 'Kevin Spacey', width: 144, height: 100 } },\n    { id: 'swilliams', data: { label: 'Saul Williams', width: 160, height: 100 } },\n    { id: 'bpitt', data: { label: 'Brad Pitt', width: 108, height: 100 } },\n    { id: 'hford', data: { label: 'Harrison Ford', width: 168, height: 100 } },\n    { id: 'lwilson', data: { label: 'Luke Wilson', width: 144, height: 100 } },\n    { id: 'kbacon', data: { label: 'Kevin Bacon', width: 121, height: 100 } },\n  ],\n  edges: [\n    { id: 'kspacey->swilliams', source: 'kspacey', target: 'swilliams' },\n    { id: 'swilliams->kbacon', source: 'swilliams', target: 'kbacon' },\n    { id: 'bpitt->kbacon', source: 'bpitt', target: 'kbacon' },\n    { id: 'hford->lwilson', source: 'hford', target: 'lwilson' },\n    { id: 'lwilson->kbacon', source: 'lwilson', target: 'kbacon' },\n  ],\n};\n\nconst graph = new Graph({\n  autoFit: 'center',\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      size: (d) => [d.data.width, d.data.height],\n      radius: 10,\n      iconText: (d) => d.data.label,\n      iconFontSize: 14,\n    },\n    palette: {\n      type: 'group',\n      field: 'label',\n    },\n  },\n  edge: {\n    type: 'polyline',\n    style: {\n      router: {\n        type: 'orth',\n      },\n    },\n  },\n  layout: {\n    type: 'dagre',\n  },\n});\n\ngraph.render();\n```\n\n---\n\n## layout / dendrogram\n\n### Dendrogram Layout\n\n**文件路径**: `layout/dendrogram/demo/basic.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\n/**\n * If the node is a leaf node\n * @param {*} d - node data\n * @returns {boolean} - whether the node is a leaf node\n */\nfunction isLeafNode(d) {\n  return !d.children || d.children.length === 0;\n}\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: (d) => (isLeafNode(d) ? 'right' : 'left'),\n          labelBackground: true,\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'dendrogram',\n        direction: 'LR', // H / V / LR / RL / TB / BT\n        nodeSep: 36,\n        rankSep: 250,\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Vertical Layout\n\n**文件路径**: `layout/dendrogram/demo/vertical.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\n/**\n * If the node is a leaf node\n * @param {*} d - node data\n * @returns {boolean} - whether the node is a leaf node\n */\nfunction isLeafNode(d) {\n  return !d.children || d.children.length === 0;\n}\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n      node: {\n        style: (d) => {\n          const style = {\n            labelText: d.id,\n            labelPlacement: 'right',\n            labelOffsetX: 2,\n            labelBackground: true,\n            ports: [{ placement: 'top' }, { placement: 'bottom' }],\n          };\n          if (isLeafNode(d)) {\n            Object.assign(style, {\n              labelTransform: [\n                ['rotate', 90],\n                ['translate', 18],\n              ],\n              labelBaseline: 'center',\n              labelTextAlign: 'left',\n            });\n          }\n          return style;\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-vertical',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'dendrogram',\n        direction: 'TB', // H / V / LR / RL / TB / BT\n        nodeSep: 50,\n        rankSep: 120,\n      },\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Radial Layout\n\n**文件路径**: `layout/dendrogram/demo/radial.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelBackground: true,\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'dendrogram',\n        radial: true,\n        nodeSep: 40,\n        rankSep: 140,\n      },\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## layout / fishbone\n\n### Fishbone Layout\n\n**文件路径**: `layout/fishbone/demo/basic.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nconst data = {\n  id: 'Quality',\n  children: [\n    {\n      id: 'Machine',\n      children: [{ id: 'Mill' }, { id: 'Mixer' }, { id: 'Metal Lathe', children: [{ id: 'Milling' }] }],\n    },\n    { id: 'Method' },\n    {\n      id: 'Material',\n      children: [\n        {\n          id: 'Masonite',\n          children: [\n            { id: 'spearMint' },\n            { id: 'pepperMint', children: [{ id: 'test3' }] },\n            { id: 'test1', children: [{ id: 'test4' }] },\n          ],\n        },\n        {\n          id: 'Marscapone',\n          children: [{ id: 'Malty' }, { id: 'Minty' }],\n        },\n        { id: 'Meat', children: [{ id: 'Mutton' }] },\n      ],\n    },\n    {\n      id: 'Man Power',\n      children: [\n        { id: 'Manager' },\n        { id: \"Master's Student\" },\n        { id: 'Magician' },\n        { id: 'Miner' },\n        { id: 'Magister', children: [{ id: 'Malpractice' }] },\n        {\n          id: 'Massage Artist',\n          children: [{ id: 'Masseur' }, { id: 'Masseuse' }],\n        },\n      ],\n    },\n    {\n      id: 'Measurement',\n      children: [{ id: 'Malleability' }],\n    },\n    {\n      id: 'Milieu',\n      children: [{ id: 'Marine' }],\n    },\n  ],\n};\n\nexport const layoutFishbone = async (context) => {\n  const graph = new Graph({\n    ...context,\n    container: 'container',\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    node: {\n      type: 'rect',\n      style: {\n        size: [32, 32],\n        // fill: () => randomColor(),\n        label: false,\n        labelFill: '#262626',\n        labelFontFamily: 'Gill Sans',\n        labelMaxLines: 2,\n        labelMaxWidth: '100%',\n        labelPlacement: 'center',\n        labelText: (d) => d.id,\n        labelWordWrap: true,\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        lineWidth: 3,\n      },\n    },\n    layout: {\n      type: 'fishbone',\n      vGap: 48,\n      hGap: 48,\n      direction: 'RL',\n    },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n    animation: false,\n  });\n\n  await graph.render();\n\n  layoutFishbone.form = (panel) => {\n    const config = {\n      type: 'fishbone',\n      direction: 'RL',\n    };\n\n    return [\n      panel\n        .add(config, 'direction', ['LR', 'RL'])\n        .name('Direction')\n        .onChange((value) => {\n          graph.setLayout((prev) => ({ ...prev, direction: value }));\n          graph.render();\n        }),\n    ];\n  };\n\n  return graph;\n};\n\nlayoutFishbone();\n```\n\n---\n\n## layout / force-directed\n\n### Clustering Force Layout\n\n**文件路径**: `layout/force-directed/demo/force.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          ports: [],\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      layout: {\n        type: 'force',\n        linkDistance: 50,\n        clustering: true,\n        nodeClusterBy: 'cluster',\n        clusterNodeStrength: 70,\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Force-Atlas 2 Layout\n\n**文件路径**: `layout/force-directed/demo/atlas2.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      layout: {\n        type: 'force-atlas2',\n        preventOverlap: true,\n        kr: 20,\n        center: [250, 250],\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n      autoResize: true,\n      zoomRange: [0.1, 5],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### D3 Force Layout\n\n**文件路径**: `layout/force-directed/demo/d3-force.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          ports: [],\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      layout: {\n        type: 'd3-force',\n        collide: {\n          strength: 0.5,\n        },\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Customize Layout Parameters For Different Nodes\n\n**文件路径**: `layout/force-directed/demo/functional-params.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node0', size: 50 },\n    { id: 'node1', size: 30 },\n    { id: 'node2', size: 30 },\n    { id: 'node3', size: 30 },\n    { id: 'node4', size: 30, isLeaf: true },\n    { id: 'node5', size: 30, isLeaf: true },\n    { id: 'node6', size: 15, isLeaf: true },\n    { id: 'node7', size: 15, isLeaf: true },\n    { id: 'node8', size: 15, isLeaf: true },\n    { id: 'node9', size: 15, isLeaf: true },\n    { id: 'node10', size: 15, isLeaf: true },\n    { id: 'node11', size: 15, isLeaf: true },\n    { id: 'node12', size: 15, isLeaf: true },\n    { id: 'node13', size: 15, isLeaf: true },\n    { id: 'node14', size: 15, isLeaf: true },\n    { id: 'node15', size: 15, isLeaf: true },\n    { id: 'node16', size: 15, isLeaf: true },\n  ],\n  edges: [\n    { source: 'node0', target: 'node1' },\n    { source: 'node0', target: 'node2' },\n    { source: 'node0', target: 'node3' },\n    { source: 'node0', target: 'node4' },\n    { source: 'node0', target: 'node5' },\n    { source: 'node1', target: 'node6' },\n    { source: 'node1', target: 'node7' },\n    { source: 'node2', target: 'node8' },\n    { source: 'node2', target: 'node9' },\n    { source: 'node2', target: 'node10' },\n    { source: 'node2', target: 'node11' },\n    { source: 'node2', target: 'node12' },\n    { source: 'node2', target: 'node13' },\n    { source: 'node3', target: 'node14' },\n    { source: 'node3', target: 'node15' },\n    { source: 'node3', target: 'node16' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      size: (d) => d.size,\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    link: {\n      distance: (d) => {\n        if (d.source.id === 'node0') {\n          return 100;\n        }\n        return 30;\n      },\n      strength: (d) => {\n        if (d.source.id === 'node1' || d.source.id === 'node2' || d.source.id === 'node3') {\n          return 0.7;\n        }\n        return 0.1;\n      },\n    },\n    manyBody: {\n      strength: (d) => {\n        if (d.isLeaf) {\n          return -50;\n        }\n        return -10;\n      },\n    },\n  },\n  behaviors: ['drag-element-force'],\n});\n\ngraph.render();\n```\n\n---\n\n### Prevent Overlap in d3-force Layout\n\n**文件路径**: `layout/force-directed/demo/prevent-overlap.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const nodes = data.nodes;\n    // randomize the node size\n    nodes.forEach((node) => {\n      node.size = Math.random() * 30 + 5;\n    });\n\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'center',\n      data,\n      node: {\n        style: {\n          size: (d) => d.size,\n          lineWidth: 1,\n        },\n      },\n      layout: {\n        type: 'd3-force',\n        collide: {\n          // Prevent nodes from overlapping by specifying a collision radius for each node.\n          radius: (d) => d.size / 2,\n        },\n      },\n      behaviors: ['drag-element-force'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Drag Fixed Nodes\n\n**文件路径**: `layout/force-directed/demo/drag-fixed.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(10).fill(0).map((_, i) => ({ id: `${i}`, label: `${i}` })),\n  edges: [\n    { source: '0', target: '1' },\n    { source: '0', target: '2' },\n    { source: '0', target: '3' },\n    { source: '0', target: '4' },\n    { source: '0', target: '5' },\n    { source: '0', target: '7' },\n    { source: '0', target: '8' },\n    { source: '0', target: '9' },\n    { source: '2', target: '3' },\n    { source: '4', target: '5' },\n    { source: '4', target: '6' },\n    { source: '5', target: '6' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.label,\n      labelPlacement: 'middle',\n      labelFill: '#fff',\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    link: {\n      distance: 100,\n      strength: 2,\n    },\n    collide: {\n      radius: 40,\n    },\n  },\n  behaviors: [\n    {\n      type: 'drag-element-force',\n      fixed: true,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n### Force Directed Bubble Chart\n\n**文件路径**: `layout/force-directed/demo/bubbles.js`\n\n```js\nimport { Graph, NodeEvent } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: '0',\n      label: '0',\n      value: 10,\n      cluster: 'a',\n      description: 'this is node 0, \\nand the value of it is 10',\n    },\n    {\n      id: '1',\n      label: '1',\n      value: 20,\n      cluster: 'b',\n      description: 'this is node 1, \\nand the value of it is 20',\n    },\n    {\n      id: '2',\n      label: '2',\n      value: 5,\n      cluster: 'a',\n      description: 'this is node 2, \\nand the value of it is 5',\n    },\n    {\n      id: '3',\n      label: '3',\n      value: 10,\n      cluster: 'a',\n      description: 'this is node 3, \\nand the value of it is 10',\n    },\n    {\n      id: '4',\n      label: '4',\n      value: 12,\n      cluster: 'c',\n      subCluster: 'sb',\n      description: 'this is node 4, \\nand the value of it is 12',\n    },\n    {\n      id: '5',\n      label: '5',\n      value: 18,\n      cluster: 'c',\n      subCluster: 'sa',\n      description: 'this is node 5, \\nand the value of it is 18',\n    },\n    {\n      id: '6',\n      label: '6',\n      value: 3,\n      cluster: 'c',\n      subCluster: 'sa',\n      description: 'this is node 6, \\nand the value of it is 3',\n    },\n    {\n      id: '7',\n      label: '7',\n      value: 7,\n      cluster: 'b',\n      subCluster: 'sa',\n      description: 'this is node 7, \\nand the value of it is 7',\n    },\n    {\n      id: '8',\n      label: '8',\n      value: 21,\n      cluster: 'd',\n      subCluster: 'sb',\n      description: 'this is node 8, \\nand the value of it is 21',\n    },\n    {\n      id: '9',\n      label: '9',\n      value: 9,\n      cluster: 'd',\n      subCluster: 'sb',\n      description: 'this is node 9, \\nand the value of it is 9',\n    },\n  ],\n  edges: [],\n};\n\nconst oriSize = {};\n\nconst nodes = data.nodes;\n// randomize the node size\nnodes.forEach((node) => {\n  node.size = Math.random() * 30 + 16;\n  oriSize[node.id] = node.size;\n});\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      size: (d) => d.size,\n      labelText: (d) => (d.size === 200 ? d.description : d.id),\n      labelPlacement: 'middle',\n      labelFill: '#fff',\n    },\n    palette: {\n      field: (d) => d.cluster,\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    collide: {\n      radius: (d) => d.size / 2,\n      strength: 0.7,\n    },\n    manyBody: {\n      strength: 30,\n    },\n  },\n  behaviors: ['drag-element-force'],\n});\n\ngraph.on(NodeEvent.CLICK, async (e) => {\n  const nodeId = e.target.id;\n  const data = graph.getNodeData(nodeId);\n  const size = data.size === oriSize[nodeId] ? 200 : oriSize[nodeId];\n  graph.updateNodeData([{ id: nodeId, size }]);\n  await graph.layout();\n});\n\ngraph.render();\n```\n\n---\n\n### 3D Force Layout\n\n**文件路径**: `layout/force-directed/demo/3d-force.js`\n\n```js\nimport { CameraSetting, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { D3Force3DLayout, Light, Line3D, ObserveCanvas3D, Sphere, ZoomCanvas3D, renderer } from '@antv/g6-extension-3d';\n\nregister(ExtensionCategory.PLUGIN, '3d-light', Light);\nregister(ExtensionCategory.NODE, 'sphere', Sphere);\nregister(ExtensionCategory.EDGE, 'line3d', Line3D);\nregister(ExtensionCategory.LAYOUT, 'd3-force-3d', D3Force3DLayout);\nregister(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting);\nregister(ExtensionCategory.BEHAVIOR, 'zoom-canvas-3d', ZoomCanvas3D);\nregister(ExtensionCategory.BEHAVIOR, 'observe-canvas-3d', ObserveCanvas3D);\n\nfetch('https://assets.antv.antgroup.com/g6/d3-force-3d.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      renderer,\n      data,\n      layout: {\n        type: 'd3-force-3d',\n      },\n      node: {\n        type: 'sphere',\n        style: {\n          materialType: 'phong',\n        },\n        palette: {\n          color: 'tableau',\n          type: 'group',\n          field: 'group',\n        },\n      },\n      edge: {\n        type: 'line3d',\n      },\n      behaviors: ['observe-canvas-3d', 'zoom-canvas-3d'],\n      plugins: [\n        {\n          type: '3d-light',\n          directional: {\n            direction: [0, 0, 1],\n          },\n        },\n      ],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Mesh Layout\n\n**文件路径**: `layout/force-directed/demo/mesh.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfunction getData(size = 10) {\n  const nodes = Array.from({ length: size * size }, (_, i) => ({ id: `${i}` }));\n  const edges = [];\n  for (let y = 0; y < size; ++y) {\n    for (let x = 0; x < size; ++x) {\n      if (y > 0) edges.push({ source: `${(y - 1) * size + x}`, target: `${y * size + x}` });\n      if (x > 0) edges.push({ source: `${y * size + (x - 1)}`, target: `${y * size + x}` });\n    }\n  }\n  return { nodes, edges };\n}\n\nconst graph = new Graph({\n  data: getData(),\n  layout: {\n    type: 'd3-force',\n    manyBody: {\n      strength: -30,\n    },\n    link: {\n      strength: 1,\n      distance: 20,\n      iterations: 10,\n    },\n  },\n  node: {\n    style: {\n      size: 10,\n      fill: '#000',\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#000',\n    },\n  },\n  behaviors: [{ type: 'drag-element-force' }, 'zoom-canvas'],\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  gui.add({ msg: 'Try to drag nodes' }, 'msg').name('Tips').disable();\n});\n```\n\n---\n\n### Collision Layout\n\n**文件路径**: `layout/force-directed/demo/collision.js`\n\n```js\nimport { BaseBehavior, ExtensionCategory, Graph, invokeLayoutMethod, register } from '@antv/g6';\n\nfunction getData(width, size = 200) {\n  const k = width / 200;\n  const r = randomUniform(k * 2, k * 8);\n  const n = 4;\n  return {\n    nodes: Array.from({ length: size }, (_, i) => ({ id: `${i}`, data: { r: r(), group: i && (i % n) + 1 } })),\n    edges: [],\n  };\n}\n\nfunction randomUniform(min, max) {\n  min = min == null ? 0 : +min;\n  max = max == null ? 1 : +max;\n  if (arguments.length === 1) ((max = min), (min = 0));\n  else max -= min;\n  return function () {\n    return Math.random() * max + min;\n  };\n}\n\nclass CollisionElement extends BaseBehavior {\n  constructor(context) {\n    super(context, {});\n    this.onPointerMove = this.onPointerMove.bind(this);\n    this.bindEvents();\n  }\n\n  bindEvents() {\n    this.context.graph.on('pointermove', this.onPointerMove);\n  }\n\n  onPointerMove(event) {\n    const pos = this.context.graph.getCanvasByClient([event.client.x, event.client.y]);\n    const layoutInstance = this.context.layout\n      ?.getLayoutInstance()\n      .find((layout) => ['d3-force', 'd3-force-3d'].includes(layout?.id));\n\n    if (layoutInstance) {\n      invokeLayoutMethod(layoutInstance, 'setFixedPosition', '0', [...pos]);\n    }\n  }\n}\n\nregister(ExtensionCategory.BEHAVIOR, 'collision-element', CollisionElement);\n\nconst container = document.getElementById('container');\nconst width = container.scrollWidth;\n\nconst graph = new Graph({\n  container,\n  data: getData(width),\n  layout: {\n    type: 'd3-force',\n    alphaTarget: 0.3,\n    velocityDecay: 0.1,\n    x: {\n      strength: 0.01,\n    },\n    y: {\n      strength: 0.01,\n    },\n    collide: {\n      radius: (d) => d.data.r,\n      iterations: 3,\n    },\n    manyBody: {\n      strength: (d, i) => (i ? 0 : (-width * 2) / 3),\n    },\n    link: false,\n  },\n  node: {\n    style: {\n      size: (d) => (d.id === '0' ? 0 : d.data.r * 2),\n    },\n    palette: {\n      color: 'tableau',\n      type: 'group',\n      field: (d) => d.data.group,\n    },\n  },\n  behaviors: ['collision-element'],\n});\n\ngraph.render();\n```\n\n---\n\n## layout / fruchterman\n\n### Basic Fruchterman Layout\n\n**文件路径**: `layout/fruchterman/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'fruchterman',\n        gravity: 5,\n        speed: 5,\n        animation: true,\n      },\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelPlacement: 'center',\n          labelText: (d) => d.id,\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Fruchterman Cluster Layout\n\n**文件路径**: `layout/fruchterman/demo/cluster.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'fruchterman',\n        gravity: 5,\n        speed: 5,\n        clustering: true,\n        nodeClusterBy: 'cluster',\n        clusterGravity: 16,\n      },\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelPlacement: 'center',\n          labelText: (d) => d.id,\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      edge: {\n        style: {\n          endArrow: true,\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Run in Web-worker\n\n**文件路径**: `layout/fruchterman/demo/run-in-web-worker.js`\n\n```js\nimport { Graph, GraphEvent } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'fruchterman',\n        speed: 20,\n        gravity: 10,\n        maxIteration: 10000,\n        workerEnabled: true,\n      },\n      node: {\n        style: {\n          size: 20,\n          labelText: (d) => d.id,\n          labelPlacement: 'center',\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      edge: {\n        style: {\n          stroke: '#ddd',\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      const msg = gui.add({ msg: 'Running...' }, 'msg').name('Tips').disable();\n      graph.on(GraphEvent.AFTER_LAYOUT, () => {\n        msg.setValue('Layout Done!');\n      });\n    });\n  });\n```\n\n---\n\n### Run in GPU\n\n**文件路径**: `layout/fruchterman/demo/run-in-gpu.js`\n\n```js\nimport { Graph, register } from '@antv/g6';\nimport { FruchtermanLayout } from '@antv/layout-gpu';\n\nregister('layout', 'fruchterman-gpu', FruchtermanLayout);\n\nfetch('https://gw.alipayobjects.com/os/basement_prod/7bacd7d1-4119-4ac1-8be3-4c4b9bcbc25f.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      node: {\n        style: {\n          size: 5,\n        },\n      },\n      edge: {\n        style: {\n          startArrow: true,\n        },\n      },\n      layout: {\n        type: 'fruchterman-gpu',\n        speed: 20,\n        gravity: 1,\n        maxIteration: 10000,\n        workerEnabled: true,\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## layout / grid\n\n### Grid Layout\n\n**文件路径**: `layout/grid/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelBackground: true,\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      layout: {\n        type: 'grid',\n        sortBy: 'id',\n        nodeSize: 32,\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      gui.add({ sortBy: 'id' }, 'sortBy', ['id', 'cluster']).onChange((type) => {\n        graph.setLayout({\n          type: 'grid',\n          sortBy: type,\n        });\n        graph.layout();\n      });\n    });\n  });\n```\n\n---\n\n## layout / indented\n\n### Two Side Indented Layout\n\n**文件路径**: `layout/indented/demo/auto-side.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nconst getNodeSide = (graph, datum) => {\n  const parentData = graph.getParentData(datum.id, 'tree');\n  if (!parentData) return 'center';\n  return datum.style.x > parentData.style.x ? 'right' : 'left';\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data: treeToGraphData(data),\n      autoFit: 'view',\n      node: {\n        style: function (d) {\n          const side = getNodeSide(this, d);\n          return {\n            labelText: d.id,\n            labelPlacement: side === 'center' ? 'bottom' : side,\n            labelBackground: true,\n            ports:\n              side === 'center'\n                ? [{ placement: 'bottom' }]\n                : side === 'right'\n                  ? [{ placement: 'bottom' }, { placement: 'left' }]\n                  : [{ placement: 'bottom' }, { placement: 'right' }],\n          };\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'polyline',\n        style: {\n          radius: 4,\n          router: {\n            type: 'orth',\n          },\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'H',\n        indent: 80,\n        preLayout: false,\n        getHeight: () => 16,\n        getWidth: () => 32,\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Right Side Indented Layout\n\n**文件路径**: `layout/indented/demo/right-side.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data: treeToGraphData(data),\n      autoFit: 'view',\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: 'right',\n          labelBackground: true,\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'polyline',\n        style: {\n          radius: 4,\n          router: {\n            type: 'orth',\n          },\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'LR',\n        indent: 80,\n        getHeight: () => 16,\n        getWidth: () => 32,\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Left Side Indented Layout\n\n**文件路径**: `layout/indented/demo/left-side.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data: treeToGraphData(data),\n      autoFit: 'view',\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: 'left',\n          labelBackground: true,\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'polyline',\n        style: {\n          radius: 4,\n          router: {\n            type: 'orth',\n          },\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'RL',\n        indent: 80,\n        getHeight: () => 16,\n        getWidth: () => 32,\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Custom Side Indented Layout\n\n**文件路径**: `layout/indented/demo/custom-side.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nconst getNodeSide = (graph, datum) => {\n  const parentData = graph.getParentData(datum.id, 'tree');\n  if (!parentData) return 'center';\n  return datum.style.x > parentData.style.x ? 'right' : 'left';\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data: treeToGraphData(data),\n      autoFit: 'view',\n      node: {\n        style: function (d) {\n          const side = getNodeSide(this, d);\n          return {\n            labelText: d.id,\n            labelPlacement: side === 'center' ? 'bottom' : side,\n            labelBackground: true,\n            ports:\n              side === 'center'\n                ? [{ placement: 'bottom' }]\n                : side === 'right'\n                  ? [{ placement: 'bottom' }, { placement: 'left' }]\n                  : [{ placement: 'bottom' }, { placement: 'right' }],\n          };\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'polyline',\n        style: {\n          radius: 4,\n          router: {\n            type: 'orth',\n          },\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'H',\n        indent: 80,\n        preLayout: false,\n        getHeight: () => 16,\n        getWidth: () => 32,\n        getSide: (d) => {\n          if (d.id === 'Regression' || d.id === 'Classification') return 'left';\n          return 'right';\n        },\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### No Drop Cap Indented Layout\n\n**文件路径**: `layout/indented/demo/no-drop-cap.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data: treeToGraphData(data),\n      autoFit: 'view',\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: 'right',\n          labelBackground: true,\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'polyline',\n        style: {\n          radius: 4,\n          router: {\n            type: 'orth',\n          },\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'LR',\n        indent: 80,\n        getHeight: () => 16,\n        getWidth: () => 32,\n        dropCap: false,\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## layout / mds\n\n### Basic MDS Layout\n\n**文件路径**: `layout/mds/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      padding: 20,\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelText: (d) => d.id,\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'mds',\n        nodeSize: 32,\n        linkDistance: 100,\n      },\n      behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## layout / mechanism\n\n### Switch Layout\n\n**文件路径**: `layout/mechanism/demo/switch.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      layout: {\n        type: 'circular',\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-node'],\n      data,\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      gui\n        .add({ layout: 'circular' }, 'layout', ['circular', 'grid', 'force', 'radial', 'concentric', 'mds'])\n        .onChange((layout) => {\n          const options = {\n            circular: { type: 'circular' },\n            grid: { type: 'grid' },\n            force: { type: 'force', preventOverlap: true },\n            radial: { type: 'radial', preventOverlap: true },\n            concentric: { type: 'concentric' },\n            mds: { type: 'mds', linkDistance: 100 },\n          };\n          graph.stopLayout();\n          graph.layout(options[layout]);\n        });\n    });\n  });\n```\n\n---\n\n### Change Data\n\n**文件路径**: `layout/mechanism/demo/change-data.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst fetchData = async (type) => {\n  if (type === 'large') {\n    const data = await fetch('https://assets.antv.antgroup.com/g6/cluster.json').then((res) => res.json());\n    return data;\n  }\n  return {\n    nodes: [{ id: 'b0' }, { id: 'b1' }, { id: 'b2' }, { id: 'b3' }, { id: 'b4' }, { id: 'b5' }],\n    edges: [\n      { source: 'b0', target: 'b1' },\n      { source: 'b0', target: 'b2' },\n      { source: 'b0', target: 'b3' },\n      { source: 'b0', target: 'b4' },\n      { source: 'b0', target: 'b5' },\n    ],\n  };\n};\n\nfetchData('small').then((data) => {\n  const graph = new Graph({\n    container: 'container',\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n    layout: {\n      type: 'force',\n      animated: true,\n      linkDistance: 100,\n      preventOverlap: true,\n    },\n    data,\n  });\n\n  graph.render();\n\n  window.addPanel((gui) => {\n    gui.add({ type: 'small' }, 'type', ['small', 'large']).onChange((type) => {\n      fetchData(type).then((data) => {\n        graph.setData(data);\n        graph.render();\n      });\n    });\n  });\n});\n```\n\n---\n\n## layout / mindmap\n\n### Auto Mindmap Layout\n\n**文件路径**: `layout/mindmap/demo/auto-side.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nconst getNodeSide = (graph, datum) => {\n  const parentData = graph.getParentData(datum.id, 'tree');\n  if (!parentData) return 'center';\n  return datum.style.x > parentData.style.x ? 'right' : 'left';\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelBackground: true,\n          labelPlacement: function (d) {\n            const side = getNodeSide(this, d);\n            return side === 'center' ? 'right' : side;\n          },\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'mindmap',\n        direction: 'H',\n        getHeight: () => 32,\n        getWidth: () => 32,\n        getVGap: () => 4,\n        getHGap: () => 64,\n      },\n      behaviors: ['collapse-expand', 'drag-canvas', 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Right Side Mindmap Layout\n\n**文件路径**: `layout/mindmap/demo/right-side.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: 'right',\n          labelBackground: true,\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'mindmap',\n        direction: 'LR',\n        getHeight: () => 32,\n        getWidth: () => 32,\n        getVGap: () => 4,\n        getHGap: () => 100,\n      },\n      behaviors: ['collapse-expand', 'drag-canvas', 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Left Side Mindmap Layout\n\n**文件路径**: `layout/mindmap/demo/left-side.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: 'left',\n          labelBackground: true,\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'mindmap',\n        direction: 'RL',\n        getHeight: () => 32,\n        getWidth: () => 32,\n        getVGap: () => 4,\n        getHGap: () => 100,\n      },\n      behaviors: ['collapse-expand', 'drag-canvas', 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Custom Mindmap Layout\n\n**文件路径**: `layout/mindmap/demo/custom-side.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nconst getNodeSide = (graph, datum) => {\n  const parentData = graph.getParentData(datum.id, 'tree');\n  if (!parentData) return 'center';\n  return datum.style.x > parentData.style.x ? 'right' : 'left';\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelBackground: true,\n          labelPlacement: function (d) {\n            const side = getNodeSide(this, d);\n            return side === 'center' ? 'right' : side;\n          },\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'mindmap',\n        direction: 'H',\n        preLayout: false,\n        getHeight: () => 32,\n        getWidth: () => 32,\n        getVGap: () => 4,\n        getHGap: () => 64,\n        getSide: (d) => {\n          if (d.id === 'Classification') {\n            return 'left';\n          }\n          return 'right';\n        },\n      },\n      behaviors: ['collapse-expand', 'drag-canvas', 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## layout / radial\n\n### Basic Radial Layout\n\n**文件路径**: `layout/radial/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/radial.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'center',\n      layout: {\n        type: 'radial',\n        nodeSize: 32,\n        unitRadius: 100,\n        linkDistance: 200,\n      },\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelPlacement: 'center',\n          labelText: (d) => d.id,\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Strict Radial Layout with Overlap Prevention\n\n**文件路径**: `layout/radial/demo/strict-prevent-overlap.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/radial.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'center',\n      layout: {\n        type: 'radial',\n        nodeSize: 32,\n        unitRadius: 90,\n        linkDistance: 200,\n        preventOverlap: true,\n        maxPreventOverlapIteration: 100,\n      },\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelPlacement: 'center',\n          labelText: (d) => d.id,\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Non-strict Radial Layout with Overlap Prevention\n\n**文件路径**: `layout/radial/demo/non-strict-prevent-overlap.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/radial.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'center',\n      layout: {\n        type: 'radial',\n        nodeSize: 32,\n        unitRadius: 90,\n        linkDistance: 200,\n        preventOverlap: true,\n        maxPreventOverlapIteration: 100,\n        strictRadial: false,\n      },\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelPlacement: 'center',\n          labelText: (d) => d.id,\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Cluster Sort\n\n**文件路径**: `layout/radial/demo/cluster-sort.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0', data: { type: 'a' } },\n    { id: '1', data: { type: 'a' } },\n    { id: '2', data: { type: 'a' } },\n    { id: '3', data: { type: 'a' } },\n    { id: '4', data: { type: 'c' } },\n    { id: '5', data: { type: 'a' } },\n    { id: '6', data: { type: 'b' } },\n    { id: '7', data: { type: 'b' } },\n    { id: '8', data: { type: 'c' } },\n    { id: '9', data: { type: 'd' } },\n    { id: '10', data: { type: 'd' } },\n    { id: '11', data: { type: 'b' } },\n    { id: '12', data: { type: 'c' } },\n    { id: '13', data: { type: 'b' } },\n    { id: '14', data: { type: 'd' } },\n    { id: '15', data: { type: 'd' } },\n    { id: '16', data: { type: 'b' } },\n    { id: '17', data: { type: 'c' } },\n    { id: '18', data: { type: 'c' } },\n    { id: '19', data: { type: 'b' } },\n    { id: '20', data: { type: 'b' } },\n    { id: '21', data: { type: 'd' } },\n    { id: '22', data: { type: 'd' } },\n    { id: '23', data: { type: 'd' } },\n    { id: '24', data: { type: 'a' } },\n    { id: '25', data: { type: 'a' } },\n    { id: '26', data: { type: 'b' } },\n    { id: '27', data: { type: 'b' } },\n    { id: '28', data: { type: 'd' } },\n    { id: '29', data: { type: 'c' } },\n    { id: '30', data: { type: 'c' } },\n    { id: '31', data: { type: 'b' } },\n    { id: '32', data: { type: 'b' } },\n    { id: '33', data: { type: 'a' } },\n  ],\n  edges: [\n    { source: '0', target: '1' },\n    { source: '0', target: '2' },\n    { source: '0', target: '3' },\n    { source: '0', target: '4' },\n    { source: '0', target: '5' },\n    { source: '0', target: '7' },\n    { source: '0', target: '8' },\n    { source: '0', target: '9' },\n    { source: '0', target: '10' },\n    { source: '0', target: '11' },\n    { source: '0', target: '13' },\n    { source: '0', target: '14' },\n    { source: '0', target: '15' },\n    { source: '0', target: '16' },\n    { source: '2', target: '3' },\n    { source: '4', target: '5' },\n    { source: '4', target: '6' },\n    { source: '5', target: '6' },\n    { source: '7', target: '13' },\n    { source: '8', target: '14' },\n    { source: '9', target: '10' },\n    { source: '10', target: '22' },\n    { source: '10', target: '14' },\n    { source: '10', target: '12' },\n    { source: '10', target: '24' },\n    { source: '10', target: '21' },\n    { source: '10', target: '20' },\n    { source: '11', target: '24' },\n    { source: '11', target: '22' },\n    { source: '11', target: '14' },\n    { source: '12', target: '13' },\n    { source: '16', target: '17' },\n    { source: '16', target: '18' },\n    { source: '16', target: '21' },\n    { source: '16', target: '22' },\n    { source: '17', target: '18' },\n    { source: '17', target: '20' },\n    { source: '18', target: '19' },\n    { source: '19', target: '20' },\n    { source: '19', target: '33' },\n    { source: '19', target: '22' },\n    { source: '19', target: '23' },\n    { source: '20', target: '21' },\n    { source: '21', target: '22' },\n    { source: '22', target: '24' },\n    { source: '22', target: '25' },\n    { source: '22', target: '26' },\n    { source: '22', target: '23' },\n    { source: '22', target: '28' },\n    { source: '22', target: '30' },\n    { source: '22', target: '31' },\n    { source: '22', target: '32' },\n    { source: '22', target: '33' },\n    { source: '23', target: '28' },\n    { source: '23', target: '27' },\n    { source: '23', target: '29' },\n    { source: '23', target: '30' },\n    { source: '23', target: '31' },\n    { source: '23', target: '33' },\n    { source: '32', target: '33' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'center',\n  data,\n  layout: {\n    type: 'radial',\n    nodeSize: 32,\n    unitRadius: 90,\n    linkDistance: 200,\n    preventOverlap: true,\n    sortBy: 'type',\n    sortStrength: 50,\n  },\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n    palette: {\n      type: 'group',\n      field: 'type',\n    },\n  },\n  edge: {\n    style: {\n      endArrow: true,\n    },\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n## layout / snake\n\n### Basic Snake Layout\n\n**文件路径**: `layout/snake/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(16).fill(0).map((_, i) => ({ id: `${i}` })),\n  edges: new Array(15).fill(0).map((_, i) => ({ source: `${i}`, target: `${i + 1}` })),\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n  },\n  layout: {\n    type: 'snake',\n    padding: 50,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n### Snake Layout with Gutter\n\n**文件路径**: `layout/snake/demo/gutter.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(16).fill(0).map((_, i) => ({ id: `${i}` })),\n  edges: new Array(15).fill(0).map((_, i) => ({ source: `${i}`, target: `${i + 1}` })),\n};\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'center',\n  data,\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n  },\n  layout: {\n    type: 'snake',\n    cols: 3,\n    rowGap: 80,\n    colGap: 120,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n## layout / sub-graph\n\n### SubGraph Layout\n\n**文件路径**: `layout/sub-graph/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfunction generateArray(groups = 10, itemsPerGroup = 6) {\n  const result = [];\n\n  for (let i = 1; i <= groups; i++) {\n    for (let j = 1; j <= itemsPerGroup; j++) {\n      const id = `${i}-${j}`;\n      result.push({\n        id,\n        labelText: id,\n      });\n    }\n  }\n\n  return result;\n}\n\nconst data = generateArray();\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: data,\n  },\n  animation: false,\n  autoFit: 'view',\n  autoResize: true,\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.labelText,\n    },\n  },\n  layout: Array.from({ length: 10 }, (_, i) => ({\n    type: 'circular',\n    nodeFilter: (node) => node.id.startsWith(`${i + 1}-`),\n    center: [\n      1000 + (i % 5) * 850, // x坐标\n      i < 5 ? 100 : 1100, // y坐标\n    ],\n  })),\n  behaviors: ['drag-canvas', 'drag-element', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n---\n\n## performance / massive-data\n\n### More than 5000 elements\n\n**文件路径**: `performance/massive-data/demo/5000.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/5000.json')\n  .then((res) => res.json())\n  .then(async (data) => {\n    const graph = new Graph({\n      container: 'container',\n      animation: false,\n      data,\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n      autoFit: 'view',\n    });\n\n    await graph.render();\n  });\n```\n\n---\n\n### About 20000 elements\n\n**文件路径**: `performance/massive-data/demo/20000.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/20000.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      animation: false,\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          size: 8,\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### More than 60000 elements\n\n**文件路径**: `performance/massive-data/demo/60000.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/60000.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      animation: false,\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          size: 4,\n          batchKey: 'node',\n        },\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## plugin / background\n\n### Background Image\n\n**文件路径**: `plugin/background/demo/background.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'background',\n      width: '800px',\n      height: '600px',\n      backgroundImage:\n        'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*0Qq0ToQm1rEAAAAAAAAAAAAADmJ7AQ/original)',\n      backgroundRepeat: 'no-repeat',\n      backgroundSize: 'cover',\n      opacity: 0.2,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n## plugin / bubble-sets\n\n### Use Bubblesets to wrap the node sets.\n\n**文件路径**: `plugin/bubble-sets/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/collection.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const groupedNodesByCluster = data.nodes.reduce((acc, node) => {\n      const cluster = node.data.cluster;\n      acc[cluster] ||= [];\n      acc[cluster].push(node.id);\n      return acc;\n    }, {});\n\n    const createStyle = (baseColor) => ({\n      fill: baseColor,\n      stroke: baseColor,\n      labelFill: '#fff',\n      labelPadding: 2,\n      labelBackgroundFill: baseColor,\n      labelBackgroundRadius: 5,\n    });\n\n    const graph = new Graph({\n      container: 'container',\n      data,\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n      node: {\n        palette: { field: 'cluster' },\n      },\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        linkDistance: (d) => {\n          if (d.source === 'node0' || d.target === 'node0') {\n            return 200;\n          }\n          return 80;\n        },\n      },\n      plugins: [\n        {\n          key: 'bubble-sets-a',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['a'],\n          labelText: 'cluster-a',\n          ...createStyle('#1783FF'),\n        },\n        {\n          key: 'bubble-sets-b',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['b'],\n          labelText: 'cluster-b',\n          ...createStyle('#00C9C9'),\n        },\n        {\n          key: 'bubble-sets-c',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['c'],\n          labelText: 'cluster-c',\n          ...createStyle('#F08F56'),\n        },\n        {\n          key: 'bubble-sets-d',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['d'],\n          labelText: 'cluster-d',\n          ...createStyle('#D580FF'),\n        },\n      ],\n      autoFit: 'center',\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## plugin / contextMenu\n\n### Context Menu\n\n**文件路径**: `plugin/contextMenu/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'contextmenu',\n      trigger: 'contextmenu', // 'click' or 'contextmenu'\n      onClick: (v) => {\n        alert('You have clicked the「' + v + '」item');\n      },\n      getItems: () => {\n        return [\n          { name: '展开一度关系', value: 'spread' },\n          { name: '查看详情', value: 'detail' },\n        ];\n      },\n      enable: (e) => e.targetType === 'node',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n## plugin / edge-bundling\n\n### Edge Bundling\n\n**文件路径**: `plugin/edge-bundling/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      layout: {\n        type: 'circular',\n      },\n      node: { style: { size: 20 } },\n      behaviors: ['drag-canvas', 'drag-element'],\n      plugins: [\n        {\n          key: 'edge-bundling',\n          type: 'edge-bundling',\n          bundleThreshold: 0.1,\n        },\n      ],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## plugin / edge-filter-lens\n\n### Edge Filter Lens\n\n**文件路径**: `plugin/edge-filter-lens/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      node: {\n        style: { size: 16 },\n        palette: {\n          field: (datum) => Math.floor(datum.style?.y / 60),\n        },\n      },\n      edge: {\n        style: {\n          label: false,\n          labelText: (d) => d.data.value?.toString(),\n          stroke: '#ccc',\n        },\n      },\n      plugins: [\n        {\n          key: 'edge-filter-lens',\n          type: 'edge-filter-lens',\n        },\n      ],\n    });\n    graph.render();\n\n    const config = {\n      trigger: 'pointermove',\n      scaleRBy: 'wheel',\n      nodeType: 'both',\n    };\n\n    window.addPanel((gui) => {\n      gui\n        .add(config, 'trigger', ['pointermove', 'click', 'drag'])\n        .name('Trigger')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'edge-filter-lens',\n            trigger: value,\n          });\n        });\n      gui\n        .add(config, 'scaleRBy', ['wheel', 'unset'])\n        .name('Scale R by')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'edge-filter-lens',\n            scaleRBy: value,\n          });\n        });\n      gui\n        .add(config, 'nodeType', ['source', 'target', 'both', 'either'])\n        .name('Node Type')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'edge-filter-lens',\n            nodeType: value,\n          });\n        });\n    });\n  });\n```\n\n---\n\n## plugin / fisheye\n\n### Fisheye\n\n**文件路径**: `plugin/fisheye/demo/basic.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nfetch('https://assets.antv.antgroup.com/g6/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          size: (datum) => datum.id.length * 2 + 10,\n          label: false,\n          labelText: (datum) => datum.id,\n          labelBackground: true,\n          icon: false,\n          iconFontFamily: 'iconfont',\n          iconText: '\\ue6f6',\n          iconFill: '#fff',\n        },\n        palette: {\n          type: 'group',\n          field: (datum) => datum.id,\n          color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n        },\n      },\n      edge: {\n        style: {\n          stroke: '#e2e2e2',\n        },\n      },\n      plugins: [{ key: 'fisheye', type: 'fisheye', nodeStyle: { label: true, icon: true } }],\n    });\n    graph.render();\n  });\n```\n\n---\n\n### Custom Fisheye\n\n**文件路径**: `plugin/fisheye/demo/custom.js`\n\n```js\nimport { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nfetch('https://assets.antv.antgroup.com/g6/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          size: (datum) => datum.id.length * 2 + 10,\n          label: false,\n          labelText: (datum) => datum.id,\n          labelBackground: true,\n          icon: false,\n          iconFontFamily: 'iconfont',\n          iconText: '\\ue6f6',\n          iconFill: '#fff',\n        },\n        palette: {\n          type: 'group',\n          field: (datum) => datum.id,\n          color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n        },\n      },\n      edge: {\n        style: {\n          stroke: '#e2e2e2',\n        },\n      },\n      plugins: [\n        {\n          key: 'fisheye',\n          type: 'fisheye',\n          trigger: 'click',\n          scaleRBy: 'wheel',\n          scaleDBy: 'drag',\n          style: { fill: '#F08F56', stroke: '#666', lineDash: [5, 5] },\n          nodeStyle: { label: true, icon: true },\n        },\n      ],\n    });\n    graph.render();\n\n    const config = {\n      trigger: 'click',\n      scaleRBy: 'wheel',\n      scaleDBy: 'drag',\n      showDPercent: true,\n      borderless: true,\n    };\n\n    window.addPanel((gui) => {\n      gui\n        .add(config, 'trigger', ['pointermove', 'click', 'drag'])\n        .name('Trigger')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'fisheye',\n            trigger: value,\n          });\n        });\n      gui\n        .add(config, 'scaleRBy', ['wheel', 'drag', 'unset'])\n        .name('Scale R by')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'fisheye',\n            scaleRBy: value,\n          });\n        });\n      gui\n        .add(config, 'scaleDBy', ['wheel', 'drag', 'unset'])\n        .name('Scale D by')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'fisheye',\n            scaleDBy: value,\n          });\n        });\n      gui\n        .add(config, 'showDPercent')\n        .name('Show D Percent')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'fisheye',\n            showDPercent: value,\n          });\n        });\n      gui\n        .add(config, 'borderless')\n        .name('Borderless')\n        .onChange((value) => {\n          const style = value\n            ? { fill: 'transparent', lineDash: 0, stroke: 'transparent' }\n            : { fill: '#F08F56', lineDash: [5, 5], stroke: '#666' };\n          graph.updatePlugin({\n            key: 'fisheye',\n            style,\n          });\n        });\n    });\n  });\n```\n\n---\n\n## plugin / fullscreen\n\n### Fullscreen\n\n**文件路径**: `plugin/fullscreen/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  data: { nodes: Array.from({ length: 20 }).map((_, i) => ({ id: `node${i}` })) },\n  autoFit: 'center',\n  background: '#fff',\n  plugins: [\n    {\n      type: 'fullscreen',\n      key: 'fullscreen',\n    },\n    function () {\n      const graph = this;\n      return {\n        type: 'toolbar',\n        key: 'toolbar',\n        position: 'top-left',\n        onClick: (item) => {\n          const fullscreenPlugin = graph.getPluginInstance('fullscreen');\n          if (item === 'request-fullscreen') {\n            fullscreenPlugin.request();\n          }\n          if (item === 'exit-fullscreen') {\n            fullscreenPlugin.exit();\n          }\n        },\n        getItems: () => {\n          return [\n            { id: 'request-fullscreen', value: 'request-fullscreen' },\n            { id: 'exit-fullscreen', value: 'exit-fullscreen' },\n          ];\n        },\n      };\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n## plugin / grid-line\n\n### Grid Line\n\n**文件路径**: `plugin/grid-line/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['drag-canvas'],\n  plugins: [{ key: 'grid-line', type: 'grid-line', follow: false }],\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  gui\n    .add({ follow: false }, 'follow')\n    .name('Follow')\n    .onChange((value) => {\n      graph.updatePlugin({\n        key: 'grid-line',\n        follow: value,\n      });\n    });\n});\n```\n\n---\n\n## plugin / history\n\n### Redo/Undo\n\n**文件路径**: `plugin/history/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [{ id: 'node-0', style: { x: 200, y: 150 } }],\n  },\n  node: {\n    style: {\n      size: 60,\n      labelText: 'Drag Me!',\n      labelPlacement: 'middle',\n      labelFill: '#fff',\n    },\n  },\n  behaviors: ['drag-element'],\n  plugins: [\n    {\n      type: 'history',\n      key: 'history',\n    },\n  ],\n});\n\ngraph.render().then(() => {\n  window.addPanel((gui) => {\n    const history = graph.getPluginInstance('history');\n    const config = {\n      undo: () => {\n        if (history.canUndo()) history.undo();\n      },\n      redo: () => {\n        if (history.canRedo()) history.redo();\n      },\n    };\n    gui.add(config, 'undo').name('⬅️ undo');\n    gui.add(config, 'redo').name('➡️ redo');\n  });\n});\n```\n\n---\n\n## plugin / hull\n\n### Use hulls to wrap the node sets.\n\n**文件路径**: `plugin/hull/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/collection.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const groupedNodesByCluster = data.nodes.reduce((acc, node) => {\n      const cluster = node.data.cluster;\n      acc[cluster] ||= [];\n      acc[cluster].push(node.id);\n      return acc;\n    }, {});\n\n    const createStyle = (baseColor) => ({\n      fill: baseColor,\n      stroke: baseColor,\n      labelFill: '#fff',\n      labelPadding: 2,\n      labelBackgroundFill: baseColor,\n      labelBackgroundRadius: 5,\n    });\n\n    const graph = new Graph({\n      container: 'container',\n      data,\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n      node: {\n        palette: { field: 'cluster' },\n      },\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        linkDistance: (d) => {\n          if (d.source === 'node0' || d.target === 'node0') {\n            return 200;\n          }\n          return 80;\n        },\n      },\n      plugins: [\n        {\n          key: 'hull-a',\n          type: 'hull',\n          members: groupedNodesByCluster['a'],\n          labelText: 'cluster-a',\n          ...createStyle('#1783FF'),\n        },\n        {\n          key: 'hull-b',\n          type: 'hull',\n          members: groupedNodesByCluster['b'],\n          labelText: 'cluster-b',\n          ...createStyle('#00C9C9'),\n        },\n        {\n          key: 'hull-c',\n          type: 'hull',\n          members: groupedNodesByCluster['c'],\n          labelText: 'cluster-c',\n          ...createStyle('#F08F56'),\n        },\n        {\n          key: 'hull-d',\n          type: 'hull',\n          members: groupedNodesByCluster['d'],\n          labelText: 'cluster-d',\n          ...createStyle('#D580FF'),\n        },\n      ],\n      autoFit: 'center',\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## plugin / legend\n\n### Default legend\n\n**文件路径**: `plugin/legend/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n    { id: 'node-3', type: 'triangle', data: { cluster: 'node-type3' } },\n    { id: 'node-4', type: 'diamond', data: { cluster: 'node-type4' } },\n  ],\n  edges: [\n    { source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } },\n    { source: 'node-1', target: 'node-4', data: { cluster: 'edge-type2' } },\n    { source: 'node-3', target: 'node-4' },\n    { source: 'node-2', target: 'node-4', data: { cluster: 'edge-type3' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: { size: 32 },\n    palette: {\n      field: 'cluster',\n    },\n  },\n  layout: {\n    type: 'force',\n  },\n  plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n### Click legend\n\n**文件路径**: `plugin/legend/demo/click.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: '2', type: 'rect', data: { cluster: 'node-type2' } },\n    { id: '3', type: 'triangle', data: { cluster: 'node-type3' } },\n    { id: '4', type: 'diamond', data: { cluster: 'node-type4' } },\n  ],\n  edges: [\n    { source: '1', target: '2', type: 'quadratic', data: { cluster: 'edge-type1' } },\n    { source: '1', target: '4', data: { cluster: 'edge-type2' } },\n    { source: '3', target: '4' },\n    { source: '2', target: '4', data: { cluster: 'edge-type3' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: { size: 32 },\n    palette: { field: 'cluster' },\n  },\n  edge: {\n    palette: { field: 'cluster' },\n  },\n  layout: {\n    type: 'force',\n  },\n  plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n      trigger: 'click',\n      gridRow: 1,\n      gridCol: 4,\n      itemLabelFontSize: 12,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n### Item style\n\n**文件路径**: `plugin/legend/demo/style.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: '1',\n      type: 'circle',\n      data: { cluster: 'node-type1' },\n    },\n    {\n      id: '2',\n      type: 'rect',\n      data: { cluster: 'node-type2' },\n    },\n    {\n      id: '3',\n      type: 'triangle',\n      data: { cluster: 'node-type3' },\n    },\n    {\n      id: '4',\n      type: 'diamond',\n      data: { cluster: 'node-type4' },\n    },\n  ],\n  edges: [\n    {\n      id: '1-2',\n      source: '1',\n      target: '2',\n      type: 'quadratic',\n      data: { cluster: 'edge-type1' },\n    },\n    {\n      id: '1-4',\n      source: '1',\n      target: '4',\n      data: { cluster: 'edge-type2' },\n    },\n    {\n      id: '3-4',\n      source: '3',\n      target: '4',\n    },\n    {\n      id: '2-4',\n      source: '2',\n      target: '4',\n      data: { cluster: 'edge-type3' },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: { size: 32 },\n    palette: { field: 'cluster' },\n  },\n  edge: {\n    palette: { field: 'cluster' },\n  },\n  layout: {\n    type: 'force',\n  },\n  plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n      titleText: 'Legend Title',\n      trigger: 'click',\n      position: 'top',\n      gridCol: 3,\n      itemLabelFontSize: 12,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n## plugin / minimap\n\n### Minimap\n\n**文件路径**: `plugin/minimap/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: { nodes: Array.from({ length: 20 }).map((_, i) => ({ id: `node${i}` })) },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'minimap',\n      size: [240, 160],\n    },\n  ],\n  node: {\n    palette: 'spectral',\n  },\n  layout: {\n    type: 'circular',\n  },\n  autoFit: 'view',\n});\n\ngraph.render();\n```\n\n---\n\n## plugin / snapline\n\n### Snapline\n\n**文件路径**: `plugin/snapline/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'snapline',\n      key: 'snapline',\n      verticalLineStyle: { stroke: '#F08F56', lineWidth: 2 },\n      horizontalLineStyle: { stroke: '#17C76F', lineWidth: 2 },\n      autoSnap: false,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n### Snapline with auto snap\n\n**文件路径**: `plugin/snapline/demo/autoSnap.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'snapline',\n      key: 'snapline',\n      verticalLineStyle: { stroke: '#F08F56', lineWidth: 2 },\n      horizontalLineStyle: { stroke: '#17C76F', lineWidth: 2 },\n      offset: Infinity,\n      autoSnap: true,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n## plugin / timebar\n\n### Time Mode\n\n**文件路径**: `plugin/timebar/demo/time.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst startTime = new Date('2023-08-01').getTime();\nconst diff = 3600 * 24 * 1000;\nconst timebarData = [10, 2, 3, 4, 15, 10, 5, 0, 3, 1].map((value, index) => ({\n  time: new Date(startTime + index * diff),\n  value,\n  label: new Date(startTime + index * diff).toLocaleString(),\n}));\nconst graphData = {\n  nodes: new Array(49).fill(0).map((_, index) => ({\n    id: `node-${index}`,\n    data: {\n      timestamp: startTime + (index % 10) * diff,\n      value: index % 20,\n      label: new Date(startTime + (index % 10) * diff).toLocaleString(),\n    },\n  })),\n  edges: new Array(49).fill(0).map((_, i) => ({\n    id: `edge-${i}`,\n    source: `node-${i % 30}`,\n    target: `node-${(i % 20) + 29}`,\n    data: {\n      edgeType: 'e1',\n    },\n  })),\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data: graphData,\n  behaviors: ['drag-canvas', 'drag-element', 'zoom-canvas'],\n  layout: {\n    type: 'grid',\n    cols: 7,\n  },\n  plugins: [\n    {\n      type: 'timebar',\n      key: 'timebar',\n      data: timebarData,\n      width: 450,\n      loop: true,\n    },\n  ],\n  autoFit: 'view',\n  padding: [10, 0, 65, 0],\n});\n\ngraph.render();\n```\n\n---\n\n### Chart Mode\n\n**文件路径**: `plugin/timebar/demo/chart.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst startTime = new Date('2023-08-01').getTime();\nconst diff = 3600 * 24 * 1000;\nconst timebarData = [10, 2, 3, 4, 15, 10, 5, 0, 3, 1].map((value, index) => ({\n  time: new Date(startTime + index * diff),\n  value,\n  label: new Date(startTime + index * diff).toLocaleString(),\n}));\nconst graphData = {\n  nodes: new Array(49).fill(0).map((_, index) => ({\n    id: `node-${index}`,\n    data: {\n      timestamp: startTime + (index % 10) * diff,\n      value: index % 20,\n      label: new Date(startTime + (index % 10) * diff).toLocaleString(),\n    },\n  })),\n  edges: new Array(49).fill(0).map((_, i) => ({\n    id: `edge-${i}`,\n    source: `node-${i % 30}`,\n    target: `node-${(i % 20) + 29}`,\n    data: {\n      edgeType: 'e1',\n    },\n  })),\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data: graphData,\n  behaviors: ['drag-canvas', 'drag-element', 'zoom-canvas'],\n  layout: {\n    type: 'grid',\n    cols: 7,\n  },\n  plugins: [\n    {\n      type: 'timebar',\n      key: 'timebar',\n      data: timebarData,\n      width: 450,\n      height: 100,\n      loop: true,\n      timebarType: 'chart',\n    },\n  ],\n  autoFit: 'view',\n  padding: [10, 0, 160, 0],\n});\n\ngraph.render();\n```\n\n---\n\n## plugin / toolbar\n\n### ToolBar\n\n**文件路径**: `plugin/toolbar/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'toolbar',\n      position: 'top-left',\n      onClick: (item) => {\n        alert('item clicked:' + item);\n      },\n      getItems: () => {\n        // G6 内置了 9 个 icon，分别是 zoom-in、zoom-out、redo、undo、edit、delete、auto-fit、export、reset\n        return [\n          { id: 'zoom-in', value: 'zoom-in' },\n          { id: 'zoom-out', value: 'zoom-out' },\n          { id: 'redo', value: 'redo' },\n          { id: 'undo', value: 'undo' },\n          { id: 'edit', value: 'edit' },\n          { id: 'delete', value: 'delete' },\n          { id: 'auto-fit', value: 'auto-fit' },\n          { id: 'export', value: 'export' },\n          { id: 'reset', value: 'reset' },\n        ];\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n### ToolBar\n\n**文件路径**: `plugin/toolbar/demo/custom.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\n// Use your own iconfont.\nconst iconFont = document.createElement('script');\niconFont.src = '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js';\ndocument.head.appendChild(iconFont);\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'toolbar',\n      position: 'right-top',\n      onClick: (item) => {\n        alert('item clicked:' + item);\n      },\n      getItems: () => {\n        return [\n          { id: 'icon-xinjian', value: 'new' },\n          { id: 'icon-fenxiang', value: 'share' },\n          { id: 'icon-chexiao', value: 'undo' },\n        ];\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n## plugin / tooltip\n\n### Tooltip\n\n**文件路径**: `plugin/tooltip/demo/basic.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: '0', data: { label: 'node-0', description: 'This is node-0.' } },\n      { id: '1', data: { label: 'node-1', description: 'This is node-1.' } },\n      { id: '2', data: { label: 'node-2', description: 'This is node-2.' } },\n      { id: '3', data: { label: 'node-3', description: 'This is node-3.' } },\n      { id: '4', data: { label: 'node-4', description: 'This is node-4.' } },\n      { id: '5', data: { label: 'node-5', description: 'This is node-5.' } },\n    ],\n    edges: [\n      { source: '0', target: '1', data: { description: 'This is edge from node 0 to node 1.' } },\n      { source: '0', target: '2', data: { description: 'This is edge from node 0 to node 2.' } },\n      { source: '0', target: '3', data: { description: 'This is edge from node 0 to node 3.' } },\n      { source: '0', target: '4', data: { description: 'This is edge from node 0 to node 4.' } },\n      { source: '0', target: '5', data: { description: 'This is edge from node 0 to node 5.' } },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  plugins: [\n    {\n      type: 'tooltip',\n      getContent: (e, items) => {\n        let result = `<h4>Custom Content</h4>`;\n        items.forEach((item) => {\n          result += `<p>Type: ${item.data.description}</p>`;\n        });\n        return result;\n      },\n    },\n  ],\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n### Show on click\n\n**文件路径**: `plugin/tooltip/demo/click.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: '0', data: { label: 'node-0', description: 'This is node-0.' } },\n      { id: '1', data: { label: 'node-1', description: 'This is node-1.' } },\n      { id: '2', data: { label: 'node-2', description: 'This is node-2.' } },\n      { id: '3', data: { label: 'node-3', description: 'This is node-3.' } },\n      { id: '4', data: { label: 'node-4', description: 'This is node-4.' } },\n      { id: '5', data: { label: 'node-5', description: 'This is node-5.' } },\n    ],\n    edges: [\n      { source: '0', target: '1', data: { description: 'This is edge from node 0 to node 1.' } },\n      { source: '0', target: '2', data: { description: 'This is edge from node 0 to node 2.' } },\n      { source: '0', target: '3', data: { description: 'This is edge from node 0 to node 3.' } },\n      { source: '0', target: '4', data: { description: 'This is edge from node 0 to node 4.' } },\n      { source: '0', target: '5', data: { description: 'This is edge from node 0 to node 5.' } },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  plugins: [\n    {\n      type: 'tooltip',\n      trigger: 'click',\n      getContent: (e, items) => {\n        let result = `<h4>Custom Content</h4>`;\n        items.forEach((item) => {\n          result += `<p>Type: ${item.data.description}</p>`;\n        });\n        return result;\n      },\n    },\n  ],\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n### Dual tooltips\n\n**文件路径**: `plugin/tooltip/demo/dual.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: '0', data: { label: 'node-0', description: 'This is node-0.' } },\n      { id: '1', data: { label: 'node-1', description: 'This is node-1.' } },\n      { id: '2', data: { label: 'node-2', description: 'This is node-2.' } },\n      { id: '3', data: { label: 'node-3', description: 'This is node-3.' } },\n      { id: '4', data: { label: 'node-4', description: 'This is node-4.' } },\n      { id: '5', data: { label: 'node-5', description: 'This is node-5.' } },\n    ],\n    edges: [\n      { source: '0', target: '1', data: { description: 'This is edge from node 0 to node 1.' } },\n      { source: '0', target: '2', data: { description: 'This is edge from node 0 to node 2.' } },\n      { source: '0', target: '3', data: { description: 'This is edge from node 0 to node 3.' } },\n      { source: '0', target: '4', data: { description: 'This is edge from node 0 to node 4.' } },\n      { source: '0', target: '5', data: { description: 'This is edge from node 0 to node 5.' } },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  plugins: [\n    function () {\n      return {\n        key: 'tooltip-click',\n        type: 'tooltip',\n        trigger: 'click',\n        getContent: (evt, items) => {\n          return `<div>click ${items[0].id}</div>`;\n        },\n        onOpenChange: (open) => {\n          const tooltip = this.getPluginInstance('tooltip-hover');\n          if (tooltip && open) tooltip.hide();\n        },\n      };\n    },\n    function () {\n      return {\n        key: 'tooltip-hover',\n        type: 'tooltip',\n        trigger: 'hover',\n        enable: (e) => {\n          const tooltip = this.getPluginInstance('tooltip-click');\n          return e.target.id !== tooltip.currentTarget;\n        },\n        getContent: (evt, items) => {\n          return `<div>hover ${items[0].id}</div>`;\n        },\n        onOpenChange: (open) => {\n          const tooltip = this.getPluginInstance('tooltip-click');\n          if (tooltip && open) {\n            tooltip.hide();\n          }\n        },\n      };\n    },\n  ],\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n```\n\n---\n\n## plugin / watermark\n\n### Text Watermark\n\n**文件路径**: `plugin/watermark/demo/text.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'watermark',\n      text: 'G6: Graph Visualization',\n      textFontSize: 14,\n      textFontFamily: 'Microsoft YaHei',\n      fill: 'rgba(0, 0, 0, 0.1)',\n      rotate: Math.PI / 12,\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n### Repeat Image\n\n**文件路径**: `plugin/watermark/demo/repeat.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'watermark',\n      width: 200,\n      height: 100,\n      rotate: Math.PI / 12,\n      imageURL: 'https://gw.alipayobjects.com/os/s/prod/antv/assets/image/logo-with-text-73b8a.svg',\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n## scene-case / default\n\n### Music Festival\n\n**文件路径**: `scene-case/default/demo/music-festival.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/music-festival.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const map = new Map();\n\n    data.forEach((datum) => {\n      const { actors, venuecity } = datum;\n      actors.forEach((actor) => {\n        if (!map.has(actor)) map.set(actor, new Set([venuecity]));\n        else map.get(actor).add(venuecity);\n      });\n    });\n\n    const nodes = Array.from(map)\n      .filter(([, city]) => city.size >= 2)\n      .sort((a, b) => -a[1].size + b[1].size)\n      .map(([name, city]) => ({\n        id: name,\n        data: {\n          city: Array.from(city),\n          value: city.size,\n        },\n      }));\n\n    return { nodes };\n  })\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      node: {\n        type: 'rect',\n        style: {\n          size: [100, 20],\n          radius: 5,\n          iconText: (d) => d.id,\n          iconFill: '#000',\n          iconWordWrap: true,\n          iconWordWrapWidth: 80,\n          iconFontSize: 15,\n          iconTextOverflow: '...',\n          iconMaxLines: 1,\n          labelText: (d) => d.data.city.join('\\n'),\n          labelFontSize: 12,\n          labelDy: 2,\n          labelFill: '#fff',\n        },\n        palette: {\n          type: 'group',\n          field: 'value',\n          color: [\n            '#FCE75A',\n            '#F5DB75',\n            '#EFCF90',\n            '#E8C3AB',\n            '#E1B7C6',\n            '#DBABE0',\n            '#D49FFB',\n            '#CD93FF',\n            '#B981F2',\n            '#7E45E8',\n          ],\n        },\n      },\n      layout: {\n        type: 'grid',\n        nodeSize: [100, 120],\n        sortBy: 'order',\n        cols: 5,\n      },\n      behaviors: [{ type: 'scroll-canvas', direction: 'y' }],\n      plugins: [\n        {\n          type: 'background',\n          background: '#000',\n        },\n      ],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Fund Flow\n\n**文件路径**: `scene-case/default/demo/fund-flow.js`\n\n```js\nimport { Rect as GRect, Text as GText } from '@antv/g';\nimport {\n  Badge,\n  CommonEvent,\n  ExtensionCategory,\n  Graph,\n  GraphEvent,\n  iconfont,\n  Label,\n  Rect,\n  register,\n  treeToGraphData,\n} from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst COLORS = {\n  B: '#1783FF',\n  R: '#F46649',\n  Y: '#DB9D0D',\n  G: '#60C42D',\n  DI: '#A7A7A7',\n};\nconst GREY_COLOR = '#CED4D9';\n\nclass TreeNode extends Rect {\n  get data() {\n    return this.context.model.getNodeLikeDatum(this.id);\n  }\n\n  get childrenData() {\n    return this.context.model.getChildrenData(this.id);\n  }\n\n  getLabelStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2 + 8,\n      y: -height / 2 + 16,\n      text: this.data.name,\n      fontSize: 12,\n      opacity: 0.85,\n      fill: '#000',\n      cursor: 'pointer',\n    };\n  }\n\n  getPriceStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2 + 8,\n      y: height / 2 - 8,\n      text: this.data.label,\n      fontSize: 16,\n      fill: '#000',\n      opacity: 0.85,\n    };\n  }\n\n  drawPriceShape(attributes, container) {\n    const priceStyle = this.getPriceStyle(attributes);\n    this.upsert('price', GText, priceStyle, container);\n  }\n\n  getCurrencyStyle(attributes) {\n    const [, height] = this.getSize(attributes);\n    return {\n      x: this.shapeMap['price'].getLocalBounds().max[0] + 4,\n      y: height / 2 - 8,\n      text: this.data.currency,\n      fontSize: 12,\n      fill: '#000',\n      opacity: 0.75,\n    };\n  }\n\n  drawCurrencyShape(attributes, container) {\n    const currencyStyle = this.getCurrencyStyle(attributes);\n    this.upsert('currency', GText, currencyStyle, container);\n  }\n\n  getPercentStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: width / 2 - 4,\n      y: height / 2 - 8,\n      text: `${((Number(this.data.variableValue) || 0) * 100).toFixed(2)}%`,\n      fontSize: 12,\n      textAlign: 'right',\n      fill: COLORS[this.data.status],\n    };\n  }\n\n  drawPercentShape(attributes, container) {\n    const percentStyle = this.getPercentStyle(attributes);\n    this.upsert('percent', GText, percentStyle, container);\n  }\n\n  getTriangleStyle(attributes) {\n    const percentMinX = this.shapeMap['percent'].getLocalBounds().min[0];\n    const [, height] = this.getSize(attributes);\n    return {\n      fill: COLORS[this.data.status],\n      x: this.data.variableUp ? percentMinX - 18 : percentMinX,\n      y: height / 2 - 16,\n      fontFamily: 'iconfont',\n      fontSize: 16,\n      text: '\\ue62d',\n      transform: this.data.variableUp ? [] : [['rotate', 180]],\n    };\n  }\n\n  drawTriangleShape(attributes, container) {\n    const triangleStyle = this.getTriangleStyle(attributes);\n    this.upsert('triangle', Label, triangleStyle, container);\n  }\n\n  getVariableStyle(attributes) {\n    const [, height] = this.getSize(attributes);\n    return {\n      fill: '#000',\n      fontSize: 12,\n      opacity: 0.45,\n      text: this.data.variableName,\n      textAlign: 'right',\n      x: this.shapeMap['triangle'].getLocalBounds().min[0] - 4,\n      y: height / 2 - 8,\n    };\n  }\n\n  drawVariableShape(attributes, container) {\n    const variableStyle = this.getVariableStyle(attributes);\n    this.upsert('variable', GText, variableStyle, container);\n  }\n\n  getCollapseStyle(attributes) {\n    if (this.childrenData.length === 0) return false;\n    const { collapsed } = attributes;\n    const [width, height] = this.getSize(attributes);\n    return {\n      backgroundFill: '#fff',\n      backgroundHeight: 16,\n      backgroundLineWidth: 1,\n      backgroundRadius: 0,\n      backgroundStroke: GREY_COLOR,\n      backgroundWidth: 16,\n      cursor: 'pointer',\n      fill: GREY_COLOR,\n      fontSize: 16,\n      text: collapsed ? '+' : '-',\n      textAlign: 'center',\n      textBaseline: 'middle',\n      x: width / 2,\n      y: 0,\n    };\n  }\n\n  drawCollapseShape(attributes, container) {\n    const collapseStyle = this.getCollapseStyle(attributes);\n    const btn = this.upsert('collapse', Badge, collapseStyle, container);\n\n    if (btn && !Reflect.has(btn, '__bind__')) {\n      Reflect.set(btn, '__bind__', true);\n      btn.addEventListener(CommonEvent.CLICK, () => {\n        const { collapsed } = this.attributes;\n        const graph = this.context.graph;\n        if (collapsed) graph.expandElement(this.id);\n        else graph.collapseElement(this.id);\n      });\n    }\n  }\n\n  getProcessBarStyle(attributes) {\n    const { rate, status } = this.data;\n    const { radius } = attributes;\n    const color = COLORS[status];\n    const percent = `${Number(rate) * 100}%`;\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2,\n      y: height / 2 - 4,\n      width: width,\n      height: 4,\n      radius: [0, 0, radius, radius],\n      fill: `linear-gradient(to right, ${color} ${percent}, ${GREY_COLOR} ${percent})`,\n    };\n  }\n\n  drawProcessBarShape(attributes, container) {\n    const processBarStyle = this.getProcessBarStyle(attributes);\n    this.upsert('process-bar', GRect, processBarStyle, container);\n  }\n\n  getKeyStyle(attributes) {\n    const keyStyle = super.getKeyStyle(attributes);\n    return {\n      ...keyStyle,\n      fill: '#fff',\n      lineWidth: 1,\n      stroke: GREY_COLOR,\n    };\n  }\n\n  render(attributes = this.parsedAttributes, container) {\n    super.render(attributes, container);\n\n    this.drawPriceShape(attributes, container);\n    this.drawCurrencyShape(attributes, container);\n    this.drawPercentShape(attributes, container);\n    this.drawTriangleShape(attributes, container);\n    this.drawVariableShape(attributes, container);\n    this.drawProcessBarShape(attributes, container);\n    this.drawCollapseShape(attributes, container);\n  }\n}\n\nregister(ExtensionCategory.NODE, 'tree-node', TreeNode);\n\nfetch('https://assets.antv.antgroup.com/g6/decision-tree.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data, {\n        getNodeData: (datum, depth) => {\n          if (!datum.style) datum.style = {};\n          datum.style.collapsed = depth >= 2;\n          if (!datum.children) return datum;\n          const { children, ...restDatum } = datum;\n          return { ...restDatum, children: children.map((child) => child.id) };\n        },\n      }),\n      node: {\n        type: 'tree-node',\n        style: {\n          size: [202, 60],\n          ports: [{ placement: 'left' }, { placement: 'right' }],\n          radius: 4,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        style: {\n          stroke: GREY_COLOR,\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'LR',\n        dropCap: false,\n        indent: 300,\n        getHeight: () => 60,\n        preLayout: false,\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Organization Chart\n\n**文件路径**: `scene-case/default/demo/organization-chart.js`\n\n```js\nimport { Badge, BaseBehavior, ExtensionCategory, Graph, GraphEvent, Label, Rect, register } from '@antv/g6';\n\nconst statusColors = {\n  online: '#17BEBB',\n  busy: '#E36397',\n  offline: '#B7AD99',\n};\n\nconst DEFAULT_LEVEL = 'detailed';\n\n/**\n * Draw a chart node with different ui based on the zoom level.\n */\nclass ChartNode extends Rect {\n  get data() {\n    return this.context.model.getElementDataById(this.id).data;\n  }\n\n  get level() {\n    return this.data.level || DEFAULT_LEVEL;\n  }\n\n  getLabelStyle() {\n    const text = this.data.name;\n    const labelStyle =\n      this.level === 'overview'\n        ? {\n            fill: '#fff',\n            fontSize: 20,\n            fontWeight: 600,\n            textAlign: 'center',\n            transform: [['translate', 0, 0]],\n          }\n        : {\n            fill: '#2078B4',\n            fontSize: 14,\n            fontWeight: 400,\n            textAlign: 'left',\n            transform: [['translate', -65, -15]],\n          };\n    return { text, ...labelStyle };\n  }\n\n  getKeyStyle(attributes) {\n    return {\n      ...super.getKeyStyle(attributes),\n      fill: this.level === 'overview' ? statusColors[this.data.status] : '#fff',\n    };\n  }\n\n  getPositionStyle(attributes) {\n    if (this.level === 'overview') return false;\n    return {\n      text: this.data.position,\n      fontSize: 8,\n      fontWeight: 400,\n      textTransform: 'uppercase',\n      fill: '#343f4a',\n      textAlign: 'left',\n      transform: [['translate', -65, 0]],\n    };\n  }\n\n  drawPositionShape(attributes, container) {\n    const positionStyle = this.getPositionStyle(attributes);\n    this.upsert('position', Label, positionStyle, container);\n  }\n\n  getStatusStyle(attributes) {\n    if (this.level === 'overview') return false;\n    return {\n      text: this.data.status,\n      fontSize: 8,\n      textAlign: 'left',\n      transform: [['translate', 40, -16]],\n      padding: [0, 4],\n      fill: '#fff',\n      backgroundFill: statusColors[this.data.status],\n    };\n  }\n\n  drawStatusShape(attributes, container) {\n    const statusStyle = this.getStatusStyle(attributes);\n    this.upsert('status', Badge, statusStyle, container);\n  }\n\n  getPhoneStyle(attributes) {\n    if (this.level === 'overview') return false;\n    return {\n      text: this.data.phone,\n      fontSize: 8,\n      fontWeight: 300,\n      textAlign: 'left',\n      transform: [['translate', -65, 20]],\n    };\n  }\n\n  drawPhoneShape(attributes, container) {\n    const style = this.getPhoneStyle(attributes);\n    this.upsert('phone', Label, style, container);\n  }\n\n  render(attributes = this.parsedAttributes, container = this) {\n    super.render(attributes, container);\n\n    this.drawPositionShape(attributes, container);\n\n    this.drawStatusShape(attributes, container);\n\n    this.drawPhoneShape(attributes, container);\n  }\n}\n\n/**\n * Implement a level of detail rendering, which will show different details based on the zoom level.\n */\nclass LevelOfDetail extends BaseBehavior {\n  prevLevel = DEFAULT_LEVEL;\n  levels = {\n    ['overview']: [0, 0.6],\n    ['detailed']: [0.6, Infinity],\n  };\n\n  constructor(context, options) {\n    super(context, options);\n    this.bindEvents();\n  }\n\n  update(options) {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  updateZoomLevel = async (e) => {\n    if ('scale' in e.data) {\n      const scale = e.data.scale;\n      const level = Object.entries(this.levels).find(([key, [min, max]]) => scale > min && scale <= max)?.[0];\n      if (level && this.prevLevel !== level) {\n        const { graph } = this.context;\n        graph.updateNodeData((prev) => prev.map((node) => ({ ...node, data: { ...node.data, level } })));\n        await graph.draw();\n        this.prevLevel = level;\n      }\n    }\n  };\n\n  bindEvents() {\n    const { graph } = this.context;\n    graph.on(GraphEvent.AFTER_TRANSFORM, this.updateZoomLevel);\n  }\n\n  unbindEvents() {\n    const { graph } = this.context;\n    graph.off(GraphEvent.AFTER_TRANSFORM, this.updateZoomLevel);\n  }\n\n  destroy() {\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n\nregister(ExtensionCategory.NODE, 'chart-node', ChartNode);\nregister(ExtensionCategory.BEHAVIOR, 'level-of-detail', LevelOfDetail);\n\nfetch('https://assets.antv.antgroup.com/g6/organization-chart.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      node: {\n        type: 'chart-node',\n        style: {\n          labelPlacement: 'center',\n          lineWidth: 1,\n          ports: [{ placement: 'top' }, { placement: 'bottom' }],\n          radius: 2,\n          shadowBlur: 10,\n          shadowColor: '#e0e0e0',\n          shadowOffsetX: 3,\n          size: [150, 60],\n          stroke: '#C0C0C0',\n        },\n      },\n      edge: {\n        type: 'polyline',\n        style: {\n          router: {\n            type: 'orth',\n          },\n          stroke: '#C0C0C0',\n        },\n      },\n      layout: {\n        type: 'dagre',\n      },\n      autoFit: 'view',\n      behaviors: ['level-of-detail', 'zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### System Performance Diagnosis Flowchart\n\n**文件路径**: `scene-case/default/demo/performance-diagnosis-flowchart.js`\n\n```js\nimport { BugOutlined } from '@ant-design/icons';\nimport { ExtensionCategory, Graph, HoverActivate, idOf, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\nimport { Flex, Typography } from 'antd';\nimport { useEffect, useRef } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nconst { Text } = Typography;\n\nconst ACTIVE_COLOR = '#f6c523';\nconst COLOR_MAP = {\n  'pre-inspection': '#3fc1c9',\n  problem: '#8983f3',\n  inspection: '#f48db4',\n  solution: '#ffaa64',\n};\n\nclass HoverElement extends HoverActivate {\n  getActiveIds(event) {\n    const { model, graph } = this.context;\n    const { targetType, target } = event;\n    const targetId = target.id;\n\n    const ids = [targetId];\n    if (targetType === 'edge') {\n      const edge = model.getEdgeDatum(targetId);\n      ids.push(edge.source, edge.target);\n    } else if (targetType === 'node') {\n      ids.push(...model.getRelatedEdgesData(targetId).map(idOf));\n    }\n\n    graph.frontElement(ids);\n\n    return ids;\n  }\n}\n\nregister(ExtensionCategory.NODE, 'react', ReactNode);\nregister(ExtensionCategory.BEHAVIOR, 'hover-element', HoverElement);\n\nconst Node = ({ data }) => {\n  const { text, type } = data.data;\n\n  const isHovered = data.states?.includes('active');\n  const isSelected = data.states?.includes('selected');\n  const color = isHovered ? ACTIVE_COLOR : COLOR_MAP[type];\n\n  const containerStyle = {\n    width: '100%',\n    height: '100%',\n    background: color,\n    border: `3px solid ${color}`,\n    borderRadius: 16,\n    cursor: 'pointer',\n  };\n\n  if (isSelected) {\n    Object.assign(containerStyle, { border: `3px solid #000` });\n  }\n\n  return (\n    <Flex style={containerStyle} align=\"center\" justify=\"center\">\n      <Flex vertical style={{ padding: '8px 16px', textAlign: 'center' }} align=\"center\" justify=\"center\">\n        {type === 'problem' && <BugOutlined style={{ color: '#fff', fontSize: 24, marginBottom: 8 }} />}\n        <Text style={{ color: '#fff', fontWeight: 600, fontSize: 16 }}>{text}</Text>\n      </Flex>\n    </Flex>\n  );\n};\n\nexport const PerformanceDiagnosisFlowchart = () => {\n  const containerRef = useRef();\n\n  useEffect(() => {\n    fetch('https://assets.antv.antgroup.com/g6/performance-diagnosis.json')\n      .then((res) => res.json())\n      .then((data) => {\n        const graph = new Graph({\n          container: containerRef.current,\n          data,\n          autoFit: 'view',\n          node: {\n            type: 'react',\n            style: (d) => {\n              const style = {\n                component: <Node data={d} />,\n                ports: [{ placement: 'top' }, { placement: 'bottom' }],\n              };\n\n              const size = {\n                'pre-inspection': [240, 120],\n                problem: [200, 120],\n                inspection: [330, 100],\n                solution: [200, 120],\n              }[d.data.type] || [200, 80];\n\n              Object.assign(style, {\n                size,\n                dx: -size[0] / 2,\n                dy: -size[1] / 2,\n              });\n              return style;\n            },\n            state: {\n              active: {\n                halo: false,\n              },\n              selected: {\n                halo: false,\n              },\n            },\n          },\n          edge: {\n            type: 'polyline',\n            style: {\n              lineWidth: 3,\n              radius: 20,\n              stroke: '#8b9baf',\n              endArrow: true,\n              labelText: (d) => d.data.text,\n              labelFill: '#8b9baf',\n              labelFontWeight: 600,\n              labelBackground: true,\n              labelBackgroundFill: '#f8f8f8',\n              labelBackgroundOpacity: 1,\n              labelBackgroundLineWidth: 3,\n              labelBackgroundStroke: '#8b9baf',\n              labelPadding: [1, 10],\n              labelBackgroundRadius: 4,\n              router: {\n                type: 'orth',\n              },\n            },\n            state: {\n              active: {\n                stroke: ACTIVE_COLOR,\n                labelBackgroundStroke: ACTIVE_COLOR,\n                halo: false,\n              },\n            },\n          },\n          layout: {\n            type: 'antv-dagre',\n          },\n          behaviors: ['zoom-canvas', 'drag-canvas', 'hover-element', 'click-select'],\n        });\n\n        graph.render();\n      });\n  }, []);\n\n  return <div style={{ width: '100%', height: '100%' }} ref={containerRef}></div>;\n};\n\nconst root = createRoot(document.getElementById('container'));\nroot.render(<PerformanceDiagnosisFlowchart />);\n```\n\n---\n\n### Sub Graph\n\n**文件路径**: `scene-case/default/demo/sub-graph.js`\n\n```js\nimport { BaseCombo, ExtensionCategory, Graph, HTML, isCollapsed, register } from '@antv/g6';\nimport { isEqual } from '@antv/util';\n\nclass SubGraphNode extends HTML {\n  connectedCallback() {\n    super.connectedCallback();\n    this.drawSubGraph();\n  }\n\n  render(attributes, container) {\n    super.render(attributes, container);\n    this.drawSubGraph();\n  }\n\n  get data() {\n    return this.context.graph.getElementData(this.id).data;\n  }\n\n  drawSubGraph() {\n    if (!this.isConnected) return;\n    if (isEqual(this.previousData, this.data)) return;\n    this.previousData = this.data;\n\n    const data = this.data;\n    this.drawGraphNode(data.data);\n  }\n\n  drawGraphNode(data) {\n    const [width, height] = this.getSize();\n    const container = this.getDomElement();\n    container.innerHTML = '';\n\n    const subGraph = new Graph({\n      container,\n      width,\n      height,\n      animation: false,\n      data: data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          iconFontFamily: 'iconfont',\n          iconText: '\\ue6e5',\n        },\n      },\n      layout: {\n        type: 'force',\n        linkDistance: 50,\n      },\n      behaviors: ['zoom-canvas', { type: 'drag-canvas', enable: (event) => event.shiftKey === true }],\n      autoFit: 'view',\n    });\n\n    subGraph.render();\n\n    this.graph = subGraph;\n  }\n\n  destroy() {\n    this.graph?.destroy();\n    super.destroy();\n  }\n}\n\nclass CardCombo extends BaseCombo {\n  getKeyStyle(attributes) {\n    const keyStyle = super.getKeyStyle(attributes);\n    const [width, height] = this.getKeySize(attributes);\n    return {\n      ...keyStyle,\n      width,\n      height,\n      x: -width / 2,\n      y: -height / 2,\n    };\n  }\n\n  drawKeyShape(attributes, container) {\n    const { collapsed } = attributes;\n    const outer = this.upsert('key', 'rect', this.getKeyStyle(attributes), container);\n    if (!outer || !collapsed) {\n      this.removeCardShape();\n      return outer;\n    }\n\n    this.drawCardShape(attributes, container);\n\n    return outer;\n  }\n\n  drawCardShape(attributes, container) {\n    const [width, height] = this.getCollapsedKeySize(attributes);\n    const data = this.context.graph.getComboData(this.id).data;\n\n    const baseX = -width / 2;\n    const baseY = -height / 2;\n\n    this.upsert(\n      'card-title',\n      'text',\n      {\n        x: baseX,\n        y: baseY,\n        text: 'Group: ' + this.id,\n        textAlign: 'left',\n        textBaseline: 'top',\n        fontSize: 16,\n        fontWeight: 'bold',\n        fill: '#4083f7',\n      },\n      container,\n    );\n\n    const gap = 10;\n    const sep = (width + gap) / data.data.length;\n    data.data.forEach(({ name, value }, index) => {\n      this.upsert(\n        `card-item-name-${index}`,\n        'text',\n        {\n          x: baseX + index * sep,\n          y: baseY + 40,\n          text: name,\n          textAlign: 'left',\n          textBaseline: 'top',\n          fontSize: 12,\n          fill: 'gray',\n        },\n        container,\n      );\n      this.upsert(\n        `card-item-value-${index}`,\n        'text',\n        {\n          x: baseX + index * sep,\n          y: baseY + 60,\n          text: value + '%',\n          textAlign: 'left',\n          textBaseline: 'top',\n          fontSize: 24,\n        },\n        container,\n      );\n    });\n  }\n\n  removeCardShape() {\n    Object.entries(this.shapeMap).forEach(([key, shape]) => {\n      if (key.startsWith('card-')) {\n        delete this.shapeMap[key];\n        shape.destroy();\n      }\n    });\n  }\n}\n\nregister(ExtensionCategory.NODE, 'sub-graph', SubGraphNode);\nregister(ExtensionCategory.COMBO, 'card', CardCombo);\n\nconst getSize = (d) => {\n  const data = d.data;\n  if (data.type === 'card') return data.status === 'expanded' ? [200, 100 * data.children.length] : [200, 100];\n  else return [200, 200];\n};\n\nconst graph = new Graph({\n  container: 'container',\n  animation: false,\n  zoom: 0.8,\n  data: {\n    nodes: [\n      {\n        id: '1',\n        combo: 'A',\n        style: { x: 120, y: 70 },\n        data: {\n          data: {\n            nodes: [\n              { id: 'node-1' },\n              { id: 'node-2' },\n              { id: 'node-3' },\n              { id: 'node-4' },\n              { id: 'node-5' },\n              { id: 'node-6' },\n              { id: 'node-7' },\n              { id: 'node-8' },\n            ],\n            edges: [\n              { source: 'node-1', target: 'node-2' },\n              { source: 'node-1', target: 'node-3' },\n              { source: 'node-1', target: 'node-4' },\n              { source: 'node-1', target: 'node-5' },\n              { source: 'node-1', target: 'node-6' },\n              { source: 'node-1', target: 'node-7' },\n              { source: 'node-1', target: 'node-8' },\n            ],\n          },\n        },\n      },\n      {\n        id: '2',\n        combo: 'C',\n        style: { x: 370, y: 70 },\n        data: {\n          data: {\n            nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }],\n            edges: [\n              { source: 'node-1', target: 'node-2' },\n              { source: 'node-1', target: 'node-3' },\n              { source: 'node-1', target: 'node-4' },\n            ],\n          },\n        },\n      },\n      {\n        id: 'node-4',\n        combo: 'D',\n        style: { x: 370, y: 200 },\n        data: {\n          data: {\n            nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }],\n            edges: [\n              { source: 'node-1', target: 'node-2' },\n              { source: 'node-1', target: 'node-3' },\n              { source: 'node-1', target: 'node-4' },\n            ],\n          },\n        },\n      },\n    ],\n    edges: [],\n    combos: [\n      {\n        id: 'root',\n        data: {\n          data: [\n            { name: 'percent', value: 50 },\n            { name: 'percent', value: 45 },\n            { name: 'percent', value: 70 },\n          ],\n        },\n      },\n      {\n        id: 'A',\n        combo: 'root',\n        data: {\n          data: [\n            { name: 'percent', value: 30 },\n            { name: 'percent', value: 90 },\n          ],\n        },\n      },\n      {\n        id: 'B',\n        combo: 'root',\n        style: { collapsed: true },\n        data: {\n          data: [\n            { name: 'percent', value: 60 },\n            { name: 'percent', value: 80 },\n          ],\n        },\n      },\n      {\n        id: 'C',\n        combo: 'B',\n        style: { collapsed: true },\n        data: {\n          data: [{ name: 'percent', value: 60 }],\n        },\n      },\n      {\n        id: 'D',\n        combo: 'B',\n        style: { collapsed: true },\n        data: {\n          data: [{ name: 'percent', value: 80 }],\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'sub-graph',\n    style: {\n      dx: -100,\n      dy: -50,\n      size: getSize,\n    },\n  },\n  combo: {\n    type: 'card',\n    style: {\n      collapsedSize: [200, 100],\n      collapsedMarker: false,\n      radius: 10,\n    },\n  },\n  behaviors: [\n    { type: 'drag-element', enable: (event) => event.shiftKey !== true },\n    'collapse-expand',\n    'zoom-canvas',\n    'drag-canvas',\n  ],\n  plugins: [\n    {\n      type: 'contextmenu',\n      getItems: (event) => {\n        const { targetType, target } = event;\n        if (!['node', 'combo'].includes(targetType)) return [];\n        const id = target.id;\n\n        if (targetType === 'combo') {\n          const data = graph.getComboData(id);\n          if (isCollapsed(data)) {\n            return [{ name: '展开', value: 'expanded' }];\n          } else return [{ name: '收起', value: 'collapsed' }];\n        }\n        return [{ name: '收起', value: 'collapsed' }];\n      },\n      onClick: (value, target, current) => {\n        const id = current.id;\n        const elementType = graph.getElementType(id);\n\n        if (elementType === 'node') {\n          const parent = graph.getParentData(id, 'combo');\n          if (parent) return graph.collapseElement(parent.id, false);\n        }\n\n        if (value === 'expanded') graph.expandElement(id, false);\n        else graph.collapseElement(id, false);\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n\n### Why Do Cats?\n\n**文件路径**: `scene-case/default/demo/why-do-cats.js`\n\n```js\n// ref: https://whydocatsanddogs.com/cats\nimport { Renderer as CanvasRenderer } from '@antv/g-canvas';\nimport { Plugin as PluginRoughCanvasRenderer } from '@antv/g-plugin-rough-canvas-renderer';\nimport { BaseLayout, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { hierarchy, pack } from '@antv/vendor/d3-hierarchy';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `\n@font-face {\nfont-family: 'handwriting';\nsrc: url('https://mass-office.alipay.com/huamei_koqzbu/afts/file/sgUeRbI3d-IAAAAAAAAAABAADnV5AQBr/font.woff2')\n  format('woff2');\n}`;\ndocument.head.appendChild(style);\n\nfunction getColor(id) {\n  const colors = [\n    '#8dd3c7',\n    '#bebada',\n    '#fb8072',\n    '#80b1d3',\n    '#fdb462',\n    '#b3de69',\n    '#fccde5',\n    '#d9d9d9',\n    '#bc80bd',\n    '#ccebc5',\n    '#ffed6f',\n  ];\n  const index = parseInt(id);\n  return colors[index % colors.length];\n}\n\nconst topics = [\n  'cat.like',\n  'cat.hate',\n  'cat.love',\n  'cat.not.like',\n  'cat.afraid_of',\n  'cat.want.to',\n  'cat.scared.of',\n  'cat.not.want_to',\n];\n\nclass BubbleLayout extends BaseLayout {\n  id = 'bubble-layout';\n\n  async execute(model, options) {\n    const { nodes = [] } = model;\n\n    const { width = 0, height = 0 } = { ...this.options, ...options };\n\n    const root = hierarchy({ id: 'root' }, (datum) => {\n      const { id } = datum;\n      if (id === 'root') return nodes.filter((node) => node.depth === 1);\n      else if (datum.depth === 2) return [];\n      else return nodes.filter((node) => node.actualParentId === id);\n    });\n\n    root.sum((d) => (+d.index_value || 0.01) ** 0.5 * 100);\n\n    pack()\n      .size([width, height])\n      .padding((node) => {\n        return node.depth === 0 ? 20 : 2;\n      })(root);\n\n    const result = { nodes: [] };\n\n    root.descendants().forEach((node) => {\n      const {\n        data: { id },\n        x,\n        y,\n        // @ts-expect-error r is exist\n        r,\n      } = node;\n\n      if (node.depth >= 1) result.nodes.push({ id, style: { x, y, size: r * 2 } });\n    });\n\n    return result;\n  }\n}\n\nregister(ExtensionCategory.LAYOUT, 'bubble-layout', BubbleLayout);\n\nfetch('https://assets.antv.antgroup.com/g6/cat-hierarchy.json')\n  .then((res) => res.json())\n  .then((rawData) => {\n    const graphData = rawData.reduce(\n      (acc, row) => {\n        const { id } = row;\n        topics.forEach((topic) => {\n          if (id.startsWith(topic)) {\n            if (id === topic) {\n              acc.nodes.push({ ...row, depth: 1 });\n            } else {\n              acc.nodes.push({ ...row, depth: 2, actualParentId: topic });\n            }\n          }\n        });\n\n        return acc;\n      },\n      { nodes: [] },\n    );\n\n    const graph = new Graph({\n      container: 'container',\n      data: graphData,\n      renderer: (layer) => {\n        const renderer = new CanvasRenderer();\n        if (layer === 'main') {\n          renderer.registerPlugin(new PluginRoughCanvasRenderer());\n        }\n        return renderer;\n      },\n      node: {\n        style: (d) => {\n          const { id, depth, id_num } = d;\n          const color = getColor(id_num);\n\n          if (depth === 1) {\n            return {\n              fill: 'none',\n              stroke: color,\n              labelFontFamily: 'handwriting',\n              labelFontSize: 20,\n              labelText: id.replace('cat.', '').replace(/\\.|_/g, ' '),\n              labelTextTransform: 'capitalize',\n              lineWidth: 1,\n              zIndex: -1,\n            };\n          }\n\n          const {\n            text,\n            style: { size: diameter },\n          } = d;\n\n          return {\n            fill: color,\n            fillOpacity: 0.7,\n            stroke: color,\n            fillStyle: 'cross-hatch',\n            hachureGap: 1.5,\n            iconFontFamily: 'handwriting',\n            iconFontSize: (diameter / text.length) * 2,\n            iconText: diameter > 20 ? text : '',\n            iconFontWeight: 'bold',\n            iconStroke: color,\n            iconLineWidth: 2,\n            lineWidth: (diameter || 20) ** 0.5 / 5,\n          };\n        },\n      },\n      layout: {\n        type: 'bubble-layout',\n        preLayout: true,\n      },\n      plugins: [\n        {\n          type: 'tooltip',\n          getContent: (event, items) => {\n            return `<span style=\"text-transform: capitalize; font-family: handwriting; font-size: 20px;\">${items[0].id.replace(/\\.|_/g, ' ')}</span>`;\n          },\n        },\n      ],\n      behaviors: [{ type: 'drag-canvas', enable: true }, 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Snake Flow Diagram\n\n**文件路径**: `scene-case/default/demo/snake-flow-diagram.js`\n\n```js\nimport { ExtensionCategory, Graph, Polyline, positionOf, register } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0', data: { label: '开始流程', time: '17:00:00' } },\n    { id: '1', data: { label: '流程1', time: '17:00:05' } },\n    { id: '2', data: { label: '流程2', time: '17:00:12' } },\n    { id: '3', data: { label: '流程3', time: '17:00:30' } },\n    { id: '4', data: { label: '流程4', time: '17:02:00' } },\n    { id: '5', data: { label: '流程5', time: '17:02:40' } },\n    { id: '6', data: { label: '流程6', time: '17:05:50' } },\n    { id: '7', data: { label: '流程7', time: '17:10:00' } },\n    { id: '8', data: { label: '流程8', time: '17:11:20' } },\n    { id: '9', data: { label: '流程9', time: '17:15:00' } },\n    { id: '10', data: { label: '流程10', time: '17:30:00' } },\n    { id: '11', data: { label: '流程11' } },\n    { id: '12', data: { label: '流程12' } },\n    { id: '13', data: { label: '流程13' } },\n    { id: '14', data: { label: '流程14' } },\n    { id: '15', data: { label: '流程结束' } },\n  ],\n  edges: [\n    { source: '0', target: '1', data: { done: true } },\n    { source: '1', target: '2', data: { done: true } },\n    { source: '2', target: '3', data: { done: true } },\n    { source: '3', target: '4', data: { done: true } },\n    { source: '4', target: '5', data: { done: true } },\n    { source: '5', target: '6', data: { done: true } },\n    { source: '6', target: '7', data: { done: true } },\n    { source: '7', target: '8', data: { done: true } },\n    { source: '8', target: '9', data: { done: true } },\n    { source: '9', target: '10', data: { done: true } },\n    { source: '10', target: '11', data: { done: false } },\n    { source: '11', target: '12', data: { done: false } },\n    { source: '12', target: '13', data: { done: false } },\n    { source: '13', target: '14', data: { done: false } },\n    { source: '14', target: '15', data: { done: false } },\n  ],\n};\n\nclass SnakePolyline extends Polyline {\n  getPoints(attributes) {\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes, false);\n\n    if (sourcePoint[1] === targetPoint[1]) return [sourcePoint, targetPoint];\n\n    const prevPointId = this.context.model\n      .getRelatedEdgesData(this.sourceNode.id)\n      .filter((edge) => edge.target === this.sourceNode.id)[0]?.source;\n    if (!prevPointId) return [sourcePoint, targetPoint];\n\n    const prevPoint = positionOf(this.context.model.getNodeLikeDatum(prevPointId));\n    const offset = -(prevPoint[0] - sourcePoint[0]) / 4;\n    return [\n      sourcePoint,\n      [sourcePoint[0] + offset, sourcePoint[1]],\n      [targetPoint[0] + offset, targetPoint[1]],\n      targetPoint,\n    ];\n  }\n}\n\nregister(ExtensionCategory.EDGE, 's-polyline', SnakePolyline);\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  background: '#fafafa',\n  autoFit: 'center',\n  node: {\n    style: {\n      fill: (d) => (d.data.time ? '#1783ff' : '#d9d9d9'),\n      lineWidth: 2,\n      size: 8,\n      stroke: (d) => (d.data.time ? 'lightblue' : ''),\n      labelFontWeight: 500,\n      labelOffsetY: 8,\n      labelText: (d) => d.data.label,\n      badge: true,\n      badges: (d) => [\n        {\n          background: false,\n          fill: '#858ca6',\n          fontSize: 10,\n          offsetY: 39,\n          placement: 'bottom',\n          text: d.data.time || '--',\n        },\n      ],\n    },\n  },\n  edge: {\n    type: 's-polyline',\n    style: {\n      lineWidth: 2,\n      stroke: (d) => (d.data.done ? '#1783ff' : '#d9d9d9'),\n    },\n  },\n  layout: {\n    type: 'snake',\n    cols: 6,\n    rowGap: 200,\n    padding: [20, 140, 80],\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n```\n\n---\n\n## scene-case / tree-graph\n\n### Indented Tree\n\n**文件路径**: `scene-case/tree-graph/demo/indented-tree.js`\n\n```js\nimport { Text as GText, Rect } from '@antv/g';\nimport {\n  Badge,\n  BaseBehavior,\n  BaseNode,\n  CommonEvent,\n  ExtensionCategory,\n  Graph,\n  NodeEvent,\n  Polyline,\n  iconfont,\n  idOf,\n  register,\n  subStyleProps,\n  treeToGraphData,\n} from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst rootId = 'Modeling Methods';\n\nconst COLORS = [\n  '#5B8FF9',\n  '#F6BD16',\n  '#5AD8A6',\n  '#945FB9',\n  '#E86452',\n  '#6DC8EC',\n  '#FF99C3',\n  '#1E9493',\n  '#FF9845',\n  '#5D7092',\n];\n\nconst TreeEvent = {\n  COLLAPSE_EXPAND: 'collapse-expand',\n  ADD_CHILD: 'add-child',\n};\n\nlet textShape;\nconst measureText = (text) => {\n  if (!textShape) textShape = new GText({ style: text });\n  textShape.attr(text);\n  return textShape.getBBox().width;\n};\n\nclass IndentedNode extends BaseNode {\n  static defaultStyleProps = {\n    ports: [\n      {\n        key: 'in',\n        placement: 'right-bottom',\n      },\n      {\n        key: 'out',\n        placement: 'left-bottom',\n      },\n    ],\n  };\n\n  constructor(options) {\n    Object.assign(options.style, IndentedNode.defaultStyleProps);\n    super(options);\n  }\n\n  get childrenData() {\n    return this.context.model.getChildrenData(this.id);\n  }\n\n  getKeyStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    const keyStyle = super.getKeyStyle(attributes);\n    return {\n      width,\n      height,\n      ...keyStyle,\n      fill: 'transparent',\n    };\n  }\n\n  drawKeyShape(attributes, container) {\n    const keyStyle = this.getKeyStyle(attributes);\n    return this.upsert('key', 'rect', keyStyle, container);\n  }\n\n  getLabelStyle(attributes) {\n    if (attributes.label === false || !attributes.labelText) return false;\n    return subStyleProps(this.getGraphicStyle(attributes), 'label');\n  }\n\n  drawIconArea(attributes, container) {\n    const [, h] = this.getSize(attributes);\n    const iconAreaStyle = {\n      fill: 'transparent',\n      height: 30,\n      width: 12,\n      x: -6,\n      y: h,\n      zIndex: -1,\n    };\n    this.upsert('icon-area', Rect, iconAreaStyle, container);\n  }\n\n  forwardEvent(target, type, listener) {\n    if (target && !Reflect.has(target, '__bind__')) {\n      Reflect.set(target, '__bind__', true);\n      target.addEventListener(type, listener);\n    }\n  }\n\n  getCountStyle(attributes) {\n    const { collapsed, color } = attributes;\n    if (collapsed) {\n      const [, height] = this.getSize(attributes);\n      return {\n        backgroundFill: color,\n        cursor: 'pointer',\n        fill: '#fff',\n        fontSize: 8,\n        padding: [0, 10],\n        text: `${this.childrenData.length}`,\n        textAlign: 'center',\n        y: height + 8,\n      };\n    }\n\n    return false;\n  }\n\n  drawCountShape(attributes, container) {\n    const countStyle = this.getCountStyle(attributes);\n    const btn = this.upsert('count', Badge, countStyle, container);\n\n    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {\n      event.stopPropagation();\n      attributes.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {\n        id: this.id,\n        collapsed: false,\n      });\n    });\n  }\n\n  isShowCollapse(attributes) {\n    return !attributes.collapsed && this.childrenData.length > 0;\n  }\n\n  getCollapseStyle(attributes) {\n    const { showIcon, color } = attributes;\n    if (!this.isShowCollapse(attributes)) return false;\n    const [, height] = this.getSize(attributes);\n    return {\n      visibility: showIcon ? 'visible' : 'hidden',\n      backgroundFill: color,\n      backgroundHeight: 12,\n      backgroundWidth: 12,\n      cursor: 'pointer',\n      fill: '#fff',\n      fontFamily: 'iconfont',\n      fontSize: 8,\n      text: '\\ue6e4',\n      textAlign: 'center',\n      x: -1, // half of edge line width\n      y: height + 8,\n    };\n  }\n\n  drawCollapseShape(attributes, container) {\n    const iconStyle = this.getCollapseStyle(attributes);\n    const btn = this.upsert('collapse-expand', Badge, iconStyle, container);\n\n    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {\n      event.stopPropagation();\n      attributes.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {\n        id: this.id,\n        collapsed: !attributes.collapsed,\n      });\n    });\n  }\n\n  getAddStyle(attributes) {\n    const { collapsed, showIcon } = attributes;\n    if (collapsed) return false;\n    const [, height] = this.getSize(attributes);\n    const color = '#ddd';\n    const lineWidth = 1;\n\n    return {\n      visibility: showIcon ? 'visible' : 'hidden',\n      backgroundFill: '#fff',\n      backgroundHeight: 12,\n      backgroundLineWidth: lineWidth,\n      backgroundStroke: color,\n      backgroundWidth: 12,\n      cursor: 'pointer',\n      fill: color,\n      fontFamily: 'iconfont',\n      text: '\\ue664',\n      textAlign: 'center',\n      x: -1,\n      y: height + (this.isShowCollapse(attributes) ? 22 : 8),\n    };\n  }\n\n  drawAddShape(attributes, container) {\n    const addStyle = this.getAddStyle(attributes);\n    const btn = this.upsert('add', Badge, addStyle, container);\n\n    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {\n      event.stopPropagation();\n      attributes.context.graph.emit(TreeEvent.ADD_CHILD, { id: this.id });\n    });\n  }\n\n  render(attributes = this.parsedAttributes, container = this) {\n    super.render(attributes, container);\n\n    this.drawCountShape(attributes, container);\n\n    this.drawIconArea(attributes, container);\n    this.drawCollapseShape(attributes, container);\n    this.drawAddShape(attributes, container);\n  }\n}\n\nclass IndentedEdge extends Polyline {\n  getControlPoints(attributes) {\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes, false);\n    const [sx] = sourcePoint;\n    const [, ty] = targetPoint;\n    return [[sx, ty]];\n  }\n}\n\nclass CollapseExpandTree extends BaseBehavior {\n  constructor(context, options) {\n    super(context, options);\n    this.bindEvents();\n  }\n\n  update(options) {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  bindEvents() {\n    const { graph } = this.context;\n\n    graph.on(NodeEvent.POINTER_ENTER, this.showIcon);\n    graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon);\n    graph.on(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);\n    graph.on(TreeEvent.ADD_CHILD, this.addChild);\n  }\n\n  unbindEvents() {\n    const { graph } = this.context;\n\n    graph.off(NodeEvent.POINTER_ENTER, this.showIcon);\n    graph.off(NodeEvent.POINTER_LEAVE, this.hideIcon);\n    graph.off(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);\n    graph.off(TreeEvent.ADD_CHILD, this.addChild);\n  }\n\n  status = 'idle';\n\n  showIcon = (event) => {\n    this.setIcon(event, true);\n  };\n\n  hideIcon = (event) => {\n    this.setIcon(event, false);\n  };\n\n  setIcon = (event, show) => {\n    if (this.status !== 'idle') return;\n    const { target } = event;\n    const id = target.id;\n    const { graph, element } = this.context;\n    graph.updateNodeData([{ id, style: { showIcon: show } }]);\n    element.draw({ animation: false, silence: true });\n  };\n\n  onCollapseExpand = async (event) => {\n    this.status = 'busy';\n    const { id, collapsed } = event;\n    const { graph } = this.context;\n    if (collapsed) await graph.collapseElement(id);\n    else await graph.expandElement(id);\n    this.status = 'idle';\n  };\n\n  addChild(event) {\n    const { onCreateChild = () => ({ id: `${Date.now()}`, style: { labelText: 'new node' } }) } = this.options;\n    const { graph } = this.context;\n    const datum = onCreateChild(event.id);\n    graph.addNodeData([datum]);\n    graph.addEdgeData([{ source: event.id, target: datum.id }]);\n    const parent = graph.getNodeData(event.id);\n    graph.updateNodeData([\n      { id: event.id, children: [...(parent.children || []), datum.id], style: { collapsed: false } },\n    ]);\n    graph.render();\n  }\n}\n\n/**\n * <zh/> 支持拖拽节点到其他节点下作为子节点\n *\n * <en/> Support dragging nodes to other nodes as child nodes\n */\nclass DragBranch extends BaseBehavior {\n  constructor(context, options) {\n    super(context, options);\n    this.bindEvents();\n  }\n\n  update(options) {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  bindEvents() {\n    const { graph } = this.context;\n\n    graph.on(NodeEvent.DRAG_START, this.onDragStart);\n    graph.on(NodeEvent.DRAG, this.onDrag);\n    graph.on(NodeEvent.DRAG_END, this.onDragEnd);\n    graph.on(NodeEvent.DRAG_ENTER, this.onDragEnter);\n    graph.on(NodeEvent.DRAG_LEAVE, this.onDragLeave);\n  }\n\n  unbindEvents() {\n    const { graph } = this.context;\n\n    graph.off(NodeEvent.DRAG_START, this.onDragStart);\n    graph.off(NodeEvent.DRAG, this.onDrag);\n    graph.off(NodeEvent.DRAG_END, this.onDragEnd);\n    graph.off(NodeEvent.DRAG_ENTER, this.onDragEnter);\n    graph.off(NodeEvent.DRAG_LEAVE, this.onDragLeave);\n  }\n\n  enable = true;\n\n  validate(event) {\n    if (this.destroyed) return false;\n    const { enable = (evt) => evt.target.id !== rootId } = this.options;\n    if (typeof enable === 'function') return enable(event);\n    return !!enable;\n  }\n\n  createShadow(target) {\n    const shadowStyle = subStyleProps(this.options, 'shadow');\n    const positionStyle = target.getShape('label').getBBox();\n\n    this.shadow = new Rect({\n      style: {\n        pointerEvents: 'none',\n        fill: '#F3F9FF',\n        fillOpacity: 0.5,\n        stroke: '#1890FF',\n        strokeOpacity: 0.9,\n        lineDash: [5, 5],\n        ...shadowStyle,\n        ...positionStyle,\n      },\n    });\n    this.context.canvas.appendChild(this.shadow);\n  }\n\n  moveShadow(offset) {\n    if (!this.shadow) return;\n    const [dx, dy] = offset;\n    this.shadow.translate(dx, dy);\n  }\n\n  destroyShadow() {\n    this.shadow?.remove();\n    this.shadow = undefined;\n  }\n\n  onDragStart = (event) => {\n    this.enable = this.validate(event);\n    if (!this.enable) return;\n\n    const { target } = event;\n    this.child = target;\n    this.createShadow(target);\n  };\n\n  getDelta(event) {\n    const zoom = this.context.graph.getZoom();\n    return [event.dx / zoom, event.dy / zoom];\n  }\n\n  onDrag = (event) => {\n    if (!this.enable) return;\n\n    const delta = this.getDelta(event);\n    this.moveShadow(delta);\n  };\n\n  onDragEnd = () => {\n    this.destroyShadow();\n    if (this.child === undefined || this.parent === undefined) return;\n\n    const { graph } = this.context;\n    const childId = this.child.id;\n    const parentId = this.parent.id;\n\n    const originalParent = graph.getParentData(childId, 'tree');\n\n    // 前后父节点不应该相同\n    // The previous and current parent nodes should not be the same\n    if (idOf(originalParent) === parentId) return;\n\n    // 新的父节点不应该是当前节点的子节点\n    // The new parent node should not be a child node of the current node\n    const ancestors = graph.getAncestorsData(parentId, 'tree');\n    if (ancestors.some((ancestor) => ancestor.id === childId)) return;\n\n    const edges = graph\n      .getEdgeData()\n      .filter((edge) => edge.target === childId)\n      .map(idOf);\n    graph.removeEdgeData(edges);\n    graph.updateNodeData([\n      { id: idOf(originalParent), children: originalParent?.children?.filter((child) => child !== childId) },\n    ]);\n    const modifiedParent = graph.getNodeData(parentId);\n    graph.updateNodeData([{ id: parentId, children: [...(modifiedParent.children || []), childId] }]);\n    graph.addEdgeData([{ source: parentId, target: childId }]);\n    graph.render();\n  };\n\n  onDragEnter = (event) => {\n    const { graph, element } = this.context;\n    const targetId = event.target.id;\n    if (targetId === this.child?.id || targetId === rootId) {\n      if (targetId === rootId) this.parent = event.target;\n      return;\n    }\n\n    this.parent = event.target;\n    graph.updateNodeData([{ id: targetId, states: ['selected'] }]);\n    element.draw({ animation: false, silence: true });\n  };\n\n  onDragLeave = (event) => {\n    const { graph, element } = this.context;\n    const targetId = event.target.id;\n\n    this.parent = undefined;\n    graph.updateNodeData([{ id: targetId, states: [] }]);\n    element.draw({ animation: false, silence: true });\n  };\n}\n\nregister(ExtensionCategory.NODE, 'indented', IndentedNode);\nregister(ExtensionCategory.EDGE, 'indented', IndentedEdge);\nregister(ExtensionCategory.BEHAVIOR, 'collapse-expand-tree', CollapseExpandTree);\nregister(ExtensionCategory.BEHAVIOR, 'drag-branch', DragBranch);\n\nfetch('https://assets.antv.antgroup.com/g6/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      x: 60,\n      data: treeToGraphData(data),\n      node: {\n        type: 'indented',\n        style: {\n          size: (d) => [measureText({ text: d.id, fontSize: 12 }) + 6, 20],\n          labelBackground: (datum) => datum.id === rootId,\n          labelBackgroundRadius: 0,\n          labelBackgroundFill: '#576286',\n          labelFill: (datum) => (datum.id === rootId ? '#fff' : '#666'),\n          labelText: (d) => d.style?.labelText || d.id,\n          labelTextAlign: (datum) => (datum.id === rootId ? 'center' : 'left'),\n          labelTextBaseline: 'top',\n          color: (datum) => {\n            const depth = graph.getAncestorsData(datum.id, 'tree').length - 1;\n            return COLORS[depth % COLORS.length] || '#576286';\n          },\n        },\n        state: {\n          selected: {\n            lineWidth: 0,\n            labelFill: '#40A8FF',\n            labelBackground: true,\n            labelFontWeight: 'normal',\n            labelBackgroundFill: '#e8f7ff',\n            labelBackgroundRadius: 10,\n          },\n        },\n      },\n      edge: {\n        type: 'indented',\n        style: {\n          radius: 16,\n          lineWidth: 2,\n          sourcePort: 'out',\n          targetPort: 'in',\n          stroke: (datum) => {\n            const depth = graph.getAncestorsData(datum.source, 'tree').length;\n            return COLORS[depth % COLORS.length];\n          },\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'LR',\n        isHorizontal: true,\n        indent: 40,\n        getHeight: () => 20,\n        getVGap: () => 10,\n      },\n      behaviors: [\n        'scroll-canvas',\n        'drag-branch',\n        'collapse-expand-tree',\n        { type: 'click-select', enable: (event) => event.targetType === 'node' && event.target.id !== rootId },\n      ],\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Mind Map\n\n**文件路径**: `scene-case/tree-graph/demo/mindmap.js`\n\n```js\nimport { Rect, Text } from '@antv/g';\nimport {\n  Badge,\n  BaseBehavior,\n  BaseNode,\n  BaseTransform,\n  CommonEvent,\n  CubicHorizontal,\n  ExtensionCategory,\n  Graph,\n  GraphEvent,\n  iconfont,\n  idOf,\n  NodeEvent,\n  positionOf,\n  register,\n  treeToGraphData,\n} from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst RootNodeStyle = {\n  fill: '#EFF0F0',\n  labelFill: '#262626',\n  labelFontSize: 24,\n  labelFontWeight: 600,\n  labelOffsetY: 8,\n  labelPlacement: 'center',\n  ports: [{ placement: 'right' }, { placement: 'left' }],\n  radius: 8,\n};\n\nconst NodeStyle = {\n  fill: 'transparent',\n  labelPlacement: 'center',\n  labelFontSize: 16,\n  ports: [{ placement: 'right-bottom' }, { placement: 'left-bottom' }],\n};\n\nconst TreeEvent = {\n  COLLAPSE_EXPAND: 'collapse-expand',\n  ADD_CHILD: 'add-child',\n};\n\nlet textShape;\nconst measureText = (text) => {\n  if (!textShape) textShape = new Text({ style: text });\n  textShape.attr(text);\n  return textShape.getBBox().width;\n};\n\nconst getNodeWidth = (nodeId, isRoot) => {\n  const padding = isRoot ? 40 : 30;\n  const nodeStyle = isRoot ? RootNodeStyle : NodeStyle;\n  return measureText({ text: nodeId, fontSize: nodeStyle.labelFontSize, fontFamily: 'Gill Sans' }) + padding;\n};\n\nconst getNodeSize = (nodeId, isRoot) => {\n  const width = getNodeWidth(nodeId, isRoot);\n  const height = isRoot ? 48 : 32;\n  return [width, height];\n};\n\nclass MindmapNode extends BaseNode {\n  static defaultStyleProps = {\n    showIcon: false,\n  };\n\n  constructor(options) {\n    Object.assign(options.style, MindmapNode.defaultStyleProps);\n    super(options);\n  }\n\n  get childrenData() {\n    return this.context.model.getChildrenData(this.id);\n  }\n\n  get rootId() {\n    return idOf(this.context.model.getRootsData()[0]);\n  }\n\n  isShowCollapse(attributes) {\n    const { collapsed, showIcon } = attributes;\n    return !collapsed && showIcon && this.childrenData.length > 0;\n  }\n\n  getCollapseStyle(attributes) {\n    const { showIcon, color, direction } = attributes;\n    if (!this.isShowCollapse(attributes)) return false;\n    const [width, height] = this.getSize(attributes);\n\n    return {\n      backgroundFill: color,\n      backgroundHeight: 12,\n      backgroundWidth: 12,\n      cursor: 'pointer',\n      fill: '#fff',\n      fontFamily: 'iconfont',\n      fontSize: 8,\n      text: '\\ue6e4',\n      textAlign: 'center',\n      transform: direction === 'left' ? [['rotate', 90]] : [['rotate', -90]],\n      visibility: showIcon ? 'visible' : 'hidden',\n      x: direction === 'left' ? -6 : width + 6,\n      y: height,\n    };\n  }\n\n  drawCollapseShape(attributes, container) {\n    const iconStyle = this.getCollapseStyle(attributes);\n    const btn = this.upsert('collapse-expand', Badge, iconStyle, container);\n\n    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {\n      event.stopPropagation();\n      this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {\n        id: this.id,\n        collapsed: !attributes.collapsed,\n      });\n    });\n  }\n\n  getCountStyle(attributes) {\n    const { collapsed, color, direction } = attributes;\n    const count = this.context.model.getDescendantsData(this.id).length;\n    if (!collapsed || count === 0) return false;\n    const [width, height] = this.getSize(attributes);\n    return {\n      backgroundFill: color,\n      backgroundHeight: 12,\n      backgroundWidth: 12,\n      cursor: 'pointer',\n      fill: '#fff',\n      fontSize: 8,\n      text: count.toString(),\n      textAlign: 'center',\n      x: direction === 'left' ? -6 : width + 6,\n      y: height,\n    };\n  }\n\n  drawCountShape(attributes, container) {\n    const countStyle = this.getCountStyle(attributes);\n    const btn = this.upsert('count', Badge, countStyle, container);\n\n    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {\n      event.stopPropagation();\n      this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {\n        id: this.id,\n        collapsed: false,\n      });\n    });\n  }\n\n  getAddStyle(attributes) {\n    const { collapsed, showIcon, direction } = attributes;\n    if (collapsed || !showIcon) return false;\n    const [width, height] = this.getSize(attributes);\n    const color = '#ddd';\n\n    const offsetX = this.isShowCollapse(attributes) ? 24 : 12;\n    const isRoot = this.id === this.rootId;\n\n    return {\n      backgroundFill: '#fff',\n      backgroundHeight: 12,\n      backgroundLineWidth: 1,\n      backgroundStroke: color,\n      backgroundWidth: 12,\n      cursor: 'pointer',\n      fill: color,\n      fontFamily: 'iconfont',\n      fontSize: 8,\n      text: '\\ue664',\n      textAlign: 'center',\n      x: isRoot ? width + 12 : direction === 'left' ? -offsetX : width + offsetX,\n      y: isRoot ? height / 2 : height,\n    };\n  }\n\n  getAddBarStyle(attributes) {\n    const { collapsed, showIcon, direction, color = '#1783FF' } = attributes;\n    if (collapsed || !showIcon) return false;\n    const [width, height] = this.getSize(attributes);\n\n    const offsetX = this.isShowCollapse(attributes) ? 12 : 0;\n    const isRoot = this.id === this.rootId;\n\n    const HEIGHT = 2;\n    const WIDTH = 6;\n\n    return {\n      cursor: 'pointer',\n      fill:\n        direction === 'left'\n          ? `linear-gradient(180deg, #fff 20%, ${color})`\n          : `linear-gradient(0deg, #fff 20%, ${color})`,\n      height: HEIGHT,\n      width: WIDTH,\n      x: isRoot ? width : direction === 'left' ? -offsetX - WIDTH : width + offsetX,\n      y: isRoot ? height / 2 - HEIGHT / 2 : height - HEIGHT / 2,\n      zIndex: -1,\n    };\n  }\n\n  drawAddShape(attributes, container) {\n    const addStyle = this.getAddStyle(attributes);\n    const addBarStyle = this.getAddBarStyle(attributes);\n    this.upsert('add-bar', Rect, addBarStyle, container);\n    const btn = this.upsert('add', Badge, addStyle, container);\n\n    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {\n      event.stopPropagation();\n      this.context.graph.emit(TreeEvent.ADD_CHILD, { id: this.id, direction: attributes.direction });\n    });\n  }\n\n  forwardEvent(target, type, listener) {\n    if (target && !Reflect.has(target, '__bind__')) {\n      Reflect.set(target, '__bind__', true);\n      target.addEventListener(type, listener);\n    }\n  }\n\n  getKeyStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    const keyShape = super.getKeyStyle(attributes);\n    return { width, height, ...keyShape };\n  }\n\n  drawKeyShape(attributes, container) {\n    const keyStyle = this.getKeyStyle(attributes);\n    return this.upsert('key', Rect, keyStyle, container);\n  }\n\n  render(attributes = this.parsedAttributes, container = this) {\n    super.render(attributes, container);\n\n    this.drawCollapseShape(attributes, container);\n    this.drawAddShape(attributes, container);\n\n    this.drawCountShape(attributes, container);\n  }\n}\n\nclass MindmapEdge extends CubicHorizontal {\n  get rootId() {\n    return idOf(this.context.model.getRootsData()[0]);\n  }\n\n  getKeyPath(attributes) {\n    const path = super.getKeyPath(attributes);\n    const isRoot = this.targetNode.id === this.rootId;\n    const labelWidth = getNodeWidth(this.targetNode.id, isRoot);\n\n    const [, tp] = this.getEndpoints(attributes);\n    const sign = this.sourceNode.getCenter()[0] < this.targetNode.getCenter()[0] ? 1 : -1;\n    return [...path, ['L', tp[0] + labelWidth * sign, tp[1]]];\n  }\n}\n\nclass CollapseExpandTree extends BaseBehavior {\n  constructor(context, options) {\n    super(context, options);\n    this.bindEvents();\n  }\n\n  update(options) {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  bindEvents() {\n    const { graph } = this.context;\n\n    graph.on(NodeEvent.POINTER_ENTER, this.showIcon);\n    graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon);\n    graph.on(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);\n    graph.on(TreeEvent.ADD_CHILD, this.addChild);\n  }\n\n  unbindEvents() {\n    const { graph } = this.context;\n\n    graph.off(NodeEvent.POINTER_ENTER, this.showIcon);\n    graph.off(NodeEvent.POINTER_LEAVE, this.hideIcon);\n    graph.off(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);\n    graph.off(TreeEvent.ADD_CHILD, this.addChild);\n  }\n\n  status = 'idle';\n\n  showIcon = (event) => {\n    this.setIcon(event, true);\n  };\n\n  hideIcon = (event) => {\n    this.setIcon(event, false);\n  };\n\n  setIcon = (event, show) => {\n    if (this.status !== 'idle') return;\n    const { target } = event;\n    const id = target.id;\n    const { graph, element } = this.context;\n    graph.updateNodeData([{ id, style: { showIcon: show } }]);\n    element.draw({ animation: false, silence: true });\n  };\n\n  onCollapseExpand = async (event) => {\n    this.status = 'busy';\n    const { id, collapsed } = event;\n    const { graph } = this.context;\n    await graph.frontElement(id);\n    if (collapsed) await graph.collapseElement(id);\n    else await graph.expandElement(id);\n    this.status = 'idle';\n  };\n\n  addChild = async (event) => {\n    this.status = 'busy';\n    const {\n      onCreateChild = () => {\n        const currentTime = new Date(Date.now()).toLocaleString();\n        return { id: `New Node in ${currentTime}` };\n      },\n    } = this.options;\n    const { graph } = this.context;\n    const datum = onCreateChild(event.id);\n    const parent = graph.getNodeData(event.id);\n\n    graph.addNodeData([datum]);\n    graph.addEdgeData([{ source: event.id, target: datum.id }]);\n    graph.updateNodeData([\n      {\n        id: event.id,\n        children: [...(parent.children || []), datum.id],\n        style: { collapsed: false, showIcon: false },\n      },\n    ]);\n    await graph.render();\n    await graph.focusElement(datum.id);\n    this.status = 'idle';\n  };\n}\n\nclass AssignColorByBranch extends BaseTransform {\n  static defaultOptions = {\n    colors: [\n      '#1783FF',\n      '#F08F56',\n      '#D580FF',\n      '#00C9C9',\n      '#7863FF',\n      '#DB9D0D',\n      '#60C42D',\n      '#FF80CA',\n      '#2491B3',\n      '#17C76F',\n    ],\n  };\n\n  constructor(context, options) {\n    super(context, Object.assign({}, AssignColorByBranch.defaultOptions, options));\n  }\n\n  beforeDraw(input) {\n    const nodes = this.context.model.getNodeData();\n\n    if (nodes.length === 0) return input;\n\n    let colorIndex = 0;\n    const dfs = (nodeId, color) => {\n      const node = nodes.find((datum) => datum.id == nodeId);\n      if (!node) return;\n\n      node.style ||= {};\n      node.style.color = color || this.options.colors[colorIndex++ % this.options.colors.length];\n      node.children?.forEach((childId) => dfs(childId, node.style?.color));\n    };\n\n    nodes.filter((node) => node.depth === 1).forEach((rootNode) => dfs(rootNode.id));\n\n    return input;\n  }\n}\n\nregister(ExtensionCategory.NODE, 'mindmap', MindmapNode);\nregister(ExtensionCategory.EDGE, 'mindmap', MindmapEdge);\nregister(ExtensionCategory.BEHAVIOR, 'collapse-expand-tree', CollapseExpandTree);\nregister(ExtensionCategory.TRANSFORM, 'assign-color-by-branch', AssignColorByBranch);\n\nconst getNodeSide = (nodeData, parentData) => {\n  if (!parentData) return 'center';\n\n  const nodePositionX = positionOf(nodeData)[0];\n  const parentPositionX = positionOf(parentData)[0];\n  return parentPositionX > nodePositionX ? 'left' : 'right';\n};\n\nfetch('https://assets.antv.antgroup.com/g6/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const rootId = data.id;\n\n    const graph = new Graph({\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      node: {\n        type: 'mindmap',\n        style: function (d) {\n          const direction = getNodeSide(d, this.getParentData(idOf(d), 'tree'));\n          const isRoot = idOf(d) === rootId;\n\n          return {\n            direction,\n            labelText: idOf(d),\n            size: getNodeSize(idOf(d), isRoot),\n            labelFontFamily: 'Gill Sans',\n            // 通过设置节点标签背景来扩大交互区域 | Expand the interaction area by setting the node label background\n            labelBackground: true,\n            labelBackgroundFill: 'transparent',\n            labelPadding: direction === 'left' ? [2, 0, 10, 40] : [2, 40, 10, 0],\n            ...(isRoot ? RootNodeStyle : NodeStyle),\n          };\n        },\n      },\n      edge: {\n        type: 'mindmap',\n        style: {\n          lineWidth: 3,\n          stroke: function (data) {\n            return this.getNodeData(data.target).style.color || '#99ADD1';\n          },\n        },\n      },\n      layout: {\n        type: 'mindmap',\n        direction: 'H',\n        getHeight: () => 30,\n        getWidth: (node) => getNodeWidth(node.id, node.id === rootId),\n        getVGap: () => 6,\n        getHGap: () => 60,\n        animation: false,\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'collapse-expand-tree'],\n      transforms: ['assign-color-by-branch'],\n      animation: false,\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Anti-Procrastination Fishbone Diagram\n\n**文件路径**: `scene-case/tree-graph/demo/anti-procrastination-fishbone.js`\n\n```js\nimport { Text } from '@antv/g';\nimport { BaseTransform, ExtensionCategory, Graph, register, treeToGraphData } from '@antv/g6';\n\nconst data = {\n  id: 'Overcome \\n procrastination',\n  children: [\n    {\n      id: 'Perfectionism',\n      children: [\n        { id: 'Correctly assess the difficulty of things' },\n        { id: 'Complete first, then improve' },\n        { id: 'Just do it' },\n      ],\n    },\n    {\n      id: 'Improve concentration',\n      children: [\n        { id: 'Pomodoro Technique' },\n        { id: 'Limited time, limited quantity, only do one thing at a time' },\n        { id: 'Improve anti-interference ability, reduce interruptions' },\n      ],\n    },\n    {\n      id: 'Set a clear task management process',\n      children: [\n        { id: 'Set priorities for completed tasks' },\n        { id: 'Break down specific executable goals' },\n        { id: 'Collect-sort-sort-execute feedback-summary' },\n      ],\n    },\n    {\n      id: 'Establish positive feedback',\n      children: [{ id: 'Do what you like' }, { id: 'Spiritual motivation' }, { id: 'Material motivation' }],\n    },\n    {\n      id: 'Relax and enjoy',\n      children: [\n        { id: 'Focus on process rather than results' },\n        { id: 'Driven by needs rather than anxiety' },\n        { id: 'Accept and understand' },\n      ],\n    },\n  ],\n};\n\nlet textShape;\nconst measureText = (style) => {\n  if (!textShape) textShape = new Text({ style });\n  textShape.attr(style);\n  return textShape.getBBox().width;\n};\n\nclass AssignColorByBranch extends BaseTransform {\n  static defaultOptions = {\n    colors: [\n      '#1783FF',\n      '#F08F56',\n      '#D580FF',\n      '#00C9C9',\n      '#7863FF',\n      '#DB9D0D',\n      '#60C42D',\n      '#FF80CA',\n      '#2491B3',\n      '#17C76F',\n    ],\n  };\n\n  constructor(context, options) {\n    super(context, Object.assign({}, AssignColorByBranch.defaultOptions, options));\n  }\n\n  beforeDraw(input) {\n    const nodes = this.context.model.getNodeData();\n\n    if (nodes.length === 0) return input;\n\n    let colorIndex = 0;\n    const dfs = (nodeId, color) => {\n      const node = nodes.find((datum) => datum.id == nodeId);\n      if (!node) return;\n\n      node.style ||= {};\n      node.style.color = color || this.options.colors[colorIndex++ % this.options.colors.length];\n      node.children?.forEach((childId) => dfs(childId, node.style?.color));\n    };\n\n    nodes.filter((node) => node.depth === 1).forEach((rootNode) => dfs(rootNode.id));\n\n    return input;\n  }\n}\n\nclass ArrangeEdgeZIndex extends BaseTransform {\n  beforeDraw(input) {\n    const { model } = this.context;\n    const { nodes, edges } = model.getData();\n\n    const oneLevelNodes = nodes.filter((node) => node.depth === 1);\n    const oneLevelNodeIds = oneLevelNodes.map((node) => node.id);\n\n    edges.forEach((edge) => {\n      if (oneLevelNodeIds.includes(edge.target)) {\n        edge.style ||= {};\n        edge.style.zIndex = oneLevelNodes.length - oneLevelNodes.findIndex((node) => node.id === edge.target);\n      }\n    });\n\n    return input;\n  }\n}\n\nregister(ExtensionCategory.TRANSFORM, 'assign-color-by-branch', AssignColorByBranch);\nregister(ExtensionCategory.TRANSFORM, 'arrange-edge-z-index', ArrangeEdgeZIndex);\n\nconst getNodeSize = (id, depth) => {\n  const FONT_FAMILY = 'system-ui, sans-serif';\n  return depth === 0\n    ? [measureText({ text: id, fontSize: 24, fontWeight: 'bold', fontFamily: FONT_FAMILY }) + 80, 70]\n    : depth === 1\n      ? [measureText({ text: id, fontSize: 18, fontFamily: FONT_FAMILY }) + 50, 42]\n      : [2, 30];\n};\n\nconst graph = new Graph({\n  autoFit: 'view',\n  padding: 10,\n  data: treeToGraphData(data),\n  node: {\n    type: 'rect',\n    style: (d) => {\n      const style = {\n        radius: 8,\n        size: getNodeSize(d.id, d.depth),\n        labelText: d.id,\n        labelPlacement: 'right',\n        labelFontFamily: 'Gill Sans',\n      };\n\n      if (d.depth === 0) {\n        Object.assign(style, {\n          fill: '#EFF0F0',\n          labelFill: '#262626',\n          labelFontWeight: 'bold',\n          labelFontSize: 24,\n          labelOffsetY: 4,\n          labelPlacement: 'center',\n          labelLineHeight: 24,\n        });\n      } else if (d.depth === 1) {\n        Object.assign(style, {\n          labelFontSize: 18,\n          labelFill: '#fff',\n          labelFillOpacity: 0.9,\n          labelOffsetY: 5,\n          labelPlacement: 'center',\n          fill: d.style?.color,\n        });\n      } else {\n        Object.assign(style, {\n          fill: 'transparent',\n          labelFontSize: 16,\n          labeFill: '#262626',\n        });\n      }\n      return style;\n    },\n  },\n  edge: {\n    type: 'polyline',\n    style: {\n      lineWidth: 3,\n      stroke: function (data) {\n        return this.getNodeData(data.target).style.color || '#99ADD1';\n      },\n    },\n  },\n  layout: {\n    type: 'fishbone',\n    direction: 'LR',\n    hGap: 40,\n    vGap: 60,\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas'],\n  transforms: ['assign-color-by-branch', 'arrange-edge-z-index'],\n});\n\ngraph.render();\n```\n\n---\n\n### Product Profitability Below Expectations\n\n**文件路径**: `scene-case/tree-graph/demo/product-fishbone.js`\n\n```js\nimport { Text } from '@antv/g';\nimport { BaseTransform, ExtensionCategory, Graph, register, treeToGraphData } from '@antv/g6';\n\nconst data = {\n  id: 'Product Profitability\\nBelow Expectations',\n  children: [\n    {\n      id: 'Problem Description',\n      children: [\n        { id: 'Brand Sales Volume' },\n        { id: 'Market Capacity' },\n        { id: 'Brand Market Share' },\n        { id: 'Total Contribution Margin' },\n      ],\n    },\n    {\n      id: 'Brand Positioning',\n      children: [{ id: 'Packaging' }, { id: 'Brand Name' }, { id: 'Selling Price' }, { id: 'Product Specifications' }],\n    },\n    {\n      id: 'Distribution Channels',\n      children: [{ id: 'Region' }, { id: 'Channel' }, { id: 'Customer Type' }, { id: 'Sales Personnel Coverage' }],\n    },\n    {\n      id: 'Market Awareness',\n      children: [\n        { id: 'Regional Weighting' },\n        { id: 'Media Mix' },\n        { id: 'Advertising Investment' },\n        { id: 'Quality Perception' },\n      ],\n    },\n    {\n      id: 'Trial Purchase',\n      children: [\n        { id: 'In-store Display' },\n        { id: 'Promotion Type' },\n        { id: 'Timing of Promotion' },\n        { id: 'Supply Assurance' },\n      ],\n    },\n    {\n      id: 'Repeat Purchase',\n      children: [\n        { id: 'Consumer Profile' },\n        { id: 'Usage Occasion' },\n        { id: 'Frequency of Use' },\n        { id: 'Returns Due to Product Issues' },\n      ],\n    },\n  ],\n};\n\nlet textShape;\nconst measureText = (style) => {\n  if (!textShape) textShape = new Text({ style });\n  textShape.attr(style);\n  return textShape.getBBox().width;\n};\n\nclass AssignColorByBranch extends BaseTransform {\n  static defaultOptions = {\n    colors: [\n      '#1783FF',\n      '#F08F56',\n      '#D580FF',\n      '#00C9C9',\n      '#7863FF',\n      '#DB9D0D',\n      '#60C42D',\n      '#FF80CA',\n      '#2491B3',\n      '#17C76F',\n    ],\n  };\n\n  constructor(context, options) {\n    super(context, Object.assign({}, AssignColorByBranch.defaultOptions, options));\n  }\n\n  beforeDraw(input) {\n    const nodes = this.context.model.getNodeData();\n\n    if (nodes.length === 0) return input;\n\n    let colorIndex = 0;\n    const dfs = (nodeId, color) => {\n      const node = nodes.find((datum) => datum.id == nodeId);\n      if (!node) return;\n\n      node.style ||= {};\n      node.style.color = color || this.options.colors[colorIndex++ % this.options.colors.length];\n      node.children?.forEach((childId) => dfs(childId, node.style?.color));\n    };\n\n    nodes.filter((node) => node.depth === 1).forEach((rootNode) => dfs(rootNode.id));\n\n    return input;\n  }\n}\n\nclass ArrangeEdgeZIndex extends BaseTransform {\n  beforeDraw(input) {\n    const { model } = this.context;\n    const { nodes, edges } = model.getData();\n\n    const oneLevelNodes = nodes.filter((node) => node.depth === 1);\n    const oneLevelNodeIds = oneLevelNodes.map((node) => node.id);\n\n    edges.forEach((edge) => {\n      if (oneLevelNodeIds.includes(edge.target)) {\n        edge.style ||= {};\n        edge.style.zIndex = oneLevelNodes.length - oneLevelNodes.findIndex((node) => node.id === edge.target);\n      }\n    });\n\n    return input;\n  }\n}\n\nregister(ExtensionCategory.TRANSFORM, 'assign-color-by-branch', AssignColorByBranch);\nregister(ExtensionCategory.TRANSFORM, 'arrange-edge-z-index', ArrangeEdgeZIndex);\n\nconst getNodeSize = (id, depth) => {\n  const FONT_FAMILY = 'system-ui, sans-serif';\n  return depth === 0\n    ? [measureText({ text: id, fontSize: 24, fontWeight: 'bold', fontFamily: FONT_FAMILY }) + 80, 90]\n    : depth === 1\n      ? [measureText({ text: id, fontSize: 18, fontFamily: FONT_FAMILY }) + 50, 42]\n      : [2, 30];\n};\n\nconst graph = new Graph({\n  autoFit: 'view',\n  padding: 30,\n  data: treeToGraphData(data),\n  node: {\n    type: 'rect',\n    style: (d) => {\n      const style = {\n        radius: 8,\n        size: getNodeSize(d.id, d.depth),\n        labelText: d.id,\n        labelPlacement: 'left',\n        labelFontFamily: 'Gill Sans',\n      };\n\n      if (d.depth === 0) {\n        Object.assign(style, {\n          fill: '#EFF0F0',\n          labelFill: '#262626',\n          labelFontWeight: 'bold',\n          labelFontSize: 24,\n          labelOffsetY: 3,\n          labelPlacement: 'center',\n          labelLineHeight: 32,\n        });\n      } else if (d.depth === 1) {\n        Object.assign(style, {\n          labelFontSize: 18,\n          labelFill: '#252525',\n          labelFillOpacity: 0.9,\n          labelOffsetY: 5,\n          labelPlacement: 'center',\n          labelFontWeight: 600,\n          fill: d.style?.color,\n          fillOpacity: 0.6,\n          lineWidth: 2,\n          stroke: '#252525',\n        });\n      } else {\n        Object.assign(style, {\n          fill: 'transparent',\n          labelFontSize: 16,\n          labeFill: '#262626',\n        });\n      }\n      return style;\n    },\n  },\n  edge: {\n    type: 'polyline',\n    style: {\n      lineWidth: 3,\n      stroke: '#252525',\n    },\n  },\n  layout: {\n    type: 'fishbone',\n    direction: 'RL',\n    hGap: 40,\n    vGap: 60,\n    getRibSep: (node) => {\n      console.log(node);\n      return node.depth === 0 ? 0 : -50;\n    },\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas'],\n  transforms: ['assign-color-by-branch', 'arrange-edge-z-index'],\n});\n\ngraph.render();\n```\n\n---\n\n### Radial Dendrogram\n\n**文件路径**: `scene-case/tree-graph/demo/radial-dendrogram.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/flare.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      padding: 50,\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          size: 12,\n          labelText: (d) => d.id,\n          labelBackground: true,\n          labelFontSize: 14,\n          labelFontFamily: 'Gill Sans',\n        },\n      },\n      edge: {\n        type: 'cubic-radial',\n        style: {\n          lineWidth: 3,\n        },\n      },\n      layout: {\n        type: 'dendrogram',\n        radial: true,\n        preLayout: false,\n      },\n      behaviors: [\n        'drag-canvas',\n        'zoom-canvas',\n        'drag-element',\n        {\n          key: 'hover-activate',\n          type: 'hover-activate',\n          degree: 5,\n          direction: 'in',\n          inactiveState: 'inactive',\n        },\n      ],\n      transforms: ['place-radial-labels'],\n      animation: false,\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n### Radial Compact Tree\n\n**文件路径**: `scene-case/tree-graph/demo/radial-compact-tree.js`\n\n```js\nimport { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/flare.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      padding: 50,\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          size: 12,\n          labelText: (d) => d.id,\n          labelBackground: true,\n          labelFontSize: 14,\n          labelFontFamily: 'Gill Sans',\n        },\n      },\n      edge: {\n        type: 'cubic-radial',\n        style: {\n          lineWidth: 3,\n        },\n      },\n      layout: {\n        type: 'compact-box',\n        radial: true,\n        direction: 'RL',\n        getVGap: () => 40,\n        getHGap: () => 80,\n        preLayout: false,\n      },\n      behaviors: [\n        'drag-canvas',\n        'zoom-canvas',\n        'drag-element',\n        {\n          key: 'hover-activate',\n          type: 'hover-activate',\n          degree: 5,\n          direction: 'in',\n          inactiveState: 'inactive',\n        },\n      ],\n      transforms: ['place-radial-labels'],\n      animation: false,\n    });\n\n    graph.render();\n  });\n```\n\n---\n\n## transform / process-parallel-edges\n\n### Bundle Mode\n\n**文件路径**: `transform/process-parallel-edges/demo/bundle.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'A', style: { x: 50, y: 350 } },\n    { id: 'B', style: { x: 250, y: 150 } },\n    { id: 'C', style: { x: 450, y: 350 } },\n  ],\n  edges: [\n    { source: 'A', target: 'C' },\n    { source: 'C', target: 'A' },\n    ...Array.from({ length: 10 }).map((_, i) => ({\n      id: `edge:A-B${i}`,\n      source: 'A',\n      target: 'B',\n      data: {\n        label: `A->B:${i}`,\n      },\n    })),\n    ...Array.from({ length: 5 }).map((_, i) => ({\n      id: `edge:B-C${i}`,\n      source: 'B',\n      target: 'C',\n      data: {\n        label: `B->C:${i}`,\n      },\n    })),\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'center',\n  data,\n  node: {\n    style: {\n      ports: [{ placement: 'center' }],\n      labelText: (d) => d.id,\n    },\n  },\n  edge: {\n    style: {\n      labelText: (d) => d?.data?.label || `${d.source}->${d.target}`,\n    },\n  },\n  behaviors: ['drag-element'],\n  transforms: ['process-parallel-edges'],\n});\n\ngraph.render();\n```\n\n---\n\n### Merge Mode\n\n**文件路径**: `transform/process-parallel-edges/demo/merge.js`\n\n```js\nimport { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'A', style: { x: 50, y: 350 } },\n    { id: 'B', style: { x: 250, y: 150 } },\n    { id: 'C', style: { x: 450, y: 350 } },\n  ],\n  edges: [\n    { source: 'A', target: 'B' },\n    { source: 'B', target: 'A' },\n    { id: 'B-C:1', source: 'B', target: 'C' },\n    { id: 'B-C:2', source: 'B', target: 'C' },\n    { source: 'A', target: 'C' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'center',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  edge: {\n    style: {\n      labelText: (d) => d?.data?.label || `${d.source}->${d.target}`,\n      startArrow: false,\n    },\n  },\n  transforms: [\n    {\n      type: 'process-parallel-edges',\n      mode: 'merge',\n      style: {\n        halo: true,\n        haloOpacity: 0.2,\n        haloStroke: 'red',\n        startArrow: true,\n      },\n    },\n  ],\n});\n\ngraph.render();\n```\n\n---\n"
  },
  {
    "path": "packages/site/examples/feature/default/demo/3d-massive.js",
    "content": "import { CameraSetting, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { Light, Line3D, ObserveCanvas3D, Sphere, ZoomCanvas3D, renderer } from '@antv/g6-extension-3d';\n\nregister(ExtensionCategory.PLUGIN, '3d-light', Light);\nregister(ExtensionCategory.NODE, 'sphere', Sphere);\nregister(ExtensionCategory.EDGE, 'line3d', Line3D);\nregister(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting);\nregister(ExtensionCategory.BEHAVIOR, 'zoom-canvas-3d', ZoomCanvas3D);\nregister(ExtensionCategory.BEHAVIOR, 'observe-canvas-3d', ObserveCanvas3D);\n\nfetch('https://assets.antv.antgroup.com/g6/eva-3d-data.json')\n  .then((res) => res.json())\n  .then(({ nodes, edges }) => {\n    const degree = new Map();\n    edges.forEach(({ source, target }) => {\n      if (!degree.has(source)) degree.set(source, 0);\n      if (!degree.has(target)) degree.set(target, 0);\n      degree.set(source, degree.get(source) + 1);\n      degree.set(target, degree.get(target) + 1);\n    });\n    nodes.forEach((node) => {\n      const { id } = node;\n      Object.assign(node.data, { degree: degree.get(id) ?? 0 });\n      return node;\n    });\n\n    return { nodes, edges };\n  })\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      renderer,\n      data,\n      animation: false,\n      node: {\n        type: 'sphere',\n        style: {\n          materialType: 'phong',\n          size: (d) => 50 + d.data.degree,\n          x: (d) => d.data.x,\n          y: (d) => d.data.y,\n          z: (d) => d.data.z,\n        },\n        palette: {\n          color: 'tableau',\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      edge: {\n        type: 'line3d',\n        style: {\n          lineWidth: 0.4,\n          opacity: 0.4,\n          stroke: '#fff',\n        },\n      },\n      behaviors: ['observe-canvas-3d', 'zoom-canvas-3d'],\n      plugins: [\n        {\n          type: 'camera-setting',\n          projectionMode: 'orthographic',\n          near: 1,\n          far: 10000,\n          fov: 45,\n          aspect: 1,\n        },\n        {\n          type: '3d-light',\n          directional: {\n            direction: [0, 0, 1],\n          },\n        },\n        {\n          type: 'background',\n          background: '#000',\n        },\n      ],\n    });\n\n    graph.draw().then(() => {\n      const camera = graph.getCanvas().getCamera();\n      let frame;\n      let counter = 0;\n      const tick = () => {\n        if (counter < 80) {\n          camera.dolly(4);\n        }\n        camera.rotate(0.4, 0);\n        counter++;\n\n        frame = requestAnimationFrame(tick);\n        if (counter > 160 && frame) {\n          cancelAnimationFrame(frame);\n        }\n      };\n\n      tick();\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/feature/default/demo/lite-solar-system.js",
    "content": "import { ExtensionCategory, Graph, register } from '@antv/g6';\nimport { Light, Sphere, renderer } from '@antv/g6-extension-3d';\n\nregister(ExtensionCategory.PLUGIN, 'light', Light);\nregister(ExtensionCategory.NODE, 'sphere', Sphere);\n\nconst graph = new Graph({\n  renderer,\n  data: {\n    nodes: [\n      {\n        id: 'sum',\n        style: {\n          x: 300,\n          y: 300,\n          radius: 100,\n          texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-mZfQr8LtPUAAAAAAAAAAAAADmJ7AQ/original',\n        },\n      },\n      {\n        id: 'mars',\n        style: {\n          x: 430,\n          y: 300,\n          z: 0,\n          radius: 20,\n          texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*mniGTZktpecAAAAAAAAAAAAADmJ7AQ/original',\n        },\n      },\n      {\n        id: 'earth',\n        style: {\n          x: 500,\n          y: 300,\n          z: 0,\n          radius: 30,\n          texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*cdTdTI2bNl8AAAAAAAAAAAAADmJ7AQ/original',\n        },\n      },\n      {\n        id: 'jupiter',\n        style: {\n          x: 600,\n          y: 300,\n          z: 0,\n          radius: 50,\n          texture: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*t_mQSZYAT70AAAAAAAAAAAAADmJ7AQ/original',\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'sphere',\n    style: {\n      materialShininess: 0,\n      labelText: (d) => d.id,\n      labelFill: '#fff',\n    },\n  },\n  plugins: [\n    {\n      type: '3d-light',\n      directional: {\n        direction: [0, 0, 1],\n      },\n    },\n    {\n      type: 'background',\n      backgroundImage:\n        'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*M_OaRrzIZOEAAAAAAAAAAAAADmJ7AQ/original)',\n      backgroundPosition: 'center',\n    },\n  ],\n});\n\ngraph.draw().then(() => {\n  const element = graph.context.element;\n\n  const sum = element.getElement('sum');\n  const mars = element.getElement('mars');\n  const earth = element.getElement('earth');\n  const jupiter = element.getElement('jupiter');\n\n  const setRotation = (element, speed) => {\n    setInterval(() => {\n      element.rotate(0, -speed, 0);\n    }, 30);\n  };\n  setRotation(sum, 0.1);\n  setRotation(mars, 0.8);\n  setRotation(earth, 1);\n  setRotation(jupiter, 0.5);\n\n  const setRevolution = (element, center, speed) => {\n    setInterval(() => {\n      const [x, y, z] = element.getPosition();\n      const [cx, , cz] = center;\n      const angle = (speed * Math.PI) / 180;\n\n      const newX = (x - cx) * Math.cos(angle) + (z - cz) * Math.sin(angle) + cx;\n      const newZ = -(x - cx) * Math.sin(angle) + (z - cz) * Math.cos(angle) + cz;\n\n      element.setPosition(newX, y, newZ);\n    }, 30);\n  };\n\n  setRevolution(mars, [300, 300, 0], 1.5);\n  setRevolution(earth, [300, 300, 0], 1);\n  setRevolution(jupiter, [300, 300, 0], 0.5);\n});\n"
  },
  {
    "path": "packages/site/examples/feature/default/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"theme.js\",\n      \"title\": {\n        \"zh\": \"主题切换\",\n        \"en\": \"Switch Theme\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*jCYaT7L2E7sAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"lite-solar-system.js\",\n      \"title\": {\n        \"zh\": \"精简太阳系\",\n        \"en\": \"Lite Solar System\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*lEL3TrCLnPsAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"3d-massive.js\",\n      \"title\": {\n        \"zh\": \"3D大数据\",\n        \"en\": \"3D Massive Data\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ZQoEQLKazPIAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"unicorns-investors.js\",\n      \"title\": {\n        \"zh\": \"信息密度\",\n        \"en\": \"Information Density\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*KaoETa-gBDIAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/feature/default/demo/theme.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst themes = {\n  '🌞 Light': {\n    theme: 'light',\n    node: {\n      style: { size: 4 },\n      palette: {\n        type: 'group',\n        field: 'cluster',\n      },\n    },\n    plugins: [{ type: 'background', background: '#fff' }],\n  },\n  '🌚 Dark': {\n    theme: 'dark',\n    node: {\n      style: { size: 4 },\n      palette: {\n        type: 'group',\n        field: 'cluster',\n      },\n    },\n    plugins: [{ type: 'background', background: '#000' }],\n  },\n  '🌎 Blue': {\n    theme: 'light',\n    node: {\n      style: { size: 4 },\n      palette: {\n        type: 'group',\n        field: 'cluster',\n        color: 'blues',\n        invert: true,\n      },\n    },\n    plugins: [{ type: 'background', background: '#f3faff' }],\n  },\n  '🌕 Yellow': {\n    background: '#fcf9f1',\n    theme: 'light',\n    node: {\n      style: { size: 4 },\n      palette: {\n        type: 'group',\n        field: 'cluster',\n        color: ['#ffe7ba', '#ffd591', '#ffc069', '#ffa940', '#fa8c16', '#d46b08', '#ad4e00', '#873800', '#612500'],\n      },\n    },\n    plugins: [{ type: 'background', background: '#fcf9f1' }],\n  },\n};\n\nfetch('https://assets.antv.antgroup.com/g6/20000.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      animation: false,\n      padding: 20,\n      autoFit: 'view',\n      theme: 'light',\n      data,\n      node: {\n        style: { size: 4 },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas'],\n      plugins: [{ type: 'background', background: '#fff' }],\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      gui.add({ theme: '🌞 Light' }, 'theme', Object.keys(themes)).onChange((theme) => {\n        graph.setOptions(themes[theme]);\n        graph.draw();\n      });\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/feature/default/demo/unicorns-investors.js",
    "content": "import { Graph } from '@antv/g6';\n\n/**\n * Inspired by https://graphcommons.com/graphs/be8bc972-5b26-4f5c-837d-a34704f33a9e\n */\nfetch('https://assets.antv.antgroup.com/g6/unicorns-investors.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const size = (node) => Math.max(...node.style.size);\n\n    const graph = new Graph({\n      data,\n      autoFit: 'view',\n      node: {\n        style: {\n          fillOpacity: 1,\n          label: true,\n          labelText: (d) => d.data?.name,\n          labelBackground: true,\n          icon: true,\n          iconText: (d) => (d.data?.type === 'Investor' ? '💰' : '🦄️'),\n          fill: (d) => (d.data?.type === 'Investor' ? '#6495ED' : '#FFA07A'),\n        },\n        state: {\n          inactive: {\n            fillOpacity: 0.3,\n            icon: false,\n            label: false,\n          },\n        },\n      },\n      edge: {\n        style: {\n          label: false,\n          labelText: (d) => d.data?.type,\n          labelBackground: true,\n        },\n        state: {\n          active: {\n            label: true,\n          },\n          inactive: {\n            strokeOpacity: 0,\n          },\n        },\n      },\n      layout: {\n        type: 'd3-force',\n        link: { distance: (edge) => size(edge.source) + size(edge.target) },\n        collide: { radius: (node) => size(node) },\n        manyBody: { strength: (node) => -4 * size(node) },\n        animation: false,\n      },\n      transforms: [\n        {\n          type: 'map-node-size',\n          scale: 'linear',\n          maxSize: 60,\n          minSize: 20,\n          mapLabelSize: [12, 24],\n        },\n      ],\n      behaviors: [\n        'drag-canvas',\n        'zoom-canvas',\n        function () {\n          return {\n            key: 'hover-activate',\n            type: 'hover-activate',\n            enable: (e) => e.targetType === 'node',\n            degree: 1,\n            inactiveState: 'inactive',\n            onHover: (e) => {\n              this.frontElement(e.target.id);\n              e.view.setCursor('pointer');\n            },\n            onHoverEnd: (e) => {\n              e.view.setCursor('default');\n            },\n          };\n        },\n        {\n          type: 'fix-element-size',\n          enable: true,\n        },\n        'auto-adapt-label',\n      ],\n      animation: false,\n    });\n\n    graph.render();\n  });\n\nconst container = document.getElementById('container');\nconst descriptionDiv = document.createElement('div');\ndescriptionDiv.innerHTML = 'Network Map of 🦄 Unicorns and Their 💰Investors - 1086 nodes, 1247 edges';\ncontainer.appendChild(descriptionDiv);\n"
  },
  {
    "path": "packages/site/examples/feature/default/index.en.md",
    "content": "---\ntitle: Theme\n---\n"
  },
  {
    "path": "packages/site/examples/feature/default/index.zh.md",
    "content": "---\ntitle: 特性\n---\n"
  },
  {
    "path": "packages/site/examples/layout/circular/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelFill: '#fff',\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'circular',\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/circular/demo/degree.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelFill: '#fff',\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'circular',\n        ordering: 'degree',\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/circular/demo/division.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'center',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelFill: '#fff',\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'circular',\n        divisions: 5,\n        radius: 200,\n        startAngle: Math.PI / 4,\n        endAngle: Math.PI,\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/circular/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"基本 Circular 布局\",\n        \"en\": \"Basic Circular Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*qJi5Q4qg6W8AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"degree.js\",\n      \"title\": {\n        \"zh\": \"按照节点度数排序\",\n        \"en\": \"Degree Ordered\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*eWecT7lzB0kAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"spiral.js\",\n      \"title\": {\n        \"zh\": \"螺旋线布局\",\n        \"en\": \"Spiral Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*x4HvSr25sHkAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"division.js\",\n      \"title\": {\n        \"zh\": \"分割环形布局\",\n        \"en\": \"Divided Circular Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*9wz2RJH1lxkAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/circular/demo/spiral.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'center',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelFill: '#fff',\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'circular',\n        startRadius: 10,\n        endRadius: 300,\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/circular/index.en.md",
    "content": "---\ntitle: Circular Layout\norder: 4\n---\n"
  },
  {
    "path": "packages/site/examples/layout/circular/index.zh.md",
    "content": "---\ntitle: Circular 环形布局\norder: 4\n---\n"
  },
  {
    "path": "packages/site/examples/layout/combo-layout/demo/combo-combined.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/combo.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'combo-combined',\n        comboPadding: 2,\n      },\n      node: {\n        style: {\n          size: 20,\n          labelText: (d) => d.id,\n        },\n        palette: {\n          type: 'group',\n          field: (d) => d.combo,\n        },\n      },\n      edge: {\n        style: (model) => {\n          const { size, color } = model.data;\n          return {\n            stroke: color || '#99ADD1',\n            lineWidth: size || 1,\n          };\n        },\n      },\n      behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n      autoFit: 'view',\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/combo-layout/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"combo-combined.js\",\n      \"title\": {\n        \"zh\": \"ComboCombined 组合布局\",\n        \"en\": \"Combo Combined Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*zPAzSZ3XxpUAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/combo-layout/index.en.md",
    "content": "---\ntitle: Combo Layout\norder: 2\n---\n"
  },
  {
    "path": "packages/site/examples/layout/combo-layout/index.zh.md",
    "content": "---\ntitle: 组合布局\norder: 2\n---\n"
  },
  {
    "path": "packages/site/examples/layout/compact-box/demo/basic.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\n/**\n * If the node is a leaf node\n * @param {*} d - node data\n * @returns {boolean} - whether the node is a leaf node\n */\nfunction isLeafNode(d) {\n  return !d.children || d.children.length === 0;\n}\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: (d) => (isLeafNode(d) ? 'right' : 'left'),\n          labelBackground: true,\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'compact-box',\n        direction: 'LR',\n        getHeight: function getHeight() {\n          return 32;\n        },\n        getWidth: function getWidth() {\n          return 32;\n        },\n        getVGap: function getVGap() {\n          return 10;\n        },\n        getHGap: function getHGap() {\n          return 100;\n        },\n      },\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/compact-box/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"紧凑树\",\n        \"en\": \"CompactBox Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Hu02Ro6UCegAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"vertical.js\",\n      \"title\": {\n        \"zh\": \"从上向下布局\",\n        \"en\": \"Top to Bottom CompactBox\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YFO6Rb1hM24AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"radial.js\",\n      \"title\": {\n        \"zh\": \"径向布局\",\n        \"en\": \"Radial Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*nwPmQqzJprwAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/compact-box/demo/radial.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelBackground: true,\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'compact-box',\n        radial: true,\n        direction: 'RL',\n        getId: function getId(d) {\n          return d.id;\n        },\n        getHeight: () => {\n          return 26;\n        },\n        getWidth: () => {\n          return 26;\n        },\n        getVGap: () => {\n          return 20;\n        },\n        getHGap: () => {\n          return 40;\n        },\n      },\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/compact-box/demo/vertical.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\n/**\n * If the node is a leaf node\n * @param {*} d - node data\n * @returns {boolean} - whether the node is a leaf node\n */\nfunction isLeafNode(d) {\n  return !d.children || d.children.length === 0;\n}\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n      node: {\n        style: (d) => {\n          const style = {\n            labelText: d.id,\n            labelPlacement: 'right',\n            labelOffsetX: 2,\n            labelBackground: true,\n            ports: [{ placement: 'top' }, { placement: 'bottom' }],\n          };\n          if (isLeafNode(d)) {\n            Object.assign(style, {\n              labelTransform: [\n                ['rotate', 90],\n                ['translate', 18],\n              ],\n              labelBaseline: 'center',\n              labelTextAlign: 'left',\n            });\n          }\n          return style;\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-vertical',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'compact-box',\n        direction: 'TB',\n        getHeight: function getHeight() {\n          return 16;\n        },\n        getWidth: function getWidth() {\n          return 16;\n        },\n        getVGap: function getVGap() {\n          return 80;\n        },\n        getHGap: function getHGap() {\n          return 20;\n        },\n      },\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/compact-box/index.en.md",
    "content": "---\ntitle: CompactBox\norder: 3\n---\n"
  },
  {
    "path": "packages/site/examples/layout/compact-box/index.zh.md",
    "content": "---\ntitle: CompactBox 紧凑树\norder: 3\n---\n"
  },
  {
    "path": "packages/site/examples/layout/concentric/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/basement_prod/8dacf27e-e1bc-4522-b6d3-4b6d9b9ed7df.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      edge: {\n        type: 'line',\n      },\n      layout: {\n        type: 'concentric',\n        nodeSize: 32,\n        maxLevelDiff: 0.5,\n        sortBy: 'degree',\n        preventOverlap: true,\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n      animation: false,\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/concentric/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"Concentric 同心圆布局\",\n        \"en\": \"Concentric Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*KXunQKOLCSAAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/concentric/index.en.md",
    "content": "---\ntitle: Concentric Layout\norder: 10\n---\n"
  },
  {
    "path": "packages/site/examples/layout/concentric/index.zh.md",
    "content": "---\ntitle: Concentric 同心圆布局\norder: 10\n---\n"
  },
  {
    "path": "packages/site/examples/layout/custom/demo/arc.js",
    "content": "import { BaseEdge, BaseLayout, ExtensionCategory, Graph, register } from '@antv/g6';\n\nclass ArcLayout extends BaseLayout {\n  async execute(data, options) {\n    const { nodeSep = 20, nodeSize } = { ...this.options, ...options };\n    const { nodes } = data;\n    return {\n      nodes: nodes.map((node, index) => ({\n        id: node.id,\n        style: {\n          x: index * (nodeSep + nodeSize),\n          y: 0,\n        },\n      })),\n    };\n  }\n}\n\nclass ArcEdge extends BaseEdge {\n  getKeyPath(attributes) {\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes);\n    const [sx, sy] = sourcePoint;\n    const [tx] = targetPoint;\n    const r = Math.abs(tx - sx) / 2;\n\n    return [\n      ['M', sx, sy],\n      ['A', r, r, 0, 0, sx < tx ? 1 : 0, tx, sy],\n    ];\n  }\n}\n\nregister(ExtensionCategory.LAYOUT, 'arc', ArcLayout);\nregister(ExtensionCategory.EDGE, 'arc', ArcEdge);\n\nconst palette = {\n  analytics: 'rgb(158, 1, 66)',\n  data: 'rgb(213, 62, 79)',\n  animate: 'rgb(244, 109, 67)',\n  display: 'rgb(253, 174, 97)',\n  flex: 'rgb(254, 224, 139)',\n  physics: 'rgb(230, 245, 152)',\n  query: 'rgb(171, 221, 164)',\n  scale: 'rgb(102, 194, 165)',\n  util: 'rgb(50, 136, 189)',\n  vis: 'rgb(94, 79, 162)',\n};\n\nfetch('https://gw.alipayobjects.com/os/basement_prod/70cde3be-22e8-4291-98f1-4d5a5b75b62f.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const getCluster = (id) => data.nodes.find((node) => node.id === id).cluster;\n\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      node: {\n        style: {\n          size: 20,\n          fill: (d) => palette[d.cluster],\n          ports: [{ position: 'top' }],\n          labelText: (d) => d.name,\n          labelTextAlign: 'start',\n          labelTextBaseline: 'middle',\n          labelTransform: [['rotate', 90]],\n        },\n      },\n      edge: {\n        type: 'arc',\n        style: {\n          stroke: (d) => `linear-gradient(${palette[getCluster(d.source)]}, ${palette[getCluster(d.source)]})`,\n          strokeOpacity: 0.5,\n        },\n      },\n      layout: {\n        type: 'arc',\n        nodeSize: 20,\n        preLayout: true,\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/custom/demo/bi-graph.js",
    "content": "import { BaseLayout, ExtensionCategory, Graph, register } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0', data: { cluster: 'A' } },\n    { id: '1', data: { cluster: 'A' } },\n    { id: '2', data: { cluster: 'A' } },\n    { id: '3', data: { cluster: 'A' } },\n    { id: '4', data: { cluster: 'A' } },\n    { id: '5', data: { cluster: 'A' } },\n    { id: '6', data: { cluster: 'B' } },\n    { id: '7', data: { cluster: 'B' } },\n    { id: '8', data: { cluster: 'B' } },\n    { id: '9', data: { cluster: 'B' } },\n  ],\n  edges: [\n    { source: '0', target: '6' },\n    { source: '0', target: '7' },\n    { source: '0', target: '9' },\n    { source: '1', target: '6' },\n    { source: '1', target: '9' },\n    { source: '1', target: '7' },\n    { source: '2', target: '8' },\n    { source: '2', target: '9' },\n    { source: '2', target: '6' },\n    { source: '3', target: '8' },\n    { source: '4', target: '6' },\n    { source: '4', target: '7' },\n    { source: '5', target: '9' },\n  ],\n};\n\nclass BiLayout extends BaseLayout {\n  id = 'bi-layout';\n\n  async execute(data, options) {\n    const { sep = 100, nodeSep = 20, nodeSize = 32 } = { ...this.options, ...options };\n\n    const [A, B] = data.nodes.reduce(\n      (acc, curr) => {\n        acc[curr.data.cluster === 'A' ? 0 : 1].push(curr);\n        return acc;\n      },\n      [[], []],\n    );\n\n    return {\n      nodes: [\n        ...A.map((node, i) => ({\n          id: node.id,\n          style: {\n            x: i * (nodeSep + nodeSize),\n            y: 0,\n          },\n        })),\n        ...B.map((node, i) => ({\n          id: node.id,\n          style: {\n            x: i * (nodeSep + nodeSize),\n            y: sep,\n          },\n        })),\n      ],\n    };\n  }\n}\n\nregister(ExtensionCategory.LAYOUT, 'bi', BiLayout);\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  autoFit: 'center',\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n    palette: {\n      type: 'group',\n      field: 'cluster',\n      color: ['#1783FF', '#D580FF'],\n    },\n  },\n  layout: {\n    type: 'bi',\n    sep: 300,\n    nodeSep: 20,\n    nodeSize: 32,\n    preLayout: true,\n  },\n  behaviors: ['drag-canvas', 'drag-element', 'zoom-canvas'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/layout/custom/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"bi-graph.js\",\n      \"title\": {\n        \"zh\": \"二分图布局\",\n        \"en\": \"Bi-graph Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*RfYnRK1rWVgAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"arc.js\",\n      \"title\": {\n        \"zh\": \"弧线图\",\n        \"en\": \"Arc Diagram\"\n      },\n      \"screenshot\": \"https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*_eivQrJXt8sAAAAAAAAAAABkARQnAQ\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/custom/index.en.md",
    "content": "---\ntitle: Custom Layout\norder: 16\n---\n"
  },
  {
    "path": "packages/site/examples/layout/custom/index.zh.md",
    "content": "---\ntitle: 自定义布局\norder: 16\n---\n"
  },
  {
    "path": "packages/site/examples/layout/dagre/demo/antv-dagre-combo.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/dagre-combo.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'center',\n      data,\n      node: {\n        type: 'rect',\n        style: {\n          size: [60, 30],\n          radius: 8,\n          labelText: (d) => d.id,\n          labelBackground: true,\n          ports: [{ placement: 'top' }, { placement: 'bottom' }],\n        },\n        palette: {\n          field: (d) => d.combo,\n        },\n      },\n      edge: {\n        type: 'cubic-vertical',\n        style: {\n          endArrow: true,\n        },\n      },\n      combo: {\n        type: 'rect',\n        style: {\n          radius: 8,\n          labelText: (d) => d.id,\n        },\n      },\n      layout: {\n        type: 'antv-dagre',\n        ranksep: 50,\n        nodesep: 5,\n        sortByCombo: true,\n      },\n      behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/dagre/demo/antv-dagre.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0' },\n    { id: '1' },\n    { id: '2' },\n    { id: '3' },\n    { id: '4' },\n    { id: '5' },\n    { id: '6' },\n    { id: '7' },\n    { id: '8' },\n    { id: '9' },\n  ],\n  edges: [\n    { source: '0', target: '1' },\n    { source: '0', target: '2' },\n    { source: '1', target: '4' },\n    { source: '0', target: '3' },\n    { source: '3', target: '4' },\n    { source: '4', target: '5' },\n    { source: '4', target: '6' },\n    { source: '5', target: '7' },\n    { source: '5', target: '8' },\n    { source: '8', target: '9' },\n    { source: '2', target: '9' },\n    { source: '3', target: '9' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'view',\n  data,\n  layout: {\n    type: 'antv-dagre',\n    nodeSize: [60, 30],\n    nodesep: 60,\n    ranksep: 40,\n    controlPoints: true,\n  },\n  node: {\n    type: 'rect',\n    style: {\n      size: [60, 30],\n      radius: 8,\n      labelText: (d) => d.id,\n      labelBackground: true,\n    },\n  },\n  edge: {\n    type: 'polyline',\n  },\n  behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  const config = { layout: 'default' };\n  const layouts = {\n    default: { type: 'antv-dagre', nodesep: 100, ranksep: 70, controlPoints: true },\n    LR: { type: 'antv-dagre', rankdir: 'LR', align: 'DL', nodesep: 50, ranksep: 70, controlPoints: true },\n    'LR&UL': { type: 'antv-dagre', rankdir: 'LR', align: 'UL', controlPoints: true, nodesep: 50, ranksep: 70 },\n  };\n\n  gui.add(config, 'layout', Object.keys(layouts)).onChange(async (layout) => {\n    graph.setLayout(layouts[layout]);\n    await graph.layout();\n    graph.fitCenter();\n  });\n});\n"
  },
  {
    "path": "packages/site/examples/layout/dagre/demo/dagre.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'kspacey', data: { label: 'Kevin Spacey', width: 144, height: 100 } },\n    { id: 'swilliams', data: { label: 'Saul Williams', width: 160, height: 100 } },\n    { id: 'bpitt', data: { label: 'Brad Pitt', width: 108, height: 100 } },\n    { id: 'hford', data: { label: 'Harrison Ford', width: 168, height: 100 } },\n    { id: 'lwilson', data: { label: 'Luke Wilson', width: 144, height: 100 } },\n    { id: 'kbacon', data: { label: 'Kevin Bacon', width: 121, height: 100 } },\n  ],\n  edges: [\n    { id: 'kspacey->swilliams', source: 'kspacey', target: 'swilliams' },\n    { id: 'swilliams->kbacon', source: 'swilliams', target: 'kbacon' },\n    { id: 'bpitt->kbacon', source: 'bpitt', target: 'kbacon' },\n    { id: 'hford->lwilson', source: 'hford', target: 'lwilson' },\n    { id: 'lwilson->kbacon', source: 'lwilson', target: 'kbacon' },\n  ],\n};\n\nconst graph = new Graph({\n  autoFit: 'center',\n  data,\n  node: {\n    type: 'rect',\n    style: {\n      size: (d) => [d.data.width, d.data.height],\n      radius: 10,\n      iconText: (d) => d.data.label,\n      iconFontSize: 14,\n    },\n    palette: {\n      type: 'group',\n      field: 'label',\n    },\n  },\n  edge: {\n    type: 'polyline',\n    style: {\n      router: {\n        type: 'orth',\n      },\n    },\n  },\n  layout: {\n    type: 'dagre',\n  },\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/layout/dagre/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"antv-dagre.js\",\n      \"title\": {\n        \"zh\": \"Dagre 流程图\",\n        \"en\": \"Dagre Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-PyRTYpI4nwAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"antv-dagre-combo.js\",\n      \"title\": {\n        \"zh\": \"带有 Combo 的流程图\",\n        \"en\": \"Dagre with Combos\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*gQGOTYlN6BMAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"dagre.js\",\n      \"title\": {\n        \"zh\": \"Dagre.js 布局\",\n        \"en\": \"Dagre.js Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*v5mBSopYr_wAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/dagre/index.en.md",
    "content": "---\ntitle: Dagre Layout\norder: 1\n---\n"
  },
  {
    "path": "packages/site/examples/layout/dagre/index.zh.md",
    "content": "---\ntitle: Dagre 布局\norder: 1\n---\n"
  },
  {
    "path": "packages/site/examples/layout/dendrogram/demo/basic.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\n/**\n * If the node is a leaf node\n * @param {*} d - node data\n * @returns {boolean} - whether the node is a leaf node\n */\nfunction isLeafNode(d) {\n  return !d.children || d.children.length === 0;\n}\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: (d) => (isLeafNode(d) ? 'right' : 'left'),\n          labelBackground: true,\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'dendrogram',\n        direction: 'LR', // H / V / LR / RL / TB / BT\n        nodeSep: 36,\n        rankSep: 250,\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/dendrogram/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"生态树\",\n        \"en\": \"Dendrogram Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*7tDPSa-LHbAAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"vertical.js\",\n      \"title\": {\n        \"zh\": \"垂直布局\",\n        \"en\": \"Vertical Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*mp3BRbyBzCEAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"radial.js\",\n      \"title\": {\n        \"zh\": \"径向布局\",\n        \"en\": \"Radial Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*nBIIR5yhTlQAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/dendrogram/demo/radial.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelBackground: true,\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'dendrogram',\n        radial: true,\n        nodeSep: 40,\n        rankSep: 140,\n      },\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/dendrogram/demo/vertical.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\n/**\n * If the node is a leaf node\n * @param {*} d - node data\n * @returns {boolean} - whether the node is a leaf node\n */\nfunction isLeafNode(d) {\n  return !d.children || d.children.length === 0;\n}\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n      node: {\n        style: (d) => {\n          const style = {\n            labelText: d.id,\n            labelPlacement: 'right',\n            labelOffsetX: 2,\n            labelBackground: true,\n            ports: [{ placement: 'top' }, { placement: 'bottom' }],\n          };\n          if (isLeafNode(d)) {\n            Object.assign(style, {\n              labelTransform: [\n                ['rotate', 90],\n                ['translate', 18],\n              ],\n              labelBaseline: 'center',\n              labelTextAlign: 'left',\n            });\n          }\n          return style;\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-vertical',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'dendrogram',\n        direction: 'TB', // H / V / LR / RL / TB / BT\n        nodeSep: 50,\n        rankSep: 120,\n      },\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/dendrogram/index.en.md",
    "content": "---\ntitle: Dendrogram\norder: 4\n---\n"
  },
  {
    "path": "packages/site/examples/layout/dendrogram/index.zh.md",
    "content": "---\ntitle: Dendrogram 生态树\norder: 4\n---\n"
  },
  {
    "path": "packages/site/examples/layout/fishbone/demo/basic.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nconst data = {\n  id: 'Quality',\n  children: [\n    {\n      id: 'Machine',\n      children: [{ id: 'Mill' }, { id: 'Mixer' }, { id: 'Metal Lathe', children: [{ id: 'Milling' }] }],\n    },\n    { id: 'Method' },\n    {\n      id: 'Material',\n      children: [\n        {\n          id: 'Masonite',\n          children: [\n            { id: 'spearMint' },\n            { id: 'pepperMint', children: [{ id: 'test3' }] },\n            { id: 'test1', children: [{ id: 'test4' }] },\n          ],\n        },\n        {\n          id: 'Marscapone',\n          children: [{ id: 'Malty' }, { id: 'Minty' }],\n        },\n        { id: 'Meat', children: [{ id: 'Mutton' }] },\n      ],\n    },\n    {\n      id: 'Man Power',\n      children: [\n        { id: 'Manager' },\n        { id: \"Master's Student\" },\n        { id: 'Magician' },\n        { id: 'Miner' },\n        { id: 'Magister', children: [{ id: 'Malpractice' }] },\n        {\n          id: 'Massage Artist',\n          children: [{ id: 'Masseur' }, { id: 'Masseuse' }],\n        },\n      ],\n    },\n    {\n      id: 'Measurement',\n      children: [{ id: 'Malleability' }],\n    },\n    {\n      id: 'Milieu',\n      children: [{ id: 'Marine' }],\n    },\n  ],\n};\n\nexport const layoutFishbone = async (context) => {\n  const graph = new Graph({\n    ...context,\n    container: 'container',\n    autoFit: 'view',\n    data: treeToGraphData(data),\n    node: {\n      type: 'rect',\n      style: {\n        size: [32, 32],\n        // fill: () => randomColor(),\n        label: false,\n        labelFill: '#262626',\n        labelFontFamily: 'Gill Sans',\n        labelMaxLines: 2,\n        labelMaxWidth: '100%',\n        labelPlacement: 'center',\n        labelText: (d) => d.id,\n        labelWordWrap: true,\n      },\n    },\n    edge: {\n      type: 'polyline',\n      style: {\n        lineWidth: 3,\n      },\n    },\n    layout: {\n      type: 'fishbone',\n      vGap: 48,\n      hGap: 48,\n      direction: 'RL',\n    },\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n    animation: false,\n  });\n\n  await graph.render();\n\n  layoutFishbone.form = (panel) => {\n    const config = {\n      type: 'fishbone',\n      direction: 'RL',\n    };\n\n    return [\n      panel\n        .add(config, 'direction', ['LR', 'RL'])\n        .name('Direction')\n        .onChange((value) => {\n          graph.setLayout((prev) => ({ ...prev, direction: value }));\n          graph.render();\n        }),\n    ];\n  };\n\n  return graph;\n};\n\nlayoutFishbone();\n"
  },
  {
    "path": "packages/site/examples/layout/fishbone/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"Fishbone 鱼骨布局\",\n        \"en\": \"Fishbone Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*CT60SrCib9QAAAAAAAAAAAAAemJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/fishbone/index.en.md",
    "content": "---\ntitle: Fishbone Layout\norder: 11\n---\n"
  },
  {
    "path": "packages/site/examples/layout/fishbone/index.zh.md",
    "content": "---\ntitle: Fishbone 鱼骨布局\norder: 11\n---\n"
  },
  {
    "path": "packages/site/examples/layout/force-directed/demo/3d-force.js",
    "content": "import { CameraSetting, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { D3Force3DLayout, Light, Line3D, ObserveCanvas3D, Sphere, ZoomCanvas3D, renderer } from '@antv/g6-extension-3d';\n\nregister(ExtensionCategory.PLUGIN, '3d-light', Light);\nregister(ExtensionCategory.NODE, 'sphere', Sphere);\nregister(ExtensionCategory.EDGE, 'line3d', Line3D);\nregister(ExtensionCategory.LAYOUT, 'd3-force-3d', D3Force3DLayout);\nregister(ExtensionCategory.PLUGIN, 'camera-setting', CameraSetting);\nregister(ExtensionCategory.BEHAVIOR, 'zoom-canvas-3d', ZoomCanvas3D);\nregister(ExtensionCategory.BEHAVIOR, 'observe-canvas-3d', ObserveCanvas3D);\n\nfetch('https://assets.antv.antgroup.com/g6/d3-force-3d.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      renderer,\n      data,\n      layout: {\n        type: 'd3-force-3d',\n      },\n      node: {\n        type: 'sphere',\n        style: {\n          materialType: 'phong',\n        },\n        palette: {\n          color: 'tableau',\n          type: 'group',\n          field: 'group',\n        },\n      },\n      edge: {\n        type: 'line3d',\n      },\n      behaviors: ['observe-canvas-3d', 'zoom-canvas-3d'],\n      plugins: [\n        {\n          type: '3d-light',\n          directional: {\n            direction: [0, 0, 1],\n          },\n        },\n      ],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/force-directed/demo/atlas2.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      layout: {\n        type: 'force-atlas2',\n        preventOverlap: true,\n        kr: 20,\n        center: [250, 250],\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n      autoResize: true,\n      zoomRange: [0.1, 5],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/force-directed/demo/bubbles.js",
    "content": "import { Graph, NodeEvent } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: '0',\n      label: '0',\n      value: 10,\n      cluster: 'a',\n      description: 'this is node 0, \\nand the value of it is 10',\n    },\n    {\n      id: '1',\n      label: '1',\n      value: 20,\n      cluster: 'b',\n      description: 'this is node 1, \\nand the value of it is 20',\n    },\n    {\n      id: '2',\n      label: '2',\n      value: 5,\n      cluster: 'a',\n      description: 'this is node 2, \\nand the value of it is 5',\n    },\n    {\n      id: '3',\n      label: '3',\n      value: 10,\n      cluster: 'a',\n      description: 'this is node 3, \\nand the value of it is 10',\n    },\n    {\n      id: '4',\n      label: '4',\n      value: 12,\n      cluster: 'c',\n      subCluster: 'sb',\n      description: 'this is node 4, \\nand the value of it is 12',\n    },\n    {\n      id: '5',\n      label: '5',\n      value: 18,\n      cluster: 'c',\n      subCluster: 'sa',\n      description: 'this is node 5, \\nand the value of it is 18',\n    },\n    {\n      id: '6',\n      label: '6',\n      value: 3,\n      cluster: 'c',\n      subCluster: 'sa',\n      description: 'this is node 6, \\nand the value of it is 3',\n    },\n    {\n      id: '7',\n      label: '7',\n      value: 7,\n      cluster: 'b',\n      subCluster: 'sa',\n      description: 'this is node 7, \\nand the value of it is 7',\n    },\n    {\n      id: '8',\n      label: '8',\n      value: 21,\n      cluster: 'd',\n      subCluster: 'sb',\n      description: 'this is node 8, \\nand the value of it is 21',\n    },\n    {\n      id: '9',\n      label: '9',\n      value: 9,\n      cluster: 'd',\n      subCluster: 'sb',\n      description: 'this is node 9, \\nand the value of it is 9',\n    },\n  ],\n  edges: [],\n};\n\nconst oriSize = {};\n\nconst nodes = data.nodes;\n// randomize the node size\nnodes.forEach((node) => {\n  node.size = Math.random() * 30 + 16;\n  oriSize[node.id] = node.size;\n});\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      size: (d) => d.size,\n      labelText: (d) => (d.size === 200 ? d.description : d.id),\n      labelPlacement: 'middle',\n      labelFill: '#fff',\n    },\n    palette: {\n      field: (d) => d.cluster,\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    collide: {\n      radius: (d) => d.size / 2,\n      strength: 0.7,\n    },\n    manyBody: {\n      strength: 30,\n    },\n  },\n  behaviors: ['drag-element-force'],\n});\n\ngraph.on(NodeEvent.CLICK, async (e) => {\n  const nodeId = e.target.id;\n  const data = graph.getNodeData(nodeId);\n  const size = data.size === oriSize[nodeId] ? 200 : oriSize[nodeId];\n  graph.updateNodeData([{ id: nodeId, size }]);\n  await graph.layout();\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/layout/force-directed/demo/collision.js",
    "content": "import { BaseBehavior, ExtensionCategory, Graph, invokeLayoutMethod, register } from '@antv/g6';\n\nfunction getData(width, size = 200) {\n  const k = width / 200;\n  const r = randomUniform(k * 2, k * 8);\n  const n = 4;\n  return {\n    nodes: Array.from({ length: size }, (_, i) => ({ id: `${i}`, data: { r: r(), group: i && (i % n) + 1 } })),\n    edges: [],\n  };\n}\n\nfunction randomUniform(min, max) {\n  min = min == null ? 0 : +min;\n  max = max == null ? 1 : +max;\n  if (arguments.length === 1) (max = min), (min = 0);\n  else max -= min;\n  return function () {\n    return Math.random() * max + min;\n  };\n}\n\nclass CollisionElement extends BaseBehavior {\n  constructor(context) {\n    super(context, {});\n    this.onPointerMove = this.onPointerMove.bind(this);\n    this.bindEvents();\n  }\n\n  bindEvents() {\n    this.context.graph.on('pointermove', this.onPointerMove);\n  }\n\n  onPointerMove(event) {\n    const pos = this.context.graph.getCanvasByClient([event.client.x, event.client.y]);\n    const layoutInstance = this.context.layout\n      ?.getLayoutInstance()\n      .find((layout) => ['d3-force', 'd3-force-3d'].includes(layout?.id));\n\n    if (layoutInstance) {\n      invokeLayoutMethod(layoutInstance, 'setFixedPosition', '0', [...pos]);\n    }\n  }\n}\n\nregister(ExtensionCategory.BEHAVIOR, 'collision-element', CollisionElement);\n\nconst container = document.getElementById('container');\nconst width = container.scrollWidth;\n\nconst graph = new Graph({\n  container,\n  data: getData(width),\n  layout: {\n    type: 'd3-force',\n    alphaTarget: 0.3,\n    velocityDecay: 0.1,\n    x: {\n      strength: 0.01,\n    },\n    y: {\n      strength: 0.01,\n    },\n    collide: {\n      radius: (d) => d.data.r,\n      iterations: 3,\n    },\n    manyBody: {\n      strength: (d, i) => (i ? 0 : (-width * 2) / 3),\n    },\n    link: false,\n  },\n  node: {\n    style: {\n      size: (d) => (d.id === '0' ? 0 : d.data.r * 2),\n    },\n    palette: {\n      color: 'tableau',\n      type: 'group',\n      field: (d) => d.data.group,\n    },\n  },\n  behaviors: ['collision-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/layout/force-directed/demo/d3-force.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          ports: [],\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      layout: {\n        type: 'd3-force',\n        collide: {\n          strength: 0.5,\n        },\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/force-directed/demo/drag-fixed.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(10).fill(0).map((_, i) => ({ id: `${i}`, label: `${i}` })),\n  edges: [\n    { source: '0', target: '1' },\n    { source: '0', target: '2' },\n    { source: '0', target: '3' },\n    { source: '0', target: '4' },\n    { source: '0', target: '5' },\n    { source: '0', target: '7' },\n    { source: '0', target: '8' },\n    { source: '0', target: '9' },\n    { source: '2', target: '3' },\n    { source: '4', target: '5' },\n    { source: '4', target: '6' },\n    { source: '5', target: '6' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.label,\n      labelPlacement: 'middle',\n      labelFill: '#fff',\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    link: {\n      distance: 100,\n      strength: 2,\n    },\n    collide: {\n      radius: 40,\n    },\n  },\n  behaviors: [\n    {\n      type: 'drag-element-force',\n      fixed: true,\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/layout/force-directed/demo/force.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          ports: [],\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      layout: {\n        type: 'force',\n        linkDistance: 50,\n        clustering: true,\n        nodeClusterBy: 'cluster',\n        clusterNodeStrength: 70,\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/force-directed/demo/functional-params.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node0', size: 50 },\n    { id: 'node1', size: 30 },\n    { id: 'node2', size: 30 },\n    { id: 'node3', size: 30 },\n    { id: 'node4', size: 30, isLeaf: true },\n    { id: 'node5', size: 30, isLeaf: true },\n    { id: 'node6', size: 15, isLeaf: true },\n    { id: 'node7', size: 15, isLeaf: true },\n    { id: 'node8', size: 15, isLeaf: true },\n    { id: 'node9', size: 15, isLeaf: true },\n    { id: 'node10', size: 15, isLeaf: true },\n    { id: 'node11', size: 15, isLeaf: true },\n    { id: 'node12', size: 15, isLeaf: true },\n    { id: 'node13', size: 15, isLeaf: true },\n    { id: 'node14', size: 15, isLeaf: true },\n    { id: 'node15', size: 15, isLeaf: true },\n    { id: 'node16', size: 15, isLeaf: true },\n  ],\n  edges: [\n    { source: 'node0', target: 'node1' },\n    { source: 'node0', target: 'node2' },\n    { source: 'node0', target: 'node3' },\n    { source: 'node0', target: 'node4' },\n    { source: 'node0', target: 'node5' },\n    { source: 'node1', target: 'node6' },\n    { source: 'node1', target: 'node7' },\n    { source: 'node2', target: 'node8' },\n    { source: 'node2', target: 'node9' },\n    { source: 'node2', target: 'node10' },\n    { source: 'node2', target: 'node11' },\n    { source: 'node2', target: 'node12' },\n    { source: 'node2', target: 'node13' },\n    { source: 'node3', target: 'node14' },\n    { source: 'node3', target: 'node15' },\n    { source: 'node3', target: 'node16' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      size: (d) => d.size,\n    },\n  },\n  layout: {\n    type: 'd3-force',\n    link: {\n      distance: (d) => {\n        if (d.source.id === 'node0') {\n          return 100;\n        }\n        return 30;\n      },\n      strength: (d) => {\n        if (d.source.id === 'node1' || d.source.id === 'node2' || d.source.id === 'node3') {\n          return 0.7;\n        }\n        return 0.1;\n      },\n    },\n    manyBody: {\n      strength: (d) => {\n        if (d.isLeaf) {\n          return -50;\n        }\n        return -10;\n      },\n    },\n  },\n  behaviors: ['drag-element-force'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/layout/force-directed/demo/mesh.js",
    "content": "import { Graph } from '@antv/g6';\n\nfunction getData(size = 10) {\n  const nodes = Array.from({ length: size * size }, (_, i) => ({ id: `${i}` }));\n  const edges = [];\n  for (let y = 0; y < size; ++y) {\n    for (let x = 0; x < size; ++x) {\n      if (y > 0) edges.push({ source: `${(y - 1) * size + x}`, target: `${y * size + x}` });\n      if (x > 0) edges.push({ source: `${y * size + (x - 1)}`, target: `${y * size + x}` });\n    }\n  }\n  return { nodes, edges };\n}\n\nconst graph = new Graph({\n  data: getData(),\n  layout: {\n    type: 'd3-force',\n    manyBody: {\n      strength: -30,\n    },\n    link: {\n      strength: 1,\n      distance: 20,\n      iterations: 10,\n    },\n  },\n  node: {\n    style: {\n      size: 10,\n      fill: '#000',\n    },\n  },\n  edge: {\n    style: {\n      stroke: '#000',\n    },\n  },\n  behaviors: [{ type: 'drag-element-force' }, 'zoom-canvas'],\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  gui.add({ msg: 'Try to drag nodes' }, 'msg').name('Tips').disable();\n});\n"
  },
  {
    "path": "packages/site/examples/layout/force-directed/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"force.js\",\n      \"title\": {\n        \"zh\": \"Force 聚类\",\n        \"en\": \"Clustering Force Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*sA14SZo9BBMAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"atlas2.js\",\n      \"title\": {\n        \"zh\": \"Force Atlas 2\",\n        \"en\": \"Force-Atlas 2 Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-HgiS6CyuuEAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"d3-force.js\",\n      \"title\": {\n        \"zh\": \"D3 力导向布局\",\n        \"en\": \"D3 Force Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-_sFS5IRGGcAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"functional-params.js\",\n      \"title\": {\n        \"zh\": \"定制不同节点的参数\",\n        \"en\": \"Customize Layout Parameters For Different Nodes\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*78tVRrG7zU8AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"prevent-overlap.js\",\n      \"title\": {\n        \"zh\": \"力导向布局防止节点重叠\",\n        \"en\": \"Prevent Overlap in d3-force Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*J802QozikwsAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"drag-fixed.js\",\n      \"title\": {\n        \"zh\": \"固定被做拽节点\",\n        \"en\": \"Drag Fixed Nodes\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*QlS5T6Wy2TUAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"bubbles.js\",\n      \"title\": {\n        \"zh\": \"力导向气泡图\",\n        \"en\": \"Force Directed Bubble Chart\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*iMR5QqrGp0cAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"3d-force.js\",\n      \"title\": {\n        \"zh\": \"3D 力导向布局\",\n        \"en\": \"3D Force Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*4mbSTJLOXkgAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"mesh.js\",\n      \"title\": {\n        \"zh\": \"网格效果\",\n        \"en\": \"Mesh Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ykD5QqSEgeEAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"collision.js\",\n      \"title\": {\n        \"zh\": \"弹性碰撞效果\",\n        \"en\": \"Collision Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*yzv_To2Wm_EAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/force-directed/demo/prevent-overlap.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const nodes = data.nodes;\n    // randomize the node size\n    nodes.forEach((node) => {\n      node.size = Math.random() * 30 + 5;\n    });\n\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'center',\n      data,\n      node: {\n        style: {\n          size: (d) => d.size,\n          lineWidth: 1,\n        },\n      },\n      layout: {\n        type: 'd3-force',\n        collide: {\n          // Prevent nodes from overlapping by specifying a collision radius for each node.\n          radius: (d) => d.size / 2,\n        },\n      },\n      behaviors: ['drag-element-force'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/force-directed/index.en.md",
    "content": "---\ntitle: Force-directed Layout\norder: 0\n---\n"
  },
  {
    "path": "packages/site/examples/layout/force-directed/index.zh.md",
    "content": "---\ntitle: 力导向图布局\norder: 0\n---\n"
  },
  {
    "path": "packages/site/examples/layout/fruchterman/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'fruchterman',\n        gravity: 5,\n        speed: 5,\n        animation: true,\n      },\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelPlacement: 'center',\n          labelText: (d) => d.id,\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/fruchterman/demo/cluster.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'fruchterman',\n        gravity: 5,\n        speed: 5,\n        clustering: true,\n        nodeClusterBy: 'cluster',\n        clusterGravity: 16,\n      },\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelPlacement: 'center',\n          labelText: (d) => d.id,\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      edge: {\n        style: {\n          endArrow: true,\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/fruchterman/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"基本 Fruchterman 布局\",\n        \"en\": \"Basic Fruchterman Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*1AvnTorIogUAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"cluster.js\",\n      \"title\": {\n        \"zh\": \"Fruchterman 聚类布局\",\n        \"en\": \"Fruchterman Cluster Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*1i4ZQaI6Iu4AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"run-in-web-worker.js\",\n      \"title\": {\n        \"zh\": \"使用 Web-worker\",\n        \"en\": \"Run in Web-worker\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*3gn9TZ3oUoIAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"run-in-gpu.js\",\n      \"title\": {\n        \"zh\": \"使用 GPU 加速\",\n        \"en\": \"Run in GPU\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*SmXBQ6fv8PoAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/fruchterman/demo/run-in-gpu.js",
    "content": "import { Graph, register } from '@antv/g6';\nimport { FruchtermanLayout } from '@antv/layout-gpu';\n\nregister('layout', 'fruchterman-gpu', FruchtermanLayout);\n\nfetch('https://gw.alipayobjects.com/os/basement_prod/7bacd7d1-4119-4ac1-8be3-4c4b9bcbc25f.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      node: {\n        style: {\n          size: 5,\n        },\n      },\n      edge: {\n        style: {\n          startArrow: true,\n        },\n      },\n      layout: {\n        type: 'fruchterman-gpu',\n        speed: 20,\n        gravity: 1,\n        maxIteration: 10000,\n        workerEnabled: true,\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/fruchterman/demo/run-in-web-worker.js",
    "content": "import { Graph, GraphEvent } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      layout: {\n        type: 'fruchterman',\n        speed: 20,\n        gravity: 10,\n        maxIteration: 10000,\n        workerEnabled: true,\n      },\n      node: {\n        style: {\n          size: 20,\n          labelText: (d) => d.id,\n          labelPlacement: 'center',\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      edge: {\n        style: {\n          stroke: '#ddd',\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      const msg = gui.add({ msg: 'Running...' }, 'msg').name('Tips').disable();\n      graph.on(GraphEvent.AFTER_LAYOUT, () => {\n        msg.setValue('Layout Done!');\n      });\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/fruchterman/index.en.md",
    "content": "---\ntitle: Fruchterman Layout\norder: 13\n---\n"
  },
  {
    "path": "packages/site/examples/layout/fruchterman/index.zh.md",
    "content": "---\ntitle: Fruchterman 图布局\norder: 13\n---\n"
  },
  {
    "path": "packages/site/examples/layout/grid/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelBackground: true,\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      layout: {\n        type: 'grid',\n        sortBy: 'id',\n        nodeSize: 32,\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      gui.add({ sortBy: 'id' }, 'sortBy', ['id', 'cluster']).onChange((type) => {\n        graph.setLayout({\n          type: 'grid',\n          sortBy: type,\n        });\n        graph.layout();\n      });\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/grid/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"Grid 网格布局\",\n        \"en\": \"Grid Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*8RYVTrENVCcAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/grid/index.en.md",
    "content": "---\ntitle: Grid\norder: 11\n---\n"
  },
  {
    "path": "packages/site/examples/layout/grid/index.zh.md",
    "content": "---\ntitle: Grid 网格布局\norder: 11\n---\n"
  },
  {
    "path": "packages/site/examples/layout/indented/demo/auto-side.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nconst getNodeSide = (graph, datum) => {\n  const parentData = graph.getParentData(datum.id, 'tree');\n  if (!parentData) return 'center';\n  return datum.style.x > parentData.style.x ? 'right' : 'left';\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data: treeToGraphData(data),\n      autoFit: 'view',\n      node: {\n        style: function (d) {\n          const side = getNodeSide(this, d);\n          return {\n            labelText: d.id,\n            labelPlacement: side === 'center' ? 'bottom' : side,\n            labelBackground: true,\n            ports:\n              side === 'center'\n                ? [{ placement: 'bottom' }]\n                : side === 'right'\n                  ? [{ placement: 'bottom' }, { placement: 'left' }]\n                  : [{ placement: 'bottom' }, { placement: 'right' }],\n          };\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'polyline',\n        style: {\n          radius: 4,\n          router: {\n            type: 'orth',\n          },\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'H',\n        indent: 80,\n        preLayout: false,\n        getHeight: () => 16,\n        getWidth: () => 32,\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/indented/demo/custom-side.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nconst getNodeSide = (graph, datum) => {\n  const parentData = graph.getParentData(datum.id, 'tree');\n  if (!parentData) return 'center';\n  return datum.style.x > parentData.style.x ? 'right' : 'left';\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data: treeToGraphData(data),\n      autoFit: 'view',\n      node: {\n        style: function (d) {\n          const side = getNodeSide(this, d);\n          return {\n            labelText: d.id,\n            labelPlacement: side === 'center' ? 'bottom' : side,\n            labelBackground: true,\n            ports:\n              side === 'center'\n                ? [{ placement: 'bottom' }]\n                : side === 'right'\n                  ? [{ placement: 'bottom' }, { placement: 'left' }]\n                  : [{ placement: 'bottom' }, { placement: 'right' }],\n          };\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'polyline',\n        style: {\n          radius: 4,\n          router: {\n            type: 'orth',\n          },\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'H',\n        indent: 80,\n        preLayout: false,\n        getHeight: () => 16,\n        getWidth: () => 32,\n        getSide: (d) => {\n          if (d.id === 'Regression' || d.id === 'Classification') return 'left';\n          return 'right';\n        },\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/indented/demo/left-side.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data: treeToGraphData(data),\n      autoFit: 'view',\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: 'left',\n          labelBackground: true,\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'polyline',\n        style: {\n          radius: 4,\n          router: {\n            type: 'orth',\n          },\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'RL',\n        indent: 80,\n        getHeight: () => 16,\n        getWidth: () => 32,\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/indented/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"auto-side.js\",\n      \"title\": {\n        \"zh\": \"子节点自动分布\",\n        \"en\": \"Two Side Indented Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Kc63QoxgLNYAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"right-side.js\",\n      \"title\": {\n        \"zh\": \"子节点右侧分布\",\n        \"en\": \"Right Side Indented Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*3PioQ4TAMx8AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"left-side.js\",\n      \"title\": {\n        \"zh\": \"子节点左侧分布\",\n        \"en\": \"Left Side Indented Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*o6uzQ5nmXJkAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"custom-side.js\",\n      \"title\": {\n        \"zh\": \"自定义子节点分布\",\n        \"en\": \"Custom Side Indented Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Kc63QoxgLNYAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"no-drop-cap.js\",\n      \"title\": {\n        \"zh\": \"首子节点不换行\",\n        \"en\": \"No Drop Cap Indented Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*bC-pRrO7srwAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/indented/demo/no-drop-cap.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data: treeToGraphData(data),\n      autoFit: 'view',\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: 'right',\n          labelBackground: true,\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'polyline',\n        style: {\n          radius: 4,\n          router: {\n            type: 'orth',\n          },\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'LR',\n        indent: 80,\n        getHeight: () => 16,\n        getWidth: () => 32,\n        dropCap: false,\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/indented/demo/right-side.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data: treeToGraphData(data),\n      autoFit: 'view',\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: 'right',\n          labelBackground: true,\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'polyline',\n        style: {\n          radius: 4,\n          router: {\n            type: 'orth',\n          },\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'LR',\n        indent: 80,\n        getHeight: () => 16,\n        getWidth: () => 32,\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/indented/index.en.md",
    "content": "---\ntitle: Indented\norder: 6\n---\n"
  },
  {
    "path": "packages/site/examples/layout/indented/index.zh.md",
    "content": "---\ntitle: Indented 缩进树\norder: 6\n---\n"
  },
  {
    "path": "packages/site/examples/layout/mds/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/cluster.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      padding: 20,\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelText: (d) => d.id,\n          labelPlacement: 'center',\n        },\n      },\n      layout: {\n        type: 'mds',\n        nodeSize: 32,\n        linkDistance: 100,\n      },\n      behaviors: ['drag-element', 'drag-canvas', 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/mds/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"基本 MDS 布局\",\n        \"en\": \"Basic MDS Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*myM6T6R_d34AAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/mds/index.en.md",
    "content": "---\ntitle: MDS Layout\norder: 12\n---\n"
  },
  {
    "path": "packages/site/examples/layout/mds/index.zh.md",
    "content": "---\ntitle: MDS 布局\norder: 12\n---\n"
  },
  {
    "path": "packages/site/examples/layout/mechanism/demo/change-data.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst fetchData = async (type) => {\n  if (type === 'large') {\n    const data = await fetch('https://assets.antv.antgroup.com/g6/cluster.json').then((res) => res.json());\n    return data;\n  }\n  return {\n    nodes: [{ id: 'b0' }, { id: 'b1' }, { id: 'b2' }, { id: 'b3' }, { id: 'b4' }, { id: 'b5' }],\n    edges: [\n      { source: 'b0', target: 'b1' },\n      { source: 'b0', target: 'b2' },\n      { source: 'b0', target: 'b3' },\n      { source: 'b0', target: 'b4' },\n      { source: 'b0', target: 'b5' },\n    ],\n  };\n};\n\nfetchData('small').then((data) => {\n  const graph = new Graph({\n    container: 'container',\n    behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n    layout: {\n      type: 'force',\n      animated: true,\n      linkDistance: 100,\n      preventOverlap: true,\n    },\n    data,\n  });\n\n  graph.render();\n\n  window.addPanel((gui) => {\n    gui.add({ type: 'small' }, 'type', ['small', 'large']).onChange((type) => {\n      fetchData(type).then((data) => {\n        graph.setData(data);\n        graph.render();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/site/examples/layout/mechanism/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"switch.js\",\n      \"title\": {\n        \"zh\": \"布局切换\",\n        \"en\": \"Switch Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*aGiCRrsJeCUAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"change-data.js\",\n      \"title\": {\n        \"zh\": \"数据动态切换\",\n        \"en\": \"Change Data\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Cq8SRL2NZtYAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/mechanism/demo/switch.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      layout: {\n        type: 'circular',\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-node'],\n      data,\n    });\n\n    graph.render();\n\n    window.addPanel((gui) => {\n      gui\n        .add({ layout: 'circular' }, 'layout', ['circular', 'grid', 'force', 'radial', 'concentric', 'mds'])\n        .onChange((layout) => {\n          const options = {\n            circular: { type: 'circular' },\n            grid: { type: 'grid' },\n            force: { type: 'force', preventOverlap: true },\n            radial: { type: 'radial', preventOverlap: true },\n            concentric: { type: 'concentric' },\n            mds: { type: 'mds', linkDistance: 100 },\n          };\n          graph.stopLayout();\n          graph.layout(options[layout]);\n        });\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/mechanism/index.en.md",
    "content": "---\ntitle: Layout Mechanism\norder: 15\n---\n"
  },
  {
    "path": "packages/site/examples/layout/mechanism/index.zh.md",
    "content": "---\ntitle: 布局机制\norder: 15\n---\n"
  },
  {
    "path": "packages/site/examples/layout/mindmap/demo/auto-side.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nconst getNodeSide = (graph, datum) => {\n  const parentData = graph.getParentData(datum.id, 'tree');\n  if (!parentData) return 'center';\n  return datum.style.x > parentData.style.x ? 'right' : 'left';\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelBackground: true,\n          labelPlacement: function (d) {\n            const side = getNodeSide(this, d);\n            return side === 'center' ? 'right' : side;\n          },\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'mindmap',\n        direction: 'H',\n        getHeight: () => 32,\n        getWidth: () => 32,\n        getVGap: () => 4,\n        getHGap: () => 64,\n      },\n      behaviors: ['collapse-expand', 'drag-canvas', 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/mindmap/demo/custom-side.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nconst getNodeSide = (graph, datum) => {\n  const parentData = graph.getParentData(datum.id, 'tree');\n  if (!parentData) return 'center';\n  return datum.style.x > parentData.style.x ? 'right' : 'left';\n};\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelBackground: true,\n          labelPlacement: function (d) {\n            const side = getNodeSide(this, d);\n            return side === 'center' ? 'right' : side;\n          },\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'mindmap',\n        direction: 'H',\n        preLayout: false,\n        getHeight: () => 32,\n        getWidth: () => 32,\n        getVGap: () => 4,\n        getHGap: () => 64,\n        getSide: (d) => {\n          if (d.id === 'Classification') {\n            return 'left';\n          }\n          return 'right';\n        },\n      },\n      behaviors: ['collapse-expand', 'drag-canvas', 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/mindmap/demo/left-side.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: 'left',\n          labelBackground: true,\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'mindmap',\n        direction: 'RL',\n        getHeight: () => 32,\n        getWidth: () => 32,\n        getVGap: () => 4,\n        getHGap: () => 100,\n      },\n      behaviors: ['collapse-expand', 'drag-canvas', 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/mindmap/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"auto-side.js\",\n      \"title\": {\n        \"zh\": \"子节点自动分布\",\n        \"en\": \"Auto Mindmap Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*im2UR5hT3WcAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"right-side.js\",\n      \"title\": {\n        \"zh\": \"子节点右侧分布\",\n        \"en\": \"Right Side Mindmap Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*p1cYSJqxbogAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"left-side.js\",\n      \"title\": {\n        \"zh\": \"子节点左侧分布\",\n        \"en\": \"Left Side Mindmap Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*iy_tQ49Nn1EAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"custom-side.js\",\n      \"title\": {\n        \"zh\": \"自定义子节点分布\",\n        \"en\": \"Custom Mindmap Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*m9p-TpQGI-YAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/mindmap/demo/right-side.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          labelPlacement: 'right',\n          labelBackground: true,\n          ports: [{ placement: 'right' }, { placement: 'left' }],\n        },\n        animation: {\n          enter: false,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        animation: {\n          enter: false,\n        },\n      },\n      layout: {\n        type: 'mindmap',\n        direction: 'LR',\n        getHeight: () => 32,\n        getWidth: () => 32,\n        getVGap: () => 4,\n        getHGap: () => 100,\n      },\n      behaviors: ['collapse-expand', 'drag-canvas', 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/mindmap/index.en.md",
    "content": "---\ntitle: Mindmap\norder: 5\n---\n"
  },
  {
    "path": "packages/site/examples/layout/mindmap/index.zh.md",
    "content": "---\ntitle: Mindmap 脑图树\norder: 5\n---\n"
  },
  {
    "path": "packages/site/examples/layout/radial/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/radial.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'center',\n      layout: {\n        type: 'radial',\n        nodeSize: 32,\n        unitRadius: 100,\n        linkDistance: 200,\n      },\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelPlacement: 'center',\n          labelText: (d) => d.id,\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/radial/demo/cluster-sort.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0', data: { type: 'a' } },\n    { id: '1', data: { type: 'a' } },\n    { id: '2', data: { type: 'a' } },\n    { id: '3', data: { type: 'a' } },\n    { id: '4', data: { type: 'c' } },\n    { id: '5', data: { type: 'a' } },\n    { id: '6', data: { type: 'b' } },\n    { id: '7', data: { type: 'b' } },\n    { id: '8', data: { type: 'c' } },\n    { id: '9', data: { type: 'd' } },\n    { id: '10', data: { type: 'd' } },\n    { id: '11', data: { type: 'b' } },\n    { id: '12', data: { type: 'c' } },\n    { id: '13', data: { type: 'b' } },\n    { id: '14', data: { type: 'd' } },\n    { id: '15', data: { type: 'd' } },\n    { id: '16', data: { type: 'b' } },\n    { id: '17', data: { type: 'c' } },\n    { id: '18', data: { type: 'c' } },\n    { id: '19', data: { type: 'b' } },\n    { id: '20', data: { type: 'b' } },\n    { id: '21', data: { type: 'd' } },\n    { id: '22', data: { type: 'd' } },\n    { id: '23', data: { type: 'd' } },\n    { id: '24', data: { type: 'a' } },\n    { id: '25', data: { type: 'a' } },\n    { id: '26', data: { type: 'b' } },\n    { id: '27', data: { type: 'b' } },\n    { id: '28', data: { type: 'd' } },\n    { id: '29', data: { type: 'c' } },\n    { id: '30', data: { type: 'c' } },\n    { id: '31', data: { type: 'b' } },\n    { id: '32', data: { type: 'b' } },\n    { id: '33', data: { type: 'a' } },\n  ],\n  edges: [\n    { source: '0', target: '1' },\n    { source: '0', target: '2' },\n    { source: '0', target: '3' },\n    { source: '0', target: '4' },\n    { source: '0', target: '5' },\n    { source: '0', target: '7' },\n    { source: '0', target: '8' },\n    { source: '0', target: '9' },\n    { source: '0', target: '10' },\n    { source: '0', target: '11' },\n    { source: '0', target: '13' },\n    { source: '0', target: '14' },\n    { source: '0', target: '15' },\n    { source: '0', target: '16' },\n    { source: '2', target: '3' },\n    { source: '4', target: '5' },\n    { source: '4', target: '6' },\n    { source: '5', target: '6' },\n    { source: '7', target: '13' },\n    { source: '8', target: '14' },\n    { source: '9', target: '10' },\n    { source: '10', target: '22' },\n    { source: '10', target: '14' },\n    { source: '10', target: '12' },\n    { source: '10', target: '24' },\n    { source: '10', target: '21' },\n    { source: '10', target: '20' },\n    { source: '11', target: '24' },\n    { source: '11', target: '22' },\n    { source: '11', target: '14' },\n    { source: '12', target: '13' },\n    { source: '16', target: '17' },\n    { source: '16', target: '18' },\n    { source: '16', target: '21' },\n    { source: '16', target: '22' },\n    { source: '17', target: '18' },\n    { source: '17', target: '20' },\n    { source: '18', target: '19' },\n    { source: '19', target: '20' },\n    { source: '19', target: '33' },\n    { source: '19', target: '22' },\n    { source: '19', target: '23' },\n    { source: '20', target: '21' },\n    { source: '21', target: '22' },\n    { source: '22', target: '24' },\n    { source: '22', target: '25' },\n    { source: '22', target: '26' },\n    { source: '22', target: '23' },\n    { source: '22', target: '28' },\n    { source: '22', target: '30' },\n    { source: '22', target: '31' },\n    { source: '22', target: '32' },\n    { source: '22', target: '33' },\n    { source: '23', target: '28' },\n    { source: '23', target: '27' },\n    { source: '23', target: '29' },\n    { source: '23', target: '30' },\n    { source: '23', target: '31' },\n    { source: '23', target: '33' },\n    { source: '32', target: '33' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'center',\n  data,\n  layout: {\n    type: 'radial',\n    nodeSize: 32,\n    unitRadius: 90,\n    linkDistance: 200,\n    preventOverlap: true,\n    sortBy: 'type',\n    sortStrength: 50,\n  },\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n    palette: {\n      type: 'group',\n      field: 'type',\n    },\n  },\n  edge: {\n    style: {\n      endArrow: true,\n    },\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/layout/radial/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"基本 Radial 辐射布局\",\n        \"en\": \"Basic Radial Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*d3P-RK4YCDYAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"strict-prevent-overlap.js\",\n      \"title\": {\n        \"zh\": \"防止节点重叠的严格辐射布局\",\n        \"en\": \"Strict Radial Layout with Overlap Prevention\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*vpXjTIFKy1QAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"non-strict-prevent-overlap.js\",\n      \"title\": {\n        \"zh\": \"防止节点重叠的非严格辐射布局\",\n        \"en\": \"Non-strict Radial Layout with Overlap Prevention\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*4I4VRa6Kyo8AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"cluster-sort.js\",\n      \"title\": {\n        \"zh\": \"排序聚类\",\n        \"en\": \"Cluster Sort\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*_GJ_RL1Qi-oAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/radial/demo/non-strict-prevent-overlap.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/radial.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'center',\n      layout: {\n        type: 'radial',\n        nodeSize: 32,\n        unitRadius: 90,\n        linkDistance: 200,\n        preventOverlap: true,\n        maxPreventOverlapIteration: 100,\n        strictRadial: false,\n      },\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelPlacement: 'center',\n          labelText: (d) => d.id,\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/radial/demo/strict-prevent-overlap.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/radial.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'center',\n      layout: {\n        type: 'radial',\n        nodeSize: 32,\n        unitRadius: 90,\n        linkDistance: 200,\n        preventOverlap: true,\n        maxPreventOverlapIteration: 100,\n      },\n      node: {\n        style: {\n          labelFill: '#fff',\n          labelPlacement: 'center',\n          labelText: (d) => d.id,\n        },\n      },\n      behaviors: ['drag-canvas', 'drag-element'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/layout/radial/index.en.md",
    "content": "---\ntitle: Radial Layout\norder: 7\n---\n"
  },
  {
    "path": "packages/site/examples/layout/radial/index.zh.md",
    "content": "---\ntitle: Radial 辐射布局\norder: 7\n---\n"
  },
  {
    "path": "packages/site/examples/layout/snake/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(16).fill(0).map((_, i) => ({ id: `${i}` })),\n  edges: new Array(15).fill(0).map((_, i) => ({ source: `${i}`, target: `${i + 1}` })),\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n  },\n  layout: {\n    type: 'snake',\n    padding: 50,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/layout/snake/demo/gutter.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: new Array(16).fill(0).map((_, i) => ({ id: `${i}` })),\n  edges: new Array(15).fill(0).map((_, i) => ({ source: `${i}`, target: `${i + 1}` })),\n};\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'center',\n  data,\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.id,\n    },\n  },\n  layout: {\n    type: 'snake',\n    cols: 3,\n    rowGap: 80,\n    colGap: 120,\n  },\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/layout/snake/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"基本 Snake 布局\",\n        \"en\": \"Basic Snake Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*pIpcQ6yX7P0AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"gutter.js\",\n      \"title\": {\n        \"zh\": \"自定义间距的 Snake 布局\",\n        \"en\": \"Snake Layout with Gutter\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*g-5JSoayUzAAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/snake/index.en.md",
    "content": "---\ntitle: Snake Layout\n---\n"
  },
  {
    "path": "packages/site/examples/layout/snake/index.zh.md",
    "content": "---\ntitle: Snake 蛇形布局\n---\n"
  },
  {
    "path": "packages/site/examples/layout/sub-graph/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nfunction generateArray(groups = 10, itemsPerGroup = 6) {\n  const result = [];\n\n  for (let i = 1; i <= groups; i++) {\n    for (let j = 1; j <= itemsPerGroup; j++) {\n      const id = `${i}-${j}`;\n      result.push({\n        id,\n        labelText: id,\n      });\n    }\n  }\n\n  return result;\n}\n\nconst data = generateArray();\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: data,\n  },\n  animation: false,\n  autoFit: 'view',\n  autoResize: true,\n  node: {\n    style: {\n      labelFill: '#fff',\n      labelPlacement: 'center',\n      labelText: (d) => d.labelText,\n    },\n  },\n  layout: Array.from({ length: 10 }, (_, i) => ({\n    type: 'circular',\n    nodeFilter: (node) => node.id.startsWith(`${i + 1}-`),\n    center: [\n      1000 + (i % 5) * 850, // x坐标\n      i < 5 ? 100 : 1100, // y坐标\n    ],\n  })),\n  behaviors: ['drag-canvas', 'drag-element', 'zoom-canvas'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/layout/sub-graph/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"子图布局\",\n        \"en\": \"SubGraph Layout\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*7dQoTLrT2xMAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/layout/sub-graph/index.en.md",
    "content": "---\ntitle: SubGraph\norder: 16\n---\n"
  },
  {
    "path": "packages/site/examples/layout/sub-graph/index.zh.md",
    "content": "---\ntitle: 子图布局\norder: 16\n---\n"
  },
  {
    "path": "packages/site/examples/performance/massive-data/demo/20000.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/20000.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      animation: false,\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          size: 8,\n        },\n        palette: {\n          type: 'group',\n          field: 'cluster',\n        },\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/performance/massive-data/demo/5000.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/5000.json')\n  .then((res) => res.json())\n  .then(async (data) => {\n    const graph = new Graph({\n      container: 'container',\n      animation: false,\n      data,\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n      autoFit: 'view',\n    });\n\n    await graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/performance/massive-data/demo/60000.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/60000.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      animation: false,\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          size: 4,\n          batchKey: 'node',\n        },\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/performance/massive-data/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"5000.js\",\n      \"title\": {\n        \"zh\": \"5000+ 元素\",\n        \"en\": \"More than 5000 elements\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*4nfBQIM_MJYAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"20000.js\",\n      \"title\": {\n        \"zh\": \"约 20000 元素\",\n        \"en\": \"About 20000 elements\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*oiL0R5rfaL8AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"60000.js\",\n      \"title\": {\n        \"zh\": \"60000+ 元素\",\n        \"en\": \"More than 60000 elements\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*EG7WRrraMAsAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/performance/massive-data/index.en.md",
    "content": "---\ntitle: Massive Data\n---\n"
  },
  {
    "path": "packages/site/examples/performance/massive-data/index.zh.md",
    "content": "---\ntitle: 大数据量\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/background/demo/background.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  width: 800,\n  height: 600,\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'background',\n      width: '800px',\n      height: '600px',\n      backgroundImage:\n        'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*0Qq0ToQm1rEAAAAAAAAAAAAADmJ7AQ/original)',\n      backgroundRepeat: 'no-repeat',\n      backgroundSize: 'cover',\n      opacity: 0.2,\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/background/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"background.js\",\n      \"title\": {\n        \"zh\": \"背景图片\",\n        \"en\": \"Background Image\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*eCkYR7cLfWwAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/background/index.en.md",
    "content": "---\ntitle: Background\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/background/index.zh.md",
    "content": "---\ntitle: Background 背景图\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/bubble-sets/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/collection.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const groupedNodesByCluster = data.nodes.reduce((acc, node) => {\n      const cluster = node.data.cluster;\n      acc[cluster] ||= [];\n      acc[cluster].push(node.id);\n      return acc;\n    }, {});\n\n    const createStyle = (baseColor) => ({\n      fill: baseColor,\n      stroke: baseColor,\n      labelFill: '#fff',\n      labelPadding: 2,\n      labelBackgroundFill: baseColor,\n      labelBackgroundRadius: 5,\n    });\n\n    const graph = new Graph({\n      container: 'container',\n      data,\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n      node: {\n        palette: { field: 'cluster' },\n      },\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        linkDistance: (d) => {\n          if (d.source === 'node0' || d.target === 'node0') {\n            return 200;\n          }\n          return 80;\n        },\n      },\n      plugins: [\n        {\n          key: 'bubble-sets-a',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['a'],\n          labelText: 'cluster-a',\n          ...createStyle('#1783FF'),\n        },\n        {\n          key: 'bubble-sets-b',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['b'],\n          labelText: 'cluster-b',\n          ...createStyle('#00C9C9'),\n        },\n        {\n          key: 'bubble-sets-c',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['c'],\n          labelText: 'cluster-c',\n          ...createStyle('#F08F56'),\n        },\n        {\n          key: 'bubble-sets-d',\n          type: 'bubble-sets',\n          members: groupedNodesByCluster['d'],\n          labelText: 'cluster-d',\n          ...createStyle('#D580FF'),\n        },\n      ],\n      autoFit: 'center',\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/plugin/bubble-sets/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"用气泡集包裹节点集\",\n        \"en\": \"Use Bubblesets to wrap the node sets.\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*JwvxTbOoWQ8AAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/bubble-sets/index.en.md",
    "content": "---\ntitle: Bubblesets\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/bubble-sets/index.zh.md",
    "content": "---\ntitle: Bubblesets 气泡集\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/contextMenu/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'contextmenu',\n      trigger: 'contextmenu', // 'click' or 'contextmenu'\n      onClick: (v) => {\n        alert('You have clicked the「' + v + '」item');\n      },\n      getItems: () => {\n        return [\n          { name: '展开一度关系', value: 'spread' },\n          { name: '查看详情', value: 'detail' },\n        ];\n      },\n      enable: (e) => e.targetType === 'node',\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/contextMenu/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"上下文菜单\",\n        \"en\": \"Context Menu\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*9o1lRbfc3YMAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/contextMenu/index.en.md",
    "content": "---\ntitle: ContextMenu\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/contextMenu/index.zh.md",
    "content": "---\ntitle: Contextmenu 上下文菜单\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/edge-bundling/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/circular.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      layout: {\n        type: 'circular',\n      },\n      node: { style: { size: 20 } },\n      behaviors: ['drag-canvas', 'drag-element'],\n      plugins: [\n        {\n          key: 'edge-bundling',\n          type: 'edge-bundling',\n          bundleThreshold: 0.1,\n        },\n      ],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/plugin/edge-bundling/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"边绑定\",\n        \"en\": \"Edge Bundling\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*aC-VQJmDwdkAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/edge-bundling/index.en.md",
    "content": "---\ntitle: EdgeBundling\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/edge-bundling/index.zh.md",
    "content": "---\ntitle: EdgeBundling 边绑定\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/edge-filter-lens/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      autoFit: 'view',\n      node: {\n        style: { size: 16 },\n        palette: {\n          field: (datum) => Math.floor(datum.style?.y / 60),\n        },\n      },\n      edge: {\n        style: {\n          label: false,\n          labelText: (d) => d.data.value?.toString(),\n          stroke: '#ccc',\n        },\n      },\n      plugins: [\n        {\n          key: 'edge-filter-lens',\n          type: 'edge-filter-lens',\n        },\n      ],\n    });\n    graph.render();\n\n    const config = {\n      trigger: 'pointermove',\n      scaleRBy: 'wheel',\n      nodeType: 'both',\n    };\n\n    window.addPanel((gui) => {\n      gui\n        .add(config, 'trigger', ['pointermove', 'click', 'drag'])\n        .name('Trigger')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'edge-filter-lens',\n            trigger: value,\n          });\n        });\n      gui\n        .add(config, 'scaleRBy', ['wheel', 'unset'])\n        .name('Scale R by')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'edge-filter-lens',\n            scaleRBy: value,\n          });\n        });\n      gui\n        .add(config, 'nodeType', ['source', 'target', 'both', 'either'])\n        .name('Node Type')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'edge-filter-lens',\n            nodeType: value,\n          });\n        });\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/plugin/edge-filter-lens/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"边过滤镜\",\n        \"en\": \"Edge Filter Lens\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*62FuSY-LFEIAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/edge-filter-lens/index.en.md",
    "content": "---\ntitle: EdgeFilterLens\n---\n\nEdgeFilterLens can keep the focused edges within the lens range, while other edges will not be displayed within that range.\n"
  },
  {
    "path": "packages/site/examples/plugin/edge-filter-lens/index.zh.md",
    "content": "---\ntitle: EdgeFilterLens 边过滤镜\n---\n\n边过滤镜可以将关注的边保留在过滤镜范围内，其他边将在该范围内不显示。\n"
  },
  {
    "path": "packages/site/examples/plugin/fisheye/demo/basic.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nfetch('https://assets.antv.antgroup.com/g6/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          size: (datum) => datum.id.length * 2 + 10,\n          label: false,\n          labelText: (datum) => datum.id,\n          labelBackground: true,\n          icon: false,\n          iconFontFamily: 'iconfont',\n          iconText: '\\ue6f6',\n          iconFill: '#fff',\n        },\n        palette: {\n          type: 'group',\n          field: (datum) => datum.id,\n          color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n        },\n      },\n      edge: {\n        style: {\n          stroke: '#e2e2e2',\n        },\n      },\n      plugins: [{ key: 'fisheye', type: 'fisheye', nodeStyle: { label: true, icon: true } }],\n    });\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/plugin/fisheye/demo/custom.js",
    "content": "import { Graph, iconfont } from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nfetch('https://assets.antv.antgroup.com/g6/relations.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data,\n      node: {\n        style: {\n          size: (datum) => datum.id.length * 2 + 10,\n          label: false,\n          labelText: (datum) => datum.id,\n          labelBackground: true,\n          icon: false,\n          iconFontFamily: 'iconfont',\n          iconText: '\\ue6f6',\n          iconFill: '#fff',\n        },\n        palette: {\n          type: 'group',\n          field: (datum) => datum.id,\n          color: ['#1783FF', '#00C9C9', '#F08F56', '#D580FF'],\n        },\n      },\n      edge: {\n        style: {\n          stroke: '#e2e2e2',\n        },\n      },\n      plugins: [\n        {\n          key: 'fisheye',\n          type: 'fisheye',\n          trigger: 'click',\n          scaleRBy: 'wheel',\n          scaleDBy: 'drag',\n          style: { fill: '#F08F56', stroke: '#666', lineDash: [5, 5] },\n          nodeStyle: { label: true, icon: true },\n        },\n      ],\n    });\n    graph.render();\n\n    const config = {\n      trigger: 'click',\n      scaleRBy: 'wheel',\n      scaleDBy: 'drag',\n      showDPercent: true,\n      borderless: true,\n    };\n\n    window.addPanel((gui) => {\n      gui\n        .add(config, 'trigger', ['pointermove', 'click', 'drag'])\n        .name('Trigger')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'fisheye',\n            trigger: value,\n          });\n        });\n      gui\n        .add(config, 'scaleRBy', ['wheel', 'drag', 'unset'])\n        .name('Scale R by')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'fisheye',\n            scaleRBy: value,\n          });\n        });\n      gui\n        .add(config, 'scaleDBy', ['wheel', 'drag', 'unset'])\n        .name('Scale D by')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'fisheye',\n            scaleDBy: value,\n          });\n        });\n      gui\n        .add(config, 'showDPercent')\n        .name('Show D Percent')\n        .onChange((value) => {\n          graph.updatePlugin({\n            key: 'fisheye',\n            showDPercent: value,\n          });\n        });\n      gui\n        .add(config, 'borderless')\n        .name('Borderless')\n        .onChange((value) => {\n          const style = value\n            ? { fill: 'transparent', lineDash: 0, stroke: 'transparent' }\n            : { fill: '#F08F56', lineDash: [5, 5], stroke: '#666' };\n          graph.updatePlugin({\n            key: 'fisheye',\n            style,\n          });\n        });\n    });\n  });\n"
  },
  {
    "path": "packages/site/examples/plugin/fisheye/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"鱼眼放大镜\",\n        \"en\": \"Fisheye\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*SPqiTLwdZ9MAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"custom.js\",\n      \"title\": {\n        \"zh\": \"自定义鱼眼放大镜\",\n        \"en\": \"Custom Fisheye\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-VyPRY-2dMIAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/fisheye/index.en.md",
    "content": "---\ntitle: Fisheye\n---\n\nFisheye is designed for focus+context exploration, it keeps the context and the relationships between context and the focus while magnifying the focus area.\n"
  },
  {
    "path": "packages/site/examples/plugin/fisheye/index.zh.md",
    "content": "---\ntitle: Fisheye 鱼眼放大镜\n---\n\nFisheye 鱼眼放大镜是为 focus+context 的探索场景设计的，它能够保证在放大关注区域的同时，保证上下文以及上下文与关注中心的关系不丢失。\n"
  },
  {
    "path": "packages/site/examples/plugin/fullscreen/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  data: { nodes: Array.from({ length: 20 }).map((_, i) => ({ id: `node${i}` })) },\n  autoFit: 'center',\n  background: '#fff',\n  plugins: [\n    {\n      type: 'fullscreen',\n      key: 'fullscreen',\n    },\n    function () {\n      const graph = this;\n      return {\n        type: 'toolbar',\n        key: 'toolbar',\n        position: 'top-left',\n        onClick: (item) => {\n          const fullscreenPlugin = graph.getPluginInstance('fullscreen');\n          if (item === 'request-fullscreen') {\n            fullscreenPlugin.request();\n          }\n          if (item === 'exit-fullscreen') {\n            fullscreenPlugin.exit();\n          }\n        },\n        getItems: () => {\n          return [\n            { id: 'request-fullscreen', value: 'request-fullscreen' },\n            { id: 'exit-fullscreen', value: 'exit-fullscreen' },\n          ];\n        },\n      };\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/fullscreen/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"全屏展示\",\n        \"en\": \"Fullscreen\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*KQlbQZxPOvgAAAAAAAAAAAAAemJ7AQ/fmt.avif\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/fullscreen/index.en.md",
    "content": "---\ntitle: Fullscreen\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/fullscreen/index.zh.md",
    "content": "---\ntitle: Fullscreen 全屏展示\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/grid-line/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['drag-canvas'],\n  plugins: [{ key: 'grid-line', type: 'grid-line', follow: false }],\n});\n\ngraph.render();\n\nwindow.addPanel((gui) => {\n  gui\n    .add({ follow: false }, 'follow')\n    .name('Follow')\n    .onChange((value) => {\n      graph.updatePlugin({\n        key: 'grid-line',\n        follow: value,\n      });\n    });\n});\n"
  },
  {
    "path": "packages/site/examples/plugin/grid-line/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"画布网格\",\n        \"en\": \"Grid Line\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*s66AQJ3FJMMAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/grid-line/index.en.md",
    "content": "---\ntitle: Grid Line\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/grid-line/index.zh.md",
    "content": "---\ntitle: Grid Line 网格线\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/history/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [{ id: 'node-0', style: { x: 200, y: 150 } }],\n  },\n  node: {\n    style: {\n      size: 60,\n      labelText: 'Drag Me!',\n      labelPlacement: 'middle',\n      labelFill: '#fff',\n    },\n  },\n  behaviors: ['drag-element'],\n  plugins: [\n    {\n      type: 'history',\n      key: 'history',\n    },\n  ],\n});\n\ngraph.render().then(() => {\n  window.addPanel((gui) => {\n    const history = graph.getPluginInstance('history');\n    const config = {\n      undo: () => {\n        if (history.canUndo()) history.undo();\n      },\n      redo: () => {\n        if (history.canRedo()) history.redo();\n      },\n    };\n    gui.add(config, 'undo').name('⬅️ undo');\n    gui.add(config, 'redo').name('➡️ redo');\n  });\n});\n"
  },
  {
    "path": "packages/site/examples/plugin/history/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"撤销重做\",\n        \"en\": \"Redo/Undo\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*5K19SrUgO6QAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/history/index.en.md",
    "content": "---\ntitle: History\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/history/index.zh.md",
    "content": "---\ntitle: History 撤销重做\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/hull/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/collection.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const groupedNodesByCluster = data.nodes.reduce((acc, node) => {\n      const cluster = node.data.cluster;\n      acc[cluster] ||= [];\n      acc[cluster].push(node.id);\n      return acc;\n    }, {});\n\n    const createStyle = (baseColor) => ({\n      fill: baseColor,\n      stroke: baseColor,\n      labelFill: '#fff',\n      labelPadding: 2,\n      labelBackgroundFill: baseColor,\n      labelBackgroundRadius: 5,\n    });\n\n    const graph = new Graph({\n      container: 'container',\n      data,\n      behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n      node: {\n        palette: { field: 'cluster' },\n      },\n      layout: {\n        type: 'force',\n        preventOverlap: true,\n        linkDistance: (d) => {\n          if (d.source === 'node0' || d.target === 'node0') {\n            return 200;\n          }\n          return 80;\n        },\n      },\n      plugins: [\n        {\n          key: 'hull-a',\n          type: 'hull',\n          members: groupedNodesByCluster['a'],\n          labelText: 'cluster-a',\n          ...createStyle('#1783FF'),\n        },\n        {\n          key: 'hull-b',\n          type: 'hull',\n          members: groupedNodesByCluster['b'],\n          labelText: 'cluster-b',\n          ...createStyle('#00C9C9'),\n        },\n        {\n          key: 'hull-c',\n          type: 'hull',\n          members: groupedNodesByCluster['c'],\n          labelText: 'cluster-c',\n          ...createStyle('#F08F56'),\n        },\n        {\n          key: 'hull-d',\n          type: 'hull',\n          members: groupedNodesByCluster['d'],\n          labelText: 'cluster-d',\n          ...createStyle('#D580FF'),\n        },\n      ],\n      autoFit: 'center',\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/plugin/hull/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"用轮廓包裹节点集合\",\n        \"en\": \"Use hulls to wrap the node sets.\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YXHtRZUKAZcAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/hull/index.en.md",
    "content": "---\ntitle: Hull\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/hull/index.zh.md",
    "content": "---\ntitle: Hull 节点集轮廓包裹\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/legend/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'node-1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: 'node-2', type: 'rect', data: { cluster: 'node-type2' } },\n    { id: 'node-3', type: 'triangle', data: { cluster: 'node-type3' } },\n    { id: 'node-4', type: 'diamond', data: { cluster: 'node-type4' } },\n  ],\n  edges: [\n    { source: 'node-1', target: 'node-2', data: { cluster: 'edge-type1' } },\n    { source: 'node-1', target: 'node-4', data: { cluster: 'edge-type2' } },\n    { source: 'node-3', target: 'node-4' },\n    { source: 'node-2', target: 'node-4', data: { cluster: 'edge-type3' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: { size: 32 },\n    palette: {\n      field: 'cluster',\n    },\n  },\n  layout: {\n    type: 'force',\n  },\n  plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/legend/demo/click.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '1', type: 'circle', data: { cluster: 'node-type1' } },\n    { id: '2', type: 'rect', data: { cluster: 'node-type2' } },\n    { id: '3', type: 'triangle', data: { cluster: 'node-type3' } },\n    { id: '4', type: 'diamond', data: { cluster: 'node-type4' } },\n  ],\n  edges: [\n    { source: '1', target: '2', type: 'quadratic', data: { cluster: 'edge-type1' } },\n    { source: '1', target: '4', data: { cluster: 'edge-type2' } },\n    { source: '3', target: '4' },\n    { source: '2', target: '4', data: { cluster: 'edge-type3' } },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: { size: 32 },\n    palette: { field: 'cluster' },\n  },\n  edge: {\n    palette: { field: 'cluster' },\n  },\n  layout: {\n    type: 'force',\n  },\n  plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n      trigger: 'click',\n      gridRow: 1,\n      gridCol: 4,\n      itemLabelFontSize: 12,\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/legend/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"默认图例\",\n        \"en\": \"Default legend\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*u9w9T7X5fZoAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"click.js\",\n      \"title\": {\n        \"zh\": \"点击图例\",\n        \"en\": \"Click legend\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YMxxTZwt54UAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"style.js\",\n      \"title\": {\n        \"zh\": \"图例项样式\",\n        \"en\": \"Item style\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*DgNhR5MeC9kAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/legend/demo/style.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    {\n      id: '1',\n      type: 'circle',\n      data: { cluster: 'node-type1' },\n    },\n    {\n      id: '2',\n      type: 'rect',\n      data: { cluster: 'node-type2' },\n    },\n    {\n      id: '3',\n      type: 'triangle',\n      data: { cluster: 'node-type3' },\n    },\n    {\n      id: '4',\n      type: 'diamond',\n      data: { cluster: 'node-type4' },\n    },\n  ],\n  edges: [\n    {\n      id: '1-2',\n      source: '1',\n      target: '2',\n      type: 'quadratic',\n      data: { cluster: 'edge-type1' },\n    },\n    {\n      id: '1-4',\n      source: '1',\n      target: '4',\n      data: { cluster: 'edge-type2' },\n    },\n    {\n      id: '3-4',\n      source: '3',\n      target: '4',\n    },\n    {\n      id: '2-4',\n      source: '2',\n      target: '4',\n      data: { cluster: 'edge-type3' },\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  node: {\n    style: { size: 32 },\n    palette: { field: 'cluster' },\n  },\n  edge: {\n    palette: { field: 'cluster' },\n  },\n  layout: {\n    type: 'force',\n  },\n  plugins: [\n    {\n      type: 'legend',\n      nodeField: 'cluster',\n      edgeField: 'cluster',\n      titleText: 'Legend Title',\n      trigger: 'click',\n      position: 'top',\n      gridCol: 3,\n      itemLabelFontSize: 12,\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/legend/index.en.md",
    "content": "---\ntitle: Legend\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/legend/index.zh.md",
    "content": "---\ntitle: Legend 图例\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/minimap/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: { nodes: Array.from({ length: 20 }).map((_, i) => ({ id: `node${i}` })) },\n  behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'minimap',\n      size: [240, 160],\n    },\n  ],\n  node: {\n    palette: 'spectral',\n  },\n  layout: {\n    type: 'circular',\n  },\n  autoFit: 'view',\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/minimap/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"小地图\",\n        \"en\": \"Minimap\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*asv5Roh25YkAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/minimap/index.en.md",
    "content": "---\ntitle: Minimap\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/minimap/index.zh.md",
    "content": "---\ntitle: Minimap 小地图\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/snapline/demo/autoSnap.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'snapline',\n      key: 'snapline',\n      verticalLineStyle: { stroke: '#F08F56', lineWidth: 2 },\n      horizontalLineStyle: { stroke: '#17C76F', lineWidth: 2 },\n      offset: Infinity,\n      autoSnap: true,\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/snapline/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'snapline',\n      key: 'snapline',\n      verticalLineStyle: { stroke: '#F08F56', lineWidth: 2 },\n      horizontalLineStyle: { stroke: '#17C76F', lineWidth: 2 },\n      autoSnap: false,\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/snapline/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"对齐线\",\n        \"en\": \"Snapline\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*MU65RolCuPkAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"autoSnap.js\",\n      \"title\": {\n        \"zh\": \"支持自动吸附\",\n        \"en\": \"Snapline with auto snap\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*67Y_RpCskBMAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/snapline/index.en.md",
    "content": "---\ntitle: Snapline\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/snapline/index.zh.md",
    "content": "---\ntitle: Snapline 对齐线\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/timebar/demo/chart.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst startTime = new Date('2023-08-01').getTime();\nconst diff = 3600 * 24 * 1000;\nconst timebarData = [10, 2, 3, 4, 15, 10, 5, 0, 3, 1].map((value, index) => ({\n  time: new Date(startTime + index * diff),\n  value,\n  label: new Date(startTime + index * diff).toLocaleString(),\n}));\nconst graphData = {\n  nodes: new Array(49).fill(0).map((_, index) => ({\n    id: `node-${index}`,\n    data: {\n      timestamp: startTime + (index % 10) * diff,\n      value: index % 20,\n      label: new Date(startTime + (index % 10) * diff).toLocaleString(),\n    },\n  })),\n  edges: new Array(49).fill(0).map((_, i) => ({\n    id: `edge-${i}`,\n    source: `node-${i % 30}`,\n    target: `node-${(i % 20) + 29}`,\n    data: {\n      edgeType: 'e1',\n    },\n  })),\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data: graphData,\n  behaviors: ['drag-canvas', 'drag-element', 'zoom-canvas'],\n  layout: {\n    type: 'grid',\n    cols: 7,\n  },\n  plugins: [\n    {\n      type: 'timebar',\n      key: 'timebar',\n      data: timebarData,\n      width: 450,\n      height: 100,\n      loop: true,\n      timebarType: 'chart',\n    },\n  ],\n  autoFit: 'view',\n  padding: [10, 0, 160, 0],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/timebar/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"time.js\",\n      \"title\": {\n        \"zh\": \"时间模式\",\n        \"en\": \"Time Mode\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*94CATK7VwhQAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"chart.js\",\n      \"title\": {\n        \"zh\": \"图表模式\",\n        \"en\": \"Chart Mode\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*RjCKS6xdRWwAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/timebar/demo/time.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst startTime = new Date('2023-08-01').getTime();\nconst diff = 3600 * 24 * 1000;\nconst timebarData = [10, 2, 3, 4, 15, 10, 5, 0, 3, 1].map((value, index) => ({\n  time: new Date(startTime + index * diff),\n  value,\n  label: new Date(startTime + index * diff).toLocaleString(),\n}));\nconst graphData = {\n  nodes: new Array(49).fill(0).map((_, index) => ({\n    id: `node-${index}`,\n    data: {\n      timestamp: startTime + (index % 10) * diff,\n      value: index % 20,\n      label: new Date(startTime + (index % 10) * diff).toLocaleString(),\n    },\n  })),\n  edges: new Array(49).fill(0).map((_, i) => ({\n    id: `edge-${i}`,\n    source: `node-${i % 30}`,\n    target: `node-${(i % 20) + 29}`,\n    data: {\n      edgeType: 'e1',\n    },\n  })),\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data: graphData,\n  behaviors: ['drag-canvas', 'drag-element', 'zoom-canvas'],\n  layout: {\n    type: 'grid',\n    cols: 7,\n  },\n  plugins: [\n    {\n      type: 'timebar',\n      key: 'timebar',\n      data: timebarData,\n      width: 450,\n      loop: true,\n    },\n  ],\n  autoFit: 'view',\n  padding: [10, 0, 65, 0],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/timebar/index.en.md",
    "content": "---\ntitle: Timebar\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/timebar/index.zh.md",
    "content": "---\ntitle: Timebar 时间条\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/toolbar/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'toolbar',\n      position: 'top-left',\n      onClick: (item) => {\n        alert('item clicked:' + item);\n      },\n      getItems: () => {\n        // G6 内置了 9 个 icon，分别是 zoom-in、zoom-out、redo、undo、edit、delete、auto-fit、export、reset\n        return [\n          { id: 'zoom-in', value: 'zoom-in' },\n          { id: 'zoom-out', value: 'zoom-out' },\n          { id: 'redo', value: 'redo' },\n          { id: 'undo', value: 'undo' },\n          { id: 'edit', value: 'edit' },\n          { id: 'delete', value: 'delete' },\n          { id: 'auto-fit', value: 'auto-fit' },\n          { id: 'export', value: 'export' },\n          { id: 'reset', value: 'reset' },\n        ];\n      },\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/toolbar/demo/custom.js",
    "content": "import { Graph } from '@antv/g6';\n\n// Use your own iconfont.\nconst iconFont = document.createElement('script');\niconFont.src = '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js';\ndocument.head.appendChild(iconFont);\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'toolbar',\n      position: 'right-top',\n      onClick: (item) => {\n        alert('item clicked:' + item);\n      },\n      getItems: () => {\n        return [\n          { id: 'icon-xinjian', value: 'new' },\n          { id: 'icon-fenxiang', value: 'share' },\n          { id: 'icon-chexiao', value: 'undo' },\n        ];\n      },\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/toolbar/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"工具栏\",\n        \"en\": \"ToolBar\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*IXshSoyeZHEAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"custom.js\",\n      \"title\": {\n        \"zh\": \"自定义工具栏\",\n        \"en\": \"ToolBar\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*U_72RJtvwEIAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/toolbar/index.en.md",
    "content": "---\ntitle: ToolBar\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/toolbar/index.zh.md",
    "content": "---\ntitle: Toolbar 工具栏\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/tooltip/demo/basic.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: '0', data: { label: 'node-0', description: 'This is node-0.' } },\n      { id: '1', data: { label: 'node-1', description: 'This is node-1.' } },\n      { id: '2', data: { label: 'node-2', description: 'This is node-2.' } },\n      { id: '3', data: { label: 'node-3', description: 'This is node-3.' } },\n      { id: '4', data: { label: 'node-4', description: 'This is node-4.' } },\n      { id: '5', data: { label: 'node-5', description: 'This is node-5.' } },\n    ],\n    edges: [\n      { source: '0', target: '1', data: { description: 'This is edge from node 0 to node 1.' } },\n      { source: '0', target: '2', data: { description: 'This is edge from node 0 to node 2.' } },\n      { source: '0', target: '3', data: { description: 'This is edge from node 0 to node 3.' } },\n      { source: '0', target: '4', data: { description: 'This is edge from node 0 to node 4.' } },\n      { source: '0', target: '5', data: { description: 'This is edge from node 0 to node 5.' } },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  plugins: [\n    {\n      type: 'tooltip',\n      getContent: (e, items) => {\n        let result = `<h4>Custom Content</h4>`;\n        items.forEach((item) => {\n          result += `<p>Type: ${item.data.description}</p>`;\n        });\n        return result;\n      },\n    },\n  ],\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/tooltip/demo/click.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: '0', data: { label: 'node-0', description: 'This is node-0.' } },\n      { id: '1', data: { label: 'node-1', description: 'This is node-1.' } },\n      { id: '2', data: { label: 'node-2', description: 'This is node-2.' } },\n      { id: '3', data: { label: 'node-3', description: 'This is node-3.' } },\n      { id: '4', data: { label: 'node-4', description: 'This is node-4.' } },\n      { id: '5', data: { label: 'node-5', description: 'This is node-5.' } },\n    ],\n    edges: [\n      { source: '0', target: '1', data: { description: 'This is edge from node 0 to node 1.' } },\n      { source: '0', target: '2', data: { description: 'This is edge from node 0 to node 2.' } },\n      { source: '0', target: '3', data: { description: 'This is edge from node 0 to node 3.' } },\n      { source: '0', target: '4', data: { description: 'This is edge from node 0 to node 4.' } },\n      { source: '0', target: '5', data: { description: 'This is edge from node 0 to node 5.' } },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  plugins: [\n    {\n      type: 'tooltip',\n      trigger: 'click',\n      getContent: (e, items) => {\n        let result = `<h4>Custom Content</h4>`;\n        items.forEach((item) => {\n          result += `<p>Type: ${item.data.description}</p>`;\n        });\n        return result;\n      },\n    },\n  ],\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/tooltip/demo/dual.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst graph = new Graph({\n  container: 'container',\n  data: {\n    nodes: [\n      { id: '0', data: { label: 'node-0', description: 'This is node-0.' } },\n      { id: '1', data: { label: 'node-1', description: 'This is node-1.' } },\n      { id: '2', data: { label: 'node-2', description: 'This is node-2.' } },\n      { id: '3', data: { label: 'node-3', description: 'This is node-3.' } },\n      { id: '4', data: { label: 'node-4', description: 'This is node-4.' } },\n      { id: '5', data: { label: 'node-5', description: 'This is node-5.' } },\n    ],\n    edges: [\n      { source: '0', target: '1', data: { description: 'This is edge from node 0 to node 1.' } },\n      { source: '0', target: '2', data: { description: 'This is edge from node 0 to node 2.' } },\n      { source: '0', target: '3', data: { description: 'This is edge from node 0 to node 3.' } },\n      { source: '0', target: '4', data: { description: 'This is edge from node 0 to node 4.' } },\n      { source: '0', target: '5', data: { description: 'This is edge from node 0 to node 5.' } },\n    ],\n  },\n  layout: {\n    type: 'grid',\n  },\n  plugins: [\n    function () {\n      return {\n        key: 'tooltip-click',\n        type: 'tooltip',\n        trigger: 'click',\n        getContent: (evt, items) => {\n          return `<div>click ${items[0].id}</div>`;\n        },\n        onOpenChange: (open) => {\n          const tooltip = this.getPluginInstance('tooltip-hover');\n          if (tooltip && open) tooltip.hide();\n        },\n      };\n    },\n    function () {\n      return {\n        key: 'tooltip-hover',\n        type: 'tooltip',\n        trigger: 'hover',\n        enable: (e) => {\n          const tooltip = this.getPluginInstance('tooltip-click');\n          return e.target.id !== tooltip.currentTarget;\n        },\n        getContent: (evt, items) => {\n          return `<div>hover ${items[0].id}</div>`;\n        },\n        onOpenChange: (open) => {\n          const tooltip = this.getPluginInstance('tooltip-click');\n          if (tooltip && open) {\n            tooltip.hide();\n          }\n        },\n      };\n    },\n  ],\n  behaviors: ['drag-canvas', 'drag-element'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/tooltip/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"basic.js\",\n      \"title\": {\n        \"zh\": \"提示框\",\n        \"en\": \"Tooltip\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ErhbR7ErhRwAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"click.js\",\n      \"title\": {\n        \"zh\": \"点击触发\",\n        \"en\": \"Show on click\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ErhbR7ErhRwAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"dual.js\",\n      \"title\": {\n        \"zh\": \"鼠标移入和点击同一元素时显示不同的提示框\",\n        \"en\": \"Dual tooltips\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*DCZ_SbSHGnYAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/tooltip/index.en.md",
    "content": "---\ntitle: Tooltip\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/tooltip/index.zh.md",
    "content": "---\ntitle: Tooltip 提示框\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/watermark/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"text.js\",\n      \"title\": {\n        \"zh\": \"文本水印\",\n        \"en\": \"Text Watermark\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*GPRuRqBUIsoAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"repeat.js\",\n      \"title\": {\n        \"zh\": \"重复图片\",\n        \"en\": \"Repeat Image\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*8NfwR5QEXvIAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/plugin/watermark/demo/repeat.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: {\n    type: 'grid',\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'watermark',\n      width: 200,\n      height: 100,\n      rotate: Math.PI / 12,\n      imageURL: 'https://gw.alipayobjects.com/os/s/prod/antv/assets/image/logo-with-text-73b8a.svg',\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/watermark/demo/text.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [{ id: 'node-0' }, { id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }],\n  edges: [\n    { source: 'node-0', target: 'node-1' },\n    { source: 'node-0', target: 'node-2' },\n    { source: 'node-0', target: 'node-3' },\n    { source: 'node-0', target: 'node-4' },\n    { source: 'node-1', target: 'node-0' },\n    { source: 'node-2', target: 'node-0' },\n    { source: 'node-3', target: 'node-0' },\n    { source: 'node-4', target: 'node-0' },\n    { source: 'node-5', target: 'node-0' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  layout: { type: 'grid' },\n  behaviors: ['zoom-canvas', 'drag-canvas', 'drag-element'],\n  plugins: [\n    {\n      type: 'watermark',\n      text: 'G6: Graph Visualization',\n      textFontSize: 14,\n      textFontFamily: 'Microsoft YaHei',\n      fill: 'rgba(0, 0, 0, 0.1)',\n      rotate: Math.PI / 12,\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/plugin/watermark/index.en.md",
    "content": "---\ntitle: Watermark\n---\n"
  },
  {
    "path": "packages/site/examples/plugin/watermark/index.zh.md",
    "content": "---\ntitle: Watermark 水印\n---\n"
  },
  {
    "path": "packages/site/examples/scene-case/default/demo/battle-array.jsx",
    "content": "import { ExtensionCategory, Graph, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\nimport styled from 'styled-components';\n\nconst Player = styled.div`\n  width: 100%;\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n`;\n\nconst Shirt = styled.div`\n  width: 40px;\n  height: 40px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  position: relative;\n\n  img {\n    width: 40px;\n    position: absolute;\n    left: 0;\n    top: 0;\n  }\n`;\n\nconst Number = styled.div`\n  color: #fff;\n  font-family: 'DingTalk-JinBuTi';\n  font-size: 10px;\n  top: 20px;\n  left: 15px;\n  z-index: 1;\n  margin-top: 16px;\n  margin-left: -2px;\n`;\n\nconst Label = styled.div`\n  max-width: 120px;\n  padding: 0 8px;\n  color: #fff;\n  font-size: 10px;\n  background-image: url('https://mdn.alipayobjects.com/huamei_92awrc/afts/img/A*s2csQ48M0AkAAAAAAAAAAAAADsvfAQ/original');\n  background-repeat: no-repeat;\n  background-size: cover;\n  display: flex;\n  justify-content: center;\n  overflow: hidden;\n  text-overflow: ellipsis;\n`;\n\nconst PlayerNode = ({ playerInfo }) => {\n  const { isTeamA, player_shirtnumber, player_name } = playerInfo;\n  return (\n    <Player>\n      <Shirt>\n        <img\n          src={\n            isTeamA\n              ? 'https://mdn.alipayobjects.com/huamei_92awrc/afts/img/A*0oAaS42vqWcAAAAAAAAAAAAADsvfAQ/original'\n              : 'https://mdn.alipayobjects.com/huamei_92awrc/afts/img/A*BYH5SauBNecAAAAAAAAAAAAADsvfAQ/original'\n          }\n        />\n        <Number>{player_shirtnumber}</Number>\n      </Shirt>\n      <Label>{player_name}</Label>\n    </Player>\n  );\n};\n\nregister(ExtensionCategory.NODE, 'react', ReactNode);\n\nconst data = {\n  nodes: [\n    {\n      id: '50251337',\n      x: 50,\n      y: 68,\n      isTeamA: '1',\n      player_id: '50251337',\n      player_shirtnumber: '19',\n      player_enName: 'Justin Kluivert',\n      player_name: '尤斯廷-克鲁伊维特',\n    },\n    {\n      id: '50436685',\n      x: 25,\n      y: 68,\n      isTeamA: '1',\n      player_id: '50436685',\n      player_shirtnumber: '24',\n      player_enName: 'Antoine Semenyo',\n      player_name: '塞门约',\n    },\n    {\n      id: '50204813',\n      x: 50,\n      y: 89,\n      isTeamA: '1',\n      player_id: '50204813',\n      player_shirtnumber: '9',\n      player_enName: 'Dominic Solanke',\n      player_name: '索兰克',\n    },\n    {\n      id: '50250175',\n      x: 75,\n      y: 68,\n      isTeamA: '1',\n      player_id: '50250175',\n      player_shirtnumber: '16',\n      player_enName: 'Marcus Tavernier',\n      player_name: '塔韦尼耶',\n    },\n    {\n      id: '50213675',\n      x: 65,\n      y: 48,\n      isTeamA: '1',\n      player_id: '50213675',\n      player_shirtnumber: '4',\n      player_enName: 'Lewis Cook',\n      player_name: '刘易斯-库克',\n    },\n    {\n      id: '50186648',\n      x: 35,\n      y: 48,\n      isTeamA: '1',\n      player_id: '50186648',\n      player_shirtnumber: '10',\n      player_enName: 'Ryan Christie',\n      player_name: '克里斯蒂',\n    },\n    {\n      id: '50279448',\n      x: 38,\n      y: 28,\n      isTeamA: '1',\n      player_id: '50279448',\n      player_shirtnumber: '6',\n      player_enName: 'Chris Mepham',\n      player_name: '迈帕姆',\n    },\n    {\n      id: '50061646',\n      x: 15,\n      y: 28,\n      isTeamA: '1',\n      player_id: '50061646',\n      player_shirtnumber: '15',\n      player_enName: 'Adam Smith',\n      player_name: '亚当-史密斯',\n    },\n    {\n      id: '50472140',\n      x: 62,\n      y: 28,\n      isTeamA: '1',\n      player_id: '50472140',\n      player_shirtnumber: '27',\n      player_enName: 'Ilya Zabarnyi',\n      player_name: '扎巴尔尼',\n    },\n    {\n      id: '50544346',\n      x: 85,\n      y: 28,\n      isTeamA: '1',\n      player_id: '50544346',\n      player_shirtnumber: '3',\n      player_enName: 'Milos Kerkez',\n      player_name: '科尔克兹',\n    },\n    {\n      id: '50062598',\n      x: 50,\n      y: 7,\n      isTeamA: '1',\n      player_id: '50062598',\n      player_shirtnumber: '1',\n      player_enName: 'Neto',\n      player_name: '内托',\n    },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  animation: false,\n  x: 10,\n  y: 50,\n  width: 480,\n  height: 720,\n  node: {\n    type: 'react',\n    style: {\n      size: [120, 60],\n      ports: [{ placement: 'center' }],\n      x: (d) => d.x * 3.5,\n      y: (d) => d.y * 3.5,\n      fill: 'transparent',\n      component: (data) => <PlayerNode playerInfo={data} />,\n    },\n  },\n  plugins: [\n    {\n      type: 'background',\n      width: '480px',\n      height: '720px',\n      backgroundImage:\n        'url(https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*EmPXQLrX2xIAAAAAAAAAAAAADmJ7AQ/original)',\n      backgroundRepeat: 'no-repeat',\n      backgroundSize: 'contain',\n      opacity: 1,\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/scene-case/default/demo/fund-flow.js",
    "content": "import { Rect as GRect, Text as GText } from '@antv/g';\nimport {\n  Badge,\n  CommonEvent,\n  ExtensionCategory,\n  Graph,\n  GraphEvent,\n  iconfont,\n  Label,\n  Rect,\n  register,\n  treeToGraphData,\n} from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst COLORS = {\n  B: '#1783FF',\n  R: '#F46649',\n  Y: '#DB9D0D',\n  G: '#60C42D',\n  DI: '#A7A7A7',\n};\nconst GREY_COLOR = '#CED4D9';\n\nclass TreeNode extends Rect {\n  get data() {\n    return this.context.model.getNodeLikeDatum(this.id);\n  }\n\n  get childrenData() {\n    return this.context.model.getChildrenData(this.id);\n  }\n\n  getLabelStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2 + 8,\n      y: -height / 2 + 16,\n      text: this.data.name,\n      fontSize: 12,\n      opacity: 0.85,\n      fill: '#000',\n      cursor: 'pointer',\n    };\n  }\n\n  getPriceStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2 + 8,\n      y: height / 2 - 8,\n      text: this.data.label,\n      fontSize: 16,\n      fill: '#000',\n      opacity: 0.85,\n    };\n  }\n\n  drawPriceShape(attributes, container) {\n    const priceStyle = this.getPriceStyle(attributes);\n    this.upsert('price', GText, priceStyle, container);\n  }\n\n  getCurrencyStyle(attributes) {\n    const [, height] = this.getSize(attributes);\n    return {\n      x: this.shapeMap['price'].getLocalBounds().max[0] + 4,\n      y: height / 2 - 8,\n      text: this.data.currency,\n      fontSize: 12,\n      fill: '#000',\n      opacity: 0.75,\n    };\n  }\n\n  drawCurrencyShape(attributes, container) {\n    const currencyStyle = this.getCurrencyStyle(attributes);\n    this.upsert('currency', GText, currencyStyle, container);\n  }\n\n  getPercentStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: width / 2 - 4,\n      y: height / 2 - 8,\n      text: `${((Number(this.data.variableValue) || 0) * 100).toFixed(2)}%`,\n      fontSize: 12,\n      textAlign: 'right',\n      fill: COLORS[this.data.status],\n    };\n  }\n\n  drawPercentShape(attributes, container) {\n    const percentStyle = this.getPercentStyle(attributes);\n    this.upsert('percent', GText, percentStyle, container);\n  }\n\n  getTriangleStyle(attributes) {\n    const percentMinX = this.shapeMap['percent'].getLocalBounds().min[0];\n    const [, height] = this.getSize(attributes);\n    return {\n      fill: COLORS[this.data.status],\n      x: this.data.variableUp ? percentMinX - 18 : percentMinX,\n      y: height / 2 - 16,\n      fontFamily: 'iconfont',\n      fontSize: 16,\n      text: '\\ue62d',\n      transform: this.data.variableUp ? [] : [['rotate', 180]],\n    };\n  }\n\n  drawTriangleShape(attributes, container) {\n    const triangleStyle = this.getTriangleStyle(attributes);\n    this.upsert('triangle', Label, triangleStyle, container);\n  }\n\n  getVariableStyle(attributes) {\n    const [, height] = this.getSize(attributes);\n    return {\n      fill: '#000',\n      fontSize: 12,\n      opacity: 0.45,\n      text: this.data.variableName,\n      textAlign: 'right',\n      x: this.shapeMap['triangle'].getLocalBounds().min[0] - 4,\n      y: height / 2 - 8,\n    };\n  }\n\n  drawVariableShape(attributes, container) {\n    const variableStyle = this.getVariableStyle(attributes);\n    this.upsert('variable', GText, variableStyle, container);\n  }\n\n  getCollapseStyle(attributes) {\n    if (this.childrenData.length === 0) return false;\n    const { collapsed } = attributes;\n    const [width, height] = this.getSize(attributes);\n    return {\n      backgroundFill: '#fff',\n      backgroundHeight: 16,\n      backgroundLineWidth: 1,\n      backgroundRadius: 0,\n      backgroundStroke: GREY_COLOR,\n      backgroundWidth: 16,\n      cursor: 'pointer',\n      fill: GREY_COLOR,\n      fontSize: 16,\n      text: collapsed ? '+' : '-',\n      textAlign: 'center',\n      textBaseline: 'middle',\n      x: width / 2,\n      y: 0,\n    };\n  }\n\n  drawCollapseShape(attributes, container) {\n    const collapseStyle = this.getCollapseStyle(attributes);\n    const btn = this.upsert('collapse', Badge, collapseStyle, container);\n\n    if (btn && !Reflect.has(btn, '__bind__')) {\n      Reflect.set(btn, '__bind__', true);\n      btn.addEventListener(CommonEvent.CLICK, () => {\n        const { collapsed } = this.attributes;\n        const graph = this.context.graph;\n        if (collapsed) graph.expandElement(this.id);\n        else graph.collapseElement(this.id);\n      });\n    }\n  }\n\n  getProcessBarStyle(attributes) {\n    const { rate, status } = this.data;\n    const { radius } = attributes;\n    const color = COLORS[status];\n    const percent = `${Number(rate) * 100}%`;\n    const [width, height] = this.getSize(attributes);\n    return {\n      x: -width / 2,\n      y: height / 2 - 4,\n      width: width,\n      height: 4,\n      radius: [0, 0, radius, radius],\n      fill: `linear-gradient(to right, ${color} ${percent}, ${GREY_COLOR} ${percent})`,\n    };\n  }\n\n  drawProcessBarShape(attributes, container) {\n    const processBarStyle = this.getProcessBarStyle(attributes);\n    this.upsert('process-bar', GRect, processBarStyle, container);\n  }\n\n  getKeyStyle(attributes) {\n    const keyStyle = super.getKeyStyle(attributes);\n    return {\n      ...keyStyle,\n      fill: '#fff',\n      lineWidth: 1,\n      stroke: GREY_COLOR,\n    };\n  }\n\n  render(attributes = this.parsedAttributes, container) {\n    super.render(attributes, container);\n\n    this.drawPriceShape(attributes, container);\n    this.drawCurrencyShape(attributes, container);\n    this.drawPercentShape(attributes, container);\n    this.drawTriangleShape(attributes, container);\n    this.drawVariableShape(attributes, container);\n    this.drawProcessBarShape(attributes, container);\n    this.drawCollapseShape(attributes, container);\n  }\n}\n\nregister(ExtensionCategory.NODE, 'tree-node', TreeNode);\n\nfetch('https://assets.antv.antgroup.com/g6/decision-tree.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      data: treeToGraphData(data, {\n        getNodeData: (datum, depth) => {\n          if (!datum.style) datum.style = {};\n          datum.style.collapsed = depth >= 2;\n          if (!datum.children) return datum;\n          const { children, ...restDatum } = datum;\n          return { ...restDatum, children: children.map((child) => child.id) };\n        },\n      }),\n      node: {\n        type: 'tree-node',\n        style: {\n          size: [202, 60],\n          ports: [{ placement: 'left' }, { placement: 'right' }],\n          radius: 4,\n        },\n      },\n      edge: {\n        type: 'cubic-horizontal',\n        style: {\n          stroke: GREY_COLOR,\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'LR',\n        dropCap: false,\n        indent: 300,\n        getHeight: () => 60,\n        preLayout: false,\n      },\n      behaviors: ['zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/scene-case/default/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"battle-array.jsx\",\n      \"title\": {\n        \"zh\": \"英格兰阵容\",\n        \"en\": \"England\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*dTxvSL2wgDIAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"music-festival.js\",\n      \"title\": {\n        \"zh\": \"音乐节\",\n        \"en\": \"Music Festival\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*IytGRZ8WaSMAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"fund-flow.js\",\n      \"title\": {\n        \"zh\": \"资金流向图\",\n        \"en\": \"Fund Flow\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ImBoQIveCtYAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"organization-chart.js\",\n      \"title\": {\n        \"zh\": \"组织架构\",\n        \"en\": \"Organization Chart\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*wgoUR6dnVcsAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"performance-diagnosis-flowchart.js\",\n      \"title\": {\n        \"zh\": \"系统性能诊断流程图\",\n        \"en\": \"System Performance Diagnosis Flowchart\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*gS6BQbU7a0UAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"sub-graph.js\",\n      \"title\": {\n        \"zh\": \"子图\",\n        \"en\": \"Sub Graph\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*2HzDTrQZ910AAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"why-do-cats.js\",\n      \"title\": {\n        \"zh\": \"猫咪喜好\",\n        \"en\": \"Why Do Cats?\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ug4vTJA7QbMAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"snake-flow-diagram.js\",\n      \"title\": {\n        \"zh\": \"S 型流程图\",\n        \"en\": \"Snake Flow Diagram\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*7L51SLW2WhcAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/scene-case/default/demo/music-festival.js",
    "content": "import { Graph } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/music-festival.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const map = new Map();\n\n    data.forEach((datum) => {\n      const { actors, venuecity } = datum;\n      actors.forEach((actor) => {\n        if (!map.has(actor)) map.set(actor, new Set([venuecity]));\n        else map.get(actor).add(venuecity);\n      });\n    });\n\n    const nodes = Array.from(map)\n      .filter(([, city]) => city.size >= 2)\n      .sort((a, b) => -a[1].size + b[1].size)\n      .map(([name, city]) => ({\n        id: name,\n        data: {\n          city: Array.from(city),\n          value: city.size,\n        },\n      }));\n\n    return { nodes };\n  })\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      node: {\n        type: 'rect',\n        style: {\n          size: [100, 20],\n          radius: 5,\n          iconText: (d) => d.id,\n          iconFill: '#000',\n          iconWordWrap: true,\n          iconWordWrapWidth: 80,\n          iconFontSize: 15,\n          iconTextOverflow: '...',\n          iconMaxLines: 1,\n          labelText: (d) => d.data.city.join('\\n'),\n          labelFontSize: 12,\n          labelDy: 2,\n          labelFill: '#fff',\n        },\n        palette: {\n          type: 'group',\n          field: 'value',\n          color: [\n            '#FCE75A',\n            '#F5DB75',\n            '#EFCF90',\n            '#E8C3AB',\n            '#E1B7C6',\n            '#DBABE0',\n            '#D49FFB',\n            '#CD93FF',\n            '#B981F2',\n            '#7E45E8',\n          ],\n        },\n      },\n      layout: {\n        type: 'grid',\n        nodeSize: [100, 120],\n        sortBy: 'order',\n        cols: 5,\n      },\n      behaviors: [{ type: 'scroll-canvas', direction: 'y' }],\n      plugins: [\n        {\n          type: 'background',\n          background: '#000',\n        },\n      ],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/scene-case/default/demo/organization-chart.js",
    "content": "import { Badge, BaseBehavior, ExtensionCategory, Graph, GraphEvent, Label, Rect, register } from '@antv/g6';\n\nconst statusColors = {\n  online: '#17BEBB',\n  busy: '#E36397',\n  offline: '#B7AD99',\n};\n\nconst DEFAULT_LEVEL = 'detailed';\n\n/**\n * Draw a chart node with different ui based on the zoom level.\n */\nclass ChartNode extends Rect {\n  get data() {\n    return this.context.model.getElementDataById(this.id).data;\n  }\n\n  get level() {\n    return this.data.level || DEFAULT_LEVEL;\n  }\n\n  getLabelStyle() {\n    const text = this.data.name;\n    const labelStyle =\n      this.level === 'overview'\n        ? {\n            fill: '#fff',\n            fontSize: 20,\n            fontWeight: 600,\n            textAlign: 'center',\n            transform: [['translate', 0, 0]],\n          }\n        : {\n            fill: '#2078B4',\n            fontSize: 14,\n            fontWeight: 400,\n            textAlign: 'left',\n            transform: [['translate', -65, -15]],\n          };\n    return { text, ...labelStyle };\n  }\n\n  getKeyStyle(attributes) {\n    return {\n      ...super.getKeyStyle(attributes),\n      fill: this.level === 'overview' ? statusColors[this.data.status] : '#fff',\n    };\n  }\n\n  getPositionStyle(attributes) {\n    if (this.level === 'overview') return false;\n    return {\n      text: this.data.position,\n      fontSize: 8,\n      fontWeight: 400,\n      textTransform: 'uppercase',\n      fill: '#343f4a',\n      textAlign: 'left',\n      transform: [['translate', -65, 0]],\n    };\n  }\n\n  drawPositionShape(attributes, container) {\n    const positionStyle = this.getPositionStyle(attributes);\n    this.upsert('position', Label, positionStyle, container);\n  }\n\n  getStatusStyle(attributes) {\n    if (this.level === 'overview') return false;\n    return {\n      text: this.data.status,\n      fontSize: 8,\n      textAlign: 'left',\n      transform: [['translate', 40, -16]],\n      padding: [0, 4],\n      fill: '#fff',\n      backgroundFill: statusColors[this.data.status],\n    };\n  }\n\n  drawStatusShape(attributes, container) {\n    const statusStyle = this.getStatusStyle(attributes);\n    this.upsert('status', Badge, statusStyle, container);\n  }\n\n  getPhoneStyle(attributes) {\n    if (this.level === 'overview') return false;\n    return {\n      text: this.data.phone,\n      fontSize: 8,\n      fontWeight: 300,\n      textAlign: 'left',\n      transform: [['translate', -65, 20]],\n    };\n  }\n\n  drawPhoneShape(attributes, container) {\n    const style = this.getPhoneStyle(attributes);\n    this.upsert('phone', Label, style, container);\n  }\n\n  render(attributes = this.parsedAttributes, container = this) {\n    super.render(attributes, container);\n\n    this.drawPositionShape(attributes, container);\n\n    this.drawStatusShape(attributes, container);\n\n    this.drawPhoneShape(attributes, container);\n  }\n}\n\n/**\n * Implement a level of detail rendering, which will show different details based on the zoom level.\n */\nclass LevelOfDetail extends BaseBehavior {\n  prevLevel = DEFAULT_LEVEL;\n  levels = {\n    ['overview']: [0, 0.6],\n    ['detailed']: [0.6, Infinity],\n  };\n\n  constructor(context, options) {\n    super(context, options);\n    this.bindEvents();\n  }\n\n  update(options) {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  updateZoomLevel = async (e) => {\n    if ('scale' in e.data) {\n      const scale = e.data.scale;\n      const level = Object.entries(this.levels).find(([key, [min, max]]) => scale > min && scale <= max)?.[0];\n      if (level && this.prevLevel !== level) {\n        const { graph } = this.context;\n        graph.updateNodeData((prev) => prev.map((node) => ({ ...node, data: { ...node.data, level } })));\n        await graph.draw();\n        this.prevLevel = level;\n      }\n    }\n  };\n\n  bindEvents() {\n    const { graph } = this.context;\n    graph.on(GraphEvent.AFTER_TRANSFORM, this.updateZoomLevel);\n  }\n\n  unbindEvents() {\n    const { graph } = this.context;\n    graph.off(GraphEvent.AFTER_TRANSFORM, this.updateZoomLevel);\n  }\n\n  destroy() {\n    this.unbindEvents();\n    super.destroy();\n  }\n}\n\nregister(ExtensionCategory.NODE, 'chart-node', ChartNode);\nregister(ExtensionCategory.BEHAVIOR, 'level-of-detail', LevelOfDetail);\n\nfetch('https://assets.antv.antgroup.com/g6/organization-chart.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      data,\n      node: {\n        type: 'chart-node',\n        style: {\n          labelPlacement: 'center',\n          lineWidth: 1,\n          ports: [{ placement: 'top' }, { placement: 'bottom' }],\n          radius: 2,\n          shadowBlur: 10,\n          shadowColor: '#e0e0e0',\n          shadowOffsetX: 3,\n          size: [150, 60],\n          stroke: '#C0C0C0',\n        },\n      },\n      edge: {\n        type: 'polyline',\n        style: {\n          router: {\n            type: 'orth',\n          },\n          stroke: '#C0C0C0',\n        },\n      },\n      layout: {\n        type: 'dagre',\n      },\n      autoFit: 'view',\n      behaviors: ['level-of-detail', 'zoom-canvas', 'drag-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/scene-case/default/demo/performance-diagnosis-flowchart.js",
    "content": "import { BugOutlined } from '@ant-design/icons';\nimport { ExtensionCategory, Graph, HoverActivate, idOf, register } from '@antv/g6';\nimport { ReactNode } from '@antv/g6-extension-react';\nimport { Flex, Typography } from 'antd';\nimport { useEffect, useRef } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nconst { Text } = Typography;\n\nconst ACTIVE_COLOR = '#f6c523';\nconst COLOR_MAP = {\n  'pre-inspection': '#3fc1c9',\n  problem: '#8983f3',\n  inspection: '#f48db4',\n  solution: '#ffaa64',\n};\n\nclass HoverElement extends HoverActivate {\n  getActiveIds(event) {\n    const { model, graph } = this.context;\n    const { targetType, target } = event;\n    const targetId = target.id;\n\n    const ids = [targetId];\n    if (targetType === 'edge') {\n      const edge = model.getEdgeDatum(targetId);\n      ids.push(edge.source, edge.target);\n    } else if (targetType === 'node') {\n      ids.push(...model.getRelatedEdgesData(targetId).map(idOf));\n    }\n\n    graph.frontElement(ids);\n\n    return ids;\n  }\n}\n\nregister(ExtensionCategory.NODE, 'react', ReactNode);\nregister(ExtensionCategory.BEHAVIOR, 'hover-element', HoverElement);\n\nconst Node = ({ data }) => {\n  const { text, type } = data.data;\n\n  const isHovered = data.states?.includes('active');\n  const isSelected = data.states?.includes('selected');\n  const color = isHovered ? ACTIVE_COLOR : COLOR_MAP[type];\n\n  const containerStyle = {\n    width: '100%',\n    height: '100%',\n    background: color,\n    border: `3px solid ${color}`,\n    borderRadius: 16,\n    cursor: 'pointer',\n  };\n\n  if (isSelected) {\n    Object.assign(containerStyle, { border: `3px solid #000` });\n  }\n\n  return (\n    <Flex style={containerStyle} align=\"center\" justify=\"center\">\n      <Flex vertical style={{ padding: '8px 16px', textAlign: 'center' }} align=\"center\" justify=\"center\">\n        {type === 'problem' && <BugOutlined style={{ color: '#fff', fontSize: 24, marginBottom: 8 }} />}\n        <Text style={{ color: '#fff', fontWeight: 600, fontSize: 16 }}>{text}</Text>\n      </Flex>\n    </Flex>\n  );\n};\n\nexport const PerformanceDiagnosisFlowchart = () => {\n  const containerRef = useRef();\n\n  useEffect(() => {\n    fetch('https://assets.antv.antgroup.com/g6/performance-diagnosis.json')\n      .then((res) => res.json())\n      .then((data) => {\n        const graph = new Graph({\n          container: containerRef.current,\n          data,\n          autoFit: 'view',\n          node: {\n            type: 'react',\n            style: (d) => {\n              const style = {\n                component: <Node data={d} />,\n                ports: [{ placement: 'top' }, { placement: 'bottom' }],\n              };\n\n              const size = {\n                'pre-inspection': [240, 120],\n                problem: [200, 120],\n                inspection: [330, 100],\n                solution: [200, 120],\n              }[d.data.type] || [200, 80];\n\n              Object.assign(style, {\n                size,\n                dx: -size[0] / 2,\n                dy: -size[1] / 2,\n              });\n              return style;\n            },\n            state: {\n              active: {\n                halo: false,\n              },\n              selected: {\n                halo: false,\n              },\n            },\n          },\n          edge: {\n            type: 'polyline',\n            style: {\n              lineWidth: 3,\n              radius: 20,\n              stroke: '#8b9baf',\n              endArrow: true,\n              labelText: (d) => d.data.text,\n              labelFill: '#8b9baf',\n              labelFontWeight: 600,\n              labelBackground: true,\n              labelBackgroundFill: '#f8f8f8',\n              labelBackgroundOpacity: 1,\n              labelBackgroundLineWidth: 3,\n              labelBackgroundStroke: '#8b9baf',\n              labelPadding: [1, 10],\n              labelBackgroundRadius: 4,\n              router: {\n                type: 'orth',\n              },\n            },\n            state: {\n              active: {\n                stroke: ACTIVE_COLOR,\n                labelBackgroundStroke: ACTIVE_COLOR,\n                halo: false,\n              },\n            },\n          },\n          layout: {\n            type: 'antv-dagre',\n          },\n          behaviors: ['zoom-canvas', 'drag-canvas', 'hover-element', 'click-select'],\n        });\n\n        graph.render();\n      });\n  }, []);\n\n  return <div style={{ width: '100%', height: '100%' }} ref={containerRef}></div>;\n};\n\nconst root = createRoot(document.getElementById('container'));\nroot.render(<PerformanceDiagnosisFlowchart />);\n"
  },
  {
    "path": "packages/site/examples/scene-case/default/demo/snake-flow-diagram.js",
    "content": "import { ExtensionCategory, Graph, Polyline, positionOf, register } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: '0', data: { label: '开始流程', time: '17:00:00' } },\n    { id: '1', data: { label: '流程1', time: '17:00:05' } },\n    { id: '2', data: { label: '流程2', time: '17:00:12' } },\n    { id: '3', data: { label: '流程3', time: '17:00:30' } },\n    { id: '4', data: { label: '流程4', time: '17:02:00' } },\n    { id: '5', data: { label: '流程5', time: '17:02:40' } },\n    { id: '6', data: { label: '流程6', time: '17:05:50' } },\n    { id: '7', data: { label: '流程7', time: '17:10:00' } },\n    { id: '8', data: { label: '流程8', time: '17:11:20' } },\n    { id: '9', data: { label: '流程9', time: '17:15:00' } },\n    { id: '10', data: { label: '流程10', time: '17:30:00' } },\n    { id: '11', data: { label: '流程11' } },\n    { id: '12', data: { label: '流程12' } },\n    { id: '13', data: { label: '流程13' } },\n    { id: '14', data: { label: '流程14' } },\n    { id: '15', data: { label: '流程结束' } },\n  ],\n  edges: [\n    { source: '0', target: '1', data: { done: true } },\n    { source: '1', target: '2', data: { done: true } },\n    { source: '2', target: '3', data: { done: true } },\n    { source: '3', target: '4', data: { done: true } },\n    { source: '4', target: '5', data: { done: true } },\n    { source: '5', target: '6', data: { done: true } },\n    { source: '6', target: '7', data: { done: true } },\n    { source: '7', target: '8', data: { done: true } },\n    { source: '8', target: '9', data: { done: true } },\n    { source: '9', target: '10', data: { done: true } },\n    { source: '10', target: '11', data: { done: false } },\n    { source: '11', target: '12', data: { done: false } },\n    { source: '12', target: '13', data: { done: false } },\n    { source: '13', target: '14', data: { done: false } },\n    { source: '14', target: '15', data: { done: false } },\n  ],\n};\n\nclass SnakePolyline extends Polyline {\n  getPoints(attributes) {\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes, false);\n\n    if (sourcePoint[1] === targetPoint[1]) return [sourcePoint, targetPoint];\n\n    const prevPointId = this.context.model\n      .getRelatedEdgesData(this.sourceNode.id)\n      .filter((edge) => edge.target === this.sourceNode.id)[0]?.source;\n    if (!prevPointId) return [sourcePoint, targetPoint];\n\n    const prevPoint = positionOf(this.context.model.getNodeLikeDatum(prevPointId));\n    const offset = -(prevPoint[0] - sourcePoint[0]) / 4;\n    return [\n      sourcePoint,\n      [sourcePoint[0] + offset, sourcePoint[1]],\n      [targetPoint[0] + offset, targetPoint[1]],\n      targetPoint,\n    ];\n  }\n}\n\nregister(ExtensionCategory.EDGE, 's-polyline', SnakePolyline);\n\nconst graph = new Graph({\n  container: 'container',\n  data,\n  background: '#fafafa',\n  autoFit: 'center',\n  node: {\n    style: {\n      fill: (d) => (d.data.time ? '#1783ff' : '#d9d9d9'),\n      lineWidth: 2,\n      size: 8,\n      stroke: (d) => (d.data.time ? 'lightblue' : ''),\n      labelFontWeight: 500,\n      labelOffsetY: 8,\n      labelText: (d) => d.data.label,\n      badge: true,\n      badges: (d) => [\n        {\n          background: false,\n          fill: '#858ca6',\n          fontSize: 10,\n          offsetY: 39,\n          placement: 'bottom',\n          text: d.data.time || '--',\n        },\n      ],\n    },\n  },\n  edge: {\n    type: 's-polyline',\n    style: {\n      lineWidth: 2,\n      stroke: (d) => (d.data.done ? '#1783ff' : '#d9d9d9'),\n    },\n  },\n  layout: {\n    type: 'snake',\n    cols: 6,\n    rowGap: 200,\n    padding: [20, 140, 80],\n  },\n  behaviors: ['drag-canvas', 'zoom-canvas'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/scene-case/default/demo/sub-graph.js",
    "content": "import { BaseCombo, ExtensionCategory, Graph, HTML, isCollapsed, register } from '@antv/g6';\nimport { isEqual } from '@antv/util';\n\nclass SubGraphNode extends HTML {\n  connectedCallback() {\n    super.connectedCallback();\n    this.drawSubGraph();\n  }\n\n  render(attributes, container) {\n    super.render(attributes, container);\n    this.drawSubGraph();\n  }\n\n  get data() {\n    return this.context.graph.getElementData(this.id).data;\n  }\n\n  drawSubGraph() {\n    if (!this.isConnected) return;\n    if (isEqual(this.previousData, this.data)) return;\n    this.previousData = this.data;\n\n    const data = this.data;\n    this.drawGraphNode(data.data);\n  }\n\n  drawGraphNode(data) {\n    const [width, height] = this.getSize();\n    const container = this.getDomElement();\n    container.innerHTML = '';\n\n    const subGraph = new Graph({\n      container,\n      width,\n      height,\n      animation: false,\n      data: data,\n      node: {\n        style: {\n          labelText: (d) => d.id,\n          iconFontFamily: 'iconfont',\n          iconText: '\\ue6e5',\n        },\n      },\n      layout: {\n        type: 'force',\n        linkDistance: 50,\n      },\n      behaviors: ['zoom-canvas', { type: 'drag-canvas', enable: (event) => event.shiftKey === true }],\n      autoFit: 'view',\n    });\n\n    subGraph.render();\n\n    this.graph = subGraph;\n  }\n\n  destroy() {\n    this.graph?.destroy();\n    super.destroy();\n  }\n}\n\nclass CardCombo extends BaseCombo {\n  getKeyStyle(attributes) {\n    const keyStyle = super.getKeyStyle(attributes);\n    const [width, height] = this.getKeySize(attributes);\n    return {\n      ...keyStyle,\n      width,\n      height,\n      x: -width / 2,\n      y: -height / 2,\n    };\n  }\n\n  drawKeyShape(attributes, container) {\n    const { collapsed } = attributes;\n    const outer = this.upsert('key', 'rect', this.getKeyStyle(attributes), container);\n    if (!outer || !collapsed) {\n      this.removeCardShape();\n      return outer;\n    }\n\n    this.drawCardShape(attributes, container);\n\n    return outer;\n  }\n\n  drawCardShape(attributes, container) {\n    const [width, height] = this.getCollapsedKeySize(attributes);\n    const data = this.context.graph.getComboData(this.id).data;\n\n    const baseX = -width / 2;\n    const baseY = -height / 2;\n\n    this.upsert(\n      'card-title',\n      'text',\n      {\n        x: baseX,\n        y: baseY,\n        text: 'Group: ' + this.id,\n        textAlign: 'left',\n        textBaseline: 'top',\n        fontSize: 16,\n        fontWeight: 'bold',\n        fill: '#4083f7',\n      },\n      container,\n    );\n\n    const gap = 10;\n    const sep = (width + gap) / data.data.length;\n    data.data.forEach(({ name, value }, index) => {\n      this.upsert(\n        `card-item-name-${index}`,\n        'text',\n        {\n          x: baseX + index * sep,\n          y: baseY + 40,\n          text: name,\n          textAlign: 'left',\n          textBaseline: 'top',\n          fontSize: 12,\n          fill: 'gray',\n        },\n        container,\n      );\n      this.upsert(\n        `card-item-value-${index}`,\n        'text',\n        {\n          x: baseX + index * sep,\n          y: baseY + 60,\n          text: value + '%',\n          textAlign: 'left',\n          textBaseline: 'top',\n          fontSize: 24,\n        },\n        container,\n      );\n    });\n  }\n\n  removeCardShape() {\n    Object.entries(this.shapeMap).forEach(([key, shape]) => {\n      if (key.startsWith('card-')) {\n        delete this.shapeMap[key];\n        shape.destroy();\n      }\n    });\n  }\n}\n\nregister(ExtensionCategory.NODE, 'sub-graph', SubGraphNode);\nregister(ExtensionCategory.COMBO, 'card', CardCombo);\n\nconst getSize = (d) => {\n  const data = d.data;\n  if (data.type === 'card') return data.status === 'expanded' ? [200, 100 * data.children.length] : [200, 100];\n  else return [200, 200];\n};\n\nconst graph = new Graph({\n  container: 'container',\n  animation: false,\n  zoom: 0.8,\n  data: {\n    nodes: [\n      {\n        id: '1',\n        combo: 'A',\n        style: { x: 120, y: 70 },\n        data: {\n          data: {\n            nodes: [\n              { id: 'node-1' },\n              { id: 'node-2' },\n              { id: 'node-3' },\n              { id: 'node-4' },\n              { id: 'node-5' },\n              { id: 'node-6' },\n              { id: 'node-7' },\n              { id: 'node-8' },\n            ],\n            edges: [\n              { source: 'node-1', target: 'node-2' },\n              { source: 'node-1', target: 'node-3' },\n              { source: 'node-1', target: 'node-4' },\n              { source: 'node-1', target: 'node-5' },\n              { source: 'node-1', target: 'node-6' },\n              { source: 'node-1', target: 'node-7' },\n              { source: 'node-1', target: 'node-8' },\n            ],\n          },\n        },\n      },\n      {\n        id: '2',\n        combo: 'C',\n        style: { x: 370, y: 70 },\n        data: {\n          data: {\n            nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }],\n            edges: [\n              { source: 'node-1', target: 'node-2' },\n              { source: 'node-1', target: 'node-3' },\n              { source: 'node-1', target: 'node-4' },\n            ],\n          },\n        },\n      },\n      {\n        id: 'node-4',\n        combo: 'D',\n        style: { x: 370, y: 200 },\n        data: {\n          data: {\n            nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }],\n            edges: [\n              { source: 'node-1', target: 'node-2' },\n              { source: 'node-1', target: 'node-3' },\n              { source: 'node-1', target: 'node-4' },\n            ],\n          },\n        },\n      },\n    ],\n    edges: [],\n    combos: [\n      {\n        id: 'root',\n        data: {\n          data: [\n            { name: 'percent', value: 50 },\n            { name: 'percent', value: 45 },\n            { name: 'percent', value: 70 },\n          ],\n        },\n      },\n      {\n        id: 'A',\n        combo: 'root',\n        data: {\n          data: [\n            { name: 'percent', value: 30 },\n            { name: 'percent', value: 90 },\n          ],\n        },\n      },\n      {\n        id: 'B',\n        combo: 'root',\n        style: { collapsed: true },\n        data: {\n          data: [\n            { name: 'percent', value: 60 },\n            { name: 'percent', value: 80 },\n          ],\n        },\n      },\n      {\n        id: 'C',\n        combo: 'B',\n        style: { collapsed: true },\n        data: {\n          data: [{ name: 'percent', value: 60 }],\n        },\n      },\n      {\n        id: 'D',\n        combo: 'B',\n        style: { collapsed: true },\n        data: {\n          data: [{ name: 'percent', value: 80 }],\n        },\n      },\n    ],\n  },\n  node: {\n    type: 'sub-graph',\n    style: {\n      dx: -100,\n      dy: -50,\n      size: getSize,\n    },\n  },\n  combo: {\n    type: 'card',\n    style: {\n      collapsedSize: [200, 100],\n      collapsedMarker: false,\n      radius: 10,\n    },\n  },\n  behaviors: [\n    { type: 'drag-element', enable: (event) => event.shiftKey !== true },\n    'collapse-expand',\n    'zoom-canvas',\n    'drag-canvas',\n  ],\n  plugins: [\n    {\n      type: 'contextmenu',\n      getItems: (event) => {\n        const { targetType, target } = event;\n        if (!['node', 'combo'].includes(targetType)) return [];\n        const id = target.id;\n\n        if (targetType === 'combo') {\n          const data = graph.getComboData(id);\n          if (isCollapsed(data)) {\n            return [{ name: '展开', value: 'expanded' }];\n          } else return [{ name: '收起', value: 'collapsed' }];\n        }\n        return [{ name: '收起', value: 'collapsed' }];\n      },\n      onClick: (value, target, current) => {\n        const id = current.id;\n        const elementType = graph.getElementType(id);\n\n        if (elementType === 'node') {\n          const parent = graph.getParentData(id, 'combo');\n          if (parent) return graph.collapseElement(parent.id, false);\n        }\n\n        if (value === 'expanded') graph.expandElement(id, false);\n        else graph.collapseElement(id, false);\n      },\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/scene-case/default/demo/why-do-cats.js",
    "content": "// ref: https://whydocatsanddogs.com/cats\nimport { Renderer as CanvasRenderer } from '@antv/g-canvas';\nimport { Plugin as PluginRoughCanvasRenderer } from '@antv/g-plugin-rough-canvas-renderer';\nimport { BaseLayout, ExtensionCategory, Graph, register } from '@antv/g6';\nimport { hierarchy, pack } from '@antv/vendor/d3-hierarchy';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `\n@font-face {\nfont-family: 'handwriting';\nsrc: url('https://mass-office.alipay.com/huamei_koqzbu/afts/file/sgUeRbI3d-IAAAAAAAAAABAADnV5AQBr/font.woff2')\n  format('woff2');\n}`;\ndocument.head.appendChild(style);\n\nfunction getColor(id) {\n  const colors = [\n    '#8dd3c7',\n    '#bebada',\n    '#fb8072',\n    '#80b1d3',\n    '#fdb462',\n    '#b3de69',\n    '#fccde5',\n    '#d9d9d9',\n    '#bc80bd',\n    '#ccebc5',\n    '#ffed6f',\n  ];\n  const index = parseInt(id);\n  return colors[index % colors.length];\n}\n\nconst topics = [\n  'cat.like',\n  'cat.hate',\n  'cat.love',\n  'cat.not.like',\n  'cat.afraid_of',\n  'cat.want.to',\n  'cat.scared.of',\n  'cat.not.want_to',\n];\n\nclass BubbleLayout extends BaseLayout {\n  id = 'bubble-layout';\n\n  async execute(model, options) {\n    const { nodes = [] } = model;\n\n    const { width = 0, height = 0 } = { ...this.options, ...options };\n\n    const root = hierarchy({ id: 'root' }, (datum) => {\n      const { id } = datum;\n      if (id === 'root') return nodes.filter((node) => node.depth === 1);\n      else if (datum.depth === 2) return [];\n      else return nodes.filter((node) => node.actualParentId === id);\n    });\n\n    root.sum((d) => (+d.index_value || 0.01) ** 0.5 * 100);\n\n    pack()\n      .size([width, height])\n      .padding((node) => {\n        return node.depth === 0 ? 20 : 2;\n      })(root);\n\n    const result = { nodes: [] };\n\n    root.descendants().forEach((node) => {\n      const {\n        data: { id },\n        x,\n        y,\n        // @ts-expect-error r is exist\n        r,\n      } = node;\n\n      if (node.depth >= 1) result.nodes.push({ id, style: { x, y, size: r * 2 } });\n    });\n\n    return result;\n  }\n}\n\nregister(ExtensionCategory.LAYOUT, 'bubble-layout', BubbleLayout);\n\nfetch('https://assets.antv.antgroup.com/g6/cat-hierarchy.json')\n  .then((res) => res.json())\n  .then((rawData) => {\n    const graphData = rawData.reduce(\n      (acc, row) => {\n        const { id } = row;\n        topics.forEach((topic) => {\n          if (id.startsWith(topic)) {\n            if (id === topic) {\n              acc.nodes.push({ ...row, depth: 1 });\n            } else {\n              acc.nodes.push({ ...row, depth: 2, actualParentId: topic });\n            }\n          }\n        });\n\n        return acc;\n      },\n      { nodes: [] },\n    );\n\n    const graph = new Graph({\n      container: 'container',\n      data: graphData,\n      renderer: (layer) => {\n        const renderer = new CanvasRenderer();\n        if (layer === 'main') {\n          renderer.registerPlugin(new PluginRoughCanvasRenderer());\n        }\n        return renderer;\n      },\n      node: {\n        style: (d) => {\n          const { id, depth, id_num } = d;\n          const color = getColor(id_num);\n\n          if (depth === 1) {\n            return {\n              fill: 'none',\n              stroke: color,\n              labelFontFamily: 'handwriting',\n              labelFontSize: 20,\n              labelText: id.replace('cat.', '').replace(/\\.|_/g, ' '),\n              labelTextTransform: 'capitalize',\n              lineWidth: 1,\n              zIndex: -1,\n            };\n          }\n\n          const {\n            text,\n            style: { size: diameter },\n          } = d;\n\n          return {\n            fill: color,\n            fillOpacity: 0.7,\n            stroke: color,\n            fillStyle: 'cross-hatch',\n            hachureGap: 1.5,\n            iconFontFamily: 'handwriting',\n            iconFontSize: (diameter / text.length) * 2,\n            iconText: diameter > 20 ? text : '',\n            iconFontWeight: 'bold',\n            iconStroke: color,\n            iconLineWidth: 2,\n            lineWidth: (diameter || 20) ** 0.5 / 5,\n          };\n        },\n      },\n      layout: {\n        type: 'bubble-layout',\n        preLayout: true,\n      },\n      plugins: [\n        {\n          type: 'tooltip',\n          getContent: (event, items) => {\n            return `<span style=\"text-transform: capitalize; font-family: handwriting; font-size: 20px;\">${items[0].id.replace(/\\.|_/g, ' ')}</span>`;\n          },\n        },\n      ],\n      behaviors: [{ type: 'drag-canvas', enable: true }, 'zoom-canvas'],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/scene-case/default/index.en.md",
    "content": "---\ntitle: Scene Case\n---\n"
  },
  {
    "path": "packages/site/examples/scene-case/default/index.zh.md",
    "content": "---\ntitle: 场景案例\n---\n"
  },
  {
    "path": "packages/site/examples/scene-case/tree-graph/demo/anti-procrastination-fishbone.js",
    "content": "import { Text } from '@antv/g';\nimport { BaseTransform, ExtensionCategory, Graph, register, treeToGraphData } from '@antv/g6';\n\nconst data = {\n  id: 'Overcome \\n procrastination',\n  children: [\n    {\n      id: 'Perfectionism',\n      children: [\n        { id: 'Correctly assess the difficulty of things' },\n        { id: 'Complete first, then improve' },\n        { id: 'Just do it' },\n      ],\n    },\n    {\n      id: 'Improve concentration',\n      children: [\n        { id: 'Pomodoro Technique' },\n        { id: 'Limited time, limited quantity, only do one thing at a time' },\n        { id: 'Improve anti-interference ability, reduce interruptions' },\n      ],\n    },\n    {\n      id: 'Set a clear task management process',\n      children: [\n        { id: 'Set priorities for completed tasks' },\n        { id: 'Break down specific executable goals' },\n        { id: 'Collect-sort-sort-execute feedback-summary' },\n      ],\n    },\n    {\n      id: 'Establish positive feedback',\n      children: [{ id: 'Do what you like' }, { id: 'Spiritual motivation' }, { id: 'Material motivation' }],\n    },\n    {\n      id: 'Relax and enjoy',\n      children: [\n        { id: 'Focus on process rather than results' },\n        { id: 'Driven by needs rather than anxiety' },\n        { id: 'Accept and understand' },\n      ],\n    },\n  ],\n};\n\nlet textShape;\nconst measureText = (style) => {\n  if (!textShape) textShape = new Text({ style });\n  textShape.attr(style);\n  return textShape.getBBox().width;\n};\n\nclass AssignColorByBranch extends BaseTransform {\n  static defaultOptions = {\n    colors: [\n      '#1783FF',\n      '#F08F56',\n      '#D580FF',\n      '#00C9C9',\n      '#7863FF',\n      '#DB9D0D',\n      '#60C42D',\n      '#FF80CA',\n      '#2491B3',\n      '#17C76F',\n    ],\n  };\n\n  constructor(context, options) {\n    super(context, Object.assign({}, AssignColorByBranch.defaultOptions, options));\n  }\n\n  beforeDraw(input) {\n    const nodes = this.context.model.getNodeData();\n\n    if (nodes.length === 0) return input;\n\n    let colorIndex = 0;\n    const dfs = (nodeId, color) => {\n      const node = nodes.find((datum) => datum.id == nodeId);\n      if (!node) return;\n\n      node.style ||= {};\n      node.style.color = color || this.options.colors[colorIndex++ % this.options.colors.length];\n      node.children?.forEach((childId) => dfs(childId, node.style?.color));\n    };\n\n    nodes.filter((node) => node.depth === 1).forEach((rootNode) => dfs(rootNode.id));\n\n    return input;\n  }\n}\n\nclass ArrangeEdgeZIndex extends BaseTransform {\n  beforeDraw(input) {\n    const { model } = this.context;\n    const { nodes, edges } = model.getData();\n\n    const oneLevelNodes = nodes.filter((node) => node.depth === 1);\n    const oneLevelNodeIds = oneLevelNodes.map((node) => node.id);\n\n    edges.forEach((edge) => {\n      if (oneLevelNodeIds.includes(edge.target)) {\n        edge.style ||= {};\n        edge.style.zIndex = oneLevelNodes.length - oneLevelNodes.findIndex((node) => node.id === edge.target);\n      }\n    });\n\n    return input;\n  }\n}\n\nregister(ExtensionCategory.TRANSFORM, 'assign-color-by-branch', AssignColorByBranch);\nregister(ExtensionCategory.TRANSFORM, 'arrange-edge-z-index', ArrangeEdgeZIndex);\n\nconst getNodeSize = (id, depth) => {\n  const FONT_FAMILY = 'system-ui, sans-serif';\n  return depth === 0\n    ? [measureText({ text: id, fontSize: 24, fontWeight: 'bold', fontFamily: FONT_FAMILY }) + 80, 70]\n    : depth === 1\n      ? [measureText({ text: id, fontSize: 18, fontFamily: FONT_FAMILY }) + 50, 42]\n      : [2, 30];\n};\n\nconst graph = new Graph({\n  autoFit: 'view',\n  padding: 10,\n  data: treeToGraphData(data),\n  node: {\n    type: 'rect',\n    style: (d) => {\n      const style = {\n        radius: 8,\n        size: getNodeSize(d.id, d.depth),\n        labelText: d.id,\n        labelPlacement: 'right',\n        labelFontFamily: 'Gill Sans',\n      };\n\n      if (d.depth === 0) {\n        Object.assign(style, {\n          fill: '#EFF0F0',\n          labelFill: '#262626',\n          labelFontWeight: 'bold',\n          labelFontSize: 24,\n          labelOffsetY: 4,\n          labelPlacement: 'center',\n          labelLineHeight: 24,\n        });\n      } else if (d.depth === 1) {\n        Object.assign(style, {\n          labelFontSize: 18,\n          labelFill: '#fff',\n          labelFillOpacity: 0.9,\n          labelOffsetY: 5,\n          labelPlacement: 'center',\n          fill: d.style?.color,\n        });\n      } else {\n        Object.assign(style, {\n          fill: 'transparent',\n          labelFontSize: 16,\n          labeFill: '#262626',\n        });\n      }\n      return style;\n    },\n  },\n  edge: {\n    type: 'polyline',\n    style: {\n      lineWidth: 3,\n      stroke: function (data) {\n        return this.getNodeData(data.target).style.color || '#99ADD1';\n      },\n    },\n  },\n  layout: {\n    type: 'fishbone',\n    direction: 'LR',\n    hGap: 40,\n    vGap: 60,\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas'],\n  transforms: ['assign-color-by-branch', 'arrange-edge-z-index'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/scene-case/tree-graph/demo/indented-tree.js",
    "content": "import { Text as GText, Rect } from '@antv/g';\nimport {\n  Badge,\n  BaseBehavior,\n  BaseNode,\n  CommonEvent,\n  ExtensionCategory,\n  Graph,\n  NodeEvent,\n  Polyline,\n  iconfont,\n  idOf,\n  register,\n  subStyleProps,\n  treeToGraphData,\n} from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst rootId = 'Modeling Methods';\n\nconst COLORS = [\n  '#5B8FF9',\n  '#F6BD16',\n  '#5AD8A6',\n  '#945FB9',\n  '#E86452',\n  '#6DC8EC',\n  '#FF99C3',\n  '#1E9493',\n  '#FF9845',\n  '#5D7092',\n];\n\nconst TreeEvent = {\n  COLLAPSE_EXPAND: 'collapse-expand',\n  ADD_CHILD: 'add-child',\n};\n\nlet textShape;\nconst measureText = (text) => {\n  if (!textShape) textShape = new GText({ style: text });\n  textShape.attr(text);\n  return textShape.getBBox().width;\n};\n\nclass IndentedNode extends BaseNode {\n  static defaultStyleProps = {\n    ports: [\n      {\n        key: 'in',\n        placement: 'right-bottom',\n      },\n      {\n        key: 'out',\n        placement: 'left-bottom',\n      },\n    ],\n  };\n\n  constructor(options) {\n    Object.assign(options.style, IndentedNode.defaultStyleProps);\n    super(options);\n  }\n\n  get childrenData() {\n    return this.context.model.getChildrenData(this.id);\n  }\n\n  getKeyStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    const keyStyle = super.getKeyStyle(attributes);\n    return {\n      width,\n      height,\n      ...keyStyle,\n      fill: 'transparent',\n    };\n  }\n\n  drawKeyShape(attributes, container) {\n    const keyStyle = this.getKeyStyle(attributes);\n    return this.upsert('key', 'rect', keyStyle, container);\n  }\n\n  getLabelStyle(attributes) {\n    if (attributes.label === false || !attributes.labelText) return false;\n    return subStyleProps(this.getGraphicStyle(attributes), 'label');\n  }\n\n  drawIconArea(attributes, container) {\n    const [, h] = this.getSize(attributes);\n    const iconAreaStyle = {\n      fill: 'transparent',\n      height: 30,\n      width: 12,\n      x: -6,\n      y: h,\n      zIndex: -1,\n    };\n    this.upsert('icon-area', Rect, iconAreaStyle, container);\n  }\n\n  forwardEvent(target, type, listener) {\n    if (target && !Reflect.has(target, '__bind__')) {\n      Reflect.set(target, '__bind__', true);\n      target.addEventListener(type, listener);\n    }\n  }\n\n  getCountStyle(attributes) {\n    const { collapsed, color } = attributes;\n    if (collapsed) {\n      const [, height] = this.getSize(attributes);\n      return {\n        backgroundFill: color,\n        cursor: 'pointer',\n        fill: '#fff',\n        fontSize: 8,\n        padding: [0, 10],\n        text: `${this.childrenData.length}`,\n        textAlign: 'center',\n        y: height + 8,\n      };\n    }\n\n    return false;\n  }\n\n  drawCountShape(attributes, container) {\n    const countStyle = this.getCountStyle(attributes);\n    const btn = this.upsert('count', Badge, countStyle, container);\n\n    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {\n      event.stopPropagation();\n      this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {\n        id: this.id,\n        collapsed: false,\n      });\n    });\n  }\n\n  isShowCollapse(attributes) {\n    return !attributes.collapsed && this.childrenData.length > 0;\n  }\n\n  getCollapseStyle(attributes) {\n    const { showIcon, color } = attributes;\n    if (!this.isShowCollapse(attributes)) return false;\n    const [, height] = this.getSize(attributes);\n    return {\n      visibility: showIcon ? 'visible' : 'hidden',\n      backgroundFill: color,\n      backgroundHeight: 12,\n      backgroundWidth: 12,\n      cursor: 'pointer',\n      fill: '#fff',\n      fontFamily: 'iconfont',\n      fontSize: 8,\n      text: '\\ue6e4',\n      textAlign: 'center',\n      x: -1, // half of edge line width\n      y: height + 8,\n    };\n  }\n\n  drawCollapseShape(attributes, container) {\n    const iconStyle = this.getCollapseStyle(attributes);\n    const btn = this.upsert('collapse-expand', Badge, iconStyle, container);\n\n    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {\n      event.stopPropagation();\n      this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {\n        id: this.id,\n        collapsed: !attributes.collapsed,\n      });\n    });\n  }\n\n  getAddStyle(attributes) {\n    const { collapsed, showIcon } = attributes;\n    if (collapsed) return false;\n    const [, height] = this.getSize(attributes);\n    const color = '#ddd';\n    const lineWidth = 1;\n\n    return {\n      visibility: showIcon ? 'visible' : 'hidden',\n      backgroundFill: '#fff',\n      backgroundHeight: 12,\n      backgroundLineWidth: lineWidth,\n      backgroundStroke: color,\n      backgroundWidth: 12,\n      cursor: 'pointer',\n      fill: color,\n      fontFamily: 'iconfont',\n      text: '\\ue664',\n      textAlign: 'center',\n      x: -1,\n      y: height + (this.isShowCollapse(attributes) ? 22 : 8),\n    };\n  }\n\n  drawAddShape(attributes, container) {\n    const addStyle = this.getAddStyle(attributes);\n    const btn = this.upsert('add', Badge, addStyle, container);\n\n    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {\n      event.stopPropagation();\n      this.context.graph.emit(TreeEvent.ADD_CHILD, { id: this.id });\n    });\n  }\n\n  render(attributes = this.parsedAttributes, container = this) {\n    super.render(attributes, container);\n\n    this.drawCountShape(attributes, container);\n\n    this.drawIconArea(attributes, container);\n    this.drawCollapseShape(attributes, container);\n    this.drawAddShape(attributes, container);\n  }\n}\n\nclass IndentedEdge extends Polyline {\n  getControlPoints(attributes) {\n    const [sourcePoint, targetPoint] = this.getEndpoints(attributes, false);\n    const [sx] = sourcePoint;\n    const [, ty] = targetPoint;\n    return [[sx, ty]];\n  }\n}\n\nclass CollapseExpandTree extends BaseBehavior {\n  constructor(context, options) {\n    super(context, options);\n    this.bindEvents();\n  }\n\n  update(options) {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  bindEvents() {\n    const { graph } = this.context;\n\n    graph.on(NodeEvent.POINTER_ENTER, this.showIcon);\n    graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon);\n    graph.on(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);\n    graph.on(TreeEvent.ADD_CHILD, this.addChild);\n  }\n\n  unbindEvents() {\n    const { graph } = this.context;\n\n    graph.off(NodeEvent.POINTER_ENTER, this.showIcon);\n    graph.off(NodeEvent.POINTER_LEAVE, this.hideIcon);\n    graph.off(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);\n    graph.off(TreeEvent.ADD_CHILD, this.addChild);\n  }\n\n  status = 'idle';\n\n  showIcon = (event) => {\n    this.setIcon(event, true);\n  };\n\n  hideIcon = (event) => {\n    this.setIcon(event, false);\n  };\n\n  setIcon = (event, show) => {\n    if (this.status !== 'idle') return;\n    const { target } = event;\n    const id = target.id;\n    const { graph, element } = this.context;\n    graph.updateNodeData([{ id, style: { showIcon: show } }]);\n    element.draw({ animation: false, silence: true });\n  };\n\n  onCollapseExpand = async (event) => {\n    this.status = 'busy';\n    const { id, collapsed } = event;\n    const { graph } = this.context;\n    if (collapsed) await graph.collapseElement(id);\n    else await graph.expandElement(id);\n    this.status = 'idle';\n  };\n\n  addChild(event) {\n    const { onCreateChild = () => ({ id: `${Date.now()}`, style: { labelText: 'new node' } }) } = this.options;\n    const { graph } = this.context;\n    const datum = onCreateChild(event.id);\n    graph.addNodeData([datum]);\n    graph.addEdgeData([{ source: event.id, target: datum.id }]);\n    const parent = graph.getNodeData(event.id);\n    graph.updateNodeData([\n      { id: event.id, children: [...(parent.children || []), datum.id], style: { collapsed: false } },\n    ]);\n    graph.render();\n  }\n}\n\n/**\n * <zh/> 支持拖拽节点到其他节点下作为子节点\n *\n * <en/> Support dragging nodes to other nodes as child nodes\n */\nclass DragBranch extends BaseBehavior {\n  constructor(context, options) {\n    super(context, options);\n    this.bindEvents();\n  }\n\n  update(options) {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  bindEvents() {\n    const { graph } = this.context;\n\n    graph.on(NodeEvent.DRAG_START, this.onDragStart);\n    graph.on(NodeEvent.DRAG, this.onDrag);\n    graph.on(NodeEvent.DRAG_END, this.onDragEnd);\n    graph.on(NodeEvent.DRAG_ENTER, this.onDragEnter);\n    graph.on(NodeEvent.DRAG_LEAVE, this.onDragLeave);\n  }\n\n  unbindEvents() {\n    const { graph } = this.context;\n\n    graph.off(NodeEvent.DRAG_START, this.onDragStart);\n    graph.off(NodeEvent.DRAG, this.onDrag);\n    graph.off(NodeEvent.DRAG_END, this.onDragEnd);\n    graph.off(NodeEvent.DRAG_ENTER, this.onDragEnter);\n    graph.off(NodeEvent.DRAG_LEAVE, this.onDragLeave);\n  }\n\n  enable = true;\n\n  validate(event) {\n    if (this.destroyed) return false;\n    const { enable = (evt) => evt.target.id !== rootId } = this.options;\n    if (typeof enable === 'function') return enable(event);\n    return !!enable;\n  }\n\n  createShadow(target) {\n    const shadowStyle = subStyleProps(this.options, 'shadow');\n    const positionStyle = target.getShape('label').getBBox();\n\n    this.shadow = new Rect({\n      style: {\n        pointerEvents: 'none',\n        fill: '#F3F9FF',\n        fillOpacity: 0.5,\n        stroke: '#1890FF',\n        strokeOpacity: 0.9,\n        lineDash: [5, 5],\n        ...shadowStyle,\n        ...positionStyle,\n      },\n    });\n    this.context.canvas.appendChild(this.shadow);\n  }\n\n  moveShadow(offset) {\n    if (!this.shadow) return;\n    const [dx, dy] = offset;\n    this.shadow.translate(dx, dy);\n  }\n\n  destroyShadow() {\n    this.shadow?.remove();\n    this.shadow = undefined;\n  }\n\n  onDragStart = (event) => {\n    this.enable = this.validate(event);\n    if (!this.enable) return;\n\n    const { target } = event;\n    this.child = target;\n    this.createShadow(target);\n  };\n\n  getDelta(event) {\n    const zoom = this.context.graph.getZoom();\n    return [event.dx / zoom, event.dy / zoom];\n  }\n\n  onDrag = (event) => {\n    if (!this.enable) return;\n\n    const delta = this.getDelta(event);\n    this.moveShadow(delta);\n  };\n\n  onDragEnd = () => {\n    this.destroyShadow();\n    if (this.child === undefined || this.parent === undefined) return;\n\n    const { graph } = this.context;\n    const childId = this.child.id;\n    const parentId = this.parent.id;\n\n    const originalParent = graph.getParentData(childId, 'tree');\n\n    // 前后父节点不应该相同\n    // The previous and current parent nodes should not be the same\n    if (idOf(originalParent) === parentId) return;\n\n    // 新的父节点不应该是当前节点的子节点\n    // The new parent node should not be a child node of the current node\n    const ancestors = graph.getAncestorsData(parentId, 'tree');\n    if (ancestors.some((ancestor) => ancestor.id === childId)) return;\n\n    const edges = graph\n      .getEdgeData()\n      .filter((edge) => edge.target === childId)\n      .map(idOf);\n    graph.removeEdgeData(edges);\n    graph.updateNodeData([\n      { id: idOf(originalParent), children: originalParent?.children?.filter((child) => child !== childId) },\n    ]);\n    const modifiedParent = graph.getNodeData(parentId);\n    graph.updateNodeData([{ id: parentId, children: [...(modifiedParent.children || []), childId] }]);\n    graph.addEdgeData([{ source: parentId, target: childId }]);\n    graph.render();\n  };\n\n  onDragEnter = (event) => {\n    const { graph, element } = this.context;\n    const targetId = event.target.id;\n    if (targetId === this.child?.id || targetId === rootId) {\n      if (targetId === rootId) this.parent = event.target;\n      return;\n    }\n\n    this.parent = event.target;\n    graph.updateNodeData([{ id: targetId, states: ['selected'] }]);\n    element.draw({ animation: false, silence: true });\n  };\n\n  onDragLeave = (event) => {\n    const { graph, element } = this.context;\n    const targetId = event.target.id;\n\n    this.parent = undefined;\n    graph.updateNodeData([{ id: targetId, states: [] }]);\n    element.draw({ animation: false, silence: true });\n  };\n}\n\nregister(ExtensionCategory.NODE, 'indented', IndentedNode);\nregister(ExtensionCategory.EDGE, 'indented', IndentedEdge);\nregister(ExtensionCategory.BEHAVIOR, 'collapse-expand-tree', CollapseExpandTree);\nregister(ExtensionCategory.BEHAVIOR, 'drag-branch', DragBranch);\n\nfetch('https://assets.antv.antgroup.com/g6/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      x: 60,\n      data: treeToGraphData(data),\n      node: {\n        type: 'indented',\n        style: {\n          size: (d) => [measureText({ text: d.id, fontSize: 12 }) + 6, 20],\n          labelBackground: (datum) => datum.id === rootId,\n          labelBackgroundRadius: 0,\n          labelBackgroundFill: '#576286',\n          labelFill: (datum) => (datum.id === rootId ? '#fff' : '#666'),\n          labelText: (d) => d.style?.labelText || d.id,\n          labelTextAlign: (datum) => (datum.id === rootId ? 'center' : 'left'),\n          labelTextBaseline: 'top',\n          color: (datum) => {\n            const depth = graph.getAncestorsData(datum.id, 'tree').length - 1;\n            return COLORS[depth % COLORS.length] || '#576286';\n          },\n        },\n        state: {\n          selected: {\n            lineWidth: 0,\n            labelFill: '#40A8FF',\n            labelBackground: true,\n            labelFontWeight: 'normal',\n            labelBackgroundFill: '#e8f7ff',\n            labelBackgroundRadius: 10,\n          },\n        },\n      },\n      edge: {\n        type: 'indented',\n        style: {\n          radius: 16,\n          lineWidth: 2,\n          sourcePort: 'out',\n          targetPort: 'in',\n          stroke: (datum) => {\n            const depth = graph.getAncestorsData(datum.source, 'tree').length;\n            return COLORS[depth % COLORS.length];\n          },\n        },\n      },\n      layout: {\n        type: 'indented',\n        direction: 'LR',\n        isHorizontal: true,\n        indent: 40,\n        getHeight: () => 20,\n        getVGap: () => 10,\n      },\n      behaviors: [\n        'scroll-canvas',\n        'drag-branch',\n        'collapse-expand-tree',\n        { type: 'click-select', enable: (event) => event.targetType === 'node' && event.target.id !== rootId },\n      ],\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/scene-case/tree-graph/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"indented-tree.js\",\n      \"title\": {\n        \"zh\": \"缩进树\",\n        \"en\": \"Indented Tree\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*r0-SS5dRxykAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"mindmap.js\",\n      \"title\": {\n        \"zh\": \"思维导图\",\n        \"en\": \"Mind Map\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*xryoSY8EQWoAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"anti-procrastination-fishbone.js\",\n      \"title\": {\n        \"zh\": \"克服拖延症（决策型鱼骨图）\",\n        \"en\": \"Anti-Procrastination Fishbone Diagram\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*olIATZ-4qMEAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"product-fishbone.js\",\n      \"title\": {\n        \"zh\": \"分析产品收益（原因型鱼骨图）\",\n        \"en\": \"Product Profitability Below Expectations\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*I0VYSYD_3bUAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"radial-dendrogram.js\",\n      \"title\": {\n        \"zh\": \"径向生态树\",\n        \"en\": \"Radial Dendrogram\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*tK5USKBOc2kAAAAAAAAAAAAADmJ7AQ/original\"\n    },\n    {\n      \"filename\": \"radial-compact-tree.js\",\n      \"title\": {\n        \"zh\": \"径向紧凑树\",\n        \"en\": \"Radial Compact Tree\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*9iPNRpKfb3IAAAAAAAAAAAAADmJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/scene-case/tree-graph/demo/mindmap.js",
    "content": "import { Rect, Text } from '@antv/g';\nimport {\n  Badge,\n  BaseBehavior,\n  BaseNode,\n  BaseTransform,\n  CommonEvent,\n  CubicHorizontal,\n  ExtensionCategory,\n  Graph,\n  GraphEvent,\n  iconfont,\n  idOf,\n  NodeEvent,\n  positionOf,\n  register,\n  treeToGraphData,\n} from '@antv/g6';\n\nconst style = document.createElement('style');\nstyle.innerHTML = `@import url('${iconfont.css}');`;\ndocument.head.appendChild(style);\n\nconst RootNodeStyle = {\n  fill: '#EFF0F0',\n  labelFill: '#262626',\n  labelFontSize: 24,\n  labelFontWeight: 600,\n  labelOffsetY: 8,\n  labelPlacement: 'center',\n  ports: [{ placement: 'right' }, { placement: 'left' }],\n  radius: 8,\n};\n\nconst NodeStyle = {\n  fill: 'transparent',\n  labelPlacement: 'center',\n  labelFontSize: 16,\n  ports: [{ placement: 'right-bottom' }, { placement: 'left-bottom' }],\n};\n\nconst TreeEvent = {\n  COLLAPSE_EXPAND: 'collapse-expand',\n  ADD_CHILD: 'add-child',\n};\n\nlet textShape;\nconst measureText = (text) => {\n  if (!textShape) textShape = new Text({ style: text });\n  textShape.attr(text);\n  return textShape.getBBox().width;\n};\n\nconst getNodeWidth = (nodeId, isRoot) => {\n  const padding = isRoot ? 40 : 30;\n  const nodeStyle = isRoot ? RootNodeStyle : NodeStyle;\n  return measureText({ text: nodeId, fontSize: nodeStyle.labelFontSize, fontFamily: 'Gill Sans' }) + padding;\n};\n\nconst getNodeSize = (nodeId, isRoot) => {\n  const width = getNodeWidth(nodeId, isRoot);\n  const height = isRoot ? 48 : 32;\n  return [width, height];\n};\n\nclass MindmapNode extends BaseNode {\n  static defaultStyleProps = {\n    showIcon: false,\n  };\n\n  constructor(options) {\n    Object.assign(options.style, MindmapNode.defaultStyleProps);\n    super(options);\n  }\n\n  get childrenData() {\n    return this.context.model.getChildrenData(this.id);\n  }\n\n  get rootId() {\n    return idOf(this.context.model.getRootsData()[0]);\n  }\n\n  isShowCollapse(attributes) {\n    const { collapsed, showIcon } = attributes;\n    return !collapsed && showIcon && this.childrenData.length > 0;\n  }\n\n  getCollapseStyle(attributes) {\n    const { showIcon, color, direction } = attributes;\n    if (!this.isShowCollapse(attributes)) return false;\n    const [width, height] = this.getSize(attributes);\n\n    return {\n      backgroundFill: color,\n      backgroundHeight: 12,\n      backgroundWidth: 12,\n      cursor: 'pointer',\n      fill: '#fff',\n      fontFamily: 'iconfont',\n      fontSize: 8,\n      text: '\\ue6e4',\n      textAlign: 'center',\n      transform: direction === 'left' ? [['rotate', 90]] : [['rotate', -90]],\n      visibility: showIcon ? 'visible' : 'hidden',\n      x: direction === 'left' ? -6 : width + 6,\n      y: height,\n    };\n  }\n\n  drawCollapseShape(attributes, container) {\n    const iconStyle = this.getCollapseStyle(attributes);\n    const btn = this.upsert('collapse-expand', Badge, iconStyle, container);\n\n    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {\n      event.stopPropagation();\n      this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {\n        id: this.id,\n        collapsed: !attributes.collapsed,\n      });\n    });\n  }\n\n  getCountStyle(attributes) {\n    const { collapsed, color, direction } = attributes;\n    const count = this.context.model.getDescendantsData(this.id).length;\n    if (!collapsed || count === 0) return false;\n    const [width, height] = this.getSize(attributes);\n    return {\n      backgroundFill: color,\n      backgroundHeight: 12,\n      backgroundWidth: 12,\n      cursor: 'pointer',\n      fill: '#fff',\n      fontSize: 8,\n      text: count.toString(),\n      textAlign: 'center',\n      x: direction === 'left' ? -6 : width + 6,\n      y: height,\n    };\n  }\n\n  drawCountShape(attributes, container) {\n    const countStyle = this.getCountStyle(attributes);\n    const btn = this.upsert('count', Badge, countStyle, container);\n\n    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {\n      event.stopPropagation();\n      this.context.graph.emit(TreeEvent.COLLAPSE_EXPAND, {\n        id: this.id,\n        collapsed: false,\n      });\n    });\n  }\n\n  getAddStyle(attributes) {\n    const { collapsed, showIcon, direction } = attributes;\n    if (collapsed || !showIcon) return false;\n    const [width, height] = this.getSize(attributes);\n    const color = '#ddd';\n\n    const offsetX = this.isShowCollapse(attributes) ? 24 : 12;\n    const isRoot = this.id === this.rootId;\n\n    return {\n      backgroundFill: '#fff',\n      backgroundHeight: 12,\n      backgroundLineWidth: 1,\n      backgroundStroke: color,\n      backgroundWidth: 12,\n      cursor: 'pointer',\n      fill: color,\n      fontFamily: 'iconfont',\n      fontSize: 8,\n      text: '\\ue664',\n      textAlign: 'center',\n      x: isRoot ? width + 12 : direction === 'left' ? -offsetX : width + offsetX,\n      y: isRoot ? height / 2 : height,\n    };\n  }\n\n  getAddBarStyle(attributes) {\n    const { collapsed, showIcon, direction, color = '#1783FF' } = attributes;\n    if (collapsed || !showIcon) return false;\n    const [width, height] = this.getSize(attributes);\n\n    const offsetX = this.isShowCollapse(attributes) ? 12 : 0;\n    const isRoot = this.id === this.rootId;\n\n    const HEIGHT = 2;\n    const WIDTH = 6;\n\n    return {\n      cursor: 'pointer',\n      fill:\n        direction === 'left'\n          ? `linear-gradient(180deg, #fff 20%, ${color})`\n          : `linear-gradient(0deg, #fff 20%, ${color})`,\n      height: HEIGHT,\n      width: WIDTH,\n      x: isRoot ? width : direction === 'left' ? -offsetX - WIDTH : width + offsetX,\n      y: isRoot ? height / 2 - HEIGHT / 2 : height - HEIGHT / 2,\n      zIndex: -1,\n    };\n  }\n\n  drawAddShape(attributes, container) {\n    const addStyle = this.getAddStyle(attributes);\n    const addBarStyle = this.getAddBarStyle(attributes);\n    this.upsert('add-bar', Rect, addBarStyle, container);\n    const btn = this.upsert('add', Badge, addStyle, container);\n\n    this.forwardEvent(btn, CommonEvent.CLICK, (event) => {\n      event.stopPropagation();\n      this.context.graph.emit(TreeEvent.ADD_CHILD, { id: this.id, direction: attributes.direction });\n    });\n  }\n\n  forwardEvent(target, type, listener) {\n    if (target && !Reflect.has(target, '__bind__')) {\n      Reflect.set(target, '__bind__', true);\n      target.addEventListener(type, listener);\n    }\n  }\n\n  getKeyStyle(attributes) {\n    const [width, height] = this.getSize(attributes);\n    const keyShape = super.getKeyStyle(attributes);\n    return { width, height, ...keyShape };\n  }\n\n  drawKeyShape(attributes, container) {\n    const keyStyle = this.getKeyStyle(attributes);\n    return this.upsert('key', Rect, keyStyle, container);\n  }\n\n  render(attributes = this.parsedAttributes, container = this) {\n    super.render(attributes, container);\n\n    this.drawCollapseShape(attributes, container);\n    this.drawAddShape(attributes, container);\n\n    this.drawCountShape(attributes, container);\n  }\n}\n\nclass MindmapEdge extends CubicHorizontal {\n  get rootId() {\n    return idOf(this.context.model.getRootsData()[0]);\n  }\n\n  getKeyPath(attributes) {\n    const path = super.getKeyPath(attributes);\n    const isRoot = this.targetNode.id === this.rootId;\n    const labelWidth = getNodeWidth(this.targetNode.id, isRoot);\n\n    const [, tp] = this.getEndpoints(attributes);\n    const sign = this.sourceNode.getCenter()[0] < this.targetNode.getCenter()[0] ? 1 : -1;\n    return [...path, ['L', tp[0] + labelWidth * sign, tp[1]]];\n  }\n}\n\nclass CollapseExpandTree extends BaseBehavior {\n  constructor(context, options) {\n    super(context, options);\n    this.bindEvents();\n  }\n\n  update(options) {\n    this.unbindEvents();\n    super.update(options);\n    this.bindEvents();\n  }\n\n  bindEvents() {\n    const { graph } = this.context;\n\n    graph.on(NodeEvent.POINTER_ENTER, this.showIcon);\n    graph.on(NodeEvent.POINTER_LEAVE, this.hideIcon);\n    graph.on(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);\n    graph.on(TreeEvent.ADD_CHILD, this.addChild);\n  }\n\n  unbindEvents() {\n    const { graph } = this.context;\n\n    graph.off(NodeEvent.POINTER_ENTER, this.showIcon);\n    graph.off(NodeEvent.POINTER_LEAVE, this.hideIcon);\n    graph.off(TreeEvent.COLLAPSE_EXPAND, this.onCollapseExpand);\n    graph.off(TreeEvent.ADD_CHILD, this.addChild);\n  }\n\n  status = 'idle';\n\n  showIcon = (event) => {\n    this.setIcon(event, true);\n  };\n\n  hideIcon = (event) => {\n    this.setIcon(event, false);\n  };\n\n  setIcon = (event, show) => {\n    if (this.status !== 'idle') return;\n    const { target } = event;\n    const id = target.id;\n    const { graph, element } = this.context;\n    graph.updateNodeData([{ id, style: { showIcon: show } }]);\n    element.draw({ animation: false, silence: true });\n  };\n\n  onCollapseExpand = async (event) => {\n    this.status = 'busy';\n    const { id, collapsed } = event;\n    const { graph } = this.context;\n    await graph.frontElement(id);\n    if (collapsed) await graph.collapseElement(id);\n    else await graph.expandElement(id);\n    this.status = 'idle';\n  };\n\n  addChild = async (event) => {\n    this.status = 'busy';\n    const {\n      onCreateChild = () => {\n        const currentTime = new Date(Date.now()).toLocaleString();\n        return { id: `New Node in ${currentTime}` };\n      },\n    } = this.options;\n    const { graph } = this.context;\n    const datum = onCreateChild(event.id);\n    const parent = graph.getNodeData(event.id);\n\n    graph.addNodeData([datum]);\n    graph.addEdgeData([{ source: event.id, target: datum.id }]);\n    graph.updateNodeData([\n      {\n        id: event.id,\n        children: [...(parent.children || []), datum.id],\n        style: { collapsed: false, showIcon: false },\n      },\n    ]);\n    await graph.render();\n    await graph.focusElement(datum.id);\n    this.status = 'idle';\n  };\n}\n\nclass AssignColorByBranch extends BaseTransform {\n  static defaultOptions = {\n    colors: [\n      '#1783FF',\n      '#F08F56',\n      '#D580FF',\n      '#00C9C9',\n      '#7863FF',\n      '#DB9D0D',\n      '#60C42D',\n      '#FF80CA',\n      '#2491B3',\n      '#17C76F',\n    ],\n  };\n\n  constructor(context, options) {\n    super(context, Object.assign({}, AssignColorByBranch.defaultOptions, options));\n  }\n\n  beforeDraw(input) {\n    const nodes = this.context.model.getNodeData();\n\n    if (nodes.length === 0) return input;\n\n    let colorIndex = 0;\n    const dfs = (nodeId, color) => {\n      const node = nodes.find((datum) => datum.id == nodeId);\n      if (!node) return;\n\n      node.style ||= {};\n      node.style.color = color || this.options.colors[colorIndex++ % this.options.colors.length];\n      node.children?.forEach((childId) => dfs(childId, node.style?.color));\n    };\n\n    nodes.filter((node) => node.depth === 1).forEach((rootNode) => dfs(rootNode.id));\n\n    return input;\n  }\n}\n\nregister(ExtensionCategory.NODE, 'mindmap', MindmapNode);\nregister(ExtensionCategory.EDGE, 'mindmap', MindmapEdge);\nregister(ExtensionCategory.BEHAVIOR, 'collapse-expand-tree', CollapseExpandTree);\nregister(ExtensionCategory.TRANSFORM, 'assign-color-by-branch', AssignColorByBranch);\n\nconst getNodeSide = (nodeData, parentData) => {\n  if (!parentData) return 'center';\n\n  const nodePositionX = positionOf(nodeData)[0];\n  const parentPositionX = positionOf(parentData)[0];\n  return parentPositionX > nodePositionX ? 'left' : 'right';\n};\n\nfetch('https://assets.antv.antgroup.com/g6/algorithm-category.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const rootId = data.id;\n\n    const graph = new Graph({\n      autoFit: 'view',\n      data: treeToGraphData(data),\n      node: {\n        type: 'mindmap',\n        style: function (d) {\n          const direction = getNodeSide(d, this.getParentData(idOf(d), 'tree'));\n          const isRoot = idOf(d) === rootId;\n\n          return {\n            direction,\n            labelText: idOf(d),\n            size: getNodeSize(idOf(d), isRoot),\n            labelFontFamily: 'Gill Sans',\n            // 通过设置节点标签背景来扩大交互区域 | Expand the interaction area by setting the node label background\n            labelBackground: true,\n            labelBackgroundFill: 'transparent',\n            labelPadding: direction === 'left' ? [2, 0, 10, 40] : [2, 40, 10, 0],\n            ...(isRoot ? RootNodeStyle : NodeStyle),\n          };\n        },\n      },\n      edge: {\n        type: 'mindmap',\n        style: {\n          lineWidth: 3,\n          stroke: function (data) {\n            return this.getNodeData(data.target).style.color || '#99ADD1';\n          },\n        },\n      },\n      layout: {\n        type: 'mindmap',\n        direction: 'H',\n        getHeight: () => 30,\n        getWidth: (node) => getNodeWidth(node.id, node.id === rootId),\n        getVGap: () => 6,\n        getHGap: () => 60,\n        animation: false,\n      },\n      behaviors: ['drag-canvas', 'zoom-canvas', 'collapse-expand-tree'],\n      transforms: ['assign-color-by-branch'],\n      animation: false,\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/scene-case/tree-graph/demo/product-fishbone.js",
    "content": "import { Text } from '@antv/g';\nimport { BaseTransform, ExtensionCategory, Graph, register, treeToGraphData } from '@antv/g6';\n\nconst data = {\n  id: 'Product Profitability\\nBelow Expectations',\n  children: [\n    {\n      id: 'Problem Description',\n      children: [\n        { id: 'Brand Sales Volume' },\n        { id: 'Market Capacity' },\n        { id: 'Brand Market Share' },\n        { id: 'Total Contribution Margin' },\n      ],\n    },\n    {\n      id: 'Brand Positioning',\n      children: [{ id: 'Packaging' }, { id: 'Brand Name' }, { id: 'Selling Price' }, { id: 'Product Specifications' }],\n    },\n    {\n      id: 'Distribution Channels',\n      children: [{ id: 'Region' }, { id: 'Channel' }, { id: 'Customer Type' }, { id: 'Sales Personnel Coverage' }],\n    },\n    {\n      id: 'Market Awareness',\n      children: [\n        { id: 'Regional Weighting' },\n        { id: 'Media Mix' },\n        { id: 'Advertising Investment' },\n        { id: 'Quality Perception' },\n      ],\n    },\n    {\n      id: 'Trial Purchase',\n      children: [\n        { id: 'In-store Display' },\n        { id: 'Promotion Type' },\n        { id: 'Timing of Promotion' },\n        { id: 'Supply Assurance' },\n      ],\n    },\n    {\n      id: 'Repeat Purchase',\n      children: [\n        { id: 'Consumer Profile' },\n        { id: 'Usage Occasion' },\n        { id: 'Frequency of Use' },\n        { id: 'Returns Due to Product Issues' },\n      ],\n    },\n  ],\n};\n\nlet textShape;\nconst measureText = (style) => {\n  if (!textShape) textShape = new Text({ style });\n  textShape.attr(style);\n  return textShape.getBBox().width;\n};\n\nclass AssignColorByBranch extends BaseTransform {\n  static defaultOptions = {\n    colors: [\n      '#1783FF',\n      '#F08F56',\n      '#D580FF',\n      '#00C9C9',\n      '#7863FF',\n      '#DB9D0D',\n      '#60C42D',\n      '#FF80CA',\n      '#2491B3',\n      '#17C76F',\n    ],\n  };\n\n  constructor(context, options) {\n    super(context, Object.assign({}, AssignColorByBranch.defaultOptions, options));\n  }\n\n  beforeDraw(input) {\n    const nodes = this.context.model.getNodeData();\n\n    if (nodes.length === 0) return input;\n\n    let colorIndex = 0;\n    const dfs = (nodeId, color) => {\n      const node = nodes.find((datum) => datum.id == nodeId);\n      if (!node) return;\n\n      node.style ||= {};\n      node.style.color = color || this.options.colors[colorIndex++ % this.options.colors.length];\n      node.children?.forEach((childId) => dfs(childId, node.style?.color));\n    };\n\n    nodes.filter((node) => node.depth === 1).forEach((rootNode) => dfs(rootNode.id));\n\n    return input;\n  }\n}\n\nclass ArrangeEdgeZIndex extends BaseTransform {\n  beforeDraw(input) {\n    const { model } = this.context;\n    const { nodes, edges } = model.getData();\n\n    const oneLevelNodes = nodes.filter((node) => node.depth === 1);\n    const oneLevelNodeIds = oneLevelNodes.map((node) => node.id);\n\n    edges.forEach((edge) => {\n      if (oneLevelNodeIds.includes(edge.target)) {\n        edge.style ||= {};\n        edge.style.zIndex = oneLevelNodes.length - oneLevelNodes.findIndex((node) => node.id === edge.target);\n      }\n    });\n\n    return input;\n  }\n}\n\nregister(ExtensionCategory.TRANSFORM, 'assign-color-by-branch', AssignColorByBranch);\nregister(ExtensionCategory.TRANSFORM, 'arrange-edge-z-index', ArrangeEdgeZIndex);\n\nconst getNodeSize = (id, depth) => {\n  const FONT_FAMILY = 'system-ui, sans-serif';\n  return depth === 0\n    ? [measureText({ text: id, fontSize: 24, fontWeight: 'bold', fontFamily: FONT_FAMILY }) + 80, 90]\n    : depth === 1\n      ? [measureText({ text: id, fontSize: 18, fontFamily: FONT_FAMILY }) + 50, 42]\n      : [2, 30];\n};\n\nconst graph = new Graph({\n  autoFit: 'view',\n  padding: 30,\n  data: treeToGraphData(data),\n  node: {\n    type: 'rect',\n    style: (d) => {\n      const style = {\n        radius: 8,\n        size: getNodeSize(d.id, d.depth),\n        labelText: d.id,\n        labelPlacement: 'left',\n        labelFontFamily: 'Gill Sans',\n      };\n\n      if (d.depth === 0) {\n        Object.assign(style, {\n          fill: '#EFF0F0',\n          labelFill: '#262626',\n          labelFontWeight: 'bold',\n          labelFontSize: 24,\n          labelOffsetY: 3,\n          labelPlacement: 'center',\n          labelLineHeight: 32,\n        });\n      } else if (d.depth === 1) {\n        Object.assign(style, {\n          labelFontSize: 18,\n          labelFill: '#252525',\n          labelFillOpacity: 0.9,\n          labelOffsetY: 5,\n          labelPlacement: 'center',\n          labelFontWeight: 600,\n          fill: d.style?.color,\n          fillOpacity: 0.6,\n          lineWidth: 2,\n          stroke: '#252525',\n        });\n      } else {\n        Object.assign(style, {\n          fill: 'transparent',\n          labelFontSize: 16,\n          labeFill: '#262626',\n        });\n      }\n      return style;\n    },\n  },\n  edge: {\n    type: 'polyline',\n    style: {\n      lineWidth: 3,\n      stroke: '#252525',\n    },\n  },\n  layout: {\n    type: 'fishbone',\n    direction: 'RL',\n    hGap: 40,\n    vGap: 60,\n    getRibSep: (node) => {\n      console.log(node);\n      return node.depth === 0 ? 0 : -50;\n    },\n  },\n  behaviors: ['zoom-canvas', 'drag-canvas'],\n  transforms: ['assign-color-by-branch', 'arrange-edge-z-index'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/scene-case/tree-graph/demo/radial-compact-tree.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/flare.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      padding: 50,\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          size: 12,\n          labelText: (d) => d.id,\n          labelBackground: true,\n          labelFontSize: 14,\n          labelFontFamily: 'Gill Sans',\n        },\n      },\n      edge: {\n        type: 'cubic-radial',\n        style: {\n          lineWidth: 3,\n        },\n      },\n      layout: {\n        type: 'compact-box',\n        radial: true,\n        direction: 'RL',\n        getVGap: () => 40,\n        getHGap: () => 80,\n        preLayout: false,\n      },\n      behaviors: [\n        'drag-canvas',\n        'zoom-canvas',\n        'drag-element',\n        {\n          key: 'hover-activate',\n          type: 'hover-activate',\n          degree: 5,\n          direction: 'in',\n          inactiveState: 'inactive',\n        },\n      ],\n      transforms: ['place-radial-labels'],\n      animation: false,\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/scene-case/tree-graph/demo/radial-dendrogram.js",
    "content": "import { Graph, treeToGraphData } from '@antv/g6';\n\nfetch('https://assets.antv.antgroup.com/g6/flare.json')\n  .then((res) => res.json())\n  .then((data) => {\n    const graph = new Graph({\n      container: 'container',\n      autoFit: 'view',\n      padding: 50,\n      data: treeToGraphData(data),\n      node: {\n        style: {\n          size: 12,\n          labelText: (d) => d.id,\n          labelBackground: true,\n          labelFontSize: 14,\n          labelFontFamily: 'Gill Sans',\n        },\n      },\n      edge: {\n        type: 'cubic-radial',\n        style: {\n          lineWidth: 3,\n        },\n      },\n      layout: {\n        type: 'dendrogram',\n        radial: true,\n        preLayout: false,\n      },\n      behaviors: [\n        'drag-canvas',\n        'zoom-canvas',\n        'drag-element',\n        {\n          key: 'hover-activate',\n          type: 'hover-activate',\n          degree: 5,\n          direction: 'in',\n          inactiveState: 'inactive',\n        },\n      ],\n      transforms: ['place-radial-labels'],\n      animation: false,\n    });\n\n    graph.render();\n  });\n"
  },
  {
    "path": "packages/site/examples/scene-case/tree-graph/index.en.md",
    "content": "---\ntitle: TreeGraph Scene Case\n---\n"
  },
  {
    "path": "packages/site/examples/scene-case/tree-graph/index.zh.md",
    "content": "---\ntitle: 树图场景案例\n---\n"
  },
  {
    "path": "packages/site/examples/transform/process-parallel-edges/demo/bundle.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'A', style: { x: 50, y: 350 } },\n    { id: 'B', style: { x: 250, y: 150 } },\n    { id: 'C', style: { x: 450, y: 350 } },\n  ],\n  edges: [\n    { source: 'A', target: 'C' },\n    { source: 'C', target: 'A' },\n    ...Array.from({ length: 10 }).map((_, i) => ({\n      id: `edge:A-B${i}`,\n      source: 'A',\n      target: 'B',\n      data: {\n        label: `A->B:${i}`,\n      },\n    })),\n    ...Array.from({ length: 5 }).map((_, i) => ({\n      id: `edge:B-C${i}`,\n      source: 'B',\n      target: 'C',\n      data: {\n        label: `B->C:${i}`,\n      },\n    })),\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'center',\n  data,\n  node: {\n    style: {\n      ports: [{ placement: 'center' }],\n      labelText: (d) => d.id,\n    },\n  },\n  edge: {\n    style: {\n      labelText: (d) => d?.data?.label || `${d.source}->${d.target}`,\n    },\n  },\n  behaviors: ['drag-element'],\n  transforms: ['process-parallel-edges'],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/transform/process-parallel-edges/demo/merge.js",
    "content": "import { Graph } from '@antv/g6';\n\nconst data = {\n  nodes: [\n    { id: 'A', style: { x: 50, y: 350 } },\n    { id: 'B', style: { x: 250, y: 150 } },\n    { id: 'C', style: { x: 450, y: 350 } },\n  ],\n  edges: [\n    { source: 'A', target: 'B' },\n    { source: 'B', target: 'A' },\n    { id: 'B-C:1', source: 'B', target: 'C' },\n    { id: 'B-C:2', source: 'B', target: 'C' },\n    { source: 'A', target: 'C' },\n  ],\n};\n\nconst graph = new Graph({\n  container: 'container',\n  autoFit: 'center',\n  data,\n  node: {\n    style: {\n      labelText: (d) => d.id,\n    },\n  },\n  edge: {\n    style: {\n      labelText: (d) => d?.data?.label || `${d.source}->${d.target}`,\n      startArrow: false,\n    },\n  },\n  transforms: [\n    {\n      type: 'process-parallel-edges',\n      mode: 'merge',\n      style: {\n        halo: true,\n        haloOpacity: 0.2,\n        haloStroke: 'red',\n        startArrow: true,\n      },\n    },\n  ],\n});\n\ngraph.render();\n"
  },
  {
    "path": "packages/site/examples/transform/process-parallel-edges/demo/meta.json",
    "content": "{\n  \"title\": {\n    \"zh\": \"中文分类\",\n    \"en\": \"Category\"\n  },\n  \"demos\": [\n    {\n      \"filename\": \"bundle.js\",\n      \"title\": {\n        \"zh\": \"捆绑模式\",\n        \"en\": \"Bundle Mode\"\n      },\n      \"screenshot\": \"https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*g2p_Qa_wZcIAAAAAAAAAAABkARQnAQ\"\n    },\n    {\n      \"filename\": \"merge.js\",\n      \"title\": {\n        \"zh\": \"合并模式\",\n        \"en\": \"Merge Mode\"\n      },\n      \"screenshot\": \"https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*3dsfSZunOjEAAAAAAAAAAAAAemJ7AQ/original\"\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/site/examples/transform/process-parallel-edges/index.en.md",
    "content": "---\ntitle: Process Parallel Edges\n---\n"
  },
  {
    "path": "packages/site/examples/transform/process-parallel-edges/index.zh.md",
    "content": "---\ntitle: Process Parallel Edges 平行边\n---\n"
  },
  {
    "path": "packages/site/mako.config.json",
    "content": "{\n  \"optimization\": {\n    \"skipModules\": false,\n    \"concatenateModules\": false\n  }\n}\n"
  },
  {
    "path": "packages/site/package.json",
    "content": "{\n  \"name\": \"@antv/g6-site\",\n  \"version\": \"5.0.0\",\n  \"private\": true,\n  \"description\": \"G6 sites deployed on gh-pages\",\n  \"keywords\": [\n    \"antv\",\n    \"g6\",\n    \"graph\",\n    \"graph analysis\",\n    \"graph editor\",\n    \"graph visualization\",\n    \"relational data\",\n    \"site\"\n  ],\n  \"homepage\": \"https://g6.antv.antgroup.com\",\n  \"bugs\": {\n    \"url\": \"https://github.com/antvis/g6/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/antvis/g6\"\n  },\n  \"license\": \"MIT\",\n  \"author\": \"https://github.com/orgs/antvis/people\",\n  \"scripts\": {\n    \"build\": \"dumi build && cp ./CNAME ./dist/CNAME\",\n    \"build:memo\": \"node --expose-gc --max-old-space-size=32768 ./node_modules/dumi/bin/dumi build\",\n    \"deploy\": \"gh-pages -d dist\",\n    \"dev\": \"dumi dev\",\n    \"doc\": \"ts-node ./scripts/generate-api.ts && ts-node ./scripts/generate-doc.ts\",\n    \"doc:add\": \"node ./scripts/doc-template.mjs\",\n    \"doc:clear\": \"ts-node ./scripts/clear-doc.ts\",\n    \"doc:dev\": \"npm run doc && rm -rf ./docs/api/reference\",\n    \"doc:sort\": \"ts-node ./scripts/sort-doc.ts\",\n    \"find-unused-demos\": \"node ./scripts/find-unused-demos.js\",\n    \"format\": \"prettier --write \\\"**/*.{js,jsx,ts,tsx,json,md}\\\"\",\n    \"format:md\": \"prettier --write \\\"**/*.md\\\"\",\n    \"lint\": \"eslint ./src  --quiet && prettier ./src  --check\",\n    \"preview\": \"dumi preview\"\n  },\n  \"dependencies\": {\n    \"@ant-design/icons\": \"^5.6.1\",\n    \"@antv/algorithm\": \"^0.1.26\",\n    \"@antv/dumi-theme-antv\": \"0.8.0-beta.25\",\n    \"@antv/g\": \"^6.1.24\",\n    \"@antv/g-svg\": \"^2.0.38\",\n    \"@antv/g-webgl\": \"^2.0.47\",\n    \"@antv/g2\": \"^5.3.2\",\n    \"@antv/g6\": \"workspace:*\",\n    \"@antv/g6-extension-3d\": \"workspace:*\",\n    \"@antv/g6-extension-react\": \"workspace:*\",\n    \"@antv/layout\": \"1.2.14-beta.8\",\n    \"@antv/layout-gpu\": \"^1.1.7\",\n    \"@antv/layout-wasm\": \"^1.4.2\",\n    \"@antv/util\": \"^3.3.10\",\n    \"antd\": \"^5.25.1\",\n    \"dumi\": \"2.4.17\",\n    \"insert-css\": \"^2.0.0\",\n    \"lodash\": \"^4.17.21\",\n    \"react\": \"^19.1.0\",\n    \"react-dom\": \"^19.1.0\",\n    \"stats.js\": \"^0.17.0\",\n    \"styled-components\": \"^6.1.18\"\n  },\n  \"devDependencies\": {\n    \"@manypkg/get-packages\": \"^2.2.2\",\n    \"@microsoft/api-documenter\": \"^7.26.27\",\n    \"@microsoft/api-extractor\": \"^7.52.8\",\n    \"@microsoft/api-extractor-model\": \"^7.30.6\",\n    \"@microsoft/tsdoc\": \"^0.15.1\",\n    \"@rushstack/node-core-library\": \"^4.3.0\",\n    \"@types/fs-extra\": \"^11.0.4\",\n    \"@types/lodash\": \"^4.17.16\",\n    \"@types/react\": \"^19.1.4\",\n    \"@types/resolve\": \"^1.20.6\",\n    \"fs-extra\": \"^11.3.0\",\n    \"gh-pages\": \"^6.3.0\",\n    \"moment\": \"^2.30.1\",\n    \"prettier\": \"^3.5.3\",\n    \"resolve\": \"^1.22.10\",\n    \"xmlbuilder\": \"^15.1.1\"\n  }\n}\n"
  },
  {
    "path": "packages/site/scripts/clear-doc.ts",
    "content": "import fs from 'fs';\nimport path from 'path';\n\n/**\n * <zh/> 清理自动生成的文档\n *\n * <en/> Clear the auto-generated documents\n */\nfunction clear() {\n  const baseDir = path.resolve(__dirname, '../docs/api');\n\n  // read gitignore\n  const gitignore = fs.readFileSync(path.resolve(baseDir, '.gitignore'), 'utf-8');\n\n  // clear all files list in .gitignore\n  const lines = gitignore.split('\\n');\n\n  lines.forEach((line) => {\n    if (!line || line.startsWith('#')) return;\n\n    const file = line.trim();\n    if (file) {\n      const filepath = path.join(baseDir, file);\n\n      if (fs.existsSync(filepath)) {\n        if (fs.lstatSync(filepath).isDirectory()) {\n          console.log('Clearing folder: ', filepath);\n          fs.rmdirSync(filepath, { recursive: true });\n        } else {\n          console.log('Clearing file: ', filepath);\n          fs.unlinkSync(filepath);\n        }\n      }\n    }\n  });\n\n  // remove .gitignore\n  fs.unlinkSync('docs/api/.gitignore');\n}\n\nclear();\n"
  },
  {
    "path": "packages/site/scripts/doc-template.mjs",
    "content": "/**\n * @description 在当前目录下创建中英文文档模板\n * @param {string} path - 文件路径，指定为 $PWD\n * @param {string} title - 文件标题\n * @param {string} order - 文件排序[可选]\n * @example npm run add-doc $PWD overview\n */\nimport chalk from 'chalk';\nimport { existsSync, writeFileSync } from 'fs';\n\nconst [path, title, order] = process.argv.slice(2);\n\nif (!path || !title) {\n  console.log(chalk.bold(chalk.green('Example:')), chalk.greenBright('npm run add-doc $PWD overview'));\n} else {\n  const zhPath = `${path}/${title}.zh.md`;\n  const enPath = `${path}/${title}.en.md`;\n\n  const content =\n    '---\\n' +\n    Object.entries({ title, order })\n      .filter(([, v]) => !!v)\n      .map(([k, v]) => `${k}: ${v}`)\n      .join('\\n') +\n    '\\n---';\n\n  if (!existsSync(zhPath) && !existsSync(enPath)) {\n    writeFileSync(zhPath, content);\n    writeFileSync(enPath, content);\n  } else console.log(chalk.bold(chalk.red('Error:')), chalk.redBright('File already exists!'));\n\n  console.log(chalk.bold(chalk.green('Success:')), chalk.greenBright('Create doc file successfully!'));\n}\n"
  },
  {
    "path": "packages/site/scripts/extract-playground.js",
    "content": "import fs from 'fs';\nimport path from 'path';\n\n// 递归查找所有 markdown 文件\nfunction findMarkdownFiles(dir) {\n  const mdFiles = [];\n\n  function traverse(currentDir) {\n    const items = fs.readdirSync(currentDir, { withFileTypes: true });\n\n    for (const item of items) {\n      if (item.isDirectory()) {\n        const fullPath = path.join(currentDir, item.name);\n        traverse(fullPath);\n      } else if (item.name.endsWith('.md')) {\n        mdFiles.push(path.join(currentDir, item.name));\n      }\n    }\n  }\n\n  traverse(dir);\n  return mdFiles;\n}\n\n// 提取文件中的 Playground 组件\nfunction extractPlaygroundComponents(filePath) {\n  const content = fs.readFileSync(filePath, 'utf8');\n  const playgroundRegex = /<Playground\\s+path=\"([^\"]+)\"\\s+rid=\"([^\"]+)\"[^>]*><\\/Playground>/g;\n  const matches = [];\n\n  let match;\n  while ((match = playgroundRegex.exec(content)) !== null) {\n    matches.push({\n      fullMatch: match[0],\n      path: match[1],\n      rid: match[2],\n      filePath: filePath,\n    });\n  }\n\n  return matches;\n}\n\n// 读取对应的代码文件内容\nfunction getCodeContent(codePath, baseDir) {\n  // 构建完整的文件路径 - 修正路径构建\n  const fullCodePath = path.join(baseDir, 'examples', codePath);\n\n  if (!fs.existsSync(fullCodePath)) {\n    console.warn(`警告: 代码文件不存在: ${fullCodePath}`);\n    return null;\n  }\n\n  try {\n    return fs.readFileSync(fullCodePath, 'utf8');\n  } catch (error) {\n    console.error(`读取文件失败 ${fullCodePath}:`, error.message);\n    return null;\n  }\n}\n\n// 生成新的代码块\nfunction generateObservableCodeBlock(codeContent, rid) {\n  return `\\`\\`\\`js | ob {. inject: true }\\n${codeContent}\\n\\`\\`\\``;\n}\n\n// 处理单个 Markdown 文件\nfunction processMarkdownFile(filePath, baseDir) {\n  console.log(`处理文件: ${path.relative(baseDir, filePath)}`);\n\n  const playgroundComponents = extractPlaygroundComponents(filePath);\n  if (playgroundComponents.length === 0) {\n    return { processed: false, count: 0 };\n  }\n\n  let content = fs.readFileSync(filePath, 'utf8');\n  let replacements = 0;\n\n  for (const component of playgroundComponents) {\n    const codeContent = getCodeContent(component.path, baseDir);\n    if (codeContent) {\n      const newCodeBlock = generateObservableCodeBlock(codeContent, component.rid);\n      content = content.replace(component.fullMatch, newCodeBlock);\n      replacements++;\n      console.log(`  ✓ 替换 Playground: ${component.path} -> Observable 代码块`);\n    } else {\n      console.log(`  ✗ 跳过 Playground: ${component.path} (代码文件未找到)`);\n    }\n  }\n\n  if (replacements > 0) {\n    fs.writeFileSync(filePath, content, 'utf8');\n    return { processed: true, count: replacements };\n  }\n\n  return { processed: false, count: 0 };\n}\n\n// 主函数\nfunction main() {\n  const docsDir = path.join(__dirname, 'docs');\n  const outputPath = path.join(__dirname, 'playground-extraction-report.md');\n\n  if (!fs.existsSync(docsDir)) {\n    console.error(`错误: docs 目录不存在: ${docsDir}`);\n    return;\n  }\n\n  console.log('正在扫描 docs 目录中的 Markdown 文件...');\n  const mdFiles = findMarkdownFiles(docsDir);\n  console.log(`找到 ${mdFiles.length} 个 Markdown 文件`);\n\n  const results = [];\n  let totalProcessed = 0;\n  let totalReplacements = 0;\n\n  for (const filePath of mdFiles) {\n    const result = processMarkdownFile(filePath, __dirname);\n    if (result.processed) {\n      results.push({\n        file: path.relative(__dirname, filePath),\n        replacements: result.count,\n      });\n      totalProcessed++;\n      totalReplacements += result.count;\n    }\n  }\n\n  // 生成报告\n  let report = '# Playground 组件提取报告\\n\\n';\n  report += `## 统计信息\\n\\n`;\n  report += `- 扫描的 Markdown 文件总数: ${mdFiles.length}\\n`;\n  report += `- 处理的文件数: ${totalProcessed}\\n`;\n  report += `- 总替换数: ${totalReplacements}\\n\\n`;\n\n  if (results.length > 0) {\n    report += `## 处理详情\\n\\n`;\n    for (const result of results) {\n      report += `### ${result.file}\\n`;\n      report += `- 替换的 Playground 组件数: ${result.replacements}\\n\\n`;\n    }\n  }\n\n  report += `## 处理时间\\n\\n`;\n  report += `${new Date().toLocaleString()}\\n`;\n\n  fs.writeFileSync(outputPath, report, 'utf8');\n\n  console.log('\\n📊 处理完成!');\n  console.log(`✅ 处理了 ${totalProcessed} 个文件`);\n  console.log(`🔄 总共替换了 ${totalReplacements} 个 Playground 组件`);\n  console.log(`📄 报告已生成: ${outputPath}`);\n}\n\n// 运行脚本\nif (require.main === module) {\n  main();\n}\n\nmodule.exports = {\n  main,\n  findMarkdownFiles,\n  extractPlaygroundComponents,\n  getCodeContent,\n  processMarkdownFile,\n};\n"
  },
  {
    "path": "packages/site/scripts/generate-api.ts",
    "content": "/**\n * @file Using API Extractor to generate API Document Model files\n */\nimport { getPackages } from '@manypkg/get-packages';\nimport type { ExtractorResult, IConfigFile } from '@microsoft/api-extractor';\nimport { Extractor, ExtractorConfig } from '@microsoft/api-extractor';\nimport { EnumMemberOrder } from '@microsoft/api-extractor-model';\nimport { FileSystem } from '@rushstack/node-core-library';\nimport { execSync } from 'child_process';\nimport * as path from 'path';\n\n/**\n * Get a path relative to the base directory of this project. If called with no\n * arguments it will return the base directory.\n * @param paths - the paths to join\n * @returns the resolved path\n */\nexport function baseDir(...paths: string[]): string {\n  return path.resolve('../../', path.join(...paths));\n}\n\nconst separator = '__';\n\n/**\n * Mangle a scoped package name. Which removes the `@` symbol and adds a `__`\n * separator.\n * @example `@antv/g6` => `antv__g6`\n * @param packageName - the package name to mangle\n * @returns the mangled package name\n */\nexport function mangleScopedPackageName(packageName: string): string {\n  const [scope, name] = packageName.split('/');\n  if (name) {\n    return [scope.replace('@', ''), name].join(separator);\n  }\n\n  return scope;\n}\n\nconst reportFolderRoot = path.resolve(path.join('support', 'api'));\nconst reportTempFolderRoot = path.resolve(reportFolderRoot, 'temp');\nconst includePackages = new Set<string>(['@antv/g6', '@antv/g6-extension-react']);\n\n/**\n * Get all typed packages.\n * @returns the list of packages\n */\nasync function getTypedPackages() {\n  const packages = await getPackages(baseDir());\n  return packages.packages.filter((pkg) => {\n    const json = pkg.packageJson;\n    return !json.private && includePackages.has(json.name);\n  });\n}\n\n/**\n * Run the api extractor for each package that extracts API type information and comments.\n */\nasync function runApiExtractor() {\n  const packages = await getTypedPackages();\n\n  const packageNameSet = new Set([...packages.map((pkg) => pkg.packageJson.name), '@antv/layout']);\n\n  for (const pkg of packages) {\n    const json = pkg.packageJson;\n    const name = mangleScopedPackageName(json.name);\n    const types = (json as any).types;\n\n    if (!types) {\n      throw new Error(`unable to find \"types\" in ${pkg.dir}`);\n    }\n\n    const relativePath = path.relative(baseDir(), pkg.dir);\n    const projectFolder = baseDir(relativePath);\n    const mainEntryPointFilePath = path.join(pkg.dir, types);\n    const packageJsonFullPath = path.join(pkg.dir, 'package.json');\n    const apiJsonFilePath = path.join(reportFolderRoot, `${name}.api.json`);\n    const reportFilePath = path.join(reportFolderRoot, `${name}.api.md`);\n    const reportTempFilePath = path.join(reportTempFolderRoot, `${name}.api.md`);\n    const reportFileName = path.parse(reportFilePath).base;\n    const reportFolder = path.parse(reportFilePath).dir;\n    const reportTempFolder = path.parse(reportTempFilePath).dir;\n\n    if (FileSystem.exists(packageJsonFullPath)) {\n      const packageJson = JSON.parse(FileSystem.readFile(packageJsonFullPath));\n      if ('build:cjs' in packageJson.scripts) {\n        execSync(`cd ${projectFolder} && pnpm run build:cjs`);\n      } else {\n        execSync(`cd ${projectFolder} && pnpm run build`);\n      }\n    } else {\n      console.error('package.json 文件未找到');\n    }\n\n    const configObject: IConfigFile = {\n      projectFolder,\n      mainEntryPointFilePath,\n      apiReport: {\n        enabled: true,\n        reportFolder,\n        reportFileName,\n        reportTempFolder,\n      },\n      docModel: {\n        enabled: true,\n        apiJsonFilePath,\n      },\n      compiler: {\n        tsconfigFilePath: path.join(projectFolder, 'src', 'tsconfig.json'),\n        overrideTsconfig: {\n          moduleResolution: 'nodenext',\n        },\n        skipLibCheck: true,\n      },\n      enumMemberOrder: EnumMemberOrder.Preserve,\n      // Make `export * from 'other-remirror-packages'` to work\n      bundledPackages: [\n        ...Object.keys(pkg.packageJson.dependencies ?? {}),\n        ...Object.keys(pkg.packageJson.devDependencies ?? {}),\n        ...Object.keys(pkg.packageJson.peerDependencies ?? {}),\n      ].filter((name) => packageNameSet.has(name)),\n    };\n\n    const extractorConfig: ExtractorConfig = ExtractorConfig.prepare({\n      configObject,\n      configObjectFullPath: undefined,\n      packageJson: json as any,\n      packageJsonFullPath,\n    });\n\n    console.log(`running API Extractor for ${json.name}`);\n\n    const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, {\n      // Equivalent to the \"--local\" command-line parameter\n      localBuild: true,\n\n      // Equivalent to the \"--verbose\" command-line parameter\n      showVerboseMessages: true,\n    });\n\n    if (extractorResult.succeeded) {\n      console.log(`successfully completed API Extractor for ${json.name}`);\n    } else {\n      console.error(\n        `API Extractor completed with ${extractorResult.errorCount} errors and ${extractorResult.warningCount} warnings`,\n      );\n      throw new Error('failed to run API Extractor');\n    }\n  }\n}\n\n/**\n * Run the API extractor and then delete the temp folder.\n * Self invoking function.\n */\n(async function run() {\n  await FileSystem.deleteFolder(reportFolderRoot);\n  await runApiExtractor();\n  await FileSystem.deleteFolder(reportTempFolderRoot);\n})();\n"
  },
  {
    "path": "packages/site/scripts/generate-doc.ts",
    "content": "/**\n * @file Generate API Markdown documentation from the API Document Model files\n */\nimport { ApiModel } from '@microsoft/api-extractor-model';\nimport { FileSystem } from '@rushstack/node-core-library';\nimport * as path from 'path';\nimport { MarkdownDocumenter } from '../src/MarkdownDocumenter';\n\nconst inputFolder = 'support/api';\nconst outputFolder = 'docs/api';\n\n(async function runApiDocumenter() {\n  const apiModel: ApiModel = new ApiModel();\n\n  if (!FileSystem.exists(inputFolder)) {\n    throw new Error('The input folder does not exist: ' + inputFolder);\n  }\n  FileSystem.ensureFolder(outputFolder);\n\n  for (const filename of FileSystem.readFolderItemNames(inputFolder)) {\n    if (filename.match(/\\.api\\.json$/i)) {\n      console.log(`Reading ${filename}`);\n      const filenamePath: string = path.join(inputFolder, filename);\n      apiModel.loadPackage(filenamePath);\n    }\n  }\n\n  const markdownDocumenter: MarkdownDocumenter = new MarkdownDocumenter({\n    apiModel,\n    outputFolder,\n  });\n\n  await markdownDocumenter.generateFiles();\n})();\n"
  },
  {
    "path": "packages/site/scripts/rewrite-ob.ts",
    "content": "import fs from 'fs';\nimport path from 'path';\n\n/**\n * Rewrites code blocks from createGraph format to Graph instance format\n *\n * Transforms:\n * createGraph(options, {width: $1, height: $2})\n *\n * To:\n * import { Graph } from \"@antv/g6\"\n * const graph = new Graph({ container:'container', width: $1, height: $2, ...options })\n * graph.render()\n *\n * Also ensures that ```js | ob {} includes inject: true\n */\n\n// Regular expression to match ob code blocks with createGraph\nconst OB_CODE_BLOCK_REGEX = /```js\\s*\\|\\s*ob(.*?)\\n([\\s\\S]*?)```/g;\nconst CREATE_GRAPH_REGEX =\n  /createGraph\\(\\s*(\\{[\\s\\S]*?\\})\\s*,\\s*\\{\\s*width\\s*:\\s*(\\d+)\\s*,\\s*height\\s*:\\s*(\\d+)\\s*\\},\\s*\\);/g;\n\n/**\n * Transform createGraph code to Graph instance code\n * @param match\n * @param options\n * @param width\n * @param height\n */\nfunction transformCreateGraphToGraphInstance(match: string, options: string, width: string, height: string): string {\n  return `import { Graph } from \"@antv/g6\";\n\nconst graph = new Graph({\n  container: 'container',\n  width: ${width},\n  height: ${height},${options.trim().slice(1, -1)}\n});\n\ngraph.render();`;\n}\n\n/**\n * Ensure inject: true is included in ob options\n * @param obOptions The options string like ' { pin: false }' or ''\n * @returns Updated options string with inject: true\n */\nfunction ensureinject(obOptions: string): string {\n  // If there are no options, add them with inject: true\n  if (!obOptions || obOptions.trim() === '') {\n    return ' { inject: true }';\n  }\n\n  // Check if there are already options\n  const trimmed = obOptions.trim();\n  if (!trimmed.includes('{')) {\n    return ` { inject: true }`;\n  }\n\n  // Parse the options to see if inject already exists\n  const optionsMatch = trimmed.match(/\\{(.*)\\}/);\n  if (!optionsMatch) {\n    return ` { inject: true }`;\n  }\n\n  const optionsContent = optionsMatch[1].trim();\n  if (optionsContent === '') {\n    return ` { inject: true }`;\n  }\n\n  // Check if inject already exists\n  if (optionsContent.includes('inject:')) {\n    return obOptions;\n  }\n\n  // Add inject: true to existing options\n  const updatedOptions = trimmed.replace(/\\{(.*)\\}/, (match, content) => {\n    const separator = content.trim() ? ', ' : '';\n    return `{ ${content}${separator}inject: true }`;\n  });\n\n  return ` ${updatedOptions}`;\n}\n\n/**\n * Process a single file to rewrite ob code blocks\n * @param filePath\n */\nfunction processFile(filePath: string): void {\n  try {\n    // Read file content\n    let content = fs.readFileSync(filePath, 'utf8');\n    let modified = false;\n\n    // Find ob code blocks and replace createGraph pattern\n    content = content.replace(OB_CODE_BLOCK_REGEX, (match, obOptions: string, codeContent: string) => {\n      // Ensure inject: true is included in ob options\n      const updatedObOptions = ensureinject(obOptions);\n\n      // Replace createGraph with Graph instance in the code content\n      const updatedCodeContent = codeContent.replace(\n        CREATE_GRAPH_REGEX,\n        (createGraphMatch: string, options: string, width: string, height: string) => {\n          modified = true;\n          return transformCreateGraphToGraphInstance(createGraphMatch, options, width, height);\n        },\n      );\n\n      // If code was modified or options were updated, the file needs to be saved\n      if (updatedCodeContent !== codeContent || updatedObOptions !== obOptions) {\n        modified = true;\n      }\n\n      // Return the updated code block\n      return `\\`\\`\\`js | ob${updatedObOptions}\\n${updatedCodeContent}\\`\\`\\``;\n    });\n\n    // Save changes if the file was modified\n    if (modified) {\n      fs.writeFileSync(filePath, content, 'utf8');\n      console.log(`Transformed: ${filePath}`);\n    }\n  } catch (error) {\n    console.error(`Error processing file ${filePath}:`, error);\n  }\n}\n\n/**\n * Recursively find all markdown files in a directory\n * @param dir\n */\nasync function findMarkdownFiles(dir: string): Promise<string[]> {\n  const files: string[] = [];\n\n  // Read directory contents\n  const dirEntries = await fs.promises.readdir(dir, { withFileTypes: true });\n\n  // Process each entry\n  for (const entry of dirEntries) {\n    const fullPath = path.join(dir, entry.name);\n\n    if (entry.isDirectory()) {\n      // Recursively search subdirectories\n      const subFiles = await findMarkdownFiles(fullPath);\n      files.push(...subFiles);\n    } else if (entry.isFile() && entry.name.endsWith('.md')) {\n      // Add markdown files to result\n      files.push(fullPath);\n    }\n  }\n\n  return files;\n}\n\n/**\n * Main function to scan and process files\n */\nasync function main() {\n  try {\n    // Find all markdown files in packages/site\n    const siteDir = path.resolve('..');\n    const files = await findMarkdownFiles(siteDir);\n    console.log(`Found ${files.length} markdown files to scan`);\n\n    let processedCount = 0;\n\n    // Process each file\n    for (const file of files) {\n      processFile(file);\n      processedCount++;\n\n      // Log progress periodically\n      if (processedCount % 50 === 0) {\n        console.log(`Processed ${processedCount}/${files.length} files`);\n      }\n    }\n\n    console.log(`Completed processing ${processedCount} files`);\n  } catch (error) {\n    console.error('Error scanning files:', error);\n  }\n}\n\n// Run the script\nmain().catch((error) => {\n  console.error('Error executing script:', error);\n  process.exit(1);\n});\n"
  },
  {
    "path": "packages/site/scripts/sort-doc.ts",
    "content": "/**\n * <zh/> 对目录下的文档进行排序\n *\n * <en/> Sort the documents in the specific directory\n */\n\nimport chalk from 'chalk';\nimport fs from 'fs';\nimport readline from 'readline';\n\nprocess.stdin.setRawMode(true);\nprocess.stdin.resume();\nprocess.stdin.setEncoding('utf8');\n\nconst rl = readline.createInterface({\n  input: process.stdin,\n  output: process.stdout,\n});\n\nreadline.emitKeypressEvents(process.stdin);\n\nconst [path] = process.argv.slice(2);\n\nif (!path) {\n  console.log('Example: ts-node sort-doc.ts $PWD');\n  process.exit(1);\n}\n\nconst orderRegex = /order: ([\\d]+)/;\n\n/**\n * Get the order of the file\n * @param file file path\n * @returns order\n */\nfunction getOrder(file: string) {\n  const content = fs.readFileSync(file, 'utf-8');\n  const match = content.match(orderRegex);\n  if (match) {\n    return parseInt(match[1]);\n  }\n  return 0;\n}\n\n// order, name\nconst docs: [number, string][] = [];\n\n/**\n * Sort the docs\n */\nfunction sortDocs() {\n  // list all files in the directory\n  const files = fs.readdirSync(path);\n\n  files.forEach((filename) => {\n    const name = filename.split('.')[0];\n    const index = docs.findIndex(([, _]) => _.startsWith(name));\n    if (index === -1) {\n      const order = getOrder(`${path}/${filename}`);\n      docs.push([order, name]);\n    }\n  });\n  docs.sort(([a], [b]) => a - b);\n\n  printDocs();\n}\n\nlet currentLine = 0;\n\n/**\n * Print the docs\n */\nfunction printDocs() {\n  readline.cursorTo(process.stdout, 0, 0);\n  readline.clearScreenDown(process.stdout);\n\n  docs.forEach(([, name], index) => {\n    if (index === currentLine) console.log(`> [${index}] ${name}`);\n    else console.log(`  [${index}] ${name}`);\n  });\n\n  console.log(chalk.greenBright('Press ↑/↓ to move, Press alt + ↑/↓ to swap, Press Ctrl + C to exit'));\n  console.log(chalk.green('Enter ' + chalk.bold('y') + ' to apply sorted order'));\n}\n\n/**\n * Handle keypress event\n */\nfunction onKeypress() {\n  process.stdin.on('keypress', (ch, key) => {\n    if (!key) return;\n    if (key.meta && (key.name === 'up' || key.name === 'down')) {\n      const swapLine = key.name === 'up' ? currentLine - 1 : currentLine + 1;\n      if (swapLine >= 0 && swapLine < docs.length) {\n        const temp = docs[currentLine];\n        docs[currentLine] = docs[swapLine];\n        docs[swapLine] = temp;\n        currentLine = swapLine;\n      }\n      printDocs();\n    } else if (key.name === 'up') {\n      currentLine = (currentLine - 1 + docs.length) % docs.length;\n      printDocs();\n    } else if (key.name === 'down') {\n      currentLine = (currentLine + 1) % docs.length;\n      printDocs();\n    } else if (key.ctrl && key.name === 'c') {\n      process.exit();\n    }\n  });\n}\n\nsortDocs();\nonKeypress();\n\nrl.on('line', (input) => {\n  if (input === 'y') {\n    docs.forEach(([order, name], index) => {\n      ['zh', 'en'].forEach((lang) => {\n        const filename = `${path}/${name}.${lang}.md`;\n        if (!fs.existsSync(filename)) return;\n        let content = fs.readFileSync(filename, 'utf-8');\n\n        if (content.match(orderRegex)) {\n          content = content.replace(orderRegex, `order: ${index}`);\n        } else {\n          // append order into meta info\n          const metaRegex = /---\\n([\\s\\S]+?)\\n---/;\n          const match = content.match(metaRegex);\n          let meta = match?.[1] || '';\n\n          if (meta) meta += `\\norder: ${index}`;\n          else meta = `order: ${index}`;\n\n          content = content.replace(metaRegex, `---\\n${meta}\\n---`);\n        }\n\n        fs.writeFileSync(filename, content, 'utf-8');\n      });\n    });\n    console.log('Saved');\n    process.exit();\n  }\n});\n"
  },
  {
    "path": "packages/site/src/MarkdownDocumenter.ts",
    "content": "/* eslint-disable jsdoc/require-param */\nimport {\n  ApiAbstractMixin,\n  ApiClass,\n  ApiDeclaredItem,\n  ApiDocumentedItem,\n  ApiEnum,\n  ApiInitializerMixin,\n  ApiInterface,\n  ApiItem,\n  ApiItemKind,\n  ApiMethod,\n  ApiModel,\n  ApiNamespace,\n  ApiOptionalMixin,\n  ApiPackage,\n  ApiParameterListMixin,\n  ApiPropertyItem,\n  ApiProtectedMixin,\n  ApiReadonlyMixin,\n  ApiReleaseTagMixin,\n  ApiReturnTypeMixin,\n  ApiStaticMixin,\n  ApiTypeAlias,\n  Excerpt,\n  ExcerptToken,\n  ExcerptTokenKind,\n  IFindApiItemsResult,\n  IResolveDeclarationReferenceResult,\n  ReleaseTag,\n} from '@microsoft/api-extractor-model';\nimport {\n  DocBlock,\n  DocCodeSpan,\n  DocComment,\n  DocFencedCode,\n  DocHtmlStartTag,\n  DocLinkTag,\n  DocNode,\n  DocNodeContainer,\n  DocNodeKind,\n  DocParagraph,\n  DocPlainText,\n  DocSection,\n  StandardTags,\n  StringBuilder,\n  TSDocConfiguration,\n} from '@microsoft/tsdoc';\nimport { FileSystem, NewlineKind, PackageName } from '@rushstack/node-core-library';\nimport { camelCase, isBoolean, kebabCase, startCase, upperFirst } from 'lodash';\nimport * as path from 'path';\nimport prettier from 'prettier';\nimport { intl } from './constants';\nimport { links } from './constants/link';\nimport { Keyword, LocaleLanguage, LocaleType } from './constants/locales/enum';\nimport { CustomMarkdownEmitter } from './markdown/CustomMarkdownEmitter';\nimport { CustomDocNodes } from './nodes/CustomDocNodeKind';\nimport { DocDetails } from './nodes/DocDetails';\nimport { DocEmphasisSpan } from './nodes/DocEmphasisSpan';\nimport { DocHeading } from './nodes/DocHeading';\nimport { DocNoteBox } from './nodes/DocNoteBox';\nimport { DocPageTitle } from './nodes/DocPageTitle';\nimport { DocTable } from './nodes/DocTable';\nimport { DocTableCell } from './nodes/DocTableCell';\nimport { DocTableRow } from './nodes/DocTableRow';\nimport { DocText } from './nodes/DocText';\nimport { DocUnorderedList } from './nodes/DocUnorderedList';\nimport { Utilities } from './utils/Utilities';\nimport {\n  ICustomExcerptToken,\n  findAccessorExcerptTokens,\n  liftPrefixExcerptTokens,\n  parseExcerptTokens,\n} from './utils/excerpt-token';\nimport { initGitignore, syncToGitignore } from './utils/gitignore';\nimport { getBlockTagByName } from './utils/parser';\n\nconst referenceFoldername = 'reference';\n\nconst supportedApiItems = [ApiItemKind.Interface, ApiItemKind.Enum, ApiItemKind.Class, ApiItemKind.TypeAlias];\n\n// 需要跳过解析的复杂类型 | Complex types that need to be skipped for parsing\nexport const interfacesToSkipParsing = ['BaseNodeStyleProps', 'BaseEdgeStyleProps', 'BaseComboStyleProps'];\n\nconst typesToSkipParsing = ['CallableValue'];\n\n/**\n * A page and its associated API items.\n */\nexport interface IPageData {\n  readonly name: string;\n  readonly apiItems: ApiItem[];\n  readonly group: string;\n}\n\nexport interface ICollectedData {\n  readonly apiModel: ApiModel;\n  /**\n   * Page data keyed by page name.\n   * Entries in this object are unique.\n   */\n  readonly pagesByName: Map<string, IPageData>;\n  /**\n   * `pagesByName` re-keyed by API name for lookup convenience.\n   * Entries are the same objects from `pagesByName`, but each page may appear multiple times.\n   */\n  readonly pagesByApi: Map<string, IPageData>;\n}\n\nexport interface IMarkdownDocumenterOptions {\n  apiModel: ApiModel;\n  outputFolder: string;\n}\n\nexport class MarkdownDocumenter {\n  private readonly _apiModel: ApiModel;\n  private readonly _tsdocConfiguration: TSDocConfiguration;\n  private readonly _markdownEmitter: CustomMarkdownEmitter;\n  private readonly _outputFolder: string;\n\n  private locale: LocaleLanguage = LocaleLanguage.EN;\n  /**\n   * The reference level of the current page, used to determine the heading level of the current page.\n   * 0: API Reference\n   * 1: Extension\n   * 2: Element Style Props\n   */\n  private referenceLevel = 0;\n\n  public constructor(options: IMarkdownDocumenterOptions) {\n    this._apiModel = options.apiModel;\n    this._outputFolder = options.outputFolder;\n    this._tsdocConfiguration = CustomDocNodes.configuration;\n    this._markdownEmitter = new CustomMarkdownEmitter(this._apiModel);\n  }\n\n  public async generateFiles() {\n    initGitignore(this._outputFolder);\n\n    const collectedData = this._initPageData(this._apiModel);\n\n    // Write the API model page\n    this.referenceLevel = 0;\n    await this._generateBilingualPages(this._writeApiItemPage.bind(this), this._apiModel);\n\n    // Write the API pages classified by extension\n    for (const [_, pageData] of collectedData.pagesByName.entries()) {\n      // 对于交互、插件、布局、数据处理\n      const extensions = ['behaviors', 'plugins', 'layouts', 'transforms'];\n      if (extensions.includes(pageData.group) && !pageData.name.startsWith('Base')) {\n        this.referenceLevel = 1;\n        await this._generateBilingualPages(this._writeExtensionPage.bind(this), pageData);\n      }\n\n      // // 对于数据\n      // if (pageData.group === 'spec' && pageData.name === 'Data') {\n      //   const dataTypes = ['GraphData', 'NodeData', 'EdgeData', 'ComboData'];\n      //   dataTypes.forEach(async (name) => {\n      //     this.referenceLevel = 1;\n      //     const apiInterface = pageData.apiItems.find(\n      //       (apiItem) => apiItem instanceof ApiInterface && apiItem.displayName === name,\n      //     ) as ApiInterface;\n      //     if (apiInterface) {\n      //       await this._generateBilingualPages(this._writeDataPage.bind(this), apiInterface);\n      //     }\n      //   });\n      // }\n\n      // 对于元素\n      // if (['elements/nodes', 'elements/edges', 'elements/combos'].includes(pageData.group)) {\n      //   this.referenceLevel = 2;\n      //   await this._generateBilingualPages(this._writeElementPage.bind(this), pageData);\n      // }\n\n      // 对于图实例，将拆分成三个页面： 配置项，实例方法，属性\n      // For graph instance, split into three pages: options, methods, properties\n      if (pageData.group === 'runtime' && pageData.name === 'Graph') {\n        this.referenceLevel = 1;\n        // this._generateBilingualPages(this._writeGraphOptionsPage.bind(this), pageData);\n        // this._generateBilingualPages(this._writeGraphMethodsPage.bind(this), pageData);\n        // this._generateBilingualPages(this._writeGraphPropertiesPage.bind(this), pageData);\n      }\n    }\n  }\n\n  private _initPageData(apiModel: ApiModel): ICollectedData {\n    const collectedData: ICollectedData = {\n      apiModel,\n      pagesByName: new Map(),\n      pagesByApi: new Map(),\n    };\n\n    for (const apiPackage of collectedData.apiModel.packages) {\n      for (const entryPoint of apiPackage.entryPoints) {\n        this._initPageDataForItem(collectedData, entryPoint, apiPackage.entryPoints as unknown as ApiItem[]);\n      }\n    }\n\n    return collectedData;\n  }\n\n  private _initPageDataForItem(collectedData: ICollectedData, apiItem: ApiItem, siblingApiItems?: ApiItem[]) {\n    if (\n      supportedApiItems.includes(apiItem.kind as unknown as ApiItemKind) &&\n      apiItem instanceof ApiDeclaredItem &&\n      apiItem.fileUrlPath\n    ) {\n      const paths = apiItem.fileUrlPath?.split('/').slice(1);\n      // For elements, group by the first two levels of the path, such as `elements.nodes`\n      // For others, group by the first level of the path, such as `behaviors`, `plugins`, `layouts`\n      const topGroup = paths?.[0];\n      let group = topGroup === 'elements' ? paths.slice(0, 2).join('/') : topGroup;\n      const target = paths?.[paths.length - 1].replace(/\\.d\\.ts$/, '');\n      let pageName = upperFirst(camelCase(target === 'index' ? paths?.[paths.length - 2] : target));\n\n      if (topGroup === 'elements' && paths[1] === 'combos') {\n        const elementType = upperFirst(paths[1].slice(0, -1));\n        if (!pageName.endsWith(elementType)) pageName += elementType;\n      }\n\n      // Special handling for @antv/layout\n      if (apiItem.fileUrlPath.includes('@antv/layout')) {\n        group = 'layouts';\n        const name =\n          target === 'types'\n            ? paths?.[paths.length - 2] === 'lib'\n              ? (apiItem as ApiInterface).name.replace('LayoutOptions', '')\n              : paths?.[paths.length - 2]\n            : target;\n        pageName = upperFirst(camelCase(name) + 'Layout');\n      }\n\n      if (!pageName) return;\n\n      let pageData = collectedData.pagesByName.get(pageName);\n\n      if (!pageData) {\n        pageData = {\n          name: pageName,\n          apiItems: [],\n          group,\n        };\n        collectedData.pagesByName.set(pageName, pageData);\n      }\n      collectedData.pagesByApi.set(apiItem.displayName, pageData);\n      pageData.apiItems.push(apiItem);\n    }\n\n    for (const memberApiItem of apiItem.members) {\n      this._initPageDataForItem(collectedData, memberApiItem, apiItem.members as ApiItem[]);\n    }\n  }\n\n  /**\n   * Generate bilingual pages\n   */\n  private async _generateBilingualPages<T>(func: (params: T) => Promise<void>, params: T) {\n    const languages = [LocaleLanguage.EN, LocaleLanguage.ZH];\n\n    for (const language of languages) {\n      this.locale = language;\n      await func(params);\n    }\n  }\n\n  private async _writeApiItemPage(apiItem: ApiItem) {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n    const output: DocSection = new DocSection({ configuration });\n\n    const scopedName: string = apiItem.getScopedNameWithinPackage();\n    let title = '';\n\n    switch (apiItem.kind) {\n      case ApiItemKind.Class:\n        title = `${scopedName} class`;\n        break;\n      case ApiItemKind.Enum:\n        title = `${scopedName} enum`;\n        break;\n      case ApiItemKind.Interface:\n        title = `${scopedName} interface`;\n        break;\n      case ApiItemKind.Constructor:\n      case ApiItemKind.ConstructSignature:\n        title = scopedName;\n        break;\n      case ApiItemKind.Method:\n      case ApiItemKind.MethodSignature:\n        title = `${scopedName} method`;\n        break;\n      case ApiItemKind.Function:\n        title = `${scopedName} function`;\n        break;\n      case ApiItemKind.Model:\n        title = `API Reference`;\n        break;\n      case ApiItemKind.Namespace:\n        title = `${scopedName} namespace`;\n        break;\n      case ApiItemKind.Package: {\n        const unscopedPackageName: string = PackageName.getUnscopedName(apiItem.displayName);\n        title = unscopedPackageName;\n        break;\n      }\n      case ApiItemKind.Property:\n      case ApiItemKind.PropertySignature:\n        title = `${scopedName} property`;\n        break;\n      case ApiItemKind.TypeAlias:\n        title = `${scopedName} type`;\n        break;\n      case ApiItemKind.Variable:\n        title = `${scopedName} variable`;\n        break;\n      default:\n        throw new Error('Unsupported API item kind: ' + apiItem.kind);\n    }\n\n    this._appendPageTitle(output, title);\n    this._writeBreadcrumb(output, apiItem);\n    output.appendNode(new DocHeading({ configuration, title }));\n\n    if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) {\n      if (apiItem.releaseTag === ReleaseTag.Alpha) {\n        this._writeAlphaWarning(output);\n      } else if (apiItem.releaseTag === ReleaseTag.Beta) {\n        this._writeBetaWarning(output);\n      }\n    }\n\n    const decoratorBlocks: DocBlock[] = [];\n\n    if (apiItem instanceof ApiDocumentedItem) {\n      const tsdocComment: DocComment | undefined = apiItem.tsdocComment;\n\n      if (tsdocComment) {\n        decoratorBlocks.push(\n          ...tsdocComment.customBlocks.filter(\n            (block) => block.blockTag.tagNameWithUpperCase === StandardTags.decorator.tagNameWithUpperCase,\n          ),\n        );\n\n        if (tsdocComment.deprecatedBlock) {\n          output.appendNode(\n            new DocNoteBox({ configuration }, [\n              new DocParagraph({ configuration }, [\n                new DocPlainText({\n                  configuration,\n                  text: 'Warning: This API is now obsolete. ',\n                }),\n              ]),\n              ...tsdocComment.deprecatedBlock.content.nodes,\n            ]),\n          );\n        }\n        this._writeSummarySection(output, apiItem);\n      }\n    }\n\n    if (apiItem instanceof ApiDeclaredItem) {\n      if (apiItem.excerpt.text.length > 0) {\n        output.appendNode(\n          new DocFencedCode({\n            configuration,\n            code: apiItem.getExcerptWithModifiers(),\n            language: 'typescript',\n          }),\n        );\n      }\n\n      this._writeHeritageTypes(output, apiItem);\n    }\n\n    if (decoratorBlocks.length > 0) {\n      output.appendNodeInParagraph(\n        new DocEmphasisSpan({ configuration, bold: true }, [new DocPlainText({ configuration, text: 'Decorators:' })]),\n      );\n      for (const decoratorBlock of decoratorBlocks) {\n        output.appendNodes(decoratorBlock.content.nodes);\n      }\n    }\n\n    let appendRemarks: boolean = true;\n    switch (apiItem.kind) {\n      case ApiItemKind.Class:\n      case ApiItemKind.Interface:\n      case ApiItemKind.Namespace:\n      case ApiItemKind.Package:\n        this._writeRemarksSection(output, apiItem);\n        appendRemarks = false;\n        break;\n    }\n\n    switch (apiItem.kind) {\n      case ApiItemKind.Class:\n        this._writeClassTables(output, apiItem as ApiClass);\n        break;\n      case ApiItemKind.Enum:\n        this._writeEnumTables(output, apiItem as ApiEnum);\n        break;\n      case ApiItemKind.Interface:\n        this._writeInterfaceTables(output, apiItem as ApiInterface);\n        break;\n      case ApiItemKind.Constructor:\n      case ApiItemKind.ConstructSignature:\n      case ApiItemKind.Method:\n      case ApiItemKind.MethodSignature:\n      case ApiItemKind.Function:\n        this._writeParameterTables(output, apiItem as ApiParameterListMixin);\n        this._writeThrowsSection(output, apiItem);\n        break;\n      case ApiItemKind.Namespace:\n        this._writePackageOrNamespaceTables(output, apiItem as ApiNamespace);\n        break;\n      case ApiItemKind.Model:\n        this._writeModelTable(output, apiItem as ApiModel);\n        break;\n      case ApiItemKind.Package:\n        this._writePackageOrNamespaceTables(output, apiItem as ApiPackage);\n        break;\n      case ApiItemKind.Property:\n      case ApiItemKind.PropertySignature:\n        break;\n      case ApiItemKind.TypeAlias:\n        break;\n      case ApiItemKind.Variable:\n        break;\n      default:\n        throw new Error('Unsupported API item kind: ' + apiItem.kind);\n    }\n\n    if (appendRemarks) {\n      this._writeRemarksSection(output, apiItem);\n    }\n\n    const filename: string = path.join(this._outputFolder, referenceFoldername, this._getFilenameForApiItem(apiItem));\n\n    if (filename.startsWith('index')) return;\n\n    await this._writeFile(filename, output, apiItem);\n  }\n\n  private async _writeExtensionPage(pageData: IPageData) {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n    const output: DocSection = new DocSection({ configuration });\n\n    this._appendPageTitle(output, pageData.name);\n\n    const apiClass = pageData.apiItems.find((apiItem) => apiItem instanceof ApiClass) as ApiClass;\n    const apiInterface = pageData.apiItems.find((apiItem) => apiItem instanceof ApiInterface) as ApiInterface;\n\n    if (apiClass) {\n      this._writeRemarksSection(output, apiClass);\n    }\n\n    this._assertDemo(output, pageData);\n\n    if (apiInterface) {\n      this._writeOptions(output, apiInterface, { includeExcerptTokens: true });\n    }\n\n    if (apiClass) this._writeAPIMethods(output, apiClass);\n\n    const filename: string = path.join(this._outputFolder, pageData.group, `${pageData.name}.${this._getLang()}.md`);\n\n    await this._writeFile(\n      filename,\n      output,\n      pageData.apiItems.find((apiItem) => apiItem instanceof ApiClass),\n    );\n  }\n\n  private async _writeDataPage(apiInterface: ApiInterface) {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n    const output: DocSection = new DocSection({ configuration });\n\n    const name = apiInterface.displayName;\n\n    const DATA_INDEX: Record<string, number> = {\n      GraphData: 0,\n      NodeData: 1,\n      EdgeData: 2,\n      ComboData: 3,\n    };\n\n    this._appendPageTitle(output, name, DATA_INDEX[name]);\n\n    this._writeRemarksSection(output, apiInterface);\n\n    this._writeOptions(output, apiInterface, { includeExcerptTokens: true, showTitle: false });\n\n    const filename: string = path.join(this._outputFolder, 'data', `${name}.${this._getLang()}.md`);\n\n    await this._writeFile(filename, output);\n  }\n\n  private async _writeElementPage(pageData: IPageData) {\n    const isBase = pageData.name.startsWith('Base');\n    // BaseNodeStyleProps 跳过自动生成\n    if (isBase) return;\n\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n    const output: DocSection = new DocSection({ configuration });\n\n    this._appendPageTitle(output, pageData.name);\n\n    const apiInterface = pageData.apiItems.find((apiItem) => apiItem instanceof ApiInterface) as ApiInterface;\n    const apiClass = pageData.apiItems.find((apiItem) => apiItem instanceof ApiClass) as ApiClass;\n\n    if (apiClass && !isBase) {\n      this._writeRemarksSection(output, apiClass);\n    }\n\n    this._assertDemo(output, pageData);\n\n    if (apiInterface) {\n      this._writeElementOptions(output, apiInterface, pageData, isBase);\n    }\n\n    const filename: string = path.join(this._outputFolder, pageData.group, `${pageData.name}.${this._getLang()}.md`);\n\n    await this._writeFile(filename, output);\n  }\n\n  /**\n   * Special for Element options, which needs to handle `Prefix`\n   */\n  private _writeElementOptions(output: DocSection, apiInterface: ApiInterface, pageData: IPageData, isBase: boolean) {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    if (!isBase) {\n      const elementType = pageData.group.split('/')[1].slice(0, -1);\n      const baseStyleFileName = upperFirst(camelCase(`base ${elementType}`));\n      const quoteText =\n        '> ' +\n        this._intl('base-props-style-tip', LocaleType.HELPER) +\n        this._getLinkInMarkdownFormat(baseStyleFileName, `./${baseStyleFileName}.${this._getLang()}.md`);\n      output.appendNodeInParagraph(new DocText({ configuration, text: quoteText }));\n    }\n\n    this._writeOptions(output, apiInterface, { showTitle: false, includeExcerptTokens: true });\n  }\n\n  private _getLinkFromExcerptToken(excerptToken: ICustomExcerptToken): string | undefined {\n    return this._getLinkFromlinks(excerptToken) || this._getLinkFromCanonicalReference(excerptToken);\n  }\n\n  private _getLinkFromlinks(excerptToken: ICustomExcerptToken): string | undefined {\n    return links[excerptToken.text];\n  }\n\n  private _getLinkFromCanonicalReference(excerptToken: ICustomExcerptToken): string | undefined {\n    let link: string | undefined = undefined;\n\n    if (excerptToken.canonicalReference) {\n      const apiItemResult = this._apiModel.resolveDeclarationReference(\n        excerptToken.canonicalReference,\n        undefined,\n      ).resolvedApiItem;\n\n      if (apiItemResult && apiItemResult instanceof ApiInterface) {\n        link = this._getLinkFilenameForApiItem(apiItemResult);\n      }\n    }\n\n    return link;\n  }\n\n  private _writeOptions(\n    output: DocSection,\n    apiItem: ApiInterface | ApiClass,\n    options?: { showTitle?: boolean; prefix?: string; includeExcerptTokens?: boolean },\n  ) {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n    const { showTitle = true, prefix = '', includeExcerptTokens } = options || {};\n    const apiMembers: readonly ApiItem[] = this._getMembersAndWriteIncompleteWarning(apiItem, output);\n\n    if (showTitle && apiMembers.length > 0) {\n      output.appendNode(new DocHeading({ configuration, title: this._intl(Keyword.OPTIONS) }));\n    }\n\n    if (includeExcerptTokens && this._isApiInterface(apiItem)) {\n      const filteredTokens = this._extractAndFilterExcerptTokens(apiItem as ApiInterface);\n      this._writeExcerptTokens(output, filteredTokens);\n    }\n\n    apiMembers.forEach((apiMember) => {\n      if (this._isApiProperty(apiMember)) {\n        this._writePropertySections(output, apiMember, prefix, configuration);\n      }\n    });\n\n    if (includeExcerptTokens && apiItem instanceof ApiInterface) {\n      const liftedTokens = this._liftPrefixExcerptTokens(apiItem);\n      this._writeExcerptTokens(output, liftedTokens);\n    }\n  }\n\n  private _extractAndFilterExcerptTokens(apiItem: ApiInterface): ICustomExcerptToken[] {\n    const excerptTokens = parseExcerptTokens(this._apiModel, apiItem);\n\n    const filterTokens = (tokens: ICustomExcerptToken[]): ICustomExcerptToken[] => {\n      return tokens\n        .filter((token) => token.type !== 'Prefix' && !interfacesToSkipParsing.includes(token.text))\n        .map((token) => ({\n          ...token,\n          children: token.children ? filterTokens(token.children) : [],\n        }));\n    };\n\n    return filterTokens(excerptTokens);\n  }\n\n  private _writePropertySections(\n    output: DocSection,\n    apiMember: ApiPropertyItem,\n    prefix: string,\n    configuration: TSDocConfiguration,\n  ): void {\n    const name = Utilities.getConciseSignature(apiMember);\n    const isRequired =\n      ApiOptionalMixin.isBaseClassOf(apiMember) &&\n      isBoolean(apiMember.isOptional) &&\n      !apiMember.isOptional &&\n      apiMember.parent?.kind === ApiItemKind.Interface;\n    const title = prefix ? camelCase(`${prefix} ${name}`) : name;\n\n    output.appendNode(\n      new DocHeading({\n        configuration,\n        title,\n        level: 2,\n        prefix: isRequired ? '<Badge type=\"success\">Required</Badge>' : '',\n      }),\n    );\n\n    // write property type\n    const propertyContent: DocNode[] = [\n      new DocEmphasisSpan({ configuration, italic: true }, [\n        ...this._createParagraphForTypeExcerpt(apiMember.propertyTypeExcerpt).getChildNodes(),\n        new DocPlainText({ configuration, text: ' ' }),\n      ]),\n    ];\n\n    // write @defaultValue tag\n    if (apiMember instanceof ApiDocumentedItem) {\n      if (apiMember.tsdocComment !== undefined) {\n        const defaultValueBlock: DocBlock | undefined = apiMember.tsdocComment.customBlocks?.find(\n          (x) => x.blockTag.tagNameWithUpperCase === StandardTags.defaultValue.tagNameWithUpperCase,\n        );\n\n        if (defaultValueBlock) {\n          propertyContent.push(\n            new DocEmphasisSpan({ configuration, bold: true }, [\n              new DocPlainText({\n                configuration,\n                text: 'Default: ',\n              }),\n            ]),\n            this._createStaticCode(defaultValueBlock.content.nodes[0] as DocSection),\n          );\n        }\n      }\n    }\n\n    output.appendNode(new DocNoteBox({ configuration }, [new DocParagraph({ configuration }, propertyContent)]));\n\n    this._writeSummarySection(output, apiMember);\n    this._writeRemarksSection(output, apiMember);\n  }\n\n  private _isApiProperty(apiItem: ApiItem): apiItem is ApiPropertyItem {\n    return apiItem.kind === ApiItemKind.Property || apiItem.kind === ApiItemKind.PropertySignature;\n  }\n\n  private _isApiInterface(apiItem: ApiItem): apiItem is ApiInterface {\n    return apiItem.kind === ApiItemKind.Interface;\n  }\n\n  private _liftPrefixExcerptTokens(apiItem: ApiInterface): ICustomExcerptToken[] {\n    const excerptTokens = parseExcerptTokens(this._apiModel, apiItem);\n    return liftPrefixExcerptTokens(excerptTokens);\n  }\n\n  private _writeExcerptTokens(output: DocSection, customExcerptTokens: ICustomExcerptToken[]) {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    for (const customExcerptToken of customExcerptTokens) {\n      const texts: { text: string; minor?: string }[] = [];\n\n      const { type, text } = customExcerptToken;\n\n      if (type === 'Prefix') {\n        const { prefix } = customExcerptToken;\n        output.appendNode(\n          new DocHeading({\n            configuration: this._tsdocConfiguration,\n            title: startCase(prefix) + ' ' + this._intl('style', LocaleType.HELPER),\n            escaped: false,\n          }),\n        );\n        if (!customExcerptToken.interface) {\n          const link = this._getLinkFromExcerptToken(customExcerptToken);\n          texts.push({ text: this._getLinkInMarkdownFormat(text, link) });\n        }\n      }\n\n      const flattenTokens = (tokens: ICustomExcerptToken[]): ICustomExcerptToken[] => {\n        return tokens.flatMap((token) => {\n          if (Array.isArray(token.children) && token.children.length > 0) {\n            return flattenTokens(token.children);\n          } else {\n            return [token];\n          }\n        });\n      };\n\n      const formattedTokens = flattenTokens([customExcerptToken]).filter((token) => token.type !== 'Prefix');\n      formattedTokens.forEach((token) => {\n        const link = this._getLinkFromExcerptToken(token);\n        const linkMd = this._getLinkInMarkdownFormat(token.text, link);\n        switch (token.type) {\n          case 'Omit':\n          case 'Pick': {\n            const fieldString = token.fields.join(',');\n            texts.push({\n              text: linkMd,\n              minor: this._intl(token.type === 'Omit' ? 'excludes' : 'includes', LocaleType.HELPER) + ' ' + fieldString,\n            });\n            break;\n          }\n          case 'Default': {\n            if (!token.interface) texts.push({ text: linkMd });\n            break;\n          }\n          default:\n            break;\n        }\n      });\n\n      if (customExcerptToken.type === 'Prefix') {\n        texts.forEach(({ text, minor }) => {\n          const title = `${customExcerptToken.prefix}{${text}}`;\n          const template = `TextStyleProps ${this._intl('prefix-description-1', LocaleType.HELPER)}\n\n- fill\n- fontSize\n- fontWeight\n- ...\n\nicon{TextStyleProps} ${this._intl('prefix-description-2', LocaleType.HELPER)}\n\n- iconFill\n- iconFontSize\n- iconFontWeight\n- ...\n`;\n\n          const nodes: DocNode[] = [\n            new DocHeading({ configuration, title, level: 2 }),\n            new DocDetails({ configuration }, this._intl('prefix-summary', LocaleType.HELPER), [\n              new DocParagraph({ configuration }, [new DocText({ configuration, text: template })]),\n            ]),\n          ];\n\n          if (minor) {\n            nodes.splice(\n              1,\n              0,\n              new DocParagraph({ configuration }, [new DocText({ configuration, text: '> ' + minor })]),\n            );\n          }\n\n          output.appendNodes(nodes);\n        });\n      }\n\n      const cache = new Set();\n\n      if (\n        (customExcerptToken.type === 'Prefix' || customExcerptToken.type === 'Default') &&\n        customExcerptToken.interface\n      ) {\n        if (cache.has(customExcerptToken.interface.name)) continue;\n        this._writeOptions(output, customExcerptToken.interface, {\n          showTitle: false,\n          prefix: 'prefix' in customExcerptToken ? customExcerptToken.prefix : '',\n        });\n        cache.add(customExcerptToken.interface.name);\n      }\n\n      formattedTokens.forEach((newToken) => {\n        const accessors = findAccessorExcerptTokens(newToken, false);\n        for (const _token of accessors) {\n          if (_token.interface) {\n            if (cache.has(_token.interface.name)) continue;\n            this._writeOptions(output, _token.interface, {\n              showTitle: false,\n              prefix: (customExcerptToken as any).prefix,\n            });\n            cache.add(_token.interface.name);\n          }\n        }\n      });\n    }\n  }\n\n  private _writeAPIMethods(\n    output: DocSection,\n    apiClass: ApiClass,\n    options?: { showTitle?: boolean; showSubTitle?: boolean },\n  ) {\n    const { showTitle = true, showSubTitle = false } = options || {};\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n    const apiMembers: ApiItem[] = this._getMembersAndWriteIncompleteWarning(apiClass, output) as ApiItem[];\n\n    const groupMembers: { [key: string]: ApiItem[] } = {};\n\n    if (!showSubTitle) groupMembers['undeclared'] = apiMembers;\n\n    if (showSubTitle) {\n      for (const apiMember of apiMembers) {\n        if (apiMember instanceof ApiDocumentedItem) {\n          const tsdocComment: DocComment | undefined = apiMember.tsdocComment;\n          let ApiCategoryDefined = false;\n          if (tsdocComment && tsdocComment.customBlocks) {\n            const apiCategoryBlock = getBlockTagByName('@apiCategory', tsdocComment);\n            if (apiCategoryBlock) {\n              const apiCategory = this._extractContentFromSection(apiCategoryBlock);\n              ApiCategoryDefined = true;\n              groupMembers[apiCategory] ||= [];\n              groupMembers[apiCategory].push(apiMember);\n            }\n          }\n          if (!ApiCategoryDefined) {\n            groupMembers['undeclared'] ||= [];\n            groupMembers['undeclared'].push(apiMember);\n          }\n        }\n      }\n    }\n\n    if (apiMembers.length > 0 && showTitle) output.appendNode(new DocHeading({ configuration, title: 'API' }));\n\n    Object.entries(groupMembers).forEach(([category, apiMembers]) => {\n      if (category !== 'undeclared' && showSubTitle) {\n        const title = this._intl(category, LocaleType.API_CATEGORY);\n        output.appendNode(new DocHeading({ configuration, title, level: 1 }));\n      }\n      if (apiMembers.length > 0) {\n        for (const apiMember of apiMembers) {\n          switch (apiMember.kind) {\n            case ApiItemKind.Method: {\n              output.appendNode(\n                new DocHeading({\n                  configuration,\n                  title: Utilities.getConciseSignature(apiMember, true),\n                  level: 2,\n                  prefix: (apiMember as ApiMethod).overloadIndex > 1 ? '<Badge type=\"warning\">Overload</Badge>' : '',\n                }),\n              );\n\n              if (apiMember instanceof ApiDocumentedItem) {\n                if (apiMember.tsdocComment !== undefined) {\n                  this._appendSection(\n                    output,\n                    this._localizeSection(apiMember.tsdocComment.summarySection, this.locale),\n                  );\n                }\n              }\n              if (apiMember instanceof ApiDeclaredItem) {\n                if (apiMember.excerpt.text.length > 0) {\n                  output.appendNode(\n                    new DocFencedCode({\n                      configuration,\n                      code: apiMember.getExcerptWithModifiers(),\n                      language: 'typescript',\n                    }),\n                  );\n                }\n                this._writeHeritageTypes(output, apiMember);\n              }\n\n              this._writeRemarksSection(output, apiMember);\n\n              const detailSection = new DocSection({ configuration });\n\n              const hasParameterAndReturn = this._writeParameterTables(\n                detailSection,\n                apiMember as ApiParameterListMixin,\n                { showTitle: false },\n              );\n\n              if (hasParameterAndReturn) {\n                output.appendNode(\n                  new DocDetails({ configuration }, this._intl(Keyword.VIEW_PARAMETERS), detailSection.nodes),\n                );\n              }\n              break;\n            }\n          }\n        }\n      }\n    });\n  }\n  private async _writeGraphOptionsPage(pageData: IPageData) {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n    const output: DocSection = new DocSection({ configuration });\n\n    this._appendPageTitle(output, 'GraphOptions', 0);\n\n    const apiInterface = pageData.apiItems.find((apiItem) => apiItem instanceof ApiInterface) as ApiInterface;\n\n    if (apiInterface) {\n      this._writeRemarksSection(output, apiInterface);\n      this._writeOptions(output, apiInterface, { showTitle: false, includeExcerptTokens: true });\n    }\n\n    const filename: string = path.join(this._outputFolder, 'graph', `option.${this._getLang()}.md`);\n\n    await this._writeFile(filename, output);\n  }\n\n  private async _writeGraphMethodsPage(pageData: IPageData) {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n    const output: DocSection = new DocSection({ configuration });\n\n    this._appendPageTitle(output, 'GraphMethods', 1);\n\n    const apiClass = pageData.apiItems.find((apiItem) => apiItem instanceof ApiClass) as ApiClass;\n\n    if (apiClass) this._writeAPIMethods(output, apiClass, { showTitle: false, showSubTitle: true });\n\n    const filename: string = path.join(this._outputFolder, 'graph', `method.${this._getLang()}.md`);\n\n    await this._writeFile(filename, output);\n  }\n\n  private async _writeGraphPropertiesPage(pageData: IPageData) {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n    const output: DocSection = new DocSection({ configuration });\n\n    this._appendPageTitle(output, 'GraphProperties', 2);\n\n    const apiClass = pageData.apiItems.find((apiItem) => apiItem instanceof ApiClass) as ApiClass;\n\n    if (apiClass) this._writeOptions(output, apiClass, { showTitle: false });\n\n    const filename: string = path.join(this._outputFolder, 'graph', `property.${this._getLang()}.md`);\n\n    await this._writeFile(filename, output);\n  }\n\n  private async _writeFile(filename: string, output: DocSection, apiItem?: ApiItem) {\n    const stringBuilder: StringBuilder = new StringBuilder();\n\n    this._markdownEmitter.emit(stringBuilder, output, {\n      contextApiItem: apiItem,\n      onGetFilenameForApiItem: (apiItemForFilename: ApiItem) => {\n        return this._getLinkFilenameForApiItem(apiItemForFilename);\n      },\n    });\n\n    const pageContent: string = await prettier.format(stringBuilder.toString(), { parser: 'markdown' });\n\n    FileSystem.ensureFolder(path.dirname(filename));\n\n    FileSystem.writeFile(filename, pageContent, {\n      convertLineEndings: NewlineKind.CrLf,\n    });\n\n    syncToGitignore(this._outputFolder, filename);\n  }\n\n  private _writeHeritageTypes(output: DocSection, apiItem: ApiDeclaredItem): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    if (apiItem instanceof ApiClass) {\n      if (apiItem.extendsType) {\n        const extendsParagraph: DocParagraph = new DocParagraph({ configuration }, [\n          new DocEmphasisSpan({ configuration, bold: true }, [\n            new DocPlainText({\n              configuration,\n              text: this._intl(Keyword.EXTENDS) + this._intl(Keyword.COLON),\n            }),\n          ]),\n        ]);\n        this._appendExcerptWithHyperlinks(extendsParagraph, apiItem.extendsType.excerpt);\n        output.appendNode(extendsParagraph);\n      }\n      if (apiItem.implementsTypes.length > 0) {\n        const implementsParagraph: DocParagraph = new DocParagraph({ configuration }, [\n          new DocEmphasisSpan({ configuration, bold: true }, [\n            new DocPlainText({\n              configuration,\n              text: this._intl(Keyword.IMPLEMENTS) + this._intl(Keyword.COLON),\n            }),\n          ]),\n        ]);\n        let needsComma: boolean = false;\n        for (const implementsType of apiItem.implementsTypes) {\n          if (needsComma) {\n            implementsParagraph.appendNode(\n              new DocPlainText({\n                configuration,\n                text: this._intl(Keyword.COMMA),\n              }),\n            );\n          }\n          this._appendExcerptWithHyperlinks(implementsParagraph, implementsType.excerpt);\n          needsComma = true;\n        }\n        output.appendNode(implementsParagraph);\n      }\n    }\n\n    if (apiItem instanceof ApiInterface) {\n      if (apiItem.extendsTypes.length > 0) {\n        const extendsParagraph: DocParagraph = new DocParagraph({ configuration }, [\n          new DocEmphasisSpan({ configuration, bold: true }, [\n            new DocPlainText({\n              configuration,\n              text: this._intl(Keyword.EXTENDS) + this._intl(Keyword.COLON),\n            }),\n          ]),\n        ]);\n        let needsComma: boolean = false;\n        for (const extendsType of apiItem.extendsTypes) {\n          if (needsComma) {\n            extendsParagraph.appendNode(\n              new DocPlainText({\n                configuration,\n                text: this._intl(Keyword.COMMA),\n              }),\n            );\n          }\n          this._appendExcerptWithHyperlinks(extendsParagraph, extendsType.excerpt);\n          needsComma = true;\n        }\n        output.appendNode(extendsParagraph);\n      }\n    }\n\n    if (apiItem instanceof ApiTypeAlias) {\n      const refs: ExcerptToken[] = apiItem.excerptTokens.filter(\n        (token) =>\n          token.kind === ExcerptTokenKind.Reference &&\n          token.canonicalReference &&\n          this._apiModel.resolveDeclarationReference(token.canonicalReference, undefined).resolvedApiItem,\n      );\n      if (refs.length > 0) {\n        const referencesParagraph: DocParagraph = new DocParagraph({ configuration }, [\n          new DocEmphasisSpan({ configuration, bold: true }, [\n            new DocPlainText({\n              configuration,\n              text: this._intl(Keyword.REFERENCES) + this._intl(Keyword.COLON),\n            }),\n          ]),\n        ]);\n        let needsComma: boolean = false;\n        const visited: Set<string> = new Set();\n        for (const ref of refs) {\n          if (visited.has(ref.text)) {\n            continue;\n          }\n          visited.add(ref.text);\n\n          if (needsComma) {\n            referencesParagraph.appendNode(\n              new DocPlainText({\n                configuration,\n                text: this._intl(Keyword.COMMA),\n              }),\n            );\n          }\n\n          this._appendExcerptTokenWithHyperlinks(referencesParagraph, ref);\n          needsComma = true;\n        }\n        output.appendNode(referencesParagraph);\n      }\n    }\n  }\n\n  private _writeRemarksSection(output: DocSection, apiItem: ApiItem): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    if (apiItem instanceof ApiDocumentedItem) {\n      const tsdocComment: DocComment | undefined = apiItem.tsdocComment;\n\n      if (tsdocComment) {\n        // Write the @remarks block\n        if (tsdocComment.remarksBlock) {\n          this._appendSection(output, this._localizeSection(tsdocComment.remarksBlock.content, this.locale));\n        }\n\n        // Write the @example blocks\n        const exampleBlocks: DocBlock[] = tsdocComment.customBlocks.filter(\n          (x) => x.blockTag.tagNameWithUpperCase === StandardTags.example.tagNameWithUpperCase,\n        );\n\n        let exampleNumber: number = 1;\n        for (const exampleBlock of exampleBlocks) {\n          const heading: string = this._intl(Keyword.EXAMPLE) + (exampleBlocks.length > 1 ? ' ' + exampleNumber : '');\n          const exampleTitle =\n            this.referenceLevel === 0\n              ? new DocHeading({ configuration, title: heading })\n              : new DocParagraph({ configuration }, [\n                  new DocEmphasisSpan({ configuration, bold: true }, [\n                    new DocPlainText({ configuration, text: heading }),\n                  ]),\n                ]);\n          output.appendNode(exampleTitle);\n\n          this._appendSection(output, exampleBlock.content);\n\n          ++exampleNumber;\n        }\n      }\n    }\n  }\n\n  private _writeThrowsSection(output: DocSection, apiItem: ApiItem): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    if (apiItem instanceof ApiDocumentedItem) {\n      const tsdocComment: DocComment | undefined = apiItem.tsdocComment;\n\n      if (tsdocComment) {\n        // Write the @throws blocks\n        const throwsBlocks: DocBlock[] = tsdocComment.customBlocks.filter(\n          (x) => x.blockTag.tagNameWithUpperCase === StandardTags.throws.tagNameWithUpperCase,\n        );\n\n        if (throwsBlocks.length > 0) {\n          const heading: string = this._intl(Keyword.EXCEPTIONS);\n          output.appendNode(new DocHeading({ configuration, title: heading }));\n\n          for (const throwsBlock of throwsBlocks) {\n            this._appendSection(output, throwsBlock.content);\n          }\n        }\n      }\n    }\n  }\n\n  /**\n   * GENERATE PAGE: MODEL\n   */\n  private _writeModelTable(output: DocSection, apiModel: ApiModel): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const packagesTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [this._intl(Keyword.PACKAGE), this._intl(Keyword.DESCRIPTION)],\n    });\n\n    for (const apiMember of apiModel.members) {\n      const row: DocTableRow = new DocTableRow({ configuration }, [\n        this._createTitleCell(apiMember),\n        this._createDescriptionCell(apiMember),\n      ]);\n\n      switch (apiMember.kind) {\n        case ApiItemKind.Package:\n          packagesTable.addRow(row);\n          this._writeApiItemPage(apiMember);\n          break;\n      }\n    }\n\n    if (packagesTable.rows.length > 0) {\n      output.appendNode(new DocHeading({ configuration, title: this._intl(Keyword.PACKAGES) }));\n      output.appendNode(packagesTable);\n    }\n  }\n\n  /**\n   * GENERATE PAGE: PACKAGE or NAMESPACE\n   */\n  private _writePackageOrNamespaceTables(output: DocSection, apiContainer: ApiPackage | ApiNamespace): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const abstractClassesTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [this._intl(Keyword.ABSTRACT_CLASS), this._intl(Keyword.DESCRIPTION)],\n    });\n\n    const classesTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [this._intl(Keyword.CLASS), this._intl(Keyword.DESCRIPTION)],\n    });\n\n    const enumerationsTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [this._intl(Keyword.ENUMERATION), this._intl(Keyword.DESCRIPTION)],\n    });\n\n    const functionsTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [this._intl(Keyword.FUNCTION), this._intl(Keyword.DESCRIPTION)],\n    });\n\n    const interfacesTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [this._intl(Keyword.INTERFACE), this._intl(Keyword.DESCRIPTION)],\n    });\n\n    const namespacesTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [this._intl(Keyword.NAMESPACE), this._intl(Keyword.DESCRIPTION)],\n    });\n\n    const variablesTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [this._intl(Keyword.VARIABLE), this._intl(Keyword.DESCRIPTION)],\n    });\n\n    const typeAliasesTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [this._intl(Keyword.TYPE_ALIAS), this._intl(Keyword.DESCRIPTION)],\n    });\n\n    const apiMembers: ReadonlyArray<ApiItem> =\n      apiContainer.kind === ApiItemKind.Package\n        ? (apiContainer as ApiPackage).entryPoints[0].members\n        : (apiContainer as ApiNamespace).members;\n\n    for (const apiMember of apiMembers) {\n      const row: DocTableRow = new DocTableRow({ configuration }, [\n        this._createTitleCell(apiMember),\n        this._createDescriptionCell(apiMember),\n      ]);\n\n      switch (apiMember.kind) {\n        case ApiItemKind.Class:\n          if (ApiAbstractMixin.isBaseClassOf(apiMember) && apiMember.isAbstract) {\n            abstractClassesTable.addRow(row);\n          } else {\n            classesTable.addRow(row);\n          }\n          this._writeApiItemPage(apiMember);\n          break;\n\n        case ApiItemKind.Enum:\n          enumerationsTable.addRow(row);\n          this._writeApiItemPage(apiMember);\n          break;\n\n        case ApiItemKind.Interface:\n          interfacesTable.addRow(row);\n          this._writeApiItemPage(apiMember);\n          break;\n\n        case ApiItemKind.Namespace:\n          namespacesTable.addRow(row);\n          this._writeApiItemPage(apiMember);\n          break;\n\n        case ApiItemKind.Function:\n          functionsTable.addRow(row);\n          this._writeApiItemPage(apiMember);\n          break;\n\n        case ApiItemKind.TypeAlias:\n          typeAliasesTable.addRow(row);\n          this._writeApiItemPage(apiMember);\n          break;\n\n        case ApiItemKind.Variable:\n          variablesTable.addRow(row);\n          this._writeApiItemPage(apiMember);\n          break;\n      }\n    }\n\n    if (classesTable.rows.length > 0) {\n      output.appendNode(new DocHeading({ configuration, title: this._intl(Keyword.CLASSES) }));\n      output.appendNode(classesTable);\n    }\n\n    if (abstractClassesTable.rows.length > 0) {\n      output.appendNode(\n        new DocHeading({\n          configuration,\n          title: this._intl(Keyword.ABSTRACT_CLASSES),\n        }),\n      );\n      output.appendNode(abstractClassesTable);\n    }\n\n    if (enumerationsTable.rows.length > 0) {\n      output.appendNode(\n        new DocHeading({\n          configuration,\n          title: this._intl(Keyword.ENUMERATIONS),\n        }),\n      );\n      output.appendNode(enumerationsTable);\n    }\n    if (functionsTable.rows.length > 0) {\n      output.appendNode(new DocHeading({ configuration, title: this._intl(Keyword.FUNCTIONS) }));\n      output.appendNode(functionsTable);\n    }\n\n    if (interfacesTable.rows.length > 0) {\n      output.appendNode(\n        new DocHeading({\n          configuration,\n          title: this._intl(Keyword.INTERFACES),\n        }),\n      );\n      output.appendNode(interfacesTable);\n    }\n\n    if (namespacesTable.rows.length > 0) {\n      output.appendNode(\n        new DocHeading({\n          configuration,\n          title: this._intl(Keyword.NAMESPACES),\n        }),\n      );\n      output.appendNode(namespacesTable);\n    }\n\n    if (variablesTable.rows.length > 0) {\n      output.appendNode(new DocHeading({ configuration, title: this._intl(Keyword.VARIABLES) }));\n      output.appendNode(variablesTable);\n    }\n\n    if (typeAliasesTable.rows.length > 0) {\n      output.appendNode(\n        new DocHeading({\n          configuration,\n          title: this._intl(Keyword.TYPE_ALIASES),\n        }),\n      );\n      output.appendNode(typeAliasesTable);\n    }\n  }\n\n  /**\n   * GENERATE PAGE: CLASS\n   */\n  private _writeClassTables(\n    output: DocSection,\n    apiClass: ApiClass,\n    options?: {\n      showTarget?: string[];\n      showEventsTableTitle?: boolean;\n      showPropertiesTableTitle?: boolean;\n      showConstructorsTableTitle?: boolean;\n      showMethodsTableTitle?: boolean;\n    },\n  ): void {\n    const {\n      showTarget = ['events', 'constructors', 'properties', 'methods'],\n      showEventsTableTitle = true,\n      showPropertiesTableTitle = true,\n      showConstructorsTableTitle = true,\n      showMethodsTableTitle = true,\n    } = options || {};\n\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const eventsTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [\n        this._intl(Keyword.PROPERTY),\n        this._intl(Keyword.MODIFIERS),\n        this._intl(Keyword.TYPE),\n        this._intl(Keyword.DESCRIPTION),\n      ],\n    });\n\n    const constructorsTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [this._intl(Keyword.CONSTRUCTOR), this._intl(Keyword.MODIFIERS), this._intl(Keyword.DESCRIPTION)],\n    });\n\n    const propertiesTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [\n        this._intl(Keyword.PROPERTY),\n        this._intl(Keyword.MODIFIERS),\n        this._intl(Keyword.TYPE),\n        this._intl(Keyword.DEFAULT_VALUE),\n        this._intl(Keyword.DESCRIPTION),\n      ],\n    });\n\n    const methodsTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [this._intl(Keyword.METHOD), this._intl(Keyword.MODIFIERS), this._intl(Keyword.DESCRIPTION)],\n    });\n\n    const apiMembers: readonly ApiItem[] = this._getMembersAndWriteIncompleteWarning(apiClass, output);\n    for (const apiMember of apiMembers) {\n      const isInherited: boolean = apiMember.parent !== apiClass;\n      switch (apiMember.kind) {\n        case ApiItemKind.Constructor: {\n          constructorsTable.addRow(\n            new DocTableRow({ configuration }, [\n              this._createTitleCell(apiMember),\n              this._createModifiersCell(apiMember),\n              this._createDescriptionCell(apiMember, isInherited),\n            ]),\n          );\n\n          this._writeApiItemPage(apiMember);\n          break;\n        }\n        case ApiItemKind.Method: {\n          methodsTable.addRow(\n            new DocTableRow({ configuration }, [\n              this._createTitleCell(apiMember),\n              this._createModifiersCell(apiMember),\n              this._createDescriptionCell(apiMember, isInherited),\n            ]),\n          );\n\n          this._writeApiItemPage(apiMember);\n          break;\n        }\n        case ApiItemKind.Property: {\n          if ((apiMember as ApiPropertyItem).isEventProperty) {\n            eventsTable.addRow(\n              new DocTableRow({ configuration }, [\n                this._createTitleCell(apiMember),\n                this._createModifiersCell(apiMember),\n                this._createPropertyTypeCell(apiMember),\n                this._createDescriptionCell(apiMember, isInherited),\n              ]),\n            );\n          } else {\n            propertiesTable.addRow(\n              new DocTableRow({ configuration }, [\n                this._createTitleCell(apiMember),\n                this._createModifiersCell(apiMember),\n                this._createPropertyTypeCell(apiMember),\n                this._createDefaultValueCell(apiMember),\n                this._createDescriptionCell(apiMember, isInherited),\n              ]),\n            );\n          }\n\n          this._writeApiItemPage(apiMember);\n          break;\n        }\n      }\n    }\n\n    if (showTarget.includes('events') && eventsTable.rows.length > 0) {\n      showEventsTableTitle && output.appendNode(new DocHeading({ configuration, title: this._intl(Keyword.EVENTS) }));\n      output.appendNode(eventsTable);\n    }\n\n    if (showTarget.includes('constructors') && constructorsTable.rows.length > 0) {\n      showConstructorsTableTitle &&\n        output.appendNode(\n          new DocHeading({\n            configuration,\n            title: this._intl(Keyword.CONSTRUCTORS),\n          }),\n        );\n      output.appendNode(constructorsTable);\n    }\n\n    if (showTarget.includes('properties') && propertiesTable.rows.length > 0) {\n      showPropertiesTableTitle &&\n        output.appendNode(\n          new DocHeading({\n            configuration,\n            title: this._intl(Keyword.PROPERTIES),\n          }),\n        );\n      output.appendNode(propertiesTable);\n    }\n\n    if (showTarget.includes('methods') && methodsTable.rows.length > 0) {\n      showMethodsTableTitle && output.appendNode(new DocHeading({ configuration, title: this._intl(Keyword.METHODS) }));\n      output.appendNode(methodsTable);\n    }\n  }\n\n  /**\n   * GENERATE PAGE: ENUM\n   */\n  private _writeEnumTables(output: DocSection, apiEnum: ApiEnum): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const enumMembersTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: ['Member', 'Value', this._intl(Keyword.DESCRIPTION)],\n    });\n\n    for (const apiEnumMember of apiEnum.members) {\n      enumMembersTable.addRow(\n        new DocTableRow({ configuration }, [\n          new DocTableCell({ configuration }, [\n            new DocParagraph({ configuration }, [\n              new DocPlainText({\n                configuration,\n                text: Utilities.getConciseSignature(apiEnumMember),\n              }),\n            ]),\n          ]),\n          this._createInitializerCell(apiEnumMember),\n          this._createDescriptionCell(apiEnumMember),\n        ]),\n      );\n    }\n\n    if (enumMembersTable.rows.length > 0) {\n      output.appendNode(\n        new DocHeading({\n          configuration,\n          title: this._intl(Keyword.ENUMERATION_MEMBERS),\n        }),\n      );\n      output.appendNode(enumMembersTable);\n    }\n  }\n\n  /**\n   * GENERATE PAGE: INTERFACE\n   */\n  private _writeInterfaceTables(\n    output: DocSection,\n    apiInterface: ApiInterface,\n    options?: {\n      showTarget?: string[];\n      showEventsTableTitle?: boolean;\n      showPropertiesTableTitle?: boolean;\n      showMethodsTableTitle?: boolean;\n    },\n  ): void {\n    const {\n      showEventsTableTitle = true,\n      showPropertiesTableTitle = true,\n      showMethodsTableTitle = true,\n      showTarget = ['events', 'properties', 'methods'],\n    } = options || {};\n\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const eventsTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [\n        this._intl(Keyword.PROPERTY),\n        this._intl(Keyword.MODIFIERS),\n        this._intl(Keyword.TYPE),\n        this._intl(Keyword.DESCRIPTION),\n      ],\n    });\n\n    const propertiesTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [\n        this._intl(Keyword.PROPERTY),\n        this._intl(Keyword.TYPE),\n        this._intl(Keyword.DEFAULT_VALUE),\n        this._intl(Keyword.DESCRIPTION),\n      ],\n    });\n\n    const methodsTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [this._intl(Keyword.METHOD), this._intl(Keyword.DESCRIPTION)],\n    });\n\n    const apiMembers: readonly ApiItem[] = this._getMembersAndWriteIncompleteWarning(apiInterface, output);\n    for (const apiMember of apiMembers) {\n      const isInherited: boolean = apiMember.parent !== apiInterface;\n      switch (apiMember.kind) {\n        case ApiItemKind.ConstructSignature:\n        case ApiItemKind.MethodSignature: {\n          methodsTable.addRow(\n            new DocTableRow({ configuration }, [\n              this._createTitleCell(apiMember),\n              this._createDescriptionCell(apiMember, isInherited),\n            ]),\n          );\n\n          this._writeApiItemPage(apiMember);\n          break;\n        }\n        case ApiItemKind.PropertySignature: {\n          if ((apiMember as ApiPropertyItem).isEventProperty) {\n            eventsTable.addRow(\n              new DocTableRow({ configuration }, [\n                this._createTitleCell(apiMember),\n                this._createPropertyTypeCell(apiMember),\n                this._createDefaultValueCell(apiMember),\n                this._createDescriptionCell(apiMember, isInherited),\n              ]),\n            );\n          } else {\n            propertiesTable.addRow(\n              new DocTableRow({ configuration }, [\n                this._createTitleCell(apiMember),\n                this._createPropertyTypeCell(apiMember),\n                this._createDefaultValueCell(apiMember),\n                this._createDescriptionCell(apiMember, isInherited),\n              ]),\n            );\n          }\n\n          this._writeApiItemPage(apiMember);\n          break;\n        }\n      }\n    }\n\n    if (showTarget.includes('events') && eventsTable.rows.length > 0) {\n      showEventsTableTitle && output.appendNode(new DocHeading({ configuration, title: this._intl(Keyword.EVENTS) }));\n      output.appendNode(eventsTable);\n    }\n\n    if (showTarget.includes('properties') && propertiesTable.rows.length > 0) {\n      showPropertiesTableTitle &&\n        output.appendNode(\n          new DocHeading({\n            configuration,\n            title: this._intl(Keyword.PROPERTIES),\n          }),\n        );\n      output.appendNode(propertiesTable);\n    }\n\n    if (showTarget.includes('methods') && methodsTable.rows.length > 0) {\n      showMethodsTableTitle && output.appendNode(new DocHeading({ configuration, title: this._intl(Keyword.METHODS) }));\n      output.appendNode(methodsTable);\n    }\n  }\n\n  /**\n   * GENERATE PAGE: FUNCTION-LIKE\n   */\n  private _writeParameterTables(\n    output: DocSection,\n    apiParameterListMixin: ApiParameterListMixin,\n    options: { showTitle?: boolean } = { showTitle: true },\n  ): boolean {\n    const { showTitle } = options;\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    /**\n     * Check if there are any parameters or returns to show\n     */\n    let hasParameters = false;\n    let hasReturns = false;\n\n    const parametersTable: DocTable = new DocTable({\n      configuration,\n      headerTitles: [this._intl(Keyword.PARAMETER), this._intl(Keyword.TYPE), this._intl(Keyword.DESCRIPTION)],\n    });\n    for (const apiParameter of apiParameterListMixin.parameters) {\n      parametersTable.addRow(\n        new DocTableRow({ configuration }, [\n          new DocTableCell({ configuration }, [\n            new DocParagraph({ configuration }, [new DocPlainText({ configuration, text: apiParameter.name })]),\n          ]),\n          new DocTableCell({ configuration }, [this._createParagraphForTypeExcerpt(apiParameter.parameterTypeExcerpt)]),\n          new DocTableCell(\n            { configuration },\n            this._createSectionForParameter(apiParameter.tsdocParamBlock as DocBlock).nodes,\n          ),\n        ]),\n      );\n    }\n\n    if (parametersTable.rows.length > 0) {\n      hasParameters = true;\n      showTitle && output.appendNode(new DocHeading({ configuration, title: 'Parameters' }));\n      output.appendNode(parametersTable);\n    }\n\n    if (ApiReturnTypeMixin.isBaseClassOf(apiParameterListMixin)) {\n      const returnTypeExcerpt: Excerpt = apiParameterListMixin.returnTypeExcerpt;\n\n      // Return type\n      const returnNodes: DocNode[] = [];\n\n      const typeExcerptNodes = this._createParagraphForTypeExcerpt(returnTypeExcerpt).nodes;\n\n      if (typeExcerptNodes.length > 0) {\n        if (\n          !(\n            typeExcerptNodes.length === 1 &&\n            typeExcerptNodes[0] instanceof DocPlainText &&\n            typeExcerptNodes[0].text === 'void'\n          )\n        ) {\n          hasReturns ||= true;\n        }\n\n        returnNodes.push(\n          new DocParagraph({ configuration }, [\n            new DocEmphasisSpan({ configuration, bold: true }, [\n              new DocPlainText({\n                configuration,\n                text: this._intl(Keyword.TYPE) + this._intl(Keyword.COLON),\n              }),\n            ]),\n            ...typeExcerptNodes,\n          ]),\n        );\n      }\n\n      // Return description\n      if (apiParameterListMixin instanceof ApiDocumentedItem) {\n        if (apiParameterListMixin.tsdocComment && apiParameterListMixin.tsdocComment.returnsBlock) {\n          returnNodes.push(\n            new DocParagraph({ configuration }, [\n              new DocEmphasisSpan({ configuration, bold: true }, [\n                new DocPlainText({\n                  configuration,\n                  text: this._intl(Keyword.DESCRIPTION) + this._intl(Keyword.COLON),\n                }),\n              ]),\n              ...this._createSectionForParameter(apiParameterListMixin.tsdocComment.returnsBlock)\n                .getChildNodes()[0]\n                .getChildNodes(),\n            ]),\n          );\n        }\n      }\n\n      if (returnNodes.length > 0) {\n        // If there are parameters, add a newline before the returns section\n        this._assertNewline(output);\n\n        output.appendNode(\n          new DocParagraph({ configuration }, [\n            new DocEmphasisSpan({ configuration, bold: true }, [\n              new DocPlainText({\n                configuration,\n                text: this._intl(Keyword.RETURNS),\n              }),\n            ]),\n            new DocPlainText({\n              configuration,\n              text: this._intl(Keyword.COLON),\n            }),\n          ]),\n        );\n        output.appendNode(new DocParagraph({ configuration }, [new DocUnorderedList({ configuration }, returnNodes)]));\n      }\n    }\n\n    return hasParameters || hasReturns;\n  }\n\n  private _assertNewline(output: DocSection): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n    output.appendNode(new DocParagraph({ configuration }));\n  }\n\n  private _createSectionForParameter(block: DocBlock): DocSection {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const description: DocSection = new DocSection({\n      configuration,\n    });\n\n    if (block) {\n      const nodes = block.content.getChildNodes()[0]?.getChildNodes() || [];\n\n      let matchBilingual = false;\n      for (let i = 0; i < nodes.length; i++) {\n        const node = nodes[i];\n        if (node instanceof DocHtmlStartTag && node.selfClosingTag) {\n          matchBilingual = true;\n          const text = (nodes[i + 1] as DocPlainText).text.replace('|', '').trim();\n\n          description.appendNode(new DocParagraph({ configuration }, [new DocPlainText({ configuration, text })]));\n          break;\n        }\n      }\n\n      if (!matchBilingual) {\n        this._appendAndMergeSection(description, block.content);\n      }\n    }\n\n    return description;\n  }\n\n  private _createParagraphForTypeExcerpt(excerpt: Excerpt): DocParagraph {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const paragraph: DocParagraph = new DocParagraph({ configuration });\n\n    if (!excerpt.text.trim()) {\n      paragraph.appendNode(new DocPlainText({ configuration, text: '(not declared)' }));\n    } else {\n      this._appendExcerptWithHyperlinks(paragraph, excerpt);\n    }\n\n    return paragraph;\n  }\n\n  private _appendExcerptWithHyperlinks(docNodeContainer: DocNodeContainer, excerpt: Excerpt): void {\n    for (const token of excerpt.spannedTokens) {\n      this._appendExcerptTokenWithHyperlinks(docNodeContainer, token);\n    }\n  }\n\n  private _parseTypeAliasTokens(apiTypeAlias: ApiTypeAlias): ExcerptToken[] {\n    const tokens = apiTypeAlias.excerptTokens.slice(1, -1).flatMap((token) => {\n      if (token.kind === ExcerptTokenKind.Reference && token.canonicalReference) {\n        const apiItemResult: IResolveDeclarationReferenceResult = this._apiModel.resolveDeclarationReference(\n          token.canonicalReference,\n          undefined,\n        );\n        if (apiItemResult.resolvedApiItem instanceof ApiTypeAlias) {\n          return this._parseTypeAliasTokens(apiItemResult.resolvedApiItem);\n        }\n      }\n      return token;\n    });\n    return tokens;\n  }\n\n  private _appendExcerptTokenWithHyperlinks(docNodeContainer: DocNodeContainer, token: ExcerptToken): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    // Markdown doesn't provide a standardized syntax for hyperlinks inside code spans, so we will render\n    // the type expression as DocPlainText.  Instead of creating multiple DocParagraphs, we can simply\n    // discard any newlines and let the renderer do normal word-wrapping.\n    const unwrappedTokenText: string = token.text.replace(/[\\r\\n]+/g, ' ');\n\n    // If it's hyperlinkable, then append a DocLinkTag\n    if (token.kind === ExcerptTokenKind.Reference && token.canonicalReference) {\n      const apiItemResult: IResolveDeclarationReferenceResult = this._apiModel.resolveDeclarationReference(\n        token.canonicalReference,\n        undefined,\n      );\n\n      if (apiItemResult.resolvedApiItem) {\n        if (\n          apiItemResult.resolvedApiItem.kind === ApiItemKind.TypeAlias &&\n          this.referenceLevel > 0 &&\n          !typesToSkipParsing.includes(apiItemResult.resolvedApiItem.displayName)\n        ) {\n          const typeAliasTokens = this._parseTypeAliasTokens(apiItemResult.resolvedApiItem as ApiTypeAlias);\n          // If the type alias is a simple single-line type alias, then render it as plain text\n          // Otherwise, render it as a hyperlink\n          if (typeAliasTokens.every((token) => !token.text.includes('\\n') && !token.text.includes('\\r'))) {\n            docNodeContainer.appendNode(\n              new DocPlainText({\n                configuration,\n                text: typeAliasTokens\n                  .map((token) => token.text.trim())\n                  .join('')\n                  .split('|')\n                  .filter((item, index, arr) => arr.indexOf(item) === index)\n                  .join(' | '),\n              }),\n            );\n            return;\n          }\n        }\n\n        docNodeContainer.appendNode(\n          new DocLinkTag({\n            configuration,\n            tagName: '@link',\n            linkText: unwrappedTokenText,\n            urlDestination: this._getLinkFilenameForApiItem(apiItemResult.resolvedApiItem),\n          }),\n        );\n        return;\n      }\n    }\n\n    // Otherwise append non-hyperlinked text\n    docNodeContainer.appendNode(new DocPlainText({ configuration, text: unwrappedTokenText }));\n  }\n\n  private _createTitleCell(apiItem: ApiItem): DocTableCell {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const section: DocSection = new DocSection({ configuration });\n\n    section.appendNodeInParagraph(\n      new DocLinkTag({\n        configuration,\n        tagName: '@link',\n        linkText: Utilities.getConciseSignature(apiItem),\n        urlDestination: this._getLinkFilenameForApiItem(apiItem),\n      }),\n    );\n\n    return new DocTableCell({ configuration }, section.nodes);\n  }\n\n  private _createDescriptionCell(apiItem: ApiItem, isInherited: boolean = false): DocTableCell {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const section: DocSection = new DocSection({ configuration });\n\n    if (ApiReleaseTagMixin.isBaseClassOf(apiItem)) {\n      if (apiItem.releaseTag === ReleaseTag.Alpha || apiItem.releaseTag === ReleaseTag.Beta) {\n        section.appendNodesInParagraph([\n          new DocEmphasisSpan({ configuration, bold: true, italic: true }, [\n            new DocPlainText({\n              configuration,\n              text: `(${apiItem.releaseTag === ReleaseTag.Alpha ? 'ALPHA' : 'BETA'})`,\n            }),\n          ]),\n          new DocPlainText({ configuration, text: ' ' }),\n        ]);\n      }\n    }\n\n    if (ApiOptionalMixin.isBaseClassOf(apiItem) && apiItem.isOptional) {\n      section.appendNodesInParagraph([\n        new DocEmphasisSpan({ configuration, italic: true }, [\n          new DocPlainText({\n            configuration,\n            text:\n              this._intl(Keyword.LEFT_PARENTHESIS) +\n              this._intl(Keyword.OPTIONAL) +\n              this._intl(Keyword.RIGHT_PARENTHESIS),\n          }),\n        ]),\n        new DocPlainText({ configuration, text: ' ' }),\n      ]);\n    }\n\n    if (apiItem instanceof ApiDocumentedItem) {\n      if (apiItem.tsdocComment !== undefined) {\n        this._writeSummarySection(section, apiItem);\n      }\n    }\n\n    if (isInherited && apiItem.parent) {\n      section.appendNode(\n        new DocParagraph({ configuration }, [\n          new DocPlainText({ configuration, text: '(Inherited from ' }),\n          new DocLinkTag({\n            configuration,\n            tagName: '@link',\n            linkText: apiItem.parent.displayName,\n            urlDestination: this._getLinkFilenameForApiItem(apiItem.parent),\n          }),\n          new DocPlainText({ configuration, text: ')' }),\n        ]),\n      );\n    }\n\n    return new DocTableCell({ configuration }, section.nodes);\n  }\n\n  private _createModifiersCell(apiItem: ApiItem): DocTableCell {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const section: DocSection = new DocSection({ configuration });\n\n    // Output modifiers in syntactically correct order: first access modifier (here: `protected`), then\n    // `static` or `abstract` (no member can be both, so the order between the two of them does not matter),\n    // last `readonly`. If `override` was supported, it would go directly before `readonly`.\n\n    if (ApiProtectedMixin.isBaseClassOf(apiItem)) {\n      if (apiItem.isProtected) {\n        section.appendNode(\n          new DocParagraph({ configuration }, [new DocCodeSpan({ configuration, code: 'protected' })]),\n        );\n      }\n    }\n\n    if (ApiStaticMixin.isBaseClassOf(apiItem)) {\n      if (apiItem.isStatic) {\n        section.appendNode(new DocParagraph({ configuration }, [new DocCodeSpan({ configuration, code: 'static' })]));\n      }\n    }\n\n    if (ApiAbstractMixin.isBaseClassOf(apiItem)) {\n      if (apiItem.isAbstract) {\n        section.appendNode(new DocParagraph({ configuration }, [new DocCodeSpan({ configuration, code: 'abstract' })]));\n      }\n    }\n\n    if (ApiReadonlyMixin.isBaseClassOf(apiItem)) {\n      if (apiItem.isReadonly) {\n        section.appendNode(new DocParagraph({ configuration }, [new DocCodeSpan({ configuration, code: 'readonly' })]));\n      }\n    }\n\n    return new DocTableCell({ configuration }, section.nodes);\n  }\n\n  private _createPropertyTypeCell(apiItem: ApiItem): DocTableCell {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const section: DocSection = new DocSection({ configuration });\n\n    if (apiItem instanceof ApiPropertyItem) {\n      section.appendNode(this._createParagraphForTypeExcerpt(apiItem.propertyTypeExcerpt));\n    }\n\n    return new DocTableCell({ configuration }, section.nodes);\n  }\n\n  private _createDefaultValueCell(apiItem: ApiItem): DocTableCell {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const section: DocSection = new DocSection({ configuration });\n\n    if (apiItem instanceof ApiDocumentedItem) {\n      if (apiItem.tsdocComment !== undefined) {\n        const defaultValueBlock: DocBlock | undefined = apiItem.tsdocComment.customBlocks?.find(\n          (x) => x.blockTag.tagNameWithUpperCase === StandardTags.defaultValue.tagNameWithUpperCase,\n        );\n\n        if (defaultValueBlock) {\n          this._appendStaticCodeNode(section, defaultValueBlock.content.nodes[0] as DocSection);\n        } else {\n          section.appendNode(new DocParagraph({ configuration }, [new DocPlainText({ configuration, text: '/' })]));\n        }\n      }\n    }\n\n    return new DocTableCell({ configuration }, section.nodes);\n  }\n\n  private _createInitializerCell(apiItem: ApiItem): DocTableCell {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const section: DocSection = new DocSection({ configuration });\n\n    if (ApiInitializerMixin.isBaseClassOf(apiItem)) {\n      if (apiItem.initializerExcerpt) {\n        section.appendNodeInParagraph(\n          new DocCodeSpan({\n            configuration,\n            code: apiItem.initializerExcerpt.text,\n          }),\n        );\n      }\n    }\n\n    return new DocTableCell({ configuration }, section.nodes);\n  }\n\n  private _writeBreadcrumb(output: DocSection, apiItem: ApiItem): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    let init = true;\n\n    for (const hierarchyItem of apiItem.getHierarchy()) {\n      switch (hierarchyItem.kind) {\n        case ApiItemKind.Model:\n        case ApiItemKind.EntryPoint:\n          // We don't show the model as part of the breadcrumb because it is the root-level container.\n          // We don't show the entry point because today API Extractor doesn't support multiple entry points;\n          // this may change in the future.\n          break;\n        default:\n          output.appendNodesInParagraph([\n            new DocPlainText({\n              configuration,\n              text: !init ? ' > ' : '',\n            }),\n            new DocLinkTag({\n              configuration,\n              tagName: '@link',\n              linkText: hierarchyItem.displayName,\n              urlDestination: this._getLinkFilenameForApiItem(hierarchyItem),\n            }),\n          ]);\n          init = false;\n      }\n    }\n  }\n\n  private _writeAlphaWarning(output: DocSection): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n    const betaWarning: string =\n      'This API is provided as an alpha preview for developers and may change' +\n      ' based on feedback that we receive.  Do not use this API in a production environment.';\n    output.appendNode(\n      new DocNoteBox({ configuration }, [\n        new DocParagraph({ configuration }, [new DocPlainText({ configuration, text: betaWarning })]),\n      ]),\n    );\n  }\n\n  private _writeBetaWarning(output: DocSection): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n    const betaWarning: string =\n      'This API is provided as a beta preview for developers and may change' +\n      ' based on feedback that we receive.  Do not use this API in a production environment.';\n    output.appendNode(\n      new DocNoteBox({ configuration }, [\n        new DocParagraph({ configuration }, [new DocPlainText({ configuration, text: betaWarning })]),\n      ]),\n    );\n  }\n\n  private _appendSection(output: DocSection, docSection: DocSection): void {\n    for (const node of docSection.nodes) {\n      output.appendNode(node);\n    }\n  }\n\n  private _appendAndMergeSection(output: DocSection, docSection: DocSection): void {\n    let firstNode: boolean = true;\n    for (const node of docSection.nodes) {\n      if (firstNode) {\n        if (node.kind === DocNodeKind.Paragraph) {\n          output.appendNodesInParagraph(node.getChildNodes());\n          firstNode = false;\n          continue;\n        }\n      }\n      firstNode = false;\n\n      output.appendNode(node);\n    }\n  }\n\n  private _createStaticCode(docSection: DocSection): DocCodeSpan {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const content = docSection\n      .getChildNodes()\n      .map((node) => {\n        if (node instanceof DocPlainText) return node.text;\n      })\n      .join('');\n\n    return new DocCodeSpan({ configuration, code: content });\n  }\n\n  private _extractContentFromSection(docSection: DocSection): string {\n    const nodes = docSection.getChildNodes();\n    for (const node of nodes) {\n      if (node instanceof DocParagraph) {\n        return this._extractContentFromSection(node as DocSection);\n      } else if (node instanceof DocPlainText) {\n        return node.text;\n      }\n    }\n    return '';\n  }\n\n  private _appendStaticCodeNode(output: DocSection, docSection: DocSection): void {\n    const content = this._extractContentFromSection(docSection);\n    output.appendNodeInParagraph(\n      new DocCodeSpan({\n        configuration: this._tsdocConfiguration,\n        code: content,\n      }),\n    );\n  }\n\n  private _appendPageTitle(output: DocSection, key: string, order?: number): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const en = intl(LocaleType.PAGE_NAME, key, LocaleLanguage.EN);\n    const zh = intl(LocaleType.PAGE_NAME, key, LocaleLanguage.ZH);\n    const title = this.locale === LocaleLanguage.ZH && en !== zh ? `${en} ${zh}` : en;\n\n    output.appendNode(new DocPageTitle({ configuration, title, order }));\n  }\n\n  /**\n   * Handle text nodes that contain a dash, should be treated as a unordered list\n   */\n  private _handleTextNodes(node: DocNode): DocNode[] {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    if (node instanceof DocPlainText && node.text.includes(' - ')) {\n      const texts = node.text.split(/ - /g);\n      return [\n        new DocPlainText({ configuration, text: texts[0] }),\n        ...(texts.slice(1) || []).flatMap((text) => [\n          new DocPlainText({ configuration, text: '-' }),\n          new DocPlainText({ configuration, text }),\n        ]),\n      ];\n    }\n    return [node];\n  }\n\n  private _parseParagraph(paragraph: DocParagraph): DocNode[][] {\n    const formattedNodes = paragraph.getChildNodes().flatMap((node) => this._handleTextNodes(node));\n\n    return formattedNodes.reduce((acc: DocNode[][], node: DocNode) => {\n      if (node instanceof DocPlainText && node.text === '-') {\n        acc.push([]);\n      } else if (acc.length) {\n        acc[acc.length - 1].push(node);\n      } else {\n        acc.push([node]);\n      }\n      return acc;\n    }, []);\n  }\n\n  private _handleHtmlStartTag(ps: DocNode[], configuration: TSDocConfiguration, formattedSection: DocSection): boolean {\n    if (ps.length > 0 && ps[0] instanceof DocHtmlStartTag) {\n      const [_htmlStartTag, ...rest] = ps;\n      formattedSection.appendNode(new DocParagraph({ configuration }, rest));\n      return true;\n    }\n    return false;\n  }\n\n  private _writeSummarySection(output: DocSection, apiItem: ApiItem): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    if (apiItem instanceof ApiDocumentedItem) {\n      if (apiItem.tsdocComment !== undefined) {\n        const localizedSection: DocSection = this._localizeSection(apiItem.tsdocComment.summarySection, this.locale);\n\n        const formattedSection = new DocSection({ configuration });\n\n        for (const nodes of localizedSection.getChildNodes()) {\n          const paragraphs = this._parseParagraph(nodes as DocParagraph);\n\n          const latest: DocNode[][] = [];\n\n          for (const ps of paragraphs) {\n            if (!this._handleHtmlStartTag(ps, configuration, formattedSection)) {\n              latest.push(ps);\n            }\n          }\n\n          formattedSection.appendNode(\n            new DocUnorderedList(\n              { configuration },\n              latest.map((nodes) => new DocParagraph({ configuration }, nodes)),\n            ),\n          );\n        }\n\n        this._appendAndMergeSection(output, formattedSection);\n      }\n    }\n  }\n\n  private _getMembersAndWriteIncompleteWarning(\n    apiClassOrInterface: ApiClass | ApiInterface,\n    output: DocSection,\n  ): readonly ApiItem[] {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const showInheritedMembers: boolean = false;\n    if (!showInheritedMembers) {\n      return apiClassOrInterface.members;\n    }\n\n    const result: IFindApiItemsResult = apiClassOrInterface.findMembersWithInheritance();\n\n    // If the result is potentially incomplete, write a short warning communicating this.\n    if (result.maybeIncompleteResult) {\n      output.appendNode(\n        new DocParagraph({ configuration }, [\n          new DocEmphasisSpan({ configuration, italic: true }, [\n            new DocPlainText({\n              configuration,\n              text: '(Some inherited members may not be shown because they are not represented in the documentation.)',\n            }),\n          ]),\n        ]),\n      );\n    }\n\n    // Log the messages for diagnostic purposes.\n    for (const message of result.messages) {\n      // console.log(`Diagnostic message for findMembersWithInheritance: ${message.text}`);\n    }\n\n    return result.items;\n  }\n\n  private _getFilenameForApiItem(apiItem: ApiItem): string {\n    if (apiItem.kind === ApiItemKind.Model) {\n      return `index.${this._getLang()}.md`;\n    }\n\n    let baseName: string = '';\n    for (const hierarchyItem of apiItem.getHierarchy()) {\n      // For overloaded methods, add a suffix such as \"MyClass.myMethod_2\".\n      let qualifiedName: string = Utilities.getSafeFilenameForName(hierarchyItem.displayName);\n      if (ApiParameterListMixin.isBaseClassOf(hierarchyItem)) {\n        if (hierarchyItem.overloadIndex > 1) {\n          // Subtract one for compatibility with earlier releases of API Documenter.\n          // (This will get revamped when we fix GitHub issue #1308)\n          qualifiedName += `_${hierarchyItem.overloadIndex - 1}`;\n        }\n      }\n\n      switch (hierarchyItem.kind) {\n        case ApiItemKind.Model:\n        case ApiItemKind.EntryPoint:\n        case ApiItemKind.EnumMember:\n          break;\n        case ApiItemKind.Package:\n          baseName = Utilities.getSafeFilenameForName(PackageName.getUnscopedName(hierarchyItem.displayName));\n          break;\n        default:\n          baseName += '.' + qualifiedName;\n      }\n    }\n    return baseName + `.${this._getLang()}.md`;\n  }\n\n  private _getLinkFilenameForApiItem(apiItem: ApiItem): string {\n    const relativeUrl: Record<number, string> = {\n      0: './',\n      1: '../reference/',\n      2: '../../reference/',\n    };\n    const prefix = relativeUrl[this.referenceLevel];\n    return prefix + this._getFilenameForApiItem(apiItem);\n  }\n\n  private _localizeSection(section: DocSection, language: LocaleLanguage): DocSection {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n    const trimmed: DocSection = new DocSection({ configuration });\n\n    let isSpecificLanguage = false;\n\n    for (const node of section.getChildNodes()) {\n      if (node instanceof DocParagraph) {\n        for (const childNode of node.getChildNodes()) {\n          if (\n            childNode instanceof DocHtmlStartTag &&\n            childNode.selfClosingTag &&\n            (childNode.name === 'zh' || childNode.name === 'en')\n          ) {\n            isSpecificLanguage = true;\n            const currentLanguage =\n              childNode.selfClosingTag && childNode.name === 'zh' ? LocaleLanguage.ZH : LocaleLanguage.EN;\n            if (currentLanguage == language) {\n              trimmed.appendNode(node);\n            }\n          }\n        }\n        // If the paragraph does not have a language tag, it is considered to be a common paragraph\n        if (!isSpecificLanguage) {\n          trimmed.appendNode(node);\n        }\n      } else if (node instanceof DocFencedCode) {\n        trimmed.appendNode(\n          new DocFencedCode({\n            configuration,\n            code: node.code,\n            language: 'typescript',\n          }),\n        );\n      }\n    }\n\n    return trimmed;\n  }\n\n  private _intl(keyword: Keyword): string;\n  private _intl(keyword: string, type: LocaleType): string;\n  private _intl(keyword: string, type: LocaleType = LocaleType.KEYWORD): string {\n    return intl(type, keyword, this.locale);\n  }\n\n  private _getLang() {\n    return this.locale === LocaleLanguage.ZH ? 'zh' : 'en';\n  }\n\n  private _getLinkInMarkdownFormat(text: string, url?: string) {\n    return url ? `[${text}](${url})` : text;\n  }\n\n  private _assertDemo(output: DocSection, pageData: IPageData): void {\n    const configuration: TSDocConfiguration = this._tsdocConfiguration;\n\n    const demoPath = path.join('common/api/', pageData.group, kebabCase(pageData.name) + '.md');\n\n    if (FileSystem.exists(demoPath)) {\n      output.appendNode(\n        new DocParagraph({ configuration }, [\n          new DocText({ configuration, text: `<embed src=\"@/${demoPath}\"></embed>` }),\n        ]),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/site/src/constants/index.ts",
    "content": "export * from './link';\nexport * from './locales';\n"
  },
  {
    "path": "packages/site/src/constants/link.ts",
    "content": "export const links: Record<string, string> = {\n  BaseStyleProps: 'https://g.antv.antgroup.com/api/basic/display-object#%E7%BB%98%E5%9B%BE%E5%B1%9E%E6%80%A7',\n  DisplayObject: 'https://g.antv.antgroup.com/api/basic/display-object',\n  GroupStyleProps: 'https://g.antv.antgroup.com/api/basic/group',\n  TextStyleProps: 'https://g.antv.antgroup.com/api/basic/text',\n  CircleStyleProps: 'https://g.antv.antgroup.com/api/basic/circle',\n  EllipseStyleProps: 'https://g.antv.antgroup.com/api/basic/ellipse',\n  RectStyleProps: 'https://g.antv.antgroup.com/api/basic/rect',\n  ImageStyleProps: 'https://g.antv.antgroup.com/api/basic/image',\n  LineStyleProps: 'https://g.antv.antgroup.com/api/basic/line',\n  PolygonStyleProps: 'https://g.antv.antgroup.com/api/basic/polygon',\n  PolylineStyleProps: 'https://g.antv.antgroup.com/api/basic/polyline',\n  PathStyleProps: 'https://g.antv.antgroup.com/api/basic/path',\n  HTMLStyleProps: 'https://g.antv.antgroup.com/api/basic/html',\n  // @antv/component\n  CategoryStyleProps:\n    'https://github.com/antvis/component/blob/22e8f25c2e9a461eade16e658f5e16843a2284aa/src/ui/legend/types.ts',\n  TooltipStyleProps:\n    'https://github.com/antvis/component/blob/22e8f25c2e9a461eade16e658f5e16843a2284aa/src/ui/tooltip/types.ts',\n  // bubblesets-js\n  IBubbleSetOptions:\n    'https://github.com/upsetjs/bubblesets-js/blob/0ef0267a2207f5d86cb0dd0064efcd2750a77b12/src/BubbleSets.ts#L48',\n};\n"
  },
  {
    "path": "packages/site/src/constants/locales/api-category.json",
    "content": "{\n  \"option\": [\"Option\", \"图配置项\"],\n  \"render\": [\"Render\", \"渲染\"],\n  \"data\": [\"Data\", \"数据\"],\n  \"instance\": [\"Instance\", \"图实例\"],\n  \"canvas\": [\"Canvas\", \"画布\"],\n  \"viewport\": [\"Viewport\", \"视口\"],\n  \"element\": [\"Element\", \"元素\"],\n  \"animation\": [\"Animation\", \"动画\"],\n  \"layout\": [\"Layout\", \"布局\"],\n  \"behavior\": [\"Behavior\", \"交互\"],\n  \"event\": [\"Event\", \"事件\"],\n  \"theme\": [\"Theme\", \"主题\"],\n  \"transform\": [\"Transform\", \"数据转换\"],\n  \"plugin\": [\"plugin\", \"插件\"],\n  \"exportImage\": [\"Export Image\", \"导出图片\"]\n}\n"
  },
  {
    "path": "packages/site/src/constants/locales/element.json",
    "content": "{\n  \"node\": [\"Node\", \"节点\"],\n  \"edge\": [\"Edge\", \"边\"],\n  \"combo\": [\"Combo\", \"组合\"]\n}\n"
  },
  {
    "path": "packages/site/src/constants/locales/enum.ts",
    "content": "export enum LocaleType {\n  API_CATEGORY = 'api-category',\n  ELEMENT = 'element',\n  HELPER = 'helper',\n  KEYWORD = 'keyword',\n  PAGE_NAME = 'page-name',\n}\n\nexport enum LocaleLanguage {\n  EN = 'en-US',\n  ZH = 'zh-CN',\n}\n\nexport enum Keyword {\n  ABSTRACT_CLASS = 'abstract-class',\n  ABSTRACT_CLASSES = 'abstract-classes',\n  CLASS = 'class',\n  CLASSES = 'classes',\n  COLON = 'colon',\n  COMMA = 'comma',\n  CONSTRUCTOR = 'constructor',\n  CONSTRUCTORS = 'constructors',\n  DECORATOR = 'decorator',\n  DEFAULT_VALUE = 'defaultValue',\n  DESCRIPTION = 'description',\n  ENUMERATION = 'enumeration',\n  ENUMERATION_MEMBERS = 'enumeration-members',\n  ENUMERATIONS = 'enumerations',\n  EVENTS = 'events',\n  EXAMPLE = 'example',\n  EXCEPTIONS = 'exceptions',\n  EXTENDS = 'extends',\n  FUNCTION = 'function',\n  FUNCTIONS = 'functions',\n  IMPLEMENTS = 'implements',\n  INTERFACE = 'interface',\n  INTERFACES = 'interfaces',\n  LEFT_PARENTHESIS = 'leftParenthesis',\n  METHOD = 'method',\n  METHODS = 'methods',\n  MODIFIERS = 'modifiers',\n  NAMESPACE = 'namespace',\n  NAMESPACES = 'namespaces',\n  OPTIONAL = 'optional',\n  OPTIONS = 'options',\n  PACKAGE = 'package',\n  PACKAGES = 'packages',\n  PARAMETER = 'parameter',\n  PROPERTIES = 'properties',\n  PROPERTY = 'property',\n  REFERENCES = 'references',\n  REMARKS = 'remarks',\n  RETURNS = 'returns',\n  RIGHT_PARENTHESIS = 'rightParenthesis',\n  TYPE = 'type',\n  TYPE_ALIAS = 'type-alias',\n  TYPE_ALIASES = 'type-aliases',\n  VARIABLE = 'variable',\n  VARIABLES = 'variables',\n  VIEW_PARAMETERS = 'view-parameters',\n}\n"
  },
  {
    "path": "packages/site/src/constants/locales/helper.json",
    "content": "{\n  \"base-props-style-tip\": [\n    \"If the element has its specific properties, we will list them below. For all generic style attributes, see\",\n    \"如果元素有其特定的属性，我们将在下面列出。对于所有的通用样式属性，见\"\n  ],\n  \"includes\": [\"Includes\", \"其中的\"],\n  \"excludes\": [\"Excludes\", \"除了\"],\n  \"style\": [\"Style\", \"样式\"],\n  \"prefix-summary\": [\n    \"An expression like icon{TextStyleProps} indicates that properties of the TextStyleProps type are prefixed with icon in camelCase format.\",\n    \"形如 icon{TextStyleProps} 的表达式表示在 TextStyleProps 属性名前以小驼峰形式加上 icon 前缀\"\n  ],\n  \"prefix-description-1\": [\"includes the following properties:\", \"包含以下属性：\"],\n  \"prefix-description-2\": [\"means you need to use the following property names:\", \"表示你需要使用以下属性名：\"]\n}\n"
  },
  {
    "path": "packages/site/src/constants/locales/index.ts",
    "content": "import { FileSystem } from '@rushstack/node-core-library';\nimport * as path from 'path';\nimport type { LocaleType } from './enum';\nimport { LocaleLanguage } from './enum';\n\nconst localeMap = new Map<LocaleType, Record<string, string[]>>();\n\nexport const intl = (type: LocaleType, key: string, lang: LocaleLanguage): string => {\n  if (!localeMap.has(type)) {\n    const filePath = path.join(__dirname, `${type}.json`);\n    if (!FileSystem.exists(filePath)) {\n      throw new Error(`The locale file does not exist: ${filePath}`);\n    }\n    localeMap.set(type, JSON.parse(FileSystem.readFile(filePath)));\n  }\n  const [en, zh] = localeMap.get(type)?.[key] || [key, key];\n  return lang === LocaleLanguage.EN ? en : zh;\n};\n"
  },
  {
    "path": "packages/site/src/constants/locales/keyword.json",
    "content": "{\n  \"abstract-class\": [\"Abstract Class\", \"抽象类\"],\n  \"abstract-classes\": [\"Abstract Classes\", \"抽象类\"],\n  \"class\": [\"Class\", \"类\"],\n  \"classes\": [\"Classes\", \"类\"],\n  \"colon\": [\": \", \"：\"],\n  \"comma\": [\", \", \"，\"],\n  \"constructor\": [\"Constructor\", \"构造函数\"],\n  \"constructors\": [\"Constructors\", \"构造函数\"],\n  \"decorator\": [\"Decorator\", \"装饰器\"],\n  \"defaultValue\": [\"Default Value\", \"默认值\"],\n  \"description\": [\"Description\", \"描述\"],\n  \"enumeration-members\": [\"Enumeration Members\", \"枚举成员\"],\n  \"enumeration\": [\"Enumeration\", \"枚举\"],\n  \"enumerations\": [\"Enumerations\", \"枚举\"],\n  \"events\": [\"Events\", \"事件\"],\n  \"example\": [\"Example\", \"示例\"],\n  \"exceptions\": [\"Exceptions\", \"异常\"],\n  \"extends\": [\"Extends\", \"继承自\"],\n  \"function\": [\"Function\", \"函数\"],\n  \"functions\": [\"Functions\", \"函数\"],\n  \"implements\": [\"Implements\", \"实现自\"],\n  \"interface\": [\"Interface\", \"接口\"],\n  \"interfaces\": [\"Interfaces\", \"接口\"],\n  \"leftParenthesis\": [\"(\", \"（\"],\n  \"method\": [\"Method\", \"方法\"],\n  \"methods\": [\"Methods\", \"方法\"],\n  \"modifiers\": [\"Modifiers\", \"修饰符\"],\n  \"namespace\": [\"Namespace\", \"命名空间\"],\n  \"namespaces\": [\"Namespaces\", \"命名空间\"],\n  \"optional\": [\"Optional\", \"可选\"],\n  \"options\": [\"Options\", \"配置项\"],\n  \"package\": [\"Package\", \"包\"],\n  \"packages\": [\"Packages\", \"包\"],\n  \"parameter\": [\"Parameter\", \"参数\"],\n  \"properties\": [\"Properties\", \"属性\"],\n  \"property\": [\"Property\", \"属性\"],\n  \"references\": [\"References\", \"引用\"],\n  \"remarks\": [\"Remarks\", \"附注\"],\n  \"returns\": [\"Returns\", \"返回值\"],\n  \"rightParenthesis\": [\")\", \"）\"],\n  \"type\": [\"Type\", \"类型\"],\n  \"type-alias\": [\"Type Alias\", \"类型别名\"],\n  \"type-aliases\": [\"Type Aliases\", \"类型别名\"],\n  \"variable\": [\"Variable\", \"变量\"],\n  \"variables\": [\"Variables\", \"变量\"],\n  \"view-parameters\": [\"View Parameters\", \"相关参数\"]\n}\n"
  },
  {
    "path": "packages/site/src/constants/locales/page-name.json",
    "content": "{\n  \"GraphOptions\": [\"Options\", \"配置项\"],\n  \"GraphMethods\": [\"API\", \"方法\"],\n  \"GraphProperties\": [\"Properties\", \"属性\"],\n  \"ElementMethods\": [\"API\", \"方法\"],\n  \"GraphData\": [\"GraphData\", \"图数据\"],\n  \"NodeData\": [\"NodeData\", \"节点数据\"],\n  \"EdgeData\": [\"EdgeData\", \"边数据\"],\n  \"ComboData\": [\"ComboData\", \"组合数据\"],\n  \"BaseNode\": [\"BaseNode\", \"节点通用样式\"],\n  \"Circle\": [\"Circle\", \"圆形\"],\n  \"Diamond\": [\"Diamond\", \"菱形\"],\n  \"Donut\": [\"Donut\", \"甜甜圈\"],\n  \"Ellipse\": [\"Ellipse\", \"椭圆形\"],\n  \"Hexagon\": [\"Hexagon\", \"六边形\"],\n  \"Html\": [\"Html\", \"HTML\"],\n  \"Image\": [\"Image\", \"图片\"],\n  \"Rect\": [\"Rect\", \"矩形\"],\n  \"Star\": [\"Star\", \"五角形\"],\n  \"Triangle\": [\"Triangle\", \"三角形\"],\n  \"BaseEdge\": [\"BaseEdge\", \"边通用样式\"],\n  \"Cubic\": [\"Cubic\", \"三次贝塞尔曲线\"],\n  \"CubicHorizontal\": [\"CubicHorizontal\", \"水平三次贝塞尔曲线\"],\n  \"CubicRadial\": [\"CubicRadial\", \"径向三次贝塞尔曲线\"],\n  \"CubicVertical\": [\"CubicVertical\", \"垂直三次贝塞尔曲线\"],\n  \"Line\": [\"Line\", \"直线\"],\n  \"Polyline\": [\"Polyline\", \"折线\"],\n  \"Quadratic\": [\"Quadratic\", \"二次贝塞尔曲线\"],\n  \"BaseCombo\": [\"BaseCombo\", \"组合基础样式\"],\n  \"CircleCombo\": [\"Circle\", \"圆形\"],\n  \"RectCombo\": [\"Rect\", \"矩形\"],\n  \"AntvDagreLayout\": [\"AntvDagre\", \"布局\"],\n  \"CircularLayout\": [\"Circular\", \"环形布局\"],\n  \"ComboCombinedLayout\": [\"ComboCombined\", \"复合布局\"],\n  \"ConcentricLayout\": [\"Concentric\", \"同心圆布局\"],\n  \"D3Force3DLayout\": [\"D3Force3D\", \"3D 力导向布局\"],\n  \"D3ForceLayout\": [\"D3Force\", \"力导向布局\"],\n  \"DagreLayout\": [\"Dagre\", \"层次布局\"],\n  \"Fishbone\": [\"Fishbone\", \"鱼骨布局\"],\n  \"ForceAtlas2Layout\": [\"ForceAtlas2\", \"力导向布局\"],\n  \"ForceLayout\": [\"Force\", \"力导向布局\"],\n  \"FruchtermanLayout\": [\"Fruchterman\", \"力导向布局\"],\n  \"GridLayout\": [\"Grid\", \"网格布局\"],\n  \"MdsLayout\": [\"Mds\", \"高维数据降维布局\"],\n  \"RadialLayout\": [\"Radial\", \"径向布局\"],\n  \"RandomLayout\": [\"Random\", \"随机布局\"],\n  \"SnakeLayout\": [\"Snake\", \"蛇形布局\"],\n  \"BaseBehavior\": [\"BaseBehavior\", \"基础交互\"],\n  \"AutoAdaptLabel\": [\"AutoAdaptLabel\", \"标签自适应显示\"],\n  \"BrushSelect\": [\"BrushSelect\", \"框选\"],\n  \"ClickSelect\": [\"ClickSelect\", \"点击选中\"],\n  \"CollapseExpand\": [\"CollapseExpand\", \"展开/收起元素\"],\n  \"CreateEdge\": [\"CreateEdge\", \"创建边\"],\n  \"DragCanvas\": [\"DragCanvas\", \"拖拽画布\"],\n  \"DragElement\": [\"DragElement\", \"拖拽元素\"],\n  \"DragElementForce\": [\"DragElementForce\", \"力导向拖拽元素\"],\n  \"FixElementSize\": [\"FixElementSize\", \"缩放画布时固定元素大小\"],\n  \"FocusElement\": [\"FocusElement\", \"聚焦元素\"],\n  \"HoverActivate\": [\"HoverActivate\", \"悬停激活\"],\n  \"LassoSelect\": [\"LassoSelect\", \"套索选择\"],\n  \"OptimizeViewportTransform\": [\"OptimizeViewportTransform\", \"操作画布时隐藏元素\"],\n  \"ScrollCanvas\": [\"ScrollCanvas\", \"滚动画布\"],\n  \"ZoomCanvas\": [\"ZoomCanvas\", \"缩放画布\"],\n  \"Background\": [\"Background\", \"背景\"],\n  \"BubbleSets\": [\"BubbleSets\", \"气泡集\"],\n  \"CameraSetting\": [\"CameraSetting\", \"相机设置\"],\n  \"Contextmenu\": [\"Contextmenu\", \"上下文菜单\"],\n  \"EdgeBundling\": [\"EdgeBundling\", \"边绑定\"],\n  \"EdgeFilterLens\": [\"EdgeFilterLens\", \"边过滤镜\"],\n  \"Fisheye\": [\"Fisheye\", \"鱼眼放大镜\"],\n  \"Fullscreen\": [\"Fullscreen\", \"全屏展示\"],\n  \"GridLine\": [\"GridLine\", \"网格线\"],\n  \"History\": [\"History\", \"历史记录\"],\n  \"Hull\": [\"Hull\", \"轮廓包围\"],\n  \"Legend\": [\"Legend\", \"图例\"],\n  \"Minimap\": [\"Minimap\", \"小地图\"],\n  \"Snapline\": [\"Snapline\", \"对齐线\"],\n  \"Timebar\": [\"Timebar\", \"时间条\"],\n  \"Toolbar\": [\"Toolbar\", \"工具栏\"],\n  \"Tooltip\": [\"Tooltip\", \"提示框\"],\n  \"Watermark\": [\"Watermark\", \"水印\"],\n  \"ProcessParallelEdges\": [\"ProcessParallelEdges\", \"平行边\"],\n  \"PlaceRadialLabels\": [\"PlaceRadialLabels\", \"径向标签\"],\n  \"MapNodeSize\": [\"MapNodeSize\", \"动态调整节点大小\"]\n}\n"
  },
  {
    "path": "packages/site/src/markdown/CustomMarkdownEmitter.ts",
    "content": "import type { ApiItem, ApiModel, IResolveDeclarationReferenceResult } from '@microsoft/api-extractor-model';\nimport type { DocLinkTag, DocNode, StringBuilder } from '@microsoft/tsdoc';\nimport { CustomDocNodeKind } from '../nodes/CustomDocNodeKind';\nimport type { DocContainer } from '../nodes/DocContainer';\nimport type { DocDetails } from '../nodes/DocDetails';\nimport type { DocEmphasisSpan } from '../nodes/DocEmphasisSpan';\nimport type { DocHeading } from '../nodes/DocHeading';\nimport type { DocNoteBox } from '../nodes/DocNoteBox';\nimport type { DocPageTitle } from '../nodes/DocPageTitle';\nimport type { DocTable } from '../nodes/DocTable';\nimport type { DocTableCell } from '../nodes/DocTableCell';\nimport type { DocText } from '../nodes/DocText';\nimport type { DocUnorderedList } from '../nodes/DocUnorderedList';\nimport type { IndentedWriter } from '../utils/IndentedWriter';\nimport type { IMarkdownEmitterContext, IMarkdownEmitterOptions } from './MarkdownEmitter';\nimport { MarkdownEmitter } from './MarkdownEmitter';\n\nexport interface ICustomMarkdownEmitterOptions extends IMarkdownEmitterOptions {\n  contextApiItem: ApiItem | undefined;\n\n  onGetFilenameForApiItem: (apiItem: ApiItem) => string | undefined;\n}\n\nexport class CustomMarkdownEmitter extends MarkdownEmitter {\n  private _apiModel: ApiModel;\n\n  public constructor(apiModel: ApiModel) {\n    super();\n\n    this._apiModel = apiModel;\n  }\n\n  public emit(stringBuilder: StringBuilder, docNode: DocNode, options: ICustomMarkdownEmitterOptions): string {\n    return super.emit(stringBuilder, docNode, options);\n  }\n\n  /** @override */\n  protected writeNode(docNode: DocNode, context: IMarkdownEmitterContext, docNodeSiblings: boolean): void {\n    const writer: IndentedWriter = context.writer;\n\n    switch (docNode.kind) {\n      case CustomDocNodeKind.Text: {\n        const docText: DocText = docNode as DocText;\n        writer.ensureNewLine();\n        writer.write(docText.text);\n        writer.ensureNewLine();\n        break;\n      }\n      case CustomDocNodeKind.Heading: {\n        const docHeading: DocHeading = docNode as DocHeading;\n        writer.ensureSkippedLine();\n\n        let prefix: string;\n        switch (docHeading.level) {\n          case 1:\n            prefix = '##';\n            break;\n          case 2:\n            prefix = '###';\n            break;\n          case 3:\n            prefix = '####';\n            break;\n          default:\n            prefix = '####';\n        }\n\n        const _prefix = docHeading.prefix ? docHeading.prefix + ' ' : '';\n        const _suffix = docHeading.suffix ? ' ' + docHeading.suffix : '';\n\n        writer.writeLine(prefix + ' ' + _prefix + this.getEscapedText(docHeading.title) + _suffix);\n        writer.writeLine();\n        break;\n      }\n      case CustomDocNodeKind.NoteBox: {\n        const docNoteBox: DocNoteBox = docNode as DocNoteBox;\n        writer.ensureNewLine();\n\n        writer.increaseIndent('> ');\n\n        this.writeNode(docNoteBox.content, context, false);\n        writer.ensureNewLine();\n\n        writer.decreaseIndent();\n\n        writer.writeLine();\n        break;\n      }\n      case CustomDocNodeKind.Table: {\n        const docTable: DocTable = docNode as DocTable;\n        // GitHub's markdown renderer chokes on tables that don't have a blank line above them,\n        // whereas VS Code's renderer is totally fine with it.\n        writer.ensureSkippedLine();\n\n        // Markdown table rows can have inconsistent cell counts.  Size the table based on the longest row.\n        let columnCount: number = 0;\n        if (docTable.header) {\n          columnCount = docTable.header.cells.length;\n        }\n        for (const row of docTable.rows) {\n          if (row.cells.length > columnCount) {\n            columnCount = row.cells.length;\n          }\n        }\n\n        writer.write('<table>');\n        if (docTable.header) {\n          writer.write('<thead><tr>');\n          for (let i: number = 0; i < columnCount; ++i) {\n            writer.write('<th>');\n            writer.ensureNewLine();\n            writer.writeLine();\n            const cell: DocTableCell | undefined = docTable.header.cells[i];\n            if (cell) {\n              this.writeNode(cell.content, context, false);\n            }\n            writer.ensureNewLine();\n            writer.writeLine();\n            writer.write('</th>');\n          }\n          writer.write('</tr></thead>');\n        }\n        writer.writeLine();\n\n        writer.write('<tbody>');\n        for (const row of docTable.rows) {\n          writer.write('<tr>');\n          for (const cell of row.cells) {\n            writer.write('<td>');\n            writer.ensureNewLine();\n            writer.writeLine();\n            this.writeNode(cell.content, context, false);\n            writer.ensureNewLine();\n            writer.writeLine();\n            writer.write('</td>');\n          }\n          writer.write('</tr>');\n          writer.writeLine();\n        }\n        writer.write('</tbody>');\n        writer.write('</table>');\n        writer.writeLine();\n\n        break;\n      }\n      case CustomDocNodeKind.EmphasisSpan: {\n        const docEmphasisSpan: DocEmphasisSpan = docNode as DocEmphasisSpan;\n        const oldBold: boolean = context.boldRequested;\n        const oldItalic: boolean = context.italicRequested;\n        context.boldRequested = docEmphasisSpan.bold;\n        context.italicRequested = docEmphasisSpan.italic;\n        this.writeNodes(docEmphasisSpan.nodes, context);\n        context.boldRequested = oldBold;\n        context.italicRequested = oldItalic;\n        break;\n      }\n      case CustomDocNodeKind.PageTitle: {\n        const docPageTitle: DocPageTitle = docNode as DocPageTitle;\n\n        writer.writeLine('---');\n        writer.writeLine('title: ' + docPageTitle.title);\n        if (docPageTitle.order !== undefined) {\n          writer.ensureNewLine();\n          writer.writeLine('order: ' + docPageTitle.order);\n        }\n        if (docPageTitle.readonly) {\n          writer.ensureNewLine();\n          writer.writeLine('readonly: true');\n        }\n        writer.writeLine('---');\n        break;\n      }\n      case CustomDocNodeKind.Details: {\n        const docDetails: DocDetails = docNode as DocDetails;\n\n        writer.write('<details>');\n\n        writer.write('<summary>');\n        writer.write(docDetails.summary || '');\n        writer.write('</summary>');\n\n        writer.ensureSkippedLine();\n        this.writeNode(docDetails.content, context, false);\n        writer.ensureNewLine();\n        writer.write('</details>');\n\n        writer.ensureSkippedLine();\n        break;\n      }\n      case CustomDocNodeKind.UnorderedList: {\n        const docUnorderedList: DocUnorderedList = docNode as DocUnorderedList;\n\n        writer.writeLine();\n\n        for (const docNode of docUnorderedList.getChildNodes()) {\n          writer.ensureNewLine();\n          writer.writeLine('- ');\n          this.writeNode(docNode, context, false);\n        }\n\n        writer.writeLine();\n        break;\n      }\n      case CustomDocNodeKind.Container: {\n        const container: DocContainer = docNode as DocContainer;\n        writer.ensureSkippedLine();\n        writer.write(`:::${container.status}` + (container.title ? `{title=${container.title}}` : ''));\n        writer.ensureSkippedLine();\n        this.writeNodes(container.getChildNodes(), context);\n        writer.write(':::');\n        writer.ensureSkippedLine();\n        break;\n      }\n      default:\n        super.writeNode(docNode, context, docNodeSiblings);\n    }\n  }\n\n  /** @override */\n  protected writeLinkTagWithCodeDestination(\n    docLinkTag: DocLinkTag,\n    context: IMarkdownEmitterContext<ICustomMarkdownEmitterOptions>,\n  ): void {\n    const options: ICustomMarkdownEmitterOptions = context.options;\n\n    const result: IResolveDeclarationReferenceResult = this._apiModel.resolveDeclarationReference(\n      docLinkTag.codeDestination!,\n      options.contextApiItem,\n    );\n\n    if (result.resolvedApiItem) {\n      const filename: string | undefined = options.onGetFilenameForApiItem(result.resolvedApiItem);\n\n      if (filename) {\n        let linkText: string = docLinkTag.linkText || '';\n        if (linkText.length === 0) {\n          // Generate a name such as Namespace1.Namespace2.MyClass.myMethod()\n          linkText = result.resolvedApiItem.getScopedNameWithinPackage();\n        }\n        if (linkText.length > 0) {\n          const encodedLinkText: string = this.getEscapedText(linkText.replace(/\\s+/g, ' '));\n\n          context.writer.write('[');\n          context.writer.write(encodedLinkText);\n          context.writer.write(`](${filename!})`);\n        } else {\n          console.log('WARNING: Unable to determine link text');\n        }\n      }\n    } else if (result.errorMessage) {\n      console.log(\n        `WARNING: Unable to resolve reference \"${docLinkTag.codeDestination!.emitAsTsdoc()}\": ` + result.errorMessage,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/site/src/markdown/MarkdownEmitter.ts",
    "content": "import type {\n  DocBlockTag,\n  DocCodeSpan,\n  DocErrorText,\n  DocEscapedText,\n  DocFencedCode,\n  DocHtmlEndTag,\n  DocHtmlStartTag,\n  DocLinkTag,\n  DocNode,\n  DocParagraph,\n  DocPlainText,\n  DocSection,\n  StringBuilder,\n} from '@microsoft/tsdoc';\nimport { DocNodeKind, DocNodeTransforms } from '@microsoft/tsdoc';\nimport { InternalError } from '@rushstack/node-core-library';\n\nimport { IndentedWriter } from '../utils/IndentedWriter';\n\nexport interface IMarkdownEmitterOptions {}\n\nexport interface IMarkdownEmitterContext<TOptions = IMarkdownEmitterOptions> {\n  writer: IndentedWriter;\n\n  boldRequested: boolean;\n  italicRequested: boolean;\n\n  writingBold: boolean;\n  writingItalic: boolean;\n\n  options: TOptions;\n}\n\n/**\n * Renders MarkupElement content in the Markdown file format.\n * For more info:  https://en.wikipedia.org/wiki/Markdown\n */\nexport class MarkdownEmitter {\n  public emit(stringBuilder: StringBuilder, docNode: DocNode, options: IMarkdownEmitterOptions): string {\n    const writer: IndentedWriter = new IndentedWriter(stringBuilder);\n\n    const context: IMarkdownEmitterContext = {\n      writer,\n\n      boldRequested: false,\n      italicRequested: false,\n\n      writingBold: false,\n      writingItalic: false,\n\n      options,\n    };\n\n    this.writeNode(docNode, context, false);\n\n    writer.ensureNewLine(); // finish the last line\n\n    return writer.toString();\n  }\n\n  protected getEscapedText(text: string): string {\n    const textWithBackslashes: string = text\n      .replace(/\\\\/g, '\\\\\\\\') // first replace the escape character\n      .replace(/[*#\\\\_|~]/g, (x) => '\\\\' + x) // then escape any special characters, except `[`,`]`,```\n      .replace(/---/g, '\\\\-\\\\-\\\\-') // hyphens only if it's 3 or more\n      .replace(/&/g, '&amp;')\n      .replace(/</g, '&lt;')\n      .replace(/>/g, '&gt;');\n    return textWithBackslashes;\n  }\n\n  protected getTableEscapedText(text: string): string {\n    return text\n      .replace(/&/g, '&amp;')\n      .replace(/\"/g, '&quot;')\n      .replace(/</g, '&lt;')\n      .replace(/>/g, '&gt;')\n      .replace(/\\|/g, '&#124;');\n  }\n\n  protected writeNode(docNode: DocNode, context: IMarkdownEmitterContext, docNodeSiblings: boolean): void {\n    const writer: IndentedWriter = context.writer;\n\n    switch (docNode.kind) {\n      case DocNodeKind.PlainText: {\n        const docPlainText: DocPlainText = docNode as DocPlainText;\n        this.writePlainText(docPlainText.text, context);\n        break;\n      }\n      case DocNodeKind.HtmlStartTag:\n      case DocNodeKind.HtmlEndTag: {\n        const docHtmlTag: DocHtmlStartTag | DocHtmlEndTag = docNode as DocHtmlStartTag | DocHtmlEndTag;\n        // write the HTML element verbatim into the output\n        writer.write(docHtmlTag.emitAsHtml());\n        break;\n      }\n      case DocNodeKind.CodeSpan: {\n        const docCodeSpan: DocCodeSpan = docNode as DocCodeSpan;\n        writer.write('`');\n        writer.write(docCodeSpan.code);\n        writer.write('`');\n        break;\n      }\n      case DocNodeKind.LinkTag: {\n        const docLinkTag: DocLinkTag = docNode as DocLinkTag;\n        if (docLinkTag.codeDestination) {\n          this.writeLinkTagWithCodeDestination(docLinkTag, context);\n        } else if (docLinkTag.urlDestination) {\n          this.writeLinkTagWithUrlDestination(docLinkTag, context);\n        } else if (docLinkTag.linkText) {\n          this.writePlainText(docLinkTag.linkText, context);\n        }\n        break;\n      }\n      case DocNodeKind.Paragraph: {\n        const docParagraph: DocParagraph = docNode as DocParagraph;\n        const trimmedParagraph: DocParagraph = DocNodeTransforms.trimSpacesInParagraph(docParagraph);\n\n        this.writeNodes(trimmedParagraph.nodes, context);\n        writer.ensureNewLine();\n        writer.writeLine();\n        break;\n      }\n      case DocNodeKind.FencedCode: {\n        const docFencedCode: DocFencedCode = docNode as DocFencedCode;\n        writer.ensureNewLine();\n        writer.write('```');\n        writer.write(docFencedCode.language);\n        writer.writeLine();\n        writer.write(docFencedCode.code);\n        writer.ensureNewLine();\n        writer.writeLine('```');\n        break;\n      }\n      case DocNodeKind.Section: {\n        const docSection: DocSection = docNode as DocSection;\n        this.writeNodes(docSection.nodes, context);\n        break;\n      }\n      case DocNodeKind.SoftBreak: {\n        if (!/^\\s?$/.test(writer.peekLastCharacter())) {\n          writer.write(' ');\n        }\n        break;\n      }\n      case DocNodeKind.EscapedText: {\n        const docEscapedText: DocEscapedText = docNode as DocEscapedText;\n        this.writePlainText(docEscapedText.decodedText, context);\n        break;\n      }\n      case DocNodeKind.ErrorText: {\n        const docErrorText: DocErrorText = docNode as DocErrorText;\n        this.writePlainText(docErrorText.text, context);\n        break;\n      }\n      case DocNodeKind.InlineTag: {\n        break;\n      }\n      case DocNodeKind.BlockTag: {\n        const tagNode: DocBlockTag = docNode as DocBlockTag;\n        console.warn('Unsupported block tag: ' + tagNode.tagName);\n        break;\n      }\n      default:\n        throw new InternalError('Unsupported DocNodeKind kind: ' + docNode.kind);\n    }\n  }\n\n  protected writeLinkTagWithCodeDestination(docLinkTag: DocLinkTag, context: IMarkdownEmitterContext): void {\n    // The subclass needs to implement this to support code destinations\n    throw new InternalError('writeLinkTagWithCodeDestination()');\n  }\n\n  protected writeLinkTagWithUrlDestination(docLinkTag: DocLinkTag, context: IMarkdownEmitterContext): void {\n    const linkText: string = docLinkTag.linkText !== undefined ? docLinkTag.linkText : docLinkTag.urlDestination!;\n\n    const encodedLinkText: string = this.getEscapedText(linkText.replace(/\\s+/g, ' '));\n\n    context.writer.write('[');\n    context.writer.write(encodedLinkText);\n    context.writer.write(`](${docLinkTag.urlDestination!})`);\n  }\n\n  protected writePlainText(text: string, context: IMarkdownEmitterContext): void {\n    const writer: IndentedWriter = context.writer;\n\n    // split out the [ leading whitespace, content, trailing whitespace ]\n    const parts: string[] = text.match(/^(\\s*)(.*?)(\\s*)$/) || [];\n\n    writer.write(parts[1]); // write leading whitespace\n\n    const middle: string = parts[2];\n\n    if (middle !== '') {\n      switch (writer.peekLastCharacter()) {\n        case '':\n        case '\\n':\n        case ' ':\n        case '[':\n        case '>':\n          // okay to put a symbol\n          break;\n        default:\n          // This is no problem:        \"**one** *two* **three**\"\n          // But this is trouble:       \"**one***two***three**\"\n          // The most general solution: \"**one**<!-- -->*two*<!-- -->**three**\"\n          writer.write('<!-- -->');\n          break;\n      }\n\n      if (context.boldRequested) {\n        writer.write('**');\n      }\n      if (context.italicRequested) {\n        writer.write('_');\n      }\n\n      writer.write(this.getEscapedText(middle));\n\n      if (context.italicRequested) {\n        writer.write('_');\n      }\n      if (context.boldRequested) {\n        writer.write('**');\n      }\n    }\n\n    writer.write(parts[3]); // write trailing whitespace\n  }\n\n  protected writeNodes(docNodes: ReadonlyArray<DocNode>, context: IMarkdownEmitterContext): void {\n    for (const docNode of docNodes) {\n      this.writeNode(docNode, context, docNodes.length > 1);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/site/src/nodes/CustomDocNodeKind.ts",
    "content": "import { DocNodeKind, TSDocConfiguration } from '@microsoft/tsdoc';\nimport { DocContainer } from './DocContainer';\nimport { DocDetails } from './DocDetails';\nimport { DocEmphasisSpan } from './DocEmphasisSpan';\nimport { DocHeading } from './DocHeading';\nimport { DocNoteBox } from './DocNoteBox';\nimport { DocPageTitle } from './DocPageTitle';\nimport { DocTable } from './DocTable';\nimport { DocTableCell } from './DocTableCell';\nimport { DocTableRow } from './DocTableRow';\nimport { DocText } from './DocText';\nimport { DocUnorderedList } from './DocUnorderedList';\n\n/**\n * Identifies custom subclasses\n */\nexport const enum CustomDocNodeKind {\n  EmphasisSpan = 'EmphasisSpan',\n  Heading = 'Heading',\n  NoteBox = 'NoteBox',\n  Table = 'Table',\n  TableCell = 'TableCell',\n  TableRow = 'TableRow',\n  PageTitle = 'PageTitle',\n  Details = 'Details',\n  UnorderedList = 'UnorderedList',\n  Container = 'Container',\n  Text = 'Text',\n}\n\nexport class CustomDocNodes {\n  private static _configuration: TSDocConfiguration | undefined;\n\n  public static get configuration(): TSDocConfiguration {\n    if (CustomDocNodes._configuration === undefined) {\n      const configuration: TSDocConfiguration = new TSDocConfiguration();\n\n      configuration.docNodeManager.registerDocNodes('@micrososft/api-documenter', [\n        {\n          docNodeKind: CustomDocNodeKind.EmphasisSpan,\n          constructor: DocEmphasisSpan,\n        },\n        { docNodeKind: CustomDocNodeKind.Heading, constructor: DocHeading },\n        { docNodeKind: CustomDocNodeKind.NoteBox, constructor: DocNoteBox },\n        { docNodeKind: CustomDocNodeKind.Table, constructor: DocTable },\n        {\n          docNodeKind: CustomDocNodeKind.TableCell,\n          constructor: DocTableCell,\n        },\n        { docNodeKind: CustomDocNodeKind.TableRow, constructor: DocTableRow },\n        {\n          docNodeKind: CustomDocNodeKind.PageTitle,\n          constructor: DocPageTitle,\n        },\n        { docNodeKind: CustomDocNodeKind.Details, constructor: DocDetails },\n        {\n          docNodeKind: CustomDocNodeKind.UnorderedList,\n          constructor: DocUnorderedList,\n        },\n        {\n          docNodeKind: CustomDocNodeKind.Container,\n          constructor: DocContainer,\n        },\n        {\n          docNodeKind: CustomDocNodeKind.Text,\n          constructor: DocText,\n        },\n      ]);\n\n      configuration.docNodeManager.registerAllowableChildren(CustomDocNodeKind.EmphasisSpan, [\n        DocNodeKind.PlainText,\n        DocNodeKind.SoftBreak,\n        DocNodeKind.Paragraph,\n        DocNodeKind.LinkTag,\n        DocNodeKind.CodeSpan,\n      ]);\n\n      configuration.docNodeManager.registerAllowableChildren(CustomDocNodeKind.UnorderedList, [\n        DocNodeKind.PlainText,\n        DocNodeKind.LinkTag,\n        DocNodeKind.Paragraph,\n        DocNodeKind.CodeSpan,\n      ]);\n\n      configuration.docNodeManager.registerAllowableChildren(DocNodeKind.Section, [\n        CustomDocNodeKind.Heading,\n        CustomDocNodeKind.NoteBox,\n        CustomDocNodeKind.Table,\n        CustomDocNodeKind.PageTitle,\n        CustomDocNodeKind.Details,\n        CustomDocNodeKind.Container,\n        CustomDocNodeKind.UnorderedList,\n      ]);\n\n      configuration.docNodeManager.registerAllowableChildren(DocNodeKind.Paragraph, [\n        CustomDocNodeKind.EmphasisSpan,\n        CustomDocNodeKind.UnorderedList,\n        CustomDocNodeKind.Text,\n      ]);\n\n      configuration.docNodeManager.registerAllowableChildren(CustomDocNodeKind.Container, [DocNodeKind.Paragraph]);\n\n      CustomDocNodes._configuration = configuration;\n    }\n    return CustomDocNodes._configuration;\n  }\n}\n"
  },
  {
    "path": "packages/site/src/nodes/DocContainer.ts",
    "content": "import type { IDocNodeParameters } from '@microsoft/tsdoc';\nimport { DocNode, DocSection } from '@microsoft/tsdoc';\nimport { CustomDocNodeKind } from './CustomDocNodeKind';\n\ntype ContainerStatus = 'info' | 'success' | 'warning' | 'error';\n/**\n * Constructor parameters for {@link DocContainer}.\n */\nexport interface IDocContainerParameters extends IDocNodeParameters {\n  status?: ContainerStatus;\n  title?: string;\n}\n\n/**\n * Represents a note box, which is typically displayed as a bordered box containing informational text.\n */\nexport class DocContainer extends DocNode {\n  public readonly content: DocSection;\n  public readonly status: 'info' | 'success' | 'warning' | 'error';\n  public readonly title: string | undefined;\n\n  public constructor(parameters: IDocContainerParameters, sectionChildNodes?: ReadonlyArray<DocNode>) {\n    super(parameters);\n    this.content = new DocSection({ configuration: this.configuration }, sectionChildNodes);\n    this.status = parameters.status || 'info';\n    this.title = parameters.title;\n  }\n\n  /** @override */\n  public get kind(): string {\n    return CustomDocNodeKind.Container;\n  }\n\n  /** @override */\n  protected onGetChildNodes(): ReadonlyArray<DocNode | undefined> {\n    return [this.content];\n  }\n}\n"
  },
  {
    "path": "packages/site/src/nodes/DocDetails.ts",
    "content": "import type { IDocNodeParameters } from '@microsoft/tsdoc';\nimport { DocNode, DocSection } from '@microsoft/tsdoc';\nimport { CustomDocNodeKind } from './CustomDocNodeKind';\n\n/**\n * Constructor parameters for {@link DocDetails}.\n */\nexport interface IDocDetailsParameters extends IDocNodeParameters {}\n\n/**\n * Represents a detail box, which is a collapsible section that can contain arbitrary content as the following format:\n *\n * ```\n * <details>\n *    <summary>Click to expand</summary>\n *    <p>Content goes here</p>\n * </details>\n */\nexport class DocDetails extends DocNode {\n  public readonly summary: string;\n  public readonly content: DocSection;\n\n  public constructor(parameters: IDocDetailsParameters, summary: string, sectionChildNodes?: ReadonlyArray<DocNode>) {\n    super(parameters);\n    this.summary = summary;\n    this.content = new DocSection({ configuration: this.configuration }, sectionChildNodes);\n  }\n\n  /** @override */\n  public get kind(): string {\n    return CustomDocNodeKind.Details;\n  }\n\n  /** @override */\n  protected onGetChildNodes(): ReadonlyArray<DocNode | undefined> {\n    return [this.content];\n  }\n}\n"
  },
  {
    "path": "packages/site/src/nodes/DocEmphasisSpan.ts",
    "content": "import type { DocNode, IDocNodeContainerParameters } from '@microsoft/tsdoc';\nimport { DocNodeContainer } from '@microsoft/tsdoc';\nimport { CustomDocNodeKind } from './CustomDocNodeKind';\n\n/**\n * Constructor parameters for {@link DocEmphasisSpan}.\n */\nexport interface IDocEmphasisSpanParameters extends IDocNodeContainerParameters {\n  bold?: boolean;\n  italic?: boolean;\n}\n\n/**\n * Represents a span of text that is styled with CommonMark emphasis (italics), strong emphasis (boldface),\n * or both.\n */\nexport class DocEmphasisSpan extends DocNodeContainer {\n  public readonly bold: boolean;\n  public readonly italic: boolean;\n\n  public constructor(parameters: IDocEmphasisSpanParameters, children?: DocNode[]) {\n    super(parameters, children);\n    this.bold = !!parameters.bold;\n    this.italic = !!parameters.italic;\n  }\n\n  /** @override */\n  public get kind(): string {\n    return CustomDocNodeKind.EmphasisSpan;\n  }\n}\n"
  },
  {
    "path": "packages/site/src/nodes/DocHeading.ts",
    "content": "import type { IDocNodeParameters } from '@microsoft/tsdoc';\nimport { DocNode } from '@microsoft/tsdoc';\nimport { CustomDocNodeKind } from './CustomDocNodeKind';\n\n/**\n * Constructor parameters for {@link DocHeading}.\n */\nexport interface IDocHeadingParameters extends IDocNodeParameters {\n  title: string;\n  level?: number;\n  escaped?: boolean;\n  prefix?: string;\n  suffix?: string;\n}\n\n/**\n * Represents a section header similar to an HTML `<h1>` or `<h2>` element.\n */\nexport class DocHeading extends DocNode {\n  public readonly title: string;\n  public readonly level: number;\n  public readonly escaped: boolean;\n  public readonly prefix: string;\n  public readonly suffix: string;\n\n  public constructor(parameters: IDocHeadingParameters) {\n    super(parameters);\n    this.title = parameters.title;\n    this.level = parameters.level !== undefined ? parameters.level : 1;\n    this.escaped = parameters.escaped || true;\n    this.prefix = parameters.prefix || '';\n    this.suffix = parameters.suffix || '';\n\n    if (this.level < 1 || this.level > 5) {\n      throw new Error('IDocHeadingParameters.level must be a number between 1 and 5');\n    }\n  }\n\n  /** @override */\n  public get kind(): string {\n    return CustomDocNodeKind.Heading;\n  }\n}\n"
  },
  {
    "path": "packages/site/src/nodes/DocNoteBox.ts",
    "content": "import type { IDocNodeParameters } from '@microsoft/tsdoc';\nimport { DocNode, DocSection } from '@microsoft/tsdoc';\nimport { CustomDocNodeKind } from './CustomDocNodeKind';\n\n/**\n * Constructor parameters for {@link DocNoteBox}.\n */\nexport interface IDocNoteBoxParameters extends IDocNodeParameters {}\n\n/**\n * Represents a note box, which is typically displayed as a bordered box containing informational text.\n */\nexport class DocNoteBox extends DocNode {\n  public readonly content: DocSection;\n\n  public constructor(parameters: IDocNoteBoxParameters, sectionChildNodes?: ReadonlyArray<DocNode>) {\n    super(parameters);\n    this.content = new DocSection({ configuration: this.configuration }, sectionChildNodes);\n  }\n\n  /** @override */\n  public get kind(): string {\n    return CustomDocNodeKind.NoteBox;\n  }\n\n  /** @override */\n  protected onGetChildNodes(): ReadonlyArray<DocNode | undefined> {\n    return [this.content];\n  }\n}\n"
  },
  {
    "path": "packages/site/src/nodes/DocPageTitle.ts",
    "content": "import type { IDocNodeParameters } from '@microsoft/tsdoc';\nimport { DocNode } from '@microsoft/tsdoc';\nimport { CustomDocNodeKind } from './CustomDocNodeKind';\n\n/**\n * Constructor parameters for {@link DocPageTitle}.\n */\nexport interface IDocPageTitleParameters extends IDocNodeParameters {\n  title: string;\n  order?: number;\n  readonly?: boolean;\n}\n\n/**\n * Represents a page title as the following format:\n * ```\n * ---\n * title: 文档标题\n * order: 1 <!-- order 越小越靠前，默认为 0 -->\n * ---\n * ```\n */\nexport class DocPageTitle extends DocNode {\n  public readonly title: string;\n  public readonly order?: number;\n  public readonly readonly?: boolean;\n\n  public constructor(parameters: IDocPageTitleParameters) {\n    super(parameters);\n    this.title = parameters.title;\n    this.order = parameters.order;\n    this.readonly = true;\n  }\n\n  /** @override */\n  public get kind(): string {\n    return CustomDocNodeKind.PageTitle;\n  }\n}\n"
  },
  {
    "path": "packages/site/src/nodes/DocTable.ts",
    "content": "import type { IDocNodeParameters } from '@microsoft/tsdoc';\nimport { DocNode } from '@microsoft/tsdoc';\nimport { CustomDocNodeKind } from './CustomDocNodeKind';\nimport type { DocTableCell } from './DocTableCell';\nimport { DocTableRow } from './DocTableRow';\n\n/**\n * Constructor parameters for {@link DocTable}.\n */\nexport interface IDocTableParameters extends IDocNodeParameters {\n  headerCells?: ReadonlyArray<DocTableCell>;\n  headerTitles?: string[];\n}\n\n/**\n * Represents table, similar to an HTML `<table>` element.\n */\nexport class DocTable extends DocNode {\n  public readonly header: DocTableRow;\n\n  private _rows: DocTableRow[];\n\n  public constructor(parameters: IDocTableParameters, rows?: ReadonlyArray<DocTableRow>) {\n    super(parameters);\n\n    this.header = new DocTableRow({ configuration: this.configuration });\n    this._rows = [];\n\n    if (parameters) {\n      if (parameters.headerTitles) {\n        if (parameters.headerCells) {\n          throw new Error(\n            'IDocTableParameters.headerCells and IDocTableParameters.headerTitles' + ' cannot both be specified',\n          );\n        }\n        for (const cellText of parameters.headerTitles) {\n          this.header.addPlainTextCell(cellText);\n        }\n      } else if (parameters.headerCells) {\n        for (const cell of parameters.headerCells) {\n          this.header.addCell(cell);\n        }\n      }\n    }\n\n    if (rows) {\n      for (const row of rows) {\n        this.addRow(row);\n      }\n    }\n  }\n\n  /** @override */\n  public get kind(): string {\n    return CustomDocNodeKind.Table;\n  }\n\n  public get rows(): ReadonlyArray<DocTableRow> {\n    return this._rows;\n  }\n\n  public addRow(row: DocTableRow): void {\n    this._rows.push(row);\n  }\n\n  public createAndAddRow(): DocTableRow {\n    const row: DocTableRow = new DocTableRow({ configuration: this.configuration });\n    this.addRow(row);\n    return row;\n  }\n\n  /** @override */\n  protected onGetChildNodes(): ReadonlyArray<DocNode | undefined> {\n    return [this.header, ...this._rows];\n  }\n}\n"
  },
  {
    "path": "packages/site/src/nodes/DocTableCell.ts",
    "content": "import type { IDocNodeParameters } from '@microsoft/tsdoc';\nimport { DocNode, DocSection } from '@microsoft/tsdoc';\nimport { CustomDocNodeKind } from './CustomDocNodeKind';\n\n/**\n * Constructor parameters for {@link DocTableCell}.\n */\nexport interface IDocTableCellParameters extends IDocNodeParameters {}\n\n/**\n * Represents table cell, similar to an HTML `<td>` element.\n */\nexport class DocTableCell extends DocNode {\n  public readonly content: DocSection;\n\n  public constructor(parameters: IDocTableCellParameters, sectionChildNodes?: ReadonlyArray<DocNode>) {\n    super(parameters);\n\n    this.content = new DocSection({ configuration: this.configuration }, sectionChildNodes);\n  }\n\n  /** @override */\n  public get kind(): string {\n    return CustomDocNodeKind.TableCell;\n  }\n}\n"
  },
  {
    "path": "packages/site/src/nodes/DocTableRow.ts",
    "content": "import type { IDocNodeParameters } from '@microsoft/tsdoc';\nimport { DocNode, DocPlainText } from '@microsoft/tsdoc';\nimport { CustomDocNodeKind } from './CustomDocNodeKind';\nimport { DocTableCell } from './DocTableCell';\n\n/**\n * Constructor parameters for {@link DocTableRow}.\n */\nexport interface IDocTableRowParameters extends IDocNodeParameters {}\n\n/**\n * Represents table row, similar to an HTML `<tr>` element.\n */\nexport class DocTableRow extends DocNode {\n  private readonly _cells: DocTableCell[];\n\n  public constructor(parameters: IDocTableRowParameters, cells?: ReadonlyArray<DocTableCell>) {\n    super(parameters);\n\n    this._cells = [];\n    if (cells) {\n      for (const cell of cells) {\n        this.addCell(cell);\n      }\n    }\n  }\n\n  /** @override */\n  public get kind(): string {\n    return CustomDocNodeKind.TableRow;\n  }\n\n  public get cells(): ReadonlyArray<DocTableCell> {\n    return this._cells;\n  }\n\n  public addCell(cell: DocTableCell): void {\n    this._cells.push(cell);\n  }\n\n  public createAndAddCell(): DocTableCell {\n    const newCell: DocTableCell = new DocTableCell({ configuration: this.configuration });\n    this.addCell(newCell);\n    return newCell;\n  }\n\n  public addPlainTextCell(cellContent: string): DocTableCell {\n    const cell: DocTableCell = this.createAndAddCell();\n    cell.content.appendNodeInParagraph(\n      new DocPlainText({\n        configuration: this.configuration,\n        text: cellContent,\n      }),\n    );\n    return cell;\n  }\n\n  /** @override */\n  protected onGetChildNodes(): ReadonlyArray<DocNode | undefined> {\n    return this._cells;\n  }\n}\n"
  },
  {
    "path": "packages/site/src/nodes/DocText.ts",
    "content": "import type { IDocNodeParameters } from '@microsoft/tsdoc';\nimport { DocNode } from '@microsoft/tsdoc';\nimport { CustomDocNodeKind } from './CustomDocNodeKind';\n\n/**\n * Constructor parameters for {@link DocText}.\n */\nexport interface IDocTextParameters extends IDocNodeParameters {\n  text: string;\n}\n\n/**\n * Different from DocPlainText, DocText won't do parsing for the text.\n */\nexport class DocText extends DocNode {\n  public readonly text: string;\n\n  public constructor(parameters: IDocTextParameters) {\n    super(parameters);\n    this.text = parameters.text;\n  }\n\n  /** @override */\n  public get kind(): string {\n    return CustomDocNodeKind.Text;\n  }\n}\n"
  },
  {
    "path": "packages/site/src/nodes/DocUnorderedList.ts",
    "content": "import type { DocNode, IDocNodeParameters } from '@microsoft/tsdoc';\nimport { DocNodeContainer } from '@microsoft/tsdoc';\nimport { CustomDocNodeKind } from './CustomDocNodeKind';\n\n/**\n * Constructor parameters for {@link DocUnorderedList}.\n */\nexport interface IDocUnorderedListParameters extends IDocNodeParameters {}\n\n/**\n * Represents an unordered list of spans.\n */\nexport class DocUnorderedList extends DocNodeContainer {\n  public constructor(parameters: IDocUnorderedListParameters, children?: DocNode[]) {\n    super(parameters, children);\n  }\n\n  /** @override */\n  public get kind(): string {\n    return CustomDocNodeKind.UnorderedList;\n  }\n}\n"
  },
  {
    "path": "packages/site/src/utils/IndentedWriter.ts",
    "content": "import type { IStringBuilder } from '@rushstack/node-core-library';\nimport { StringBuilder } from '@rushstack/node-core-library';\n\n/**\n * A utility for writing indented text.\n * @remarks\n *\n * Note that the indentation is inserted at the last possible opportunity.\n * For example, this code...\n *\n * ```ts\n *   writer.write('begin\\n');\n *   writer.increaseIndent();\n *   writer.write('one\\ntwo\\n');\n *   writer.decreaseIndent();\n *   writer.increaseIndent();\n *   writer.decreaseIndent();\n *   writer.write('end');\n * ```\n *\n * ...would produce this output:\n *\n * ```\n *   begin\n *     one\n *     two\n *   end\n * ```\n */\nexport class IndentedWriter {\n  /**\n   * The text characters used to create one level of indentation.\n   * Two spaces by default.\n   */\n  public defaultIndentPrefix: string = '  ';\n\n  private readonly _builder: IStringBuilder;\n\n  private _latestChunk: string | undefined;\n  private _previousChunk: string | undefined;\n  private _atStartOfLine: boolean;\n\n  private readonly _indentStack: string[];\n  private _indentText: string;\n\n  private _beforeStack: string[];\n  private _isWritingBeforeStack: boolean;\n\n  public constructor(builder?: IStringBuilder) {\n    this._builder = builder === undefined ? new StringBuilder() : builder;\n\n    this._latestChunk = undefined;\n    this._previousChunk = undefined;\n    this._atStartOfLine = true;\n\n    this._indentStack = [];\n    this._indentText = '';\n\n    this._beforeStack = [];\n    this._isWritingBeforeStack = false;\n  }\n\n  /**\n   * Retrieves the output that was built so far.\n   * @returns The output string.\n   */\n  public getText(): string {\n    return this._builder.toString();\n  }\n\n  public toString(): string {\n    return this.getText();\n  }\n\n  /**\n   * Increases the indentation.  Normally the indentation is two spaces,\n   * however an arbitrary prefix can optional be specified.  (For example,\n   * the prefix could be \"// \" to indent and comment simultaneously.)\n   * Each call to IndentedWriter.increaseIndent() must be followed by a\n   * corresponding call to IndentedWriter.decreaseIndent().\n   * @param indentPrefix - The string to prepend to each line of indented text.\n   */\n  public increaseIndent(indentPrefix?: string): void {\n    this._indentStack.push(indentPrefix !== undefined ? indentPrefix : this.defaultIndentPrefix);\n    this._updateIndentText();\n  }\n\n  /**\n   * Decreases the indentation, reverting the effect of the corresponding call\n   * to IndentedWriter.increaseIndent().\n   */\n  public decreaseIndent(): void {\n    this._indentStack.pop();\n    this._updateIndentText();\n  }\n\n  /**\n   * A shorthand for ensuring that increaseIndent()/decreaseIndent() occur\n   * in pairs.\n   * @param scope - A callback function that will be invoked with the indentation increased.\n   * @param indentPrefix - The string to prepend to each line of indented text.\n   *  If not provided, the default indent prefix will be used.\n   */\n  public indentScope(scope: () => void, indentPrefix?: string): void {\n    this.increaseIndent(indentPrefix);\n    scope();\n    this.decreaseIndent();\n  }\n\n  /**\n   * Adds a newline if the file pointer is not already at the start of the line (or start of the stream).\n   */\n  public ensureNewLine(): void {\n    const lastCharacter: string = this.peekLastCharacter();\n    if (lastCharacter !== '\\n' && lastCharacter !== '') {\n      this._writeNewLine();\n    }\n  }\n\n  /**\n   * Adds up to two newlines to ensure that there is a blank line above the current line.\n   */\n  public ensureSkippedLine(): void {\n    if (this.peekLastCharacter() !== '\\n') {\n      this._writeNewLine();\n    }\n\n    const secondLastCharacter: string = this.peekSecondLastCharacter();\n    if (secondLastCharacter !== '\\n' && secondLastCharacter !== '') {\n      this._writeNewLine();\n    }\n  }\n\n  /**\n   * Returns the last character that was written, or an empty string if no characters have been written yet.\n   * @returns The last character that was written.\n   */\n  public peekLastCharacter(): string {\n    if (this._latestChunk !== undefined) {\n      return this._latestChunk.substr(-1, 1);\n    }\n    return '';\n  }\n\n  /**\n   * Returns the second to last character that was written, or an empty string if less than one characters\n   * have been written yet.\n   * @returns The second to last character that was written.\n   */\n  public peekSecondLastCharacter(): string {\n    if (this._latestChunk !== undefined) {\n      if (this._latestChunk.length > 1) {\n        return this._latestChunk.substr(-2, 1);\n      }\n      if (this._previousChunk !== undefined) {\n        return this._previousChunk.substr(-1, 1);\n      }\n    }\n    return '';\n  }\n\n  /**\n   * Writes `before` and `after` messages if and only if `mayWrite` writes anything.\n   *\n   * If `mayWrite` writes \"CONTENT\", this method will write \"<before>CONTENT<after>\".\n   * If `mayWrite` writes nothing, this method will write nothing.\n   * @param before - The message to write before the content.\n   * @param after - The message to write after the content.\n   * @param mayWrite - The callback that may write content.\n   */\n  public writeTentative(before: string, after: string, mayWrite: () => void): void {\n    this._beforeStack.push(before);\n\n    // If this function writes anything, then _all_ messages in the \"before stack\" will also be\n    // written. This means that the stack will be empty (as when we write a message from the stack,\n    // we remove it from the stack).\n    mayWrite();\n\n    // If the stack is not empty, it means that `mayWrite` didn't write anything. Pop the last-\n    // added message from the stack, we'll never write it. Otherwise, if the stack is empty, then\n    // write the \"after\" message.\n    if (this._beforeStack.length > 0) {\n      this._beforeStack.pop();\n    } else {\n      this.write(after);\n    }\n  }\n\n  /**\n   * Writes some text to the internal string buffer, applying indentation according\n   * to the current indentation level.  If the string contains multiple newlines,\n   * each line will be indented separately.\n   * @param message - The text to write.\n   */\n  public write(message: string): void {\n    if (message.length === 0) {\n      return;\n    }\n\n    if (!this._isWritingBeforeStack) {\n      this._writeBeforeStack();\n    }\n\n    // If there are no newline characters, then append the string verbatim\n    if (!/[\\r\\n]/.test(message)) {\n      this._writeLinePart(message);\n      return;\n    }\n\n    // Otherwise split the lines and write each one individually\n    let first: boolean = true;\n    for (const linePart of message.split('\\n')) {\n      if (!first) {\n        this._writeNewLine();\n      } else {\n        first = false;\n      }\n      if (linePart) {\n        this._writeLinePart(linePart.replace(/[\\r]/g, ''));\n      }\n    }\n  }\n\n  /**\n   * A shorthand for writing an optional message, followed by a newline.\n   * Indentation is applied following the semantics of IndentedWriter.write().\n   * @param message - The text to write.\n   */\n  public writeLine(message: string = ''): void {\n    if (message.length > 0) {\n      this.write(message);\n    } else if (!this._isWritingBeforeStack) {\n      this._writeBeforeStack();\n    }\n\n    this._writeNewLine();\n  }\n\n  /**\n   * Writes a string that does not contain any newline characters.\n   * @param message - The text to write.\n   */\n  private _writeLinePart(message: string): void {\n    if (message.length > 0) {\n      if (this._atStartOfLine && this._indentText.length > 0) {\n        this._write(this._indentText);\n      }\n      this._write(message);\n      this._atStartOfLine = false;\n    }\n  }\n\n  private _writeNewLine(): void {\n    if (this._atStartOfLine && this._indentText.length > 0) {\n      this._write(this._indentText);\n    }\n\n    this._write('\\n');\n    this._atStartOfLine = true;\n  }\n\n  private _write(s: string): void {\n    this._previousChunk = this._latestChunk;\n    this._latestChunk = s;\n    this._builder.append(s);\n  }\n\n  /**\n   * Writes all messages in our before stack, processing them in FIFO order. This stack is\n   * populated by the `writeTentative` method.\n   */\n  private _writeBeforeStack(): void {\n    this._isWritingBeforeStack = true;\n\n    for (const message of this._beforeStack) {\n      this.write(message);\n    }\n\n    this._isWritingBeforeStack = false;\n    this._beforeStack = [];\n  }\n\n  private _updateIndentText(): void {\n    this._indentText = this._indentStack.join('');\n  }\n}\n"
  },
  {
    "path": "packages/site/src/utils/Utilities.ts",
    "content": "import type { ApiItem } from '@microsoft/api-extractor-model';\nimport { ApiParameterListMixin } from '@microsoft/api-extractor-model';\nexport class Utilities {\n  private static readonly _badFilenameCharsRegExp: RegExp = /[^a-z0-9_\\-.]/gi;\n\n  /**\n   * Generates a concise signature for a function.  Example: \"getArea(width, height)\"\n   * @param apiItem - The ApiItem for which to generate a signature.\n   * @param scoped - If true, the signature will include the parent name.  Example: \"Rectangle.getArea(width, height)\"\n   * @returns The concise signature.\n   */\n  public static getConciseSignature(apiItem: ApiItem, scoped = false): string {\n    let prefixString = '';\n    if (scoped) {\n      prefixString = apiItem.getScopedNameWithinPackage().split('.').slice(0, -1).join('.') + '.';\n      // [MARK]: 特殊处理，History_2 -> History\n      if (prefixString.endsWith('_2.')) {\n        prefixString = prefixString.slice(0, -3) + '.';\n      }\n    }\n    if (ApiParameterListMixin.isBaseClassOf(apiItem)) {\n      return prefixString + apiItem.displayName + '(' + apiItem.parameters.map((x) => x.name).join(', ') + ')';\n    }\n    return prefixString + apiItem.displayName;\n  }\n\n  /**\n   * Converts bad filename characters to underscores.\n   * @param name - The name to convert.\n   */\n  public static getSafeFilenameForName(name: string): string {\n    // TODO: This can introduce naming collisions.\n    // We will fix that as part of https://github.com/microsoft/rushstack/issues/1308\n    return name.replace(Utilities._badFilenameCharsRegExp, '_').toLowerCase();\n  }\n}\n"
  },
  {
    "path": "packages/site/src/utils/excerpt-token.ts",
    "content": "import { ApiInterface, ApiModel, ExcerptTokenKind, type ExcerptToken } from '@microsoft/api-extractor-model';\nimport { camelCase } from 'lodash';\nimport { interfacesToSkipParsing } from '../MarkdownDocumenter';\n\nexport type BaseExcerptToken = {\n  text: string;\n  canonicalReference?: ExcerptToken['canonicalReference'];\n  interface?: ApiInterface;\n  children?: ICustomExcerptToken[];\n  parent?: ICustomExcerptToken;\n};\n\nexport type ICustomExcerptToken =\n  | ({\n      type: 'Prefix';\n      prefix: string;\n    } & BaseExcerptToken)\n  | ({\n      type: 'Omit' | 'Pick';\n      fields: string[];\n    } & BaseExcerptToken)\n  | ({ type: 'Default' } & BaseExcerptToken);\n\n/**\n * 将 ExcerptToken 按 'Prefix'、'Omit'、'Pick'、'Default' 分类\n * @param apiModel\n * @param apiItem\n * @param parentToken\n * @param flatten\n * @returns\n */\nexport function parseExcerptTokens(\n  apiModel: ApiModel,\n  apiItem: ApiInterface,\n  parentToken?: ICustomExcerptToken,\n  flatten?: boolean,\n): ICustomExcerptToken[] {\n  const customExcerptTokens: ICustomExcerptToken[] = [];\n  let flag = false;\n\n  for (const [index, token] of apiItem.excerptTokens.entries()) {\n    let customToken: ICustomExcerptToken | undefined = undefined;\n    if (token.kind === ExcerptTokenKind.Reference) {\n      const excludeTags = ['Record', 'Partial'];\n      if (excludeTags.includes(token.text)) continue;\n      if (token.text === 'Prefix') {\n        const content = apiItem.excerptTokens[index + 1].text;\n        const prefixMatch = content.match(/<\\s*'(\\w+)'/)!;\n        const referenceToken = apiItem.excerptTokens[index + 2];\n        customToken = {\n          type: token.text,\n          prefix: prefixMatch[1],\n          fields: undefined,\n          text: referenceToken.text,\n          canonicalReference: referenceToken.canonicalReference,\n        } as ICustomExcerptToken;\n        flag = true;\n      } else if (['Omit', 'Pick'].includes(token.text)) {\n        const content = apiItem.excerptTokens[index + 3].text;\n        const referenceToken = apiItem.excerptTokens[index + 2];\n        const fields = (content.match(/'(\\w+)'/g) || []).map((x) => x.replace(/'/g, ''));\n        customToken = {\n          type: token.text,\n          fields: fields,\n          text: referenceToken.text,\n          canonicalReference: referenceToken.canonicalReference,\n        } as ICustomExcerptToken;\n        flag = true;\n      } else {\n        if (!flag) {\n          customToken = { type: 'Default', text: token.text, canonicalReference: token.canonicalReference };\n        }\n        flag = false;\n      }\n    }\n\n    if (customToken && customToken.canonicalReference) {\n      const resolvedApiItem = apiModel.resolveDeclarationReference(\n        customToken.canonicalReference,\n        undefined,\n      ).resolvedApiItem;\n      if (resolvedApiItem instanceof ApiInterface) {\n        customToken.interface = resolvedApiItem;\n        customToken.children = parseExcerptTokens(apiModel, resolvedApiItem, customToken);\n      }\n      customToken.parent = parentToken;\n      customExcerptTokens.push(customToken);\n    }\n  }\n\n  return customExcerptTokens;\n}\n\nexport const liftPrefixExcerptTokens = (items: ICustomExcerptToken[]): ICustomExcerptToken[] => {\n  let topLevelPrefixes: ICustomExcerptToken[] = [];\n\n  for (const item of items) {\n    if (interfacesToSkipParsing.includes(item.text)) continue;\n\n    if (item.type === 'Prefix') {\n      const newItem = { ...item, prefix: getAccessorExcerptTokensPrefixes(item) };\n      topLevelPrefixes.push(newItem);\n    }\n\n    if (item.children && item.children.length > 0) {\n      const childPrefixes = liftPrefixExcerptTokens(item.children);\n      topLevelPrefixes = topLevelPrefixes.concat(childPrefixes);\n    }\n  }\n\n  return topLevelPrefixes;\n};\n\n/**\n *\n * @param targetToken\n * @param includeSelf\n */\nexport function findAccessorExcerptTokens(targetToken: ICustomExcerptToken, includeSelf = true): ICustomExcerptToken[] {\n  let path: ICustomExcerptToken[] = [targetToken];\n  let currentToken = targetToken.parent;\n\n  while (currentToken) {\n    path = [currentToken, ...path];\n    currentToken = currentToken.parent;\n  }\n\n  return includeSelf ? path : path.slice(1);\n}\n\n/**\n *\n * @param targetToken\n */\nexport function getAccessorExcerptTokensPrefixes(targetToken: ICustomExcerptToken) {\n  const tokens = findAccessorExcerptTokens(targetToken);\n  const tokenString = tokens\n    .map((token) => (token as any).prefix || '')\n    .filter(Boolean)\n    .join(' ');\n  return camelCase(tokenString);\n}\n"
  },
  {
    "path": "packages/site/src/utils/gitignore.ts",
    "content": "import { FileSystem } from '@rushstack/node-core-library';\nimport * as path from 'path';\n\n/**\n * Get the path to the .gitignore file\n * @param outputFolder - The output folder\n * @returns The path to the .gitignore file\n */\nfunction getGitignorePath(outputFolder: string): string {\n  return path.join(outputFolder, '.gitignore');\n}\n\n/**\n * Initialize the .gitignore file\n * @param outputFolder - The output folder\n */\nexport function initGitignore(outputFolder: string) {\n  const gitPath = getGitignorePath(outputFolder);\n  FileSystem.writeFile(gitPath, '');\n}\n\n/**\n * Sync the filename to the .gitignore file\n * @param outputFolder - The output folder\n * @param filename - The filename to sync\n */\nexport function syncToGitignore(outputFolder: string, filename: string) {\n  const gitPath = getGitignorePath(outputFolder);\n  FileSystem.appendToFile(gitPath, `\\n${filename.replace(outputFolder, '')}`);\n}\n"
  },
  {
    "path": "packages/site/src/utils/parser.ts",
    "content": "import type { DocBlock, DocComment, DocSection } from '@microsoft/tsdoc';\n\n/**\n *\n * @param tagName\n * @param docComment\n */\nexport function getBlockTagByName(tagName: string, docComment: DocComment): DocSection | undefined {\n  const tag = docComment.customBlocks.find((customBlock: DocBlock) => customBlock.blockTag.tagName === tagName);\n  return tag && tag.content;\n}\n"
  },
  {
    "path": "packages/site/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"jsx\": \"react\",\n    \"lib\": [\"dom\"],\n    \"paths\": {\n      \"@antv/g6\": [\"../g6/lib/index\"]\n    },\n    \"module\": \"CommonJS\"\n  },\n  \"include\": [\".dumirc.ts\"]\n}\n"
  },
  {
    "path": "playwright.config.ts",
    "content": "import { defineConfig, devices } from '@playwright/test';\n\n/**\n * Read environment variables from file.\n * https://github.com/motdotla/dotenv\n */\n// import dotenv from 'dotenv';\n// dotenv.config({ path: path.resolve(__dirname, '.env') });\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\nexport default defineConfig({\n  testDir: './tests',\n  /* Run tests in files in parallel */\n  fullyParallel: true,\n  /* Fail the build on CI if you accidentally left test.only in the source code. */\n  forbidOnly: !!process.env.CI,\n  /* Retry on CI only */\n  retries: process.env.CI ? 5 : 0,\n  /* Opt out of parallel tests on CI. */\n  workers: process.env.CI ? 1 : undefined,\n  /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n  reporter: 'html',\n  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n  use: {\n    /* Base URL to use in actions like `await page.goto('/')`. */\n    baseURL: 'http://localhost:8080',\n\n    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n    trace: 'on-first-retry',\n  },\n\n  /* Configure projects for major browsers */\n  projects: [\n    {\n      name: 'chromium',\n      use: { ...devices['Desktop Chrome'] },\n    },\n\n    // {\n    //   name: 'firefox',\n    //   use: { ...devices['Desktop Firefox'] },\n    // },\n\n    // {\n    //   name: 'webkit',\n    //   use: { ...devices['Desktop Safari'] },\n    // },\n\n    /* Test against mobile viewports. */\n    // {\n    //   name: 'Mobile Chrome',\n    //   use: { ...devices['Pixel 5'] },\n    // },\n    // {\n    //   name: 'Mobile Safari',\n    //   use: { ...devices['iPhone 12'] },\n    // },\n\n    /* Test against branded browsers. */\n    // {\n    //   name: 'Microsoft Edge',\n    //   use: { ...devices['Desktop Edge'], channel: 'msedge' },\n    // },\n    // {\n    //   name: 'Google Chrome',\n    //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },\n    // },\n  ],\n\n  /* Run your local dev server before starting the tests */\n  webServer: {\n    command: 'npm run dev:g6',\n    url: 'http://localhost:8080',\n    reuseExistingServer: !process.env.CI,\n  },\n});\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - 'packages/*'"
  },
  {
    "path": "scripts/demo-to-test/core/global.js",
    "content": "// global object\nlet GLOBALSTATE = {\n  staticCode: [],\n};\n\nconst RESETGLOBAL = () => {\n  GLOBALSTATE = { staticCode: [] };\n};\n\nconst SETGLOBAL = ([start, end]) => {\n  GLOBALSTATE['staticCode'].push([start, end]);\n};\n\nconst INGLOBALRANGE = ([start, end]) => {\n  const { staticCode } = GLOBALSTATE;\n  let flag = false;\n  for (let i = 0; i < staticCode.length; i++) {\n    const [s, e] = staticCode[i];\n    if ((start >= s && start <= e) || (end >= s && end <= e)) {\n      flag = true;\n    }\n  }\n  return flag;\n};\n\nmodule.exports = { RESETGLOBAL, INGLOBALRANGE, SETGLOBAL };\n"
  },
  {
    "path": "scripts/demo-to-test/core/index.js",
    "content": "const { parser, transformSign } = require('./parser');\n\nmodule.exports = { parser, transformSign };\n"
  },
  {
    "path": "scripts/demo-to-test/core/parser.js",
    "content": "const fs = require('fs');\nconst babel = require('@babel/core');\nconst chalk = require('chalk');\nconst { get } = require('lodash');\nconst { isNewExpression, getObjectValue, transformSign } = require('./utils');\n\nconst meta = {\n  config: {}, // 图表配置项\n  code: '', // 原始代码\n};\n\n/**\n * @param {string} params\n * @param {string} type\n * @description 解析代码，提取 config\n */\nconst parser = (params, type) => {\n  try {\n    meta.code = fs.readFileSync(params, 'utf-8');\n    const visitorExpressions = {\n      // new Chart\n      NewExpression(path) {\n        const { node } = path;\n        if (isNewExpression(node)) {\n          meta.config = getObjectValue(get(node, 'arguments.0'), meta.code);\n        }\n      },\n    };\n    const vistorPlugins = {\n      visitor: visitorExpressions,\n    };\n    babel.transform(fs.readFileSync(params, 'utf-8'), {\n      plugins: [vistorPlugins, '@babel/plugin-transform-typescript'],\n    });\n\n    console.log(chalk.green(`解析成功：${params}`));\n\n    return {\n      success: true,\n      config: meta.config,\n    };\n  } catch (err) {\n    console.log(chalk.red(`解析出错：params: ${params}; type: ${type}`));\n    console.log(chalk.red(`出错信息：${err}`));\n    return {\n      success: false,\n      errMessage: err,\n      errPath: params,\n    };\n  }\n};\n\nmodule.exports = { parser, transformSign };\n"
  },
  {
    "path": "scripts/demo-to-test/core/utils.js",
    "content": "const { get } = require('lodash');\nconst { SETGLOBAL } = require('./global');\n\nconst SIGN = '-FN-';\nconst startRegex = new RegExp(`\"${SIGN}`, 'g');\nconst endRegex = new RegExp(`${SIGN}\"`, 'g');\n\n/**\n * new表达式\n * @param {*} node\n */\nconst isNewExpression = (node) => {\n  return get(node, 'callee.name') === 'Graph';\n};\n\n/**\n * Is function\n * @param {string} nodeType\n */\nconst isFunction = (nodeType) => {\n  return /FunctionExpression|ArrowFunctionExpression/.test(nodeType);\n};\n\n/**\n * Use single\n * @param {string} str\n */\nconst useSign = (str) => {\n  return replaceEnter(`${SIGN}${str}${SIGN}`);\n};\n\n/**\n * Replace enter\n * @param {*} str\n */\nconst replaceEnter = (str) => {\n  return str.replace(/\\n/g, ' ');\n};\n\n/**\n * transform sign\n * @param {string} str\n * @description 解析 meta 时，去掉 SIGN\n */\nconst transformSign = (str) => {\n  return str.replace(startRegex, '').replace(endRegex, '');\n};\n\n/**\n * Get loop object\n * @param {*} node\n * @param {string} code  code\n */\nconst getObjectValue = (node, code) => {\n  const { properties } = node;\n  const obj = {};\n  properties.forEach((item) => {\n    const { key, value } = item;\n    if (!['container', 'width', 'height', 'data'].includes(key.name)) {\n      if (isFunction(value.type)) {\n        const { start, end } = value;\n        SETGLOBAL([start, end]);\n        obj[key.name] = useSign(code.slice(start, end));\n      } else if (value.type === 'ArrayExpression') {\n        const { start, end } = value;\n        SETGLOBAL([start, end]);\n        obj[key.name] = useSign(code.slice(start, end));\n      } else if (value.type === 'ObjectExpression') {\n        // recursive\n        obj[key.name] = getObjectValue(value, code);\n      } else {\n        obj[key.name] = getValue(value);\n      }\n    }\n  });\n  return obj;\n};\n\n/**\n * Get node value\n * @param {*} arg\n * @param {string} code\n */\nconst getValue = (arg, code = '') => {\n  const { start, end } = arg;\n\n  // .encode('y', 'frequency')\n  if (arg.type === 'StringLiteral') {\n    return arg.value;\n  }\n  // .encode('y', EPSILON)\n  if (arg.type === 'Identifier') {\n    return useSign(arg.name);\n  }\n  // .scale('y', { nice: true)\n  if (arg.type === 'ObjectExpression') {\n    return getObjectValue(arg, code);\n  }\n  // .encode('y', (d) => (d.sex === 1 ? -d.people : d.people))\n  if (isFunction(arg.type)) {\n    SETGLOBAL([start, end]);\n    return useSign(code.slice(start, end));\n  }\n  /**\n   * .data({\n   *  transform: [\n   *    {\n   *      type: 'filter',\n   *      callback: (d) => d.year === 2000,\n   *    },\n   *  ],\n   * })\n   */\n  if (arg.type === 'ArrayExpression') {\n    SETGLOBAL([start, end]);\n    return useSign(code.slice(start, end));\n  }\n  return arg.value;\n};\n\nmodule.exports = {\n  isNewExpression,\n  isFunction,\n  useSign,\n  replaceEnter,\n  getObjectValue,\n  transformSign,\n};\n"
  },
  {
    "path": "scripts/demo-to-test/index.js",
    "content": "/**\n * 为指定路径下的示例添加测试用例\n * @param {string} path demo 路径\n * @param {string} prifix 前缀\n * @param {string} data 数据文件的\n * @example\n * - node scripts/demo-to-test/index net/radialLayout layout radial.json\n * - explain: 为 site/exmples/net/radialLayout 路径下的 demo 示例添加测试文件；layout 为文件名、Fn前缀；radial.json 为数据文件\n */\nconst fs = require('fs');\nconst path = require('path');\nconst prettier = require('prettier');\nconst { uniq, camelCase, kebabCase } = require('lodash');\nconst { parser, transformSign } = require('./core');\nconst specTemplate = require('./template/spec');\nconst testTemplate = require('./template/test');\nconst itTemplate = require('./template/it');\n\nconst args = process.argv.slice(2);\nconst demoPath = args[0];\nconst name = kebabCase(demoPath.split('/').pop());\nconst prefix = args[1];\nconst dataFile = args[2];\n\nconst prePath = `../../packages/`;\nconst demoDir = path.resolve(__dirname, prePath, `site/examples/${demoPath}/demo`);\nconst testDir = path.resolve(__dirname, prePath, `g6/__tests__/demo/case`);\nconst unitDir = path.resolve(__dirname, prePath, `g6/__tests__/unit/${prefix}s`);\nconst exportDir = path.resolve(__dirname, prePath, `g6/__tests__/demo/case/index.ts`);\n\nconst unitMeta = [];\n\n/**\n * 移除 object string 左右 {}，直接插入到已有 object 中\n * @param text\n */\nconst removeFirstAndLastTwoChars = (text) => {\n  if (text.length < 4) {\n    return '';\n  }\n  return text.substring(2, text.length - 2);\n};\n\nconst formatCode = async (code) => {\n  const formattedContent = await prettier.format(code, {\n    semi: true,\n    singleQuote: true,\n    printWidth: 120,\n    parser: 'typescript',\n  });\n  return formattedContent;\n};\n\nconst initUnit = async () => {\n  const unitFiles = fs.readdirSync(unitDir);\n  let content = '';\n  let imported = '';\n  unitMeta.forEach(async (kebabName) => {\n    const camelName = camelCase(kebabName);\n    const sampleKebabName = kebabName.replace(`${prefix}-${name}-`, '');\n    const config = { camelName, sampleKebabName, sampleKebabNameWithSpace: sampleKebabName.replace(/-/g, ' ') };\n    let exist = !!imported;\n    content += exist ? '\\n\\n' + itTemplate(config) : itTemplate(config);\n    imported += exist ? ',\\n' + camelName : camelName;\n    fs.appendFileSync(exportDir, `export * from './${kebabName}';\\n`, 'utf-8');\n  });\n  const filename = `./${name}.spec.ts`;\n  if (!unitFiles.includes(filename)) {\n    const formattedContent = await formatCode(\n      specTemplate({\n        content,\n        imported,\n        describe: `${uniq(name.split('-')).join(' ')}`,\n      }),\n    );\n    fs.writeFileSync(path.resolve(unitDir, filename), formattedContent, 'utf-8');\n  }\n};\n\nconst initTest = async (dir) => {\n  const files = await fs.readdirSync(dir);\n  const testFiles = fs.readdirSync(testDir);\n  files.forEach(async (file, index) => {\n    const filePath = path.resolve(dir, file);\n    const stats = fs.statSync(filePath);\n    const kebabName = uniq(`${prefix}-${name}-${kebabCase(file.replace('.ts', ''))}`.split('-')).join('-');\n    const camelName = camelCase(kebabName);\n    if (stats.isFile() && file.endsWith('.ts')) {\n      unitMeta.push(kebabName);\n      const { config, success } = parser(filePath);\n      if (success) {\n        const formattedContent = await formatCode(\n          testTemplate({\n            config: removeFirstAndLastTwoChars(transformSign(JSON.stringify(config, null, 2))),\n            filename: camelName,\n            dataFile,\n          }),\n        );\n        if (!testFiles.includes(`${kebabName}.ts`)) {\n          fs.writeFileSync(path.resolve(testDir, `./${kebabName}.ts`), formattedContent, 'utf-8');\n        }\n      }\n    }\n    if (index === files.length - 1) {\n      initUnit();\n    }\n  });\n};\n\ninitTest(demoDir);\n"
  },
  {
    "path": "scripts/demo-to-test/template/it.js",
    "content": "const itTemplate = ({ camelName, sampleKebabName, sampleKebabNameWithSpace }) => {\n  return `it('${sampleKebabNameWithSpace}', async () => {\n    const graph = await createDemoGraph(${camelName});\n    await expect(graph).toMatchSnapshot(__filename, '${sampleKebabName}');\n    graph.destroy();\n  });`;\n};\n\nmodule.exports = itTemplate;\n"
  },
  {
    "path": "scripts/demo-to-test/template/spec.js",
    "content": "const specTemplate = ({ imported, content, describe }) => {\n  return `import {\n    ${imported}\n  } from '@@/demo/case';\n  import { createDemoGraph } from '@@/utils';\n\n  describe('${describe}', () => {\n    ${content}\n  })`;\n};\n\nmodule.exports = specTemplate;\n"
  },
  {
    "path": "scripts/demo-to-test/template/test.js",
    "content": "const testTemplate = ({ config, filename, dataFile }) => {\n  return `import { Graph } from '@/src';\n  import data from '@@/dataset/${dataFile}';\n  import type { STDTestCase } from '../types';\n\n  export const ${filename}: STDTestCase = async (context) => {\n    const graph = new Graph({\n      ...context,\n      data,\n      ${config}\n    });\n\n    await graph.render();\n\n    return graph;\n  };\n  `;\n};\n\nmodule.exports = testTemplate;\n"
  },
  {
    "path": "scripts/version.sh",
    "content": "#!/bin/bash\n\npnpm changeset \npnpm changeset version\n\ncd ./packages/g6 && npm run version"
  },
  {
    "path": "tests/g6/elements/node-element.spec.ts",
    "content": "import { expect, test } from '@playwright/test';\n\ntest.describe('element node html', () => {\n  test('html', async ({ page }) => {\n    await page.goto('/?Demo=elementNodeHTML&Renderer=canvas&GridLine=true&Theme=light&Animation=false');\n\n    await page.waitForSelector('.key');\n\n    const clip = { x: 100, y: 100, width: 240, height: 180 };\n\n    await expect(page).toHaveScreenshot({ clip });\n  });\n});\n"
  },
  {
    "path": "tests/g6/plugins/plugins-minimap.spec.ts",
    "content": "import { expect, test } from '@playwright/test';\n\ntest.describe('plugin minimap', () => {\n  test('default', async ({ page }) => {\n    page.goto('/?Demo=pluginMinimap&Renderer=canvas&GridLine=true&Theme=light&Animation=false');\n\n    await page.waitForSelector('.g6-minimap');\n\n    const clip = { x: 0, y: 0, width: 500, height: 500 };\n\n    await expect(page).toHaveScreenshot({ clip });\n\n    // wheel on to zoom\n    await page.mouse.move(250, 250);\n\n    await page.mouse.wheel(0, 10);\n\n    await expect(page).toHaveScreenshot({ clip, maxDiffPixels: 100 });\n\n    await page.mouse.wheel(0, -20);\n\n    await expect(page).toHaveScreenshot({ clip, maxDiffPixels: 100 });\n\n    // drag minimap mask\n    await page.mouse.move(375, 425);\n    await page.mouse.down();\n    await page.mouse.move(400, 450, { steps: 10 });\n    await page.mouse.up();\n    await expect(page).toHaveScreenshot({ clip, maxDiffPixels: 100 });\n\n    // drag mask overflow\n    await page.mouse.move(375, 400);\n    await page.mouse.down();\n    await page.mouse.move(550, 550, { steps: 50 });\n    await page.mouse.up();\n    await expect(page).toHaveScreenshot({ clip, maxDiffPixels: 100 });\n\n    // drag canvas\n    // playwright mouse 模拟操作无法正常触发 g canvas 的 drag 事件\n    // 因此这里直接调用 graph 实例的方法\n    // playwright mouse simulation operation cannot trigger the drag event of g canvas normally\n    // So here directly call the method of the graph instance\n    await page.evaluate(() => (window as any).graph.translateTo([100, 100], false));\n\n    await expect(page).toHaveScreenshot({ clip, maxDiffPixels: 100 });\n  });\n});\n"
  },
  {
    "path": "tests/g6/plugins/plugins-tooltip.spec.ts",
    "content": "import { expect, test } from '@playwright/test';\n\ntest.describe('plugin tooltip', () => {\n  test('bug: tooltip should has correct position after graph resize', async ({ page }) => {\n    page.goto('/?Demo=bugTooltipResize&Renderer=canvas&GridLine=true&Theme=light&Animation=false');\n\n    await page.waitForSelector('.tooltip');\n\n    const clip = { x: 0, y: 0, width: 500, height: 500 };\n\n    await page.mouse.move(375, 250);\n\n    await expect(page).toHaveScreenshot({ clip });\n\n    await page.mouse.move(250, 250);\n\n    // wait for div content is 'resize'\n    const resize = page.getByRole('button', { name: 'resize' });\n\n    await resize?.click();\n\n    await page.mouse.move(285, 250);\n\n    await expect(page).toHaveScreenshot({ clip });\n  });\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowSyntheticDefaultImports\": true,\n    \"declaration\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"isolatedModules\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"Node\",\n    \"pretty\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"target\": \"ES6\",\n    \"types\": [\"node\", \"@types/jest\"]\n  },\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "turbo.json",
    "content": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"pipeline\": {\n    \"build\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\"dist/**\", \"esm/**\", \"lib/**\"]\n    },\n    \"ci\": {\n      \"dependsOn\": [\"^ci\"]\n    },\n    \"version\": {}\n  }\n}\n"
  }
]