12 KiB
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
- Overview
- Widget Add-On vs Custom Widget
- Supported Chart Types
- JSON Structure
- Implementation Examples
- 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
- Open story in Edit mode
- Select a supported chart widget
- Open Builder panel
- Scroll to Custom Add-Ons section
- Toggle Enable Custom Add-Ons to ON
- Select your installed add-on
Installation
Same process as custom widgets:
- Navigate to Analytic Applications > Custom Widgets
- Click + to add new
- Upload the add-on JSON file
- 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
- No Styling Panel: Add-ons only support main + builder components
- Extension Target: Must specify what part of the widget to extend
- Context Data: SAC provides chart context (scales, dimensions, data) via methods
- Limited Scope: Can only modify supported parts of supported chart types
Resources
Last Updated: 2025-11-22