feat: vue-echarts-next first version

This commit is contained in:
Justineo
2021-02-07 16:16:13 +08:00
parent e4ff8e68e0
commit ee12ad9658
22 changed files with 3029 additions and 855 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
dist

3049
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,22 @@
{ {
"name": "vue-echarts-next", "name": "vue-echarts-next",
"version": "0.1.0", "version": "0.1.0",
"private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build:demo": "vue-cli-service build",
"build": "rollup -c rollup.config.js",
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"main": "dist/index.cjs.min.js",
"module": "dist/index.esm.min.js",
"unpkg": "dist/index.umd.min.js",
"jsdelivr": "dist/index.umd.min.js",
"dependencies": { "dependencies": {
"core-js": "^3.6.5", "core-js": "^3.6.5",
"vue": "^3.0.0" "resize-detector": "^0.2.2"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-node-resolve": "^11.1.1",
"@typescript-eslint/eslint-plugin": "^2.33.0", "@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0", "@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-babel": "~4.5.0",
@ -19,12 +24,24 @@
"@vue/cli-plugin-typescript": "~4.5.0", "@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-service": "~4.5.0", "@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0", "@vue/compiler-sfc": "^3.0.0",
"@vue/composition-api": "^1.0.0-rc.1",
"@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^5.0.2", "@vue/eslint-config-typescript": "^5.0.2",
"echarts": "^5.0.2",
"eslint": "^6.7.2", "eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.3", "eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^7.0.0-0", "eslint-plugin-vue": "^7.0.0-0",
"postcss": "^8.2.5",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"typescript": "~3.9.3" "rollup": "^2.38.5",
"rollup-plugin-postcss": "^4.0.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.29.0",
"typescript": "^4.1.3",
"vue": "npm:vue@^3.0.5"
},
"peerDependencies": {
"echarts": "^5.0.2",
"vue": "^2.6.11 || ^3.0.0"
} }
} }

98
rollup.config.js Normal file
View File

@ -0,0 +1,98 @@
import typescript from "rollup-plugin-typescript2";
import { terser } from "rollup-plugin-terser";
import postcss from "rollup-plugin-postcss";
import resolve from "@rollup/plugin-node-resolve";
/** @type {import('rollup').RollupOptions} */
const options = [
{
plugins: [
typescript({
useTsconfigDeclarationDir: true
}),
postcss()
],
external: ["vue", "echarts/core", "resize-detector"],
input: "src/index.ts",
output: [
{
file: "dist/index.esm.js",
format: "es",
sourcemap: true
},
{
file: "dist/index.esm.min.js",
format: "es",
sourcemap: true,
plugins: [
terser({
format: {
comments: false
}
})
]
},
{
file: "dist/index.cjs.js",
format: "cjs",
exports: "default",
sourcemap: true
},
{
file: "dist/index.cjs.min.js",
format: "cjs",
exports: "default",
sourcemap: true,
plugins: [
terser({
format: {
comments: false
}
})
]
}
]
},
{
plugins: [
resolve(),
typescript({
useTsconfigDeclarationDir: true
}),
postcss()
],
external: ["vue", "echarts/core"],
input: "src/all.ts",
output: [
{
file: "dist/index.umd.js",
format: "umd",
name: "VueECharts",
sourcemap: true,
globals: {
vue: "Vue",
"echarts/core": "echarts"
}
},
{
file: "dist/index.umd.min.js",
format: "umd",
name: "VueECharts",
sourcemap: true,
globals: {
vue: "Vue",
"echarts/core": "echarts"
},
plugins: [
terser({
format: {
comments: false
}
})
]
}
]
}
];
export default options;

View File

@ -1,27 +0,0 @@
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
</template>
<script lang="ts">
import { defineComponent } from "vue";
import HelloWorld from "./components/HelloWorld.vue";
export default defineComponent({
name: "App",
components: {
HelloWorld
}
});
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

195
src/ECharts.ts Normal file
View File

@ -0,0 +1,195 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
defineComponent,
ref,
shallowRef,
toRefs,
watch,
computed,
inject,
onMounted,
onUnmounted,
h,
PropType
} from "vue";
import { init as initChart } from "echarts/core";
import { EChartsType, OptionType } from "@/types";
import {
usePublicAPI,
useAutoresize,
autoresizeProps,
useLoading,
loadingProps
} from "./composables";
import "./style.css";
type InitParameters = Parameters<typeof initChart>;
type ThemeParameter = InitParameters[1];
type InitOptsParameter = InitParameters[2];
export default defineComponent({
name: "echarts",
props: {
options: Object as PropType<OptionType>,
theme: {
type: [Object, String] as PropType<ThemeParameter>
},
initOptions: Object as PropType<InitOptsParameter>,
group: String,
manualUpdate: Boolean,
...autoresizeProps,
...loadingProps
},
setup(props, { attrs }) {
const defaultInitOptions = inject(
"echartsInitOptions",
{}
) as InitOptsParameter;
const root = ref<HTMLElement>();
const chart = shallowRef<EChartsType>();
const manualOptions = shallowRef<OptionType>();
const realOptions = computed(
() => manualOptions.value || props.options || Object.create(null)
);
function init(options?: OptionType) {
if (chart.value || !root.value) {
return;
}
const instance = (chart.value = initChart(
root.value,
props.theme,
props.initOptions || defaultInitOptions
));
if (props.group) {
instance.group = props.group;
}
Object.keys(attrs)
.filter(key => key.indexOf(`on`) === 0)
.forEach(key => {
const handler = attrs[key] as any;
if (!handler) {
return;
}
if (key.indexOf("onZr:") === 0) {
instance.getZr().on(key.slice(5).toLowerCase(), handler);
} else {
instance.on(key.slice(2).toLowerCase(), handler);
}
});
instance.setOption(options || realOptions.value, true);
}
function mergeOptions(options: OptionType, ...rest: any[]) {
if (props.manualUpdate) {
manualOptions.value = options;
}
if (!chart.value) {
init(options);
} else {
chart.value.setOption(options, ...rest);
}
}
function cleanup() {
if (chart.value) {
chart.value.dispose();
chart.value = undefined;
}
}
const {
theme,
initOptions,
group,
autoresize,
manualUpdate,
loading,
loadingOptions
} = toRefs(props);
let unwatchOptions: (() => void) | null = null;
watch(
manualUpdate,
manualUpdate => {
if (typeof unwatchOptions === "function") {
unwatchOptions();
unwatchOptions = null;
}
if (!manualUpdate) {
unwatchOptions = watch(
() => props.options,
(val, oldVal) => {
if (!val) {
return;
}
if (!chart.value) {
init();
} else {
// mutating `options` will lead to merging
// replacing it with new reference will lead to not merging
// eg.
// `this.options = Object.assign({}, this.options, { ... })`
// will trigger `this.chart.setOption(val, true)
// `this.options.title.text = 'Trends'`
// will trigger `this.chart.setOption(val, false)`
chart.value.setOption(val, val !== oldVal);
}
},
{ deep: true }
);
}
},
{
immediate: true
}
);
watch([theme, initOptions], () => {
cleanup();
init();
});
watch(
() => group,
group => {
if (group && group.value && chart.value) {
chart.value.group = group.value;
}
}
);
const publicApi = usePublicAPI(chart, init);
useLoading(chart, loading, loadingOptions);
useAutoresize(chart, autoresize, root, realOptions);
onMounted(() => {
if (props.options) {
init();
}
});
onUnmounted(cleanup);
return {
root,
mergeOptions,
...publicApi
};
},
render() {
return h("div", {
ref: "root",
class: "echarts"
});
}
});

3
src/all.ts Normal file
View File

@ -0,0 +1,3 @@
import "echarts";
export { default } from "./ECharts";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,124 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br />
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener"
>vue-cli documentation</a
>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
target="_blank"
rel="noopener"
>babel</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
target="_blank"
rel="noopener"
>typescript</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
target="_blank"
rel="noopener"
>eslint</a
>
</li>
</ul>
<h3>Essential Links</h3>
<ul>
<li>
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
</li>
<li>
<a href="https://forum.vuejs.org" target="_blank" rel="noopener"
>Forum</a
>
</li>
<li>
<a href="https://chat.vuejs.org" target="_blank" rel="noopener"
>Community Chat</a
>
</li>
<li>
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
>Twitter</a
>
</li>
<li>
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
</li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li>
<a href="https://router.vuejs.org" target="_blank" rel="noopener"
>vue-router</a
>
</li>
<li>
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
</li>
<li>
<a
href="https://github.com/vuejs/vue-devtools#vue-devtools"
target="_blank"
rel="noopener"
>vue-devtools</a
>
</li>
<li>
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
>vue-loader</a
>
</li>
<li>
<a
href="https://github.com/vuejs/awesome-vue"
target="_blank"
rel="noopener"
>awesome-vue</a
>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "HelloWorld",
props: {
msg: String
}
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

54
src/composables/api.ts Normal file
View File

@ -0,0 +1,54 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Ref } from "vue";
import { EChartsType, OptionType } from "@/types";
const METHOD_NAMES = [
"getWidth",
"getHeight",
"getDom",
"getOption",
"resize",
"dispatchAction",
"convertToPixel",
"convertFromPixel",
"getDataURL",
"getConnectedDataURL",
"appendData",
"clear",
"isDisposed",
"dispose"
] as const;
type MethodName = typeof METHOD_NAMES[number];
type PublicMethods = Pick<EChartsType, MethodName>;
export function usePublicAPI(
chart: Ref<EChartsType | undefined>,
init: (options?: OptionType) => void
) {
function makePublicMethod<T extends MethodName>(
name: T
): (...args: Parameters<EChartsType[T]>) => ReturnType<EChartsType[T]> {
return (...args) => {
if (!chart.value) {
init();
}
if (!chart.value) {
throw new Error("ECharts is not initialized yet.");
}
return (chart.value[name] as any).apply(chart.value, args);
};
}
function makePublicMethods(): PublicMethods {
const methods = Object.create(null);
METHOD_NAMES.forEach(name => {
methods[name] = makePublicMethod(name);
});
return methods as PublicMethods;
}
return makePublicMethods();
}

View File

@ -0,0 +1,50 @@
import { Ref, watch } from "vue";
import { addListener, removeListener, ResizeCallback } from "resize-detector";
import { EChartsType, OptionsType } from "@/types";
export function useAutoresize(
chart: Ref<EChartsType | undefined>,
autoresize: Ref<boolean>,
root: Ref<HTMLElement | undefined>,
options: Ref<OptionsType>
): void {
let resizeListener: ResizeCallback | null = null;
let lastArea = 0;
function getArea() {
const el = root.value;
if (!el) {
return 0;
}
return el.offsetWidth * el.offsetHeight;
}
watch([root, chart, autoresize], ([root, chart, autoresize], _, cleanup) => {
if (root && chart && autoresize) {
lastArea = getArea();
resizeListener = () => {
if (lastArea === 0) {
chart.setOption(Object.create(null), true);
chart.resize();
chart.setOption(options.value, true);
} else {
chart.resize();
}
lastArea = getArea();
};
addListener(root, resizeListener);
}
cleanup(() => {
if (resizeListener && root) {
lastArea = 0;
removeListener(root, resizeListener);
}
});
});
}
export const autoresizeProps = {
autoresize: Boolean
};

3
src/composables/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from "./api";
export * from "./autoresize";
export * from "./loading";

View File

@ -0,0 +1,41 @@
import { Ref, PropType, watchEffect } from "vue";
import { EChartsType } from "@/types";
export interface LoadingOptions {
text?: string;
color?: string;
textColor?: string;
maskColor?: string;
zlevel?: number;
fontSize?: number;
showSpinner?: boolean;
spinnerRadius?: number;
lineWidth?: number;
fontWeight?: string | number;
fontStyle?: string;
fontFamily?: string;
}
export function useLoading(
chart: Ref<EChartsType | undefined>,
loading: Ref<boolean>,
loadingOptions?: Ref<LoadingOptions | undefined>
): void {
watchEffect(() => {
const instance = chart.value;
if (!instance) {
return;
}
if (loading.value) {
instance.showLoading(loadingOptions?.value);
} else {
instance.hideLoading();
}
});
}
export const loadingProps = {
loading: Boolean,
loadingOptions: Object as PropType<LoadingOptions>
};

154
src/demo/App.vue Normal file
View File

@ -0,0 +1,154 @@
<template>
<article>
<div class="settings">
<label><input type="checkbox" v-model="autoresize" /> Autoresize</label>
<label
><input type="checkbox" v-model="defaultTheme" /> Default theme</label
>
<label><input type="checkbox" v-model="loading" /> Loading</label>
<label><input type="checkbox" v-model="useSvg" /> Use SVG</label>
<label><input type="checkbox" v-model="useRef" /> Use ref data</label>
<button @click="mutate" :disabled="useRef">Mutate data</button>
<button @click="set" :disabled="!useRef">Set data</button>
<button @click="mutateLoadingOptions" :disabled="!loading">
Mutate loading options
</button>
</div>
<v-chart
style="width: 100%; height: 400px"
ref="foo"
:autoresize="autoresize"
:options="realOptions"
:loading="loading"
:loading-options="loadingOptions"
:theme="defaultTheme ? null : 'dark'"
:init-options="initOptions"
@click="log('echarts')"
@zr:click="log('zr')"
/>
</article>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, ref } from "vue";
import VChart from "../ECharts";
import * as echarts from "echarts/core";
import { GridComponent } from "echarts/components";
import { LineChart } from "echarts/charts";
import { CanvasRenderer, SVGRenderer } from "echarts/renderers";
echarts.use([GridComponent, LineChart, CanvasRenderer, SVGRenderer]);
export default defineComponent({
name: "App",
components: {
VChart
},
setup() {
const foo = ref();
const options = reactive({
xAxis: {
type: "category",
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
},
yAxis: {
type: "value"
},
series: [
{
data: [150, 230, 224, 218, 135, 147, 260],
type: "line"
}
]
});
const optionsRef = ref({
xAxis: {
type: "category",
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
},
yAxis: {
type: "value"
},
series: [
{
data: [233, 128, 184, 302, 208, 287, 212],
type: "line"
}
]
});
const loadingOptions = reactive({
text: "正在加载..."
});
const autoresize = ref<boolean>(true);
const defaultTheme = ref<boolean>(true);
const useSvg = ref<boolean>(false);
const useRef = ref<boolean>(false);
const loading = ref<boolean>(false);
const initOptions = computed(() => ({
renderer: useSvg.value ? "svg" : "canvas"
}));
const realOptions = computed(() =>
useRef.value ? optionsRef.value : options
);
function mutate() {
options.series[0].data = [150, 230, 224, 218, 135, 147, 260].map(
val => val + Math.round(50 * Math.random())
);
}
function set() {
optionsRef.value = {
xAxis: {
type: "category",
data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
},
yAxis: {
type: "value"
},
series: [
{
data: [
233 + Math.round(50 * Math.random()),
128 + Math.round(50 * Math.random()),
184 + Math.round(50 * Math.random()),
302 + Math.round(50 * Math.random()),
208 + Math.round(50 * Math.random()),
287 + Math.round(50 * Math.random()),
212 + Math.round(50 * Math.random())
],
type: "line"
}
]
};
}
function mutateLoadingOptions() {
loadingOptions.text += ".";
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function log(...args: any[]) {
console.log(...args);
}
return {
realOptions,
autoresize,
defaultTheme,
useSvg,
useRef,
initOptions,
mutate,
set,
loading,
loadingOptions,
mutateLoadingOptions,
foo,
log
};
}
});
</script>

1
src/index.ts Normal file
View File

@ -0,0 +1 @@
export { default } from "./ECharts";

4
src/style.css Normal file
View File

@ -0,0 +1,4 @@
.echarts {
width: 400px;
height: 300px;
}

5
src/types.ts Normal file
View File

@ -0,0 +1,5 @@
import { init } from "echarts/core";
export type EChartsType = ReturnType<typeof init>;
type SetOptionType = EChartsType["setOption"];
export type OptionType = Parameters<SetOptionType>[0];

View File

@ -1,39 +1,32 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "esnext", "target": "ES5",
"module": "esnext", "module": "ESNext",
"strict": true, "strict": true,
"jsx": "preserve", "jsx": "preserve",
"importHelpers": true, "importHelpers": true,
"moduleResolution": "node", "moduleResolution": "node",
"removeComments": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "baseUrl": ".",
"types": [ "types": ["webpack-env"],
"webpack-env"
],
"paths": { "paths": {
"@/*": [ "@/*": ["src/*"]
"src/*"
]
}, },
"lib": [ "lib": ["ESNext", "DOM", "DOM.Iterable", "ScriptHost"],
"esnext", // "declaration": true,
"dom", // "declarationDir": "dist"
"dom.iterable",
"scripthost"
]
}, },
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",
"src/**/*.tsx", "src/**/*.tsx",
"src/**/*.vue", "src/**/*.vue",
"tests/**/*.ts", "shims-vue.d.ts",
"tests/**/*.tsx" "src/demo/**/*.ts",
"src/demo/**/*.vue"
], ],
"exclude": [ "exclude": ["node_modules"]
"node_modules"
]
} }

10
vue.config.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
outputDir: "demo",
transpileDependencies: ["resize-detector"],
chainWebpack: config => {
config
.entry("app")
.clear()
.add("./src/demo/main.ts");
}
};