MicroStrategy ONE

Paginate Custom Widgets for PDF Export

If you create custom widgets in your visualization, you can set the supportPagination parameter to true to allow your widget to flow across multiple pages when it is exported to PDF.

Copy
    mstrmojo.plugins.D3BoxPlot.D3BoxPlot = mstrmojo.declare(
        mstrmojo.CustomVisBase,
        null,
        {
            scriptClass: "mstrmojo.plugins.D3BoxPlot.D3BoxPlot",
            cssClass: "d3boxplot",
            // ...
            supportNEE: true, // indicate the widget supports PDF exporting by New Export Engine
            supportPagination: true, // indicate the widget supports pagination when export to PDF
            // ...

The flowchart below demonstrates the overview of the export framework for custom visualizations. The framework iterates over the content from left to right (horizontal pagination) and then from top to bottom (vertical pagination). The blue nodes in the graph are the APIs that can be overwritten to customize the behavior of the pagination workflow.

APIs for Customizing Pagination and Their Default Behavior

Typically, a visualization with overflow content generates scroll bars on a web page, so the default logic assumes that pagination behavior is triggered by scroll bars. There are some cases in which pagination logic should be triggered even if the page does not contain scroll bars, which is discussed further below in the D3 Mekko Chart section.

The following code sample shows the APIs that can be used to customize pagination behavior. In the function body, the default behavior is introduced in the form of comments.

Copy
{    
    /**
    * Get the visualization's container dom node which contains scroll bars.
    * 
    * @return {HTMLElement}
    */
    getPaginationContainer: function getPaginationContainer() {
        // Return this.domNode as result.
    },

    /**
    * Executed at the very beginning of the pagination workflow.
    * If there is any asynchronous operation, return a promise which will be resolved when the asynchronous operation finishes. Otherwise, do not return anything.
    * 
    * @return {Promise|undefined}
    */
    preExport: function preExport() {
        // Enlarge the content size to the least larger integral number(aka, ceil) of magnitude of pagination container. The rendered content will remain unchanged.
        // This behavior will guarantee scrolling to last page to be successful and the last page will not duplicate printed content.
        // For example, the PDF page size is 800 * 600, and the content size is 1200 * 1500, than the content size would be expanded to 1600 * 1800.
    },

    /**
    * Executed at the very end of the pagination workflow.
    * 
    * @return {Promise|undefined}
    */
    postExport: function postExport() {
        // do nothing
    },

    /**
    * Executed before printing PDF.
    * 
    * @return {Promise|undefined}
    */
    prePrint: function prePrint() {
        // do nothing
    },

    /**
    * Executed after printing PDF.
    * 
    * @return {Promise|undefined}
    */
    postPrint: function postPrint() {
        // do nothing
    },

    /**
    * Check if the visualization could be scrolled down at this moment.
    * This function is used by scrollToNextVerticalPage().
    * 
    * @return {boolean}
    */
    canScrollDown: function canScrollDown() {
        // return Math.ceil(container.scrollTop + container.clientHeight) < Math.floor(container.scrollHeight).
    },

    /**
    * Check if the visualization could be scrolled right at this moment.
        * This function is used by scrollToNextHorizontalPage().
    * 
    * @return {boolean}
    */
    canScrollRight: function canScrollRight() {
        // return Math.ceil(container.scrollLeft + container.clientWidth) < Math.floor(container.scrollWidth).
    },

    /**
    * Try to scroll down the pagination container.
    * 
    * @return {Promise|undefined}
    */
    scrollToNextVerticalPage: function scrollToNextVerticalPage() {
        // If canScrollDown() returns false, return Promise.reject().
        // Or else, set the pagination container's scrollTop value: container.scrollTop += container.clientHeight, then return a promise resolves immediately.
    },

    /**
    * Try to scroll right the pagination container.
    * 
    * @return {Promise|undefined}
    */
    scrollToNextHorizontalPage: function scrollToNextHorizontalPage() {
        // If canScrollRight() returns false, return Promise.reject().
        // Or else, set the pagination container's scrollLeft value: container.scrollLeft += container.clientWidth, then return a promise resolves immediately.
    },

    /**
    * Try to scroll the pagination container to the left most.
    * 
    * @return {Promise|undefined}
    */
    scrollToFirstHorizontalPage: function scrollToFirstHorizontalPage() {
        // Set container.scrollLeft = 0, then return a promise resolves immediately.
    },
}

How to Customize the Pagination API

You can override the APIs in your visualization declaration. One example is the D3 box plot, shown in the code sample below, that overrides the canScrollDown() and canScrollRight() APIs.

Copy
mstrmojo.plugins.D3BoxPlot.D3BoxPlot = mstrmojo.declare(
    // ......

    supportNEE: true, // indicate the widget supports PDF exporting by New Export Engine
    supportPagination: true, // indicate the widget supports pagination when export to PDF
    
    canScrollDown: function canScrollDown() {
        return false; // disable vertical pagination since there is no meaningful vertical scrolling for BoxPlot
    },

    canScrollRight: function canScrollRight() {
        var container = this.domNode;
        return Math.ceil(container.scrollLeft + container.clientWidth + 8) < Math.floor(container.scrollWidth); // add an extra 8px for right-side v-scrollbar
    },

    // ......

Since the D3 box plot does not support vertical pagination, the canScrollDown() API is overwritten and immediately returned as false.

Even though the vertical scroll bar is useless, it still appears and occupies eight pixels of horizontal scroll bar length, so the canScrollRight() API also needs to be taken into consideration.

D3 Mekko Chart

As mentioned earlier, most pagination scenarios come with scroll bars, but there are still outliers, such as the D3 Mekko chart. It is an animation frame-based visualization, so the scrolling assumption does not work when exporting it to PDF.

Modify the preExport API

The Mekko chart generates a series of frames, based on data, and can apply transitions between frames if the auto play animation is set to true.

  1. To export all of the frames to PDF, you must navigate to each frame and capture a clear view of it.
  2. In the preExport API, stop playing the animation and go to the first frame.
  3. To minimize export time, set the animation transition duration time to the minimum value.
  4. After pausing at the animation frame, you must to wait to allow all rendering events (such as text rendering) to complete before printing the PDF. You can accomplish this using delayedPromiseBetweenFrames().

See the following code sample of changes to the preExport API:

Copy
preExport: function() {
    if (myStoryBoard) {
        this.animationFrameCount = myStoryBoard._getCategories().length;
        myStoryBoard.frameDuration = 1;

        return new Promise((resolve, reject) => {
            setTimeout(() => {
                myStoryBoard._goToFrameIndex(0);
                myStoryBoard.pauseAnimation();
                resolve();
            }, exportInterval);
        }).then(() => {
            this.animationFrameIndex = 1;
            return this.delayedPromiseBetweenFrames();
        });
    }
},

// Wait for afterDraw event to render the text
delayedPromiseBetweenFrames: function() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, exportInterval);
    });
},

Modify Scrolling Behavior

To support pagination for a Mekko chart, you need to override a few APIs for scrolling behavior.

  1. Since there are no scroll bars in a Mekko chart, the scroll functions actually switches between animation frames. This pagination process only involves one dimension, so you can rewrite the scrollToNextHorizontalPage method to switch frames and disable vertical pagination.
  2. Override the canScrollRight() method by checking that the iteration index does not overflow.
  3. Disable scrollToNextVerticalPage() by returning Promise.reject() immediately to reduce function calls. As a result, overriding canScrollDown() becomes unnecessary since it is never called.

See the following code sample for disabling the APIs:

Copy
canScrollRight: function() {
    return this.animationFrameCount > 1 && this.animationFrameIndex < this.animationFrameCount;
},

scrollToNextHorizontalPage: function() {
    if (window.mstrApp && mstrApp.isExporting && myStoryBoard && this.canScrollRight()) {
        myStoryBoard._goToFrameIndex(this.animationFrameIndex);
        myStoryBoard.pauseAnimation();
        this.animationFrameIndex++;
        return this.delayedPromiseBetweenFrames();
    }
    else {
        return Promise.reject('Finish Horizontal iteration');
    }
},

scrollToNextVerticalPage: function() {
    return Promise.reject("Mekko chart doesn't scroll vertically");
},

Configure the Time Limit for Each Pagination API Execution

The exporter.renderer.rendering.timeout in the application.properties file is used to control the time limit for each execution of the Pagination API. The default value is 60 seconds. If there are any async API functions that take longer to execute, the pagination fails.