Building a Dynamic Salesforce LWC Chart Using Apex and Chart.js
Introduction
If you’ve ever tried creating something a bit more interactive than a standard Salesforce dashboard, you already know the pain: reports are powerful, but they’re not always flexible. Sometimes you want a chart that reacts instantly, lives inside an LWC, or updates without refreshing the entire page.
That’s where the salesforce lwc chart approach comes in.
Using Apex + LWC + Chart.js, you can build a lightweight, dynamic chart that hooks directly into your CRM data and feels like a modern front-end experience.
We’ll walk through building a reusable component that renders Opportunity pipeline analytics—but you can swap the query for any object.
What This Component Delivers
This reusable chart does the heavy lifting for you:
- Pulls grouped Opportunity data from an Apex method
- Displays it in a bar, line, or doughnut chart
- Lets users switch chart types on the fly
- Loads smoothly without page reloads
- Works for any object with just a small Apex tweak
Step 1 — Build the Apex Controller
This Apex class aggregates Opportunity data and prepares it for the chart.
public with sharing class OpportunityReport {
@AuraEnabled(cacheable=true)
public static List<DataSet> getOpportunityRecord() {
List<AggregateResult> result = [
SELECT Count(ID) cnt, SUM(Amount) totalAmount, StageName
FROM Opportunity
GROUP BY StageName
LIMIT 10
];
List<DataSet> dataSet = new List<DataSet>();
for (AggregateResult arr : result) {
String stage = (String) arr.get('StageName');
Integer total = (Integer) arr.get('cnt');
Decimal amount = (Decimal) arr.get('totalAmount');
dataSet.add(new DataSet(stage, total, amount));
}
return dataSet;
}
public class DataSet {
public DataSet(String label, Integer count, Decimal amount) {
this.label = label;
this.Count = count;
this.Amount = amount;
}
@AuraEnabled
public String label { get; set; }
@AuraEnabled
public Integer Count { get; set; }
@AuraEnabled
public Decimal Amount { get; set; }
}
}Step 2 — Add Chart.js as a Static Resource
- Download Chart.js from the official website https://www.chartjs.org/
- Go to Setup → Static Resources → New
- Upload the file with naming conventions like: ChartJS
- Set Cache Control → Public
That’s all you need to make Chart.js globally available for your LWC.
Step 3 — Create the Parent Component (opportunityReport)
This LWC pulls data from Apex and passes it to the chart component.
<template>
<lightning-card title="Opportunity Report">
<div class="slds-m-around_medium">
<lightning-combobox
name="chartType"
label="Chart Type"
value={chartType}
placeholder="Select Chart Type"
options={chartTypeOptions}
onchange={handleChartTypeChange}>
</lightning-combobox>
<div class="slds-m-top_medium" style="height: 400px;">
<c-opportunity-chart
chart-type={chartType}
chart-data={chartData}>
</c-opportunity-chart>
</div>
</div>
</lightning-card>
</template>opportunityReport.js
import { LightningElement, wire, track } from 'lwc';
import getOpportunityRecord from '@salesforce/apex/OpportunityReport.getOpportunityRecord';
export default class OpportunityReport extends LightningElement {
@track opps = [];
@track chartData = {};
@track chartType = 'bar';
// Define options as a property
chartTypeOptions = [
{ label: 'Bar', value: 'bar' },
{ label: 'Doughnut', value: 'doughnut' },
{ label: 'Scatter', value: 'scatter' },
{ label: 'Line', value: 'line' },
{ label: 'Pie', value: 'pie' },
{ label: 'Radar', value: 'radar' },
{ label: 'Polar Area', value: 'polarArea' }
];
@wire(getOpportunityRecord)
wiredOppData({ error, data }) {
if (data) {
this.opps = data;
console.log('Apex Data:', data);
const labels = data.map(item => item.label);
const counts = data.map(item => item.Count);
// Important! Clone to remove reactive Proxy
this.chartData = JSON.parse(JSON.stringify({ labels, counts }));
} else if (error) {
console.error('Apex error:', error);
}
}
// Handles dropdown change
handleChartTypeChange(event) {
this.chartType = event.detail.value;
console.log('Chart type changed to:', this.chartType);
// Force child update
this.updateChildChart();
}
// Sends chart updates to child LWC
updateChildChart() {
const chartComp = this.template.querySelector('c-opportunity-chart');
if (chartComp) {
chartComp.setChartType(this.chartType);
}
}
}opportunityReport.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>65.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__RecordPage</target>
<target>
lightning__HomePage</target>
<target>lightning__Tab</target>
</targets>
</LightningComponentBundle>Step 4 — Create the Child Component (opportunityChart)
This LWC actually renders the Chart.js canvas and updates it when chartType changes.
<template>
<div style="height: 350px;">
<canvas></canvas>
</div>
</template>opportunityChart.js
import { LightningElement, api } from 'lwc';
import chartjs from '@salesforce/resourceUrl/ChartJS';
import { loadScript } from 'lightning/platformResourceLoader';
export default class OpportunityChart extends LightningElement {
@api chartType = 'bar';
@api chartData; // expected: { labels: [...], counts: [...] }
chart;
chartLoaded = false;
async renderedCallback() {
if (this.chartLoaded) return;
try {
await loadScript(this, chartjs);
this.chartLoaded = true;
console.log('ChartJS Loaded');
this.renderChart();
} catch (error) {
console.error('ChartJS failed to load', error);
}
}
@api
updateChart() {
this.renderChart();
}
renderChart() {
if (!this.chartLoaded || !this.chartData) return;
const canvas = this.template.querySelector('canvas');
if (!canvas) {
console.warn('Canvas not found.');
return;
}
// Destroy previous chart if exists
if (this.chart) {
this.chart.destroy();
}
// Clone to avoid LWC Proxy mutation
const cleanData = JSON.parse(JSON.stringify(this.chartData));
const ctx = canvas.getContext('2d');
this.chart = new Chart(ctx, {
type: this.chartType,
data: {
labels: cleanData.labels,
datasets: [
{
label: 'Opportunity Count',
data: cleanData.counts,
backgroundColor: [
'#FF6384', '#36A2EB', '#FFCE56',
'#4BC0C0', '#9966FF', '#FF9F40',
'#abf583ff', '#f959a4ff', '#116562ff'
],
borderWidth: 2
}
]
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
}
// Automatically re-render chart when parent updates properties
@api
setChartType(type) {
this.chartType = type;
this.renderChart();
}
@api
setChartData(data) {
this.chartData = data;
this.renderChart();
}
}Final Output
Refresh your Lightning page and—boom—you have a clean, responsive, interactive chart that reacts to user input without a full reload.
Conclusion
If you ever felt boxed in by native Salesforce dashboards, building your own salesforce lwc chart is a game-changer. With Apex supplying the data, LWC controlling the UI, and Chart.js handling the visuals, you get a setup that’s flexible, fast, and surprisingly lightweight.
This same foundation can grow into:
- object-based analytics dashboards
- real-time visual updates
- drill-down reporting
- forecasting charts
- embedded analytics inside flows or custom apps
Once you build one chart this way, you’ll quickly start thinking of ten more use cases.
