Skip to Content
Author's profile photo François Henrotte

Do you know the Catmull-Rom algorithm ?

Ten years ago I was asked to display some graphics containing only 5 values, but it was to be displayed as a nice curve. “MS Excel does it automatically, we want to have the same visual result in SAP”.
Well, the requirement is easily expressed… but not easily achieved…

At those times we had already the GRAPH_MATRIX_2D function so I was able to display the 5 points. I just had to find a way to extrapolate a spline from those points. I came across the Catmull-Rom algorithm and found an implementation of it that I could translate into ABAP.

This algorithm is used mainly in the field of 3D rendering to have smooth curves.
My implementation works only on one dimension which is the height.

I give here a short program (sorry for the naming conventions but it is mainly intended to show the results of the algorithm) displaying some points and the interpolated curve. You can change the values in the SELECT-OPTIONS and see the curve related to the new values.

As usual, I used a docking container in order to avoid building a dynpro. This is also useful to keep all the code into a single program, so you can test it directly after a copy/paste.

Note: I don’t use the old graphical functions anymore, as we have now the class cl_gui_chart_engine. This one is more powerful and there is no limit to 32 points. If you are not used to this class, the program is also a good way to learn about it.

Note2: on older SAP releases, you will have to concatenate the names of the points in place of using the variables in { }.

REPORT zzcurve.

TYPES: BEGIN OF ty_point,
         y TYPE p DECIMALS 2,
       END OF ty_point.

TYPES: BEGIN OF ty_dattab,
         txt TYPE char20,
         val TYPE p DECIMALS 2,
       END OF ty_dattab.

DATA: w_point1 TYPE ty_point,
      w_point2 TYPE ty_point,
      w_point3 TYPE ty_point,
      w_point4 TYPE ty_point,
      w_new    TYPE ty_point,
      w_coeff  TYPE p DECIMALS 2,
      w_tabix  TYPE i,
      w_index  TYPE i.

DATA: ws_dattab TYPE ty_dattab,
      wt_dattab TYPE TABLE OF ty_dattab.

DATA: w_container TYPE REF TO cl_gui_docking_container.
DATA: w_graph     TYPE REF TO cl_gui_chart_engine.
DATA: w_ixml      TYPE REF TO if_ixml.
DATA: w_stream    TYPE REF TO if_ixml_stream_factory.

DATA: w_repid TYPE repid,
      w_dynnr TYPE dynnr.


SELECTION-SCREEN: BEGIN OF BLOCK b01 NO INTERVALS.
SELECT-OPTIONS: so_pnts FOR ws_dattab-val NO INTERVALS.
PARAMETERS: p_nb TYPE int4 DEFAULT 10.
SELECTION-SCREEN: END OF BLOCK b01,
                  SKIP.

PARAMETERS: p_lines TYPE c AS CHECKBOX USER-COMMAND lns.


INITIALIZATION.
  so_pnts-low = 2222.
  APPEND so_pnts.
  so_pnts-low = 1111.
  APPEND so_pnts.
  so_pnts-low = 5432.
  APPEND so_pnts.
  so_pnts-low = 3333.
  APPEND so_pnts.


AT SELECTION-SCREEN OUTPUT.
  CHECK so_pnts[] IS NOT INITIAL.

  IF w_container IS INITIAL.
*   Create global objects
    w_ixml = cl_ixml=>create( ).
    w_stream = w_ixml->create_stream_factory( ).

    w_repid = sy-repid.
    w_dynnr = sy-dynnr.
    CREATE OBJECT w_container
      EXPORTING
        repid  = w_repid
        dynnr  = w_dynnr
        side   = w_container->dock_at_bottom
        ratio  = 80
      EXCEPTIONS
        OTHERS = 1.

    CREATE OBJECT w_graph
      EXPORTING
        parent = w_container.
  ENDIF.


  PERFORM f_create_settings.

  REFRESH: wt_dattab.

  LOOP AT so_pnts.
    w_tabix = sy-tabix.
    w_index = sy-tabix + 1.

    WRITE: / 'Point', sy-tabix, so_pnts-low.

    CASE w_index.
      WHEN 2.
        CLEAR: w_point1, w_point2.
        w_point3-y = so_pnts-low.
        READ TABLE so_pnts INDEX w_index.
        w_point4-y = so_pnts-low.
      WHEN OTHERS.
        w_point1 = w_point2.
        w_point2 = w_point3.
        w_point3-y = so_pnts-low.
        READ TABLE so_pnts INDEX w_index.
        IF sy-subrc = 0.
          w_point4-y = so_pnts-low.
        ENDIF.
    ENDCASE.

    DO.
      ws_dattab-txt = |Point{ w_tabix }-{ sy-index }|.
      w_coeff = sy-index * ( 1 / p_nb ).
      IF w_coeff >= 1.
        EXIT.
      ENDIF.

      PERFORM f_derive_point USING w_point1 w_point2 w_point3 w_point4
                          CHANGING w_new.

      ws_dattab-val = w_new-y.
      APPEND ws_dattab TO wt_dattab.
    ENDDO.

    ws_dattab-txt = |Point{ w_tabix }-O|.
    ws_dattab-val = w_point3-y.
    APPEND ws_dattab TO wt_dattab.

  ENDLOOP.

  PERFORM f_create_data USING wt_dattab.

  CALL METHOD w_graph->render.


***
*   FORMS
***
FORM f_derive_point USING i_p1 TYPE ty_point
                          i_p2 TYPE ty_point
                          i_p3 TYPE ty_point
                          i_p4 TYPE ty_point
                 CHANGING c_pn TYPE ty_point.
  DATA: l_square TYPE f,
        l_cube   TYPE f.


  l_square = w_coeff * w_coeff.
  l_cube   = l_square * w_coeff.

*  c_pn-x = '0.5' * ( ( 2 * i_p2-x )
*         + ( -1 * ( i_p1-x ) + i_p3-x ) * w_coeff
*         + ( 2 * i_p1-x - 5 * i_p2-x + 4 * i_p3-x - i_p4-x ) * l_square
*         + ( -1 * ( i_p1-x ) + 3 * i_p2-x - 3 * i_p3-x + i_p4-x ) * l_cube ).

  c_pn-y = '0.5' * ( ( 2 * i_p2-y )
         + ( -1 * ( i_p1-y ) + i_p3-y ) * w_coeff
         + ( 2 * i_p1-y - 5 * i_p2-y + 4 * i_p3-y - i_p4-y ) * l_square
         + ( -1 * ( i_p1-y ) + 3 * i_p2-y - 3 * i_p3-y + i_p4-y ) * l_cube ).

ENDFORM.


FORM f_create_settings.
  DATA: l_ixml_custom_doc TYPE REF TO if_ixml_document,
        l_ostream         TYPE REF TO if_ixml_ostream,
        l_xstr            TYPE xstring.

  DATA: l_root           TYPE REF TO if_ixml_element,
        l_globalsettings TYPE REF TO if_ixml_element,
        l_default        TYPE REF TO if_ixml_element,
        l_elements       TYPE REF TO if_ixml_element,
        l_chartelements  TYPE REF TO if_ixml_element,
        l_title          TYPE REF TO if_ixml_element,
        l_element        TYPE REF TO if_ixml_element,
        l_encoding       TYPE REF TO if_ixml_encoding.


* Build the settings as an XML file
  l_ixml_custom_doc = w_ixml->create_document( ).

  l_encoding = w_ixml->create_encoding(
    byte_order = if_ixml_encoding=>co_little_endian
    character_set = 'utf-8' ).
  l_ixml_custom_doc->set_encoding( l_encoding ).

  l_root = l_ixml_custom_doc->create_simple_element(
            name = 'SAPChartCustomizing' parent = l_ixml_custom_doc ).
  l_root->set_attribute( name = 'version' value = '2.0' ).   "1.1

  l_globalsettings = l_ixml_custom_doc->create_simple_element(
            name = 'GlobalSettings' parent = l_root ).

  l_element = l_ixml_custom_doc->create_simple_element(
            name = 'FileType' parent = l_globalsettings ).
  l_element->if_ixml_node~set_value( 'PNG' ).

  IF p_lines IS INITIAL.
    l_element = l_ixml_custom_doc->create_simple_element(
              name = 'ChartType' parent = l_globalsettings ).
    l_element->if_ixml_node~set_value( 'Columns' ).   "Lines, Pie
    l_element = l_ixml_custom_doc->create_simple_element(
              name = 'Dimension' parent = l_globalsettings ).
    l_element->if_ixml_node~set_value( 'Three' ).   "PseudoThree
  ELSE.
    l_element = l_ixml_custom_doc->create_simple_element(
              name = 'ChartType' parent = l_globalsettings ).
    l_element->if_ixml_node~set_value( 'Lines' ).   "Columns, Pie
    l_element = l_ixml_custom_doc->create_simple_element(
              name = 'Dimension' parent = l_globalsettings ).
    l_element->if_ixml_node~set_value( 'Two' ).
  ENDIF.

  l_element = l_ixml_custom_doc->create_simple_element(
            name = 'Width' parent = l_globalsettings ).
  l_element->if_ixml_node~set_value( '640' ).
  l_element = l_ixml_custom_doc->create_simple_element(
            name = 'Height' parent = l_globalsettings ).
  l_element->if_ixml_node~set_value( '360' ).

  l_default = l_ixml_custom_doc->create_simple_element(
            name = 'Defaults' parent = l_globalsettings ).
  l_element = l_ixml_custom_doc->create_simple_element(
            name = 'FontFamily' parent = l_default ).
  l_element->if_ixml_node~set_value( 'Arial' ).

  l_elements = l_ixml_custom_doc->create_simple_element(
            name = 'Elements' parent = l_root ).
  l_chartelements = l_ixml_custom_doc->create_simple_element(
            name = 'ChartElements' parent = l_elements ).
  l_title = l_ixml_custom_doc->create_simple_element(
            name = 'Title' parent = l_chartelements ).

  l_element = l_ixml_custom_doc->create_simple_element(
            name = 'Caption' parent = l_title ).
  l_element->if_ixml_node~set_value( 'Smooth spline' ).

* Set the settings of Graphics
  l_ostream = w_stream->create_ostream_xstring( l_xstr ).
  CALL METHOD l_ixml_custom_doc->render EXPORTING ostream = l_ostream.
  w_graph->set_customizing( xdata = l_xstr ).

ENDFORM.


FORM f_create_data USING it_data TYPE STANDARD TABLE.
  DATA: l_ixml_data_doc TYPE REF TO if_ixml_document,
        l_ostream       TYPE REF TO if_ixml_ostream,
        l_xstr          TYPE xstring.

  DATA: l_simplechartdata TYPE REF TO if_ixml_element,
        l_categories      TYPE REF TO if_ixml_element,
        l_series          TYPE REF TO if_ixml_element,
        l_element         TYPE REF TO if_ixml_element,
        l_encoding        TYPE REF TO if_ixml_encoding.

  FIELD-SYMBOLS: <ls_data> TYPE any,
                 <lv_key>  TYPE any,
                 <lv_val>  TYPE any.


  l_ixml_data_doc = w_ixml->create_document( ).

  l_encoding = w_ixml->create_encoding(
    byte_order = if_ixml_encoding=>co_little_endian
    character_set = 'utf-8' ).
  l_ixml_data_doc->set_encoding( l_encoding ).

  l_simplechartdata = l_ixml_data_doc->create_simple_element(
            name = 'SimpleChartData' parent = l_ixml_data_doc ).
  l_categories = l_ixml_data_doc->create_simple_element(
            name = 'Categories' parent = l_simplechartdata ).

  LOOP AT it_data ASSIGNING <ls_data>.
    ASSIGN COMPONENT 1 OF STRUCTURE <ls_data> TO <lv_key>.
    l_element = l_ixml_data_doc->create_simple_element(
              name = 'C' parent = l_categories ).
    l_element->if_ixml_node~set_value( CONV string( <lv_key> ) ).
  ENDLOOP.

  l_series = l_ixml_data_doc->create_simple_element(
            name = 'Series' parent = l_simplechartdata ).
  l_series->set_attribute( name = 'label' value = 'Values' ).

  LOOP AT it_data ASSIGNING <ls_data>.
    ASSIGN COMPONENT 2 OF STRUCTURE <ls_data> TO <lv_val>.
    l_element = l_ixml_data_doc->create_simple_element(
              name = 'S' parent = l_series ).
    l_element->if_ixml_node~set_value( CONV string( <lv_val> ) ).
  ENDLOOP.

  l_ostream = w_stream->create_ostream_xstring( l_xstr ).
  CALL METHOD l_ixml_data_doc->render EXPORTING ostream = l_ostream.
  w_graph->set_data( xdata = l_xstr ).

ENDFORM.

Assigned Tags

      4 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Christopher Solomon
      Christopher Solomon

      What??!! No runtime output example screenshot?!?!?! haha......thanks....love when people "play" with ABAP like this. I think most of us have either "tinkered" with it or thought of it. I remember years and years back where someone took on the task to "try" to make a DOOM like game with ABAP....it didn't go anywhere but was an interesting idea.
       

      Author's profile photo François Henrotte
      François Henrotte
      Blog Post Author

      No screenshot indeed. You need to deserve it. But it is worth trying...

      For the real geeks, an interesting extension would be to apply it to X/Y points, this is possible if you set the Chart type to "Scatter" in place of "Lines". Then it is not category-based anymore and you have to use tag ChartData in place of SimpleChartData and to provide X and Y values. It would require to rebuild the whole application for points having X and Y. A nice exercice 🙂
       

      Author's profile photo Jelena Perfiljeva
      Jelena Perfiljeva

      Hm, at first I thought "Catmull-Rom" would have something to do with the Dilbert comics. 🙂 Learned something new today.

      Great scott, this is some hardcore stuff. Now if only you could paint two red lines but one of them in green ink that would be terrific. 🙂

       

      Author's profile photo François Henrotte
      François Henrotte
      Blog Post Author

      Yes we can 🙂

       

      (updated version from 21.01 : Make Graphics look great again)