Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
pascal_gaulin
Advisor
Advisor
24,801

DISCLAIMER: The SCN, Content, and Services are being provided to You AS IS. To the fullest extent allowable by law, SAP does not guarantee or warrant any features or qualities of the SCN, Content, or Services or give any undertaking with regard to any other quality. Statements and explanations to SCN, Content or Services in promotional material or on SCN and in the documentation are made for explanatory purposes only; they are not meant to constitute any guarantee or warranty of certain features. No warranty or undertaking shall be implied by a User from any published SAP description of or advertisement except to the extent SAP has expressly confirmed such warranty or undertaking in writing. Warranties are validly given only with the express written confirmation of SAPs management.


One of the great features delivered in Web Intelligence 4.2 is the “Custom Elements” feature. Custom Elements are external visualizations defined by you or by third-parties and which can be used in your Web Intelligence reports, like any native chart.

A Custom Element can be anything you want: a new type of chart or table, or any other kind of visualization, as long as it complies with the following rules:

  • Support for at least one media type output – text/HTML is preferred, bitmap format is also recommended to be able to print or publish the Custom Element to PDF or Excel
  • Support for the Web Intelligence metadata and data feeding model
  • (In a future release) Support for the Web Intelligence settings model

Multiple Custom Elements can be delivered by a same service. This service is accessed through a simple HTTP URL, managed from the BOE Central Management Console.

Public APIs have been defined to support the communication between the Web Intelligence clients and the Custom Elements service. See the documentation on SAP Help Portal: http://help.sap.com/businessobject/product_guides/sbo42/en/sbo42sp1_webisl_dev_guide_en.pdf

Pre-requisites for this sample

This sample is based on Google Charts (see https://developers.google.com/chart/) and uses some open source software as well as a proprietary JavaScript application to wrap the Google Charts API in a Custom Elements service.

Get the necessary open source software

In this sample, we chose to run the service on a very simple JavaScript server called NodeJS.  It is available for free on NodeJS website: http://nodejs.org.


Download and execute the MSI file for Windows (in this sample we are assuming the installation is done on Microsoft Windows). Note that for the purpose of this sample, we have used NodeJS v5.1.0.


In order to create a bitmap output from your Custom Elements, you will also need to install PhantomJS, available on the PhantomJS website: http://phantomjs.org. It comes as a ZIP file:

  1. Extract the EXE file from the ZIP
  2. Paste it into the NodeJS folder (“C:\Program Files\nodejs”, by default)

Finally, you will also need a few NodeJS plugins to execute the sample application. To download and install these plugins:

  1. Open a command window in Administrator mode in the NodeJS folder
  2. Set the npm proxy: this is mandatory if you access the internet through a proxy server, since the following instructions will download additional packages:

    npm config set proxy "http://your_proxy:port"

    3. Type in the following instructions (do not copy and paste, to prevent special characters from being copied):

        npm install phantom-proxy (necessary to use phantomJS from nodeJS)

        npm install xmldoc (necessary to parse the XML code)

        npm install body-parser (necessary to parse the commands from Web Intelligence)

        npm install pm2 –g (PM2 is a NodeJS process manager and is mandatory to manage your Custom Elements service)


Note that there are a few errors and warnings when installing these plugins but they are not blocking:



If the instructions “npm install” run in a few seconds, then it is very likely that the proxy setting is incorrect. Typically, these instructions should take a few minutes to download and install each additional package. All plugins are installed in the “node_modules” sub-folder of NodeJS.


Build your Custom Elements service

Create a JavaScript program to use the Google Charts API

In the NodeJS folder, save the “CustomElementsGoogleCharts.txt” file attached to this article and change its file name extension into CustomElementsGoogleCharts.js. This application contains the necessary JavaScript code to use a limited set of the Google Charts API as Custom Elements in Web Intelligence documents.

You will need to modify this code to choose a port number. For instance:

// Set the port number

app.listen(8095);


Start the sample

Using PM2, you are now ready to start your Custom Elements service.

  1. Open a command window in the NodeJS folder.
  2. Type in the following command:

    pm2 start "CustomElementsGoogleCharts.js"


If successful, you should now see in the command window a table showing CustomElementsGoogleCharts running as a service (pid value might differ from the example below):


Concerning the PM2 NodeJS process manager, the following commands can be useful:

    pm2 list                                            Shows all processes installed on your NodeJS server

    pm2 stop <App name|id|all>        Stops all or the specified JavaScript application

    pm2 restart <App name|id|all>  Restarts all or the specified JavaScript application

    pm2 delete <App name|id|all>    Removes all or the specified JavaScript application from the server

    pm2 reload <App name|all>          Reloads all or the specified JavaScript application - useful after you have modified the application


Test your Custom Elements service


Test the service in a browser: http://your_service:port  (no backslash at the end of the URL!). The port number is the one set in your service.

If all goes well, you should see this message in your browser: “Server up and running!” If you don’t see that message, check the NodeJS error log file stored in C:\Users\your_user_account\.pm2\logs.

More information on how to debug NodeJS applications can be found on the GitHub Web site: https://github.com/joyent/node/wiki/Using-Eclipse-as-Node-Applications-Debugger

Plug your Custom Elements service into Web Intelligence

This last step is the easiest! In order to use your Custom Elements service in Web Intelligence, you need to declare that service in the BI Platform Central Management Console (CMC).


In the Custom Elements tab of the Web Intelligence application parameters:

  1. In the CMC, click on “Applications”, right-click on “Web Intelligence” and select “Properties”
  2. In the “Properties” dialog box, click on the “Custom Elements” tab and then click on “Add Service…”
  3. Give a name to your service
  4. Enter its URL. This should be “http://your_service:port” (no backslash at the end of the URL!)
  5. Click on “Test” to make sure the service is alive and correctly answers all tests. If it does, then the supported media should be displayed.
  6. Keep the default “Element Format” value and click OK.
  7. On the next screen, make sure you click in the checkbox to enable your service, then save and close the window.
  8. Next time you edit a Web Intelligence document on that BI Platform you should see the Custom Elements button in the Web Intelligence toolbar. If you click on that button, you should be able to insert one of the proposed Google Charts in the document you are editing.


That’s it!

76 Comments
Matthew_Shaw
Product and Topic Expert
Product and Topic Expert
0 Kudos

Wonderful document, thank you so much Pascal

With this I was able to build this

alfons_gonzalez
Active Participant
0 Kudos

Great. This looks very promising. I am sure that this will make WebI a solid candidate for advanced visualization pruposes with outstanding calculation capabilities!

Would be possible to include (in the future) another sample of custom element service based on the other well-known API visualization: D3.js

Thanks

hayden_gill
Participant
0 Kudos

It really would be great to see something based on D3, as quite a few Lumira extensions are based on D3 as well. It would let us see how easy/difficult it will be to leverage the work the Lumira dev community has put in.

Matthew_Shaw
Product and Topic Expert
Product and Topic Expert
0 Kudos

Thank you Hayden and Alfons. Over the next week or so I'm going to try and take a Lumira Extension and bring it into Webi. Webi needs a 'wrapper' around it, but in theory its quite easy. I'll keep you posted! Regards Matthew

hayden_gill
Participant
0 Kudos

That would be fantastic, especially if you can walk us through the steps required/where the two interfaces differ.

Verakso
Participant
0 Kudos

The official documentation on how to make the corresponding web service is a bit vague, so this is an excellent and understandable example.

From what I have heard, this was also presented at the DSUG event last month with a more extended example. It would be nice if that presentation could be shared here on SCN as well :smile:

pascal_gaulin
Advisor
Advisor
0 Kudos

Hello Thomas,

At the DSAG, we have shown early previews of what some of SAP partners are currently building with this new feature... :smile:

Galigeo WebIntelligence Custom Elements - YouTube

Embedding CMaps Analytics in Webi 4.2 - YouTube

    Pascal.

Former Member
0 Kudos

Hi Pascal,

From the user guide, I found that there is a query parameter 'locale'. So I want to know how to use this query parameter, do you have any ideas?

arnaud_develay
Associate
Associate
0 Kudos

If you want a better integration with Web Intelligence, you should use this parameter to localize your response.

For example, when Web Intelligence retrieves the list of supported visualizations your service will be called using the following URL:

http://hostname:port/api/visualizations?locale=en_US

In this case, you should provide a JSON response with localized name and description.

Former Member
0 Kudos

Works great, thank you Pascal!

craig_wilson2
Explorer
0 Kudos

I'm trying to create a custom element using .NET.  For the render, the documentation is contradictory.  It says:

Request

URI: api/visualizations/render<vizID>/render

HTTP Method: POST

Then it gives the example:

Request Example:POST /api/visualizations/funnel/feeds/render?locale=en_US

I'm having trouble getting it to Render.  Could you let me know what the correct path should be?

pascal_gaulin
Advisor
Advisor
0 Kudos

Hello Craig,

This is en error in the documentation. It has been corrected in the next version (not yet available).

The documentation should read:

URI: api/visualizations/<vizID>/render

And the example is:

POST /api/visualizations/funnel/render?locale=en_US

Regards,

    Pascal.

craig_wilson2
Explorer
0 Kudos

I'm not able to create more than 2 dimensional feeds. When I do, there is an informational message at the top of the custom element saying that only 2 of type dimension are allowed.  I'm trying to create a Google Gantt chart, which needs Task Name, Start Date and End Date at a bare minimum.  Why is there a limit on dimensions?  The Gantt chart could use up to 7 dimensions.

pascal_gaulin
Advisor
Advisor
0 Kudos

Hello Craig,

Although you can define and use as many feeds as you want, the number of dimensional axis is limited to 2. This is because the Custom Elements feeding model is based on the Web Intelligence chart engine feeding model. In all WebI charts, the feeding works like in a cross-table, i.e. with a maximum of 2 axis.

Regards,

Pascal.

Matthew_Shaw
Product and Topic Expert
Product and Topic Expert
0 Kudos

Perhaps a small, but important note! This solution will work in Web Intelligence, but not in Web Intelligence Rich Client. When using Web Intelligence Rich Client, the visualisation will appear blank.

pascal_gaulin
Advisor
Advisor
0 Kudos

Hi Matthew,

This is because the Custom Elements service goes through the BOE server where it has been configured. The Rich Client therefore needs to be connected to that BOE server in order to show the Custom Elements visualizations.

Best,

Pascal.

craig_wilson2
Explorer
0 Kudos

I can't understand what is going on with the data that Webi is passing to the custom element.  Here is a picture of the table, and below is the JSON from Fiddler of the render HTTP POST.

Oddities:

1. It is not combining the two "5/9/16" values.  I think this may be a leading or trailing space in my data

2. How do I relate Task Name with Task Start Date?

3. The measures are even more crazy.  I have no idea what to make of them or how to deal with them!

{

  "width": 400,

  "height": 300,

  "dpi": 96,

  "font": {

    "name": "Arial",

    "size": 9,

    "color": "#333333",

    "isBold": false,

    "isItalic": false

  },

  "feeding": [

    {

      "id": "task-name",

      "expressions": [

        {

          "dataId": 1

        }

      ]

    },

    {

      "id": "start-date",

      "expressions": [

        {

          "dataId": 3

        }

      ]

    },

    {

      "id": "duration",

      "expressions": [

        {

          "dataId": 5

        }

      ]

    },

    {

      "id": "percent-complete",

      "expressions": [

        {

          "dataId": 7

        }

      ]

    }

  ],

  "data": [

    {

      "values": {

        "dataType": "string",

        "dataStructure": "simple",

        "rawvalues": [

          "DMR,

          "DMR Ops",

          "IMR",

          "PjMR",

          "PMR",

          "PMR Ops",

          "PPR",

          "SMR for Partners",

          "SMR for Services"

        ],

        "cardinality": 1

      },

      "id": "1",

      "title": "Task Name",

      "type": "dimension"

    },

    {

      "values": {

        "dataType": "string",

        "dataStructure": "simple",

        "rawvalues": [

          "2/1/16",

          "4/11/16",

          "5/9/16",

          "5/9/16",

          "5/18/16",

          "6/8/16"

        ],

        "cardinality": 1

      },

      "id": "3",

      "title": "Task Start Date",

      "type": "dimension"

    },

    {

      "values": {

        "dataType": "double",

        "dataStructure": "simple",

        "rawvalues": [

          [

            128,

            1.7e+308,

            1.7e+308,

            123,

            65,

            1.7e+308,

            53,

            1.7e+308,

            1.7e+308

          ],

          [

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            0,

            1.7e+308,

            1.7e+308,

            1.7e+308

          ],

          [

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            4

          ],

          [

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            0,

            1.7e+308

          ],

          [

            1.7e+308,

            1.7e+308,

            0,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308

          ],

          [

            1.7e+308,

            6,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308

          ]

        ],

        "cardinality": 2

      },

      "id": "5",

      "title": "Task Duration (days)",

      "type": "measure"

    },

    {

      "values": {

        "dataType": "double",

        "dataStructure": "simple",

        "rawvalues": [

          [

            8,

            1.7e+308,

            1.7e+308,

            3,

            6,

            1.7e+308,

            25,

            1.7e+308,

            1.7e+308

          ],

          [

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            11,

            1.7e+308,

            1.7e+308,

            1.7e+308

          ],

          [

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            13

          ],

          [

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            9,

            1.7e+308

          ],

          [

            1.7e+308,

            1.7e+308,

            18,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308

          ],

          [

            1.7e+308,

            14,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308,

            1.7e+308

          ]

        ],

        "cardinality": 2

      },

      "id": "7",

      "title": "Task Percent Complete (fake data)",

      "type": "measure"

    }

  ]

}

arnaud_develay
Associate
Associate
0 Kudos

Hello Craig,

There are a lot of NaN values (1.7e+308) in your dataset.

I think there is something wrong with your feeding declaration. Can you paste the result of a call to /api/<vizid>/feeds ?

Thanks,

Arnaud

arnaud_develay
Associate
Associate
0 Kudos

Hello Craig,

You can have as much dimensional feed as you want, but each dimesional feeds must be bound to one of our 2 axis: the dataset model of CustomElement is the same as the one for CrossTable.

Regards,

Arnaud

craig_wilson2
Explorer
0 Kudos

Ah.  Thank you for that clarification.  I reviewed the CrossTable and saw that I can bind multiple dimensions to a single axis.  That was what I needed to pass multiple dimensions to the Google Gantt chart, and I've now got it working!  Thank you!

hayden_gill
Participant
0 Kudos

Hi Craig, where did you find the documentation on the CrossTable ? Thanks

arnaud_develay
Associate
Associate
0 Kudos

You're welcome.

Can you share a screenshot of the result ?

craig_wilson2
Explorer
0 Kudos

I didn't actually look at documentation.  I just remembered that for many charts in general, you can add more than 1 dimension to a single axis.  The Web Intelligence user guide might document this, I'm not sure.  The Webi charting engine tries to interpret all the data and pass what would be the final representation on a chart.  For my Gantt chart, that would not work because I need more than 2 dimensions and I don't want Webi to combine/interpret the data.  I just need the raw data for each dimension, and the order of the rows needs to be preserved across the data so that I can reconstruct a row with multiple columns in my code.  Using multiple dimensions on a single axis accomplishes this.  I'm posting a screenshot in response to Arnauds request below.

craig_wilson2
Explorer
0 Kudos

pascal_gaulin
Advisor
Advisor
0 Kudos

Thank you Craig.

Indeed, Custom Elements use the same feeding model than WebI traditional charts and this model is based on a cross-table, i.e.: two axis with as many dimensions as you wish on each axis.

franciscoalmeida
Explorer
0 Kudos

Thanks a lot, great guide.

I have not been able to get it to work with WebI Java Applet - it only worked with the HTML version (although I can view the custom elements via the applet interface - I cannot create a new custom element via Applet). Using 4.2 SP2.

com.businessobjects.rebean.wi.exception.CustomElementExceptions$CustomElementAvailableTypesException: An exception occured when retrieving the feeding metadata from the CustomElement third party service. (Error: RWI 00634)

craig_wilson2
Explorer
0 Kudos

Is it possible to use the custom element html format when viewing a report in the Webi html viewer and then substitute the custom element image format when the report is exported/viewed in PDF mode?  How does one accomplish this so that one report can be used for both viewing (html format) and exporting (image format) of a custom element?

pascal_gaulin
Advisor
Advisor
0 Kudos

Hello Craig,

Yes, it is possible. In the CMC, set the media to text/html. Then, WebI clients will automatically request html content when displaying the custom element content in documents, while WebI server will keep requesting for bitmap content when exporting to PDF or Excel.

Whatever media you set in the CMC, WebI server will always request a bitmap content when exporting to PDF or Excel.

Regards,

    Pascal.

craig_wilson2
Explorer
0 Kudos

Perfect!  Thanks!

0 Kudos

Hi,

Is there a bug that the second try call is also not POST (Apache access log):

10.1.9.157 - - [01/Aug/2016:13:02:31 +0000] "POST /custom/api/visualizations/EmailAlert/render?format=text/html&locale=en_US HTTP/1.1" 301 310

10.1.9.157 - - [01/Aug/2016:13:02:31 +0000] "GET /custom/api/visualizations/EmailAlert/render/?format=text/html&locale=en_US HTTP/1.1" 200 117

Thanks, Timo

arnaud_develay
Associate
Associate
0 Kudos

Hello,

I don't think this call comes from Web Intelligence. Every calls to /render endpoint are made with the verb POST. GET is simply not implemented on our side.

Arnaud

0 Kudos

Here is speedometer gauge but using PHP. The render file cannot be in /render/index.php, so file "render" needs to be able to execute PHP. Add this to httpd.conf:

<Location "/gauge/api/visualizations/gauge">

  ForceType application/x-httpd-php

</Location>

FEEDS:

<?php

header("Content-type:application/json");

echo '{

"feeds": [

{

"id": "Title",

"name": "Title Text",

"description": "Title text",

"type": "dimension",

"axis": "0",

"min": "1",

"max": "1"

},

{

"id": "Reverse",

"name": "Reverse Colors (Y/N)",

"description": "Reverse Colors",

"type": "dimension",

"axis": "1",

"min": "1",

"max": "1"

},

{

"id": "Value",

"name": "Value to Display",

"description": "Value to Display",

"type": "measure",

"min": "1",

"max": "1"

},

{

"id": "Min",

"name": "Scale Minimum Value",

"description": "Scale Minimum Value",

"type": "measure",

"min": "0",

"max": "1"

},

{

"id": "Max",

"name": "Scale Maximum Value",

"description": "Scale Maximum Value",

"type": "measure",

"min": "0",

"max": "1"

},

{

"id": "Red",

"name": "Red Band Start",

"description": "Red Band Size",

"type": "measure",

"min": "0",

"max": "1"

},

{

"id": "Yellow",

"name": "Yellow Band Start",

"description": "Yellow Band Size",

"type": "measure",

"min": "0",

"max": "1"

},

{

"id": "Green",

"name": "Green Band Start",

"description": "Green Band Size",

"type": "measure",

"min": "0",

"max": "1"

},

{

"id": "Size",

"name": "Size of the Graph",

"description": "Size of Graph",

"type": "measure",

"min": "0",

"max": "1"

}]}';

?>

RENDER:

<?php

$a = file_get_contents('php://input');

header("Content-type:text/html");

$b = json_decode($a,true);

isset( $b['data']['0']['values']['rawvalues'][0]) ? $label = $b['data']['0']['values']['rawvalues'][0] : $label = '' ;

isset( $b['data']['1']['values']['rawvalues'][0]) ? $reverse = $b['data']['1']['values']['rawvalues'][0] : $reverse = 'N' ;

isset( $b['data']['2']['values']['rawvalues'][0][0]) ? $value = $b['data']['2']['values']['rawvalues'][0][0] : $value = 0 ;

isset( $b['data']['3']['values']['rawvalues'][0][0]) ? $min = $b['data']['3']['values']['rawvalues'][0][0] : $min = 0 ;

isset( $b['data']['4']['values']['rawvalues'][0][0]) ? $max = $b['data']['4']['values']['rawvalues'][0][0] : $max = 100 ;

isset( $b['data']['5']['values']['rawvalues'][0][0]) ? $red = $b['data']['5']['values']['rawvalues'][0][0] : $red = 100 ;

isset( $b['data']['6']['values']['rawvalues'][0][0]) ? $yellow = $b['data']['6']['values']['rawvalues'][0][0] : $yellow = 100 ;

isset( $b['data']['7']['values']['rawvalues'][0][0]) ? $green = $b['data']['7']['values']['rawvalues'][0][0] : $green = 100 ;

isset( $b['data']['8']['values']['rawvalues'][0][0]) ? $size = $b['data']['8']['values']['rawvalues'][0][0] : $size = '200' ;

?>

<html>

  <head>

    <script type="text/javascript" src="https://www.google.com/jsapi"></script>

    <script type="text/javascript">

      google.load("visualization", "1", {packages:["gauge"]});

      google.setOnLoadCallback(drawChart);

      function drawChart() {

        var data = google.visualization.arrayToDataTable([

          ['Label', 'Value'],

          ['<?php echo $label; ?>', <?php echo $value; ?>]

        ]);

        var options = {

          <?php if ($reverse=='N') { ?>

          redFrom: <?php echo $red; ?>, redTo: <?php echo $yellow; ?>,

          yellowFrom:<?php echo $yellow; ?>, yellowTo: <?php echo $green; ?>,

          greenFrom:<?php echo $green; ?>, greenTo: <?php echo $max; ?>,

          <?php } else { ?>

           redFrom: <?php echo $red; ?>, redTo: <?php echo $max; ?>,

          yellowFrom:<?php echo $yellow; ?>, yellowTo: <?php echo $red; ?>,

          greenFrom:<?php echo $green; ?>, greenTo: <?php echo $yellow; ?>,

          <?php } ?>

          height: <?php echo $size; ?>,

          minorTicks: 5, max: <?php echo $max; ?>, min: <?php echo $min; ?>

        };

        var chart = new google.visualization.Gauge(document.getElementById('chart_div'));

        chart.draw(data, options);

      }

    </script>

  </head>

  <body>

    <div id="chart_div"></div>

  </body>

</html>

vivek_kumar1
Explorer
0 Kudos

Hi,

Please let me know if anyone worked on d3 visualization custom element or have brief idea about this.

Thanks,

Vivek

pascal_gaulin
Advisor
Advisor
0 Kudos

Hi Vivek,

Yes, we have tested some D3 visualizations, such as the famous calendar example. That works fine. Like with the Google chart sample we have documented in this article, it needs a little bit of coding for the wrapper: to list the visualization, its feeds and then to render it.

Regards,

Pascal.

vivek_kumar1
Explorer
0 Kudos

Hi Pascal,

Thanks for your reply.

Could you please share some sample code for wrapping?

will this code work for Lumira CVOM extensions too?

Thanks,

Vivek

pascal_gaulin
Advisor
Advisor
0 Kudos

Hi Vivek,

The only code we can share, is the one in this sample. But, basically, it's very similar to the code used with a D3 visualization, or a Lumira visualization or any other kind of javascript visualization... You just need to figure out how to map the Custom Elements APIs to the visualization library you want to use.

Regards,

Pascal.

craig_wilson2
Explorer
0 Kudos

Pascal,

Have you worked with the new ability in SP3 to provide Custom Settings?  I can get custom settings to show up for the user to interact with, but they are not being passed back to me in the render call (api/visualizations/<vizID>/render).  The documentation says "Rendering setting values are saved in the Web Intelligence document" on page 62 of the "SAP BusinessObjects BI Developer’s Guide for Web Intelligence and the BI Semantic Layer", but that doesn't appear to be the case for me.  Once I provide a value for a custom setting, it seems to disappear.

pascal_gaulin
Advisor
Advisor
0 Kudos
Hello Craig,

I have personally not played with the new Custom Elements settings API in Web Intelligence 4.2 SP3. However some of my colleagues did. I also know that some SAP partners are currently working with it.

I know one bug has been found with the "string" type parameter, in the dHTML client, but I have not heard of any other problem...
Regards,


Pascal.
Former Member
0 Kudos

Hi Pascal,
can I have this attachment (CustomElementsGoogleCharts.txt, couldn’t see it in this article), as I’m newbie to Java and have no idea on how to build one from scratch? and when you said testing service http://your_service:port, what did you mean, is it BO server URL or what else.

 

craig_wilson2
Explorer
0 Kudos

I created a ticket with SAP support, but they are struggling to even begin to help me because they have not worked at all with Custom Elements. Could you ask your colleagues to describe the basic flow of the custom settings?  As I understand it, I create a json string that defines the format for the custom settings I want.  This json format is returned to Web Intelligence when the GET api/visualizations/<viz_id>/settings is invoked when a user right clicks a custom element and chooses format.  From that point on, Web Intelligence server side code should handle saving the values the user provides?  It should also pass those values to the POST api/visualizations/<viz_id>/render call?  But Web Intelligence is doing neither.  It is not saving, and it is not passing setting values back.  I must be missing something?

Former Member
0 Kudos
Hi,

As per my understanding, there are mainly two parts i.e.

1. Develop your custom element
2. Publish your custom element using NodeJS as a URL

In order to achieve the first step, is there any reference document available ? I am not getting it anywhere. Please help me to find it.
pascal_gaulin
Advisor
Advisor
0 Kudos
Dear all,

It seems the sample attachment has been lost during the SCN migration. I'm deeply sorry about that. Here it is again, inline :

// DISCLAIMER: The SCN, Content, and Services are being provided to You AS IS.
// To the fullest extent allowable by law, SAP does not guarantee or warrant any
// features or qualities of the SCN, Content, or Services or give any undertaking
// with regard to any other quality. Statements and explanations to SCN, Content
// or Services in promotional material or on SCN and in the documentation are
// made for explanatory purposes only; they are not meant to constitute any
// guarantee or warranty of certain features. No warranty or undertaking shall be
// implied by a User from any published SAP description of or advertisement
// except to the extent SAP has expressly confirmed such warranty or undertaking
// in writing. Warranties are validly given only with the express written
// confirmation of SAPs management.

// For a description of the Google visualizations used in this application,
// see https://developers.google.com/chart/interactive/docs/

// Initialization
var express = require('express');
var bodyParser = require('body-parser')
var path = require('path');
var phantomProxy = require('phantom-proxy');
var fs = require('fs');
var util = require('util');
var app = express();
app.use(bodyParser.json());
var urlencodedParser = bodyParser.urlencoded({ extended: true });
var CR = String.fromCharCode(13);

// Set the port number
app.listen(port);

app.get("/", function(req, res) {
res.send("Server Up and Running !");
});

app.get("/api/about", function(req, res) {
logInfo("About");
res.send("Google Visualizations");
});

app.get("/api/formats", function(req, res) {
logInfo("Start Format");
var format = {"formats": ["text/html", "image/png"]};
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(format));
logInfo("Stop Format");
});

app.get("/api/visualizations", function(req, res) {
logInfo("Start Visualizations");
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(factory));
logInfo("Stop Visualizations");
});

app.get("/api/visualizations/:vizid/feeds", function(req, res) {
logInfo("Start feeds");
var viz = factory.getViz(req.params.vizid);
logInfo(" Viz ID => " + viz.name);

var feedArray = [];
if (!(viz.categoryAxisMin == 0 && viz.categoryAxisMax == 0)) {
feedArray.push(
{
"id": "category-axis",
"name": viz.categoryAxisName,
"description": viz.categoryAxisDesc,
"axis": "0",
"type": "dimension",
"min": ""+viz.categoryAxisMin+"",
"max": ""+viz.categoryAxisMax+""
}
);
}
if (!(viz.regionColorMin == 0 && viz.regionColorMax == 0)) {
feedArray.push(
{
"id": "region-color",
"name": viz.regionColorName,
"description": viz.regionColorDesc,
"axis": "1",
"type": "dimension",
"min": ""+viz.regionColorMin+"",
"max": ""+viz.regionColorMax+""
}
);
}
if (!(viz.primaryValueMin == 0 && viz.primaryValueMax == 0)) {
feedArray.push(
{
"id": "primary-values",
"name": viz.primaryValueName,
"description": viz.primaryValueDesc,
"type": "measure",
"min": ""+viz.primaryValueMin+"",
"max": ""+viz.primaryValueMax+""
}
);
}
var feeds = {
"feeds": feedArray
};
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(feeds));
logInfo("Stop feeds");
});

app.post("/api/visualizations/:vizid/render", function(req, res) {
logInfo("Start Render");
var viz = factory.getViz(req.params.vizid);
var html = viz.generate(req);
var height = req.body.height;
var width = req.body.width;
var format = req.headers.accept;
logInfo(" Viz ID => "+viz);

// Bitmap generation
if(format == "image/png") {
var self = this;
var randomnumber=Math.floor(Math.random()*101)
var path = 'html_page_'+randomnumber+'.html';
fs.writeFileSync(path, html);
phantomProxy.create(function(proxy) {
proxy.page.open(path, function (result) {
proxy.page.set('viewportSize', { width: width, height: height });
proxy.page.set('clipRect', { top: 0, left: 0, width: width, height: height });
proxy.page.waitForSelector("#chart", function (result){
proxy.page.renderBase64('PNG', function(result){
var bitmap = new Buffer(result, 'base64');
res.writeHead(200, {'Content-Type': 'image/png' });
res.end(bitmap, 'binary');
phantomProxy.end();
fs.unlinkSync(path); // Delete temp file
});
});
});
});
}

// HTML generation
else {
res.writeHead(200, {'Content-Type': 'text/html' });
res.end(html);
}
logInfo("Stop Render");
});

var VizFactory = function() {
this.visualizations = [];
this.getViz = function(id) {
for (var index = 0 ; index < this.visualizations.length ; index++) {
if (this.visualizations[index].id === id) return this.visualizations[index];
}
};
};

var Visualization = function(id, name, catMin, catMax, regMin, regMax, valMin, valMax, catName, catDesc, regName, regDesc, valName, valDesc) {
this.id = id;
this.name = name;
this.categoryAxisMin = catMin;
this.categoryAxisMax = catMax;
this.categoryAxisName = catName;
this.categoryAxisDesc = catDesc;
this.regionColorMin = regMin;
this.regionColorMax = regMax;
this.regionColorName = regName;
this.regionColorDesc = regDesc;
this.primaryValueMin = valMin;
this.primaryValueMax = valMax;
this.primaryValueName = valName;
this.primaryValueDesc = valDesc;
};


/*******************
* Google Pie Chart
*
* 1: ID
* 2: Name
* 3: Category feeds min = 1
* 4: Category feeds max = 1
* 5: Region feeds min = 0 (optional)
* 6: Region feeds max = 1
* 7: Value feeds min = 1
* 8: Value feeds max = 1
* 9: Category feeds name
* 10: Category feeds description
* 11: Region feeds name
* 12: Region feeds description
* 13: Value feeds name
* 14: Value feeds description
*/

var vizPie = new Visualization("googlepie", "Google Pie", 1, 1, 0, 1, 1, 1, "Sectors Dimension", "Dimension on which the pie will be splitted", "Treillis Dimension", "Dimension to repeat the pies", "Measure", "Measure values");

vizPie.generate = function(req) {
var w = req.body.width?req.body.width:800;
var h = req.body.height?req.body.height:800;
var hCell = h;
var dimData = getData(req, getFeedings(req, "category-axis")[0].dataId);
var regFeed = getFeedings(req, "region-color");
var mesData = getData(req, getFeedings(req, "primary-values")[0].dataId);
var count0 = 1;
var count1 = 1;
var values = [];
var value = [];

if (regFeed) {
if (regFeed.length === 1) {
var regData0 = getData(req, regFeed[0].dataId);
count0 = regData0.values.rawvalues.length;
for (var j = 0 ; j < count0 ; j++) {
value = [];
var reg = regData0.values.rawvalues[j];
for (var i = 0 ; i < dimData.values.rawvalues.length ; i++) {
var dim = dimData.values.rawvalues[i];
var mes = mesData.values.rawvalues[j][i];
if (mes == 1.7e+308) mes = 0; // 1.7e+308 = WebI overflow
value.push([dim.toString(), mes]);
}
values.push(value);
}
} else {
var regData0 = getData(req, regFeed[0].dataId);
var regData1 = getData(req, regFeed[1].dataId);
var regValues0 = distinct(regData0.values.rawvalues);
var regValues1 = distinct(regData1.values.rawvalues);
count0 = regValues0.length;
count1 = regValues1.length;
hCell = Math.floor(h/count1);
var allData = {};
for (var i = 0 ; i < mesData.values.rawvalues.length ; i++) {
if (!allData[regData0.values.rawvalues[i]]) allData[regData0.values.rawvalues[i]] = {};
allData[regData0.values.rawvalues[i]][regData1.values.rawvalues[i]] = mesData.values.rawvalues[i];
}
for (var j = 0 ; j < count1 ; j++) {
for (var i = 0 ; i < count0 ; i++) {
var k = i + count0*j;
value = [];
if (allData[regValues0[i]] && allData[regValues0[i]][regValues1[j]]) {
var m = allData[regValues0[i]][regValues1[j]];
for (var l = 0 ; l < dimData.values.rawvalues.length ; l++) {
var dim = dimData.values.rawvalues[l];
var mes = m[l];
if (mes == 1.7e+308) mes = 0;
value.push([dim.toString(), mes]);
}
}
values.push(value);
}
}
}
} else {
for (var i = 0 ; i < dimData.values.rawvalues.length ; i++) {
var dim = dimData.values.rawvalues[i];
var mes = mesData.values.rawvalues[i];
if (mes == 1.7e+308) mes = 0;
value.push([dim.toString(), mes]);
}
values.push(value);
}
var html =
'<!DOCTYPE html>' + CR +
'<html style="height:100%; display:table">' + CR +
' <head>' + CR +
' <script type="text/javascript" src="https://www.google.com/jsapi"></script>' + CR +
' <script type="text/javascript">' + CR +
' google.load("visualization", "1.0", {"packages":["corechart"]});' + CR +
' google.setOnLoadCallback(drawChart);' + CR +
' function drawChart() {' + CR +
' var data, chart;' + CR;
for (var j = 0 ; j < count1 ; j++) {
for (var i = 0 ; i < count0 ; i++) {
var k = j + count1 * i;
html +=
' data = new google.visualization.DataTable();' + CR +
' data.addColumn("string", "' + dimData.title + '");' + CR +
' data.addColumn("number", "' + mesData.title + '");' + CR +
' data.addRows(' + JSON.stringify(values[k]) + ');' + CR +
' var options = {' + CR;
if (count1 > 100) {
html += ' title: "' + regValues1[j] + ' / ' + regValues0[i] + '",' + CR;
} else if (count0 > 100) {
html += ' title: "' + regData0.values.rawvalues[k] + '",' + CR;
}
html +=
' backgroundColor: "transparent",' + CR +
' legend: "none"' + CR +
' };' + CR +
' new google.visualization.PieChart(document.getElementById("chart_cell_' + j + '_' + i + '")).draw(data, options);;' + CR;
}
}
html +=
' }' + CR +
' </script>' + CR +
' </head>' + CR +
' <body style="border:0; margin:0; padding:0; height:100%; vertical-align:middle; display:table-cell">' + CR +
' <table style="border:0; margin:0; padding:0; width:100%; height:100%; table-layout:fixed">' + CR;
for (var j = 0 ; j < count1 ; j++) {
html += ' <tr style="border:0; margin:0; padding:0">' + CR;
for (var i = 0 ; i < count0 ; i++) {
var k = i + i*j;
html += ' <td style="border:0; margin:0; padding:0; height:' + hCell + 'px" id="chart_cell_' + j + '_' + i + '"></td>' + CR;
}
html += ' </tr>' + CR;
}
html +=
' </table>' + CR +
' </body>' + CR +
'</html>' + CR;
return html;
};


/*********************
* Google Gauge Chart
*
* 1: ID
* 2: Name
* 3: Category feeds min = 1
* 4: Category feeds max = 1
* 5: Region feeds min = 0 (unused)
* 6: Region feeds max = 0 (unused)
* 7: Value feeds min = 1
* 8: Value feeds max = 1
* 9: Category feeds name
* 10: Category feeds description
* 11: Region feeds name
* 12: Region feeds description
* 13: Value feeds name
* 14: Value feeds description
*/
var vizGauge = new Visualization("googlegauge", "Google Gauge", 1, 1, 0, 0, 1, 1, "Gauge Dimension", "Dimension values to repeat the gauges", "N/A", "N/A", "Measure", "Gauge value (should be 1 to 100)")

vizGauge.generate = function(req) {
var w = req.body.width?req.body.width:800;
var h = req.body.height?req.body.height:800;
var dimData = getData(req, getFeedings(req, "category-axis")[0].dataId);
var mesData = getData(req, getFeedings(req, "primary-values")[0].dataId);
var values = [['Label', 'Value']];
var RenderingValues = [];

for (var i = 0 ; i < dimData.values.rawvalues.length ; i++) {
values.push([dimData.values.rawvalues[i], 0]);
RenderingValues.push(mesData.values.rawvalues[i]);
}
var html =
'<!DOCTYPE html>' + CR +
'<html style="height:100%; margin:auto; display:table">' + CR +
' <head>' + CR +
' <script type="text/javascript" src="https://www.google.com/jsapi"></script>' + CR +
' <script type="text/javascript">' + CR +
' google.load("visualization", "1", {packages:["gauge"]});' + CR +
' google.setOnLoadCallback(drawChart);' + CR +
' function drawChart() {' + CR +
' var data = google.visualization.arrayToDataTable(' + CR +
' ' + JSON.stringify(values) + CR +
' );' + CR +
' var options = {' + CR +
' width:' + w + ',' + CR +
' height:' + h + ',' + CR +
' greenFrom: 0,' + CR +
' greenTo: 20,' + CR +
' redFrom: 90,' + CR +
' redTo: 100,' + CR +
' yellowFrom:75,' + CR +
' yellowTo: 90,' + CR +
' minorTicks: 5,' + CR +
' animation : {' + CR +
' duration: 2500,' + CR +
' easing: "out"' + CR +
' }' + CR +
' };' + CR +
' var chart = new google.visualization.Gauge(document.getElementById("chart_div"));' + CR +
' chart.draw(data, options);' + CR +
' setInterval(function() { '+ CR;
for(var j = 0;j < values.length-1 ; j++){
html += 'data.setValue('+j+',1,'+ RenderingValues[j] +');' + CR;
}
html +=
' chart.draw(data,options);' + CR +
' }, 1000);' + CR +
' }' + CR +
' </script>' + CR +
' </head>' + CR +
' <body style="border:0; margin:0; padding:0; height:100%; vertical-align:middle; display:table-cell">' + CR +
' <div style="border:0; margin:0; padding:0" id="chart_div" onmouseover="drawChart();"></div>' + CR +
' </body>' + CR +
'</html>' + CR;
return html;
};


/************************
* Google Sankey Diagram
*
* 1: ID
* 2: Name
* 3: Category feeds min = 2
* 4: Category feeds max = 2
* 5: Region feeds min = 0 (unused)
* 6: Region feeds max = 0 (unused)
* 7: Value feeds min = 1
* 8: Value feeds max = 1
* 9: Category feeds name
* 10: Category feeds description
* 11: Region feeds name
* 12: Region feeds description
* 13: Value feeds name
* 14: Value feeds description
*/
var vizSankey = new Visualization("googlesankey", "Google Sankey", 2, 2, 0, 0, 1, 1, "Source & Destination Dimensions", "Source and destination dimensions", "N/A", "N/A", "Measure", "Measure values")

vizSankey.generate = function(req) {
var w = req.body.width?req.body.width:1100;
var h = req.body.height?req.body.height:900;
var dimCount = getFeedings(req,"category-axis").length;
var mesCount;
var values = [];
var V = [];
var mesData;
var FromDimId, FromDimData, ToDimId, ToDimData;

if(getFeedings(req,"primary-values")){
mesCount = getFeedings(req,"primary-values").length;
} else {
mesCount = 0;
}
for(var j = 0; j < dimCount-1; j++){
if(mesCount == 1){
mesData = getData(req, getFeedings(req, "primary-values")[0].dataId);
} else if(mesCount == dimCount-1){
mesData = getData(req, getFeedings(req, "primary-values")[j].dataId);
}
FromDimId = getFeedings(req, "category-axis")[j].dataId;
ToDimId = getFeedings(req, "category-axis")[j+1].dataId;
FromDimData = getData(req, FromDimId );
ToDimData = getData(req, ToDimId);
for (var i = 0 ; i < FromDimData.values.rawvalues.length; i++) {
var From = FromDimData.values.rawvalues[i];
var To = ToDimData.values.rawvalues[i];
if(From == To){
To += ' ' + ToDimData.title;
ToDimData.values.rawvalues[i] = To;
}
if(!V[From]){
V[From]= {};
}
if(!V[From][To]){
V[From][To]= 0;
}
if(!mesCount){
V[From][To] += 1; // =1 if no ponderation desired
} else {
V[From][To] += mesData.values.rawvalues[i];
}
}
FromDimLov = distinct(FromDimData.values.rawvalues);
ToDimLov = distinct(ToDimData.values.rawvalues);
for(var k = 0; k < FromDimLov.length;k++){
for(var l = 0; l < ToDimLov.length; l++){
if(V[FromDimLov[k]][ToDimLov[l]]){
values.push([FromDimLov[k],ToDimLov[l],V[FromDimLov[k]][ToDimLov[l]]]);
}
}
}
}
var html =
'<!DOCTYPE html>' + CR +
'<html style="height:100%; margin:auto; display:table">' + CR +
' <head>' + CR +
' <script type="text/javascript" src="https://www.google.com/jsapi"></script>' + CR +
' <script type="text/javascript">' + CR +
' google.load("visualization", "1.1", {packages:["sankey"]});' + CR +
' google.setOnLoadCallback(drawChart);' + CR +
' function drawChart() {' + CR +
' var data = new google.visualization.DataTable();' + CR +
' data.addColumn(\'string\', \'From\');' + CR +
' data.addColumn(\'string\', \'To\');' + CR +
' data.addColumn(\'number\', \'Weight\');' + CR;
for(var n = 0; n < values.length ; n++){
html +=
' data.addRows([' + CR +
' [ "'+ values[n][0] +'", "'+ values[n][1] +'", '+ values[n][2] +' ]' + CR +
' ]);' + CR;
}
html +=
' // Sets chart options.' + CR +
' var options = {' + CR +
' width: ' + w +',' + CR +
' height: ' + h + ',' + CR +
' };' + CR +
' // Instantiates and draws our chart, passing in some options.' + CR +
' var chart = new google.visualization.Sankey(document.getElementById(\'sankey_basic\'));' + CR +
' chart.draw(data, options);' + CR +
' }' + CR +
'</script>' + CR +
'</head>' + CR +
' <body style="border:0; margin:0; padding:0; height:100%; vertical-align:middle; display:table-cell">' + CR +
' <div style="border:0; margin:0; padding:0" id="sankey_basic"></div>' + CR +
' </body>' + CR +
'</html>' + CR;
return html;
}

var factory = new VizFactory();
factory.visualizations.push(vizGauge);
factory.visualizations.push(vizPie);
factory.visualizations.push(vizSankey);

function getFeedings(req, id) {
for (var i = 0 ; i < req.body.feeding.length ; i++) {
if (req.body.feeding[i].id === id) {
return req.body.feeding[i].expressions;
}
}
};

function getData(req, id) {
for (var i = 0 ; i < req.body.data.length ; i++) {
if (req.body.data[i].id == id) {
return req.body.data[i];
}
}
};

function distinct(arr) {
var values = [];
for (var i = 0 ; i < arr.length ; i++) {
var value = arr[i];
if (values.indexOf(value) === -1) {
values.push(value);
}
}
return values;
};

function logInfo(info){
var currentdate = new Date();
console.log(currentdate.toLocaleString() + " => " + info);
};
pascal_gaulin
Advisor
Advisor
0 Kudos
Hi Richard,

See below.

Regards,


Pascal.
pascal_gaulin
Advisor
Advisor
0 Kudos
Hi Devesh,

See below.

Regards,

Pascal.
Former Member
0 Kudos
Where did you get the gantt chart visualizations? Can that be exported to Excel and PDF?
lionel_valadou1
Explorer
0 Kudos
When using the Google gauge extension, is it possible to print it to a PDF file?

 
Former Member
0 Kudos
Hi Pascal,

I didn't find the file (CustomElementsGoogleCharts.txt) attached to this article.

can you send it to me at asy041atgmail
Former Member
0 Kudos
Hi Pascal,
can I have the file CustomElementsGoogleCharts.txt, couldn’t see it in this article.

 

Thanks,

 

Regards

 

José

 
0 Kudos
Hi All,

 

I have few queries :

1.We do not have internet access on our servers.So what needs to be mentioned for proxy “http://your_proxy:port

 

2.How can we get below packages installed

npm install phantom-proxy (necessary to use phantomJS from nodeJS)

npm install xmldoc (necessary to parse the XML code)

npm install body-parser (necessary to parse the commands from Web Intelligence)

npm install pm2 –g (PM2 is a NodeJS process manager and is mandatory to manage your Custom Elements service)

 

3.The above setting are for windows server can we work with Linux servers.

 

Regards.
pascal_gaulin
Advisor
Advisor
0 Kudos
Hi,

It is up to the Custom Elements service to implement the bitmap generation required for printing and publishing.

Regards,

Pascal.