Compare commits

..

25 Commits

Author SHA1 Message Date
Tobias Åström
d15b246db4 Merge pull request #17 from qlik-oss/tsm/QLIK-94112-promise
Make sure promise resolves properly
2019-03-08 14:58:33 +01:00
Tobias Åström
fc8e9b0ba4 Make sure promise resolves properly 2019-03-08 11:17:35 +01:00
John Lunde
b443deca27 Merge pull request #16 from qlik-oss/feature/QPE-592
[QPE-592] only load babel if not already loaded
2019-02-14 13:01:43 +01:00
Kristoffer Lind
8a24c3ee92 only load babel if not already loaded 2019-02-13 15:02:12 +01:00
Tobias Åström
464d137095 Add blackduck 2019-02-04 16:51:08 +01:00
John Lunde
903a2caa1d Merge pull request #15 from qlik-oss/caele/QPE-524
Fix issue with coloring
2019-01-11 14:56:14 +01:00
Tobias Åström
d68b2ed863 Fix issue with coloring 2019-01-10 13:12:14 +01:00
Piotr Nestorow
f87bc3ea88 Merge pull request #14 from qlik-oss/caele-test
Simplification of Network chart data
2019-01-10 09:04:53 +01:00
Tobias Åström
b7753143fd Hide undefined edge labels 2018-12-19 08:46:49 +01:00
Tobias Åström
8f239687f3 Correct selections and add descriptions 2018-12-18 16:54:36 +01:00
Tobias Åström
d970d05711 Make the last dimension optional 2018-12-14 11:09:31 +01:00
Tobias Åström
192f4a8597 Update coloring 2018-12-12 16:47:35 +01:00
Tobias Åström
6eff5e1fd0 Change to allow strings instead of only numerics for IDs 2018-12-12 16:47:02 +01:00
Martin Walter
ad63832d18 Merge pull request #13 from qlik-oss/feature/QPE-416
[QPE-416] Removed delete/replace flag
2018-12-05 13:53:24 +01:00
Martin Walter
28b3aeb676 [QPE-416] Removed delete/replace flag 2018-12-05 13:37:39 +01:00
Tobias Åström
321c71825e disable snapshot 2018-11-30 13:35:23 +01:00
Tobias Åström
6433daee95 Merge branch 'master' of https://github.com/qlik-oss/network-vis-chart 2018-11-30 13:31:24 +01:00
Tobias Åström
d210ad3908 disable snapshot 2018-11-30 13:31:05 +01:00
Tobias Åström
f461493b0f Added data clarification and example 2018-11-29 11:10:36 +01:00
Tobias Åström
4312078951 Added data sample 2018-11-29 11:06:49 +01:00
Tobias Åström
7c53f8811b Update qlik-network-chart.qext 2018-11-28 09:47:37 +01:00
Tobias Åström
51ea042580 Update qlik-network-chart.qext 2018-11-27 14:55:39 +01:00
Martin Walter
f9242dff27 Merge pull request #12 from qlik-oss/Caele-qext-update
Update qlik-network-chart.qext
2018-11-23 10:42:37 +01:00
Martin Walter
50fc4289e0 [QPE-331] Updated preview 2018-11-23 10:39:52 +01:00
Tobias Åström
71944b4a9e Update qlik-network-chart.qext 2018-11-23 09:10:25 +01:00
10 changed files with 274 additions and 176 deletions

View File

@@ -18,6 +18,14 @@ jobs:
- run:
name: Install dependencies
command: npm install
- run:
name: BlackDuck scan
command: curl -s https://blackducksoftware.github.io/hub-detect/hub-detect.sh | bash -s -- \
--blackduck.url="https://qliktech.blackducksoftware.com" \
--blackduck.trust.cert=true \
--blackduck.username="svc-blackduck" \
--blackduck.password=${svc_blackduck} \
--detect.project.name="viz-bundle-qlik-network-chart"
- run:
name: Run tests
command: npm run test-once

BIN
assets/network.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -17,6 +17,10 @@ gulp.task('zip-build', function(){
.pipe(gulp.dest(settings.buildDestination));
});
gulp.task('add-assets', function(){
return gulp.src("./assets/**/*").pipe(gulp.dest(settings.buildDestination));
});
gulp.task('webpack-build', done => {
webpack(webpackConfig, (error, statistics) => {
const compilationErrors = statistics && statistics.compilation.errors;
@@ -42,7 +46,7 @@ gulp.task('update-qext-version', function () {
});
gulp.task('build',
gulp.series('remove-build-folder', 'webpack-build', 'update-qext-version', 'zip-build')
gulp.series('remove-build-folder', 'webpack-build', 'update-qext-version', 'add-assets', 'zip-build')
);
gulp.task('watch', () => new Promise((resolve, reject) => {

View File

@@ -8,9 +8,9 @@ Tested with Qlik Sense 2.2.3.
### Dimensions
4 dimensions are mandatory :
1. node identifier
2. node label
3. node parent identifier
1. node identifier (consecutive numbers starting at 0)
2. node label (Any text)
3. node parent identifier (Refers to node indentifier)
4. node group
### Measures
@@ -28,6 +28,8 @@ The measures are optional
* Display Shadow : switch to enable shadow effects behind edge and nodes
### Sample
A data sample can be found here: https://github.com/qlik-oss/network-vis-chart/blob/master/resources/Network%20data.xlsx
QVF based on characters from Victor Hugo's novel , Les Misérables.
![Network chart](resources/network_chart_v1.png)

BIN
resources/Network data.xlsx Normal file

Binary file not shown.

View File

@@ -3,7 +3,7 @@ set -o errexit
echo "Creating release for version: $VERSION"
echo "Artifact name: ./build/${3}_${VERSION}.zip"
$HOME/bin/ghr -t ${ghoauth} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${VERSION} "./build/${3}_${4}.zip"
$HOME/bin/ghr -t ${ghoauth} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${VERSION} "./build/${3}_${4}.zip"
# Usage

View File

@@ -5,10 +5,37 @@ Tested on Qlik Sense 2.2.3
Agilos.com takes no responsibility for any code.
Use at your own risk.
*/
import "@babel/polyfill";
if (!window._babelPolyfill) { // eslint-disable-line no-underscore-dangle
require('@babel/polyfill'); // eslint-disable-line global-require
}
import paint from './paint';
import './styles/main.less';
const dimDesc = [
"Node Identifier",
"Node Label",
"Node Parent",
"Node Group"
];
const dimLongDesc = [
"Node Identifier - a field in the dataset which should be presented as a node in the network diagram."
+ " these control the actual elements presented in the network diagram.",
"Node Label - controls what field holds the data that described the nodes in the network"
+ " diagram. The field content will be presented as label.",
"Node Parent - is used to determine the ancestor node for the individual nodes."
+ " This field will be used for describing the relationships between network elements.",
"Node Group - is a field which describes groups of a node in the network."
+ " This is used to apply the same color to several nodes."
];
const measureDesc = [
"Tooltip",
"Node size",
"Edge size"
];
const component = {
initialProperties: {
version: 1.0,
@@ -24,8 +51,11 @@ const component = {
//property panel
data: {
dimensions: {
min: 4,
max: 4
min: 3,
max: 4,
description(properties, index) {
return dimDesc[index];
}
/*
1. Dimension: Node ID, numeric (Event ID or else) or String
2. Dimension: Node Label
@@ -35,7 +65,10 @@ const component = {
},
measures: {
min: 0,
max: 3
max: 3,
description(properties, index) {
return measureDesc[index];
}
/*
1. Measure: title text for tooltip (optional)
2. Measure: node value
@@ -51,7 +84,22 @@ const component = {
uses: "data",
items:{
dimensions:{
disabledRef: ""
disabledRef: "",
items: {
helpDesc: {
component: 'text',
style: 'qlik-network-chart-italic-property',
label: function(properties, handler) {
var index;
handler.getDimensions().forEach((element, i) => {
if(element.qDef.cId === properties.qDef.cId) {
index = i;
}
});
return dimLongDesc[index];
}
}
}
},
measures: {
disabledRef: ""
@@ -150,10 +198,11 @@ const component = {
}
},
support: {
export: true
export: true,
snapshot: false
},
snapshot: {
canTakeSnapshot: true
canTakeSnapshot: false
},
paint: paint
};

View File

@@ -3,184 +3,215 @@ import qlik from 'qlik';
import { createTooltipHTML } from './tooltip';
import { escapeHTML } from './utilities';
const colorScheme = 'Diverging Classes';
function isTextCellNotEmpty(c) {
return (c.qText && !(c.qIsNull || c.qText.trim() == ''));
}
function getColor (index, colors) {
return colors[index % colors.length];
}
function paint ( $element, layout, qTheme, component ) {
const colorScale = qTheme.properties.scales
.find(scale => scale.name === colorScheme).scale;
const colors = colorScale[colorScale.length - 1];
function getColor (number) {
return colors[number % colors.length];
}
return new qlik.Promise(function(resolve) {
var qData = layout.qHyperCube.qDataPages[0],
id = layout.qInfo.qId,
containerId = 'network-container_' + id;
const colorScale = qTheme.properties.palettes.data[0];
const numDimensions = layout.qHyperCube.qDimensionInfo.length;
const numMeasures = layout.qHyperCube.qMeasureInfo.length;
if(qData && qData.qMatrix) {
$element.empty().append($('<div />')
.attr({ id: containerId })
.toggleClass('is-edit-mode', component.inEditState())
.css({
height: $element.height(),
width: $element.width(),
overflow: 'auto'
}));
var qData = layout.qHyperCube.qDataPages[0],
id = layout.qInfo.qId,
containerId = 'network-container_' + id;
var dataSet = qData.qMatrix.map(function(e){
const nodeName = e[1].qText;
const groupNumber = e[3].qText;
if(qData && qData.qMatrix) {
$element.empty().append($('<div />')
.attr({ id: containerId })
.toggleClass('is-edit-mode', component.inEditState())
.css({
height: $element.height(),
width: $element.width(),
overflow: 'auto'
}));
const dataItem = {
id: e[0].qNum,
label: nodeName,
group: groupNumber,
parentid : e[2].qNum
var dataSet = qData.qMatrix.map(function(e){
const nodeName = e[1].qText;
let groupNumber;
const dataItem = {
id: e[0].qText,
eNum: e[0].qElemNumber,
label: nodeName,
parentid : e[2].qText
};
if(numDimensions === 4) {
groupNumber = e[3].qText;
dataItem.group = groupNumber;
}
// optional measures set
if (numMeasures > 0) {
const tooltip = e[numDimensions];
if (isTextCellNotEmpty(tooltip)) {
const tooltipText = tooltip.qText;
dataItem.title = escapeHTML(tooltipText);
} else if(numMeasures > 1) {
// This part is a bit fishy and should be tested
const nodeMeasure = e[numDimensions+1].qText;
dataItem.title = createTooltipHTML({
name: nodeName,
groupNumber,
nodeMeasure
});
}
}
if (numMeasures > 1) {
if (e[numDimensions+1].qNum) {
// node value - to scale node shape size
dataItem.nodeValue = e[5].qNum;
}
}
if (numMeasures > 2) {
if (e[numDimensions+2].qNum) {
// edge value - to scale edge width
dataItem.edgeValue = e[numDimensions+2].qNum;
}
}
return dataItem;
});
// Require 2 arrays : nodes and edges - nodes array must be unique values of IDs !
var uniqueId = [];
var nodes = [];
var edges = [];
const groups = {};
for(let i = 0; i< dataSet.length; i++){
if (layout.displayEdgeLabel && dataSet[i].edgeValue !== undefined) {
edges.push({
"from":dataSet[i].id,
"to":dataSet[i].parentid,
"value":dataSet[i].edgeValue,
"label": `${dataSet[i].edgeValue}`
}); // with labels
} else {
edges.push({
"from":dataSet[i].id,
"to":dataSet[i].parentid,
"value":dataSet[i].edgeValue
}); // create edges
}
// process uniqueness
if(uniqueId.indexOf(dataSet[i].id) === -1) {
uniqueId.push(dataSet[i].id);
var nodeItem = {
id: dataSet[i].id,
eNum: dataSet[i].eNum,
label: dataSet[i].label,
title: dataSet[i].title,
group: dataSet[i].group,
value: dataSet[i].nodeValue
};
nodes.push(nodeItem); // create node
groups[nodeItem.group] = {};
}
}
const colors = colorScale.scale[Math.min(Object.keys(groups).length-1, colorScale.scale.length-1)];
Object.keys(groups).forEach(function(g,i) {
groups[g].color = getColor(i, colors);
});
// create dataset for \\
var data = {
nodes: nodes,
edges: edges
};
// optional measures set
if (e.length > 4) {
const tooltip = e[4];
// create a network
var container = document.getElementById(containerId);
if (isTextCellNotEmpty(tooltip)) {
const tooltipText = tooltip.qText;
dataItem.title = escapeHTML(tooltipText);
} else {
const nodeMeasure = e[5].qText;
dataItem.title = createTooltipHTML({
name: nodeName,
groupNumber,
nodeMeasure
});
var options = {
groups: groups,
layout: {
randomSeed: 1
},
nodes: {
shape:layout.nodeShape,
shadow:layout.shadowMode
},
edges: {
shadow:layout.shadowMode,
font: {
align: layout.posEdgeLabel
},
smooth: {
type: layout.edgeType
}
},
interaction: {
hideEdgesOnDrag: true,
tooltipDelay: 100
},
physics: {
forceAtlas2Based: {
gravitationalConstant: -100,
centralGravity: 0.005,
springLength: 230,
springConstant: 0.18
},
maxVelocity: 146,
solver: 'forceAtlas2Based',
timestep: 0.35,
stabilization: { iterations: 150 }
}
}
};
var network = new Network(container, data, options);
network.fit();
if (e.length > 5) {
if (e[5].qNum) {
// node value - to scale node shape size
dataItem.nodeValue = e[5].qNum;
// Handle Selection on 1-node
$("#"+containerId).css('cursor','default');
network.on('select', function (properties) {
if (properties.hasOwnProperty("nodes")) {
if (properties.nodes.length > 0) {
// find connected nodes to selection
var connectedNodes = network.getConnectedNodes(properties.nodes[0]);
// append node to the array
connectedNodes.push(properties.nodes[0]);
const toSelect = [];
connectedNodes.forEach(function(node) {
var id;
data.nodes.forEach(function(dataNode) {
// Find match, ignore null
if(dataNode.id === node && node !== "-") {
id = dataNode.eNum;
}
});
if(id !== undefined) {
// Remove duplicates
toSelect.indexOf(id) === -1 && toSelect.push(id);
}
});
//Make the selections
component.backendApi.selectValues(0,toSelect,false);
}
}
}
});
if (e.length > 6) {
if (e[6].qNum) {
// edge value - to scale edge width
dataItem.edgeValue = e[6].qNum;
}
}
return dataItem;
});
// Require 2 arrays : nodes and edges - nodes array must be unique values of IDs !
var uniqueId = [];
var nodes = [];
var edges = [];
const groups = {};
for(let i = 0; i< dataSet.length; i++){
if (layout.displayEdgeLabel) {
edges.push({
"from":dataSet[i].id,
"to":dataSet[i].parentid,
"value":dataSet[i].edgeValue,
"label": `${dataSet[i].edgeValue}`
}); // with labels
} else {
edges.push({
"from":dataSet[i].id,
"to":dataSet[i].parentid,
"value":dataSet[i].edgeValue
}); // create edges
}
// process uniqueness
if(uniqueId.indexOf(dataSet[i].id) === -1) {
uniqueId.push(dataSet[i].id);
var nodeItem = {
id: dataSet[i].id,
label: dataSet[i].label,
title: dataSet[i].title,
group: dataSet[i].group,
value: dataSet[i].nodeValue
};
nodes.push(nodeItem); // create node
groups[nodeItem.group] = {
color: getColor(nodeItem.group)
};
}
network.on('stabilizationIterationsDone', function() {
resolve();
});
} else {
resolve();
}
// create dataset for \\
var data = {
nodes: nodes,
edges: edges
};
// create a network
var container = document.getElementById(containerId);
var options = {
groups: groups,
layout: {
randomSeed: 1
},
nodes: {
shape:layout.nodeShape,
shadow:layout.shadowMode
},
edges: {
shadow:layout.shadowMode,
font: {
align: layout.posEdgeLabel
},
smooth: {
type: layout.edgeType
}
},
interaction: {
hideEdgesOnDrag: true,
tooltipDelay: 100
},
physics: {
forceAtlas2Based: {
gravitationalConstant: -100,
centralGravity: 0.005,
springLength: 230,
springConstant: 0.18
},
maxVelocity: 146,
solver: 'forceAtlas2Based',
timestep: 0.35,
stabilization: { iterations: 150 }
}
};
var network = new Network(container, data, options);
network.fit();
// Handle Selection on 1-node
$("#"+containerId).css('cursor','default');
network.on('select', function (properties) {
if (properties.hasOwnProperty("nodes")) {
if (properties.nodes.length > 0) {
// find connected nodes to selection
var connectedNodes = network.getConnectedNodes(properties.nodes[0]);
// append node to the array
connectedNodes.push(properties.nodes[0]);
//Make the selections
component.backendApi.selectValues(0,connectedNodes,false);
}
}
});
}
});
}
function themePaint ($element, layout) {
@@ -188,8 +219,8 @@ function themePaint ($element, layout) {
try {
const app = qlik.currApp(this);
app.theme.getApplied().then( function( qTheme ) {
paint($element, layout, qTheme, component);
return app.theme.getApplied().then( function( qTheme ) {
return paint($element, layout, qTheme, component);
});
} catch (exception) {
console.error(exception); // eslint-disable-line no-console

View File

@@ -1,7 +1,7 @@
{
"name" : "Network chart",
"description" : "Network chart",
"icon" : "extension",
"description" : "Displays hierarchical or relational dimensions as nodes and edges´, with measures to show the significance of its links.",
"icon" : "bubbles",
"type" : "visualization",
"version": "X.Y.Z",
"preview" : "network.png",

View File

@@ -11,3 +11,7 @@
padding: 10px;
max-width: 200px;
}
.qlik-network-chart-italic-property {
font-style: italic;
}