Skip to Content
Technical Articles

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.

/
12 Comments
You must be Logged on to comment or reply to a post.
  • 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

     

    • 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

  • 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

    • 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

  • 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

  • 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

  • Hi Jason,

     

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

     

    Thanks,

    Girish Lakhani

    • 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)
      })()