All skills
secondsky avatar

/sap-sac-custom-widget

@4d87d99
by Eddiesecondsky/sap-skills456 stars
120

SAP Analytics Cloud (SAC) Custom Widget development. Use when building custom visualizations, extending SAC with Web Components, or creating Widget Add-Ons. Covers JSON metadata, JavaScript Web Components, lifecycle functions, data binding with feeds, styling/builder panels, property/event/method definitions, third-party library integration, hosting, security, performance, and debugging. Includes Widget Add-On feature (QRC Q4 2023+) and templates for widgets, charts, and KPI cards.

Use this Skill: https://skilld.dev/gh/secondsky/sap-skills/sap-sac-custom-widget

This session only. Nothing lands on disk.

referencesadvanced-topics.md

≈4.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

SAP SAC Custom Widget Advanced Topics

Advanced features including custom types, script data types, and administration.

Source: SAP Custom Widget Developer Guide


Table of Contents

  1. Custom Types
  2. Script API Data Types
  3. Widget Installation
  4. Third-Party Library Integration
  5. Advanced Data Binding
  6. 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:

{
  "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:

{
  "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

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:

{
  "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.currentSelection = selection;"
    }
  }
}

Usage in Scripts:

// In SAC script
var selection = {
  "Account": "Revenue",
  "Year": "2024"
};
Widget_1.setSelection(selection);

MemberInfo Type

Information about a dimension member:

// 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:

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:

// ResultMemberInfo structure
{
  id: "MEMBER_ID",
  description: "Member Name",
  parentId: "PARENT_ID",      // For hierarchies
  properties: {
    "Property1": "Value1"
  }
}

DataSource Methods

Access data source information:

// 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:

{
  "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:

    {
      "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:

For generated enterprise/offline packages, prefer local vendor/ copies of these libraries. Treat the CDN URLs below as general reference examples, not defaults for /widget-generate output.

Library Use Case CDN
ECharts Charts 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
Chart.js Charts https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js
Leaflet Maps https://unpkg.com/leaflet@1.9/dist/leaflet.js
Moment.js Dates https://cdn.jsdelivr.net/npm/moment@2/moment.min.js

Integration Pattern

(function() {
  // Library URLs
  const LIBS = {
    echarts: "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

{
  "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

_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

// 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

{
  "properties": {
    "titleKey": {
      "type": "string",
      "default": "WIDGET_TITLE",
      "description": "Translation key for title"
    }
  }
}

Translation Pattern

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);
  }

  // The story script writes a declared string property such as `locale`.
  set locale(locale) {
    this._locale = String(locale || "en").substring(0, 2); // "en-US" -> "en"
    this._render();
  }
}

The widget cannot reliably discover the SAC user's identity or the SAC UI language. Treat navigator.language as browser context only. Pass a locale or translated viewer content through a declared property or story script. A custom locale property should be documented in widget.json and written by story script when the viewer language matters. Keep authored empty strings distinct from missing values when seeding localized configuration, and validate every language slot.


Debugging Advanced Widgets

Console Inspection

// 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

_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`);
  }
}

Export & Viewport Loading (2026-06-12)

PDF/PPTX/Google Slides Export

To support export, set "supportsExport": true in the root object and implement serializeCustomWidgetToImage():

async serializeCustomWidgetToImage(exportOptions) {
  const canvas = await html2canvas(this._shadowRoot.querySelector(".container"));
  return canvas.toDataURL("image/png");
}

The exportOptions object (from the Optimized Story Experience) provides IExportOptions with page size, paper size, and orientation. The method must return a Base64-encoded image string. The framework calls this method automatically when a user exports the story.

Prerequisite: supportsExport: true in widget.json root object. Only available in Optimized Story Experience.

Viewport Loading

When supportsViewportLoading is set to true, the widget is lazy-loaded when it scrolls into the visible viewport. To signal rendering completion (for performance measurement), fire the customWidgetRenderComplete custom event:

onCustomWidgetAfterUpdate(changedProperties) {
  this._render();
  this.dispatchEvent(new Event("customWidgetRenderComplete"));
}

Prerequisite: supportsViewportLoading: true in widget.json root object. Optimized Story Experience only.

Linked Analysis Filter on Selection

When supportsLinkedAnalysisFilterOnSelection is set to true, the widget participates in linked analysis based on Filter on Data Point Selection. Data binding linked analysis APIs:

const binding = this.dataBindings.getDataBinding("myDataBinding");
const linkedAnalysis = binding.getLinkedAnalysis();

linkedAnalysis.setFilters(selection);
linkedAnalysis.removeFilters();
const isEnabled = linkedAnalysis.isDataPointSelectionEnabled();

Prerequisite: supportsLinkedAnalysisFilterOnSelection: true in widget.json root object. Optimized Story Experience only.


Resources


Last Updated: 2026-06-12

Source: SKILL.md on GitHub

1 alert16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides a comprehensive environment and templates for developing SAP Analytics Cloud (SAC) Custom Widgets. It promotes robust security best practices, including subresource integrity (SRI), XSS prevention via input sanitization, and proper Shadow DOM encapsulation. It includes local tools for scaffold generation and iteration that operate on the user's machine without requiring external packages or services.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    3/16 files flagged

  • ZeroLeaks5mo

    2 findings · Score: 80/100

Signed by skilld at 4d87d99. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated last month
What it can do
Reads files Runs commands Network
All 3 allowed tools
ReadBashWebFetch
Other metadata
metadata
{
  "maintainer": "Eduard Jiglau",
  "maintainer_email": "hello@sap-ai-skills.com",
  "website": "https://sap-ai-skills.com",
  "version": "2.4.1",
  "last_verified": "2026-06-12",
  "sac_version": "2026.8",
  "errors_prevented": "40+",
  "official_docs": [
    "https://help.sap.com/docs/SAP_ANALYTICS_CLOUD/0ac8c6754ff84605a4372468d002f2bf/75311f67527c41638ceb89af9cd8af3e.html",
    "https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf"
  ],
  "samples_repo": "https://github.com/SAP-samples/analytics-cloud-datasphere-community-content/tree/main/SAC_Custom_Widgets",
  "keywords": [
    "sap analytics cloud",
    "sac custom widget",
    "web component sac",
    "json metadata widget",
    "widget lifecycle functions",
    "onCustomWidgetBeforeUpdate",
    "onCustomWidgetAfterUpdate",
    "onCustomWidgetResize",
    "onCustomWidgetDestroy",
    "sac data binding",
    "dataBindings feeds",
    "styling panel widget",
    "builder panel widget",
    "sac echarts integration",
    "sac d3js integration",
    "third party library sac",
    "widget hosting sac",
    "integrity hash widget",
    "sha256 integrity",
    "widget security cors",
    "sac widget debugging",
    "sac analytics designer widget",
    "optimized story experience widget",
    "sac widget api",
    "widget add-on",
    "sac script api widget",
    "shadow dom web component",
    "sac tooltip customization",
    "plot area addon",
    "sac resource zip upload",
    "root relative widget url",
    "resource file upload",
    "builder focus collapse state",
    "self contained component js",
    "resource zip artifact naming",
    "chat download artifacts"
  ]
}

README badge

README badge for secondsky/sap-skills/sap-sac-custom-widget