GoodData.UI: Custom Visualizations

  • 28 September 2021
  • 0 replies
  • 42 views
GoodData.UI: Custom Visualizations
Userlevel 2

GoodData.UI is a powerful JavaScript/React SDK that lets you build fully custom analytics applications on top of the GoodData platform, including fully custom visualizations. There are two ways how to approach the implementation: A higher-level approach using a React component <Execute />, and a lower-level end-to-end approach using a raw JavaScript Promise.

<Execute /> Component

Building custom visualizations by utilizing the <Execute /> component is very straightforward and should be fairly familiar even to a beginner React developer. The <Execute /> component offers a straightforward interface for handling loading and error states and ensures its results are always up to date by preventing issues related to asynchronous race conditions.

For more, see How to Create a Custom Visualization docs, the Execute examples, and this interactive example.

End-to-End Flow

Sometimes using the React component <Execute /> is not the most convenient way, and you are better off with a raw JavaScript Promise. For example:

  • When you don’t aim to visualize the execution result, but you only want to make a result-based decision in your application’s logic.

  • When you are building a sophisticated visualization with advanced paging and sorting, like a complex pivot table with lazy loading and infinite scrolling.

  • When you don’t want to use React.

Unfortunately, by following this raw path, you will lose some fancy benefits that come with our React components. Those lost benefits include but are not limited to error handling, loading handling, and asynchronous race conditions.

Race Conditions

Let’s assume this code sample:

import React, { useState, useEffect } from 'react';
import { MeasureGroupIdentifier, newTwoDimensional } from '@gooddata/sdk-model';

import { workspace } from '../../constants';
import { useBackend } from '../../contexts/Auth';

const CustomChart = ({ measure, viewBy, stackBy, dateFilter }) => {
const backend = useBackend();
const [chartData, setChartData] = useState([]);
const [isLoadingData, setIsLoadingData] = useState(true);

useEffect(() => {
const fetchData = async () => {
const result = await backend
.workspace(workspace)
.execution()
.forItems([measure, viewBy], [dateFilter])
.withDimensions(...newTwoDimensional([viewBy], [MeasureGroupIdentifier, stackBy]))
.execute();

const data = await result.readAll();

// some data transformation

setChartData(data);
};

fetchData();
}, [measure, viewBy, stackBy, dateFilter, backend]);

if (isLoadingData) {
return <div>Loading data…</div>;
}

return <div>Custom Chart showing {chartData}</div>;
};

export default CustomChart;

This will work most of the time, but unfortunately, not always. Once one or more properties are updated, i.e., there is a change in measure, viewBy, stackBy, or dateFilter, React will make sure this component will re-render and trigger a new execution. Now assume that the user is able to change the properties many times in a short period of time, for example, by quickly checking and unchecking a checkbox that affects whether the date filter is applied or not. There are now multiple asynchronous execution requests running in parallel. And the problem is that asynchronous execution requests might not arrive in the right order. It can happen that requests fired earlier will get resolved last, overriding results from latter requests and causing inconsistency between UI and execution results.

This is a common problem referred to as race conditions, so this is not specific to GoodData, but rather a general programming problem to solve. There are many articles on this topic; just google for “avoiding race conditions with react hooks.” One way to work around this is to prevent users from triggering unnecessary executions by disabling certain UI elements while there’s an execution in progress. Another workaround could be to debounce those API calls, making sure they aren’t fired too close to each other. However, to help fix the problem without working around it, the GoodData.UI comes equipped with useCancelablePromise:

import React, { useState } from 'react';
import { MeasureGroupIdentifier, newTwoDimensional } from '@gooddata/sdk-model';
import { useCancelablePromise } from '@gooddata/sdk-ui';

import { workspace } from '../../constants';
import { useBackend } from '../../contexts/Auth';

const CustomChart = ({ measure, viewBy, stackBy, dateFilter }) => {
const backend = useBackend();
const [chartData, setChartData] = useState([]);
const [isLoadingData, setIsLoadingData] = useState(true);

useCancelablePromise({
promise: async () => {
const result = await backend
.workspace(workspace)
.execution()
.forItems([measure, viewBy], [dateFilter])
.withDimensions(...newTwoDimensional([viewBy], [MeasureGroupIdentifier, stackBy]))
.execute();

const data = await result.readAll();

// some data transformation

setChartData(data);
}
}, [measure, viewBy, stackBy, dateFilter, backend]);

if (isLoadingData) {
return <div>Loading data…</div>;
}

return <div>Custom Chart showing {chartData}</div>;
};

export default CustomChart;

The name useCancelablePromise is a bit misleading. With the current JavaScript specification, once a Promise is fired, it cannot be technically canceled. What useCancelablePromise does is that it simply ignores the results that arrive from stale requests and makes sure that only the Promise callback is only applied if the request is still relevant.

Note: Unfortunately, eslint rule react-hooks/exhaustive-deps doesn't help us here, so we have to be extra careful when listing all dependencies when working with useCancelablePromise hook from @gooddata/sdk-ui; you should only use useCancelablePromise hook for GET operations. It is *not* recommended to use it for POST, PUT, or DELETE.

Besides useCancelablePromise, there are also useExecutionDataView, useInsightDataView, and useDataExport. All these methods are currently in Beta, but no major breaking changes are expected. Once the proper documentation is released, they will be officially productionalized. For more information, look into the sdk-ui module of https://github.com/gooddata/gooddata-ui-sdk.

Summary

There are two distinct ways how to create custom components with GoodData.UI. The simpler and more straightforward <Execute /> component, and more low-level and robust end-to-end flow. If you’d like to learn more about custom components, take our GoodData University course Building Custom Visualizations. Have fun coding!


0 replies

Be the first to reply!

Reply