UNPKG

vue-echarts

Version:

Vue.js component for Apache ECharts™.

606 lines (594 loc) • 17.3 kB
import { Teleport, computed, defineComponent, h, inject, nextTick, onBeforeUnmount, onMounted, onUnmounted, onUpdated, shallowReactive, shallowRef, toRefs, toValue, warn, watch, watchEffect } from "vue"; import { init, throttle } from "echarts/core"; //#region src/composables/api.ts const METHOD_NAMES = [ "getWidth", "getHeight", "getDom", "getOption", "resize", "dispatchAction", "convertToPixel", "convertFromPixel", "containPixel", "getDataURL", "getConnectedDataURL", "appendData", "clear", "isDisposed", "dispose" ]; function usePublicAPI(chart) { function makePublicMethod(name) { const fn = function(...args) { if (!chart.value) throw new Error("ECharts is not initialized yet."); return Reflect.apply(chart.value[name], chart.value, args); }; return fn; } return METHOD_NAMES.reduce((acc, name) => { acc[name] = makePublicMethod(name); return acc; }, {}); } //#endregion //#region src/composables/autoresize.ts function useAutoresize(chart, autoresize, root) { watch([ root, chart, autoresize ], ([root$1, chart$1, autoresize$1], _, onCleanup) => { let ro = null; if (root$1 && chart$1 && autoresize$1) { const { offsetWidth, offsetHeight } = root$1; const { throttle: wait = 100, onResize } = autoresize$1 === true ? {} : autoresize$1; let initialResizeTriggered = false; const callback = () => { chart$1.resize(); onResize?.(); }; const resizeCallback = wait ? throttle(callback, wait) : callback; ro = new ResizeObserver(() => { if (!initialResizeTriggered) { initialResizeTriggered = true; if (root$1.offsetWidth === offsetWidth && root$1.offsetHeight === offsetHeight) return; } if (root$1.offsetWidth === 0 || root$1.offsetHeight === 0) return; resizeCallback(); }); ro.observe(root$1); } onCleanup(() => { if (ro) { ro.disconnect(); ro = null; } }); }); } const autoresizeProps = { autoresize: [Boolean, Object] }; //#endregion //#region src/composables/loading.ts const LOADING_OPTIONS_KEY = Symbol(); function useLoading(chart, loading, loadingOptions) { const defaultLoadingOptions = inject(LOADING_OPTIONS_KEY, {}); const realLoadingOptions = computed(() => ({ ...toValue(defaultLoadingOptions), ...loadingOptions?.value })); watchEffect(() => { const instance = chart.value; if (!instance) return; if (loading.value) instance.showLoading(realLoadingOptions.value); else instance.hideLoading(); }); } const loadingProps = { loading: Boolean, loadingOptions: Object }; //#endregion //#region src/utils.ts function isBrowser() { return typeof window !== "undefined" && typeof document !== "undefined"; } const onRE = /^on[^a-z]/; const isOn = (key) => onRE.test(key); function omitOn(attrs) { const result = {}; for (const key in attrs) if (!isOn(key)) result[key] = attrs[key]; return result; } function isValidArrayIndex(key) { const num = Number(key); return Number.isInteger(num) && num >= 0 && num < Math.pow(2, 32) - 1 && String(num) === key; } function isSameSet(a, b) { 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; } function isPlainObject(v) { return v != null && typeof v === "object" && !Array.isArray(v); } const LOG_PREFIX = "[vue-echarts]"; function warn$1(message) { warn(`${LOG_PREFIX} ${message}`); } //#endregion //#region src/composables/slot.ts const SLOT_OPTION_PATHS = { tooltip: ["tooltip", "formatter"], dataView: [ "toolbox", "feature", "dataView", "optionToContent" ] }; const SLOT_PREFIXES = Object.keys(SLOT_OPTION_PATHS); function isValidSlotName(key) { return SLOT_PREFIXES.some((slotPrefix) => key === slotPrefix || key.startsWith(slotPrefix + "-")); } function useSlotOption(slots, onSlotsChange) { const detachedRoot = isBrowser() ? document.createElement("div") : void 0; const containers = shallowReactive({}); const initialized = shallowReactive({}); const params = shallowReactive({}); const isMounted = shallowRef(false); const teleportedSlots = () => { return isMounted.value && detachedRoot ? h(Teleport, { to: detachedRoot }, Object.entries(slots).filter(([key]) => isValidSlotName(key)).map(([key, slot]) => { const slotName = key; return h("div", { ref: (el) => { if (el instanceof HTMLElement) containers[slotName] = el; }, style: { display: "contents" } }, initialized[slotName] ? slot?.(params[slotName]) : void 0); })) : void 0; }; function isObject(val) { return val !== null && typeof val === "object" && !Array.isArray(val); } function patchOption(src) { const root = { ...src }; const ensureChild = (parent, seg) => { const next = parent[seg]; if (Array.isArray(next)) { parent[seg] = [...next]; return parent[seg]; } if (isObject(next)) { parent[seg] = { ...next }; return parent[seg]; } if (next === void 0) { parent[seg] = isValidArrayIndex(seg) ? [] : {}; return parent[seg]; } }; Object.keys(slots).filter((key) => { const valid = isValidSlotName(key); if (!valid) warn$1(`Invalid slot name: ${key}`); return valid; }).forEach((key) => { const [prefix, ...rest] = key.split("-"); const tail = SLOT_OPTION_PATHS[prefix]; if (!tail) return; const path = [...rest, ...tail]; if (path.length === 0) return; let cur = root; for (let i = 0; i < path.length - 1; i++) { cur = ensureChild(cur, path[i]); if (!cur) return; } cur[path[path.length - 1]] = (p) => { initialized[key] = true; params[key] = p; return containers[key]; }; }); return root; } let slotNames = []; onUpdated(() => { const newSlotNames = Object.keys(slots).filter(isValidSlotName); if (!isSameSet(newSlotNames, slotNames)) { slotNames.forEach((key) => { if (!newSlotNames.includes(key)) { delete params[key]; delete initialized[key]; delete containers[key]; } }); slotNames = newSlotNames; onSlotsChange(); } }); onMounted(() => { isMounted.value = true; }); onUnmounted(() => { detachedRoot?.remove(); }); return { teleportedSlots, patchOption }; } //#endregion //#region src/wc.ts let registered = null; const TAG_NAME = "x-vue-echarts"; function register() { if (registered != null) return registered; const registry = globalThis.customElements; if (!isBrowser() || !registry?.get) { registered = false; return registered; } if (!registry.get(TAG_NAME)) try { class ECElement extends HTMLElement { __dispose = null; disconnectedCallback() { if (this.__dispose) { this.__dispose(); this.__dispose = null; } } } registry.define(TAG_NAME, ECElement); } catch { registered = false; return registered; } registered = true; return registered; } //#endregion //#region src/update.ts /** * Read an item's `id` as a string. * Only accept string or number. Other types are ignored to surface inconsistent data early. */ function readId(item) { if (!isPlainObject(item)) return; const raw = item.id; if (typeof raw === "string") return raw; if (typeof raw === "number" && Number.isFinite(raw)) return String(raw); } /** * Build a minimal signature from a full ECharts option. * Only top-level keys are inspected. */ function buildSignature(option) { const opt = option; const optionsLength = Array.isArray(opt.options) ? opt.options.length : 0; const mediaLength = Array.isArray(opt.media) ? opt.media.length : 0; const arrays = Object.create(null); const objects = []; const scalars = []; for (const key of Object.keys(opt)) { if (key === "options" || key === "media") continue; const value = opt[key]; if (Array.isArray(value)) { const items = value; const ids = /* @__PURE__ */ new Set(); let noIdCount = 0; for (let i = 0; i < items.length; i++) { const id = readId(items[i]); if (id !== void 0) ids.add(id); else noIdCount++; } arrays[key] = { idsSorted: ids.size > 0 ? Array.from(ids).sort() : [], noIdCount }; } else if (isPlainObject(value)) objects.push(key); else if (value !== void 0) scalars.push(key); } if (objects.length > 1) objects.sort(); if (scalars.length > 1) scalars.sort(); return { optionsLength, mediaLength, arrays, objects, scalars }; } function diffKeys(prevKeys, nextKeys) { if (prevKeys.length === 0) return []; if (nextKeys.length === 0) return prevKeys.slice(); const nextSet = new Set(nextKeys); const missing = []; for (let i = 0; i < prevKeys.length; i++) { const key = prevKeys[i]; if (!nextSet.has(key)) missing.push(key); } return missing; } function hasMissingIds(prevIds, nextIds) { if (prevIds.length === 0) return false; if (nextIds.length === 0) return true; const nextSet = new Set(nextIds); for (let i = 0; i < prevIds.length; i++) if (!nextSet.has(prevIds[i])) return true; return false; } /** * Produce an update plan plus a normalized option that encodes common deletions. * Falls back to `notMerge: true` when the change looks complex. */ function planUpdate(prev, option) { const next = buildSignature(option); if (!prev) return { option, signature: next, plan: { notMerge: false } }; if (next.optionsLength < prev.optionsLength) return { option, signature: next, plan: { notMerge: true } }; if (next.mediaLength < prev.mediaLength) return { option, signature: next, plan: { notMerge: true } }; if (diffKeys(prev.scalars, next.scalars).length > 0) return { option, signature: next, plan: { notMerge: true } }; const replace = /* @__PURE__ */ new Set(); const overrides = /* @__PURE__ */ new Map(); const missingObjects = diffKeys(prev.objects, next.objects); for (let i = 0; i < missingObjects.length; i++) overrides.set(missingObjects[i], null); for (const key of Object.keys(prev.arrays)) { const prevArray = prev.arrays[key]; if (!prevArray) continue; const nextArray = next.arrays[key]; if (!nextArray) { if (prevArray.idsSorted.length > 0 || prevArray.noIdCount > 0) { overrides.set(key, []); replace.add(key); } continue; } if (hasMissingIds(prevArray.idsSorted, nextArray.idsSorted)) { replace.add(key); continue; } if (nextArray.noIdCount < prevArray.noIdCount) replace.add(key); } let normalizedOption = option; let signature = next; if (overrides.size > 0) { const clone = { ...option }; overrides.forEach((value, key) => { clone[key] = value; }); normalizedOption = clone; signature = buildSignature(normalizedOption); } const replaceMerge = replace.size > 0 ? Array.from(replace).sort() : void 0; return { option: normalizedOption, signature, plan: replaceMerge ? { notMerge: false, replaceMerge } : { notMerge: false } }; } //#endregion //#region src/style.css?raw var style_default = "x-vue-echarts{display:block;width:100%;height:100%;min-width:0;}\nx-vue-echarts>:first-child,x-vue-echarts>:first-child>canvas{border-radius:inherit;}\n"; //#endregion //#region src/style.ts if (typeof document !== "undefined") if (Array.isArray(document.adoptedStyleSheets) && "replaceSync" in CSSStyleSheet.prototype) { const sheet = new CSSStyleSheet(); sheet.replaceSync(style_default); document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet]; } else { const styleEl = document.createElement("style"); styleEl.textContent = style_default; document.head.appendChild(styleEl); } //#endregion //#region src/ECharts.ts const wcRegistered = register(); const THEME_KEY = Symbol(); const INIT_OPTIONS_KEY = Symbol(); const UPDATE_OPTIONS_KEY = Symbol(); var ECharts_default = defineComponent({ name: "Echarts", inheritAttrs: false, props: { option: Object, theme: { type: [Object, String] }, initOptions: Object, updateOptions: Object, group: String, manualUpdate: Boolean, ...autoresizeProps, ...loadingProps }, emits: {}, slots: Object, setup(props, { attrs, expose, slots }) { const root = shallowRef(); const chart = shallowRef(); const defaultTheme = inject(THEME_KEY, null); const defaultInitOptions = inject(INIT_OPTIONS_KEY, null); const defaultUpdateOptions = inject(UPDATE_OPTIONS_KEY, null); const { autoresize, manualUpdate, loading, loadingOptions } = toRefs(props); const realTheme = computed(() => props.theme || toValue(defaultTheme)); const realInitOptions = computed(() => props.initOptions || toValue(defaultInitOptions) || void 0); const realUpdateOptions = computed(() => props.updateOptions || toValue(defaultUpdateOptions)); const nonEventAttrs = computed(() => omitOn(attrs)); const nativeListeners = {}; const listeners = /* @__PURE__ */ new Map(); const { teleportedSlots, patchOption } = useSlotOption(slots, () => { if (!manualUpdate.value && props.option && chart.value) applyOption(chart.value, props.option); }); let lastSignature; function resolveUpdateOptions(plan) { const result = {}; const replacements = (plan?.replaceMerge ?? []).filter((key) => key != null); if (replacements.length > 0) result.replaceMerge = [...new Set(replacements)]; if (plan?.notMerge !== void 0) result.notMerge = plan.notMerge; return result; } function applyOption(instance, option, override, manual = false) { const patched = patchOption(option); if (manual) { instance.setOption(patched, override ?? {}); lastSignature = void 0; return; } if (realUpdateOptions.value) { const updateOptions$1 = override ?? realUpdateOptions.value; instance.setOption(patched, updateOptions$1); lastSignature = void 0; return; } const planned = planUpdate(lastSignature, patched); const updateOptions = resolveUpdateOptions(planned.plan); instance.setOption(planned.option, updateOptions); lastSignature = planned.signature; } Object.keys(attrs).filter((key) => isOn(key)).forEach((key) => { if (key.indexOf("Native:") === 2) { const nativeKey = `on${key.charAt(9).toUpperCase()}${key.slice(10)}`; nativeListeners[nativeKey] = attrs[key]; return; } let event = key.charAt(2).toLowerCase() + key.slice(3); let zr; if (event.indexOf("zr:") === 0) { zr = true; event = event.substring(3); } let once; if (event.substring(event.length - 4) === "Once") { once = true; event = event.substring(0, event.length - 4); } listeners.set({ event, zr, once }, attrs[key]); }); function init$1() { if (!root.value) return; const instance = chart.value = init(root.value, realTheme.value, realInitOptions.value); if (props.group) instance.group = props.group; listeners.forEach((handler, { zr, once, event }) => { if (!handler) return; const target = zr ? instance.getZr() : instance; if (once) { const raw = handler; let called = false; handler = (...args) => { if (called) return; called = true; raw(...args); target.off(event, handler); }; } target.on(event, handler); }); function resize() { if (instance && !instance.isDisposed()) instance.resize(); } function commit() { const { option } = props; if (manualUpdate.value) { if (option) applyOption(instance, option, void 0, true); return; } if (option) applyOption(instance, option); } if (autoresize.value) nextTick(() => { resize(); commit(); }); else commit(); } const setOption = (option, notMerge, lazyUpdate) => { if (!props.manualUpdate) { warn$1("`setOption` is only available when `manual-update` is `true`."); return; } const updateOptions = typeof notMerge === "boolean" ? { notMerge, lazyUpdate } : notMerge; if (!chart.value) return; applyOption(chart.value, option, updateOptions ?? void 0, true); }; function cleanup() { if (chart.value) { chart.value.dispose(); chart.value = void 0; } lastSignature = void 0; } watch(() => props.option, (option) => { if (!option) { lastSignature = void 0; return; } if (manualUpdate.value) { warn$1("`option` prop changes are ignored when `manual-update` is `true`."); return; } if (!chart.value) return; applyOption(chart.value, option); }, { deep: true }); watch([manualUpdate, realInitOptions], () => { cleanup(); init$1(); }, { deep: true }); watch(realTheme, (theme) => { chart.value?.setTheme(theme || {}); }, { deep: true }); watchEffect(() => { if (props.group && chart.value) chart.value.group = props.group; }); const publicApi = usePublicAPI(chart); useLoading(chart, loading, loadingOptions); useAutoresize(chart, autoresize, root); onMounted(() => { init$1(); }); onBeforeUnmount(() => { if (wcRegistered && root.value) root.value.__dispose = cleanup; else cleanup(); }); expose({ setOption, root, chart, ...publicApi }); return (() => h(TAG_NAME, { ...nonEventAttrs.value, ...nativeListeners, ref: root, class: ["echarts", nonEventAttrs.value.class] }, teleportedSlots())); } }); //#endregion //#region src/index.ts var src_default = ECharts_default; //#endregion export { INIT_OPTIONS_KEY, LOADING_OPTIONS_KEY, THEME_KEY, UPDATE_OPTIONS_KEY, src_default as default }; //# sourceMappingURL=index.js.map