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