Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
2af87e2d42 |
@@ -19,8 +19,13 @@ jobs:
|
||||
name: Install dependencies
|
||||
command: npm install
|
||||
- run:
|
||||
name: Run tests
|
||||
command: npm run test-once
|
||||
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"
|
||||
|
||||
bump-version:
|
||||
<<: *defaults
|
||||
@@ -48,16 +53,18 @@ jobs:
|
||||
command: |
|
||||
export VERSION=$(scripts/get-bumped-version.sh)
|
||||
echo "Version: ${VERSION}"
|
||||
npm run build
|
||||
npm run build:zip
|
||||
sudo chmod +x scripts/verify-files.sh
|
||||
scripts/verify-files.sh
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
- persist_to_workspace:
|
||||
root: ~/qlik-network-chart
|
||||
paths:
|
||||
- build
|
||||
- dist
|
||||
- store_artifacts:
|
||||
path: build
|
||||
destination: build
|
||||
path: dist
|
||||
destination: dist
|
||||
deploy:
|
||||
<<: *defaults
|
||||
steps:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
BUMPED_VERSION
|
||||
|
||||
BIN
assets/network.png
Normal file
BIN
assets/network.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
91
gulpfile.js
91
gulpfile.js
@@ -1,20 +1,61 @@
|
||||
var gulp = require('gulp');
|
||||
var gutil = require('gulp-util');
|
||||
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");
|
||||
var pkg = require('./package.json');
|
||||
|
||||
gulp.task('remove-build-folder', function(){
|
||||
return del([settings.buildDestination], { force: true });
|
||||
var DIST = './dist';
|
||||
var VERSION = process.env.VERSION || 'local-dev';
|
||||
|
||||
gulp.task('qext', function () {
|
||||
var qext = {
|
||||
name: 'Network chart',
|
||||
type: 'visualization',
|
||||
description: pkg.description + '\nVersion: ' + VERSION,
|
||||
version: VERSION,
|
||||
icon: 'bubbles',
|
||||
preview: 'network.png',
|
||||
keywords: 'qlik-sense, visualization',
|
||||
author: pkg.author,
|
||||
homepage: pkg.homepage,
|
||||
license: pkg.license,
|
||||
repository: pkg.repository,
|
||||
dependencies: {
|
||||
'qlik-sense': '>=5.5.x'
|
||||
}
|
||||
};
|
||||
if (pkg.contributors) {
|
||||
qext.contributors = pkg.contributors;
|
||||
}
|
||||
var src = require('stream').Readable({
|
||||
objectMode: true
|
||||
});
|
||||
src._read = function () {
|
||||
this.push(new gutil.File({
|
||||
cwd: '',
|
||||
base: '',
|
||||
path: pkg.name + '.qext',
|
||||
contents: Buffer.from(JSON.stringify(qext, null, 4))
|
||||
}));
|
||||
this.push(null);
|
||||
};
|
||||
return src.pipe(gulp.dest(DIST));
|
||||
});
|
||||
|
||||
gulp.task('clean', function(){
|
||||
return del([DIST], { force: true });
|
||||
});
|
||||
|
||||
gulp.task('zip-build', function(){
|
||||
return gulp.src(settings.buildDestination + '/**/*')
|
||||
.pipe(zip(`${settings.name}_${settings.version}.zip`))
|
||||
.pipe(gulp.dest(settings.buildDestination));
|
||||
return gulp.src(DIST + '/**/*')
|
||||
.pipe(zip(`${pkg.name}_${VERSION}.zip`))
|
||||
.pipe(gulp.dest(DIST));
|
||||
});
|
||||
|
||||
gulp.task('add-assets', function(){
|
||||
return gulp.src('./assets/**/*').pipe(gulp.dest(DIST));
|
||||
});
|
||||
|
||||
gulp.task('webpack-build', done => {
|
||||
@@ -33,39 +74,13 @@ gulp.task('webpack-build', 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.series('clean', 'webpack-build', 'qext', 'add-assets')
|
||||
);
|
||||
|
||||
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('zip',
|
||||
gulp.series('build', 'zip-build')
|
||||
);
|
||||
|
||||
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' }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
24
package.json
24
package.json
@@ -1,18 +1,15 @@
|
||||
{
|
||||
"name": "qlik-network-chart",
|
||||
"version": "0.0.1",
|
||||
"description": "Network chart",
|
||||
"keywords": "network chart qliksense extension",
|
||||
"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>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "gulp build",
|
||||
"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"
|
||||
"build:zip": "gulp zip",
|
||||
"eslint": "eslint src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.1.5",
|
||||
@@ -20,29 +17,20 @@
|
||||
"@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-util": "^3.0.7",
|
||||
"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"
|
||||
"webpack": "4.25.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"vis": "4.21.0"
|
||||
|
||||
58
readme.md
58
readme.md
@@ -1,66 +1,20 @@
|
||||
# Qlik Network Chart
|
||||
This extension is part of the extension bundles for Qlik Sense. The repository is maintained and moderated by Qlik RD.
|
||||
|
||||
[](https://circleci.com/gh/qlik-oss/network-vis-chart)
|
||||
|
||||
Qlik Sense extension to visualize networks data based on library vis.js (http://visjs.org).
|
||||
Tested with Qlik Sense 2.2.3.
|
||||
|
||||
### Dimensions
|
||||
4 dimensions are mandatory :
|
||||
|
||||
1. node identifier
|
||||
2. node label
|
||||
3. node parent identifier
|
||||
4. node group
|
||||
|
||||
### Measures
|
||||
The measures are optional
|
||||
|
||||
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
|
||||
|
||||
### 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
|
||||
|
||||
### Sample
|
||||
QVF based on characters from Victor Hugo's novel , Les Misérables.
|
||||

|
||||
|
||||
|
||||
# Getting Started
|
||||
|
||||
## Installation
|
||||
|
||||
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:
|
||||
|
||||
a. **Qlik Sense Desktop**: unzip to a directory under [My Documents]/Qlik/Sense/Extensions.
|
||||
|
||||
b. **Qlik Sense Server**: import the zip file in the QMC.
|
||||
Feel free to fork and suggest pull requests for improvements and bug fixes. Changes will be moderated and reviewed before inclusion in future bundle versions. Please note that emphasis is on backward compatibility, i.e. breaking changes will most likely not be approved.
|
||||
|
||||
Usage documentation for the extension is available at https://help.qlik.com.
|
||||
|
||||
# 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.
|
||||
1. Create a new app and add Network chart 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`.
|
||||
4. Run `npm run build` - to build a dev-version to the /dist folder.
|
||||
5. Move the content of the /dist folder to the extension directory. Usually in `C:/Users/<user>/Documents/Qlik/Sense/Extensions/qlik-network-chart`.
|
||||
|
||||
# Original Author
|
||||
|
||||
**Michael Laenen**
|
||||
|
||||
* [github.com/miclae76](https://github.com/miclae76)
|
||||
|
||||
BIN
resources/Network data.xlsx
Normal file
BIN
resources/Network data.xlsx
Normal file
Binary file not shown.
@@ -2,8 +2,8 @@
|
||||
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"
|
||||
echo "Artifact name: ./dist/${3}_${VERSION}.zip"
|
||||
$HOME/bin/ghr -t ${ghoauth} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} ${VERSION} "./dist/${3}_${4}.zip"
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
25
scripts/verify-files.sh
Normal file
25
scripts/verify-files.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# The build script has a known race-condition that sometimes causes it to not include all files
|
||||
# in the built zip. This script verifies the that the zip contains the correct number of files.
|
||||
|
||||
set -o errexit
|
||||
|
||||
echo "Verifying built file count"
|
||||
|
||||
while read line; do
|
||||
if [[ $line =~ ^\"name\": ]]; then
|
||||
name=${line#*: \"}
|
||||
name=${name%\"*}
|
||||
fi
|
||||
done < package.json
|
||||
|
||||
expected_file_count=$(($(find dist -type f | wc -l)-1))
|
||||
zip_file_count=$(zipinfo dist/${name}_${VERSION}.zip | grep ^- | wc -l)
|
||||
|
||||
if [ "${expected_file_count}" -ne "${zip_file_count}" ]; then
|
||||
# File count is incorrect
|
||||
echo "Expected file count ${expected_file_count}, but was ${zip_file_count}"
|
||||
exit 1
|
||||
fi
|
||||
echo "File count OK"
|
||||
exit 0
|
||||
13
settings.js
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
|
||||
};
|
||||
136
src/index.js
136
src/index.js
@@ -5,8 +5,36 @@ 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: {
|
||||
@@ -21,30 +49,62 @@ const component = {
|
||||
}
|
||||
},
|
||||
//property panel
|
||||
data: {
|
||||
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
|
||||
*/
|
||||
}
|
||||
},
|
||||
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
|
||||
*/
|
||||
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"
|
||||
@@ -58,13 +118,14 @@ const component = {
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
uses: "settings",
|
||||
type: "items",
|
||||
label: "Settings",
|
||||
items: {
|
||||
edgeType: {
|
||||
ref: "edgeType",
|
||||
type: "string",
|
||||
component: "dropdown",
|
||||
label: "Egde Type",
|
||||
label: "Edge Type",
|
||||
options: [
|
||||
{ value: 'dynamic' },
|
||||
{ value: 'continuous' },
|
||||
@@ -133,14 +194,35 @@ const component = {
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
snapshot: {
|
||||
canTakeSnapshot: true
|
||||
support: {
|
||||
export: true,
|
||||
snapshot: false
|
||||
},
|
||||
snapshot: {
|
||||
canTakeSnapshot: false
|
||||
},
|
||||
|
||||
paint: paint
|
||||
};
|
||||
|
||||
|
||||
335
src/paint.js
335
src/paint.js
@@ -1,149 +1,230 @@
|
||||
import { Network } from 'vis/index-network';
|
||||
import qlik from 'qlik';
|
||||
import { createTooltipHTML } from './tooltip';
|
||||
import { escapeHTML } from './utilities';
|
||||
|
||||
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;
|
||||
function getColor (index, colors) {
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
|
||||
function paint ( $element, layout, qTheme, component ) {
|
||||
|
||||
if(qData && qData.qMatrix) {
|
||||
$element.empty().append($('<div />')
|
||||
.attr({ id: containerId })
|
||||
.css({
|
||||
height: $element.height(),
|
||||
width: $element.width(),
|
||||
overflow: 'auto'
|
||||
}));
|
||||
return new qlik.Promise(function(resolve) {
|
||||
|
||||
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
|
||||
const colorScale = qTheme.properties.palettes.data[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.empty().append($('<div />')
|
||||
.attr({ id: containerId })
|
||||
.toggleClass('is-edit-mode', component.inEditState())
|
||||
.css({
|
||||
height: $element.height(),
|
||||
width: $element.width(),
|
||||
overflow: 'auto'
|
||||
}));
|
||||
|
||||
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) {
|
||||
// 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;
|
||||
// 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();
|
||||
|
||||
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 = [];
|
||||
|
||||
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
|
||||
}
|
||||
network.on('stabilizationIterationsDone', function() {
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// create dataset for \\
|
||||
var data = {
|
||||
nodes: nodes,
|
||||
edges: edges
|
||||
};
|
||||
function themePaint ($element, layout) {
|
||||
const component = this;
|
||||
try {
|
||||
const app = qlik.currApp(this);
|
||||
|
||||
// create a network
|
||||
var container = document.getElementById(containerId);
|
||||
|
||||
var options = {
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
return app.theme.getApplied().then( function( qTheme ) {
|
||||
return paint($element, layout, qTheme, component);
|
||||
});
|
||||
} catch (exception) {
|
||||
console.error(exception); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
export default paint;
|
||||
export default themePaint;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
17
src/styles/main.less
Normal file
17
src/styles/main.less
Normal file
@@ -0,0 +1,17 @@
|
||||
.is-edit-mode > div {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
28
src/tooltip.js
Normal file
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
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,20 +1,31 @@
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const StyleLintPlugin = require('stylelint-webpack-plugin');
|
||||
const settings = require('./settings');
|
||||
const packageJSON = require('./package.json');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
console.log('Webpack mode:', settings.mode); // eslint-disable-line no-console
|
||||
const DIST = path.resolve("./dist");
|
||||
const MODE = process.env.NODE_ENV || 'development';
|
||||
|
||||
console.log('Webpack mode:', MODE); // eslint-disable-line no-console
|
||||
|
||||
const config = {
|
||||
devtool: 'source-map',
|
||||
entry: [
|
||||
'./src/index.js'
|
||||
],
|
||||
mode: settings.mode,
|
||||
mode: MODE,
|
||||
output: {
|
||||
path: settings.buildDestination,
|
||||
filename: settings.name + '.js',
|
||||
libraryTarget: 'umd'
|
||||
filename: `${packageJSON.name}.js`,
|
||||
libraryTarget: 'amd',
|
||||
path: DIST
|
||||
},
|
||||
externals: {
|
||||
qlik: {
|
||||
amd: 'qlik',
|
||||
commonjs: 'qlik',
|
||||
commonjs2: 'qlik',
|
||||
root: '_'
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
@@ -63,9 +74,6 @@ const config = {
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new CopyWebpackPlugin([
|
||||
'src/' + settings.name + '.qext'
|
||||
], {}),
|
||||
new StyleLintPlugin(),
|
||||
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/en$/),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user