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 iframe allows 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 option object, dramatically reducing effort compared to writing full HTML/CSS/JS. The Lite version uses a built‑in BaseChart component 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 iframe provides 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.
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);The custom element can be used in HTML as follows:
<html>
<head>
<script src="./my-element.js"></script>
</head>
<body>
<my-element></my-element>
</body>
</html>CSS selectors such as :host and :host-context() allow styling of the shadow root. Example:
:host-context(h1) {
background-color: red;
}Validation of a Web Components‑Based Plugin
A React project created with create‑react‑app is used for testing. The my-element.js file implements a custom element that renders an icon; clicking the icon opens a modal displaying attributes x and y. The element is added to public/index.html and used in App.js.
The full implementation of my-element.js is shown below:
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);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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
