1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2026-08-05 23:39:17 +08:00

Compare commits

..

24 Commits

Author SHA1 Message Date
lana-k
529c3390b3 #139 fix deselection in multi-trace charts 2026-07-26 20:34:08 +02:00
lana-k
acea6090e8 0.31.0 2026-07-26 17:55:10 +02:00
lana-k
130996b8c1 #139 tests 2026-07-26 17:54:06 +02:00
lana-k
6074ccc835 #139 tests 2026-07-26 14:59:08 +02:00
lana-k
62786e7dda #139 tests 2026-07-23 23:14:17 +02:00
lana-k
6fbddb69bb #139 tests 2026-07-21 22:55:03 +02:00
lana-k
9838421d65 #139 view details of selected points 2026-07-20 21:37:24 +02:00
lana-k
686a09a8ce fix import 2026-07-12 21:56:29 +02:00
lana-k
4e9d1a785c 0.30.1 2026-07-12 17:19:37 +02:00
lana-k
2b6de12dc5 fix: show custom chart settings in pivot #138 2026-07-12 17:19:07 +02:00
lana-k
39d1604959 fix: save chart settings wihtout data #137 2026-07-12 17:18:41 +02:00
lana-k
053b8fbecb wip 2026-07-08 20:39:43 +02:00
lana-k
1043a03c96 fix plotly patch file 2026-07-05 19:38:03 +02:00
lana-k
b6ec71338b fix plotly warnings see issue 7809 2026-07-05 19:12:24 +02:00
lana-k
4bb6676648 selection view wip 2026-05-25 17:50:57 +02:00
lana-k
f3a75346fd fix typo 2026-04-15 22:14:59 +02:00
lana-k
366ffcff10 0.30.0 2026-04-08 22:29:52 +02:00
lana-k
778db0d9eb #136 tests 2026-04-08 22:16:22 +02:00
lana-k
c00d76904e fix lint 2026-04-06 20:37:27 +02:00
lana-k
4cc6ec86ea #136 save seedLayout settings 2026-04-06 20:31:51 +02:00
lana-k
c0d972c2ab Support circle pack as initial algorithm #136 2026-04-06 19:36:44 +02:00
lana-k
d9435a80c3 Add seed layout #136 2026-04-06 19:36:44 +02:00
lana-k
62fb92d824 premultiply colors in sigma 2026-04-06 19:36:44 +02:00
saaj
5019013eff Update Docker test file to NodeJS 18, fix Karma config filename (#135) 2026-04-05 17:13:06 +02:00
32 changed files with 2337 additions and 230 deletions

View File

@@ -3,12 +3,12 @@
# docker build -t sqliteviz/test -f Dockerfile.test . # docker build -t sqliteviz/test -f Dockerfile.test .
# #
FROM node:12.22-bullseye FROM node:18.20.8-bookworm
RUN set -ex; \ RUN set -ex; \
apt update; \ apt update; \
apt install -y chromium firefox-esr; \ apt install -y chromium firefox-esr; \
npm install -g npm@7 npm install -g npm@10
WORKDIR /tmp/build WORKDIR /tmp/build
@@ -19,6 +19,6 @@ RUN npm install
COPY . . COPY . .
RUN set -ex; \ RUN set -ex; \
sed -i 's/browsers: \[.*\],/browsers: ['"'FirefoxHeadlessTouch'"'],/' karma.conf.js sed -i 's/browsers: \[.*\],/browsers: ['"'FirefoxHeadlessTouch'"'],/' karma.conf.cjs
RUN npm run lint -- --no-fix && npm run test RUN npm run lint -- --no-fix && npm run test

221
package-lock.json generated
View File

@@ -1,12 +1,13 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.28.2", "version": "0.30.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.28.2", "version": "0.30.0",
"hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@sigma/export-image": "^3.0.0", "@sigma/export-image": "^3.0.0",
@@ -23,13 +24,13 @@
"nanoid": "^3.1.12", "nanoid": "^3.1.12",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pivottable": "^2.23.0", "pivottable": "^2.23.0",
"plotly.js": "^2.35.2", "plotly.js": "^2.35.3",
"promise-worker": "^2.0.1", "promise-worker": "^2.0.1",
"react": "^16.14.0", "react": "^16.14.0",
"react-chart-editor": "^0.46.1", "react-chart-editor": "^0.46.1",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"seedrandom": "^3.0.5", "seedrandom": "^3.0.5",
"sigma": "^3.0.1", "sigma": "3.0.2",
"sql.js": "file:./lib/sql-js", "sql.js": "file:./lib/sql-js",
"tiny-emitter": "^2.1.0", "tiny-emitter": "^2.1.0",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
@@ -64,6 +65,8 @@
"karma-spec-reporter": "^0.0.36", "karma-spec-reporter": "^0.0.36",
"karma-vite": "^1.0.5", "karma-vite": "^1.0.5",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"patch-package": "^8.0.1",
"postinstall-postinstall": "^2.1.0",
"prettier": "3.5.3", "prettier": "3.5.3",
"process": "^0.11.10", "process": "^0.11.10",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
@@ -3093,6 +3096,12 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true "peer": true
}, },
"node_modules/@yarnpkg/lockfile": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
"dev": true
},
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "2.0.0", "version": "2.0.0",
"license": "ISC", "license": "ISC",
@@ -4776,6 +4785,21 @@
"node": ">=6.0" "node": ">=6.0"
} }
}, },
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/sibiraj-s"
}
],
"engines": {
"node": ">=8"
}
},
"node_modules/cipher-base": { "node_modules/cipher-base": {
"version": "1.0.6", "version": "1.0.6",
"dev": true, "dev": true,
@@ -7809,6 +7833,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/find-yarn-workspace-root": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
"dev": true,
"dependencies": {
"micromatch": "^4.0.2"
}
},
"node_modules/flat-cache": { "node_modules/flat-cache": {
"version": "3.2.0", "version": "3.2.0",
"dev": true, "dev": true,
@@ -10358,11 +10391,36 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-stable-stringify": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
"integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"isarray": "^2.0.5",
"jsonify": "^0.0.1",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/json-stable-stringify-without-jsonify": { "node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1", "version": "1.0.1",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-stable-stringify/node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true
},
"node_modules/json-stringify-pretty-compact": { "node_modules/json-stringify-pretty-compact": {
"version": "4.0.0", "version": "4.0.0",
"license": "MIT" "license": "MIT"
@@ -10385,6 +10443,15 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"node_modules/jsonify": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/jsonpointer": { "node_modules/jsonpointer": {
"version": "5.0.1", "version": "5.0.1",
"dev": true, "dev": true,
@@ -10683,6 +10750,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/klaw-sync": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.1.11"
}
},
"node_modules/lazy-cache": { "node_modules/lazy-cache": {
"version": "1.0.4", "version": "1.0.4",
"dev": true, "dev": true,
@@ -12318,6 +12394,22 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/open": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
"dev": true,
"dependencies": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/optimist": { "node_modules/optimist": {
"version": "0.6.1", "version": "0.6.1",
"dev": true, "dev": true,
@@ -12550,6 +12642,106 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/patch-package": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz",
"integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
"dev": true,
"dependencies": {
"@yarnpkg/lockfile": "^1.1.0",
"chalk": "^4.1.2",
"ci-info": "^3.7.0",
"cross-spawn": "^7.0.3",
"find-yarn-workspace-root": "^2.0.0",
"fs-extra": "^10.0.0",
"json-stable-stringify": "^1.0.2",
"klaw-sync": "^6.0.0",
"minimist": "^1.2.6",
"open": "^7.4.2",
"semver": "^7.5.3",
"slash": "^2.0.0",
"tmp": "^0.2.4",
"yaml": "^2.2.2"
},
"bin": {
"patch-package": "index.js"
},
"engines": {
"node": ">=14",
"npm": ">5"
}
},
"node_modules/patch-package/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/patch-package/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/patch-package/node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/patch-package/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/patch-package/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/patch-package/node_modules/yaml": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"dev": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/path-browserify": { "node_modules/path-browserify": {
"version": "1.0.1", "version": "1.0.1",
"dev": true, "dev": true,
@@ -12994,6 +13186,13 @@
"version": "4.2.0", "version": "4.2.0",
"license": "MIT" "license": "MIT"
}, },
"node_modules/postinstall-postinstall": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz",
"integrity": "sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ==",
"dev": true,
"hasInstallScript": true
},
"node_modules/potpack": { "node_modules/potpack": {
"version": "1.0.2", "version": "1.0.2",
"license": "ISC" "license": "ISC"
@@ -14454,6 +14653,15 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/smob": { "node_modules/smob": {
"version": "1.5.0", "version": "1.5.0",
"dev": true, "dev": true,
@@ -15571,9 +15779,10 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/tmp": { "node_modules/tmp": {
"version": "0.2.3", "version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=14.14" "node": ">=14.14"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.29.0", "version": "0.31.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -10,7 +10,8 @@
"serve": "vite preview", "serve": "vite preview",
"test": "karma start karma.conf.cjs", "test": "karma start karma.conf.cjs",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src", "lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write" "format": "prettier . --write",
"postinstall": "patch-package"
}, },
"dependencies": { "dependencies": {
"@sigma/export-image": "^3.0.0", "@sigma/export-image": "^3.0.0",
@@ -27,13 +28,13 @@
"nanoid": "^3.1.12", "nanoid": "^3.1.12",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pivottable": "^2.23.0", "pivottable": "^2.23.0",
"plotly.js": "^2.35.2", "plotly.js": "^2.35.3",
"promise-worker": "^2.0.1", "promise-worker": "^2.0.1",
"react": "^16.14.0", "react": "^16.14.0",
"react-chart-editor": "^0.46.1", "react-chart-editor": "^0.46.1",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"seedrandom": "^3.0.5", "seedrandom": "^3.0.5",
"sigma": "^3.0.1", "sigma": "3.0.2",
"sql.js": "file:./lib/sql-js", "sql.js": "file:./lib/sql-js",
"tiny-emitter": "^2.1.0", "tiny-emitter": "^2.1.0",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
@@ -68,6 +69,8 @@
"karma-spec-reporter": "^0.0.36", "karma-spec-reporter": "^0.0.36",
"karma-vite": "^1.0.5", "karma-vite": "^1.0.5",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"patch-package": "^8.0.1",
"postinstall-postinstall": "^2.1.0",
"prettier": "3.5.3", "prettier": "3.5.3",
"process": "^0.11.10", "process": "^0.11.10",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",

View File

@@ -0,0 +1,26 @@
diff --git a/node_modules/plotly.js/dist/plotly.js b/node_modules/plotly.js/dist/plotly.js
index 28f5612..5b9956d 100644
--- a/node_modules/plotly.js/dist/plotly.js
+++ b/node_modules/plotly.js/dist/plotly.js
@@ -34384,6 +34384,9 @@ var layoutUIControlPatterns = [{
pattern: /^(shapes|annotations)/,
attr: 'editrevision'
}, {
+ pattern: /^selections/,
+ attr: 'selectionrevision'
+},{
pattern: /^title\.text$/,
attr: 'editrevision'
}];
diff --git a/node_modules/plotly.js/src/plot_api/plot_api.js b/node_modules/plotly.js/src/plot_api/plot_api.js
index 75e98f5..5c38053 100644
--- a/node_modules/plotly.js/src/plot_api/plot_api.js
+++ b/node_modules/plotly.js/src/plot_api/plot_api.js
@@ -2394,6 +2394,7 @@ var layoutUIControlPatterns = [
{pattern: /^legend\.(x|y)$/, attr: 'editrevision'},
{pattern: /^(shapes|annotations)/, attr: 'editrevision'},
+ {pattern: /^selections/, attr: 'selectionrevision'},
{pattern: /^title\.text$/, attr: 'editrevision'}
];

24
patches/sigma+3.0.2.patch Normal file
View File

@@ -0,0 +1,24 @@
diff --git a/node_modules/sigma/dist/colors-beb06eb2.esm.js b/node_modules/sigma/dist/colors-beb06eb2.esm.js
index b7130d1..bf101b6 100644
--- a/node_modules/sigma/dist/colors-beb06eb2.esm.js
+++ b/node_modules/sigma/dist/colors-beb06eb2.esm.js
@@ -51,7 +51,6 @@ function _nonIterableRest() {
function _slicedToArray(r, e) {
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
}
-
var HTML_COLORS = {
black: "#000000",
silver: "#C0C0C0",
@@ -267,7 +266,10 @@ for (var htmlColor in HTML_COLORS) {
FLOAT_COLOR_CACHE[HTML_COLORS[htmlColor]] = FLOAT_COLOR_CACHE[htmlColor];
}
function rgbaToFloat(r, g, b, a, masking) {
- INT32[0] = a << 24 | b << 16 | g << 8 | r;
+ const r_= Math.floor(r * a / 255)
+ const g_= Math.floor(g * a / 255)
+ const b_= Math.floor(b * a / 255)
+ INT32[0] = a << 24 | b_ << 16 | g_ << 8 | r_;
if (masking) INT32[0] = INT32[0] & 0xfeffffff;
return FLOAT32[0];
}

View File

@@ -1,30 +1,45 @@
<template> <template>
<div ref="chartContainer" class="chart-container"> <div class="chart-container">
<div v-show="!dataSources" class="warning data-view-warning"> <div v-show="!dataSources" class="warning data-view-warning">
There is no data to build a chart. Run your SQL query and make sure the There is no data to build a chart. Run your SQL query and make sure the
result is not empty. result is not empty.
</div> </div>
<div <splitpanes
:before="{ size: 70, max: 100 }"
:after="{ size: 30, max: 50, hidden: !showValueViewer }"
:default="{ before: 70, after: 30 }"
class="chart" class="chart"
:style="{ height: !dataSources ? 'calc(100% - 40px)' : '100%' }" :style="{ height: !dataSources ? 'calc(100% - 40px)' : '100%' }"
> >
<PlotlyEditor <template #left-pane>
ref="plotlyEditor" <div ref="chartContainer" :style="{ height: '100%' }">
:data="state.data" <PlotlyEditor
:layout="state.layout" ref="plotlyEditor"
:frames="state.frames" :data="state.data"
:config="config" :layout="state.layout"
:dataSources="dataSources" :frames="state.frames"
:dataSourceOptions="dataSourceOptions" :config="config"
:plotly="plotly" :dataSources="dataSources"
:useResizeHandler="useResizeHandler" :dataSourceOptions="dataSourceOptions"
:debug="true" :plotly="plotly"
:advancedTraceTypeSelector="true" :useResizeHandler="useResizeHandler"
:hideControls="!showViewSettings" :debug="true"
@update="update" :advancedTraceTypeSelector="true"
@render="onRender" :hideControls="!showViewSettings"
/> @update="update"
</div> @render="onRender"
/>
</div>
</template>
<template v-if="showValueViewer" #right-pane>
<value-viewer
:empty="!selectedItems || selectedItems.length === 0"
emptyMessage="No points selected to view"
:value="JSON.stringify(selectedItems)"
defaultFormat="json"
/>
</template>
</splitpanes>
</div> </div>
</template> </template>
@@ -37,11 +52,16 @@ import chartHelper from '@/lib/chartHelper'
import * as dereference from 'react-chart-editor/lib/lib/dereference' import * as dereference from 'react-chart-editor/lib/lib/dereference'
import fIo from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events' import events from '@/lib/utils/events'
import Splitpanes from '@/components/Common/Splitpanes'
import ValueViewer from '@/components/ValueViewer.vue'
import { nextTick } from 'vue'
export default { export default {
name: 'Chart', name: 'Chart',
components: { components: {
PlotlyEditor: applyPureReactInVue(ReactPlotlyEditorWithPlotRef) PlotlyEditor: applyPureReactInVue(ReactPlotlyEditorWithPlotRef),
Splitpanes,
ValueViewer
}, },
props: { props: {
dataSources: Object, dataSources: Object,
@@ -49,7 +69,8 @@ export default {
exportToPngEnabled: Boolean, exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean, exportToSvgEnabled: Boolean,
forPivot: Boolean, forPivot: Boolean,
showViewSettings: Boolean showViewSettings: Boolean,
showValueViewer: Boolean
}, },
emits: [ emits: [
'update:exportToSvgEnabled', 'update:exportToSvgEnabled',
@@ -60,18 +81,21 @@ export default {
data() { data() {
return { return {
plotly, plotly,
state: this.initOptions || { state: this.initOptions
data: [], ? JSON.parse(JSON.stringify(this.initOptions))
layout: { autosize: true }, : {
frames: [] data: [],
}, layout: { autosize: true },
frames: []
},
config: { config: {
editable: true, editable: true,
displaylogo: false, displaylogo: false,
modeBarButtonsToRemove: ['toImage'] modeBarButtonsToRemove: ['toImage']
}, },
resizeObserver: null, resizeObserver: null,
useResizeHandler: this.$store.state.isWorkspaceVisible useResizeHandler: this.$store.state.isWorkspaceVisible,
selectedItems: null
} }
}, },
computed: { computed: {
@@ -84,6 +108,7 @@ export default {
// we need to update state.data in order to update the graph // we need to update state.data in order to update the graph
// https://github.com/plotly/react-chart-editor/issues/948 // https://github.com/plotly/react-chart-editor/issues/948
if (this.dataSources) { if (this.dataSources) {
chartHelper.clearSelection(this.state.data, this.state.layout)
dereference.default(this.state.data, this.dataSources) dereference.default(this.state.data, this.dataSources)
this.updatePlotly() this.updatePlotly()
} }
@@ -115,13 +140,24 @@ export default {
this.$emit('update:exportToSvgEnabled', true) this.$emit('update:exportToSvgEnabled', true)
this.$emit('update:exportToHtmlEnabled', true) this.$emit('update:exportToHtmlEnabled', true)
}, },
mounted() { async mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize) this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.chartContainer) this.resizeObserver.observe(this.$refs.chartContainer)
if (this.dataSources) { if (this.dataSources) {
dereference.default(this.state.data, this.dataSources) dereference.default(this.state.data, this.dataSources)
} }
this.handleResize() this.handleResize()
await nextTick()
const plotlyDiv =
this.$refs.plotlyEditor?.$el?.querySelector('.js-plotly-plot')
plotlyDiv?.on('plotly_selected', selectionEvent => {
if (selectionEvent) {
this.selectedItems = this.getSelectedData()
}
})
plotlyDiv?.on('plotly_deselect', () => {
this.selectedItems = this.getSelectedData()
})
}, },
activated() { activated() {
this.useResizeHandler = true this.useResizeHandler = true
@@ -144,6 +180,8 @@ export default {
// TODO: check changes and enable Save button if needed // TODO: check changes and enable Save button if needed
}, },
update(data, layout, frames) { update(data, layout, frames) {
chartHelper.clearSelection(data, layout)
this.state = { data, layout, frames } this.state = { data, layout, frames }
this.$emit('update') this.$emit('update')
}, },
@@ -178,6 +216,12 @@ export default {
}, },
prepareCopy(type = 'png') { prepareCopy(type = 'png') {
return chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type) return chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type)
},
getSelectedData() {
return chartHelper.getRowsByIndexFromDataSources(
this.dataSources,
chartHelper.getSelectedPointsIndexes(this.state.data)
)
} }
} }
} }

View File

@@ -58,9 +58,9 @@
</icon-button> </icon-button>
<icon-button <icon-button
v-if="mode === 'graph'" v-if="mode !== 'pivot'"
ref="viewNodeOrEdgeBtn" ref="viewDetailsBtn"
tooltip="View node or edge details" tooltip="View details"
tooltipPosition="top-left" tooltipPosition="top-left"
:active="viewValuePanelVisible" :active="viewValuePanelVisible"
@click="viewValuePanelVisible = !viewValuePanelVisible" @click="viewValuePanelVisible = !viewValuePanelVisible"

View File

@@ -125,6 +125,13 @@ export default {
}, },
async checkFile(file) { async checkFile(file) {
if (!file) {
alert(
'You should drop a file. ' +
'It can be an SQLite database, CSV file, JSON or NDJSON file.'
)
return
}
this.state = 'dropping' this.state = 'dropping'
this.newDb = database.getNewDatabase() this.newDb = database.getNewDatabase()

View File

@@ -28,7 +28,7 @@
</multiselect> </multiselect>
</Field> </Field>
<Field label="Seed value"> <Field label="Seed value" fieldContainerClassName="test_seed_value">
<NumericInput <NumericInput
:value="modelValue.seedValue" :value="modelValue.seedValue"
@update="update('seedValue', $event)" @update="update('seedValue', $event)"

View File

@@ -102,7 +102,7 @@ export default {
]), ]),
сolorscaleDirections: markRaw([ сolorscaleDirections: markRaw([
{ label: 'Normal', value: 'normal' }, { label: 'Normal', value: 'normal' },
{ label: 'Recersed', value: 'reversed' } { label: 'Reversed', value: 'reversed' }
]), ]),
colorSourceUsageOptions: markRaw([ colorSourceUsageOptions: markRaw([
{ label: 'Direct', value: 'direct' }, { label: 'Direct', value: 'direct' },

View File

@@ -0,0 +1,86 @@
<template>
<Field label="Initial algorithm">
<Dropdown
:options="layoutOptions"
:value="modelValue.initialAlgorithm"
:clearable="false"
className="test_fa2_initial_layout_algorithm_select"
@change="updateInitialAlgorithm"
/>
</Field>
<component
:is="layoutSettingsComponentMap[modelValue.initialAlgorithm]"
v-if="modelValue.initialAlgorithm !== 'circular'"
:modelValue="modelValue"
:keyOptions="keyOptions"
@update:model-value="$emit('update:modelValue', $event)"
/>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import Field from 'react-chart-editor/lib/components/fields/Field'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import 'react-chart-editor/lib/react-chart-editor.css'
import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue'
export default {
components: {
Field: applyPureReactInVue(Field),
Dropdown: applyPureReactInVue(Dropdown),
NumericInput: applyPureReactInVue(NumericInput),
RandomLayoutSettings,
CirclePackLayoutSettings
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
layoutOptions: markRaw([
{ label: 'Circular', value: 'circular' },
{ label: 'Random', value: 'random' },
{ label: 'Circle pack', value: 'circlepack' }
]),
layoutSettingsComponentMap: markRaw({
random: RandomLayoutSettings,
circlepack: CirclePackLayoutSettings
}),
defaultSeedLayoutSettings: {
circular: { initialAlgorithm: 'circular' },
random: { initialAlgorithm: 'random', seedValue: 1 },
circlepack: {
initialAlgorithm: 'circlepack',
hierarchyAttributes: [],
seedValue: 1
}
}
}
},
methods: {
updateInitialAlgorithm(newAlgorithm) {
const newSettings = {
...this.modelValue
}
const prevAlgorithmSettings =
this.defaultSeedLayoutSettings[this.modelValue.initialAlgorithm]
Object.keys(prevAlgorithmSettings).forEach(key => {
delete newSettings[key]
prevAlgorithmSettings[key] = this.modelValue[key]
})
this.$emit('update:modelValue', {
...newSettings,
...this.defaultSeedLayoutSettings[newAlgorithm]
})
}
}
}
</script>

View File

@@ -171,6 +171,17 @@
/> />
</Fold> </Fold>
<template v-if="settings.layout.type === 'forceAtlas2'"> <template v-if="settings.layout.type === 'forceAtlas2'">
<Fold name="Seed layout">
<Field>
If you already built a graph using another layout, the initial
algorithm doesn't apply unless you restart it.
</Field>
<ForceAtlasSeedLayoutSettings
v-model="settings.layout.options"
:keyOptions="keysOptions"
@update:model-value="updateLayout(settings.layout.type)"
/>
</Fold>
<Fold name="Advanced layout settings"> <Fold name="Advanced layout settings">
<AdvancedForceAtlasLayoutSettings <AdvancedForceAtlasLayoutSettings
v-model="settings.layout.options" v-model="settings.layout.options"
@@ -182,6 +193,7 @@
<Button <Button
variant="secondary" variant="secondary"
class="test_fa2_reset" class="test_fa2_reset"
title="Set the settings to default or previously saved ones."
@click="resetFA2LayoutSettings" @click="resetFA2LayoutSettings"
> >
Reset Reset
@@ -192,16 +204,25 @@
@click="toggleFA2Layout" @click="toggleFA2Layout"
> >
<template #node:icon> <template #node:icon>
<div <div>
:style="{
padding: '0 3px'
}"
>
<RunIcon v-if="!fa2Running" /> <RunIcon v-if="!fa2Running" />
<StopIcon v-else /> <PauseIcon v-else />
</div> </div>
</template> </template>
{{ fa2Running ? 'Stop' : 'Start' }} {{ fa2Running ? 'Pause' : 'Continue' }}
</Button>
<Button
variant="primary"
class="test_fa2_restart"
title="Clear node coordinates and run the layout algorithm anew."
@click="restartFA2Layout"
>
<template #node:icon>
<div>
<RestartIcon />
</div>
</template>
Restart
</Button> </Button>
</div> </div>
</template> </template>
@@ -234,13 +255,15 @@ import Button from 'react-chart-editor/lib/components/widgets/Button'
import Field from 'react-chart-editor/lib/components/fields/Field' import Field from 'react-chart-editor/lib/components/fields/Field'
import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue' import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue'
import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue' import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue'
import ForceAtlasSeedLayoutSettings from '@/components/Graph/ForceAtlasSeedLayoutSettings.vue'
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue' import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue'
import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue' import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
import FA2Layout from 'graphology-layout-forceatlas2/worker' import FA2Layout from 'graphology-layout-forceatlas2/worker'
import * as forceAtlas2 from 'graphology-layout-forceatlas2' import * as forceAtlas2 from 'graphology-layout-forceatlas2'
import RunIcon from '@/components/svg/run.vue' import RunIcon from '@/components/svg/run.vue'
import StopIcon from '@/components/svg/stop.vue' import RestartIcon from '@/components/svg/restart.vue'
import PauseIcon from '@/components/svg/pause.vue'
import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image' import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image'
import { import {
buildNodes, buildNodes,
@@ -248,7 +271,8 @@ import {
updateNodes, updateNodes,
updateEdges, updateEdges,
reduceNodes, reduceNodes,
reduceEdges reduceEdges,
clearNodeCoordinates
} from '@/lib/graphHelper' } from '@/lib/graphHelper'
import Graph from 'graphology' import Graph from 'graphology'
import { circular, random, circlepack } from 'graphology-layout' import { circular, random, circlepack } from 'graphology-layout'
@@ -273,14 +297,16 @@ export default {
Button: applyPureReactInVue(Button), Button: applyPureReactInVue(Button),
ColorPicker: applyPureReactInVue(ColorPicker), ColorPicker: applyPureReactInVue(ColorPicker),
RunIcon, RunIcon,
StopIcon, RestartIcon,
PauseIcon,
RandomLayoutSettings, RandomLayoutSettings,
CirclePackLayoutSettings, CirclePackLayoutSettings,
NodeColorSettings, NodeColorSettings,
NodeSizeSettings, NodeSizeSettings,
EdgeSizeSettings, EdgeSizeSettings,
EdgeColorSettings, EdgeColorSettings,
AdvancedForceAtlasLayoutSettings AdvancedForceAtlasLayoutSettings,
ForceAtlasSeedLayoutSettings
}, },
inject: ['tabLayout'], inject: ['tabLayout'],
props: { props: {
@@ -311,6 +337,12 @@ export default {
circlepack: CirclePackLayoutSettings, circlepack: CirclePackLayoutSettings,
forceAtlas2: ForceAtlasLayoutSettings forceAtlas2: ForceAtlasLayoutSettings
}), }),
layoutMethodMap: markRaw({
circular: this.applyCircularLayout,
random: this.applyRandomLayout,
circlepack: this.applyCirclePackLayout,
forceAtlas2: this.applyFA2Layout
}),
selectedNodeId: undefined, selectedNodeId: undefined,
hoveredNodeId: undefined, hoveredNodeId: undefined,
selectedEdgeId: undefined, selectedEdgeId: undefined,
@@ -642,75 +674,76 @@ export default {
this.fa2Layout.kill() this.fa2Layout.kill()
} }
if (layoutType === 'circular') { this.layoutMethodMap[layoutType]()
circular.assign(this.graph)
return
}
if (layoutType === 'random') { if (layoutType === 'forceAtlas2' && layoutType !== prevLayout) {
random.assign(this.graph, { this.autoRunFA2Layout()
rng: seedrandom(this.settings.layout.options.seedValue)
})
return
} }
},
if (layoutType === 'circlepack') { applyCircularLayout() {
this.graph.forEachNode(nodeId => { circular.assign(this.graph)
this.graph.updateNode(nodeId, attributes => { },
const newAttributes = { ...attributes } applyRandomLayout() {
// Delete old hierarchy attributes random.assign(this.graph, {
Object.keys(newAttributes) rng: seedrandom(this.settings.layout.options.seedValue)
.filter(key => key.startsWith('hierarchyAttribute')) })
.forEach( },
hierarchyAttributeKey => applyCirclePackLayout() {
delete newAttributes[hierarchyAttributeKey] this.graph.forEachNode(nodeId => {
) this.graph.updateNode(nodeId, attributes => {
// Set new hierarchy attributes const newAttributes = { ...attributes }
this.settings.layout.options.hierarchyAttributes?.forEach( // Delete old hierarchy attributes
(hierarchyAttribute, index) => { Object.keys(newAttributes)
newAttributes['hierarchyAttribute' + index] = .filter(key => key.startsWith('hierarchyAttribute'))
attributes.data[hierarchyAttribute] .forEach(
} hierarchyAttributeKey =>
delete newAttributes[hierarchyAttributeKey]
) )
// Set new hierarchy attributes
return newAttributes this.settings.layout.options.hierarchyAttributes?.forEach(
}) (hierarchyAttribute, index) => {
}) newAttributes['hierarchyAttribute' + index] =
attributes.data[hierarchyAttribute]
circlepack.assign(this.graph, { }
hierarchyAttributes:
this.settings.layout.options.hierarchyAttributes?.map(
(_, index) => 'hierarchyAttribute' + index
) || [],
rng: seedrandom(this.settings.layout.options.seedValue)
})
return
}
if (layoutType === 'forceAtlas2') {
if (
!this.graph.someNode(
(nodeKey, attributes) =>
typeof attributes.x === 'number' &&
typeof attributes.y === 'number'
) )
) {
circular.assign(this.graph)
}
this.fa2Layout = markRaw( return newAttributes
new FA2Layout(this.graph, { })
getEdgeWeight: (_, attr) => })
this.settings.layout.options.weightSource
? attr.data[this.settings.layout.options.weightSource] circlepack.assign(this.graph, {
: 1, hierarchyAttributes:
settings: this.settings.layout.options this.settings.layout.options.hierarchyAttributes?.map(
}) (_, index) => 'hierarchyAttribute' + index
) || [],
rng: seedrandom(this.settings.layout.options.seedValue)
})
},
applyFA2Layout() {
if (
!this.graph.someNode(
(nodeKey, attributes) =>
typeof attributes.x === 'number' && typeof attributes.y === 'number'
) )
if (layoutType !== prevLayout) { ) {
this.autoRunFA2Layout() this.layoutMethodMap[this.settings.layout.options.initialAlgorithm]()
}
} }
const fa2settings = { ...this.settings.layout.options }
// delete all custom settings because they can break the algorithm running
delete fa2settings.initialAlgorithm
delete fa2settings.seedValue
delete fa2settings.initialIterationsAmount
delete fa2settings.hierarchyAttributes
this.fa2Layout = markRaw(
new FA2Layout(this.graph, {
getEdgeWeight: (_, attr) =>
this.settings.layout.options.weightSource
? attr.data[this.settings.layout.options.weightSource]
: 1,
settings: fa2settings
})
)
}, },
toggleFA2Layout() { toggleFA2Layout() {
if (this.fa2Layout.isRunning()) { if (this.fa2Layout.isRunning()) {
@@ -731,6 +764,15 @@ export default {
this.checkIteration = null this.checkIteration = null
} }
}, },
restartFA2Layout() {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
}
this.fa2Layout.kill()
clearNodeCoordinates(this.graph)
this.applyFA2Layout()
this.autoRunFA2Layout()
},
autoRunFA2Layout() { autoRunFA2Layout() {
let iteration = 1 let iteration = 1
this.checkIteration = () => { this.checkIteration = () => {
@@ -748,6 +790,7 @@ export default {
setRecommendedFA2Settings() { setRecommendedFA2Settings() {
const sensibleSettings = forceAtlas2.default.inferSettings(this.graph) const sensibleSettings = forceAtlas2.default.inferSettings(this.graph)
this.settings.layout.options = { this.settings.layout.options = {
initialAlgorithm: 'circular',
initialIterationsAmount: 50, initialIterationsAmount: 50,
adjustSizes: false, adjustSizes: false,
barnesHutOptimize: false, barnesHutOptimize: false,
@@ -809,4 +852,8 @@ export default {
flex-grow: 1; flex-grow: 1;
flex-basis: 0; flex-basis: 0;
} }
.force-atlas-buttons :deep(.button__icon > div) {
padding: 0 3px;
}
</style> </style>

View File

@@ -139,7 +139,7 @@ export default {
]), ]),
сolorscaleDirections: markRaw([ сolorscaleDirections: markRaw([
{ label: 'Normal', value: 'normal' }, { label: 'Normal', value: 'normal' },
{ label: 'Recersed', value: 'reversed' } { label: 'Reversed', value: 'reversed' }
]), ]),
colorSourceUsageOptions: markRaw([ colorSourceUsageOptions: markRaw([
{ label: 'Direct', value: 'direct' }, { label: 'Direct', value: 'direct' },

View File

@@ -1,5 +1,5 @@
<template> <template>
<Field label="Seed value"> <Field label="Seed value" fieldContainerClassName="test_seed_value">
<NumericInput <NumericInput
:value="modelValue.seedValue" :value="modelValue.seedValue"
@update="update('seedValue', $event)" @update="update('seedValue', $event)"

View File

@@ -227,7 +227,7 @@ export default {
events.send(eventName) events.send(eventName)
}, },
_keyListener(e) { _keyListener(e) {
if (this.$route.path === '/workspace') { if (this.$route.path === '/workspace' && this.currentInquiryTab) {
// Run query Ctrl+R or Ctrl+Enter // Run query Ctrl+R or Ctrl+Enter
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) { if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {
e.preventDefault() e.preventDefault()

View File

@@ -19,6 +19,7 @@
<chart <chart
ref="customChart" ref="customChart"
v-bind="customChartComponentProps" v-bind="customChartComponentProps"
:showViewSettings="showViewSettings"
@update="$emit('update')" @update="$emit('update')"
@loading-image-completed="$emit('loadingImageCompleted')" @loading-image-completed="$emit('loadingImageCompleted')"
/> />

View File

@@ -0,0 +1,27 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 4C3 3.44772 3.14924 3 3.33333 3H6.66667C6.85076 3 7 3.44772 7
4V14C7 14.5523 6.85076 15 6.66667 15H3.33333C3.14924 15 3 14.5523 3 14V4Z"
fill="#A2B1C6"
/>
<path
d="M11 4C11 3.44772 11.1492 3 11.3333 3H14.6667C14.8508 3 15 3.44772 15
4V14C15 14.5523 14.8508 15 14.6667 15H11.3333C11.1492 15 11 14.5523 11
14V4Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'PauseIcon'
}
</script>

View File

@@ -0,0 +1,36 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.34708 3.32922C8.63088 2.90468 10.017 2.90308 11.3022
3.32434C11.9084 3.52308 12.4768 3.8114 12.9906 4.1759V3.34387C12.9906
2.79159 13.4384 2.34387 13.9906 2.34387C14.5429 2.34387 14.9906 2.79159
14.9906 3.34387V6.67102C14.9906 7.22331 14.5429 7.67102 13.9906
7.67102H10.6635C10.1112 7.67102 9.66349 7.22331 9.66349 6.67102C9.66351
6.11876 10.1112 5.67102 10.6635 5.67102H11.6313C11.3351 5.4851 11.0154
5.33498 10.6791 5.22473C9.80069 4.93681 8.85289 4.93738 7.97501
5.22766C7.09726 5.51795 6.33574 6.08228 5.80216 6.83704C5.26867
7.59191 4.99088 8.49846 5.01017 9.42297C5.02954 10.3475 5.34482
11.2417 5.90958 11.9738C6.47435 12.7058 7.25871 13.237 8.14787
13.4904C9.03716 13.7437 9.9843 13.7055 10.85 13.381C11.7157 13.0565
12.4548 12.463 12.9584 11.6876C13.2592 11.2244 13.879 11.0929 14.3422
11.3937C14.805 11.6945 14.9366 12.3135 14.6361 12.7765C13.8996 13.9106
12.8185 14.7794 11.5522 15.254C10.2859 15.7287 8.90058 15.7846 7.60001
15.4142C6.2994 15.0437 5.15167 14.2661 4.3256 13.1954C3.49956 12.1247
3.03849 10.8169 3.01017 9.46497C2.98191 8.1131 3.38782 6.7872 4.16837
5.68274C4.94892 4.57842 6.06331 3.75379 7.34708 3.32922Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'RestartIcon'
}
</script>

View File

@@ -1,21 +0,0 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 4C3 3.44772 3.44772 3 4 3H14C14.5523 3 15 3.44772 15 4V14C15
14.5523 14.5523 15 14 15H4C3.44772 15 3 14.5523 3 14V4Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'StopIcon'
}
</script>

View File

@@ -13,6 +13,37 @@ export function getOptionsFromDataSources(dataSources) {
})) }))
} }
export function getSelectedPointsIndexes(stateData) {
if (!stateData) {
return []
}
return Array.from(
new Set(stateData.flatMap(data => data.selectedpoints || []))
)
}
export function getRowsByIndexFromDataSources(dataSources, rowIndexes) {
if (!dataSources) {
return []
}
const dataSourceColumns = Object.keys(dataSources)
return rowIndexes.map(rowIndex =>
dataSourceColumns.reduce((result, columnName) => {
result[columnName] = dataSources[columnName][rowIndex]
return result
}, {})
)
}
export function clearSelection(data, layout) {
if (layout?.selections?.length > 0) {
layout.selections = []
data.forEach(dataItem => delete dataItem.selectedpoints)
}
}
export function getOptionsForSave(state, dataSources) { export function getOptionsForSave(state, dataSources) {
// we don't need to save the data, only settings // we don't need to save the data, only settings
// so we modify state.data using dereference // so we modify state.data using dereference
@@ -22,6 +53,9 @@ export function getOptionsForSave(state, dataSources) {
emptySources[key] = [] emptySources[key] = []
} }
dereference.default(stateCopy.data, emptySources) dereference.default(stateCopy.data, emptySources)
// Also, we don't need to save selections
clearSelection(stateCopy.data, stateCopy.layout)
return stateCopy return stateCopy
} }
@@ -72,5 +106,8 @@ export default {
getOptionsForSave, getOptionsForSave,
getImageDataUrl, getImageDataUrl,
getHtml, getHtml,
getChartData getChartData,
getRowsByIndexFromDataSources,
clearSelection,
getSelectedPointsIndexes
} }

View File

@@ -36,6 +36,13 @@ export function dataSourceIsValid(dataSources) {
} }
} }
export function clearNodeCoordinates(graph) {
graph.forEachNode((nodeId, attributes) => {
attributes.x = undefined
attributes.y = undefined
})
}
export function buildNodes(graph, dataSources, options) { export function buildNodes(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0] const docColumn = Object.keys(dataSources)[0]
const { objectType, nodeId } = options.structure const { objectType, nodeId } = options.structure
@@ -144,6 +151,9 @@ export function reduceNodes(nodeId, nodeData, interactionState, settings) {
if (selectedNodeId || hoveredNodeId || hoveredEdgeId || selectedEdgeId) { if (selectedNodeId || hoveredNodeId || hoveredEdgeId || selectedEdgeId) {
res.zIndex = 2 res.zIndex = 2
res.highlighted = nodeId === selectedNodeId || nodeId === hoveredNodeId res.highlighted = nodeId === selectedNodeId || nodeId === hoveredNodeId
if (res.highlighted) {
res.labelColor = 'black'
}
const isInHoveredFamily = const isInHoveredFamily =
nodeId === hoveredNodeId || nodeId === hoveredNodeId ||

View File

@@ -1,3 +1,5 @@
import { clearSelection } from '@/lib/chartHelper'
export default { export default {
_migrate(installedVersion, inquiries) { _migrate(installedVersion, inquiries) {
if (installedVersion < 2) { if (installedVersion < 2) {
@@ -17,6 +19,25 @@ export default {
}) })
} }
if (installedVersion < 4) {
inquiries.forEach(inquiry => {
if (
inquiry.viewType === 'graph' &&
inquiry.viewOptions.layout.type === 'forceAtlas2'
) {
inquiry.viewOptions.layout.options.initialAlgorithm = 'circular'
}
})
}
if (installedVersion < 5) {
inquiries.forEach(inquiry => {
if (inquiry.viewType === 'chart') {
clearSelection(inquiry.viewOptions.data, inquiry.viewOptions.layout)
}
})
}
return inquiries return inquiries
} }
} }

View File

@@ -5,9 +5,10 @@ import migration from './_migrations'
const migrate = migration._migrate const migrate = migration._migrate
const myInquiriesKey = 'myInquiries' const myInquiriesKey = 'myInquiries'
const latestVersion = 5
export default { export default {
version: 3, version: latestVersion,
myInquiriesKey, myInquiriesKey,
getStoredInquiries() { getStoredInquiries() {
let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey)) let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey))
@@ -21,8 +22,8 @@ export default {
return [] return []
} }
if (myInquiries.version === 2) { if (myInquiries.version < latestVersion) {
myInquiries = migrate(2, myInquiries.inquiries) myInquiries = migrate(myInquiries.version, myInquiries.inquiries)
this.updateStorage(myInquiries) this.updateStorage(myInquiries)
return myInquiries return myInquiries
} }
@@ -69,8 +70,8 @@ export default {
// Turn data into array if they are not // Turn data into array if they are not
inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries
inquiryList = migrate(1, inquiryList) inquiryList = migrate(1, inquiryList)
} else if (inquiries.version === 2) { } else if (inquiries.version < latestVersion) {
inquiryList = migrate(2, inquiries.inquiries) inquiryList = migrate(inquiries.version, inquiries.inquiries)
} else { } else {
inquiryList = inquiries.inquiries || [] inquiryList = inquiries.inquiries || []
} }
@@ -110,8 +111,8 @@ export default {
if (!data.version) { if (!data.version) {
return data.length > 0 ? migrate(1, data) : [] return data.length > 0 ? migrate(1, data) : []
} else if (data.version === 2) { } else if (data.version < latestVersion) {
return migrate(2, data.inquiries) return migrate(data.version, data.inquiries)
} else { } else {
return data.inquiries return data.inquiries
} }

View File

@@ -244,4 +244,287 @@ describe('Chart.vue', () => {
) )
wrapper.unmount() wrapper.unmount()
}) })
it('selections are shown in value viewer', async () => {
const dataSources = {
name: ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin'],
points: [100, 90, 95, 80]
}
const wrapper = mount(Chart, {
attachTo: document.body,
props: {
dataSources,
initOptions: {
data: [
{
type: 'scatter',
mode: 'markers',
x: ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin'],
xsrc: 'name',
meta: {
columnNames: {
x: 'name',
y: 'points'
}
},
y: [100, 90, 95, 80],
ysrc: 'points',
selectedpoints: [1, 2]
}
],
layout: {
autosize: true,
mapbox: {
style: 'open-street-map'
},
dragmode: 'select',
selections: [
{
xref: 'x',
yref: 'y',
line: {
width: 1,
dash: 'dot'
},
type: 'rect',
x0: 0.6238202370500439,
y0: 98.1184336198663,
x1: 2.4324242756804213,
y1: 87.03915950334289
}
],
title: {
subtitle: {
text: 'Click to enter Plot subtitle'
}
},
xaxis: {
range: [-0.20324846356453027, 3.20324846356453],
autorange: true,
type: 'category'
},
yaxis: {
range: [78.4909264565425, 101.5090735434575],
autorange: true,
type: 'linear'
}
},
frames: []
},
showViewSettings: true,
showValueViewer: true
},
global: {
mocks: { $store }
}
})
await flushPromises()
const valueViewerText = wrapper
.findComponent({ name: 'ValueViewer' })
.text()
expect(valueViewerText).to.contain(`"name": "Hufflepuff"`)
expect(valueViewerText).to.contain(`"points": 90`)
expect(valueViewerText).to.contain(`"name": "Ravenclaw"`)
expect(valueViewerText).to.contain(`"points": 95`)
expect(valueViewerText).not.to.contain(`"name": "Gryffindor"`)
expect(valueViewerText).not.to.contain(`"points": 100`)
expect(valueViewerText).not.to.contain(`"name": "Slytherin"`)
expect(valueViewerText).not.to.contain(`"points": 80`)
wrapper.unmount()
})
it('clears selections on dataSources change', async () => {
const dataSources = {
name: ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin'],
points: [100, 90, 95, 80]
}
const wrapper = mount(Chart, {
attachTo: document.body,
props: {
dataSources,
initOptions: {
data: [
{
type: 'scatter',
mode: 'markers',
x: ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin'],
xsrc: 'name',
meta: {
columnNames: {
x: 'name',
y: 'points'
}
},
y: [100, 90, 95, 80],
ysrc: 'points',
selectedpoints: [1, 2]
}
],
layout: {
autosize: true,
mapbox: {
style: 'open-street-map'
},
dragmode: 'select',
selections: [
{
xref: 'x',
yref: 'y',
line: {
width: 1,
dash: 'dot'
},
type: 'rect',
x0: 0.6238202370500439,
y0: 98.1184336198663,
x1: 2.4324242756804213,
y1: 87.03915950334289
}
],
title: {
subtitle: {
text: 'Click to enter Plot subtitle'
}
},
xaxis: {
range: [-0.20324846356453027, 3.20324846356453],
autorange: true,
type: 'category'
},
yaxis: {
range: [78.4909264565425, 101.5090735434575],
autorange: true,
type: 'linear'
}
},
frames: []
},
showViewSettings: true,
showValueViewer: true
},
global: {
mocks: { $store }
}
})
await flushPromises()
const valueViewerText = wrapper
.findComponent({ name: 'ValueViewer' })
.text()
expect(valueViewerText).to.contain(`"name": "Hufflepuff"`)
expect(valueViewerText).to.contain(`"name": "Ravenclaw"`)
await wrapper.setProps({
dataSources: {
name: ['Gryffindor', 'Hufflepuff'],
points: [100, 90]
}
})
await flushPromises()
expect(wrapper.findComponent({ name: 'ValueViewer' }).text()).equal(
`No points selected to view`
)
wrapper.unmount()
})
it('clears selections on changes in chart settings', async () => {
const dataSources = {
name: ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin'],
points: [100, 90, 95, 80]
}
const wrapper = mount(Chart, {
attachTo: document.body,
props: {
dataSources,
initOptions: {
data: [
{
type: 'scatter',
mode: 'markers',
x: ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin'],
xsrc: 'name',
meta: {
columnNames: {
x: 'name',
y: 'points'
}
},
y: [100, 90, 95, 80],
ysrc: 'points',
selectedpoints: [1, 2]
}
],
layout: {
autosize: true,
mapbox: {
style: 'open-street-map'
},
dragmode: 'select',
selections: [
{
xref: 'x',
yref: 'y',
line: {
width: 1,
dash: 'dot'
},
type: 'rect',
x0: 0.6238202370500439,
y0: 98.1184336198663,
x1: 2.4324242756804213,
y1: 87.03915950334289
}
],
title: {
subtitle: {
text: 'Click to enter Plot subtitle'
}
},
xaxis: {
range: [-0.20324846356453027, 3.20324846356453],
autorange: true,
type: 'category'
},
yaxis: {
range: [78.4909264565425, 101.5090735434575],
autorange: true,
type: 'linear'
}
},
frames: []
},
showViewSettings: true,
showValueViewer: true
},
global: {
mocks: { $store }
}
})
await flushPromises()
const valueViewerText = wrapper
.findComponent({ name: 'ValueViewer' })
.text()
expect(valueViewerText).to.contain(`"name": "Hufflepuff"`)
expect(valueViewerText).to.contain(`"name": "Ravenclaw"`)
// Add another trace
await wrapper.find('button.js-add-button').wrapperElement.click()
await flushPromises()
expect(wrapper.findComponent({ name: 'ValueViewer' }).text()).equal(
`No points selected to view`
)
wrapper.unmount()
})
}) })

View File

@@ -429,37 +429,44 @@ describe('DataView.vue', () => {
} }
}) })
// viewNodeOrEdgeBtn is not disaplyed in chart mode // viewDetailsBtn is disaplyed in chart mode
expect( expect(wrapper.findComponent({ ref: 'viewDetailsBtn' }).exists()).to.equal(
wrapper.findComponent({ ref: 'viewNodeOrEdgeBtn' }).exists() true
).to.equal(false) )
// Switch to pivot // Switch to pivot
const pivotBtn = wrapper.findComponent({ ref: 'pivotBtn' }) const pivotBtn = wrapper.findComponent({ ref: 'pivotBtn' })
await pivotBtn.trigger('click') await pivotBtn.trigger('click')
// viewNodeOrEdgeBtn is not disaplyed in pivot mode // viewDetailsBtn is not disaplyed in pivot mode
expect( expect(wrapper.findComponent({ ref: 'viewDetailsBtn' }).exists()).to.equal(
wrapper.findComponent({ ref: 'viewNodeOrEdgeBtn' }).exists() false
).to.equal(false) )
// Switch to graph // Switch to graph
const graphBtn = wrapper.findComponent({ ref: 'graphBtn' }) const graphBtn = wrapper.findComponent({ ref: 'graphBtn' })
await graphBtn.trigger('click') await graphBtn.trigger('click')
// viewNodeOrEdgeBtn is disaplyed in graph mode // viewDetailsBtn is disaplyed in graph mode
const viewNodeOrEdgeBtn = wrapper.findComponent({ const viewDetailsBtn = wrapper.findComponent({
ref: 'viewNodeOrEdgeBtn' ref: 'viewDetailsBtn'
}) })
expect(viewNodeOrEdgeBtn.exists()).to.equal(true) expect(viewDetailsBtn.exists()).to.equal(true)
// by default node viewer is hidden // by default details viewer is hidden
expect(wrapper.findComponent({ name: 'value-viewer' }).exists()).to.equal( expect(wrapper.findComponent({ name: 'value-viewer' }).exists()).to.equal(
false false
) )
// Click to show node viewer // Click to show details viewer
await viewNodeOrEdgeBtn.trigger('click') await viewDetailsBtn.trigger('click')
expect(wrapper.findComponent({ name: 'value-viewer' }).exists()).to.equal(
true
)
// Switch to chart
const chartBtn = wrapper.findComponent({ ref: 'chartBtn' })
await chartBtn.trigger('click')
expect(wrapper.findComponent({ name: 'value-viewer' }).exists()).to.equal( expect(wrapper.findComponent({ name: 'value-viewer' }).exists()).to.equal(
true true
) )

View File

@@ -1309,7 +1309,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true) expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1) await waitCondition(() => stopSpy.callCount === 1)
expect(wrapper.text()).to.contain('Start') expect(wrapper.text()).to.contain('Continue')
const coordinates = graph const coordinates = graph
.export() .export()
@@ -1349,6 +1349,7 @@ describe('GraphEditor', () => {
layout: { layout: {
type: 'forceAtlas2', type: 'forceAtlas2',
options: { options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 55, initialIterationsAmount: 55,
gravity: 1.5, gravity: 1.5,
scalingRatio: 1.2, scalingRatio: 1.2,
@@ -1383,7 +1384,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true) expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1) await waitCondition(() => stopSpy.callCount === 1)
expect(wrapper.text()).to.contain('Start') expect(wrapper.text()).to.contain('Continue')
const initialCoordinates = graph const initialCoordinates = graph
.export() .export()
@@ -1397,7 +1398,7 @@ describe('GraphEditor', () => {
new Event('blur', { bubbles: true }) new Event('blur', { bubbles: true })
) )
// Call nextTick after setting number input, // Call nextTick after setting number input,
// otherwise the value will be changed beck to initial for some reason // otherwise the value will be changed back to initial for some reason
await nextTick() await nextTick()
expect(wrapper.vm.settings.layout.options.gravity).to.equal(12) expect(wrapper.vm.settings.layout.options.gravity).to.equal(12)
@@ -1405,6 +1406,30 @@ describe('GraphEditor', () => {
// Algorithm wasn't called // Algorithm wasn't called
expect(startSpy.calledOnce).to.equal(true) expect(startSpy.calledOnce).to.equal(true)
// Change initial algorithm
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[1].trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options.initialAlgorithm).to.equal(
'random'
)
// Change seed value
const seedValueInput = wrapper.find('.test_seed_value input')
await seedValueInput.setValue(123)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
expect(wrapper.vm.settings.layout.options.seedValue).to.equal(123)
// Change scaling ratio // Change scaling ratio
const scalingInput = wrapper.find('.test_fa2_scaling input') const scalingInput = wrapper.find('.test_fa2_scaling input')
await scalingInput.setValue(2) await scalingInput.setValue(2)
@@ -1493,13 +1518,13 @@ describe('GraphEditor', () => {
false false
) )
// Click Start // Click Continue
const toggleButton = wrapper.find('button.test_fa2_toggle') const toggleButton = wrapper.find('button.test_fa2_toggle')
await toggleButton.trigger('click') await toggleButton.trigger('click')
expect(toggleButton.text()).to.contain('Stop') expect(toggleButton.text()).to.contain('Pause')
expect(startSpy.callCount).to.equal(2) expect(startSpy.callCount).to.equal(2)
// Wait a bit and click Stop // Wait a bit and click Pause
await time.sleep(500) await time.sleep(500)
await toggleButton.trigger('click') await toggleButton.trigger('click')
expect(stopSpy.callCount).to.equal(2) expect(stopSpy.callCount).to.equal(2)
@@ -1513,9 +1538,479 @@ describe('GraphEditor', () => {
// Click Reset // Click Reset
await wrapper.find('button.test_fa2_reset').trigger('click') await wrapper.find('button.test_fa2_reset').trigger('click')
expect(toggleButton.text()).to.contain('Start') expect(toggleButton.text()).to.contain('Continue')
expect(startSpy.callCount).to.equal(2) expect(startSpy.callCount).to.equal(2)
expect(wrapper.vm.settings.layout.options).to.eql({ expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'circular',
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
wrapper.unmount()
})
it('FA2: restarts and applies selected initial layout', async () => {
const stopSpy = sinon.spy(FA2Layout.prototype, 'stop')
const startSpy = sinon.spy(FA2Layout.prototype, 'start')
const wrapper = mount(GraphEditor, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"type": 0, "node_id": 1, "size": 20}',
'{"type": 0, "node_id": 2, "size": 2}',
'{"type": 0, "node_id": 3, "size": 2}',
'{"type": 0, "node_id": 4, "size": 2}',
'{"type": 1, "source": 1, "target": 3, "wgt": 20}',
'{"type": 1, "source": 1, "target": 2, "wgt": 15}',
'{"type": 1, "source": 1, "target": 4, "wgt": 5}'
]
},
initOptions: {
...defaultInitOptions,
structure: {
nodeId: 'node_id',
objectType: 'type',
edgeSource: 'source',
edgeTarget: 'target'
},
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
}
}
},
showViewSettings: true
},
global: {
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const graph = wrapper.vm.graph
const styleMenuItem = wrapper.findAll('.sidebar__group__title')[1]
await styleMenuItem.trigger('click')
const layoutMenuItem = wrapper.findAll('.sidebar__item')[4]
await layoutMenuItem.trigger('click')
expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1)
const initialCoordinates = graph
.export()
.nodes.map(node => `x:${node.attributes.x},y:${node.attributes.y}`)
.join()
// Change initial algorithm to Random
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[1].trigger('click')
await nextTick()
// Click Restart
const restartButton = wrapper.find('button.test_fa2_restart')
await restartButton.trigger('click')
const toggleButton = wrapper.find('button.test_fa2_toggle')
expect(toggleButton.text()).to.contain('Pause')
expect(startSpy.callCount).to.equal(2)
// Wait until restarting finished
await waitCondition(() => stopSpy.callCount === 2)
const randomCoordinates1 = graph
.export()
.nodes.map(node => `x:${node.attributes.x},y:${node.attributes.y}`)
.join()
// Change seed value to 123
let seedValueInput = wrapper.find('.test_seed_value input')
await seedValueInput.setValue(123)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
// Click Restart
await restartButton.trigger('click')
expect(toggleButton.text()).to.contain('Pause')
expect(startSpy.callCount).to.equal(3)
// Wait until restarting finished
await waitCondition(() => stopSpy.callCount === 3)
const randomCoordinates2 = graph
.export()
.nodes.map(node => `x:${node.attributes.x},y:${node.attributes.y}`)
.join()
// Change seed value back to 1
await seedValueInput.setValue(1)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
// Click Restart
await restartButton.trigger('click')
// Wait until restarting finished
await waitCondition(() => stopSpy.callCount === 4)
const randomCoordinates1After = graph
.export()
.nodes.map(node => `x:${node.attributes.x},y:${node.attributes.y}`)
.join()
// Change initial algorithm to CirclePack
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[2].trigger('click')
await nextTick()
// Change seed value to 456
seedValueInput = wrapper.find('.test_seed_value input')
await seedValueInput.setValue(456)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
// Set another hierarchy
const hierarchyInput = wrapper.find(
'.multiselect.sqliteviz-select .multiselect__select'
)
await hierarchyInput.trigger('mousedown')
await wrapper
.find('ul.multiselect__content')
.findAll('li')[2]
.find('span')
.trigger('click')
await nextTick()
// Click Restart
await restartButton.trigger('click')
// Wait until restarting finished
await waitCondition(() => stopSpy.callCount === 5)
const circlePackCoordinates = graph
.export()
.nodes.map(node => `x:${node.attributes.x},y:${node.attributes.y}`)
.join()
expect(initialCoordinates).not.to.equal(randomCoordinates1)
expect(randomCoordinates1).not.to.equal(randomCoordinates2)
expect(randomCoordinates1).to.equal(randomCoordinates1After)
expect(circlePackCoordinates).not.to.equal(randomCoordinates1After)
expect(circlePackCoordinates).not.to.equal(initialCoordinates)
wrapper.unmount()
})
it('FA2: restore previously set parameters for initial layout', async () => {
const wrapper = mount(GraphEditor, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"type": 0, "node_id": 1}',
'{"type": 0, "node_id": 2}',
'{"type": 1, "source": 1, "target": 2, "wgt": 15}'
]
},
initOptions: {
...defaultInitOptions,
structure: {
nodeId: 'node_id',
objectType: 'type',
edgeSource: 'source',
edgeTarget: 'target'
},
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
}
}
},
showViewSettings: true
},
global: {
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const styleMenuItem = wrapper.findAll('.sidebar__group__title')[1]
await styleMenuItem.trigger('click')
const layoutMenuItem = wrapper.findAll('.sidebar__item')[4]
await layoutMenuItem.trigger('click')
// Change initial algorithm to Random
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[1].trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'random',
seedValue: 1,
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Change seed value to 123
let seedValueInput = wrapper.find('.test_seed_value input')
await seedValueInput.setValue(123)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'random',
seedValue: 123,
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Change initial algorithm to CirclePack
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[2].trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'circlepack',
seedValue: 1,
hierarchyAttributes: [],
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Change seed value to 456
seedValueInput = wrapper.find('.test_seed_value input')
await seedValueInput.setValue(456)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'circlepack',
seedValue: 456,
hierarchyAttributes: [],
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Set another hierarchy
const hierarchyInput = wrapper.find(
'.multiselect.sqliteviz-select .multiselect__select'
)
await hierarchyInput.trigger('mousedown')
await wrapper
.find('ul.multiselect__content')
.findAll('li')[1]
.find('span')
.trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'circlepack',
seedValue: 456,
hierarchyAttributes: ['node_id'],
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Change initial algorithm to Random
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[1].trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'random',
seedValue: 123,
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Change initial algorithm to Circular
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[0].trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'circular',
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Change initial algorithm to CirclePack
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[2].trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'circlepack',
seedValue: 456,
hierarchyAttributes: ['node_id'],
initialIterationsAmount: 55, initialIterationsAmount: 55,
gravity: 1.5, gravity: 1.5,
scalingRatio: 1.2, scalingRatio: 1.2,
@@ -1603,6 +2098,26 @@ describe('GraphEditor', () => {
) )
await nextTick() await nextTick()
// Change initial algorithm
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[1].trigger('click')
await nextTick()
// Change seed value
const seedValueInput = wrapper.find('.test_seed_value input')
await seedValueInput.setValue(123)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
// Change scaling ratio // Change scaling ratio
const scalingInput = wrapper.find('.test_fa2_scaling input') const scalingInput = wrapper.find('.test_fa2_scaling input')
await scalingInput.setValue(2) await scalingInput.setValue(2)
@@ -1671,6 +2186,8 @@ describe('GraphEditor', () => {
await nextTick() await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({ expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'random',
seedValue: 123,
initialIterationsAmount: 120, initialIterationsAmount: 120,
gravity: 12, gravity: 12,
scalingRatio: 2, scalingRatio: 2,
@@ -1756,6 +2273,7 @@ describe('GraphEditor', () => {
layout: { layout: {
type: 'forceAtlas2', type: 'forceAtlas2',
options: { options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50, initialIterationsAmount: 50,
gravity: 1.5, gravity: 1.5,
scalingRatio: 1.2, scalingRatio: 1.2,
@@ -1788,7 +2306,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true) expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1) await waitCondition(() => stopSpy.callCount === 1)
// Click Start // Click Continue
const toggleButton = wrapper.find('button.test_fa2_toggle') const toggleButton = wrapper.find('button.test_fa2_toggle')
await toggleButton.trigger('click') await toggleButton.trigger('click')
expect(startSpy.callCount).to.equal(2) expect(startSpy.callCount).to.equal(2)
@@ -1884,7 +2402,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true) expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1) await waitCondition(() => stopSpy.callCount === 1)
expect(wrapper.text()).to.contain('Start') expect(wrapper.text()).to.contain('Continue')
const coordinates = graph const coordinates = graph
.export() .export()

View File

@@ -210,8 +210,8 @@ describe('MainMenu.vue', () => {
isSaved: false isSaved: false
} }
const state = { const state = {
currentTab: tab, currentTab: null,
tabs: [tab], tabs: [],
db: {} db: {}
} }
const store = createStore({ state }) const store = createStore({ state })
@@ -228,6 +228,17 @@ describe('MainMenu.vue', () => {
const ctrlR = new KeyboardEvent('keydown', { key: 'r', ctrlKey: true }) const ctrlR = new KeyboardEvent('keydown', { key: 'r', ctrlKey: true })
const metaR = new KeyboardEvent('keydown', { key: 'r', metaKey: true }) const metaR = new KeyboardEvent('keydown', { key: 'r', metaKey: true })
// execute is not called - no tabs open
document.dispatchEvent(ctrlR)
expect(tab.execute.calledOnce).to.equal(false)
document.dispatchEvent(metaR)
expect(tab.execute.calledOnce).to.equal(false)
// Open a tab
store.state.currentTab = tab
store.state.tabs = [tab]
// Running is enabled and route path is workspace // Running is enabled and route path is workspace
document.dispatchEvent(ctrlR) document.dispatchEvent(ctrlR)
expect(state.currentTab.execute.calledOnce).to.equal(true) expect(state.currentTab.execute.calledOnce).to.equal(true)
@@ -257,8 +268,8 @@ describe('MainMenu.vue', () => {
isSaved: false isSaved: false
} }
const state = { const state = {
currentTab: tab, currentTab: null,
tabs: [tab], tabs: [],
db: {} db: {}
} }
const store = createStore({ state }) const store = createStore({ state })
@@ -281,6 +292,17 @@ describe('MainMenu.vue', () => {
key: 'Enter', key: 'Enter',
metaKey: true metaKey: true
}) })
// execute is not called - no tabs open
document.dispatchEvent(ctrlEnter)
expect(tab.execute.calledOnce).to.equal(false)
document.dispatchEvent(metaEnter)
expect(tab.execute.calledOnce).to.equal(false)
// Open a tab
store.state.currentTab = tab
store.state.tabs = [tab]
// Running is enabled and route path is workspace // Running is enabled and route path is workspace
document.dispatchEvent(ctrlEnter) document.dispatchEvent(ctrlEnter)
expect(state.currentTab.execute.calledOnce).to.equal(true) expect(state.currentTab.execute.calledOnce).to.equal(true)
@@ -347,8 +369,8 @@ describe('MainMenu.vue', () => {
isSaved: false isSaved: false
} }
const state = { const state = {
currentTab: tab, currentTab: null,
tabs: [tab], tabs: [],
db: {} db: {}
} }
const store = createStore({ state }) const store = createStore({ state })
@@ -365,6 +387,17 @@ describe('MainMenu.vue', () => {
const ctrlS = new KeyboardEvent('keydown', { key: 's', ctrlKey: true }) const ctrlS = new KeyboardEvent('keydown', { key: 's', ctrlKey: true })
const metaS = new KeyboardEvent('keydown', { key: 's', metaKey: true }) const metaS = new KeyboardEvent('keydown', { key: 's', metaKey: true })
// onSave is not called - no tabs open
document.dispatchEvent(ctrlS)
expect(wrapper.vm.onSave.calledOnce).to.equal(false)
document.dispatchEvent(metaS)
expect(wrapper.vm.onSave.calledOnce).to.equal(false)
// Open a tab
store.state.currentTab = tab
store.state.tabs = [tab]
// tab is unsaved and route is /workspace // tab is unsaved and route is /workspace
document.dispatchEvent(ctrlS) document.dispatchEvent(ctrlS)
expect(wrapper.vm.onSave.calledOnce).to.equal(true) expect(wrapper.vm.onSave.calledOnce).to.equal(true)
@@ -394,8 +427,8 @@ describe('MainMenu.vue', () => {
isSaved: false isSaved: false
} }
const state = { const state = {
currentTab: tab, currentTab: null,
tabs: [tab], tabs: [],
db: {} db: {}
} }
const store = createStore({ state }) const store = createStore({ state })
@@ -420,6 +453,17 @@ describe('MainMenu.vue', () => {
metaKey: true, metaKey: true,
shiftKey: true shiftKey: true
}) })
// onSaveAs is not called - no tabs open
document.dispatchEvent(ctrlS)
expect(wrapper.vm.onSaveAs.calledOnce).to.equal(false)
document.dispatchEvent(metaS)
expect(wrapper.vm.onSaveAs.calledOnce).to.equal(false)
// Open a tab
store.state.currentTab = tab
store.state.tabs = [tab]
// tab is unsaved and route is /workspace // tab is unsaved and route is /workspace
document.dispatchEvent(ctrlS) document.dispatchEvent(ctrlS)
expect(wrapper.vm.onSaveAs.calledOnce).to.equal(true) expect(wrapper.vm.onSaveAs.calledOnce).to.equal(true)

View File

@@ -21,37 +21,69 @@ describe('chartHelper.js', () => {
]) ])
}) })
it('getSelectedPointsIndexes returns unique selected point indexes', () => {
const stateData = [
{ selectedpoints: [1, 2, 3] },
{ selectedpoints: [3, 4] },
{ selectedpoints: [] },
{}
]
const indexes = chartHelper.getSelectedPointsIndexes(stateData)
expect(indexes).to.eql([1, 2, 3, 4])
})
it('getOptionsForSave', () => { it('getOptionsForSave', () => {
const state = { const state = {
data: { data: [
foo: {}, {
bar: {} foo: {},
}, bar: {},
layout: {}, selectedpoints: []
}
],
layout: { selections: [{}] },
frames: {} frames: {}
} }
const dataSources = { const dataSources = {
id: [1, 2], id: [1, 2],
name: ['foo', 'bar'] name: ['foo', 'bar']
} }
sinon.stub(dereference, 'default') const dereferenceArgs = []
sinon.stub(dereference, 'default').callsFake((data, emptySources) => {
dereferenceArgs[0] = JSON.stringify(data)
dereferenceArgs[1] = JSON.stringify(emptySources)
})
sinon.spy(JSON, 'parse') sinon.spy(JSON, 'parse')
const ds = chartHelper.getOptionsForSave(state, dataSources) const ds = chartHelper.getOptionsForSave(state, dataSources)
expect(dereference.default.calledOnce).to.equal(true) expect(dereference.default.calledOnce).to.equal(true)
expect(dereferenceArgs[0]).to.eql(
const args = dereference.default.firstCall.args JSON.stringify([
expect(args[0]).to.eql({ {
foo: {}, foo: {},
bar: {} bar: {},
}) selectedpoints: []
expect(args[1]).to.eql({ }
id: [], ])
name: [] )
}) expect(dereferenceArgs[1]).to.eql(
JSON.stringify({
id: [],
name: []
})
)
expect(ds).to.equal(JSON.parse.returnValues[0]) expect(ds).to.equal(JSON.parse.returnValues[0])
expect(ds.layout.selections).to.eql([])
expect(ds.data).to.eql([
{
foo: {},
bar: {}
}
])
}) })
it('getImageDataUrl returns dataUrl', async () => { it('getImageDataUrl returns dataUrl', async () => {
@@ -101,4 +133,32 @@ describe('chartHelper.js', () => {
'Plotly.newPlot(el, "plotly data", "plotly layout"' 'Plotly.newPlot(el, "plotly data", "plotly layout"'
) )
}) })
it('getRowsByIndexFromDataSources', () => {
const dataSources = {
id: [1, 2, 3],
name: ['Harry', 'Draco', 'Ron'],
points: [10, null, 7]
}
const rows = chartHelper.getRowsByIndexFromDataSources(dataSources, [1, 2])
expect(rows).to.eql([
{ id: 2, name: 'Draco', points: null },
{ id: 3, name: 'Ron', points: 7 }
])
})
it('clearSelection', () => {
const layout = {
selections: [{}],
someLayoutField: 1
}
const data = [
{ selectedpoints: [], someField: 1 },
{ someField: 2 },
{ selectedpoints: [], someField: 3 }
]
chartHelper.clearSelection(data, layout)
expect(layout).to.eql({ selections: [], someLayoutField: 1 })
expect(data).to.eql([{ someField: 1 }, { someField: 2 }, { someField: 3 }])
})
}) })

View File

@@ -2,6 +2,7 @@ import { expect } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import * as graphHelper from '@/lib/graphHelper' import * as graphHelper from '@/lib/graphHelper'
import Graph from 'graphology' import Graph from 'graphology'
import { random } from 'graphology-layout'
describe('graphHelper.js', () => { describe('graphHelper.js', () => {
afterEach(() => { afterEach(() => {
@@ -1231,7 +1232,11 @@ describe('graphHelper.js', () => {
} }
} }
const nodeData = { color: '#FF0000CC', label: 'Node label' } const nodeData = {
color: '#FF0000CC',
label: 'Node label',
labelColor: 'blue'
}
let interactionState = { let interactionState = {
selectedNodeId: 'node-1', selectedNodeId: 'node-1',
@@ -1249,6 +1254,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'black',
zIndex: 2, zIndex: 2,
highlighted: true, highlighted: true,
forceLabel: true forceLabel: true
@@ -1258,6 +1264,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'black',
zIndex: 2, zIndex: 2,
highlighted: true, highlighted: true,
forceLabel: true forceLabel: true
@@ -1268,6 +1275,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'blue',
zIndex: 2, zIndex: 2,
highlighted: false, highlighted: false,
forceLabel: true forceLabel: true
@@ -1278,6 +1286,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'blue',
zIndex: 2, zIndex: 2,
highlighted: false, highlighted: false,
forceLabel: true forceLabel: true
@@ -1288,6 +1297,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#3300cc', color: '#3300cc',
label: '', label: '',
labelColor: 'blue',
zIndex: 1, zIndex: 1,
highlighted: false highlighted: false
}) })
@@ -1308,6 +1318,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'black',
zIndex: 2, zIndex: 2,
highlighted: true, highlighted: true,
forceLabel: true forceLabel: true
@@ -1318,6 +1329,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'blue',
zIndex: 2, zIndex: 2,
highlighted: false, highlighted: false,
forceLabel: true forceLabel: true
@@ -1328,6 +1340,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'blue',
zIndex: 2, zIndex: 2,
highlighted: false, highlighted: false,
forceLabel: true forceLabel: true
@@ -1338,6 +1351,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'blue',
zIndex: 2, zIndex: 2,
highlighted: false, highlighted: false,
forceLabel: true forceLabel: true
@@ -1348,6 +1362,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#3300cc', color: '#3300cc',
label: '', label: '',
labelColor: 'blue',
zIndex: 1, zIndex: 1,
highlighted: false highlighted: false
}) })
@@ -1368,6 +1383,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'blue',
zIndex: 2, zIndex: 2,
highlighted: false, highlighted: false,
forceLabel: true forceLabel: true
@@ -1377,6 +1393,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'blue',
zIndex: 2, zIndex: 2,
highlighted: false, highlighted: false,
forceLabel: true forceLabel: true
@@ -1386,6 +1403,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'blue',
zIndex: 2, zIndex: 2,
highlighted: false, highlighted: false,
forceLabel: true forceLabel: true
@@ -1395,6 +1413,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'blue',
zIndex: 2, zIndex: 2,
highlighted: false, highlighted: false,
forceLabel: true forceLabel: true
@@ -1405,6 +1424,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#3300cc', color: '#3300cc',
label: '', label: '',
labelColor: 'blue',
zIndex: 1, zIndex: 1,
highlighted: false highlighted: false
}) })
@@ -1425,6 +1445,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'blue',
zIndex: 2, zIndex: 2,
highlighted: false, highlighted: false,
forceLabel: true forceLabel: true
@@ -1434,6 +1455,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'black',
zIndex: 2, zIndex: 2,
highlighted: true, highlighted: true,
forceLabel: true forceLabel: true
@@ -1443,6 +1465,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'blue',
zIndex: 2, zIndex: 2,
highlighted: false, highlighted: false,
forceLabel: true forceLabel: true
@@ -1452,6 +1475,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label', label: 'Node label',
labelColor: 'blue',
zIndex: 2, zIndex: 2,
highlighted: false, highlighted: false,
forceLabel: true forceLabel: true
@@ -1462,6 +1486,7 @@ describe('graphHelper.js', () => {
).to.eql({ ).to.eql({
color: '#3300cc', color: '#3300cc',
label: '', label: '',
labelColor: 'blue',
zIndex: 1, zIndex: 1,
highlighted: false highlighted: false
}) })
@@ -1481,32 +1506,37 @@ describe('graphHelper.js', () => {
graphHelper.reduceNodes('node-1', nodeData, interactionState, settings) graphHelper.reduceNodes('node-1', nodeData, interactionState, settings)
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label' label: 'Node label',
labelColor: 'blue'
}) })
expect( expect(
graphHelper.reduceNodes('node-2', nodeData, interactionState, settings) graphHelper.reduceNodes('node-2', nodeData, interactionState, settings)
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label' label: 'Node label',
labelColor: 'blue'
}) })
expect( expect(
graphHelper.reduceNodes('node-1.1', nodeData, interactionState, settings) graphHelper.reduceNodes('node-1.1', nodeData, interactionState, settings)
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label' label: 'Node label',
labelColor: 'blue'
}) })
expect( expect(
graphHelper.reduceNodes('node-2.1', nodeData, interactionState, settings) graphHelper.reduceNodes('node-2.1', nodeData, interactionState, settings)
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label' label: 'Node label',
labelColor: 'blue'
}) })
expect( expect(
graphHelper.reduceNodes('node-3', nodeData, interactionState, settings) graphHelper.reduceNodes('node-3', nodeData, interactionState, settings)
).to.eql({ ).to.eql({
color: '#FF0000CC', color: '#FF0000CC',
label: 'Node label' label: 'Node label',
labelColor: 'blue'
}) })
}) })
@@ -2193,4 +2223,44 @@ describe('graphHelper.js', () => {
) )
).to.eql(edgeData) ).to.eql(edgeData)
}) })
it('clearNodeCoordinates', () => {
const dataSources = {
doc: [
'{"type": 0, "node_id": 1, "label": "cat"}',
'{"type": 0, "node_id": 2, "label": "dog"}'
]
}
const graph = new Graph()
const options = {
structure: {
nodeId: 'node_id',
objectType: 'type',
edgeSource: null,
edgeTarget: null
}
}
graphHelper.buildNodes(graph, dataSources, options)
random.assign(graph)
graphHelper.clearNodeCoordinates(graph)
expect(graph.export().nodes).to.eql([
{
key: '1',
attributes: {
data: { type: 0, node_id: 1, label: 'cat' },
x: undefined,
y: undefined
}
},
{
key: '2',
attributes: {
data: { type: 0, node_id: 2, label: 'dog' },
x: undefined,
y: undefined
}
}
])
})
}) })

View File

@@ -8,14 +8,22 @@ describe('_migrations.js', () => {
id: '123', id: '123',
name: 'foo', name: 'foo',
query: 'SELECT * FROM foo', query: 'SELECT * FROM foo',
chart: { here_are: 'foo chart settings' }, chart: {
data: [{ selectedpoints: ['some selected points'] }, {}],
layout: { selections: [{}, {}] },
frames: 'foo chart frames'
},
createdAt: '2021-05-06T11:05:50.877Z' createdAt: '2021-05-06T11:05:50.877Z'
}, },
{ {
id: '456', id: '456',
name: 'bar', name: 'bar',
query: 'SELECT * FROM bar', query: 'SELECT * FROM bar',
chart: { here_are: 'bar chart settings' }, chart: {
data: [{}, {}],
layout: {},
frames: 'bar chart frames'
},
createdAt: '2021-05-07T11:05:50.877Z' createdAt: '2021-05-07T11:05:50.877Z'
} }
] ]
@@ -26,7 +34,11 @@ describe('_migrations.js', () => {
name: 'foo', name: 'foo',
query: 'SELECT * FROM foo', query: 'SELECT * FROM foo',
viewType: 'chart', viewType: 'chart',
viewOptions: { here_are: 'foo chart settings' }, viewOptions: {
data: [{}, {}],
layout: { selections: [] },
frames: 'foo chart frames'
},
createdAt: '2021-05-06T11:05:50.877Z' createdAt: '2021-05-06T11:05:50.877Z'
}, },
{ {
@@ -34,7 +46,7 @@ describe('_migrations.js', () => {
name: 'bar', name: 'bar',
query: 'SELECT * FROM bar', query: 'SELECT * FROM bar',
viewType: 'chart', viewType: 'chart',
viewOptions: { here_are: 'bar chart settings' }, viewOptions: { data: [{}, {}], layout: {}, frames: 'bar chart frames' },
createdAt: '2021-05-07T11:05:50.877Z' createdAt: '2021-05-07T11:05:50.877Z'
} }
]) ])
@@ -47,7 +59,11 @@ describe('_migrations.js', () => {
name: 'foo', name: 'foo',
query: 'SELECT * FROM foo', query: 'SELECT * FROM foo',
viewType: 'chart', viewType: 'chart',
viewOptions: { here_are: 'foo chart settings' }, viewOptions: {
data: [{ selectedpoints: ['some selected points'] }, {}],
layout: { selections: [{}, {}] },
frames: 'foo chart frames'
},
createdAt: '2021-05-06T11:05:50.877Z' createdAt: '2021-05-06T11:05:50.877Z'
}, },
{ {
@@ -82,7 +98,22 @@ describe('_migrations.js', () => {
label: { source: null, color: '#a2b1c6' } label: { source: null, color: '#a2b1c6' }
} }
}, },
layout: { type: 'circular', options: null } layout: {
type: 'forceAtlas2',
options: {
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
}, },
createdAt: '2021-05-07T11:05:50.877Z' createdAt: '2021-05-07T11:05:50.877Z'
} }
@@ -93,7 +124,11 @@ describe('_migrations.js', () => {
name: 'foo', name: 'foo',
query: 'SELECT * FROM foo', query: 'SELECT * FROM foo',
viewType: 'chart', viewType: 'chart',
viewOptions: { here_are: 'foo chart settings' }, viewOptions: {
data: [{}, {}],
layout: { selections: [] },
frames: 'foo chart frames'
},
createdAt: '2021-05-06T11:05:50.877Z' createdAt: '2021-05-06T11:05:50.877Z'
}, },
{ {
@@ -130,7 +165,300 @@ describe('_migrations.js', () => {
label: { source: null, color: '#a2b1c6' } label: { source: null, color: '#a2b1c6' }
} }
}, },
layout: { type: 'circular', options: null } layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
},
createdAt: '2021-05-07T11:05:50.877Z'
}
])
})
it('migrates from version 3 to the current', () => {
const oldInquiries = [
{
id: '123',
name: 'foo',
query: 'SELECT * FROM foo',
viewType: 'chart',
viewOptions: {
data: [{ selectedpoints: ['some selected points'] }, {}],
layout: { selections: [{}, {}] },
frames: 'foo chart frames'
},
createdAt: '2021-05-06T11:05:50.877Z'
},
{
id: '456',
name: 'bar',
query: 'SELECT * FROM bar',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed',
opacity: 100
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: {
type: 'forceAtlas2',
options: {
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
},
createdAt: '2021-05-07T11:05:50.877Z'
}
]
expect(migrations._migrate(3, oldInquiries)).to.eql([
{
id: '123',
name: 'foo',
query: 'SELECT * FROM foo',
viewType: 'chart',
viewOptions: {
data: [{}, {}],
layout: { selections: [] },
frames: 'foo chart frames'
},
createdAt: '2021-05-06T11:05:50.877Z'
},
{
id: '456',
name: 'bar',
query: 'SELECT * FROM bar',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed',
opacity: 100
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
},
createdAt: '2021-05-07T11:05:50.877Z'
}
])
})
it('migrates from version 4 to the current', () => {
const oldInquiries = [
{
id: '123',
name: 'foo',
query: 'SELECT * FROM foo',
viewType: 'chart',
viewOptions: {
data: [{ selectedpoints: ['some selected points'] }, {}],
layout: { selections: [{}, {}] },
frames: 'foo chart frames'
},
createdAt: '2021-05-06T11:05:50.877Z'
},
{
id: '456',
name: 'bar',
query: 'SELECT * FROM bar',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed',
opacity: 100
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
},
createdAt: '2021-05-07T11:05:50.877Z'
}
]
expect(migrations._migrate(4, oldInquiries)).to.eql([
{
id: '123',
name: 'foo',
query: 'SELECT * FROM foo',
viewType: 'chart',
viewOptions: {
data: [{}, {}],
layout: { selections: [] },
frames: 'foo chart frames'
},
createdAt: '2021-05-06T11:05:50.877Z'
},
{
id: '456',
name: 'bar',
query: 'SELECT * FROM bar',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed',
opacity: 100
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
}, },
createdAt: '2021-05-07T11:05:50.877Z' createdAt: '2021-05-07T11:05:50.877Z'
} }

View File

@@ -92,9 +92,24 @@ describe('storedInquiries.js', () => {
label: { source: null, color: '#a2b1c6' } label: { source: null, color: '#a2b1c6' }
} }
}, },
layout: { type: 'circular', options: null } layout: {
type: 'forceAtlas2',
options: {
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
}, },
name: 'student graph', name: 'student graph FA2',
updatedAt: '2026-01-19T21:49:40.708Z', updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z' createdAt: '2026-01-19T21:46:13.899Z'
}, },
@@ -102,7 +117,11 @@ describe('storedInquiries.js', () => {
id: 'Yh1Hc9v7P3mRPZVM59QiD', id: 'Yh1Hc9v7P3mRPZVM59QiD',
query: 'SELECT * from test', query: 'SELECT * from test',
viewType: 'chart', viewType: 'chart',
viewOptions: 'some chart view options', viewOptions: {
data: [{ selectedpoints: [] }, {}],
layout: { selections: [{}, {}] },
frames: 'some chart frames'
},
name: 'student chart', name: 'student chart',
updatedAt: '2026-01-19T21:49:40.708Z', updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z' createdAt: '2026-01-19T21:46:13.899Z'
@@ -145,9 +164,25 @@ describe('storedInquiries.js', () => {
label: { source: null, color: '#a2b1c6' } label: { source: null, color: '#a2b1c6' }
} }
}, },
layout: { type: 'circular', options: null } layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
}, },
name: 'student graph', name: 'student graph FA2',
updatedAt: '2026-01-19T21:49:40.708Z', updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z' createdAt: '2026-01-19T21:46:13.899Z'
}, },
@@ -155,7 +190,11 @@ describe('storedInquiries.js', () => {
id: 'Yh1Hc9v7P3mRPZVM59QiD', id: 'Yh1Hc9v7P3mRPZVM59QiD',
query: 'SELECT * from test', query: 'SELECT * from test',
viewType: 'chart', viewType: 'chart',
viewOptions: 'some chart view options', viewOptions: {
data: [{}, {}],
layout: { selections: [] },
frames: 'some chart frames'
},
name: 'student chart', name: 'student chart',
updatedAt: '2026-01-19T21:49:40.708Z', updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z' createdAt: '2026-01-19T21:46:13.899Z'
@@ -244,7 +283,7 @@ describe('storedInquiries.js', () => {
const str = storedInquiries.serialiseInquiries(inquiryList) const str = storedInquiries.serialiseInquiries(inquiryList)
const parsedJson = JSON.parse(str) const parsedJson = JSON.parse(str)
expect(parsedJson.version).to.equal(3) expect(parsedJson.version).to.equal(5)
expect(parsedJson.inquiries).to.have.lengthOf(2) expect(parsedJson.inquiries).to.have.lengthOf(2)
expect(parsedJson.inquiries[1]).to.eql(inquiryList[1]) expect(parsedJson.inquiries[1]).to.eql(inquiryList[1])
expect(parsedJson.inquiries[0]).to.eql({ expect(parsedJson.inquiries[0]).to.eql({
@@ -305,7 +344,11 @@ describe('storedInquiries.js', () => {
"name": "foo", "name": "foo",
"query": "select * from foo", "query": "select * from foo",
"viewType": "chart", "viewType": "chart",
"viewOptions": [], "viewOptions": {
"data": [{"selectedpoints": []}, {}],
"layout": {"selections": [{}, {}]},
"frames": "some chart frames"
},
"createdAt": "2020-11-03T14:17:49.524Z" "createdAt": "2020-11-03T14:17:49.524Z"
}, },
{ {
@@ -339,7 +382,22 @@ describe('storedInquiries.js', () => {
"label": { "source": null, "color": "#a2b1c6" } "label": { "source": null, "color": "#a2b1c6" }
} }
}, },
"layout": { "type": "circular", "options": null } "layout": {
"type": "forceAtlas2",
"options": {
"initialIterationsAmount": 50,
"adjustSizes": false,
"barnesHutOptimize": false,
"barnesHutTheta": 0.5,
"edgeWeightInfluence": 0,
"gravity": 1,
"linLogMode": false,
"outboundAttractionDistribution": false,
"scalingRatio": 1,
"slowDown": 1,
"strongGravityMode": false
}
}
}, },
"name": "student graph", "name": "student graph",
"createdAt": "2026-01-19T21:46:13.899Z" "createdAt": "2026-01-19T21:46:13.899Z"
@@ -355,7 +413,11 @@ describe('storedInquiries.js', () => {
name: 'foo', name: 'foo',
query: 'select * from foo', query: 'select * from foo',
viewType: 'chart', viewType: 'chart',
viewOptions: [], viewOptions: {
data: [{}, {}],
layout: { selections: [] },
frames: 'some chart frames'
},
createdAt: '2020-11-03T14:17:49.524Z' createdAt: '2020-11-03T14:17:49.524Z'
}, },
{ {
@@ -391,7 +453,23 @@ describe('storedInquiries.js', () => {
label: { source: null, color: '#a2b1c6' } label: { source: null, color: '#a2b1c6' }
} }
}, },
layout: { type: 'circular', options: null } layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
}, },
name: 'student graph', name: 'student graph',
createdAt: '2026-01-19T21:46:13.899Z' createdAt: '2026-01-19T21:46:13.899Z'
@@ -486,7 +564,7 @@ describe('storedInquiries.js', () => {
it('importInquiries', async () => { it('importInquiries', async () => {
const str = `{ const str = `{
"version": 3, "version": 4,
"inquiries": [{ "inquiries": [{
"id": 1, "id": 1,
"name": "foo", "name": "foo",
@@ -545,7 +623,11 @@ describe('storedInquiries.js', () => {
"name": "foo", "name": "foo",
"query": "select * from foo", "query": "select * from foo",
"viewType": "chart", "viewType": "chart",
"viewOptions": [], "viewOptions": {
"data": [{"selectedpoints": []}, {}],
"layout": {"selections": [{}, {}]},
"frames": "some chart frames"
},
"createdAt": "2020-11-03T14:17:49.524Z" "createdAt": "2020-11-03T14:17:49.524Z"
}, },
{ {
@@ -579,7 +661,22 @@ describe('storedInquiries.js', () => {
"label": { "source": null, "color": "#a2b1c6" } "label": { "source": null, "color": "#a2b1c6" }
} }
}, },
"layout": { "type": "circular", "options": null } "layout": {
"type": "forceAtlas2",
"options": {
"initialIterationsAmount": 50,
"adjustSizes": false,
"barnesHutOptimize": false,
"barnesHutTheta": 0.5,
"edgeWeightInfluence": 0,
"gravity": 1,
"linLogMode": false,
"outboundAttractionDistribution": false,
"scalingRatio": 1,
"slowDown": 1,
"strongGravityMode": false
}
}
}, },
"name": "student graph", "name": "student graph",
"createdAt": "2026-01-19T21:46:13.899Z" "createdAt": "2026-01-19T21:46:13.899Z"
@@ -596,7 +693,11 @@ describe('storedInquiries.js', () => {
name: 'foo', name: 'foo',
query: 'select * from foo', query: 'select * from foo',
viewType: 'chart', viewType: 'chart',
viewOptions: [], viewOptions: {
data: [{}, {}],
layout: { selections: [] },
frames: 'some chart frames'
},
createdAt: '2020-11-03T14:17:49.524Z' createdAt: '2020-11-03T14:17:49.524Z'
}, },
{ {
@@ -632,7 +733,23 @@ describe('storedInquiries.js', () => {
label: { source: null, color: '#a2b1c6' } label: { source: null, color: '#a2b1c6' }
} }
}, },
layout: { type: 'circular', options: null } layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
}, },
name: 'student graph', name: 'student graph',
createdAt: '2026-01-19T21:46:13.899Z' createdAt: '2026-01-19T21:46:13.899Z'
@@ -642,7 +759,7 @@ describe('storedInquiries.js', () => {
it('readPredefinedInquiries', async () => { it('readPredefinedInquiries', async () => {
const str = `{ const str = `{
"version": 3, "version": 5,
"inquiries": [ "inquiries": [
{ {
"id": 1, "id": 1,

View File

@@ -0,0 +1,122 @@
import { expect } from 'chai'
import { mount, flushPromises } from '@vue/test-utils'
import actions from '@/store/actions'
import mutations from '@/store/mutations'
import { createStore } from 'vuex'
import MainView from '@/views/MainView.vue'
import { routes } from '@/router'
import { createRouter, createWebHistory } from 'vue-router'
describe('MainView.vue', () => {
it('Saves inquiry without data', async () => {
const state = {
db: {},
tabs: [],
inquiries: []
}
const store = createStore({ state, actions, mutations })
const router = createRouter({
history: createWebHistory(),
routes: routes
})
router.push('/workspace')
await router.isReady()
const wrapper = mount(MainView, {
global: {
stubs: {
'router-link': true,
teleport: false,
'app-diagnostic-info': true,
transition: false,
schema: true,
'run-result': true
},
plugins: [store, router]
},
attachTo: document.body
})
await wrapper.find('#create-btn').trigger('click')
store.state.tabs[1].result = {
columns: ['id', 'name'],
values: {
id: [1, 2],
name: ['Harry', 'Drako']
}
}
await flushPromises()
// Add trace
await wrapper
.findAllComponents({ name: 'Chart' })[1]
.find('button.js-add-button')
.wrapperElement.click()
await flushPromises()
// Select data for axis x
await wrapper
.findAll('.field .dropdown-container .Select__indicator')[0]
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[0].trigger('click')
// Select data for axis y
await wrapper
.findAll('.field .dropdown-container .Select__indicator')[2]
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[1].trigger('click')
// Click Save in the top menu
await wrapper.find('#save-btn').trigger('click')
// Input inquiry name
document.querySelector('.dialog input').value = 'test'
document.querySelector('.dialog input').dispatchEvent(
new InputEvent('input', {
data: 'test'
})
)
// Click Save in the dialog
document
.querySelector('.dialog .dialog-buttons-container button.primary')
.click()
const inquiriesInStore = store.state.inquiries
expect(inquiriesInStore.length).to.equal(1)
expect(inquiriesInStore[0].createdAt).to.not.be.null
expect(inquiriesInStore[0].createdAt).to.not.be.undefined
expect(inquiriesInStore[0].updateAt).to.not.be.null
expect(inquiriesInStore[0].updatedAt).to.not.be.undefined
expect(inquiriesInStore[0].id).to.not.be.null
expect(inquiriesInStore[0].id).to.not.be.undefined
expect(inquiriesInStore[0].name).to.equal('test')
expect(inquiriesInStore[0].viewType).to.equal('chart')
expect(inquiriesInStore[0].viewOptions.data).to.eql([
{
type: 'scatter',
mode: 'markers',
x: null,
xsrc: 'id',
meta: {
columnNames: {
x: 'id',
y: 'name'
}
},
y: null,
ysrc: 'name'
}
])
wrapper.unmount()
})
})