Extending SAP Analytics Cloud’s Visualization Capability with Matplotlib (Python) (2024)

Matplotlib is a comprehensive library for creating static, animated, and interactive visualizations in Python.

You can extend the visualization capabilities in SAP Analytics Cloud with Matplotlib. In this blog post, I would like to share with you how to quickly add Matplotlib as custom widgets to your analytic application or optimized story.

The following video shows how Matplotlib looks like in SAP Analytics Cloud.

SAP Analytics Cloud custom widget framework enables developers to create the web component. Matplotlib is a Python library, so the main idea is to introduce Pyodide to enable Python code execution in web component.

Extending SAP Analytics Cloud’s Visualization Capability with Matplotlib (Python) (1)

Here’re the detailed steps about how to implement a custom widget with Pyodide and Matplotlib:

1, Define Data Binding in custom widget JSON file

Here’s the sample code:

{
"dataBindings": {
"dataBinding": {
"feeds": [
{
"id": "dimensions",
"description": "Dimensions",
"type": "dimension"
},
{
"id": "measures",
"description": "Measures",
"type": "mainStructureMember"
}
]
}
}
}

For more details, refer to: Using Data Binding

2, Implement custom widget in main.js

The main.js file implements the following core workflows:

a, Read the data from SAP Analytics Cloud binding framework.

b, Pass the data to Pyodide so that the Python script can consume the data.

c, Call Pyodide to run the Python script.

d, Get the result of Python script and render the result as visualization.

Here’s the sample code:

// a, Read the data from SAP Analytics Cloud binding framework
const dataBinding = this.dataBinding
const { data, metadata } = dataBinding
// ...

// b, Pass the data to Pyodide so that the Python script could consume the data
window._pyodide_matplotlib_data = data.map(dp => {
// ...
})

// c, Call Pyodide to run the Python script
this._pyodide.runPython(this.py)

// d, Get the result of Python script and render the result as Visualization
this._pyplotfigure.src = this._pyodide.globals.get('img_str')


3, Use the custom widget in SAP Analytics Cloud.

After uploading the custom widget to SAP Analytics Cloud and inserting it to your analytic application or optimized story, to render the visualization:

a, In the Builder panel of the custom widget, bind it to a data source.

b, In the Styling panel, write the Python script, which is stored as a string variable in the custom widget. (this.py in the example above)

c, Apply the data binding and the Python script.

Then, the visualization is rendered in the application or story.

Extending SAP Analytics Cloud’s Visualization Capability with Matplotlib (Python) (2)



index.json


{
"eula": "",
"vendor": "SAP",
"license": "",
"id": "com.sap.sac.sample.pyodide.matplotlib",
"version": "1.0.0",
"supportsMobile": true,
"name": "Pyodide Matplotlib",
"newInstancePrefix": "PyodideMatplotlib",
"description": "A sample custom widget based on Pyodide and Matplotlib",
"webcomponents": [
{
"kind": "main",
"tag": "com-sap-sample-pyodide-matplotlib",
"url": "http://localhost:3000/pyodide/matplotlib/main.js",
"integrity": "",
"ignoreIntegrity": true
},
{
"kind": "styling",
"tag": "com-sap-sample-pyodide-matplotlib-styling",
"url": "http://localhost:3000/pyodide/matplotlib/styling.js",
"integrity": "",
"ignoreIntegrity": true
}
],
"properties": {
"width": {
"type": "integer",
"default": 600
},
"height": {
"type": "integer",
"default": 420
},
"py": {
"type": "string"
}
},
"methods": {},
"events": {},
"dataBindings": {
"dataBinding": {
"feeds": [
{
"id": "dimensions",
"description": "Dimensions",
"type": "dimension"
},
{
"id": "measures",
"description": "Measures",
"type": "mainStructureMember"
}
]
}
}
}

main.js


var getScriptPromisify = (src) => {
return new Promise(resolve => {
$.getScript(src, resolve)
})
}

const parseMetadata = metadata => {
const { dimensions: dimensionsMap, mainStructureMembers: measuresMap } = metadata
const dimensions = []
for (const key in dimensionsMap) {
const dimension = dimensionsMap[key]
dimensions.push({ key, ...dimension })
}
const measures = []
for (const key in measuresMap) {
const measure = measuresMap[key]
measures.push({ key, ...measure })
}
return { dimensions, measures, dimensionsMap, measuresMap }
}

(function () {
const template = document.createElement('template')
template.innerHTML = `
<style>
</style>
<div id="root" style="width: 100%; height: 100%; text-align: center;">
<img id="pyplotfigure"/>
</div>
`
class Main extends HTMLElement {
constructor () {
super()

this._shadowRoot = this.attachShadow({ mode: 'open' })
this._shadowRoot.appendChild(template.content.cloneNode(true))

this._root = this._shadowRoot.getElementById('root')
this._pyplotfigure = this._shadowRoot.getElementById('pyplotfigure')

this._props = {}

this._pyodide = null
this.bootstrap()
}

async onCustomWidgetAfterUpdate (changedProps) {
this.render()
}

onCustomWidgetResize (width, height) {
this.render()
}

async bootstrap () {
// https://cdnjs.cloudflare.com/ajax/libs/pyodide/0.21.3/pyodide.js
// https://cdn.staticfile.org/pyodide/0.21.3/pyodide.js
await getScriptPromisify('https://cdn.jsdelivr.net/pyodide/v0.21.3/full/pyodide.js')
const pyodide = await loadPyodide()
await pyodide.loadPackage('matplotlib')

this._pyodide = pyodide
this.render()
}

async render () {
this.dispose()

if (!this._pyodide) { return }
if (!this.py) { return }

const dataBinding = this.dataBinding
if (!dataBinding || dataBinding.state !== 'success') { return }

const { data, metadata } = dataBinding
const { dimensions, measures } = parseMetadata(metadata)

if (dimensions.length !== 1) { return }
if (measures.length !== 3) { return }

const [d] = dimensions
const [m0, m1, m2] = measures
const million = 1000 * 1000
// window._pyodide_matplotlib_data = [[11, 12, 15], [13, 6, 20], [10, 8, 12], [12, 15, 8]]
window._pyodide_matplotlib_data = data.map(dp => {
return [
dp[m0.key].raw / million,
dp[m1.key].raw / million,
dp[m2.key].raw / million
]
})

window._pyodide_matplotlib_title = `${[m0.label, m1.label, m2.label].join(', ')} per ${d.description}`

// https://pyodide.org/en/stable/usage/type-conversions.html
this._pyodide.runPython(this.py)
this._pyplotfigure.src = this._pyodide.globals.get('img_str')
this._pyplotfigure.style.width = '100%'
this._pyplotfigure.style.height = '100%'
}

dispose () {
this._pyplotfigure.src = ''
this._pyplotfigure.style.width = ''
this._pyplotfigure.style.height = ''
}
}

customElements.define('com-sap-sample-pyodide-matplotlib', Main)
})()


styling.js


const template = document.createElement('template')
template.innerHTML = `
<style>
#root div {
margin: 0.5rem;
}
#root .title {
font-weight: bold;
}
#root #code {
width: 100%;
height: 480px;
}
</style>
<div id="root" style="width: 100%; height: 100%;">
<div class="title">Python code</div>
<textarea id="code"></textarea>
</div>
<div>
<button id="button">Apply</button>
</div>
`

const PY_DEFAULT = `from matplotlib import pyplot as plt
import numpy as np
import io, base64
from js import _pyodide_matplotlib_data, _pyodide_matplotlib_title

SAC_DATA = _pyodide_matplotlib_data.to_py()
SAC_TITLE = _pyodide_matplotlib_title

# Generate data points from SAC_DATA
x = []
y = []
scale = []
for row in SAC_DATA:
x.append(row[0])
y.append(row[1])
scale.append(row[2])
# Map each onto a scatterplot we'll create with Matplotlib
fig, ax = plt.subplots()
ax.scatter(x=x, y=y, c=scale, s=np.abs(scale)*200)
ax.set(title=SAC_TITLE)
# plt.show()
buf = io.BytesIO()
fig.savefig(buf, format='png')
buf.seek(0)
img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')`

class Styling extends HTMLElement {
constructor () {
super()

this._shadowRoot = this.attachShadow({ mode: 'open' })
this._shadowRoot.appendChild(template.content.cloneNode(true))
this._root = this._shadowRoot.getElementById('root')

this._code = this._shadowRoot.getElementById('code')
this._code.value = PY_DEFAULT

this._button = this._shadowRoot.getElementById('button')
this._button.addEventListener('click', () => {
const py = this._code.value
this.dispatchEvent(new CustomEvent('propertiesChanged', { detail: { properties: { py } } }))
})
}

// ------------------
// LifecycleCallbacks
// ------------------
async onCustomWidgetBeforeUpdate (changedProps) {
}

async onCustomWidgetAfterUpdate (changedProps) {
if (changedProps.py) {
this._code.value = changedProps.py
}
}

async onCustomWidgetResize (width, height) {
}

async onCustomWidgetDestroy () {
this.dispose()
}

// ------------------
//
// ------------------

dispose () {
}
}

customElements.define('com-sap-sample-pyodide-matplotlib-styling', Styling)

This concludes the blog. Feel free to share your thoughts below.

Extending SAP Analytics Cloud’s Visualization Capability with Matplotlib (Python) (2024)
Top Articles
What is Project Manager? and Roles and Responsibilities
What Is Communicative English: The Communicative Approach | English School
Terrorist Usually Avoid Tourist Locations
Identifont Upload
Craigslist Parsippany Nj Rooms For Rent
Craigslist Kennewick Pasco Richland
Housing Intranet Unt
LeBron James comes out on fire, scores first 16 points for Cavaliers in Game 2 vs. Pacers
Brenna Percy Reddit
Citymd West 146Th Urgent Care - Nyc Photos
Labor Gigs On Craigslist
Guilford County | NCpedia
Kitty Piggy Ssbbw
Xxn Abbreviation List 2023
111 Cubic Inch To Cc
Curry Ford Accident Today
FDA Approves Arcutis’ ZORYVE® (roflumilast) Topical Foam, 0.3% for the Treatment of Seborrheic Dermatitis in Individuals Aged 9 Years and Older - Arcutis Biotherapeutics
Allentown Craigslist Heavy Equipment
Acts 16 Nkjv
Sussyclassroom
Dragonvale Valor Dragon
Craigslistodessa
The Boogeyman (Film, 2023) - MovieMeter.nl
Angel Haynes Dropbox
My Reading Manga Gay
2487872771
Alima Becker
Homewatch Caregivers Salary
Pch Sunken Treasures
Baldur's Gate 3 Dislocated Shoulder
Rust Belt Revival Auctions
SOC 100 ONL Syllabus
KM to M (Kilometer to Meter) Converter, 1 km is 1000 m
Compare Plans and Pricing - MEGA
Publictributes
Qlima© Petroleumofen Elektronischer Laserofen SRE 9046 TC mit 4,7 KW CO2 Wächter • EUR 425,95
Restored Republic June 6 2023
Carroll White Remc Outage Map
Actor and beloved baritone James Earl Jones dies at 93
Winta Zesu Net Worth
Rush Copley Swim Lessons
Dr Mayy Deadrick Paradise Valley
Watch Chainsaw Man English Sub/Dub online Free on HiAnime.to
Comanche Or Crow Crossword Clue
Alba Baptista Bikini, Ethnicity, Marriage, Wedding, Father, Shower, Nazi
Perc H965I With Rear Load Bracket
Wolf Of Wallstreet 123 Movies
Hcs Smartfind
Convert Celsius to Kelvin
Access One Ummc
Latest Posts
Article information

Author: Pres. Lawanda Wiegand

Last Updated:

Views: 5991

Rating: 4 / 5 (51 voted)

Reviews: 82% of readers found this page helpful

Author information

Name: Pres. Lawanda Wiegand

Birthday: 1993-01-10

Address: Suite 391 6963 Ullrich Shore, Bellefort, WI 01350-7893

Phone: +6806610432415

Job: Dynamic Manufacturing Assistant

Hobby: amateur radio, Taekwondo, Wood carving, Parkour, Skateboarding, Running, Rafting

Introduction: My name is Pres. Lawanda Wiegand, I am a inquisitive, helpful, glamorous, cheerful, open, clever, innocent person who loves writing and wants to share my knowledge and understanding with you.