Files
gh-secondsky-sap-skills-ski…/references/widget-addon-guide.md
2025-11-30 08:55:27 +08:00

12 KiB

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


Table of Contents

  1. Overview
  2. Widget Add-On vs Custom Widget
  3. Supported Chart Types
  4. JSON Structure
  5. Implementation Examples
  6. 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

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

(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

(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

(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


Last Updated: 2025-11-22