Export data from an embedded chart
Introduction
In this tutorial, you will learn how to use qlik-api to export data from a chart embedded in a web application with qlik-embed web components.
Requirements
To complete the tutorial you will need the following:
- HTML and JavaScript experience
- A Qlik Cloud tenant
- A Qlik Analytics app
- A Qlik analytics app id and object id to export data
- An OAuth client configured in your Qlik Cloud tenant
- A web server to host your application
1.0.0 Add qlik-embed to the web application
1.0.1 Configure qlik-embed connection script
When you use qlik-embed
, a host configuration (hostConfig
) is needed to load
the library and connect it to your Qlik Cloud tenant.
Add the host configuration as a child of the head
element in your web page.
<script
crossorigin="anonymous"
type="application/javascript"
src="https://cdn.jsdelivr.net/npm/@qlik/embed-web-components@1/dist/index.min.js"
data-host="https://<tenant>.<region>.qlikcloud.com"
data-client-id="<clientId>"
data-redirect-uri="https://your-web-application.example.com/oauth-callback.html"
data-access-token-storage="session"
></script>
Note: This tutorial uses an OAuth2 SPA host configuration which requires an OAuth callback page. Review the Connect qlik-embed tutorial for more ways to connect qlik-embed to your tenant.
1.0.2 Add qlik-embed element
In the HTML body of your web application, add a qlik-embed
element with an
analytics/chart
ui.
<qlik-embed
id="visualization"
ui="analytics/chart"
app-id="a51a902d-76a9-4c53-85d2-066b44240146"
object-id="ZxDKp"
disable-cell-padding="true"
></qlik-embed>
Enter the ID for the analytics application where the chart exists into the app-id
property.
Enter the ID for the chart into the object-id
property.
1.0.3 Add export button
Add the export button to the HTML page. In addition, add a loader div
element
to show a loading icon when the data export is generating.
<div>
<button id="exportData">Export data</button>
</div>
<div id="loader" class="loader" style="display:none;"></div>
2.0.0 Add export logic to the web application
Exporting data from an embedded chart uses the companion library to qlik-embed
named qlik-api. qlik-api
provides typescript interfaces to Qlik Cloud’s REST
APIs and the Qlik Analytics Engine, also known as qix
.
Add a script element to your web application and set the type to module.
<script type="module">
//Add javascript here...
</script>
2.0.1 Import libraries
Add the auth
, reports
, and tempContents
modules from qlik-api using the
import
command.
The fileSaver
library assists with downloading the export file to your computer or
device.
import { auth, reports, tempContents } from "https://cdn.jsdelivr.net/npm/@qlik/api@1/index.min.js";
import fileSaver from 'https://cdn.jsdelivr.net/npm/file-saver@2.0.5/+esm'
2.0.2 Configure qlik-api connection
When you use qlik-embed and qlik-api together, they can share the same authenticated session. However, you do need to set the host configuration for qlik-api so that it can connect to your Qlik Cloud tenant.
Using the auth.setDefaultHostConfig
method, configure the qlik-api connection.
The parameters to use are similar to those found in the qlik-embed script with
slightly different syntax.
auth.setDefaultHostConfig({
host: "<tenant>.<region>.qlikcloud.com",
authType: "Oauth2",
clientId: "<clientId>",
redirectUri: "https://your-web-application.example.com/oauth-callback.html",
accessTokenStorage: "session",
autoRedirect: true,
});
2.0.3 Get reference to qlik-embed object
When you add a qlik-embed
object to a web application, you can easily obtain
access to the source analytics application’s composition and data model.
Access the object from the DOM using the getElementById
method. Once you have
a handle on the object, you can access the qlik-embed
API reference.
const vizEl = document.getElementById("visualization");
const appId = vizEl.getAttribute("app-id");
const refApi = await vizEl.getRefApi();
The refApi
variable represents a connection to the analytics session. You can
now make a getDoc
call to access the complete analytics application model,
or make a getObject
call to access the genericObject defining the embedded
visualization.
Create references to both the doc and the object. Then create a reference to the layout of the object.
const doc = await refApi.getDoc();
const theObject = await refApi.getObject();
const objLayout = await theObject.getLayout();
2.1.0 Request export report task
2.1.1 Create temporary bookmark
Temporary bookmarks make it easier to send the current selection state of the analytics application to the reporting services API. The reporting API generates the requested output based upon the temporary bookmark.
Create the temporary bookmark using the doc
object, supplying the chart object
ID from the object layout.
const tempB = await doc.createTemporaryBookmark(
{
qOptions: {
qIncludeAllPatches: true,
qIncludeVariables: true,
qSaveVariableExpressions: true
},
qObjectIdsToPatch: [
objLayout.qInfo.qId
]
}
);
The function will return the ID for the bookmark so it can be supplied to the data extract request.
2.1.2 Create reporting request payload
Create the data extract payload. Copy the code snippet and update the following properties:
Note: variables for these properties are referenced in the code.
appId
: The Qlik Analytics application ID to specify the object source.id
: The ID of the object present in the object layout variable.temporaryBookmarkV2.id
: The temporary bookmark ID obtained in step 2.1.1.
const reportPayload = {
type: "sense-data-1.0",
meta: {
exportDeadline:"P0Y0M0DT0H8M0S",
tags:["qlik-embed-download"]
},
senseDataTemplate: {
appId: appId,
id: objLayout.qInfo.qId,
selectionType: "temporaryBookmarkV2",
temporaryBookmarkV2: {
id: tempB
}
},
output: {
outputId: "Chart_excel",
type:"xlsx"}
}
Note: See the reporting service request generation endpoint for all available payload properties.
2.1.3 Helper functions
When a user makes a report request, it kicks off a reporting lifecycle that you will want your web application to handle gracefully. In the example code for this tutorial, there are a number of helper functions for managing the report request lifecycle.
showLoader
: Makes visible the DOM element withid="loader"
.hideLoader
: Makes hidden the DOM element withid="loader"
.extractReportId
: Obtains the report request ID from the report status URL.waitUntil
: Evaluates the report request status on an interval, stopping when the report generation completes.getDownloadId
: Obtains the data extract output ID so the file can be downloaded.createFileName
: Formats the name of the downloadable file to ensure uniqueness.
Note: These helper functions are part of this example to help provide an end-to-end experience. They are not required to execute a report request or download the resulting file.
2.1.4 Request and handle the data export
Use a try catch code block to perform the report request. Inside the try block,
add a reports.createReport
call including the report payload from step
2.1.2.
Obtain the status URL to monitor the reporting task from the content-location
header returned from the createReport
call. Then get the report ID from the
status URL.
Use the waitUntil
function to monitor the report request using the report ID.
When the report generation completes, get the download ID.
The download ID is the reference to the downloadable file’s temporary storage location.
Use the tempContents.downloadTempFile
function with the download ID to get the
file. Use the fileSaver.saveAs
function to invoke the save dialog box in the
browser.
try {
showLoader();
const reportReq = await reports.createReport(reportPayload);
let statusURL = reportReq.headers.get("content-location");
const reportId = extractReportId(statusURL);
if (!reportId) {
throw new Error("Invalid report ID");
}
// Set interval to check status every 5 seconds
const wait = await waitUntil(reportId);
const downloadId = getDownloadId(wait.location);
let dle = await tempContents.downloadTempFile(downloadId, {inline: 1});
hideLoader();
fileSaver.saveAs(dle.data, `${createFileName(wait.filename)}.xlsx`);
} catch (err) {
console.log(err);
}
3.0.0 Connect export logic to the button
Connect the exportData
function to the button you added to the HTML in step
1.0.3 by adding a click event listener.
document.getElementById("exportData").addEventListener("click", async function() {
exportData();
});
Full code
qlik-embed html and export button
<div id="analytics-chart" class="container">
<div class="sub-container">
<div>
<button id="exportData">Export data</button>
</div>
<div id="loader" class="loader" style="display:none;"></div>
</div>
<div class="sub-container">
<div class="viz">
<qlik-embed
id="visualization"
ui="analytics/chart"
app-id="<app-id>"
object-id="<object-id>"
disable-cell-padding="true"
></qlik-embed>
</div>
</div>
</div>
exportData function
<script type="module">
import { auth, qix, users, reports, tempContents } from "https://cdn.jsdelivr.net/npm/@qlik/api@1/index.min.js";
import fileSaver from 'https://cdn.jsdelivr.net/npm/file-saver@2.0.5/+esm'
const vizEl = document.getElementById("visualization");
const appId = vizEl.getAttribute("app-id");
const refApi = await vizEl.getRefApi();
const doc = await refApi.getDoc();
const theObject = await refApi.getObject();
const objLayout = await theObject.getLayout();
auth.setDefaultHostConfig({
host: "<tenant>.<region>.qlikcloud.com",
authType: "Oauth2",
clientId: "<clientId>",
redirectUri: "https://your-web-application.example.com/oauth-callback.html",
accessTokenStorage: "session",
autoRedirect: true,
});
document.getElementById("exportData")
.addEventListener("click", async function() {
exportData(doc, objLayout);
});
async function exportData(doc, objLayout) {
const tempB = await doc.createTemporaryBookmark(
{
qOptions: {
qIncludeAllPatches: true,
qIncludeVariables: true,
qSaveVariableExpressions: true
},
qObjectIdsToPatch: [
objLayout.qInfo.qId
]
}
);
const reportPayload = {
type: "sense-data-1.0",
meta: {
exportDeadline:"P0Y0M0DT0H8M0S",
tags:["qlik-embed-download"]
},
senseDataTemplate: {
appId: appId,
id: objLayout.qInfo.qId,
selectionType: "temporaryBookmarkV2",
temporaryBookmarkV2: {
id: tempB
}
},
output: {
outputId: "Chart_excel",
type:"xlsx"}
}
try {
showLoader();
const reportReq = await reports.createReport(reportPayload);
let statusURL = reportReq.headers.get("content-location");
const reportId = extractReportId(statusURL);
if (!reportId) {
throw new Error("Invalid report ID");
}
// Set interval to check status every 5 seconds
const wait = await waitUntil(reportId);
const downloadId = getDownloadId(wait.location);
let dle = await tempContents.downloadTempFile(downloadId, {inline: 1});
hideLoader();
fileSaver.saveAs(dle.data, `${createFileName(wait.filename)}.xlsx`);
} catch (err) {
console.log(err);
}
}
function showLoader() {
document.getElementById("loader").style.display = "block";
}
function hideLoader() {
document.getElementById("loader").style.display = "none";
}
// Function to create a filename
function createFileName(additionalInfo) {
const currentDateTime = new Date().toISOString();
return `${additionalInfo}-${currentDateTime}`;
}
async function waitUntil(reportId) {
return await new Promise(resolve => {
const interval = setInterval(() => {
return reports.getReportStatus(reportId).
then((status) => {
console.log(status);
console.log(`Current status: ${status.data.status}`);
if (status.data.status === "done") {
console.log(status);
let result = {
location: status.data.results[0].location,
filename: status.data.results[0].outputId,
};
clearInterval(interval);
resolve(result);
};
});
}, 5000);
});
}
function extractReportId(url) {
const regex = /reports\/(.*?)\/status/;
const match = url.match(regex);
if (match && match[1]) {
return match[1];
}
return null;
}
function getDownloadId(url) {
// Define a regular expression to match the last part of the path
const regex = /\/([^\/?#]+)(?:[?#]|$)/;
// Execute the regular expression on the URL
const matches = url.match(regex);
// Return the matched string, or null if no match is found
return matches ? matches[1] : null;
}
</script>
supporting CSS for HTML
.viz {
height: 600px;
width: 100%;
padding: 16px;
border: 1px solid #bbb;
border-radius: 3px;
box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.2);
position:relative;
}
.container {
padding: 8px;
gap: 8px;
position: relative;
display: flex;
flex-direction: column;
box-sizing: border-box;
margin-top: 50px;
padding-top: 50px;
}
.sub-container {
display: flex;
flex: 1 0 auto;
flex-direction: row;
align-content: stretch;
gap: 10px;
}
.loader {
border: 4px solid #a9a9a9; /* Light grey */
border-top: 4px solid #3498db; /* Blue */
border-radius: 50%;
width: 16px;
height: 16px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}