Repository files navigation

Markdown Chart

English | 简体中文

Note

All six @datafe-open/markdown-chart* packages are published on npm. The install commands below use the public packages. Maintainers should follow RELEASING.md for subsequent releases.

markdown-chart provides portable chart blocks for streaming Markdown, with inspectable data and pluggable renderers. Its core is framework-neutral and independent of any chat product.

The project includes independent ECharts and KPI renderers plus adapters for markdown-it, Vue 3, and react-markdown. The registry-based core can accept future Plotly, Vega, or other renderer packages without adding chart-specific switches to the core.

Packages

PackagePurpose
@datafe-open/markdown-chartRenderer registry, canonical markdown-chart routing, and lifecycle controller
@datafe-open/markdown-chart-echartsStrict JSON-only canonical ECharts renderer and deprecated ChatBI legacy adapter
@datafe-open/markdown-chart-kpiStrict responsive multi-KPI cards with optional host-owned reference actions
@datafe-open/markdown-chart-markdown-itSafe placeholder plugin and environment side channel
@datafe-open/markdown-chart-vueVue 3 component and composable
@datafe-open/markdown-chart-reactreact-markdown code/pre adapter

Canonical Markdown

```markdown-chart{ "version": 1, "renderer": "echarts", "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "spec": { "title": { "text": "Monthly sales" }, "xAxis": { "type": "category" }, "yAxis": {}, "series": [{ "type": "bar", "encode": { "x": "month", "y": "sales" } }] }}```

There is only one protocol version, on the outer markdown-chart envelope. data is renderer-neutral so hosts can expose the inline rows independently, for example in a “View data” action. spec belongs to the selected renderer and does not repeat the data or version.

For ECharts, the shared card title comes only from spec.title.text, which is the ECharts option's title.text. If it is absent or blank, the title element is omitted instead of showing a fallback. The Chart/Data controls remain right-aligned, and the chart keeps 8px of vertical spacing from the toolbar.

KPI renderer

KPI cards use the same canonical data/configuration separation as ECharts. A shared default data dataset contains one row per time point; spec binds fields and safe formatting:

```markdown-chart{ "version": 1, "renderer": "kpi", "data": { "kind": "inline", "source": [ { "day": "2026-08-31", "unmet": 0.38, "revenue": 16800000 }, { "day": "2026-09-01", "unmet": 0.4, "revenue": 18000000 } ] }, "spec": { "timeField": "day", "items": [ { "id": "unmet_demand", "title": "Unmet demand", "value": { "field": "unmet", "format": { "style": "percent", "maximumFractionDigits": 0 } }, "status": { "text": { "literal": "At risk" }, "tone": { "literal": "negative" } }, "trend": { "type": "area", "compare": { "lag": 1, "mode": "absolute", "polarity": "lower-is-better" } }, "references": [ { "ref": "docs://metrics/unmet-demand", "label": "Metric definition" } ] }, { "id": "revenue", "title": "Revenue", "value": { "field": "revenue", "format": { "style": "currency", "currency": "CNY", "notation": "compact" } } } ] }}```

The renderer supports 1–12 items, lastNonNull reduction, safe structured Intl.NumberFormat options, field/literal status bindings, and optional line/area sparklines with deterministic lag comparison. Items with fewer than two valid trend points fall back to a plain KPI. A group can freely mix items with and without trends.

Prefer the shared default data when KPI items use the same grain, filters, and source. When they do not, put additional canonical ChartData objects in the top-level datasets map and select one with item.dataset. Omitting dataset selects the default data; the selector is deliberately named dataset so it cannot be confused with KPI references:

{
"version": 1,
"renderer": "kpi",
"data": { "kind": "inline", "source": [{ "revenue": 18000000 }] },
"datasets": {
"inventory": {
"kind": "inline",
"source": [{ "day": "2026-09-01", "stock": 23 }]
}
},
"spec": {
"items": [
{ "id": "revenue", "title": "Revenue", "value": { "field": "revenue" } },
{
"id": "inventory",
"title": "Inventory",
"dataset": "inventory",
"value": { "field": "stock" },
"trend": { "type": "line", "timeField": "day" }
}
]
}
}

Every selected inline/ref dataset is materialized once, even when multiple KPI items select it. Row and cell limits apply to the complete selected collection. The built-in Data view continues to inspect the default data; a named-only KPI group renders without that single-dataset toggle.

Both inline and referenced ChartData are supported. For ref data, provide the host-owned resolver through the adapter's independent kpi option; the result is materialized once for the value, sparkline, comparison, and Data view:

constkpiOptions=useMemo(()=>({validateDataRef: (ref)=>ref.startsWith('dataset://'),resolveDataRef: (ref,{ signal })=>loadDataset(ref,signal),}),[],);<MarkdownChartsource={source}kpi={kpiOptions}/>

Each item accepts up to three references. The renderer treats every reference as opaque and never fetches or navigates. A host selects allowed references and handles clicks through the generic action API. The optional KPI referenceIcon factory lets a trusted host supply its own decorative glyph; the renderer falls back to the link icon and never interprets what the custom icon represents:

<MarkdownChartsource={source}kpi={{referenceIcon: ({ document })=>createHostReferenceIcon(document),}}referenceActions={{canOpen: ({ reference })=>reference.ref.startsWith('docs://'),open: ({ reference })=>openDocumentation(reference.ref),}}/>

Keep the KPI options and referenceActions.canOpen / open callbacks stable across streaming renders so completed cards and resolved data can be reused.

Hosts can inspect canonical data without loading a chart runtime:

pnpm add @datafe-open/markdown-chart
import{parseMarkdownChartEnvelope}from'@datafe-open/markdown-chart';// chartFenceBody is the JSON text inside one markdown-chart fence.const{ data }=parseMarkdownChartEnvelope(chartFenceBody);if(data?.kind==='inline'){showDataTable(data.dimensions,data.source);}

Compact ECharts fence

The ECharts package also registers the exact echarts-fulldata fence used by the dataworks-chart skill. It is strict JSON, never JavaScript, and is a renderer-owned shorthand for the equivalent canonical envelope:

```echarts-fulldata{ "version": 1, "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "option": { "title": { "text": "Monthly sales" }, "series": [{ "type": "bar" }] }}```

The compact envelope saves the fixed renderer / spec wrapper while using the same option validation, title, data-ref resolution, and Chart/Data view as canonical ECharts. The singular echart-fulldata fence is not registered.

React + react-markdown

With the canonical Markdown above stored in source:

pnpm add echarts @datafe-open/markdown-chart-react
import{MarkdownChart}from'@datafe-open/markdown-chart-react';exportfunctionApp({ source }: {source: string}){return<MarkdownChartsource={source}/>;}

Vue 3 + markdown-it

pnpm add echarts @datafe-open/markdown-chart-vue
<script setup lang="ts">import { MarkdownChart } from'@datafe-open/markdown-chart-vue';defineProps<{ source:string }>();</script>
<template>
<MarkdownChart :source="source" />
</template>

Both components register ECharts and KPI automatically. They load ECharts on its first chart mount and apply a 360px minimum height to chart placeholders; the KPI renderer uses its compact content height. Canonical inline data and referenced data returned by resolveDataRef also enable a built-in icon-based Chart/Data switch with a bounded, scrollable data table. The card, toolbar, icons, table, and default ECharts palette/axes/tooltip/series styling are adapted from the Qwen Code WebShell implementation, while explicit ECharts option values still win. The React package includes react-markdown, and the Vue package includes markdown-it. Pass a custom registry, parser, theme, or renderer options only when the defaults are not sufficient. See Third-party notices for attribution.

Card colors can be aligned with the host through --markdown-chart-background, --markdown-chart-subtle-background, --markdown-chart-accent, and --markdown-chart-accent-foreground. The selected Chart/Data icon foreground falls back to the chart background and then #ffffff; hosts with a custom accent can override the accent and its foreground as a pair. Advanced registries can set createEChartsRenderer({ defaultStyle: false }) to disable the presentation defaults. Validation, canonical data injection, and data-ref resolution still apply.

Streaming

Pass the outer document streaming state to the framework component:

<MarkdownChartsource={source}streaming={isStreaming}/>
<MarkdownChart :source="source" :streaming="isStreaming" />

Closed chart fences render immediately and keep their mounted chart instance as later text arrives. Only the active unterminated tail fence waits for more input, including when a chart fence is nested in a blockquote. Pending fences and asynchronous parsing, data resolution, and runtime mounting show a built-in loading indicator instead of a blank placeholder. Use loadingLabel to localize its text and --markdown-chart-loading-color to align its color. Advanced React applications pass the same state to MarkdownChartProvider; advanced Vue applications pass it to MarkdownChart.

Localized labels

React and Vue hosts can pass a partial labels object to localize the Chart/Data controls, accessibility labels, empty-data text, truncation notice, and chart error fallback:

<MarkdownChartsource={source}labels={{chartUnavailable: '图表不可用',viewMode: '视图模式',chart: '图表',data: '数据',showChart: '显示图表',showData: '显示数据',noData: '暂无数据',tableNotice: ({ visibleRows, totalRows, visibleColumns, totalColumns })=>`显示 ${visibleRows}/${totalRows} 行,${visibleColumns}/${totalColumns} 列`,}}/>

The same MarkdownChartLabelOverrides type is accepted by MarkdownChartProvider, MarkdownChartBlock, the Vue composable/mounting utility, and the markdown-it plugin. Omitted labels use the English defaults.

Advanced setup

Create and pass a registry only when adding renderers or resolving host data:

import{ChartRendererRegistry}from'@datafe-open/markdown-chart';import{createEChartsRenderer}from'@datafe-open/markdown-chart-echarts';constregistry=newChartRendererRegistry();registry.register(createEChartsRenderer({resolveDataRef: async(ref,meta)=>loadApplicationDataset(ref,meta.signal),}));

The resolver returns { dimensions?, source }. If it omits dimensions, the dimensions declared on the ref are retained. ECharts uses the materialized rows for both option.dataset and the shared Chart/Data view, so referenced datasets can be inspected without duplicating them inline.

Pass the same live registry to framework adapters. Renderer aliases registered later, such as vega-lite or plotly, are then recognized without updating an adapter language list. The package never fetches a data reference itself; applications decide which reference schemes are allowed.

Existing react-markdown applications

The provider and components API remains available when the host already owns the surrounding Markdown renderer. Using the configured registry above, the integration remains:

importReactMarkdownfrom'react-markdown';import{MarkdownChartProvider,createMarkdownChartComponents,}from'@datafe-open/markdown-chart-react';constchartComponents=createMarkdownChartComponents({chartStyle: {minHeight: 360},});<MarkdownChartProviderregistry={registry}streaming={isStreaming}><ReactMarkdowncomponents={chartComponents}>{source}</ReactMarkdown></MarkdownChartProvider>

The provider infers source from its direct ReactMarkdown child, so streaming support does not add another required prop in this common advanced setup.

Because the application imports react-markdown directly in this mode, declare every directly imported package as an application dependency:

pnpm add echarts react-markdown \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-react

Existing Vue + markdown-it applications

Vue applications can keep their existing markdown-it instance and pass the same registry to both the plugin and component:

<script setup lang="ts">import { ChartRendererRegistry } from'@datafe-open/markdown-chart';import { createEChartsRenderer } from'@datafe-open/markdown-chart-echarts';import { markdownChartPlugin } from'@datafe-open/markdown-chart-markdown-it';import { MarkdownChart } from'@datafe-open/markdown-chart-vue';importMarkdownItfrom'markdown-it';defineProps<{ source:string; isStreaming:boolean }>();const registry =newChartRendererRegistry().register(createEChartsRenderer());const markdownIt =newMarkdownIt({ html: false }).use(markdownChartPlugin, {registry,});</script>
<template>
<MarkdownChart
:source="source"
:streaming="isStreaming"
:markdown-it="markdownIt"
:registry="registry"
/>
</template>

Declare the packages imported by this advanced setup directly:

pnpm add echarts markdown-it \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-markdown-it \
@datafe-open/markdown-chart-vue

See SPEC.md, SECURITY.md, and the Vue and React examples. Simple and advanced modes live in separate runnable folders with independent dependency manifests.

Development

pnpm install
pnpm test
pnpm typecheck
pnpm build
pnpm check:pack

The root build validates both publishable packages and all React/Vue Vite examples. Example workspaces are private and are never included in package tarballs. Published-package changes use Changesets; see RELEASING.md for bootstrap and automated release steps.

License

MIT. Portions are adapted from Qwen Code under Apache-2.0; see Third-party notices.

About

Extensible JSON chart rendering for Markdown, with ECharts, markdown-it, Vue, and react-markdown adapters

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Markdown Chart

English | 简体中文

Note

All six @datafe-open/markdown-chart* packages are published on npm. The install commands below use the public packages. Maintainers should follow RELEASING.md for subsequent releases.

markdown-chart provides portable chart blocks for streaming Markdown, with inspectable data and pluggable renderers. Its core is framework-neutral and independent of any chat product.

The project includes independent ECharts and KPI renderers plus adapters for markdown-it, Vue 3, and react-markdown. The registry-based core can accept future Plotly, Vega, or other renderer packages without adding chart-specific switches to the core.

Packages

PackagePurpose
@datafe-open/markdown-chartRenderer registry, canonical markdown-chart routing, and lifecycle controller
@datafe-open/markdown-chart-echartsStrict JSON-only canonical ECharts renderer and deprecated ChatBI legacy adapter
@datafe-open/markdown-chart-kpiStrict responsive multi-KPI cards with optional host-owned reference actions
@datafe-open/markdown-chart-markdown-itSafe placeholder plugin and environment side channel
@datafe-open/markdown-chart-vueVue 3 component and composable
@datafe-open/markdown-chart-reactreact-markdown code/pre adapter

Canonical Markdown

```markdown-chart{ "version": 1, "renderer": "echarts", "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "spec": { "title": { "text": "Monthly sales" }, "xAxis": { "type": "category" }, "yAxis": {}, "series": [{ "type": "bar", "encode": { "x": "month", "y": "sales" } }] }}```

There is only one protocol version, on the outer markdown-chart envelope. data is renderer-neutral so hosts can expose the inline rows independently, for example in a “View data” action. spec belongs to the selected renderer and does not repeat the data or version.

For ECharts, the shared card title comes only from spec.title.text, which is the ECharts option's title.text. If it is absent or blank, the title element is omitted instead of showing a fallback. The Chart/Data controls remain right-aligned, and the chart keeps 8px of vertical spacing from the toolbar.

KPI renderer

KPI cards use the same canonical data/configuration separation as ECharts. A shared default data dataset contains one row per time point; spec binds fields and safe formatting:

```markdown-chart{ "version": 1, "renderer": "kpi", "data": { "kind": "inline", "source": [ { "day": "2026-08-31", "unmet": 0.38, "revenue": 16800000 }, { "day": "2026-09-01", "unmet": 0.4, "revenue": 18000000 } ] }, "spec": { "timeField": "day", "items": [ { "id": "unmet_demand", "title": "Unmet demand", "value": { "field": "unmet", "format": { "style": "percent", "maximumFractionDigits": 0 } }, "status": { "text": { "literal": "At risk" }, "tone": { "literal": "negative" } }, "trend": { "type": "area", "compare": { "lag": 1, "mode": "absolute", "polarity": "lower-is-better" } }, "references": [ { "ref": "docs://metrics/unmet-demand", "label": "Metric definition" } ] }, { "id": "revenue", "title": "Revenue", "value": { "field": "revenue", "format": { "style": "currency", "currency": "CNY", "notation": "compact" } } } ] }}```

The renderer supports 1–12 items, lastNonNull reduction, safe structured Intl.NumberFormat options, field/literal status bindings, and optional line/area sparklines with deterministic lag comparison. Items with fewer than two valid trend points fall back to a plain KPI. A group can freely mix items with and without trends.

Prefer the shared default data when KPI items use the same grain, filters, and source. When they do not, put additional canonical ChartData objects in the top-level datasets map and select one with item.dataset. Omitting dataset selects the default data; the selector is deliberately named dataset so it cannot be confused with KPI references:

{
"version": 1,
"renderer": "kpi",
"data": { "kind": "inline", "source": [{ "revenue": 18000000 }] },
"datasets": {
"inventory": {
"kind": "inline",
"source": [{ "day": "2026-09-01", "stock": 23 }]
}
},
"spec": {
"items": [
{ "id": "revenue", "title": "Revenue", "value": { "field": "revenue" } },
{
"id": "inventory",
"title": "Inventory",
"dataset": "inventory",
"value": { "field": "stock" },
"trend": { "type": "line", "timeField": "day" }
}
]
}
}

Every selected inline/ref dataset is materialized once, even when multiple KPI items select it. Row and cell limits apply to the complete selected collection. The built-in Data view continues to inspect the default data; a named-only KPI group renders without that single-dataset toggle.

Both inline and referenced ChartData are supported. For ref data, provide the host-owned resolver through the adapter's independent kpi option; the result is materialized once for the value, sparkline, comparison, and Data view:

constkpiOptions=useMemo(()=>({validateDataRef: (ref)=>ref.startsWith('dataset://'),resolveDataRef: (ref,{ signal })=>loadDataset(ref,signal),}),[],);<MarkdownChartsource={source}kpi={kpiOptions}/>

Each item accepts up to three references. The renderer treats every reference as opaque and never fetches or navigates. A host selects allowed references and handles clicks through the generic action API. The optional KPI referenceIcon factory lets a trusted host supply its own decorative glyph; the renderer falls back to the link icon and never interprets what the custom icon represents:

<MarkdownChartsource={source}kpi={{referenceIcon: ({ document })=>createHostReferenceIcon(document),}}referenceActions={{canOpen: ({ reference })=>reference.ref.startsWith('docs://'),open: ({ reference })=>openDocumentation(reference.ref),}}/>

Keep the KPI options and referenceActions.canOpen / open callbacks stable across streaming renders so completed cards and resolved data can be reused.

Hosts can inspect canonical data without loading a chart runtime:

pnpm add @datafe-open/markdown-chart
import{parseMarkdownChartEnvelope}from'@datafe-open/markdown-chart';// chartFenceBody is the JSON text inside one markdown-chart fence.const{ data }=parseMarkdownChartEnvelope(chartFenceBody);if(data?.kind==='inline'){showDataTable(data.dimensions,data.source);}

Compact ECharts fence

The ECharts package also registers the exact echarts-fulldata fence used by the dataworks-chart skill. It is strict JSON, never JavaScript, and is a renderer-owned shorthand for the equivalent canonical envelope:

```echarts-fulldata{ "version": 1, "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "option": { "title": { "text": "Monthly sales" }, "series": [{ "type": "bar" }] }}```

The compact envelope saves the fixed renderer / spec wrapper while using the same option validation, title, data-ref resolution, and Chart/Data view as canonical ECharts. The singular echart-fulldata fence is not registered.

React + react-markdown

With the canonical Markdown above stored in source:

pnpm add echarts @datafe-open/markdown-chart-react
import{MarkdownChart}from'@datafe-open/markdown-chart-react';exportfunctionApp({ source }: {source: string}){return<MarkdownChartsource={source}/>;}

Vue 3 + markdown-it

pnpm add echarts @datafe-open/markdown-chart-vue
<script setup lang="ts">import { MarkdownChart } from'@datafe-open/markdown-chart-vue';defineProps<{ source:string }>();</script>
<template>
<MarkdownChart :source="source" />
</template>

Both components register ECharts and KPI automatically. They load ECharts on its first chart mount and apply a 360px minimum height to chart placeholders; the KPI renderer uses its compact content height. Canonical inline data and referenced data returned by resolveDataRef also enable a built-in icon-based Chart/Data switch with a bounded, scrollable data table. The card, toolbar, icons, table, and default ECharts palette/axes/tooltip/series styling are adapted from the Qwen Code WebShell implementation, while explicit ECharts option values still win. The React package includes react-markdown, and the Vue package includes markdown-it. Pass a custom registry, parser, theme, or renderer options only when the defaults are not sufficient. See Third-party notices for attribution.

Card colors can be aligned with the host through --markdown-chart-background, --markdown-chart-subtle-background, --markdown-chart-accent, and --markdown-chart-accent-foreground. The selected Chart/Data icon foreground falls back to the chart background and then #ffffff; hosts with a custom accent can override the accent and its foreground as a pair. Advanced registries can set createEChartsRenderer({ defaultStyle: false }) to disable the presentation defaults. Validation, canonical data injection, and data-ref resolution still apply.

Streaming

Pass the outer document streaming state to the framework component:

<MarkdownChartsource={source}streaming={isStreaming}/>
<MarkdownChart :source="source" :streaming="isStreaming" />

Closed chart fences render immediately and keep their mounted chart instance as later text arrives. Only the active unterminated tail fence waits for more input, including when a chart fence is nested in a blockquote. Pending fences and asynchronous parsing, data resolution, and runtime mounting show a built-in loading indicator instead of a blank placeholder. Use loadingLabel to localize its text and --markdown-chart-loading-color to align its color. Advanced React applications pass the same state to MarkdownChartProvider; advanced Vue applications pass it to MarkdownChart.

Localized labels

React and Vue hosts can pass a partial labels object to localize the Chart/Data controls, accessibility labels, empty-data text, truncation notice, and chart error fallback:

<MarkdownChartsource={source}labels={{chartUnavailable: '图表不可用',viewMode: '视图模式',chart: '图表',data: '数据',showChart: '显示图表',showData: '显示数据',noData: '暂无数据',tableNotice: ({ visibleRows, totalRows, visibleColumns, totalColumns })=>`显示 ${visibleRows}/${totalRows} 行,${visibleColumns}/${totalColumns} 列`,}}/>

The same MarkdownChartLabelOverrides type is accepted by MarkdownChartProvider, MarkdownChartBlock, the Vue composable/mounting utility, and the markdown-it plugin. Omitted labels use the English defaults.

Advanced setup

Create and pass a registry only when adding renderers or resolving host data:

import{ChartRendererRegistry}from'@datafe-open/markdown-chart';import{createEChartsRenderer}from'@datafe-open/markdown-chart-echarts';constregistry=newChartRendererRegistry();registry.register(createEChartsRenderer({resolveDataRef: async(ref,meta)=>loadApplicationDataset(ref,meta.signal),}));

The resolver returns { dimensions?, source }. If it omits dimensions, the dimensions declared on the ref are retained. ECharts uses the materialized rows for both option.dataset and the shared Chart/Data view, so referenced datasets can be inspected without duplicating them inline.

Pass the same live registry to framework adapters. Renderer aliases registered later, such as vega-lite or plotly, are then recognized without updating an adapter language list. The package never fetches a data reference itself; applications decide which reference schemes are allowed.

Existing react-markdown applications

The provider and components API remains available when the host already owns the surrounding Markdown renderer. Using the configured registry above, the integration remains:

importReactMarkdownfrom'react-markdown';import{MarkdownChartProvider,createMarkdownChartComponents,}from'@datafe-open/markdown-chart-react';constchartComponents=createMarkdownChartComponents({chartStyle: {minHeight: 360},});<MarkdownChartProviderregistry={registry}streaming={isStreaming}><ReactMarkdowncomponents={chartComponents}>{source}</ReactMarkdown></MarkdownChartProvider>

The provider infers source from its direct ReactMarkdown child, so streaming support does not add another required prop in this common advanced setup.

Because the application imports react-markdown directly in this mode, declare every directly imported package as an application dependency:

pnpm add echarts react-markdown \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-react

Existing Vue + markdown-it applications

Vue applications can keep their existing markdown-it instance and pass the same registry to both the plugin and component:

<script setup lang="ts">import { ChartRendererRegistry } from'@datafe-open/markdown-chart';import { createEChartsRenderer } from'@datafe-open/markdown-chart-echarts';import { markdownChartPlugin } from'@datafe-open/markdown-chart-markdown-it';import { MarkdownChart } from'@datafe-open/markdown-chart-vue';importMarkdownItfrom'markdown-it';defineProps<{ source:string; isStreaming:boolean }>();const registry =newChartRendererRegistry().register(createEChartsRenderer());const markdownIt =newMarkdownIt({ html: false }).use(markdownChartPlugin, {registry,});</script>
<template>
<MarkdownChart
:source="source"
:streaming="isStreaming"
:markdown-it="markdownIt"
:registry="registry"
/>
</template>

Declare the packages imported by this advanced setup directly:

pnpm add echarts markdown-it \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-markdown-it \
@datafe-open/markdown-chart-vue

See SPEC.md, SECURITY.md, and the Vue and React examples. Simple and advanced modes live in separate runnable folders with independent dependency manifests.

Development

pnpm install
pnpm test
pnpm typecheck
pnpm build
pnpm check:pack

The root build validates both publishable packages and all React/Vue Vite examples. Example workspaces are private and are never included in package tarballs. Published-package changes use Changesets; see RELEASING.md for bootstrap and automated release steps.

License

MIT. Portions are adapted from Qwen Code under Apache-2.0; see Third-party notices.

About

Extensible JSON chart rendering for Markdown, with ECharts, markdown-it, Vue, and react-markdown adapters

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Markdown Chart

English | 简体中文

Note

All six @datafe-open/markdown-chart* packages are published on npm. The install commands below use the public packages. Maintainers should follow RELEASING.md for subsequent releases.

markdown-chart provides portable chart blocks for streaming Markdown, with inspectable data and pluggable renderers. Its core is framework-neutral and independent of any chat product.

The project includes independent ECharts and KPI renderers plus adapters for markdown-it, Vue 3, and react-markdown. The registry-based core can accept future Plotly, Vega, or other renderer packages without adding chart-specific switches to the core.

Packages

PackagePurpose
@datafe-open/markdown-chartRenderer registry, canonical markdown-chart routing, and lifecycle controller
@datafe-open/markdown-chart-echartsStrict JSON-only canonical ECharts renderer and deprecated ChatBI legacy adapter
@datafe-open/markdown-chart-kpiStrict responsive multi-KPI cards with optional host-owned reference actions
@datafe-open/markdown-chart-markdown-itSafe placeholder plugin and environment side channel
@datafe-open/markdown-chart-vueVue 3 component and composable
@datafe-open/markdown-chart-reactreact-markdown code/pre adapter

Canonical Markdown

```markdown-chart{ "version": 1, "renderer": "echarts", "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "spec": { "title": { "text": "Monthly sales" }, "xAxis": { "type": "category" }, "yAxis": {}, "series": [{ "type": "bar", "encode": { "x": "month", "y": "sales" } }] }}```

There is only one protocol version, on the outer markdown-chart envelope. data is renderer-neutral so hosts can expose the inline rows independently, for example in a “View data” action. spec belongs to the selected renderer and does not repeat the data or version.

For ECharts, the shared card title comes only from spec.title.text, which is the ECharts option's title.text. If it is absent or blank, the title element is omitted instead of showing a fallback. The Chart/Data controls remain right-aligned, and the chart keeps 8px of vertical spacing from the toolbar.

KPI renderer

KPI cards use the same canonical data/configuration separation as ECharts. A shared default data dataset contains one row per time point; spec binds fields and safe formatting:

```markdown-chart{ "version": 1, "renderer": "kpi", "data": { "kind": "inline", "source": [ { "day": "2026-08-31", "unmet": 0.38, "revenue": 16800000 }, { "day": "2026-09-01", "unmet": 0.4, "revenue": 18000000 } ] }, "spec": { "timeField": "day", "items": [ { "id": "unmet_demand", "title": "Unmet demand", "value": { "field": "unmet", "format": { "style": "percent", "maximumFractionDigits": 0 } }, "status": { "text": { "literal": "At risk" }, "tone": { "literal": "negative" } }, "trend": { "type": "area", "compare": { "lag": 1, "mode": "absolute", "polarity": "lower-is-better" } }, "references": [ { "ref": "docs://metrics/unmet-demand", "label": "Metric definition" } ] }, { "id": "revenue", "title": "Revenue", "value": { "field": "revenue", "format": { "style": "currency", "currency": "CNY", "notation": "compact" } } } ] }}```

The renderer supports 1–12 items, lastNonNull reduction, safe structured Intl.NumberFormat options, field/literal status bindings, and optional line/area sparklines with deterministic lag comparison. Items with fewer than two valid trend points fall back to a plain KPI. A group can freely mix items with and without trends.

Prefer the shared default data when KPI items use the same grain, filters, and source. When they do not, put additional canonical ChartData objects in the top-level datasets map and select one with item.dataset. Omitting dataset selects the default data; the selector is deliberately named dataset so it cannot be confused with KPI references:

{
"version": 1,
"renderer": "kpi",
"data": { "kind": "inline", "source": [{ "revenue": 18000000 }] },
"datasets": {
"inventory": {
"kind": "inline",
"source": [{ "day": "2026-09-01", "stock": 23 }]
}
},
"spec": {
"items": [
{ "id": "revenue", "title": "Revenue", "value": { "field": "revenue" } },
{
"id": "inventory",
"title": "Inventory",
"dataset": "inventory",
"value": { "field": "stock" },
"trend": { "type": "line", "timeField": "day" }
}
]
}
}

Every selected inline/ref dataset is materialized once, even when multiple KPI items select it. Row and cell limits apply to the complete selected collection. The built-in Data view continues to inspect the default data; a named-only KPI group renders without that single-dataset toggle.

Both inline and referenced ChartData are supported. For ref data, provide the host-owned resolver through the adapter's independent kpi option; the result is materialized once for the value, sparkline, comparison, and Data view:

constkpiOptions=useMemo(()=>({validateDataRef: (ref)=>ref.startsWith('dataset://'),resolveDataRef: (ref,{ signal })=>loadDataset(ref,signal),}),[],);<MarkdownChartsource={source}kpi={kpiOptions}/>

Each item accepts up to three references. The renderer treats every reference as opaque and never fetches or navigates. A host selects allowed references and handles clicks through the generic action API. The optional KPI referenceIcon factory lets a trusted host supply its own decorative glyph; the renderer falls back to the link icon and never interprets what the custom icon represents:

<MarkdownChartsource={source}kpi={{referenceIcon: ({ document })=>createHostReferenceIcon(document),}}referenceActions={{canOpen: ({ reference })=>reference.ref.startsWith('docs://'),open: ({ reference })=>openDocumentation(reference.ref),}}/>

Keep the KPI options and referenceActions.canOpen / open callbacks stable across streaming renders so completed cards and resolved data can be reused.

Hosts can inspect canonical data without loading a chart runtime:

pnpm add @datafe-open/markdown-chart
import{parseMarkdownChartEnvelope}from'@datafe-open/markdown-chart';// chartFenceBody is the JSON text inside one markdown-chart fence.const{ data }=parseMarkdownChartEnvelope(chartFenceBody);if(data?.kind==='inline'){showDataTable(data.dimensions,data.source);}

Compact ECharts fence

The ECharts package also registers the exact echarts-fulldata fence used by the dataworks-chart skill. It is strict JSON, never JavaScript, and is a renderer-owned shorthand for the equivalent canonical envelope:

```echarts-fulldata{ "version": 1, "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "option": { "title": { "text": "Monthly sales" }, "series": [{ "type": "bar" }] }}```

The compact envelope saves the fixed renderer / spec wrapper while using the same option validation, title, data-ref resolution, and Chart/Data view as canonical ECharts. The singular echart-fulldata fence is not registered.

React + react-markdown

With the canonical Markdown above stored in source:

pnpm add echarts @datafe-open/markdown-chart-react
import{MarkdownChart}from'@datafe-open/markdown-chart-react';exportfunctionApp({ source }: {source: string}){return<MarkdownChartsource={source}/>;}

Vue 3 + markdown-it

pnpm add echarts @datafe-open/markdown-chart-vue
<script setup lang="ts">import { MarkdownChart } from'@datafe-open/markdown-chart-vue';defineProps<{ source:string }>();</script>
<template>
<MarkdownChart :source="source" />
</template>

Both components register ECharts and KPI automatically. They load ECharts on its first chart mount and apply a 360px minimum height to chart placeholders; the KPI renderer uses its compact content height. Canonical inline data and referenced data returned by resolveDataRef also enable a built-in icon-based Chart/Data switch with a bounded, scrollable data table. The card, toolbar, icons, table, and default ECharts palette/axes/tooltip/series styling are adapted from the Qwen Code WebShell implementation, while explicit ECharts option values still win. The React package includes react-markdown, and the Vue package includes markdown-it. Pass a custom registry, parser, theme, or renderer options only when the defaults are not sufficient. See Third-party notices for attribution.

Card colors can be aligned with the host through --markdown-chart-background, --markdown-chart-subtle-background, --markdown-chart-accent, and --markdown-chart-accent-foreground. The selected Chart/Data icon foreground falls back to the chart background and then #ffffff; hosts with a custom accent can override the accent and its foreground as a pair. Advanced registries can set createEChartsRenderer({ defaultStyle: false }) to disable the presentation defaults. Validation, canonical data injection, and data-ref resolution still apply.

Streaming

Pass the outer document streaming state to the framework component:

<MarkdownChartsource={source}streaming={isStreaming}/>
<MarkdownChart :source="source" :streaming="isStreaming" />

Closed chart fences render immediately and keep their mounted chart instance as later text arrives. Only the active unterminated tail fence waits for more input, including when a chart fence is nested in a blockquote. Pending fences and asynchronous parsing, data resolution, and runtime mounting show a built-in loading indicator instead of a blank placeholder. Use loadingLabel to localize its text and --markdown-chart-loading-color to align its color. Advanced React applications pass the same state to MarkdownChartProvider; advanced Vue applications pass it to MarkdownChart.

Localized labels

React and Vue hosts can pass a partial labels object to localize the Chart/Data controls, accessibility labels, empty-data text, truncation notice, and chart error fallback:

<MarkdownChartsource={source}labels={{chartUnavailable: '图表不可用',viewMode: '视图模式',chart: '图表',data: '数据',showChart: '显示图表',showData: '显示数据',noData: '暂无数据',tableNotice: ({ visibleRows, totalRows, visibleColumns, totalColumns })=>`显示 ${visibleRows}/${totalRows} 行,${visibleColumns}/${totalColumns} 列`,}}/>

The same MarkdownChartLabelOverrides type is accepted by MarkdownChartProvider, MarkdownChartBlock, the Vue composable/mounting utility, and the markdown-it plugin. Omitted labels use the English defaults.

Advanced setup

Create and pass a registry only when adding renderers or resolving host data:

import{ChartRendererRegistry}from'@datafe-open/markdown-chart';import{createEChartsRenderer}from'@datafe-open/markdown-chart-echarts';constregistry=newChartRendererRegistry();registry.register(createEChartsRenderer({resolveDataRef: async(ref,meta)=>loadApplicationDataset(ref,meta.signal),}));

The resolver returns { dimensions?, source }. If it omits dimensions, the dimensions declared on the ref are retained. ECharts uses the materialized rows for both option.dataset and the shared Chart/Data view, so referenced datasets can be inspected without duplicating them inline.

Pass the same live registry to framework adapters. Renderer aliases registered later, such as vega-lite or plotly, are then recognized without updating an adapter language list. The package never fetches a data reference itself; applications decide which reference schemes are allowed.

Existing react-markdown applications

The provider and components API remains available when the host already owns the surrounding Markdown renderer. Using the configured registry above, the integration remains:

importReactMarkdownfrom'react-markdown';import{MarkdownChartProvider,createMarkdownChartComponents,}from'@datafe-open/markdown-chart-react';constchartComponents=createMarkdownChartComponents({chartStyle: {minHeight: 360},});<MarkdownChartProviderregistry={registry}streaming={isStreaming}><ReactMarkdowncomponents={chartComponents}>{source}</ReactMarkdown></MarkdownChartProvider>

The provider infers source from its direct ReactMarkdown child, so streaming support does not add another required prop in this common advanced setup.

Because the application imports react-markdown directly in this mode, declare every directly imported package as an application dependency:

pnpm add echarts react-markdown \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-react

Existing Vue + markdown-it applications

Vue applications can keep their existing markdown-it instance and pass the same registry to both the plugin and component:

<script setup lang="ts">import { ChartRendererRegistry } from'@datafe-open/markdown-chart';import { createEChartsRenderer } from'@datafe-open/markdown-chart-echarts';import { markdownChartPlugin } from'@datafe-open/markdown-chart-markdown-it';import { MarkdownChart } from'@datafe-open/markdown-chart-vue';importMarkdownItfrom'markdown-it';defineProps<{ source:string; isStreaming:boolean }>();const registry =newChartRendererRegistry().register(createEChartsRenderer());const markdownIt =newMarkdownIt({ html: false }).use(markdownChartPlugin, {registry,});</script>
<template>
<MarkdownChart
:source="source"
:streaming="isStreaming"
:markdown-it="markdownIt"
:registry="registry"
/>
</template>

Declare the packages imported by this advanced setup directly:

pnpm add echarts markdown-it \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-markdown-it \
@datafe-open/markdown-chart-vue

See SPEC.md, SECURITY.md, and the Vue and React examples. Simple and advanced modes live in separate runnable folders with independent dependency manifests.

Development

pnpm install
pnpm test
pnpm typecheck
pnpm build
pnpm check:pack

The root build validates both publishable packages and all React/Vue Vite examples. Example workspaces are private and are never included in package tarballs. Published-package changes use Changesets; see RELEASING.md for bootstrap and automated release steps.

License

MIT. Portions are adapted from Qwen Code under Apache-2.0; see Third-party notices.

About

Extensible JSON chart rendering for Markdown, with ECharts, markdown-it, Vue, and react-markdown adapters

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Markdown Chart

English | 简体中文

Note

All six @datafe-open/markdown-chart* packages are published on npm. The install commands below use the public packages. Maintainers should follow RELEASING.md for subsequent releases.

markdown-chart provides portable chart blocks for streaming Markdown, with inspectable data and pluggable renderers. Its core is framework-neutral and independent of any chat product.

The project includes independent ECharts and KPI renderers plus adapters for markdown-it, Vue 3, and react-markdown. The registry-based core can accept future Plotly, Vega, or other renderer packages without adding chart-specific switches to the core.

Packages

PackagePurpose
@datafe-open/markdown-chartRenderer registry, canonical markdown-chart routing, and lifecycle controller
@datafe-open/markdown-chart-echartsStrict JSON-only canonical ECharts renderer and deprecated ChatBI legacy adapter
@datafe-open/markdown-chart-kpiStrict responsive multi-KPI cards with optional host-owned reference actions
@datafe-open/markdown-chart-markdown-itSafe placeholder plugin and environment side channel
@datafe-open/markdown-chart-vueVue 3 component and composable
@datafe-open/markdown-chart-reactreact-markdown code/pre adapter

Canonical Markdown

```markdown-chart{ "version": 1, "renderer": "echarts", "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "spec": { "title": { "text": "Monthly sales" }, "xAxis": { "type": "category" }, "yAxis": {}, "series": [{ "type": "bar", "encode": { "x": "month", "y": "sales" } }] }}```

There is only one protocol version, on the outer markdown-chart envelope. data is renderer-neutral so hosts can expose the inline rows independently, for example in a “View data” action. spec belongs to the selected renderer and does not repeat the data or version.

For ECharts, the shared card title comes only from spec.title.text, which is the ECharts option's title.text. If it is absent or blank, the title element is omitted instead of showing a fallback. The Chart/Data controls remain right-aligned, and the chart keeps 8px of vertical spacing from the toolbar.

KPI renderer

KPI cards use the same canonical data/configuration separation as ECharts. A shared default data dataset contains one row per time point; spec binds fields and safe formatting:

```markdown-chart{ "version": 1, "renderer": "kpi", "data": { "kind": "inline", "source": [ { "day": "2026-08-31", "unmet": 0.38, "revenue": 16800000 }, { "day": "2026-09-01", "unmet": 0.4, "revenue": 18000000 } ] }, "spec": { "timeField": "day", "items": [ { "id": "unmet_demand", "title": "Unmet demand", "value": { "field": "unmet", "format": { "style": "percent", "maximumFractionDigits": 0 } }, "status": { "text": { "literal": "At risk" }, "tone": { "literal": "negative" } }, "trend": { "type": "area", "compare": { "lag": 1, "mode": "absolute", "polarity": "lower-is-better" } }, "references": [ { "ref": "docs://metrics/unmet-demand", "label": "Metric definition" } ] }, { "id": "revenue", "title": "Revenue", "value": { "field": "revenue", "format": { "style": "currency", "currency": "CNY", "notation": "compact" } } } ] }}```

The renderer supports 1–12 items, lastNonNull reduction, safe structured Intl.NumberFormat options, field/literal status bindings, and optional line/area sparklines with deterministic lag comparison. Items with fewer than two valid trend points fall back to a plain KPI. A group can freely mix items with and without trends.

Prefer the shared default data when KPI items use the same grain, filters, and source. When they do not, put additional canonical ChartData objects in the top-level datasets map and select one with item.dataset. Omitting dataset selects the default data; the selector is deliberately named dataset so it cannot be confused with KPI references:

{
"version": 1,
"renderer": "kpi",
"data": { "kind": "inline", "source": [{ "revenue": 18000000 }] },
"datasets": {
"inventory": {
"kind": "inline",
"source": [{ "day": "2026-09-01", "stock": 23 }]
}
},
"spec": {
"items": [
{ "id": "revenue", "title": "Revenue", "value": { "field": "revenue" } },
{
"id": "inventory",
"title": "Inventory",
"dataset": "inventory",
"value": { "field": "stock" },
"trend": { "type": "line", "timeField": "day" }
}
]
}
}

Every selected inline/ref dataset is materialized once, even when multiple KPI items select it. Row and cell limits apply to the complete selected collection. The built-in Data view continues to inspect the default data; a named-only KPI group renders without that single-dataset toggle.

Both inline and referenced ChartData are supported. For ref data, provide the host-owned resolver through the adapter's independent kpi option; the result is materialized once for the value, sparkline, comparison, and Data view:

constkpiOptions=useMemo(()=>({validateDataRef: (ref)=>ref.startsWith('dataset://'),resolveDataRef: (ref,{ signal })=>loadDataset(ref,signal),}),[],);<MarkdownChartsource={source}kpi={kpiOptions}/>

Each item accepts up to three references. The renderer treats every reference as opaque and never fetches or navigates. A host selects allowed references and handles clicks through the generic action API. The optional KPI referenceIcon factory lets a trusted host supply its own decorative glyph; the renderer falls back to the link icon and never interprets what the custom icon represents:

<MarkdownChartsource={source}kpi={{referenceIcon: ({ document })=>createHostReferenceIcon(document),}}referenceActions={{canOpen: ({ reference })=>reference.ref.startsWith('docs://'),open: ({ reference })=>openDocumentation(reference.ref),}}/>

Keep the KPI options and referenceActions.canOpen / open callbacks stable across streaming renders so completed cards and resolved data can be reused.

Hosts can inspect canonical data without loading a chart runtime:

pnpm add @datafe-open/markdown-chart
import{parseMarkdownChartEnvelope}from'@datafe-open/markdown-chart';// chartFenceBody is the JSON text inside one markdown-chart fence.const{ data }=parseMarkdownChartEnvelope(chartFenceBody);if(data?.kind==='inline'){showDataTable(data.dimensions,data.source);}

Compact ECharts fence

The ECharts package also registers the exact echarts-fulldata fence used by the dataworks-chart skill. It is strict JSON, never JavaScript, and is a renderer-owned shorthand for the equivalent canonical envelope:

```echarts-fulldata{ "version": 1, "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "option": { "title": { "text": "Monthly sales" }, "series": [{ "type": "bar" }] }}```

The compact envelope saves the fixed renderer / spec wrapper while using the same option validation, title, data-ref resolution, and Chart/Data view as canonical ECharts. The singular echart-fulldata fence is not registered.

React + react-markdown

With the canonical Markdown above stored in source:

pnpm add echarts @datafe-open/markdown-chart-react
import{MarkdownChart}from'@datafe-open/markdown-chart-react';exportfunctionApp({ source }: {source: string}){return<MarkdownChartsource={source}/>;}

Vue 3 + markdown-it

pnpm add echarts @datafe-open/markdown-chart-vue
<script setup lang="ts">import { MarkdownChart } from'@datafe-open/markdown-chart-vue';defineProps<{ source:string }>();</script>
<template>
<MarkdownChart :source="source" />
</template>

Both components register ECharts and KPI automatically. They load ECharts on its first chart mount and apply a 360px minimum height to chart placeholders; the KPI renderer uses its compact content height. Canonical inline data and referenced data returned by resolveDataRef also enable a built-in icon-based Chart/Data switch with a bounded, scrollable data table. The card, toolbar, icons, table, and default ECharts palette/axes/tooltip/series styling are adapted from the Qwen Code WebShell implementation, while explicit ECharts option values still win. The React package includes react-markdown, and the Vue package includes markdown-it. Pass a custom registry, parser, theme, or renderer options only when the defaults are not sufficient. See Third-party notices for attribution.

Card colors can be aligned with the host through --markdown-chart-background, --markdown-chart-subtle-background, --markdown-chart-accent, and --markdown-chart-accent-foreground. The selected Chart/Data icon foreground falls back to the chart background and then #ffffff; hosts with a custom accent can override the accent and its foreground as a pair. Advanced registries can set createEChartsRenderer({ defaultStyle: false }) to disable the presentation defaults. Validation, canonical data injection, and data-ref resolution still apply.

Streaming

Pass the outer document streaming state to the framework component:

<MarkdownChartsource={source}streaming={isStreaming}/>
<MarkdownChart :source="source" :streaming="isStreaming" />

Closed chart fences render immediately and keep their mounted chart instance as later text arrives. Only the active unterminated tail fence waits for more input, including when a chart fence is nested in a blockquote. Pending fences and asynchronous parsing, data resolution, and runtime mounting show a built-in loading indicator instead of a blank placeholder. Use loadingLabel to localize its text and --markdown-chart-loading-color to align its color. Advanced React applications pass the same state to MarkdownChartProvider; advanced Vue applications pass it to MarkdownChart.

Localized labels

React and Vue hosts can pass a partial labels object to localize the Chart/Data controls, accessibility labels, empty-data text, truncation notice, and chart error fallback:

<MarkdownChartsource={source}labels={{chartUnavailable: '图表不可用',viewMode: '视图模式',chart: '图表',data: '数据',showChart: '显示图表',showData: '显示数据',noData: '暂无数据',tableNotice: ({ visibleRows, totalRows, visibleColumns, totalColumns })=>`显示 ${visibleRows}/${totalRows} 行,${visibleColumns}/${totalColumns} 列`,}}/>

The same MarkdownChartLabelOverrides type is accepted by MarkdownChartProvider, MarkdownChartBlock, the Vue composable/mounting utility, and the markdown-it plugin. Omitted labels use the English defaults.

Advanced setup

Create and pass a registry only when adding renderers or resolving host data:

import{ChartRendererRegistry}from'@datafe-open/markdown-chart';import{createEChartsRenderer}from'@datafe-open/markdown-chart-echarts';constregistry=newChartRendererRegistry();registry.register(createEChartsRenderer({resolveDataRef: async(ref,meta)=>loadApplicationDataset(ref,meta.signal),}));

The resolver returns { dimensions?, source }. If it omits dimensions, the dimensions declared on the ref are retained. ECharts uses the materialized rows for both option.dataset and the shared Chart/Data view, so referenced datasets can be inspected without duplicating them inline.

Pass the same live registry to framework adapters. Renderer aliases registered later, such as vega-lite or plotly, are then recognized without updating an adapter language list. The package never fetches a data reference itself; applications decide which reference schemes are allowed.

Existing react-markdown applications

The provider and components API remains available when the host already owns the surrounding Markdown renderer. Using the configured registry above, the integration remains:

importReactMarkdownfrom'react-markdown';import{MarkdownChartProvider,createMarkdownChartComponents,}from'@datafe-open/markdown-chart-react';constchartComponents=createMarkdownChartComponents({chartStyle: {minHeight: 360},});<MarkdownChartProviderregistry={registry}streaming={isStreaming}><ReactMarkdowncomponents={chartComponents}>{source}</ReactMarkdown></MarkdownChartProvider>

The provider infers source from its direct ReactMarkdown child, so streaming support does not add another required prop in this common advanced setup.

Because the application imports react-markdown directly in this mode, declare every directly imported package as an application dependency:

pnpm add echarts react-markdown \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-react

Existing Vue + markdown-it applications

Vue applications can keep their existing markdown-it instance and pass the same registry to both the plugin and component:

<script setup lang="ts">import { ChartRendererRegistry } from'@datafe-open/markdown-chart';import { createEChartsRenderer } from'@datafe-open/markdown-chart-echarts';import { markdownChartPlugin } from'@datafe-open/markdown-chart-markdown-it';import { MarkdownChart } from'@datafe-open/markdown-chart-vue';importMarkdownItfrom'markdown-it';defineProps<{ source:string; isStreaming:boolean }>();const registry =newChartRendererRegistry().register(createEChartsRenderer());const markdownIt =newMarkdownIt({ html: false }).use(markdownChartPlugin, {registry,});</script>
<template>
<MarkdownChart
:source="source"
:streaming="isStreaming"
:markdown-it="markdownIt"
:registry="registry"
/>
</template>

Declare the packages imported by this advanced setup directly:

pnpm add echarts markdown-it \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-markdown-it \
@datafe-open/markdown-chart-vue

See SPEC.md, SECURITY.md, and the Vue and React examples. Simple and advanced modes live in separate runnable folders with independent dependency manifests.

Development

pnpm install
pnpm test
pnpm typecheck
pnpm build
pnpm check:pack

The root build validates both publishable packages and all React/Vue Vite examples. Example workspaces are private and are never included in package tarballs. Published-package changes use Changesets; see RELEASING.md for bootstrap and automated release steps.

License

MIT. Portions are adapted from Qwen Code under Apache-2.0; see Third-party notices.

About

Extensible JSON chart rendering for Markdown, with ECharts, markdown-it, Vue, and react-markdown adapters

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Markdown Chart

English | 简体中文

Note

All six @datafe-open/markdown-chart* packages are published on npm. The install commands below use the public packages. Maintainers should follow RELEASING.md for subsequent releases.

markdown-chart provides portable chart blocks for streaming Markdown, with inspectable data and pluggable renderers. Its core is framework-neutral and independent of any chat product.

The project includes independent ECharts and KPI renderers plus adapters for markdown-it, Vue 3, and react-markdown. The registry-based core can accept future Plotly, Vega, or other renderer packages without adding chart-specific switches to the core.

Packages

PackagePurpose
@datafe-open/markdown-chartRenderer registry, canonical markdown-chart routing, and lifecycle controller
@datafe-open/markdown-chart-echartsStrict JSON-only canonical ECharts renderer and deprecated ChatBI legacy adapter
@datafe-open/markdown-chart-kpiStrict responsive multi-KPI cards with optional host-owned reference actions
@datafe-open/markdown-chart-markdown-itSafe placeholder plugin and environment side channel
@datafe-open/markdown-chart-vueVue 3 component and composable
@datafe-open/markdown-chart-reactreact-markdown code/pre adapter

Canonical Markdown

```markdown-chart{ "version": 1, "renderer": "echarts", "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "spec": { "title": { "text": "Monthly sales" }, "xAxis": { "type": "category" }, "yAxis": {}, "series": [{ "type": "bar", "encode": { "x": "month", "y": "sales" } }] }}```

There is only one protocol version, on the outer markdown-chart envelope. data is renderer-neutral so hosts can expose the inline rows independently, for example in a “View data” action. spec belongs to the selected renderer and does not repeat the data or version.

For ECharts, the shared card title comes only from spec.title.text, which is the ECharts option's title.text. If it is absent or blank, the title element is omitted instead of showing a fallback. The Chart/Data controls remain right-aligned, and the chart keeps 8px of vertical spacing from the toolbar.

KPI renderer

KPI cards use the same canonical data/configuration separation as ECharts. A shared default data dataset contains one row per time point; spec binds fields and safe formatting:

```markdown-chart{ "version": 1, "renderer": "kpi", "data": { "kind": "inline", "source": [ { "day": "2026-08-31", "unmet": 0.38, "revenue": 16800000 }, { "day": "2026-09-01", "unmet": 0.4, "revenue": 18000000 } ] }, "spec": { "timeField": "day", "items": [ { "id": "unmet_demand", "title": "Unmet demand", "value": { "field": "unmet", "format": { "style": "percent", "maximumFractionDigits": 0 } }, "status": { "text": { "literal": "At risk" }, "tone": { "literal": "negative" } }, "trend": { "type": "area", "compare": { "lag": 1, "mode": "absolute", "polarity": "lower-is-better" } }, "references": [ { "ref": "docs://metrics/unmet-demand", "label": "Metric definition" } ] }, { "id": "revenue", "title": "Revenue", "value": { "field": "revenue", "format": { "style": "currency", "currency": "CNY", "notation": "compact" } } } ] }}```

The renderer supports 1–12 items, lastNonNull reduction, safe structured Intl.NumberFormat options, field/literal status bindings, and optional line/area sparklines with deterministic lag comparison. Items with fewer than two valid trend points fall back to a plain KPI. A group can freely mix items with and without trends.

Prefer the shared default data when KPI items use the same grain, filters, and source. When they do not, put additional canonical ChartData objects in the top-level datasets map and select one with item.dataset. Omitting dataset selects the default data; the selector is deliberately named dataset so it cannot be confused with KPI references:

{
"version": 1,
"renderer": "kpi",
"data": { "kind": "inline", "source": [{ "revenue": 18000000 }] },
"datasets": {
"inventory": {
"kind": "inline",
"source": [{ "day": "2026-09-01", "stock": 23 }]
}
},
"spec": {
"items": [
{ "id": "revenue", "title": "Revenue", "value": { "field": "revenue" } },
{
"id": "inventory",
"title": "Inventory",
"dataset": "inventory",
"value": { "field": "stock" },
"trend": { "type": "line", "timeField": "day" }
}
]
}
}

Every selected inline/ref dataset is materialized once, even when multiple KPI items select it. Row and cell limits apply to the complete selected collection. The built-in Data view continues to inspect the default data; a named-only KPI group renders without that single-dataset toggle.

Both inline and referenced ChartData are supported. For ref data, provide the host-owned resolver through the adapter's independent kpi option; the result is materialized once for the value, sparkline, comparison, and Data view:

constkpiOptions=useMemo(()=>({validateDataRef: (ref)=>ref.startsWith('dataset://'),resolveDataRef: (ref,{ signal })=>loadDataset(ref,signal),}),[],);<MarkdownChartsource={source}kpi={kpiOptions}/>

Each item accepts up to three references. The renderer treats every reference as opaque and never fetches or navigates. A host selects allowed references and handles clicks through the generic action API. The optional KPI referenceIcon factory lets a trusted host supply its own decorative glyph; the renderer falls back to the link icon and never interprets what the custom icon represents:

<MarkdownChartsource={source}kpi={{referenceIcon: ({ document })=>createHostReferenceIcon(document),}}referenceActions={{canOpen: ({ reference })=>reference.ref.startsWith('docs://'),open: ({ reference })=>openDocumentation(reference.ref),}}/>

Keep the KPI options and referenceActions.canOpen / open callbacks stable across streaming renders so completed cards and resolved data can be reused.

Hosts can inspect canonical data without loading a chart runtime:

pnpm add @datafe-open/markdown-chart
import{parseMarkdownChartEnvelope}from'@datafe-open/markdown-chart';// chartFenceBody is the JSON text inside one markdown-chart fence.const{ data }=parseMarkdownChartEnvelope(chartFenceBody);if(data?.kind==='inline'){showDataTable(data.dimensions,data.source);}

Compact ECharts fence

The ECharts package also registers the exact echarts-fulldata fence used by the dataworks-chart skill. It is strict JSON, never JavaScript, and is a renderer-owned shorthand for the equivalent canonical envelope:

```echarts-fulldata{ "version": 1, "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "option": { "title": { "text": "Monthly sales" }, "series": [{ "type": "bar" }] }}```

The compact envelope saves the fixed renderer / spec wrapper while using the same option validation, title, data-ref resolution, and Chart/Data view as canonical ECharts. The singular echart-fulldata fence is not registered.

React + react-markdown

With the canonical Markdown above stored in source:

pnpm add echarts @datafe-open/markdown-chart-react
import{MarkdownChart}from'@datafe-open/markdown-chart-react';exportfunctionApp({ source }: {source: string}){return<MarkdownChartsource={source}/>;}

Vue 3 + markdown-it

pnpm add echarts @datafe-open/markdown-chart-vue
<script setup lang="ts">import { MarkdownChart } from'@datafe-open/markdown-chart-vue';defineProps<{ source:string }>();</script>
<template>
<MarkdownChart :source="source" />
</template>

Both components register ECharts and KPI automatically. They load ECharts on its first chart mount and apply a 360px minimum height to chart placeholders; the KPI renderer uses its compact content height. Canonical inline data and referenced data returned by resolveDataRef also enable a built-in icon-based Chart/Data switch with a bounded, scrollable data table. The card, toolbar, icons, table, and default ECharts palette/axes/tooltip/series styling are adapted from the Qwen Code WebShell implementation, while explicit ECharts option values still win. The React package includes react-markdown, and the Vue package includes markdown-it. Pass a custom registry, parser, theme, or renderer options only when the defaults are not sufficient. See Third-party notices for attribution.

Card colors can be aligned with the host through --markdown-chart-background, --markdown-chart-subtle-background, --markdown-chart-accent, and --markdown-chart-accent-foreground. The selected Chart/Data icon foreground falls back to the chart background and then #ffffff; hosts with a custom accent can override the accent and its foreground as a pair. Advanced registries can set createEChartsRenderer({ defaultStyle: false }) to disable the presentation defaults. Validation, canonical data injection, and data-ref resolution still apply.

Streaming

Pass the outer document streaming state to the framework component:

<MarkdownChartsource={source}streaming={isStreaming}/>
<MarkdownChart :source="source" :streaming="isStreaming" />

Closed chart fences render immediately and keep their mounted chart instance as later text arrives. Only the active unterminated tail fence waits for more input, including when a chart fence is nested in a blockquote. Pending fences and asynchronous parsing, data resolution, and runtime mounting show a built-in loading indicator instead of a blank placeholder. Use loadingLabel to localize its text and --markdown-chart-loading-color to align its color. Advanced React applications pass the same state to MarkdownChartProvider; advanced Vue applications pass it to MarkdownChart.

Localized labels

React and Vue hosts can pass a partial labels object to localize the Chart/Data controls, accessibility labels, empty-data text, truncation notice, and chart error fallback:

<MarkdownChartsource={source}labels={{chartUnavailable: '图表不可用',viewMode: '视图模式',chart: '图表',data: '数据',showChart: '显示图表',showData: '显示数据',noData: '暂无数据',tableNotice: ({ visibleRows, totalRows, visibleColumns, totalColumns })=>`显示 ${visibleRows}/${totalRows} 行,${visibleColumns}/${totalColumns} 列`,}}/>

The same MarkdownChartLabelOverrides type is accepted by MarkdownChartProvider, MarkdownChartBlock, the Vue composable/mounting utility, and the markdown-it plugin. Omitted labels use the English defaults.

Advanced setup

Create and pass a registry only when adding renderers or resolving host data:

import{ChartRendererRegistry}from'@datafe-open/markdown-chart';import{createEChartsRenderer}from'@datafe-open/markdown-chart-echarts';constregistry=newChartRendererRegistry();registry.register(createEChartsRenderer({resolveDataRef: async(ref,meta)=>loadApplicationDataset(ref,meta.signal),}));

The resolver returns { dimensions?, source }. If it omits dimensions, the dimensions declared on the ref are retained. ECharts uses the materialized rows for both option.dataset and the shared Chart/Data view, so referenced datasets can be inspected without duplicating them inline.

Pass the same live registry to framework adapters. Renderer aliases registered later, such as vega-lite or plotly, are then recognized without updating an adapter language list. The package never fetches a data reference itself; applications decide which reference schemes are allowed.

Existing react-markdown applications

The provider and components API remains available when the host already owns the surrounding Markdown renderer. Using the configured registry above, the integration remains:

importReactMarkdownfrom'react-markdown';import{MarkdownChartProvider,createMarkdownChartComponents,}from'@datafe-open/markdown-chart-react';constchartComponents=createMarkdownChartComponents({chartStyle: {minHeight: 360},});<MarkdownChartProviderregistry={registry}streaming={isStreaming}><ReactMarkdowncomponents={chartComponents}>{source}</ReactMarkdown></MarkdownChartProvider>

The provider infers source from its direct ReactMarkdown child, so streaming support does not add another required prop in this common advanced setup.

Because the application imports react-markdown directly in this mode, declare every directly imported package as an application dependency:

pnpm add echarts react-markdown \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-react

Existing Vue + markdown-it applications

Vue applications can keep their existing markdown-it instance and pass the same registry to both the plugin and component:

<script setup lang="ts">import { ChartRendererRegistry } from'@datafe-open/markdown-chart';import { createEChartsRenderer } from'@datafe-open/markdown-chart-echarts';import { markdownChartPlugin } from'@datafe-open/markdown-chart-markdown-it';import { MarkdownChart } from'@datafe-open/markdown-chart-vue';importMarkdownItfrom'markdown-it';defineProps<{ source:string; isStreaming:boolean }>();const registry =newChartRendererRegistry().register(createEChartsRenderer());const markdownIt =newMarkdownIt({ html: false }).use(markdownChartPlugin, {registry,});</script>
<template>
<MarkdownChart
:source="source"
:streaming="isStreaming"
:markdown-it="markdownIt"
:registry="registry"
/>
</template>

Declare the packages imported by this advanced setup directly:

pnpm add echarts markdown-it \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-markdown-it \
@datafe-open/markdown-chart-vue

See SPEC.md, SECURITY.md, and the Vue and React examples. Simple and advanced modes live in separate runnable folders with independent dependency manifests.

Development

pnpm install
pnpm test
pnpm typecheck
pnpm build
pnpm check:pack

The root build validates both publishable packages and all React/Vue Vite examples. Example workspaces are private and are never included in package tarballs. Published-package changes use Changesets; see RELEASING.md for bootstrap and automated release steps.

License

MIT. Portions are adapted from Qwen Code under Apache-2.0; see Third-party notices.

About

Extensible JSON chart rendering for Markdown, with ECharts, markdown-it, Vue, and react-markdown adapters

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Markdown Chart

English | 简体中文

Note

All six @datafe-open/markdown-chart* packages are published on npm. The install commands below use the public packages. Maintainers should follow RELEASING.md for subsequent releases.

markdown-chart provides portable chart blocks for streaming Markdown, with inspectable data and pluggable renderers. Its core is framework-neutral and independent of any chat product.

The project includes independent ECharts and KPI renderers plus adapters for markdown-it, Vue 3, and react-markdown. The registry-based core can accept future Plotly, Vega, or other renderer packages without adding chart-specific switches to the core.

Packages

PackagePurpose
@datafe-open/markdown-chartRenderer registry, canonical markdown-chart routing, and lifecycle controller
@datafe-open/markdown-chart-echartsStrict JSON-only canonical ECharts renderer and deprecated ChatBI legacy adapter
@datafe-open/markdown-chart-kpiStrict responsive multi-KPI cards with optional host-owned reference actions
@datafe-open/markdown-chart-markdown-itSafe placeholder plugin and environment side channel
@datafe-open/markdown-chart-vueVue 3 component and composable
@datafe-open/markdown-chart-reactreact-markdown code/pre adapter

Canonical Markdown

```markdown-chart{ "version": 1, "renderer": "echarts", "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "spec": { "title": { "text": "Monthly sales" }, "xAxis": { "type": "category" }, "yAxis": {}, "series": [{ "type": "bar", "encode": { "x": "month", "y": "sales" } }] }}```

There is only one protocol version, on the outer markdown-chart envelope. data is renderer-neutral so hosts can expose the inline rows independently, for example in a “View data” action. spec belongs to the selected renderer and does not repeat the data or version.

For ECharts, the shared card title comes only from spec.title.text, which is the ECharts option's title.text. If it is absent or blank, the title element is omitted instead of showing a fallback. The Chart/Data controls remain right-aligned, and the chart keeps 8px of vertical spacing from the toolbar.

KPI renderer

KPI cards use the same canonical data/configuration separation as ECharts. A shared default data dataset contains one row per time point; spec binds fields and safe formatting:

```markdown-chart{ "version": 1, "renderer": "kpi", "data": { "kind": "inline", "source": [ { "day": "2026-08-31", "unmet": 0.38, "revenue": 16800000 }, { "day": "2026-09-01", "unmet": 0.4, "revenue": 18000000 } ] }, "spec": { "timeField": "day", "items": [ { "id": "unmet_demand", "title": "Unmet demand", "value": { "field": "unmet", "format": { "style": "percent", "maximumFractionDigits": 0 } }, "status": { "text": { "literal": "At risk" }, "tone": { "literal": "negative" } }, "trend": { "type": "area", "compare": { "lag": 1, "mode": "absolute", "polarity": "lower-is-better" } }, "references": [ { "ref": "docs://metrics/unmet-demand", "label": "Metric definition" } ] }, { "id": "revenue", "title": "Revenue", "value": { "field": "revenue", "format": { "style": "currency", "currency": "CNY", "notation": "compact" } } } ] }}```

The renderer supports 1–12 items, lastNonNull reduction, safe structured Intl.NumberFormat options, field/literal status bindings, and optional line/area sparklines with deterministic lag comparison. Items with fewer than two valid trend points fall back to a plain KPI. A group can freely mix items with and without trends.

Prefer the shared default data when KPI items use the same grain, filters, and source. When they do not, put additional canonical ChartData objects in the top-level datasets map and select one with item.dataset. Omitting dataset selects the default data; the selector is deliberately named dataset so it cannot be confused with KPI references:

{
"version": 1,
"renderer": "kpi",
"data": { "kind": "inline", "source": [{ "revenue": 18000000 }] },
"datasets": {
"inventory": {
"kind": "inline",
"source": [{ "day": "2026-09-01", "stock": 23 }]
}
},
"spec": {
"items": [
{ "id": "revenue", "title": "Revenue", "value": { "field": "revenue" } },
{
"id": "inventory",
"title": "Inventory",
"dataset": "inventory",
"value": { "field": "stock" },
"trend": { "type": "line", "timeField": "day" }
}
]
}
}

Every selected inline/ref dataset is materialized once, even when multiple KPI items select it. Row and cell limits apply to the complete selected collection. The built-in Data view continues to inspect the default data; a named-only KPI group renders without that single-dataset toggle.

Both inline and referenced ChartData are supported. For ref data, provide the host-owned resolver through the adapter's independent kpi option; the result is materialized once for the value, sparkline, comparison, and Data view:

constkpiOptions=useMemo(()=>({validateDataRef: (ref)=>ref.startsWith('dataset://'),resolveDataRef: (ref,{ signal })=>loadDataset(ref,signal),}),[],);<MarkdownChartsource={source}kpi={kpiOptions}/>

Each item accepts up to three references. The renderer treats every reference as opaque and never fetches or navigates. A host selects allowed references and handles clicks through the generic action API. The optional KPI referenceIcon factory lets a trusted host supply its own decorative glyph; the renderer falls back to the link icon and never interprets what the custom icon represents:

<MarkdownChartsource={source}kpi={{referenceIcon: ({ document })=>createHostReferenceIcon(document),}}referenceActions={{canOpen: ({ reference })=>reference.ref.startsWith('docs://'),open: ({ reference })=>openDocumentation(reference.ref),}}/>

Keep the KPI options and referenceActions.canOpen / open callbacks stable across streaming renders so completed cards and resolved data can be reused.

Hosts can inspect canonical data without loading a chart runtime:

pnpm add @datafe-open/markdown-chart
import{parseMarkdownChartEnvelope}from'@datafe-open/markdown-chart';// chartFenceBody is the JSON text inside one markdown-chart fence.const{ data }=parseMarkdownChartEnvelope(chartFenceBody);if(data?.kind==='inline'){showDataTable(data.dimensions,data.source);}

Compact ECharts fence

The ECharts package also registers the exact echarts-fulldata fence used by the dataworks-chart skill. It is strict JSON, never JavaScript, and is a renderer-owned shorthand for the equivalent canonical envelope:

```echarts-fulldata{ "version": 1, "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "option": { "title": { "text": "Monthly sales" }, "series": [{ "type": "bar" }] }}```

The compact envelope saves the fixed renderer / spec wrapper while using the same option validation, title, data-ref resolution, and Chart/Data view as canonical ECharts. The singular echart-fulldata fence is not registered.

React + react-markdown

With the canonical Markdown above stored in source:

pnpm add echarts @datafe-open/markdown-chart-react
import{MarkdownChart}from'@datafe-open/markdown-chart-react';exportfunctionApp({ source }: {source: string}){return<MarkdownChartsource={source}/>;}

Vue 3 + markdown-it

pnpm add echarts @datafe-open/markdown-chart-vue
<script setup lang="ts">import { MarkdownChart } from'@datafe-open/markdown-chart-vue';defineProps<{ source:string }>();</script>
<template>
<MarkdownChart :source="source" />
</template>

Both components register ECharts and KPI automatically. They load ECharts on its first chart mount and apply a 360px minimum height to chart placeholders; the KPI renderer uses its compact content height. Canonical inline data and referenced data returned by resolveDataRef also enable a built-in icon-based Chart/Data switch with a bounded, scrollable data table. The card, toolbar, icons, table, and default ECharts palette/axes/tooltip/series styling are adapted from the Qwen Code WebShell implementation, while explicit ECharts option values still win. The React package includes react-markdown, and the Vue package includes markdown-it. Pass a custom registry, parser, theme, or renderer options only when the defaults are not sufficient. See Third-party notices for attribution.

Card colors can be aligned with the host through --markdown-chart-background, --markdown-chart-subtle-background, --markdown-chart-accent, and --markdown-chart-accent-foreground. The selected Chart/Data icon foreground falls back to the chart background and then #ffffff; hosts with a custom accent can override the accent and its foreground as a pair. Advanced registries can set createEChartsRenderer({ defaultStyle: false }) to disable the presentation defaults. Validation, canonical data injection, and data-ref resolution still apply.

Streaming

Pass the outer document streaming state to the framework component:

<MarkdownChartsource={source}streaming={isStreaming}/>
<MarkdownChart :source="source" :streaming="isStreaming" />

Closed chart fences render immediately and keep their mounted chart instance as later text arrives. Only the active unterminated tail fence waits for more input, including when a chart fence is nested in a blockquote. Pending fences and asynchronous parsing, data resolution, and runtime mounting show a built-in loading indicator instead of a blank placeholder. Use loadingLabel to localize its text and --markdown-chart-loading-color to align its color. Advanced React applications pass the same state to MarkdownChartProvider; advanced Vue applications pass it to MarkdownChart.

Localized labels

React and Vue hosts can pass a partial labels object to localize the Chart/Data controls, accessibility labels, empty-data text, truncation notice, and chart error fallback:

<MarkdownChartsource={source}labels={{chartUnavailable: '图表不可用',viewMode: '视图模式',chart: '图表',data: '数据',showChart: '显示图表',showData: '显示数据',noData: '暂无数据',tableNotice: ({ visibleRows, totalRows, visibleColumns, totalColumns })=>`显示 ${visibleRows}/${totalRows} 行,${visibleColumns}/${totalColumns} 列`,}}/>

The same MarkdownChartLabelOverrides type is accepted by MarkdownChartProvider, MarkdownChartBlock, the Vue composable/mounting utility, and the markdown-it plugin. Omitted labels use the English defaults.

Advanced setup

Create and pass a registry only when adding renderers or resolving host data:

import{ChartRendererRegistry}from'@datafe-open/markdown-chart';import{createEChartsRenderer}from'@datafe-open/markdown-chart-echarts';constregistry=newChartRendererRegistry();registry.register(createEChartsRenderer({resolveDataRef: async(ref,meta)=>loadApplicationDataset(ref,meta.signal),}));

The resolver returns { dimensions?, source }. If it omits dimensions, the dimensions declared on the ref are retained. ECharts uses the materialized rows for both option.dataset and the shared Chart/Data view, so referenced datasets can be inspected without duplicating them inline.

Pass the same live registry to framework adapters. Renderer aliases registered later, such as vega-lite or plotly, are then recognized without updating an adapter language list. The package never fetches a data reference itself; applications decide which reference schemes are allowed.

Existing react-markdown applications

The provider and components API remains available when the host already owns the surrounding Markdown renderer. Using the configured registry above, the integration remains:

importReactMarkdownfrom'react-markdown';import{MarkdownChartProvider,createMarkdownChartComponents,}from'@datafe-open/markdown-chart-react';constchartComponents=createMarkdownChartComponents({chartStyle: {minHeight: 360},});<MarkdownChartProviderregistry={registry}streaming={isStreaming}><ReactMarkdowncomponents={chartComponents}>{source}</ReactMarkdown></MarkdownChartProvider>

The provider infers source from its direct ReactMarkdown child, so streaming support does not add another required prop in this common advanced setup.

Because the application imports react-markdown directly in this mode, declare every directly imported package as an application dependency:

pnpm add echarts react-markdown \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-react

Existing Vue + markdown-it applications

Vue applications can keep their existing markdown-it instance and pass the same registry to both the plugin and component:

<script setup lang="ts">import { ChartRendererRegistry } from'@datafe-open/markdown-chart';import { createEChartsRenderer } from'@datafe-open/markdown-chart-echarts';import { markdownChartPlugin } from'@datafe-open/markdown-chart-markdown-it';import { MarkdownChart } from'@datafe-open/markdown-chart-vue';importMarkdownItfrom'markdown-it';defineProps<{ source:string; isStreaming:boolean }>();const registry =newChartRendererRegistry().register(createEChartsRenderer());const markdownIt =newMarkdownIt({ html: false }).use(markdownChartPlugin, {registry,});</script>
<template>
<MarkdownChart
:source="source"
:streaming="isStreaming"
:markdown-it="markdownIt"
:registry="registry"
/>
</template>

Declare the packages imported by this advanced setup directly:

pnpm add echarts markdown-it \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-markdown-it \
@datafe-open/markdown-chart-vue

See SPEC.md, SECURITY.md, and the Vue and React examples. Simple and advanced modes live in separate runnable folders with independent dependency manifests.

Development

pnpm install
pnpm test
pnpm typecheck
pnpm build
pnpm check:pack

The root build validates both publishable packages and all React/Vue Vite examples. Example workspaces are private and are never included in package tarballs. Published-package changes use Changesets; see RELEASING.md for bootstrap and automated release steps.

License

MIT. Portions are adapted from Qwen Code under Apache-2.0; see Third-party notices.

About

Extensible JSON chart rendering for Markdown, with ECharts, markdown-it, Vue, and react-markdown adapters

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Markdown Chart

English | 简体中文

Note

All six @datafe-open/markdown-chart* packages are published on npm. The install commands below use the public packages. Maintainers should follow RELEASING.md for subsequent releases.

markdown-chart provides portable chart blocks for streaming Markdown, with inspectable data and pluggable renderers. Its core is framework-neutral and independent of any chat product.

The project includes independent ECharts and KPI renderers plus adapters for markdown-it, Vue 3, and react-markdown. The registry-based core can accept future Plotly, Vega, or other renderer packages without adding chart-specific switches to the core.

Packages

PackagePurpose
@datafe-open/markdown-chartRenderer registry, canonical markdown-chart routing, and lifecycle controller
@datafe-open/markdown-chart-echartsStrict JSON-only canonical ECharts renderer and deprecated ChatBI legacy adapter
@datafe-open/markdown-chart-kpiStrict responsive multi-KPI cards with optional host-owned reference actions
@datafe-open/markdown-chart-markdown-itSafe placeholder plugin and environment side channel
@datafe-open/markdown-chart-vueVue 3 component and composable
@datafe-open/markdown-chart-reactreact-markdown code/pre adapter

Canonical Markdown

```markdown-chart{ "version": 1, "renderer": "echarts", "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "spec": { "title": { "text": "Monthly sales" }, "xAxis": { "type": "category" }, "yAxis": {}, "series": [{ "type": "bar", "encode": { "x": "month", "y": "sales" } }] }}```

There is only one protocol version, on the outer markdown-chart envelope. data is renderer-neutral so hosts can expose the inline rows independently, for example in a “View data” action. spec belongs to the selected renderer and does not repeat the data or version.

For ECharts, the shared card title comes only from spec.title.text, which is the ECharts option's title.text. If it is absent or blank, the title element is omitted instead of showing a fallback. The Chart/Data controls remain right-aligned, and the chart keeps 8px of vertical spacing from the toolbar.

KPI renderer

KPI cards use the same canonical data/configuration separation as ECharts. A shared default data dataset contains one row per time point; spec binds fields and safe formatting:

```markdown-chart{ "version": 1, "renderer": "kpi", "data": { "kind": "inline", "source": [ { "day": "2026-08-31", "unmet": 0.38, "revenue": 16800000 }, { "day": "2026-09-01", "unmet": 0.4, "revenue": 18000000 } ] }, "spec": { "timeField": "day", "items": [ { "id": "unmet_demand", "title": "Unmet demand", "value": { "field": "unmet", "format": { "style": "percent", "maximumFractionDigits": 0 } }, "status": { "text": { "literal": "At risk" }, "tone": { "literal": "negative" } }, "trend": { "type": "area", "compare": { "lag": 1, "mode": "absolute", "polarity": "lower-is-better" } }, "references": [ { "ref": "docs://metrics/unmet-demand", "label": "Metric definition" } ] }, { "id": "revenue", "title": "Revenue", "value": { "field": "revenue", "format": { "style": "currency", "currency": "CNY", "notation": "compact" } } } ] }}```

The renderer supports 1–12 items, lastNonNull reduction, safe structured Intl.NumberFormat options, field/literal status bindings, and optional line/area sparklines with deterministic lag comparison. Items with fewer than two valid trend points fall back to a plain KPI. A group can freely mix items with and without trends.

Prefer the shared default data when KPI items use the same grain, filters, and source. When they do not, put additional canonical ChartData objects in the top-level datasets map and select one with item.dataset. Omitting dataset selects the default data; the selector is deliberately named dataset so it cannot be confused with KPI references:

{
"version": 1,
"renderer": "kpi",
"data": { "kind": "inline", "source": [{ "revenue": 18000000 }] },
"datasets": {
"inventory": {
"kind": "inline",
"source": [{ "day": "2026-09-01", "stock": 23 }]
}
},
"spec": {
"items": [
{ "id": "revenue", "title": "Revenue", "value": { "field": "revenue" } },
{
"id": "inventory",
"title": "Inventory",
"dataset": "inventory",
"value": { "field": "stock" },
"trend": { "type": "line", "timeField": "day" }
}
]
}
}

Every selected inline/ref dataset is materialized once, even when multiple KPI items select it. Row and cell limits apply to the complete selected collection. The built-in Data view continues to inspect the default data; a named-only KPI group renders without that single-dataset toggle.

Both inline and referenced ChartData are supported. For ref data, provide the host-owned resolver through the adapter's independent kpi option; the result is materialized once for the value, sparkline, comparison, and Data view:

constkpiOptions=useMemo(()=>({validateDataRef: (ref)=>ref.startsWith('dataset://'),resolveDataRef: (ref,{ signal })=>loadDataset(ref,signal),}),[],);<MarkdownChartsource={source}kpi={kpiOptions}/>

Each item accepts up to three references. The renderer treats every reference as opaque and never fetches or navigates. A host selects allowed references and handles clicks through the generic action API. The optional KPI referenceIcon factory lets a trusted host supply its own decorative glyph; the renderer falls back to the link icon and never interprets what the custom icon represents:

<MarkdownChartsource={source}kpi={{referenceIcon: ({ document })=>createHostReferenceIcon(document),}}referenceActions={{canOpen: ({ reference })=>reference.ref.startsWith('docs://'),open: ({ reference })=>openDocumentation(reference.ref),}}/>

Keep the KPI options and referenceActions.canOpen / open callbacks stable across streaming renders so completed cards and resolved data can be reused.

Hosts can inspect canonical data without loading a chart runtime:

pnpm add @datafe-open/markdown-chart
import{parseMarkdownChartEnvelope}from'@datafe-open/markdown-chart';// chartFenceBody is the JSON text inside one markdown-chart fence.const{ data }=parseMarkdownChartEnvelope(chartFenceBody);if(data?.kind==='inline'){showDataTable(data.dimensions,data.source);}

Compact ECharts fence

The ECharts package also registers the exact echarts-fulldata fence used by the dataworks-chart skill. It is strict JSON, never JavaScript, and is a renderer-owned shorthand for the equivalent canonical envelope:

```echarts-fulldata{ "version": 1, "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "option": { "title": { "text": "Monthly sales" }, "series": [{ "type": "bar" }] }}```

The compact envelope saves the fixed renderer / spec wrapper while using the same option validation, title, data-ref resolution, and Chart/Data view as canonical ECharts. The singular echart-fulldata fence is not registered.

React + react-markdown

With the canonical Markdown above stored in source:

pnpm add echarts @datafe-open/markdown-chart-react
import{MarkdownChart}from'@datafe-open/markdown-chart-react';exportfunctionApp({ source }: {source: string}){return<MarkdownChartsource={source}/>;}

Vue 3 + markdown-it

pnpm add echarts @datafe-open/markdown-chart-vue
<script setup lang="ts">import { MarkdownChart } from'@datafe-open/markdown-chart-vue';defineProps<{ source:string }>();</script>
<template>
<MarkdownChart :source="source" />
</template>

Both components register ECharts and KPI automatically. They load ECharts on its first chart mount and apply a 360px minimum height to chart placeholders; the KPI renderer uses its compact content height. Canonical inline data and referenced data returned by resolveDataRef also enable a built-in icon-based Chart/Data switch with a bounded, scrollable data table. The card, toolbar, icons, table, and default ECharts palette/axes/tooltip/series styling are adapted from the Qwen Code WebShell implementation, while explicit ECharts option values still win. The React package includes react-markdown, and the Vue package includes markdown-it. Pass a custom registry, parser, theme, or renderer options only when the defaults are not sufficient. See Third-party notices for attribution.

Card colors can be aligned with the host through --markdown-chart-background, --markdown-chart-subtle-background, --markdown-chart-accent, and --markdown-chart-accent-foreground. The selected Chart/Data icon foreground falls back to the chart background and then #ffffff; hosts with a custom accent can override the accent and its foreground as a pair. Advanced registries can set createEChartsRenderer({ defaultStyle: false }) to disable the presentation defaults. Validation, canonical data injection, and data-ref resolution still apply.

Streaming

Pass the outer document streaming state to the framework component:

<MarkdownChartsource={source}streaming={isStreaming}/>
<MarkdownChart :source="source" :streaming="isStreaming" />

Closed chart fences render immediately and keep their mounted chart instance as later text arrives. Only the active unterminated tail fence waits for more input, including when a chart fence is nested in a blockquote. Pending fences and asynchronous parsing, data resolution, and runtime mounting show a built-in loading indicator instead of a blank placeholder. Use loadingLabel to localize its text and --markdown-chart-loading-color to align its color. Advanced React applications pass the same state to MarkdownChartProvider; advanced Vue applications pass it to MarkdownChart.

Localized labels

React and Vue hosts can pass a partial labels object to localize the Chart/Data controls, accessibility labels, empty-data text, truncation notice, and chart error fallback:

<MarkdownChartsource={source}labels={{chartUnavailable: '图表不可用',viewMode: '视图模式',chart: '图表',data: '数据',showChart: '显示图表',showData: '显示数据',noData: '暂无数据',tableNotice: ({ visibleRows, totalRows, visibleColumns, totalColumns })=>`显示 ${visibleRows}/${totalRows} 行,${visibleColumns}/${totalColumns} 列`,}}/>

The same MarkdownChartLabelOverrides type is accepted by MarkdownChartProvider, MarkdownChartBlock, the Vue composable/mounting utility, and the markdown-it plugin. Omitted labels use the English defaults.

Advanced setup

Create and pass a registry only when adding renderers or resolving host data:

import{ChartRendererRegistry}from'@datafe-open/markdown-chart';import{createEChartsRenderer}from'@datafe-open/markdown-chart-echarts';constregistry=newChartRendererRegistry();registry.register(createEChartsRenderer({resolveDataRef: async(ref,meta)=>loadApplicationDataset(ref,meta.signal),}));

The resolver returns { dimensions?, source }. If it omits dimensions, the dimensions declared on the ref are retained. ECharts uses the materialized rows for both option.dataset and the shared Chart/Data view, so referenced datasets can be inspected without duplicating them inline.

Pass the same live registry to framework adapters. Renderer aliases registered later, such as vega-lite or plotly, are then recognized without updating an adapter language list. The package never fetches a data reference itself; applications decide which reference schemes are allowed.

Existing react-markdown applications

The provider and components API remains available when the host already owns the surrounding Markdown renderer. Using the configured registry above, the integration remains:

importReactMarkdownfrom'react-markdown';import{MarkdownChartProvider,createMarkdownChartComponents,}from'@datafe-open/markdown-chart-react';constchartComponents=createMarkdownChartComponents({chartStyle: {minHeight: 360},});<MarkdownChartProviderregistry={registry}streaming={isStreaming}><ReactMarkdowncomponents={chartComponents}>{source}</ReactMarkdown></MarkdownChartProvider>

The provider infers source from its direct ReactMarkdown child, so streaming support does not add another required prop in this common advanced setup.

Because the application imports react-markdown directly in this mode, declare every directly imported package as an application dependency:

pnpm add echarts react-markdown \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-react

Existing Vue + markdown-it applications

Vue applications can keep their existing markdown-it instance and pass the same registry to both the plugin and component:

<script setup lang="ts">import { ChartRendererRegistry } from'@datafe-open/markdown-chart';import { createEChartsRenderer } from'@datafe-open/markdown-chart-echarts';import { markdownChartPlugin } from'@datafe-open/markdown-chart-markdown-it';import { MarkdownChart } from'@datafe-open/markdown-chart-vue';importMarkdownItfrom'markdown-it';defineProps<{ source:string; isStreaming:boolean }>();const registry =newChartRendererRegistry().register(createEChartsRenderer());const markdownIt =newMarkdownIt({ html: false }).use(markdownChartPlugin, {registry,});</script>
<template>
<MarkdownChart
:source="source"
:streaming="isStreaming"
:markdown-it="markdownIt"
:registry="registry"
/>
</template>

Declare the packages imported by this advanced setup directly:

pnpm add echarts markdown-it \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-markdown-it \
@datafe-open/markdown-chart-vue

See SPEC.md, SECURITY.md, and the Vue and React examples. Simple and advanced modes live in separate runnable folders with independent dependency manifests.

Development

pnpm install
pnpm test
pnpm typecheck
pnpm build
pnpm check:pack

The root build validates both publishable packages and all React/Vue Vite examples. Example workspaces are private and are never included in package tarballs. Published-package changes use Changesets; see RELEASING.md for bootstrap and automated release steps.

License

MIT. Portions are adapted from Qwen Code under Apache-2.0; see Third-party notices.

About

Extensible JSON chart rendering for Markdown, with ECharts, markdown-it, Vue, and react-markdown adapters

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Markdown Chart

English | 简体中文

Note

All six @datafe-open/markdown-chart* packages are published on npm. The install commands below use the public packages. Maintainers should follow RELEASING.md for subsequent releases.

markdown-chart provides portable chart blocks for streaming Markdown, with inspectable data and pluggable renderers. Its core is framework-neutral and independent of any chat product.

The project includes independent ECharts and KPI renderers plus adapters for markdown-it, Vue 3, and react-markdown. The registry-based core can accept future Plotly, Vega, or other renderer packages without adding chart-specific switches to the core.

Packages

PackagePurpose
@datafe-open/markdown-chartRenderer registry, canonical markdown-chart routing, and lifecycle controller
@datafe-open/markdown-chart-echartsStrict JSON-only canonical ECharts renderer and deprecated ChatBI legacy adapter
@datafe-open/markdown-chart-kpiStrict responsive multi-KPI cards with optional host-owned reference actions
@datafe-open/markdown-chart-markdown-itSafe placeholder plugin and environment side channel
@datafe-open/markdown-chart-vueVue 3 component and composable
@datafe-open/markdown-chart-reactreact-markdown code/pre adapter

Canonical Markdown

```markdown-chart{ "version": 1, "renderer": "echarts", "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "spec": { "title": { "text": "Monthly sales" }, "xAxis": { "type": "category" }, "yAxis": {}, "series": [{ "type": "bar", "encode": { "x": "month", "y": "sales" } }] }}```

There is only one protocol version, on the outer markdown-chart envelope. data is renderer-neutral so hosts can expose the inline rows independently, for example in a “View data” action. spec belongs to the selected renderer and does not repeat the data or version.

For ECharts, the shared card title comes only from spec.title.text, which is the ECharts option's title.text. If it is absent or blank, the title element is omitted instead of showing a fallback. The Chart/Data controls remain right-aligned, and the chart keeps 8px of vertical spacing from the toolbar.

KPI renderer

KPI cards use the same canonical data/configuration separation as ECharts. A shared default data dataset contains one row per time point; spec binds fields and safe formatting:

```markdown-chart{ "version": 1, "renderer": "kpi", "data": { "kind": "inline", "source": [ { "day": "2026-08-31", "unmet": 0.38, "revenue": 16800000 }, { "day": "2026-09-01", "unmet": 0.4, "revenue": 18000000 } ] }, "spec": { "timeField": "day", "items": [ { "id": "unmet_demand", "title": "Unmet demand", "value": { "field": "unmet", "format": { "style": "percent", "maximumFractionDigits": 0 } }, "status": { "text": { "literal": "At risk" }, "tone": { "literal": "negative" } }, "trend": { "type": "area", "compare": { "lag": 1, "mode": "absolute", "polarity": "lower-is-better" } }, "references": [ { "ref": "docs://metrics/unmet-demand", "label": "Metric definition" } ] }, { "id": "revenue", "title": "Revenue", "value": { "field": "revenue", "format": { "style": "currency", "currency": "CNY", "notation": "compact" } } } ] }}```

The renderer supports 1–12 items, lastNonNull reduction, safe structured Intl.NumberFormat options, field/literal status bindings, and optional line/area sparklines with deterministic lag comparison. Items with fewer than two valid trend points fall back to a plain KPI. A group can freely mix items with and without trends.

Prefer the shared default data when KPI items use the same grain, filters, and source. When they do not, put additional canonical ChartData objects in the top-level datasets map and select one with item.dataset. Omitting dataset selects the default data; the selector is deliberately named dataset so it cannot be confused with KPI references:

{
"version": 1,
"renderer": "kpi",
"data": { "kind": "inline", "source": [{ "revenue": 18000000 }] },
"datasets": {
"inventory": {
"kind": "inline",
"source": [{ "day": "2026-09-01", "stock": 23 }]
}
},
"spec": {
"items": [
{ "id": "revenue", "title": "Revenue", "value": { "field": "revenue" } },
{
"id": "inventory",
"title": "Inventory",
"dataset": "inventory",
"value": { "field": "stock" },
"trend": { "type": "line", "timeField": "day" }
}
]
}
}

Every selected inline/ref dataset is materialized once, even when multiple KPI items select it. Row and cell limits apply to the complete selected collection. The built-in Data view continues to inspect the default data; a named-only KPI group renders without that single-dataset toggle.

Both inline and referenced ChartData are supported. For ref data, provide the host-owned resolver through the adapter's independent kpi option; the result is materialized once for the value, sparkline, comparison, and Data view:

constkpiOptions=useMemo(()=>({validateDataRef: (ref)=>ref.startsWith('dataset://'),resolveDataRef: (ref,{ signal })=>loadDataset(ref,signal),}),[],);<MarkdownChartsource={source}kpi={kpiOptions}/>

Each item accepts up to three references. The renderer treats every reference as opaque and never fetches or navigates. A host selects allowed references and handles clicks through the generic action API. The optional KPI referenceIcon factory lets a trusted host supply its own decorative glyph; the renderer falls back to the link icon and never interprets what the custom icon represents:

<MarkdownChartsource={source}kpi={{referenceIcon: ({ document })=>createHostReferenceIcon(document),}}referenceActions={{canOpen: ({ reference })=>reference.ref.startsWith('docs://'),open: ({ reference })=>openDocumentation(reference.ref),}}/>

Keep the KPI options and referenceActions.canOpen / open callbacks stable across streaming renders so completed cards and resolved data can be reused.

Hosts can inspect canonical data without loading a chart runtime:

pnpm add @datafe-open/markdown-chart
import{parseMarkdownChartEnvelope}from'@datafe-open/markdown-chart';// chartFenceBody is the JSON text inside one markdown-chart fence.const{ data }=parseMarkdownChartEnvelope(chartFenceBody);if(data?.kind==='inline'){showDataTable(data.dimensions,data.source);}

Compact ECharts fence

The ECharts package also registers the exact echarts-fulldata fence used by the dataworks-chart skill. It is strict JSON, never JavaScript, and is a renderer-owned shorthand for the equivalent canonical envelope:

```echarts-fulldata{ "version": 1, "data": { "kind": "inline", "dimensions": ["month", "sales"], "source": [["Jan", 100], ["Feb", 180]] }, "option": { "title": { "text": "Monthly sales" }, "series": [{ "type": "bar" }] }}```

The compact envelope saves the fixed renderer / spec wrapper while using the same option validation, title, data-ref resolution, and Chart/Data view as canonical ECharts. The singular echart-fulldata fence is not registered.

React + react-markdown

With the canonical Markdown above stored in source:

pnpm add echarts @datafe-open/markdown-chart-react
import{MarkdownChart}from'@datafe-open/markdown-chart-react';exportfunctionApp({ source }: {source: string}){return<MarkdownChartsource={source}/>;}

Vue 3 + markdown-it

pnpm add echarts @datafe-open/markdown-chart-vue
<script setup lang="ts">import { MarkdownChart } from'@datafe-open/markdown-chart-vue';defineProps<{ source:string }>();</script>
<template>
<MarkdownChart :source="source" />
</template>

Both components register ECharts and KPI automatically. They load ECharts on its first chart mount and apply a 360px minimum height to chart placeholders; the KPI renderer uses its compact content height. Canonical inline data and referenced data returned by resolveDataRef also enable a built-in icon-based Chart/Data switch with a bounded, scrollable data table. The card, toolbar, icons, table, and default ECharts palette/axes/tooltip/series styling are adapted from the Qwen Code WebShell implementation, while explicit ECharts option values still win. The React package includes react-markdown, and the Vue package includes markdown-it. Pass a custom registry, parser, theme, or renderer options only when the defaults are not sufficient. See Third-party notices for attribution.

Card colors can be aligned with the host through --markdown-chart-background, --markdown-chart-subtle-background, --markdown-chart-accent, and --markdown-chart-accent-foreground. The selected Chart/Data icon foreground falls back to the chart background and then #ffffff; hosts with a custom accent can override the accent and its foreground as a pair. Advanced registries can set createEChartsRenderer({ defaultStyle: false }) to disable the presentation defaults. Validation, canonical data injection, and data-ref resolution still apply.

Streaming

Pass the outer document streaming state to the framework component:

<MarkdownChartsource={source}streaming={isStreaming}/>
<MarkdownChart :source="source" :streaming="isStreaming" />

Closed chart fences render immediately and keep their mounted chart instance as later text arrives. Only the active unterminated tail fence waits for more input, including when a chart fence is nested in a blockquote. Pending fences and asynchronous parsing, data resolution, and runtime mounting show a built-in loading indicator instead of a blank placeholder. Use loadingLabel to localize its text and --markdown-chart-loading-color to align its color. Advanced React applications pass the same state to MarkdownChartProvider; advanced Vue applications pass it to MarkdownChart.

Localized labels

React and Vue hosts can pass a partial labels object to localize the Chart/Data controls, accessibility labels, empty-data text, truncation notice, and chart error fallback:

<MarkdownChartsource={source}labels={{chartUnavailable: '图表不可用',viewMode: '视图模式',chart: '图表',data: '数据',showChart: '显示图表',showData: '显示数据',noData: '暂无数据',tableNotice: ({ visibleRows, totalRows, visibleColumns, totalColumns })=>`显示 ${visibleRows}/${totalRows} 行,${visibleColumns}/${totalColumns} 列`,}}/>

The same MarkdownChartLabelOverrides type is accepted by MarkdownChartProvider, MarkdownChartBlock, the Vue composable/mounting utility, and the markdown-it plugin. Omitted labels use the English defaults.

Advanced setup

Create and pass a registry only when adding renderers or resolving host data:

import{ChartRendererRegistry}from'@datafe-open/markdown-chart';import{createEChartsRenderer}from'@datafe-open/markdown-chart-echarts';constregistry=newChartRendererRegistry();registry.register(createEChartsRenderer({resolveDataRef: async(ref,meta)=>loadApplicationDataset(ref,meta.signal),}));

The resolver returns { dimensions?, source }. If it omits dimensions, the dimensions declared on the ref are retained. ECharts uses the materialized rows for both option.dataset and the shared Chart/Data view, so referenced datasets can be inspected without duplicating them inline.

Pass the same live registry to framework adapters. Renderer aliases registered later, such as vega-lite or plotly, are then recognized without updating an adapter language list. The package never fetches a data reference itself; applications decide which reference schemes are allowed.

Existing react-markdown applications

The provider and components API remains available when the host already owns the surrounding Markdown renderer. Using the configured registry above, the integration remains:

importReactMarkdownfrom'react-markdown';import{MarkdownChartProvider,createMarkdownChartComponents,}from'@datafe-open/markdown-chart-react';constchartComponents=createMarkdownChartComponents({chartStyle: {minHeight: 360},});<MarkdownChartProviderregistry={registry}streaming={isStreaming}><ReactMarkdowncomponents={chartComponents}>{source}</ReactMarkdown></MarkdownChartProvider>

The provider infers source from its direct ReactMarkdown child, so streaming support does not add another required prop in this common advanced setup.

Because the application imports react-markdown directly in this mode, declare every directly imported package as an application dependency:

pnpm add echarts react-markdown \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-react

Existing Vue + markdown-it applications

Vue applications can keep their existing markdown-it instance and pass the same registry to both the plugin and component:

<script setup lang="ts">import { ChartRendererRegistry } from'@datafe-open/markdown-chart';import { createEChartsRenderer } from'@datafe-open/markdown-chart-echarts';import { markdownChartPlugin } from'@datafe-open/markdown-chart-markdown-it';import { MarkdownChart } from'@datafe-open/markdown-chart-vue';importMarkdownItfrom'markdown-it';defineProps<{ source:string; isStreaming:boolean }>();const registry =newChartRendererRegistry().register(createEChartsRenderer());const markdownIt =newMarkdownIt({ html: false }).use(markdownChartPlugin, {registry,});</script>
<template>
<MarkdownChart
:source="source"
:streaming="isStreaming"
:markdown-it="markdownIt"
:registry="registry"
/>
</template>

Declare the packages imported by this advanced setup directly:

pnpm add echarts markdown-it \
@datafe-open/markdown-chart \
@datafe-open/markdown-chart-echarts \
@datafe-open/markdown-chart-markdown-it \
@datafe-open/markdown-chart-vue

See SPEC.md, SECURITY.md, and the Vue and React examples. Simple and advanced modes live in separate runnable folders with independent dependency manifests.

Development

pnpm install
pnpm test
pnpm typecheck
pnpm build
pnpm check:pack

The root build validates both publishable packages and all React/Vue Vite examples. Example workspaces are private and are never included in package tarballs. Published-package changes use Changesets; see RELEASING.md for bootstrap and automated release steps.

License

MIT. Portions are adapted from Qwen Code under Apache-2.0; see Third-party notices.

About

Extensible JSON chart rendering for Markdown, with ECharts, markdown-it, Vue, and react-markdown adapters

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages