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:
@@ -1,25 +1,25 @@
|
||||
@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
|
||||
Title="Career Mapping"
|
||||
Subtitle="Event-Career Relationships"
|
||||
ShowBackButton="true"
|
||||
BackButtonUrl="/events" />
|
||||
BackButtonUrl="/events"/>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<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>
|
||||
</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>
|
||||
@@ -33,18 +33,18 @@ else
|
||||
<MudPaper Elevation="2" Class="pa-6">
|
||||
<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.
|
||||
This diagram shows the connections between events and their related career fields.
|
||||
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()
|
||||
@@ -55,7 +55,7 @@ else
|
||||
private async Task LoadDataAsync()
|
||||
{
|
||||
_isLoading = true;
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var events = await Context.Events
|
||||
@@ -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;
|
||||
|
||||
// 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)
|
||||
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)
|
||||
// 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}");
|
||||
var fieldNodeId = fieldNodeIds[field.Id];
|
||||
edges.Add(new Edge
|
||||
{
|
||||
From = eventNodeId,
|
||||
To = fieldNodeId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
return new NetworkData
|
||||
{
|
||||
Nodes = nodes,
|
||||
Edges = edges
|
||||
};
|
||||
}
|
||||
|
||||
private string EscapeMermaidLabel(string label)
|
||||
private NetworkOptions GetNetworkOptions(Network network)
|
||||
{
|
||||
if (string.IsNullOrEmpty(label))
|
||||
return string.Empty;
|
||||
|
||||
// Escape quotes and other special characters for Mermaid labels
|
||||
return label
|
||||
.Replace("\"", """)
|
||||
.Replace("\n", " ")
|
||||
.Replace("\r", " ")
|
||||
.Trim();
|
||||
return new NetworkOptions
|
||||
{
|
||||
AutoResize = true,
|
||||
Physics = new PhysicsOptions
|
||||
{
|
||||
Enabled = true
|
||||
},
|
||||
Height = "600px"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,4 +26,4 @@
|
||||
@using MudBlazor
|
||||
@using Core.Entities
|
||||
@using Data
|
||||
@using Blazorade.Mermaid.Components
|
||||
@using VisNetwork.Blazor
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user