Skip to content

Commit 8a1784c

Browse files
authored
Merge pull request #586 from RedisInsight/RI-2633
[RedisGraph] Support for node grouping and pulsing
2 parents 7377547 + 5c2ab4e commit 8a1784c

File tree

4 files changed

+98
-34
lines changed

4 files changed

+98
-34
lines changed

redisinsight/ui/src/packages/redisgraph/src/Graph.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,6 @@ export default function Graph(props: { graphKey: string, data: any[] }) {
181181
if (d3.select(nodeSvg).attr('class').indexOf('selected') > 0) {
182182
d3.select(nodeSvg)
183183
.attr('class', 'node')
184-
} else {
185-
d3.select(nodeSvg)
186-
.attr('class', 'node selected')
187184
}
188185
},
189186
async onNodeDoubleClick(nodeSvg, node) {

redisinsight/ui/src/packages/redisgraph/src/graphd3.ts

Lines changed: 92 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,8 @@ function GraphD3(_selector: HTMLDivElement, _options: any): IGraphD3 {
621621
let info: any
622622
let nodes: INode[]
623623
let relationship: d3.Selection<SVGGElement, IRelationship, SVGGElement, any>
624+
let labelCounter = 0;
625+
let labels: { [key: string]: number } = { }
624626
let relationshipOutline
625627
let relationshipOverlay
626628
let relationshipText
@@ -644,7 +646,7 @@ function GraphD3(_selector: HTMLDivElement, _options: any): IGraphD3 {
644646
const options = { ...DEFAULT_OPTIONS, ..._options }
645647
let zoom: d3.ZoomBehavior<Element, unknown> = options.graphZoom
646648

647-
const {labelColors, edgeColors} = options
649+
const { labelColors, edgeColors } = options
648650

649651
function color() {
650652
return COLORS[Math.floor(Math.random() * COLORS.length)]
@@ -725,11 +727,15 @@ function GraphD3(_selector: HTMLDivElement, _options: any): IGraphD3 {
725727
.html(`<strong>${property}</strong> ${value}`)
726728
}
727729

728-
function stickNode(event, d) {
730+
function stickNode(ele, event, d) {
729731
/* eslint-disable */
730732
d.fx = event.x
731733
d.fy = event.y
732734
/* eslint-enable */
735+
736+
// Add a ring to specify that the node was selected
737+
d3.select(ele).attr('class', 'node selected')
738+
733739
}
734740

735741
function dragEnded(event, d) {
@@ -743,7 +749,7 @@ function GraphD3(_selector: HTMLDivElement, _options: any): IGraphD3 {
743749
}
744750

745751
function dragged(event, d) {
746-
stickNode(event, d)
752+
stickNode(this, event, d)
747753
}
748754

749755
function dragStarted(event, d) {
@@ -812,13 +818,29 @@ function GraphD3(_selector: HTMLDivElement, _options: any): IGraphD3 {
812818
}
813819
})
814820
.on('dblclick', function onNodeDoubleClick(event, d) {
815-
stickNode(event, d)
821+
stickNode(this, event, d)
816822

817823
if (typeof options.onNodeDoubleClick === 'function') {
818824
options.onNodeDoubleClick(this, d, event)
819825
}
820826

821-
d3.select('.graphd3-graph').transition().call(zoom.translateTo as any, d.x, d.y)
827+
[...new Set(nodes.map(n => n.labels[0]))].forEach(l => {
828+
if (labels[l] === undefined) {
829+
labels[l] = labelCounter
830+
labelCounter += 1
831+
}
832+
})
833+
834+
// Pulse on double click
835+
Utils.pulse(d3.select(event.currentTarget).select('.outline'));
836+
837+
// Calculating the next positio of the node takes some times
838+
// so start transition only after the calculation. So delay
839+
// starting the transition by some milliseconds.
840+
setTimeout(() => d3.select('.graphd3-graph')
841+
.transition()
842+
.duration(500)
843+
.call(zoom.translateTo as any, d.x, d.y), 10)
822844
})
823845
.on('mouseenter', function onNodeMouseEnter(event, d) {
824846
if (info) {
@@ -1252,26 +1274,20 @@ function GraphD3(_selector: HTMLDivElement, _options: any): IGraphD3 {
12521274
function initSimulation() {
12531275
const spreadFactor = 1.25
12541276
return d3.forceSimulation()
1255-
// .force('x', d3.forceCollide().strength(0.002))
1256-
// .force('y', d3.forceCollide().strength(0.002))
1257-
// .force('y', d3.force().strength(0.002))
1258-
.velocityDecay(0.4)
1259-
.force('collide', d3.forceCollide().radius(() => options.minCollision * spreadFactor).iterations(10))
1260-
.force('charge', d3.forceManyBody().strength(-400))
1261-
.force('link', d3.forceLink().id((d: IRelationship) => d.id))
1262-
.force('center', d3.forceCenter(svg.node().parentElement.parentElement.clientWidth / 2,
1263-
svg.node().parentElement.parentElement.clientHeight / 2))
1264-
.force('centerX', d3.forceX(0).strength(0.03))
1265-
.force('centerX', d3.forceX(0).strength(0.03))
1266-
.on('tick', () => {
1267-
tick()
1268-
})
1269-
.on('end', () => {
1270-
if (options.zoomFit && !justLoaded) {
1271-
justLoaded = true
1272-
zoomFit()
1273-
}
1274-
})
1277+
.force('link', d3.forceLink().id((d: IRelationship) => d.id).distance(70))
1278+
.force('charge', d3.forceManyBody().strength((d, i) => i ? -5000 : 500))
1279+
.force("y", d3.forceY(svg.node().parentElement.parentElement.clientHeight / 2))
1280+
.force('center', d3.forceCenter(svg.node().parentElement.parentElement.clientWidth / 2,
1281+
svg.node().parentElement.parentElement.clientHeight / 2))
1282+
.on('tick', () => {
1283+
tick()
1284+
})
1285+
.on('end', () => {
1286+
if (options.zoomFit && !justLoaded) {
1287+
justLoaded = true
1288+
zoomFit()
1289+
}
1290+
})
12751291
}
12761292

12771293
function init() {
@@ -1298,7 +1314,22 @@ function GraphD3(_selector: HTMLDivElement, _options: any): IGraphD3 {
12981314
} else {
12991315
console.error('Error: graphData is empty!')
13001316
}
1301-
}
1317+
1318+
[...new Set(nodes.map(n => n.labels[0]))].forEach(l => {
1319+
if (labels[l] === undefined) {
1320+
labels[l] = labelCounter
1321+
labelCounter += 1
1322+
}
1323+
});
1324+
1325+
simulation
1326+
.force("x", d3.forceX(d => d.angleX || 0))
1327+
.force("y", d3.forceY(d => d.angleY || 0))
1328+
.force('center', d3.forceCenter(svg.node().parentElement.parentElement.clientWidth / 2,
1329+
svg.node().parentElement.parentElement.clientHeight / 2))
1330+
.force('centerX', d3.forceX(0).strength(0.03))
1331+
.force('centerX', d3.forceX(0).strength(0.03))
1332+
}
13021333

13031334
init()
13041335

@@ -1421,10 +1452,12 @@ function GraphD3(_selector: HTMLDivElement, _options: any): IGraphD3 {
14211452

14221453

14231454
function mapData(d: IGraph) {
1424-
d.relationships.map((r) => {
1455+
d.relationships.map(r => {
14251456
let source = findNode(r.startNode, d.nodes)
14261457
let target = findNode(r.endNode, d.nodes);
14271458

1459+
source.links = source.links ? Array.from(new Set([...source.links, target.labels[0]])) : [target.labels[0]];
1460+
14281461
(r.source = source),
14291462
(r.target = target),
14301463
(r.naturalAngle = 0),
@@ -1433,6 +1466,37 @@ function GraphD3(_selector: HTMLDivElement, _options: any): IGraphD3 {
14331466
})
14341467
return r
14351468
})
1469+
1470+
nodes.map(n => {
1471+
if (n.links !== undefined) {
1472+
let labels = {}
1473+
n.links.forEach((l, i) => {
1474+
labels[l] = i;
1475+
});
1476+
1477+
const equalAngles = 360 / Object.keys(labels).length
1478+
let angleIterator = 0
1479+
Object.keys(labels).map(l => {
1480+
labels[l] = angleIterator;
1481+
angleIterator += equalAngles;
1482+
});
1483+
1484+
n.targetLabels = labels;
1485+
}
1486+
})
1487+
1488+
d.relationships.map(r => {
1489+
const radius = 1300
1490+
const onlyOne = Object.keys(r.source.targetLabels).length === 1
1491+
1492+
const degree = Math.PI * 2 * r.source.targetLabels[r.target.labels[0]] / 360;
1493+
const angleX = onlyOne ? 0 : radius * Math.sin(degree)
1494+
const angleY = onlyOne ? 0 : radius * Math.cos(degree)
1495+
1496+
r.target.angleX = angleX;
1497+
r.target.angleY = angleY;
1498+
})
1499+
14361500
}
14371501

14381502
function groupedRelationships(): NodePair[] {
@@ -1441,7 +1505,7 @@ function GraphD3(_selector: HTMLDivElement, _options: any): IGraphD3 {
14411505
} = {}
14421506
for (const relationship of Array.from(relationships)) {
14431507
let nodePair = new NodePair(relationship.source, relationship.target)
1444-
nodePair = groups[nodePair.toString()] != null ? groups[nodePair.toString()] : nodePair
1508+
nodePair = groups[nodePair.toString()] != null ? groups[nodePair.toString()] : nodePair
14451509
nodePair.relationships.push(relationship)
14461510
groups[nodePair.toString()] = nodePair
14471511
}

redisinsight/ui/src/packages/redisgraph/src/styles/styles.less

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@
225225
border-radius: 4px;
226226
opacity: 1;
227227
margin-right: 12px !important;
228+
padding-right: 10px !important;
229+
padding-left: 10px !important;
228230
}
229231

230232
.box-edge-type {

redisinsight/ui/src/packages/redisgraph/src/utils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
import * as d3 from 'd3'
22

33
export function pulse(node: d3.Selection<SVGElement, any, any, any>) {
4-
var times = 0
4+
var times = 0;
55
(function repeat() {
66
node
77
.transition()
88
.duration(100)
9+
.attr("class", "")
910
.attr("data-pulse", "true")
1011
.attr("stroke", "purple")
1112
.attr("stroke-width", 0)
1213
.attr('stroke-opacity', 0)
1314
.transition()
1415
.duration(500)
1516
.attr("stroke-width", 0)
16-
.attr('stroke-opacity', 0.5)
17+
.attr('stroke-opacity', 1)
1718
.transition()
1819
.duration(1000)
1920
.attr("stroke-width", 65)
2021
.attr('stroke-opacity', 0)
2122
.ease(d3.easeCubicInOut)
2223
.on("end", () => {
2324
if (times === 3) {
24-
node.transition().attr("data-pulse", "")
25+
node.transition().attr("data-pulse", "").attr("class", "outline")
2526
return
2627
}
2728
times++

0 commit comments

Comments
 (0)