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.