├── theme.json ├── wbfolder.wbl ├── sankey.png ├── images ├── animation.gif └── sankeyphoto.png ├── SenseSankey.qext ├── .gitattributes ├── .gitignore ├── style.css ├── md5.min.js ├── README.md ├── sankeymore.js ├── SenseSankey.js └── d3.min.js /theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": "Default" 3 | } -------------------------------------------------------------------------------- /wbfolder.wbl: -------------------------------------------------------------------------------- 1 | NewTemplate.qext; 2 | NewTemplate.js; 3 | style.css -------------------------------------------------------------------------------- /sankey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xavierlp/SenseSankey/HEAD/sankey.png -------------------------------------------------------------------------------- /images/animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xavierlp/SenseSankey/HEAD/images/animation.gif -------------------------------------------------------------------------------- /images/sankeyphoto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xavierlp/SenseSankey/HEAD/images/sankeyphoto.png -------------------------------------------------------------------------------- /SenseSankey.qext: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SenseSankey", 3 | "description": "Sankey Graph", 4 | "icon": "extension", 5 | "type": "visualization", 6 | "version": 2.34, 7 | "preview" : "sankey.png", 8 | "author": "QlikTech International AB" 9 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # ========================= 18 | # Operating System Files 19 | # ========================= 20 | 21 | # OSX 22 | # ========================= 23 | 24 | .DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must ends with two \r. 29 | Icon 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* Put css styles in here */ 2 | 3 | .ttip { 4 | background: #fff; 5 | box-shadow: 0 0 5px #999999; 6 | -webkit-box-shadow: 1px 2px 6px rgba(0,0,0, 0.5); 7 | pointer-events: none; 8 | padding: 7px; 9 | text-align:right; 10 | opacity:0.8; 11 | border-radius: 4px; 12 | border: 1px solid #ffffff 13 | } 14 | 15 | 16 | .node rect { 17 | cursor: crosshair 18 | fill-opacity: .9; 19 | shape-rendering: crispEdges; 20 | } 21 | 22 | .node text { 23 | pointer-events: none; 24 | text-shadow: 0 1px 0 #fff; 25 | } 26 | 27 | .link { 28 | fill: none; 29 | stroke: #000; 30 | stroke-opacity: .2; 31 | } 32 | 33 | .link:hover { 34 | stroke-opacity: .5; 35 | } 36 | .nodeTitle{ 37 | font-family:Arial, Helvetica, sans-serif;font-size:11px; 38 | } 39 | -------------------------------------------------------------------------------- /md5.min.js: -------------------------------------------------------------------------------- 1 | !function(n){"use strict";function t(n,t){var r=(65535&n)+(65535&t),e=(n>>16)+(t>>16)+(r>>16);return e<<16|65535&r}function r(n,t){return n<>>32-t}function e(n,e,o,u,c,f){return t(r(t(t(e,n),t(u,f)),c),o)}function o(n,t,r,o,u,c,f){return e(t&r|~t&o,n,t,u,c,f)}function u(n,t,r,o,u,c,f){return e(t&o|r&~o,n,t,u,c,f)}function c(n,t,r,o,u,c,f){return e(t^r^o,n,t,u,c,f)}function f(n,t,r,o,u,c,f){return e(r^(t|~o),n,t,u,c,f)}function i(n,r){n[r>>5]|=128<>>9<<4)+14]=r;var e,i,a,h,d,l=1732584193,g=-271733879,v=-1732584194,m=271733878;for(e=0;e>5]>>>t%32&255);return r}function h(n){var t,r=[];for(r[(n.length>>2)-1]=void 0,t=0;t>5]|=(255&n.charCodeAt(t/8))<16&&(o=i(o,8*n.length)),r=0;16>r;r+=1)u[r]=909522486^o[r],c[r]=1549556828^o[r];return e=i(u.concat(h(t)),512+8*t.length),a(i(c.concat(e),640))}function g(n){var t,r,e="0123456789abcdef",o="";for(r=0;r>>4&15)+e.charAt(15&t);return o}function v(n){return unescape(encodeURIComponent(n))}function m(n){return d(v(n))}function p(n){return g(m(n))}function s(n,t){return l(v(n),v(t))}function C(n,t){return g(s(n,t))}function A(n,t,r){return t?r?s(t,n):C(t,n):r?m(n):p(n)}"function"==typeof define&&define.amd?define(function(){return A}):"object"==typeof module&&module.exports?module.exports=A:n.md5=A}(this); 2 | //# sourceMappingURL=md5.min.js.map -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SenseSankey 2 | Qlik Sense Sankey Extension 3 | 4 | Developed by Xavier Le Pitre, based on D3.js 5 | 6 | Display data with Sankey diagram. 7 | Typically used to visualize cost, energy or flows between processes. Helpful in locating dominant contributions to an overall flow. 8 | The size of each flow is calculated by the value. 9 | 10 | The extension is based on : 11 | 12 | Original radar chart extension, developped by John Park : here
13 | Mike Bostock's Sankey chart : here
14 | D3.js 15 | 16 | V2.34 Added export support by [julmot](https://github.com/julmot) 17 | 18 | V2.33 Added support for negative values by [vbakke](https://github.com/vbakke) 19 | 20 | V2.32 Update code to be compatible with September 2018 21 | 22 | V2.31 Add Images at the start and then end of the flow 23 | + Manage 6 dimensions and more 24 | 25 | ![alt tag](images/sankeyphoto.png) 26 | 27 | 28 | V2.2 Correct the comma issue 29 | 30 | V2.1 Resolved conflict with other extensions made by Brixm 31 | 32 | V2.0 New color Selection to be compatible with QlikSense 3.1 SR2 33 | 34 | V1.4 add clickables links 35 | 36 | V1.3.1 Correct data with commas + Choice of device symbol. 37 | 38 | V1.3 Add new option for persistent colors 39 | 40 | 41 | 42 | ![alt tag](images/animation.gif) 43 | 44 | License 45 | 46 | Please, if you update this extension feel free to send me your pull requests to help others users to enjoy all features! 47 | 48 | Troubleshooting 49 | If you install Qlik Sense and the Extensions as instructed but get the error “Invalid Visualization”, it most likely is because Qlik Sense occasionally has problems with its cache. To resolve this issue you need to clear the browser cache, but how you do that depends on how you run Qlik Sense. 50 | 51 | Below you will find a short step by step guide on how to clear the cache depending on how you run Qlik Sense desktop. Terminology may differ between languages but the functionality should be the same. 52 | 53 | The Desktop Application 54 | 55 | Navigate to the app you are having problems with. 56 | Hold Ctrl+Shift and right click anywhere in the app. 57 | Click the “Show DevTools” option. 58 | Once the DevTools tab is open you will find a tab called “Network”, located between “Elements” and “Sources”. Click it. 59 | On the very top of the “Network” tab you will find the check box “Disable Cache”. Check it if not already checked. 60 | Now, navigate back to the app you were having problems with and hit F5 to refresh. 61 | If you have the same problem with other apps just repeat the process with that app. 62 | 63 | Google Chrome 64 | 65 | Navigate to the app you are having problems with. 66 | Press F12 or Ctrl+Shift+j to open the developer tools. 67 | Once the developer tools page is open you will find a tab called “Network”. It will be between “Elements” and “Sources”. Click it. 68 | On the very top of the “Network” tab you will find the check box “Disable Cache”. Check it if not already checked. 69 | Hit F5 to refresh. 70 | You can now close the developer tools by pressing F12 again. 71 | 72 | If you have the same problem with other apps, just repeat the process with that app. 73 | 74 | Firefox 75 | 76 | Navigate to the app you are having problems with. 77 | Press F12 to open the developer tools. 78 | Once the developer tools page is open you will find an icon that looks like a small cogwheel. It is located in the top-right corner of the developer tools. Click it. 79 | Look for a check box labeled “Disable Cache”. Check it if not already checked. 80 | Hit F5 to refresh. 81 | You can now close the developer tools by pressing F12 again. 82 | 83 | If you have the same problem with other apps, just repeat the process with that app. 84 | 85 | Internet Explorer 86 | 87 | This is for Internet Explorer 9, but the steps for Internet Explorer 10 and 11 is most likely very similar. 88 | 89 | Navigate to the Menu Button (the one looking like a small cogwheel) and click it. 90 | Navigate to the “Safety” option and click the “Delete Browsing History” button. 91 | Uncheck everything except “Preserve Favorites website data” and “Temporary Internet files”. 92 | Press Delete. 93 | Hit F5 to refresh. 94 | If you have the same problem with other apps, just repeat the process with that app. 95 | 96 | Once an app is working the cache should not be a problem again. 97 | 98 | The MIT License (MIT) 99 | 100 | Copyright (c) 2015 Xavier Le Pitre 101 | 102 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 103 | 104 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 105 | 106 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 107 | -------------------------------------------------------------------------------- /sankeymore.js: -------------------------------------------------------------------------------- 1 | senseSankey = function() { 2 | var sankey = {}, 3 | nodeWidth = 24, 4 | nodePadding = 8, 5 | size = [1, 1], 6 | nodes = [], 7 | links = []; 8 | 9 | sankey.nodeWidth = function(_) { 10 | if (!arguments.length) return nodeWidth; 11 | nodeWidth = +_; 12 | return sankey; 13 | }; 14 | 15 | sankey.nodePadding = function(_) { 16 | if (!arguments.length) return nodePadding; 17 | nodePadding = +_; 18 | return sankey; 19 | }; 20 | 21 | sankey.nodes = function(_) { 22 | if (!arguments.length) return nodes; 23 | nodes = _; 24 | return sankey; 25 | }; 26 | 27 | sankey.links = function(_) { 28 | if (!arguments.length) return links; 29 | links = _; 30 | return sankey; 31 | }; 32 | 33 | sankey.size = function(_) { 34 | if (!arguments.length) return size; 35 | size = _; 36 | return sankey; 37 | }; 38 | 39 | sankey.layout = function(iterations) { 40 | computeNodeLinks(); 41 | computeNodeValues(); 42 | computeNodeBreadths(); 43 | computeNodeDepths(iterations); 44 | computeLinkDepths(); 45 | return sankey; 46 | }; 47 | 48 | sankey.relayout = function() { 49 | computeLinkDepths(); 50 | return sankey; 51 | }; 52 | 53 | sankey.link = function() { 54 | var curvature = .5; 55 | 56 | function link(d) { 57 | var x0 = d.source.x + d.source.dx, 58 | x1 = d.target.x, 59 | xi = d3.interpolateNumber(x0, x1), 60 | x2 = xi(curvature), 61 | x3 = xi(1 - curvature), 62 | y0 = d.source.y + d.sy + d.dy / 2, 63 | y1 = d.target.y + d.ty + d.dy / 2; 64 | return "M" + x0 + "," + y0 65 | + "C" + x2 + "," + y0 66 | + " " + x3 + "," + y1 67 | + " " + x1 + "," + y1; 68 | } 69 | 70 | link.curvature = function(_) { 71 | if (!arguments.length) return curvature; 72 | curvature = +_; 73 | return link; 74 | }; 75 | 76 | return link; 77 | }; 78 | 79 | 80 | // Populate the sourceLinks and targetLinks for each node. 81 | // Also, if the source and target are not objects, assume they are indices. 82 | function computeNodeLinks() { 83 | nodes.forEach(function(node) { 84 | node.sourceLinks = []; 85 | node.targetLinks = []; 86 | }); 87 | links.forEach(function(link) { 88 | var source = link.source, 89 | target = link.target; 90 | if (typeof source === "number") source = link.source = nodes[link.source]; 91 | if (typeof target === "number") target = link.target = nodes[link.target]; 92 | //console.log('jrp5'); 93 | source.sourceLinks.push(link); 94 | target.targetLinks.push(link); 95 | //console.log('jrp7') 96 | }); 97 | } 98 | 99 | // Compute the value (size) of each node by summing the associated links. 100 | function computeNodeValues() { 101 | nodes.forEach(function(node) { 102 | node.value = Math.max( 103 | d3.sum(node.sourceLinks, value), 104 | d3.sum(node.targetLinks, value) 105 | ); 106 | node.absValue = Math.max( 107 | d3.sum(node.sourceLinks, absValue), 108 | d3.sum(node.targetLinks, absValue) 109 | ); 110 | }); 111 | } 112 | 113 | // Iteratively assign the breadth (x-position) for each node. 114 | // Nodes are assigned the maximum breadth of incoming neighbors plus one; 115 | // nodes with no incoming links are assigned breadth zero, while 116 | // nodes with no outgoing links are assigned the maximum breadth. 117 | function computeNodeBreadths() { 118 | var remainingNodes = nodes, 119 | nextNodes, 120 | x = 0; 121 | 122 | while (remainingNodes.length) { 123 | nextNodes = []; 124 | remainingNodes.forEach(function(node) { 125 | node.x = x; 126 | node.dx = nodeWidth; 127 | node.sourceLinks.forEach(function(link) { 128 | nextNodes.push(link.target); 129 | }); 130 | }); 131 | remainingNodes = nextNodes; 132 | ++x; 133 | } 134 | 135 | // 136 | moveSinksRight(x); 137 | scaleNodeBreadths((size[0] - nodeWidth) / (x - 1)); 138 | } 139 | 140 | function moveSourcesRight() { 141 | nodes.forEach(function(node) { 142 | if (!node.targetLinks.length) { 143 | node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1; 144 | } 145 | }); 146 | } 147 | 148 | function moveSinksRight(x) { 149 | nodes.forEach(function(node) { 150 | if (!node.sourceLinks.length) { 151 | node.x = x - 1; 152 | } 153 | }); 154 | } 155 | 156 | function scaleNodeBreadths(kx) { 157 | nodes.forEach(function(node) { 158 | node.x *= kx; 159 | }); 160 | } 161 | 162 | function computeNodeDepths(iterations) { 163 | var nodesByBreadth = d3.nest() 164 | .key(function(d) { return d.x; }) 165 | .sortKeys(d3.ascending) 166 | .entries(nodes) 167 | .map(function(d) { return d.values; }); 168 | 169 | // 170 | initializeNodeDepth(); 171 | resolveCollisions(); 172 | for (var alpha = 1; iterations > 0; --iterations) { 173 | relaxRightToLeft(alpha *= .99); 174 | resolveCollisions(); 175 | relaxLeftToRight(alpha); 176 | resolveCollisions(); 177 | } 178 | 179 | function initializeNodeDepth() { 180 | var ky = d3.min(nodesByBreadth, function(nodes) { 181 | return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, absValue); 182 | }); 183 | 184 | nodesByBreadth.forEach(function(nodes) { 185 | nodes.forEach(function(node, i) { 186 | node.y = i; 187 | node.dy = node.absValue * ky; 188 | }); 189 | }); 190 | 191 | links.forEach(function(link) { 192 | link.dy = link.absValue * ky; 193 | }); 194 | } 195 | 196 | function relaxLeftToRight(alpha) { 197 | nodesByBreadth.forEach(function(nodes, breadth) { 198 | nodes.forEach(function(node) { 199 | if (node.targetLinks.length) { 200 | var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, absValue); 201 | node.y += (y - center(node)) * alpha; 202 | } 203 | }); 204 | }); 205 | 206 | function weightedSource(link) { 207 | return center(link.source) * link.absValue; 208 | } 209 | } 210 | 211 | function relaxRightToLeft(alpha) { 212 | nodesByBreadth.slice().reverse().forEach(function(nodes) { 213 | nodes.forEach(function(node) { 214 | if (node.sourceLinks.length) { 215 | var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, absValue); 216 | node.y += (y - center(node)) * alpha; 217 | } 218 | }); 219 | }); 220 | 221 | function weightedTarget(link) { 222 | return center(link.target) * link.absValue; 223 | } 224 | } 225 | 226 | function resolveCollisions() { 227 | nodesByBreadth.forEach(function(nodes) { 228 | var node, 229 | dy, 230 | y0 = 0, 231 | n = nodes.length, 232 | i; 233 | 234 | // Push any overlapping nodes down. 235 | nodes.sort(ascendingDepth); 236 | for (i = 0; i < n; ++i) { 237 | node = nodes[i]; 238 | dy = y0 - node.y; 239 | if (dy > 0) node.y += dy; 240 | y0 = node.y + node.dy + nodePadding; 241 | } 242 | 243 | // If the bottommost node goes outside the bounds, push it back up. 244 | dy = y0 - nodePadding - size[1]; 245 | if (dy > 0) { 246 | y0 = node.y -= dy; 247 | 248 | // Push any overlapping nodes back up. 249 | for (i = n - 2; i >= 0; --i) { 250 | node = nodes[i]; 251 | dy = node.y + node.dy + nodePadding - y0; 252 | if (dy > 0) node.y -= dy; 253 | y0 = node.y; 254 | } 255 | } 256 | }); 257 | } 258 | 259 | function ascendingDepth(a, b) { 260 | return a.y - b.y; 261 | } 262 | } 263 | 264 | function computeLinkDepths() { 265 | nodes.forEach(function(node) { 266 | node.sourceLinks.sort(ascendingTargetDepth); 267 | node.targetLinks.sort(ascendingSourceDepth); 268 | }); 269 | nodes.forEach(function(node) { 270 | var sy = 0, ty = 0; 271 | node.sourceLinks.forEach(function(link) { 272 | link.sy = sy; 273 | sy += link.dy; 274 | }); 275 | node.targetLinks.forEach(function(link) { 276 | link.ty = ty; 277 | ty += link.dy; 278 | }); 279 | }); 280 | 281 | function ascendingSourceDepth(a, b) { 282 | return a.source.y - b.source.y; 283 | } 284 | 285 | function ascendingTargetDepth(a, b) { 286 | return a.target.y - b.target.y; 287 | } 288 | } 289 | 290 | function center(node) { 291 | return node.y + node.dy / 2; 292 | } 293 | 294 | function value(link) { 295 | return link.value; 296 | } 297 | 298 | function absValue(link) { 299 | return link.absValue; 300 | } 301 | 302 | return sankey; 303 | }; 304 | 305 | -------------------------------------------------------------------------------- /SenseSankey.js: -------------------------------------------------------------------------------- 1 | requirejs.config({ 2 | shim : { 3 | "extensions/SenseSankey/sankeymore" : { 4 | deps : ["extensions/SenseSankey/d3.min"], 5 | exports: 'd3.sankey' 6 | } 7 | } 8 | }); 9 | 10 | define( 11 | [ 12 | "jquery", 13 | "text!./style.css", 14 | "text!./theme.json", 15 | "extensions/SenseSankey/md5.min", 16 | "extensions/SenseSankey/sankeymore" 17 | ], 18 | 19 | function($, cssContent, Theme, md5) { 20 | 21 | 'use strict'; 22 | Theme = JSON.parse(Theme); 23 | var SenseSankeyVersion = "2.33"; 24 | 25 | $( "