From d9435a80c349a264a40e071cca9c00c21ab6b8ec Mon Sep 17 00:00:00 2001 From: lana-k Date: Sun, 5 Apr 2026 17:39:28 +0200 Subject: [PATCH] Add seed layout #136 --- .../Graph/ForceAtlasSeedLayoutSettings.vue | 60 +++++ src/components/Graph/GraphEditor.vue | 120 +++++++--- src/components/svg/pause.vue | 27 +++ src/components/svg/restart.vue | 36 +++ src/components/svg/stop.vue | 21 -- src/lib/graphHelper.js | 10 + src/lib/storedInquiries/_migrations.js | 12 + src/lib/storedInquiries/index.js | 15 +- tests/components/Graph/GraphEditor.spec.js | 211 +++++++++++++++++- tests/lib/graphHelper.spec.js | 78 ++++++- tests/lib/storedInquiries/_migrations.spec.js | 167 +++++++++++++- .../storedInquiries/storedInquiries.spec.js | 118 +++++++++- 12 files changed, 785 insertions(+), 90 deletions(-) create mode 100644 src/components/Graph/ForceAtlasSeedLayoutSettings.vue create mode 100644 src/components/svg/pause.vue create mode 100644 src/components/svg/restart.vue delete mode 100644 src/components/svg/stop.vue diff --git a/src/components/Graph/ForceAtlasSeedLayoutSettings.vue b/src/components/Graph/ForceAtlasSeedLayoutSettings.vue new file mode 100644 index 0000000..c447e7d --- /dev/null +++ b/src/components/Graph/ForceAtlasSeedLayoutSettings.vue @@ -0,0 +1,60 @@ + + + diff --git a/src/components/Graph/GraphEditor.vue b/src/components/Graph/GraphEditor.vue index 9bb183a..f0e32aa 100644 --- a/src/components/Graph/GraphEditor.vue +++ b/src/components/Graph/GraphEditor.vue @@ -171,6 +171,17 @@ /> @@ -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,30 +712,44 @@ export default { } if (layoutType === 'forceAtlas2') { - if ( - !this.graph.someNode( - (nodeKey, attributes) => - typeof attributes.x === 'number' && - typeof attributes.y === 'number' - ) - ) { - circular.assign(this.graph) - } - - this.fa2Layout = markRaw( - new FA2Layout(this.graph, { - getEdgeWeight: (_, attr) => - this.settings.layout.options.weightSource - ? attr.data[this.settings.layout.options.weightSource] - : 1, - settings: this.settings.layout.options - }) - ) + 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' + ) + ) { + if (this.settings.layout.options.initialAlgorithm === 'circular') { + this.applyCircularLayout() + } else { + this.applyRandomLayout() + } + } + + this.fa2Layout = markRaw( + new FA2Layout(this.graph, { + getEdgeWeight: (_, attr) => + this.settings.layout.options.weightSource + ? attr.data[this.settings.layout.options.weightSource] + : 1, + settings: this.settings.layout.options + }) + ) + }, toggleFA2Layout() { if (this.fa2Layout.isRunning()) { this.stopFA2Layout() @@ -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; +} diff --git a/src/components/svg/pause.vue b/src/components/svg/pause.vue new file mode 100644 index 0000000..479721c --- /dev/null +++ b/src/components/svg/pause.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/components/svg/restart.vue b/src/components/svg/restart.vue new file mode 100644 index 0000000..0fc9fda --- /dev/null +++ b/src/components/svg/restart.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/components/svg/stop.vue b/src/components/svg/stop.vue deleted file mode 100644 index 2c2b67c..0000000 --- a/src/components/svg/stop.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/src/lib/graphHelper.js b/src/lib/graphHelper.js index e8cc69b..d70eb3c 100644 --- a/src/lib/graphHelper.js +++ b/src/lib/graphHelper.js @@ -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 || diff --git a/src/lib/storedInquiries/_migrations.js b/src/lib/storedInquiries/_migrations.js index b54f4bf..0ab6a3f 100644 --- a/src/lib/storedInquiries/_migrations.js +++ b/src/lib/storedInquiries/_migrations.js @@ -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 } } diff --git a/src/lib/storedInquiries/index.js b/src/lib/storedInquiries/index.js index bb102a4..508e986 100644 --- a/src/lib/storedInquiries/index.js +++ b/src/lib/storedInquiries/index.js @@ -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 } diff --git a/tests/components/Graph/GraphEditor.spec.js b/tests/components/Graph/GraphEditor.spec.js index fa09d50..b9639d5 100644 --- a/tests/components/Graph/GraphEditor.spec.js +++ b/tests/components/Graph/GraphEditor.spec.js @@ -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() diff --git a/tests/lib/graphHelper.spec.js b/tests/lib/graphHelper.spec.js index ee30788..bf5ed2d 100644 --- a/tests/lib/graphHelper.spec.js +++ b/tests/lib/graphHelper.spec.js @@ -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' } + } + } + ]) + }) }) diff --git a/tests/lib/storedInquiries/_migrations.spec.js b/tests/lib/storedInquiries/_migrations.spec.js index 9a2f53e..507bfe0 100644 --- a/tests/lib/storedInquiries/_migrations.spec.js +++ b/tests/lib/storedInquiries/_migrations.spec.js @@ -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' } diff --git a/tests/lib/storedInquiries/storedInquiries.spec.js b/tests/lib/storedInquiries/storedInquiries.spec.js index 5b3079e..97ffbb1 100644 --- a/tests/lib/storedInquiries/storedInquiries.spec.js +++ b/tests/lib/storedInquiries/storedInquiries.spec.js @@ -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,