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

Add seed layout #136

This commit is contained in:
lana-k
2026-04-05 17:39:28 +02:00
parent 62fb92d824
commit d9435a80c3
12 changed files with 785 additions and 90 deletions

View File

@@ -0,0 +1,60 @@
<template>
<Field label="Initial algorithm">
<Dropdown
:options="layoutOptions"
:value="modelValue.initialAlgorithm"
:clearable="false"
className="test_fa2_initial_layout_algorithm_select"
@change="update('initialAlgorithm', $event)"
/>
</Field>
<Field
v-if="modelValue.initialAlgorithm === 'random'"
label="Seed value"
fieldContainerClassName="test_fa2_seed_value"
>
<NumericInput
:value="modelValue.seedValue"
@update="update('seedValue', $event)"
/>
</Field>
</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'
export default {
components: {
Field: applyPureReactInVue(Field),
Dropdown: applyPureReactInVue(Dropdown),
NumericInput: applyPureReactInVue(NumericInput)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
layoutOptions: markRaw([
{ label: 'Circular', value: 'circular' },
{ label: 'Random', value: 'random' }
])
}
},
methods: {
update(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>

View File

@@ -171,6 +171,17 @@
/>
</Fold>
<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">
<AdvancedForceAtlasLayoutSettings
v-model="settings.layout.options"
@@ -182,6 +193,7 @@
<Button
variant="secondary"
class="test_fa2_reset"
title="Set the settings to default or previously saved ones."
@click="resetFA2LayoutSettings"
>
Reset
@@ -192,16 +204,25 @@
@click="toggleFA2Layout"
>
<template #node:icon>
<div
:style="{
padding: '0 3px'
}"
>
<div>
<RunIcon v-if="!fa2Running" />
<StopIcon v-else />
<PauseIcon v-else />
</div>
</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>
</div>
</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 RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue'
import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue'
import ForceAtlasSeedLayoutSettings from '@/components/Graph/ForceAtlasSeedLayoutSettings.vue'
// eslint-disable-next-line max-len
import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue'
import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
import FA2Layout from 'graphology-layout-forceatlas2/worker'
import * as forceAtlas2 from 'graphology-layout-forceatlas2'
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 {
buildNodes,
@@ -248,7 +271,8 @@ import {
updateNodes,
updateEdges,
reduceNodes,
reduceEdges
reduceEdges,
clearNodeCoordinates
} from '@/lib/graphHelper'
import Graph from 'graphology'
import { circular, random, circlepack } from 'graphology-layout'
@@ -273,14 +297,16 @@ export default {
Button: applyPureReactInVue(Button),
ColorPicker: applyPureReactInVue(ColorPicker),
RunIcon,
StopIcon,
RestartIcon,
PauseIcon,
RandomLayoutSettings,
CirclePackLayoutSettings,
NodeColorSettings,
NodeSizeSettings,
EdgeSizeSettings,
EdgeColorSettings,
AdvancedForceAtlasLayoutSettings
AdvancedForceAtlasLayoutSettings,
ForceAtlasSeedLayoutSettings
},
inject: ['tabLayout'],
props: {
@@ -643,14 +669,12 @@ export default {
}
if (layoutType === 'circular') {
circular.assign(this.graph)
this.applyCircularLayout()
return
}
if (layoutType === 'random') {
random.assign(this.graph, {
rng: seedrandom(this.settings.layout.options.seedValue)
})
this.applyRandomLayout()
return
}
@@ -688,14 +712,32 @@ export default {
}
if (layoutType === 'forceAtlas2') {
this.applyFA2Layout()
if (layoutType !== prevLayout) {
this.autoRunFA2Layout()
}
}
},
applyCircularLayout() {
circular.assign(this.graph)
},
applyRandomLayout() {
random.assign(this.graph, {
rng: seedrandom(this.settings.layout.options.seedValue)
})
},
applyFA2Layout() {
if (
!this.graph.someNode(
(nodeKey, attributes) =>
typeof attributes.x === 'number' &&
typeof attributes.y === 'number'
typeof attributes.x === 'number' && typeof attributes.y === 'number'
)
) {
circular.assign(this.graph)
if (this.settings.layout.options.initialAlgorithm === 'circular') {
this.applyCircularLayout()
} else {
this.applyRandomLayout()
}
}
this.fa2Layout = markRaw(
@@ -707,10 +749,6 @@ export default {
settings: this.settings.layout.options
})
)
if (layoutType !== prevLayout) {
this.autoRunFA2Layout()
}
}
},
toggleFA2Layout() {
if (this.fa2Layout.isRunning()) {
@@ -731,6 +769,14 @@ export default {
this.checkIteration = null
}
},
restartFA2Layout() {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
}
clearNodeCoordinates(this.graph)
this.applyFA2Layout()
this.autoRunFA2Layout()
},
autoRunFA2Layout() {
let iteration = 1
this.checkIteration = () => {
@@ -748,6 +794,8 @@ export default {
setRecommendedFA2Settings() {
const sensibleSettings = forceAtlas2.default.inferSettings(this.graph)
this.settings.layout.options = {
initialAlgorithm: 'circular',
seedValue: 1,
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
@@ -809,4 +857,8 @@ export default {
flex-grow: 1;
flex-basis: 0;
}
.force-atlas-buttons :deep(.button__icon > div) {
padding: 0 3px;
}
</style>

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

View File

@@ -17,6 +17,18 @@ export default {
})
}
if (installedVersion < 4) {
inquiries.forEach(inquiry => {
if (
inquiry.viewType === 'graph' &&
inquiry.viewOptions.layout.type === 'forceAtlas2'
) {
inquiry.viewOptions.layout.options.initialAlgorithm = 'circular'
inquiry.viewOptions.layout.options.seedValue = 1
}
})
}
return inquiries
}
}

View File

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

View File

@@ -1309,7 +1309,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1)
expect(wrapper.text()).to.contain('Start')
expect(wrapper.text()).to.contain('Continue')
const coordinates = graph
.export()
@@ -1349,6 +1349,8 @@ describe('GraphEditor', () => {
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
seedValue: 1,
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
@@ -1383,7 +1385,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1)
expect(wrapper.text()).to.contain('Start')
expect(wrapper.text()).to.contain('Continue')
const initialCoordinates = graph
.export()
@@ -1397,7 +1399,7 @@ describe('GraphEditor', () => {
new Event('blur', { bubbles: true })
)
// 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()
expect(wrapper.vm.settings.layout.options.gravity).to.equal(12)
@@ -1405,6 +1407,30 @@ describe('GraphEditor', () => {
// Algorithm wasn't called
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_fa2_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
const scalingInput = wrapper.find('.test_fa2_scaling input')
await scalingInput.setValue(2)
@@ -1493,13 +1519,13 @@ describe('GraphEditor', () => {
false
)
// Click Start
// Click Continue
const toggleButton = wrapper.find('button.test_fa2_toggle')
await toggleButton.trigger('click')
expect(toggleButton.text()).to.contain('Stop')
expect(toggleButton.text()).to.contain('Pause')
expect(startSpy.callCount).to.equal(2)
// Wait a bit and click Stop
// Wait a bit and click Pause
await time.sleep(500)
await toggleButton.trigger('click')
expect(stopSpy.callCount).to.equal(2)
@@ -1513,9 +1539,11 @@ describe('GraphEditor', () => {
// Click Reset
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(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'circular',
seedValue: 1,
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
@@ -1533,6 +1561,149 @@ describe('GraphEditor', () => {
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',
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
}
}
},
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_fa2_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 () => {
const wrapper = mount(GraphEditor, {
attachTo: document.body,
@@ -1603,6 +1774,26 @@ describe('GraphEditor', () => {
)
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_fa2_seed_value input')
await seedValueInput.setValue(123)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
// Change scaling ratio
const scalingInput = wrapper.find('.test_fa2_scaling input')
await scalingInput.setValue(2)
@@ -1671,6 +1862,8 @@ describe('GraphEditor', () => {
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'random',
seedValue: 123,
initialIterationsAmount: 120,
gravity: 12,
scalingRatio: 2,
@@ -1788,7 +1981,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1)
// Click Start
// Click Continue
const toggleButton = wrapper.find('button.test_fa2_toggle')
await toggleButton.trigger('click')
expect(startSpy.callCount).to.equal(2)
@@ -1884,7 +2077,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1)
expect(wrapper.text()).to.contain('Start')
expect(wrapper.text()).to.contain('Continue')
const coordinates = graph
.export()

View File

@@ -2,6 +2,7 @@ import { expect } from 'chai'
import sinon from 'sinon'
import * as graphHelper from '@/lib/graphHelper'
import Graph from 'graphology'
import { random } from 'graphology-layout'
describe('graphHelper.js', () => {
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 = {
selectedNodeId: 'node-1',
@@ -1249,6 +1254,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'black',
zIndex: 2,
highlighted: true,
forceLabel: true
@@ -1258,6 +1264,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'black',
zIndex: 2,
highlighted: true,
forceLabel: true
@@ -1268,6 +1275,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'blue',
zIndex: 2,
highlighted: false,
forceLabel: true
@@ -1278,6 +1286,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'blue',
zIndex: 2,
highlighted: false,
forceLabel: true
@@ -1288,6 +1297,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#3300cc',
label: '',
labelColor: 'blue',
zIndex: 1,
highlighted: false
})
@@ -1308,6 +1318,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'black',
zIndex: 2,
highlighted: true,
forceLabel: true
@@ -1318,6 +1329,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'blue',
zIndex: 2,
highlighted: false,
forceLabel: true
@@ -1328,6 +1340,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'blue',
zIndex: 2,
highlighted: false,
forceLabel: true
@@ -1338,6 +1351,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'blue',
zIndex: 2,
highlighted: false,
forceLabel: true
@@ -1348,6 +1362,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#3300cc',
label: '',
labelColor: 'blue',
zIndex: 1,
highlighted: false
})
@@ -1368,6 +1383,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'blue',
zIndex: 2,
highlighted: false,
forceLabel: true
@@ -1377,6 +1393,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'blue',
zIndex: 2,
highlighted: false,
forceLabel: true
@@ -1386,6 +1403,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'blue',
zIndex: 2,
highlighted: false,
forceLabel: true
@@ -1395,6 +1413,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'blue',
zIndex: 2,
highlighted: false,
forceLabel: true
@@ -1405,6 +1424,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#3300cc',
label: '',
labelColor: 'blue',
zIndex: 1,
highlighted: false
})
@@ -1425,6 +1445,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'blue',
zIndex: 2,
highlighted: false,
forceLabel: true
@@ -1434,6 +1455,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'black',
zIndex: 2,
highlighted: true,
forceLabel: true
@@ -1443,6 +1465,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'blue',
zIndex: 2,
highlighted: false,
forceLabel: true
@@ -1452,6 +1475,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#FF0000CC',
label: 'Node label',
labelColor: 'blue',
zIndex: 2,
highlighted: false,
forceLabel: true
@@ -1462,6 +1486,7 @@ describe('graphHelper.js', () => {
).to.eql({
color: '#3300cc',
label: '',
labelColor: 'blue',
zIndex: 1,
highlighted: false
})
@@ -1481,32 +1506,37 @@ describe('graphHelper.js', () => {
graphHelper.reduceNodes('node-1', nodeData, interactionState, settings)
).to.eql({
color: '#FF0000CC',
label: 'Node label'
label: 'Node label',
labelColor: 'blue'
})
expect(
graphHelper.reduceNodes('node-2', nodeData, interactionState, settings)
).to.eql({
color: '#FF0000CC',
label: 'Node label'
label: 'Node label',
labelColor: 'blue'
})
expect(
graphHelper.reduceNodes('node-1.1', nodeData, interactionState, settings)
).to.eql({
color: '#FF0000CC',
label: 'Node label'
label: 'Node label',
labelColor: 'blue'
})
expect(
graphHelper.reduceNodes('node-2.1', nodeData, interactionState, settings)
).to.eql({
color: '#FF0000CC',
label: 'Node label'
label: 'Node label',
labelColor: 'blue'
})
expect(
graphHelper.reduceNodes('node-3', nodeData, interactionState, settings)
).to.eql({
color: '#FF0000CC',
label: 'Node label'
label: 'Node label',
labelColor: 'blue'
})
})
@@ -2193,4 +2223,40 @@ describe('graphHelper.js', () => {
)
).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' }
}
},
{
key: '2',
attributes: {
data: { type: 0, node_id: 2, label: 'dog' }
}
}
])
})
})

View File

@@ -82,7 +82,22 @@ describe('_migrations.js', () => {
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'
}
@@ -130,7 +145,155 @@ describe('_migrations.js', () => {
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
seedValue: 1,
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',
seedValue: 1,
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'
}

View File

@@ -92,9 +92,24 @@ describe('storedInquiries.js', () => {
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',
createdAt: '2026-01-19T21:46:13.899Z'
},
@@ -145,9 +160,26 @@ describe('storedInquiries.js', () => {
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
seedValue: 1,
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',
createdAt: '2026-01-19T21:46:13.899Z'
},
@@ -244,7 +276,7 @@ describe('storedInquiries.js', () => {
const str = storedInquiries.serialiseInquiries(inquiryList)
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[1]).to.eql(inquiryList[1])
expect(parsedJson.inquiries[0]).to.eql({
@@ -339,7 +371,22 @@ describe('storedInquiries.js', () => {
"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",
"createdAt": "2026-01-19T21:46:13.899Z"
@@ -391,7 +438,24 @@ describe('storedInquiries.js', () => {
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
seedValue: 1,
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',
createdAt: '2026-01-19T21:46:13.899Z'
@@ -486,7 +550,7 @@ describe('storedInquiries.js', () => {
it('importInquiries', async () => {
const str = `{
"version": 3,
"version": 4,
"inquiries": [{
"id": 1,
"name": "foo",
@@ -579,7 +643,22 @@ describe('storedInquiries.js', () => {
"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",
"createdAt": "2026-01-19T21:46:13.899Z"
@@ -632,7 +711,24 @@ describe('storedInquiries.js', () => {
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
seedValue: 1,
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',
createdAt: '2026-01-19T21:46:13.899Z'
@@ -642,7 +738,7 @@ describe('storedInquiries.js', () => {
it('readPredefinedInquiries', async () => {
const str = `{
"version": 3,
"version": 4,
"inquiries": [
{
"id": 1,