Lasso filtering in Qlik Sense Extensions




A few months ago I created a lasso plugin for D3 that mimics the lasso functionality available in Qlik Sense out-of-the-box charts. The ultimate goal in building the plugin was to use it in Qlik Sense Extensions to enable selections in the same way the out of the box charts function.

Recently, Brian Booden and Ralf Becher built an awesome hexagonal binning extension that utilizes the d3 lasso plugin to enable selections. Through this process they gave me some great feedback on issues they found. Having resolved those, I’ve updated the library and would like to demonstrate how others can integrate the lasso plugin with their own Qlik Sense Extensions.

Brian and Ralf's extension Brian and Ralf's extension

Note: the lasso plugin is made for D3 and the example below will use a d3-based extension object. However, the lasso could easily be extended to work in conjunction with visualizations built using libraries other than d3.

In this post, I will show you how to take an existing extension object and modify the code to add lasso functionality. The code for the starting extension object and the final extension with the lasso incorporated can be found here. Before we get started, let’s first take a look at the lasso plugin itself.

D3 Lasso

The D3 Lasso plugin enables users to draw a closed path on SVG elements and determine what is inside and outside of that path. This function enables users to “lasso” elements by drawing a closed shape around them.

The plugin takes in a list of items to check when lassoing. This list is fed in as a d3 selection. The items of the lasso are the elements that will be evaluated as being inside or outside of the user’s current lasso. Each item has two boolean properties added to it: possible and selected.

The lasso works in 3 stages, which are triggered as events:

  1. lasso_start: when the user first starts drawing a lasso
  2. lasso_draw: while the user is lassoing
  3. lasso_end: when the user stops drawing a lasso

On lasso_start, all items are automatically given properties of:
  possible: false
  selected: false

On lasso_draw, items are checked against the current lasso in progress to determine whether they are wrapped by the lasso or not. If they are, “possible” is set to true; if they are not, “possible” is set to false.

Lasso example showing items tagged during drawing Lasso example showing items tagged during drawing



On lasso_end, any items that were possible at the time of the lasso being ended are given the property value
  selected: true

With these values assigned, we can use the lasso and basic JavaScript to do things like style elements as a user is lassoing, or execute a Qlik Sense selection when a lasso is finished.

A lasso can be defined with the following settings:

  • items
    a d3 selection. Each element in the selection will be tagged with lasso-specific properties when the lasso is used
  • hoverSelect
    a boolean that determines whether objects can be lassoed by hovering over an element during lassoing
  • closePathSelect
    a boolean that determines whether objects can be lassoed by drawing a loop around them
  • closePathDistance
    a number that specifies the maximum distance in pixels from the lasso origin that a lasso needs to be drawn in order to complete the loop and select elements
  • area
    a selection representing the element to be used as a target area for the lasso event
  • on
    a type of event and a function for that event. There are 3 types of events that can be defined: start, draw, and end

More information about the lasso setup is available in the plugin's README. Now, let's look at an example extension object where we will implement the lasso.

BasicScatter Extension Review

The BasicScatter extension takes 1 dimension and 3 measures. Each dimension is represented by 1 circle. Measure 1 is used to position the circle on the x-axis. Measure 2 is used to position the circle on the y-axis. Measure 3 is used to size the circle. The starting extension looks like this:
BasicScatter

In the paint code, first we get the properties of the extension and create a container div for it. If the div already exists, we empty it to reset the extension.
[code]
// Get the extension container properties
var height = $element.height(),
width = $element.width(),
id = "container_" + layout.qInfo.qId;

// Get the data
var qMatrix = layout.qHyperCube.qDataPages[0].qMatrix;

// Create or empty the chart container
if(document.getElementById(id)) {
$("#" + id).empty();
}
else {
$element.append($("<div />").attr("id",id).width(width).height(height));
}
[/code]

Next, we set up margins, add an svg, and add a plot area based on those margins.
[code]
// Define the plot margins
var margin = {
top:10,
bottom:10,
left:10,
right:10
};

// Create the svg
var svg = d3.select("#" + id).append("svg")
.attr("width",width)
.attr("height",height);

// Create a plot transformed by the margins
var plot = svg.append("g")
.attr('transform','translate(' + margin.left + ',' + margin.top + ')');
[/code]

Scales for the x position, y position, and circle size are set up based on our extension’s data.
[code]
// Create the x scale
var x = d3.scale.linear()
.domain([layout.qHyperCube.qMeasureInfo[0].qMin,layout.qHyperCube.qMeasureInfo[0].qMax])
.range([0, width - margin.left - margin.right]);

// Create the y scale
var y = d3.scale.linear()
.domain([layout.qHyperCube.qMeasureInfo[1].qMin,layout.qHyperCube.qMeasureInfo[1].qMax])
.range([height-margin.top-margin.bottom, 0]);

// Create the area scale
var a = d3.scale.linear()
.domain([layout.qHyperCube.qMeasureInfo[2].qMin,layout.qHyperCube.qMeasureInfo[2].qMax])
.range([1*Math.PI,100*Math.PI]);
[/code]

Finally, we draw the circles on the plot using data coming from the extension and our scales.
[code]
// Create the circles
var circles = plot.selectAll('circle')
.data(qMatrix)
.enter()
.append('circle')
.attr('cx',function(d) {return x(d[1].qNum);})
.attr('cy',function(d) {return y(d[2].qNum);})
.attr('r',function(d) {return Math.sqrt(a(d[3].qNum)/Math.PI);});
[/code]

Also note that our “style.css” file has our base styling definitions for the circles, which are steelblue with 50% opacity.
[code]
.qv-object-BasicScatter circle {
fill: steelblue;
fill-opacity: .5;
}
[/code]

Now let's take this extension and add the lasso.

Integrating a Lasso into BasicScatter

We will modify the BasicScatter extension to add in a lasso. In the GitHub repo, I've saved the modified version as a separate extension called BasicScatterLasso. Our modified extension will enable lasso selections like this:
BasicScatterLasso

Loading the Plugin

The first thing we need to do in order to use the lasso in our extension is load in the lasso.js plugin. The lasso plugin requires d3 be loaded first, as it will be added to the d3 namespace. One way to do this with RequireJS, which is used by Qlik Sense, is to modify our define statement and use a shim:
[code]
requirejs.config({
paths: {
d3: "../extensions/basicscatterlasso/d3",
lasso: "../extensions/basicscatterlasso/lasso"
},
shim : {
"lasso" : {
"exports" : "lasso",
"deps" : ["d3"]
}
}
});
define( [
"text!./style.css",
"lasso"
],
[/code]

In our new requirejs statement, we are telling Require where to load d3 and lasso from. More importantly, in the shim we tell Require that d3 is a dependency for lasso, so if lasso is loaded, it will load d3 first. Now in our define statement, we can just load lasso and Require will know to go and load d3 first.

Adding CSS

In order for our lasso to be visible on screen, we need to define some CSS for it. We also will go ahead and define some CSS for our circles based on new classes for when they are “possible” or “not-possible” in a lasso:
[code]
.qv-object-BasicScatterLasso circle.possible {
stroke: black;
stroke-width: 2px;
}

.qv-object-BasicScatterLasso circle.not-possible {
fill: gray;
fill-opacity: .25;
}

.lasso path {
stroke: rgb(80,80,80);
stroke-width:2px;
}

.lasso .drawn {
fill-opacity:.05 ;
}

.lasso .loop_close {
fill:none;
stroke-dasharray: 4,4;
}

.lasso .origin {
fill:#3399FF;
fill-opacity:.5;
}
[/code]

Later in our extension code, we will use lasso events like “lasso_start” and “lasso_draw” to add and remove the appropriate classes from the circles. These classes and their CSS will give users feedback on what they are lassoing.

Define the Lasso Area

In order for our users to lasso, we need to define a “hit” area for the lasso. The “hit” area is where users can click and drag to start a lasso.

Let's assume our initial chart looks something like this:
Chart

Our initial thought may be to just append an invisible rectangle on top of the chart and make this the hit area. This will definitely cover our bases, as the hit area, shown in red, will look like this:
Hit 1

However, because this rectangle covers all of our SVG elements, it will block any other interactions we have defined, such as hover over functions on our circles. Because our lasso is going to use mouseover selections during lassoing, we need these hover functions.

So instead of putting the rectangle on top of the chart, we can put it in the back of the chart as the first thing we add to the SVG. The result is:
Hit 2

However, now the chart circles are covering areas of the rectangle. This means we can’t start a lasso from clicking on a circle; we can only start a lasso by clicking around a circle. If we draw just the hit area, without the circle elements, it looks like this:
Hit 3

This will be an issue if we have lots of circles on the screen with few gaps. In order to get around this problem, we can add the circles as part of the “hit” area as well. So the total “hit” area will be our background rectangle + the circles on top, resulting in a hit area that covers our entire plot while preserving our hover over functions:
Hit 4

An easy and maintainable way to accomplish this combined hit area is to use classes + d3 selections. We can define a common class for any elements in our chart that should be part of the hit area. In this case, we will use “.lassoable”.

First, create a rectangle right after the svg and give it a class of “lassoable”:
[code]
// Create a rectangle in the background for lassoing
var bg = svg.append('rect')
.attr('class','lassoable')
.attr('x',0)
.attr('y',0)
.attr('width',width)
.attr('height',height)
.attr('opacity',0);
[/code]

Then, adjust the circle code block to add the class of “lassoable” as well:
[code]
// Create the circles
var circles = plot.selectAll('circle')
.data(qMatrix)
.enter()
.append('circle')
.attr('class','lassoable')
.attr('cx',function(d) {return x(d[1].qNum);})
.attr('cy',function(d) {return y(d[2].qNum);})
.attr('r',function(d) {return Math.sqrt(a(d[3].qNum)/Math.PI);});
[/code]

When we configure our lasso, we will use this .lassoable class to select the hit area.

Configure the Lasso

At the bottom of our code, let’s create a new lasso instance with the following settings:

  • closing a path can make selections
  • the distance to auto close a path is 75 pixels
  • items can be moused over during lasso to select as well
  • the lasso should check our circles while lassoing
  • the lasso should execute some custom functions during its various stages

The code should look like this:
[code]
// Define the lasso
var lasso = d3.lasso()
.closePathDistance(75) // max distance for the lasso loop to be closed
.closePathSelect(true) // can items be selected by closing the path?
.hoverSelect(true) // can items by selected by hovering over them?
.area(svg.selectAll('.lassoable')) // a lasso can be drawn on the bg rectangle and any of the circles on top of it
.items(circles) // the circles will be evaluated for lassoing
.on("start",lasso_start) // lasso start function
.on("draw",lasso_draw) // lasso draw function
.on("end",lasso_end); // lasso end function
[/code]

For our lasso items, we feed in our circles.
For our hit area, we select anything in our SVG with the class “.lassoable”. This will cover our background rectangle and our circles in front of it.
For our lasso events, we define three named functions that we will create next.

Lasso Event Functions

Below our lasso configuration, lets define functions for lasso start, draw, and end.

For lasso start, we just want to style all of the lasso items as not possible, since the initial lasso will have no selections:
[code]
function lasso_start() {
lasso.items()
.classed({"not-possible":true}); // style as not possible
}
[/code]

For lasso draw, we want to apply two styles: possible for any lasso items that are in the possible lasso selection, and not-possible for any lasso times outside the possible lasso selection. We use .filter() to filter the lasso items based on these criteria and apply the appropriate classes:
[code]
function lasso_draw() {

// Style the possible dots
lasso.items().filter(function(d) {return d.possible===true})
.classed({"not-possible":false,"possible":true});

// Style the not possible dot
lasso.items().filter(function(d) {return d.possible===false})
.classed({"not-possible":true,"possible":false});

}
[/code]

For lasso end, we want to pull all of the lasso items that were selected and apply a Qlik Sense selection for those items. The Qlik Sense selection can be applied using the backendApi, which can be accessed from the extension itself. We can access the extension using the “this” statement within our paint code. The backendAPI has a selectValues function that takes in the dimension number of the extension to filter, an array of element numbers, and a boolean for the toggle mode. More information about this function can be found here.

Using .filter(), we can filter the lasso items for those that were marked with selected: true on lasso end. Then we can use .map() to access each of these selected items and extract the qElemNumbers from the data we bound to the items using d3 in our circles code block.
[code]
var self = this;

function lasso_end() {

// Get all the lasso items that were "selected" by the user
var selectedItems = lasso.items()
.filter(function(d) {
return d.selected;
});

// Retrieve the dimension element numbers for the selected items
var elemNos = selectedItems[0]
.map(function(d) {
return d.__data__[0].qElemNumber;
});

// Filter these dimension values
self.backendApi.selectValues(0,elemNos,true);
}
[/code]

Create the Lasso

Now that we've configured the lasso, the only thing left to do is to create it in our SVG. This can be accomplished with a simple call at the end of our extension:
[code]
svg.call(lasso);
[/code]

You now should have an extension with a lasso that can be used to make selections! Once again, you can review the before and after code for this extension here.

Happy lassoing

-Speros

TAGS: Qlik Sense, Visualization

Related News

April 2018.

Read More

Using modern web dev tools to develop powerful extensions

Have you ever wanted a button to set a...

Read More