mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-07 02:28:54 +08:00
Compare commits
3 Commits
1601514cca
...
4232f15c04
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4232f15c04 | ||
|
|
9d562d11b8 | ||
|
|
54cdbbc8b9 |
@@ -4,7 +4,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@*": ["./src/*"]
|
"@\/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
543
package-lock.json
generated
543
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@
|
|||||||
"format": "prettier . --write"
|
"format": "prettier . --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@sigma/export-image": "^3.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"codemirror": "^5.65.18",
|
"codemirror": "^5.65.18",
|
||||||
"codemirror-editor-vue3": "^2.8.0",
|
"codemirror-editor-vue3": "^2.8.0",
|
||||||
|
|||||||
117
src/components/Graph/AdvancedForceAtlasLayoutSettings.vue
Normal file
117
src/components/Graph/AdvancedForceAtlasLayoutSettings.vue
Normal 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>
|
||||||
137
src/components/Graph/EdgeColorSettings.vue
Normal file
137
src/components/Graph/EdgeColorSettings.vue
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<Field label="Color">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="edgeColorTypeOptions"
|
||||||
|
:activeOption="modelValue.type"
|
||||||
|
@option-change="updateColorType"
|
||||||
|
/>
|
||||||
|
<Field v-if="modelValue.type === 'constant'">
|
||||||
|
<ColorPicker
|
||||||
|
:selectedColor="modelValue.value"
|
||||||
|
@color-change="updateSettings('value', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<template v-else>
|
||||||
|
<Field>
|
||||||
|
<Dropdown
|
||||||
|
v-if="modelValue.type === 'variable'"
|
||||||
|
:options="keyOptions"
|
||||||
|
:value="modelValue.source"
|
||||||
|
@change="updateSettings('source', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="colorSourceUsageOptions"
|
||||||
|
:activeOption="modelValue.sourceUsage"
|
||||||
|
@option-change="updateSettings('sourceUsage', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field v-if="modelValue.sourceUsage === 'map_to'">
|
||||||
|
<ColorscalePicker
|
||||||
|
:selected="modelValue.colorscale"
|
||||||
|
className="colorscale-picker"
|
||||||
|
@colorscale-change="updateSettings('colorscale', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field v-if="modelValue.type !== 'constant'" label="Color as">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="сolorAsOptions"
|
||||||
|
:activeOption="modelValue.mode"
|
||||||
|
@option-change="updateSettings('mode', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field v-if="modelValue.type !== 'constant'" label="Colorscale direction">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="сolorscaleDirections"
|
||||||
|
:activeOption="modelValue.colorscaleDirection"
|
||||||
|
@option-change="updateSettings('colorscaleDirection', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
||||||
|
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||||
|
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
|
||||||
|
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
|
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
||||||
|
Field: applyPureReactInVue(Field),
|
||||||
|
ColorscalePicker: applyPureReactInVue(ColorscalePicker),
|
||||||
|
ColorPicker: applyPureReactInVue(ColorPicker)
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
keyOptions: Array
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
edgeColorTypeOptions: markRaw([
|
||||||
|
{ label: 'Constant', value: 'constant' },
|
||||||
|
{ label: 'Variable', value: 'variable' }
|
||||||
|
]),
|
||||||
|
сolorAsOptions: markRaw([
|
||||||
|
{ label: 'Continious', value: 'continious' },
|
||||||
|
{ label: 'Categorical', value: 'categorical' }
|
||||||
|
]),
|
||||||
|
сolorscaleDirections: markRaw([
|
||||||
|
{ label: 'Normal', value: 'normal' },
|
||||||
|
{ label: 'Recersed', value: 'reversed' }
|
||||||
|
]),
|
||||||
|
colorSourceUsageOptions: markRaw([
|
||||||
|
{ label: 'Direct', value: 'direct' },
|
||||||
|
{ label: 'Map to', value: 'map_to' }
|
||||||
|
]),
|
||||||
|
defaultColorSettings: {
|
||||||
|
constant: { value: '#1F77B4' },
|
||||||
|
variable: {
|
||||||
|
source: null,
|
||||||
|
sourceUsage: 'map_to',
|
||||||
|
colorscale: null,
|
||||||
|
mode: 'categorical',
|
||||||
|
colorscaleDirection: 'normal'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateColorType(newColorType) {
|
||||||
|
const currentColorType = this.modelValue.type
|
||||||
|
this.defaultColorSettings[currentColorType] = this.modelValue
|
||||||
|
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
type: newColorType,
|
||||||
|
...this.defaultColorSettings[newColorType]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateSettings(attributeName, value) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
[attributeName]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.customPickerContainer) {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
93
src/components/Graph/EdgeSizeSettings.vue
Normal file
93
src/components/Graph/EdgeSizeSettings.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<Field label="Size">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="edgeSizeTypeOptions"
|
||||||
|
:activeOption="modelValue.type"
|
||||||
|
@option-change="updateSizeType"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<NumericInput
|
||||||
|
v-if="modelValue.type === 'constant'"
|
||||||
|
:value="modelValue.value"
|
||||||
|
:min="1"
|
||||||
|
@update="updateSettings('value', $event)"
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
v-if="modelValue.type === 'variable'"
|
||||||
|
:options="keyOptions"
|
||||||
|
:value="modelValue.source"
|
||||||
|
@change="updateSettings('source', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<template v-if="modelValue.type !== 'constant'">
|
||||||
|
<Field label="Size scale">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.scale"
|
||||||
|
@update="updateSettings('scale', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Minimum size">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.min"
|
||||||
|
@update="updateSettings('min', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
|
||||||
|
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
||||||
|
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
|
NumericInput: applyPureReactInVue(NumericInput),
|
||||||
|
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
||||||
|
Field: applyPureReactInVue(Field)
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
keyOptions: Array
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
edgeSizeTypeOptions: markRaw([
|
||||||
|
{ label: 'Constant', value: 'constant' },
|
||||||
|
{ label: 'Variable', value: 'variable' }
|
||||||
|
]),
|
||||||
|
defaultSizeSettings: {
|
||||||
|
constant: { value: 4 },
|
||||||
|
variable: { source: null, scale: 1, min: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateSizeType(newSizeType) {
|
||||||
|
const currentSizeType = this.modelValue.type
|
||||||
|
this.defaultSizeSettings[currentSizeType] = this.modelValue
|
||||||
|
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
type: newSizeType,
|
||||||
|
...this.defaultSizeSettings[newSizeType]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateSettings(attributeName, value) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
[attributeName]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -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', {
|
||||||
|
|||||||
155
src/components/Graph/NodeColorSettings.vue
Normal file
155
src/components/Graph/NodeColorSettings.vue
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<Field label="Color">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="nodeColorTypeOptions"
|
||||||
|
:activeOption="modelValue.type"
|
||||||
|
@option-change="updateColorType"
|
||||||
|
/>
|
||||||
|
<Field v-if="modelValue.type === 'constant'">
|
||||||
|
<ColorPicker
|
||||||
|
:selectedColor="modelValue.value"
|
||||||
|
@color-change="updateSettings('value', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<template v-else>
|
||||||
|
<Field>
|
||||||
|
<Dropdown
|
||||||
|
v-if="modelValue.type === 'variable'"
|
||||||
|
:options="keyOptions"
|
||||||
|
:value="modelValue.source"
|
||||||
|
@change="updateSettings('source', $event)"
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
v-if="modelValue.type === 'calculated'"
|
||||||
|
:options="nodeCalculatedColorMethodOptions"
|
||||||
|
:value="modelValue.method"
|
||||||
|
@change="updateSettings('method', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="colorSourceUsageOptions"
|
||||||
|
:activeOption="modelValue.sourceUsage"
|
||||||
|
@option-change="updateSettings('sourceUsage', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field v-if="modelValue.sourceUsage === 'map_to'">
|
||||||
|
<ColorscalePicker
|
||||||
|
:selected="modelValue.colorscale"
|
||||||
|
className="colorscale-picker"
|
||||||
|
@colorscale-change="updateSettings('colorscale', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field v-if="modelValue.type !== 'constant'" label="Color as">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="сolorAsOptions"
|
||||||
|
:activeOption="modelValue.mode"
|
||||||
|
@option-change="updateSettings('mode', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field v-if="modelValue.type !== 'constant'" label="Colorscale direction">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="сolorscaleDirections"
|
||||||
|
:activeOption="modelValue.colorscaleDirection"
|
||||||
|
@option-change="updateSettings('colorscaleDirection', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
||||||
|
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||||
|
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
|
||||||
|
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
|
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
||||||
|
Field: applyPureReactInVue(Field),
|
||||||
|
ColorscalePicker: applyPureReactInVue(ColorscalePicker),
|
||||||
|
ColorPicker: applyPureReactInVue(ColorPicker)
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
keyOptions: Array
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
nodeColorTypeOptions: markRaw([
|
||||||
|
{ label: 'Constant', value: 'constant' },
|
||||||
|
{ label: 'Variable', value: 'variable' },
|
||||||
|
{ label: 'Calculated', value: 'calculated' }
|
||||||
|
]),
|
||||||
|
nodeCalculatedColorMethodOptions: markRaw([
|
||||||
|
{ label: 'Degree', value: 'degree' },
|
||||||
|
{ label: 'In degree', value: 'inDegree' },
|
||||||
|
{ label: 'Out degree', value: 'outDegree' }
|
||||||
|
]),
|
||||||
|
сolorAsOptions: markRaw([
|
||||||
|
{ label: 'Continious', value: 'continious' },
|
||||||
|
{ label: 'Categorical', value: 'categorical' }
|
||||||
|
]),
|
||||||
|
сolorscaleDirections: markRaw([
|
||||||
|
{ label: 'Normal', value: 'normal' },
|
||||||
|
{ label: 'Recersed', value: 'reversed' }
|
||||||
|
]),
|
||||||
|
colorSourceUsageOptions: markRaw([
|
||||||
|
{ label: 'Direct', value: 'direct' },
|
||||||
|
{ label: 'Map to', value: 'map_to' }
|
||||||
|
]),
|
||||||
|
defaultColorSettings: {
|
||||||
|
constant: { value: '#1F77B4' },
|
||||||
|
variable: {
|
||||||
|
source: null,
|
||||||
|
sourceUsage: 'map_to',
|
||||||
|
colorscale: null,
|
||||||
|
mode: 'categorical',
|
||||||
|
colorscaleDirection: 'normal'
|
||||||
|
},
|
||||||
|
calculated: {
|
||||||
|
method: 'degree',
|
||||||
|
sourceUsage: 'map_to',
|
||||||
|
colorscale: null,
|
||||||
|
mode: 'continious',
|
||||||
|
colorscaleDirection: 'normal'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateColorType(newColorType) {
|
||||||
|
const currentColorType = this.modelValue.type
|
||||||
|
this.defaultColorSettings[currentColorType] = this.modelValue
|
||||||
|
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
type: newColorType,
|
||||||
|
...this.defaultColorSettings[newColorType]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateSettings(attributeName, value) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
[attributeName]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.customPickerContainer) {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
118
src/components/Graph/NodeSizeSettings.vue
Normal file
118
src/components/Graph/NodeSizeSettings.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<Field label="Size">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="nodeSizeTypeOptions"
|
||||||
|
:activeOption="modelValue.type"
|
||||||
|
@option-change="updateSizeType"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<NumericInput
|
||||||
|
v-if="modelValue.type === 'constant'"
|
||||||
|
:value="modelValue.value"
|
||||||
|
:min="1"
|
||||||
|
@update="updateSettings('value', $event)"
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
v-if="modelValue.type === 'variable'"
|
||||||
|
:options="keyOptions"
|
||||||
|
:value="modelValue.source"
|
||||||
|
@change="updateSettings('source', $event)"
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
v-if="modelValue.type === 'calculated'"
|
||||||
|
:options="nodeCalculatedSizeMethodOptions"
|
||||||
|
:value="modelValue.method"
|
||||||
|
@change="updateSettings('method', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<template v-if="modelValue.type !== 'constant'">
|
||||||
|
<Field label="Size scale">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.scale"
|
||||||
|
@update="updateSettings('scale', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Size mode">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="nodeSizeModeOptions"
|
||||||
|
:activeOption="modelValue.mode"
|
||||||
|
@option-change="updateSettings('mode', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Minimum size">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.min"
|
||||||
|
@update="updateSettings('min', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
|
||||||
|
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
||||||
|
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
|
NumericInput: applyPureReactInVue(NumericInput),
|
||||||
|
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
||||||
|
Field: applyPureReactInVue(Field)
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
keyOptions: Array
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
nodeSizeTypeOptions: markRaw([
|
||||||
|
{ label: 'Constant', value: 'constant' },
|
||||||
|
{ label: 'Variable', value: 'variable' },
|
||||||
|
{ label: 'Calculated', value: 'calculated' }
|
||||||
|
]),
|
||||||
|
nodeCalculatedSizeMethodOptions: markRaw([
|
||||||
|
{ label: 'Degree', value: 'degree' },
|
||||||
|
{ label: 'In degree', value: 'inDegree' },
|
||||||
|
{ label: 'Out degree', value: 'outDegree' }
|
||||||
|
]),
|
||||||
|
nodeSizeModeOptions: markRaw([
|
||||||
|
{ label: 'Area', value: 'area' },
|
||||||
|
{ label: 'Diameter', value: 'diameter' }
|
||||||
|
]),
|
||||||
|
defaultSizeSettings: {
|
||||||
|
constant: { value: 4 },
|
||||||
|
variable: { source: null, scale: 1, mode: 'diameter', min: 1 },
|
||||||
|
calculated: { method: 'degree', scale: 1, mode: 'diameter', min: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateSizeType(newSizeType) {
|
||||||
|
const currentSizeType = this.modelValue.type
|
||||||
|
this.defaultSizeSettings[currentSizeType] = this.modelValue
|
||||||
|
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
type: newSizeType,
|
||||||
|
...this.defaultSizeSettings[newSizeType]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateSettings(attributeName, value) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
[attributeName]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { nanoid } from 'nanoid'
|
|
||||||
import { COLOR_PICKER_CONSTANTS } from 'react-colorscales'
|
import { COLOR_PICKER_CONSTANTS } from 'react-colorscales'
|
||||||
import tinycolor from 'tinycolor2'
|
import tinycolor from 'tinycolor2'
|
||||||
|
|
||||||
@@ -14,10 +13,10 @@ 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,
|
||||||
|
labelColor: options.style.nodes.label.color
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -37,7 +36,8 @@ export function buildEdges(graph, dataSources, options) {
|
|||||||
const target = edge[edgeTarget]
|
const target = edge[edgeTarget]
|
||||||
if (graph.hasNode(source) && graph.hasNode(target)) {
|
if (graph.hasNode(source) && graph.hasNode(target)) {
|
||||||
graph.addEdge(source, target, {
|
graph.addEdge(source, target, {
|
||||||
data: edge
|
data: edge,
|
||||||
|
labelColor: options.style.edges.label.color
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -88,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
|
||||||
})
|
})
|
||||||
@@ -97,10 +97,11 @@ export function updateEdges(graph, attributeUpdates) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getUpdateLabelMethod(labelSettings) {
|
function getUpdateLabelMethod(labelSettings) {
|
||||||
const { source } = labelSettings
|
const { source, color } = labelSettings
|
||||||
return attributes => {
|
return attributes => {
|
||||||
const label = attributes.data[source] ?? ''
|
const label = attributes.data[source] ?? ''
|
||||||
attributes.label = label.toString()
|
attributes.label = label.toString()
|
||||||
|
attributes.labelColor = color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,64 +302,6 @@ export function getOptionsFromDataSources(dataSources) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOptionsForSave(state, dataSources) {
|
|
||||||
// we don't need to save the data, only settings
|
|
||||||
// so we modify state.data using dereference
|
|
||||||
const stateCopy = JSON.parse(JSON.stringify(state))
|
|
||||||
const emptySources = {}
|
|
||||||
for (const key in dataSources) {
|
|
||||||
emptySources[key] = []
|
|
||||||
}
|
|
||||||
dereference.default(stateCopy.data, emptySources)
|
|
||||||
return stateCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getImageDataUrl(element, type) {
|
|
||||||
const chartElement = element.querySelector('.js-plotly-plot')
|
|
||||||
return await plotly.toImage(chartElement, {
|
|
||||||
format: type,
|
|
||||||
width: null,
|
|
||||||
height: null
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getChartData(element) {
|
|
||||||
const chartElement = element.querySelector('.js-plotly-plot')
|
|
||||||
return {
|
|
||||||
data: chartElement.data,
|
|
||||||
layout: chartElement.layout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getHtml(options) {
|
|
||||||
const chartId = nanoid()
|
|
||||||
return `
|
|
||||||
<script src="https://cdn.plot.ly/plotly-latest.js" charset="UTF-8"></script>
|
|
||||||
<div id="${chartId}"></div>
|
|
||||||
<script>
|
|
||||||
const el = document.getElementById("${chartId}")
|
|
||||||
|
|
||||||
let timeout
|
|
||||||
function debounceResize() {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
var r = el.getBoundingClientRect()
|
|
||||||
Plotly.relayout(el, {width: r.width, height: r.height})
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(debounceResize)
|
|
||||||
resizeObserver.observe(el)
|
|
||||||
|
|
||||||
Plotly.newPlot(el, ${JSON.stringify(options.data)}, ${JSON.stringify(options.layout)})
|
|
||||||
</script>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getOptionsFromDataSources,
|
getOptionsFromDataSources
|
||||||
getOptionsForSave,
|
|
||||||
getImageDataUrl,
|
|
||||||
getHtml,
|
|
||||||
getChartData
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,11 +45,16 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
dataSources: Object,
|
dataSources: Object,
|
||||||
initOptions: Object,
|
initOptions: Object,
|
||||||
importToPngEnabled: Boolean,
|
exportToPngEnabled: Boolean,
|
||||||
importToSvgEnabled: Boolean,
|
exportToSvgEnabled: Boolean,
|
||||||
forPivot: Boolean
|
forPivot: Boolean
|
||||||
},
|
},
|
||||||
emits: ['update:importToSvgEnabled', 'update', 'loadingImageCompleted'],
|
emits: [
|
||||||
|
'update:exportToSvgEnabled',
|
||||||
|
'update:exportToHtmlEnabled',
|
||||||
|
'update',
|
||||||
|
'loadingImageCompleted'
|
||||||
|
],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
plotly,
|
plotly,
|
||||||
@@ -102,7 +107,8 @@ export default {
|
|||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
this.$emit('update:importToSvgEnabled', true)
|
this.$emit('update:exportToSvgEnabled', true)
|
||||||
|
this.$emit('update:exportToHtmlEnabled', true)
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||||
|
|||||||
@@ -39,6 +39,16 @@
|
|||||||
</Field>
|
</Field>
|
||||||
</Fold>
|
</Fold>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
<Panel group="Style" name="General">
|
||||||
|
<Fold name="General">
|
||||||
|
<Field label="Background color">
|
||||||
|
<ColorPicker
|
||||||
|
:selectedColor="settings.style.backgroundColor"
|
||||||
|
@color-change="settings.style.backgroundColor = $event"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Fold>
|
||||||
|
</Panel>
|
||||||
<Panel group="Style" name="Nodes">
|
<Panel group="Style" name="Nodes">
|
||||||
<Fold name="Nodes">
|
<Fold name="Nodes">
|
||||||
<Field label="Label">
|
<Field label="Label">
|
||||||
@@ -49,130 +59,23 @@
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Size">
|
<Field label="Label color">
|
||||||
<RadioBlocks
|
<ColorPicker
|
||||||
:options="nodeSizeTypeOptions"
|
:selectedColor="settings.style.nodes.label.color"
|
||||||
:activeOption="settings.style.nodes.size.type"
|
@color-change="updateNodes('label.color', $event)"
|
||||||
@option-change="updateNodes('size.type', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field>
|
|
||||||
<NumericInput
|
|
||||||
v-if="settings.style.nodes.size.type === 'constant'"
|
|
||||||
:value="settings.style.nodes.size.value"
|
|
||||||
:min="1"
|
|
||||||
@update="updateNodes('size.value', $event)"
|
|
||||||
/>
|
|
||||||
<Dropdown
|
|
||||||
v-if="settings.style.nodes.size.type === 'variable'"
|
|
||||||
:options="keysOptions"
|
|
||||||
:value="settings.style.nodes.size.source"
|
|
||||||
@change="updateNodes('size.source', $event)"
|
|
||||||
/>
|
|
||||||
<Dropdown
|
|
||||||
v-if="settings.style.nodes.size.type === 'calculated'"
|
|
||||||
:options="nodeCalculatedSizeMethodOptions"
|
|
||||||
:value="settings.style.nodes.size.method"
|
|
||||||
@change="updateNodes('size.method', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<template v-if="settings.style.nodes.size.type !== 'constant'">
|
|
||||||
<Field label="Size scale">
|
|
||||||
<NumericInput
|
|
||||||
:value="settings.style.nodes.size.scale"
|
|
||||||
@update="updateNodes('size.scale', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Size mode">
|
|
||||||
<RadioBlocks
|
|
||||||
:options="nodeSizeModeOptions"
|
|
||||||
:activeOption="settings.style.nodes.size.mode"
|
|
||||||
@option-change="updateNodes('size.mode', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Minimum size">
|
|
||||||
<NumericInput
|
|
||||||
:value="settings.style.nodes.size.min"
|
|
||||||
@update="updateNodes('size.min', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<Field label="Color">
|
|
||||||
<RadioBlocks
|
|
||||||
:options="nodeColorTypeOptions"
|
|
||||||
:activeOption="settings.style.nodes.color.type"
|
|
||||||
@option-change="updateNodes('color.type', $event)"
|
|
||||||
/>
|
|
||||||
<Field v-if="settings.style.nodes.color.type === 'constant'">
|
|
||||||
<ColorPicker
|
|
||||||
:selectedColor="settings.style.nodes.color.value"
|
|
||||||
@color-change="updateNodes('color.value', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<template v-else>
|
|
||||||
<Field>
|
|
||||||
<Dropdown
|
|
||||||
v-if="settings.style.nodes.color.type === 'variable'"
|
|
||||||
:options="keysOptions"
|
|
||||||
:value="settings.style.nodes.color.source"
|
|
||||||
@change="updateNodes('color.source', $event)"
|
|
||||||
/>
|
|
||||||
<Dropdown
|
|
||||||
v-if="settings.style.nodes.color.type === 'calculated'"
|
|
||||||
:options="nodeCalculatedColorMethodOptions"
|
|
||||||
:value="settings.style.nodes.color.method"
|
|
||||||
@change="updateNodes('color.method', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="colorSourceUsageOptions"
|
|
||||||
:activeOption="settings.style.nodes.color.sourceUsage"
|
|
||||||
@option-change="updateNodes('color.sourceUsage', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
v-if="settings.style.nodes.color.sourceUsage === 'map_to'"
|
|
||||||
>
|
|
||||||
<ColorscalePicker
|
|
||||||
:selected="settings.style.nodes.color.colorscale"
|
|
||||||
className="colorscale-picker"
|
|
||||||
@colorscale-change="updateNodes('color.colorscale', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</template>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
v-if="settings.style.nodes.color.type !== 'constant'"
|
|
||||||
label="Color as"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="сolorAsOptions"
|
|
||||||
:activeOption="settings.style.nodes.color.mode"
|
|
||||||
@option-change="updateNodes('color.mode', $event)"
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field
|
<NodeSizeSettings
|
||||||
v-if="settings.style.nodes.color.type !== 'constant'"
|
v-model="settings.style.nodes.size"
|
||||||
label="Colorscale direction"
|
:keyOptions="keysOptions"
|
||||||
>
|
@update:model-value="updateNodes('size', $event)"
|
||||||
<RadioBlocks
|
/>
|
||||||
:options="сolorscaleDirections"
|
<NodeColorSettings
|
||||||
:activeOption="settings.style.nodes.color.colorscaleDirection"
|
v-model="settings.style.nodes.color"
|
||||||
@option-change="
|
:keyOptions="keysOptions"
|
||||||
updateNodes('color.colorscaleDirection', $event)
|
@update:model-value="updateNodes('color', $event)"
|
||||||
"
|
/>
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Fold>
|
</Fold>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
@@ -194,110 +97,24 @@
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Size">
|
<Field label="Label color">
|
||||||
<RadioBlocks
|
<ColorPicker
|
||||||
:options="edgeSizeTypeOptions"
|
:selectedColor="settings.style.edges.label.color"
|
||||||
:activeOption="settings.style.edges.size.type"
|
@color-change="updateEdges('label.color', $event)"
|
||||||
@option-change="updateEdges('size.type', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field>
|
|
||||||
<NumericInput
|
|
||||||
v-if="settings.style.edges.size.type === 'constant'"
|
|
||||||
:value="settings.style.edges.size.value"
|
|
||||||
:min="1"
|
|
||||||
@update="updateEdges('size.value', $event)"
|
|
||||||
/>
|
|
||||||
<Dropdown
|
|
||||||
v-if="settings.style.edges.size.type === 'variable'"
|
|
||||||
:options="keysOptions"
|
|
||||||
:value="settings.style.edges.size.source"
|
|
||||||
@change="updateEdges('size.source', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<template v-if="settings.style.edges.size.type !== 'constant'">
|
|
||||||
<Field label="Size scale">
|
|
||||||
<NumericInput
|
|
||||||
:value="settings.style.edges.size.scale"
|
|
||||||
@update="updateEdges('size.scale', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Minimum size">
|
|
||||||
<NumericInput
|
|
||||||
:value="settings.style.edges.size.min"
|
|
||||||
@update="updateEdges('size.min', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<Field label="Color">
|
|
||||||
<RadioBlocks
|
|
||||||
:options="edgeColorTypeOptions"
|
|
||||||
:activeOption="settings.style.edges.color.type"
|
|
||||||
@option-change="updateEdges('color.type', $event)"
|
|
||||||
/>
|
|
||||||
<Field v-if="settings.style.edges.color.type === 'constant'">
|
|
||||||
<ColorPicker
|
|
||||||
:selectedColor="settings.style.edges.color.value"
|
|
||||||
@color-change="updateEdges('color.value', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<template v-else>
|
|
||||||
<Field>
|
|
||||||
<Dropdown
|
|
||||||
v-if="settings.style.edges.color.type === 'variable'"
|
|
||||||
:options="keysOptions"
|
|
||||||
:value="settings.style.edges.color.source"
|
|
||||||
@change="updateEdges('color.source', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="colorSourceUsageOptions"
|
|
||||||
:activeOption="settings.style.edges.color.sourceUsage"
|
|
||||||
@option-change="updateEdges('color.sourceUsage', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
v-if="settings.style.edges.color.sourceUsage === 'map_to'"
|
|
||||||
>
|
|
||||||
<ColorscalePicker
|
|
||||||
:selected="settings.style.edges.color.colorscale"
|
|
||||||
className="colorscale-picker"
|
|
||||||
@colorscale-change="updateEdges('color.colorscale', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</template>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
v-if="settings.style.edges.color.type !== 'constant'"
|
|
||||||
label="Color as"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="сolorAsOptions"
|
|
||||||
:activeOption="settings.style.edges.color.mode"
|
|
||||||
@option-change="updateEdges('color.mode', $event)"
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field
|
<EdgeSizeSettings
|
||||||
v-if="settings.style.edges.color.type !== 'constant'"
|
v-model="settings.style.edges.size"
|
||||||
label="Colorscale direction"
|
:keyOptions="keysOptions"
|
||||||
>
|
@update:model-value="updateEdges('size', $event)"
|
||||||
<RadioBlocks
|
/>
|
||||||
:options="сolorscaleDirections"
|
|
||||||
:activeOption="settings.style.edges.color.colorscaleDirection"
|
<EdgeColorSettings
|
||||||
@option-change="
|
v-model="settings.style.edges.color"
|
||||||
updateEdges('color.colorscaleDirection', $event)
|
:keyOptions="keysOptions"
|
||||||
"
|
@update:model-value="updateEdges('color', $event)"
|
||||||
/>
|
/>
|
||||||
</Field>
|
|
||||||
</Fold>
|
</Fold>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel group="Style" name="Layout">
|
<Panel group="Style" name="Layout">
|
||||||
@@ -316,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
|
||||||
@@ -326,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>
|
||||||
@@ -339,7 +168,8 @@
|
|||||||
ref="graph"
|
ref="graph"
|
||||||
:style="{
|
:style="{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%'
|
width: '100%',
|
||||||
|
backgroundColor: settings.style.backgroundColor
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -350,21 +180,21 @@ import { markRaw } from 'vue'
|
|||||||
import { applyPureReactInVue } from 'veaury'
|
import { applyPureReactInVue } from 'veaury'
|
||||||
import GraphEditorControls from '@/lib/GraphEditorControls.jsx'
|
import GraphEditorControls from '@/lib/GraphEditorControls.jsx'
|
||||||
import { PanelMenuWrapper, Panel, Fold, Section } from 'react-chart-editor'
|
import { PanelMenuWrapper, Panel, Fold, Section } from 'react-chart-editor'
|
||||||
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
||||||
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||||
import Button from 'react-chart-editor/lib/components/widgets/Button'
|
|
||||||
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
|
|
||||||
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
|
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
|
||||||
|
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 'react-chart-editor/lib/react-chart-editor.css'
|
|
||||||
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'
|
||||||
import RunIcon from '@/components/svg/run.vue'
|
import RunIcon from '@/components/svg/run.vue'
|
||||||
import StopIcon from '@/components/svg/stop.vue'
|
import StopIcon from '@/components/svg/stop.vue'
|
||||||
|
import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image'
|
||||||
import {
|
import {
|
||||||
buildNodes,
|
buildNodes,
|
||||||
buildEdges,
|
buildEdges,
|
||||||
@@ -375,6 +205,10 @@ import Graph from 'graphology'
|
|||||||
import { circular, random, circlepack } from 'graphology-layout'
|
import { circular, random, circlepack } from 'graphology-layout'
|
||||||
import Sigma from 'sigma'
|
import Sigma from 'sigma'
|
||||||
import seedrandom from 'seedrandom'
|
import seedrandom from 'seedrandom'
|
||||||
|
import NodeColorSettings from '@/components/Graph/NodeColorSettings.vue'
|
||||||
|
import NodeSizeSettings from '@/components/Graph/NodeSizeSettings.vue'
|
||||||
|
import EdgeSizeSettings from '@/components/Graph/EdgeSizeSettings.vue'
|
||||||
|
import EdgeColorSettings from '@/components/Graph/EdgeColorSettings.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -383,71 +217,34 @@ export default {
|
|||||||
Panel: applyPureReactInVue(Panel),
|
Panel: applyPureReactInVue(Panel),
|
||||||
PanelSection: applyPureReactInVue(Section),
|
PanelSection: applyPureReactInVue(Section),
|
||||||
Dropdown: applyPureReactInVue(Dropdown),
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
NumericInput: applyPureReactInVue(NumericInput),
|
|
||||||
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
||||||
Field: applyPureReactInVue(Field),
|
Field: applyPureReactInVue(Field),
|
||||||
Fold: applyPureReactInVue(Fold),
|
Fold: applyPureReactInVue(Fold),
|
||||||
ColorscalePicker: applyPureReactInVue(ColorscalePicker),
|
|
||||||
ColorPicker: applyPureReactInVue(ColorPicker),
|
|
||||||
Button: applyPureReactInVue(Button),
|
Button: applyPureReactInVue(Button),
|
||||||
|
ColorPicker: applyPureReactInVue(ColorPicker),
|
||||||
RunIcon,
|
RunIcon,
|
||||||
StopIcon,
|
StopIcon,
|
||||||
RandomLayoutSettings,
|
RandomLayoutSettings,
|
||||||
CirclePackLayoutSettings
|
CirclePackLayoutSettings,
|
||||||
|
NodeColorSettings,
|
||||||
|
NodeSizeSettings,
|
||||||
|
EdgeSizeSettings,
|
||||||
|
EdgeColorSettings,
|
||||||
|
AdvancedForceAtlasLayoutSettings
|
||||||
},
|
},
|
||||||
|
inject: ['tabLayout'],
|
||||||
props: {
|
props: {
|
||||||
dataSources: Object
|
dataSources: Object,
|
||||||
|
initOptions: Object
|
||||||
},
|
},
|
||||||
|
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,
|
||||||
nodeSizeTypeOptions: markRaw([
|
checkIteration: null,
|
||||||
{ label: 'Constant', value: 'constant' },
|
|
||||||
{ label: 'Variable', value: 'variable' },
|
|
||||||
{ label: 'Calculated', value: 'calculated' }
|
|
||||||
]),
|
|
||||||
edgeSizeTypeOptions: markRaw([
|
|
||||||
{ label: 'Constant', value: 'constant' },
|
|
||||||
{ label: 'Variable', value: 'variable' }
|
|
||||||
]),
|
|
||||||
nodeCalculatedSizeMethodOptions: markRaw([
|
|
||||||
{ label: 'Degree', value: 'degree' },
|
|
||||||
{ label: 'In degree', value: 'inDegree' },
|
|
||||||
{ label: 'Out degree', value: 'outDegree' }
|
|
||||||
]),
|
|
||||||
nodeSizeModeOptions: markRaw([
|
|
||||||
{ label: 'Area', value: 'area' },
|
|
||||||
{ label: 'Diameter', value: 'diameter' }
|
|
||||||
]),
|
|
||||||
nodeColorTypeOptions: markRaw([
|
|
||||||
{ label: 'Constant', value: 'constant' },
|
|
||||||
{ label: 'Variable', value: 'variable' },
|
|
||||||
{ label: 'Calculated', value: 'calculated' }
|
|
||||||
]),
|
|
||||||
edgeColorTypeOptions: markRaw([
|
|
||||||
{ label: 'Constant', value: 'constant' },
|
|
||||||
{ label: 'Variable', value: 'variable' }
|
|
||||||
]),
|
|
||||||
nodeCalculatedColorMethodOptions: markRaw([
|
|
||||||
{ label: 'Degree', value: 'degree' },
|
|
||||||
{ label: 'In degree', value: 'inDegree' },
|
|
||||||
{ label: 'Out degree', value: 'outDegree' }
|
|
||||||
]),
|
|
||||||
сolorAsOptions: markRaw([
|
|
||||||
{ label: 'Continious', value: 'continious' },
|
|
||||||
{ label: 'Categorical', value: 'categorical' }
|
|
||||||
]),
|
|
||||||
сolorscaleDirections: markRaw([
|
|
||||||
{ label: 'Normal', value: 'normal' },
|
|
||||||
{ label: 'Recersed', value: 'reversed' }
|
|
||||||
]),
|
|
||||||
colorSourceUsageOptions: markRaw([
|
|
||||||
{ label: 'Direct', value: 'direct' },
|
|
||||||
{ label: 'Map to', value: 'map_to' }
|
|
||||||
]),
|
|
||||||
visibilityOptions: markRaw([
|
visibilityOptions: markRaw([
|
||||||
{ label: 'Show', value: true },
|
{ label: 'Show', value: true },
|
||||||
{ label: 'Hide', value: false }
|
{ label: 'Hide', value: false }
|
||||||
@@ -464,66 +261,52 @@ export default {
|
|||||||
forceAtlas2: ForceAtlasLayoutSettings
|
forceAtlas2: ForceAtlasLayoutSettings
|
||||||
}),
|
}),
|
||||||
|
|
||||||
settings: {
|
settings: this.initOptions
|
||||||
structure: {
|
? JSON.parse(JSON.stringify(this.initOptions))
|
||||||
nodeId: null,
|
: {
|
||||||
objectType: null,
|
structure: {
|
||||||
edgeSource: null,
|
nodeId: null,
|
||||||
edgeTarget: null
|
objectType: null,
|
||||||
},
|
edgeSource: null,
|
||||||
style: {
|
edgeTarget: null
|
||||||
nodes: {
|
|
||||||
size: {
|
|
||||||
type: 'constant',
|
|
||||||
value: 16,
|
|
||||||
source: null,
|
|
||||||
scale: 1,
|
|
||||||
mode: 'diameter',
|
|
||||||
method: 'degree',
|
|
||||||
min: 0
|
|
||||||
},
|
},
|
||||||
color: {
|
style: {
|
||||||
type: 'constant',
|
backgroundColor: 'white',
|
||||||
value: '#1F77B4',
|
nodes: {
|
||||||
source: null,
|
size: {
|
||||||
sourceUsage: 'map_to',
|
type: 'constant',
|
||||||
colorscale: null,
|
value: 4
|
||||||
colorscaleDirection: 'normal',
|
},
|
||||||
method: 'degree',
|
color: {
|
||||||
mode: 'continious'
|
type: 'constant',
|
||||||
|
value: '#1F77B4'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
source: null,
|
||||||
|
color: '#444444'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
edges: {
|
||||||
|
showDirection: true,
|
||||||
|
size: {
|
||||||
|
type: 'constant',
|
||||||
|
value: 2
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: 'constant',
|
||||||
|
value: '#a2b1c6'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
source: null,
|
||||||
|
color: '#a2b1c6'
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
label: {
|
layout: {
|
||||||
source: null
|
type: 'circular',
|
||||||
|
options: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
edges: {
|
|
||||||
showDirection: true,
|
|
||||||
size: {
|
|
||||||
type: 'constant',
|
|
||||||
value: 2,
|
|
||||||
source: null,
|
|
||||||
scale: 1,
|
|
||||||
min: 0
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: 'constant',
|
|
||||||
value: '#a2b1c6',
|
|
||||||
source: null,
|
|
||||||
sourceUsage: 'map_to',
|
|
||||||
colorscale: null,
|
|
||||||
colorscaleDirection: 'normal',
|
|
||||||
mode: 'continious'
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
source: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
layout: {
|
|
||||||
type: 'circular',
|
|
||||||
options: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
layoutOptionsArchive: {
|
layoutOptionsArchive: {
|
||||||
random: null,
|
random: null,
|
||||||
circlepack: null,
|
circlepack: null,
|
||||||
@@ -536,9 +319,15 @@ export default {
|
|||||||
if (!this.dataSources) {
|
if (!this.dataSources) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return this.dataSources[Object.keys(this.dataSources)[0] || 'doc'].map(
|
try {
|
||||||
json => JSON.parse(json)
|
return (
|
||||||
)
|
this.dataSources[Object.keys(this.dataSources)[0] || 'doc'].map(
|
||||||
|
json => JSON.parse(json)
|
||||||
|
) || []
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
keysOptions() {
|
keysOptions() {
|
||||||
if (!this.dataSources) {
|
if (!this.dataSources) {
|
||||||
@@ -558,11 +347,30 @@ export default {
|
|||||||
this.buildGraph()
|
this.buildGraph()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.$emit('update')
|
||||||
|
}
|
||||||
|
},
|
||||||
'settings.structure': {
|
'settings.structure': {
|
||||||
deep: true,
|
deep: true,
|
||||||
handler() {
|
handler() {
|
||||||
this.buildGraph()
|
this.buildGraph()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
tabLayout: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
if (this.tabLayout.dataView !== 'hidden' && this.renderer) {
|
||||||
|
this.renderer.scheduleRender()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.dataSources) {
|
||||||
|
this.buildGraph()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -579,11 +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' },
|
||||||
|
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
|
||||||
@@ -617,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,
|
this.settings.layout.options = {
|
||||||
barnesHutTheta: 0.5,
|
seedValue: 1
|
||||||
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 = {
|
|
||||||
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
|
||||||
@@ -698,26 +503,108 @@ 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() {
|
||||||
|
return downloadAsPNG(this.renderer, {
|
||||||
|
backgroundColor: this.settings.style.backgroundColor
|
||||||
|
})
|
||||||
|
},
|
||||||
|
prepareCopy() {
|
||||||
|
return drawOnCanvas(this.renderer, {
|
||||||
|
backgroundColor: this.settings.style.backgroundColor
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -731,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>
|
||||||
|
|||||||
@@ -7,20 +7,22 @@
|
|||||||
<div
|
<div
|
||||||
class="graph"
|
class="graph"
|
||||||
:style="{
|
:style="{
|
||||||
height: !dataSources ? 'calc(100% - 40px)' : '100%',
|
height: !dataSources ? 'calc(100% - 40px)' : '100%'
|
||||||
'background-color': 'white'
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<GraphEditor :dataSources="dataSources" />
|
<GraphEditor
|
||||||
|
ref="graphEditor"
|
||||||
|
:dataSources="dataSources"
|
||||||
|
:initOptions="initOptions"
|
||||||
|
@update="$emit('update')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import 'react-chart-editor/lib/react-chart-editor.css'
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
import fIo from '@/lib/utils/fileIo'
|
|
||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
import GraphEditor from './GraphEditor.vue'
|
import GraphEditor from './GraphEditor.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -29,30 +31,51 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
dataSources: Object,
|
dataSources: Object,
|
||||||
initOptions: Object,
|
initOptions: Object,
|
||||||
importToPngEnabled: Boolean,
|
exportToPngEnabled: Boolean,
|
||||||
importToSvgEnabled: Boolean
|
exportToSvgEnabled: Boolean,
|
||||||
|
exportToHtmlEnabled: Boolean
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
'update:exportToSvgEnabled',
|
||||||
|
'update:exportToHtmlEnabled',
|
||||||
|
'update',
|
||||||
|
'loadingImageCompleted'
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
resizeObserver: null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:importToSvgEnabled', 'update', 'loadingImageCompleted'],
|
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.$emit('update:importToSvgEnabled', true)
|
this.$emit('update:exportToSvgEnabled', false)
|
||||||
|
this.$emit('update:exportToHtmlEnabled', false)
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||||
|
this.resizeObserver.observe(this.$refs.graphContainer)
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.resizeObserver.unobserve(this.$refs.graphContainer)
|
||||||
},
|
},
|
||||||
mounted() {},
|
|
||||||
methods: {
|
methods: {
|
||||||
getOptionsForSave() {},
|
getOptionsForSave() {
|
||||||
|
return this.$refs.graphEditor.settings
|
||||||
|
},
|
||||||
async saveAsPng() {
|
async saveAsPng() {
|
||||||
const url = await this.prepareCopy()
|
await this.$refs.graphEditor.saveAsPng()
|
||||||
this.$emit('loadingImageCompleted')
|
this.$emit('loadingImageCompleted')
|
||||||
fIo.downloadFromUrl(url, 'chart')
|
|
||||||
},
|
},
|
||||||
|
async prepareCopy() {
|
||||||
async saveAsSvg() {
|
return await this.$refs.graphEditor.prepareCopy()
|
||||||
const url = await this.prepareCopy('svg')
|
|
||||||
fIo.downloadFromUrl(url, 'chart')
|
|
||||||
},
|
},
|
||||||
|
async handleResize() {
|
||||||
saveAsHtml() {},
|
const renderer = this.$refs.graphEditor.renderer
|
||||||
async prepareCopy(type = 'png') {}
|
if (renderer) {
|
||||||
|
renderer.refresh()
|
||||||
|
renderer.getCamera().setState({ x: 0.5, y: 0.5 })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -46,14 +46,15 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
dataSources: Object,
|
dataSources: Object,
|
||||||
initOptions: Object,
|
initOptions: Object,
|
||||||
importToPngEnabled: Boolean,
|
exportToPngEnabled: Boolean,
|
||||||
importToSvgEnabled: Boolean
|
exportToSvgEnabled: Boolean
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
'loadingImageCompleted',
|
'loadingImageCompleted',
|
||||||
'update',
|
'update',
|
||||||
'update:importToSvgEnabled',
|
'update:exportToSvgEnabled',
|
||||||
'update:importToPngEnabled'
|
'update:exportToPngEnabled',
|
||||||
|
'update:exportToHtmlEnabled'
|
||||||
],
|
],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -110,11 +111,11 @@ export default {
|
|||||||
immediate: true,
|
immediate: true,
|
||||||
handler() {
|
handler() {
|
||||||
this.$emit(
|
this.$emit(
|
||||||
'update:importToPngEnabled',
|
'update:exportToPngEnabled',
|
||||||
this.pivotOptions.rendererName !== 'TSV Export'
|
this.pivotOptions.rendererName !== 'TSV Export'
|
||||||
)
|
)
|
||||||
this.$emit(
|
this.$emit(
|
||||||
'update:importToSvgEnabled',
|
'update:exportToSvgEnabled',
|
||||||
this.viewStandartChart || this.viewCustomChart
|
this.viewStandartChart || this.viewCustomChart
|
||||||
)
|
)
|
||||||
events.send('viz_pivot.render', null, {
|
events.send('viz_pivot.render', null, {
|
||||||
@@ -126,6 +127,9 @@ export default {
|
|||||||
this.show()
|
this.show()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.$emit('update:exportToHtmlEnabled', true)
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.show()
|
this.show()
|
||||||
// We need to detect resizing because plotly doesn't resize when resize its container
|
// We need to detect resizing because plotly doesn't resize when resize its container
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
<component
|
<component
|
||||||
:is="mode"
|
:is="mode"
|
||||||
ref="viewComponent"
|
ref="viewComponent"
|
||||||
v-model:importToPngEnabled="importToPngEnabled"
|
v-model:exportToPngEnabled="exportToPngEnabled"
|
||||||
v-model:importToSvgEnabled="importToSvgEnabled"
|
v-model:exportToSvgEnabled="exportToSvgEnabled"
|
||||||
:initOptions="mode === initMode ? initOptions : undefined"
|
v-model:exportToHtmlEnabled="exportToHtmlEnabled"
|
||||||
|
:initOptions="initOptionsByMode[mode]"
|
||||||
:data-sources="dataSource"
|
:data-sources="dataSource"
|
||||||
@loading-image-completed="loadingImage = false"
|
@loading-image-completed="loadingImage = false"
|
||||||
@update="$emit('update')"
|
@update="$emit('update')"
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
<div class="side-tool-bar-divider" />
|
<div class="side-tool-bar-divider" />
|
||||||
|
|
||||||
<icon-button
|
<icon-button
|
||||||
:disabled="!importToPngEnabled || loadingImage"
|
:disabled="!exportToPngEnabled || loadingImage"
|
||||||
:loading="loadingImage"
|
:loading="loadingImage"
|
||||||
tooltip="Save as PNG image"
|
tooltip="Save as PNG image"
|
||||||
tooltipPosition="top-left"
|
tooltipPosition="top-left"
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
</icon-button>
|
</icon-button>
|
||||||
<icon-button
|
<icon-button
|
||||||
ref="svgExportBtn"
|
ref="svgExportBtn"
|
||||||
:disabled="!importToSvgEnabled"
|
:disabled="!exportToSvgEnabled"
|
||||||
tooltip="Save as SVG"
|
tooltip="Save as SVG"
|
||||||
tooltipPosition="top-left"
|
tooltipPosition="top-left"
|
||||||
@click="saveAsSvg"
|
@click="saveAsSvg"
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
|
|
||||||
<icon-button
|
<icon-button
|
||||||
ref="htmlExportBtn"
|
ref="htmlExportBtn"
|
||||||
|
:disabled="!exportToHtmlEnabled"
|
||||||
tooltip="Save as HTML"
|
tooltip="Save as HTML"
|
||||||
tooltipPosition="top-left"
|
tooltipPosition="top-left"
|
||||||
@click="saveAsHtml"
|
@click="saveAsHtml"
|
||||||
@@ -136,12 +138,18 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
mode: this.initMode || 'chart',
|
mode: this.initMode || 'chart',
|
||||||
importToPngEnabled: true,
|
exportToPngEnabled: true,
|
||||||
importToSvgEnabled: true,
|
exportToSvgEnabled: true,
|
||||||
|
exportToHtmlEnabled: true,
|
||||||
loadingImage: false,
|
loadingImage: false,
|
||||||
copyingImage: false,
|
copyingImage: false,
|
||||||
preparingCopy: false,
|
preparingCopy: false,
|
||||||
dataToCopy: null
|
dataToCopy: null,
|
||||||
|
initOptionsByMode: {
|
||||||
|
chart: this.initMode === 'chart' ? this.initOptions : null,
|
||||||
|
pivot: this.initMode === 'pivot' ? this.initOptions : null,
|
||||||
|
graph: this.initMode === 'graph' ? this.initOptions : null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -150,9 +158,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
mode() {
|
mode(newMode, oldMode) {
|
||||||
this.$emit('update')
|
this.$emit('update')
|
||||||
this.importToPngEnabled = true
|
this.exportToPngEnabled = true
|
||||||
|
this.initOptionsByMode[oldMode] = this.getOptionsForSave()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ import Splitpanes from '@/components/Splitpanes'
|
|||||||
import SqlEditor from './SqlEditor'
|
import SqlEditor from './SqlEditor'
|
||||||
import DataView from './DataView'
|
import DataView from './DataView'
|
||||||
import RunResult from './RunResult'
|
import RunResult from './RunResult'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick, computed } from 'vue'
|
||||||
|
|
||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
@@ -80,6 +80,11 @@ export default {
|
|||||||
RunResult,
|
RunResult,
|
||||||
Splitpanes
|
Splitpanes
|
||||||
},
|
},
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
tabLayout: computed(() => this.tab.layout)
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
tab: Object
|
tab: Object
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user