Compare commits

..

21 Commits

Author SHA1 Message Date
36c3cb97a9 chore: release v8.0.0-beta.1 2025-08-10 23:37:53 +08:00
55c68b48b7 chore: release and publish from github actions (#850) 2025-08-10 23:31:34 +08:00
c232e71c47 chore: add echarts 6 features in codegen
* chore: add echarts 6 features in codegen

* update according to 772cf01859
2025-08-10 23:31:34 +08:00
e568005bb2 docs: update version, deps and docs for 8.0 beta (#849)
* chore: up version and deps

* chore: use pnpm CLI to get versions

* add note for echarts 6 upgrade guide

* remove docs script

let cdn to redirect for us

* Revert "remove docs script"

This reverts commit 3bc237db9100864f2813249ac1693735a658e646.

* update demo links
2025-08-10 23:31:23 +08:00
8ed975e09b feat!: inject style via constructable CSSStyleSheet and remove CSP entry (#847)
* chore: not inject inline css on server

* feat!: remove csp entry

* keep csp title in readme

* chore: switch to rolldown and tsdown

* update

* dedupe

* update according to review

* emphasize "both" in csp section

* load css with unplugin-raw

* change tsdown entry
2025-08-10 23:26:17 +08:00
570a26c262 feat: rendering tooltips and dataView with slots (#838)
* feat: experimental component rendered tooltip

* revert slot in VChart

* feat: use tooltip composable

* feat: try createApp

* feat: use pie chart as tooltip

* feat: switch to createVNode

The limitation is that the tooltip detached from the current component tree, not provide/inject

will try teleport next

* feat: try component with teleport

* wip

* add xAxis example

* refactor with shallowReactive

* Support dynamic slot

* fix: fill empty elements with object in array

* shallow copy option along the path

* ssr friendly

* vibe docs

* typo

* update according to the review

* add dataView slot

* chore: fix warnings and errors in demo (#839)

* chore: suppress warning in demo

* chore: prevent multiple intializations of esbuild-wasm in demo HMR

* feat: dynamically update the theme (#841)

Co-authored-by: GU Yiling <justice360@gmail.com>

* feat: add dataView slot

* vibe docs

---------

Co-authored-by: GU Yiling <justice360@gmail.com>

* fix docs typo

* update according to the review

* small fix

* remove wrapper around slotProp

* update comments

* remove anys

* add tooltip slot prop type

* target to vue 3.3

* move slot related codes to slot.ts

---------

Co-authored-by: GU Yiling <justice360@gmail.com>
2025-08-10 23:26:17 +08:00
df640ebce6 chore: remove large mode for flight example (#845) 2025-08-10 23:26:17 +08:00
30e7934aab feat: dynamically update the theme (#841)
Co-authored-by: GU Yiling <justice360@gmail.com>
2025-08-10 23:26:12 +08:00
6155bbb409 chore: fix warnings and errors in demo (#839)
* chore: suppress warning in demo

* chore: prevent multiple intializations of esbuild-wasm in demo HMR
2025-08-10 23:22:04 +08:00
fa42af0723 refactor: use Web Components without native class support detection (#836) 2025-08-10 23:22:04 +08:00
8b7ef5e6e1 refactor: switch to generated .d.ts (#835)
* build: generate d.ts

* fix: preserve PublicMethods

* fix: avoid exposing types of attrs

* refactor: use existing setoption type

* fix: expose root and chart

* feat: use symbol as injection key

* chore: add comment for the type casting of the exposed
2025-08-10 23:22:04 +08:00
522dd7cc5c chore: ESLint Flat Config (#834)
* chore: eslint flat config

* chore: format

* update according to review

* chore: remove prettier config and format

* fix: move handler to script to bypass eslint

* chore: config eslint for lang=js block

* docs: add surrounding empty lines for code block

* chore: also minify css in csp build

* chore: publint
2025-08-10 23:22:04 +08:00
077bd3ec40 build: migrate demo from webpack to Vite (#832) 2025-08-10 23:22:04 +08:00
473fed37a2 chore: remove @vue/runtime-core from peerDependencies
@vue/runtime-core was added here for supporting typescript in vue < 2.7
2025-08-10 23:22:04 +08:00
7ae6892fe6 refactor: change listeners from object to Map 2025-08-10 23:22:03 +08:00
71c106ae29 refactor: rename realListeners to listeners 2025-08-10 23:22:03 +08:00
381489da2f docs: update provide/inject section 2025-08-10 23:22:03 +08:00
2fb0dc2233 refactor: simplify render function 2025-08-10 23:22:03 +08:00
b6c84aab7e feat: support getter in provide/inject 2025-08-10 23:22:03 +08:00
c6a1228c9d docs: remove vue 2 related content 2025-08-10 23:21:47 +08:00
9067505a3a feat!: remove vue 2 2025-08-10 23:19:04 +08:00
27 changed files with 873 additions and 2330 deletions

1
.github/CODEOWNERS vendored
View File

@ -1 +0,0 @@
* @ecomfe/vue-echarts

View File

@ -1,5 +1,5 @@
name: "🐞 Bug Report"
description: Create a bug report for Vue ECharts
description: Create a bug report for Vue-ECharts
body:
- type: markdown
attributes:
@ -9,14 +9,14 @@ body:
id: confirmation
attributes:
label: Confirmation
description: Before submitting this issue, please make sure that the problem only occurs in Vue ECharts and is not related to ECharts itself.
description: Before submitting this issue, please make sure that the problem only occurs in Vue-ECharts and is not related to ECharts itself.
options:
- label: I can confirm this problem is not reproducible with ECharts itself.
required: true
- type: dropdown
id: integration
attributes:
label: How are you introducing Vue ECharts into your project?
label: How are you introducing Vue-ECharts into your project?
options:
- ES Module imports
- "<script> tag"

View File

@ -1,5 +1,5 @@
name: "🐞 Bug 报告"
description: 给 Vue ECharts 报告 bug
description: 给 Vue-ECharts 报告 bug
body:
- type: markdown
attributes:
@ -9,14 +9,14 @@ body:
id: confirmation
attributes:
label: 请确认
description: 在提交此问题前,请确认问题仅在 Vue ECharts 中发生,而与 ECharts 本身无关。
description: 在提交此问题前,请确认问题仅在 Vue-ECharts 中发生,而与 ECharts 本身无关。
options:
- label: 我可以确认这个问题无法在 ECharts 项目本身中复现。
required: true
- type: dropdown
id: integration
attributes:
label: 您是如何将 Vue ECharts 引入项目的?
label: 您是如何将 Vue-ECharts 引入项目的?
options:
- 通过 ES 模块 import
- "<script> 标签"

View File

@ -1,5 +1,5 @@
name: "✨ Feature Request"
description: Create a feature request for Vue ECharts
description: Create a feature request for Vue-ECharts
body:
- type: markdown
attributes:

View File

@ -1,5 +1,5 @@
name: "✨ 新功能建议"
description: 给 Vue ECharts 提交新功能建议
description: 给 Vue-ECharts 提交新功能建议
body:
- type: markdown
attributes:

View File

@ -1,29 +1,26 @@
name: CI
permissions:
contents: read
on:
pull_request:
push:
branches:
- main
- "8.0" # remove this after 8.0 is merged into main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache: "pnpm"
- name: Install dependencies
run: pnpm install
@ -34,9 +31,6 @@ jobs:
- name: Typecheck
run: pnpm run typecheck && pnpm run dev:typecheck
- name: Test
run: pnpm run test
- name: Build
run: pnpm run build

View File

@ -16,13 +16,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
@ -41,9 +41,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Test
run: pnpm run test
- name: Build
run: pnpm run build

View File

@ -1,21 +0,0 @@
# Repository Guidelines
## Project Structure & Module Organization
Core source lives in `src/`, implemented in TypeScript with the Vue 3 Composition API. Key entry modules include `src/index.ts` for exports, `src/ECharts.ts` for the main component, and utilities under `src/composables/` and `src/utils.ts`. Demo site assets sit in `demo/` (Vite-powered) and should be kept in sync with new features. Bundled artifacts in `dist/` are generated by `pnpm build`; avoid editing them manually. Shared build helpers reside in `scripts/`.
## Build, Test, and Development Commands
Use `pnpm install` to set up dependencies. `pnpm dev` serves the demo playground at `http://localhost:5173` for interactive testing. Run `pnpm build` (tsdown) to produce distributable output under `dist/`, and `pnpm dev:build` or `pnpm dev:preview` when you need the Vite bundle for the demo. `pnpm typecheck` validates the librarys TypeScript contracts, `pnpm lint` applies ESLint with autofix, `pnpm format` runs Prettier, and `pnpm test` executes the Vitest suite. Use `pnpm publint` before releases to confirm the published surface.
## Coding Style & Naming Conventions
This project targets Vue 3 + TypeScript with ECMAScript modules. Follow the existing 2-space indentation, trailing commas where valid, and single quotes for strings unless interpolation is required. Components and exported composables use PascalCase (e.g., `VChart`), while local helpers remain camelCase. Honor `eslint.config.ts` and Prettier defaults; always run `pnpm lint && pnpm format` before sending patches. Keep public exports centralized in `src/index.ts` and add accompanying CSS changes to `src/style.css`.
## Testing Guidelines
There is no standalone unit-test runner yet; rely on TypeScript, linting, and manual QA in the demo. Before opening a PR, run `pnpm lint`, `pnpm typecheck`, and `pnpm build`. Exercise relevant demos in `demo/src/` and add or update examples that showcase new behaviors. For major fixes, include reproduction and verification steps in the PR description so reviewers can follow along.
## Commit & Pull Request Guidelines
Commit history follows Conventional Commits (`type(scope): summary`), e.g., `feat(runtime): add renderer option` or `chore(deps): update vue`. Use concise, imperative summaries and group related changes together. PRs should describe user-facing effects, list verification commands, and link issues with `Fixes #123` when applicable. Include screenshots or GIFs for visual updates to the demo, and note any doc changes (`README.md`, `demo/`) in the description. Ensure CI checks mirror local commands: `pnpm lint`, `pnpm typecheck`, and `pnpm build`.

View File

@ -1,7 +1,3 @@
## 8.0.0-beta.2
- Added [smart update](https://github.com/ecomfe/vue-echarts/blob/main/README.md#smart-update) strategy to make updates more effortless.
## 8.0.0-beta.1
### Breaking changes
@ -289,7 +285,7 @@
- Update peer dependency for `echarts` to `^5.0.2`.
- Update peer dependency for `vue` to `^2.6.11 || ^3.0.0`.
- Now `@vue/composition-api` is required to be installed to use Vue ECharts with Vue 2.
- Now `@vue/composition-api` is required to be installed to use Vue-ECharts with Vue 2.
- `options` is renamed to **`option`** to align with ECharts itself.
- Updating `option` will respect **`update-options`** configs instead of checking reference change.
- `watch-shallow` is removed. Use **`manual-update`** for performance critical scenarios.

View File

@ -1,8 +1,10 @@
<p align="center"><a href="https://vue-echarts.dev/"><img alt="Vue ECharts" src="https://raw.githubusercontent.com/ecomfe/vue-echarts/refs/heads/main/demo/public/favicon.svg" width="96"></a></p>
<h1 align="center">Vue ECharts</h1>
<h1 align="center">Vue-ECharts</h1>
<p align="center">Vue.js component for Apache ECharts™.</p>
<p align="center"><a href="https://npmjs.com/package/vue-echarts"><img alt="npm version" src="https://img.shields.io/npm/v/vue-echarts"></a> <a href="https://vue-echarts.dev/"><img src="https://img.shields.io/badge/Demo%20%C2%BB-20c3aa" alt="View demo"></a> <a href="./README.zh-Hans.md"><img src="https://img.shields.io/badge/%E4%B8%AD%E6%96%87%E7%89%88%20%C2%BB-000" alt="前往中文版"></a></p>
<p align="center"><a href="https:///pr.new/ecomfe/vue-echarts"><img alt="Open in Codeflow" src="https://developer.stackblitz.com/img/open_in_codeflow.svg" height="28"></a> <a href="https://codesandbox.io/p/github/ecomfe/vue-echarts"><img alt="Edit in CodeSandbox" src="https://assets.codesandbox.io/github/button-edit-lime.svg" height="28"></a></p>
---
> Still using Vue 2? Read v7 docs [here →](https://github.com/ecomfe/vue-echarts/tree/7.x)
@ -17,7 +19,7 @@ npm install echarts vue-echarts
#### Example
<details>
<summary><a href="https://stackblitz.com/edit/vue-echarts-8?file=src%2FApp.vue">Demo →</a></summary>
<summary>Vue 3 <a href="https://stackblitz.com/edit/vue-echarts-8?file=src%2FApp.vue">Demo →</a></summary>
```vue
<template>
@ -112,17 +114,17 @@ import "echarts";
Drop `<script>` inside your HTML file and access the component via `window.VueECharts`.
<details>
<summary><a href="https://stackblitz.com/edit/vue-echarts-8-global?file=index.html">Demo →</a></summary>
<summary>Vue 3 <a href="https://stackblitz.com/edit/vue-echarts-8-global?file=index.html">Demo →</a></summary>
<!-- scripts:start -->
<!-- vue3Scripts:start -->
```html
<script src="https://cdn.jsdelivr.net/npm/echarts@6.0.0"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.21"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-echarts@8.0.0-beta.2"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.18"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-echarts@8.0.0-beta.1"></script>
```
<!-- scripts:end -->
<!-- vue3Scripts:end -->
```js
const app = Vue.createApp(...)
@ -151,17 +153,14 @@ See more examples [here](https://github.com/ecomfe/vue-echarts/tree/main/demo).
- `option: object`
ECharts' universal interface. Modifying this prop triggers Vue ECharts to compute an update plan and call `setOption`. Read more [here →](https://echarts.apache.org/en/option.html)
ECharts' universal interface. Modifying this prop will trigger ECharts' `setOption` method. Read more [here →](https://echarts.apache.org/en/option.html)
#### Smart Update
- If you supply `update-options` (via prop or injection), Vue ECharts forwards it directly to `setOption` and skips the planner.
- Manual `setOption` calls (only available when `manual-update` is `true`) behave like native ECharts, honouring only the per-call override you pass in.
- Otherwise, Vue ECharts analyses the change: removed objects become `null`, removed arrays become `[]` with `replaceMerge`, ID/anonymous deletions trigger `replaceMerge`, and risky changes fall back to `notMerge: true`.
> [!TIP]
> When `update-options` is not specified, `notMerge: false` will be specified by default when the `setOption` method is called if the `option` object is modified directly and the reference remains unchanged; otherwise, if a new reference is bound to `option`, `notMerge: true` will be specified.
- `update-options: object`
Options for updating chart option. If supplied (or injected), Vue ECharts forwards it directly to `setOption`, skipping the [smart update](#smart-update). See `echartsInstance.setOption`'s `opts` parameter [here →](https://echarts.apache.org/en/api.html#echartsInstance.setOption)
Options for updating chart option. See `echartsInstance.setOption`'s `opts` parameter [here →](https://echarts.apache.org/en/api.html#echartsInstance.setOption)
Injection key: `UPDATE_OPTIONS_KEY`.
@ -185,7 +184,7 @@ See more examples [here](https://github.com/ecomfe/vue-echarts/tree/main/demo).
- `manual-update: boolean` (default: `false`)
For performance critical scenarios (having a large dataset) we'd better bypass Vue's reactivity system for `option` prop. By specifying `manual-update` prop with `true` and not providing `option` prop, the dataset won't be watched any more. After doing so, you need to retrieve the component instance with `ref` and manually call `setOption` method to update the chart (manual `setOption` calls are ignored when `manual-update` is `false`).
For performance critical scenarios (having a large dataset) we'd better bypass Vue's reactivity system for `option` prop. By specifying `manual-update` prop with `true` and not providing `option` prop, the dataset won't be watched any more. After doing so, you need to retrieve the component instance with `ref` and manually call `setOption` method to update the chart.
### Events
@ -200,7 +199,7 @@ You can bind events with Vue's `v-on` directive.
> [!NOTE]
> Only the `.once` event modifier is supported as other modifiers are tightly coupled with the DOM event system.
Vue ECharts support the following events:
Vue-ECharts support the following events:
- `highlight` [](https://echarts.apache.org/en/api.html#events.highlight)
- `downplay` [](https://echarts.apache.org/en/api.html#events.downplay)
@ -250,7 +249,7 @@ See supported events [here →](https://echarts.apache.org/en/api.html#events)
#### Native DOM Events
As Vue ECharts binds events to the ECharts instance by default, there is some caveat when using native DOM events. You need to prefix the event name with `native:` to bind native DOM events.
As Vue-ECharts binds events to the ECharts instance by default, there is some caveat when using native DOM events. You need to prefix the event name with `native:` to bind native DOM events.
```vue
<template>
@ -260,7 +259,7 @@ As Vue ECharts binds events to the ECharts instance by default, there is some ca
### Provide / Inject
Vue ECharts provides provide/inject API for `theme`, `init-options`, `update-options` and `loading-options` to help configuring contextual options. eg. for `theme` you can use the provide API like this:
Vue-ECharts provides provide/inject API for `theme`, `init-options`, `update-options` and `loading-options` to help configuring contextual options. eg. for `theme` you can use the provide API like this:
<details>
<summary>Composition API</summary>
@ -334,11 +333,11 @@ export default {
> The following ECharts instance methods aren't exposed because their functionality is already provided by component [props](#props):
>
> - [`showLoading`](https://echarts.apache.org/en/api.html#echartsInstance.showLoading) / [`hideLoading`](https://echarts.apache.org/en/api.html#echartsInstance.hideLoading): use the `loading` and `loading-options` props instead.
> - [`setTheme`](https://echarts.apache.org/en/api.html#echartsInstance.setTheme): use the `theme` prop instead.
> - `setTheme`: use the `theme` prop instead.
### Slots
Vue ECharts allows you to define ECharts option's [`tooltip.formatter`](https://echarts.apache.org/en/option.html#tooltip.formatter) and [`toolbox.feature.dataView.optionToContent`](https://echarts.apache.org/en/option.html#toolbox.feature.dataView.optionToContent) callbacks via Vue slots instead of defining them in your `option` object. This simplifies custom HTMLElement rendering using familiar Vue templating.
Vue-ECharts allows you to define ECharts option's [`tooltip.formatter`](https://echarts.apache.org/en/option.html#tooltip.formatter) and [`toolbox.feature.dataView.optionToContent`](https://echarts.apache.org/en/option.html#toolbox.feature.dataView.optionToContent) callbacks via Vue slots instead of defining them in your `option` object. This simplifies custom HTMLElement rendering using familiar Vue templating.
**Slot Naming Convention**

View File

@ -1,8 +1,10 @@
<p align="center"><a href="https://vue-echarts.dev/"><img alt="Vue ECharts" src="https://raw.githubusercontent.com/ecomfe/vue-echarts/refs/heads/main/demo/public/favicon.svg" width="96"></a></p>
<h1 align="center">Vue ECharts</h1>
<h1 align="center">Vue-ECharts</h1>
<p align="center">Apache ECharts™ 的 Vue.js 组件。</p>
<p align="center"><a href="https://npmjs.com/package/vue-echarts"><img alt="npm 版本" src="https://img.shields.io/npm/v/vue-echarts"></a> <a href="https://vue-echarts.dev/"><img src="https://img.shields.io/badge/%E6%BC%94%E7%A4%BA%20%C2%BB-20c3aa" alt="查看演示"></a></p>
<p align="center"><a href="https://npmjs.com/package/vue-echarts"><img alt="npm 版本" src="https://img.shields.io/npm/v/vue-echarts"></a> <a href="https://vue-echarts.dev/"><img src="https://img.shields.io/badge/%E6%BC%94%E7%A4%BA%20%C2%BB-20c3aa" alt="查看演示"></a> <a href="./README.zh-Hans.md"></p>
<p align="center"><a href="https:///pr.new/ecomfe/vue-echarts"><img alt="Open in Codeflow" src="https://developer.stackblitz.com/img/open_in_codeflow.svg" height="28"></a> <a href="https://codesandbox.io/p/github/ecomfe/vue-echarts"><img alt="Edit in CodeSandbox" src="https://assets.codesandbox.io/github/button-edit-lime.svg" height="28"></a></p>
---
> 还在使用 Vue 2可以继续阅读老版本的文档。[前往 →](https://github.com/ecomfe/vue-echarts/blob/7.x/README.zh-Hans.md)
@ -17,7 +19,7 @@ npm install echarts vue-echarts
#### 示例
<details>
<summary><a href="https://stackblitz.com/edit/vue-echarts-8?file=src%2FApp.vue">Demo →</a></summary>
<summary>Vue 3 <a href="https://stackblitz.com/edit/vue-echarts-8?file=src%2FApp.vue">Demo →</a></summary>
```vue
<template>
@ -112,17 +114,17 @@ import "echarts";
用如下方式在 HTML 中插入 `<script>` 标签,并且通过 `window.VueECharts` 来访问组件接口:
<details>
<summary><a href="https://stackblitz.com/edit/vue-echarts-8-global?file=index.html">Demo →</a></summary>
<summary>Vue 3 <a href="https://stackblitz.com/edit/vue-echarts-8-global?file=index.html">Demo →</a></summary>
<!-- scripts:start -->
<!-- vue3Scripts:start -->
```html
<script src="https://cdn.jsdelivr.net/npm/echarts@6.0.0"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.21"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-echarts@8.0.0-beta.2"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.18"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-echarts@8.0.0-beta.1"></script>
```
<!-- scripts:end -->
<!-- vue3Scripts:end -->
```js
const app = Vue.createApp(...)
@ -153,15 +155,12 @@ app.component('v-chart', VueECharts)
ECharts 的万能接口。修改这个 prop 会触发 ECharts 实例的 `setOption` 方法。查看[详情 →](https://echarts.apache.org/zh/option.html)
#### 智能更新
- 如果提供了 `update-options`(或通过 inject 注入Vue ECharts 会直接把它传给 `setOption`,不会执行智能计划。
- 手动调用 `setOption`(仅当 `manual-update``true` 时可用)与原生 ECharts 保持一致,只使用本次调用传入的参数。
- 其他情况下Vue ECharts 会分析差异:删除的对象写入 `null`,删除的数组写入 `[]` 并加入 `replaceMerge`ID/匿名项减少时追加 `replaceMerge`,风险较高的变更会退回 `notMerge: true`
> [!TIP]
> 在没有指定 `update-options` 时,如果直接修改 `option` 对象而引用保持不变,`setOption` 方法调用时将默认指定 `notMerge: false`;否则,如果为 `option` 绑定一个新的引用,将指定 `notMerge: true`。
- `update-options: object`
图表更新的配置项。一旦提供(或通过 inject 注入Vue ECharts 会直接把它传给 `setOption` 并跳过智能更新。请参考 `echartsInstance.setOption``opts` 参数。[前往 →](https://echarts.apache.org/zh/api.html#echartsInstance.setOption)
图表更新的配置项。请参考 `echartsInstance.setOption``opts` 参数。[前往 →](https://echarts.apache.org/zh/api.html#echartsInstance.setOption)
Inject 键名:`UPDATE_OPTIONS_KEY`
@ -185,7 +184,7 @@ app.component('v-chart', VueECharts)
- `manual-update: boolean`(默认值`false`
在性能敏感(数据量很大)的场景下,我们最好对于 `option` prop 绕过 Vue 的响应式系统。当将 `manual-update` 指定为 `true` 且不传入 `option` prop 时,数据将不会被监听。此时需要用 `ref` 获取组件实例手动调用 `setOption` 来更新图表(当 `manual-update``false` 时,手动调用 `setOption` 会被忽略)
在性能敏感(数据量很大)的场景下,我们最好对于 `option` prop 绕过 Vue 的响应式系统。当将 `manual-update` prop 指定为 `true` 且不传入 `option` prop 时,数据将不会被监听。然后,需要用 `ref` 获取组件实例以后手动调用 `setOption` 方法来更新图表。
### 事件
@ -200,7 +199,7 @@ app.component('v-chart', VueECharts)
> [!NOTE]
> 仅支持 `.once` 修饰符,因为其它修饰符都与 DOM 事件机制强耦合。
Vue ECharts 支持如下事件:
Vue-ECharts 支持如下事件:
- `highlight` [](https://echarts.apache.org/zh/api.html#events.highlight)
- `downplay` [](https://echarts.apache.org/zh/api.html#events.downplay)
@ -250,7 +249,7 @@ Vue ECharts 支持如下事件:
#### 原生 DOM 事件
由于 Vue ECharts 默认将事件绑定到 ECharts 实例,因此在使用原生 DOM 事件时需要做一些特殊处理。你需要在事件名称前加上 `native:` 前缀来绑定原生 DOM 事件。
由于 Vue-ECharts 默认将事件绑定到 ECharts 实例,因此在使用原生 DOM 事件时需要做一些特殊处理。你需要在事件名称前加上 `native:` 前缀来绑定原生 DOM 事件。
```vue
<template>
@ -260,7 +259,7 @@ Vue ECharts 支持如下事件:
### Provide / Inject
Vue ECharts 为 `theme``init-options``update-options``loading-options` 提供了 provide/inject API以通过上下文配置选项。例如可以通过如下方式来使用 provide API 为 `theme` 提供上下文配置:
Vue-ECharts 为 `theme``init-options``update-options``loading-options` 提供了 provide/inject API以通过上下文配置选项。例如可以通过如下方式来使用 provide API 为 `theme` 提供上下文配置:
<details>
<summary>组合式 API</summary>
@ -334,11 +333,11 @@ export default {
> 如下 ECharts 实例方法没有被暴露,因为它们的功能已经通过组件 [props](#props) 提供了:
>
> - [`showLoading`](https://echarts.apache.org/zh/api.html#echartsInstance.showLoading) / [`hideLoading`](https://echarts.apache.org/zh/api.html#echartsInstance.hideLoading):请使用 `loading` 和 `loading-options` prop。
> - [`setTheme`](https://echarts.apache.org/zh/api.html#echartsInstance.setTheme):请使用 `theme` prop。
> - `setTheme`:请使用 `theme` prop。
### 插槽Slots
Vue ECharts 允许你通过 Vue 插槽来定义 ECharts 配置中的 [`tooltip.formatter`](https://echarts.apache.org/zh/option.html#tooltip.formatter) 和 [`toolbox.feature.dataView.optionToContent`](https://echarts.apache.org/zh/option.html#toolbox.feature.dataView.optionToContent) 回调,而无需在 `option` 对象中定义它们。你可以使用熟悉的 Vue 模板语法来编写自定义提示框或数据视图中的内容。
Vue-ECharts 允许你通过 Vue 插槽来定义 ECharts 配置中的 [`tooltip.formatter`](https://echarts.apache.org/zh/option.html#tooltip.formatter) 和 [`toolbox.feature.dataView.optionToContent`](https://echarts.apache.org/zh/option.html#toolbox.feature.dataView.optionToContent) 回调,而无需在 `option` 对象中定义它们。你可以使用熟悉的 Vue 模板语法来编写自定义提示框或数据视图中的内容。
**插槽命名约定**

View File

@ -54,7 +54,7 @@ watch(codeOpen, (open) => {
<logo-chart />
<h1>
<a href="https://github.com/ecomfe/vue-echarts">Vue ECharts</a>
<a href="https://github.com/ecomfe/vue-echarts">Vue-ECharts</a>
</h1>
<p class="desc">
Vue.js component for Apache ECharts. (<a

View File

@ -4,13 +4,12 @@
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="/favicon.ico"/>
<link rel="icon" href="/favicon.svg" type="image/svg+xml"/>
<link rel="icon" href="/favicon.ico" />
<link
href="https://fonts.googleapis.com/css?family=Inter:300,500;display=swap"
rel="stylesheet"
/>
<title>Vue ECharts: Vue.js component for Apache ECharts™.</title>
<title>Vue-ECharts: Vue.js component for Apache ECharts™.</title>
</head>
<body>
<div id="app"></div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><g clip-path="url(#a)"><path fill="#42B883" d="M12 0a12 12 0 1 0 0 24 12 12 0 0 0 0-24Zm-.792 5.307c2.192-.025 4.366 1.134 5.43 3.304.909 1.852.878 3.61-.098 5.645-.477.995-.487 1.06-.241 1.578.214.452.727.779 1.221.779.454 0 1.15-.586 1.252-1.054.1-.454-.193-1.118-.607-1.377a10.147 10.147 0 0 1-.393-.255c-.129-.1.42-.38.741-.38.687 0 1.247.526 1.375 1.29.055.333.134.422.44.502.859.222 1.297 1.451.755 2.116-.22.27-.23.271-.305.042-.267-.801-.666-1.12-1.403-1.12-.319 0-.572.128-1.098.556-1.006.82-1.866 1.303-2.907 1.632-1.276.384-2.752.478-4.086.156-2.162-.431-4.232-2.11-5.252-4.257C4.758 11.782 5.135 9 7.033 7.077a5.924 5.924 0 0 1 4.175-1.77Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h24v24H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 817 B

View File

@ -3,7 +3,6 @@
"include": ["./**/*", "./**/*.vue"],
"compilerOptions": {
"types": ["vite/client"],
"allowJs": true,
"noUncheckedIndexedAccess": false
"allowJs": true
}
}

View File

@ -1,6 +1,6 @@
{
"name": "vue-echarts",
"version": "8.0.0-beta.2",
"version": "8.0.0-beta.1",
"description": "Vue.js component for Apache ECharts™.",
"license": "MIT",
"repository": {
@ -19,11 +19,9 @@
"dev:preview": "vite preview",
"dev:typecheck": "vue-tsc -p ./demo",
"docs": "jiti ./scripts/docs.ts",
"release": "bumpp --execute \"pnpm run docs\" --all",
"test": "vitest run",
"test:watch": "vitest watch"
"release": "pnpm run docs && bumpp --all"
},
"packageManager": "pnpm@10.16.0",
"packageManager": "pnpm@10.14.0",
"type": "module",
"main": "dist/index.js",
"unpkg": "dist/index.min.js",
@ -42,21 +40,21 @@
},
"devDependencies": {
"@highlightjs/vue-plugin": "^2.1.0",
"@types/node": "^22.17.1",
"@typescript-eslint/utils": "^8.39.1",
"@types/node": "^22.17.0",
"@typescript-eslint/utils": "^8.39.0",
"@vercel/analytics": "^1.5.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.8.0",
"@vue/tsconfig": "^0.7.0",
"@vueuse/core": "^13.6.0",
"bumpp": "^10.2.3",
"bumpp": "^10.2.2",
"comment-mark": "^2.0.1",
"echarts": "^6.0.0",
"echarts-gl": "^2.0.9",
"echarts-liquidfill": "^3.1.0",
"esbuild-wasm": "^0.25.9",
"eslint": "^9.33.0",
"esbuild-wasm": "^0.25.8",
"eslint": "^9.32.0",
"eslint-plugin-vue": "^10.4.0",
"highlight.js": "^11.11.1",
"jiti": "^2.5.1",
@ -65,11 +63,10 @@
"prettier": "^3.6.2",
"publint": "^0.3.12",
"releaselog": "^6.0.3",
"tsdown": "^0.15.0",
"tsdown": "^0.13.3",
"typescript": "^5.9.2",
"unplugin-raw": "^0.6.0",
"vite": "npm:rolldown-vite@latest",
"vitest": "^3.2.4",
"unplugin-raw": "^0.5.1",
"vite": "npm:rolldown-vite@^7.1.0",
"vue": "^3.5.18",
"vue-tsc": "^3.0.5"
},
@ -78,9 +75,6 @@
"allowedVersions": {
"echarts": "6"
}
},
"overrides": {
"vite": "npm:rolldown-vite@latest"
}
}
}

2307
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild

View File

@ -1,24 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"minimumReleaseAge": "7 days",
"internalChecksFilter": "strict",
"packageRules": [
{
"matchUpdateTypes": ["patch"],
"groupName": "patches after 7 days",
"automerge": true
},
{
"matchUpdateTypes": ["minor"],
"excludePackageNames": ["vue", "echarts"],
"groupName": "minors after 7 days",
"automerge": false
},
{
"matchPackageNames": ["vue", "echarts"],
"matchUpdateTypes": ["minor"],
"automerge": false
}
]
}

View File

@ -42,7 +42,7 @@ README_FILES.forEach((file) => {
writeFileSync(
file,
commentMark(content, {
scripts: getCodeBlock(getScripts()),
vue3Scripts: getCodeBlock(getScripts()),
}),
"utf8",
);

View File

@ -11,10 +11,8 @@ import {
nextTick,
watchEffect,
toValue,
warn,
} from "vue";
import { init as initChart } from "echarts/core";
import type { EChartsOption } from "echarts";
import {
usePublicAPI,
@ -27,8 +25,6 @@ import {
import type { PublicMethods, SlotsTypes } from "./composables";
import { isOn, omitOn } from "./utils";
import { register, TAG_NAME } from "./wc";
import { planUpdate } from "./smart-update";
import type { Signature, UpdatePlan } from "./smart-update";
import type { PropType, InjectionKey } from "vue";
import type {
@ -75,19 +71,24 @@ export default defineComponent({
setup(props, { attrs, expose, slots }) {
const root = shallowRef<EChartsElement>();
const chart = shallowRef<EChartsType>();
const manualOption = shallowRef<Option>();
const defaultTheme = inject(THEME_KEY, null);
const defaultInitOptions = inject(INIT_OPTIONS_KEY, null);
const defaultUpdateOptions = inject(UPDATE_OPTIONS_KEY, null);
const { autoresize, manualUpdate, loading, loadingOptions } = toRefs(props);
const realOption = computed(() => props.option || {});
const realTheme = computed(() => props.theme || toValue(defaultTheme));
const realOption = computed(
() => manualOption.value || props.option || null,
);
const realTheme = computed(
() => props.theme || toValue(defaultTheme) || {},
);
const realInitOptions = computed(
() => props.initOptions || toValue(defaultInitOptions) || undefined,
() => props.initOptions || toValue(defaultInitOptions) || {},
);
const realUpdateOptions = computed(
() => props.updateOptions || toValue(defaultUpdateOptions),
() => props.updateOptions || toValue(defaultUpdateOptions) || {},
);
const nonEventAttrs = computed(() => omitOn(attrs));
const nativeListeners: Record<string, unknown> = {};
@ -97,70 +98,13 @@ export default defineComponent({
const { teleportedSlots, patchOption } = useSlotOption(slots, () => {
if (!manualUpdate.value && props.option && chart.value) {
applyOption(chart.value, props.option);
chart.value.setOption(
patchOption(props.option),
realUpdateOptions.value,
);
}
});
let lastSignature: Signature | undefined;
function resolveUpdateOptions(
plan?: UpdatePlan,
override?: UpdateOptions,
): UpdateOptions {
const base = realUpdateOptions.value;
const result: UpdateOptions = { ...override };
const replacements = [
...(plan?.replaceMerge ?? []),
...(override?.replaceMerge ?? []),
].filter((key): key is string => key != null);
if (replacements.length > 0) {
result.replaceMerge = [...new Set(replacements)];
} else {
delete result.replaceMerge;
}
const notMerge = override?.notMerge ?? plan?.notMerge;
if (notMerge !== undefined) {
result.notMerge = notMerge;
} else {
delete result.notMerge;
}
return base ? { ...base, ...result } : result;
}
function applyOption(
instance: EChartsType,
option: Option,
override?: UpdateOptions,
manual = false,
) {
const patched = patchOption(option);
if (manual) {
instance.setOption(patched, override ?? {});
lastSignature = undefined;
return;
}
if (realUpdateOptions.value) {
const updateOptions = override ?? realUpdateOptions.value;
instance.setOption(patched, updateOptions);
lastSignature = undefined;
return;
}
const planned = planUpdate(
lastSignature,
patched as unknown as EChartsOption,
);
const updateOptions = resolveUpdateOptions(planned.plan, override);
instance.setOption(planned.option, updateOptions);
lastSignature = planned.signature;
}
// We are converting all `on<Event>` props and collect them into `listeners` so that
// we can bind them to the chart instance later.
// For `onNative:<event>` props, we just strip the `Native:` part and collect them into
@ -196,7 +140,7 @@ export default defineComponent({
listeners.set({ event, zr, once }, attrs[key]);
});
function init(option?: Option, manual = false, override?: UpdateOptions) {
function init(option?: Option) {
if (!root.value) {
return;
}
@ -242,8 +186,7 @@ export default defineComponent({
function commit() {
const opt = option || realOption.value;
if (opt) {
applyOption(instance, opt, override, manual);
override = undefined;
instance.setOption(patchOption(opt), realUpdateOptions.value);
}
}
@ -263,20 +206,17 @@ export default defineComponent({
notMerge,
lazyUpdate?: boolean,
) => {
if (!props.manualUpdate) {
warn(
"[vue-echarts] setOption is only available when manual-update is true.",
);
return;
}
const updateOptions =
typeof notMerge === "boolean" ? { notMerge, lazyUpdate } : notMerge;
if (props.manualUpdate) {
manualOption.value = option;
}
if (!chart.value) {
init(option, true, updateOptions ?? undefined);
init(option);
} else {
applyOption(chart.value, option, updateOptions ?? undefined, true);
chart.value.setOption(patchOption(option), updateOptions);
}
};
@ -285,7 +225,6 @@ export default defineComponent({
chart.value.dispose();
chart.value = undefined;
}
lastSignature = undefined;
}
let unwatchOption: (() => void) | null = null;
@ -300,15 +239,19 @@ export default defineComponent({
if (!manualUpdate) {
unwatchOption = watch(
() => props.option,
(option) => {
(option, oldOption) => {
if (!option) {
lastSignature = undefined;
return;
}
if (!chart.value) {
init();
} else {
applyOption(chart.value, option);
chart.value.setOption(patchOption(option), {
// mutating `option` will lead to `notMerge: false` and
// replacing it with new reference will lead to `notMerge: true`
notMerge: option !== oldOption,
...realUpdateOptions.value,
});
}
},
{ deep: true },
@ -334,7 +277,7 @@ export default defineComponent({
watch(
realTheme,
(theme) => {
chart.value?.setTheme(theme || {});
chart.value?.setTheme(theme);
},
{
deep: true,

View File

@ -1,243 +0,0 @@
import type { EChartsOption } from "echarts";
import { isPlainObject } from "./utils";
export interface UpdatePlan {
notMerge: boolean;
replaceMerge?: string[];
}
/** Summary of a top-level array key for deletion detection. */
export interface ArraySummary {
/** Unique, sorted string ids extracted from items' `id` field. */
idsSorted: string[];
/** Count of items without an `id` field. */
noIdCount: number;
}
/** Minimal signature of an option used to decide setOption behavior. */
export interface Signature {
/** Lengths of `option.options` and `option.media` (0 if not arrays). */
optionsLength: number;
mediaLength: number;
/** Map of array-typed top-level keys to their summaries. */
arrays: Record<string, ArraySummary | undefined>;
/** Sorted list of object-typed top-level keys. */
objects: string[];
/** Sorted list of scalar-typed top-level keys (string|number|boolean|null). */
scalars: string[];
}
/**
* Read an item's `id` as a string.
* Only accept string or number. Other types are ignored to surface inconsistent data early.
*/
function readId(item: unknown): string | undefined {
if (!isPlainObject(item)) {
return undefined;
}
const raw = (item as { id?: unknown }).id;
if (typeof raw === "string") {
return raw;
}
if (typeof raw === "number" && Number.isFinite(raw)) {
return String(raw);
}
return undefined;
}
/**
* Build a minimal signature from a full ECharts option.
* Only top-level keys are inspected.
*/
export function buildSignature(option: EChartsOption): Signature {
const opt = option as Record<string, unknown>;
const optionsLength = Array.isArray(opt.options)
? (opt.options as unknown[]).length
: 0;
const mediaLength = Array.isArray(opt.media)
? (opt.media as unknown[]).length
: 0;
const arrays: Record<string, ArraySummary | undefined> = Object.create(null);
const objects: string[] = [];
const scalars: string[] = [];
for (const key of Object.keys(opt)) {
if (key === "options" || key === "media") {
continue;
}
const value = opt[key];
if (Array.isArray(value)) {
const items = value as unknown[];
const ids = new Set<string>();
let noIdCount = 0;
for (let i = 0; i < items.length; i++) {
const id = readId(items[i]);
if (id !== undefined) {
ids.add(id);
} else {
noIdCount++;
}
}
const idsSorted = ids.size > 0 ? Array.from(ids).sort() : [];
arrays[key] = { idsSorted, noIdCount };
} else if (isPlainObject(value)) {
objects.push(key);
} else {
// scalar: string | number | boolean | null (undefined is treated as "absent")
if (value !== undefined) {
scalars.push(key);
}
}
}
if (objects.length > 1) {
objects.sort();
}
if (scalars.length > 1) {
scalars.sort();
}
return { optionsLength, mediaLength, arrays, objects, scalars };
}
function diffKeys(
prevKeys: readonly string[],
nextKeys: readonly string[],
): string[] {
if (prevKeys.length === 0) {
return [];
}
if (nextKeys.length === 0) {
return prevKeys.slice();
}
const nextSet = new Set(nextKeys);
const missing: string[] = [];
for (let i = 0; i < prevKeys.length; i++) {
const key = prevKeys[i];
if (!nextSet.has(key)) {
missing.push(key);
}
}
return missing;
}
function hasMissingIds(
prevIds: readonly string[],
nextIds: readonly string[],
): boolean {
if (prevIds.length === 0) {
return false;
}
if (nextIds.length === 0) {
return true;
}
const nextSet = new Set(nextIds);
for (let i = 0; i < prevIds.length; i++) {
if (!nextSet.has(prevIds[i])) {
return true;
}
}
return false;
}
export interface PlannedUpdate {
option: EChartsOption;
signature: Signature;
plan: UpdatePlan;
}
/**
* Produce an update plan plus a normalized option that encodes common deletions.
* Falls back to `notMerge: true` when the change looks complex.
*/
export function planUpdate(
prev: Signature | undefined,
option: EChartsOption,
): PlannedUpdate {
const next = buildSignature(option);
if (!prev) {
return { option, signature: next, plan: { notMerge: false } };
}
if (next.optionsLength < prev.optionsLength) {
return { option, signature: next, plan: { notMerge: true } };
}
if (next.mediaLength < prev.mediaLength) {
return { option, signature: next, plan: { notMerge: true } };
}
if (diffKeys(prev.scalars, next.scalars).length > 0) {
return { option, signature: next, plan: { notMerge: true } };
}
const replace = new Set<string>();
const overrides = new Map<string, null | []>();
const missingObjects = diffKeys(prev.objects, next.objects);
for (let i = 0; i < missingObjects.length; i++) {
overrides.set(missingObjects[i], null);
}
for (const key of Object.keys(prev.arrays)) {
const prevArray = prev.arrays[key];
if (!prevArray) {
continue;
}
const nextArray = next.arrays[key];
if (!nextArray) {
if (prevArray.idsSorted.length > 0 || prevArray.noIdCount > 0) {
overrides.set(key, []);
replace.add(key);
}
continue;
}
if (hasMissingIds(prevArray.idsSorted, nextArray.idsSorted)) {
replace.add(key);
continue;
}
if (nextArray.noIdCount < prevArray.noIdCount) {
replace.add(key);
}
}
let normalizedOption = option;
let signature = next;
if (overrides.size > 0) {
const clone = { ...(option as Record<string, unknown>) };
overrides.forEach((value, key) => {
clone[key] = value;
});
normalizedOption = clone as EChartsOption;
signature = buildSignature(normalizedOption);
}
const replaceMerge =
replace.size > 0 ? Array.from(replace).sort() : undefined;
const plan = replaceMerge
? { notMerge: false, replaceMerge }
: { notMerge: false };
return {
option: normalizedOption,
signature,
plan,
};
}

View File

@ -38,7 +38,3 @@ export function isSameSet<T>(a: T[], b: T[]): boolean {
return true;
}
export function isPlainObject(v: unknown): v is Record<string, unknown> {
return v != null && typeof v === "object" && !Array.isArray(v);
}

View File

@ -1,298 +0,0 @@
import { describe, it, expect } from "vitest";
import { buildSignature, planUpdate } from "../src/smart-update";
import type { EChartsOption } from "echarts";
describe("smart-update", () => {
describe("buildSignature", () => {
it("collects scalars, objects, and array summaries", () => {
const option: EChartsOption = {
title: { text: "foo" },
tooltip: { show: true },
color: "#000",
dataset: [{ id: "ds1", source: [] }, { source: [] }],
series: [
{ id: "a", type: "bar" },
{ type: "line" },
],
};
const signature = buildSignature(option);
expect(signature.objects).toEqual(["title", "tooltip"]);
expect(signature.scalars).toEqual(["color"]);
expect(signature.arrays.dataset?.idsSorted).toEqual(["ds1"]);
expect(signature.arrays.dataset?.noIdCount).toBe(1);
expect(signature.arrays.series?.idsSorted).toEqual(["a"]);
expect(signature.arrays.series?.noIdCount).toBe(1);
});
});
describe("planUpdate", () => {
describe("bootstrap & neutral cases", () => {
it("returns neutral plan when previous signature missing", () => {
const option: EChartsOption = {
legend: { show: true },
series: [{ type: "bar", data: [1, 2, 3] }],
};
const result = planUpdate(undefined, option);
expect(result.plan.notMerge).toBe(false);
expect(result.plan.replaceMerge).toBeUndefined();
expect(result.option).toEqual(option);
});
it("returns neutral plan when signatures match", () => {
const option: EChartsOption = {
title: { text: "foo" },
series: [{ id: "a" }],
};
const prev = buildSignature(option);
const next = planUpdate(prev, option);
expect(next.plan.notMerge).toBe(false);
expect(next.plan.replaceMerge).toBeUndefined();
expect(next.option).toEqual(option);
});
it("keeps merge when scalar value changes", () => {
const prev = buildSignature({ color: "red" });
const next = planUpdate(prev, { color: "blue" });
expect(next.plan.notMerge).toBe(false);
expect(next.plan.replaceMerge).toBeUndefined();
expect(next.option.color).toBe("blue");
});
it("keeps merge when new series IDs are added", () => {
const base: EChartsOption = {
series: [{ id: "latte", type: "bar", data: [10, 20] }],
};
const update: EChartsOption = {
series: [
{ id: "latte", type: "bar", data: [12, 24] },
{ id: "mocha", type: "bar", data: [14, 28] },
],
};
const result = planUpdate(buildSignature(base), update);
expect(result.plan.notMerge).toBe(false);
expect(result.plan.replaceMerge).toBeUndefined();
expect(result.option.series).toEqual(update.series);
});
});
describe("shrink detection", () => {
it("forces rebuild when options shrink", () => {
const prev = buildSignature({ options: [{}, {}] });
const { plan } = planUpdate(prev, { options: [{}] });
expect(plan.notMerge).toBe(true);
});
it("forces rebuild when scalars disappear", () => {
const prev = buildSignature({ color: "red", title: { text: "foo" } });
const { plan } = planUpdate(prev, { title: { text: "foo" } });
expect(plan.notMerge).toBe(true);
});
it("injects null for removed objects", () => {
const prev = buildSignature({ legend: { show: true } });
const next = planUpdate(prev, {});
expect(next.option.legend).toBeNull();
expect(next.plan.notMerge).toBe(false);
});
it("injects empty array and replaceMerge when array removed", () => {
const prev = buildSignature({ series: [{ id: "a" }, {}] });
const next = planUpdate(prev, {});
expect(next.option.series).toEqual([]);
expect(next.plan.replaceMerge).toEqual(["series"]);
});
it("adds replaceMerge when ids shrink", () => {
const prev = buildSignature({ series: [{ id: "a" }, { id: "b" }] });
const next = planUpdate(prev, { series: [{ id: "a" }] });
expect(next.plan.replaceMerge).toEqual(["series"]);
});
it("adds replaceMerge when anonymous count shrinks", () => {
const prev = buildSignature({ series: [{}, {}] });
const next = planUpdate(prev, { series: [{}] });
expect(next.plan.replaceMerge).toEqual(["series"]);
});
});
describe("real data scenarios", () => {
it("handles legend removal and series shrink", () => {
const base: EChartsOption = {
legend: { show: true },
dataset: [
{
id: "sales",
source: [
["product", "2015", "2016"],
["Matcha Latte", 43.3, 85.8],
],
},
],
series: [
{ id: "2015", type: "bar", datasetId: "sales" },
{ id: "2016", type: "bar", datasetId: "sales" },
],
};
const update: EChartsOption = {
dataset: [
{
id: "sales",
source: [
["product", "2015"],
["Matcha Latte", 55.1],
],
},
],
series: [{ id: "2015", type: "bar", datasetId: "sales" }],
};
const result = planUpdate(buildSignature(base), update);
expect(result.option.legend).toBeNull();
expect(result.option.series).toEqual(update.series);
expect(result.plan.notMerge).toBe(false);
expect(result.plan.replaceMerge).toEqual(["series"]);
});
it("clears dataset when removed entirely", () => {
const base: EChartsOption = {
dataset: [
{
id: "sales",
source: [
["product", "2015"],
["Latte", 30],
],
},
],
series: [{ id: "sales-series", type: "bar", datasetId: "sales" }],
};
const update: EChartsOption = {
series: [{ id: "sales-series", type: "bar", data: [35] }],
};
const result = planUpdate(buildSignature(base), update);
expect(result.option.dataset).toEqual([]);
expect(result.plan.notMerge).toBe(false);
expect(result.plan.replaceMerge).toContain("dataset");
});
it("tracks multiple array shrink operations", () => {
const base: EChartsOption = {
legend: { show: true },
dataset: [
{
id: "2015",
source: [
["Latte", 30],
["Mocha", 24],
],
},
{
id: "2016",
source: [
["Latte", 40],
["Mocha", 35],
],
},
],
series: [
{ id: "latte", type: "bar", datasetId: "2015" },
{ id: "mocha", type: "bar", datasetId: "2016" },
],
};
const update: EChartsOption = {
series: [{ id: "latte", type: "bar", datasetId: "2015" }],
};
const result = planUpdate(buildSignature(base), update);
expect(result.option.legend).toBeNull();
expect(result.option.dataset).toEqual([]);
expect(result.plan.notMerge).toBe(false);
expect(result.plan.replaceMerge).toEqual(["dataset", "series"]);
});
it("injects null for tooltip removal while keeping explicit arrays", () => {
const base: EChartsOption = {
tooltip: { trigger: "axis" },
xAxis: [{ type: "category", data: ["Jan", "Feb"] }],
series: [{ type: "line", data: [10, 20] }],
};
const update: EChartsOption = {
xAxis: [{ type: "category", data: ["Jan", "Feb"] }],
series: [{ type: "line", data: [12, 18] }],
};
const result = planUpdate(buildSignature(base), update);
expect(result.option.tooltip).toBeNull();
expect(result.option.xAxis).toEqual(update.xAxis);
expect(result.plan.notMerge).toBe(false);
expect(result.plan.replaceMerge).toBeUndefined();
});
it("handles dataset to series data migration", () => {
const base: EChartsOption = {
dataset: [
{
id: "sales",
source: [
["Latte", 30],
["Mocha", 40],
],
},
],
series: [{ id: "sales", type: "bar", datasetId: "sales" }],
};
const update: EChartsOption = {
series: [{ id: "sales", type: "bar", data: [35, 44] }],
};
const result = planUpdate(buildSignature(base), update);
expect(result.option.dataset).toEqual([]);
expect(result.option.series).toEqual(update.series);
expect(result.plan.replaceMerge).toEqual(["dataset"]);
});
it("tracks series ID removal while keeping modifications", () => {
const base: EChartsOption = {
series: [
{ id: "latte", type: "bar", data: [10, 20] },
{ id: "mocha", type: "bar", data: [15, 25] },
],
};
const update: EChartsOption = {
series: [{ id: "latte", type: "line", data: [11, 22] }],
};
const result = planUpdate(buildSignature(base), update);
expect(result.option.series).toEqual(update.series);
expect(result.plan.replaceMerge).toEqual(["series"]);
});
});
});
});

View File

@ -1,8 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["vitest/globals", "node"],
"noEmit": true
},
"include": ["tests/**/*.ts", "src/**/*.ts"]
}

View File

@ -1,12 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
include: ["tests/**/*.test.ts"],
coverage: {
reporter: ["text", "lcov"],
},
},
});