Tutorial: How to Build a Qlik Sense Extension with D3


Tutorial: How to Build a Qlik Sense Extension with D3

Speros Kokenes
August 18, 2014
Miscellany
22

The new Qlik Sense APIs make it extremely simple to build extensions that produce new visualizations. A common source of these visualizations is d3.js, a powerful javascript library for visualizing data.

The following tutorial will show how to create an extension using existing d3 code. The tutorial will go into a bit of detail, so following each step will take some time. However, the steps themselves are simple and without explanation can be completed in under 10 minutes. While the tutorial goes into some granular detail, there are 4 basic steps:

1) Get some d3 code to use

2) Get some test data to use and load it into Qlik Sense

3) Initialize a new extension and set up its properties

4) Insert the d3 code into the extension and modify it to source data from the extension

Prerequisites: This tutorial assumes you know something about Qlik Sense scripting and JavaScript, so it will not attempt to teach either. However, all code samples and steps will come with brief explanations, so you could copy and paste your way through the parts you don’t fully understand.

Resources:

The tutorial will show you how to create the following chart:

viz shot

Now onto the tutorial.

Get some d3 code to use

I am going to implement the d3 scatterplot seen here: http://bl.ocks.org/mbostock/3887118. The chart plots points by 2 metrics and also colors them based on an attribute dimension. Qlik Sense has a built in scatterplot that plots points based on 1 dimension. It does not offer the option to add a second dimension which could group the points by color. The d3 scatterplot introduces this new functionality.

If you look at the source code below the chart, we see two files: an html file that renders the chart, and a .tsv file that contains data. We will be using subsets of the html file and using Qlik Sense data instead of the tsv file.

We will come back to this example when we are ready to implement the d3 code.

Get some test data and load it into Qlik Sense

We need a data set with 2 metrics and 2 dimensions for the scatterplot. The 2 metrics will be plotted on the x and y axis. 1 dimension will be used to draw every point; the second dimension will be used to assign a color to the points. I pulled a sample data set from the Human Development Report 2014. The data set is included above in the Resources section.

The table headers are:

Development Group Country Obesity % Life Expectancy at age 60

I picked “Obesity %” and “Life Expectancy at age 60” as my two metrics. The plotted dimension will be each country. For the color, I will use the development group that each country is placed in. I also removed any countries that were missing data.

Save your data set to a folder where you can load it from.

Creating a new app in Qlik Sense

The first thing we need to do is create a new Qlik Sense app. Open Qlik Sense. You should see the hub. Select “Create new app” in the top right corner.

1

Give your application a name and then click on it to open it.

Modify the load script and load your data

We have created and opened a new application, which is empty. Now we need to load our data. Data can be loaded with the data load editor. Click on the compass icon in the top left corner and select “Data load editor” to open this window.

2

The data load editor contains a scripting language for loading data into Qlik Sense. To the left, you will see a panel that organizes your load script into different tabs. The Main tab is automatically populated with some configuration variables. We do not need to modify this. Click the “+” sign to create a new tab below Main. Give the tab a name.

A blank scripting tab should now be visible on the right. In order to generate our load script, we need to set up a connection to the folder where we will load from. To the right of the scripting area is a button that says “Create connection”. Click this button and navigate to the folder where your csv is saved. Give this folder connection a name and click Save.

data load

Once we have created a connection to a folder, we can use that connection to load files from that folder. On the right, the connection we just created appears along with a table icon. Clicking the table icon brings up a menu to load a specific file from the connection we created. Click on the sample data file and click “Select”.

A table loading wizard will pop up that will help us configure our load. It provides a preview of the data coming in, as well as parameters to change. We have a comma separated value data source with labels in the first row that will act as our column names. Change the settings of the load statement to reflect this:

Table Load configure

Click “Insert script” to import the generated load script into your script editor. It should look something like this:

sample script

We can now load the data into our application be clicking the “Load Data” button in the top right corner.

Initialize a new extension and set up its properties

Now that we have data ready to test, we can set up the framework for our new extension. Instead of creating our extension code from scratch, we will use an existing extension that comes with Qlik Sense and modify the code for our purposes.

Navigate to the Extensions folder. This folder is located in My Documents\Qlik\Sense\Extensions. You will find a folder for each extension you currently have installed. For this tutorial, we will duplicate the “SimpleTable” extension.

Initialize the extension

Create a duplicate of the “SimpleTable” extension folder and rename it to “TwoDimScatter”. Then open the folder. You should see the following files:

  • com-qliktech-simpletable.js – the Javascript file that pulls the data from Qlik Sense and creates a custom visualization
  • com-qliktech-simpletable.qext – a properties file that contains attributes like the name of the extension
  • simpletable.css – a styling file where we can define visual attributes like fonts and colors
  • wbfolder.wbl – I’m not sure what this is for and we don’t need to modify it

Update all of the file names except for the wbfolder.wbl file with “twodimscatter”, the name of our extension. For example, “com-qliktech-simpletable.js” should become “twodimscatter.js”. Also, we need to include d3.min.js in this folder, which will allow us to use the D3 library. The latest version is linked to in the Resources section above. The resulting folder should look something like this:

folder screenshot

Configure the extension properties file

Open the file “twodimscatter.qext” in a text editor. You should see JSON with properties like name, author, etc. These values will be seen in the Qlik Sense front end where you select objects to add to a sheet. Update the values to reflect your new extension. For example, my code says:

{
	"name" : "Two Dimensional Scatter",
	"description" : "A scatter plot that uses two dimensions",
	"icon" : "table",
	"type" : "visualization",
	"version": "1.0",
	"preview" : "table",
	"author": "Speros"
}

Save and close this file.

Prepare the CSS

Open the file “twodimscatter.css” in a text editor. We are eventually going to insert CSS specific to the d3 extension in here. The CSS file will contain styling rules like font sizes and colors for the elements of our visualization. For now, let’s clean out the contents of this file and keep only what we need. The first row has an entry for “.qv-object-com-qliktech-simpletable div.qv-object-content-container”. The “div.qv-object-content-container” is the div that will hold our extension. The first class listed there, “.qv-object-com-qliktech-simpletable”, is a class that will be applied our extension object. We want to include this class distinction so that our CSS only applies to elements we create in our extension. We will keep this entry, but with 2 modifications:

1) rename the first class from “.qv-object-com-qliktech-simpletable” to “.qv-object-twodimscatter”.

2) remove the line “overflow: auto;”

The rest of the text in the CSS file can be deleted. Your CSS file should look like this:

.qv-object-twodimscatter div.qv-object-content-container {
}

Set up the JS file

Open the Javascript file with a text editor. This file dictates what the extension object does. We are going to set up the initial properties and functions of our extension first.

File Dependencies

On the very first line of the file, you will see a define() statement that takes in a list of files to load, and then runs a function once they are loaded. This statement allows us to load files that our extension code will be dependent on before we try to run that extension code. Define() is part of the AMD API and is available to us in extensions because of Qlik Sense’s use of RequireJS.

We need to modify this define statement to grab the files we need for our extension: jQuery, our css file, and our d3.min.js file. Change the define array from

[“jquery”, “text!./simpletable.css”]

to

[“jquery”, “text!./twodimscatter.css”,”./d3.min”]

Our extension code will now load these files before attempting to draw anything. Note: Qlik Sense comes with jQuery, so we do not have to keep a local copy in our extension. We will use jQuery in our extension to append our visualization to the extension object container.

Properties and Definitions

Once we have defined our necessary files, we need to change the properties and definition of our extension. These properties include the width and length of the data set to retrieve, as well as constraints on how many dimensions and expressions are needed to run the extension.

At the top of the file, you should see the following code that sets these properties:

	return {
		initialProperties : {
			version: 1.0,
			qHyperCubeDef : {
				qDimensions : [],
				qMeasures : [],
				qInitialDataFetch : [{
					qWidth : 10,
					qHeight : 50
				}]
			}
		},
		definition : {
			type : "items",
			component : "accordion",
			items : {
				dimensions : {
					uses : "dimensions",
					min : 1
				},
				measures : {
					uses : "measures",
					min : 0
				},
				sorting : {
					uses : "sorting"
				},
				settings : {
					uses : "settings",
					items : {
						initFetchRows : {
							ref : "qHyperCubeDef.qInitialDataFetch.0.qHeight",
							label : "Initial fetch rows",
							type : "number",
							defaultValue : 50
						},
					}
				}
			}
		},

On line 2, an initialProperties object is created. On line 10, the qWidth specifies how many columns the extension should have. Change the qWidth from “10” to “4”, since we have two dimensions and two measures. The qHeight attribute below specifies how many rows can be loaded. Change the value from “50” to “1000”. Note that in Qlik Sense .96, 1000 rows is the most that can be entered here.

On line 13, a definition object is defined. This object includes an dimension and measure limits. On line 19 and 23, you will see that the minimum dimensions has been set to 1 and the minimum measures has been set to 0. For our extension, we want exactly 2 dimensions and 2 measures.

On line 19, change the min dimensions from “1” to “2”. Then, add a comma, and on a new line add a max value of “2”. Do the same for the measures. This restricts our extension to needing exactly 2 dimensions and 2 measures to be executed. The result should look like:

dimensions : {
					uses : "dimensions",
					min : 2,
					max: 2
				},
				measures : {
					uses : "measures",
					min : 2,
					max: 2
				},

On line 32, an items object is created. This object can be deleted for our extension. Delete lines 32 to 39. You can also delete the preceding comma. The result should look like:

settings : {
					uses : "settings"
				}

 The paint function

Our last step in setting up our function is to clear out the paint function. The paint function gets run every time the visualization should render. For example, filtering the data in Qlik Sense will cause the visualization to re-paint. For our purposes, we can create the following modifications:

1) modify the function to take in two parameters: $element and layout

function($element,layout) {

2) clear out the current contents of the function

function($element,layout) {}

3) console out these two elements so we can investigate their contents

function($element,layout) {

console.log($element);

console.log(layout);

}

Console.log() will take any input and print it to the JavaScript console, which we can view in Qlik Sense and in web browsers. By logging these elements, we can use the console to figure out what they contain and how we can use them to draw our visualization.

Our paint function should look like this:

paint : function($element,layout) {
	console.log($element);
	console.log(layout);
}

Initialize the extension in Qlik Sense and review

Let’s take a break from coding the extension and set it up to confirm that it works. We can also look at the objects we logged to the console.

Open the Qlik Sense application you created and create a new sheet from the App Overview screen.

5

Give the sheet a name and open it. It will initially be blank. In the top right corner, press the “Edit” button to enter Edit mode. There is a panel on the left side of the page where you can select charts to add to the page. Scroll down to the Two Dimensional Scatterplot chart and drag it onto the dashboard.

two dim scatter left sideAfter adding the chart, Qlik Sense will give you the option to add your dimensions and measures. Add “Development Group” as the first dimension and “Country” as the second dimension. For the measures, we will need to pick the field to use and what calculation should be done on it. Select “Obesity %” and “Life expectancy at age 60” for the measures. For the functions, “sum” or “avg” can be used. Because these values are recorded at the country level, we will only be summing or averaging a single number, so the result will be the same. I suggest the avg function so that if the data is restructured at a more granular level later, the measure will still calculate a valid result.

add avg measure

Once the dimensions and measures have been added, we can adjust the measure labels. On the right panel, click on the Measure heading to expand the panel. Each measure should be listed. Clicking on the measures will expand options for the measures, including adding a label. Add labels like “Obesity (%)” and “Life expectancy at age 60 (yrs)”. We will use these labels for our axes later.

We now have a blank extension with sample data loaded into it. Before we move on the final step of implementing the d3 code, let’s take a look at the items we output to the console in our JavaScript.

Open the developer tools view by Ctrl+Shift+right clicking on the page and selecting “Show Dev Tools”. Select “Console” from the top to view the JavaScript console. You should see the two items we output before. You can use the console to explore these items. For example, if I open up the layout object I can find the data that we will want to pull for our extension:

console shot

The image above shows where a single row of data for Albania is stored. We can see that the layout has an object called qHyperCube, which contains an array called qDataPages, which contains an object that contains an array called qMatrix, which contains an array of objects for each row of data. Each object in the row represents a different column. We can use this information to write our JavaScript code that will assemble the data for the d3 visualization.

We can also find the label titles in the layout object under qHyperCube.qMeasureInfo.qFallbackTitle:

qMeasureInfo

The console in general is a good tool for tracing and debugging code.

Insert and Modify the D3 Code

Open us the “twodimscatter.js” file with a text editor.

Setting up the data and container div

The first thing we need to do is extract the necessary information from our extension. We need the data to render, the width and height of the object we have available to work with, and the unique id of our chart object. The unique id will come in handy when we need to create a new DOM element to hold our visualization.

Now that we’ve used the console to find out where our source data is located and where the measure labels are, we can build our data area in JavaScript with the following lines of code:

			// get qMatrix data array
			var qMatrix = layout.qHyperCube.qDataPages[0].qMatrix;
			// create a new array that contains the measure labels
			var measureLabels = layout.qHyperCube.qMeasureInfo.map(function(d) {
				return d.qFallbackTitle;
			});
			// Create a new array for our extension with a row for each row in the qMatrix
			var data = qMatrix.map(function(d) {
				// for each element in the matrix, create a new object that has a property
				// for the grouping dimension, the first metric, and the second metric
			 	return {
			 		"Dim1":d[0].qText,
			 		"Metric1":d[2].qNum,
			 		"Metric2":d[3].qNum
			 	}
			});

We now have an array called measureLabels that contains the text values of our measure labels, and we have a data array with our data in it. Notice that we do not store both dimensions in this array; we only keep the development group which is used to color the points. This is to mimic the data that the scatterplot d3 code takes in; it does not require us to list the id of each record, which in our example is the country name.

Now let’s capture the width, height, and id of the chart object. Once we have that information, we will use jQuery to add a new container div to the chart which will hold our d3 visualization. If the div already exists, we will empty its contents so we can redraw the chart from scratch.

			
			// Chart object width
			var width = $element.width();
			// Chart object height
			var height = $element.height();
			// Chart object id
			var id = "container_" + layout.qInfo.qId;
			// Check to see if the chart element has already been created
			if (document.getElementById(id)) {
				// if it has been created, empty it's contents so we can redraw it
			 	$("#" + id).empty();
			}
			else {
				// if it hasn't been created, create it with the appropiate id and size
			 	$element.append($('<div />').attr("id", id).width(width).height(height));
			}

Finally, let’s call our viz function that will actually house our d3 code. We will keep the viz function separate from the paint function for organization purposes.

viz(data,measureLabels,width,height,id);

The full paint function should look like this:

		paint : function($element,layout) {
			// get qMatrix data array
			var qMatrix = layout.qHyperCube.qDataPages[0].qMatrix;
			// create a new array that contains the measure labels
			var measureLabels = layout.qHyperCube.qMeasureInfo.map(function(d) {
				return d.qFallbackTitle;
			});
			// Create a new array for our extension with a row for each row in the qMatrix
			var data = qMatrix.map(function(d) {
				// for each element in the matrix, create a new object that has a property
				// for the grouping dimension, the first metric, and the second metric
			 	return {
			 		"Dim1":d[0].qText,
			 		"Metric1":d[2].qNum,
			 		"Metric2":d[3].qNum
			 	}
			});

			// Chart object width
			var width = $element.width();
			// Chart object height
			var height = $element.height();
			// Chart object id
			var id = "container_" + layout.qInfo.qId;

			// Check to see if the chart element has already been created
			if (document.getElementById(id)) {
				// if it has been created, empty it's contents so we can redraw it
			 	$("#" + id).empty();
			}
			else {
				// if it hasn't been created, create it with the appropiate id and size
			 	$element.append($('<div />;').attr("id", id).width(width).height(height));
			}

			viz(data,measureLabels,width,height,id);

		}

We can create our new viz function at the bottom of our JS file, outside of the define() statement. We need to define it with the appropriate number of inputs:

var viz = function(data,labels,width,height,id) {
};

Copying the D3 code

Let’s return to the d3 example we want to use and copy out the JavaScript. The JavaScript portion of the file is located inside the html file, inside a <script> tag in the body. We want to copy from the start of the “var margin” statement down to the start of the “</script>” statement. Do not copy the “</script>” line; just up to the lines before it. We can then paste this code into our empty viz function. The code should look like this:

var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var x = d3.scale.linear()
    .range([0, width]);

var y = d3.scale.linear()
    .range([height, 0]);

var color = d3.scale.category10();

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

d3.tsv("data.tsv", function(error, data) {
  data.forEach(function(d) {
    d.sepalLength = +d.sepalLength;
    d.sepalWidth = +d.sepalWidth;
  });

  x.domain(d3.extent(data, function(d) { return d.sepalWidth; })).nice();
  y.domain(d3.extent(data, function(d) { return d.sepalLength; })).nice();

  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis)
    .append("text")
      .attr("class", "label")
      .attr("x", width)
      .attr("y", -6)
      .style("text-anchor", "end")
      .text("Sepal Width (cm)");

  svg.append("g")
      .attr("class", "y axis")
      .call(yAxis)
    .append("text")
      .attr("class", "label")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", ".71em")
      .style("text-anchor", "end")
      .text("Sepal Length (cm)")

  svg.selectAll(".dot")
      .data(data)
    .enter().append("circle")
      .attr("class", "dot")
      .attr("r", 3.5)
      .attr("cx", function(d) { return x(d.sepalWidth); })
      .attr("cy", function(d) { return y(d.sepalLength); })
      .style("fill", function(d) { return color(d.species); });

  var legend = svg.selectAll(".legend")
      .data(color.domain())
    .enter().append("g")
      .attr("class", "legend")
      .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });

  legend.append("rect")
      .attr("x", width - 18)
      .attr("width", 18)
      .attr("height", 18)
      .style("fill", color);

  legend.append("text")
      .attr("x", width - 24)
      .attr("y", 9)
      .attr("dy", ".35em")
      .style("text-anchor", "end")
      .text(function(d) { return d; });

});

Modifying the D3 code

We now need to make some changes to the D3 code to fit with our extension code. The following changes need to be made:

  1. We can delete the data loading portion of the D3 code. Our data has already been loaded via the extension.
  2. Modify the hard-coded width and height to use our input values
  3. Append our svg element, which will contain our chart, to the div we created instead of the html body
  4. Change all data references from the original data set to reference the columns in our extension data set

1. Deleting the D3 data load

In the sample d3 code about halfway down, there is a line that says

d3.tsv("data.tsv", function(error, data) {
  data.forEach(function(d) {
    d.sepalLength = +d.sepalLength;
    d.sepalWidth = +d.sepalWidth;
  });

The function d3.tsv takes two parameters: a file path, and a callback function to execute. The callback function runs the visualization building code once the file “data.tsv” has been loaded. We want to delete this call, as well as the data.forEach statement below it. Our data variable is already being populated when we call the viz function in our paint method, so we do not need to modify it. Note that the callback function encapsulates not only this data.forEach statement, but the rest of the visualization code as well. Therefore, when we delete this section, we must also delete the closing “});” at the end of our visualization code, which ends after the legend.append(“text”) statement. The result should now look something like this:

var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var x = d3.scale.linear()
    .range([0, width]);

var y = d3.scale.linear()
    .range([height, 0]);

var color = d3.scale.category10();

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

x.domain(d3.extent(data, function(d) { return d.sepalWidth; })).nice();
y.domain(d3.extent(data, function(d) { return d.sepalLength; })).nice();

svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis)
  .append("text")
    .attr("class", "label")
    .attr("x", width)
    .attr("y", -6)
    .style("text-anchor", "end")
    .text("Sepal Width (cm)");

svg.append("g")
    .attr("class", "y axis")
    .call(yAxis)
  .append("text")
    .attr("class", "label")
    .attr("transform", "rotate(-90)")
    .attr("y", 6)
    .attr("dy", ".71em")
    .style("text-anchor", "end")
    .text("Sepal Length (cm)")

svg.selectAll(".dot")
    .data(data)
  .enter().append("circle")
    .attr("class", "dot")
    .attr("r", 3.5)
    .attr("cx", function(d) { return x(d.sepalWidth); })
    .attr("cy", function(d) { return y(d.sepalLength); })
    .style("fill", function(d) { return color(d.species); });

var legend = svg.selectAll(".legend")
    .data(color.domain())
  .enter().append("g")
    .attr("class", "legend")
    .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });

legend.append("rect")
    .attr("x", width - 18)
    .attr("width", 18)
    .attr("height", 18)
    .style("fill", color);

legend.append("text")
    .attr("x", width - 24)
    .attr("y", 9)
    .attr("dy", ".35em")
    .style("text-anchor", "end")
    .text(function(d) { return d; });

2. Update the width and height values

The second and third lines of the D3 code specify that the width and height should be hard-coded values minus the size of the margins. In this case, the width and height are coded to 960 and 500. We can replace that with our width and height input values:

	    width = width - margin.left - margin.right,
	    height = height - margin.top - margin.bottom;

3. Append the svg element to our container

On the var svg line, the d3 code appends a new svg element to the body of the page. We can modify this statement so that the d3 code selects the container div we created and appends the new svg there instead.

var svg = d3.select("#"+id).append("svg")

4. Update the data references to use the correct property names

If we review the D3 code, we’ll see that the sections which draw visual elements based on data reference the original data table’s column headers. For example:

svg.selectAll(".dot")
    .data(data)
  .enter().append("circle")
    .attr("class", "dot")
    .attr("r", 3.5)
    .attr("cx", function(d) { return x(d.sepalWidth); })
    .attr("cy", function(d) { return y(d.sepalLength); })
    .style("fill", function(d) { return color(d.species); });

We need to replace all references in the code to d.sepalWidth to d.Metric1; d.sepalLength to d.Metric2, and d.species to d.Dim1.

The labels for the axes are also hard-coded, like in the x-axis definition where the .text() is set to the string “Sepal Width (cm)”:

svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis)
  .append("text")
    .attr("class", "label")
    .attr("x", width)
    .attr("y", -6)
    .style("text-anchor", "end")
    .text("Sepal Width (cm)");

Rather than hard-coding labels for our axes, we can use the labels variable we inputted to dynamically apply the appropriate labels. The labels variable is an array whose first element is the label for metric 1 and whose second element is the label for metric 2. We can rewrite the x-axis with this variable:

	  svg.append("g")
	      .attr("class", "x axis")
	      .attr("transform", "translate(0," + height + ")")
	      .call(xAxis)
	    .append("text")
	      .attr("class", "label")
	      .attr("x", width)
	      .attr("y", -6)
	      .style("text-anchor", "end")
	      .text(labels[0]);

The resulting viz function, with all of these updates made, should look like this (modified lines are highlighted):

var viz = function (data,labels,width,height,id) {

	var margin = {top: 20, right: 20, bottom: 30, left: 40},
	    width = width - margin.left - margin.right,
	    height = height - margin.top - margin.bottom;

	var x = d3.scale.linear()
	    .range([0, width]);

	var y = d3.scale.linear()
	    .range([height, 0]);

	var color = d3.scale.category10();

	var xAxis = d3.svg.axis()
	    .scale(x)
	    .orient("bottom");

	var yAxis = d3.svg.axis()
	    .scale(y)
	    .orient("left");

	var svg = d3.select("#"+id).append("svg")
	    .attr("width", width + margin.left + margin.right)
	    .attr("height", height + margin.top + margin.bottom)
	  .append("g")
	    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

	  x.domain(d3.extent(data, function(d) { return d.Metric1; })).nice();
	  y.domain(d3.extent(data, function(d) { return d.Metric2; })).nice();

	  svg.append("g")
	      .attr("class", "x axis")
	      .attr("transform", "translate(0," + height + ")")
	      .call(xAxis)
	    .append("text")
	      .attr("class", "label")
	      .attr("x", width)
	      .attr("y", -6)
	      .style("text-anchor", "end")
	      .text(labels[0]);

	  svg.append("g")
	      .attr("class", "y axis")
	      .call(yAxis)
	    .append("text")
	      .attr("class", "label")
	      .attr("transform", "rotate(-90)")
	      .attr("y", 6)
	      .attr("dy", ".71em")
	      .style("text-anchor", "end")
	      .text(labels[1])

	  svg.selectAll(".dot")
	      .data(data)
	    .enter().append("circle")
	      .attr("class", "dot")
	      .attr("r", 3.5)
	      .attr("cx", function(d) { return x(d.Metric1); })
	      .attr("cy", function(d) { return y(d.Metric2); })
	      .style("fill", function(d) { return color(d.Dim1); });

	  var legend = svg.selectAll(".legend")
	      .data(color.domain())
	    .enter().append("g")
	      .attr("class", "legend")
	      .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });

	  legend.append("rect")
	      .attr("x", width - 18)
	      .attr("width", 18)
	      .attr("height", 18)
	      .style("fill", color);

	  legend.append("text")
	      .attr("x", width - 24)
	      .attr("y", 9)
	      .attr("dy", ".35em")
	      .style("text-anchor", "end")
	      .text(function(d) { return d; });

}

Modify the CSS

The last step in setting up our extension is modify the CSS. If you look at the original d3 chart’s HTML code, the top of the code contains a style tag with the following values:

body {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.dot {
  stroke: #000;
}

We can modify this code and place it into our twodimscatter.css file. The modifications we need to make are:

  1. Change the “body” style to apply to our container object instead
  2. Add the “.qv-object-twodimscatter” class so that our CSS doesn’t affect any elements outside our chart object that have classes like “dot” or elements like paths and lines

Adding these modifications results in a twodimscatter.css file like this:

.qv-object-twodimscatter div.qv-object-content-container {
	font: 10px sans-serif;
}

.qv-object-twodimscatter .axis path,
.qv-object-twodimscatter .axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.qv-object-twodimscatter .dot {
  stroke: #000;
}

Viewing the Extension

Once these changes have been made, we can save all of our files and view the changes in our application. Open the Qlik Sense application and navigate to the sheet with the extension object. If the sheet was already open, press “F5” to refresh the page. The extension will now render a scatterplot with a color legend. You can modify the sheet further to add listboxes and other charts to utilize Qlik Sense’s associative model with this d3 extension.

viz shot

Next Steps

We can take this extension further by modifying the JavaScript file to add interactive components, such as making Qlik Sense selections and creating pop-up labels for the points with their country names. A future post will cover these enhancements.

All source code for this extension can be found here. The JavaScript file is commented so you can follow along with the d3 code.

Additional Resources

If you want to learn further about any of the components of this solution, here are some useful resources:

-Speros

22 thoughts on “Tutorial: How to Build a Qlik Sense Extension with D3

  1. Hi,
    for some Reason neither my nore your code will accept dimensions. (I get to choose them in sense but they won’t be used)
    I changed the wbl file contents to twodimscatter, since the problem reminded me of a similar problem in qlikview with the properties qvpp. Didn’t work though.

    Any insights or similar errors anyone?

  2. Hey Julian,

    I haven’t seen that issue. So it doesn’t work even if you use the completed code that I posted on GitHub?

    Are you using version .96 of Qlik Sense? Just want to confirm you aren’t using one of the prior beta versions.

  3. this works with some tweaks .. dimensions and measures should be set like this

    dimensions: {
    uses: “dimensions”,
    min: 2,
    },
    measures: {
    uses: “measures”,
    min: 2
    },

  4. As per developer manual, The Workbench load file (wbl) is used by the Mashup workbench and lists the name of all files included in the mashup project. When you save a mashup project from the Mashup workbench, a wbl file is automatically included in the project.

  5. Hi guys,
    Inspite of following all the steps I’m not able to generate the scatter chart as explained by the auther. Don’t know what could be the error or mistake but I’ve cross checked with what is given in the post but it doesn’t work…..Please help guys

  6. Hi Speros,

    This is really good.

    Did you have a chance to extend this with the “Next Steps”?? Looking forward to it…

  7. Hi,

    With reference to this: “Give the sheet a name and open it. It will initially be blank. In the top right corner, press the “Edit” button to enter Edit mode. There is a panel on the left side of the page where you can select charts to add to the page. Scroll down to the Two Dimensional Scatterplot chart and drag it onto the dashboard.”

    In my panel I did not have the option of The Two Dimensional Scatterplot chart. Any idea why? How can I troubleshoot?

    Thanks for helping 🙂

  8. Hi Speros,

    Thanks for this great tutorial. I am using QlikSense Desktop v2.1.1 and I am facing an issue. When I drag the Two Dimensional Scatter item into the worksheet, I am getting the option to add 2 dimensions and 2 measures. All good up-to this point.

    But as soon as I add the last dimension/measure, the chart goes blank. It’s not accepting the 4th dimension/measure.

    Any idea about such behaviour?

  9. Dev :
    Hi Speros,
    Thanks for this great tutorial. I am using QlikSense Desktop v2.1.1 and I am facing an issue. When I drag the Two Dimensional Scatter item into the worksheet, I am getting the option to add 2 dimensions and 2 measures. All good up-to this point.
    But as soon as I add the last dimension/measure, the chart goes blank. It’s not accepting the 4th dimension/measure.
    Any idea about such behaviour?

    Its working now.

  10. I have followed every given step. but extension is not reflecting in qliksesnse . Please help me out on this issue

  11. Hello ! Thank you for your tutorial very clear and useful 🙂

    I tried to add a third measure in the extension (that I used to play with the size of the bubble) but I can’t see the third mesure in the qMatrix there is only 4 elements by Object (the 2 dimensions and the 2 measures as there are in your tutorial) do you know why ?
    It’s really embarrassing because I need 5 measures for what I need 🙂

    (again thank you for your tutorial 🙂 )

  12. Hi Dev
    how did you resolve your problem.. when I drag the extension object to the worksheet it is blank.. don’t know what is the reason have tried everything.. appreciate if you can help me out..

  13. Hi Speros, thanks for a great tutorial. A quick question though.

    I’m trying to reuse your code and expand it a little. In doing so, I need to dynamically name the columns in the Dim1, Dim2 data variable.

    In other words, inside the data variable, I need a column which is ‘Product Category’, and ‘Customer’, and ‘Sales’ since I can’t override these columns later on.

    Is there any way to make this return function pull the Dim1 and Metric1 and Metric2 through a variable or function and not be hard-coded?

    I’ve tried many variations, but somehow I can’t get it.
    var data = qMatrix.map(function(d) {
    // for each element in the matrix, create a new object that has a property
    // for the grouping dimension, the first metric, and the second metric
    return {
    “Dim1”:d[0].qText,
    “Metric1”:d[2].qNum,
    “Metric2”:d[3].qNum
    }
    });

  14. Hello, when i try to download the ressource D3.min the link seems to fail. Where i can download it plz?
    Thanks

Leave a Reply

Your email address will not be published. Required fields are marked *