Rendering

Rendering makes sure that your data is transformed to pixels that eventually display on your screen.

The result of rendering can be a Document Object Model (DOM) structure, a scalable vector graphic (svg), or pixels on a canvas, depending on which output technology you choose. All code samples use svg rendering in d3. However, in your custom visualization you can choose any other way of rendering.

Within customvis-lib, you must implement a create phase and an update phase:

  • The create phase is run exactly once, when the visualization gets initialized.

  • The update phase is run each time an aspect of the visualization changes, like the size, data or properties of the visualization.

Implementing these phases is optional. The RenderBase base class provides a default implementation for you.

Create

The create phase is run when the visualization gets initialized. You can implement this phase by overriding the create() method of RenderBase. In this method, you receive the DOM node of the container element for your visualization. You don’t must override create if your visualization does not have initialization code. However, if you do, you can add more DOM or svg nodes to your container and optionally return an Element as the new root node of your visualization. An example:

protected create( _node: HTMLElement ): void
{
    // Append an svg node that scales to the size of its container.
    d3.select( _node ).append( "svg" )
        .attr( "width", "100%" )
        .attr( "height", "100%" );
    // No return value means _node will be the root of our visualization.
}

protected update( _info: UpdateInfo ): void
{
    // _info.node will be the same '_node' that was passed to 'create'.
}

If your initialization requires asynchronous calls, you can return a Promise object that resolves with either an Element or void.

protected create( _node: HTMLElement ): void
{
    return new Promise( resolve =>
    {
        // doInit performs async initialization.
        doInit( function cb()
        {
            // Init is done, resolve the Promise. Optionally you can
            // resolve with an `Element` that will become the root
            // node of your visualization.
            resolve();
        } );
    } );
}

Update

The update phase gets called each time something changes in the size, properties, or data of the visualization. You can implement the update phase by overriding the update() method of RenderBase.

Render completion

The implementation of update in its most basic form is the following code:

protected update( _info: UpdateInfo ): void
{
    _info.node.textContent = "Hello World: " + Date.now().toString();
}

There is no return value, which means that the rendering completed at the end of the update function.

Each visualization has the responsibility to notify its host when it completed rendering in the update phase. Notification is done through the return type: returning void implies that rendering is done when the update function ends and returning a Promise means that the host should wait for that promise to be fulfilled.

If you want your visualization to show animations or transitions, this is often an asynchronous process. Therefore, the update function can return a Promise object that resolves as soon as the transition of the visualization completed. An example illustrates this:

protected update( info: UpdateInfo ): Promise<void>
{
    const t = d3.transition().duration( 300 );

    d3.select( _info.node )
        .selectAll( "rect" )
        .data( _info.data.rows )
        .join( "rect" )
            .attr( "x", d => scale( d.tuple( 0 ).key ) )
            .attr( "width", d => scale.bandwidth() )
            .transition( t ) // bind to our transition
                .attr( "height", d => d.values( 0 ) );

    return new Promise( resolve => t.on( "end", () => resolve() ) );
}

In the RenderBase base class, you can see that the update method has the return signature void | Promise<void>. However, in your override you can be more explicit and define either void or Promise<void> as the return type.

If for some reason, you want the host application to know that rendering failed, you can either return a rejected promise or produces a new Error( "msg" ).

UpdateInfo

The one parameter that is passed to update is an object of type UpdateInfo. You can see this object as the state that you are about to render. The UpdateInfo class holds the following information:

  • reason: The reason why rendering should take place. Check the flags in this object to see which aspects of the visualization need rendering. Available flags are:
    • data: data is changed since last call to update.
    • size: size is changed since last call to update.
    • properties: properties are changed since last call to update.
    • decorations: decorations (selected, highlighted) are changed since last call to update.
  • data: The data that should be rendered, or null if there is no data. If there is no data to render, it is the responsibility of the visualization to ensure that the visualization is cleared and rendered in an empty state.

  • node: The HTML element on which rendering should take place. This is the element that was returned previously from the RenderBase.create method.

  • properties: The current set of properties. Properties can also be accessed through the RenderBase.properties attribute.

  • locale: The data locale that should be used for rendering. Do not use for translations, only for custom data formatting where that is necessary.

The following example shows how you can use the fields of UpdateInfo to provide an optimized implementation of update:

protected update( _info: UpdateInfo ): void
{
    const reason = _info.reason;

    if ( !_info.data )
        return renderEmpty( _info.node );

    // Call expensive data processing function only if data has changed.
    if ( reason.data )
        this.data = processData( _info.data );

    // Perform rendering, based on `this.data` ...
}