mirror of
https://github.com/ecomfe/vue-echarts.git
synced 2025-11-05 20:36:09 +08:00
Compare commits
21 Commits
v8.0.0-bet
...
8.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 36c3cb97a9 | |||
| 55c68b48b7 | |||
| c232e71c47 | |||
| e568005bb2 | |||
| 8ed975e09b | |||
| 570a26c262 | |||
| df640ebce6 | |||
| 30e7934aab | |||
| 6155bbb409 | |||
| fa42af0723 | |||
| 8b7ef5e6e1 | |||
| 522dd7cc5c | |||
| 077bd3ec40 | |||
| 473fed37a2 | |||
| 7ae6892fe6 | |||
| 71c106ae29 | |||
| 381489da2f | |||
| 2fb0dc2233 | |||
| b6c84aab7e | |||
| c6a1228c9d | |||
| 9067505a3a |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
||||
* @ecomfe/vue-echarts
|
||||
6
.github/ISSUE_TEMPLATE/bug-report.en-US.yml
vendored
6
.github/ISSUE_TEMPLATE/bug-report.en-US.yml
vendored
@ -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"
|
||||
|
||||
@ -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> 标签"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
name: "✨ 新功能建议"
|
||||
description: 给 Vue ECharts 提交新功能建议
|
||||
description: 给 Vue-ECharts 提交新功能建议
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
|
||||
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@ -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
|
||||
|
||||
|
||||
21
AGENTS.md
21
AGENTS.md
@ -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 library’s 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`.
|
||||
@ -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.
|
||||
|
||||
41
README.md
41
README.md
@ -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**
|
||||
|
||||
|
||||
@ -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 模板语法来编写自定义提示框或数据视图中的内容。
|
||||
|
||||
**插槽命名约定**
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 |
@ -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 |
@ -3,7 +3,6 @@
|
||||
"include": ["./**/*", "./**/*.vue"],
|
||||
"compilerOptions": {
|
||||
"types": ["vite/client"],
|
||||
"allowJs": true,
|
||||
"noUncheckedIndexedAccess": false
|
||||
"allowJs": true
|
||||
}
|
||||
}
|
||||
|
||||
30
package.json
30
package.json
@ -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
2307
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -42,7 +42,7 @@ README_FILES.forEach((file) => {
|
||||
writeFileSync(
|
||||
file,
|
||||
commentMark(content, {
|
||||
scripts: getCodeBlock(getScripts()),
|
||||
vue3Scripts: getCodeBlock(getScripts()),
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
115
src/ECharts.ts
115
src/ECharts.ts
@ -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,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals", "node"],
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["tests/**/*.ts", "src/**/*.ts"]
|
||||
}
|
||||
@ -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"],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user