---
description: Chart.js visualization development in Salesforce LWC with Locker Service compatibility. Auto-invoked when building charts, dashboards, or data visualizations in LWC using Chart.js.
---

# Chart.js in Salesforce LWC (Locker Service Compatible)

You are an expert at building Chart.js visualizations inside Salesforce Lightning Web Components. Locker Service imposes restrictions that break Chart.js defaults. Always follow the patterns below.

---

## Critical Rules (NON-NEGOTIABLE)

1. **Use Chart.js 3.9.1** — Chart.js 4.x uses ResizeObserver which Locker Service blocks. Upload 3.9.1 UMD build as a Static Resource.
2. **`responsive: false`** — ALWAYS set this in chart options. Prevents ResizeObserver crash.
3. **`new window.Chart(ctx, ...)`** — Use `window.Chart` explicitly, not bare `Chart`.
4. **`canvas.getContext('2d')`** — Pass the 2d rendering context to the Chart constructor, not the canvas element.
5. **Explicit canvas dimensions** — Set `canvas.width` and `canvas.height` from the container's `clientWidth`/`clientHeight`.
6. **`lwc:dom="manual"`** — Place on the container div. Create canvas via `document.createElement('canvas')`.

---

## Static Resource Setup

Download Chart.js 3.9.1 UMD build and upload as a Static Resource:

```bash
curl -sL "https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js" -o chartjs3.js
```

Upload as Static Resource named `chartjs3` (or `chartjs`) with `CacheControl: Public`.

---

## Working Component Pattern

### HTML Template

```html
<template>
    <lightning-card title="My Chart">
        <template if:true={isLoading}>
            <lightning-spinner alternative-text="Loading"></lightning-spinner>
        </template>
        <div class="chart-container" lwc:dom="manual" data-chart="myChart"></div>
    </lightning-card>
</template>
```

### CSS

```css
.chart-container {
    height: 300px;
    padding: 0.5rem;
    position: relative;
}
```

### JavaScript

```javascript
import { LightningElement, wire } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';
import chartJs from '@salesforce/resourceUrl/chartjs3';
import getMyData from '@salesforce/apex/MyController.getMyData';

export default class MyChart extends LightningElement {
    chart;
    chartJsLoaded = false;
    isLoading = true;
    rawData;

    @wire(getMyData)
    wiredData({ data, error }) {
        if (data) {
            this.rawData = data;
            this.isLoading = false;
            if (this.chartJsLoaded) {
                this.renderChart();
            }
        } else if (error) {
            this.isLoading = false;
        }
    }

    renderedCallback() {
        if (this.chartJsLoaded) return;
        loadScript(this, chartJs)
            .then(() => {
                this.chartJsLoaded = true;
                if (this.rawData) {
                    this.renderChart();
                }
            })
            .catch(err => {
                console.error('Chart.js load error', err);
            });
    }

    getCanvas(chartType) {
        const container = this.template.querySelector(`[data-chart="${chartType}"]`);
        if (!container) return null;
        let canvas = container.querySelector('canvas');
        if (!canvas) {
            canvas = document.createElement('canvas');
            container.appendChild(canvas);
        }
        canvas.width = container.clientWidth || 400;
        canvas.height = container.clientHeight || 280;
        return canvas;
    }

    renderChart() {
        if (!this.rawData || !this.chartJsLoaded) return;

        const canvas = this.getCanvas('myChart');
        if (!canvas) return;

        if (this.chart) {
            this.chart.destroy();
        }

        const ctx = canvas.getContext('2d');
        this.chart = new window.Chart(ctx, {
            type: 'bar',
            data: {
                labels: this.rawData.map(r => r.label),
                datasets: [{
                    label: 'My Dataset',
                    data: this.rawData.map(r => r.value),
                    backgroundColor: 'rgba(1, 118, 211, 0.8)'
                }]
            },
            options: {
                responsive: false,  // CRITICAL for Locker Service
                plugins: {
                    legend: { position: 'bottom' }
                },
                scales: {
                    y: {
                        beginAtZero: true,
                        ticks: {
                            callback: v => '$' + (v / 1000000).toFixed(1) + 'M'
                        }
                    }
                }
            }
        });
    }
}
```

### Metadata

```xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>62.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>
```

---

## Chart Type Examples

### Horizontal Bar

```javascript
options: {
    responsive: false,
    indexAxis: 'y',
    plugins: { legend: { display: false } },
    scales: {
        x: { beginAtZero: true, ticks: { callback: v => '$' + (v/1e6).toFixed(1) + 'M' } }
    }
}
```

### Stacked Bar

```javascript
options: {
    responsive: false,
    scales: {
        x: { stacked: true },
        y: { stacked: true, beginAtZero: true }
    }
}
```

### Line Chart

```javascript
{
    type: 'line',
    data: {
        labels,
        datasets: [{
            label: 'Series 1',
            data: values,
            borderColor: 'rgba(1, 118, 211, 1)',
            backgroundColor: 'rgba(1, 118, 211, 0.1)',
            fill: true,
            tension: 0.4
        }]
    },
    options: { responsive: false }
}
```

### Doughnut

```javascript
{
    type: 'doughnut',
    data: {
        labels,
        datasets: [{
            data: values,
            backgroundColor: ['#0176d3', '#04844b', '#ff9800', '#9c27b0']
        }]
    },
    options: {
        responsive: false,
        plugins: {
            legend: { position: 'right' }
        }
    }
}
```

### Mixed Chart (Bar + Line)

```javascript
{
    type: 'bar',
    data: {
        labels,
        datasets: [
            { type: 'bar', label: 'Count', data: barData, yAxisID: 'y' },
            { type: 'line', label: 'Trend', data: lineData, yAxisID: 'y1', borderColor: '#ff9800' }
        ]
    },
    options: {
        responsive: false,
        scales: {
            y:  { beginAtZero: true, position: 'left' },
            y1: { beginAtZero: true, position: 'right', grid: { drawOnChartArea: false } }
        }
    }
}
```

---

## Multiple Charts in One Component

Use `data-chart` attributes to distinguish containers:

```html
<div class="chart-container" lwc:dom="manual" data-chart="pipeline"></div>
<div class="chart-container" lwc:dom="manual" data-chart="revenue"></div>
```

```javascript
renderAllCharts() {
    if (!this.chartJsLoaded || !window.Chart) return;
    this.renderPipelineChart();
    this.renderRevenueChart();
}

renderPipelineChart() {
    const canvas = this.getCanvas('pipeline');
    // ... create chart
}

renderRevenueChart() {
    const canvas = this.getCanvas('revenue');
    // ... create chart
}
```

---

## Refresh Pattern

```javascript
@api
get refreshKey() { return this._refreshKey; }
set refreshKey(value) {
    this._refreshKey = value;
    if (this.wiredResult) {
        refreshApex(this.wiredResult).finally(() => {
            this.isLoading = false;
        });
    }
}
```

---

## Common Pitfalls

| Pitfall | Fix |
|---------|-----|
| `ResizeObserver is not a constructor` | Set `responsive: false` |
| Chart.js 4.x crashes in Locker | Use Chart.js 3.9.1 |
| `Chart is not a constructor` | Use `window.Chart` |
| Canvas has 0 dimensions | Set explicit `canvas.width`/`canvas.height` |
| Chart doesn't render in tabs | Use `scheduleRender()` with `Promise.resolve().then(...)` |
| Cached old static resource | Upload with new name (e.g., `chartjs3`) |

---

## Alternative: Pure SVG/CSS (No Library)

For simple visualizations, consider SVG/CSS instead of Chart.js to avoid library issues entirely:

- **Bar charts**: CSS `width` percentage on styled divs
- **Donuts/Pies**: SVG `<path>` with `describeArc()` helper
- **Gauges**: SVG arc paths with computed percentages

---

*Free Claude Code skill from [Cumulus Vision](https://cumulusvision.com) — Senior Salesforce engineering for complex environments.*
