Skip to Content
Technical Articles
Author's profile photo Jason Yang

Extending SAP Analytics Cloud’s Visualization Capability with Apache ECharts

Apache ECharts is a free, powerful, interactive charting and data visualization library. Many intuitive examples can be found on its official website https://echarts.apache.org/examples/en/index.html

Bringing Apache ECharts in SAP Analytics Cloud will extend its visualization capability. In this blog post, I would like to share with you how to quickly add Apache ECharts as custom widgets into SAP Analytics Cloud using the code template.

Here is the demo shows how Apache ECharts looks like in SAP Analytics Cloud.

(source: https://echarts.apache.org/en/index.html)

Code template: add an Apache ECharts into SAP Analytics Cloud in 5 minutes

 

(source: https://echarts.apache.org/en/index.html)

Try it by yourself

  1. Create a folder structure like c:\web\echarts\prepared
  2. Copy following code and save it as c:\web\echarts\prepared\index.json
    {
      "eula": "",
      "vendor": "SAP",
      "license": "",
      "id": "com.sap.sac.sample.echarts.prepared",
      "version": "1.0.0",
      "name": "ECharts Prepared",
      "newInstancePrefix": "EChartPrepared",
      "description": "A sample custom widget wrapped EChart Prepared",
      "webcomponents": [
        {
          "kind": "main",
          "tag": "com-sap-sample-echarts-prepared",
          "url": "http://localhost:3000/echarts/prepared/main.js",
          "integrity": "",
          "ignoreIntegrity": true
        }
      ],
      "properties": {
    		"width": {
    			"type": "integer",
    			"default": 600
    		},
    		"height": {
    			"type": "integer",
    			"default": 420
    		}
      },
      "methods": {
      },
      "events": {
      }
    }
  3. Copy following code and save it as c:\web\echarts\prepared\main.js
    var getScriptPromisify = (src) => {
      return new Promise(resolve => {
        $.getScript(src, resolve)
      })
    }
    
    (function () {
      const prepared = document.createElement('template')
      prepared.innerHTML = `
          <style>
          </style>
          <div id="root" style="width: 100%; height: 100%;">
          </div>
        `
      class SamplePrepared extends HTMLElement {
        constructor () {
          super()
    
          this._shadowRoot = this.attachShadow({ mode: 'open' })
          this._shadowRoot.appendChild(prepared.content.cloneNode(true))
    
          this._root = this._shadowRoot.getElementById('root')
    
          this._props = {}
    
          this.render()
        }
    
        onCustomWidgetResize (width, height) {
          this.render()
        }
    
        async render () {
          await getScriptPromisify('https://cdn.bootcdn.net/ajax/libs/echarts/5.0.0/echarts.min.js')
    
          const chart = echarts.init(this._root)
          const option = {
            // https://echarts.apache.org/examples/zh/index.html
          }
          chart.setOption(option)
        }
      }
    
      customElements.define('com-sap-sample-echarts-prepared', SamplePrepared)
    })()
  4. Pick an Apache ECharts example from its official website: https://echarts.apache.org/examples/en/index.html
  5. Copy the code from code snippet of your selected Apache ECharts example, and replace the “option” placeholder in c:\web\echarts\prepared\main.js. Remember to keep the “const” keyword before “option”
  6. Upload c:\web\echarts\prepared\index.json to SAP Analytics Cloud
  7. Start your local web server.I personally like lite-server, a very light weight web server. You can install it with two steps:
  • Install lite-server: from OS’s terminal command line: npm install -g lite-server

Then from OS’s terminal command:

  • cd c:\web (this is important as the relative path is now hard-coded in index.json)
  • lite-server

Congratulation! Now you can open SAP Analytics Cloud to create an Analytic Application, and you can directly add your own Apache ECharts as a widget.

Optional step: you may want to rename your custom widget, like from “prepared” to “demo”. Then follow these steps:

  1. Rename the folder “prepared” to “demo”
  2. In index.json and main.js replace all (case sensitive)
  • “prepared” to “demo”
  • “Prepared” to “Demo”

Further step: render SAP Analytics Cloud model data in Apache ECharts

You may also want to render SAP Analytics Cloud model data in the Apache ECharts. Supporting Model Data binding to custom widgets is already in the product roadmap (https://saphanajourney.com/sap-analytics-cloud/product-updates/sac-download-road-map/). Before that, the recommended approach is to have a hidden table that pulls the data you would like to visualize, then send the whole result set to the Apache ECharts custom widget.

var resultSet = Table_1.getDataSource().getResultSet();
EChartLifeExpectancy2_1.render(resultSet);

It would be more efficient from development perspective to parse the result set in your custom widget’s main.js as you would be able to use any third-party library to do so. The custom widget EChartLifeExpectancy2 in the demo explains the details.

The index.json of EChartLifeExpectancy2 is as below. Now we added a render function that accepts “resultset” parameter passed from SAP Analytics Cloud.

{
	"eula": "",
	"vendor": "SAP",
	"license": "",
	"id": "com.sap.sac.sample.echarts.life_expectancy2",
	"version": "1.0.0",
	"name": "ECharts LifeExpectancy2",
	"newInstancePrefix": "EChartLifeExpectancy2",
	"description": "A sample custom widget wrapped EChart LifeExpectancy",
	"webcomponents": [
	  {
		"kind": "main",
		"tag": "com-sap-sample-echarts-life_expectancy2",
		"url": "http://localhost:3000/echarts/life_expectancy2/main.js",
		"integrity": "",
		"ignoreIntegrity": true
	  }
	],
	"properties": {
		"width": {
			"type": "integer",
			"default": 600
		},
		"height": {
			"type": "integer",
			"default": 420
		}
	},
	"methods": {
		  "render": {
			  "description": "Render",
			  "parameters": [
				  {
					  "name": "resultSet",
					  "type": "any",
					  "description": "The json"
				  }
			  ]
		  }
	},
	"events": {
  
	}
  }

In the main.js, it parses the “resultset” to construct the “option” that Apache ECharts accepts. As the code snippet is long, I only take a screenshot as below:

Then set the option to the chart:

myChart.setOption(option)

Then your custom widget will be able to render data coming from SAP Analytics Cloud.

Additional information

SAP Analytics Cloud Custom Widget Developer Guide is a helpful resource:

https://help.sap.com/viewer/0ac8c6754ff84605a4372468d002f2bf/release/en-US

Summary

Besides Apache ECharts, the code template can be adapted to bring other charting libraries to help your build an even more attractive analytic application.

Hope my example could give you some ideas.

Assigned Tags

      14 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Nancy Lu
      Nancy Lu

      Thanks for sharing, Jason! It is very helpful for customers to extend the visualizations using custom widgets.

      Author's profile photo Richard Xiaolong Gou
      Richard Xiaolong Gou

      Incredible visualization effects. Kudos for sharing, Jason.

      Author's profile photo Ankit Vaidya
      Ankit Vaidya

      Nice informative and insightful post. Thanks a ton Jason.

      Author's profile photo Adem Baykal
      Adem Baykal

      Great blog Jason, Thanks a ton for sharing this with the community.

      Author's profile photo Jerome Maillard
      Jerome Maillard

      Hi Jason,

      Just a question because I tried to reproduce and it doesn't work. I specified a URL that is accessible through internet (http://3.1xx.6x.2xx:3000/prepared/main.js) to ensure the SAC platform could access it biut I think flews are not opened . How can we manage with that?

      I can see the main.js file when I call this URL http://3.1xx.6x.2xx:3000/prepared/main.js

      Thanks

       

      Author's profile photo Jason Yang
      Jason Yang
      Blog Post Author

      Hi Jerome,

      My default path is http://localhost:3000/echarts/prepared/main.js, have a folder "echarts" as well.

      Make sure you update the path in the .json file as well if you use a different one.

      Probably you could send me your sample via email if there is still an issue.

       

      thanks,

      Jason

      Author's profile photo Jerome Maillard
      Jerome Maillard

      Hi Jason,

      Thanks, this is a mixed content issue as I use HTTP and not HTTPS.

      Another question, I don't see how we can use Custom Widgets with Data coming from SAC sources like Excel sheets, Universes .

      Could you tell me more about that or give some links that explains how to deal with that?

      Thanks again

      Jérôme

      Author's profile photo Jason Yang
      Jason Yang
      Blog Post Author

      Sorry Jerome for the late response.

      You can build SAC data model based on Excel sheets, Universe.

      Currently you can directly bind a data model to a custom widget - it is on the roadmap.

      The workaround is shown in my second example to have a hidden table bound to a data model, then get the resultset and send to the custom widget.

      var resultSet = Table_1.getDataSource().getResultSet();
      EChartLifeExpectancy2_1.render(resultSet);

       

      thanks,

      Jason

      Author's profile photo Lutful Haider
      Lutful Haider

      Hi Jason Yang,

      That’s a nice tutorial .I am trying to create a gantt chart custom widget application with echarts library .I am having hard time binding data into custom widget.Do I need to create an hidden table in here as well?Do you have any example of that hidden table from where custom widget will get data?I mean How does hidden table will look like?

      Thank you

      Author's profile photo Jason Yang
      Jason Yang
      Blog Post Author

      Hi Lutful,

      A hidden table is required before SAP releases the feature of supporting data binding to custom widget.

      What does a hidden table looks like depends on the complexity of the custom widget. It can be very simple like for this basic column chart: https://echarts.apache.org/examples/en/editor.html?c=bar-simple, but can be complex like the gantt chart: https://echarts.apache.org/examples/en/editor.html?c=custom-gantt-flight

      A rule of thumb is that you need to carefully the sample code snippet of the specific Apache echart, and construct a hidden table with data it requires.

      Thanks,

      Jason

      Author's profile photo Teena Gattu
      Teena Gattu

      Hi Jason,

      I am tried to implement your logic as you mentioned ,i am trying to implement for this gauge report.

      i have created both index file and main.js file as mentioned, but i think i am facing some issue while running lite server in npm,when i upload the custom widget in SAC in application ,it is giving the below error.

      can you please help me to understand the issue.

      Thanks,

      Kishore

      Author's profile photo Jason Yang
      Jason Yang
      Blog Post Author

      Hi Teena,

      The error is to too generic to tell the root cause.Do you mind sending me your code? It is better a Github link so that you don't need to paste the code here.

      In your screenshot, you show the "Full Code" tab on EChart, please switch to the "Edit Example" tab, and copy the code to your main.js, make sure having"const" keyword before "option".

      Thanks,

      Jason

      Author's profile photo Girish Lakhani
      Girish Lakhani

      Hi Jason,

       

      Could you please share main.js file code that contains resultset data formation (full code of render method)?

       

      Thanks,

      Girish Lakhani

      Author's profile photo Jason Yang
      Jason Yang
      Blog Post Author
      var getScriptPromisify = (src) => {
        return new Promise(resolve => {
          $.getScript(src, resolve)
        })
      }
      
      (function () {
        const template = document.createElement('template')
        template.innerHTML = `
            <style>
            #root {
              background-color: #100c2a;
            }
            #placeholder {
              padding-top: 1em;
              text-align: center;
              font-size: 1.5em;
              color: white;
            }
            </style>
            <div id="root" style="width: 100%; height: 100%;">
              <div id="placeholder">Time-Series Animation Chart</div>
            </div>
          `
        class SampleLifeExpectancy2 extends HTMLElement {
          constructor () {
            super()
      
            this._shadowRoot = this.attachShadow({ mode: 'open' })
            this._shadowRoot.appendChild(template.content.cloneNode(true))
      
            this._root = this._shadowRoot.getElementById('root')
      
            this._props = {}
          }
      
          // ------------------
          // Scripting methods
          // ------------------
          async render (resultSet) {
            await getScriptPromisify('https://cdn.bootcdn.net/ajax/libs/echarts/5.0.0/echarts.min.js')
      
            this._placeholder = this._root.querySelector('#placeholder')
            if (this._placeholder) {
              this._root.removeChild(this._placeholder)
              this._placeholder = null
            }
            if (this._myChart) {
              echarts.dispose(this._myChart)
            }
            var myChart = this._myChart = echarts.init(this._root, 'dark')
      
            const MEASURE_DIMENSION = '@MeasureDimension'
            const countries = []
            const timeline = []
            const series = []
            resultSet.forEach(dp => {
              const { rawValue, description } = dp[MEASURE_DIMENSION]
              const country = dp.Country.description
              const year = Number(dp.timeline.description)
      
              if (countries.indexOf(country) === -1) {
                countries.push(country)
              }
              if (timeline.indexOf(year) === -1) {
                timeline.push(year)
              }
              const iT = timeline.indexOf(year)
              series[iT] = series[iT] || []
              const iC = countries.indexOf(country)
              series[iT][iC] = series[iT][iC] || []
      
              let iV
              if (description === 'Income') { iV = 0 }
              if (description === 'LifeExpect') { iV = 1 }
              if (description === 'Population') { iV = 2 }
              series[iT][iC][iV] = rawValue
              series[iT][iC][3] = country
              series[iT][iC][4] = year
            })
      
            const data = {
              countries,
              series,
              timeline
            }
            // console.log(data)
            // $.get('https://cdn.jsdelivr.net/gh/apache/incubator-echarts-website@asf-site/examples' + '/data/asset/data/life-expectancy.json', function (data) {
            //   console.log(data)
            // })
      
            var itemStyle = {
              opacity: 0.8,
              shadowBlur: 10,
              shadowOffsetX: 0,
              shadowOffsetY: 0,
              shadowColor: 'rgba(0, 0, 0, 0.5)'
            }
      
            var sizeFunction = function (x) {
              var y = Math.sqrt(x / 5e8) + 0.1
              return y * 80
            }
            // Schema:
            var schema = [
              { name: 'Income', index: 0, text: 'Income', unit: 'USD' },
              { name: 'LifeExpectancy', index: 1, text: 'LifeExpectancy', unit: 'Year' },
              { name: 'Population', index: 2, text: 'Population', unit: '' },
              { name: 'Country', index: 3, text: 'Country', unit: '' }
            ]
      
            const option = {
              baseOption: {
                timeline: {
                  axisType: 'category',
                  orient: 'vertical',
                  autoPlay: true,
                  inverse: true,
                  playInterval: 1000,
                  left: null,
                  right: 0,
                  top: 20,
                  bottom: 20,
                  width: 55,
                  height: null,
                  label: {
                    color: '#999'
                  },
                  symbol: 'none',
                  lineStyle: {
                    color: '#555'
                  },
                  checkpointStyle: {
                    color: '#bbb',
                    borderColor: '#777',
                    borderWidth: 2
                  },
                  controlStyle: {
                    showNextBtn: false,
                    showPrevBtn: false,
                    color: '#666',
                    borderColor: '#666'
                  },
                  emphasis: {
                    label: {
                      color: '#fff'
                    },
                    controlStyle: {
                      color: '#aaa',
                      borderColor: '#aaa'
                    }
                  },
                  data: []
                },
                backgroundColor: '#100c2a',
                title: [{
                  text: data.timeline[0],
                  textAlign: 'center',
                  left: '63%',
                  top: '55%',
                  textStyle: {
                    fontSize: 100,
                    color: 'rgba(255, 255, 255, 0.7)'
                  }
                }, {
                  text: 'Life expectancy vs. GDP',
                  left: 'center',
                  top: 10,
                  textStyle: {
                    color: '#aaa',
                    fontWeight: 'normal',
                    fontSize: 20
                  }
                }],
                tooltip: {
                  padding: 5,
                  backgroundColor: '#222',
                  borderColor: '#777',
                  borderWidth: 1,
                  formatter: function (obj) {
                    var value = obj.value
                    return schema[3].text + ':' + value[3] + '<br>' +
                                        schema[1].text + ':' + value[1] + schema[1].unit + '<br>' +
                                        schema[0].text + ':' + value[0] + schema[0].unit + '<br>' +
                                        schema[2].text + ':' + value[2] + '<br>'
                  }
                },
                grid: {
                  top: 100,
                  containLabel: true,
                  left: 30,
                  right: '110'
                },
                xAxis: {
                  type: 'log',
                  name: 'Income',
                  max: 100000,
                  min: 300,
                  nameGap: 25,
                  nameLocation: 'middle',
                  nameTextStyle: {
                    fontSize: 18
                  },
                  splitLine: {
                    show: false
                  },
                  axisLine: {
                    lineStyle: {
                      color: '#ccc'
                    }
                  },
                  axisLabel: {
                    formatter: '{value} $'
                  }
                },
                yAxis: {
                  type: 'value',
                  name: 'LifeExpectancy',
                  max: 100,
                  nameTextStyle: {
                    color: '#ccc',
                    fontSize: 18
                  },
                  axisLine: {
                    lineStyle: {
                      color: '#ccc'
                    }
                  },
                  splitLine: {
                    show: false
                  },
                  axisLabel: {
                    formatter: '{value} Years'
                  }
                },
                visualMap: [
                  {
                    show: false,
                    dimension: 3,
                    categories: data.countries,
                    calculable: true,
                    precision: 0.1,
                    textGap: 30,
                    textStyle: {
                      color: '#ccc'
                    },
                    inRange: {
                      color: (function () {
                        var colors = ['#bcd3bb', '#e88f70', '#edc1a5', '#9dc5c8', '#e1e8c8', '#7b7c68', '#e5b5b5', '#f0b489', '#928ea8', '#bda29a']
                        return colors.concat(colors)
                      })()
                    }
                  }
                ],
                series: [
                  {
                    type: 'scatter',
                    itemStyle: itemStyle,
                    data: data.series[0],
                    symbolSize: function (val) {
                      return sizeFunction(val[2])
                    }
                  }
                ],
                animationDurationUpdate: 1000,
                animationEasingUpdate: 'quinticInOut'
              },
              options: []
            }
      
            for (var n = 0; n < data.timeline.length; n++) {
              option.baseOption.timeline.data.push(data.timeline[n])
              option.options.push({
                title: {
                  show: true,
                  text: data.timeline[n] + ''
                },
                series: {
                  name: data.timeline[n],
                  type: 'scatter',
                  itemStyle: itemStyle,
                  data: data.series[n],
                  symbolSize: function (val) {
                    return sizeFunction(val[2])
                  }
                }
              })
            }
      
            myChart.setOption(option)
          }
        }
      
        customElements.define('com-sap-sample-echarts-life_expectancy2', SampleLifeExpectancy2)
      })()