ECharts integration
markstream-vue supports rendering ECharts charts through custom components. This page covers three recommended approaches, from simplest to most advanced.
Why ECharts with markstream-vue?
ECharts is a powerful charting library that works naturally with the streaming architecture of markstream-vue. You can:
- Write chart options directly in Markdown code blocks
- Stream chart updates progressively (ideal for AI-generated content)
- Reuse existing ECharts themes and configurations
- Export charts as images
Approach 1: Custom CodeBlock component (Recommended)
This approach treats ECharts as a special code block language, similar to how Mermaid is handled. It's the most user-friendly and follows the existing architecture pattern.
1.1 Create the ECharts component
Create EChartsBlockNode.vue:
<script setup lang="ts">
import * as echarts from 'echarts'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
interface Props {
node: {
type: 'code_block'
language: string
code: string
loading?: boolean
}
isDark?: boolean
}
const props = defineProps<Props>()
// Only render echarts code blocks
const isECharts = computed(() => props.node.language === 'echarts' || props.node.language === 'chart')
const chartRef = ref<HTMLElement>()
let chartInstance: echarts.ECharts | null = null
async function initChart() {
if (!isECharts.value || !chartRef.value)
return
// Dispose existing instance
if (chartInstance) {
chartInstance.dispose()
}
// Initialize with theme based on dark mode
const theme = props.isDark ? 'dark' : undefined
chartInstance = echarts.init(chartRef.value, theme)
try {
// Parse JSON option from code
const option = JSON.parse(props.node.code)
chartInstance.setOption(option, true)
}
catch (error) {
console.error('[ECharts] Invalid option:', error)
chartInstance?.dispose()
chartInstance = null
}
}
// Reinitialize on theme change
watch(() => props.isDark, () => {
initChart()
})
// Reinitialize on code change
watch(() => props.node.code, () => {
if (chartInstance) {
try {
const option = JSON.parse(props.node.code)
chartInstance.setOption(option, true)
}
catch (error) {
console.error('[ECharts] Invalid option:', error)
}
}
})
onMounted(initChart)
onBeforeUnmount(() => {
chartInstance?.dispose()
chartInstance = null
})
</script>
<template>
<div v-if="isECharts" ref="chartRef" class="echarts-container" style="width: 100%; height: 400px" />
<slot v-else />
</template>
<style scoped>
.echarts-container {
margin: 1rem 0;
border-radius: 0.5rem;
overflow: hidden;
}
</style>1.2 Register the custom component
// main.ts or your component entry
import { setCustomComponents } from 'markstream-vue'
import EChartsBlockNode from './components/EChartsBlockNode.vue'
setCustomComponents({
code_block: EChartsBlockNode
})1.3 Use in Markdown
Now you can write ECharts charts directly in Markdown:
\```echarts
{
"title": { "text": "Sales Data" },
"tooltip": {},
"xAxis": {
"type": "category",
"data": ["Mon", "Tue", "Wed", "Thu", "Fri"]
},
"yAxis": { "type": "value" },
"series": [{
"type": "bar",
"data": [120, 200, 150, 80, 70]
}]
}
\```1.4 Install ECharts
pnpm add echarts
# or
npm install echartsPros & Cons
Pros:
- Familiar code block syntax
- Follows existing Mermaid pattern
- Works with streaming updates
- Easy to implement
Cons:
- Overrides all
code_blockrendering (need to handle non-echarts blocks) - Requires JSON parsing (no JS expressions)
Approach 2: Custom HTML tag
Use a custom HTML-like tag for ECharts charts, providing clearer separation from regular code blocks.
2.1 Configure custom HTML tags
// parser configuration
import { createMarkdownParser } from 'markstream-vue/parser'
const parser = createMarkdownParser({
customHtmlTags: ['echarts', 'chart']
})2.2 Register the component
import { setCustomComponents } from 'markstream-vue'
import EChartsComponent from './components/EChartsComponent.vue'
setCustomComponents({
echarts: EChartsComponent,
chart: EChartsComponent
})2.3 Create the component
<script setup lang="ts">
import * as echarts from 'echarts'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
interface Props {
node: {
type: string
tag: string
content: string
attrs?: Record<string, string>
}
isDark?: boolean
}
const props = defineProps<Props>()
const chartRef = ref<HTMLElement>()
let chartInstance: echarts.ECharts | null = null
// Parse attributes for width/height
const width = computed(() => props.node.attrs?.width || '100%')
const height = computed(() => props.node.attrs?.height || '400px')
async function initChart() {
if (!chartRef.value)
return
if (chartInstance) {
chartInstance.dispose()
}
const theme = props.isDark ? 'dark' : undefined
chartInstance = echarts.init(chartRef.value, theme)
try {
const option = JSON.parse(props.node.content)
chartInstance.setOption(option, true)
}
catch (error) {
console.error('[ECharts] Invalid option:', error)
}
}
watch(() => props.isDark, initChart)
watch(() => props.node.content, () => {
if (chartInstance) {
try {
const option = JSON.parse(props.node.content)
chartInstance.setOption(option, true)
}
catch (error) {
console.error('[ECharts] Invalid option:', error)
}
}
})
onMounted(initChart)
onBeforeUnmount(() => {
chartInstance?.dispose()
})
</script>
<template>
<div ref="chartRef" class="echarts-chart" :style="{ width, height }" />
</template>2.4 Use in Markdown
<echarts width="100%" height="500px">
{
"series": [{
"type": "pie",
"data": [
{ "value": 335, "name": "Direct" },
{ "value": 310, "name": "Email" },
{ "value": 234, "name": "Affiliate" }
]
}]
}
</echarts>Pros & Cons
Pros:
- Clear type separation
- Supports custom attributes (width, height)
- Doesn't interfere with code blocks
Cons:
- Requires parser configuration
- Less familiar syntax
- Not standard Markdown
Approach 3: VmrContainer integration
Use ECharts inside a custom container (similar to admonitions), ideal for documentation with chart descriptions.
3.1 Register container component
import { setCustomComponents } from 'markstream-vue'
import EChartsContainerNode from './components/EChartsContainerNode.vue'
setCustomComponents({
vmr_container: EChartsContainerNode
})3.2 Create the container component
<script setup lang="ts">
import * as echarts from 'echarts'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
interface Props {
node: {
type: 'vmr_container'
name: string
children?: Array<{ type: string, raw: string }>
}
isDark?: boolean
}
const props = defineProps<Props>()
// Only handle echarts containers
const isEChartsContainer = computed(() => props.node.name === 'echarts')
const chartRef = ref<HTMLElement>()
let chartInstance: echarts.ECharts | null = null
// Extract JSON from children
const chartOption = computed(() => {
if (!props.node.children || props.node.children.length === 0) {
return null
}
const code = props.node.children[0].raw
try {
return JSON.parse(code)
}
catch {
return null
}
})
async function initChart() {
if (!isEChartsContainer.value || !chartRef.value || !chartOption.value)
return
if (chartInstance) {
chartInstance.dispose()
}
const theme = props.isDark ? 'dark' : undefined
chartInstance = echarts.init(chartRef.value, theme)
chartInstance.setOption(chartOption.value, true)
}
watch(() => props.isDark, initChart)
watch(chartOption, (option) => {
if (chartInstance && option) {
chartInstance.setOption(option, true)
}
})
onMounted(initChart)
onBeforeUnmount(() => {
chartInstance?.dispose()
})
</script>
<template>
<div v-if="isEChartsContainer" class="vmr-container vmr-container-echarts">
<div ref="chartRef" style="width: 100%; height: 400px" />
<slot v-if="!chartOption" />
</div>
<div v-else class="vmr-container" :class="`vmr-container-${node.name}`">
<slot />
</div>
</template>
<style scoped>
.vmr-container-echarts {
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
margin: 1rem 0;
}
.dark .vmr-container-echarts {
border-color: #374151;
}
</style>3.3 Use in Markdown
:::echarts
{
"title": { "text": "Traffic Sources" },
"series": [{
"type": "pie",
"radius": "50%",
"data": [
{ "value": 1048, "name": "Search" },
{ "value": 735, "name": "Direct" },
{ "value": 580, "name": "Marketing" }
]
}]
}
:::Pros & Cons
Pros:
- Consistent with existing container system
- Supports additional content around charts
- Good for documentation
Cons:
- More complex setup
- Requires parsing children
SSR Considerations
ECharts requires the DOM and doesn't support server-side rendering. Use one of these strategies:
For Nuxt 3
<template>
<ClientOnly>
<MarkdownRender :content="markdown" />
</ClientOnly>
</template>For SSR with fallback
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const isClient = ref(false)
onMounted(() => {
isClient.value = true
})
</script>
<template>
<MarkdownRender v-if="isClient" :content="markdown" />
<div v-else class="echarts-placeholder">
Loading chart...
</div>
</template>Check for window in component
const canRender = computed(() => typeof window !== 'undefined' && isECharts.value)Advanced Features
ResizeObserver for responsive charts
import { useResizeObserver } from '@vueuse/core'
useResizeObserver(chartRef, (entries) => {
if (chartInstance && entries[0]) {
const { width, height } = entries[0].contentRect
if (width > 0 && height > 0) {
chartInstance.resize()
}
}
})Export as image
function exportChart() {
if (!chartInstance)
return
const url = chartInstance.getDataURL({
type: 'png',
pixelRatio: 2,
backgroundColor: props.isDark ? '#1a1a1a' : '#fff'
})
const link = document.createElement('a')
link.href = url
link.download = 'chart.png'
link.click()
}Theme switching with ECharts themes
import dark from 'echarts/theme/dark'
const theme = computed(() => props.isDark ? 'dark' : undefined)
chartInstance = echarts.init(chartRef.value, theme.value)Which approach should I choose?
| Approach | Best for | Complexity |
|---|---|---|
| CodeBlock | Most users, simple integration | ⭐ |
| Custom HTML tag | Advanced users, multiple chart types | ⭐⭐ |
| VmrContainer | Documentation, styled containers | ⭐⭐⭐ |
Recommendation: Start with Approach 1 (CodeBlock). It's the most straightforward and aligns with how users already interact with Mermaid diagrams.
Complete example
Here's a complete minimal setup using Approach 1:
import MarkdownRender, { setCustomComponents } from 'markstream-vue'
// main.ts
import { createApp } from 'vue'
import EChartsBlockNode from './EChartsBlockNode.vue'
import 'markstream-vue/index.css'
import 'echarts'
// Register custom component
setCustomComponents({
code_block: EChartsBlockNode
})
const app = createApp({
components: { MarkdownRender },
template: `
<MarkdownRender :content="markdown" />
`,
data() {
return {
markdown: `<!-- eslint-disable -->
\`\`\`echarts
{
"xAxis": { "type": "category", "data": ["A", "B", "C"] },
"yAxis": { "type": "value" },
"series": [{ "type": "bar", "data": [10, 20, 30] }]
}
\`\`\``
}
}
})
app.mount('#app')Troubleshooting
Chart not rendering — Check browser console for JSON parse errors. Ensure the code block content is valid JSON.
Blank container — Verify ECharts is installed and the DOM element has dimensions. Set explicit width/height styles.
Theme not switching — ECharts instances need to be re-created when changing themes. Use
watch(() => props.isDark)to dispose and reinitialize.SSR errors — Wrap the component in
<ClientOnly>(Nuxt) or checktypeof window !== 'undefined'before initializing.Memory leaks — Always call
chartInstance.dispose()inonBeforeUnmount.
Still having issues? Check the ECharts documentation or open an issue on GitHub.