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:
Now onto the tutorial.
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.
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.
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.
Give your application a name and then click on it to open it.
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.
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.
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:
Click "Insert script" to import the generated load script into your script editor. It should look something like this:
We can now load the data into our application be clicking the "Load Data" button in the top right corner.
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.
Create a duplicate of the "SimpleTable" extension folder and rename it to "TwoDimScatter". Then open the folder. You should see the following files:
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:
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:
[code lang="js"]{
"name" : "Two Dimensional Scatter",
"description" : "A scatter plot that uses two dimensions",
"icon" : "table",
"type" : "visualization",
"version": "1.0",
"preview" : "table",
"author": "Speros"
}[/code]
Save and close this file.
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:
[code lang="css"].qv-object-twodimscatter div.qv-object-content-container {
}[/code]
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.
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.
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:
[code lang="js"] 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
},
}
}
}
},[/code]
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:
[code lang="js"]dimensions : {
uses : "dimensions",
min : 2,
max: 2
},
measures : {
uses : "measures",
min : 2,
max: 2
},[/code]
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:
[code lang="js"]settings : {
uses : "settings"
}[/code]
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:
[code lang="js"]paint : function($element,layout) {
console.log($element);
console.log(layout);
}[/code]
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.
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.
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:
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:
The console in general is a good tool for tracing and debugging code.
Open us the "twodimscatter.js" file with a text editor.
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:
[code lang="js"] // 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
}
});
[/code]
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.
[code lang="js"]
// 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));
}[/code]
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.
[code lang="js"]viz(data,measureLabels,width,height,id);[/code]
The full paint function should look like this:
[code lang="js"] 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);
}[/code]
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:
[code lang="js"]var viz = function(data,labels,width,height,id) {
};[/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:
[code lang="js"]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; });
});[/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:
In the sample d3 code about halfway down, there is a line that says
[code lang="js"]d3.tsv("data.tsv", function(error, data) {
data.forEach(function(d) {
d.sepalLength = +d.sepalLength;
d.sepalWidth = +d.sepalWidth;
});[/code]
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:
[code lang="js"]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; });[/code]
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:
[code lang="js"] width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;[/code]
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.
[code lang="js"]var svg = d3.select("#"+id).append("svg")[/code]
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:
[code lang="js" highlight="6,7,8"]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); });[/code]
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)":
[code lang="js" highlight="10"]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)");[/code]
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:
[code lang="js" highlight="10"] 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]);[/code]
The resulting viz function, with all of these updates made, should look like this (modified lines are highlighted):
[code lang="js" highlight="4,5,29,30,41,52,59,60,61"]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; });
}[/code]
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:
[code lang="css"]body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.dot {
stroke: #000;
}[/code]
We can modify this code and place it into our twodimscatter.css file. The modifications we need to make are:
Adding these modifications results in a twodimscatter.css file like this:
[code lang="css"].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;
}[/code]
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.
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.
If you want to learn further about any of the components of this solution, here are some useful resources:
-Speros