File: DataFrameKernelExtension.cs
Web Access
Project: src\src\Microsoft.Data.Analysis.Interactive\Microsoft.Data.Analysis.Interactive.csproj (Microsoft.Data.Analysis.Interactive)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.DotNet.Interactive;
using Microsoft.DotNet.Interactive.Commands;
using Microsoft.DotNet.Interactive.Formatting;
using Microsoft.DotNet.Interactive.Formatting.TabularData;
using static Microsoft.DotNet.Interactive.Formatting.PocketViewTags;
 
namespace Microsoft.Data.Analysis.Interactive
{
    public class DataFrameKernelExtension : IKernelExtension
    {
        public async Task OnLoadAsync(Kernel kernel)
        {
            RegisterDataFrame();
            if (Kernel.Root?.FindKernelByName("csharp") is { } csKernel)
            {
                await LoadExtensionApiAsync(csKernel);
            }
        }
 
        private static async Task LoadExtensionApiAsync(Kernel cSharpKernel)
        {
            await cSharpKernel.SendAsync(new SubmitCode($@"#r ""{typeof(DataFrameKernelExtension).Assembly.Location}""
using {typeof(TabularDataResource).Namespace};"));
        }
 
        public static void RegisterDataFrame()
        {
            Formatter.Register<DataFrame>((df, writer) =>
            {
                const int maxRowCount = 10000;
                const int rowsPerPage = 25;
 
                var uniqueId = DateTime.Now.Ticks;
 
                var header = new List<IHtmlContent>
                {
                    th(i("index"))
                };
                header.AddRange(df.Columns.Select(c => (IHtmlContent)th(c.Name)));
 
                if (df.Rows.Count > rowsPerPage)
                {
                    var maxMessage = df.Rows.Count > maxRowCount ? $" (showing a max of {maxRowCount} rows)" : string.Empty;
                    var title = h3[style: "text-align: center;"]($"DataFrame - {df.Rows.Count} rows {maxMessage}");
 
                    // table body
                    var rowCount = Math.Min(maxRowCount, df.Rows.Count);
                    var rows = new List<List<IHtmlContent>>();
                    for (var index = 0; index < rowCount; index++)
                    {
                        var cells = new List<IHtmlContent>
                        {
                            td(i((index)))
                        };
                        foreach (var obj in df.Rows[index])
                        {
                            cells.Add(td(obj));
                        }
                        rows.Add(cells);
                    }
 
                    //navigator
                    var footer = new List<IHtmlContent>();
                    BuildHideRowsScript(uniqueId);
 
                    var paginateScriptFirst = BuildHideRowsScript(uniqueId) + GotoPageIndex(uniqueId, 0) + BuildPageScript(uniqueId, rowsPerPage);
                    footer.Add(button[style: "margin: 2px;", onclick: paginateScriptFirst]("⏮"));
 
                    var paginateScriptPrevTen = BuildHideRowsScript(uniqueId) + UpdatePageIndex(uniqueId, -10, (rowCount - 1) / rowsPerPage) + BuildPageScript(uniqueId, rowsPerPage);
                    footer.Add(button[style: "margin: 2px;", onclick: paginateScriptPrevTen]("⏪"));
 
                    var paginateScriptPrev = BuildHideRowsScript(uniqueId) + UpdatePageIndex(uniqueId, -1, (rowCount - 1) / rowsPerPage) + BuildPageScript(uniqueId, rowsPerPage);
                    footer.Add(button[style: "margin: 2px;", onclick: paginateScriptPrev]("◀️"));
 
                    footer.Add(b[style: "margin: 2px;"]("Page"));
                    footer.Add(b[id: $"page_{uniqueId}", style: "margin: 2px;"]("1"));
 
                    var paginateScriptNext = BuildHideRowsScript(uniqueId) + UpdatePageIndex(uniqueId, 1, (rowCount - 1) / rowsPerPage) + BuildPageScript(uniqueId, rowsPerPage);
                    footer.Add(button[style: "margin: 2px;", onclick: paginateScriptNext]("▶️"));
 
                    var paginateScriptNextTen = BuildHideRowsScript(uniqueId) + UpdatePageIndex(uniqueId, 10, (rowCount - 1) / rowsPerPage) + BuildPageScript(uniqueId, rowsPerPage);
                    footer.Add(button[style: "margin: 2px;", onclick: paginateScriptNextTen]("⏩"));
 
                    var paginateScriptLast = BuildHideRowsScript(uniqueId) + GotoPageIndex(uniqueId, (rowCount - 1) / rowsPerPage) + BuildPageScript(uniqueId, rowsPerPage);
                    footer.Add(button[style: "margin: 2px;", onclick: paginateScriptLast]("⏭️"));
 
                    //table
                    var t = table[id: $"table_{uniqueId}"](
                        caption(title),
                        thead(tr(header)),
                        tbody(rows.Select(r => tr[style: "display: none"](r))),
                        tfoot(tr(td[colspan: df.Columns.Count + 1, style: "text-align: center;"](footer)))
                    );
                    writer.Write(t);
 
                    //show first page
                    writer.Write($"<script>{BuildPageScript(uniqueId, rowsPerPage)}</script>");
                }
                else
                {
                    var rows = new List<List<IHtmlContent>>();
                    for (var index = 0; index < df.Rows.Count; index++)
                    {
                        var cells = new List<IHtmlContent>
                        {
                            td(i((index)))
                        };
                        foreach (var obj in df.Rows[index])
                        {
                            cells.Add(td(obj));
                        }
                        rows.Add(cells);
                    }
 
                    //table
                    var t = table[id: $"table_{uniqueId}"](
                        thead(tr(header)),
                        tbody(rows.Select(r => tr(r)))
                    );
                    writer.Write(t);
                }
            }, "text/html");
        }
 
        private static string BuildHideRowsScript(long uniqueId)
        {
            var script = $"var allRows = document.querySelectorAll('#table_{uniqueId} tbody tr:nth-child(n)'); ";
            script += "for (let i = 0; i < allRows.length; i++) { allRows[i].style.display='none'; } ";
            return script;
        }
 
        private static string BuildPageScript(long uniqueId, int size)
        {
            var script = $"var page = parseInt(document.querySelector('#page_{uniqueId}').innerHTML) - 1; ";
            script += $"var pageRows = document.querySelectorAll(`#table_{uniqueId} tbody tr:nth-child(n + ${{page * {size} + 1 }})`); ";
            script += $"for (let j = 0; j < {size}; j++) {{ pageRows[j].style.display='table-row'; }} ";
            return script;
        }
 
        private static string GotoPageIndex(long uniqueId, long page)
        {
            var script = $"document.querySelector('#page_{uniqueId}').innerHTML = {page + 1}; ";
            return script;
        }
 
        private static string UpdatePageIndex(long uniqueId, int step, long maxPage)
        {
            var script = $"var page = parseInt(document.querySelector('#page_{uniqueId}').innerHTML) - 1; ";
            script += $"page = parseInt(page) + parseInt({step}); ";
            script += $"page = page < 0 ? 0 : page; ";
            script += $"page = page > {maxPage} ? {maxPage} : page; ";
            script += $"document.querySelector('#page_{uniqueId}').innerHTML = page + 1; ";
            return script;
        }
    }
}