Skip to content

Commit

Permalink
feat: add VChartLight and VChart.server
Browse files Browse the repository at this point in the history
  • Loading branch information
kingyue737 committed Jun 6, 2024
1 parent 273be24 commit c9d688d
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 49 deletions.
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,24 @@

Nuxt Module for Apache ECharts™

> 🚧 **Work in Progress**
> [!IMPORTANT]
>
> Nuxt ECharts is currently in active development and not usable for production yet.
> Nuxt ECharts is currently in active development and based on [experimental `<NuxtIsland>`](https://nuxt.com/docs/api/components/nuxt-island). If you found any issue, design flaw, or have ideas to improve it, please open an [issue](https://github.com/kingyue737/nuxt-echarts/issues) or a [Discussion](https://github.com/kingyue737/nuxt-echarts/discussions).
- [&nbsp;Release Notes](/CHANGELOG.md)
<!-- - [🏀 Online playground](https://stackblitz.com/github/your-org/nuxt-echarts?file=playground%2Fapp.vue) -->
- 🏀 &nbsp;Online playground (WIP)
- 📖 &nbsp;Documentation (WIP)
<!-- - [🏀 Online playground](https://stackblitz.com/github/kingyue737/nuxt-echarts?file=playground%2Fapp.vue) -->
<!-- - [📖 &nbsp;Documentation](https://example.com) -->

## Features (WIP)
## Features

<!-- Highlight some of the features your module provide here -->
-&nbsp;**SSR**: Server-side SVG Rendering with [Nuxt server components](https://nuxt.com/docs/guide/directory-structure/components#server-components)

- &nbsp;**SSR**: experimental server-only component, lightweight client runtime

- 🛠️&nbsp;**Configurable**: import only necessary components and charts for smaller bundle size
- 🦾&nbsp;**Type Strong**: generate ECharts option type based on your config
- ♾️&nbsp;**Client Hydration**: lazy-loading Full ECharts or [lightweight client runtime](https://echarts.apache.org/handbook/en/how-to/cross-platform/server#using-lightweight-runtime)
- 🛠️&nbsp;**Configurable**: import only [necessary functionality](https://echarts.apache.org/handbook/en/basics/import#shrinking-bundle-size) for shrinking bundle size
- 🦾&nbsp;**Type Strong**: auto-import [ECharts option type](https://echarts.apache.org/handbook/en/basics/import#creating-an-option-type-in-typescript) based on your config
- 🌲&nbsp;**Tree-shaking**: Components and ECharts are only included if you use them

## Quick Setup

Expand Down
15 changes: 13 additions & 2 deletions playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { registerTheme } from 'echarts/core'
import theme from './theme.json'
registerTheme('ovilia-green', theme)
// provide(THEME_KEY, 'dark')
function random() {
return Math.round(300 + Math.random() * 700) / 10
}
function getData(): ECOption {
return {
animation: false,
dataset: {
dimensions: ['Product', '2015', '2016', '2017'],
source: [
Expand Down Expand Up @@ -57,20 +59,29 @@ const option = shallowRef(getData())
function refreshData() {
option.value = getData()
}
const initOptions = { height: 400, width: 800 }
function test() {
console.log('jin')
}
</script>

<template>
<div style="width: 800px; height: 400px">
<VChart
:option="option"
theme="ovilia-green"
:init-options="initOptions"
autoresize
:loading="loading"
:loading-options="loadingOptions"
/>
</div>
<div style="width: 800px; height: 400px">
<VChartServer :option="option" />
<div style="width: 800px">
<VChartServer :option="option" :init-options="initOptions" />
</div>
<div style="width: 800px">
<VChartLight :option="option" :init-options="initOptions" @click="test" />
</div>
<button @click="refreshData">Refresh</button>
</template>
21 changes: 8 additions & 13 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
defineNuxtModule,
addPlugin,
createResolver,
addComponent,
addComponentsDir,
addImports,
addTemplate,
addTypeTemplate,
Expand All @@ -24,12 +24,12 @@ export default defineNuxtModule<ModuleOptions>({
renderer: 'canvas',
},
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
const { resolve } = createResolver(import.meta.url)

// Do not add the extension since the `.ts` will be transpiled to `.mjs` after `npm run prepack`
addPlugin(resolver.resolve('./runtime/plugin'))
const entry = resolver.resolve('./runtime/components/VChart')
addComponent({ name: 'VChart', filePath: entry })
addPlugin(resolve('./runtime/plugin'))
addComponentsDir({ path: resolve('runtime/components') })
nuxt.options.css.push(resolve('./runtime/style.css'))

const rendererName =
options.renderer === 'canvas' ? 'CanvasRenderer' : 'SVGRenderer'
Expand Down Expand Up @@ -95,19 +95,14 @@ export default defineNuxtModule<ModuleOptions>({
'UPDATE_OPTIONS_KEY',
'LOADING_OPTIONS_KEY',
]
injectionKeys.forEach((name) => addImports({ name, from: entry }))
injectionKeys.forEach((name) =>
addImports({ name, from: resolve('./runtime/utils/injection') }),
)

if (options.ssr) {
//@ts-expect-error We create the `experimental` object if it doesn't exist yet
nuxt.options.experimental ||= {}
nuxt.options.experimental.componentIslands = true

addComponent({
name: 'VChartServer',
filePath: resolver.resolve(
'runtime/components/VChartServer.server.vue',
),
})
}
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,15 @@ import {
nextTick,
watchEffect,
type PropType,
type InjectionKey,
} from 'vue'
import { init as initChart } from 'echarts/core'
import type {
EChartsType,
EventTarget,
Option,
Theme,
ThemeInjection,
InitOptions,
InitOptionsInjection,
UpdateOptions,
UpdateOptionsInjection,
Emits,
} from '../types'
import {
Expand All @@ -36,20 +32,18 @@ import {
} from '../composables'
import { isOn, omitOn } from '../utils/on'
import { register, TAG_NAME, type EChartsElement } from '../utils/wc'
import '../style.css'
import '#build/echarts.mjs'

const wcRegistered = register()
import {
THEME_KEY,
INIT_OPTIONS_KEY,
UPDATE_OPTIONS_KEY,
} from '../utils/injection'

export const THEME_KEY = 'ecTheme' as unknown as InjectionKey<ThemeInjection>
export const INIT_OPTIONS_KEY =
'ecInitOptions' as unknown as InjectionKey<InitOptionsInjection>
export const UPDATE_OPTIONS_KEY =
'ecUpdateOptions' as unknown as InjectionKey<UpdateOptionsInjection>
export { LOADING_OPTIONS_KEY } from '../composables'
const wcRegistered = register()

export default defineComponent({
name: 'echarts',
name: 'VChart',
props: {
option: Object as PropType<Option>,
theme: {
Expand Down Expand Up @@ -276,7 +270,9 @@ export default defineComponent({

useAutoresize(chart, autoresize, inner)

onMounted(() => {
onMounted(async () => {
// `.client` components are rendered only after being mounted
await nextTick()
init()
})

Expand Down
5 changes: 5 additions & 0 deletions src/runtime/components/VChart.server.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script setup lang="ts"></script>

<template>
<VChartServer v-bind="$attrs" />
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@
import * as echarts from 'echarts'
import { ref } from 'vue'
import { defu } from 'defu'
import type { Option, InitOptions } from '../types'
import type { Option, InitOptions, Theme } from '../types'
const props = defineProps<{ option: Option; initOption?: InitOptions }>()
const props = defineProps<{
option?: Option
initOptions?: InitOptions
theme?: Theme
}>()
const svgStr = ref('')
const initOption: InitOptions = defu(
// echarts.util.merge()
const initOptions: InitOptions = defu(
{ renderer: 'svg', ssr: true },
props.initOption,
{
width: 400,
height: 300,
},
props.initOptions,
)
let chart = echarts.init(null, null, initOption)
chart.setOption(props.option)
let chart = echarts.init(null, props.theme, initOptions)
chart.setOption(props.option || {})
svgStr.value = chart.renderToSVGString()
chart.dispose()
Expand Down
99 changes: 99 additions & 0 deletions src/runtime/components/VChartLight.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script lang="ts">
import {
hydrate,
type ECSSRClientEventParams,
type ECSSREvent,
} from 'echarts/ssr/client/index'
import type { InitOptions, Option, Theme } from '../types'
import { useAttrs, watch, nextTick, ref, onMounted, defineComponent } from 'vue'
import type VChartServer from './VChartServer.vue'
export default defineComponent({
inheritAttrs: false,
emits: {} as unknown as Record<ECSSREvent, ECSSRHandler>,
})
</script>

<script setup lang="ts">
const props = defineProps<{
option?: Option
theme?: Theme
initOptions?: InitOptions
}>()
defineOptions({ inheritAttrs: false })
// defineEmits<{
// (e: ECSSREvent, params: ECSSRClientEventParams): string | undefined
// }>()
type ECSSRHandler = (params: ECSSRClientEventParams) => string | undefined
type ECSSREventOn = `on${Capitalize<ECSSREvent>}`
const root = ref<HTMLElement | null>(null)
const attrs = useAttrs() as Partial<Record<ECSSREventOn, ECSSRHandler>>
let container: HTMLElement
function updateChart(svgStr?: string) {
if (container) {
if (svgStr != null) container.innerHTML = svgStr
// Use the lightweight runtime to give the chart interactive capabilities
hydrate(container, {
on: {
click: attrs.onClick
? (params) => {
console.log('11')
const svg = attrs.onClick!(params)
svg && updateChart(svg)
}
: undefined,
mouseout: attrs.onMouseout
? (params) => {
const svg = attrs.onMouseout!(params)
svg && updateChart(svg)
}
: undefined,
mouseover: attrs.onMouseover
? (params) => {
const svg = attrs.onMouseover!(params)
svg && updateChart(svg)
}
: undefined,
},
})
} else {
console.warn('chart-container not found')
}
}
watch(
[() => props.option, () => props.initOptions, () => props.theme],
async () => {
await nextTick()
updateChart()
},
)
onMounted(async () => {
await nextTick()
container = root.value?.querySelector?.('.vue-echarts-inner') as HTMLElement
updateChart()
const observer = new MutationObserver(() => {
console.log('jin')
updateChart()
})
// call 'observe' on that MutationObserver instance,
// passing it the element to observe, and the options object
observer.observe(container, {
characterData: false,
childList: true,
attributes: false,
})
})
defineExpose({ updateChart })
</script>

<template>
<div ref="root">
<VChartServer :option="option" :init-options="initOptions" :theme="theme" />
</div>
</template>
28 changes: 28 additions & 0 deletions src/runtime/components/VChartServer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { computed, unref, inject } from 'vue'
import type { InitOptions, Option, Theme } from '../types'
import { THEME_KEY, INIT_OPTIONS_KEY } from '../utils/injection'
const defaultTheme = inject(THEME_KEY, null)
const defaultInitOptions = inject(INIT_OPTIONS_KEY, null)
const props = defineProps<{
option?: Option
theme?: Theme
initOptions?: InitOptions
}>()
const realTheme = computed(() => props.theme || unref(defaultTheme) || {})
const realInitOptions = computed(
// @ts-expect-error unknown computed type error
() => props.initOptions || unref(defaultInitOptions) || {},
)
</script>

<template>
<VChartIsland
:theme="realTheme"
:option="option"
:init-options="realInitOptions"
/>
</template>
13 changes: 13 additions & 0 deletions src/runtime/utils/injection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { InjectionKey } from 'vue'
import type {
ThemeInjection,
InitOptionsInjection,
UpdateOptionsInjection,
} from '../types'

export const THEME_KEY = 'ecTheme' as unknown as InjectionKey<ThemeInjection>
export const INIT_OPTIONS_KEY =
'ecInitOptions' as unknown as InjectionKey<InitOptionsInjection>
export const UPDATE_OPTIONS_KEY =
'ecUpdateOptions' as unknown as InjectionKey<UpdateOptionsInjection>
export { LOADING_OPTIONS_KEY } from '../composables'

0 comments on commit c9d688d

Please sign in to comment.