# ECharts Integration for SAP SAC Custom Widgets
Guide for integrating Apache ECharts library with SAP Analytics Cloud custom widgets.
**Source**: [SAP Hands-on Guide](https://community.sap.com/t5/technology-blog-posts-by-sap/sap-analytics-cloud-custom-widget-hands-on-guide/ba-p/13573631)
---
## Table of Contents
1. [Overview](#overview)
2. [Basic ECharts Widget](#basic-echarts-widget)
3. [Data-Bound ECharts Widget](#data-bound-echarts-widget)
4. [Common Chart Types](#common-chart-types)
5. [Styling Panel for ECharts](#styling-panel-for-echarts)
6. [Performance Considerations](#performance-considerations)
---
## Overview
Apache ECharts is a powerful charting library that can be integrated into SAC custom widgets to create advanced visualizations not available in standard SAC charts.
**ECharts CDN**: `[https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js`](https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js`)
**Key Benefits**:
- 20+ chart types (sankey, treemap, sunburst, radar, etc.)
- Rich animation and interaction
- Excellent performance with large datasets
- Extensive customization options
---
## Basic ECharts Widget
### echarts-widget.json
```json
{
"id": "com.company.echartswidget",
"version": "1.0.0",
"name": "ECharts Widget",
"description": "Custom chart using Apache ECharts",
"vendor": "Company Name",
"license": "MIT",
"icon": "",
"webcomponents": [
{
"kind": "main",
"tag": "echarts-widget",
"url": "/echarts-widget.js",
"integrity": "",
"ignoreIntegrity": true
}
],
"properties": {
"title": {
"type": "string",
"default": "ECharts Demo"
},
"chartType": {
"type": "string",
"default": "bar"
},
"colorScheme": {
"type": "string",
"default": "#5470c6,#91cc75,#fac858,#ee6666,#73c0de"
}
},
"methods": {
"refresh": {
"description": "Refresh the chart",
"body": "this._refresh();"
}
},
"events": {
"onChartClick": {
"description": "Fired when chart element is clicked"
}
}
}
```
### echarts-widget.js
```javascript
(function() {
// Load ECharts library
const ECHARTS_CDN = "[https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";](https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";)
const template = document.createElement("template");
template.innerHTML = `
`;
class EchartsWidget extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: "open" });
this._shadowRoot.appendChild(template.content.cloneNode(true));
this._props = {
title: "ECharts Demo",
chartType: "bar",
colorScheme: "#5470c6,#91cc75,#fac858,#ee6666,#73c0de"
};
this._chart = null;
this._echartsLoaded = false;
}
connectedCallback() {
this._loadEcharts();
}
_loadEcharts() {
if (window.echarts) {
this._echartsLoaded = true;
this._initChart();
return;
}
const script = document.createElement("script");
script.src = ECHARTS_CDN;
script.onload = () => {
this._echartsLoaded = true;
this._initChart();
};
script.onerror = () => {
this._shadowRoot.getElementById("chart").innerHTML =
'Failed to load ECharts library
';
};
document.head.appendChild(script);
}
_initChart() {
const container = this._shadowRoot.getElementById("chart");
container.innerHTML = "";
this._chart = echarts.init(container);
// Handle click events
this._chart.on("click", (params) => {
this.dispatchEvent(new CustomEvent("onChartClick", {
detail: {
name: params.name,
value: params.value,
seriesName: params.seriesName
}
}));
});
this._render();
}
onCustomWidgetBeforeUpdate(changedProperties) {
this._props = { ...this._props, ...changedProperties };
}
onCustomWidgetAfterUpdate(changedProperties) {
this._render();
}
onCustomWidgetResize() {
if (this._chart) {
this._chart.resize();
}
}
onCustomWidgetDestroy() {
if (this._chart) {
this._chart.dispose();
this._chart = null;
}
}
_render() {
if (!this._chart || !this._echartsLoaded) return;
const colors = this._props.colorScheme.split(",").map(c => c.trim());
// Demo data - replace with data binding data
const option = this._getChartOption(colors);
this._chart.setOption(option, true);
}
_getChartOption(colors) {
const chartType = this._props.chartType;
const baseOption = {
title: {
text: this._props.title,
left: "center",
textStyle: {
fontFamily: '"72", Arial, sans-serif',
fontSize: 16,
fontWeight: 600
}
},
color: colors,
tooltip: {
trigger: chartType === "pie" ? "item" : "axis"
},
grid: {
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true
}
};
// Demo data
const categories = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];
const values = [150, 230, 224, 218, 135, 147];
switch (chartType) {
case "bar":
return {
...baseOption,
xAxis: { type: "category", data: categories },
yAxis: { type: "value" },
series: [{ type: "bar", data: values }]
};
case "line":
return {
...baseOption,
xAxis: { type: "category", data: categories },
yAxis: { type: "value" },
series: [{ type: "line", data: values, smooth: true }]
};
case "pie":
return {
...baseOption,
series: [{
type: "pie",
radius: "60%",
data: categories.map((name, i) => ({ name, value: values[i] }))
}]
};
default:
return {
...baseOption,
xAxis: { type: "category", data: categories },
yAxis: { type: "value" },
series: [{ type: "bar", data: values }]
};
}
}
_refresh() {
this._render();
}
// Property getters/setters
get title() { return this._props.title; }
set title(v) { this._props.title = v; }
get chartType() { return this._props.chartType; }
set chartType(v) { this._props.chartType = v; }
get colorScheme() { return this._props.colorScheme; }
set colorScheme(v) { this._props.colorScheme = v; }
}
customElements.define("echarts-widget", EchartsWidget);
})();
```
---
## Data-Bound ECharts Widget
Integrate with SAC data models via data binding.
### echarts-databound.json
```json
{
"id": "com.company.echartsdatabound",
"version": "1.0.0",
"name": "ECharts Data-Bound",
"description": "ECharts with SAC data binding",
"vendor": "Company Name",
"license": "MIT",
"icon": "",
"webcomponents": [
{
"kind": "main",
"tag": "echarts-databound",
"url": "/echarts-databound.js",
"integrity": "",
"ignoreIntegrity": true
}
],
"properties": {
"title": { "type": "string", "default": "Data Chart" },
"chartType": { "type": "string", "default": "bar" },
"showLegend": { "type": "boolean", "default": true }
},
"methods": {},
"events": {
"onDataPointClick": {
"description": "Fired when data point is clicked"
}
},
"dataBindings": {
"chartData": {
"feeds": [
{ "id": "categories", "description": "Categories", "type": "dimension" },
{ "id": "values", "description": "Values", "type": "mainStructureMember" }
]
}
}
}
```
### echarts-databound.js
```javascript
(function() {
const ECHARTS_CDN = "[https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";](https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";)
const template = document.createElement("template");
template.innerHTML = `
📊
Add data binding to display chart
`;
class EchartsDatabound extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: "open" });
this._shadowRoot.appendChild(template.content.cloneNode(true));
this._props = {
title: "Data Chart",
chartType: "bar",
showLegend: true
};
this._chart = null;
this._echartsLoaded = false;
}
connectedCallback() {
this._loadEcharts();
}
_loadEcharts() {
if (window.echarts) {
this._echartsLoaded = true;
this._initChart();
return;
}
const script = document.createElement("script");
script.src = ECHARTS_CDN;
script.onload = () => {
this._echartsLoaded = true;
this._initChart();
};
document.head.appendChild(script);
}
_initChart() {
const container = this._shadowRoot.getElementById("chart");
// Check for data
if (!this._hasData()) {
return;
}
container.innerHTML = "";
this._chart = echarts.init(container);
this._chart.on("click", (params) => {
this.dispatchEvent(new CustomEvent("onDataPointClick", {
detail: {
category: params.name,
value: params.value,
dataIndex: params.dataIndex
}
}));
});
this._render();
}
_hasData() {
return this.chartData &&
this.chartData.data &&
this.chartData.data.length > 0;
}
onCustomWidgetBeforeUpdate(changedProperties) {
this._props = { ...this._props, ...changedProperties };
}
onCustomWidgetAfterUpdate(changedProperties) {
// Re-init if we now have data
if (!this._chart && this._hasData() && this._echartsLoaded) {
this._initChart();
}
this._render();
}
onCustomWidgetResize() {
if (this._chart) {
this._chart.resize();
}
}
onCustomWidgetDestroy() {
if (this._chart) {
this._chart.dispose();
this._chart = null;
}
}
_render() {
if (!this._chart || !this._echartsLoaded) return;
if (!this._hasData()) {
this._chart.clear();
return;
}
const { categories, values } = this._parseDataBinding();
const option = this._buildChartOption(categories, values);
this._chart.setOption(option, true);
}
_parseDataBinding() {
const data = this.chartData.data;
const categories = [];
const values = [];
data.forEach(row => {
// Get category (first dimension)
if (row.categories_0) {
categories.push(row.categories_0.label || row.categories_0.id);
}
// Get value (first measure)
if (row.values_0) {
values.push(row.values_0.raw || 0);
}
});
return { categories, values };
}
_buildChartOption(categories, values) {
const chartType = this._props.chartType;
const option = {
title: {
text: this._props.title,
left: "center",
textStyle: {
fontFamily: '"72", Arial, sans-serif',
fontSize: 16
}
},
tooltip: {
trigger: chartType === "pie" ? "item" : "axis",
formatter: chartType === "pie" ? "{b}: {c} ({d}%)" : undefined
},
legend: {
show: this._props.showLegend,
bottom: 0
},
color: ["#5470c6", "#91cc75", "#fac858", "#ee6666", "#73c0de", "#3ba272"]
};
switch (chartType) {
case "bar":
return {
...option,
xAxis: {
type: "category",
data: categories,
axisLabel: { rotate: categories.length > 6 ? 45 : 0 }
},
yAxis: { type: "value" },
grid: { left: "3%", right: "4%", bottom: "15%", containLabel: true },
series: [{
type: "bar",
data: values,
itemStyle: { borderRadius: [4, 4, 0, 0] }
}]
};
case "line":
return {
...option,
xAxis: {
type: "category",
data: categories,
boundaryGap: false
},
yAxis: { type: "value" },
grid: { left: "3%", right: "4%", bottom: "15%", containLabel: true },
series: [{
type: "line",
data: values,
smooth: true,
areaStyle: { opacity: 0.3 }
}]
};
case "pie":
return {
...option,
series: [{
type: "pie",
radius: ["40%", "70%"],
center: ["50%", "55%"],
data: categories.map((name, i) => ({
name,
value: values[i]
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: "rgba(0, 0, 0, 0.5)"
}
},
label: {
formatter: "{b}: {d}%"
}
}]
};
case "horizontal-bar":
return {
...option,
xAxis: { type: "value" },
yAxis: {
type: "category",
data: categories
},
grid: { left: "3%", right: "4%", bottom: "3%", containLabel: true },
series: [{
type: "bar",
data: values,
itemStyle: { borderRadius: [0, 4, 4, 0] }
}]
};
default:
return option;
}
}
// Property getters/setters
get title() { return this._props.title; }
set title(v) { this._props.title = v; }
get chartType() { return this._props.chartType; }
set chartType(v) { this._props.chartType = v; }
get showLegend() { return this._props.showLegend; }
set showLegend(v) { this._props.showLegend = v; }
}
customElements.define("echarts-databound", EchartsDatabound);
})();
```
---
## Common Chart Types
### Sankey Diagram
```javascript
_buildSankeyOption(data) {
return {
series: [{
type: "sankey",
layout: "none",
emphasis: { focus: "adjacency" },
data: data.nodes, // [{name: "A"}, {name: "B"}]
links: data.links // [{source: "A", target: "B", value: 100}]
}]
};
}
```
### Treemap
```javascript
_buildTreemapOption(data) {
return {
series: [{
type: "treemap",
data: data, // [{name: "A", value: 100, children: [...]}]
levels: [
{ itemStyle: { borderWidth: 3 } },
{ itemStyle: { borderWidth: 1 } }
]
}]
};
}
```
### Radar Chart
```javascript
_buildRadarOption(categories, values) {
return {
radar: {
indicator: categories.map(name => ({ name, max: Math.max(...values) * 1.2 }))
},
series: [{
type: "radar",
data: [{ value: values, name: "Values" }]
}]
};
}
```
### Gauge Chart
```javascript
_buildGaugeOption(value, target) {
return {
series: [{
type: "gauge",
progress: { show: true, width: 18 },
axisLine: { lineStyle: { width: 18 } },
axisTick: { show: false },
splitLine: { length: 15, lineStyle: { width: 2 } },
axisLabel: { distance: 25 },
detail: {
valueAnimation: true,
formatter: "{value}%",
fontSize: 24
},
data: [{ value: value, name: "Progress" }]
}]
};
}
```
---
## Styling Panel for ECharts
### echarts-styling.js
```javascript
(function() {
const template = document.createElement("template");
template.innerHTML = `
`;
class EchartsStyling extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: "open" });
this._shadowRoot.appendChild(template.content.cloneNode(true));
this._props = {};
// Title
this._shadowRoot.getElementById("titleInput").addEventListener("change", (e) => {
this._fire({ title: e.target.value });
});
// Chart type
this._shadowRoot.getElementById("chartTypeSelect").addEventListener("change", (e) => {
this._fire({ chartType: e.target.value });
});
// Legend
this._shadowRoot.getElementById("legendCheckbox").addEventListener("change", (e) => {
this._fire({ showLegend: e.target.checked });
});
// Colors
for (let i = 1; i <= 5; i++) {
this._shadowRoot.getElementById(`color${i}`).addEventListener("input", () => {
this._updateColors();
});
}
}
_fire(properties) {
this.dispatchEvent(new CustomEvent("propertiesChanged", {
detail: { properties }
}));
}
_updateColors() {
const colors = [];
for (let i = 1; i <= 5; i++) {
colors.push(this._shadowRoot.getElementById(`color${i}`).value);
}
this._fire({ colorScheme: colors.join(",") });
}
onCustomWidgetBeforeUpdate(changedProperties) {
this._props = { ...this._props, ...changedProperties };
}
onCustomWidgetAfterUpdate(changedProperties) {
if (changedProperties.title !== undefined) {
this._shadowRoot.getElementById("titleInput").value = changedProperties.title;
}
if (changedProperties.chartType !== undefined) {
this._shadowRoot.getElementById("chartTypeSelect").value = changedProperties.chartType;
}
if (changedProperties.showLegend !== undefined) {
this._shadowRoot.getElementById("legendCheckbox").checked = changedProperties.showLegend;
}
if (changedProperties.colorScheme !== undefined) {
const colors = changedProperties.colorScheme.split(",");
colors.forEach((color, i) => {
const input = this._shadowRoot.getElementById(`color${i + 1}`);
if (input) input.value = color.trim();
});
}
}
}
customElements.define("echarts-styling", EchartsStyling);
})();
```
---
## Performance Considerations
### 1. Lazy Load ECharts
Only load when widget is used:
```javascript
_loadEcharts() {
return new Promise((resolve, reject) => {
if (window.echarts) {
resolve(window.echarts);
return;
}
const script = document.createElement("script");
script.src = ECHARTS_CDN;
script.onload = () => resolve(window.echarts);
script.onerror = reject;
document.head.appendChild(script);
});
}
```
### 2. Debounce Resize
```javascript
onCustomWidgetResize() {
if (this._resizeTimer) {
clearTimeout(this._resizeTimer);
}
this._resizeTimer = setTimeout(() => {
if (this._chart) {
this._chart.resize();
}
}, 100);
}
```
### 3. Use notMerge for Large Updates
```javascript
this._chart.setOption(option, { notMerge: true });
```
### 4. Limit Data Points
```javascript
_parseDataBinding() {
const data = this.chartData.data;
const MAX_POINTS = 100;
// Limit data for performance
const limitedData = data.slice(0, MAX_POINTS);
// ... parse data
}
```
### 5. Dispose on Destroy
```javascript
onCustomWidgetDestroy() {
if (this._chart) {
this._chart.dispose();
this._chart = null;
}
}
```
---
## ECharts Resources
- **ECharts Documentation**: [https://echarts.apache.org/en/index.html](https://echarts.apache.org/en/index.html)
- **ECharts Examples**: [https://echarts.apache.org/examples/en/index.html](https://echarts.apache.org/examples/en/index.html)
- **ECharts Option Reference**: [https://echarts.apache.org/en/option.html](https://echarts.apache.org/en/option.html)
- **ECharts CDN**: [https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js](https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js)
---
**Last Updated**: 2025-11-22