🌐
🔍 100%
👁️

🎙️ Sélectionner une voix

🎙️ Select a voice

📊 TP 9 : Visualisation de Données & Dashboarding

Module : Programmation .Net C#

Durée : 2h

Objectif : Utiliser des requêtes d'agrégation (LINQ GroupBy) pour préparer des données analytiques. Intégrer la librairie Radzen pour créer un tableau de bord interactif (Graphiques, Jauges, et filtrage croisé).

Pré-requis :

  • Avoir terminé le TP 8.
  • Comprendre le fonctionnement de LINQ (notamment GroupBy).

Activité 1 : Installation et Configuration de Radzen (15 min)

Pour ne pas réinventer la roue, nous utilisons Radzen.Blazor, une librairie open-source très performante pour la visualisation de données. (Référence : Documentation officielle)

  1. Installation du package NuGet :
    Dans le terminal intégré de VS Code, tapez :
dotnet add package Radzen.Blazor

2. Importation globale :
Ouvrez le fichier Components/_Imports.razor et ajoutez :

@using Radzen
@using Radzen.Blazor

3. Configuration du thème :
Ouvrez le fichier Components/App.razor. Dans la balise <head>, ajoutez le composant de thème Radzen :

<RadzenTheme Theme="material" />

4. Enregistrement des services :
Ouvrez le fichier Program.cs et ajoutez cette ligne :

builder.Services.AddRadzenComponents();

5. Inclusion du script JavaScript :
Toujours dans Components/App.razor, juste avant la fermeture de la balise </body>, ajoutez :

<script src="_content/Radzen.Blazor/Radzen.Blazor.js?v=@(typeof(Radzen.Colors).Assembly.GetName().Version)"></script>

💡 Remarque : Le paramètre ?v=@(...) est un mécanisme de cache-busting qui force le navigateur à recharger le script après chaque mise à jour du package.


Activité 2 : Préparation Analytique des Données (LINQ) (25 min)

Un graphique a besoin de données agrégées. Nous allons créer une requête LINQ qui sera traduite en GROUP BY SQL.

  1. Dans Models/SensorData.cs (tout en bas du fichier), ajoutez un petit objet pour transporter nos statistiques :
public class LocationStat
{
    public string LocationName { get; set; }
    public double AverageValue { get; set; }
}

2. Ouvrez Services/ISensorService.cs et ajoutez :

Task<List<LocationStat>> GetAverageValueByLocationAsync();

3. Implémentez la méthode dans Services/SensorService.cs :

public async Task<List<LocationStat>> GetAverageValueByLocationAsync()
{
    // EF Core traduit ceci en : SELECT Location, AVG(Value) FROM Sensors GROUP BY Location
    return await _context.Sensors
        .Include(s => s.Location)
        .GroupBy(s => s.Location.Name)
        .Select(g => new LocationStat 
        { 
            LocationName = g.Key ?? "Inconnu", 
            AverageValue = g.Average(s => s.Value) 
        })
        .ToListAsync();
}

Activité 3 : Le Diagramme en Barres (30 min)

Nous allons afficher ce résultat visuellement dans notre Dashboard.

  1. Ouvrez Components/Pages/MyDashboard.razor.
  2. Dans le bloc @code, ajoutez la variable et chargez les données :
private List<LocationStat> LocationStats = new();

protected override async Task OnInitializedAsync()
{
    await LoadAll();
    LocationStats = await SensorService.GetAverageValueByLocationAsync();
}

3. Dans le HTML (au-dessus de votre SensorTable), ajoutez le graphique :

<div class="row mb-4">
    <div class="col-md-8">
        <div class="card shadow-sm h-100">
            <div class="card-header bg-white">
                <h5 class="mb-0"><i class="bi bi-bar-chart-fill text-primary"></i> Températures Moyennes</h5>
            </div>
            <div class="card-body">
                <RadzenChart>
                    <RadzenColumnSeries Data="@LocationStats" CategoryProperty="LocationName" ValueProperty="AverageValue" Title="Moyenne" Fill="#0d6efd" />
                    <RadzenValueAxis Formatter="@((value) => Math.Round((double)value, 1) + " °C")" />
                    <RadzenLegend Visible="false" />
                </RadzenChart>
            </div>
        </div>
    </div>
    <!-- L'emplacement pour la colonne de droite (Activité 5) -->
</div>

Activité 4 : L'Interactivité "PowerBI" (Filtrage Croisé) (25 min)

C'est ici que l'approche "Composants" prend tout son sens. Nous allons faire en sorte que cliquer sur une barre du graphique filtre le tableau situé en dessous.

  1. Toujours dans Components/Pages/MyDashboard.razor (bloc @code), ajoutez une méthode pour réagir au clic sur le graphique :
private string? SelectedLocationFilter = null;

// Cette méthode sera appelée par Radzen
private void OnChartClick(SeriesClickEventArgs args)
{
    // args.Category contient le nom du lieu cliqué (ex: "Labo")
    string clickedLocation = args.Category.ToString();

    // Bascule (Toggle) : si on reclique sur le même, on annule le filtre
    if (SelectedLocationFilter == clickedLocation)
        SelectedLocationFilter = null;
    else
        SelectedLocationFilter = clickedLocation;
}

// On crée une propriété calculée qui filtre la liste pour le tableau
private List<SensorData> FilteredSensors => SelectedLocationFilter == null 
     ? Sensors 
     : Sensors.Where(s => s.Location?.Name == SelectedLocationFilter).ToList();

2. Dans le HTML de MyDashboard.razor, connectez l'événement au graphique :

<RadzenChart SeriesClick="@OnChartClick">

3. Mettez à jour votre tableau pour qu'il utilise la liste filtrée au lieu de la liste complète :

<!-- On passe 'FilteredSensors' au lieu de 'Sensors' -->
<SensorTable Sensors="FilteredSensors" OnDeleteClicked="DeleteSensor" />

Testez ! Cliquez sur la barre d'un lieu : le tableau se met à jour instantanément sans recharger la page.


Activité 5 : L'Indicateur de Jauge (Radial Gauge) (25 min)

Pour surveiller la valeur maximale critique du réseau, rien ne vaut une jauge. Nous allons l'ajouter à côté du diagramme en barres.

  1. Dans la <div class="row mb-4"> de l'activité 3, ajoutez une nouvelle colonne :
<div class="col-md-4">
    <div class="card shadow-sm h-100">
        <div class="card-header bg-white">
            <h5 class="mb-0"><i class="bi bi-speedometer2 text-danger"></i> Valeur Max Détectée</h5>
        </div>
        <div class="card-body text-center d-flex flex-column justify-content-center">
            
            <RadzenRadialGauge Style="width: 100%; height: 250px;">
                <RadzenRadialGaugeScale StartAngle="0" EndAngle="100" Step="20">
                    <RadzenRadialGaugeScalePointer Value="@Max" Length="0.6" ShowValue="true" />
                    <!-- Zone verte -->
                    <RadzenRadialGaugeScaleRange From="0" To="40" Fill="green" />
                    <!-- Zone orange -->
                    <RadzenRadialGaugeScaleRange From="40" To="70" Fill="orange" />
                    <!-- Zone rouge -->
                    <RadzenRadialGaugeScaleRange From="70" To="100" Fill="red" />
                </RadzenRadialGaugeScale>
            </RadzenRadialGauge>

        </div>
    </div>
</div>

Remarque : Nous réutilisons ici la variable @Max que nous avions calculée au TP 6.


🚀 Exercices d'application (En autonomie)

En tant que Data Engineer, on vous demande d'ajouter un diagramme de répartition (Pie Chart / Donut).

Exercice 1 : Le Service (La donnée)

  1. Créez une nouvelle classe LocationCountStat (avec LocationName et Count).
  2. Dans le SensorService, créez une méthode GetSensorCountByLocationAsync().
  3. Écrivez la requête LINQ pour compter le nombre de capteurs par lieu. Indice : Utilisez .GroupBy(...) puis .Select(g => new { ... Count = g.Count() }).

Exercice 2 : Le Graphique (Donut Chart)

  1. Dans Components/Pages/MyDashboard.razor, récupérez ces nouvelles données via le service.
  2. Ajoutez une nouvelle "Card" en bas de votre tableau de bord.
  3. En vous aidant de l'auto-complétion de VS Code, utilisez le composant <RadzenDonutSeries /> à l'intérieur d'un <RadzenChart> pour afficher la répartition du nombre de sondes.

Code squelette pour l'exercice 2 :

<RadzenChart>
    <RadzenDonutSeries Data="@CountStats" CategoryProperty="LocationName" ValueProperty="Count">
        <TitleTemplate>
            <div class="rz-donut-content">
                <div>Total</div>
                <div>@TotalSondes</div>
            </div>
        </TitleTemplate>
    </RadzenDonutSeries>
</RadzenChart>

🏁 Synthèse du TP 9

Ce TP démontre la puissance de Blazor pour le développement d'outils analytiques internes (BI).

📊 LAB 9: Data Visualization & Dashboarding

Module: .Net C# Programming

Duration: 2h

Objective: Use aggregation queries (LINQ GroupBy) to prepare analytical data. Integrate the Radzen library to create an interactive dashboard (Charts, Gauges, and cross-filtering).

Prerequisites:

  • Have completed LAB 8.
  • Understand how LINQ works (especially GroupBy).

Activity 1: Radzen Installation and Configuration (15 min)

To avoid reinventing the wheel, we use Radzen.Blazor, a very powerful open-source library for data visualization. (Reference: Official documentation)

  1. NuGet package installation:
    In the integrated VS Code terminal, type:
dotnet add package Radzen.Blazor

2. Global import:
Open the Components/_Imports.razor file and add:

@using Radzen
@using Radzen.Blazor

3. Theme configuration:
Open the Components/App.razor file. In the <head> tag, add the Radzen theme component:

<RadzenTheme Theme="material" />

4. Service registration:
Open the Program.cs file and add this line:

builder.Services.AddRadzenComponents();

5. JavaScript inclusion:
Still in Components/App.razor, just before the closing </body> tag, add:

<script src="_content/Radzen.Blazor/Radzen.Blazor.js?v=@(typeof(Radzen.Colors).Assembly.GetName().Version)"></script>

💡 Note: The ?v=@(...) parameter is a cache-busting mechanism that forces the browser to reload the script after each package update.


Activity 2: Analytical Data Preparation (LINQ) (25 min)

A chart needs aggregated data. We will create a LINQ query that will be translated into a SQL GROUP BY.

  1. In Models/SensorData.cs (at the very bottom of the file), add a small object to transport our statistics:
public class LocationStat
{
    public string LocationName { get; set; }
    public double AverageValue { get; set; }
}

2. Open Services/ISensorService.cs and add:

Task<List<LocationStat>> GetAverageValueByLocationAsync();

3. Implement the method in Services/SensorService.cs:

public async Task<List<LocationStat>> GetAverageValueByLocationAsync()
{
    // EF Core translates this into: SELECT Location, AVG(Value) FROM Sensors GROUP BY Location
    return await _context.Sensors
        .Include(s => s.Location)
        .GroupBy(s => s.Location.Name)
        .Select(g => new LocationStat 
        { 
            LocationName = g.Key ?? "Unknown", 
            AverageValue = g.Average(s => s.Value) 
        })
        .ToListAsync();
}

Activity 3: The Bar Chart (30 min)

We are going to display this result visually in our Dashboard.

  1. Open Components/Pages/MyDashboard.razor.
  2. In the @code block, add the variable and load the data:
private List<LocationStat> LocationStats = new();

protected override async Task OnInitializedAsync()
{
    await LoadAll();
    LocationStats = await SensorService.GetAverageValueByLocationAsync();
}

3. In the HTML (above your SensorTable), add the chart:

<div class="row mb-4">
    <div class="col-md-8">
        <div class="card shadow-sm h-100">
            <div class="card-header bg-white">
                <h5 class="mb-0"><i class="bi bi-bar-chart-fill text-primary"></i> Average Temperatures</h5>
            </div>
            <div class="card-body">
                <RadzenChart>
                    <RadzenColumnSeries Data="@LocationStats" CategoryProperty="LocationName" ValueProperty="AverageValue" Title="Average" Fill="#0d6efd" />
                    <RadzenValueAxis Formatter="@((value) => Math.Round((double)value, 1) + " °C")" />
                    <RadzenLegend Visible="false" />
                </RadzenChart>
            </div>
        </div>
    </div>
    <!-- The placeholder for the right column (Activity 5) -->
</div>

Activity 4: "PowerBI" Interactivity (Cross-Filtering) (25 min)

This is where the "Components" approach makes total sense. We will make it so that clicking on a chart bar filters the table below it.

  1. Still in Components/Pages/MyDashboard.razor (@code block), add a method to react to the chart click:
private string? SelectedLocationFilter = null;

// This method will be called by Radzen
private void OnChartClick(SeriesClickEventArgs args)
{
    // args.Category contains the clicked location name (e.g., "Lab")
    string clickedLocation = args.Category.ToString();

    // Toggle: if we click on the same one again, we clear the filter
    if (SelectedLocationFilter == clickedLocation)
        SelectedLocationFilter = null;
    else
        SelectedLocationFilter = clickedLocation;
}

// We create a computed property that filters the list for the table
private List<SensorData> FilteredSensors => SelectedLocationFilter == null 
     ? Sensors 
     : Sensors.Where(s => s.Location?.Name == SelectedLocationFilter).ToList();

2. In the HTML of MyDashboard.razor, connect the event to the chart:

<RadzenChart SeriesClick="@OnChartClick">

3. Update your table so it uses the filtered list instead of the full list:

<!-- We pass 'FilteredSensors' instead of 'Sensors' -->
<SensorTable Sensors="FilteredSensors" OnDeleteClicked="DeleteSensor" />

Test it! Click on a location bar: the table updates instantly without reloading the page.


Activity 5: The Radial Gauge (25 min)

To monitor the critical maximum value of the network, nothing beats a gauge. We will add it next to the bar chart.

  1. In the <div class="row mb-4"> from activity 3, add a new column:
<div class="col-md-4">
    <div class="card shadow-sm h-100">
        <div class="card-header bg-white">
            <h5 class="mb-0"><i class="bi bi-speedometer2 text-danger"></i> Max Detected Value</h5>
        </div>
        <div class="card-body text-center d-flex flex-column justify-content-center">
            
            <RadzenRadialGauge Style="width: 100%; height: 250px;">
                <RadzenRadialGaugeScale StartAngle="0" EndAngle="100" Step="20">
                    <RadzenRadialGaugeScalePointer Value="@Max" Length="0.6" ShowValue="true" />
                    <!-- Green zone -->
                    <RadzenRadialGaugeScaleRange From="0" To="40" Fill="green" />
                    <!-- Orange zone -->
                    <RadzenRadialGaugeScaleRange From="40" To="70" Fill="orange" />
                    <!-- Red zone -->
                    <RadzenRadialGaugeScaleRange From="70" To="100" Fill="red" />
                </RadzenRadialGaugeScale>
            </RadzenRadialGauge>

        </div>
    </div>
</div>

Note: Here we are reusing the @Max variable that we calculated in LAB 6.


🚀 Practice Exercises (On your own)

As a Data Engineer, you are asked to add a distribution chart (Pie Chart / Donut).

Exercise 1: The Service (The data)

  1. Create a new LocationCountStat class (with LocationName and Count).
  2. In the SensorService, create a GetSensorCountByLocationAsync() method.
  3. Write the LINQ query to count the number of sensors per location. Hint: Use .GroupBy(...) then .Select(g => new { ... Count = g.Count() }).

Exercise 2: The Chart (Donut Chart)

  1. In Components/Pages/MyDashboard.razor, retrieve this new data via the service.
  2. Add a new "Card" at the bottom of your dashboard.
  3. Using VS Code auto-completion, use the <RadzenDonutSeries /> component inside a <RadzenChart> to display the sensor count distribution.

Skeleton code for exercise 2:

<RadzenChart>
    <RadzenDonutSeries Data="@CountStats" CategoryProperty="LocationName" ValueProperty="Count">
        <TitleTemplate>
            <div class="rz-donut-content">
                <div>Total</div>
                <div>@TotalSondes</div>
            </div>
        </TitleTemplate>
    </RadzenDonutSeries>
</RadzenChart>

🏁 LAB 9 Summary

This lab demonstrates the power of Blazor for developing internal analytical tools (BI).