{
"dataBindings": {
"dataBinding": {
"feeds": [
{
"id": "dimensions",
"description": "Dimensions",
"type": "dimension"
},
{
"id": "measures",
"description": "Measures",
"type": "mainStructureMember"
}
]
}
}
}
// 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')
{
"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"
}
]
}
}
}
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)
})()
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)
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
20 | |
11 | |
8 | |
8 | |
7 | |
7 | |
7 | |
6 | |
6 | |
6 |