Hi hazel14,
Please refer below sample code.
HTML
<asp:GridView ID="gvPivot" runat="server" OnRowCreated="OnRowCreated" AutoGenerateColumns="false">
    <Columns>
        <asp:BoundField DataField="matrik" HeaderText="Matrik" />
        <asp:BoundField DataField="name" HeaderText="Name" />
        <asp:BoundField DataField="Final" HeaderText=" Final " />
        <asp:BoundField DataField="Grade" HeaderText="Grade" />
    </Columns>
</asp:GridView>
Code
C#
protected void Page_Load(object sender, EventArgs e)
{
    if (!this.IsPostBack)
    {
        this.BindGrid();
    }
}
private void BindGrid()
{
    DataTable dt = new DataTable();
    dt.Columns.Add("test_name");
    dt.Columns.Add("test_weight", typeof(int));
    dt.Columns.Add("matrik", typeof(int));
    dt.Columns.Add("name", typeof(string));
    dt.Columns.Add("Final", typeof(int));
    dt.Columns.Add("Grade", typeof(string));
    dt.Columns.Add("Markah", typeof(int));
    dt.Rows.Add("name1", "100", "50", "John", "150", "A+", "75");
    dt.Rows.Add("name3", "100", "50", "John", "100", "A", "55");
    dt.Rows.Add("name2", "100", "75", "Robert", "200", "E", "87");
    gvPivot.DataSource = this.PivotData(dt,gvPivot);
    gvPivot.DataBind();
}
//string columnName = $"{testName} ({testWeight})";'
protected void OnRowCreated(object sender, GridViewRowEventArgs e)
{
    if (e.Row.RowType == DataControlRowType.Header)
    {
        for (int i = 0; i < e.Row.Cells.Count; i++)
        {
            e.Row.Cells[i].Text = e.Row.Cells[i].Text.Replace(" (", "<br />");
        }
    }
}
private DataTable PivotData(DataTable sourceTable, GridView gv)
{
    DataTable pivotTable = new DataTable();
    pivotTable.Columns.Add("matrik", typeof(int));
    pivotTable.Columns.Add("name", typeof(string));
    //Extract unique categories and create columns for them
    var TestName = sourceTable.AsEnumerable()
        .Select(row => row.Field<string>("test_name"))
        .Distinct()
        .ToList();
    foreach (var test_name in TestName)
    {
        pivotTable.Columns.Add(test_name, typeof(string));
        BoundField boundField = new BoundField();
        boundField.HeaderText = test_name;
        boundField.DataField = test_name;
        gv.Columns.Add(boundField);
    }
    pivotTable.Columns.Add("Final", typeof(int));
    pivotTable.Columns.Add("Grade", typeof(string));
    // Group data by MatricNo and Name
    var groupedData = sourceTable.AsEnumerable()
    .GroupBy(row => new
    {
        MatricNo = row.Field<int>("matrik"),
        Name = row.Field<string>("name"),
        Final = row.Field<int>("Final"),
        Grade = row.Field<string>("Grade")
    });
    foreach (var group in groupedData)
    {
        DataRow newRow = pivotTable.NewRow();
        newRow["matrik"] = group.Key.MatricNo;
        newRow["name"] = group.Key.Name;
        newRow["Final"] = group.Key.Final;
        newRow["Grade"] = group.Key.Grade;
        foreach (var row in group)
        {
            newRow[row.Field<string>("test_name")] = row.Field<int>("Markah");
        }
        pivotTable.Rows.Add(newRow);
    }
    return pivotTable;
}
VB.Net
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load
    If Not Me.IsPostBack Then
        Me.BindGrid()
    End If
End Sub
Private Sub BindGrid()
    Dim dt As DataTable = New DataTable()
    dt.Columns.Add("test_name")
    dt.Columns.Add("test_weight", GetType(Integer))
    dt.Columns.Add("matrik", GetType(Integer))
    dt.Columns.Add("name", GetType(String))
    dt.Columns.Add("Final", GetType(Integer))
    dt.Columns.Add("Grade", GetType(String))
    dt.Columns.Add("Markah", GetType(Integer))
    dt.Rows.Add("name1", "100", "50", "John", "150", "A+", "75")
    dt.Rows.Add("name3", "100", "50", "John", "100", "A", "55")
    dt.Rows.Add("name2", "100", "75", "Robert", "200", "E", "87")
    gvPivot.DataSource = Me.PivotData(dt, gvPivot)
    gvPivot.DataBind()
End Sub
Protected Sub OnRowCreated(ByVal sender As Object, ByVal e As GridViewRowEventArgs)
    If e.Row.RowType = DataControlRowType.Header Then
        For i As Integer = 0 To e.Row.Cells.Count - 1
            e.Row.Cells(i).Text = e.Row.Cells(i).Text.Replace(" (", "<br />")
        Next
    End If
End Sub
Private Function PivotData(ByVal sourceTable As DataTable, ByVal gv As GridView) As DataTable
    Dim pivotTable As DataTable = New DataTable()
    pivotTable.Columns.Add("matrik", GetType(Integer))
    pivotTable.Columns.Add("name", GetType(String))
    Dim TestName = sourceTable.AsEnumerable().Select(Function(row) row.Field(Of String)("test_name")).Distinct().ToList()
    For Each test_name In TestName
        pivotTable.Columns.Add(test_name, GetType(String))
        Dim boundField As BoundField = New BoundField()
        boundField.HeaderText = test_name
        boundField.DataField = test_name
        gv.Columns.Add(boundField)
    Next
    pivotTable.Columns.Add("Final", GetType(Integer))
    pivotTable.Columns.Add("Grade", GetType(String))
    Dim groupedData = sourceTable.AsEnumerable().GroupBy(Function(row) New With {Key .MatricNo = row.Field(Of Integer)("matrik"),
                                                        Key .Name = row.Field(Of String)("name"),
                                                        Key .Final = row.Field(Of Integer)("Final"),
                                                        Key .Grade = row.Field(Of String)("Grade")})
    For Each group In groupedData
        Dim newRow As DataRow = pivotTable.NewRow()
        newRow("matrik") = group.Key.MatricNo
        newRow("name") = group.Key.Name
        newRow("Final") = group.Key.Final
        newRow("Grade") = group.Key.Grade
        For Each row In group
            newRow(row.Field(Of String)("test_name")) = row.Field(Of Integer)("Markah")
        Next
        pivotTable.Rows.Add(newRow)
    Next
    Return pivotTable
End Function
Screenshot
