Using JavaScript Library to inject What-Ifs

The purpose of this document is to show ready-to-use example that relies on datastories.display.export_javascript_library to export a JavaScript tool to embed the What-Ifs component in a dashboard.

It is assumed the user already ran stories and exported their models in RSX files. As a reminder, this can be achieved in the SDK by using a code of the form:

story.model.export('/my-favorite/location/model_1.rsx')

For this example, it will be assumed the user has two models model_1.rsx and model_2.rsx, but the example can be adapted to one or any number of models.

Exporting the JavaScript library

The JavaScript library can be exported to a file of your choice, through:

from datastories.display import export_javascript_library
export_javascript_library(file_path='/my-favorite/location/DataStoriesLibrary.js')

The export_javascript_library exports a JavaScript file that contains the DataStories libraries. In particular, it exhibits a method on the window object:

window.dsRender: DOMElement => Promise<VueComponent>

that takes as input an existing DOMElement, and runs on it the DataStories VueJS engine. The result is a promise of the resulting VueJS component.

Example use case

From within /my-favorite/location, create a file called view.html and containing the following content:

<html><head></head>
    <body>
    <h1>This is an example dashboard</h1>

    <p>
        <!-- MODEL SELECTOR -->
        <label>Select a model:
            <select id="model_ctrl">
                <option value="/model_1.rsx">Model 1</option>
                <option value="/model_2.rsx">Model 2</option>
            </select>
        </label>
        <br>
        <button id="model_load_btn">Load model</button>
    </p>
    <hr>
    <p>
        <!-- DRIVER VALUE SETTER -->
        <label>Driver name:
            <input id="driver_name_ctrl" />
        </label>
        <br>
        <label>Slider value:
            <input id="driver_value_ctrl" />
        </label>
        <br>
        <button id="driver_update_btn">Update Driver Value</button>
    </p>
    <hr>
    <!-- WHAT-IFS COMPONENT CONTAINER -->
    <div id="whatifs_container"></div>


    <!-- LIBRARY INCLUSION -->
    <script src="DataStoriesLibrary.js"></script>

    <!-- USE CASE EXAMPLE -->
    <script>
    /* We define two utility functions that load and modify the drivers, respectively */
    const container = document.getElementById("whatifs_container");
    let currentComponent$ = undefined;

    function loadModel(url) {
        // The following small step is required to let the container element untouched by the VueJS engine
        container.innerHTML = "";
        const containerToRender = document.createElement("div");
        container.appendChild(containerToRender);

        // We encode the VueJS component in the working container
        containerToRender.innerHTML = `<whatifs-controller :model-url="'${url}'" :show-controls = "true" :show-console = "true" :show-optimizer = "false" />`;

        // We render the component once the DataStories library is loaded, and we resolve once the component is mounted:
        currentComponent$ = window.dsRender(containerToRender)
            .then(renderedContainer => renderedContainer.$children[0]);
    }

    function setSliders(sliders) {
        currentComponent$
        .then(component => component.setDriverValuesFromObject(sliders));
    }

    // Loading a model: (this could be called by some user interaction)
    document.getElementById("model_load_btn").addEventListener("click", () => {
        var url = document.getElementById("model_ctrl").value;
        loadModel(url);
    })

    // For any loaded model: (This is an example of interaction)
    document.getElementById("driver_update_btn").addEventListener("click", () => {
        var value = parseFloat(document.getElementById("driver_value_ctrl").value);
        var name = document.getElementById("driver_name_ctrl").value;
        var sliders = {}; sliders[name] = value;
        setSliders(sliders);
    });
    </script>

    </body>
</html>

In the above example, we are creating a simple HTML page made up of two groups of controllers: a model selector and a driver-value setter.

The model selector allows us to control which one of the RSX model is going to be displayed on screen, while the driver-value setter allows us to set values of drivers. The DOM elements are controlled by proper event listeners, that is regular JavaScript code.

Selecting a model

We use the dsRender function to create a small utility method that loads the What-Ifs component. Since this is a VueJS component, it has to be encoded as a descriptive string, and then rendered by the engine:

function loadModel(url) {
    // The following small step is required to let the container element untouched by the VueJS engine
    container.innerHTML = "";
    const containerToRender = document.createElement("div");
    container.appendChild(containerToRender);

    // We encode the VueJS component in the working container
    containerToRender.innerHTML = `<whatifs-controller :model-url="'${url}'" :show-controls = "true" :show-console = "true" :show-optimizer = "false" />`;

    // We render the component once the DataStories library is loaded, and we resolve once the component is mounted:
    currentComponent$ = window.dsRender(containerToRender)
        .then(renderedContainer => renderedContainer.$children[0]);
}

Some remarks are welcome. Because VueJS mutates the component to render, we do not directly work in the container, but in a child instead. This trick allows you to re-hydrate the same target if you want to change model as we are doing here.

A second point to note is that since we render a container, the dsRender method resolves with this container. The What-Ifs Controller is its first child, hence we need to access it in a final step:

currentComponent$ = window.dsRender(containerToRender)
    .then(renderedContainer => renderedContainer.$children[0]);

We store the result in a reusable variable, that is a promise of component. The asynchronous programming allows you to better integrate with spinning-wheels and other hooks you might have, to detect when is the VueJs component mounted.

In-between is the real business of what-ifs component creation:

containerToRender.innerHTML = `<whatifs-controller :model-url="'${url}'" :show-controls = "true" :show-console = "true" :show-optimizer = "false" />`;

Beware the usage of quotes around the :model-url parameter. The URL can be any resource location to the RSX model.

Setting Driver Values

In order to update the sliders values, we create a small utility around the main functionality, to better handle the asynchronous behavior:

function setSliders(sliders) {
    currentComponent$
    .then(component => component.setDriverValuesFromObject(sliders));
}

Here we decided to act as simple as possible on the currentComponent$ promise, but you can compose it as better suits you.

The setDriverValuesFromObject method expects a dictionnary of drivers and their values. For example, if the RSX model contains drivers named “Bedrooms” and “Living area”, a valid sliders object could be:

{
    "Bedrooms": 6,
    "Living area": 200
}

In the above use case, we set only one driver at a time, but you can set many of them.

Deploying the example

Once you have copied the content of the view.html in /my-favorite/location (the one you have saved the RSX model), you can quickly set-up an elementary Python3 static server:

cd /my-favorite/location
python3 -m http.server

You should see a message of the kind:

Serving HTTP on :: port 8000 (http://[::]:8000/) ...

Once you see this message, you can open your internet browser and target the location http://localhost:8000/view.html. You should see the HTML page we just built up.

Note: The server port 8000 might be different for you.