mirror of
https://github.com/lana-k/sqliteviz.git
synced 2026-03-22 05:56:16 +08:00
#133 highlight nodes and edges
This commit is contained in:
@@ -58,8 +58,9 @@
|
|||||||
</icon-button>
|
</icon-button>
|
||||||
|
|
||||||
<icon-button
|
<icon-button
|
||||||
ref="viewNodeValueBtn"
|
v-if="mode === 'graph'"
|
||||||
tooltip="View node"
|
ref="viewNodeOrEdgeBtn"
|
||||||
|
tooltip="View node or edge details"
|
||||||
tooltipPosition="top-left"
|
tooltipPosition="top-left"
|
||||||
:active="viewValuePanelVisible"
|
:active="viewValuePanelVisible"
|
||||||
@click="viewValuePanelVisible = !viewValuePanelVisible"
|
@click="viewValuePanelVisible = !viewValuePanelVisible"
|
||||||
|
|||||||
@@ -67,6 +67,15 @@
|
|||||||
@color-change="settings.style.backgroundColor = $event"
|
@color-change="settings.style.backgroundColor = $event"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Highlight mode">
|
||||||
|
<Dropdown
|
||||||
|
:options="highlightModeOptions"
|
||||||
|
:value="settings.style.highlightMode"
|
||||||
|
className="test_highlight_mode_select"
|
||||||
|
@change="updateHighlightNodeMode"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
</Fold>
|
</Fold>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel group="Style" name="Nodes">
|
<Panel group="Style" name="Nodes">
|
||||||
@@ -237,7 +246,9 @@ import {
|
|||||||
buildNodes,
|
buildNodes,
|
||||||
buildEdges,
|
buildEdges,
|
||||||
updateNodes,
|
updateNodes,
|
||||||
updateEdges
|
updateEdges,
|
||||||
|
reduceNodes,
|
||||||
|
reduceEdges
|
||||||
} from '@/lib/graphHelper'
|
} from '@/lib/graphHelper'
|
||||||
import Graph from 'graphology'
|
import Graph from 'graphology'
|
||||||
import { circular, random, circlepack } from 'graphology-layout'
|
import { circular, random, circlepack } from 'graphology-layout'
|
||||||
@@ -277,7 +288,7 @@ export default {
|
|||||||
initOptions: Object,
|
initOptions: Object,
|
||||||
showViewSettings: Boolean
|
showViewSettings: Boolean
|
||||||
},
|
},
|
||||||
emits: ['update'],
|
emits: ['update', 'selectItem', 'deselectItem'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
graph: new Graph({ multi: true, allowSelfLoops: true }),
|
graph: new Graph({ multi: true, allowSelfLoops: true }),
|
||||||
@@ -300,7 +311,10 @@ export default {
|
|||||||
circlepack: CirclePackLayoutSettings,
|
circlepack: CirclePackLayoutSettings,
|
||||||
forceAtlas2: ForceAtlasLayoutSettings
|
forceAtlas2: ForceAtlasLayoutSettings
|
||||||
}),
|
}),
|
||||||
|
selectedNodeId: undefined,
|
||||||
|
hoveredNodeId: undefined,
|
||||||
|
selectedEdgeId: undefined,
|
||||||
|
hoveredEdgeId: undefined,
|
||||||
settings: this.initOptions
|
settings: this.initOptions
|
||||||
? JSON.parse(JSON.stringify(this.initOptions))
|
? JSON.parse(JSON.stringify(this.initOptions))
|
||||||
: {
|
: {
|
||||||
@@ -312,6 +326,7 @@ export default {
|
|||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
|
highlightMode: 'node_and_neighbors',
|
||||||
nodes: {
|
nodes: {
|
||||||
size: {
|
size: {
|
||||||
type: 'constant',
|
type: 'constant',
|
||||||
@@ -352,7 +367,15 @@ export default {
|
|||||||
random: null,
|
random: null,
|
||||||
circlepack: null,
|
circlepack: null,
|
||||||
forceAtlas2: null
|
forceAtlas2: null
|
||||||
|
},
|
||||||
|
highlightModeOptions: markRaw([
|
||||||
|
{ label: 'Node alone', value: 'node_alone' },
|
||||||
|
{ label: 'Node and neighbors', value: 'node_and_neighbors' },
|
||||||
|
{
|
||||||
|
label: 'Include edges between neighbors',
|
||||||
|
value: 'include_neighbor_edges'
|
||||||
}
|
}
|
||||||
|
])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -379,6 +402,46 @@ export default {
|
|||||||
}, new Set())
|
}, new Set())
|
||||||
|
|
||||||
return Array.from(keySet)
|
return Array.from(keySet)
|
||||||
|
},
|
||||||
|
neighborsOfSelectedNode() {
|
||||||
|
if (this.settings.style.highlightMode === 'node_alone') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return this.selectedNodeId
|
||||||
|
? new Set(this.graph.neighbors(this.selectedNodeId))
|
||||||
|
: undefined
|
||||||
|
},
|
||||||
|
neighborsOfHoveredNode() {
|
||||||
|
if (this.settings.style.highlightMode === 'node_alone') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return this.hoveredNodeId
|
||||||
|
? new Set(this.graph.neighbors(this.hoveredNodeId))
|
||||||
|
: undefined
|
||||||
|
},
|
||||||
|
hoveredEdgeExtremities() {
|
||||||
|
return this.hoveredEdgeId
|
||||||
|
? this.graph.extremities(this.hoveredEdgeId)
|
||||||
|
: []
|
||||||
|
},
|
||||||
|
selectedEdgeExtremities() {
|
||||||
|
return this.selectedEdgeId
|
||||||
|
? this.graph.extremities(this.selectedEdgeId)
|
||||||
|
: []
|
||||||
|
},
|
||||||
|
interactionState() {
|
||||||
|
return {
|
||||||
|
selectedNodeId: this.selectedNodeId,
|
||||||
|
hoveredNodeId: this.hoveredNodeId,
|
||||||
|
selectedEdgeId: this.selectedEdgeId,
|
||||||
|
hoveredEdgeId: this.hoveredEdgeId,
|
||||||
|
|
||||||
|
neighborsOfSelectedNode: this.neighborsOfSelectedNode,
|
||||||
|
neighborsOfHoveredNode: this.neighborsOfHoveredNode,
|
||||||
|
|
||||||
|
hoveredEdgeExtremities: this.hoveredEdgeExtremities,
|
||||||
|
selectedEdgeExtremities: this.selectedEdgeExtremities
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -423,6 +486,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
buildGraph() {
|
buildGraph() {
|
||||||
|
this.clearSelection()
|
||||||
if (this.renderer) {
|
if (this.renderer) {
|
||||||
this.renderer.kill()
|
this.renderer.kill()
|
||||||
}
|
}
|
||||||
@@ -440,12 +504,85 @@ export default {
|
|||||||
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' },
|
||||||
|
enableEdgeEvents: true,
|
||||||
|
zIndex: true,
|
||||||
|
nodeReducer: (node, data) =>
|
||||||
|
reduceNodes(node, data, this.interactionState, this.settings),
|
||||||
|
edgeReducer: (edge, data) =>
|
||||||
|
reduceEdges(
|
||||||
|
edge,
|
||||||
|
data,
|
||||||
|
this.interactionState,
|
||||||
|
this.settings,
|
||||||
|
this.graph
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
this.renderer.on('clickNode', ({ node }) => {
|
||||||
|
this.selectedNodeId = node
|
||||||
|
this.selectedEdgeId = undefined
|
||||||
|
this.$emit('selectItem', this.graph.getNodeAttributes(node).data)
|
||||||
|
this.renderer.refresh({
|
||||||
|
skipIndexation: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.renderer.on('clickEdge', ({ edge }) => {
|
||||||
|
this.selectedEdgeId = edge
|
||||||
|
this.selectedNodeId = undefined
|
||||||
|
this.$emit('selectItem', this.graph.getEdgeAttributes(edge).data)
|
||||||
|
this.renderer.refresh({
|
||||||
|
skipIndexation: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.renderer.on('clickStage', () => {
|
||||||
|
this.clearSelection()
|
||||||
|
this.renderer.refresh({
|
||||||
|
skipIndexation: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.renderer.on('enterNode', ({ node }) => {
|
||||||
|
this.hoveredNodeId = node
|
||||||
|
this.renderer.refresh({
|
||||||
|
skipIndexation: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.renderer.on('enterEdge', ({ edge }) => {
|
||||||
|
this.hoveredEdgeId = edge
|
||||||
|
this.renderer.refresh({
|
||||||
|
skipIndexation: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.renderer.on('leaveNode', () => {
|
||||||
|
this.hoveredNodeId = undefined
|
||||||
|
this.renderer.refresh({
|
||||||
|
skipIndexation: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.renderer.on('leaveEdge', () => {
|
||||||
|
this.hoveredEdgeId = undefined
|
||||||
|
this.renderer.refresh({
|
||||||
|
skipIndexation: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
if (this.settings.layout.type === 'forceAtlas2') {
|
if (this.settings.layout.type === 'forceAtlas2') {
|
||||||
this.autoRunFA2Layout()
|
this.autoRunFA2Layout()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
clearSelection() {
|
||||||
|
this.selectedNodeId = undefined
|
||||||
|
this.selectedEdgeId = undefined
|
||||||
|
this.$emit('deselectItem')
|
||||||
|
},
|
||||||
|
updateHighlightNodeMode(mode) {
|
||||||
|
this.settings.style.highlightMode = mode
|
||||||
|
|
||||||
|
if (this.renderer) {
|
||||||
|
this.renderer.refresh({
|
||||||
|
skipIndexation: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
updateStructure(attributeName, value) {
|
updateStructure(attributeName, value) {
|
||||||
this.settings.structure[attributeName] = value
|
this.settings.structure[attributeName] = value
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,14 +31,17 @@
|
|||||||
:initOptions="initOptions"
|
:initOptions="initOptions"
|
||||||
:showViewSettings="showViewSettings"
|
:showViewSettings="showViewSettings"
|
||||||
@update="$emit('update')"
|
@update="$emit('update')"
|
||||||
|
@selectItem="selectedItem = $event"
|
||||||
|
@deselectItem="selectedItem = null"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="showValueViewer" #right-pane>
|
<template v-if="showValueViewer" #right-pane>
|
||||||
<value-viewer
|
<value-viewer
|
||||||
:empty="!selectedNode"
|
:empty="!selectedItem"
|
||||||
empty-message="No node selected to view"
|
empty-message="No node or edge selected to view"
|
||||||
:cellValue="'{}'"
|
:cellValue="JSON.stringify(selectedItem)"
|
||||||
|
default-format="json"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</splitpanes>
|
</splitpanes>
|
||||||
@@ -75,7 +78,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
resizeObserver: null,
|
resizeObserver: null,
|
||||||
selectedNode: {}
|
selectedItem: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
@@ -75,7 +75,11 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
cellValue: [String, Number, Uint8Array],
|
cellValue: [String, Number, Uint8Array],
|
||||||
empty: Boolean,
|
empty: Boolean,
|
||||||
emptyMessage: String
|
emptyMessage: String,
|
||||||
|
defaultFormat: {
|
||||||
|
type: String,
|
||||||
|
default: 'text'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -83,7 +87,7 @@ export default {
|
|||||||
{ text: 'Text', value: 'text' },
|
{ text: 'Text', value: 'text' },
|
||||||
{ text: 'JSON', value: 'json' }
|
{ text: 'JSON', value: 'json' }
|
||||||
],
|
],
|
||||||
currentFormat: 'text',
|
currentFormat: this.defaultFormat,
|
||||||
lineWrapping: false,
|
lineWrapping: false,
|
||||||
formattedJson: '',
|
formattedJson: '',
|
||||||
messages: []
|
messages: []
|
||||||
@@ -121,19 +125,25 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentFormat() {
|
currentFormat: {
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
this.messages = []
|
this.messages = []
|
||||||
this.formattedJson = ''
|
this.formattedJson = ''
|
||||||
if (this.currentFormat === 'json') {
|
if (this.currentFormat === 'json') {
|
||||||
this.formatJson(this.cellValue)
|
this.formatJson(this.cellValue)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
cellValue() {
|
cellValue: {
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
this.messages = []
|
this.messages = []
|
||||||
if (this.currentFormat === 'json') {
|
if (this.currentFormat === 'json') {
|
||||||
this.formatJson(this.cellValue)
|
this.formatJson(this.cellValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
formatJson(jsonStr) {
|
formatJson(jsonStr) {
|
||||||
|
|||||||
@@ -127,6 +127,107 @@ export function updateEdges(graph, attributeUpdates) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function reduceNodes(node, data, interactionState, settings) {
|
||||||
|
const {
|
||||||
|
selectedNodeId,
|
||||||
|
hoveredNodeId,
|
||||||
|
selectedEdgeId,
|
||||||
|
hoveredEdgeId,
|
||||||
|
neighborsOfSelectedNode,
|
||||||
|
neighborsOfHoveredNode,
|
||||||
|
selectedEdgeExtremities,
|
||||||
|
hoveredEdgeExtremities
|
||||||
|
} = interactionState
|
||||||
|
|
||||||
|
const res = { ...data }
|
||||||
|
|
||||||
|
if (selectedNodeId || hoveredNodeId || hoveredEdgeId || selectedEdgeId) {
|
||||||
|
res.zIndex = 2
|
||||||
|
res.highlighted = node === selectedNodeId || node === hoveredNodeId
|
||||||
|
|
||||||
|
const isInHoveredFamily =
|
||||||
|
node === hoveredNodeId ||
|
||||||
|
neighborsOfHoveredNode?.has(node) ||
|
||||||
|
hoveredEdgeExtremities.includes(node)
|
||||||
|
const isInSelectedFamily =
|
||||||
|
node === selectedNodeId ||
|
||||||
|
neighborsOfSelectedNode?.has(node) ||
|
||||||
|
selectedEdgeExtremities.includes(node)
|
||||||
|
if (isInSelectedFamily || isInHoveredFamily) {
|
||||||
|
res.forceLabel = true
|
||||||
|
} else {
|
||||||
|
res.color = getDiminishedColor(data.color, settings.style.backgroundColor)
|
||||||
|
res.label = ''
|
||||||
|
res.zIndex = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reduceEdges(edge, data, interactionState, settings, graph) {
|
||||||
|
const {
|
||||||
|
selectedNodeId,
|
||||||
|
hoveredNodeId,
|
||||||
|
selectedEdgeId,
|
||||||
|
hoveredEdgeId,
|
||||||
|
neighborsOfSelectedNode,
|
||||||
|
neighborsOfHoveredNode
|
||||||
|
} = interactionState
|
||||||
|
|
||||||
|
const res = { ...data }
|
||||||
|
if (hoveredEdgeId || selectedEdgeId || selectedNodeId || hoveredNodeId) {
|
||||||
|
const extremities = graph.extremities(edge)
|
||||||
|
res.zIndex = 2
|
||||||
|
const isHighlighted = hoveredEdgeId === edge || selectedEdgeId === edge
|
||||||
|
|
||||||
|
let isVisible
|
||||||
|
if (settings.style.highlightMode === 'node_alone') {
|
||||||
|
isVisible = isHighlighted
|
||||||
|
} else if (settings.style.highlightMode === 'node_and_neighbors') {
|
||||||
|
isVisible =
|
||||||
|
isHighlighted ||
|
||||||
|
(selectedNodeId && extremities.includes(selectedNodeId)) ||
|
||||||
|
(hoveredNodeId && extremities.includes(hoveredNodeId))
|
||||||
|
} else {
|
||||||
|
isVisible =
|
||||||
|
isHighlighted ||
|
||||||
|
(selectedNodeId &&
|
||||||
|
extremities.every(
|
||||||
|
n => n === selectedNodeId || neighborsOfSelectedNode.has(n)
|
||||||
|
)) ||
|
||||||
|
(hoveredNodeId &&
|
||||||
|
extremities.every(
|
||||||
|
n => n === hoveredNodeId || neighborsOfHoveredNode.has(n)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
if (isHighlighted) {
|
||||||
|
res.size = res.size * 2
|
||||||
|
res.forceLabel = true
|
||||||
|
} else if (!isVisible) {
|
||||||
|
res.color = getDiminishedColor(data.color, settings.style.backgroundColor)
|
||||||
|
res.zIndex = 1
|
||||||
|
res.label = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDiminishedColor(color, bgColor) {
|
||||||
|
const colorObj = tinycolor(color)
|
||||||
|
const colorOpacity = colorObj.getAlpha()
|
||||||
|
colorObj.setAlpha(0.25 * colorOpacity)
|
||||||
|
|
||||||
|
const fg = colorObj.toRgb()
|
||||||
|
const bg = tinycolor(bgColor).toRgb()
|
||||||
|
|
||||||
|
const r = Math.round(fg.r * fg.a + bg.r * (1 - fg.a))
|
||||||
|
const g = Math.round(fg.g * fg.a + bg.g * (1 - fg.a))
|
||||||
|
const b = Math.round(fg.b * fg.a + bg.b * (1 - fg.a))
|
||||||
|
|
||||||
|
return tinycolor({ r, g, b, a: 1 }).toHexString()
|
||||||
|
}
|
||||||
|
|
||||||
function getUpdateLabelMethod(labelSettings) {
|
function getUpdateLabelMethod(labelSettings) {
|
||||||
const { source, color } = labelSettings
|
const { source, color } = labelSettings
|
||||||
return attributes => {
|
return attributes => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default {
|
|||||||
inquiries.forEach(inquiry => {
|
inquiries.forEach(inquiry => {
|
||||||
if (inquiry.viewType === 'graph') {
|
if (inquiry.viewType === 'graph') {
|
||||||
inquiry.viewOptions.style.nodes.color.opacity = 100
|
inquiry.viewOptions.style.nodes.color.opacity = 100
|
||||||
|
inquiry.viewOptions.style.highlightMode = 'node_and_neighbors'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export default {
|
|||||||
// Turn data into array if they are not
|
// Turn data into array if they are not
|
||||||
inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries
|
inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries
|
||||||
inquiryList = migrate(1, inquiryList)
|
inquiryList = migrate(1, inquiryList)
|
||||||
|
} else if (inquiries.version === 2) {
|
||||||
|
inquiryList = migrate(2, inquiries.inquiries)
|
||||||
} else {
|
} else {
|
||||||
inquiryList = inquiries.inquiries || []
|
inquiryList = inquiries.inquiries || []
|
||||||
}
|
}
|
||||||
@@ -108,6 +110,8 @@ export default {
|
|||||||
|
|
||||||
if (!data.version) {
|
if (!data.version) {
|
||||||
return data.length > 0 ? migrate(1, data) : []
|
return data.length > 0 ? migrate(1, data) : []
|
||||||
|
} else if (data.version === 2) {
|
||||||
|
return migrate(2, data.inquiries)
|
||||||
} else {
|
} else {
|
||||||
return data.inquiries
|
return data.inquiries
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user