From 34e04a7b9877c136b98993f2f8e85c027107df4c Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Mon, 2 Jun 2025 21:33:25 +0800 Subject: [PATCH 01/27] feat: experimental component rendered tooltip --- demo/Demo.vue | 2 + demo/examples/LineChart.vue | 106 ++++++++++++++++++++++++++++++++++++ src/ECharts.ts | 37 ++++++++++--- 3 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 demo/examples/LineChart.vue diff --git a/demo/Demo.vue b/demo/Demo.vue index 9270f02..9e33e98 100644 --- a/demo/Demo.vue +++ b/demo/Demo.vue @@ -8,6 +8,7 @@ import { track } from "@vercel/analytics"; import LogoChart from "./examples/LogoChart.vue"; import BarChart from "./examples/BarChart.vue"; +import LineChart from "./examples/LineChart.vue"; import PieChart from "./examples/PieChart.vue"; import PolarChart from "./examples/PolarChart.vue"; import ScatterChart from "./examples/ScatterChart.vue"; @@ -74,6 +75,7 @@ watch(codeOpen, (open) => { </p> <bar-chart /> + <line-chart /> <pie-chart /> <polar-chart /> <scatter-chart /> diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue new file mode 100644 index 0000000..c60ba6d --- /dev/null +++ b/demo/examples/LineChart.vue @@ -0,0 +1,106 @@ +<script setup> +import { use } from "echarts/core"; +import { LineChart } from "echarts/charts"; +import { + GridComponent, + DatasetComponent, + LegendComponent, + TooltipComponent, +} from "echarts/components"; +import { shallowRef } from "vue"; +import VChart from "../../src/ECharts"; +import VExample from "./Example.vue"; + +use([ + DatasetComponent, + GridComponent, + LegendComponent, + LineChart, + TooltipComponent, +]); + +const option = shallowRef({ + legend: { top: 20 }, + tooltip: { + trigger: "axis", + showContent: false, + }, + dataset: { + source: [ + ["product", "2012", "2013", "2014", "2015", "2016", "2017"], + ["Milk Tea", 56.5, 82.1, 88.7, 70.1, 53.4, 85.1], + ["Matcha Latte", 51.1, 51.4, 55.1, 53.3, 73.8, 68.7], + ["Cheese Cocoa", 40.1, 62.2, 69.5, 36.4, 45.2, 32.5], + ["Walnut Brownie", 25.2, 37.1, 41.2, 18, 33.9, 49.1], + ], + }, + xAxis: { type: "category" }, + yAxis: {}, + series: [ + { + type: "line", + smooth: true, + seriesLayoutBy: "row", + emphasis: { focus: "series" }, + }, + { + type: "line", + smooth: true, + seriesLayoutBy: "row", + emphasis: { focus: "series" }, + }, + { + type: "line", + smooth: true, + seriesLayoutBy: "row", + emphasis: { focus: "series" }, + }, + { + type: "line", + smooth: true, + seriesLayoutBy: "row", + emphasis: { focus: "series" }, + }, + ], +}); +</script> + +<template> + <v-example + id="line" + title="Line chart" + desc="(with component rendered tooltip)" + > + <v-chart :option="option" autoresize> + <template #tooltip="{ params, show }"> + <div + v-if="show" + :style="{ + position: 'absolute', + top: '0px', + left: '0px', + transform: `translate3d(${params.x + 20}px, ${params.y + 20}px, 0px)`, + zIndex: 1000, + pointerEvents: 'none', + transition: + 'opacity 0.2s cubic-bezier(0.23, 1, 0.32, 1), visibility 0.2s cubic-bezier(0.23, 1, 0.32, 1), transform 0.4s cubic-bezier(0.23, 1, 0.32, 1)', + }" + > + <div + style=" + background: rgba(255, 255, 255, 0.2); + padding: 10px; + border-radius: 4px; + border: 1px solid rgb(102, 102, 102); + will-change: transform; + backdrop-filter: blur(8px); + box-shadow: rgba(0, 0, 0, 0.2) 1px 2px 10px; + " + > + {{ params }} + </div> + </div> + </template> + </v-chart> + </v-example> +</template> diff --git a/src/ECharts.ts b/src/ECharts.ts index 66e2646..b87ac6e 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -24,7 +24,7 @@ import { import { isOn, omitOn, toValue } from "./utils"; import { register, TAG_NAME } from "./wc"; -import type { PropType, InjectionKey } from "vue"; +import type { PropType, InjectionKey, SlotsType } from "vue"; import type { EChartsType, SetOptionType, @@ -65,7 +65,10 @@ export default defineComponent({ }, emits: {} as unknown as Emits, inheritAttrs: false, - setup(props, { attrs, expose }) { + slots: Object as SlotsType<{ + tooltip: { params: any; show: boolean }; + }>, + setup(props, { attrs, expose, slots }) { const root = shallowRef<EChartsElement>(); const chart = shallowRef<EChartsType>(); const manualOption = shallowRef<Option>(); @@ -93,6 +96,9 @@ export default defineComponent({ const listeners: Map<{ event: string; once?: boolean; zr?: boolean }, any> = new Map(); + const tooltipShow = shallowRef(false); + const tooltipParams = shallowRef<any>(null); + // 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 @@ -178,6 +184,14 @@ export default defineComponent({ } } + instance.on("showTip", (params) => { + tooltipShow.value = true; + tooltipParams.value = params; + }); + instance.on("hideTip", () => { + tooltipShow.value = false; + }); + if (autoresize.value) { // Try to make chart fit to container in case container size // is changed synchronously or in already queued microtasks @@ -302,11 +316,18 @@ export default defineComponent({ // This type casting ensures TypeScript correctly types the exposed members // that will be available when using this component. return (() => - h(TAG_NAME, { - ...nonEventAttrs.value, - ...nativeListeners, - ref: root, - class: ["echarts", ...(nonEventAttrs.value.class || [])], - })) as unknown as typeof exposed & PublicMethods; + h( + TAG_NAME, + { + ...nonEventAttrs.value, + ...nativeListeners, + ref: root, + class: ["echarts", ...(nonEventAttrs.value.class || [])], + }, + slots.tooltip?.({ + params: tooltipParams.value, + show: tooltipShow.value, + }), + )) as unknown as typeof exposed & PublicMethods; }, }); From 79a1d7d299a4bfe82bac763bb453ea3bb041d9d0 Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Sat, 7 Jun 2025 16:59:28 +0800 Subject: [PATCH 02/27] revert slot in VChart --- demo/examples/LineChart.vue | 32 +------------------------------- src/ECharts.ts | 37 ++++++++----------------------------- 2 files changed, 9 insertions(+), 60 deletions(-) diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue index c60ba6d..b43c2e3 100644 --- a/demo/examples/LineChart.vue +++ b/demo/examples/LineChart.vue @@ -71,36 +71,6 @@ const option = shallowRef({ title="Line chart" desc="(with component rendered tooltip)" > - <v-chart :option="option" autoresize> - <template #tooltip="{ params, show }"> - <div - v-if="show" - :style="{ - position: 'absolute', - top: '0px', - left: '0px', - transform: `translate3d(${params.x + 20}px, ${params.y + 20}px, 0px)`, - zIndex: 1000, - pointerEvents: 'none', - transition: - 'opacity 0.2s cubic-bezier(0.23, 1, 0.32, 1), visibility 0.2s cubic-bezier(0.23, 1, 0.32, 1), transform 0.4s cubic-bezier(0.23, 1, 0.32, 1)', - }" - > - <div - style=" - background: rgba(255, 255, 255, 0.2); - padding: 10px; - border-radius: 4px; - border: 1px solid rgb(102, 102, 102); - will-change: transform; - backdrop-filter: blur(8px); - box-shadow: rgba(0, 0, 0, 0.2) 1px 2px 10px; - " - > - {{ params }} - </div> - </div> - </template> - </v-chart> + <v-chart :option="option" autoresize /> </v-example> </template> diff --git a/src/ECharts.ts b/src/ECharts.ts index b87ac6e..66e2646 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -24,7 +24,7 @@ import { import { isOn, omitOn, toValue } from "./utils"; import { register, TAG_NAME } from "./wc"; -import type { PropType, InjectionKey, SlotsType } from "vue"; +import type { PropType, InjectionKey } from "vue"; import type { EChartsType, SetOptionType, @@ -65,10 +65,7 @@ export default defineComponent({ }, emits: {} as unknown as Emits, inheritAttrs: false, - slots: Object as SlotsType<{ - tooltip: { params: any; show: boolean }; - }>, - setup(props, { attrs, expose, slots }) { + setup(props, { attrs, expose }) { const root = shallowRef<EChartsElement>(); const chart = shallowRef<EChartsType>(); const manualOption = shallowRef<Option>(); @@ -96,9 +93,6 @@ export default defineComponent({ const listeners: Map<{ event: string; once?: boolean; zr?: boolean }, any> = new Map(); - const tooltipShow = shallowRef(false); - const tooltipParams = shallowRef<any>(null); - // 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 @@ -184,14 +178,6 @@ export default defineComponent({ } } - instance.on("showTip", (params) => { - tooltipShow.value = true; - tooltipParams.value = params; - }); - instance.on("hideTip", () => { - tooltipShow.value = false; - }); - if (autoresize.value) { // Try to make chart fit to container in case container size // is changed synchronously or in already queued microtasks @@ -316,18 +302,11 @@ export default defineComponent({ // This type casting ensures TypeScript correctly types the exposed members // that will be available when using this component. return (() => - h( - TAG_NAME, - { - ...nonEventAttrs.value, - ...nativeListeners, - ref: root, - class: ["echarts", ...(nonEventAttrs.value.class || [])], - }, - slots.tooltip?.({ - params: tooltipParams.value, - show: tooltipShow.value, - }), - )) as unknown as typeof exposed & PublicMethods; + h(TAG_NAME, { + ...nonEventAttrs.value, + ...nativeListeners, + ref: root, + class: ["echarts", ...(nonEventAttrs.value.class || [])], + })) as unknown as typeof exposed & PublicMethods; }, }); From 42c3569d8a9365b794aa3ec1739e4692cc2ffe55 Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Sat, 7 Jun 2025 17:49:36 +0800 Subject: [PATCH 03/27] feat: use tooltip composable --- demo/examples/LineChart.vue | 34 ++++++++++++++++++++++++++++-- src/composables/tooltip.ts | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 src/composables/tooltip.ts diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue index b43c2e3..4fdd294 100644 --- a/demo/examples/LineChart.vue +++ b/demo/examples/LineChart.vue @@ -1,4 +1,4 @@ -<script setup> +<script setup lang="ts"> import { use } from "echarts/core"; import { LineChart } from "echarts/charts"; import { @@ -9,6 +9,7 @@ import { } from "echarts/components"; import { shallowRef } from "vue"; import VChart from "../../src/ECharts"; +import { createTooltipTemplate } from "../../src/composables/tooltip"; import VExample from "./Example.vue"; use([ @@ -19,11 +20,19 @@ use([ TooltipComponent, ]); +type MyParams = { + seriesName: string; + seriesIndex: number; + data: number[]; + marker: string; +}[]; +const { define: DefineTooltip, formatter } = createTooltipTemplate<MyParams>(); + const option = shallowRef({ legend: { top: 20 }, tooltip: { trigger: "axis", - showContent: false, + formatter, }, dataset: { source: [ @@ -72,5 +81,26 @@ const option = shallowRef({ desc="(with component rendered tooltip)" > <v-chart :option="option" autoresize /> + <!-- TODO: use a Pie Chart as tooltip --> + <define-tooltip v-slot="params"> + <b>Custom Tooltip</b> + <table> + <thead> + <tr> + <th>Product</th> + <th>Value</th> + </tr> + </thead> + <tbody> + <tr v-for="(item, i) in params" :key="i"> + <td> + <span v-html="item.marker" /> + {{ item.seriesName }} + </td> + <td>{{ item.data[i + 1] }}</td> + </tr> + </tbody> + </table> + </define-tooltip> </v-example> </template> diff --git a/src/composables/tooltip.ts b/src/composables/tooltip.ts new file mode 100644 index 0000000..e3a9e4e --- /dev/null +++ b/src/composables/tooltip.ts @@ -0,0 +1,41 @@ +import { + h, + render, + defineComponent, + Slot, + Fragment, + type DefineComponent, +} from "vue"; + +export function createTooltipTemplate<Params extends object>() { + let slot: Slot | undefined; + + const define = defineComponent({ + setup(_, { slots }) { + return () => { + slot = slots.default; + }; + }, + }) as DefineComponent & { + new (): { $slots: { default(_: Params): any } }; + }; + + const formatter = (params: Params): HTMLElement[] => { + if (!slot) { + throw new Error( + `[vue-echarts] Failed to find the definition of tooltip template`, + ); + } + + const container = document.createElement("div"); + + const vnode = h(Fragment, null, slot(params)); + // console.log(params); + + render(vnode, container); + + return Array.from(container.children) as HTMLElement[]; + }; + + return { define, formatter }; +} From 978317eef9c50a3b84d734455ea14bbae09f786d Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Sat, 7 Jun 2025 21:16:57 +0800 Subject: [PATCH 04/27] feat: try createApp --- src/composables/tooltip.ts | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/composables/tooltip.ts b/src/composables/tooltip.ts index e3a9e4e..63eaac1 100644 --- a/src/composables/tooltip.ts +++ b/src/composables/tooltip.ts @@ -1,14 +1,17 @@ import { - h, - render, defineComponent, Slot, - Fragment, + shallowRef, + createApp, + onUnmounted, + type App, type DefineComponent, } from "vue"; export function createTooltipTemplate<Params extends object>() { let slot: Slot | undefined; + let app: App<Element> | undefined; + const props = shallowRef<Params>(); const define = defineComponent({ setup(_, { slots }) { @@ -20,22 +23,33 @@ export function createTooltipTemplate<Params extends object>() { new (): { $slots: { default(_: Params): any } }; }; - const formatter = (params: Params): HTMLElement[] => { + const formatter = (params: Params) => { + props.value = params; + if (!slot) { throw new Error( `[vue-echarts] Failed to find the definition of tooltip template`, ); } - const container = document.createElement("div"); - - const vnode = h(Fragment, null, slot(params)); - // console.log(params); - - render(vnode, container); + if (!app) { + app = createApp({ + // root component is just a render function + render() { + // call the slot function with your props + // return slot!(props); + return slot!(props.value); + }, + }); + app.mount(document.createElement("div")); + } - return Array.from(container.children) as HTMLElement[]; + return app._container!.innerHTML; }; + onUnmounted(() => { + app?.unmount(); + }); + return { define, formatter }; } From 89743cf67389799fea48b7f906acfa1274098955 Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Sun, 8 Jun 2025 22:26:10 +0800 Subject: [PATCH 05/27] feat: use pie chart as tooltip --- demo/examples/Example.vue | 2 +- demo/examples/LineChart.vue | 65 ++++++++++++++++++++----------------- package.json | 2 +- src/composables/tooltip.ts | 5 +-- 4 files changed, 38 insertions(+), 36 deletions(-) diff --git a/demo/examples/Example.vue b/demo/examples/Example.vue index 88fa11e..3e36779 100644 --- a/demo/examples/Example.vue +++ b/demo/examples/Example.vue @@ -43,7 +43,7 @@ defineProps({ width: fit-content; margin: 2em auto; - .echarts { + > .echarts { width: calc(60vw + 4em); height: 360px; max-width: 720px; diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue index 4fdd294..9febc2b 100644 --- a/demo/examples/LineChart.vue +++ b/demo/examples/LineChart.vue @@ -1,6 +1,6 @@ -<script setup lang="ts"> +<script setup> import { use } from "echarts/core"; -import { LineChart } from "echarts/charts"; +import { LineChart, PieChart } from "echarts/charts"; import { GridComponent, DatasetComponent, @@ -18,21 +18,22 @@ use([ LegendComponent, LineChart, TooltipComponent, + PieChart, ]); -type MyParams = { - seriesName: string; - seriesIndex: number; - data: number[]; - marker: string; -}[]; -const { define: DefineTooltip, formatter } = createTooltipTemplate<MyParams>(); +const { define: DefineTooltip, formatter } = createTooltipTemplate(); const option = shallowRef({ legend: { top: 20 }, tooltip: { trigger: "axis", - formatter, + formatter: (params) => { + const source = [params[0].dimensionNames, params[0].data]; + const dataset = { source }; + const option = { ...pieOption, dataset }; + option.series[0].label.formatter = params[0].name; + return formatter(option); + }, }, dataset: { source: [ @@ -72,6 +73,23 @@ const option = shallowRef({ }, ], }); + +const pieOption = { + animation: false, + series: [ + { + type: "pie", + radius: ["60%", "100%"], + seriesLayoutBy: "row", + itemStyle: { + borderRadius: 5, + borderColor: "#fff", + borderWidth: 2, + }, + label: { position: "center" }, + }, + ], +}; </script> <template> @@ -81,26 +99,13 @@ const option = shallowRef({ desc="(with component rendered tooltip)" > <v-chart :option="option" autoresize /> - <!-- TODO: use a Pie Chart as tooltip --> - <define-tooltip v-slot="params"> - <b>Custom Tooltip</b> - <table> - <thead> - <tr> - <th>Product</th> - <th>Value</th> - </tr> - </thead> - <tbody> - <tr v-for="(item, i) in params" :key="i"> - <td> - <span v-html="item.marker" /> - {{ item.seriesName }} - </td> - <td>{{ item.data[i + 1] }}</td> - </tr> - </tbody> - </table> + <define-tooltip v-slot="opt"> + <v-chart + :style="{ width: '100px', height: '100px' }" + :option="opt" + :update-options="{ notMerge: false }" + autoresize + /> </define-tooltip> </v-example> </template> diff --git a/package.json b/package.json index e11bc79..69142bd 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "docs": "node ./scripts/docs.mjs", "prepublishOnly": "pnpm run typecheck && pnpm run dev:typecheck && pnpm run build && publint" }, - "packageManager": "pnpm@10.11.0", + "packageManager": "pnpm@10.11.1", "type": "module", "main": "dist/index.js", "unpkg": "dist/index.min.js", diff --git a/src/composables/tooltip.ts b/src/composables/tooltip.ts index 63eaac1..bc3a804 100644 --- a/src/composables/tooltip.ts +++ b/src/composables/tooltip.ts @@ -34,17 +34,14 @@ export function createTooltipTemplate<Params extends object>() { if (!app) { app = createApp({ - // root component is just a render function render() { - // call the slot function with your props - // return slot!(props); return slot!(props.value); }, }); app.mount(document.createElement("div")); } - return app._container!.innerHTML; + return app._container!; }; onUnmounted(() => { From 585279ef50bd3efb74132a348e62ccab72acd04c Mon Sep 17 00:00:00 2001 From: Yue JIN <yjin@nustarnuclear.com> Date: Mon, 9 Jun 2025 15:06:48 +0800 Subject: [PATCH 06/27] feat: switch to createVNode The limitation is that the tooltip detached from the current component tree, not provide/inject will try teleport next --- demo/examples/LineChart.vue | 1 - src/composables/tooltip.ts | 36 ++++++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue index 9febc2b..74c388f 100644 --- a/demo/examples/LineChart.vue +++ b/demo/examples/LineChart.vue @@ -75,7 +75,6 @@ const option = shallowRef({ }); const pieOption = { - animation: false, series: [ { type: "pie", diff --git a/src/composables/tooltip.ts b/src/composables/tooltip.ts index bc3a804..3e438bd 100644 --- a/src/composables/tooltip.ts +++ b/src/composables/tooltip.ts @@ -2,16 +2,25 @@ import { defineComponent, Slot, shallowRef, - createApp, + createVNode, + render, onUnmounted, - type App, + getCurrentInstance, type DefineComponent, } from "vue"; export function createTooltipTemplate<Params extends object>() { let slot: Slot | undefined; - let app: App<Element> | undefined; + let container: HTMLElement | undefined; const props = shallowRef<Params>(); + const internal = getCurrentInstance(); + + if (!internal) { + throw new Error( + `[vue-echarts] createTooltipTemplate must be used in a setup function`, + ); + } + const { appContext } = internal; const define = defineComponent({ setup(_, { slots }) { @@ -32,20 +41,27 @@ export function createTooltipTemplate<Params extends object>() { ); } - if (!app) { - app = createApp({ - render() { - return slot!(props.value); + if (!container) { + const component = defineComponent({ + setup() { + return () => slot!(props.value); }, }); - app.mount(document.createElement("div")); + const vnode = createVNode(component); + vnode.appContext = appContext; + + container = document.createElement("div"); + render(vnode, container); } - return app._container!; + return container; }; onUnmounted(() => { - app?.unmount(); + if (container) { + render(null, container); + container.remove(); + } }); return { define, formatter }; From c6bab0d892c9a339e491ff70482946d884a852d7 Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Mon, 9 Jun 2025 23:00:31 +0800 Subject: [PATCH 07/27] feat: try component with teleport --- demo/examples/LineChart.vue | 14 ++++---- package.json | 2 +- src/ECharts.ts | 1 + src/EChartsTooltip.ts | 30 ++++++++++++++++ src/composables/tooltip.ts | 68 ------------------------------------- 5 files changed, 38 insertions(+), 77 deletions(-) create mode 100644 src/EChartsTooltip.ts delete mode 100644 src/composables/tooltip.ts diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue index 74c388f..4738daf 100644 --- a/demo/examples/LineChart.vue +++ b/demo/examples/LineChart.vue @@ -7,9 +7,8 @@ import { LegendComponent, TooltipComponent, } from "echarts/components"; -import { shallowRef } from "vue"; -import VChart from "../../src/ECharts"; -import { createTooltipTemplate } from "../../src/composables/tooltip"; +import { useTemplateRef, shallowRef } from "vue"; +import VChart, { EChartsTooltip as VChartTooltip } from "../../src/ECharts"; import VExample from "./Example.vue"; use([ @@ -21,7 +20,7 @@ use([ PieChart, ]); -const { define: DefineTooltip, formatter } = createTooltipTemplate(); +const tooltipRef = useTemplateRef("tooltipRef"); const option = shallowRef({ legend: { top: 20 }, @@ -32,7 +31,7 @@ const option = shallowRef({ const dataset = { source }; const option = { ...pieOption, dataset }; option.series[0].label.formatter = params[0].name; - return formatter(option); + return tooltipRef.value?.formatter(option); }, }, dataset: { @@ -98,13 +97,12 @@ const pieOption = { desc="(with component rendered tooltip)" > <v-chart :option="option" autoresize /> - <define-tooltip v-slot="opt"> + <v-chart-tooltip ref="tooltipRef" v-slot="opt"> <v-chart :style="{ width: '100px', height: '100px' }" :option="opt" - :update-options="{ notMerge: false }" autoresize /> - </define-tooltip> + </v-chart-tooltip> </v-example> </template> diff --git a/package.json b/package.json index 69142bd..d3c2944 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "docs": "node ./scripts/docs.mjs", "prepublishOnly": "pnpm run typecheck && pnpm run dev:typecheck && pnpm run build && publint" }, - "packageManager": "pnpm@10.11.1", + "packageManager": "pnpm@10.12.1", "type": "module", "main": "dist/index.js", "unpkg": "dist/index.min.js", diff --git a/src/ECharts.ts b/src/ECharts.ts index 66e2646..97b0ecc 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -48,6 +48,7 @@ export const INIT_OPTIONS_KEY: InjectionKey<InitOptionsInjection> = Symbol(); export const UPDATE_OPTIONS_KEY: InjectionKey<UpdateOptionsInjection> = Symbol(); export { LOADING_OPTIONS_KEY } from "./composables"; +export { default as EChartsTooltip } from "./EChartsTooltip"; export default defineComponent({ name: "echarts", diff --git a/src/EChartsTooltip.ts b/src/EChartsTooltip.ts new file mode 100644 index 0000000..932e3ce --- /dev/null +++ b/src/EChartsTooltip.ts @@ -0,0 +1,30 @@ +import { defineComponent, shallowRef, onBeforeUnmount, h, Teleport } from "vue"; + +export default defineComponent({ + name: "VChartTooltip", + methods: {} as { formatter: (params: any) => HTMLDivElement | undefined }, + setup(_, { slots, expose }) { + const container = document?.createElement("div"); + const initialized = shallowRef(false); + const state = shallowRef<any>(); + + function formatter(params: any) { + initialized.value = true; + state.value = params; + return container; + } + + onBeforeUnmount(() => { + container?.remove(); + }); + + expose({ formatter }); + + return () => { + const slotContent = initialized.value + ? slots.default?.(state.value) + : undefined; + return h(Teleport as any, { to: container }, slotContent); + }; + }, +}); diff --git a/src/composables/tooltip.ts b/src/composables/tooltip.ts deleted file mode 100644 index 3e438bd..0000000 --- a/src/composables/tooltip.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - defineComponent, - Slot, - shallowRef, - createVNode, - render, - onUnmounted, - getCurrentInstance, - type DefineComponent, -} from "vue"; - -export function createTooltipTemplate<Params extends object>() { - let slot: Slot | undefined; - let container: HTMLElement | undefined; - const props = shallowRef<Params>(); - const internal = getCurrentInstance(); - - if (!internal) { - throw new Error( - `[vue-echarts] createTooltipTemplate must be used in a setup function`, - ); - } - const { appContext } = internal; - - const define = defineComponent({ - setup(_, { slots }) { - return () => { - slot = slots.default; - }; - }, - }) as DefineComponent & { - new (): { $slots: { default(_: Params): any } }; - }; - - const formatter = (params: Params) => { - props.value = params; - - if (!slot) { - throw new Error( - `[vue-echarts] Failed to find the definition of tooltip template`, - ); - } - - if (!container) { - const component = defineComponent({ - setup() { - return () => slot!(props.value); - }, - }); - const vnode = createVNode(component); - vnode.appContext = appContext; - - container = document.createElement("div"); - render(vnode, container); - } - - return container; - }; - - onUnmounted(() => { - if (container) { - render(null, container); - container.remove(); - } - }); - - return { define, formatter }; -} From 2072c3b1841ebf46d6b70e1f9b014ea0b955b775 Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Sat, 21 Jun 2025 23:25:32 +0800 Subject: [PATCH 08/27] wip --- demo/data/line.js | 49 +++++++++++++++++ demo/examples/LineChart.vue | 107 +++++++++++------------------------- src/ECharts.ts | 25 ++++++--- src/EChartsTooltip.ts | 30 ---------- src/composables/tooltip.ts | 71 ++++++++++++++++++++++++ src/utils.ts | 6 ++ 6 files changed, 174 insertions(+), 114 deletions(-) create mode 100644 demo/data/line.js delete mode 100644 src/EChartsTooltip.ts create mode 100644 src/composables/tooltip.ts diff --git a/demo/data/line.js b/demo/data/line.js new file mode 100644 index 0000000..631dec4 --- /dev/null +++ b/demo/data/line.js @@ -0,0 +1,49 @@ +export default function getData() { + return { + legend: { top: 20 }, + tooltip: { + trigger: "axis", + }, + dataset: { + source: [ + ["product", "2012", "2013", "2014", "2015", "2016", "2017"], + ["Milk Tea", 56.5, 82.1, 88.7, 70.1, 53.4, 85.1], + ["Matcha Latte", 51.1, 51.4, 55.1, 53.3, 73.8, 68.7], + ["Cheese Cocoa", 40.1, 62.2, 69.5, 36.4, 45.2, 32.5], + ["Walnut Brownie", 25.2, 37.1, 41.2, 18, 33.9, 49.1], + ], + }, + xAxis: { + type: "category", + triggerEvent: true, + tooltip: { show: true }, + }, + yAxis: {}, + series: [ + { + type: "line", + smooth: true, + seriesLayoutBy: "row", + emphasis: { focus: "series" }, + }, + { + type: "line", + smooth: true, + seriesLayoutBy: "row", + emphasis: { focus: "series" }, + }, + { + type: "line", + smooth: true, + seriesLayoutBy: "row", + emphasis: { focus: "series" }, + }, + { + type: "line", + smooth: true, + seriesLayoutBy: "row", + emphasis: { focus: "series" }, + }, + ], + }; +} diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue index 4738daf..acf7242 100644 --- a/demo/examples/LineChart.vue +++ b/demo/examples/LineChart.vue @@ -7,9 +7,10 @@ import { LegendComponent, TooltipComponent, } from "echarts/components"; -import { useTemplateRef, shallowRef } from "vue"; -import VChart, { EChartsTooltip as VChartTooltip } from "../../src/ECharts"; +import { shallowRef } from "vue"; +import VChart from "../../src/ECharts"; import VExample from "./Example.vue"; +import getData from "../data/line"; use([ DatasetComponent, @@ -20,74 +21,27 @@ use([ PieChart, ]); -const tooltipRef = useTemplateRef("tooltipRef"); +const option = shallowRef(getData()); -const option = shallowRef({ - legend: { top: 20 }, - tooltip: { - trigger: "axis", - formatter: (params) => { - const source = [params[0].dimensionNames, params[0].data]; - const dataset = { source }; - const option = { ...pieOption, dataset }; - option.series[0].label.formatter = params[0].name; - return tooltipRef.value?.formatter(option); - }, - }, - dataset: { - source: [ - ["product", "2012", "2013", "2014", "2015", "2016", "2017"], - ["Milk Tea", 56.5, 82.1, 88.7, 70.1, 53.4, 85.1], - ["Matcha Latte", 51.1, 51.4, 55.1, 53.3, 73.8, 68.7], - ["Cheese Cocoa", 40.1, 62.2, 69.5, 36.4, 45.2, 32.5], - ["Walnut Brownie", 25.2, 37.1, 41.2, 18, 33.9, 49.1], - ], - }, - xAxis: { type: "category" }, - yAxis: {}, - series: [ - { - type: "line", - smooth: true, - seriesLayoutBy: "row", - emphasis: { focus: "series" }, - }, - { - type: "line", - smooth: true, - seriesLayoutBy: "row", - emphasis: { focus: "series" }, - }, - { - type: "line", - smooth: true, - seriesLayoutBy: "row", - emphasis: { focus: "series" }, - }, - { - type: "line", - smooth: true, - seriesLayoutBy: "row", - emphasis: { focus: "series" }, - }, - ], -}); - -const pieOption = { - series: [ - { - type: "pie", - radius: ["60%", "100%"], - seriesLayoutBy: "row", - itemStyle: { - borderRadius: 5, - borderColor: "#fff", - borderWidth: 2, +function getPieOption(params) { + const option = { + dataset: { source: [params[0].dimensionNames, params[0].data] }, + series: [ + { + type: "pie", + radius: ["60%", "100%"], + seriesLayoutBy: "row", + itemStyle: { + borderRadius: 5, + borderColor: "#fff", + borderWidth: 2, + }, + label: { position: "center", formatter: params[0].name }, }, - label: { position: "center" }, - }, - ], -}; + ], + }; + return option; +} </script> <template> @@ -96,13 +50,14 @@ const pieOption = { title="Line chart" desc="(with component rendered tooltip)" > - <v-chart :option="option" autoresize /> - <v-chart-tooltip ref="tooltipRef" v-slot="opt"> - <v-chart - :style="{ width: '100px', height: '100px' }" - :option="opt" - autoresize - /> - </v-chart-tooltip> + <v-chart :option="option" autoresize> + <template #tooltip="{ params }"> + <v-chart + :style="{ width: '100px', height: '100px' }" + :option="getPieOption(params)" + autoresize + /> + </template> + </v-chart> </v-example> </template> diff --git a/src/ECharts.ts b/src/ECharts.ts index 97b0ecc..cb79ef6 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -40,6 +40,7 @@ import type { import type { EChartsElement } from "./wc"; import "./style.css"; +import { useTooltip } from "./composables/tooltip"; const wcRegistered = register(); @@ -48,7 +49,6 @@ export const INIT_OPTIONS_KEY: InjectionKey<InitOptionsInjection> = Symbol(); export const UPDATE_OPTIONS_KEY: InjectionKey<UpdateOptionsInjection> = Symbol(); export { LOADING_OPTIONS_KEY } from "./composables"; -export { default as EChartsTooltip } from "./EChartsTooltip"; export default defineComponent({ name: "echarts", @@ -66,7 +66,7 @@ export default defineComponent({ }, emits: {} as unknown as Emits, inheritAttrs: false, - setup(props, { attrs, expose }) { + setup(props, { attrs, expose, slots }) { const root = shallowRef<EChartsElement>(); const chart = shallowRef<EChartsType>(); const manualOption = shallowRef<Option>(); @@ -175,6 +175,7 @@ export default defineComponent({ function commit() { const opt = option || realOption.value; if (opt) { + mutateOption(opt); instance.setOption(opt, realUpdateOptions.value); } } @@ -205,6 +206,7 @@ export default defineComponent({ if (!chart.value) { init(option); } else { + mutateOption(option); chart.value.setOption(option, updateOptions || {}); } }; @@ -235,6 +237,7 @@ export default defineComponent({ if (!chart.value) { init(); } else { + mutateOption(option); chart.value.setOption(option, { // mutating `option` will lead to `notMerge: false` and // replacing it with new reference will lead to `notMerge: true` @@ -275,6 +278,8 @@ export default defineComponent({ useAutoresize(chart, autoresize, root); + const { teleportedSlots, mutateOption } = useTooltip(slots); + onMounted(() => { init(); }); @@ -303,11 +308,15 @@ export default defineComponent({ // This type casting ensures TypeScript correctly types the exposed members // that will be available when using this component. return (() => - h(TAG_NAME, { - ...nonEventAttrs.value, - ...nativeListeners, - ref: root, - class: ["echarts", ...(nonEventAttrs.value.class || [])], - })) as unknown as typeof exposed & PublicMethods; + h( + TAG_NAME, + { + ...nonEventAttrs.value, + ...nativeListeners, + ref: root, + class: ["echarts", nonEventAttrs.value.class], + }, + teleportedSlots(), + )) as unknown as typeof exposed & PublicMethods; }, }); diff --git a/src/EChartsTooltip.ts b/src/EChartsTooltip.ts deleted file mode 100644 index 932e3ce..0000000 --- a/src/EChartsTooltip.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { defineComponent, shallowRef, onBeforeUnmount, h, Teleport } from "vue"; - -export default defineComponent({ - name: "VChartTooltip", - methods: {} as { formatter: (params: any) => HTMLDivElement | undefined }, - setup(_, { slots, expose }) { - const container = document?.createElement("div"); - const initialized = shallowRef(false); - const state = shallowRef<any>(); - - function formatter(params: any) { - initialized.value = true; - state.value = params; - return container; - } - - onBeforeUnmount(() => { - container?.remove(); - }); - - expose({ formatter }); - - return () => { - const slotContent = initialized.value - ? slots.default?.(state.value) - : undefined; - return h(Teleport as any, { to: container }, slotContent); - }; - }, -}); diff --git a/src/composables/tooltip.ts b/src/composables/tooltip.ts new file mode 100644 index 0000000..1540c07 --- /dev/null +++ b/src/composables/tooltip.ts @@ -0,0 +1,71 @@ +import { + type ShallowRef, + Slots, + Teleport, + h, + onBeforeUnmount, + shallowRef, +} from "vue"; +import { parseProperties } from "../utils"; +import { Option } from "src/types"; + +export function useTooltip(slots: Slots) { + const tooltipSlots = Object.fromEntries( + Object.entries(slots).filter( + ([key]) => key === "tooltip" || key.startsWith("tooltip:"), + ), + ); + const initialized: Record<string, ShallowRef<boolean>> = {}; + const params: Record<string, ShallowRef<any>> = {}; + const containers: Record<string, HTMLElement> = {}; + const properties: Record<string, string[]> = {}; + + Object.keys(tooltipSlots).forEach((key) => { + initialized[key] = shallowRef(false); + params[key] = shallowRef(null); + properties[key] = + key === "tooltip" ? [] : parseProperties(key.replace("tooltip:", "")); + containers[key] = document?.createElement("div"); + }); + + const teleportedSlots = () => + Object.keys(tooltipSlots).map((key) => { + const slot = tooltipSlots[key]; + const slotContent = initialized[key].value + ? slot?.({ params: params[key].value }) + : undefined; + return h(Teleport as any, { to: containers[key] }, slotContent); + }); + + function mutateOption(option: Option) { + Object.keys(tooltipSlots).forEach((key) => { + let current: any = option; + if (key !== "tooltip") { + for (const prop of properties[key]) { + current = current[prop]; + if (current == null) { + console.warn(`[vue-echarts] "option.${key}" is not defined`); + return; + } + } + } + current.tooltip ??= {}; + current.tooltip.formatter = (p: any) => { + initialized[key].value = true; + params[key].value = p; + return containers[key]; + }; + }); + } + + onBeforeUnmount(() => { + Object.values(containers).forEach((container) => { + container?.remove(); + }); + }); + + return { + teleportedSlots, + mutateOption, + }; +} diff --git a/src/utils.ts b/src/utils.ts index 6dec3f5..bdb01cb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -29,3 +29,9 @@ const isFunction = (val: unknown): val is Function => typeof val === "function"; export function toValue<T>(source: MaybeRefOrGetter<T>): T { return isFunction(source) ? source() : unref(source); } + +export function parseProperties(path: string) { + // Convert bracket notation to dot notation and split the path + // "[2].series[0].data" -> ["2", "series", "0", "data"] + return path.replace(/\[(\w+)\]/g, ".$1").split("."); +} From 177e875a7d39c11094c77aff56a2d3abf09887da Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Sun, 22 Jun 2025 12:09:37 +0800 Subject: [PATCH 09/27] add xAxis example --- demo/examples/LineChart.vue | 3 +++ package.json | 2 +- pnpm-lock.yaml | 31 +++++++++++++++++++------------ src/ECharts.ts | 5 ++++- src/composables/tooltip.ts | 26 +++++++++++++------------- 5 files changed, 40 insertions(+), 27 deletions(-) diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue index acf7242..530c42f 100644 --- a/demo/examples/LineChart.vue +++ b/demo/examples/LineChart.vue @@ -58,6 +58,9 @@ function getPieOption(params) { autoresize /> </template> + <template #tooltip:xAxis="{ params }"> + Year: <b>{{ params.name }}</b> + </template> </v-chart> </v-example> </template> diff --git a/package.json b/package.json index d3c2944..a45bddd 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@vue/tsconfig": "^0.7.0", "@vueuse/core": "^13.1.0", "comment-mark": "^2.0.1", - "echarts": "^5.5.1", + "echarts": "^5.6.0", "echarts-gl": "^2.0.9", "echarts-liquidfill": "^3.1.0", "esbuild-wasm": "^0.23.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 192fa13..64e3b65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,14 +39,14 @@ importers: specifier: ^2.0.1 version: 2.0.1 echarts: - specifier: ^5.5.1 - version: 5.5.1 + specifier: ^5.6.0 + version: 5.6.0 echarts-gl: specifier: ^2.0.9 - version: 2.0.9(echarts@5.5.1) + version: 2.0.9(echarts@5.6.0) echarts-liquidfill: specifier: ^3.1.0 - version: 3.1.0(echarts@5.5.1) + version: 3.1.0(echarts@5.6.0) esbuild-wasm: specifier: ^0.23.0 version: 0.23.0 @@ -820,8 +820,8 @@ packages: peerDependencies: echarts: ^5.0.1 - echarts@5.5.1: - resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==} + echarts@5.6.0: + resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1511,6 +1511,9 @@ packages: zrender@5.6.0: resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==} + zrender@5.6.1: + resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} + snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} @@ -2134,20 +2137,20 @@ snapshots: eastasianwidth@0.2.0: {} - echarts-gl@2.0.9(echarts@5.5.1): + echarts-gl@2.0.9(echarts@5.6.0): dependencies: claygl: 1.3.0 - echarts: 5.5.1 + echarts: 5.6.0 zrender: 5.6.0 - echarts-liquidfill@3.1.0(echarts@5.5.1): + echarts-liquidfill@3.1.0(echarts@5.6.0): dependencies: - echarts: 5.5.1 + echarts: 5.6.0 - echarts@5.5.1: + echarts@5.6.0: dependencies: tslib: 2.3.0 - zrender: 5.6.0 + zrender: 5.6.1 emoji-regex@8.0.0: {} @@ -2819,3 +2822,7 @@ snapshots: zrender@5.6.0: dependencies: tslib: 2.3.0 + + zrender@5.6.1: + dependencies: + tslib: 2.3.0 diff --git a/src/ECharts.ts b/src/ECharts.ts index cb79ef6..9dda1c9 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -24,7 +24,7 @@ import { import { isOn, omitOn, toValue } from "./utils"; import { register, TAG_NAME } from "./wc"; -import type { PropType, InjectionKey } from "vue"; +import type { PropType, InjectionKey, SlotsType } from "vue"; import type { EChartsType, SetOptionType, @@ -65,6 +65,9 @@ export default defineComponent({ ...loadingProps, }, emits: {} as unknown as Emits, + slots: Object as SlotsType< + Record<"tooltip" | `tooltip:${string}`, { params: any }> + >, inheritAttrs: false, setup(props, { attrs, expose, slots }) { const root = shallowRef<EChartsElement>(); diff --git a/src/composables/tooltip.ts b/src/composables/tooltip.ts index 1540c07..8c28c85 100644 --- a/src/composables/tooltip.ts +++ b/src/composables/tooltip.ts @@ -1,13 +1,13 @@ import { - type ShallowRef, - Slots, - Teleport, h, - onBeforeUnmount, + Teleport, + onUnmounted, shallowRef, + type ShallowRef, + type Slots, } from "vue"; import { parseProperties } from "../utils"; -import { Option } from "src/types"; +import type { Option } from "src/types"; export function useTooltip(slots: Slots) { const tooltipSlots = Object.fromEntries( @@ -40,13 +40,13 @@ export function useTooltip(slots: Slots) { function mutateOption(option: Option) { Object.keys(tooltipSlots).forEach((key) => { let current: any = option; - if (key !== "tooltip") { - for (const prop of properties[key]) { - current = current[prop]; - if (current == null) { - console.warn(`[vue-echarts] "option.${key}" is not defined`); - return; - } + for (const prop of properties[key]) { + current = current[prop]; + if (current == null) { + console.warn( + `[vue-echarts] "option.${key.replace("tooltip:", "")}" is not defined`, + ); + return; } } current.tooltip ??= {}; @@ -58,7 +58,7 @@ export function useTooltip(slots: Slots) { }); } - onBeforeUnmount(() => { + onUnmounted(() => { Object.values(containers).forEach((container) => { container?.remove(); }); From 117df61ddee9f86c740c95337605fc684f805a9a Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Sun, 22 Jun 2025 19:46:37 +0800 Subject: [PATCH 10/27] refactor with shallowReactive --- src/composables/tooltip.ts | 64 ++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/src/composables/tooltip.ts b/src/composables/tooltip.ts index 8c28c85..3623f80 100644 --- a/src/composables/tooltip.ts +++ b/src/composables/tooltip.ts @@ -1,11 +1,4 @@ -import { - h, - Teleport, - onUnmounted, - shallowRef, - type ShallowRef, - type Slots, -} from "vue"; +import { h, Teleport, onUnmounted, shallowReactive, type Slots } from "vue"; import { parseProperties } from "../utils"; import type { Option } from "src/types"; @@ -15,27 +8,34 @@ export function useTooltip(slots: Slots) { ([key]) => key === "tooltip" || key.startsWith("tooltip:"), ), ); - const initialized: Record<string, ShallowRef<boolean>> = {}; - const params: Record<string, ShallowRef<any>> = {}; - const containers: Record<string, HTMLElement> = {}; - const properties: Record<string, string[]> = {}; - - Object.keys(tooltipSlots).forEach((key) => { - initialized[key] = shallowRef(false); - params[key] = shallowRef(null); - properties[key] = - key === "tooltip" ? [] : parseProperties(key.replace("tooltip:", "")); - containers[key] = document?.createElement("div"); - }); + const detachedRoot = document?.createElement("div"); + const containers = shallowReactive<Record<string, HTMLElement>>({}); + const initialized = shallowReactive<Record<string, boolean>>({}); + const params = shallowReactive<Record<string, any>>({}); + const properties = Object.fromEntries( + Object.keys(tooltipSlots).map((key) => [ + key, + key === "tooltip" ? [] : parseProperties(key.replace("tooltip:", "")), + ]), + ); - const teleportedSlots = () => - Object.keys(tooltipSlots).map((key) => { - const slot = tooltipSlots[key]; - const slotContent = initialized[key].value - ? slot?.({ params: params[key].value }) - : undefined; - return h(Teleport as any, { to: containers[key] }, slotContent); - }); + const teleportedSlots = () => { + return h( + Teleport as any, + { to: detachedRoot }, + Object.keys(tooltipSlots).map((key) => { + const slot = tooltipSlots[key]; + const slotContent = initialized[key] + ? slot?.({ params: params[key] }) + : undefined; + return h( + "div", + { ref: (el) => (containers[key] = el as HTMLElement) }, + slotContent, + ); + }), + ); + }; function mutateOption(option: Option) { Object.keys(tooltipSlots).forEach((key) => { @@ -51,17 +51,15 @@ export function useTooltip(slots: Slots) { } current.tooltip ??= {}; current.tooltip.formatter = (p: any) => { - initialized[key].value = true; - params[key].value = p; + initialized[key] = true; + params[key] = p; return containers[key]; }; }); } onUnmounted(() => { - Object.values(containers).forEach((container) => { - container?.remove(); - }); + detachedRoot?.remove(); }); return { From 2dccfdb80ab99cef575c4a8d7b7c808ac27582ac Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Mon, 23 Jun 2025 23:27:43 +0800 Subject: [PATCH 11/27] Support dynamic slot --- demo/data/line.js | 7 ++- demo/examples/LineChart.vue | 15 ++++- src/ECharts.ts | 47 +++++++++++++-- src/composables/tooltip.ts | 117 ++++++++++++++++++++++-------------- 4 files changed, 132 insertions(+), 54 deletions(-) diff --git a/demo/data/line.js b/demo/data/line.js index 631dec4..64371c5 100644 --- a/demo/data/line.js +++ b/demo/data/line.js @@ -16,9 +16,12 @@ export default function getData() { xAxis: { type: "category", triggerEvent: true, - tooltip: { show: true }, + tooltip: { show: true, formatter: "" }, + }, + yAxis: { + triggerEvent: true, + tooltip: { show: true, formatter: "" }, }, - yAxis: {}, series: [ { type: "line", diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue index 530c42f..b1404a5 100644 --- a/demo/examples/LineChart.vue +++ b/demo/examples/LineChart.vue @@ -22,6 +22,7 @@ use([ ]); const option = shallowRef(getData()); +const axis = shallowRef("xAxis"); function getPieOption(params) { const option = { @@ -58,9 +59,19 @@ function getPieOption(params) { autoresize /> </template> - <template #tooltip:xAxis="{ params }"> - Year: <b>{{ params.name }}</b> + <template #[`tooltip:${axis}`]="{ params }"> + {{ axis === "xAxis" ? "Year" : "Value" }}: + <b>{{ params.name }}</b> </template> </v-chart> + <template #extra> + <p class="actions"> + Custom tooltip on + <select v-model="axis"> + <option value="xAxis">X Axis</option> + <option value="yAxis">Y Axis</option> + </select> + </p> + </template> </v-example> </template> diff --git a/src/ECharts.ts b/src/ECharts.ts index 9dda1c9..743f7a9 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -178,8 +178,16 @@ export default defineComponent({ function commit() { const opt = option || realOption.value; if (opt) { - mutateOption(opt); - instance.setOption(opt, realUpdateOptions.value); + const tooltipOption = createTooltipOption(); + instance.setOption(opt, { + ...realUpdateOptions.value, + lazyUpdate: true, + }); + instance.setOption(tooltipOption, { + ...realUpdateOptions.value, + notMerge: false, + silent: true, + }); } } @@ -209,8 +217,16 @@ export default defineComponent({ if (!chart.value) { init(option); } else { - mutateOption(option); - chart.value.setOption(option, updateOptions || {}); + const tooltipOption = createTooltipOption(); + chart.value.setOption(option, { + ...(updateOptions || {}), + lazyUpdate: true, + }); + chart.value.setOption(tooltipOption, { + ...(updateOptions || {}), + notMerge: false, + silent: true, + }); } }; @@ -240,12 +256,18 @@ export default defineComponent({ if (!chart.value) { init(); } else { - mutateOption(option); + const tooltipOption = createTooltipOption(); chart.value.setOption(option, { // mutating `option` will lead to `notMerge: false` and // replacing it with new reference will lead to `notMerge: true` notMerge: option !== oldOption, ...realUpdateOptions.value, + lazyUpdate: true, + }); + chart.value.setOption(tooltipOption, { + ...realUpdateOptions.value, + notMerge: false, + silent: true, }); } }, @@ -281,7 +303,20 @@ export default defineComponent({ useAutoresize(chart, autoresize, root); - const { teleportedSlots, mutateOption } = useTooltip(slots); + const { teleportedSlots, createTooltipOption } = useTooltip(slots, () => { + if (!manualUpdate.value && props.option && chart.value) { + const tooltipOption = createTooltipOption(); + chart.value.setOption(props.option, { + ...realUpdateOptions.value, + lazyUpdate: true, + }); + chart.value.setOption(tooltipOption, { + ...realUpdateOptions.value, + notMerge: false, + silent: true, + }); + } + }); onMounted(() => { init(); diff --git a/src/composables/tooltip.ts b/src/composables/tooltip.ts index 3623f80..5724cb2 100644 --- a/src/composables/tooltip.ts +++ b/src/composables/tooltip.ts @@ -1,69 +1,98 @@ -import { h, Teleport, onUnmounted, shallowReactive, type Slots } from "vue"; +import { + h, + Teleport, + onUpdated, + onUnmounted, + shallowReactive, + type Slots, +} from "vue"; import { parseProperties } from "../utils"; import type { Option } from "src/types"; -export function useTooltip(slots: Slots) { - const tooltipSlots = Object.fromEntries( - Object.entries(slots).filter( - ([key]) => key === "tooltip" || key.startsWith("tooltip:"), - ), - ); +function isTooltipSlot(key: string) { + return key === "tooltip" || key.startsWith("tooltip:"); +} + +export function useTooltip(slots: Slots, onSlotsChange?: () => void) { const detachedRoot = document?.createElement("div"); const containers = shallowReactive<Record<string, HTMLElement>>({}); const initialized = shallowReactive<Record<string, boolean>>({}); const params = shallowReactive<Record<string, any>>({}); - const properties = Object.fromEntries( - Object.keys(tooltipSlots).map((key) => [ - key, - key === "tooltip" ? [] : parseProperties(key.replace("tooltip:", "")), - ]), - ); + // Teleport the tooltip slots to a detached root const teleportedSlots = () => { return h( Teleport as any, { to: detachedRoot }, - Object.keys(tooltipSlots).map((key) => { - const slot = tooltipSlots[key]; - const slotContent = initialized[key] - ? slot?.({ params: params[key] }) - : undefined; - return h( - "div", - { ref: (el) => (containers[key] = el as HTMLElement) }, - slotContent, - ); - }), + Object.entries(slots) + .filter(([key]) => isTooltipSlot(key)) + .map(([key, slot]) => { + const slotContent = initialized[key] + ? slot?.({ params: params[key] }) + : undefined; + return h( + "div", + { ref: (el) => (containers[key] = el as HTMLElement), name: key }, + slotContent, + ); + }), ); }; - function mutateOption(option: Option) { - Object.keys(tooltipSlots).forEach((key) => { - let current: any = option; - for (const prop of properties[key]) { - current = current[prop]; - if (current == null) { - console.warn( - `[vue-echarts] "option.${key.replace("tooltip:", "")}" is not defined`, - ); - return; - } - } - current.tooltip ??= {}; - current.tooltip.formatter = (p: any) => { - initialized[key] = true; - params[key] = p; - return containers[key]; - }; - }); + // Create a minimal option with component rendered tooltip formatter + function createTooltipOption(): Option { + const option: any = {}; + + Object.keys(slots) + .filter((key) => isTooltipSlot(key)) + .forEach((key) => { + const path = + key === "tooltip" ? [] : parseProperties(key.replace("tooltip:", "")); + let current = option; + path.forEach((k) => { + if (!(k in current)) { + // If the key is a number, create an array, otherwise create an object + current[k] = isNaN(Number(k)) ? {} : []; + } + current = current[k]; + }); + current.tooltip = { + formatter(p: any) { + initialized[key] = true; + params[key] = p; + return containers[key]; + }, + }; + }); + + return option; } + // `slots` is not reactive and cannot be watched + // so we need to watch it manually + let slotNames = Object.keys(slots).filter((key) => isTooltipSlot(key)); + onUpdated(() => { + const newSlotNames = Object.keys(slots).filter((key) => isTooltipSlot(key)); + if (newSlotNames.join() !== slotNames.join()) { + // Clean up params and initialized for removed slots + slotNames.forEach((key) => { + if (!(key in slots)) { + delete params[key]; + delete initialized[key]; + delete containers[key]; + } + }); + slotNames = newSlotNames; + onSlotsChange?.(); + } + }); + onUnmounted(() => { detachedRoot?.remove(); }); return { teleportedSlots, - mutateOption, + createTooltipOption, }; } From 322fda4711b28cf897bc31c0bf102c10c82d73aa Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Tue, 24 Jun 2025 11:17:19 +0800 Subject: [PATCH 12/27] fix: fill empty elements with object in array --- src/composables/tooltip.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/composables/tooltip.ts b/src/composables/tooltip.ts index 5724cb2..ed513be 100644 --- a/src/composables/tooltip.ts +++ b/src/composables/tooltip.ts @@ -49,10 +49,18 @@ export function useTooltip(slots: Slots, onSlotsChange?: () => void) { const path = key === "tooltip" ? [] : parseProperties(key.replace("tooltip:", "")); let current = option; - path.forEach((k) => { + path.forEach((k, index, arr) => { if (!(k in current)) { - // If the key is a number, create an array, otherwise create an object - current[k] = isNaN(Number(k)) ? {} : []; + // If the next key is a number, create an array, otherwise create an object + current[k] = isNaN(Number(arr[index + 1])) ? {} : []; + // fill the non-existent elements with empty objects + if (Array.isArray(current) && !isNaN(Number(k))) { + for (let i = 0; i < Number(k); i++) { + if (current[i] == undefined) { + current[i] = {}; + } + } + } } current = current[k]; }); From 49ad09b8096a86a73eea88755ab269d1641017ed Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Tue, 1 Jul 2025 11:32:14 +0800 Subject: [PATCH 13/27] shallow copy option along the path --- demo/examples/LineChart.vue | 2 +- src/ECharts.ts | 47 +++++-------------------- src/composables/tooltip.ts | 69 ++++++++++++++++++++----------------- src/utils.ts | 7 ++-- 4 files changed, 49 insertions(+), 76 deletions(-) diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue index b1404a5..039e060 100644 --- a/demo/examples/LineChart.vue +++ b/demo/examples/LineChart.vue @@ -59,7 +59,7 @@ function getPieOption(params) { autoresize /> </template> - <template #[`tooltip:${axis}`]="{ params }"> + <template #[`tooltip-${axis}`]="{ params }"> {{ axis === "xAxis" ? "Year" : "Value" }}: <b>{{ params.name }}</b> </template> diff --git a/src/ECharts.ts b/src/ECharts.ts index 743f7a9..a33b504 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -178,16 +178,7 @@ export default defineComponent({ function commit() { const opt = option || realOption.value; if (opt) { - const tooltipOption = createTooltipOption(); - instance.setOption(opt, { - ...realUpdateOptions.value, - lazyUpdate: true, - }); - instance.setOption(tooltipOption, { - ...realUpdateOptions.value, - notMerge: false, - silent: true, - }); + instance.setOption(patchOption(opt), realUpdateOptions.value); } } @@ -217,16 +208,7 @@ export default defineComponent({ if (!chart.value) { init(option); } else { - const tooltipOption = createTooltipOption(); - chart.value.setOption(option, { - ...(updateOptions || {}), - lazyUpdate: true, - }); - chart.value.setOption(tooltipOption, { - ...(updateOptions || {}), - notMerge: false, - silent: true, - }); + chart.value.setOption(patchOption(option), updateOptions); } }; @@ -256,18 +238,11 @@ export default defineComponent({ if (!chart.value) { init(); } else { - const tooltipOption = createTooltipOption(); - chart.value.setOption(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, - lazyUpdate: true, - }); - chart.value.setOption(tooltipOption, { - ...realUpdateOptions.value, - notMerge: false, - silent: true, }); } }, @@ -303,18 +278,12 @@ export default defineComponent({ useAutoresize(chart, autoresize, root); - const { teleportedSlots, createTooltipOption } = useTooltip(slots, () => { + const { teleportedSlots, patchOption } = useTooltip(slots, () => { if (!manualUpdate.value && props.option && chart.value) { - const tooltipOption = createTooltipOption(); - chart.value.setOption(props.option, { - ...realUpdateOptions.value, - lazyUpdate: true, - }); - chart.value.setOption(tooltipOption, { - ...realUpdateOptions.value, - notMerge: false, - silent: true, - }); + chart.value.setOption( + patchOption(props.option), + realUpdateOptions.value, + ); } }); diff --git a/src/composables/tooltip.ts b/src/composables/tooltip.ts index ed513be..9c2e5b8 100644 --- a/src/composables/tooltip.ts +++ b/src/composables/tooltip.ts @@ -6,11 +6,11 @@ import { shallowReactive, type Slots, } from "vue"; -import { parseProperties } from "../utils"; -import type { Option } from "src/types"; +import type { Option } from "../types"; +import { isValidArrayIndex } from "../utils"; function isTooltipSlot(key: string) { - return key === "tooltip" || key.startsWith("tooltip:"); + return key === "tooltip" || key.startsWith("tooltip-"); } export function useTooltip(slots: Slots, onSlotsChange?: () => void) { @@ -39,41 +39,46 @@ export function useTooltip(slots: Slots, onSlotsChange?: () => void) { ); }; - // Create a minimal option with component rendered tooltip formatter - function createTooltipOption(): Option { - const option: any = {}; + // Shallow clone the option along the path and patch the tooltip formatter + function patchOption(src: Option): Option { + const root = { ...src }; Object.keys(slots) .filter((key) => isTooltipSlot(key)) .forEach((key) => { - const path = - key === "tooltip" ? [] : parseProperties(key.replace("tooltip:", "")); - let current = option; - path.forEach((k, index, arr) => { - if (!(k in current)) { - // If the next key is a number, create an array, otherwise create an object - current[k] = isNaN(Number(arr[index + 1])) ? {} : []; - // fill the non-existent elements with empty objects - if (Array.isArray(current) && !isNaN(Number(k))) { - for (let i = 0; i < Number(k); i++) { - if (current[i] == undefined) { - current[i] = {}; - } - } - } + const path = key.split("-"); + path.push(path.shift()!); + let cur: any = root; + + for (let i = 0; i < path.length; i++) { + const seg = path[i]; + const next = cur[seg]; + + if (i < path.length - 1) { + // shallow-clone the link; create empty shell if missing + cur[seg] = next + ? Array.isArray(next) + ? next.slice() + : { ...next } + : isValidArrayIndex(seg) + ? [] + : {}; + cur = cur[seg]; + } else { + // final node = tooltip + cur[seg] = { + ...(next || {}), + formatter(p: any) { + initialized[key] = true; + params[key] = p; + return containers[key]; + }, + }; } - current = current[k]; - }); - current.tooltip = { - formatter(p: any) { - initialized[key] = true; - params[key] = p; - return containers[key]; - }, - }; + } }); - return option; + return root; } // `slots` is not reactive and cannot be watched @@ -101,6 +106,6 @@ export function useTooltip(slots: Slots, onSlotsChange?: () => void) { return { teleportedSlots, - createTooltipOption, + patchOption, }; } diff --git a/src/utils.ts b/src/utils.ts index bdb01cb..546ecca 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -30,8 +30,7 @@ export function toValue<T>(source: MaybeRefOrGetter<T>): T { return isFunction(source) ? source() : unref(source); } -export function parseProperties(path: string) { - // Convert bracket notation to dot notation and split the path - // "[2].series[0].data" -> ["2", "series", "0", "data"] - return path.replace(/\[(\w+)\]/g, ".$1").split("."); +export function isValidArrayIndex(key: string): boolean { + const num = Number(key); + return Number.isInteger(num) && num >= 0 && num < Math.pow(2, 32) - 1; } From 9c2f4313c8106d10188a0bdfa41c39875233799f Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Tue, 1 Jul 2025 15:11:37 +0800 Subject: [PATCH 14/27] ssr friendly --- src/composables/tooltip.ts | 49 +++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/composables/tooltip.ts b/src/composables/tooltip.ts index 9c2e5b8..8b179fb 100644 --- a/src/composables/tooltip.ts +++ b/src/composables/tooltip.ts @@ -3,6 +3,8 @@ import { Teleport, onUpdated, onUnmounted, + onMounted, + shallowRef, shallowReactive, type Slots, } from "vue"; @@ -13,30 +15,35 @@ function isTooltipSlot(key: string) { return key === "tooltip" || key.startsWith("tooltip-"); } -export function useTooltip(slots: Slots, onSlotsChange?: () => void) { - const detachedRoot = document?.createElement("div"); +export function useTooltip(slots: Slots, onSlotsChange: () => void) { + const detachedRoot = + typeof window !== "undefined" ? document.createElement("div") : undefined; const containers = shallowReactive<Record<string, HTMLElement>>({}); const initialized = shallowReactive<Record<string, boolean>>({}); const params = shallowReactive<Record<string, any>>({}); + const isMounted = shallowRef(false); // Teleport the tooltip slots to a detached root const teleportedSlots = () => { - return h( - Teleport as any, - { to: detachedRoot }, - Object.entries(slots) - .filter(([key]) => isTooltipSlot(key)) - .map(([key, slot]) => { - const slotContent = initialized[key] - ? slot?.({ params: params[key] }) - : undefined; - return h( - "div", - { ref: (el) => (containers[key] = el as HTMLElement), name: key }, - slotContent, - ); - }), - ); + // Make tooltip slots client-side only to avoid SSR hydration mismatch + return isMounted.value + ? h( + Teleport as any, + { to: detachedRoot, defer: true }, + Object.entries(slots) + .filter(([key]) => isTooltipSlot(key)) + .map(([key, slot]) => { + const slotContent = initialized[key] + ? slot?.({ params: params[key] }) + : undefined; + return h( + "div", + { ref: (el) => (containers[key] = el as HTMLElement) }, + slotContent, + ); + }), + ) + : undefined; }; // Shallow clone the option along the path and patch the tooltip formatter @@ -96,10 +103,14 @@ export function useTooltip(slots: Slots, onSlotsChange?: () => void) { } }); slotNames = newSlotNames; - onSlotsChange?.(); + onSlotsChange(); } }); + onMounted(() => { + isMounted.value = true; + }); + onUnmounted(() => { detachedRoot?.remove(); }); From 0acb7b2d12c13373492a27e8cc5f38accf4a8ef8 Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Tue, 1 Jul 2025 16:28:06 +0800 Subject: [PATCH 15/27] vibe docs --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++--- README.zh-Hans.md | 56 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9ad752b..ff5db43 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,8 @@ See more examples [here](https://github.com/ecomfe/vue-echarts/tree/main/demo). ECharts' universal interface. Modifying this prop will trigger ECharts' `setOption` method. Read more [here →](https://echarts.apache.org/en/option.html) - > 💡 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. + > [!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` @@ -195,8 +196,7 @@ You can bind events with Vue's `v-on` directive. </template> ``` -> **Note** -> +> [!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: @@ -331,6 +331,62 @@ export default { - `clear` [→](https://echarts.apache.org/en/api.html#echartsInstance.clear) - `dispose` [→](https://echarts.apache.org/en/api.html#echartsInstance.dispose) +### Slots + +Vue-ECharts allows you to define ECharts option's `tooltip.formatter` callbacks via Vue slots instead of defining them in your `option` object. This simplifies custom tooltip rendering using familiar Vue templating. + +**Slot Naming Convention** + +- Slot names begin with `tooltip`, followed by hyphen-separated path segments to the target formatter. +- Each segment corresponds to an `option` property name or an array index (for arrays, use the numeric index). +- The constructed slot name maps directly to the nested `formatter` it overrides. + +**Example mappings**: + +- `tooltip` → `option.tooltip.formatter` +- `tooltip-baseOption` → `option.baseOption.tooltip.formatter` +- `tooltip-xAxis-1` → `option.xAxis[1].tooltip.formatter` +- `tooltip-series-2-data-4` → `option.series[2].data[4].tooltip.formatter` + +<details> +<summary>Usage</summary> + +```vue +<template> + <v-chart :option="chartOptions"> + <!-- Override global tooltip.formatter --> + <template #tooltip="{ params }"> + <table> + <tr> + <th>Series</th> + <th>Value</th> + </tr> + <tr v-for="(s, i) in params" :key="i"> + <td>{{ s.seriesName }}</td> + <td>{{ s.data }}</td> + </tr> + </table> + </template> + + <!-- Override tooltip on xAxis --> + <template #tooltip-xAxis="{ params }"> + <div>X-Axis : {{ params.value }}</div> + </template> + </v-chart> +</template> +``` + +[Example→](https://vue-echarts.dev/#line) + +</details> + +#### Slot Props + +- `params`: The first argument passed to the original [`tooltip.formatter`](https://echarts.apache.org/en/option.html#tooltip.formatter) callback. + +> [!NOTE] +> Slots take precedence over any `tooltip.formatter` defined in `props.option`. If a matching slot is present, the slot's content will render instead of using `option`'s formatter. + ### Static Methods Static methods can be accessed from [`echarts` itself](https://echarts.apache.org/en/api.html#echarts). diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 7bb8289..bd0a1ce 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -331,6 +331,62 @@ export default { - `clear` [→](https://echarts.apache.org/zh/api.html#echartsInstance.clear) - `dispose` [→](https://echarts.apache.org/zh/api.html#echartsInstance.dispose) +### 插槽(Slots) + +Vue-ECharts 允许你通过 Vue 插槽来定义 ECharts 配置中的 `tooltip.formatter` 回调,而无需在 `option` 对象中定义它们。这简化了自定义提示框的渲染,让你可以用熟悉的 Vue 模板语法来编写。 + +**插槽命名约定** + +- 插槽名称以 `tooltip` 开头,后面跟随用连字符分隔的路径片段,用于定位要覆盖的 `formatter`。 +- 每个片段对应 `option` 对象的属性名或数组索引(数组索引使用数字形式)。 +- 拼接后的插槽名称直接映射到要覆盖的嵌套 `formatter`。 + +**示例映射**: + +- `tooltip` → `option.tooltip.formatter` +- `tooltip-baseOption` → `option.baseOption.tooltip.formatter` +- `tooltip-xAxis-1` → `option.xAxis[1].tooltip.formatter` +- `tooltip-series-2-data-4` → `option.series[2].data[4].tooltip.formatter` + +<details> +<summary>用法示例</summary> + +```vue +<template> + <v-chart :option="chartOptions"> + <!-- 覆盖全局 tooltip.formatter --> + <template #tooltip="{ params }"> + <table> + <tr> + <th>系列名称</th> + <th>数值</th> + </tr> + <tr v-for="(item, idx) in params" :key="idx"> + <td>{{ item.seriesName }}</td> + <td>{{ item.data }}</td> + </tr> + </table> + </template> + + <!-- 覆盖x轴 tooltip.formatter --> + <template #tooltip-xAxis="{ params }"> + <div>X 轴: {{ params.value }}</div> + </template> + </v-chart> +</template> +``` + +[Example→](https://vue-echarts.dev/#line) + +</details> + +#### 插槽Props + +- `params`:[`tooltip.formatter`](https://echarts.apache.org/zh/option.html#tooltip.formatter) 回调的第一个参数。 + +> [!NOTE] +> 插槽内容会优先于 `props.option` 中对应的 `tooltip.formatter` 渲染,如果两者同时存在。 + ### 静态方法 静态方法请直接通过 [`echarts` 本身](https://echarts.apache.org/zh/api.html#echarts)进行调用。 From 3556afe81728046fca7a69d900300e567d7240ef Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Tue, 1 Jul 2025 16:43:43 +0800 Subject: [PATCH 16/27] typo --- README.zh-Hans.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.zh-Hans.md b/README.zh-Hans.md index bd0a1ce..18ef12c 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -155,7 +155,8 @@ app.component('v-chart', VueECharts) ECharts 的万能接口。修改这个 prop 会触发 ECharts 实例的 `setOption` 方法。查看[详情 →](https://echarts.apache.org/zh/option.html) - > 💡 在没有指定 `update-options` 时,如果直接修改 `option` 对象而引用保持不变,`setOption` 方法调用时将默认指定 `notMerge: false`;否则,如果为 `option` 绑定一个新的引用,将指定 `notMerge: true`。 + > [!TIP] + > 在没有指定 `update-options` 时,如果直接修改 `option` 对象而引用保持不变,`setOption` 方法调用时将默认指定 `notMerge: false`;否则,如果为 `option` 绑定一个新的引用,将指定 `notMerge: true`。 - `update-options: object` @@ -195,8 +196,7 @@ app.component('v-chart', VueECharts) </template> ``` -> **Note** -> +> [!NOTE] > 仅支持 `.once` 修饰符,因为其它修饰符都与 DOM 事件机制强耦合。 Vue-ECharts 支持如下事件: From 6aa8d6daa3d055e8af74c721f71df489518a09e5 Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Thu, 3 Jul 2025 23:46:55 +0800 Subject: [PATCH 17/27] update according to the review --- README.md | 4 ++-- README.zh-Hans.md | 2 +- demo/data/line.js | 4 ++++ demo/examples/LineChart.vue | 9 ++++++++- src/composables/tooltip.ts | 2 +- src/utils.ts | 7 ++++++- 6 files changed, 22 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ff5db43..236548e 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ See more examples [here](https://github.com/ecomfe/vue-echarts/tree/main/demo). ECharts' universal interface. Modifying this prop will trigger ECharts' `setOption` method. Read more [here →](https://echarts.apache.org/en/option.html) > [!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. + > 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` @@ -376,7 +376,7 @@ Vue-ECharts allows you to define ECharts option's `tooltip.formatter` callbacks </template> ``` -[Example→](https://vue-echarts.dev/#line) +[Example →](https://vue-echarts.dev/#line) </details> diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 18ef12c..4b948c6 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -376,7 +376,7 @@ Vue-ECharts 允许你通过 Vue 插槽来定义 ECharts 配置中的 `tooltip.fo </template> ``` -[Example→](https://vue-echarts.dev/#line) +[示例 →](https://vue-echarts.dev/#line) </details> diff --git a/demo/data/line.js b/demo/data/line.js index 64371c5..35f1601 100644 --- a/demo/data/line.js +++ b/demo/data/line.js @@ -1,5 +1,9 @@ export default function getData() { return { + textStyle: { + fontFamily: 'Inter, "Helvetica Neue", Arial, sans-serif', + fontWeight: 300, + }, legend: { top: 20 }, tooltip: { trigger: "axis", diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue index 039e060..659c893 100644 --- a/demo/examples/LineChart.vue +++ b/demo/examples/LineChart.vue @@ -37,7 +37,14 @@ function getPieOption(params) { borderColor: "#fff", borderWidth: 2, }, - label: { position: "center", formatter: params[0].name }, + label: { + position: "center", + formatter: params[0].name, + textStyle: { + fontFamily: 'Inter, "Helvetica Neue", Arial, sans-serif', + fontWeight: 300, + }, + }, }, ], }; diff --git a/src/composables/tooltip.ts b/src/composables/tooltip.ts index 8b179fb..7c36f03 100644 --- a/src/composables/tooltip.ts +++ b/src/composables/tooltip.ts @@ -65,7 +65,7 @@ export function useTooltip(slots: Slots, onSlotsChange: () => void) { // shallow-clone the link; create empty shell if missing cur[seg] = next ? Array.isArray(next) - ? next.slice() + ? [...next] : { ...next } : isValidArrayIndex(seg) ? [] diff --git a/src/utils.ts b/src/utils.ts index 546ecca..4d3a383 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -32,5 +32,10 @@ export function toValue<T>(source: MaybeRefOrGetter<T>): T { export function isValidArrayIndex(key: string): boolean { const num = Number(key); - return Number.isInteger(num) && num >= 0 && num < Math.pow(2, 32) - 1; + return ( + Number.isInteger(num) && + num >= 0 && + num < Math.pow(2, 32) - 1 && + String(num) === key + ); } From 887102d1f55f9b024459151bbd62c1da17208860 Mon Sep 17 00:00:00 2001 From: Yue JIN <40021217+kingyue737@users.noreply.github.com> Date: Fri, 4 Jul 2025 17:02:23 +0800 Subject: [PATCH 18/27] 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> --- README.md | 60 ++++++++++++------- README.zh-Hans.md | 64 +++++++++++++------- demo/CodeGen.vue | 6 +- demo/data/logo.js | 6 +- demo/data/radar.ts | 1 + demo/examples/LineChart.vue | 28 ++++++++- package.json | 6 +- pnpm-lock.yaml | 30 +++++----- scripts/docs.mjs | 2 +- src/ECharts.ts | 19 ++++-- src/composables/index.ts | 1 + src/composables/{tooltip.ts => slot.ts} | 80 ++++++++++++++----------- 12 files changed, 196 insertions(+), 107 deletions(-) rename src/composables/{tooltip.ts => slot.ts} (54%) diff --git a/README.md b/README.md index 236548e..a0d293d 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Drop `<script>` inside your HTML file and access the component via `window.VueEC ```html <script src="https://cdn.jsdelivr.net/npm/vue@3.5.13"></script> -<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1"></script> +<script src="https://cdn.jsdelivr.net/npm/echarts@6.0.0-beta.1"></script> <script src="https://cdn.jsdelivr.net/npm/vue-echarts@7.0.3"></script> ``` @@ -331,15 +331,21 @@ export default { - `clear` [→](https://echarts.apache.org/en/api.html#echartsInstance.clear) - `dispose` [→](https://echarts.apache.org/en/api.html#echartsInstance.dispose) +> [!NOTE] +> 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`: use the `theme` prop instead. + ### Slots -Vue-ECharts allows you to define ECharts option's `tooltip.formatter` callbacks via Vue slots instead of defining them in your `option` object. This simplifies custom tooltip 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** -- Slot names begin with `tooltip`, followed by hyphen-separated path segments to the target formatter. +- Slot names begin with `tooltip`/`dataView`, followed by hyphen-separated path segments to the target. - Each segment corresponds to an `option` property name or an array index (for arrays, use the numeric index). -- The constructed slot name maps directly to the nested `formatter` it overrides. +- The constructed slot name maps directly to the nested callback it overrides. **Example mappings**: @@ -347,6 +353,8 @@ Vue-ECharts allows you to define ECharts option's `tooltip.formatter` callbacks - `tooltip-baseOption` → `option.baseOption.tooltip.formatter` - `tooltip-xAxis-1` → `option.xAxis[1].tooltip.formatter` - `tooltip-series-2-data-4` → `option.series[2].data[4].tooltip.formatter` +- `dataView` → `option.toolbox.feature.dataView.optionToContent` +- `dataView-media[1]-option` → `option.media[1].option.toolbox.feature.dataView.optionToContent` <details> <summary>Usage</summary> @@ -354,24 +362,38 @@ Vue-ECharts allows you to define ECharts option's `tooltip.formatter` callbacks ```vue <template> <v-chart :option="chartOptions"> - <!-- Override global tooltip.formatter --> + <!-- Global `tooltip.formatter` --> <template #tooltip="{ params }"> - <table> - <tr> - <th>Series</th> - <th>Value</th> - </tr> - <tr v-for="(s, i) in params" :key="i"> - <td>{{ s.seriesName }}</td> - <td>{{ s.data }}</td> - </tr> - </table> + <div v-for="(param, i) in params" :key="i"> + <span v-html="param.marker" /> + <span>{{ param.seriesName }}</span> + <span>{{ param.value[0] }}</span> + </div> </template> - <!-- Override tooltip on xAxis --> + <!-- Tooltip on xAxis --> <template #tooltip-xAxis="{ params }"> <div>X-Axis : {{ params.value }}</div> </template> + + <!-- Data View Content --> + <template #dataView="{ option }"> + <table> + <thead> + <tr> + <th v-for="(t, i) in option.dataset[0].source[0]" :key="i"> + {{ t }} + </th> + </tr> + </thead> + <tbody> + <tr v-for="(row, i) in option.dataset[0].source.slice(1)" :key="i"> + <th>{{ row[0] }}</th> + <td v-for="(v, i) in row.slice(1)" :key="i">{{ v }}</td> + </tr> + </tbody> + </table> + </template> </v-chart> </template> ``` @@ -380,12 +402,8 @@ Vue-ECharts allows you to define ECharts option's `tooltip.formatter` callbacks </details> -#### Slot Props - -- `params`: The first argument passed to the original [`tooltip.formatter`](https://echarts.apache.org/en/option.html#tooltip.formatter) callback. - > [!NOTE] -> Slots take precedence over any `tooltip.formatter` defined in `props.option`. If a matching slot is present, the slot's content will render instead of using `option`'s formatter. +> Slots take precedence over the corresponding callback defined in `props.option`. ### Static Methods diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 4b948c6..e4b9456 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -120,7 +120,7 @@ import "echarts"; ```html <script src="https://cdn.jsdelivr.net/npm/vue@3.5.13"></script> -<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1"></script> +<script src="https://cdn.jsdelivr.net/npm/echarts@6.0.0-beta.1"></script> <script src="https://cdn.jsdelivr.net/npm/vue-echarts@7.0.3"></script> ``` @@ -331,15 +331,21 @@ export default { - `clear` [→](https://echarts.apache.org/zh/api.html#echartsInstance.clear) - `dispose` [→](https://echarts.apache.org/zh/api.html#echartsInstance.dispose) +> [!NOTE] +> 如下 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`:请使用 `theme` prop。 + ### 插槽(Slots) -Vue-ECharts 允许你通过 Vue 插槽来定义 ECharts 配置中的 `tooltip.formatter` 回调,而无需在 `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 模板语法来编写自定义提示框或数据视图中的内容。 **插槽命名约定** -- 插槽名称以 `tooltip` 开头,后面跟随用连字符分隔的路径片段,用于定位要覆盖的 `formatter`。 -- 每个片段对应 `option` 对象的属性名或数组索引(数组索引使用数字形式)。 -- 拼接后的插槽名称直接映射到要覆盖的嵌套 `formatter`。 +- 插槽名称以 `tooltip`/`dataView` 开头,后面跟随用连字符分隔的路径片段,用于定位目标。 +- 每个路径片段对应 `option` 对象的属性名或数组索引(数组索引使用数字形式)。 +- 拼接后的插槽名称直接映射到要覆盖的嵌套回调函数。 **示例映射**: @@ -347,6 +353,8 @@ Vue-ECharts 允许你通过 Vue 插槽来定义 ECharts 配置中的 `tooltip.fo - `tooltip-baseOption` → `option.baseOption.tooltip.formatter` - `tooltip-xAxis-1` → `option.xAxis[1].tooltip.formatter` - `tooltip-series-2-data-4` → `option.series[2].data[4].tooltip.formatter` +- `dataView` → `option.toolbox.feature.dataView.optionToContent` +- `dataView-media[1]-option` → `option.media[1].option.toolbox.feature.dataView.optionToContent` <details> <summary>用法示例</summary> @@ -354,23 +362,37 @@ Vue-ECharts 允许你通过 Vue 插槽来定义 ECharts 配置中的 `tooltip.fo ```vue <template> <v-chart :option="chartOptions"> - <!-- 覆盖全局 tooltip.formatter --> + <!-- 全局 `tooltip.formatter` --> <template #tooltip="{ params }"> - <table> - <tr> - <th>系列名称</th> - <th>数值</th> - </tr> - <tr v-for="(item, idx) in params" :key="idx"> - <td>{{ item.seriesName }}</td> - <td>{{ item.data }}</td> - </tr> - </table> + <div v-for="(param, i) in params" :key="i"> + <span v-html="param.marker" /> + <span>{{ param.seriesName }}</span> + <span>{{ param.value[0] }}</span> + </div> </template> - <!-- 覆盖x轴 tooltip.formatter --> + <!-- x轴 tooltip --> <template #tooltip-xAxis="{ params }"> - <div>X 轴: {{ params.value }}</div> + <div>X轴: {{ params.value }}</div> + </template> + + <!-- 数据视图内容 --> + <template #dataView="{ option }"> + <table> + <thead> + <tr> + <th v-for="(t, i) in option.dataset[0].source[0]" :key="i"> + {{ t }} + </th> + </tr> + </thead> + <tbody> + <tr v-for="(row, i) in option.dataset[0].source.slice(1)" :key="i"> + <th>{{ row[0] }}</th> + <td v-for="(v, i) in row.slice(1)" :key="i">{{ v }}</td> + </tr> + </tbody> + </table> </template> </v-chart> </template> @@ -380,12 +402,8 @@ Vue-ECharts 允许你通过 Vue 插槽来定义 ECharts 配置中的 `tooltip.fo </details> -#### 插槽Props - -- `params`:[`tooltip.formatter`](https://echarts.apache.org/zh/option.html#tooltip.formatter) 回调的第一个参数。 - > [!NOTE] -> 插槽内容会优先于 `props.option` 中对应的 `tooltip.formatter` 渲染,如果两者同时存在。 +> 插槽会优先于 `props.option` 中对应的回调函数。 ### 静态方法 diff --git a/demo/CodeGen.vue b/demo/CodeGen.vue index 08cbfb0..3793bd9 100644 --- a/demo/CodeGen.vue +++ b/demo/CodeGen.vue @@ -78,7 +78,11 @@ const transformedCode = ref(""); const transformErrors = ref([]); onMounted(async () => { - await initialize({ wasmURL }); + // prevent multiple initializations during HMR + if (!window.__esbuildInitialized) { + await initialize({ wasmURL }); + window.__esbuildInitialized = true; + } initializing.value = false; diff --git a/demo/data/logo.js b/demo/data/logo.js index 6947e3d..017578b 100644 --- a/demo/data/logo.js +++ b/demo/data/logo.js @@ -20,10 +20,8 @@ export default { }, shape: `path://${d}`, label: { - normal: { - formatter() { - return ""; - }, + formatter() { + return ""; }, }, itemStyle: { diff --git a/demo/data/radar.ts b/demo/data/radar.ts index 8d00c21..6cd8bea 100644 --- a/demo/data/radar.ts +++ b/demo/data/radar.ts @@ -27,6 +27,7 @@ export const useScoreStore = defineStore("store", () => { fontWeight: 300, }, radar: { + splitNumber: 4, indicator: scores.value.map(({ name, max }, index) => { if (index === activeIndex) { return { name, max, color: "goldenrod" }; diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue index 659c893..eded634 100644 --- a/demo/examples/LineChart.vue +++ b/demo/examples/LineChart.vue @@ -6,6 +6,7 @@ import { DatasetComponent, LegendComponent, TooltipComponent, + ToolboxComponent, } from "echarts/components"; import { shallowRef } from "vue"; import VChart from "../../src/ECharts"; @@ -18,6 +19,7 @@ use([ LegendComponent, LineChart, TooltipComponent, + ToolboxComponent, PieChart, ]); @@ -56,7 +58,7 @@ function getPieOption(params) { <v-example id="line" title="Line chart" - desc="(with component rendered tooltip)" + desc="(with tooltip and dataView slots)" > <v-chart :option="option" autoresize> <template #tooltip="{ params }"> @@ -70,6 +72,23 @@ function getPieOption(params) { {{ axis === "xAxis" ? "Year" : "Value" }}: <b>{{ params.name }}</b> </template> + <template #dataView="{ option }"> + <table style="margin: 20px auto"> + <thead> + <tr> + <th v-for="(t, i) in option.dataset[0].source[0]" :key="i"> + {{ t }} + </th> + </tr> + </thead> + <tbody> + <tr v-for="(row, i) in option.dataset[0].source.slice(1)" :key="i"> + <th>{{ row[0] }}</th> + <td v-for="(v, i) in row.slice(1)" :key="i">{{ v }}</td> + </tr> + </tbody> + </table> + </template> </v-chart> <template #extra> <p class="actions"> @@ -82,3 +101,10 @@ function getPieOption(params) { </template> </v-example> </template> + +<style scoped> +th, +td { + padding: 4px 8px; +} +</style> diff --git a/package.json b/package.json index a45bddd..163c7b9 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "docs": "node ./scripts/docs.mjs", "prepublishOnly": "pnpm run typecheck && pnpm run dev:typecheck && pnpm run build && publint" }, - "packageManager": "pnpm@10.12.1", + "packageManager": "pnpm@10.12.4", "type": "module", "main": "dist/index.js", "unpkg": "dist/index.min.js", @@ -36,7 +36,7 @@ "dist" ], "peerDependencies": { - "echarts": "^5.5.1", + "echarts": "^6.0.0-beta.1", "vue": "^3.1.1" }, "devDependencies": { @@ -50,7 +50,7 @@ "@vue/tsconfig": "^0.7.0", "@vueuse/core": "^13.1.0", "comment-mark": "^2.0.1", - "echarts": "^5.6.0", + "echarts": "^6.0.0-beta.1", "echarts-gl": "^2.0.9", "echarts-liquidfill": "^3.1.0", "esbuild-wasm": "^0.23.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64e3b65..5cd968d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,14 +39,14 @@ importers: specifier: ^2.0.1 version: 2.0.1 echarts: - specifier: ^5.6.0 - version: 5.6.0 + specifier: ^6.0.0-beta.1 + version: 6.0.0-beta.1 echarts-gl: specifier: ^2.0.9 - version: 2.0.9(echarts@5.6.0) + version: 2.0.9(echarts@6.0.0-beta.1) echarts-liquidfill: specifier: ^3.1.0 - version: 3.1.0(echarts@5.6.0) + version: 3.1.0(echarts@6.0.0-beta.1) esbuild-wasm: specifier: ^0.23.0 version: 0.23.0 @@ -820,8 +820,8 @@ packages: peerDependencies: echarts: ^5.0.1 - echarts@5.6.0: - resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} + echarts@6.0.0-beta.1: + resolution: {integrity: sha512-hEtCVOohAWr8fCMNXwg0cRZjkWO+LwbhO30cX/fzwb2LF4sHt06YHVWlAQclayhwHlxCyYtMG9FkFnNUAHK72Q==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1511,8 +1511,8 @@ packages: zrender@5.6.0: resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==} - zrender@5.6.1: - resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} + zrender@6.0.0-rc.1: + resolution: {integrity: sha512-DWYxDvSHb69PlZ9bs2C4NHt0xHMojHztGForDFAiNSzw9XDwycwXAhJydFrNyq/vy0I8usTZ+KbtZyrX+6ePJQ==} snapshots: @@ -2137,20 +2137,20 @@ snapshots: eastasianwidth@0.2.0: {} - echarts-gl@2.0.9(echarts@5.6.0): + echarts-gl@2.0.9(echarts@6.0.0-beta.1): dependencies: claygl: 1.3.0 - echarts: 5.6.0 + echarts: 6.0.0-beta.1 zrender: 5.6.0 - echarts-liquidfill@3.1.0(echarts@5.6.0): + echarts-liquidfill@3.1.0(echarts@6.0.0-beta.1): dependencies: - echarts: 5.6.0 + echarts: 6.0.0-beta.1 - echarts@5.6.0: + echarts@6.0.0-beta.1: dependencies: tslib: 2.3.0 - zrender: 5.6.1 + zrender: 6.0.0-rc.1 emoji-regex@8.0.0: {} @@ -2823,6 +2823,6 @@ snapshots: dependencies: tslib: 2.3.0 - zrender@5.6.1: + zrender@6.0.0-rc.1: dependencies: tslib: 2.3.0 diff --git a/scripts/docs.mjs b/scripts/docs.mjs index dc9c42c..b8e1abc 100644 --- a/scripts/docs.mjs +++ b/scripts/docs.mjs @@ -8,7 +8,7 @@ const CDN_PREFIX = "https://cdn.jsdelivr.net/npm/"; const DEP_VERSIONS = { vue: "3.5.13", - echarts: "5.5.1", + echarts: "6.0.0-beta.1", [name]: version, }; diff --git a/src/ECharts.ts b/src/ECharts.ts index a33b504..56fdc98 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -19,6 +19,7 @@ import { autoresizeProps, useLoading, loadingProps, + useSlotOption, type PublicMethods, } from "./composables"; import { isOn, omitOn, toValue } from "./utils"; @@ -40,7 +41,6 @@ import type { import type { EChartsElement } from "./wc"; import "./style.css"; -import { useTooltip } from "./composables/tooltip"; const wcRegistered = register(); @@ -66,7 +66,8 @@ export default defineComponent({ }, emits: {} as unknown as Emits, slots: Object as SlotsType< - Record<"tooltip" | `tooltip:${string}`, { params: any }> + Record<"tooltip" | `tooltip-${string}`, { params: any }> & + Record<"dataView" | `dataView-${string}`, { option: Option }> >, inheritAttrs: false, setup(props, { attrs, expose, slots }) { @@ -256,7 +257,7 @@ export default defineComponent({ ); watch( - [realTheme, realInitOptions], + realInitOptions, () => { cleanup(); init(); @@ -266,6 +267,16 @@ export default defineComponent({ }, ); + watch( + realTheme, + (theme) => { + chart.value?.setTheme(theme); + }, + { + deep: true, + }, + ); + watchEffect(() => { if (props.group && chart.value) { chart.value.group = props.group; @@ -278,7 +289,7 @@ export default defineComponent({ useAutoresize(chart, autoresize, root); - const { teleportedSlots, patchOption } = useTooltip(slots, () => { + const { teleportedSlots, patchOption } = useSlotOption(slots, () => { if (!manualUpdate.value && props.option && chart.value) { chart.value.setOption( patchOption(props.option), diff --git a/src/composables/index.ts b/src/composables/index.ts index 7708f91..68526de 100644 --- a/src/composables/index.ts +++ b/src/composables/index.ts @@ -1,3 +1,4 @@ export * from "./api"; export * from "./autoresize"; export * from "./loading"; +export * from "./slot"; diff --git a/src/composables/tooltip.ts b/src/composables/slot.ts similarity index 54% rename from src/composables/tooltip.ts rename to src/composables/slot.ts index 7c36f03..cbedc49 100644 --- a/src/composables/tooltip.ts +++ b/src/composables/slot.ts @@ -11,11 +11,19 @@ import { import type { Option } from "../types"; import { isValidArrayIndex } from "../utils"; -function isTooltipSlot(key: string) { - return key === "tooltip" || key.startsWith("tooltip-"); +const SLOT_PATH_MAP = { + tooltip: ["tooltip", "formatter"], + dataView: ["toolbox", "feature", "dataView", "optionToContent"], +}; +type SlotPrefix = keyof typeof SLOT_PATH_MAP; + +function isValidSlotName(key: string) { + return Object.keys(SLOT_PATH_MAP).some( + (slotPrefix) => key === slotPrefix || key.startsWith(slotPrefix + "-"), + ); } -export function useTooltip(slots: Slots, onSlotsChange: () => void) { +export function useSlotOption(slots: Slots, onSlotsChange: () => void) { const detachedRoot = typeof window !== "undefined" ? document.createElement("div") : undefined; const containers = shallowReactive<Record<string, HTMLElement>>({}); @@ -31,14 +39,18 @@ export function useTooltip(slots: Slots, onSlotsChange: () => void) { Teleport as any, { to: detachedRoot, defer: true }, Object.entries(slots) - .filter(([key]) => isTooltipSlot(key)) + .filter(([key]) => isValidSlotName(key)) .map(([key, slot]) => { + const propName = key.startsWith("tooltip") ? "params" : "option"; const slotContent = initialized[key] - ? slot?.({ params: params[key] }) + ? slot?.({ [propName]: params[key] }) : undefined; return h( "div", - { ref: (el) => (containers[key] = el as HTMLElement) }, + { + ref: (el) => (containers[key] = el as HTMLElement), + style: { display: "contents" }, + }, slotContent, ); }), @@ -46,43 +58,37 @@ export function useTooltip(slots: Slots, onSlotsChange: () => void) { : undefined; }; - // Shallow clone the option along the path and patch the tooltip formatter + // Shallow clone the option along the path and override the target callback function patchOption(src: Option): Option { const root = { ...src }; Object.keys(slots) - .filter((key) => isTooltipSlot(key)) + .filter((key) => isValidSlotName(key)) .forEach((key) => { const path = key.split("-"); - path.push(path.shift()!); - let cur: any = root; + const prefix = path.shift() as SlotPrefix; + path.push(...SLOT_PATH_MAP[prefix]); - for (let i = 0; i < path.length; i++) { + let cur: any = root; + for (let i = 0; i < path.length - 1; i++) { const seg = path[i]; const next = cur[seg]; - if (i < path.length - 1) { - // shallow-clone the link; create empty shell if missing - cur[seg] = next - ? Array.isArray(next) - ? [...next] - : { ...next } - : isValidArrayIndex(seg) - ? [] - : {}; - cur = cur[seg]; - } else { - // final node = tooltip - cur[seg] = { - ...(next || {}), - formatter(p: any) { - initialized[key] = true; - params[key] = p; - return containers[key]; - }, - }; - } + // shallow-clone the link; create empty shell if missing + cur[seg] = next + ? Array.isArray(next) + ? [...next] + : { ...next } + : isValidArrayIndex(seg) + ? [] + : {}; + cur = cur[seg]; } + cur[path[path.length - 1]] = (p: any) => { + initialized[key] = true; + params[key] = p; + return containers[key]; + }; }); return root; @@ -90,9 +96,15 @@ export function useTooltip(slots: Slots, onSlotsChange: () => void) { // `slots` is not reactive and cannot be watched // so we need to watch it manually - let slotNames = Object.keys(slots).filter((key) => isTooltipSlot(key)); + let slotNames: string[] = []; onUpdated(() => { - const newSlotNames = Object.keys(slots).filter((key) => isTooltipSlot(key)); + const newSlotNames = Object.keys(slots).filter((key) => { + if (isValidSlotName(key)) { + return true; + } + console.warn(`[vue-echarts] Invalid slot name: ${key}`); + return false; + }); if (newSlotNames.join() !== slotNames.join()) { // Clean up params and initialized for removed slots slotNames.forEach((key) => { From 429943ab283864ee9b0b4df7523557afdeda2672 Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Mon, 7 Jul 2025 13:41:06 +0800 Subject: [PATCH 19/27] fix docs typo --- README.md | 2 +- README.zh-Hans.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a0d293d..ca8e014 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,7 @@ Vue-ECharts allows you to define ECharts option's [`tooltip.formatter`](https:// - `tooltip-xAxis-1` → `option.xAxis[1].tooltip.formatter` - `tooltip-series-2-data-4` → `option.series[2].data[4].tooltip.formatter` - `dataView` → `option.toolbox.feature.dataView.optionToContent` -- `dataView-media[1]-option` → `option.media[1].option.toolbox.feature.dataView.optionToContent` +- `dataView-media-1-option` → `option.media[1].option.toolbox.feature.dataView.optionToContent` <details> <summary>Usage</summary> diff --git a/README.zh-Hans.md b/README.zh-Hans.md index e4b9456..f2d1fe4 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -354,7 +354,7 @@ Vue-ECharts 允许你通过 Vue 插槽来定义 ECharts 配置中的 [`tooltip.f - `tooltip-xAxis-1` → `option.xAxis[1].tooltip.formatter` - `tooltip-series-2-data-4` → `option.series[2].data[4].tooltip.formatter` - `dataView` → `option.toolbox.feature.dataView.optionToContent` -- `dataView-media[1]-option` → `option.media[1].option.toolbox.feature.dataView.optionToContent` +- `dataView-media-1-option` → `option.media[1].option.toolbox.feature.dataView.optionToContent` <details> <summary>用法示例</summary> From c8a8869ed05f06fbd9bf33fc265bf51e589fce6a Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Tue, 15 Jul 2025 16:43:37 +0800 Subject: [PATCH 20/27] update according to the review --- src/composables/slot.ts | 79 +++++++++++++++++++++++++---------------- src/utils.ts | 13 +++++++ 2 files changed, 62 insertions(+), 30 deletions(-) diff --git a/src/composables/slot.ts b/src/composables/slot.ts index cbedc49..48bb1a6 100644 --- a/src/composables/slot.ts +++ b/src/composables/slot.ts @@ -6,19 +6,28 @@ import { onMounted, shallowRef, shallowReactive, - type Slots, + warn, } from "vue"; +import type { Slots } from "vue"; import type { Option } from "../types"; -import { isValidArrayIndex } from "../utils"; +import { isValidArrayIndex, isSameSet } from "../utils"; -const SLOT_PATH_MAP = { - tooltip: ["tooltip", "formatter"], - dataView: ["toolbox", "feature", "dataView", "optionToContent"], -}; -type SlotPrefix = keyof typeof SLOT_PATH_MAP; +const SLOT_CONFIG = { + tooltip: { + path: ["tooltip", "formatter"], + propNames: ["params"], + }, + dataView: { + path: ["toolbox", "feature", "dataView", "optionToContent"], + propNames: ["option"], + }, +} as const; +type SlotPrefix = keyof typeof SLOT_CONFIG; +type SlotName = SlotPrefix | `${SlotPrefix}-${string}`; +const SLOT_PREFIXES = Object.keys(SLOT_CONFIG) as SlotPrefix[]; -function isValidSlotName(key: string) { - return Object.keys(SLOT_PATH_MAP).some( +function isValidSlotName(key: string): key is SlotName { + return SLOT_PREFIXES.some( (slotPrefix) => key === slotPrefix || key.startsWith(slotPrefix + "-"), ); } @@ -26,9 +35,13 @@ function isValidSlotName(key: string) { export function useSlotOption(slots: Slots, onSlotsChange: () => void) { const detachedRoot = typeof window !== "undefined" ? document.createElement("div") : undefined; - const containers = shallowReactive<Record<string, HTMLElement>>({}); - const initialized = shallowReactive<Record<string, boolean>>({}); - const params = shallowReactive<Record<string, any>>({}); + const containers = shallowReactive<Partial<Record<SlotName, HTMLElement>>>( + {}, + ); + const initialized = shallowReactive<Partial<Record<SlotName, boolean>>>({}); + const params = shallowReactive< + Partial<Record<SlotName, Record<string, any>>> + >({}); const isMounted = shallowRef(false); // Teleport the tooltip slots to a detached root @@ -41,14 +54,14 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { Object.entries(slots) .filter(([key]) => isValidSlotName(key)) .map(([key, slot]) => { - const propName = key.startsWith("tooltip") ? "params" : "option"; - const slotContent = initialized[key] - ? slot?.({ [propName]: params[key] }) + const slotName = key as SlotName; + const slotContent = initialized[slotName] + ? slot?.(params[slotName]) : undefined; return h( "div", { - ref: (el) => (containers[key] = el as HTMLElement), + ref: (el) => (containers[slotName] = el as HTMLElement), style: { display: "contents" }, }, slotContent, @@ -63,11 +76,17 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { const root = { ...src }; Object.keys(slots) - .filter((key) => isValidSlotName(key)) + .filter((key) => { + const isValidSlot = isValidSlotName(key); + if (!isValidSlot) { + warn(`Invalid slot name: ${key}`); + } + return isValidSlot; + }) .forEach((key) => { const path = key.split("-"); const prefix = path.shift() as SlotPrefix; - path.push(...SLOT_PATH_MAP[prefix]); + path.push(...SLOT_CONFIG[prefix].path); let cur: any = root; for (let i = 0; i < path.length - 1; i++) { @@ -84,9 +103,15 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { : {}; cur = cur[seg]; } - cur[path[path.length - 1]] = (p: any) => { + cur[path[path.length - 1]] = (...args: any[]) => { initialized[key] = true; - params[key] = p; + params[key] = SLOT_CONFIG[prefix].propNames.reduce( + (acc, paramName, index) => { + acc[paramName] = args[index]; + return acc; + }, + {} as Record<string, any>, + ); return containers[key]; }; }); @@ -96,19 +121,13 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { // `slots` is not reactive and cannot be watched // so we need to watch it manually - let slotNames: string[] = []; + let slotNames: SlotName[] = []; onUpdated(() => { - const newSlotNames = Object.keys(slots).filter((key) => { - if (isValidSlotName(key)) { - return true; - } - console.warn(`[vue-echarts] Invalid slot name: ${key}`); - return false; - }); - if (newSlotNames.join() !== slotNames.join()) { + const newSlotNames = Object.keys(slots).filter(isValidSlotName); + if (!isSameSet(newSlotNames, slotNames)) { // Clean up params and initialized for removed slots slotNames.forEach((key) => { - if (!(key in slots)) { + if (!newSlotNames.includes(key)) { delete params[key]; delete initialized[key]; delete containers[key]; diff --git a/src/utils.ts b/src/utils.ts index 4d3a383..2169a9e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -39,3 +39,16 @@ export function isValidArrayIndex(key: string): boolean { String(num) === key ); } + +export function isSameSet<T>(a: T[], b: T[]): boolean { + const setA = new Set(a); + const setB = new Set(b); + + if (setA.size !== setB.size) return false; + + for (const val of setA) { + if (!setB.has(val)) return false; + } + + return true; +} From f5fdc88bc8c0cfc8626a35e05b8091f808c808c7 Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Tue, 15 Jul 2025 17:30:37 +0800 Subject: [PATCH 21/27] small fix --- demo/examples/LineChart.vue | 6 ++---- src/composables/slot.ts | 13 +++++-------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue index eded634..e458627 100644 --- a/demo/examples/LineChart.vue +++ b/demo/examples/LineChart.vue @@ -42,10 +42,8 @@ function getPieOption(params) { label: { position: "center", formatter: params[0].name, - textStyle: { - fontFamily: 'Inter, "Helvetica Neue", Arial, sans-serif', - fontWeight: 300, - }, + fontFamily: 'Inter, "Helvetica Neue", Arial, sans-serif', + fontWeight: 300, }, }, ], diff --git a/src/composables/slot.ts b/src/composables/slot.ts index 48bb1a6..271b3e3 100644 --- a/src/composables/slot.ts +++ b/src/composables/slot.ts @@ -24,6 +24,7 @@ const SLOT_CONFIG = { } as const; type SlotPrefix = keyof typeof SLOT_CONFIG; type SlotName = SlotPrefix | `${SlotPrefix}-${string}`; +type SlotRecord<T> = Partial<Record<SlotName, T>>; const SLOT_PREFIXES = Object.keys(SLOT_CONFIG) as SlotPrefix[]; function isValidSlotName(key: string): key is SlotName { @@ -35,13 +36,9 @@ function isValidSlotName(key: string): key is SlotName { export function useSlotOption(slots: Slots, onSlotsChange: () => void) { const detachedRoot = typeof window !== "undefined" ? document.createElement("div") : undefined; - const containers = shallowReactive<Partial<Record<SlotName, HTMLElement>>>( - {}, - ); - const initialized = shallowReactive<Partial<Record<SlotName, boolean>>>({}); - const params = shallowReactive< - Partial<Record<SlotName, Record<string, any>>> - >({}); + const containers = shallowReactive<SlotRecord<HTMLElement>>({}); + const initialized = shallowReactive<SlotRecord<boolean>>({}); + const params = shallowReactive<SlotRecord<Record<string, any>>>({}); const isMounted = shallowRef(false); // Teleport the tooltip slots to a detached root @@ -79,7 +76,7 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { .filter((key) => { const isValidSlot = isValidSlotName(key); if (!isValidSlot) { - warn(`Invalid slot name: ${key}`); + warn(`Invalid vue-echarts slot name: ${key}`); } return isValidSlot; }) From 49d807bd5f0b1c5da92c0b080543ac42bce48a40 Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Sat, 19 Jul 2025 16:20:23 +0800 Subject: [PATCH 22/27] remove wrapper around slotProp --- README.md | 6 +++--- README.zh-Hans.md | 6 +++--- demo/examples/LineChart.vue | 6 +++--- src/ECharts.ts | 4 ++-- src/composables/slot.ts | 30 +++++++++--------------------- 5 files changed, 20 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index ca8e014..a05e0b4 100644 --- a/README.md +++ b/README.md @@ -363,7 +363,7 @@ Vue-ECharts allows you to define ECharts option's [`tooltip.formatter`](https:// <template> <v-chart :option="chartOptions"> <!-- Global `tooltip.formatter` --> - <template #tooltip="{ params }"> + <template #tooltip="params"> <div v-for="(param, i) in params" :key="i"> <span v-html="param.marker" /> <span>{{ param.seriesName }}</span> @@ -372,12 +372,12 @@ Vue-ECharts allows you to define ECharts option's [`tooltip.formatter`](https:// </template> <!-- Tooltip on xAxis --> - <template #tooltip-xAxis="{ params }"> + <template #tooltip-xAxis="params"> <div>X-Axis : {{ params.value }}</div> </template> <!-- Data View Content --> - <template #dataView="{ option }"> + <template #dataView="option"> <table> <thead> <tr> diff --git a/README.zh-Hans.md b/README.zh-Hans.md index f2d1fe4..6b6d3be 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -363,7 +363,7 @@ Vue-ECharts 允许你通过 Vue 插槽来定义 ECharts 配置中的 [`tooltip.f <template> <v-chart :option="chartOptions"> <!-- 全局 `tooltip.formatter` --> - <template #tooltip="{ params }"> + <template #tooltip="params"> <div v-for="(param, i) in params" :key="i"> <span v-html="param.marker" /> <span>{{ param.seriesName }}</span> @@ -372,12 +372,12 @@ Vue-ECharts 允许你通过 Vue 插槽来定义 ECharts 配置中的 [`tooltip.f </template> <!-- x轴 tooltip --> - <template #tooltip-xAxis="{ params }"> + <template #tooltip-xAxis="params"> <div>X轴: {{ params.value }}</div> </template> <!-- 数据视图内容 --> - <template #dataView="{ option }"> + <template #dataView="option"> <table> <thead> <tr> diff --git a/demo/examples/LineChart.vue b/demo/examples/LineChart.vue index e458627..30173d4 100644 --- a/demo/examples/LineChart.vue +++ b/demo/examples/LineChart.vue @@ -59,18 +59,18 @@ function getPieOption(params) { desc="(with tooltip and dataView slots)" > <v-chart :option="option" autoresize> - <template #tooltip="{ params }"> + <template #tooltip="params"> <v-chart :style="{ width: '100px', height: '100px' }" :option="getPieOption(params)" autoresize /> </template> - <template #[`tooltip-${axis}`]="{ params }"> + <template #[`tooltip-${axis}`]="params"> {{ axis === "xAxis" ? "Year" : "Value" }}: <b>{{ params.name }}</b> </template> - <template #dataView="{ option }"> + <template #dataView="option"> <table style="margin: 20px auto"> <thead> <tr> diff --git a/src/ECharts.ts b/src/ECharts.ts index 56fdc98..5561fde 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -66,8 +66,8 @@ export default defineComponent({ }, emits: {} as unknown as Emits, slots: Object as SlotsType< - Record<"tooltip" | `tooltip-${string}`, { params: any }> & - Record<"dataView" | `dataView-${string}`, { option: Option }> + Record<"tooltip" | `tooltip-${string}`, any> & + Record<"dataView" | `dataView-${string}`, Option> >, inheritAttrs: false, setup(props, { attrs, expose, slots }) { diff --git a/src/composables/slot.ts b/src/composables/slot.ts index 271b3e3..88243f2 100644 --- a/src/composables/slot.ts +++ b/src/composables/slot.ts @@ -12,20 +12,14 @@ import type { Slots } from "vue"; import type { Option } from "../types"; import { isValidArrayIndex, isSameSet } from "../utils"; -const SLOT_CONFIG = { - tooltip: { - path: ["tooltip", "formatter"], - propNames: ["params"], - }, - dataView: { - path: ["toolbox", "feature", "dataView", "optionToContent"], - propNames: ["option"], - }, +const SLOT_OPTION_PATHS = { + tooltip: ["tooltip", "formatter"], + dataView: ["toolbox", "feature", "dataView", "optionToContent"], } as const; -type SlotPrefix = keyof typeof SLOT_CONFIG; +type SlotPrefix = keyof typeof SLOT_OPTION_PATHS; type SlotName = SlotPrefix | `${SlotPrefix}-${string}`; type SlotRecord<T> = Partial<Record<SlotName, T>>; -const SLOT_PREFIXES = Object.keys(SLOT_CONFIG) as SlotPrefix[]; +const SLOT_PREFIXES = Object.keys(SLOT_OPTION_PATHS) as SlotPrefix[]; function isValidSlotName(key: string): key is SlotName { return SLOT_PREFIXES.some( @@ -47,7 +41,7 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { return isMounted.value ? h( Teleport as any, - { to: detachedRoot, defer: true }, + { to: detachedRoot }, Object.entries(slots) .filter(([key]) => isValidSlotName(key)) .map(([key, slot]) => { @@ -83,7 +77,7 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { .forEach((key) => { const path = key.split("-"); const prefix = path.shift() as SlotPrefix; - path.push(...SLOT_CONFIG[prefix].path); + path.push(...SLOT_OPTION_PATHS[prefix]); let cur: any = root; for (let i = 0; i < path.length - 1; i++) { @@ -100,15 +94,9 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { : {}; cur = cur[seg]; } - cur[path[path.length - 1]] = (...args: any[]) => { + cur[path[path.length - 1]] = (p: any) => { initialized[key] = true; - params[key] = SLOT_CONFIG[prefix].propNames.reduce( - (acc, paramName, index) => { - acc[paramName] = args[index]; - return acc; - }, - {} as Record<string, any>, - ); + params[key] = p; return containers[key]; }; }); From fe3040acda3c3d62028668034bb465344d5c5a2b Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Sat, 19 Jul 2025 16:31:15 +0800 Subject: [PATCH 23/27] update comments --- README.md | 2 ++ README.zh-Hans.md | 2 ++ src/composables/slot.ts | 13 ++++++------- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a05e0b4..cd990b2 100644 --- a/README.md +++ b/README.md @@ -356,6 +356,8 @@ Vue-ECharts allows you to define ECharts option's [`tooltip.formatter`](https:// - `dataView` → `option.toolbox.feature.dataView.optionToContent` - `dataView-media-1-option` → `option.media[1].option.toolbox.feature.dataView.optionToContent` +The slot props correspond to the first parameter of the callback function. + <details> <summary>Usage</summary> diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 6b6d3be..dceef5f 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -356,6 +356,8 @@ Vue-ECharts 允许你通过 Vue 插槽来定义 ECharts 配置中的 [`tooltip.f - `dataView` → `option.toolbox.feature.dataView.optionToContent` - `dataView-media-1-option` → `option.media[1].option.toolbox.feature.dataView.optionToContent` +插槽的 props 对象对应回调函数的第一个参数。 + <details> <summary>用法示例</summary> diff --git a/src/composables/slot.ts b/src/composables/slot.ts index 88243f2..8ba3bbd 100644 --- a/src/composables/slot.ts +++ b/src/composables/slot.ts @@ -35,9 +35,9 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { const params = shallowReactive<SlotRecord<Record<string, any>>>({}); const isMounted = shallowRef(false); - // Teleport the tooltip slots to a detached root + // Teleport the slots to a detached root const teleportedSlots = () => { - // Make tooltip slots client-side only to avoid SSR hydration mismatch + // Make slots client-side only to avoid SSR hydration mismatch return isMounted.value ? h( Teleport as any, @@ -62,7 +62,7 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { : undefined; }; - // Shallow clone the option along the path and override the target callback + // Shallow-clone the option along the path and override the target callback function patchOption(src: Option): Option { const root = { ...src }; @@ -84,7 +84,7 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { const seg = path[i]; const next = cur[seg]; - // shallow-clone the link; create empty shell if missing + // Shallow-clone the link; create empty shell if missing cur[seg] = next ? Array.isArray(next) ? [...next] @@ -104,13 +104,12 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { return root; } - // `slots` is not reactive and cannot be watched - // so we need to watch it manually + // `slots` is not reactive, so we need to watch it manually let slotNames: SlotName[] = []; onUpdated(() => { const newSlotNames = Object.keys(slots).filter(isValidSlotName); if (!isSameSet(newSlotNames, slotNames)) { - // Clean up params and initialized for removed slots + // Clean up states for removed slots slotNames.forEach((key) => { if (!newSlotNames.includes(key)) { delete params[key]; From c7d9f451669316a5913cb6412e379cffbed90244 Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Tue, 22 Jul 2025 12:40:13 +0800 Subject: [PATCH 24/27] remove anys --- src/composables/slot.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/composables/slot.ts b/src/composables/slot.ts index 8ba3bbd..f01b747 100644 --- a/src/composables/slot.ts +++ b/src/composables/slot.ts @@ -32,7 +32,7 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { typeof window !== "undefined" ? document.createElement("div") : undefined; const containers = shallowReactive<SlotRecord<HTMLElement>>({}); const initialized = shallowReactive<SlotRecord<boolean>>({}); - const params = shallowReactive<SlotRecord<Record<string, any>>>({}); + const params = shallowReactive<SlotRecord<unknown>>({}); const isMounted = shallowRef(false); // Teleport the slots to a detached root @@ -40,7 +40,7 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { // Make slots client-side only to avoid SSR hydration mismatch return isMounted.value ? h( - Teleport as any, + Teleport, { to: detachedRoot }, Object.entries(slots) .filter(([key]) => isValidSlotName(key)) @@ -94,7 +94,7 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { : {}; cur = cur[seg]; } - cur[path[path.length - 1]] = (p: any) => { + cur[path[path.length - 1]] = (p: unknown) => { initialized[key] = true; params[key] = p; return containers[key]; From 7e6132f18b6f8772d88279283f4107610200cda9 Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Tue, 22 Jul 2025 13:47:49 +0800 Subject: [PATCH 25/27] add tooltip slot prop type --- src/ECharts.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ECharts.ts b/src/ECharts.ts index 5561fde..977065c 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -12,6 +12,7 @@ import { watchEffect, } from "vue"; import { init as initChart } from "echarts/core"; +import type { TooltipComponentFormatterCallbackParams } from "echarts"; import { usePublicAPI, @@ -66,7 +67,10 @@ export default defineComponent({ }, emits: {} as unknown as Emits, slots: Object as SlotsType< - Record<"tooltip" | `tooltip-${string}`, any> & + Record< + "tooltip" | `tooltip-${string}`, + TooltipComponentFormatterCallbackParams + > & Record<"dataView" | `dataView-${string}`, Option> >, inheritAttrs: false, From 00f222a438fdb255851c406dbc1b6ed34e400b8a Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Thu, 24 Jul 2025 22:10:01 +0800 Subject: [PATCH 26/27] target to vue 3.3 --- package.json | 2 +- src/ECharts.ts | 3 ++- src/composables/loading.ts | 5 ++--- src/types.ts | 11 +---------- src/utils.ts | 14 -------------- 5 files changed, 6 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 163c7b9..ec7620b 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ ], "peerDependencies": { "echarts": "^6.0.0-beta.1", - "vue": "^3.1.1" + "vue": "^3.3.0" }, "devDependencies": { "@highlightjs/vue-plugin": "^2.1.0", diff --git a/src/ECharts.ts b/src/ECharts.ts index 977065c..aa3982d 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -10,6 +10,7 @@ import { h, nextTick, watchEffect, + toValue, } from "vue"; import { init as initChart } from "echarts/core"; import type { TooltipComponentFormatterCallbackParams } from "echarts"; @@ -23,7 +24,7 @@ import { useSlotOption, type PublicMethods, } from "./composables"; -import { isOn, omitOn, toValue } from "./utils"; +import { isOn, omitOn } from "./utils"; import { register, TAG_NAME } from "./wc"; import type { PropType, InjectionKey, SlotsType } from "vue"; diff --git a/src/composables/loading.ts b/src/composables/loading.ts index e305fc9..6c76379 100644 --- a/src/composables/loading.ts +++ b/src/composables/loading.ts @@ -1,5 +1,4 @@ -import { inject, computed, watchEffect } from "vue"; -import { toValue } from "../utils"; +import { inject, computed, watchEffect, toValue } from "vue"; import type { Ref, InjectionKey, PropType } from "vue"; import type { @@ -18,7 +17,7 @@ export function useLoading( ): void { const defaultLoadingOptions = inject(LOADING_OPTIONS_KEY, {}); const realLoadingOptions = computed(() => ({ - ...(toValue(defaultLoadingOptions) || {}), + ...toValue(defaultLoadingOptions), ...loadingOptions?.value, })); diff --git a/src/types.ts b/src/types.ts index 0e44e81..df574f8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,17 +1,8 @@ import { init } from "echarts/core"; import type { SetOptionOpts, ECElementEvent, ElementEvent } from "echarts/core"; -import type { Ref, ShallowRef, WritableComputedRef, ComputedRef } from "vue"; +import type { MaybeRefOrGetter } from "vue"; -export type MaybeRef<T = any> = - | T - | Ref<T> - | ShallowRef<T> - | WritableComputedRef<T>; -export type MaybeRefOrGetter<T = any> = - | MaybeRef<T> - | ComputedRef<T> - | (() => T); export type Injection<T> = MaybeRefOrGetter<T | null>; type InitType = typeof init; diff --git a/src/utils.ts b/src/utils.ts index 2169a9e..4dc1e11 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,3 @@ -import type { MaybeRefOrGetter } from "./types"; -import { unref } from "vue"; - type Attrs = Record<string, any>; // Copied from @@ -19,17 +16,6 @@ export function omitOn(attrs: Attrs): Attrs { return result; } -// Copied from -// https://github.com/vuejs/core/blob/3cb4db21efa61852b0541475b4ddf57fdec4c479/packages/shared/src/general.ts#L49-L50 -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -const isFunction = (val: unknown): val is Function => typeof val === "function"; - -// Copied from -// https://github.com/vuejs/core/blob/3cb4db21efa61852b0541475b4ddf57fdec4c479/packages/reactivity/src/ref.ts#L246-L248 -export function toValue<T>(source: MaybeRefOrGetter<T>): T { - return isFunction(source) ? source() : unref(source); -} - export function isValidArrayIndex(key: string): boolean { const num = Number(key); return ( From f356f89bf73ff26760dab0b382d3925f92707f15 Mon Sep 17 00:00:00 2001 From: Yue JIN <yuejin13@qq.com> Date: Thu, 24 Jul 2025 22:46:17 +0800 Subject: [PATCH 27/27] move slot related codes to slot.ts --- src/ECharts.ts | 31 ++++++++++++------------------- src/composables/slot.ts | 11 ++++++++++- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/ECharts.ts b/src/ECharts.ts index aa3982d..93739f0 100644 --- a/src/ECharts.ts +++ b/src/ECharts.ts @@ -13,7 +13,6 @@ import { toValue, } from "vue"; import { init as initChart } from "echarts/core"; -import type { TooltipComponentFormatterCallbackParams } from "echarts"; import { usePublicAPI, @@ -22,12 +21,12 @@ import { useLoading, loadingProps, useSlotOption, - type PublicMethods, } from "./composables"; +import type { PublicMethods, SlotsTypes } from "./composables"; import { isOn, omitOn } from "./utils"; import { register, TAG_NAME } from "./wc"; -import type { PropType, InjectionKey, SlotsType } from "vue"; +import type { PropType, InjectionKey } from "vue"; import type { EChartsType, SetOptionType, @@ -67,13 +66,7 @@ export default defineComponent({ ...loadingProps, }, emits: {} as unknown as Emits, - slots: Object as SlotsType< - Record< - "tooltip" | `tooltip-${string}`, - TooltipComponentFormatterCallbackParams - > & - Record<"dataView" | `dataView-${string}`, Option> - >, + slots: Object as SlotsTypes, inheritAttrs: false, setup(props, { attrs, expose, slots }) { const root = shallowRef<EChartsElement>(); @@ -103,6 +96,15 @@ export default defineComponent({ const listeners: Map<{ event: string; once?: boolean; zr?: boolean }, any> = new Map(); + const { teleportedSlots, patchOption } = useSlotOption(slots, () => { + if (!manualUpdate.value && props.option && chart.value) { + chart.value.setOption( + patchOption(props.option), + realUpdateOptions.value, + ); + } + }); + // 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 @@ -294,15 +296,6 @@ export default defineComponent({ useAutoresize(chart, autoresize, root); - const { teleportedSlots, patchOption } = useSlotOption(slots, () => { - if (!manualUpdate.value && props.option && chart.value) { - chart.value.setOption( - patchOption(props.option), - realUpdateOptions.value, - ); - } - }); - onMounted(() => { init(); }); diff --git a/src/composables/slot.ts b/src/composables/slot.ts index f01b747..b355526 100644 --- a/src/composables/slot.ts +++ b/src/composables/slot.ts @@ -8,9 +8,10 @@ import { shallowReactive, warn, } from "vue"; -import type { Slots } from "vue"; +import type { Slots, SlotsType } from "vue"; import type { Option } from "../types"; import { isValidArrayIndex, isSameSet } from "../utils"; +import type { TooltipComponentFormatterCallbackParams } from "echarts"; const SLOT_OPTION_PATHS = { tooltip: ["tooltip", "formatter"], @@ -135,3 +136,11 @@ export function useSlotOption(slots: Slots, onSlotsChange: () => void) { patchOption, }; } + +export type SlotsTypes = SlotsType< + Record< + "tooltip" | `tooltip-${string}`, + TooltipComponentFormatterCallbackParams + > & + Record<"dataView" | `dataView-${string}`, Option> +>;