1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-06 18:18:53 +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": { "compilerOptions": {
"baseUrl": "./", "baseUrl": "./",
"paths": { "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> <template>
<Field label="Adjust sizes"> <Field label="Initial iterations">
<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 <NumericInput
:value="modelValue.barnesHutTheta" :value="modelValue.initialIterationsAmount"
@update="update('barnesHutTheta', $event)" :min="1"
/> @update="update('initialIterationsAmount', $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>
@@ -59,45 +13,17 @@
@update="update('scalingRatio', $event)" @update="update('scalingRatio', $event)"
/> />
</Field> </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> </template>
<script> <script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury' import { applyPureReactInVue } from 'veaury'
import Field from 'react-chart-editor/lib/components/fields/Field' 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 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 'react-chart-editor/lib/react-chart-editor.css'
export default { export default {
components: { components: {
Field: applyPureReactInVue(Field), Field: applyPureReactInVue(Field),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Dropdown: applyPureReactInVue(Dropdown),
NumericInput: applyPureReactInVue(NumericInput) NumericInput: applyPureReactInVue(NumericInput)
}, },
props: { props: {
@@ -105,14 +31,6 @@ export default {
keyOptions: Array keyOptions: Array
}, },
emits: ['update:modelValue'], emits: ['update:modelValue'],
data() {
return {
booleanOptions: markRaw([
{ label: 'Yes', value: true },
{ label: 'No', value: false }
])
}
},
methods: { methods: {
update(attributeName, value) { update(attributeName, value) {
this.$emit('update:modelValue', { this.$emit('update:modelValue', {

View File

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

View File

@@ -133,8 +133,19 @@
:keyOptions="keysOptions" :keyOptions="keysOptions"
@update:model-value="updateLayout(settings.layout.type)" @update:model-value="updateLayout(settings.layout.type)"
/> />
</Fold>
<Field v-if="settings.layout.type === 'forceAtlas2'"> <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"> <Button variant="primary" @click="toggleFA2Layout">
<template #node:icon> <template #node:icon>
<div <div
@@ -143,12 +154,13 @@
}" }"
> >
<RunIcon v-if="!fa2Running" /> <RunIcon v-if="!fa2Running" />
<StopIcon v-else /></div <StopIcon v-else />
></template> </div>
</template>
{{ fa2Running ? 'Stop' : 'Start' }} {{ fa2Running ? 'Stop' : 'Start' }}
</Button> </Button>
</Field> </div>
</Fold> </template>
</Panel> </Panel>
</PanelMenuWrapper> </PanelMenuWrapper>
</GraphEditorControls> </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 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 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 forceAtlas2 from 'graphology-layout-forceatlas2' import forceAtlas2 from 'graphology-layout-forceatlas2'
@@ -216,7 +229,8 @@ export default {
NodeColorSettings, NodeColorSettings,
NodeSizeSettings, NodeSizeSettings,
EdgeSizeSettings, EdgeSizeSettings,
EdgeColorSettings EdgeColorSettings,
AdvancedForceAtlasLayoutSettings
}, },
inject: ['tabLayout'], inject: ['tabLayout'],
props: { props: {
@@ -226,10 +240,11 @@ export default {
emits: ['update'], emits: ['update'],
data() { data() {
return { return {
graph: new Graph(), graph: new Graph({ multi: true, allowSelfLoops: true }),
renderer: null, renderer: null,
fa2Layout: null, fa2Layout: null,
fa2Running: false, fa2Running: false,
checkIteration: null,
visibilityOptions: markRaw([ visibilityOptions: markRaw([
{ label: 'Show', value: true }, { label: 'Show', value: true },
{ label: 'Hide', value: false } { label: 'Hide', value: false }
@@ -246,7 +261,9 @@ export default {
forceAtlas2: ForceAtlasLayoutSettings forceAtlas2: ForceAtlasLayoutSettings
}), }),
settings: this.initOptions || { settings: this.initOptions
? JSON.parse(JSON.stringify(this.initOptions))
: {
structure: { structure: {
nodeId: null, nodeId: null,
objectType: null, objectType: null,
@@ -370,13 +387,16 @@ export default {
updateNodes(this.graph, this.settings.style.nodes) updateNodes(this.graph, this.settings.style.nodes)
updateEdges(this.graph, this.settings.style.edges) 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, { this.renderer = new Sigma(this.graph, this.$refs.graph, {
renderEdgeLabels: true, renderEdgeLabels: true,
allowInvalidContainer: true, allowInvalidContainer: true,
labelColor: { attribute: 'labelColor', color: '#444444' }, labelColor: { attribute: 'labelColor', color: '#444444' },
edgeLabelColor: { attribute: 'labelColor', color: '#a2b1c6' } edgeLabelColor: { attribute: 'labelColor', color: '#a2b1c6' }
}) })
if (this.settings.layout.type === 'forceAtlas2') {
this.autoRunFA2Layout()
}
}, },
updateStructure(attributeName, value) { updateStructure(attributeName, value) {
this.settings.structure[attributeName] = value this.settings.structure[attributeName] = value
@@ -410,41 +430,33 @@ export default {
}) })
}, },
updateLayout(layoutType) { updateLayout(layoutType) {
if (layoutType !== this.settings.layout.type) {
const prevLayout = 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.layoutOptionsArchive[prevLayout] = this.settings.layout.options
this.settings.layout.options = this.layoutOptionsArchive[layoutType] this.settings.layout.options = this.layoutOptionsArchive[layoutType]
if (layoutType === 'forceAtlas2' && !this.settings.layout.options) { if (!this.settings.layout.options) {
const sensibleSettings = forceAtlas2.inferSettings(this.graph) if (layoutType === 'forceAtlas2') {
this.settings.layout.options = { this.setRecommendedFA2Settings()
adjustSizes: false, } else if (['random', 'circlepack'].includes(layoutType)) {
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) {
this.settings.layout.options = { this.settings.layout.options = {
seedValue: 1 seedValue: 1
} }
} else if (
layoutType === 'circlepack' &&
!this.settings.layout.options
) {
this.settings.layout.options = {
seedValue: 1
} }
} }
this.settings.layout.type = layoutType 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') { if (layoutType === 'circular') {
circular.assign(this.graph) circular.assign(this.graph)
return return
@@ -491,27 +503,99 @@ export default {
} }
if (layoutType === 'forceAtlas2') { if (layoutType === 'forceAtlas2') {
if (this.fa2Layout) { if (
this.fa2Layout.kill() !this.graph.someNode(
(nodeKey, attributes) =>
typeof attributes.x === 'number' &&
typeof attributes.y === 'number'
)
) {
circular.assign(this.graph)
} }
this.fa2Layout = markRaw( this.fa2Layout = markRaw(
new FA2Layout(this.graph, { new FA2Layout(this.graph, {
getEdgeWeight: (_, attr) => 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 settings: this.settings.layout.options
}) })
) )
if (layoutType !== prevLayout) {
this.autoRunFA2Layout()
}
} }
}, },
toggleFA2Layout() { toggleFA2Layout() {
if (this.fa2Layout.isRunning()) { if (this.fa2Layout.isRunning()) {
this.fa2Running = false this.stopFA2Layout()
this.fa2Layout.stop()
} else { } else {
this.fa2Running = true this.fa2Running = true
this.fa2Layout.start() 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() { saveAsPng() {
return downloadAsPNG(this.renderer, { return downloadAsPNG(this.renderer, {
backgroundColor: this.settings.style.backgroundColor backgroundColor: this.settings.style.backgroundColor
@@ -534,4 +618,14 @@ export default {
:deep(.customPickerContainer) { :deep(.customPickerContainer) {
float: right; float: right;
} }
.force-atlas-buttons {
display: flex;
width: 100%;
gap: 16px;
}
.force-atlas-buttons :deep(button) {
flex-grow: 1;
flex-basis: 0;
}
</style> </style>

View File

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