diff --git a/src/components/DataView.vue b/src/components/DataView.vue
index 9d70f61..3350812 100644
--- a/src/components/DataView.vue
+++ b/src/components/DataView.vue
@@ -58,8 +58,9 @@
+
+
+
+
@@ -237,7 +246,9 @@ import {
buildNodes,
buildEdges,
updateNodes,
- updateEdges
+ updateEdges,
+ reduceNodes,
+ reduceEdges
} from '@/lib/graphHelper'
import Graph from 'graphology'
import { circular, random, circlepack } from 'graphology-layout'
@@ -277,7 +288,7 @@ export default {
initOptions: Object,
showViewSettings: Boolean
},
- emits: ['update'],
+ emits: ['update', 'selectItem', 'deselectItem'],
data() {
return {
graph: new Graph({ multi: true, allowSelfLoops: true }),
@@ -300,7 +311,10 @@ export default {
circlepack: CirclePackLayoutSettings,
forceAtlas2: ForceAtlasLayoutSettings
}),
-
+ selectedNodeId: undefined,
+ hoveredNodeId: undefined,
+ selectedEdgeId: undefined,
+ hoveredEdgeId: undefined,
settings: this.initOptions
? JSON.parse(JSON.stringify(this.initOptions))
: {
@@ -312,6 +326,7 @@ export default {
},
style: {
backgroundColor: 'white',
+ highlightMode: 'node_and_neighbors',
nodes: {
size: {
type: 'constant',
@@ -352,7 +367,15 @@ export default {
random: null,
circlepack: 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: {
@@ -379,6 +402,46 @@ export default {
}, new Set())
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: {
@@ -423,6 +486,7 @@ export default {
},
methods: {
buildGraph() {
+ this.clearSelection()
if (this.renderer) {
this.renderer.kill()
}
@@ -440,12 +504,85 @@ export default {
renderEdgeLabels: true,
allowInvalidContainer: true,
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') {
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) {
this.settings.structure[attributeName] = value
},
diff --git a/src/components/Graph/index.vue b/src/components/Graph/index.vue
index f2f8c6e..15619d8 100644
--- a/src/components/Graph/index.vue
+++ b/src/components/Graph/index.vue
@@ -31,14 +31,17 @@
:initOptions="initOptions"
:showViewSettings="showViewSettings"
@update="$emit('update')"
+ @selectItem="selectedItem = $event"
+ @deselectItem="selectedItem = null"
/>
@@ -75,7 +78,7 @@ export default {
data() {
return {
resizeObserver: null,
- selectedNode: {}
+ selectedItem: {}
}
},
computed: {
diff --git a/src/components/ValueViewer.vue b/src/components/ValueViewer.vue
index 39e109c..c39fae3 100644
--- a/src/components/ValueViewer.vue
+++ b/src/components/ValueViewer.vue
@@ -75,7 +75,11 @@ export default {
props: {
cellValue: [String, Number, Uint8Array],
empty: Boolean,
- emptyMessage: String
+ emptyMessage: String,
+ defaultFormat: {
+ type: String,
+ default: 'text'
+ }
},
data() {
return {
@@ -83,7 +87,7 @@ export default {
{ text: 'Text', value: 'text' },
{ text: 'JSON', value: 'json' }
],
- currentFormat: 'text',
+ currentFormat: this.defaultFormat,
lineWrapping: false,
formattedJson: '',
messages: []
@@ -121,17 +125,23 @@ export default {
}
},
watch: {
- currentFormat() {
- this.messages = []
- this.formattedJson = ''
- if (this.currentFormat === 'json') {
- this.formatJson(this.cellValue)
+ currentFormat: {
+ immediate: true,
+ handler() {
+ this.messages = []
+ this.formattedJson = ''
+ if (this.currentFormat === 'json') {
+ this.formatJson(this.cellValue)
+ }
}
},
- cellValue() {
- this.messages = []
- if (this.currentFormat === 'json') {
- this.formatJson(this.cellValue)
+ cellValue: {
+ immediate: true,
+ handler() {
+ this.messages = []
+ if (this.currentFormat === 'json') {
+ this.formatJson(this.cellValue)
+ }
}
}
},
diff --git a/src/lib/graphHelper.js b/src/lib/graphHelper.js
index e94dd75..cf1c896 100644
--- a/src/lib/graphHelper.js
+++ b/src/lib/graphHelper.js
@@ -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) {
const { source, color } = labelSettings
return attributes => {
diff --git a/src/lib/storedInquiries/_migrations.js b/src/lib/storedInquiries/_migrations.js
index a1f92ea..b54f4bf 100644
--- a/src/lib/storedInquiries/_migrations.js
+++ b/src/lib/storedInquiries/_migrations.js
@@ -12,6 +12,7 @@ export default {
inquiries.forEach(inquiry => {
if (inquiry.viewType === 'graph') {
inquiry.viewOptions.style.nodes.color.opacity = 100
+ inquiry.viewOptions.style.highlightMode = 'node_and_neighbors'
}
})
}
diff --git a/src/lib/storedInquiries/index.js b/src/lib/storedInquiries/index.js
index 94b546e..bb102a4 100644
--- a/src/lib/storedInquiries/index.js
+++ b/src/lib/storedInquiries/index.js
@@ -69,6 +69,8 @@ export default {
// Turn data into array if they are not
inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries
inquiryList = migrate(1, inquiryList)
+ } else if (inquiries.version === 2) {
+ inquiryList = migrate(2, inquiries.inquiries)
} else {
inquiryList = inquiries.inquiries || []
}
@@ -108,6 +110,8 @@ export default {
if (!data.version) {
return data.length > 0 ? migrate(1, data) : []
+ } else if (data.version === 2) {
+ return migrate(2, data.inquiries)
} else {
return data.inquiries
}