Skip to main content

Service Map Visualization

NopeSight provides powerful visualization capabilities to help you understand complex service architectures, dependencies, and real-time health status. Our interactive visualizations make it easy to explore relationships, identify issues, and plan changes.

Visualization Types

Application Dependency Mapping (ADM) View

The ADM view provides process-level visibility into application dependencies, showing exactly which processes communicate with each other across your infrastructure.

Key Features

  • Process Discovery: Click on any target node to discover which process is listening on the target port
  • Interactive Navigation: Navigate through process dependencies across multiple servers
  • Process Grouping: Automatically groups connections by process name for clarity
  • Focus Mode: Click on a process to focus only on its connections
  • Real-time Discovery: Discovers listening processes in real-time without requiring a full scan

Implementation

// ADM visualization uses ReactFlow for process dependency graphs
const ProcessDependencyGraph = () => {
const [nodes, setNodes] = useState([]);
const [edges, setEdges] = useState([]);
const [listeningProcesses, setListeningProcesses] = useState(new Map());

// Discover listening process when target node is clicked
const handleNodeClick = async (event, node) => {
if (node.data.isTarget) {
const targetPort = node.data.targetPorts?.[0];
const response = await api.get(
`/ci/${node.data.serverCiId}/listening-process/${targetPort}`
);

if (response.data.found) {
// Create intermediate process node
createListeningProcessNode(
node.data.serverCiId,
response.data.process.name,
targetPort
);
}
}
};
};

Service Topology View

Interactive Visualization Components

class ServiceMapVisualizer {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.width = this.container.clientWidth;
this.height = this.container.clientHeight;

// Initialize D3.js visualization
this.svg = d3.select(this.container)
.append('svg')
.attr('width', this.width)
.attr('height', this.height);

// Create zoom behavior
this.zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on('zoom', this.handleZoom.bind(this));

this.svg.call(this.zoom);

// Initialize force simulation
this.simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(d => d.id).distance(100))
.force('charge', d3.forceManyBody().strength(-500))
.force('center', d3.forceCenter(this.width / 2, this.height / 2))
.force('collision', d3.forceCollide().radius(50));
}

renderServiceMap(serviceData) {
// Clear existing visualization
this.svg.selectAll('*').remove();

// Create container for zoom
const container = this.svg.append('g')
.attr('class', 'zoom-container');

// Prepare data
const nodes = this.prepareNodes(serviceData);
const links = this.prepareLinks(serviceData);

// Create arrow markers for directed edges
this.createArrowMarkers(container);

// Draw links
const link = container.append('g')
.attr('class', 'links')
.selectAll('line')
.data(links)
.enter().append('line')
.attr('class', d => `link ${d.type}`)
.attr('stroke-width', d => Math.sqrt(d.weight))
.attr('marker-end', 'url(#arrowhead)');

// Draw nodes
const node = container.append('g')
.attr('class', 'nodes')
.selectAll('g')
.data(nodes)
.enter().append('g')
.attr('class', 'node')
.call(this.drag());

// Add node circles
node.append('circle')
.attr('r', d => this.getNodeRadius(d))
.attr('fill', d => this.getNodeColor(d))
.attr('stroke', '#fff')
.attr('stroke-width', 2);

// Add node icons
node.append('text')
.attr('class', 'node-icon')
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.text(d => this.getNodeIcon(d));

// Add node labels
node.append('text')
.attr('class', 'node-label')
.attr('x', 0)
.attr('y', d => this.getNodeRadius(d) + 15)
.attr('text-anchor', 'middle')
.text(d => d.name);

// Add health indicators
node.append('circle')
.attr('class', 'health-indicator')
.attr('r', 8)
.attr('cx', d => this.getNodeRadius(d) - 5)
.attr('cy', d => -this.getNodeRadius(d) + 5)
.attr('fill', d => this.getHealthColor(d.health));

// Add tooltips
this.addTooltips(node, link);

// Add interactivity
this.addInteractivity(node, link);

// Start simulation
this.simulation
.nodes(nodes)
.on('tick', () => this.tick(link, node));

this.simulation.force('link')
.links(links);
}

getNodeColor(node) {
const colorMap = {
'web': '#4CAF50',
'api': '#2196F3',
'service': '#FF9800',
'database': '#9C27B0',
'cache': '#00BCD4',
'external': '#F44336',
'queue': '#795548'
};

return colorMap[node.type] || '#757575';
}

getHealthColor(health) {
if (health >= 95) return '#4CAF50'; // Green
if (health >= 80) return '#8BC34A'; // Light Green
if (health >= 60) return '#FFC107'; // Amber
if (health >= 40) return '#FF9800'; // Orange
return '#F44336'; // Red
}
}

Visualization Layouts

Hierarchical Layout

Hierarchical Layout:
Purpose: Show service layers and dependencies
Best For:
- Business service hierarchy
- Technical stack visualization
- Data flow representation

Layout Options:
- Top-down: Business to infrastructure
- Left-right: Request flow
- Radial: Service ecosystem
- Nested: Component grouping

Visual Encoding:
- Y-axis: Service layer
- X-axis: Logical grouping
- Node size: Importance/traffic
- Edge width: Dependency strength

Force-Directed Layout

class ForceDirectedLayout:
def __init__(self):
self.forces = {
'repulsion': -500,
'attraction': 0.1,
'centering': 0.5,
'collision': 50
}

def calculate_layout(self, nodes, edges):
"""Calculate force-directed layout positions"""

positions = self.initialize_positions(nodes)

for iteration in range(100):
forces = {}

# Calculate repulsive forces between all nodes
for i, node1 in enumerate(nodes):
force = Vector2D(0, 0)

for j, node2 in enumerate(nodes):
if i != j:
diff = positions[node1.id] - positions[node2.id]
distance = diff.magnitude()

if distance > 0:
# Coulomb's law
repulsion = self.forces['repulsion'] / (distance ** 2)
force += diff.normalize() * repulsion

forces[node1.id] = force

# Calculate attractive forces along edges
for edge in edges:
source_pos = positions[edge.source]
target_pos = positions[edge.target]
diff = target_pos - source_pos
distance = diff.magnitude()

if distance > 0:
# Hooke's law
attraction = distance * self.forces['attraction']
force = diff.normalize() * attraction

forces[edge.source] -= force
forces[edge.target] += force

# Apply forces
for node in nodes:
positions[node.id] += forces[node.id] * 0.1

# Apply centering force
center_force = -positions[node.id] * self.forces['centering']
positions[node.id] += center_force * 0.1

return positions

Matrix View

class DependencyMatrix {
renderMatrix(services, dependencies) {
const matrix = [];
const nodes = services.map(s => s.name);

// Initialize matrix
nodes.forEach((source, i) => {
matrix[i] = [];
nodes.forEach((target, j) => {
matrix[i][j] = {
source: source,
target: target,
value: 0,
dependencies: []
};
});
});

// Populate matrix with dependencies
dependencies.forEach(dep => {
const sourceIndex = nodes.indexOf(dep.source);
const targetIndex = nodes.indexOf(dep.target);

if (sourceIndex >= 0 && targetIndex >= 0) {
matrix[sourceIndex][targetIndex].value++;
matrix[sourceIndex][targetIndex].dependencies.push(dep);
}
});

// Create heatmap visualization
const cellSize = 30;
const margin = {top: 100, right: 50, bottom: 50, left: 100};

const svg = d3.select('#matrix-container')
.append('svg')
.attr('width', cellSize * nodes.length + margin.left + margin.right)
.attr('height', cellSize * nodes.length + margin.top + margin.bottom);

const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);

// Color scale
const colorScale = d3.scaleSequential()
.interpolator(d3.interpolateBlues)
.domain([0, d3.max(matrix.flat(), d => d.value)]);

// Draw cells
const cells = g.selectAll('.cell')
.data(matrix.flat())
.enter().append('rect')
.attr('class', 'cell')
.attr('x', d => nodes.indexOf(d.target) * cellSize)
.attr('y', d => nodes.indexOf(d.source) * cellSize)
.attr('width', cellSize - 2)
.attr('height', cellSize - 2)
.attr('fill', d => d.value > 0 ? colorScale(d.value) : '#f0f0f0')
.on('mouseover', this.showDependencyDetails)
.on('click', this.drillIntoDepedency);

// Add labels
this.addMatrixLabels(g, nodes, cellSize);

return svg.node();
}
}

Real-time Visualization

Live Health Monitoring

Real-time Updates:
Data Sources:
- Metrics stream (1s intervals)
- Event stream (real-time)
- Alert notifications
- Performance counters

Visual Updates:
Health Status:
- Color changes
- Pulse animations
- Status icons
- Alert badges

Traffic Flow:
- Animated connections
- Flow direction
- Volume indicators
- Latency heatmap

Performance Metrics:
- Inline sparklines
- Gauge indicators
- Trend arrows
- Threshold alerts

Animation and Transitions

class AnimatedServiceMap {
animateHealthChange(nodeId, oldHealth, newHealth) {
const node = d3.select(`#node-${nodeId}`);
const healthIndicator = node.select('.health-indicator');

// Animate color transition
healthIndicator
.transition()
.duration(1000)
.attrTween('fill', () => {
const interpolate = d3.interpolateRgb(
this.getHealthColor(oldHealth),
this.getHealthColor(newHealth)
);
return t => interpolate(t);
});

// Add pulse effect for critical changes
if (newHealth < 50 && oldHealth >= 50) {
this.addPulseAnimation(node);
}

// Update metrics display
this.updateNodeMetrics(nodeId, {health: newHealth});
}

animateTrafficFlow(linkId, volume) {
const link = d3.select(`#link-${linkId}`);

// Create flow particles
const particles = [];
const particleCount = Math.min(volume / 100, 10);

for (let i = 0; i < particleCount; i++) {
particles.push({
id: `particle-${linkId}-${i}`,
delay: i * 200
});
}

// Animate particles along path
particles.forEach(particle => {
const circle = link.append('circle')
.attr('r', 3)
.attr('fill', '#2196F3')
.attr('opacity', 0.8);

circle.transition()
.delay(particle.delay)
.duration(2000)
.attrTween('transform', () => {
const path = link.node();
const length = path.getTotalLength();

return t => {
const point = path.getPointAtLength(t * length);
return `translate(${point.x},${point.y})`;
};
})
.on('end', () => circle.remove());
});
}
}

Interactive Features

Drill-Down Navigation

class InteractiveNavigation:
def setup_drill_down(self, visualization):
"""Enable drill-down navigation in service maps"""

# Node click handler
def on_node_click(node):
if node.type == 'service_group':
# Expand service group
expanded_view = self.expand_service_group(node)
visualization.transition_to_view(expanded_view)

elif node.type == 'service':
# Show service details
self.show_service_details(node)

elif node.type == 'component':
# Drill into component
component_view = self.create_component_view(node)
visualization.render_view(component_view)

# Edge click handler
def on_edge_click(edge):
# Show dependency details
dependency_details = {
'source': edge.source,
'target': edge.target,
'protocol': edge.protocol,
'traffic_volume': self.get_traffic_volume(edge),
'latency': self.get_latency_metrics(edge),
'error_rate': self.get_error_rate(edge),
'recent_changes': self.get_recent_changes(edge)
}

self.show_dependency_panel(dependency_details)

# Register handlers
visualization.on('node:click', on_node_click)
visualization.on('edge:click', on_edge_click)
class ServiceMapFilters {
constructor(visualization) {
this.viz = visualization;
this.activeFilters = new Set();

this.filterTypes = {
health: {
critical: health => health < 50,
warning: health => health >= 50 && health < 80,
healthy: health => health >= 80
},
type: {
services: node => node.type === 'service',
databases: node => node.type === 'database',
external: node => node.type === 'external'
},
criticality: {
critical: node => node.criticality === 'critical',
high: node => node.criticality === 'high',
medium: node => node.criticality === 'medium',
low: node => node.criticality === 'low'
}
};
}

applyFilter(filterType, filterValue) {
const filter = this.filterTypes[filterType][filterValue];

if (!filter) return;

// Update active filters
const filterId = `${filterType}:${filterValue}`;
if (this.activeFilters.has(filterId)) {
this.activeFilters.delete(filterId);
} else {
this.activeFilters.add(filterId);
}

// Apply visual filtering
this.viz.selectAll('.node')
.transition()
.duration(300)
.style('opacity', node => {
// Check all active filters
for (const activeFilter of this.activeFilters) {
const [type, value] = activeFilter.split(':');
const filterFn = this.filterTypes[type][value];

if (!filterFn(node)) {
return 0.2; // Dimmed
}
}
return 1; // Full opacity
});

// Update edges based on filtered nodes
this.updateEdgeVisibility();
}

searchServices(query) {
const lowerQuery = query.toLowerCase();

this.viz.selectAll('.node')
.transition()
.duration(300)
.style('opacity', node => {
const matches = node.name.toLowerCase().includes(lowerQuery) ||
node.description?.toLowerCase().includes(lowerQuery) ||
node.tags?.some(tag => tag.toLowerCase().includes(lowerQuery));

return matches ? 1 : 0.2;
})
.style('stroke', node => {
const matches = node.name.toLowerCase().includes(lowerQuery);
return matches ? '#FF5722' : '#fff';
})
.style('stroke-width', node => {
const matches = node.name.toLowerCase().includes(lowerQuery);
return matches ? 4 : 2;
});
}
}

Advanced Visualizations

3D Service Maps

class ServiceMap3D {
constructor(containerId) {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);

this.renderer = new THREE.WebGLRenderer();
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById(containerId).appendChild(this.renderer.domElement);

// Add controls
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);

// Add lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4);
directionalLight.position.set(10, 10, 10);
this.scene.add(directionalLight);

this.camera.position.z = 50;
}

render3DServiceMap(serviceData) {
// Create service layers
const layers = this.organizeIntoLayers(serviceData);

layers.forEach((layer, layerIndex) => {
const y = (layerIndex - layers.length / 2) * 20;

layer.nodes.forEach((node, nodeIndex) => {
// Create node geometry
const geometry = this.getNodeGeometry(node.type);
const material = new THREE.MeshPhongMaterial({
color: this.getNodeColor(node),
transparent: true,
opacity: 0.8
});

const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(
(nodeIndex - layer.nodes.length / 2) * 15,
y,
0
);

mesh.userData = node;
this.scene.add(mesh);

// Add label
this.addLabel(mesh, node.name);

// Store for connection drawing
node.mesh = mesh;
});
});

// Draw connections
serviceData.dependencies.forEach(dep => {
const source = this.findNodeMesh(dep.source);
const target = this.findNodeMesh(dep.target);

if (source && target) {
const connection = this.createConnection(source, target, dep);
this.scene.add(connection);
}
});

// Start animation loop
this.animate();
}

createConnection(source, target, dependency) {
const points = [];
points.push(source.position);

// Create curved path
const midPoint = new THREE.Vector3();
midPoint.addVectors(source.position, target.position);
midPoint.divideScalar(2);
midPoint.y += 10; // Arc height

const curve = new THREE.QuadraticBezierCurve3(
source.position,
midPoint,
target.position
);

const geometry = new THREE.TubeGeometry(curve, 20, 0.5, 8, false);
const material = new THREE.MeshPhongMaterial({
color: this.getDependencyColor(dependency),
transparent: true,
opacity: 0.6
});

return new THREE.Mesh(geometry, material);
}
}

Time-Series Visualization

class ServiceTimeSeriesViz:
def create_service_timeline(self, service_id, time_range):
"""Create time-series visualization of service health"""

# Fetch historical data
history = self.get_service_history(service_id, time_range)

# Create timeline visualization
fig = go.Figure()

# Add health score trace
fig.add_trace(go.Scatter(
x=history['timestamps'],
y=history['health_scores'],
mode='lines',
name='Health Score',
line=dict(color='green', width=2)
))

# Add incident markers
incidents = history['incidents']
fig.add_trace(go.Scatter(
x=[i['timestamp'] for i in incidents],
y=[i['health_at_incident'] for i in incidents],
mode='markers',
name='Incidents',
marker=dict(
color='red',
size=10,
symbol='x'
),
text=[i['description'] for i in incidents],
hoverinfo='text'
))

# Add change markers
changes = history['changes']
fig.add_trace(go.Scatter(
x=[c['timestamp'] for c in changes],
y=[c['health_after_change'] for c in changes],
mode='markers',
name='Changes',
marker=dict(
color='blue',
size=8,
symbol='triangle-up'
),
text=[c['description'] for c in changes],
hoverinfo='text'
))

# Add annotations for significant events
for event in history['significant_events']:
fig.add_annotation(
x=event['timestamp'],
y=event['health_score'],
text=event['label'],
showarrow=True,
arrowhead=2,
arrowsize=1,
arrowwidth=2,
arrowcolor='black',
ax=0,
ay=-40
)

# Update layout
fig.update_layout(
title=f'Service Health Timeline: {service_id}',
xaxis_title='Time',
yaxis_title='Health Score',
hovermode='x',
showlegend=True
)

return fig

Performance Optimization

Large-Scale Visualization

Performance Strategies:
Data Reduction:
- Level-of-detail (LOD) rendering
- Viewport culling
- Node aggregation
- Edge bundling

Rendering Optimization:
- WebGL acceleration
- Canvas rendering for large datasets
- Virtual scrolling
- Progressive rendering

Interaction Optimization:
- Debounced updates
- Request animation frame
- Web workers for layout
- Lazy loading

Canvas Rendering

class CanvasServiceMap {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.nodes = [];
this.edges = [];
this.viewport = {
x: 0,
y: 0,
scale: 1
};
}

render() {
// Clear canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

// Save context
this.ctx.save();

// Apply viewport transformation
this.ctx.translate(this.viewport.x, this.viewport.y);
this.ctx.scale(this.viewport.scale, this.viewport.scale);

// Render edges first (lower z-index)
this.renderEdges();

// Render nodes
this.renderNodes();

// Restore context
this.ctx.restore();

// Request next frame
requestAnimationFrame(() => this.render());
}

renderNodes() {
// Cull nodes outside viewport
const visibleNodes = this.nodes.filter(node =>
this.isInViewport(node)
);

visibleNodes.forEach(node => {
// Draw node circle
this.ctx.beginPath();
this.ctx.arc(node.x, node.y, node.radius, 0, 2 * Math.PI);
this.ctx.fillStyle = this.getNodeColor(node);
this.ctx.fill();
this.ctx.strokeStyle = '#fff';
this.ctx.lineWidth = 2;
this.ctx.stroke();

// Draw health indicator
if (node.health < 80) {
this.ctx.beginPath();
this.ctx.arc(
node.x + node.radius - 5,
node.y - node.radius + 5,
5,
0,
2 * Math.PI
);
this.ctx.fillStyle = this.getHealthColor(node.health);
this.ctx.fill();
}

// Draw label
this.ctx.fillStyle = '#000';
this.ctx.font = '12px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText(
node.name,
node.x,
node.y + node.radius + 15
);
});
}

isInViewport(node) {
const buffer = 50; // Extra margin
const viewBounds = {
left: -this.viewport.x / this.viewport.scale - buffer,
right: (-this.viewport.x + this.canvas.width) / this.viewport.scale + buffer,
top: -this.viewport.y / this.viewport.scale - buffer,
bottom: (-this.viewport.y + this.canvas.height) / this.viewport.scale + buffer
};

return node.x >= viewBounds.left &&
node.x <= viewBounds.right &&
node.y >= viewBounds.top &&
node.y <= viewBounds.bottom;
}
}

Export and Sharing

Export Formats

class VisualizationExporter:
def export_visualization(self, visualization, format='png'):
"""Export visualization in various formats"""

exporters = {
'png': self.export_as_png,
'svg': self.export_as_svg,
'pdf': self.export_as_pdf,
'json': self.export_as_json,
'graphml': self.export_as_graphml
}

if format not in exporters:
raise ValueError(f"Unsupported format: {format}")

return exporters[format](visualization)

def export_as_svg(self, visualization):
"""Export as vector SVG"""

svg_content = visualization.get_svg_content()

# Add metadata
metadata = f"""
<metadata>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<rdf:Description>
<dc:title>{visualization.title}</dc:title>
<dc:creator>NopeSight Service Mapping</dc:creator>
<dc:date>{datetime.now().isoformat()}</dc:date>
<dc:description>{visualization.description}</dc:description>
</rdf:Description>
</rdf:RDF>
</metadata>
"""

# Insert metadata
svg_content = svg_content.replace(
'<svg',
f'<svg xmlns:dc="http://purl.org/dc/elements/1.1/"'
)
svg_content = svg_content.replace(
'>',
f'>{metadata}',
1
)

return svg_content

Sharing and Collaboration

Sharing Options:
Direct Sharing:
- Shareable links
- Embedded widgets
- API access
- Webhook notifications

Export Options:
- Image formats (PNG, JPEG)
- Vector formats (SVG, PDF)
- Data formats (JSON, GraphML)
- Interactive HTML

Collaboration Features:
- Annotations
- Comments
- Version history
- Real-time updates

Best Practices

1. Visual Design

  • ✅ Use consistent color coding
  • ✅ Provide clear legends
  • ✅ Minimize visual clutter
  • ✅ Enable progressive disclosure

2. Performance

  • ✅ Implement viewport culling
  • ✅ Use appropriate rendering technology
  • ✅ Optimize for common screen sizes
  • ✅ Cache computed layouts

3. Usability

  • ✅ Provide multiple view options
  • ✅ Enable easy navigation
  • ✅ Include search and filter
  • ✅ Support keyboard shortcuts

4. Accessibility

  • ✅ Provide text alternatives
  • ✅ Support screen readers
  • ✅ Use ARIA labels
  • ✅ Enable keyboard navigation

Integration Examples

Dashboard Integration

class ServiceMapDashboard {
integrateServiceMap(dashboardId, serviceMapConfig) {
const dashboard = this.getDashboard(dashboardId);

// Create service map widget
const widget = {
id: 'service-map-main',
type: 'service-map',
title: 'Service Architecture',
position: {x: 0, y: 0, w: 8, h: 6},
config: {
serviceId: serviceMapConfig.serviceId,
viewType: 'force-directed',
showHealth: true,
showTraffic: true,
autoRefresh: 30000,
filters: ['critical-services']
}
};

// Add complementary widgets
const healthWidget = {
id: 'service-health-summary',
type: 'health-grid',
title: 'Service Health',
position: {x: 8, y: 0, w: 4, h: 3},
config: {
serviceId: serviceMapConfig.serviceId,
showSparklines: true
}
};

const dependencyWidget = {
id: 'dependency-matrix',
type: 'matrix',
title: 'Dependencies',
position: {x: 8, y: 3, w: 4, h: 3},
config: {
serviceId: serviceMapConfig.serviceId,
interactive: true
}
};

dashboard.addWidgets([widget, healthWidget, dependencyWidget]);

// Setup widget communication
this.setupWidgetCommunication([widget, healthWidget, dependencyWidget]);

return dashboard;
}
}

Next Steps