1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-06 10:08:52 +08:00

autostart, reset and fixes

This commit is contained in:
lana-k
2025-08-17 21:21:21 +02:00
parent 9d562d11b8
commit 4232f15c04
6 changed files with 303 additions and 173 deletions

View File

@@ -4,7 +4,7 @@
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@*": ["./src/*"]
"@\/*": ["./src/*"]
}
}
}

View File

@@ -0,0 +1,117 @@
<template>
<Field label="Adjust sizes">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.adjustSizes"
@option-change="update('adjustSizes', $event)"
/>
</Field>
<Field label="Barnes-Hut optimize">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.barnesHutOptimize"
@option-change="update('barnesHutOptimize', $event)"
/>
</Field>
<Field v-show="modelValue.barnesHutOptimize" label="Barnes-Hut Theta">
<NumericInput
:value="modelValue.barnesHutTheta"
@update="update('barnesHutTheta', $event)"
/>
</Field>
<Field label="Gravity">
<NumericInput
:value="modelValue.gravity"
@update="update('gravity', $event)"
/>
</Field>
<Field label="Strong gravity mode">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.strongGravityMode"
@option-change="update('strongGravityMode', $event)"
/>
</Field>
<Field label="Noack's LinLog model">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.linLogMode"
@option-change="update('linLogMode', $event)"
/>
</Field>
<Field label="Out bound attraction distribution">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.outboundAttractionDistribution"
@option-change="update('outboundAttractionDistribution', $event)"
/>
</Field>
<Field label="Slow down">
<NumericInput
:value="modelValue.slowDown"
:min="0"
@update="update('slowDown', $event)"
/>
</Field>
<Field label="Edge weight">
<Dropdown
:options="keyOptions"
:value="modelValue.weightSource"
@change="update('weightSource', $event)"
/>
</Field>
<Field v-show="modelValue.weightSource" label="Edge weight influence">
<NumericInput
:value="modelValue.edgeWeightInfluence"
@update="update('edgeWeightInfluence', $event)"
/>
</Field>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import Field from 'react-chart-editor/lib/components/fields/Field'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
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),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Dropdown: applyPureReactInVue(Dropdown),
NumericInput: applyPureReactInVue(NumericInput)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
booleanOptions: markRaw([
{ label: 'Yes', value: true },
{ label: 'No', value: false }
])
}
},
methods: {
update(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>

View File

@@ -1,55 +1,9 @@
<template>
<Field label="Adjust sizes">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.adjustSizes"
@option-change="update('adjustSizes', $event)"
/>
</Field>
<Field label="Barnes-Hut optimize">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.barnesHutOptimize"
@option-change="update('barnesHutOptimize', $event)"
/>
</Field>
<Field v-show="modelValue.barnesHutOptimize" label="Barnes-Hut Theta">
<Field label="Initial iterations">
<NumericInput
:value="modelValue.barnesHutTheta"
@update="update('barnesHutTheta', $event)"
/>
</Field>
<Field label="Gravity">
<NumericInput
:value="modelValue.gravity"
@update="update('gravity', $event)"
/>
</Field>
<Field label="Strong gravity mode">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.strongGravityMode"
@option-change="update('strongGravityMode', $event)"
/>
</Field>
<Field label="Noack's LinLog model">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.linLogMode"
@option-change="update('linLogMode', $event)"
/>
</Field>
<Field label="Out bound attraction distribution">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.outboundAttractionDistribution"
@option-change="update('outboundAttractionDistribution', $event)"
:value="modelValue.initialIterationsAmount"
:min="1"
@update="update('initialIterationsAmount', $event)"
/>
</Field>
@@ -59,45 +13,17 @@
@update="update('scalingRatio', $event)"
/>
</Field>
<Field label="Slow down">
<NumericInput
:value="modelValue.slowDown"
:min="1"
@update="update('slowDown', $event)"
/>
</Field>
<Field label="Edge weight influence">
<NumericInput
:value="modelValue.edgeWeightInfluence"
@update="update('edgeWeightInfluence', $event)"
/>
</Field>
<Field label="Edge weight">
<Dropdown
:options="keyOptions"
:value="modelValue.weightSource"
@change="update('weightSource', $event)"
/>
</Field>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import Field from 'react-chart-editor/lib/components/fields/Field'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
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),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Dropdown: applyPureReactInVue(Dropdown),
NumericInput: applyPureReactInVue(NumericInput)
},
props: {
@@ -105,14 +31,6 @@ export default {
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
booleanOptions: markRaw([
{ label: 'Yes', value: true },
{ label: 'No', value: false }
])
}
},
methods: {
update(attributeName, value) {
this.$emit('update:modelValue', {

View File

@@ -13,7 +13,6 @@ export function buildNodes(graph, dataSources, options) {
const nodes = dataSources[docColumn]
.map(json => JSON.parse(json))
.filter(item => item[objectType] === TYPE_NODE)
nodes.forEach(node => {
graph.addNode(node[nodeId], {
data: node,
@@ -89,8 +88,8 @@ export function updateEdges(graph, attributeUpdates) {
}
graph.forEachEdge((edgeId, attributes, source, target) => {
graph.updateEdge(source, target, attributes => {
const newAttributes = { ...attributes }
graph.updateEdgeWithKey(edgeId, source, target, attr => {
const newAttributes = { ...attr }
changeMethods.forEach(method => method(newAttributes, edgeId))
return newAttributes
})

View File

@@ -133,8 +133,19 @@
:keyOptions="keysOptions"
@update:model-value="updateLayout(settings.layout.type)"
/>
<Field v-if="settings.layout.type === 'forceAtlas2'">
</Fold>
<template v-if="settings.layout.type === 'forceAtlas2'">
<Fold name="Advanced layout settings">
<AdvancedForceAtlasLayoutSettings
v-model="settings.layout.options"
:keyOptions="keysOptions"
@update:model-value="updateLayout(settings.layout.type)"
/>
</Fold>
<div class="force-atlas-buttons">
<Button variant="secondary" @click="resetFA2LayoutSettings">
Reset
</Button>
<Button variant="primary" @click="toggleFA2Layout">
<template #node:icon>
<div
@@ -143,12 +154,13 @@
}"
>
<RunIcon v-if="!fa2Running" />
<StopIcon v-else /></div
></template>
<StopIcon v-else />
</div>
</template>
{{ fa2Running ? 'Stop' : 'Start' }}
</Button>
</Field>
</Fold>
</div>
</template>
</Panel>
</PanelMenuWrapper>
</GraphEditorControls>
@@ -176,6 +188,7 @@ 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 AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue'
import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
import FA2Layout from 'graphology-layout-forceatlas2/worker'
import forceAtlas2 from 'graphology-layout-forceatlas2'
@@ -216,7 +229,8 @@ export default {
NodeColorSettings,
NodeSizeSettings,
EdgeSizeSettings,
EdgeColorSettings
EdgeColorSettings,
AdvancedForceAtlasLayoutSettings
},
inject: ['tabLayout'],
props: {
@@ -226,10 +240,11 @@ export default {
emits: ['update'],
data() {
return {
graph: new Graph(),
graph: new Graph({ multi: true, allowSelfLoops: true }),
renderer: null,
fa2Layout: null,
fa2Running: false,
checkIteration: null,
visibilityOptions: markRaw([
{ label: 'Show', value: true },
{ label: 'Hide', value: false }
@@ -246,7 +261,9 @@ export default {
forceAtlas2: ForceAtlasLayoutSettings
}),
settings: this.initOptions || {
settings: this.initOptions
? JSON.parse(JSON.stringify(this.initOptions))
: {
structure: {
nodeId: null,
objectType: null,
@@ -370,13 +387,16 @@ export default {
updateNodes(this.graph, this.settings.style.nodes)
updateEdges(this.graph, this.settings.style.edges)
circular.assign(this.graph)
this.updateLayout(this.settings.layout.type)
this.renderer = new Sigma(this.graph, this.$refs.graph, {
renderEdgeLabels: true,
allowInvalidContainer: true,
labelColor: { attribute: 'labelColor', color: '#444444' },
edgeLabelColor: { attribute: 'labelColor', color: '#a2b1c6' }
})
if (this.settings.layout.type === 'forceAtlas2') {
this.autoRunFA2Layout()
}
},
updateStructure(attributeName, value) {
this.settings.structure[attributeName] = value
@@ -410,41 +430,33 @@ export default {
})
},
updateLayout(layoutType) {
if (layoutType !== this.settings.layout.type) {
const prevLayout = this.settings.layout.type
// Change layout type? - restore layout settings or set default settings
if (layoutType !== prevLayout) {
this.layoutOptionsArchive[prevLayout] = this.settings.layout.options
this.settings.layout.options = this.layoutOptionsArchive[layoutType]
if (layoutType === 'forceAtlas2' && !this.settings.layout.options) {
const sensibleSettings = forceAtlas2.inferSettings(this.graph)
this.settings.layout.options = {
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 1,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false,
...sensibleSettings
}
} else if (layoutType === 'random' && !this.settings.layout.options) {
if (!this.settings.layout.options) {
if (layoutType === 'forceAtlas2') {
this.setRecommendedFA2Settings()
} else if (['random', 'circlepack'].includes(layoutType)) {
this.settings.layout.options = {
seedValue: 1
}
} else if (
layoutType === 'circlepack' &&
!this.settings.layout.options
) {
this.settings.layout.options = {
seedValue: 1
}
}
this.settings.layout.type = layoutType
}
// In any case kill FA2 if it exists
if (this.fa2Layout) {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
}
this.fa2Layout.kill()
}
if (layoutType === 'circular') {
circular.assign(this.graph)
return
@@ -491,27 +503,99 @@ export default {
}
if (layoutType === 'forceAtlas2') {
if (this.fa2Layout) {
this.fa2Layout.kill()
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) =>
attr.data[this.settings.layout.options.weightSource || 'weight'],
this.settings.layout.options.weightSource
? attr.data[this.settings.layout.options.weightSource]
: 1,
settings: this.settings.layout.options
})
)
if (layoutType !== prevLayout) {
this.autoRunFA2Layout()
}
}
},
toggleFA2Layout() {
if (this.fa2Layout.isRunning()) {
this.fa2Running = false
this.fa2Layout.stop()
this.stopFA2Layout()
} else {
this.fa2Running = true
this.fa2Layout.start()
}
},
stopFA2Layout() {
this.fa2Running = false
this.fa2Layout.stop()
if (this.checkIteration) {
this.fa2Layout.worker.removeEventListener(
'message',
this.checkIteration
)
this.checkIteration = null
}
},
autoRunFA2Layout() {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
}
let iteration = 1
this.checkIteration = () => {
if (
iteration === this.settings.layout.options.initialIterationsAmount
) {
this.stopFA2Layout()
}
iteration++
}
this.fa2Layout.worker.addEventListener('message', this.checkIteration)
this.fa2Running = true
this.fa2Layout.start()
},
setRecommendedFA2Settings() {
const sensibleSettings = forceAtlas2.inferSettings(this.graph)
this.settings.layout.options = {
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false,
...sensibleSettings
}
if (
[Infinity, -Infinity].includes(this.settings.layout.options.slowDown)
) {
this.settings.layout.options.slowDown = 1
}
},
resetFA2LayoutSettings() {
if (this.initOptions?.layout.type === 'forceAtlas2') {
this.settings.layout = JSON.parse(
JSON.stringify(this.initOptions.layout)
)
} else {
this.setRecommendedFA2Settings()
}
this.updateLayout(this.settings.layout.type)
},
saveAsPng() {
return downloadAsPNG(this.renderer, {
backgroundColor: this.settings.style.backgroundColor
@@ -534,4 +618,14 @@ export default {
:deep(.customPickerContainer) {
float: right;
}
.force-atlas-buttons {
display: flex;
width: 100%;
gap: 16px;
}
.force-atlas-buttons :deep(button) {
flex-grow: 1;
flex-basis: 0;
}
</style>

View File

@@ -71,11 +71,13 @@ export default {
},
async handleResize() {
const renderer = this.$refs.graphEditor.renderer
if (renderer) {
renderer.refresh()
renderer.getCamera().setState({ x: 0.5, y: 0.5 })
}
}
}
}
</script>
<style scoped>