Skip to Content

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:

/wp-content/uploads/2016/03/nodejs_909507.jpg


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):

/wp-content/uploads/2016/03/pm2_909508.jpg

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!

Speedometer.jpg

To report this post you need to login first.

51 Comments

You must be Logged on to comment or reply to a post.

  1. Alfons Gonzalez Comas

    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

    (0) 
    1. Hayden Gill

      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.

      (0) 
      1. Matthew Shaw

        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

        (0) 
        1. Alfons Gonzalez Comas

          Hi Matthew,

           

          Any update about the D3 based sample? This google chart implementation is a good demo of charting capabilities but is restricted to charts delivered by Google.

           

          Thx

          (0) 
  2. Thomas Nielsen

    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 🙂

    (0) 
  3. Liankun Zhang

    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?

    (0) 
  4. Craig Wilson

    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?

    (0) 
    1. Pascal GAULIN Post author

      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.

      (0) 
  5. Craig Wilson

    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.

    (0) 
    1. Pascal GAULIN Post author

      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.

      (0) 
    2. Arnaud DEVELAY

      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

      (0) 
      1. Craig Wilson

        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!

        (0) 
          1. Craig Wilson

            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.

            (0) 
            1. Pascal GAULIN Post author

              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.

              (0) 
  6. Matthew Shaw

    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.

    (0) 
    1. Pascal GAULIN Post author

      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.

      (0) 
      1. Francisco Almeida

        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)

        (0) 
  7. Craig Wilson

    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!

    task data image.png

    {

      “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”

        }

      ]

    }

    (0) 
  8. Arnaud DEVELAY

    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

    (0) 
  9. Craig Wilson

    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?

    (0) 
    1. Pascal GAULIN Post author

      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.

      (0) 
  10. Timo ROUHUNKOSKI

    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

    (0) 
    1. Arnaud DEVELAY

      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) 
  11. Timo ROUHUNKOSKI

    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>

    (0) 
    1. Pascal GAULIN Post author

      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.

      (0) 
      1. vivek kumar

        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

        (0) 
        1. Pascal GAULIN Post author

          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.

          (0) 
  12. Craig Wilson

    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.

    (0) 
    1. Pascal GAULIN Post author

      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.

      (0) 
      1. Craig Wilson

        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?

        (0) 
  13. Richard Wei

    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.

     

    (0) 
      1. Jose COUTIN

        Hi Pascal,
        can I have the file CustomElementsGoogleCharts.txt, couldn’t see it in this article.

         

        Thanks,

         

        Regards

         

        José

         

        (0) 
  14. Devesh Mishra

    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.

    (0) 
  15. Pascal GAULIN Post author

    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);
    };

    (0) 
    1. Pascal GAULIN Post author

      Hi,

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

      Regards,

      Pascal.

       

      (0) 
  16. Abhi SAP

    Hi Pascal,

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

    can you send it to me at asy041atgmail

    (0) 
  17. Milind Chavan

    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.

    (0) 

Leave a Reply