It sounds like what you're encountering is due to Chart.js plugin registration lifecycle differences across environments, especially when using frameworks like Angular and React, which render components asynchronously and sometimes with timing delays.
Why this happens:
When you register the plugin inline like :
<sk-chart [data]="data" [options]="options" type="bar" [plugins]="dataLabels"></sk-chart>
...the plugin is passed to the chart instance just-in-time. However, in Angular/React, rendering and DOM lifecycles can delay the rendering or make it non-deterministic in relation to Lit's internal lifecycle (firstUpdated, updated, etc.), especially when nested inside custom elements.
So what happens is:
- Chart is initialized with
dataLabels plugin
- Plugin is not fully applied before layouting begins (or is applied too late)
- Labels flicker or are rendered incorrectly on first render
- Works perfectly only after a re-render or full plugin registration
In contrast, when you do:
Chart.register(chartDataLabels);
You're making the plugin globally availableto all Chart.js instances before they are initialized, which guarantess correct behavior regardless of the framework timing.
Why Vanilla JS or Storybook Works:
These environments don’t introduce additional component lifecycles or asynchronous rendering — so your Lit element has full control over timing. Hence, inline plugin works predictably.
Workarounds (without global registration):
- Delay chart rendering until after the component is fully connected and stable:
In your Lit component, add something like this in firstUpdated:
requestAnimationFrame(() => {
this.renderChart(); // or recreate it manually
});
- Force chart re-creation after a short delay:
setTimeout(() => this.renderChart(), 0); // Let Angular finish DOM patching
- Use
updateComplete from Lit:
this.updateComplete.then(() => {
this.renderChart();
});
- In Angular, ensure bindings are stable before rendering the chart. You can also wrap
<sk-schart> in *ngIf that flips true after a ```setTimeout to mimic a post-render registration.
Recommendation:
If global registration is acceptable in your architecture, it's the most stable solution. Otherwise, defer chart rendering using updateComplete, requestAnimationFrame, or a setTimeout to fix flickering and misalignment when using inline plugins.
Sample Code
GOAL
- Use
chartjs-plugin-datalabels via inline registration
- Prevent label misalignment or flickering in Angular/React
- No global
Chart.register(...) inside your component
Setup Assumptions
- You have Chart.js and chartjs-plugin-datalabels installed
- You are embedding your Lit component (
sk-chart) in an Angular or React app
Lit Component : sk-chart.ts
import { LitElement, html, css, property } from 'lit';
import { customElement } from 'lit/decorators.js';
import Chart from 'chart.js/auto';
@customElement('sk-chart')
export class SkChart extends LitElement {
@property({ type: Object }) data = {};
@property({ type: Object }) options = {};
@property({ type: String }) type = 'bar';
@property({ type: Array }) plugins: any[] = [];
private chart: Chart | null = null;
static styles = css`
canvas {
width: 100%;
height: auto;
}
`;
firstUpdated() {
// Delay rendering chart until after the DOM + plugins are fully ready
this.updateComplete.then(() => {
requestAnimationFrame(() => this.renderChart());
});
}
updated(changedProps: Map<string, unknown>) {
if (
changedProps.has('data') ||
changedProps.has('options') ||
changedProps.has('type') ||
changedProps.has('plugins')
) {
this.updateComplete.then(() => {
requestAnimationFrame(() => this.renderChart());
});
}
}
renderChart() {
const canvas = this.shadowRoot?.getElementById('chart') as HTMLCanvasElement;
if (this.chart) {
this.chart.destroy();
}
this.chart = new Chart(canvas, {
type: this.type,
data: this.data,
options: this.options,
plugins: this.plugins || [],
});
}
render() {
return html`<canvas id="chart"></canvas>`;
}
}
Angular Usage Example :
app.component.ts
import { Component } from '@angular/core';
import chartDataLabels from 'chartjs-plugin-datalabels';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
data = {
labels: ['Jan', 'Feb', 'Mar'],
datasets: [
{
label: 'Sales',
data: [100, 200, 300],
backgroundColor: '#2196f3'
}
]
};
options = {
plugins: {
datalabels: {
color: '#000',
anchor: 'end',
align: 'end',
offset: 4,
font: {
weight: 'bold'
}
}
}
};
type = 'bar';
plugins = [chartDataLabels];
}
app.component.html
<sk-chart
[data]="data"
[options]="options"
[type]="type"
[plugins]="plugins">
</sk-chart>
Result
- No need for
Chart.register(chartDataLabels)
- Data labels render correctly at the start (no shift)
- Works smoothly inside Angular and React
- Component remains reusable and framework-agnostic