Highlight immediate siblings on hover.

This commit is contained in:
Kia Lam
2022-01-18 09:41:16 -08:00
parent 1246b14e7e
commit 826a069be0

View File

@@ -2,8 +2,102 @@ import React, { useEffect, useCallback } from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import * as d3 from 'd3'; import * as d3 from 'd3';
function MeshGraph({ data }) { // function MeshGraph({ data }) {
console.log('data', data); function MeshGraph() {
const data = {
nodes: [
{
hostname: "aapc1.local",
node_state: "healthy",
node_type: "control",
id: 1
},
{
hostname: "aapc2.local",
node_type: "control",
node_state: "disabled",
id: 2
},
{
hostname: "aapc3.local",
node_type: "control",
node_state: "healthy",
id: 3
},
{
hostname: "aape1.local",
node_type: "execution",
node_state: "error",
id: 4
},
{
hostname: "aape2.local",
node_type: "execution",
node_state: "error",
id: 5
},
{
hostname: "aape3.local",
node_type: "execution",
node_state: "healthy",
id: 6
},
{
hostname: "aape4.local",
node_type: "execution",
node_state: "healthy",
id: 7
},
{
hostname: "aaph1.local",
node_type: "hop",
node_state: "disabled",
id: 8
},
{
hostname: "aaph2.local",
node_type: "hop",
node_state: "healthy",
id: 9
},
{
hostname: "aaph3.local",
node_type: "hop",
node_state: "error",
id: 10
}
],
links: [
{ source: "aapc1.local", target: "aapc2.local" },
{ source: "aapc1.local", target: "aapc3.local" },
{ source: "aapc1.local", target: "aape1.local" },
{ source: "aapc1.local", target: "aape2.local" },
{ source: "aapc2.local", target: "aapc3.local" },
{ source: "aapc2.local", target: "aape1.local" },
{ source: "aapc2.local", target: "aape2.local" },
{ source: "aapc3.local", target: "aape1.local" },
{ source: "aapc3.local", target: "aape2.local" },
{ source: "aape3.local", target: "aaph1.local" },
{ source: "aape3.local", target: "aaph2.local" },
{ source: "aape4.local", target: "aaph3.local" },
{ source: "aaph1.local", target: "aapc1.local" },
{ source: "aaph1.local", target: "aapc2.local" },
{ source: "aaph1.local", target: "aapc3.local" },
{ source: "aaph2.local", target: "aapc1.local" },
{ source: "aaph2.local", target: "aapc2.local" },
{ source: "aaph2.local", target: "aapc3.local" },
{ source: "aaph3.local", target: "aaph1.local" },
{ source: "aaph3.local", target: "aaph2.local" }
]
};
const draw = useCallback(() => { const draw = useCallback(() => {
const margin = 80; const margin = 80;
const getWidth = () => { const getWidth = () => {
@@ -22,6 +116,15 @@ function MeshGraph({ data }) {
}; };
const width = getWidth(); const width = getWidth();
const height = 600; const height = 600;
const defaultRadius = 6;
const highlightRadius = 9;
const zoom = d3
.zoom()
.scaleExtent([1, 8])
.on('zoom', function (event) {
svg.selectAll('.links, .nodes').attr('transform', event.transform);
});
/* Add SVG */ /* Add SVG */
d3.selectAll(`#chart > *`).remove(); d3.selectAll(`#chart > *`).remove();
@@ -32,9 +135,11 @@ function MeshGraph({ data }) {
.attr('width', `${width + margin}px`) .attr('width', `${width + margin}px`)
.attr('height', `${height + margin}px`) .attr('height', `${height + margin}px`)
.append('g') .append('g')
.attr('transform', `translate(${margin}, ${margin})`); .attr('transform', `translate(${margin}, ${margin})`)
.call(zoom);
const color = d3.scaleOrdinal(d3.schemeCategory10); const color = d3.scaleOrdinal(d3.schemeCategory10);
const graph = data;
const simulation = d3 const simulation = d3
.forceSimulation() .forceSimulation()
@@ -55,32 +160,31 @@ function MeshGraph({ data }) {
) )
.force('center', d3.forceCenter(width / 2, height / 2)); .force('center', d3.forceCenter(width / 2, height / 2));
const graph = data;
const link = svg const link = svg
.append('g') .append('g')
.attr('class', 'links') .attr('class', `links`)
.selectAll('path') .selectAll('path')
.data(graph.links) .data(graph.links)
.enter() .enter()
.append('path') .append('path')
.attr('class', (d, i) => `link-${i}`)
.style('fill', 'none') .style('fill', 'none')
.style('stroke', '#ccc') .style('stroke', '#ccc')
.style('stroke-width', '2px') .style('stroke-width', '2px')
.attr('pointer-events', 'visibleStroke') .attr('pointer-events', 'none')
.on('mouseover', function (event, d) { .on('mouseover', function (event, d) {
tooltip // tooltip
.html(`source: ${d.source.hostname} <br>target: ${d.target.hostname}`) // .html(`source: ${d.source.hostname} <br>target: ${d.target.hostname}`)
.style('visibility', 'visible'); // .style('visibility', 'visible');
d3.select(this).transition().style('cursor', 'pointer'); d3.select(this).transition().style('cursor', 'pointer');
}) })
.on('mousemove', function () { .on('mousemove', function () {
tooltip // tooltip
.style('top', event.pageY - 10 + 'px') // .style('top', event.pageY - 10 + 'px')
.style('left', event.pageX + 10 + 'px'); // .style('left', event.pageX + 10 + 'px');
}) })
.on('mouseout', function () { .on('mouseout', function () {
tooltip.html(``).style('visibility', 'hidden'); // tooltip.html(``).style('visibility', 'hidden');
}); });
const node = svg const node = svg
@@ -90,20 +194,24 @@ function MeshGraph({ data }) {
.data(graph.nodes) .data(graph.nodes)
.enter() .enter()
.append('g') .append('g')
.on('mouseover', function (event, d) { .on('mouseenter', function (event, d) {
d3.select(this).transition().style('cursor', 'pointer');
highlightSiblings(d)
tooltip tooltip
.html( .html(
`name: ${d.hostname} <br>type: ${d.node_type} <br>status: ${d.node_state}` `<h3>Details</h3> <hr>name: ${d.hostname} <br>type: ${d.node_type} <br>status: ${d.node_state} <br> <a>Click on a node to view the details</a>`
) )
.style('visibility', 'visible'); .style('visibility', 'visible')
// .style('visibility', 'visible');
// d3.select(this).transition().attr('r', 9).style('cursor', 'pointer'); // d3.select(this).transition().attr('r', 9).style('cursor', 'pointer');
}) })
.on('mousemove', function () { .on('mousemove', function () {
tooltip // tooltip
.style('top', event.pageY - 10 + 'px') // .style('top', event.pageY - 10 + 'px')
.style('left', event.pageX + 10 + 'px'); // .style('left', event.pageX + 10 + 'px');
}) })
.on('mouseout', function () { .on('mouseleave', function (event, d) {
deselectSiblings(d)
tooltip.html(``).style('visibility', 'hidden'); tooltip.html(``).style('visibility', 'hidden');
// d3.select(this).attr('r', 6); // d3.select(this).attr('r', 6);
}); });
@@ -112,30 +220,38 @@ function MeshGraph({ data }) {
.append('circle') .append('circle')
.attr('r', 8) .attr('r', 8)
.attr('class', (d) => d.node_state) .attr('class', (d) => d.node_state)
.attr('stroke', d => d.node_state === 'disabled' ? '#c6c6c6' : '#50D050') .attr('stroke', d => renderHealthColor(d.node_state))
.attr('fill', d => d.node_state === 'disabled' ? '#c6c6c6' : '#50D050'); .attr('fill', d => renderHealthColor(d.node_state));
const nodeRings = node const nodeRings = node
.append('circle') .append('circle')
.attr('r', 6) .attr('r', defaultRadius)
.attr('class', (d) => d.node_type) .attr('class', (d) => d.node_type)
.attr('class', (d) => `id-${d.id}`)
.attr('fill', function (d) { .attr('fill', function (d) {
return color(d.node_type); return color(d.node_type);
}); })
.attr('stroke', 'white');
svg.call(expandGlow); svg.call(expandGlow);
const legend = svg const legend = svg
.append('text')
.attr('x', 10)
.attr('y', 20)
.text('Legend')
svg
.append('g') .append('g')
.attr('class', 'chart-legend')
.selectAll('g') .selectAll('g')
.attr('class', 'chart-legend')
.data(graph.nodes) .data(graph.nodes)
.enter() .enter()
.append('circle') .append('circle')
.attr('cx', 10) .attr('cx', 10)
.attr('cy', function (d, i) { .attr('cy', function (d, i) {
return 100 + i * 25; return 50 + i * 25;
}) })
.attr('r', 7) .attr('r', defaultRadius)
.attr('class', (d) => d.node_type) .attr('class', (d) => d.node_type)
.style('fill', function (d) { .style('fill', function (d) {
return color(d.node_type); return color(d.node_type);
@@ -150,7 +266,7 @@ function MeshGraph({ data }) {
.append('text') .append('text')
.attr('x', 20) .attr('x', 20)
.attr('y', function (d, i) { .attr('y', function (d, i) {
return 100 + i * 25; return 50 + i * 25;
}) })
.text((d) => `${d.hostname} - ${d.node_type}`) .text((d) => `${d.hostname} - ${d.node_type}`)
.attr('text-anchor', 'left') .attr('text-anchor', 'left')
@@ -161,14 +277,20 @@ function MeshGraph({ data }) {
.append('div') .append('div')
.attr('class', 'd3-tooltip') .attr('class', 'd3-tooltip')
.style('position', 'absolute') .style('position', 'absolute')
.style('top', '200px')
.style('right', '40px')
.style('z-index', '10') .style('z-index', '10')
.style('visibility', 'hidden') .style('visibility', 'hidden')
.style('padding', '15px') .style('padding', '15px')
.style('background', 'rgba(0,0,0,0.6)') // .style('border', '1px solid #e6e6e6')
.style('border-radius', '5px') // .style('box-shadow', '5px 5px 5px #e6e6e6')
.style('color', '#fff') .style('max-width', '15%')
// .style('background', 'rgba(0,0,0,0.6)')
// .style('border-radius', '5px')
// .style('color', '#fff')
.style('font-family', 'sans-serif') .style('font-family', 'sans-serif')
.text('a simple tooltip'); .style('color', '#e6e6e')
.text('');
const labels = node const labels = node
.append('text') .append('text')
@@ -183,6 +305,7 @@ function MeshGraph({ data }) {
function ticked() { function ticked() {
link.attr('d', linkArc); link.attr('d', linkArc);
node.attr('transform', function (d) { node.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')'; return 'translate(' + d.x + ',' + d.y + ')';
}); });
@@ -209,7 +332,8 @@ function MeshGraph({ data }) {
} }
function contractGlow() { function contractGlow() {
healthRings svg
.selectAll('.healthy')
.transition() .transition()
.duration(1000) .duration(1000)
.attr('stroke-width', '1px') .attr('stroke-width', '1px')
@@ -217,21 +341,79 @@ function MeshGraph({ data }) {
} }
function expandGlow() { function expandGlow() {
healthRings svg
.selectAll('.healthy')
.transition() .transition()
.duration(1000) .duration(1000)
.attr('stroke-width', '4.5px') .attr('stroke-width', '4.5px')
.on('end', contractGlow); .on('end', contractGlow);
} }
const zoom = d3 function renderHealthColor(nodeState) {
.zoom() const colorKey = {
.scaleExtent([1, 8]) 'disabled': '#c6c6c6',
.on('zoom', function (event) { 'healthy': '#50D050',
svg.selectAll('.links, .nodes').attr('transform', event.transform); 'error': '#ff6766'
}); };
return colorKey[nodeState];
}
svg.call(zoom); function renderNodeClass(nodeState) {
const colorKey = {
'disabled': 'node-disabled',
'healthy': 'node-healthy',
'error': 'node-error'
};
return colorKey[nodeState];
}
function highlightSiblings(node) {
setTimeout(function() {
svg.selectAll(`id-${node.id}`)
.attr('r', highlightRadius);
const immediate = graph.links.filter(link => node.hostname === link.source.hostname || node.hostname === link.target.hostname);
immediate.forEach(s => {
const links = svg.selectAll(`.link-${s.index}`)
.transition()
.style('stroke', '#6e6e6e')
const sourceNodes = svg.selectAll(`.id-${s.source.id}`)
.transition()
.attr('r', highlightRadius)
const targetNodes = svg.selectAll(`.id-${s.target.id}`)
.transition()
.attr('r', highlightRadius)
})
}, 0)
}
function deselectSiblings(node) {
svg.selectAll(`id-${node.id}`)
.attr('r', defaultRadius);
const immediate = graph.links.filter(link => node.hostname === link.source.hostname || node.hostname === link.target.hostname);
immediate.forEach(s => {
const links = svg.selectAll(`.link-${s.index}`)
.transition()
.style('stroke', '#ccc')
svg.selectAll(`.id-${s.source.id}`)
.transition()
.attr('r', defaultRadius)
svg.selectAll(`.id-${s.target.id}`)
.transition()
.attr('r', defaultRadius)
})
}
// const zoom = d3
// .zoom()
// .scaleExtent([1, 8])
// .on('zoom', function (event) {
// svg.selectAll('.links, .nodes').attr('transform', event.transform);
// });
// svg.call(zoom);
// node.call(zoom);
// link.call(zoom);
}, [data]); }, [data]);
useEffect(() => { useEffect(() => {