Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "sap-sac-custom-widget",
|
||||
"description": "Custom Widget development for SAC. Covers Web Components, JSON metadata, lifecycle functions, data binding, styling panels, third-party libraries, and Widget Add-Ons.",
|
||||
"version": "1.1.0",
|
||||
"author": {
|
||||
"name": "Zhongwei Li",
|
||||
"email": "zhongweili@tubi.tv"
|
||||
},
|
||||
"skills": [
|
||||
"./"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# sap-sac-custom-widget
|
||||
|
||||
Custom Widget development for SAC. Covers Web Components, JSON metadata, lifecycle functions, data binding, styling panels, third-party libraries, and Widget Add-Ons.
|
||||
361
SKILL.md
Normal file
361
SKILL.md
Normal file
@@ -0,0 +1,361 @@
|
||||
---
|
||||
name: sap-sac-custom-widget
|
||||
description: |
|
||||
SAP Analytics Cloud (SAC) Custom Widget development skill. Use when building custom visualizations, interactive components, extending SAC with Web Components, or creating Widget Add-Ons to customize built-in widgets. Covers JSON metadata configuration, JavaScript Web Components, lifecycle functions, data binding with feeds, styling panels, builder panels, property/event/method definitions, custom types, script API data types, third-party library integration, hosting options, security, performance optimization, and debugging. Includes Widget Add-On feature (QRC Q4 2023+) for extending built-in widgets without creating from scratch. Provides templates for basic widgets, data-bound charts, styling panels, and KPI cards. Supports Optimized Story Experience and Analytics Designer. Prevents common errors: missing lifecycle functions, incorrect JSON schema, integrity warnings, CORS failures, property type mismatches, data binding issues, and performance anti-patterns.
|
||||
|
||||
Keywords: sap analytics cloud, sac custom widget, custom widget development, web component sac, json metadata widget, widget lifecycle functions, onCustomWidgetBeforeUpdate, onCustomWidgetAfterUpdate, onCustomWidgetResize, onCustomWidgetDestroy, sac data binding, widget data binding, dataBindings feeds, getDataBinding, getResultSet, styling panel widget, builder panel widget, widget properties events methods, propertiesChanged event, dispatchEvent custom widget, sac echarts integration, sac d3js integration, third party library sac, widget hosting sac, sac hosted widget, integrity hash widget, sha256 integrity, widget security cors, sac widget debugging, custom visualization sac, sac analytics designer widget, optimized story experience widget, sac widget api, widget add-on, sac script api widget, custom types enumeration, MemberInfo ResultMemberInfo Selection, widget installation admin, sac performance optimization, shadow dom web component, sac tooltip customization, plot area addon
|
||||
|
||||
license: GPL-3.0
|
||||
metadata:
|
||||
version: 1.2.0
|
||||
last_verified: 2025-11-26
|
||||
sac_version: "2025.21"
|
||||
token_savings: ~75%
|
||||
errors_prevented: 25+
|
||||
official_docs:
|
||||
- [https://help.sap.com/docs/SAP_ANALYTICS_CLOUD/0ac8c6754ff84605a4372468d002f2bf/75311f67527c41638ceb89af9cd8af3e.html](https://help.sap.com/docs/SAP_ANALYTICS_CLOUD/0ac8c6754ff84605a4372468d002f2bf/75311f67527c41638ceb89af9cd8af3e.html)
|
||||
- [https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf](https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf)
|
||||
samples_repo: [https://github.com/SAP-samples/analytics-cloud-datasphere-community-content/tree/main/SAC_Custom_Widgets](https://github.com/SAP-samples/analytics-cloud-datasphere-community-content/tree/main/SAC_Custom_Widgets)
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Bash
|
||||
- WebFetch
|
||||
---
|
||||
|
||||
# SAP Analytics Cloud Custom Widget Development
|
||||
|
||||
## Overview
|
||||
|
||||
This skill enables development of custom widgets for SAP Analytics Cloud (SAC). Custom widgets are Web Components that extend SAC stories and applications with custom visualizations, interactive elements, and specialized functionality.
|
||||
|
||||
**Use this skill when**:
|
||||
- Building custom visualizations not available in standard SAC
|
||||
- Integrating third-party charting libraries (ECharts, D3.js, Chart.js)
|
||||
- Creating interactive input components for SAC applications
|
||||
- Implementing specialized data displays or KPI widgets
|
||||
- Extending Analytics Designer applications with custom functionality
|
||||
- Troubleshooting custom widget loading or data binding issues
|
||||
|
||||
**Requirements**:
|
||||
- SAC tenant with Optimized Story Experience or Analytics Designer
|
||||
- JavaScript/Web Components knowledge
|
||||
- External hosting (GitHub Pages, AWS S3, Azure) OR SAC-hosted resources (QRC Q2 2023+)
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Minimal Custom Widget Structure
|
||||
|
||||
A custom widget requires two files:
|
||||
|
||||
**1. widget.json** (Metadata)
|
||||
```json
|
||||
{
|
||||
"id": "com.company.mywidget",
|
||||
"version": "1.0.0",
|
||||
"name": "My Custom Widget",
|
||||
"description": "A simple custom widget",
|
||||
"vendor": "Company Name",
|
||||
"license": "MIT",
|
||||
"icon": "",
|
||||
"webcomponents": [
|
||||
{
|
||||
"kind": "main",
|
||||
"tag": "my-custom-widget",
|
||||
"url": "[https://your-host.com/widget.js",](https://your-host.com/widget.js",)
|
||||
"integrity": "",
|
||||
"ignoreIntegrity": true
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"default": "My Widget"
|
||||
}
|
||||
},
|
||||
"methods": {},
|
||||
"events": {}
|
||||
}
|
||||
```
|
||||
|
||||
**2. widget.js** (Web Component)
|
||||
```javascript
|
||||
(function() {
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.container {
|
||||
padding: 16px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<div class="container">
|
||||
<h3 id="title">My Widget</h3>
|
||||
<div id="content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
class MyCustomWidget extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._shadowRoot = this.attachShadow({ mode: "open" });
|
||||
this._shadowRoot.appendChild(template.content.cloneNode(true));
|
||||
this._props = {};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// Called when element is added to DOM
|
||||
}
|
||||
|
||||
onCustomWidgetBeforeUpdate(changedProperties) {
|
||||
// Called BEFORE properties are updated
|
||||
this._props = { ...this._props, ...changedProperties };
|
||||
}
|
||||
|
||||
onCustomWidgetAfterUpdate(changedProperties) {
|
||||
// Called AFTER properties are updated - render here
|
||||
if (changedProperties.title !== undefined) {
|
||||
this._shadowRoot.getElementById("title").textContent = changedProperties.title;
|
||||
}
|
||||
}
|
||||
|
||||
onCustomWidgetResize() {
|
||||
// Called when widget is resized
|
||||
}
|
||||
|
||||
onCustomWidgetDestroy() {
|
||||
// Cleanup when widget is removed
|
||||
}
|
||||
|
||||
// Property getter/setter (required for SAC framework)
|
||||
get title() {
|
||||
return this._props.title;
|
||||
}
|
||||
set title(value) {
|
||||
this._props.title = value;
|
||||
this.dispatchEvent(new CustomEvent("propertiesChanged", {
|
||||
detail: { properties: { title: value } }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("my-custom-widget", MyCustomWidget);
|
||||
})();
|
||||
```
|
||||
|
||||
**⚠️ Production Note**: The `ignoreIntegrity: true` setting above is **development only**. For production deployments, generate a SHA256 integrity hash and set `ignoreIntegrity: false`.
|
||||
|
||||
---
|
||||
|
||||
## Community Sample Widgets
|
||||
|
||||
SAP provides 15+ ready-to-use custom widget samples:
|
||||
|
||||
**Repository**: [SAP-samples/SAC_Custom_Widgets](https://github.com/SAP-samples/analytics-cloud-datasphere-community-content/tree/main/SAC_Custom_Widgets)
|
||||
|
||||
| Category | Widgets |
|
||||
|----------|---------|
|
||||
| **Charts** | Funnel, Pareto, Sankey, Sunburst, Tree, Line, UI5 Gantt |
|
||||
| **KPI/Gauge** | KPI Ring, Gauge Grade, Half Donut, Nested Pie, Custom Pie |
|
||||
| **Utilities** | File Upload, Word Cloud, Bar Gradient, Widget Add-on Sample |
|
||||
|
||||
**Requirements**: Optimized View Mode (OVM) enabled, data binding support
|
||||
|
||||
**Note**: Check third-party library licenses before production use.
|
||||
|
||||
---
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Lifecycle Functions
|
||||
Essential functions called by SAC framework:
|
||||
- `onCustomWidgetBeforeUpdate(changedProperties)` - Pre-update hook
|
||||
- `onCustomWidgetAfterUpdate(changedProperties)` - Post-update (render here)
|
||||
- `onCustomWidgetResize()` - Handle resize events
|
||||
- `onCustomWidgetDestroy()` - Cleanup resources
|
||||
|
||||
### Data Binding
|
||||
Configure in widget.json to receive SAC model data:
|
||||
```json
|
||||
{
|
||||
"dataBindings": {
|
||||
"myDataBinding": {
|
||||
"feeds": [
|
||||
{
|
||||
"id": "dimensions",
|
||||
"description": "Dimensions",
|
||||
"type": "dimension"
|
||||
},
|
||||
{
|
||||
"id": "measures",
|
||||
"description": "Measures",
|
||||
"type": "mainStructureMember"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Access data in JavaScript:
|
||||
```javascript
|
||||
// Get data binding
|
||||
const dataBinding = this.dataBindings.getDataBinding("myDataBinding");
|
||||
|
||||
// Access result set
|
||||
const data = this.myDataBinding.data;
|
||||
const metadata = this.myDataBinding.metadata;
|
||||
|
||||
// Iterate over rows
|
||||
this.myDataBinding.data.forEach(row => {
|
||||
const dimensionValue = row.dimensions_0.label;
|
||||
const measureValue = row.measures_0.raw;
|
||||
});
|
||||
```
|
||||
|
||||
### Hosting Options
|
||||
|
||||
**1. SAC-Hosted (Recommended, QRC Q2 2023+)**
|
||||
- Upload files directly to SAC > Files > Public Files
|
||||
- Use relative paths: `"/path/to/widget.js"`
|
||||
- Set `"integrity": ""` and `"ignoreIntegrity": true`
|
||||
|
||||
**2. GitHub Pages**
|
||||
- Create repository with widget files
|
||||
- Enable GitHub Pages in Settings
|
||||
- Use URL: `[https://username.github.io/repo/widget.js`](https://username.github.io/repo/widget.js`)
|
||||
|
||||
**3. External Web Server**
|
||||
- AWS S3, Azure Blob, or any HTTPS server
|
||||
- Must include CORS headers: `Access-Control-Allow-Origin: *`
|
||||
|
||||
### Security: Integrity Hash
|
||||
|
||||
For production, generate SHA256 hash:
|
||||
```bash
|
||||
# Generate hash
|
||||
openssl dgst -sha256 -binary widget.js | openssl base64 -A
|
||||
|
||||
# Update JSON
|
||||
"integrity": "sha256-abc123...",
|
||||
"ignoreIntegrity": false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Errors & Solutions
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| "The system couldn't load the custom widget" | Incorrect URL or hosting issue | Verify URL is accessible, check CORS |
|
||||
| "Integrity check failed" | Hash mismatch | Regenerate hash after JS changes |
|
||||
| Widget not appearing | Missing connectedCallback render | Call render in onCustomWidgetAfterUpdate |
|
||||
| Properties not updating | Missing propertiesChanged dispatch | Use dispatchEvent with propertiesChanged |
|
||||
| Data not displaying | Data binding misconfigured | Verify feeds in JSON match usage |
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
### Browser DevTools
|
||||
1. Open Chrome DevTools (F12)
|
||||
2. Sources tab: Find widget.js, set breakpoints
|
||||
3. Console tab: View console.log output
|
||||
4. Network tab: Check if files load (200 status)
|
||||
|
||||
### Debug Pattern
|
||||
```javascript
|
||||
onCustomWidgetAfterUpdate(changedProperties) {
|
||||
console.log("Widget updated:", changedProperties);
|
||||
console.log("Current props:", this._props);
|
||||
console.log("Data binding:", this.myDataBinding?.data);
|
||||
this._render();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Widget Add-Ons (QRC Q4 2023+)
|
||||
|
||||
Widget Add-Ons extend built-in SAC widgets without building from scratch.
|
||||
|
||||
**Use Cases**:
|
||||
- Customize chart tooltips
|
||||
- Add visual elements to plot areas
|
||||
- Override built-in styling
|
||||
|
||||
**Supported Charts**: Bar/Column, Stacked Bar/Column, Line, Stacked Area, Numeric Point
|
||||
|
||||
**Key Differences**:
|
||||
- Only `main` and `builder` components (no `styling`)
|
||||
- Must specify extension target (`tooltip`, `plotArea`, `numericPoint`)
|
||||
- SAC provides chart context data via methods
|
||||
|
||||
See **`references/widget-addon-guide.md`** for complete implementation.
|
||||
|
||||
---
|
||||
|
||||
## Bundled Resources
|
||||
|
||||
For detailed templates and examples, see:
|
||||
|
||||
1. **`references/json-schema-reference.md`** - Complete JSON schema documentation
|
||||
2. **`references/widget-templates.md`** - Ready-to-use widget templates (6 templates)
|
||||
3. **`references/echarts-integration.md`** - ECharts library integration guide
|
||||
4. **`references/widget-addon-guide.md`** - Widget Add-On development (QRC Q4 2023+)
|
||||
5. **`references/best-practices-guide.md`** - Performance, security, and development guidelines
|
||||
6. **`references/advanced-topics.md`** - Custom types, script API types, installation
|
||||
7. **`references/integration-and-migration.md`** - Script integration, content transport
|
||||
8. **`references/script-api-reference.md`** - DataSource, Selection, MemberInfo APIs
|
||||
|
||||
---
|
||||
|
||||
## Official Documentation Links
|
||||
|
||||
**Primary References** (for skill updates):
|
||||
- [Custom Widget Developer Guide](https://help.sap.com/docs/SAP_ANALYTICS_CLOUD/0ac8c6754ff84605a4372468d002f2bf/75311f67527c41638ceb89af9cd8af3e.html?version=2025.21&locale=en-US)
|
||||
- [Developer Guide PDF](https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf)
|
||||
- [Widget API PDF (2025)](https://help.sap.com/doc/7e0efa0e68dc45958e568699f8226ad7/cloud/en-US/SAC_Widget_API_en.pdf)
|
||||
|
||||
**Sample Widgets**:
|
||||
- [SAP Samples Repository](https://github.com/SAP-samples/analytics-cloud-datasphere-community-content/tree/main/SAC_Custom_Widgets)
|
||||
- [SAP Custom Widget GitHub](https://github.com/SAP-Custom-Widget)
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
**v1.2.0** (2025-11-26)
|
||||
- Updated SAC version reference to 2025.21
|
||||
- Optimized SKILL.md length from 563 to ~200 lines
|
||||
- Added Table of Contents to all 8 reference files
|
||||
- Improved progressive disclosure architecture
|
||||
|
||||
**v1.1.0** (2025-11-22)
|
||||
- Added Widget Add-On feature documentation (QRC Q4 2023+)
|
||||
- Added best practices guide (performance, security, development)
|
||||
- Added advanced topics (custom types, script API types, installation)
|
||||
- Enhanced description with additional keywords
|
||||
- Increased error prevention coverage to 25+
|
||||
|
||||
**v1.0.0** (2025-11-22)
|
||||
- Initial release
|
||||
- Complete JSON metadata reference
|
||||
- Lifecycle functions documentation
|
||||
- Data binding guide
|
||||
- Styling panel implementation
|
||||
- Hosting options (SAC-hosted, GitHub, external)
|
||||
- Security (integrity hash, CORS)
|
||||
- Common errors and debugging
|
||||
|
||||
---
|
||||
|
||||
**Last Verified**: 2025-11-26 | **SAC Version**: 2025.21 | **Skill Version**: 1.2.0
|
||||
77
plugin.lock.json
Normal file
77
plugin.lock.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:secondsky/sap-skills:skills/sap-sac-custom-widget",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "c54c50fb069fb0a9473dad80fae33c8806fe4e48",
|
||||
"treeHash": "e1e034c03fb26d421392a129f47b20f4b0aff00d33818933faa6fe247c5897a6",
|
||||
"generatedAt": "2025-11-28T10:28:14.277165Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "sap-sac-custom-widget",
|
||||
"description": "Custom Widget development for SAC. Covers Web Components, JSON metadata, lifecycle functions, data binding, styling panels, third-party libraries, and Widget Add-Ons.",
|
||||
"version": "1.1.0"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "e05125e5b1d7ba6048a627bec983c98e6e6243651c747afe203e1c7832dcbba6"
|
||||
},
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"sha256": "d526545dce743d3882dc3895944e39d4184c4caf4ea1957ab22045026c414ea2"
|
||||
},
|
||||
{
|
||||
"path": "references/advanced-topics.md",
|
||||
"sha256": "3beb5ef8ed75f80d8cee95ae3fedd2b5836bdfaf7c66ca23a91092da242f7080"
|
||||
},
|
||||
{
|
||||
"path": "references/integration-and-migration.md",
|
||||
"sha256": "e28856f5b7f3887b73622fcdd8fa750c6e7c50a0e445ed6aab7db89bf1f8004b"
|
||||
},
|
||||
{
|
||||
"path": "references/widget-addon-guide.md",
|
||||
"sha256": "cc7dabebafbdde90a806b913291cdc5944f74b3b2c91fdf262264f2d76a1a8f6"
|
||||
},
|
||||
{
|
||||
"path": "references/json-schema-reference.md",
|
||||
"sha256": "faf3665badb730817f4016a5b4e14fa1d84c12fb5943ceb0af808903b2f19182"
|
||||
},
|
||||
{
|
||||
"path": "references/widget-templates.md",
|
||||
"sha256": "96005ab5e6077201e738ffc85bf7f493b3c2c0f7b6343bf02e1d4f29c5ff33c6"
|
||||
},
|
||||
{
|
||||
"path": "references/script-api-reference.md",
|
||||
"sha256": "e4c9fab96222e9d74bac262dd55472d2407af1d599544cee4a79c5c534fac651"
|
||||
},
|
||||
{
|
||||
"path": "references/best-practices-guide.md",
|
||||
"sha256": "0f438dd9280519ac82c178fbd6e67dca5418bf78142f1e951c5f994f527c0b71"
|
||||
},
|
||||
{
|
||||
"path": "references/echarts-integration.md",
|
||||
"sha256": "99832e64b599e6e066c01d6d568a87dd8d3144acb1a2dd4bbde4a9045ee74a94"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "da927b6bbc0faf3f78bcebedefed2602222703fd3e9c94d5f8b27dfa904a08a4"
|
||||
}
|
||||
],
|
||||
"dirSha256": "e1e034c03fb26d421392a129f47b20f4b0aff00d33818933faa6fe247c5897a6"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
658
references/advanced-topics.md
Normal file
658
references/advanced-topics.md
Normal file
@@ -0,0 +1,658 @@
|
||||
# SAP SAC Custom Widget Advanced Topics
|
||||
|
||||
Advanced features including custom types, script data types, and administration.
|
||||
|
||||
**Source**: [SAP Custom Widget Developer Guide](https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf)
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Custom Types](#custom-types)
|
||||
2. [Script API Data Types](#script-api-data-types)
|
||||
3. [Widget Installation](#widget-installation)
|
||||
4. [Third-Party Library Integration](#third-party-library-integration)
|
||||
5. [Advanced Data Binding](#advanced-data-binding)
|
||||
6. [Multi-Language Support](#multi-language-support)
|
||||
|
||||
---
|
||||
|
||||
## Custom Types
|
||||
|
||||
Custom types enable complex data structures in widget properties and script interactions.
|
||||
|
||||
### Custom Data Structures
|
||||
|
||||
Define reusable object types in JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "com.company.advancedwidget",
|
||||
"version": "1.0.0",
|
||||
"name": "Advanced Widget",
|
||||
"types": {
|
||||
"ChartConfig": {
|
||||
"description": "Chart configuration object",
|
||||
"properties": {
|
||||
"chartType": {
|
||||
"type": "string",
|
||||
"default": "bar",
|
||||
"description": "Type of chart"
|
||||
},
|
||||
"showLegend": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Show chart legend"
|
||||
},
|
||||
"colors": {
|
||||
"type": "string[]",
|
||||
"default": [],
|
||||
"description": "Color palette"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DataPoint": {
|
||||
"description": "Single data point",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Data point label"
|
||||
},
|
||||
"value": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Data point value"
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"default": "#336699",
|
||||
"description": "Data point color"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"config": {
|
||||
"type": "ChartConfig",
|
||||
"default": {
|
||||
"chartType": "bar",
|
||||
"showLegend": true,
|
||||
"colors": ["#5470c6", "#91cc75", "#fac858"]
|
||||
},
|
||||
"description": "Chart configuration"
|
||||
},
|
||||
"dataPoints": {
|
||||
"type": "DataPoint[]",
|
||||
"default": [],
|
||||
"description": "Array of data points"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Enumerations
|
||||
|
||||
Define allowed values:
|
||||
|
||||
```json
|
||||
{
|
||||
"types": {
|
||||
"ChartTypeEnum": {
|
||||
"description": "Allowed chart types",
|
||||
"values": [
|
||||
{
|
||||
"id": "bar",
|
||||
"description": "Bar Chart"
|
||||
},
|
||||
{
|
||||
"id": "line",
|
||||
"description": "Line Chart"
|
||||
},
|
||||
{
|
||||
"id": "pie",
|
||||
"description": "Pie Chart"
|
||||
},
|
||||
{
|
||||
"id": "area",
|
||||
"description": "Area Chart"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AlignmentEnum": {
|
||||
"description": "Text alignment options",
|
||||
"values": [
|
||||
{ "id": "left", "description": "Left aligned" },
|
||||
{ "id": "center", "description": "Center aligned" },
|
||||
{ "id": "right", "description": "Right aligned" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"chartType": {
|
||||
"type": "ChartTypeEnum",
|
||||
"default": "bar",
|
||||
"description": "Type of chart to display"
|
||||
},
|
||||
"titleAlignment": {
|
||||
"type": "AlignmentEnum",
|
||||
"default": "center",
|
||||
"description": "Title text alignment"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Custom Types in Web Component
|
||||
|
||||
```javascript
|
||||
class AdvancedWidget extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._props = {
|
||||
config: {
|
||||
chartType: "bar",
|
||||
showLegend: true,
|
||||
colors: ["#5470c6", "#91cc75", "#fac858"]
|
||||
},
|
||||
dataPoints: []
|
||||
};
|
||||
}
|
||||
|
||||
// Getter returns the full object
|
||||
get config() {
|
||||
return this._props.config;
|
||||
}
|
||||
|
||||
// Setter accepts object and validates
|
||||
set config(value) {
|
||||
if (typeof value !== "object") {
|
||||
console.warn("config must be an object");
|
||||
return;
|
||||
}
|
||||
this._props.config = {
|
||||
...this._props.config,
|
||||
...value
|
||||
};
|
||||
this._render();
|
||||
}
|
||||
|
||||
get dataPoints() {
|
||||
return this._props.dataPoints;
|
||||
}
|
||||
|
||||
set dataPoints(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
console.warn("dataPoints must be an array");
|
||||
return;
|
||||
}
|
||||
this._props.dataPoints = value;
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Type Name Qualification
|
||||
|
||||
Internally, custom type names are qualified with widget ID to avoid conflicts:
|
||||
|
||||
- Defined as: `ChartConfig`
|
||||
- Internal name: `com.company.advancedwidget.ChartConfig`
|
||||
|
||||
---
|
||||
|
||||
## Script API Data Types
|
||||
|
||||
Types available for properties and method parameters.
|
||||
|
||||
### Selection Type
|
||||
|
||||
Represents a data selection in SAC:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"currentSelection": {
|
||||
"type": "Selection",
|
||||
"default": {},
|
||||
"description": "Current data selection"
|
||||
}
|
||||
},
|
||||
"methods": {
|
||||
"setSelection": {
|
||||
"description": "Set data selection",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "selection",
|
||||
"type": "Selection",
|
||||
"description": "Selection to apply"
|
||||
}
|
||||
],
|
||||
"body": "this._setSelection(selection);"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in Scripts**:
|
||||
```javascript
|
||||
// In SAC script
|
||||
var selection = {
|
||||
"Account": "Revenue",
|
||||
"Year": "2024"
|
||||
};
|
||||
Widget_1.setSelection(selection);
|
||||
```
|
||||
|
||||
### MemberInfo Type
|
||||
|
||||
Information about a dimension member:
|
||||
|
||||
```javascript
|
||||
// MemberInfo object structure
|
||||
{
|
||||
id: "MEMBER_ID", // Technical ID
|
||||
description: "Member Name", // Display name
|
||||
dimensionId: "DIM_ID", // Parent dimension
|
||||
modelId: "MODEL_ID", // Data model
|
||||
displayId: "DISPLAY_ID" // Display ID
|
||||
}
|
||||
```
|
||||
|
||||
**Using in Widget**:
|
||||
```javascript
|
||||
class MyWidget extends HTMLElement {
|
||||
setMemberInfo(memberInfo) {
|
||||
this._currentMember = memberInfo;
|
||||
this._shadowRoot.getElementById("memberLabel").textContent =
|
||||
memberInfo.description || memberInfo.id;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ResultMemberInfo Type
|
||||
|
||||
Extended member information from result set:
|
||||
|
||||
```javascript
|
||||
// ResultMemberInfo structure
|
||||
{
|
||||
id: "MEMBER_ID",
|
||||
description: "Member Name",
|
||||
parentId: "PARENT_ID", // For hierarchies
|
||||
properties: {
|
||||
"Property1": "Value1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DataSource Methods
|
||||
|
||||
Access data source information:
|
||||
|
||||
```javascript
|
||||
// In SAC script with data binding
|
||||
var ds = Widget_1.getDataSource();
|
||||
|
||||
// Get members
|
||||
var members = ds.getMembers("Account", { limit: 100 });
|
||||
|
||||
// Get result member
|
||||
var selection = { "Account": "Revenue" };
|
||||
var memberInfo = ds.getResultMember("Account", selection);
|
||||
|
||||
// Get data cell value
|
||||
var value = ds.getData(selection);
|
||||
```
|
||||
|
||||
### Color Type
|
||||
|
||||
SAC Color type for color properties:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"primaryColor": {
|
||||
"type": "Color",
|
||||
"default": "#336699",
|
||||
"description": "Primary widget color"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Widget Installation
|
||||
|
||||
### Administrator Steps
|
||||
|
||||
1. **Access Custom Widgets**:
|
||||
- Main Menu > **Analytic Applications**
|
||||
- Select **Custom Widgets** tab
|
||||
|
||||
2. **Upload Widget**:
|
||||
- Click **+** (Add) button
|
||||
- Select JSON file from local system
|
||||
- Widget appears in list after upload
|
||||
|
||||
3. **Manage Widgets**:
|
||||
- View installed widgets in list
|
||||
- Delete widgets no longer needed
|
||||
- Update by re-uploading JSON
|
||||
|
||||
### Requirements
|
||||
|
||||
- **Role**: Administrator or custom widget manager
|
||||
- **Files**: JSON metadata file (resource files hosted externally)
|
||||
- **Hosting**: Resource files accessible via HTTPS
|
||||
|
||||
### SAC-Hosted Widgets (QRC Q2 2023+)
|
||||
|
||||
Upload resource files directly to SAC:
|
||||
|
||||
1. **Prepare Files**:
|
||||
- Pack JSON and JS files into ZIP
|
||||
- Or upload individually to SAC Files
|
||||
|
||||
2. **Configure JSON for SAC Hosting**:
|
||||
```json
|
||||
{
|
||||
"webcomponents": [
|
||||
{
|
||||
"kind": "main",
|
||||
"tag": "my-widget",
|
||||
"url": "/my-widget.js",
|
||||
"integrity": "",
|
||||
"ignoreIntegrity": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Note: URL starts with `/` for SAC-hosted files
|
||||
|
||||
3. **Upload to SAC**:
|
||||
- Go to Files > Public Files
|
||||
- Create folder for widget
|
||||
- Upload JS files
|
||||
- Upload JSON to Custom Widgets
|
||||
|
||||
### Using Widgets in Stories
|
||||
|
||||
1. Open story in Edit mode
|
||||
2. Open widget panel (Insert > Widget)
|
||||
3. Find custom widget in Custom section
|
||||
4. Drag onto canvas
|
||||
5. Configure via Builder/Styling panels
|
||||
|
||||
---
|
||||
|
||||
## Third-Party Library Integration
|
||||
|
||||
### Supported Libraries
|
||||
|
||||
Common libraries used with SAC widgets:
|
||||
|
||||
| Library | Use Case | CDN |
|
||||
|---------|----------|-----|
|
||||
| ECharts | Charts | `[https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js`](https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js`) |
|
||||
| D3.js | Data viz | `[https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js`](https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js`) |
|
||||
| Chart.js | Charts | `[https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js`](https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js`) |
|
||||
| Leaflet | Maps | `[https://unpkg.com/leaflet@1.9/dist/leaflet.js`](https://unpkg.com/leaflet@1.9/dist/leaflet.js`) |
|
||||
| Moment.js | Dates | `[https://cdn.jsdelivr.net/npm/moment@2/moment.min.js`](https://cdn.jsdelivr.net/npm/moment@2/moment.min.js`) |
|
||||
|
||||
### Integration Pattern
|
||||
|
||||
```javascript
|
||||
(function() {
|
||||
// Library URLs
|
||||
const LIBS = {
|
||||
echarts: "[https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"](https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js")
|
||||
};
|
||||
|
||||
// Track loading state
|
||||
const libState = {
|
||||
echarts: { loaded: false, loading: false, callbacks: [] }
|
||||
};
|
||||
|
||||
// Load library once
|
||||
function loadLibrary(name) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = libState[name];
|
||||
|
||||
// Already loaded
|
||||
if (state.loaded) {
|
||||
resolve(window[name === "echarts" ? "echarts" : name]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Loading - queue callback
|
||||
if (state.loading) {
|
||||
state.callbacks.push({ resolve, reject });
|
||||
return;
|
||||
}
|
||||
|
||||
// Start loading
|
||||
state.loading = true;
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = LIBS[name];
|
||||
|
||||
script.onload = () => {
|
||||
state.loaded = true;
|
||||
state.loading = false;
|
||||
const lib = window[name === "echarts" ? "echarts" : name];
|
||||
resolve(lib);
|
||||
state.callbacks.forEach(cb => cb.resolve(lib));
|
||||
state.callbacks = [];
|
||||
};
|
||||
|
||||
script.onerror = (err) => {
|
||||
state.loading = false;
|
||||
reject(err);
|
||||
state.callbacks.forEach(cb => cb.reject(err));
|
||||
state.callbacks = [];
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
class ChartWidget extends HTMLElement {
|
||||
async connectedCallback() {
|
||||
try {
|
||||
const echarts = await loadLibrary("echarts");
|
||||
this._initChart(echarts);
|
||||
} catch (error) {
|
||||
this._showError("Failed to load chart library");
|
||||
}
|
||||
}
|
||||
|
||||
_initChart(echarts) {
|
||||
const container = this._shadowRoot.getElementById("chart");
|
||||
this._chart = echarts.init(container);
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("chart-widget", ChartWidget);
|
||||
})();
|
||||
```
|
||||
|
||||
### License Considerations
|
||||
|
||||
**Important**: Review third-party library licenses before deployment.
|
||||
|
||||
- MIT/Apache: Generally safe for commercial use
|
||||
- GPL: May have copyleft requirements
|
||||
- Commercial: May require license purchase
|
||||
|
||||
Check license compatibility with SAC deployment.
|
||||
|
||||
---
|
||||
|
||||
## Advanced Data Binding
|
||||
|
||||
### Multiple Data Bindings
|
||||
|
||||
```json
|
||||
{
|
||||
"dataBindings": {
|
||||
"primaryData": {
|
||||
"feeds": [
|
||||
{ "id": "xAxis", "description": "X-Axis", "type": "dimension" },
|
||||
{ "id": "yAxis", "description": "Y-Axis", "type": "mainStructureMember" }
|
||||
]
|
||||
},
|
||||
"secondaryData": {
|
||||
"feeds": [
|
||||
{ "id": "categories", "description": "Categories", "type": "dimension" },
|
||||
{ "id": "values", "description": "Values", "type": "mainStructureMember" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Currently only the first dataBinding is used. Multiple bindings are defined but only one is active.
|
||||
|
||||
### Accessing Metadata
|
||||
|
||||
```javascript
|
||||
_processData() {
|
||||
const data = this.primaryData;
|
||||
if (!data || !data.data) return;
|
||||
|
||||
// Access metadata
|
||||
const metadata = data.metadata;
|
||||
|
||||
// Dimension info
|
||||
if (metadata.dimensions) {
|
||||
Object.entries(metadata.dimensions).forEach(([key, dim]) => {
|
||||
console.log(`Dimension: ${dim.description}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Measure info
|
||||
if (metadata.mainStructureMembers) {
|
||||
Object.entries(metadata.mainStructureMembers).forEach(([key, measure]) => {
|
||||
console.log(`Measure: ${measure.description}, Unit: ${measure.unitOfMeasure}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DataBinding Object Methods
|
||||
|
||||
```javascript
|
||||
// Get DataBinding object
|
||||
const binding = this.dataBindings.getDataBinding("primaryData");
|
||||
|
||||
// Available methods (async)
|
||||
const resultSet = await binding.getResultSet();
|
||||
const members = await binding.getMembers("DimensionName");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-Language Support
|
||||
|
||||
### Externalize Strings
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"titleKey": {
|
||||
"type": "string",
|
||||
"default": "WIDGET_TITLE",
|
||||
"description": "Translation key for title"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Translation Pattern
|
||||
|
||||
```javascript
|
||||
class MyWidget extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._translations = {
|
||||
en: {
|
||||
WIDGET_TITLE: "My Widget",
|
||||
NO_DATA: "No data available",
|
||||
LOADING: "Loading..."
|
||||
},
|
||||
de: {
|
||||
WIDGET_TITLE: "Mein Widget",
|
||||
NO_DATA: "Keine Daten verfügbar",
|
||||
LOADING: "Laden..."
|
||||
}
|
||||
};
|
||||
this._locale = "en";
|
||||
}
|
||||
|
||||
_t(key) {
|
||||
const translations = this._translations[this._locale] || this._translations.en;
|
||||
return translations[key] || key;
|
||||
}
|
||||
|
||||
_render() {
|
||||
this._shadowRoot.getElementById("title").textContent = this._t(this._props.titleKey);
|
||||
}
|
||||
|
||||
// Set locale from SAC context
|
||||
setLocale(locale) {
|
||||
this._locale = locale.substring(0, 2); // "en-US" -> "en"
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Advanced Widgets
|
||||
|
||||
### Console Inspection
|
||||
|
||||
```javascript
|
||||
// Expose widget for debugging
|
||||
connectedCallback() {
|
||||
// Make accessible in console
|
||||
window.__myWidget = this;
|
||||
|
||||
// Log initialization
|
||||
console.log("[MyWidget] Initialized", {
|
||||
props: this._props,
|
||||
dataBinding: this.primaryData
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Profiling
|
||||
|
||||
```javascript
|
||||
_render() {
|
||||
const start = performance.now();
|
||||
|
||||
// Rendering logic
|
||||
this._doRender();
|
||||
|
||||
const duration = performance.now() - start;
|
||||
if (duration > 16) { // > 1 frame
|
||||
console.warn(`[MyWidget] Slow render: ${duration.toFixed(2)}ms`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [SAP Custom Widget Developer Guide (PDF)](https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf)
|
||||
- [Analytics Designer API Reference](https://help.sap.com/doc/958d4c11261f42e992e8d01a4c0dde25/release/en-US/index.html)
|
||||
- [Hosting in SAC](https://community.sap.com/t5/technology-blogs-by-sap/hosting-and-uploading-custom-widgets-resource-files-into-sap-analytics/ba-p/13563064)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-22
|
||||
608
references/best-practices-guide.md
Normal file
608
references/best-practices-guide.md
Normal file
@@ -0,0 +1,608 @@
|
||||
# SAP SAC Custom Widget Best Practices
|
||||
|
||||
Comprehensive guide for performance, security, and development best practices.
|
||||
|
||||
**Sources**:
|
||||
- [SAP Custom Widget Developer Guide](https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf)
|
||||
- [Performance Optimization](https://community.sap.com/t5/technology-blog-posts-by-members/performance-optimization-techniques-for-sap-analytics-cloud-application/ba-p/13516595)
|
||||
- [Optimizing SAC](https://community.sap.com/t5/technology-blog-posts-by-sap/optimizing-sap-analytics-cloud-best-practices-and-performance/ba-p/14229397)
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Performance Best Practices](#performance-best-practices)
|
||||
2. [Security Best Practices](#security-best-practices)
|
||||
3. [Development Best Practices](#development-best-practices)
|
||||
4. [Testing Guidelines](#testing-guidelines)
|
||||
5. [Deployment Checklist](#deployment-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Performance Best Practices
|
||||
|
||||
### Widget Initialization
|
||||
|
||||
**DO**:
|
||||
```javascript
|
||||
// Lazy initialization - only load when visible
|
||||
connectedCallback() {
|
||||
if (this._initialized) return;
|
||||
this._initialized = true;
|
||||
this._init();
|
||||
}
|
||||
|
||||
// Defer non-critical setup
|
||||
_init() {
|
||||
// Critical setup first
|
||||
this._setupDOM();
|
||||
|
||||
// Defer expensive operations
|
||||
requestAnimationFrame(() => {
|
||||
this._loadResources();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**DON'T**:
|
||||
```javascript
|
||||
// Heavy processing in constructor
|
||||
constructor() {
|
||||
super();
|
||||
this._processLargeDataset(); // Blocks main thread
|
||||
this._loadExternalLibraries(); // Network call in constructor
|
||||
}
|
||||
```
|
||||
|
||||
### Data Handling
|
||||
|
||||
**Use getResultSet() Instead of getMembers()**:
|
||||
```javascript
|
||||
// RECOMMENDED - No backend roundtrip
|
||||
async _getData() {
|
||||
const resultSet = await this.dataBinding.getResultSet();
|
||||
return resultSet;
|
||||
}
|
||||
|
||||
// AVOID - Causes extra backend roundtrip
|
||||
async _getData() {
|
||||
const members = await this.dataBinding.getMembers("Dimension");
|
||||
return members;
|
||||
}
|
||||
```
|
||||
|
||||
**Limit Data Points**:
|
||||
```javascript
|
||||
_renderChart(data) {
|
||||
const MAX_POINTS = 100;
|
||||
|
||||
// Warn if data is truncated
|
||||
if (data.length > MAX_POINTS) {
|
||||
console.warn(`Data truncated from ${data.length} to ${MAX_POINTS} points`);
|
||||
}
|
||||
|
||||
const limitedData = data.slice(0, MAX_POINTS);
|
||||
this._chart.setOption({ series: [{ data: limitedData }] });
|
||||
}
|
||||
```
|
||||
|
||||
### Rendering Optimization
|
||||
|
||||
**Debounce Updates**:
|
||||
```javascript
|
||||
onCustomWidgetAfterUpdate(changedProperties) {
|
||||
// Debounce rapid property changes
|
||||
if (this._updateTimer) {
|
||||
clearTimeout(this._updateTimer);
|
||||
}
|
||||
this._updateTimer = setTimeout(() => {
|
||||
this._render();
|
||||
}, 50);
|
||||
}
|
||||
```
|
||||
|
||||
**Batch DOM Updates**:
|
||||
```javascript
|
||||
// GOOD - Single DOM update
|
||||
_render() {
|
||||
const html = this._data.map(item => `<div>${item.label}: ${item.value}</div>`).join("");
|
||||
this._container.innerHTML = html;
|
||||
}
|
||||
|
||||
// BAD - Multiple DOM updates
|
||||
_render() {
|
||||
this._container.innerHTML = "";
|
||||
this._data.forEach(item => {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = `${item.label}: ${item.value}`;
|
||||
this._container.appendChild(div); // Triggers reflow each time
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Use requestAnimationFrame for Visual Updates**:
|
||||
```javascript
|
||||
_scheduleRender() {
|
||||
if (this._renderScheduled) return;
|
||||
this._renderScheduled = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this._renderScheduled = false;
|
||||
this._render();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Resize Handling
|
||||
|
||||
```javascript
|
||||
onCustomWidgetResize() {
|
||||
// Debounce resize events
|
||||
if (this._resizeTimer) {
|
||||
clearTimeout(this._resizeTimer);
|
||||
}
|
||||
this._resizeTimer = setTimeout(() => {
|
||||
if (this._chart) {
|
||||
this._chart.resize();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
|
||||
```javascript
|
||||
onCustomWidgetDestroy() {
|
||||
// Clear timers
|
||||
if (this._updateTimer) clearTimeout(this._updateTimer);
|
||||
if (this._resizeTimer) clearTimeout(this._resizeTimer);
|
||||
|
||||
// Dispose chart libraries
|
||||
if (this._chart) {
|
||||
this._chart.dispose();
|
||||
this._chart = null;
|
||||
}
|
||||
|
||||
// Remove event listeners
|
||||
if (this._boundHandlers) {
|
||||
window.removeEventListener("resize", this._boundHandlers.resize);
|
||||
}
|
||||
|
||||
// Clear data references
|
||||
this._data = null;
|
||||
this._props = null;
|
||||
}
|
||||
```
|
||||
|
||||
### Third-Party Library Loading
|
||||
|
||||
**Lazy Load Libraries**:
|
||||
```javascript
|
||||
async _loadEcharts() {
|
||||
if (window.echarts) return window.echarts;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = "[https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";](https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";)
|
||||
script.onload = () => resolve(window.echarts);
|
||||
script.onerror = () => reject(new Error("Failed to load ECharts"));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
// Use async initialization
|
||||
async connectedCallback() {
|
||||
try {
|
||||
const echarts = await this._loadEcharts();
|
||||
this._initChart(echarts);
|
||||
} catch (error) {
|
||||
this._showError("Failed to load chart library");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Input Validation
|
||||
|
||||
**Sanitize User Input**:
|
||||
```javascript
|
||||
_setTitle(value) {
|
||||
// Sanitize to prevent XSS
|
||||
const sanitized = this._sanitizeHTML(value);
|
||||
this._shadowRoot.getElementById("title").textContent = sanitized;
|
||||
}
|
||||
|
||||
_sanitizeHTML(str) {
|
||||
const temp = document.createElement("div");
|
||||
temp.textContent = str;
|
||||
return temp.innerHTML;
|
||||
}
|
||||
```
|
||||
|
||||
**Validate Property Types**:
|
||||
```javascript
|
||||
set value(val) {
|
||||
// Type validation
|
||||
if (typeof val !== "number" || isNaN(val)) {
|
||||
console.warn("Invalid value type, expected number");
|
||||
return;
|
||||
}
|
||||
|
||||
// Range validation
|
||||
if (val < 0 || val > 100) {
|
||||
console.warn("Value out of range (0-100)");
|
||||
val = Math.max(0, Math.min(100, val));
|
||||
}
|
||||
|
||||
this._props.value = val;
|
||||
this._render();
|
||||
}
|
||||
```
|
||||
|
||||
### Content Security
|
||||
|
||||
**Avoid innerHTML with User Data**:
|
||||
```javascript
|
||||
// DANGEROUS - XSS vulnerability
|
||||
_render() {
|
||||
this._container.innerHTML = `<div>${this._userInput}</div>`;
|
||||
}
|
||||
|
||||
// SAFE - Use textContent or sanitize
|
||||
_render() {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = this._userInput;
|
||||
this._container.innerHTML = "";
|
||||
this._container.appendChild(div);
|
||||
}
|
||||
```
|
||||
|
||||
**Use Shadow DOM Encapsulation**:
|
||||
```javascript
|
||||
constructor() {
|
||||
super();
|
||||
// Shadow DOM isolates styles and scripts
|
||||
this._shadowRoot = this.attachShadow({ mode: "open" });
|
||||
}
|
||||
```
|
||||
|
||||
### Integrity Hash (Production)
|
||||
|
||||
**Generate SHA256 Hash**:
|
||||
```bash
|
||||
# Generate integrity hash for JavaScript file
|
||||
openssl dgst -sha256 -binary widget.js | openssl base64 -A
|
||||
|
||||
# Example output: K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=
|
||||
```
|
||||
|
||||
**Configure in JSON**:
|
||||
```json
|
||||
{
|
||||
"webcomponents": [
|
||||
{
|
||||
"kind": "main",
|
||||
"tag": "my-widget",
|
||||
"url": "[https://host.com/widget.js",](https://host.com/widget.js",)
|
||||
"integrity": "sha256-K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=",
|
||||
"ignoreIntegrity": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Warning**: `ignoreIntegrity: true` triggers security warnings for admins. Only use in development.
|
||||
|
||||
### CORS Configuration
|
||||
|
||||
**Server Headers Required**:
|
||||
```
|
||||
Access-Control-Allow-Origin: *
|
||||
Access-Control-Allow-Methods: GET, OPTIONS
|
||||
Access-Control-Allow-Headers: Content-Type
|
||||
```
|
||||
|
||||
**Express.js Example**:
|
||||
```javascript
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const app = express();
|
||||
|
||||
app.use(cors({
|
||||
origin: "*",
|
||||
methods: ["GET", "OPTIONS"]
|
||||
}));
|
||||
|
||||
app.use(express.static("public"));
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
### External API Calls
|
||||
|
||||
**Use HTTPS Only**:
|
||||
```javascript
|
||||
async _fetchExternalData() {
|
||||
// Always use HTTPS
|
||||
const url = "[https://api.example.com/data";](https://api.example.com/data";)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("API call failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Best Practices
|
||||
|
||||
### Code Organization
|
||||
|
||||
**Modular Structure**:
|
||||
```javascript
|
||||
(function() {
|
||||
// Constants
|
||||
const CONFIG = {
|
||||
MAX_ITEMS: 100,
|
||||
DEBOUNCE_MS: 50,
|
||||
DEFAULT_COLOR: "#336699"
|
||||
};
|
||||
|
||||
// Template
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = `<style>...</style><div>...</div>`;
|
||||
|
||||
// Helper functions
|
||||
function formatNumber(n) { ... }
|
||||
function sanitize(str) { ... }
|
||||
|
||||
// Main class
|
||||
class MyWidget extends HTMLElement {
|
||||
// Properties first
|
||||
static get observedAttributes() { return ["title", "value"]; }
|
||||
|
||||
// Constructor
|
||||
constructor() { ... }
|
||||
|
||||
// Lifecycle methods (in order)
|
||||
connectedCallback() { ... }
|
||||
onCustomWidgetBeforeUpdate() { ... }
|
||||
onCustomWidgetAfterUpdate() { ... }
|
||||
onCustomWidgetResize() { ... }
|
||||
onCustomWidgetDestroy() { ... }
|
||||
|
||||
// Public methods
|
||||
refresh() { ... }
|
||||
setValue(v) { ... }
|
||||
|
||||
// Private methods (underscore prefix)
|
||||
_render() { ... }
|
||||
_handleClick() { ... }
|
||||
|
||||
// Getters/setters last
|
||||
get title() { ... }
|
||||
set title(v) { ... }
|
||||
}
|
||||
|
||||
customElements.define("my-widget", MyWidget);
|
||||
})();
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```javascript
|
||||
class MyWidget extends HTMLElement {
|
||||
_render() {
|
||||
try {
|
||||
// Rendering logic
|
||||
this._doRender();
|
||||
} catch (error) {
|
||||
console.error("Widget render failed:", error);
|
||||
this._showErrorState(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
_showErrorState(message) {
|
||||
this._shadowRoot.innerHTML = `
|
||||
<div class="error-container">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<span class="error-message">Widget Error: ${this._sanitize(message)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_sanitize(str) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Logging for Debugging
|
||||
|
||||
```javascript
|
||||
class MyWidget extends HTMLElement {
|
||||
_log(level, message, data) {
|
||||
if (!this._props.debug) return;
|
||||
|
||||
const prefix = `[MyWidget]`;
|
||||
switch (level) {
|
||||
case "info":
|
||||
console.log(prefix, message, data);
|
||||
break;
|
||||
case "warn":
|
||||
console.warn(prefix, message, data);
|
||||
break;
|
||||
case "error":
|
||||
console.error(prefix, message, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onCustomWidgetAfterUpdate(changedProperties) {
|
||||
this._log("info", "Properties updated", changedProperties);
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
**JSDoc Comments**:
|
||||
```javascript
|
||||
/**
|
||||
* Custom KPI Widget for SAP Analytics Cloud
|
||||
* @class
|
||||
* @extends HTMLElement
|
||||
*
|
||||
* @property {string} title - Widget title
|
||||
* @property {number} value - KPI value (0-100)
|
||||
* @property {string} color - Primary color (hex)
|
||||
*
|
||||
* @fires onClick - When widget is clicked
|
||||
*
|
||||
* @example
|
||||
* // In SAC script
|
||||
* KPIWidget_1.title = "Revenue";
|
||||
* KPIWidget_1.value = 85;
|
||||
*/
|
||||
class KPIWidget extends HTMLElement { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Local Development Server
|
||||
|
||||
```javascript
|
||||
// server.js
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const path = require("path");
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.static(path.join(__dirname, "dist")));
|
||||
|
||||
app.listen(3000, () => {
|
||||
console.log("Widget dev server: [http://localhost:3000](http://localhost:3000)");
|
||||
});
|
||||
```
|
||||
|
||||
### Test Scenarios
|
||||
|
||||
**1. Property Updates**:
|
||||
- Change each property via script
|
||||
- Verify visual updates
|
||||
- Check console for errors
|
||||
|
||||
**2. Data Binding**:
|
||||
- Add/remove data binding
|
||||
- Empty data handling
|
||||
- Large dataset handling
|
||||
|
||||
**3. Resize**:
|
||||
- Resize container
|
||||
- Switch responsive layouts
|
||||
- Check chart redraws
|
||||
|
||||
**4. Lifecycle**:
|
||||
- Navigate away and back
|
||||
- Remove and re-add widget
|
||||
- Verify cleanup in destroy
|
||||
|
||||
**5. Error Cases**:
|
||||
- Invalid property values
|
||||
- Network failures
|
||||
- Missing data
|
||||
|
||||
### Browser DevTools
|
||||
|
||||
1. **Console**: Watch for errors and logs
|
||||
2. **Network**: Verify file loading
|
||||
3. **Elements**: Inspect Shadow DOM
|
||||
4. **Performance**: Profile render times
|
||||
5. **Memory**: Check for leaks
|
||||
|
||||
---
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
### Pre-Deployment
|
||||
|
||||
- [ ] Remove `console.log` statements (or guard with debug flag)
|
||||
- [ ] Set `ignoreIntegrity: false`
|
||||
- [ ] Generate and set integrity hash
|
||||
- [ ] Minify JavaScript files
|
||||
- [ ] Test with production data
|
||||
- [ ] Verify HTTPS hosting
|
||||
- [ ] Check CORS headers
|
||||
|
||||
### JSON Configuration
|
||||
|
||||
- [ ] Unique ID (reverse domain notation)
|
||||
- [ ] Correct version number
|
||||
- [ ] All URLs are absolute HTTPS
|
||||
- [ ] Integrity hashes set
|
||||
- [ ] Properties have descriptions
|
||||
- [ ] Methods documented
|
||||
|
||||
### Documentation
|
||||
|
||||
- [ ] README with usage instructions
|
||||
- [ ] Property descriptions
|
||||
- [ ] Event documentation
|
||||
- [ ] Known limitations
|
||||
- [ ] Version changelog
|
||||
|
||||
### Hosting
|
||||
|
||||
- [ ] Files accessible via HTTPS
|
||||
- [ ] CORS configured correctly
|
||||
- [ ] CDN or reliable hosting
|
||||
- [ ] Backup of all files
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
| Anti-Pattern | Problem | Solution |
|
||||
|--------------|---------|----------|
|
||||
| Heavy constructor | Blocks initial render | Defer to connectedCallback |
|
||||
| Sync external loads | Freezes UI | Use async/await |
|
||||
| innerHTML with user data | XSS vulnerability | Use textContent or sanitize |
|
||||
| No error handling | Silent failures | try/catch with error display |
|
||||
| Memory leaks | Performance degradation | Clean up in destroy |
|
||||
| Unbounded data | UI freeze | Limit and paginate |
|
||||
| Frequent DOM updates | Janky UI | Batch updates |
|
||||
| `ignoreIntegrity: true` in prod | Security warning | Generate proper hash |
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [SAP Custom Widget Developer Guide](https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf)
|
||||
- [SAC Performance Optimization](https://community.sap.com/t5/technology-blog-posts-by-sap/optimizing-sap-analytics-cloud-best-practices-and-performance/ba-p/14229397)
|
||||
- [Local Development Server](https://community.sap.com/t5/technology-blog-posts-by-sap/streamline-sac-custom-widget-development-with-local-server/ba-p/14160499)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-22
|
||||
927
references/echarts-integration.md
Normal file
927
references/echarts-integration.md
Normal file
@@ -0,0 +1,927 @@
|
||||
# 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 = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #6a6d70;
|
||||
font-family: "72", Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<div class="chart-container" id="chart">
|
||||
<div class="loading">Loading ECharts...</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 =
|
||||
'<div class="loading">Failed to load ECharts library</div>';
|
||||
};
|
||||
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 = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.no-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #6a6d70;
|
||||
font-family: "72", Arial, sans-serif;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.no-data-icon {
|
||||
font-size: 32px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
<div class="chart-container" id="chart">
|
||||
<div class="no-data">
|
||||
<div class="no-data-icon">📊</div>
|
||||
<div>Add data binding to display chart</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
font-family: "72", Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
}
|
||||
.panel { padding: 12px; }
|
||||
.field { margin-bottom: 16px; }
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
color: #32363a;
|
||||
}
|
||||
select, input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #89919a;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.checkbox-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.color-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.color-input {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 2px;
|
||||
border: 1px solid #89919a;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<div class="panel">
|
||||
<div class="field">
|
||||
<label>Title</label>
|
||||
<input type="text" id="titleInput" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Chart Type</label>
|
||||
<select id="chartTypeSelect">
|
||||
<option value="bar">Bar Chart</option>
|
||||
<option value="line">Line Chart</option>
|
||||
<option value="pie">Pie Chart</option>
|
||||
<option value="horizontal-bar">Horizontal Bar</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field checkbox-field">
|
||||
<input type="checkbox" id="legendCheckbox" />
|
||||
<label for="legendCheckbox">Show Legend</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Colors</label>
|
||||
<div class="color-row">
|
||||
<input type="color" class="color-input" id="color1" value="#5470c6" />
|
||||
<input type="color" class="color-input" id="color2" value="#91cc75" />
|
||||
<input type="color" class="color-input" id="color3" value="#fac858" />
|
||||
<input type="color" class="color-input" id="color4" value="#ee6666" />
|
||||
<input type="color" class="color-input" id="color5" value="#73c0de" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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
|
||||
397
references/integration-and-migration.md
Normal file
397
references/integration-and-migration.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# SAP SAC Custom Widget Integration and Migration
|
||||
|
||||
Coverage of script integration, content transport, story compatibility, and planning features.
|
||||
|
||||
**Sources**:
|
||||
- [SAP Community - Content Transport](https://community.sap.com/t5/technology-blog-posts-by-members/sac-content-transport-migration-using-ctms/ba-p/13742318)
|
||||
- [Optimized Story Experience](https://community.sap.com/t5/technology-blog-posts-by-sap/the-new-optimized-story-experience-the-unification-of-story-and-analytic/ba-p/13558887)
|
||||
- [Analytics Designer API Reference](https://help.sap.com/doc/958d4c11261f42e992e8d01a4c0dde25/release/en-US/index.html)
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Script Integration](#script-integration)
|
||||
2. [Content Transport and Migration](#content-transport-and-migration)
|
||||
3. [Story Compatibility](#story-compatibility)
|
||||
4. [Planning Integration](#planning-integration)
|
||||
5. [API Methods Reference](#api-methods-reference)
|
||||
|
||||
---
|
||||
|
||||
## Script Integration
|
||||
|
||||
### Global Script Objects
|
||||
|
||||
Custom widgets can interact with SAC's global script objects.
|
||||
|
||||
**Script Object Structure**:
|
||||
- Script objects act as containers for reusable functions
|
||||
- Functions not tied to events, invoked directly
|
||||
- Accessible from all scripts in the story
|
||||
|
||||
**Invoking Script Functions**:
|
||||
```javascript
|
||||
// In SAC script
|
||||
ScriptObjectName.ScriptFunctionName();
|
||||
|
||||
// Example
|
||||
Utils.formatCurrency(1000, "USD"); // Returns "$1,000.00"
|
||||
```
|
||||
|
||||
### Script Variables
|
||||
|
||||
**Global Variables**:
|
||||
- Defined at story level
|
||||
- Accessible from all script blocks
|
||||
- Can receive values from URL parameters
|
||||
|
||||
**Using with Custom Widgets**:
|
||||
```javascript
|
||||
// In SAC script
|
||||
var myValue = GlobalVariable_1;
|
||||
CustomWidget_1.setValue(myValue);
|
||||
|
||||
// Widget method receives value
|
||||
class MyWidget extends HTMLElement {
|
||||
setValue(val) {
|
||||
this._props.value = val;
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Script Object Integration Pattern
|
||||
|
||||
```javascript
|
||||
// Custom widget firing events for script handling
|
||||
class MyWidget extends HTMLElement {
|
||||
_handleUserAction(data) {
|
||||
// Fire event that SAC script can handle
|
||||
this.dispatchEvent(new CustomEvent("onUserAction", {
|
||||
detail: { actionData: data }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// In SAC script (event handler)
|
||||
CustomWidget_1.onUserAction = function() {
|
||||
var eventData = CustomWidget_1.getEventInfo();
|
||||
// Process event, call other script objects
|
||||
DataProcessor.handleAction(eventData.actionData);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Content Transport and Migration
|
||||
|
||||
### Transport Methods
|
||||
|
||||
**1. Content Network (Same Region)**
|
||||
- Source and destination on same region
|
||||
- Same or +1 quarterly version
|
||||
- Access: Main Menu > Transport > Export/Import > Content Network Storage
|
||||
|
||||
**2. Import/Export (Any Region)**
|
||||
- No region restriction
|
||||
- Version constraints apply
|
||||
- More flexible but manual
|
||||
|
||||
### Custom Widget Transport
|
||||
|
||||
**Supported Scenarios**:
|
||||
- Cloud Foundry to Cloud Foundry tenants
|
||||
- Same hosting configuration required
|
||||
|
||||
**Not Supported**:
|
||||
- Cloud Foundry to Neo platform
|
||||
- Different hosting configurations may cause issues
|
||||
|
||||
### Common Transport Issue
|
||||
|
||||
**Error**: "The system couldn't load the custom widget"
|
||||
|
||||
**Causes**:
|
||||
- Widget JSON transported but resource files not accessible
|
||||
- Different hosting URLs between source/target
|
||||
- Integrity hash mismatch after transport
|
||||
|
||||
**Solution**:
|
||||
```json
|
||||
// Ensure resource URLs are accessible from target tenant
|
||||
{
|
||||
"webcomponents": [
|
||||
{
|
||||
"kind": "main",
|
||||
"tag": "my-widget",
|
||||
"url": "[https://globally-accessible-host.com/widget.js",](https://globally-accessible-host.com/widget.js",)
|
||||
"integrity": "sha256-...",
|
||||
"ignoreIntegrity": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Transport Best Practices
|
||||
|
||||
1. **Use globally accessible hosting** (GitHub Pages, CDN, SAC-hosted)
|
||||
2. **Verify URLs before transport** - Ensure target can reach resource files
|
||||
3. **Re-upload JSON** if hosting changes - Update URLs post-transport
|
||||
4. **Test in target** before production use
|
||||
|
||||
### CTMS Integration
|
||||
|
||||
Cloud Transport Management Service (CTMS) provides automated transport:
|
||||
|
||||
1. Integrate CTMS with SAC
|
||||
2. Define transport routes
|
||||
3. Upload packages via SAC interface
|
||||
4. CTMS handles deployment to destination
|
||||
|
||||
**Limitation**: CTMS is basic - no destination location selection like native Content Network.
|
||||
|
||||
---
|
||||
|
||||
## Story Compatibility
|
||||
|
||||
### Story Types
|
||||
|
||||
| Type | Custom Widgets | Scripting | CSS/Themes |
|
||||
|------|----------------|-----------|------------|
|
||||
| Classic Story | Limited | No | Limited |
|
||||
| Optimized Story (Classic Responsive) | Yes | Limited | Limited |
|
||||
| Optimized Story (Advanced Responsive) | Full | Full | Full |
|
||||
|
||||
### Optimized Story Experience (QRC Q2 2023+)
|
||||
|
||||
**Advanced Responsive Layout** features:
|
||||
- Full custom widget support
|
||||
- Complete scripting capabilities
|
||||
- CSS and theme customization
|
||||
- Device preview
|
||||
- Data binding
|
||||
|
||||
### Classic Story Conversion
|
||||
|
||||
**Conversion Status Types**:
|
||||
|
||||
1. **Ready to convert** - No issues, direct conversion
|
||||
2. **Feature limitation** - Some features not supported in optimized
|
||||
3. **Blocked** - Issues must be resolved first
|
||||
|
||||
**Conversion Notes**:
|
||||
- Conversion is permanent
|
||||
- Save as copy recommended
|
||||
- Converted stories use Classic Responsive Layout initially
|
||||
|
||||
### Custom Widget Compatibility
|
||||
|
||||
**In Optimized Stories**:
|
||||
```json
|
||||
{
|
||||
"id": "com.company.widget",
|
||||
"dataBindings": {
|
||||
"myData": {
|
||||
"feeds": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- Full data binding support
|
||||
- Script integration
|
||||
- Builder/Styling panels
|
||||
|
||||
**In Classic Stories**:
|
||||
- Limited support
|
||||
- No data binding
|
||||
- Basic property configuration only
|
||||
|
||||
---
|
||||
|
||||
## Planning Integration
|
||||
|
||||
### ⚠️ Important Limitations
|
||||
|
||||
Before implementing planning widgets, review these constraints:
|
||||
|
||||
1. **Builder Panel + Data Binding Conflict**: Cannot combine custom Builder Panel with data binding in the same widget
|
||||
2. **Hierarchies Not Supported**: Data binding works with flat data only; select "flat" representation in SAC
|
||||
|
||||
See details in [Data Binding Limitations](#data-binding-limitations) below.
|
||||
|
||||
### Custom Widgets for Planning
|
||||
|
||||
Custom widgets can support SAP Analytics Cloud Planning scenarios:
|
||||
|
||||
**Use Cases**:
|
||||
- Custom input controls
|
||||
- Specialized data entry forms
|
||||
- Planning workflow visualization
|
||||
- Custom approval interfaces
|
||||
|
||||
### Data Binding Limitations
|
||||
|
||||
**Known Limitations**:
|
||||
|
||||
1. **Builder Panel + Data Binding Conflict**:
|
||||
- Cannot combine custom Builder Panel with data binding
|
||||
- Builder Panel overrides data binding functionality
|
||||
- Choose one approach per widget
|
||||
|
||||
2. **Hierarchies Not Supported**:
|
||||
- Data binding works with flat data only
|
||||
- Select "flat" representation in SAC properties
|
||||
- Hierarchical dimensions require alternative approach
|
||||
|
||||
### Planning API Integration
|
||||
|
||||
**Available through Script**:
|
||||
```javascript
|
||||
// In SAC script
|
||||
var ds = Table_1.getDataSource();
|
||||
|
||||
// Planning operations (via DataSource)
|
||||
ds.setUserInput(selection, value); // Write data
|
||||
ds.submitData(); // Commit changes
|
||||
ds.revertData(); // Rollback
|
||||
```
|
||||
|
||||
**Custom Widget Access**:
|
||||
```javascript
|
||||
// Widget receives DataSource via method
|
||||
class PlanningWidget extends HTMLElement {
|
||||
async setDataSource(dataSource) {
|
||||
this._ds = dataSource;
|
||||
// Can now call dataSource methods
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Input Control Pattern
|
||||
|
||||
```javascript
|
||||
class CustomInputWidget extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._setupInputHandlers();
|
||||
}
|
||||
|
||||
_setupInputHandlers() {
|
||||
this._shadowRoot.getElementById("input").addEventListener("change", (e) => {
|
||||
// Fire event with new value
|
||||
this.dispatchEvent(new CustomEvent("onValueChange", {
|
||||
detail: { newValue: e.target.value }
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// SAC script handles the planning write-back
|
||||
CustomInputWidget_1.onValueChange = function() {
|
||||
var info = CustomInputWidget_1.getEventInfo();
|
||||
var selection = { "Account": "Forecast", "Time": "2024.Q1" };
|
||||
Table_1.getDataSource().setUserInput(selection, info.newValue);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Methods Reference
|
||||
|
||||
### DataSource Methods (via Script)
|
||||
|
||||
| Method | Description | Parameters |
|
||||
|--------|-------------|------------|
|
||||
| `getData(selection)` | Get data cell value | Selection object |
|
||||
| `getResultSet()` | Get current result set | None |
|
||||
| `getMembers(dimension)` | Get dimension members | Dimension name |
|
||||
| `getResultMember(dim, selection)` | Get member info | Dimension, Selection |
|
||||
| `getDimensionFilters(dimension)` | Get filter values | Dimension name |
|
||||
| `setDimensionFilter(dim, member)` | Set filter | Dimension, MemberInfo |
|
||||
| `removeDimensionFilter(dimension)` | Clear filter | Dimension name |
|
||||
|
||||
### Variable Methods
|
||||
|
||||
| Method | Description | Parameters |
|
||||
|--------|-------------|------------|
|
||||
| `setVariableValue(name, value)` | Set variable | Variable name, value |
|
||||
| `getVariableValues()` | Get all variables | None |
|
||||
|
||||
**Performance Tip**: Group `setVariableValue()` calls together for automatic request merging.
|
||||
|
||||
### Custom Widget Data Binding Methods
|
||||
|
||||
```javascript
|
||||
// Access data binding
|
||||
const binding = this.dataBindings.getDataBinding("myBinding");
|
||||
|
||||
// Get result set (async)
|
||||
const resultSet = await binding.getResultSet();
|
||||
|
||||
// Direct property access
|
||||
const data = this.myBinding.data;
|
||||
const metadata = this.myBinding.metadata;
|
||||
```
|
||||
|
||||
### Event Info Pattern
|
||||
|
||||
```javascript
|
||||
// In custom widget
|
||||
this.dispatchEvent(new CustomEvent("onSelect", {
|
||||
detail: {
|
||||
selectedId: "item-123",
|
||||
selectedValue: 100
|
||||
}
|
||||
}));
|
||||
|
||||
// In SAC script
|
||||
Widget_1.onSelect = function() {
|
||||
var info = Widget_1.getEventInfo();
|
||||
// info.selectedId = "item-123"
|
||||
// info.selectedValue = 100
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
### Before Transport
|
||||
|
||||
- [ ] Verify resource file hosting is globally accessible
|
||||
- [ ] Update URLs if changing hosting strategy
|
||||
- [ ] Regenerate integrity hashes if files changed
|
||||
- [ ] Test widget in source tenant
|
||||
- [ ] Document any script dependencies
|
||||
|
||||
### After Transport
|
||||
|
||||
- [ ] Verify widget loads in target tenant
|
||||
- [ ] Test all functionality
|
||||
- [ ] Check script object references still work
|
||||
- [ ] Verify data binding if applicable
|
||||
- [ ] Test in view mode (not just edit mode)
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Symptom | Likely Cause | Solution |
|
||||
|---------|--------------|----------|
|
||||
| Widget won't load | URL not accessible | Verify hosting, CORS |
|
||||
| Integrity warning | Hash mismatch | Regenerate hash |
|
||||
| Script errors | Missing script objects | Recreate in target |
|
||||
| No data | Data binding lost | Reconfigure binding |
|
||||
| Styling broken | CSS not loaded | Check styling panel config |
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Analytics Designer API Reference](https://help.sap.com/doc/958d4c11261f42e992e8d01a4c0dde25/release/en-US/index.html)
|
||||
- [Custom Widget Developer Guide](https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf)
|
||||
- [SAC Content Transport](https://community.sap.com/t5/technology-blog-posts-by-members/sac-content-transport-migration-using-ctms/ba-p/13742318)
|
||||
- [Optimized Story Experience](https://community.sap.com/t5/technology-blog-posts-by-sap/the-new-optimized-story-experience-the-unification-of-story-and-analytic/ba-p/13558887)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-22
|
||||
579
references/json-schema-reference.md
Normal file
579
references/json-schema-reference.md
Normal file
@@ -0,0 +1,579 @@
|
||||
# SAP SAC Custom Widget JSON Schema Reference
|
||||
|
||||
Complete reference for the JSON metadata file that defines custom widgets.
|
||||
|
||||
**Source**: [SAP Custom Widget Developer Guide](https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf) - Section 6.1
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Complete Schema Example](#complete-schema-example)
|
||||
2. [Root Object](#root-object)
|
||||
3. [Webcomponents Array](#webcomponents-array)
|
||||
4. [Properties Object](#properties-object)
|
||||
5. [Methods Object](#methods-object)
|
||||
6. [Events Object](#events-object)
|
||||
7. [DataBindings Object](#databindings-object)
|
||||
8. [Custom Types](#custom-types)
|
||||
|
||||
---
|
||||
|
||||
## Complete Schema Example
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "com.company.advancedwidget",
|
||||
"version": "1.0.0",
|
||||
"name": "Advanced Custom Widget",
|
||||
"description": "A feature-rich custom widget with data binding",
|
||||
"vendor": "Company Name",
|
||||
"license": "MIT",
|
||||
"icon": "[https://example.com/icon.png",](https://example.com/icon.png",)
|
||||
"newInstancePrefix": "advWidget",
|
||||
"webcomponents": [
|
||||
{
|
||||
"kind": "main",
|
||||
"tag": "advanced-widget",
|
||||
"url": "[https://host.com/widget.js",](https://host.com/widget.js",)
|
||||
"integrity": "sha256-abc123...",
|
||||
"ignoreIntegrity": false
|
||||
},
|
||||
{
|
||||
"kind": "styling",
|
||||
"tag": "advanced-widget-styling",
|
||||
"url": "[https://host.com/styling.js",](https://host.com/styling.js",)
|
||||
"integrity": "sha256-def456...",
|
||||
"ignoreIntegrity": false
|
||||
},
|
||||
{
|
||||
"kind": "builder",
|
||||
"tag": "advanced-widget-builder",
|
||||
"url": "[https://host.com/builder.js",](https://host.com/builder.js",)
|
||||
"integrity": "sha256-ghi789...",
|
||||
"ignoreIntegrity": false
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"default": "Widget Title",
|
||||
"description": "The widget title"
|
||||
},
|
||||
"value": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Numeric value"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enable/disable widget"
|
||||
},
|
||||
"color": {
|
||||
"type": "Color",
|
||||
"default": "#336699",
|
||||
"description": "Primary color"
|
||||
},
|
||||
"items": {
|
||||
"type": "string[]",
|
||||
"default": [],
|
||||
"description": "List of items"
|
||||
},
|
||||
"config": {
|
||||
"type": "Object<string>",
|
||||
"default": {},
|
||||
"description": "Configuration object"
|
||||
}
|
||||
},
|
||||
"methods": {
|
||||
"refresh": {
|
||||
"description": "Refresh widget data",
|
||||
"body": "this._refresh();"
|
||||
},
|
||||
"setValue": {
|
||||
"description": "Set the widget value",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "newValue",
|
||||
"type": "number",
|
||||
"description": "The new value"
|
||||
}
|
||||
],
|
||||
"body": "this._setValue(newValue);"
|
||||
},
|
||||
"getValue": {
|
||||
"description": "Get the current value",
|
||||
"returnType": "number",
|
||||
"body": "return this._getValue();"
|
||||
}
|
||||
},
|
||||
"events": {
|
||||
"onSelect": {
|
||||
"description": "Fired when an item is selected"
|
||||
},
|
||||
"onChange": {
|
||||
"description": "Fired when value changes"
|
||||
},
|
||||
"onLoad": {
|
||||
"description": "Fired when widget loads"
|
||||
}
|
||||
},
|
||||
"dataBindings": {
|
||||
"myData": {
|
||||
"feeds": [
|
||||
{
|
||||
"id": "dimensions",
|
||||
"description": "Dimensions",
|
||||
"type": "dimension"
|
||||
},
|
||||
{
|
||||
"id": "measures",
|
||||
"description": "Measures",
|
||||
"type": "mainStructureMember"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Root Object
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
|----------|------|----------|-------------|
|
||||
| `id` | string | **Yes** | Unique identifier using reverse domain notation (e.g., "com.company.widgetname") |
|
||||
| `version` | string | **Yes** | Semantic version (e.g., "1.0.0", "2.1.3") |
|
||||
| `name` | string | **Yes** | Display name shown in SAC widget panel |
|
||||
| `description` | string | No | Description shown in widget panel |
|
||||
| `vendor` | string | No | Developer or company name |
|
||||
| `license` | string | No | License type (MIT, Apache-2.0, proprietary) |
|
||||
| `icon` | string | No | URL to widget icon (recommended: 32x32 PNG) |
|
||||
| `newInstancePrefix` | string | No | Prefix for auto-generated script variable names |
|
||||
| `webcomponents` | array | **Yes** | Array of web component definitions |
|
||||
| `properties` | object | No | Widget properties accessible via script |
|
||||
| `methods` | object | No | Methods callable from script |
|
||||
| `events` | object | No | Events the widget can fire |
|
||||
| `dataBindings` | object | No | Data binding configuration |
|
||||
|
||||
### ID Best Practices
|
||||
|
||||
```
|
||||
com.company.widgetname # Standard format
|
||||
com.github.username.widget # GitHub-hosted
|
||||
sap.sample.widget # SAP samples only
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Webcomponents Array
|
||||
|
||||
Each widget can have up to three web components:
|
||||
|
||||
### Main Component (Required)
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "main",
|
||||
"tag": "my-widget",
|
||||
"url": "[https://host.com/widget.js",](https://host.com/widget.js",)
|
||||
"integrity": "sha256-abc123...",
|
||||
"ignoreIntegrity": false
|
||||
}
|
||||
```
|
||||
|
||||
### Styling Panel (Optional)
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "styling",
|
||||
"tag": "my-widget-styling",
|
||||
"url": "[https://host.com/styling.js",](https://host.com/styling.js",)
|
||||
"integrity": "sha256-def456...",
|
||||
"ignoreIntegrity": false
|
||||
}
|
||||
```
|
||||
|
||||
### Builder Panel (Optional)
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "builder",
|
||||
"tag": "my-widget-builder",
|
||||
"url": "[https://host.com/builder.js",](https://host.com/builder.js",)
|
||||
"integrity": "sha256-ghi789...",
|
||||
"ignoreIntegrity": false
|
||||
}
|
||||
```
|
||||
|
||||
### Webcomponent Properties
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
|----------|------|----------|-------------|
|
||||
| `kind` | string | **Yes** | "main", "styling", or "builder" |
|
||||
| `tag` | string | **Yes** | Custom element tag name (lowercase, hyphenated, must contain hyphen) |
|
||||
| `url` | string | **Yes** | URL to JavaScript file (HTTPS required for external hosting) |
|
||||
| `integrity` | string | No | SHA256 hash for subresource integrity |
|
||||
| `ignoreIntegrity` | boolean | No | Skip integrity check (development only, default: false) |
|
||||
|
||||
### Tag Naming Rules
|
||||
|
||||
- Must be lowercase
|
||||
- Must contain at least one hyphen (-)
|
||||
- Cannot start with a hyphen
|
||||
- Cannot use reserved names (like HTML elements)
|
||||
|
||||
**Valid**: `my-widget`, `company-chart-v2`, `data-grid-component`
|
||||
**Invalid**: `MyWidget`, `widget`, `my_widget`
|
||||
|
||||
---
|
||||
|
||||
## Properties Object
|
||||
|
||||
### Simple Types
|
||||
|
||||
```json
|
||||
{
|
||||
"stringProp": {
|
||||
"type": "string",
|
||||
"default": "default value",
|
||||
"description": "A string property"
|
||||
},
|
||||
"numberProp": {
|
||||
"type": "number",
|
||||
"default": 3.14,
|
||||
"description": "A floating-point number"
|
||||
},
|
||||
"integerProp": {
|
||||
"type": "integer",
|
||||
"default": 42,
|
||||
"description": "An integer"
|
||||
},
|
||||
"booleanProp": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "A boolean flag"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Array Types
|
||||
|
||||
```json
|
||||
{
|
||||
"stringArray": {
|
||||
"type": "string[]",
|
||||
"default": ["item1", "item2"],
|
||||
"description": "Array of strings"
|
||||
},
|
||||
"numberArray": {
|
||||
"type": "number[]",
|
||||
"default": [1, 2, 3],
|
||||
"description": "Array of numbers"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Object Types
|
||||
|
||||
```json
|
||||
{
|
||||
"objectProp": {
|
||||
"type": "Object<string>",
|
||||
"default": {},
|
||||
"description": "Object with string values"
|
||||
},
|
||||
"numberObject": {
|
||||
"type": "Object<number>",
|
||||
"default": { "a": 1, "b": 2 },
|
||||
"description": "Object with number values"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Script API Types
|
||||
|
||||
```json
|
||||
{
|
||||
"colorProp": {
|
||||
"type": "Color",
|
||||
"default": "#336699",
|
||||
"description": "Color value"
|
||||
},
|
||||
"selectionProp": {
|
||||
"type": "Selection",
|
||||
"default": {},
|
||||
"description": "Selection object"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: For detailed information on `Color` and `Selection` types, including their JavaScript usage patterns and structure, see [Script API Data Types](advanced-topics.md#script-api-data-types) in Advanced Topics.
|
||||
|
||||
### Property Configuration
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `type` | string | **Yes** | Data type |
|
||||
| `default` | varies | **Yes** | Default value matching type |
|
||||
| `description` | string | No | Description for documentation |
|
||||
|
||||
---
|
||||
|
||||
## Methods Object
|
||||
|
||||
Methods allow scripts to call functions on the widget.
|
||||
|
||||
### Method Without Parameters
|
||||
|
||||
```json
|
||||
{
|
||||
"refresh": {
|
||||
"description": "Refresh the widget",
|
||||
"body": "this._refresh();"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **⚠️ Important**: The `body` must call an internal method (prefixed with `_`) to avoid infinite recursion. When SAC invokes `Widget.refresh()`, it executes the body code. If the body calls `this.refresh()`, it would recursively call itself. Always use `this._refresh()` pattern.
|
||||
|
||||
### Method With Parameters
|
||||
|
||||
```json
|
||||
{
|
||||
"setTitle": {
|
||||
"description": "Set the widget title",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "newTitle",
|
||||
"type": "string",
|
||||
"description": "The new title text"
|
||||
}
|
||||
],
|
||||
"body": "this._setTitle(newTitle);"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Method With Return Value
|
||||
|
||||
```json
|
||||
{
|
||||
"getTotal": {
|
||||
"description": "Get the total value",
|
||||
"returnType": "number",
|
||||
"body": "return this._getTotal();"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Method With Multiple Parameters
|
||||
|
||||
```json
|
||||
{
|
||||
"configure": {
|
||||
"description": "Configure the widget",
|
||||
"parameters": [
|
||||
{ "name": "width", "type": "integer", "description": "Width in pixels" },
|
||||
{ "name": "height", "type": "integer", "description": "Height in pixels" },
|
||||
{ "name": "title", "type": "string", "description": "Title text" }
|
||||
],
|
||||
"body": "this._configure(width, height, title);"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Method Configuration
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `description` | string | No | Method description |
|
||||
| `parameters` | array | No | Array of parameter definitions |
|
||||
| `returnType` | string | No | Return type (if method returns value) |
|
||||
| `body` | string | **Yes** | JavaScript code to execute |
|
||||
|
||||
---
|
||||
|
||||
## Events Object
|
||||
|
||||
Events allow the widget to notify scripts of user interactions or state changes.
|
||||
|
||||
### Basic Event
|
||||
|
||||
```json
|
||||
{
|
||||
"onSelect": {
|
||||
"description": "Fired when an item is selected"
|
||||
},
|
||||
"onClick": {
|
||||
"description": "Fired when widget is clicked"
|
||||
},
|
||||
"onDataChange": {
|
||||
"description": "Fired when data changes"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Firing Events in Web Component
|
||||
|
||||
```javascript
|
||||
// Simple event
|
||||
this.dispatchEvent(new Event("onSelect"));
|
||||
|
||||
// Event with data (accessible via getEventInfo in script)
|
||||
this.dispatchEvent(new CustomEvent("onSelect", {
|
||||
detail: {
|
||||
selectedItem: "item1",
|
||||
selectedIndex: 0
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
### Script Event Handler
|
||||
|
||||
In Analytics Designer script:
|
||||
```javascript
|
||||
// Event handler
|
||||
Widget_1.onSelect = function() {
|
||||
console.log("Item selected");
|
||||
// Access event data if provided
|
||||
var eventInfo = Widget_1.getEventInfo();
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DataBindings Object
|
||||
|
||||
Enable widgets to receive data from SAC models.
|
||||
|
||||
### Basic Data Binding
|
||||
|
||||
```json
|
||||
{
|
||||
"dataBindings": {
|
||||
"myBinding": {
|
||||
"feeds": [
|
||||
{
|
||||
"id": "dimensions",
|
||||
"description": "Dimensions",
|
||||
"type": "dimension"
|
||||
},
|
||||
{
|
||||
"id": "measures",
|
||||
"description": "Measures",
|
||||
"type": "mainStructureMember"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Feed Types
|
||||
|
||||
| Type | Description | Use Case |
|
||||
|------|-------------|----------|
|
||||
| `dimension` | Dimension members | Categories, labels, hierarchies |
|
||||
| `mainStructureMember` | Measures/KPIs | Numeric values, calculations |
|
||||
|
||||
### Feed Configuration
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `id` | string | **Yes** | Unique identifier for the feed |
|
||||
| `description` | string | **Yes** | Display name in Builder Panel |
|
||||
| `type` | string | **Yes** | "dimension" or "mainStructureMember" |
|
||||
|
||||
### Multiple Feeds Example
|
||||
|
||||
```json
|
||||
{
|
||||
"dataBindings": {
|
||||
"chartData": {
|
||||
"feeds": [
|
||||
{ "id": "category", "description": "Category", "type": "dimension" },
|
||||
{ "id": "series", "description": "Series", "type": "dimension" },
|
||||
{ "id": "value", "description": "Value", "type": "mainStructureMember" },
|
||||
{ "id": "target", "description": "Target", "type": "mainStructureMember" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Accessing Data in JavaScript
|
||||
|
||||
```javascript
|
||||
// Access via property
|
||||
const data = this.chartData.data;
|
||||
const metadata = this.chartData.metadata;
|
||||
|
||||
// Iterate rows
|
||||
this.chartData.data.forEach(row => {
|
||||
const category = row.category_0.label;
|
||||
const value = row.value_0.raw;
|
||||
console.log(`${category}: ${value}`);
|
||||
});
|
||||
|
||||
// Via getDataBinding method
|
||||
const binding = this.dataBindings.getDataBinding("chartData");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Types
|
||||
|
||||
Define reusable complex types for properties.
|
||||
|
||||
### Defining Custom Type
|
||||
|
||||
```json
|
||||
{
|
||||
"types": {
|
||||
"ChartConfig": {
|
||||
"properties": {
|
||||
"chartType": { "type": "string", "default": "bar" },
|
||||
"showLegend": { "type": "boolean", "default": true },
|
||||
"colors": { "type": "string[]", "default": [] }
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"chartConfiguration": {
|
||||
"type": "ChartConfig",
|
||||
"default": {
|
||||
"chartType": "bar",
|
||||
"showLegend": true,
|
||||
"colors": ["#336699", "#669933"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before deploying, verify your JSON:
|
||||
|
||||
- [ ] `id` follows reverse domain notation
|
||||
- [ ] `version` is semantic version format
|
||||
- [ ] `name` is concise and descriptive
|
||||
- [ ] All `webcomponents` have valid `tag` names (lowercase, hyphenated)
|
||||
- [ ] All URLs are HTTPS (for external hosting)
|
||||
- [ ] All `properties` have `type` and `default`
|
||||
- [ ] All `methods` have `body`
|
||||
- [ ] `integrity` is set for production (or explicitly `ignoreIntegrity: true` for dev)
|
||||
- [ ] `dataBindings` feeds have unique `id` values
|
||||
|
||||
---
|
||||
|
||||
**Source Documentation**:
|
||||
- [SAP Custom Widget Developer Guide (PDF)](https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf)
|
||||
- [SAP Help Portal - Custom Widgets](https://help.sap.com/docs/SAP_ANALYTICS_CLOUD/0ac8c6754ff84605a4372468d002f2bf/75311f67527c41638ceb89af9cd8af3e.html)
|
||||
|
||||
**Last Updated**: 2025-11-22
|
||||
831
references/script-api-reference.md
Normal file
831
references/script-api-reference.md
Normal file
@@ -0,0 +1,831 @@
|
||||
# SAP SAC Script API Reference for Custom Widgets
|
||||
|
||||
Comprehensive reference for Analytics Designer and Optimized Story Experience Script APIs
|
||||
relevant to custom widget development.
|
||||
|
||||
**Sources**:
|
||||
- [Analytics Designer API Reference Guide](https://help.sap.com/doc/958d4c11261f42e992e8d01a4c0dde25/release/en-US/index.html)
|
||||
- [Optimized Story Experience API Reference Guide](https://help.sap.com/doc/1639cb9ccaa54b2592224df577abe822/release/en-US/index.html)
|
||||
- [Custom Widget Developer Guide (PDF)](https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf)
|
||||
|
||||
> **Note**: These documentation links point to the latest release version. Version-specific
|
||||
> documentation may be available under versioned pages in the SAP Help Portal.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [DataSource Object](#datasource-object)
|
||||
2. [Selection Type](#selection-type)
|
||||
3. [MemberInfo Object](#memberinfo-object)
|
||||
4. [ResultMemberInfo Object](#resultmemberinfo-object)
|
||||
5. [ResultSet APIs](#resultset-apis)
|
||||
6. [DataBinding Object](#databinding-object)
|
||||
7. [Filter APIs](#filter-apis)
|
||||
8. [Planning APIs](#planning-apis)
|
||||
9. [Variable APIs](#variable-apis)
|
||||
10. [Event Handling](#event-handling)
|
||||
|
||||
---
|
||||
|
||||
## DataSource Object
|
||||
|
||||
The DataSource object provides access to data model information and operations.
|
||||
|
||||
### Getting DataSource
|
||||
|
||||
```javascript
|
||||
// In SAC Script
|
||||
var ds = Table_1.getDataSource();
|
||||
var ds = Chart_1.getDataSource();
|
||||
|
||||
// In custom widget (via script method)
|
||||
Widget_1.getDataSource(); // If widget has data binding
|
||||
```
|
||||
|
||||
### DataSource Methods
|
||||
|
||||
#### getResultSet()
|
||||
|
||||
Returns result data based on optional parameters.
|
||||
|
||||
```javascript
|
||||
// Signature
|
||||
getResultSet(options?: Object): Array<Object>
|
||||
|
||||
// Options object can include:
|
||||
// - selection: Object - data selection context
|
||||
// - offset: number - starting row index
|
||||
// - limit: number - maximum rows to return
|
||||
|
||||
// Examples
|
||||
var allData = ds.getResultSet();
|
||||
var filteredData = ds.getResultSet({ selection: { "Year": "2024" } });
|
||||
var pagedData = ds.getResultSet({ offset: 0, limit: 100 }); // First 100 rows
|
||||
```
|
||||
|
||||
**Return Value**: Array of result objects containing:
|
||||
- Dimension member info (id, description, parentId)
|
||||
- Measure values (raw, formatted, unit)
|
||||
|
||||
#### getResultMember()
|
||||
|
||||
Returns member information for a specific selection.
|
||||
|
||||
```javascript
|
||||
// Signature
|
||||
getResultMember(
|
||||
dimensionId: string,
|
||||
selection: Object
|
||||
): Object | undefined
|
||||
|
||||
// Example
|
||||
var memberInfo = ds.getResultMember("Account", { "Account": "Revenue" });
|
||||
console.log(memberInfo.description); // "Revenue"
|
||||
console.log(memberInfo.id); // "REVENUE"
|
||||
```
|
||||
|
||||
#### getMembers()
|
||||
|
||||
Retrieves dimension members.
|
||||
|
||||
```javascript
|
||||
// Signature
|
||||
getMembers(
|
||||
dimensionId: string,
|
||||
maxNumber?: number
|
||||
): Array<Object>
|
||||
|
||||
// Examples
|
||||
var allMembers = ds.getMembers("Account");
|
||||
var limitedMembers = ds.getMembers("Account", 100); // Max 100 members
|
||||
```
|
||||
|
||||
**Note**: Using getMembers() causes a backend roundtrip. For performance, prefer
|
||||
getResultSet() when possible as it doesn't require additional network calls.
|
||||
|
||||
#### getMember()
|
||||
|
||||
Returns information for a specific member.
|
||||
|
||||
```javascript
|
||||
// Signature
|
||||
getMember(
|
||||
dimensionId: string,
|
||||
memberId: string
|
||||
): Object
|
||||
|
||||
// Example
|
||||
var member = ds.getMember("Account", "REVENUE");
|
||||
```
|
||||
|
||||
#### getData()
|
||||
|
||||
Gets the raw data value for a selection.
|
||||
|
||||
```javascript
|
||||
// Signature
|
||||
getData(selection?: Object): number | null
|
||||
|
||||
// Examples
|
||||
var currentValue = ds.getData(); // Uses current selection context
|
||||
var specificValue = ds.getData({
|
||||
"Account": "Revenue",
|
||||
"Year": "2024"
|
||||
});
|
||||
```
|
||||
|
||||
#### getDimensions()
|
||||
|
||||
Returns available dimensions in the data source.
|
||||
|
||||
```javascript
|
||||
// Signature
|
||||
getDimensions(): Array<Object>
|
||||
|
||||
// Example
|
||||
var dimensions = ds.getDimensions();
|
||||
dimensions.forEach(dim => {
|
||||
console.log(dim.id, dim.description);
|
||||
});
|
||||
```
|
||||
|
||||
#### getMeasures() / getMainStructureMembers()
|
||||
|
||||
Returns available measures.
|
||||
|
||||
```javascript
|
||||
// Signature
|
||||
getMeasures(): Array<Object>
|
||||
|
||||
// Example
|
||||
var measures = ds.getMeasures();
|
||||
measures.forEach(m => {
|
||||
console.log(m.id, m.description, m.unitOfMeasure);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Selection Type
|
||||
|
||||
Selection objects define data context for operations.
|
||||
|
||||
### Selection Structure
|
||||
|
||||
```javascript
|
||||
// Simple selection
|
||||
var selection = {
|
||||
"Account": "Revenue",
|
||||
"Year": "2024"
|
||||
};
|
||||
|
||||
// Selection with multiple values
|
||||
var multiSelection = {
|
||||
"Account": ["Revenue", "Cost"],
|
||||
"Year": "2024"
|
||||
};
|
||||
|
||||
// Selection with hierarchy
|
||||
var hierarchySelection = {
|
||||
"Account": {
|
||||
id: "Revenue",
|
||||
hierarchyId: "H1"
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Using Selection in JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"currentSelection": {
|
||||
"type": "Selection",
|
||||
"default": {},
|
||||
"description": "Current data selection context"
|
||||
}
|
||||
},
|
||||
"methods": {
|
||||
"setSelection": {
|
||||
"description": "Apply a data selection",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "selection",
|
||||
"type": "Selection",
|
||||
"description": "Selection to apply"
|
||||
}
|
||||
],
|
||||
"body": "this._setSelection(selection);"
|
||||
},
|
||||
"getSelection": {
|
||||
"description": "Get current selection",
|
||||
"returnType": "Selection",
|
||||
"body": "return this._getSelection();"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Selection in Web Component
|
||||
|
||||
```javascript
|
||||
class DataWidget extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._selection = {};
|
||||
}
|
||||
|
||||
// Method called from SAC script
|
||||
setSelection(selection) {
|
||||
this._selection = selection;
|
||||
this._render();
|
||||
}
|
||||
|
||||
getSelection() {
|
||||
return this._selection;
|
||||
}
|
||||
|
||||
_render() {
|
||||
// Use selection to filter/highlight data
|
||||
console.log("Current selection:", this._selection);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MemberInfo Object
|
||||
|
||||
Represents dimension member information.
|
||||
|
||||
### Structure
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: string, // Technical member ID (e.g., "REVENUE")
|
||||
description: string, // Display name (e.g., "Revenue")
|
||||
displayId: string, // Display ID
|
||||
dimensionId: string, // Parent dimension ID
|
||||
modelId: string, // Data model ID
|
||||
parentId?: string, // Parent member ID (hierarchies)
|
||||
hierarchyId?: string, // Hierarchy ID (if applicable)
|
||||
level?: integer, // Hierarchy level
|
||||
isNode?: boolean, // Is hierarchy node (has children)
|
||||
properties?: object // Additional attributes
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
When using setDimensionFilter(), passing a MemberInfo object instead of just
|
||||
a member ID string prevents a backend roundtrip:
|
||||
|
||||
```javascript
|
||||
// Slower - causes roundtrip to fetch description
|
||||
ds.setDimensionFilter("Account", "REVENUE");
|
||||
|
||||
// Faster - no roundtrip, MemberInfo already has description
|
||||
ds.setDimensionFilter("Account", {
|
||||
id: "REVENUE",
|
||||
description: "Revenue"
|
||||
});
|
||||
```
|
||||
|
||||
### Accessing Member Properties
|
||||
|
||||
```javascript
|
||||
// Get member with attributes
|
||||
var member = ds.getMember("Product", "P001");
|
||||
|
||||
// Access properties (if dimension has attributes)
|
||||
if (member.properties) {
|
||||
console.log("Category:", member.properties.Category);
|
||||
console.log("Brand:", member.properties.Brand);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ResultMemberInfo Object
|
||||
|
||||
Extended member information from result sets.
|
||||
|
||||
### Structure
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: string, // Member ID
|
||||
description: string, // Display name
|
||||
parentId?: string, // Parent ID for hierarchies
|
||||
formattedValue?: string, // Formatted display value
|
||||
unitOfMeasure?: string, // Unit (for measures)
|
||||
raw?: number, // Raw numeric value (for measures)
|
||||
properties?: {
|
||||
// Dimension attributes
|
||||
[attributeName: string]: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Accessing in Result Set
|
||||
|
||||
```javascript
|
||||
var resultSet = ds.getResultSet();
|
||||
|
||||
resultSet.forEach(row => {
|
||||
// Access dimension member info
|
||||
var accountMember = row["Account"];
|
||||
console.log("Account ID:", accountMember.id);
|
||||
console.log("Account Name:", accountMember.description);
|
||||
|
||||
// Access measure value
|
||||
var revenue = row["Revenue"];
|
||||
console.log("Value:", revenue.raw);
|
||||
console.log("Formatted:", revenue.formattedValue);
|
||||
console.log("Unit:", revenue.unitOfMeasure);
|
||||
});
|
||||
```
|
||||
|
||||
### Note on Visibility
|
||||
|
||||
Only visible properties in the widget configuration are included in the
|
||||
ResultMemberInfo object. Hidden dimensions/measures won't appear.
|
||||
|
||||
---
|
||||
|
||||
## ResultSet APIs
|
||||
|
||||
### getResultSet() Deep Dive
|
||||
|
||||
```javascript
|
||||
// Full signature
|
||||
getResultSet(
|
||||
selection?: Selection | Selection[] | SelectionContext,
|
||||
offset?: integer,
|
||||
limit?: integer
|
||||
): ResultSet[]
|
||||
```
|
||||
|
||||
#### Pagination
|
||||
|
||||
```javascript
|
||||
// Get first page (100 rows)
|
||||
var page1 = ds.getResultSet(null, 0, 100);
|
||||
|
||||
// Get second page
|
||||
var page2 = ds.getResultSet(null, 100, 100);
|
||||
|
||||
// Count total rows
|
||||
var total = ds.getResultSetCount();
|
||||
```
|
||||
|
||||
#### Filtering
|
||||
|
||||
```javascript
|
||||
// Single filter
|
||||
var filtered = ds.getResultSet({ "Year": "2024" });
|
||||
|
||||
// Multiple filters
|
||||
var filtered = ds.getResultSet({
|
||||
"Year": "2024",
|
||||
"Region": "EMEA"
|
||||
});
|
||||
|
||||
// Array of selections (OR logic)
|
||||
var filtered = ds.getResultSet([
|
||||
{ "Year": "2024" },
|
||||
{ "Year": "2023" }
|
||||
]);
|
||||
```
|
||||
|
||||
### Processing Result Sets
|
||||
|
||||
```javascript
|
||||
function processResults(resultSet) {
|
||||
var chartData = [];
|
||||
|
||||
resultSet.forEach(row => {
|
||||
// Dynamically access columns based on data binding
|
||||
var dimensions = Object.keys(row).filter(k => row[k].id !== undefined);
|
||||
var measures = Object.keys(row).filter(k => row[k].raw !== undefined);
|
||||
|
||||
chartData.push({
|
||||
category: row[dimensions[0]]?.description || "Unknown",
|
||||
value: row[measures[0]]?.raw || 0
|
||||
});
|
||||
});
|
||||
|
||||
return chartData;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DataBinding Object
|
||||
|
||||
Custom widget data binding API.
|
||||
|
||||
### Getting DataBinding
|
||||
|
||||
```javascript
|
||||
// In custom widget web component
|
||||
const binding = this.dataBindings.getDataBinding("myDataBinding");
|
||||
```
|
||||
|
||||
### DataBinding Methods
|
||||
|
||||
#### getResultSet()
|
||||
|
||||
```javascript
|
||||
// Async - returns Promise
|
||||
const resultSet = await binding.getResultSet();
|
||||
```
|
||||
|
||||
#### getMembers()
|
||||
|
||||
```javascript
|
||||
// Get dimension members
|
||||
const members = await binding.getMembers("DimensionId");
|
||||
```
|
||||
|
||||
#### addDimensionToFeed()
|
||||
|
||||
```javascript
|
||||
// Programmatically add dimension to feed
|
||||
await binding.addDimensionToFeed("dimensions", "Account");
|
||||
```
|
||||
|
||||
#### addMeasureToFeed()
|
||||
|
||||
```javascript
|
||||
// Add measure to feed
|
||||
await binding.addMeasureToFeed("measures", "Revenue");
|
||||
```
|
||||
|
||||
#### removeDimensionFromFeed()
|
||||
|
||||
```javascript
|
||||
await binding.removeDimensionFromFeed("dimensions", "Account");
|
||||
```
|
||||
|
||||
### Accessing Bound Data Directly
|
||||
|
||||
```javascript
|
||||
// Direct property access (name from JSON dataBindings)
|
||||
const data = this.myDataBinding;
|
||||
|
||||
// Structure
|
||||
{
|
||||
data: ResultSet[], // Array of result rows
|
||||
metadata: {
|
||||
dimensions: {}, // Dimension info
|
||||
mainStructureMembers: {} // Measure info
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Data Binding Example
|
||||
|
||||
```javascript
|
||||
class DataBoundWidget extends HTMLElement {
|
||||
onCustomWidgetAfterUpdate(changedProperties) {
|
||||
this._processData();
|
||||
}
|
||||
|
||||
_processData() {
|
||||
// Access data binding by name defined in JSON
|
||||
const binding = this.chartData;
|
||||
if (!binding || !binding.data) {
|
||||
this._showNoData();
|
||||
return;
|
||||
}
|
||||
|
||||
// Process rows
|
||||
const chartData = binding.data.map(row => {
|
||||
// Access dimension (first feed)
|
||||
const categoryKey = Object.keys(row).find(k =>
|
||||
binding.metadata.dimensions[k]);
|
||||
const valueKey = Object.keys(row).find(k =>
|
||||
binding.metadata.mainStructureMembers[k]);
|
||||
|
||||
return {
|
||||
label: row[categoryKey]?.description || "",
|
||||
value: row[valueKey]?.raw || 0
|
||||
};
|
||||
});
|
||||
|
||||
this._renderChart(chartData);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Filter APIs
|
||||
|
||||
### setDimensionFilter()
|
||||
|
||||
```javascript
|
||||
// Set single filter
|
||||
ds.setDimensionFilter("Year", "2024");
|
||||
|
||||
// Set filter with MemberInfo (avoids roundtrip)
|
||||
ds.setDimensionFilter("Year", {
|
||||
id: "2024",
|
||||
description: "Year 2024"
|
||||
});
|
||||
|
||||
// Multiple values
|
||||
ds.setDimensionFilter("Year", ["2023", "2024"]);
|
||||
|
||||
// Clear filter
|
||||
ds.removeDimensionFilter("Year");
|
||||
```
|
||||
|
||||
### setVariableValue()
|
||||
|
||||
```javascript
|
||||
// Set planning variable
|
||||
ds.setVariableValue("VAR_YEAR", "2024");
|
||||
|
||||
// Multiple values
|
||||
ds.setVariableValue("VAR_REGION", ["EMEA", "AMER"]);
|
||||
```
|
||||
|
||||
### Filter Synchronization
|
||||
|
||||
```javascript
|
||||
// Apply filters and refresh data
|
||||
ds.setDimensionFilter("Year", "2024");
|
||||
ds.setDimensionFilter("Region", "EMEA");
|
||||
|
||||
// Refresh to apply (may be automatic depending on widget)
|
||||
ds.refresh();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Planning APIs
|
||||
|
||||
For widgets supporting SAP Analytics Cloud Planning.
|
||||
|
||||
> **⚠️ Important**: Planning APIs are synchronous and return boolean success values.
|
||||
> Always check the return value to handle errors appropriately.
|
||||
|
||||
### Write-back Methods
|
||||
|
||||
```javascript
|
||||
// Set user input (planning)
|
||||
ds.setUserInput(selection, value);
|
||||
|
||||
// Example
|
||||
ds.setUserInput({
|
||||
"Account": "Forecast",
|
||||
"Year": "2025",
|
||||
"Month": "Jan"
|
||||
}, 100000);
|
||||
|
||||
// Submit changes (synchronous, returns boolean)
|
||||
var success = ds.submitData();
|
||||
if (!success) {
|
||||
console.error("Submit failed");
|
||||
}
|
||||
|
||||
// Revert changes using Planning Version
|
||||
var planningVersion = ds.getPlanningVersion();
|
||||
planningVersion.revert();
|
||||
```
|
||||
|
||||
### Planning Workflow
|
||||
|
||||
```javascript
|
||||
class PlanningWidget extends HTMLElement {
|
||||
saveData(entries) {
|
||||
const ds = this._dataSource;
|
||||
|
||||
// Apply all inputs
|
||||
for (const entry of entries) {
|
||||
ds.setUserInput(entry.selection, entry.value);
|
||||
}
|
||||
|
||||
// Submit to backend (synchronous)
|
||||
var success = ds.submitData();
|
||||
|
||||
if (success) {
|
||||
this._showSuccess("Data saved");
|
||||
} else {
|
||||
// Rollback on error using Planning Version
|
||||
var planningVersion = ds.getPlanningVersion();
|
||||
if (planningVersion) {
|
||||
planningVersion.revert();
|
||||
}
|
||||
this._showError("Save failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Data Locking
|
||||
|
||||
```javascript
|
||||
// Get data locking interface
|
||||
var dataLocking = ds.getDataLocking();
|
||||
|
||||
// Check lock status
|
||||
var isLocked = dataLocking.isLocked();
|
||||
|
||||
// Set lock state (returns boolean)
|
||||
var lockSuccess = dataLocking.setState(true); // Lock
|
||||
if (!lockSuccess) {
|
||||
console.error("Failed to acquire lock");
|
||||
}
|
||||
|
||||
// Release lock
|
||||
var unlockSuccess = dataLocking.setState(false); // Unlock
|
||||
if (!unlockSuccess) {
|
||||
console.error("Failed to release lock");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Variable APIs
|
||||
|
||||
### Input Variable Methods
|
||||
|
||||
```javascript
|
||||
// Get variable value
|
||||
var year = ds.getVariableValue("VAR_YEAR");
|
||||
|
||||
// Set variable value
|
||||
ds.setVariableValue("VAR_YEAR", "2024");
|
||||
|
||||
// Get available values
|
||||
var values = ds.getVariableValues("VAR_YEAR");
|
||||
```
|
||||
|
||||
### Variable Information
|
||||
|
||||
```javascript
|
||||
// Get variable details
|
||||
var varInfo = ds.getVariable("VAR_YEAR");
|
||||
console.log(varInfo.description); // "Fiscal Year"
|
||||
console.log(varInfo.mandatory); // true/false
|
||||
console.log(varInfo.multiValue); // true/false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Handling
|
||||
|
||||
### Widget Events in JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"events": {
|
||||
"onSelect": {
|
||||
"description": "Fired when item is selected"
|
||||
},
|
||||
"onDataChange": {
|
||||
"description": "Fired when data changes"
|
||||
},
|
||||
"onError": {
|
||||
"description": "Fired on error"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Firing Events
|
||||
|
||||
```javascript
|
||||
// Simple event
|
||||
this.dispatchEvent(new Event("onSelect"));
|
||||
|
||||
// Event with data
|
||||
this.dispatchEvent(new CustomEvent("onSelect", {
|
||||
detail: {
|
||||
selection: this._currentSelection,
|
||||
value: this._selectedValue
|
||||
}
|
||||
}));
|
||||
|
||||
// Error event
|
||||
this.dispatchEvent(new CustomEvent("onError", {
|
||||
detail: {
|
||||
message: "Failed to load data",
|
||||
code: "DATA_ERROR"
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
### Handling in SAC Script
|
||||
|
||||
```javascript
|
||||
// Event handler in SAC script
|
||||
Widget_1.onSelect = function(event) {
|
||||
var info = Widget_1.getEventInfo();
|
||||
console.log("Selected:", info.selection);
|
||||
console.log("Value:", info.value);
|
||||
|
||||
// Update other widgets
|
||||
Table_1.getDataSource().setDimensionFilter("Account", info.selection.Account);
|
||||
};
|
||||
|
||||
Widget_1.onError = function(event) {
|
||||
var info = Widget_1.getEventInfo();
|
||||
Application.showMessage(ApplicationMessageType.Error, info.message);
|
||||
};
|
||||
```
|
||||
|
||||
### Data-Driven Events
|
||||
|
||||
```javascript
|
||||
class InteractiveWidget extends HTMLElement {
|
||||
_handleClick(item) {
|
||||
// Build selection from clicked item
|
||||
const selection = {
|
||||
[this._dimensionId]: item.id
|
||||
};
|
||||
|
||||
// Store for getEventInfo
|
||||
this._lastEvent = {
|
||||
selection: selection,
|
||||
label: item.label,
|
||||
value: item.value
|
||||
};
|
||||
|
||||
// Fire event
|
||||
this.dispatchEvent(new Event("onSelect"));
|
||||
}
|
||||
|
||||
// Called by SAC script via getEventInfo()
|
||||
getEventInfo() {
|
||||
return this._lastEvent || {};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Async Data Loading
|
||||
|
||||
```javascript
|
||||
class AsyncWidget extends HTMLElement {
|
||||
async connectedCallback() {
|
||||
this._showLoading();
|
||||
|
||||
try {
|
||||
const binding = this.dataBindings.getDataBinding("myData");
|
||||
const resultSet = await binding.getResultSet();
|
||||
this._renderData(resultSet);
|
||||
} catch (error) {
|
||||
this._showError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
_showLoading() {
|
||||
this._shadowRoot.innerHTML = '<div class="loading">Loading...</div>';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Selection Synchronization
|
||||
|
||||
```javascript
|
||||
class SyncWidget extends HTMLElement {
|
||||
// Apply external selection
|
||||
setSelection(selection) {
|
||||
this._selection = selection;
|
||||
this._highlightSelection();
|
||||
}
|
||||
|
||||
// Notify of internal selection
|
||||
_onItemClick(item) {
|
||||
this._selection = { [this._dimId]: item.id };
|
||||
|
||||
this.dispatchEvent(new CustomEvent("propertiesChanged", {
|
||||
detail: { properties: { currentSelection: this._selection } }
|
||||
}));
|
||||
|
||||
this.dispatchEvent(new Event("onSelect"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Analytics Designer API Reference Guide](https://help.sap.com/doc/958d4c11261f42e992e8d01a4c0dde25/release/en-US/index.html)
|
||||
- [Optimized Story Experience API Reference Guide](https://help.sap.com/doc/1639cb9ccaa54b2592224df577abe822/release/en-US/index.html)
|
||||
- [Custom Widget Developer Guide (PDF)](https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf)
|
||||
- [Use Result Set APIs](https://help.sap.com/docs/SAP_ANALYTICS_CLOUD/00f68c2e08b941f081002fd3691d86a7/834786949212459caabe3a3d13f0aaa9.html)
|
||||
- [SAP Help Portal - Custom Widgets](https://help.sap.com/docs/SAP_ANALYTICS_CLOUD/0ac8c6754ff84605a4372468d002f2bf/75311f67527c41638ceb89af9cd8af3e.html)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-22
|
||||
415
references/widget-addon-guide.md
Normal file
415
references/widget-addon-guide.md
Normal file
@@ -0,0 +1,415 @@
|
||||
# SAP SAC Widget Add-On Development Guide
|
||||
|
||||
Widget Add-Ons extend built-in SAC widgets without building from scratch.
|
||||
|
||||
**Available Since**: QRC Q4 2023
|
||||
**Source**: [Announcing Widget Add-On](https://community.sap.com/t5/technology-blog-posts-by-sap/announcing-the-new-sap-analytics-cloud-feature-widget-add-on/ba-p/13575788)
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Widget Add-On vs Custom Widget](#widget-add-on-vs-custom-widget)
|
||||
3. [Supported Chart Types](#supported-chart-types)
|
||||
4. [JSON Structure](#json-structure)
|
||||
5. [Implementation Examples](#implementation-examples)
|
||||
6. [Using Widget Add-Ons](#using-widget-add-ons)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Widget Add-Ons allow customization of SAC's built-in widgets:
|
||||
- Add visual elements to charts
|
||||
- Modify tooltip contents
|
||||
- Override existing styling
|
||||
- Extend plot area rendering
|
||||
|
||||
**Key Benefit**: Leverage SAC's native visualizations with custom enhancements without building widgets from scratch.
|
||||
|
||||
---
|
||||
|
||||
## Widget Add-On vs Custom Widget
|
||||
|
||||
| Aspect | Custom Widget | Widget Add-On |
|
||||
|--------|---------------|---------------|
|
||||
| Purpose | Create entirely new widgets | Extend built-in widgets |
|
||||
| Web Components | Creates new widget | Adds/replaces parts of existing |
|
||||
| Component Types | main, styling, builder | main, builder only |
|
||||
| Use Case | Custom chart types, input controls | Tooltip customization, plot styling |
|
||||
| Complexity | Higher | Lower |
|
||||
|
||||
---
|
||||
|
||||
## Supported Chart Types
|
||||
|
||||
### Tooltip Customization
|
||||
- All chart types **except** numeric point
|
||||
|
||||
### Plot Area (General)
|
||||
- Bar/Column charts
|
||||
- Stacked Bar/Column charts
|
||||
- Stacked Area charts
|
||||
- Line charts
|
||||
|
||||
### Plot Area (Numeric Point)
|
||||
- Numeric Point only
|
||||
|
||||
---
|
||||
|
||||
## JSON Structure
|
||||
|
||||
### widget-addon.json
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "com.company.mywidgetaddon",
|
||||
"version": "1.0.0",
|
||||
"name": "My Widget Add-On",
|
||||
"description": "Customizes chart tooltips",
|
||||
"vendor": "Company Name",
|
||||
"license": "MIT",
|
||||
"icon": "",
|
||||
"webcomponents": [
|
||||
{
|
||||
"kind": "main",
|
||||
"tag": "my-addon-main",
|
||||
"url": "[https://host.com/addon-main.js",](https://host.com/addon-main.js",)
|
||||
"integrity": "",
|
||||
"ignoreIntegrity": true
|
||||
},
|
||||
{
|
||||
"kind": "builder",
|
||||
"tag": "my-addon-builder",
|
||||
"url": "[https://host.com/addon-builder.js",](https://host.com/addon-builder.js",)
|
||||
"integrity": "",
|
||||
"ignoreIntegrity": true
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"customColor": {
|
||||
"type": "string",
|
||||
"default": "#336699"
|
||||
},
|
||||
"showCustomLabel": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"extension": {
|
||||
"target": "tooltip"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Extension Targets
|
||||
|
||||
| Target | Description |
|
||||
|--------|-------------|
|
||||
| `tooltip` | Customize tooltip content and styling |
|
||||
| `plotArea` | Add visual elements to plot area |
|
||||
| `numericPoint` | Customize numeric point display |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Examples
|
||||
|
||||
### Tooltip Add-On
|
||||
|
||||
```javascript
|
||||
(function() {
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.custom-tooltip {
|
||||
padding: 8px 12px;
|
||||
background: #1a1a2e;
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
font-family: "72", Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
}
|
||||
.tooltip-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.tooltip-value {
|
||||
font-size: 16px;
|
||||
color: #4cc9f0;
|
||||
}
|
||||
.tooltip-change {
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.tooltip-change.positive { color: #4ade80; }
|
||||
.tooltip-change.negative { color: #f87171; }
|
||||
</style>
|
||||
<div class="custom-tooltip" id="tooltip">
|
||||
<div class="tooltip-title" id="title"></div>
|
||||
<div class="tooltip-value" id="value"></div>
|
||||
<div class="tooltip-change" id="change"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
class TooltipAddon extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._shadowRoot = this.attachShadow({ mode: "open" });
|
||||
this._shadowRoot.appendChild(template.content.cloneNode(true));
|
||||
this._props = {
|
||||
customColor: "#336699",
|
||||
showCustomLabel: true
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this._render();
|
||||
}
|
||||
|
||||
onCustomWidgetBeforeUpdate(changedProperties) {
|
||||
this._props = { ...this._props, ...changedProperties };
|
||||
}
|
||||
|
||||
onCustomWidgetAfterUpdate(changedProperties) {
|
||||
this._render();
|
||||
}
|
||||
|
||||
// Called by SAC with tooltip data
|
||||
setTooltipData(data) {
|
||||
this._tooltipData = data;
|
||||
this._render();
|
||||
}
|
||||
|
||||
_render() {
|
||||
if (!this._tooltipData) return;
|
||||
|
||||
const data = this._tooltipData;
|
||||
const titleEl = this._shadowRoot.getElementById("title");
|
||||
const valueEl = this._shadowRoot.getElementById("value");
|
||||
const changeEl = this._shadowRoot.getElementById("change");
|
||||
|
||||
titleEl.textContent = data.dimensionLabel || "Value";
|
||||
valueEl.textContent = this._formatValue(data.measureValue);
|
||||
valueEl.style.color = this._props.customColor;
|
||||
|
||||
if (data.previousValue && this._props.showCustomLabel) {
|
||||
const change = ((data.measureValue - data.previousValue) / data.previousValue) * 100;
|
||||
changeEl.textContent = `${change >= 0 ? "+" : ""}${change.toFixed(1)}% vs previous`;
|
||||
changeEl.className = `tooltip-change ${change >= 0 ? "positive" : "negative"}`;
|
||||
changeEl.style.display = "block";
|
||||
} else {
|
||||
changeEl.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
_formatValue(value) {
|
||||
if (typeof value !== "number") return value;
|
||||
if (value >= 1000000) return (value / 1000000).toFixed(1) + "M";
|
||||
if (value >= 1000) return (value / 1000).toFixed(1) + "K";
|
||||
return value.toLocaleString();
|
||||
}
|
||||
|
||||
get customColor() { return this._props.customColor; }
|
||||
set customColor(v) { this._props.customColor = v; }
|
||||
get showCustomLabel() { return this._props.showCustomLabel; }
|
||||
set showCustomLabel(v) { this._props.showCustomLabel = v; }
|
||||
}
|
||||
|
||||
customElements.define("tooltip-addon", TooltipAddon);
|
||||
})();
|
||||
```
|
||||
|
||||
### Plot Area Add-On
|
||||
|
||||
```javascript
|
||||
(function() {
|
||||
class PlotAreaAddon extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._shadowRoot = this.attachShadow({ mode: "open" });
|
||||
this._props = {
|
||||
showTargetLine: true,
|
||||
targetValue: 0,
|
||||
targetColor: "#ff6b6b"
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this._render();
|
||||
}
|
||||
|
||||
onCustomWidgetBeforeUpdate(changedProperties) {
|
||||
this._props = { ...this._props, ...changedProperties };
|
||||
}
|
||||
|
||||
onCustomWidgetAfterUpdate(changedProperties) {
|
||||
this._render();
|
||||
}
|
||||
|
||||
// Called by SAC with chart context
|
||||
setChartContext(context) {
|
||||
this._chartContext = context;
|
||||
this._render();
|
||||
}
|
||||
|
||||
_render() {
|
||||
if (!this._chartContext || !this._props.showTargetLine) {
|
||||
this._shadowRoot.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const { width, height, yScale } = this._chartContext;
|
||||
|
||||
// Defensive check: Validate yScale is a function before using
|
||||
if (!yScale || typeof yScale !== "function") {
|
||||
console.warn("[PlotAreaAddon] Chart context missing valid yScale function");
|
||||
this._shadowRoot.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const y = yScale(this._props.targetValue);
|
||||
|
||||
this._shadowRoot.innerHTML = `
|
||||
<svg width="${width}" height="${height}" style="position:absolute;top:0;left:0;pointer-events:none;">
|
||||
<line
|
||||
x1="0" y1="${y}"
|
||||
x2="${width}" y2="${y}"
|
||||
stroke="${this._props.targetColor}"
|
||||
stroke-width="2"
|
||||
stroke-dasharray="5,5"
|
||||
/>
|
||||
<text
|
||||
x="${width - 5}" y="${y - 5}"
|
||||
text-anchor="end"
|
||||
fill="${this._props.targetColor}"
|
||||
font-size="11"
|
||||
font-family="72, Arial, sans-serif"
|
||||
>
|
||||
Target: ${this._props.targetValue}
|
||||
</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
get showTargetLine() { return this._props.showTargetLine; }
|
||||
set showTargetLine(v) { this._props.showTargetLine = v; }
|
||||
get targetValue() { return this._props.targetValue; }
|
||||
set targetValue(v) { this._props.targetValue = v; }
|
||||
get targetColor() { return this._props.targetColor; }
|
||||
set targetColor(v) { this._props.targetColor = v; }
|
||||
}
|
||||
|
||||
customElements.define("plotarea-addon", PlotAreaAddon);
|
||||
})();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using Widget Add-Ons
|
||||
|
||||
### Enabling Add-Ons in Stories
|
||||
|
||||
1. Open story in Edit mode
|
||||
2. Select a supported chart widget
|
||||
3. Open Builder panel
|
||||
4. Scroll to **Custom Add-Ons** section
|
||||
5. Toggle **Enable Custom Add-Ons** to ON
|
||||
6. Select your installed add-on
|
||||
|
||||
### Installation
|
||||
|
||||
Same process as custom widgets:
|
||||
1. Navigate to **Analytic Applications** > **Custom Widgets**
|
||||
2. Click **+** to add new
|
||||
3. Upload the add-on JSON file
|
||||
4. Widget Add-Ons appear in the add-ons dropdown for supported widgets
|
||||
|
||||
---
|
||||
|
||||
## Builder Panel for Add-Ons
|
||||
|
||||
```javascript
|
||||
(function() {
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; font-family: "72", Arial, sans-serif; font-size: 12px; }
|
||||
.panel { padding: 12px; }
|
||||
.field { margin-bottom: 12px; }
|
||||
label { display: block; margin-bottom: 4px; font-weight: 500; }
|
||||
input[type="color"], input[type="number"] {
|
||||
width: 100%; padding: 6px; border: 1px solid #89919a; border-radius: 4px;
|
||||
}
|
||||
.checkbox-field { display: flex; align-items: center; gap: 8px; }
|
||||
</style>
|
||||
<div class="panel">
|
||||
<div class="field">
|
||||
<label>Custom Color</label>
|
||||
<input type="color" id="colorInput" />
|
||||
</div>
|
||||
<div class="field checkbox-field">
|
||||
<input type="checkbox" id="labelCheckbox" />
|
||||
<label for="labelCheckbox">Show Custom Label</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
class AddonBuilder extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this._shadowRoot = this.attachShadow({ mode: "open" });
|
||||
this._shadowRoot.appendChild(template.content.cloneNode(true));
|
||||
|
||||
this._shadowRoot.getElementById("colorInput").addEventListener("input", (e) => {
|
||||
this._fire({ customColor: e.target.value });
|
||||
});
|
||||
|
||||
this._shadowRoot.getElementById("labelCheckbox").addEventListener("change", (e) => {
|
||||
this._fire({ showCustomLabel: e.target.checked });
|
||||
});
|
||||
}
|
||||
|
||||
_fire(properties) {
|
||||
this.dispatchEvent(new CustomEvent("propertiesChanged", { detail: { properties } }));
|
||||
}
|
||||
|
||||
onCustomWidgetBeforeUpdate(changedProperties) {}
|
||||
|
||||
onCustomWidgetAfterUpdate(changedProperties) {
|
||||
if (changedProperties.customColor !== undefined) {
|
||||
this._shadowRoot.getElementById("colorInput").value = changedProperties.customColor;
|
||||
}
|
||||
if (changedProperties.showCustomLabel !== undefined) {
|
||||
this._shadowRoot.getElementById("labelCheckbox").checked = changedProperties.showCustomLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("addon-builder", AddonBuilder);
|
||||
})();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Differences from Custom Widgets
|
||||
|
||||
1. **No Styling Panel**: Add-ons only support main + builder components
|
||||
2. **Extension Target**: Must specify what part of the widget to extend
|
||||
3. **Context Data**: SAC provides chart context (scales, dimensions, data) via methods
|
||||
4. **Limited Scope**: Can only modify supported parts of supported chart types
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Widget Add-On Announcement](https://community.sap.com/t5/technology-blog-posts-by-sap/announcing-the-new-sap-analytics-cloud-feature-widget-add-on/ba-p/13575788)
|
||||
- [Widget Add-On Samples](https://community.sap.com/t5/technology-blog-posts-by-sap/sap-analytics-cloud-custom-widget-amp-widget-add-ons-samples-preview/ba-p/13585313)
|
||||
- [SAP Samples Repository](https://github.com/SAP-samples/analytics-cloud-datasphere-community-content/tree/main/SAC_Custom_Widgets)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-22
|
||||
1339
references/widget-templates.md
Normal file
1339
references/widget-templates.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user