The development of chart plugins is a key component of custom visualization, enabling users to turn personalized data presentations into reality. This article will explore the internal structure and development process of chart plugins in depth, guiding you through concrete examples to understand the project architecture and complete the creation of a custom chart plugin. (Currently, the custom visualization feature is still in the beta testing phase. If you have specific needs for this feature, please contact the Volcano Engine business team to obtain more details about purchasing and collaboration.)
Caution: Before reading and completing the configuration items in this article, you need to complete the preparation steps such as environment configuration, initialization, and debugging. For more details, refer to Overview of custom visualization.
In Overview of custom visualization, you completed the following configuration items:
contributes property in the structure description file package.json.This section will explain the above configuration items in detail using the project structure of a sample plugin:
├── assets // 存放资源的目录,比如 icon ├── dist // dev/build 的产物 │ └── index.html // src/index.html 的编译产物 │ └── main.umd.js // src/main.ts 的编译产物 ├── src // 源码目录 │ └── index.html // (存在自定义图表时)自定义图表的 html 文件 │ └── main.ts // 插件入口文件,用于本产品中注册插件 ├── package.json // 插件描述文件 ├── extension.config.js // 插件 CLI 配置文件
In this file, you need to export two lifecycle methods, activate and deactivate. The application will call these methods when the plugin is activated and deactivated.
activate method, you need to call the registration method in the plugin context object provided by the application to complete the registration of chart information.deactivate method is used for cleanup when uninstalling the plugin and is empty by default.export function activate(context) { context.vizQueryChartRenderer.register({ id: 'extension-demo' }) } export function deactivate() {}
For security reasons, the plugin entry file runs in an isolated Javascript sandbox, so you cannot call browser APIs within it.
For extension features such as custom charts that require rendering, you must additionally provide an HTML file to load the chart, such as src/index.html.
The code related to rendering runs in an iframe sandbox, and you can call browser APIs to implement the required functionality.
This file is a superset based on package.json, and uses the main and contributes fields to specify the plugin entry file location and which extension features the plugin includes, respectively.
For example, for the custom chart in the sample plugin package, you need to fill in the relevant information in the vizQuery.chart.renderer extension point.
{ "name": "@datawind/extension-template-react-echarts", "version": "2.0.3", "main": "dist/main.umd.js", "scripts": { "dev": "datawind-extension dev", "build": "datawind-extension build" }, "dependencies": { "react": "^16.0.0", "react-dom": "^16.0.0", "echarts": "~5.3.2" }, "devDependencies": { "@types/react": "~18.0.9", "@types/react-dom": "^16.0.0", "typescript": "^4.3.2" }, "contributes": { "vizQuery.chart.renderer": [ { "id": "extension-demo", "name": "extension-react", "content": "dist/index.html", "icon": "" } ] } }
Note that the file paths configured in this description file (such as image icons and plugin entry files) are not source code paths, but paths to the compiled output, such as files under assets/** and dist/**.
extension.config.js allows users to configure behaviors related to @datawind/extension-cli.
entry - Configures the plugin entry file. In the sample code, this configuration compiles src/main.ts as the entry file into dist/main.umd.js.htmlEntry - Configures the HTML entry file. Currently, this is only used in custom chart scenarios. In the sample code, this configuration compiles src/index.html as the entry file into dist/index.html.devPort - Configures the plugin debug port. The default is 5000.The configuration example is as follows:
module.exports = { entry: { main: 'src/main.ts', }, htmlEntry: { index: 'src/index.html', } }
After initializing the project, you can see the following contents in the project folder:
assets - Stores resource filessrc/main.ts - Plugin entry file. You need to register the chart plugin in the activate function within this file.src/index.html - Chart HTML filesrc/chart.ts - Chart implementation codepackage.json - Plugin information. You need to configure chart information in the contributes section under the vizQuery.chart.renderer extension point.extension.config.js - Plugin build configurationNext, you can further understand the file structure of this project.
First, for chart plugins, call the activate method in the plugin entry file to register a custom chart renderer using context.vizQueryChartRenderer.register.
import { FieldMap } from './types' export const activate = (context) => { context.vizQueryChartRenderer.register({ id: "extension-demo", fields: [ { label: "维度", location: FieldMap.Dimension, fieldType: 0, }, { label: "指标", location: FieldMap.Measure, fieldType: 1, } ], constraints: [ { [FieldMap.Dimension]: [1, 1], [FieldMap.Measure]: [1, 1], } ] }) } export const deactivate = () => { }
Second, the object passed into the register method contains several fields:
id - Custom chart ID, which must match the corresponding description in package.jsonfields - Custom fields. Pie Chart requires two fields, Dimension and Measure. Use fieldType to specify whether the field needs to be aggregated.constraints - Custom description of the field count limitation for drawing charts. Pie Chart supports one dimension and one metric.type Constraints = Record<string, number[]>[]
Each constraint is in an OR relationship with the others, and multiple conditions within a constraint are in an AND relationship.
Finally, for more examples, please refer to the following code:
context.vizQueryChartRenderer.register({ // ... constraints: [ { // This configuration indicates: At least one field must be placed in the dimension field area [FieldMap.Dimension]: [1, Infinity], // This configuration indicates: There is no restriction on the metric field area [FieldMap.Measure]: [0, Infinity], }, { // This configuration indicates that only one field can be placed in the dimension field area [FieldMap.Dimension]: [1], // This configuration indicates that 2 to 10 fields can be placed in the metric field area [FieldMap.Measure]: [2, 10], }, ], });
settings - Custom configuration; Pie Chart currently does not support custom configuration.You can also read "Custom chart data structure" to learn more about the details of related fields.
Chart HTML, where the referenced resource file ./chart.ts can implement the chart plugin. All <script type="module" src='*' /> imported in this file, if the configured src is a local relative path, will undergo dependency analysis and be packaged together with HTML resources during the compilation phase. As follows:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>react echarts</title> </head> <body> <div id="root"></div> <script type="module" src="/src/index.tsx"></script> </body> </html>
Chart implementation code:
window.onmessage receives an event where e.data contains type as propertiesChange, it retrieves chart data from data.vizData and calls the setVizData method.transformData method to convert vizData into the data structure required by echarts.import React, { useEffect, useState, useRef } from 'react' import * as echarts from 'echarts' import { FieldMap } from './types' interface RenderData { name: string value: number } const useChart = (chartRef: React.RefObject<HTMLElement>, options: any) => { let chartInstance: echarts.ECharts const renderChart = () => { if (chartRef?.current) { chartInstance = echarts.init(chartRef?.current) chartInstance.setOption(options) } } useEffect(() => { renderChart() }, [options]) useEffect(() => { return () => { chartInstance && chartInstance.dispose() } }, []) return } const App: React.FC = () => { const chartRef = useRef<HTMLElement>(null) const [vizData, setVizData] = useState<any>(null) const [renderData, setRenderData] = useState<RenderData[]>() useEffect(() => { window.onmessage = ( e: MessageEvent<{ type: string data: { vizData: any; language: 'zh_CN' | 'en_US'; } }> ) => { const { type, data } = e.data if (type === 'propertiesChange') { setVizData(data.vizData) } } }, []) useEffect(() => { if (vizData) { const data = transformData(vizData) setRenderData(data) } }, [vizData]) // Convert vizData to the echarts data structure const transformData = (vizData) => { const { locationMap, datasets } = vizData const nameField = locationMap[FieldMap.Dimension]?.[0] const valueField = locationMap[FieldMap.Measure]?.[0] const data = datasets.map((item) => ({ name: item[nameField], value: parseFloat(item[valueField]), })) return data } const options = { title: { text: 'react echarts demo', left: 'center' }, tooltip: { trigger: 'item' }, legend: { left: "center", top: "bottom", orient: "horizontal", }, series: [ { name: 'Access From', type: 'pie', radius: '50%', data: renderData, } ] } useChart(chartRef, options) return ( <> <div style={{ width: "1280px", height: "480px" }} ref={chartRef} /> </> ) } export default App
VizData is the data structure used by this product to describe charts; for related information, refer to Custom Chart Data Structure and Plugin Property Configuration.
Since the chart implementation code runs in an iframe sandbox, there are no API restrictions on its execution environment. Therefore, you are free to use browser APIs to implement custom charts.
For custom charts, you need to describe the chart extension point in the package.json under contributes.
The custom chart extension point for visual query is vizQuery.chart.renderer:
id - Chart id, consistent with the id registered in the plugin entry file.name - Chart name, appears in the chart information for visual query.icon - Chart icon file address, used as the chart icon in visual query.content - Chart HTML file address.{ "name": "@datawind/extension-template-react-echarts", "version": "2.0.3", "main": "dist/main.umd.js", "scripts": { "dev": "datawind-extension dev", "build": "datawind-extension build" }, "dependencies": { "react": "^16.0.0", "react-dom": "^16.0.0", "echarts": "~5.3.2" }, "devDependencies": { "@types/react": "~18.0.9", "@types/react-dom": "^16.0.0", "typescript": "^4.3.2" }, "contributes": { "vizQuery.chart.renderer": [ { "id": "extension-demo", "name": "extension-react", "content": "dist/index.html", "icon": "" } ] } }
In the sample code, entry indicates that src/main.ts needs to be compiled to dist/main.umd.js as the plugin entry file.
In addition to the default plugin entry compilation configuration, custom charts also require providing chart HTML compilation configuration. In the sample code, htmlEntry indicates that src/index.html needs to be compiled to dist/index.html as the entry file.
module.exports = { entry: { main: 'src/main.ts', }, htmlEntry: { index: 'src/index.html', }, react: true, }