Initial commit
This commit is contained in:
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