Compare commits
103 Commits
project-co
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f116d38a2 | ||
|
|
291e2bd05f | ||
|
|
7fa1ee419f | ||
|
|
e26d77cb5d | ||
|
|
f6facc8d21 | ||
|
|
e646bd242b | ||
|
|
047b248c01 | ||
|
|
39cacf167f | ||
|
|
d140c89e5a | ||
|
|
d5ed44c62c | ||
|
|
4249dfa8cb | ||
|
|
7198257f40 | ||
|
|
e74991445a | ||
|
|
2914f13f6e | ||
|
|
bd12b6519b | ||
|
|
ec4af50cff | ||
|
|
43f32a470a | ||
|
|
a2209e160f | ||
|
|
63ba3a9362 | ||
|
|
dd0af64115 | ||
|
|
aa2ef96469 | ||
|
|
e9b087bde2 | ||
|
|
25d5f4e4da | ||
|
|
f02e0c17e5 | ||
|
|
e406acef3a | ||
|
|
9669ccfdaf | ||
|
|
a99a14c541 | ||
|
|
b9c7d2b0f1 | ||
|
|
955d954b6f | ||
|
|
4baadc1157 | ||
|
|
e467da6b46 | ||
|
|
7f4c6af61d | ||
|
|
3e199979fd | ||
|
|
3e1384c900 | ||
|
|
3ecaf3de42 | ||
|
|
d95c0f572e | ||
|
|
fd2f9fa277 | ||
|
|
e2aac7a294 | ||
|
|
b6bcbe7f75 | ||
|
|
f4441ef683 | ||
|
|
3a832e7d6a | ||
|
|
22da42de9f | ||
|
|
fddf286a8e | ||
|
|
e6692b8779 | ||
|
|
4341fdb5db | ||
|
|
d02852b2ed | ||
|
|
25f6593f35 | ||
|
|
76a22121a9 | ||
|
|
d4154fde09 | ||
|
|
d65b619546 | ||
|
|
d15b246db4 | ||
|
|
fc8e9b0ba4 | ||
|
|
b443deca27 | ||
|
|
8a24c3ee92 | ||
|
|
464d137095 | ||
|
|
903a2caa1d | ||
|
|
d68b2ed863 | ||
|
|
f87bc3ea88 | ||
|
|
b7753143fd | ||
|
|
8f239687f3 | ||
|
|
d970d05711 | ||
|
|
192f4a8597 | ||
|
|
6eff5e1fd0 | ||
|
|
ad63832d18 | ||
|
|
28b3aeb676 | ||
|
|
321c71825e | ||
|
|
6433daee95 | ||
|
|
d210ad3908 | ||
|
|
f461493b0f | ||
|
|
4312078951 | ||
|
|
7c53f8811b | ||
|
|
51ea042580 | ||
|
|
f9242dff27 | ||
|
|
50fc4289e0 | ||
|
|
71944b4a9e | ||
|
|
401944e837 | ||
|
|
67b1e97951 | ||
|
|
8079887f10 | ||
|
|
43c5856986 | ||
|
|
ec98314793 | ||
|
|
0953911571 | ||
|
|
0a673631b2 | ||
|
|
b1ade90e8b | ||
|
|
4fa54e3fb2 | ||
|
|
ba89c2108f | ||
|
|
e9a28e4f0b | ||
|
|
c088774e75 | ||
|
|
7d6bd5696a | ||
|
|
dd30f94b34 | ||
|
|
2eef3679c7 | ||
|
|
7518b6c9ce | ||
|
|
11f2bad2bd | ||
|
|
06a28a2ae7 | ||
|
|
5c84aeb8ee | ||
|
|
fe1c3d90e2 | ||
|
|
78a441036c | ||
|
|
7cba160828 | ||
|
|
15be850423 | ||
|
|
6087cb5619 | ||
|
|
2af87e2d42 | ||
|
|
925fcb8824 | ||
|
|
c7ee067b95 | ||
|
|
68bdaa7411 |
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["env", {
|
||||
"targets": {
|
||||
"chrome": "47"
|
||||
}
|
||||
}]
|
||||
]
|
||||
}
|
||||
@@ -10,85 +10,36 @@ defaults: &defaults
|
||||
PACKAGE_NAME: "qlik-network-chart"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:stretch-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm install
|
||||
command: yarn
|
||||
- run:
|
||||
name: Run tests
|
||||
command: npm run test-once
|
||||
|
||||
bump-version:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
name: Lint
|
||||
command: yarn eslint
|
||||
- run:
|
||||
name: Bump version
|
||||
command: scripts/bump-version.sh $GITHUB_ORG $GITHUB_REPO
|
||||
- persist_to_workspace:
|
||||
root: ~/qlik-network-chart
|
||||
paths:
|
||||
- BUMPED_VERSION
|
||||
|
||||
build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/qlik-network-chart
|
||||
name: BlackDuck scan
|
||||
command: curl -s https://detect.synopsys.com/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: Install dependencies
|
||||
command: npm install
|
||||
- run:
|
||||
name: Build and package
|
||||
command: |
|
||||
export VERSION=$(scripts/get-bumped-version.sh)
|
||||
echo "Version: ${VERSION}"
|
||||
npm run build
|
||||
name: Build
|
||||
command: yarn build && yarn sense
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
- persist_to_workspace:
|
||||
root: ~/qlik-network-chart
|
||||
paths:
|
||||
- build
|
||||
- store_artifacts:
|
||||
path: build
|
||||
destination: build
|
||||
deploy:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/qlik-network-chart
|
||||
- run:
|
||||
name: Install ghr
|
||||
command: scripts/install-ghr.sh
|
||||
- run:
|
||||
name: Create GitHub Release
|
||||
command: |
|
||||
export VERSION=$(scripts/get-bumped-version.sh)
|
||||
echo "Version: ${VERSION}"
|
||||
scripts/create-release.sh $GITHUB_ORG $GITHUB_REPO $PACKAGE_NAME $VERSION
|
||||
path: dist
|
||||
destination: dist
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
master_flow:
|
||||
jobs:
|
||||
- test
|
||||
- bump-version:
|
||||
requires:
|
||||
- test
|
||||
- build:
|
||||
requires:
|
||||
- bump-version
|
||||
- deploy:
|
||||
requires:
|
||||
- build
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- build
|
||||
|
||||
@@ -42,7 +42,7 @@ module.exports = {
|
||||
"no-cond-assign": ["warn"],
|
||||
"no-fallthrough": ["warn"],
|
||||
"no-undef": ["warn"],
|
||||
"no-unused-vars": ["warn"],
|
||||
"no-unused-vars": ["error"],
|
||||
"no-use-before-define": ["warn", { "functions": false, "classes": false, "variables": false }],
|
||||
"no-useless-escape": ["warn"],
|
||||
"no-useless-return": ["warn"],
|
||||
|
||||
4
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
BUMPED_VERSION
|
||||
sn-network-chart-ext/
|
||||
core/esm
|
||||
|
||||
136
api-specifications/properties.json
Normal file
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"scriptappy": "1.0.0",
|
||||
"info": {
|
||||
"name": "sn-network-chart:properties",
|
||||
"description": "Network chart generic object definition",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"stability": "experimental",
|
||||
"x-qlik-visibility": "public"
|
||||
},
|
||||
"entries": {},
|
||||
"definitions": {
|
||||
"module.exports.displayEdgeLabel": {
|
||||
"optional": true,
|
||||
"defaultValue": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"module.exports.edgeType": {
|
||||
"optional": true,
|
||||
"defaultValue": "dynamic",
|
||||
"kind": "union",
|
||||
"items": [
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'dynamic'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'continuous'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'discrete'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'diagonalCross'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'straightCross'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'horizontal'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'vertical'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'curvedCW'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'curvedCCW'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'cubicBezier'"
|
||||
}
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"module.exports.nodeShape": {
|
||||
"optional": true,
|
||||
"defaultValue": "dot",
|
||||
"kind": "union",
|
||||
"items": [
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'dot'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'square'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'star'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'triangle'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'triangleDown'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'diamond'"
|
||||
}
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"module.exports.posEdgeLabel": {
|
||||
"optional": true,
|
||||
"defaultValue": "top",
|
||||
"kind": "union",
|
||||
"items": [
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'top'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'middle'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'bottom'"
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"value": "'horizontal'"
|
||||
}
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"module.exports.qHyperCubeDef": {
|
||||
"kind": "object",
|
||||
"entries": {}
|
||||
},
|
||||
"module.exports.shadowMode": {
|
||||
"optional": true,
|
||||
"defaultValue": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"module.exports.version": {
|
||||
"description": "Current version of this generic object definition",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
assets/network.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
8
core/package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"module": "esm/index.js",
|
||||
"peerDependencies": {
|
||||
"@nebula.js/stardust": ">=1.1.1",
|
||||
"vis-network": "9.1.0",
|
||||
"vis-data": "^7.0.0"
|
||||
}
|
||||
}
|
||||
72
gulpfile.js
@@ -1,72 +0,0 @@
|
||||
var gulp = require('gulp');
|
||||
var zip = require('gulp-zip');
|
||||
var del = require('del');
|
||||
var settings = require('./settings');
|
||||
var webpackConfig = require('./webpack.config');
|
||||
var webpack = require('webpack');
|
||||
var WebpackDevServer = require('webpack-dev-server');
|
||||
var jeditor = require("gulp-json-editor");
|
||||
|
||||
gulp.task('remove-build-folder', function(){
|
||||
return del([settings.buildDestination], { force: true });
|
||||
});
|
||||
|
||||
gulp.task('zip-build', function(){
|
||||
return gulp.src(settings.buildDestination + '/**/*')
|
||||
.pipe(zip(`${settings.name}_${settings.version}.zip`))
|
||||
.pipe(gulp.dest(settings.buildDestination));
|
||||
});
|
||||
|
||||
gulp.task('webpack-build', done => {
|
||||
webpack(webpackConfig, (error, statistics) => {
|
||||
const compilationErrors = statistics && statistics.compilation.errors;
|
||||
const hasCompilationErrors = !statistics || (compilationErrors && compilationErrors.length > 0);
|
||||
|
||||
console.log(statistics && statistics.toString({ chunks: false, colors: true })); // eslint-disable-line no-console
|
||||
|
||||
if (error || hasCompilationErrors) {
|
||||
console.log('Build has errors or eslint errors, fail it'); // eslint-disable-line no-console
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('update-qext-version', function () {
|
||||
return gulp.src(`${settings.buildDestination}/${settings.name}.qext`)
|
||||
.pipe(jeditor({
|
||||
'version': settings.version
|
||||
}))
|
||||
.pipe(gulp.dest(settings.buildDestination));
|
||||
});
|
||||
|
||||
gulp.task('build',
|
||||
gulp.series('remove-build-folder', 'webpack-build', 'update-qext-version', 'zip-build')
|
||||
);
|
||||
|
||||
gulp.task('watch', () => new Promise((resolve, reject) => {
|
||||
webpackConfig.entry.unshift('webpack-dev-server/client?http://localhost:' + settings.port);
|
||||
const compiler = webpack(webpackConfig);
|
||||
const originalOutputFileSystem = compiler.outputFileSystem;
|
||||
const devServer = new WebpackDevServer(compiler, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
},
|
||||
}).listen(settings.port, 'localhost', error => {
|
||||
compiler.outputFileSystem = originalOutputFileSystem;
|
||||
if (error) {
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Listening at localhost:' + settings.port);
|
||||
|
||||
resolve(null, devServer);
|
||||
});
|
||||
}));
|
||||
|
||||
gulp.task('default',
|
||||
gulp.series('build')
|
||||
);
|
||||
@@ -1,35 +0,0 @@
|
||||
const settings = require('./settings');
|
||||
|
||||
module.exports = (config) => {
|
||||
config.set({
|
||||
browsers: ['SlimChromeHeadless'],
|
||||
customLaunchers: {
|
||||
SlimChromeHeadless: {
|
||||
base: 'ChromeHeadless',
|
||||
flags: ['--headless', '--disable-gpu', '--disable-translate', '--disable-extensions']
|
||||
}
|
||||
},
|
||||
files: [
|
||||
{ pattern: 'src/*.spec.js', watched: false }
|
||||
],
|
||||
frameworks: ['jasmine'],
|
||||
preprocessors: {
|
||||
'src/*.spec.js': ['webpack', 'sourcemap']
|
||||
},
|
||||
webpack: {
|
||||
devtool: 'source-map',
|
||||
mode: settings.mode,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: [/node_modules/],
|
||||
loaders: ['babel-loader']
|
||||
},
|
||||
{ test: /\.less$/, loader: 'ignore-loader' },
|
||||
{ test: /\.json$/, loader: 'ignore-loader' }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
21
nebula.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const { name, version } = require(path.resolve(__dirname, './package.json')); // eslint-disable-line
|
||||
|
||||
const versionHash = crypto
|
||||
.createHash('md5')
|
||||
.update(`${name}@${version}`)
|
||||
.digest('hex')
|
||||
.slice(0, 4);
|
||||
|
||||
const replacementStrings = {
|
||||
'process.env.VERSION_HASH': JSON.stringify(versionHash),
|
||||
'process.env.PACKAGE_VERSION': JSON.stringify(version),
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
build: {
|
||||
replacementStrings,
|
||||
},
|
||||
};
|
||||
11636
package-lock.json
generated
71
package.json
@@ -1,47 +1,42 @@
|
||||
{
|
||||
"name": "qlik-network-chart",
|
||||
"version": "0.0.1",
|
||||
"description": "Network chart",
|
||||
"keywords": "network chart qliksense extension",
|
||||
"name": "@nebula.js/sn-network-chart",
|
||||
"version": "0.1.0",
|
||||
"description": "Displays hierarchical or relational dimensions as nodes and edges´, with measures to show the significance of its links.",
|
||||
"homepage": "",
|
||||
"repository": "https://github.com/qlik-oss/network-vis-chart",
|
||||
"author": "Michael Laenen (miclae76) <m.laenen@contactoffice.net>",
|
||||
"author": "QLIK",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "gulp build",
|
||||
"start": "nebula serve --build false --type qlik-network-chart",
|
||||
"watch": "nebula serve --type sn-network-chart",
|
||||
"watch:legacy": "nebula serve --type qlik-network-chart",
|
||||
"build": "nebula build --core core",
|
||||
"sense": "nebula sense --meta resources/meta.json && shx cp resources/network_chart_v1.png sn-network-chart-ext",
|
||||
"eslint": "eslint src",
|
||||
"eslint:fix": "eslint --fix src",
|
||||
"test": "karma start karma.conf.js",
|
||||
"test-once": "karma start karma.conf.js --single-run",
|
||||
"watch": "gulp watch"
|
||||
"spec": "scriptappy-from-jsdoc -c ./spec-configs/props.conf.js",
|
||||
"prepublishOnly": "shx rm -rf dist && shx rm -rf core/esm && shx rm -rf sn-network-chart-ext && yarn build && yarn sense"
|
||||
},
|
||||
"files": [
|
||||
"api-specifications",
|
||||
"dist",
|
||||
"core",
|
||||
"sn-network-chart-ext"
|
||||
],
|
||||
"main": "dist/sn-network-chart.js",
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.1.5",
|
||||
"@babel/polyfill": "7.0.0",
|
||||
"@babel/preset-env": "7.1.5",
|
||||
"babel-eslint": "10.0.1",
|
||||
"babel-loader": "8.0.4",
|
||||
"copy-webpack-plugin": "4.6.0",
|
||||
"css-loader": "1.0.1",
|
||||
"del": "3.0.0",
|
||||
"eslint": "5.8.0",
|
||||
"eslint-loader": "2.1.1",
|
||||
"file-loader": "2.0.0",
|
||||
"gulp": "4.0.0",
|
||||
"gulp-json-editor": "2.4.3",
|
||||
"gulp-zip": "4.2.0",
|
||||
"jasmine-core": "3.3.0",
|
||||
"karma": "3.1.1",
|
||||
"karma-chrome-launcher": "2.2.0",
|
||||
"karma-jasmine": "1.1.2",
|
||||
"karma-sourcemap-loader": "0.3.7",
|
||||
"karma-webpack": "3.0.5",
|
||||
"less": "3.8.1",
|
||||
"less-loader": "4.1.0",
|
||||
"style-loader": "0.23.1",
|
||||
"stylelint": "9.7.1",
|
||||
"stylelint-webpack-plugin": "0.10.5",
|
||||
"webpack": "4.25.1",
|
||||
"webpack-cli": "3.1.2",
|
||||
"webpack-dev-server": "3.1.10"
|
||||
"@nebula.js/cli": "1.7.0",
|
||||
"@nebula.js/cli-build": "1.7.0",
|
||||
"@nebula.js/cli-sense": "1.7.0",
|
||||
"@nebula.js/cli-serve": "1.7.0",
|
||||
"@nebula.js/stardust": "1.7.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"scriptappy-from-jsdoc": "^0.7.0",
|
||||
"eslint": "7.32.0",
|
||||
"shx": "^0.3.3",
|
||||
"vis-network": "9.1.0",
|
||||
"vis-data": "^7.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nebula.js/stardust": ">=1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
104
readme.md
@@ -1,66 +1,76 @@
|
||||
# Qlik Network Chart
|
||||
# @nebula.js/sn-network-chart
|
||||
|
||||
[](https://circleci.com/gh/qlik-oss/network-vis-chart)
|
||||
The network chart is built using [visjs network visualization](https://github.com/visjs/vis-network) to display networks of nodes and edges. It was originally forked from [miclae76/network-vis-chart](miclae76/network-vis-chart) and has since been converted to use Nebula.
|
||||
|
||||
Qlik Sense extension to visualize networks data based on library vis.js (http://visjs.org).
|
||||
Tested with Qlik Sense 2.2.3.
|
||||
## Requirements
|
||||
|
||||
### Dimensions
|
||||
4 dimensions are mandatory :
|
||||
Requires `@nebula.js/stardust` version `1.7.0` or later.
|
||||
|
||||
1. node identifier
|
||||
2. node label
|
||||
3. node parent identifier
|
||||
4. node group
|
||||
## Installing
|
||||
|
||||
### Measures
|
||||
The measures are optional
|
||||
If you use npm: `npm install @nebula.js/sn-network-chart`.
|
||||
|
||||
1. tooltip : expression that will be push in the tooltip when hover on a node
|
||||
2. node value : used to scale the node size
|
||||
3. edge value : used to scale the edge width
|
||||
You can also load through the script tag directly from [https://unpkg.com](https://unpkg.com/@nebula.js/sn-network-chart).
|
||||
|
||||
### Additional network settings
|
||||
* Edge Type : select type of curve between nodes
|
||||
* Node Shape : dot, square, diamond, triangle ...
|
||||
* Display Edge Value : switch to display the measures on edge curves
|
||||
* Position Edge Label : top, bottom, middle, horizontal
|
||||
* Display Shadow : switch to enable shadow effects behind edge and nodes
|
||||
## Usage
|
||||
|
||||
### Sample
|
||||
QVF based on characters from Victor Hugo's novel , Les Misérables.
|
||||

|
||||
```js
|
||||
import { embed } from '@nebula.js/stardust';
|
||||
import network from '@nebula.js/sn-network-chart';
|
||||
|
||||
// 'app' is an enigma app model
|
||||
const nuked = embed(app, {
|
||||
types: [
|
||||
{
|
||||
// register grid chart - qlik-network-chart is the default name in sense
|
||||
name: 'qlik-network-chart',
|
||||
load: () => Promise.resolve(network),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
# Getting Started
|
||||
// Rendering a simple network chart
|
||||
nuked.render({
|
||||
element: document.querySelector('.network'),
|
||||
type: 'qlik-network-chart',
|
||||
fields: ['Source', 'Target', '=Sum(Flow)'],
|
||||
properties: {
|
||||
title: 'Visualization of network flows',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Installation
|
||||
## Data sample
|
||||
|
||||
1. Download the extension zip, `qlik-network-chart_<version>.zip`, from the latest release(https://github.com/qlik-oss/network-vis-chart/releases/latest)
|
||||
2. Install the extension:
|
||||
Check `resources/Network data.xlsx` for an example. The simplest data form is where each row represents an edge in the network. Take this example of airport connections:
|
||||
|
||||
a. **Qlik Sense Desktop**: unzip to a directory under [My Documents]/Qlik/Sense/Extensions.
|
||||
| AirportID | AirportName | LinkToId | Volume |
|
||||
|-----------|----------------|----------|--------|
|
||||
| 0 | Soekarno-Hatta | 3 | 23000 |
|
||||
| 1 | Halim | 0 | 5460 |
|
||||
| 2 | Changi | 0 | 10870 |
|
||||
| 3 | KLCC | 1 | 2780 |
|
||||
| 4 | Don Muang | 1 | 4800 |
|
||||
| 4 | Don Muang | 2 | 7800 |
|
||||
|
||||
b. **Qlik Sense Server**: import the zip file in the QMC.
|
||||
Sense inline load script example:
|
||||
|
||||
|
||||
# Developing the extension
|
||||
|
||||
If you want to do code changes to the extension follow these simple steps to get going.
|
||||
|
||||
1. Get Qlik Sense Desktop
|
||||
1. Create a new app and add the extension to a sheet.
|
||||
2. Clone the repository
|
||||
3. Run `npm install`
|
||||
4. Set the environment variable `BUILD_PATH` to your extensions directory. It will be something like `C:/Users/<user>/Documents/Qlik/Sense/Extensions/<extension_name>`.
|
||||
5. You now have two options. Either run the watch task or the build task. They are explained below. Both of them default to development mode but can be run in production by setting `NODE_ENV=production` before running the npm task.
|
||||
|
||||
a. **Watch**: `npm run watch`. This will start a watcher which will rebuild the extension and output all needed files to the `buildFolder` for each code change you make. See your changes directly in your Qlik Sense app.
|
||||
|
||||
b. **Build**: `npm run build`. If you want to build the extension package. The output zip-file can be found in the `buildFolder`.
|
||||
```
|
||||
Load * Inline [
|
||||
AirportID, AirportName, LinktoID,Volume
|
||||
0,Soekarno-Hatta,3,23000
|
||||
1,Halim,0,5460
|
||||
2,Changi,0,10870
|
||||
3,KLCC,1,2780
|
||||
4,Don Muang,1,4800
|
||||
4,Don Muang,2,7800
|
||||
];
|
||||
```
|
||||
|
||||
# Original Author
|
||||
|
||||
**Michael Laenen**
|
||||
|
||||
* [github.com/miclae76](https://github.com/miclae76)
|
||||
|
||||
# Contributors
|
||||
**Göran Sander**
|
||||
* [github.com/mountaindude](https://github.com/mountaindude)
|
||||
|
||||
BIN
resources/Network data.xlsx
Normal file
4
resources/meta.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Network chart",
|
||||
"icon": "bubbles"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
|
||||
join_by () {
|
||||
local IFS="$1"; shift; echo "$*";
|
||||
}
|
||||
|
||||
if [ "${CIRCLE_BRANCH}" == "master" ]; then
|
||||
# get version from repo
|
||||
OLD_VERSION="$(scripts/get-latest-version.sh $1 $2)"
|
||||
echo "Latest GitHub release version: ${OLD_VERSION}"
|
||||
|
||||
# split into array
|
||||
IFS='.' read -ra ARRAY_VERSION <<< "$OLD_VERSION"
|
||||
|
||||
# bump minor
|
||||
ARRAY_VERSION[1]=$((ARRAY_VERSION[1]+1))
|
||||
|
||||
# join into string
|
||||
NEW_VERSION=$(join_by . ${ARRAY_VERSION[@]})
|
||||
elif [[ ! -z "${CIRCLE_BRANCH}" && ! -z "${CIRCLE_BUILD_NUM}" ]]; then
|
||||
NEW_VERSION="$(echo ${CIRCLE_BRANCH} | sed -e 's/\//-/g')_${CIRCLE_BUILD_NUM}"
|
||||
else
|
||||
NEW_VERSION="dev"
|
||||
fi
|
||||
|
||||
echo "Bumped version: ${NEW_VERSION}"
|
||||
echo "${NEW_VERSION}" > BUMPED_VERSION
|
||||
|
||||
|
||||
# Usage
|
||||
# $ bump-version.sh qlik-oss qsSimpleKPI
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
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"
|
||||
|
||||
|
||||
# Usage
|
||||
# $ create-release.sh qlik-oss qsSimpleKPI qlik-multi-kpi 0.3.1
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
|
||||
echo "$(head -n 1 BUMPED_VERSION)"
|
||||
|
||||
# Usage
|
||||
# $ get-bumped-version.sh
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
|
||||
VERSION=$(curl --silent "https://api.github.com/repos/$1/$2/releases/latest" | # Get latest release from GitHub api
|
||||
grep '"tag_name":' | # Get tag line
|
||||
sed -E 's/.*"([^"]+)".*/\1/') # Pluck JSON value
|
||||
|
||||
if [ -z "${VERSION}" ]; then
|
||||
VERSION="0.1.0"
|
||||
fi
|
||||
|
||||
echo "${VERSION}"
|
||||
|
||||
### Inspired by https://gist.github.com/lukechilds/a83e1d7127b78fef38c2914c4ececc3c
|
||||
# Usage
|
||||
# $ get-latest-version.sh qlik-oss qsSimpleKPI
|
||||
# 0.12.0
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -o errexit -o verbose
|
||||
|
||||
URL="https://github.com/tcnksm/ghr/releases/download/v0.5.4/ghr_v0.5.4_linux_386.zip"
|
||||
echo "Version to install: $URL"
|
||||
|
||||
echo "Installing ghr"
|
||||
curl -L ${URL} > ghr.zip
|
||||
mkdir -p "$HOME/bin"
|
||||
export PATH="$HOME/bin:$PATH"
|
||||
unzip ghr.zip -d "$HOME/bin"
|
||||
rm ghr.zip
|
||||
13
settings.js
@@ -1,13 +0,0 @@
|
||||
const path = require('path');
|
||||
const packageJSON = require('./package.json');
|
||||
|
||||
const defaultBuildDestination = path.resolve("./build");
|
||||
|
||||
module.exports = {
|
||||
buildDestination: process.env.BUILD_PATH || defaultBuildDestination,
|
||||
mode: process.env.NODE_ENV || 'development',
|
||||
name: packageJSON.name,
|
||||
version: process.env.VERSION || 'local-dev',
|
||||
url: process.env.BUILD_URL || defaultBuildDestination,
|
||||
port: 8082
|
||||
};
|
||||
26
spec-configs/props.conf.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const path = require('path');
|
||||
|
||||
const pkg = require(path.resolve(__dirname, '../package.json')); // eslint-disable-line
|
||||
|
||||
module.exports = {
|
||||
glob: ['./src/extension/properties.js'],
|
||||
package: path.resolve(__dirname, '../package.json'),
|
||||
api: {
|
||||
stability: 'experimental',
|
||||
properties: {
|
||||
'x-qlik-visibility': 'public',
|
||||
},
|
||||
visibility: 'public',
|
||||
name: `${pkg.name}:properties`,
|
||||
version: pkg.version,
|
||||
description: 'Network chart generic object definition',
|
||||
},
|
||||
output: {
|
||||
file: path.resolve(__dirname, '../api-specifications/properties.json'),
|
||||
},
|
||||
parse: {
|
||||
types: {
|
||||
NxMeasure: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
Before Width: | Height: | Size: 25 KiB |
34
src/extension/data.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { dimDesc, measureDesc } from './strings';
|
||||
|
||||
export default function data() {
|
||||
return {
|
||||
targets: [{
|
||||
path: '/qHyperCubeDef',
|
||||
dimensions: {
|
||||
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
|
||||
3. Dimension: Node Parent ID, numeric (Event ID or else) or String
|
||||
4. Dimension: Node Cluster
|
||||
*/
|
||||
},
|
||||
measures: {
|
||||
min: 0,
|
||||
max: 3,
|
||||
description(properties, index) {
|
||||
return measureDesc[index];
|
||||
}
|
||||
/*
|
||||
1. Measure: title text for tooltip (optional)
|
||||
2. Measure: node value
|
||||
3. Measure: edge value
|
||||
*/
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
164
src/extension/ext.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import { dimLongDesc } from './strings';
|
||||
|
||||
export default function ext() {
|
||||
return {
|
||||
definition: {
|
||||
type: "items",
|
||||
component: "accordion",
|
||||
items: {
|
||||
data: {
|
||||
uses: "data",
|
||||
items:{
|
||||
dimensions:{
|
||||
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: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
sorting: {
|
||||
uses: "sorting"
|
||||
},
|
||||
addons: {
|
||||
uses: "addons",
|
||||
items: {
|
||||
dataHandling: {
|
||||
uses: "dataHandling"
|
||||
}
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
type: "items",
|
||||
uses: 'settings',
|
||||
items: {
|
||||
presentation: {
|
||||
type: 'items',
|
||||
grouped: false,
|
||||
translation: 'properties.presentation',
|
||||
items: {
|
||||
edgeType: {
|
||||
ref: "edgeType",
|
||||
type: "string",
|
||||
component: "dropdown",
|
||||
label: "Edge Type",
|
||||
options: [
|
||||
{ value: 'dynamic' },
|
||||
{ value: 'continuous' },
|
||||
{ value: 'discrete' },
|
||||
{ value: 'diagonalCross' },
|
||||
{ value: 'straightCross' },
|
||||
{ value: 'horizontal' },
|
||||
{ value: 'vertical' },
|
||||
{ value: 'curvedCW' },
|
||||
{ value: 'curvedCCW' },
|
||||
{ value: 'cubicBezier' }
|
||||
],
|
||||
defaultValue: "dynamic"
|
||||
},
|
||||
displayEdgeLabel : {
|
||||
ref: "displayEdgeLabel",
|
||||
type: "boolean",
|
||||
component: "switch",
|
||||
label: "Display Edge Value",
|
||||
options: [{
|
||||
value: true,
|
||||
label: "On"
|
||||
}, {
|
||||
value: false,
|
||||
label: "Off"
|
||||
}],
|
||||
defaultValue: false
|
||||
},
|
||||
posEdgeLabel: {
|
||||
ref: "posEdgeLabel",
|
||||
type: "string",
|
||||
component: "dropdown",
|
||||
label: "Position Edge Label",
|
||||
options: [
|
||||
{ value: 'top' }, { value: 'middle' }, { value: 'bottom' }, { value: 'horizontal' }
|
||||
],
|
||||
defaultValue: "top"
|
||||
},
|
||||
nodeShape: {
|
||||
ref: "nodeShape",
|
||||
type: "string",
|
||||
component: "dropdown",
|
||||
label: "Node Shape",
|
||||
options: [
|
||||
{ value: 'dot' },
|
||||
{ value: 'square' },
|
||||
{ value: 'star' },
|
||||
{ value: 'triangle' },
|
||||
{ value: 'triangleDown' },
|
||||
{ value: 'diamond' }
|
||||
],
|
||||
defaultValue: "dot"
|
||||
},
|
||||
shadowMode: {
|
||||
ref: "shadowMode",
|
||||
type: "boolean",
|
||||
component: "switch",
|
||||
label: "Display Shadow",
|
||||
options: [{
|
||||
value: true,
|
||||
label: "On"
|
||||
}, {
|
||||
value: false,
|
||||
label: "Off"
|
||||
}],
|
||||
defaultValue: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
about: {
|
||||
component: 'items',
|
||||
label: 'About',
|
||||
items: {
|
||||
header: {
|
||||
label: 'Network chart',
|
||||
style: 'header',
|
||||
component: 'text'
|
||||
},
|
||||
paragraph1: {
|
||||
label:
|
||||
`Network chart is Qlik Sense chart which
|
||||
allows you to draw a network of connected
|
||||
nodes and edges from a data set to a sheet.`,
|
||||
component: 'text'
|
||||
},
|
||||
paragraph2: {
|
||||
label: 'Network chart is based upon an extension created by Michael Laenen.',
|
||||
component: 'text'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
support: {
|
||||
export: true,
|
||||
snapshot: true,
|
||||
exportData: true
|
||||
},
|
||||
snapshot: {
|
||||
canTakeSnapshot: true
|
||||
},
|
||||
};
|
||||
}
|
||||
54
src/extension/properties.js
Normal file
@@ -0,0 +1,54 @@
|
||||
export default {
|
||||
/**
|
||||
* Current version of this generic object definition
|
||||
* @type {string}
|
||||
*/
|
||||
version: process.env.PACKAGE_VERSION,
|
||||
/**
|
||||
* @typedef
|
||||
*/
|
||||
qHyperCubeDef: {
|
||||
qDimensions: [],
|
||||
qMeasures: [],
|
||||
qInitialDataFetch: [{
|
||||
qWidth: 7,
|
||||
qHeight: 1400
|
||||
}]
|
||||
},
|
||||
/**
|
||||
* @type {boolean=}
|
||||
*/
|
||||
showTitles: false,
|
||||
/**
|
||||
* @type {string=}
|
||||
*/
|
||||
title: '',
|
||||
/**
|
||||
* @type {string=}
|
||||
*/
|
||||
subtitle: '',
|
||||
/**
|
||||
* @type {string=}
|
||||
*/
|
||||
footnote: '',
|
||||
/**
|
||||
* @type {('dynamic'|'continuous'|'discrete'|'diagonalCross'|'straightCross'|'horizontal'|'vertical'|'curvedCW'|'curvedCCW'|'cubicBezier')=}
|
||||
*/
|
||||
edgeType: "dynamic",
|
||||
/**
|
||||
* @type {boolean=}
|
||||
*/
|
||||
displayEdgeLabel: false,
|
||||
/**
|
||||
* @type {('top'|'middle'|'bottom'|'horizontal')=}
|
||||
*/
|
||||
posEdgeLabel: "top",
|
||||
/**
|
||||
* @type {('dot'|'square'|'star'|'triangle'|'triangleDown'|'diamond')=}
|
||||
*/
|
||||
nodeShape: "dot",
|
||||
/**
|
||||
* @type {boolean=}
|
||||
*/
|
||||
shadowMode: false,
|
||||
};
|
||||
23
src/extension/strings.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export const dimDesc = [
|
||||
"Node Identifier",
|
||||
"Node Label",
|
||||
"Node Parent",
|
||||
"Node Group"
|
||||
];
|
||||
|
||||
export 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."
|
||||
];
|
||||
|
||||
export const measureDesc = [
|
||||
"Tooltip",
|
||||
"Node size",
|
||||
"Edge size"
|
||||
];
|
||||
173
src/index.js
@@ -1,148 +1,33 @@
|
||||
/*
|
||||
Created by Michael Laenen - michael.laenen@agilos.com - (c) 2016
|
||||
Tested on Qlik Sense 2.2.3
|
||||
import { useElement, usePromise, useEffect, useStaleLayout, useTheme, useRect, useState, useConstraints, useSelections } from '@nebula.js/stardust';
|
||||
import data from './extension/data';
|
||||
import ext from './extension/ext';
|
||||
import properties from './extension/properties';
|
||||
import paint from './sn-paint';
|
||||
|
||||
Agilos.com takes no responsibility for any code.
|
||||
Use at your own risk.
|
||||
*/
|
||||
import "@babel/polyfill";
|
||||
import paint from './paint';
|
||||
import './styles/vis.min.css';
|
||||
export default function supernova() {
|
||||
return {
|
||||
qae: {
|
||||
properties,
|
||||
data: data(),
|
||||
},
|
||||
component() {
|
||||
const layout = useStaleLayout();
|
||||
const element = useElement();
|
||||
const theme = useTheme();
|
||||
const rect = useRect();
|
||||
const constraints = useConstraints();
|
||||
const selections = useSelections();
|
||||
const [network, setNetwork] = useState();
|
||||
|
||||
const component = {
|
||||
initialProperties: {
|
||||
version: 1.0,
|
||||
qHyperCubeDef: {
|
||||
qDimensions: [],
|
||||
qMeasures: [],
|
||||
qInitialDataFetch: [{
|
||||
qWidth: 7,
|
||||
qHeight: 1400
|
||||
}]
|
||||
}
|
||||
},
|
||||
//property panel
|
||||
definition: {
|
||||
type: "items",
|
||||
component: "accordion",
|
||||
items: {
|
||||
dimensions: {
|
||||
uses: "dimensions",
|
||||
min: 4,
|
||||
max: 4
|
||||
/*
|
||||
1. Dimension: Node ID, numeric (Event ID or else) or String
|
||||
2. Dimension: Node Label
|
||||
3. Dimension: Node Parent ID, numeric (Event ID or else) or String
|
||||
4. Dimension: Node Cluster
|
||||
*/
|
||||
},
|
||||
measures: {
|
||||
uses: "measures",
|
||||
min: 0,
|
||||
max: 3
|
||||
/*
|
||||
1. Measure: title text for tooltip (optional)
|
||||
2. Measure: node value
|
||||
3. Measure: edge value
|
||||
*/
|
||||
},
|
||||
sorting: {
|
||||
uses: "sorting"
|
||||
},
|
||||
addons: {
|
||||
uses: "addons",
|
||||
items: {
|
||||
dataHandling: {
|
||||
uses: "dataHandling"
|
||||
}
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
uses: "settings",
|
||||
items: {
|
||||
edgeType: {
|
||||
ref: "edgeType",
|
||||
type: "string",
|
||||
component: "dropdown",
|
||||
label: "Egde Type",
|
||||
options: [
|
||||
{ value: 'dynamic' },
|
||||
{ value: 'continuous' },
|
||||
{ value: 'discrete' },
|
||||
{ value: 'diagonalCross' },
|
||||
{ value: 'straightCross' },
|
||||
{ value: 'horizontal' },
|
||||
{ value: 'vertical' },
|
||||
{ value: 'curveCW' },
|
||||
{ value: 'curveCCW' },
|
||||
{ value: 'cubicBezier' }
|
||||
],
|
||||
defaultValue: "dynamic"
|
||||
},
|
||||
displayEdgeLabel : {
|
||||
ref: "displayEdgeLabel",
|
||||
type: "boolean",
|
||||
component: "switch",
|
||||
label: "Display Edge Value",
|
||||
options: [{
|
||||
value: true,
|
||||
label: "On"
|
||||
}, {
|
||||
value: false,
|
||||
label: "Off"
|
||||
}],
|
||||
defaultValue: false
|
||||
},
|
||||
posEdgeLabel: {
|
||||
ref: "posEdgeLabel",
|
||||
type: "string",
|
||||
component: "dropdown",
|
||||
label: "Position Edge Label",
|
||||
options: [
|
||||
{ value: 'top' }, { value: 'middle' }, { value: 'bottom' }, { value: 'horizontal' }
|
||||
],
|
||||
defaultValue: "top"
|
||||
},
|
||||
nodeShape: {
|
||||
ref: "nodeShape",
|
||||
type: "string",
|
||||
component: "dropdown",
|
||||
label: "Node Shape",
|
||||
options: [
|
||||
{ value: 'dot' },
|
||||
{ value: 'square' },
|
||||
{ value: 'star' },
|
||||
{ value: 'triangle' },
|
||||
{ value: 'triangleDown' },
|
||||
{ value: 'diamond' }
|
||||
],
|
||||
defaultValue: "dot"
|
||||
},
|
||||
shadowMode: {
|
||||
ref: "shadowMode",
|
||||
type: "boolean",
|
||||
component: "switch",
|
||||
label: "Display Shadow",
|
||||
options: [{
|
||||
value: true,
|
||||
label: "On"
|
||||
}, {
|
||||
value: false,
|
||||
label: "Off"
|
||||
}],
|
||||
defaultValue: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
useEffect(()=> {
|
||||
network && network.fit();
|
||||
}, [rect.width, rect.height]);
|
||||
|
||||
snapshot: {
|
||||
canTakeSnapshot: true
|
||||
},
|
||||
usePromise(()=>
|
||||
paint({ element,layout, theme, constraints, selections }).then((n)=>setNetwork(n)),
|
||||
[layout, element, theme.name(), constraints ]);
|
||||
},
|
||||
|
||||
paint: paint
|
||||
};
|
||||
|
||||
export default component;
|
||||
ext: ext(),
|
||||
};
|
||||
}
|
||||
|
||||
146
src/paint.js
@@ -1,146 +0,0 @@
|
||||
import vis from './scripts/vis.min';
|
||||
|
||||
function isTextCellNotEmpty(c) {
|
||||
return (c.qText && !(c.qIsNull || c.qText.trim() == ''));
|
||||
}
|
||||
|
||||
function paint ( $element, layout ) {
|
||||
var _this = this,
|
||||
qData = layout.qHyperCube.qDataPages[0],
|
||||
id = layout.qInfo.qId,
|
||||
containerId = 'network-container_' + id;
|
||||
|
||||
|
||||
if(qData && qData.qMatrix) {
|
||||
$element.empty().append($('<div />')
|
||||
.attr({ id: containerId })
|
||||
.css({
|
||||
height: $element.height(),
|
||||
width: $element.width(),
|
||||
overflow: 'auto'
|
||||
}));
|
||||
|
||||
var dataSet = qData.qMatrix.map(function(e){
|
||||
var dataItem = {
|
||||
id: e[0].qNum,
|
||||
label: e[1].qText,
|
||||
group: e[3].qText,
|
||||
parentid : e[2].qNum
|
||||
};
|
||||
|
||||
// optional measures set
|
||||
if (e.length > 4) {
|
||||
// tooltip
|
||||
if (isTextCellNotEmpty(e[4])) {
|
||||
dataItem.title = e[4].qText;
|
||||
} else {
|
||||
dataItem.title = "*** Default Tooltip ***" + "<BR/>" + "Name:" + e[1].qText + "<BR/>" + "Group:" + e[3].qText;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.length > 5) {
|
||||
if (e[5].qNum) {
|
||||
// node value - to scale node shape size
|
||||
dataItem.nodeValue = e[5].qNum;
|
||||
}
|
||||
}
|
||||
|
||||
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 = [];
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// create dataset for \\
|
||||
var data = {
|
||||
nodes: nodes,
|
||||
edges: edges
|
||||
};
|
||||
|
||||
// create a network
|
||||
var container = document.getElementById(containerId);
|
||||
|
||||
var options = {
|
||||
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 vis.Network(container, data, options);
|
||||
|
||||
// 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
|
||||
_this.backendApi.selectValues(0,connectedNodes,false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default paint;
|
||||
@@ -1,7 +0,0 @@
|
||||
import paint from './paint';
|
||||
|
||||
describe('paint', () => {
|
||||
it('should be defined', () => {
|
||||
expect(paint).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name" : "Network chart",
|
||||
"description" : "Network chart",
|
||||
"icon" : "extension",
|
||||
"type" : "visualization",
|
||||
"version": "X.Y.Z",
|
||||
"preview" : "network.png",
|
||||
"keywords": "qlik-sense, visualization",
|
||||
"author": "Michael Laenen <michael.laenen@agilos.com>",
|
||||
"homepage": "",
|
||||
"license": "MIT",
|
||||
"repository": "",
|
||||
"dependencies": {
|
||||
"qlik-sense": ">=5.5.x"
|
||||
}
|
||||
}
|
||||
47
src/scripts/vis.min.js
vendored
223
src/sn-paint.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import { Network } from 'vis-network';
|
||||
import { createTooltipHTML } from './tooltip';
|
||||
import { escapeHTML } from './utilities';
|
||||
import './styles/main.css';
|
||||
|
||||
function isTextCellNotEmpty(c) {
|
||||
return (c.qText && !(c.qIsNull || c.qText.trim() == ''));
|
||||
}
|
||||
|
||||
function getColor (index, colors) {
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
|
||||
export default function paint ( { element,layout, theme, selections, constraints } ) {
|
||||
return new Promise((resolve) => {
|
||||
const colorScale = theme.getDataColorPalettes()[0];
|
||||
const numDimensions = layout.qHyperCube.qDimensionInfo.length;
|
||||
const numMeasures = layout.qHyperCube.qMeasureInfo.length;
|
||||
|
||||
var qData = layout.qHyperCube.qDataPages[0],
|
||||
id = layout.qInfo.qId,
|
||||
containerId = 'network-container_' + id;
|
||||
|
||||
if(qData && qData.qMatrix) {
|
||||
element.textContent = '';
|
||||
const topDiv = document.createElement("div");
|
||||
topDiv.setAttribute('id', containerId);
|
||||
topDiv.classList.add('sn-network-top');
|
||||
constraints.passive && topDiv.classList.add('is-edit-mode');
|
||||
element.append(topDiv);
|
||||
|
||||
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[numDimensions+1].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.colors[Math.min(Object.keys(groups).length-1, colorScale.colors.length-1)];
|
||||
|
||||
Object.keys(groups).forEach(function(g,i) {
|
||||
groups[g].color = getColor(i, colors);
|
||||
});
|
||||
|
||||
// create dataset for \\
|
||||
var data = {
|
||||
nodes: nodes,
|
||||
edges: edges
|
||||
};
|
||||
|
||||
// create a network
|
||||
var container = document.getElementById(containerId);
|
||||
|
||||
var options = {
|
||||
groups: groups,
|
||||
layout: {
|
||||
randomSeed: 34545 //"0.6610209392878246:1631081903504"
|
||||
},
|
||||
nodes: {
|
||||
shape:layout.nodeShape,
|
||||
shadow:layout.shadowMode
|
||||
},
|
||||
edges: {
|
||||
shadow:layout.shadowMode,
|
||||
font: {
|
||||
align: layout.posEdgeLabel
|
||||
},
|
||||
smooth: {
|
||||
type: layout.edgeType
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
hideEdgesOnDrag: true,
|
||||
selectable: !constraints.active && !constraints.select,
|
||||
tooltipDelay: 100,
|
||||
multiselect: true,
|
||||
selectConnectedEdges: true
|
||||
},
|
||||
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();
|
||||
network.on('select', function (properties) {
|
||||
if (Object.prototype.hasOwnProperty.call(properties, "nodes") && !constraints.active && !constraints.select) {
|
||||
const nodes = network.getSelectedNodes();
|
||||
if (nodes.length > 0) {
|
||||
// find connected nodes to selection
|
||||
var conNodes = nodes.map(n => network.getConnectedNodes(n));
|
||||
// append nodes to the array
|
||||
conNodes.push(nodes);
|
||||
var connectedNodes = conNodes.flat();
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
//network.selectNodes(connectedNodes);
|
||||
|
||||
if (!selections.isActive()) {
|
||||
selections.begin('/qHyperCubeDef');
|
||||
}
|
||||
|
||||
//Make the selections
|
||||
selections.select({
|
||||
method: 'selectHyperCubeValues',
|
||||
params: ['/qHyperCubeDef', 0, toSelect, false],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
network.on('stabilizationIterationsDone', function() {
|
||||
network.stopSimulation();
|
||||
resolve(network);
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 665 B |
24
src/styles/main.css
Normal file
@@ -0,0 +1,24 @@
|
||||
.is-edit-mode > div {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sn-network-top {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: 'auto';
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.vis-tooltip {
|
||||
position: absolute;
|
||||
background-color: #333;
|
||||
opacity: 0.95;
|
||||
border-radius: 5px;
|
||||
color: #eee;
|
||||
padding: 10px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.qlik-network-chart-italic-property {
|
||||
font-style: italic;
|
||||
}
|
||||
1
src/styles/vis.min.css
vendored
28
src/tooltip.js
Normal file
@@ -0,0 +1,28 @@
|
||||
function createEntry(header, value) {
|
||||
const entry = document.createElement('div');
|
||||
const nameHeader = document.createElement('span');
|
||||
const nameHeaderValue = document.createTextNode(header);
|
||||
const nameValueContainer = document.createElement('b');
|
||||
const nameValue = document.createTextNode(value);
|
||||
|
||||
nameHeader.appendChild(nameHeaderValue);
|
||||
nameValueContainer.appendChild(nameValue);
|
||||
|
||||
entry.appendChild(nameHeader);
|
||||
entry.appendChild(nameValueContainer);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function createTooltipHTML({ name, groupNumber, nodeMeasure }) {
|
||||
const tooltip = document.createElement('div');
|
||||
const nameEntry = createEntry('Name: ', name);
|
||||
const groupNumberEntry = createEntry('Group number: ', groupNumber);
|
||||
const nodeMeasureEntry = createEntry('Node measure: ', nodeMeasure);
|
||||
|
||||
tooltip.appendChild(nameEntry);
|
||||
tooltip.appendChild(groupNumberEntry);
|
||||
tooltip.appendChild(nodeMeasureEntry);
|
||||
|
||||
return tooltip.innerHTML;
|
||||
}
|
||||
5
src/utilities.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export function escapeHTML(str){
|
||||
var span = document.createElement('span');
|
||||
span.appendChild(document.createTextNode(str));
|
||||
return span.innerHTML;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
"at-rule-no-unknown": true,
|
||||
"block-no-empty": true,
|
||||
"color-no-invalid-hex": true,
|
||||
"comment-no-empty": true,
|
||||
"declaration-block-no-duplicate-properties": [
|
||||
true,
|
||||
{
|
||||
ignore: ["consecutive-duplicates-with-different-values"]
|
||||
}
|
||||
],
|
||||
"declaration-block-no-shorthand-property-overrides": true,
|
||||
"font-family-no-duplicate-names": true,
|
||||
"font-family-no-missing-generic-family-keyword": true,
|
||||
"function-calc-no-unspaced-operator": true,
|
||||
"function-linear-gradient-no-nonstandard-direction": true,
|
||||
"keyframe-declaration-no-important": true,
|
||||
"media-feature-name-no-unknown": true,
|
||||
"no-descending-specificity": true,
|
||||
"no-duplicate-at-import-rules": true,
|
||||
"no-duplicate-selectors": true,
|
||||
"no-empty-source": true,
|
||||
"no-extra-semicolons": true,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
"property-no-unknown": true,
|
||||
"selector-pseudo-class-no-unknown": true,
|
||||
"selector-pseudo-element-no-unknown": true,
|
||||
"selector-type-no-unknown": true,
|
||||
"string-no-newline": true,
|
||||
"unit-no-unknown": true
|
||||
}
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const StyleLintPlugin = require('stylelint-webpack-plugin');
|
||||
const settings = require('./settings');
|
||||
|
||||
console.log('Webpack mode:', settings.mode); // eslint-disable-line no-console
|
||||
|
||||
const config = {
|
||||
devtool: 'source-map',
|
||||
entry: [
|
||||
'./src/index.js'
|
||||
],
|
||||
mode: settings.mode,
|
||||
output: {
|
||||
path: settings.buildDestination,
|
||||
filename: settings.name + '.js',
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
enforce: "pre",
|
||||
test: /\.js$/,
|
||||
exclude: /(node_modules)/,
|
||||
loader: "eslint-loader",
|
||||
options: {
|
||||
failOnError: true
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /.(less|css)$/,
|
||||
use: ['style-loader', 'css-loader', 'less-loader']
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif)$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CopyWebpackPlugin([
|
||||
'src/' + settings.name + '.qext'
|
||||
], {}),
|
||||
new StyleLintPlugin()
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||