diff --git a/DotNetTry.sln b/DotNetTry.sln index b895613f0..90abc5ec4 100644 --- a/DotNetTry.sln +++ b/DotNetTry.sln @@ -63,6 +63,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Interactiv EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Interactive", "Microsoft.DotNet.Interactive\Microsoft.DotNet.Interactive.csproj", "{2BB7CCD7-73D1-4B16-82EC-A5D0183F8CF5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XPlot.DotNet.Interactive.KernelExtensions", "XPlot.DotNet.Interactive.KernelExtensions\XPlot.DotNet.Interactive.KernelExtensions.csproj", "{90A9DF5F-CBEE-4B6B-8B58-BA94B0BDCF3C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -181,6 +183,10 @@ Global {2BB7CCD7-73D1-4B16-82EC-A5D0183F8CF5}.Debug|Any CPU.Build.0 = Debug|Any CPU {2BB7CCD7-73D1-4B16-82EC-A5D0183F8CF5}.Release|Any CPU.ActiveCfg = Release|Any CPU {2BB7CCD7-73D1-4B16-82EC-A5D0183F8CF5}.Release|Any CPU.Build.0 = Release|Any CPU + {90A9DF5F-CBEE-4B6B-8B58-BA94B0BDCF3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90A9DF5F-CBEE-4B6B-8B58-BA94B0BDCF3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90A9DF5F-CBEE-4B6B-8B58-BA94B0BDCF3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90A9DF5F-CBEE-4B6B-8B58-BA94B0BDCF3C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -214,6 +220,7 @@ Global {91902AAC-F4E9-4648-AC6B-4E4A722D3CC5} = {8192FEAD-BCE6-4E62-97E5-2E9EA884BD71} {113A4166-5734-4F6E-B609-D6CF42679399} = {6EE8F484-DFA2-4F0F-939F-400CE78DFAC2} {2BB7CCD7-73D1-4B16-82EC-A5D0183F8CF5} = {6EE8F484-DFA2-4F0F-939F-400CE78DFAC2} + {90A9DF5F-CBEE-4B6B-8B58-BA94B0BDCF3C} = {6EE8F484-DFA2-4F0F-939F-400CE78DFAC2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D6CD99BA-B16B-4570-8910-225CBDFFA3AD} diff --git a/MLS.Agent.Tests/GetChartHtmlTests.cs b/MLS.Agent.Tests/GetChartHtmlTests.cs new file mode 100644 index 000000000..38bb3017f --- /dev/null +++ b/MLS.Agent.Tests/GetChartHtmlTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using FluentAssertions; +using HtmlAgilityPack; +using System.Linq; +using XPlot.DotNet.Interactive.KernelExtensions; +using XPlot.Plotly; +using Xunit; + +namespace MLS.Agent.Tests +{ + public partial class XplotKernelExtensionTests + { + public class GetChartHtmlTests + { + [Fact] + public void Returns_the_html_with_div() + { + var extension = new XPlotKernelExtension(); + var html = extension.GetChartHtml(new PlotlyChart()); + var document = new HtmlDocument(); + document.LoadHtml(html); + + document.DocumentNode.SelectSingleNode("//div").InnerHtml.Should().NotBeNull(); + document.DocumentNode.SelectSingleNode("//div").Id.Should().NotBeNullOrEmpty(); + } + + [Fact] + public void Returns_the_html_with_script_containing_require_config() + { + var extension = new XPlotKernelExtension(); + var html = extension.GetChartHtml(new PlotlyChart()); + var document = new HtmlDocument(); + document.LoadHtml(html); + + document.DocumentNode.SelectSingleNode("//script").InnerHtml.Should().Contain("require.config({paths:{plotly:\'https://cdn.plot.ly/plotly-latest.min\'}});"); + } + + [Fact] + public void Returns_the_html_with_script_containing_require_plotly_and_call_to_new_plot_function() + { + var extension = new XPlotKernelExtension(); + var html = extension.GetChartHtml(new PlotlyChart()); + var document = new HtmlDocument(); + document.LoadHtml(html); + + var divId = document.DocumentNode.SelectSingleNode("//div").Id; + document.DocumentNode + .SelectSingleNode("//script") + .InnerHtml.Split("\n") + .Select(item => item.Trim()) + .Where(item => !string.IsNullOrWhiteSpace(item)) + .Should() + .ContainInOrder(@"require(['plotly'], function(Plotly) {", + "var data = null;", + @"var layout = """";", + $"Plotly.newPlot('{divId}', data, layout);"); + } + } + + } +} \ No newline at end of file diff --git a/MLS.Agent.Tests/MLS.Agent.Tests.csproj b/MLS.Agent.Tests/MLS.Agent.Tests.csproj index 28021468e..b0b1e5e43 100644 --- a/MLS.Agent.Tests/MLS.Agent.Tests.csproj +++ b/MLS.Agent.Tests/MLS.Agent.Tests.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/MLS.Agent.Tests/XplotKernelExtensionTests.cs b/MLS.Agent.Tests/XplotKernelExtensionTests.cs new file mode 100644 index 000000000..67d65e645 --- /dev/null +++ b/MLS.Agent.Tests/XplotKernelExtensionTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using FluentAssertions; +using Microsoft.DotNet.Interactive; +using Microsoft.DotNet.Interactive.Commands; +using Microsoft.DotNet.Interactive.Events; +using System.Linq; +using System.Threading.Tasks; +using WorkspaceServer.Tests.Kernel; +using Xunit; +using Xunit.Abstractions; + +namespace MLS.Agent.Tests +{ + public partial class XplotKernelExtensionTests : CSharpKernelTestBase + { + + public XplotKernelExtensionTests(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task When_a_chart_is_returned_the_value_produced_has_html_with_the_require_config_call() + { + var kernel = CreateKernel(); + kernel.UseXplot(); + + await kernel.SendAsync(new SubmitCode("using XPlot.Plotly;")); + await kernel.SendAsync(new SubmitCode("new PlotlyChart()")); + + KernelEvents + .ValuesOnly() + .OfType() + .Should(). + ContainSingle(valueProduced => + valueProduced.FormattedValues.Any(formattedValue => + formattedValue.MimeType == "text/html" && + formattedValue.Value.ToString().Contains("require([\'plotly\'], function(Plotly)") + && formattedValue.Value.ToString().Contains("require.config({paths:{plotly:\'https://cdn.plot.ly/plotly-latest.min\'}});") + )); + } + } +} \ No newline at end of file diff --git a/MLS.Agent/CommandLine/CommandLineParser.cs b/MLS.Agent/CommandLine/CommandLineParser.cs index 7c02d6ebf..702313ac8 100644 --- a/MLS.Agent/CommandLine/CommandLineParser.cs +++ b/MLS.Agent/CommandLine/CommandLineParser.cs @@ -494,6 +494,7 @@ private static CompositeKernel CreateKernel() .UseNugetDirective() .UseExtendDirective() .UseKernelHelpers() + .UseXplot() }; } } diff --git a/MLS.Agent/ExceptionExtensions.cs b/MLS.Agent/ExceptionExtensions.cs index 23ca15749..4edff89c1 100644 --- a/MLS.Agent/ExceptionExtensions.cs +++ b/MLS.Agent/ExceptionExtensions.cs @@ -30,4 +30,4 @@ public static int ToHttpStatusCode(this Exception exception) } } } -} +} \ No newline at end of file diff --git a/MLS.Agent/KernelExtensions.cs b/MLS.Agent/KernelExtensions.cs new file mode 100644 index 000000000..73675aafb --- /dev/null +++ b/MLS.Agent/KernelExtensions.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Clockwise; +using Microsoft.DotNet.Interactive; +using XPlot.DotNet.Interactive.KernelExtensions; + +namespace MLS.Agent +{ + public static class KernelExtensions + { + public static T UseXplot(this T kernel) + where T : KernelBase + { + var extension = new XPlotKernelExtension(); + extension.OnLoadAsync(kernel).Wait(); + return kernel; + } + } +} diff --git a/MLS.Agent/MLS.Agent.csproj b/MLS.Agent/MLS.Agent.csproj index 55bc9f44d..5e835a73f 100644 --- a/MLS.Agent/MLS.Agent.csproj +++ b/MLS.Agent/MLS.Agent.csproj @@ -109,6 +109,7 @@ + diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/Microsoft.DotNet.Interactive.Jupyter.Tests.csproj b/Microsoft.DotNet.Interactive.Jupyter.Tests/Microsoft.DotNet.Interactive.Jupyter.Tests.csproj index 28875454a..eada3649f 100644 --- a/Microsoft.DotNet.Interactive.Jupyter.Tests/Microsoft.DotNet.Interactive.Jupyter.Tests.csproj +++ b/Microsoft.DotNet.Interactive.Jupyter.Tests/Microsoft.DotNet.Interactive.Jupyter.Tests.csproj @@ -8,7 +8,7 @@ - + all diff --git a/Microsoft.DotNet.Try.Markdown.Tests/Microsoft.DotNet.Try.Markdown.Tests.csproj b/Microsoft.DotNet.Try.Markdown.Tests/Microsoft.DotNet.Try.Markdown.Tests.csproj index 9d1c3aa51..78817e1c6 100644 --- a/Microsoft.DotNet.Try.Markdown.Tests/Microsoft.DotNet.Try.Markdown.Tests.csproj +++ b/Microsoft.DotNet.Try.Markdown.Tests/Microsoft.DotNet.Try.Markdown.Tests.csproj @@ -9,7 +9,7 @@ - + diff --git a/WorkspaceServer.Tests/Kernel/TimestampedExtensions.cs b/WorkspaceServer.Tests/Kernel/TimestampedExtensions.cs index 83ba5a54e..9a9d4bea5 100644 --- a/WorkspaceServer.Tests/Kernel/TimestampedExtensions.cs +++ b/WorkspaceServer.Tests/Kernel/TimestampedExtensions.cs @@ -8,7 +8,7 @@ namespace WorkspaceServer.Tests.Kernel { - internal static class TimestampedExtensions + public static class TimestampedExtensions { public static IEnumerable ValuesOnly(this IEnumerable> source) { diff --git a/WorkspaceServer/Kernel/CSharpKernel.cs b/WorkspaceServer/Kernel/CSharpKernel.cs index d7bb9e817..7182d72bf 100644 --- a/WorkspaceServer/Kernel/CSharpKernel.cs +++ b/WorkspaceServer/Kernel/CSharpKernel.cs @@ -66,7 +66,8 @@ private void SetupScriptOptions() typeof(Task<>).Assembly, typeof(IKernel).Assembly, typeof(CSharpKernel).Assembly, - typeof(PocketView).Assembly); + typeof(PocketView).Assembly, + typeof(XPlot.Plotly.PlotlyChart).Assembly); } private (bool shouldExecute, string completeSubmission) IsBufferACompleteSubmission(string input) diff --git a/WorkspaceServer/WorkspaceServer.csproj b/WorkspaceServer/WorkspaceServer.csproj index 8c52aad93..0513697d7 100644 --- a/WorkspaceServer/WorkspaceServer.csproj +++ b/WorkspaceServer/WorkspaceServer.csproj @@ -92,6 +92,7 @@ all runtime; build; native; contentfiles; analyzers + diff --git a/XPlot.DotNet.Interactive.KernelExtensions/XPlot.DotNet.Interactive.KernelExtensions.csproj b/XPlot.DotNet.Interactive.KernelExtensions/XPlot.DotNet.Interactive.KernelExtensions.csproj new file mode 100644 index 000000000..0441a0e5c --- /dev/null +++ b/XPlot.DotNet.Interactive.KernelExtensions/XPlot.DotNet.Interactive.KernelExtensions.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + diff --git a/XPlot.DotNet.Interactive.KernelExtensions/XPlotKernelExtension.cs b/XPlot.DotNet.Interactive.KernelExtensions/XPlotKernelExtension.cs new file mode 100644 index 000000000..b713610ae --- /dev/null +++ b/XPlot.DotNet.Interactive.KernelExtensions/XPlotKernelExtension.cs @@ -0,0 +1,55 @@ +using HtmlAgilityPack; +using Microsoft.DotNet.Interactive; +using Microsoft.DotNet.Interactive.Rendering; +using System; +using System.Text; +using System.Threading.Tasks; +using XPlot.Plotly; +using static Microsoft.DotNet.Interactive.Rendering.PocketViewTags; + +namespace XPlot.DotNet.Interactive.KernelExtensions +{ + public class XPlotKernelExtension : IKernelExtension + { + public Task OnLoadAsync(IKernel kernel) + { + Formatter.Register((chart, writer) => + { + writer.Write(GetChartHtml(chart)); + }, "text/html"); + + return Task.CompletedTask; + } + + public string GetChartHtml(PlotlyChart chart) + { + var document = new HtmlDocument(); + document.LoadHtml(chart.GetInlineHtml()); + + var divNode = document.DocumentNode.SelectSingleNode("//div"); + var scriptNode = document.DocumentNode.SelectSingleNode("//script"); + + var newHtmlDocument = new HtmlDocument(); + newHtmlDocument.DocumentNode.ChildNodes.Add(divNode); + newHtmlDocument.DocumentNode.ChildNodes.Add(GetScriptNodeWithRequire(scriptNode)); + + return newHtmlDocument.DocumentNode.WriteContentTo(); + } + + private static HtmlNode GetScriptNodeWithRequire(HtmlNode scriptNode) + { + var newScript = new StringBuilder(); + + newScript.AppendLine(""); + return HtmlNode.CreateNode(newScript.ToString()); + } + } +}