Add VisNetwork integration to CareerMapping component

Updated the CareerMapping component to utilize VisNetwork for visualizing relationships between events and career fields. Replaced the previous Mermaid diagram implementation with a network graph, enhancing interactivity and visual clarity. Adjusted data generation logic to support the new network structure and updated relevant namespaces in the project files.
This commit is contained in:
2025-12-29 12:58:30 -05:00
parent 7266ab609b
commit 1d3167710d
4 changed files with 76 additions and 60 deletions
@@ -1,9 +1,9 @@
@page "/events/career-mapping"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@using WebApp.Components.Shared.Components
@using Core.Utility
@using Core.Entities
@using VisNetwork.Blazor.Models
@using Edge = VisNetwork.Blazor.Models.Edge
@inject AppDbContext Context
<PageHeader
@@ -19,7 +19,7 @@
<MudText Typo="Typo.body1" Class="mt-4">Loading career mapping data...</MudText>
</MudPaper>
}
else if (string.IsNullOrWhiteSpace(_mermaidDefinition))
else if (_networkData == null || !_networkData.Nodes.Any())
{
<MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h6" Class="mb-4">No Career Field Mappings Found</MudText>
@@ -34,17 +34,17 @@ else
<MudText Typo="Typo.h6" Class="mb-4">Event-Career Field Relationships</MudText>
<MudText Typo="Typo.body2" Class="mb-4 mud-text-secondary">
This diagram shows the connections between events and their related career fields.
Events are shown on the left, career fields on the right. Career fields are clusters of related careers.
Events are shown in blue, career fields in green. Career fields are clusters of related careers.
Use mouse to zoom and pan the graph.
</MudText>
<div class="mermaid-diagram-container" style="overflow-x: auto; min-height: 400px;">
<MermaidDiagram Definition="@_mermaidDefinition" />
<div style="width: 100%; height: 600px; border: 1px solid #ddd; border-radius: 4px;">
<Network Id="careerMappingNetwork" Data="@_networkData" Options="@GetNetworkOptions"/>
</div>
<MudText Style="white-space:pre-wrap;">@_mermaidDefinition</MudText>
</MudPaper>
}
@code {
private string _mermaidDefinition = string.Empty;
private NetworkData? _networkData;
private bool _isLoading = true;
protected override async Task OnInitializedAsync()
@@ -71,7 +71,7 @@ else
if (eventsWithFields.Any())
{
_mermaidDefinition = GenerateMermaidDiagram(eventsWithFields);
_networkData = GenerateNetworkData(eventsWithFields);
}
}
finally
@@ -80,16 +80,16 @@ else
}
}
private string GenerateMermaidDiagram(List<EventDefinition> events)
private NetworkData GenerateNetworkData(List<EventDefinition> events)
{
var builder = new System.Text.StringBuilder();
builder.AppendLine("graph LR");
var nodes = new List<Node>();
var edges = new List<Edge>();
// Dictionary to track node IDs and labels (to avoid duplicates)
var eventNodeIds = new Dictionary<int, (string Id, string Label)>();
var fieldNodeIds = new Dictionary<int, (string Id, string Label)>();
var fieldCounter = 1;
// Dictionary to track node IDs (to avoid duplicates)
var eventNodeIds = new Dictionary<int, string>();
var fieldNodeIds = new Dictionary<int, string>();
var eventCounter = 1;
var fieldCounter = 1;
// Dictionary to track which events connect to which career fields
var eventToFields = new Dictionary<int, HashSet<CareerField>>();
@@ -100,8 +100,17 @@ else
if (!eventNodeIds.ContainsKey(evt.Id))
{
var eventNodeId = $"E{eventCounter++}";
var eventLabel = EscapeMermaidLabel(evt.Name);
eventNodeIds[evt.Id] = (eventNodeId, eventLabel);
eventNodeIds[evt.Id] = eventNodeId;
// Add event node (blue)
nodes.Add(new Node
{
Id = eventNodeId,
Label = evt.Name,
Color = new NodeColorType { Background = "#90C3F5", Border = "#7DB3F0" },
Shape = "box",
Size = 25
});
}
// Get related career fields for this event's careers
@@ -110,65 +119,70 @@ else
{
eventToFields[evt.Id] = new HashSet<CareerField>(relatedFields);
// Track career field nodes
// Track and add career field nodes
foreach (var field in relatedFields)
{
if (!fieldNodeIds.ContainsKey(field.Id))
{
var fieldNodeId = $"F{fieldCounter++}";
var fieldLabel = EscapeMermaidLabel(field.Name);
fieldNodeIds[field.Id] = (fieldNodeId, fieldLabel);
}
}
}
}
fieldNodeIds[field.Id] = fieldNodeId;
// Second pass: define all event nodes (blue)
foreach (var (id, (nodeId, label)) in eventNodeIds.OrderBy(e => e.Value.Label))
// Add career field node (green)
nodes.Add(new Node
{
builder.AppendLine($" {nodeId}[\"{label}\"]:::eventNode");
}
// Third pass: define all career field nodes (green)
foreach (var (id, (nodeId, label)) in fieldNodeIds.OrderBy(f => f.Value.Label))
Id = fieldNodeId,
Label = field.Name,
Color = new NodeColorType
{
builder.AppendLine($" {nodeId}[\"{label}\"]:::fieldNode");
Background = "#90F8B0",
Border = "#80E8A0",
Highlight = new NodeColorType.BorderBackgroundColor { Background = "#90F8B0", Border = "#80E8A0" }
},
Shape = "box",
Size = 20
});
}
}
}
}
// Fourth pass: define all edges (events on left, fields on right)
// Second pass: create edges from events to career fields
foreach (var evt in events.OrderBy(e => e.Name))
{
if (eventToFields.TryGetValue(evt.Id, out var fields))
{
var currentEventNodeId = eventNodeIds[evt.Id].Id;
var eventNodeId = eventNodeIds[evt.Id];
foreach (var field in fields.OrderBy(f => f.Name))
{
var currentFieldNodeId = fieldNodeIds[field.Id].Id;
builder.AppendLine($" {currentEventNodeId} --> {currentFieldNodeId}");
}
}
}
// Add styling for different colors
builder.AppendLine("");
builder.AppendLine("classDef eventNode fill:#4A90E2,stroke:#2E5C8A,stroke-width:2px,color:#fff");
builder.AppendLine("classDef fieldNode fill:#50C878,stroke:#2D8659,stroke-width:2px,color:#fff");
return builder.ToString();
}
private string EscapeMermaidLabel(string label)
var fieldNodeId = fieldNodeIds[field.Id];
edges.Add(new Edge
{
if (string.IsNullOrEmpty(label))
return string.Empty;
// Escape quotes and other special characters for Mermaid labels
return label
.Replace("\"", "&quot;")
.Replace("\n", " ")
.Replace("\r", " ")
.Trim();
From = eventNodeId,
To = fieldNodeId
});
}
}
}
return new NetworkData
{
Nodes = nodes,
Edges = edges
};
}
private NetworkOptions GetNetworkOptions(Network network)
{
return new NetworkOptions
{
AutoResize = true,
Physics = new PhysicsOptions
{
Enabled = true
},
Height = "600px"
};
}
}
+1 -1
View File
@@ -26,4 +26,4 @@
@using MudBlazor
@using Core.Entities
@using Data
@using Blazorade.Mermaid.Components
@using VisNetwork.Blazor
+2
View File
@@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
using MudBlazor.Services;
using Serilog;
using System.Text.Json;
using VisNetwork.Blazor;
using WebApp;
using WebApp.Authentication;
using WebApp.Components;
@@ -118,6 +119,7 @@ builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddMudServices();
builder.Services.AddVisNetwork();
// Configure Data Protection to persist keys across container restarts
var keysPath = Path.Combine(builder.Environment.ContentRootPath, "DataProtectionKeys");
+1 -1
View File
@@ -14,8 +14,8 @@
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="BlazorSortableList" Version="2.1.0" />
<PackageReference Include="Blazorade.Mermaid" Version="1.3.0" />
<PackageReference Include="Heron.MudCalendar" Version="3.4.0" />
<PackageReference Include="VisNetwork.Blazor" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="9.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.8" />