How to Build Plugin‑Based Custom Charts with Web Components in a React Data Platform
This article explains why a data‑analysis platform needs a plugin architecture for custom charts, compares iframe‑based and Web Components approaches, shows how to implement a lightweight ECharts‑based chart plugin, and provides step‑by‑step code samples and technical selection criteria for frontend developers.
Background
As more customers adopt the data‑analysis platform across diverse industries, a single standardized product can no longer satisfy varied business scenarios. The platform must support multiple deployment environments (private, cloud, SaaS) and allow features to be picked for specific versions without forcing users to upgrade.
To meet these needs, a plugin solution is required where custom logic can be updated in real time without depending on the main project’s release cycle. The custom‑chart feature is presented as such a plugin.
Chart Plugin Solution
Users edit custom charts by writing HTML, CSS, and JavaScript, which are rendered in an
iframe. The code can be packaged as a JSON file and installed as a plugin, enabling users to create charts quickly by selecting view data.
Communication between the parent page and the
iframeallows data transfer, but this approach has drawbacks: high developer skill requirement, limited extensibility (e.g., data formatting), and performance overhead due to iframe isolation. Therefore, a lightweight version called Custom Chart Lite was introduced.
Chart Plugin Upgrade
Custom Chart Lite is built on ECharts and only requires developers to provide the
optionobject, dramatically reducing effort compared to writing full HTML/CSS/JS. The Lite version uses a built‑in
BaseChartcomponent instead of an
iframe, simplifying data interaction and enabling additional capabilities.
Technical Selection for Plugin Architecture
Environment isolation : Plugins must not pollute the host page’s styles or variables.
Technology maturity : Broad browser support and active community are essential.
Adaptability : The solution should work across platforms and frameworks.
Communication simplicity : Complex messaging increases implementation difficulty.
DOM structure sharing : Useful for scenarios like centered modal dialogs.
Dynamic loading and updating : Plugins should be loadable and updatable at runtime.
While
iframeprovides isolation, it suffers from performance and integration issues. Articles such as “Why Not Iframe” and the micro‑frontend framework Qiankun discuss these drawbacks.
Web Components have been adopted by many companies and open‑source projects (e.g., YouTube, GitHub). They offer a promising alternative.
What Are Web Components?
Web Components are a set of standards that enable the creation of reusable custom elements. Introduced in 2011 and standardized in 2014, they consist of three core technologies:
custom elements,
shadow DOM, and
HTML templates.
<code>class MyElement extends HTMLElement {
constructor() {
super();
// create a shadow root
const shadowRoot = this.attachShadow({ mode: 'open' });
const container = document.createElement('div');
container.setAttribute('id', 'container');
container.innerText = "hello, my custom element";
shadowRoot.appendChild(container);
}
}
customElements.define('my-element', MyElement);
</code>The custom element can be used in HTML as follows:
<code><html>
<head>
<script src="./my-element.js"></script>
</head>
<body>
<my-element></my-element>
</body>
</html>
</code>CSS selectors such as
:hostand
:host-context()allow styling of the shadow root. Example:
<code>:host-context(h1) {
background-color: red;
}
</code>Validation of a Web Components‑Based Plugin
A React project created with
create‑react‑appis used for testing. The
my-element.jsfile implements a custom element that renders an icon; clicking the icon opens a modal displaying attributes
xand
y. The element is added to
public/index.htmland used in
App.js.
The full implementation of
my-element.jsis shown below:
<code>class MyElement extends HTMLElement {
constructor() {
super();
this.init();
this.open = false;
this.triggerOpen = this.triggerOpen.bind(this);
this.triggerClose = this.triggerClose.bind(this);
}
init() {
const shadowRoot = this.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
#container { height: 100% }
.icon-wrapper { display: flex; align-items: center; justify-content: center; height: 40px; width: 40px; border-radius: 100%; overflow: hidden; background-color: #fff; box-shadow: 0 2px 4px rgb(206,224,245); cursor: pointer; }
.icon-wrapper:hover { box-shadow: 0 4px 6px rgba(57,85,163,0.8); }
.icon-wrapper svg { width: 20px; height: 20px; }
.modal-wrapper { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.3); visibility: hidden; transform: scale(0); transition: opacity 0.25s 0s, transform 0.25s; }
.modal-wrapper.show { visibility: visible; transform: scale(1.0); }
.modal-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: 300px; background-color: white; border-radius: 2px; padding: 12px; max-height: 300px; }
`;
const container = document.createElement('div');
container.setAttribute('id', 'container');
const iconWrapper = document.createElement('div');
iconWrapper.setAttribute('class', 'icon-wrapper');
iconWrapper.innerHTML = `
<svg viewBox="0 0 1024 1024" width="200" height="200"><path d="..."/></svg>
`;
const modalWrapper = document.createElement('div');
modalWrapper.setAttribute('class', 'modal-wrapper');
const content = document.createElement('div');
content.setAttribute('class', 'modal-content');
modalWrapper.appendChild(content);
container.appendChild(iconWrapper);
container.appendChild(modalWrapper);
shadowRoot.appendChild(style);
shadowRoot.appendChild(container);
}
connectedCallback() {
const iconWrapper = this.shadowRoot.querySelector('#container .icon-wrapper');
iconWrapper.addEventListener('click', this.triggerOpen);
const maskWrapper = this.shadowRoot.querySelector('#container .modal-wrapper');
maskWrapper.addEventListener('click', this.triggerClose);
}
disconnectedCallback() {
const wrapper = this.shadowRoot.querySelector('#container .icon-wrapper');
wrapper && wrapper.removeEventListener('click', this.triggerOpen);
const maskWrapper = this.shadowRoot.querySelector('#container .modal-wrapper');
maskWrapper && maskWrapper.removeEventListener('click', this.triggerClose);
}
triggerOpen() {
const modalWrapper = this.shadowRoot.querySelector('#container .modal-wrapper');
if (modalWrapper) {
const maskContent = modalWrapper.querySelector('.modal-content');
maskContent.innerHTML = `
<p>x: ${this.getAttribute('x')}</p>
<p>y: ${this.getAttribute('y')}</p>
`;
modalWrapper.classList.add('show');
}
}
triggerClose() {
const modalWrapper = this.shadowRoot.querySelector('#container .modal-wrapper');
modalWrapper.classList.remove('show');
}
}
customElements.define('my-element', MyElement);
</code>Because writing native Web Components can be verbose, libraries such as Lit allow developers to author components with a React‑like syntax, improving productivity. Other component libraries like Wired Elements and Lithops UI are also available.
Summary
Shadow DOM provides style isolation while allowing shared DOM structures.
Data can be passed via attributes (strings) or properties (complex types).
Loading a JavaScript file for a custom element enables dynamic updates and cache negotiation.
The team will continue exploring Web Components to enable more plugin scenarios beyond charting.
GuanYuan Data Tech Team
Practical insights from the GuanYuan Data Tech Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.