1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2026-05-06 20:09:18 +08:00

Compare commits

...

5 Commits

Author SHA1 Message Date
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
16 changed files with 1085 additions and 137 deletions

219
package-lock.json generated
View File

@@ -1,12 +1,13 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.28.2", "version": "0.29.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.28.2", "version": "0.29.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",
@@ -29,7 +30,7 @@
"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

@@ -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",
@@ -33,7 +34,7 @@
"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",

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

@@ -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,19 +674,21 @@ 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) {
this.autoRunFA2Layout()
}
},
applyCircularLayout() {
circular.assign(this.graph)
},
applyRandomLayout() {
random.assign(this.graph, { random.assign(this.graph, {
rng: seedrandom(this.settings.layout.options.seedValue) rng: seedrandom(this.settings.layout.options.seedValue)
}) })
return },
} applyCirclePackLayout() {
if (layoutType === 'circlepack') {
this.graph.forEachNode(nodeId => { this.graph.forEachNode(nodeId => {
this.graph.updateNode(nodeId, attributes => { this.graph.updateNode(nodeId, attributes => {
const newAttributes = { ...attributes } const newAttributes = { ...attributes }
@@ -684,33 +718,32 @@ export default {
) || [], ) || [],
rng: seedrandom(this.settings.layout.options.seedValue) rng: seedrandom(this.settings.layout.options.seedValue)
}) })
return },
} applyFA2Layout() {
if (layoutType === 'forceAtlas2') {
if ( if (
!this.graph.someNode( !this.graph.someNode(
(nodeKey, attributes) => (nodeKey, attributes) =>
typeof attributes.x === 'number' && typeof attributes.x === 'number' && typeof attributes.y === 'number'
typeof attributes.y === 'number'
) )
) { ) {
circular.assign(this.graph) 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( this.fa2Layout = markRaw(
new FA2Layout(this.graph, { new FA2Layout(this.graph, {
getEdgeWeight: (_, attr) => getEdgeWeight: (_, attr) =>
this.settings.layout.options.weightSource this.settings.layout.options.weightSource
? attr.data[this.settings.layout.options.weightSource] ? attr.data[this.settings.layout.options.weightSource]
: 1, : 1,
settings: this.settings.layout.options settings: fa2settings
}) })
) )
if (layoutType !== prevLayout) {
this.autoRunFA2Layout()
}
}
}, },
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

@@ -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

@@ -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

@@ -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

@@ -17,6 +17,17 @@ export default {
}) })
} }
if (installedVersion < 4) {
inquiries.forEach(inquiry => {
if (
inquiry.viewType === 'graph' &&
inquiry.viewOptions.layout.type === 'forceAtlas2'
) {
inquiry.viewOptions.layout.options.initialAlgorithm = 'circular'
}
})
}
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 = 4
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

@@ -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,10 @@ 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, initialIterationsAmount: 55,
gravity: 1.5, gravity: 1.5,
scalingRatio: 1.2, scalingRatio: 1.2,
@@ -1533,6 +1559,148 @@ describe('GraphEditor', () => {
wrapper.unmount() 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
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
const 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()
expect(initialCoordinates).not.to.equal(randomCoordinates1)
expect(randomCoordinates1).not.to.equal(randomCoordinates2)
expect(randomCoordinates1).to.equal(randomCoordinates1After)
wrapper.unmount()
})
it('FA2: resets parameters to default', async () => { it('FA2: resets parameters to default', async () => {
const wrapper = mount(GraphEditor, { const wrapper = mount(GraphEditor, {
attachTo: document.body, attachTo: document.body,
@@ -1603,6 +1771,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 +1859,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 +1946,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 +1979,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 +2075,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

@@ -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

@@ -82,7 +82,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'
} }
@@ -130,7 +145,153 @@ 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: { here_are: 'foo chart settings' },
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: { here_are: 'foo chart settings' },
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'
}, },
@@ -145,9 +160,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'
}, },
@@ -244,7 +275,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(4)
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({
@@ -339,7 +370,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"
@@ -391,7 +437,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 +548,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",
@@ -579,7 +641,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"
@@ -632,7 +709,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 +735,7 @@ describe('storedInquiries.js', () => {
it('readPredefinedInquiries', async () => { it('readPredefinedInquiries', async () => {
const str = `{ const str = `{
"version": 3, "version": 4,
"inquiries": [ "inquiries": [
{ {
"id": 1, "id": 1,