Skip to content

Instantly share code, notes, and snippets.

@kMutagene
Created July 5, 2020 19:29
Show Gist options
  • Save kMutagene/77aca4941e89f96bdbf5700f45ff27b3 to your computer and use it in GitHub Desktop.
Save kMutagene/77aca4941e89f96bdbf5700f45ff27b3 to your computer and use it in GitHub Desktop.
Kaleido.FSharp POC
//Use F#5 preview to use "#r:nuget..."
#r "nuget:FSharp.Plotly,Version=2.0.0-alpha"
#r "nuget:Fake.Core.Process"
open System
open System.Diagnostics
open Fake.IO
open FSharp.Plotly
open Newtonsoft.Json
let randomPoints =
let rnd = new System.Random()
[for i = 0 to 999 do yield rnd.NextDouble(),rnd.NextDouble()]
let testFig =
Chart.Point(
randomPoints
)
|> Chart.withX_AxisStyle("TestTitle_X",Showgrid=false)
|> Chart.withY_AxisStyle("TestTitle_Y",Showgrid=false)
testFig |> Chart.Show
module Kaleido =
//input and output should most likely be dynamic objects themselves instead of record types
type KaleidoInput () =
inherit DynamicObj()
///This function makes it possible to render Charts
///generated with FSharp.Plotly
static member ofChart
(
format:string,
gChart:GenericChart.GenericChart
) =
let data = GenericChart.getTraces gChart
let layout = GenericChart.getLayout gChart
let kI = KaleidoInput()
data |> DynObj.setValue kI "data"
layout |> DynObj.setValue kI "layout"
format |> DynObj.setValue kI "format"
kI
//TODO: Extend with all fields possibly returned by Kaleido and handle null properly
type KaleidoResponse = {
Code : int
Message : string
Format : string
Result : string
}
with
static member fromJsonString (jsonString) =
jsonString |> JsonConvert.DeserializeObject<KaleidoResponse>
///Starts a process using the kaleido executable and a scope name
//TODO: abstract scopes as types
let start executablePath scopeName =
let startInfo =
new ProcessStartInfo(
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardInput = true,
RedirectStandardError = false,
FileName = executablePath,
Arguments = scopeName
)
let kaleidoProcess = new Process(StartInfo = startInfo)
if kaleidoProcess.Start() then
let startupMsg = kaleidoProcess.StandardOutput.ReadLine()
printfn "%s" startupMsg
kaleidoProcess
else
failwith "Error starting Kaleido process"
let stop (kaleidoProcess:System.Diagnostics.Process) =
kaleidoProcess.Kill()
kaleidoProcess.Dispose()
///Render the given KaleidoInput using the given process.
///use keepRunning = true if you want to use the process again afterwards.
let render (kaleidoProcess:System.Diagnostics.Process) (keepRunning:bool) (input:KaleidoInput) =
input
|> JsonConvert.SerializeObject
|> kaleidoProcess.StandardInput.WriteLine
let result =
kaleidoProcess.StandardOutput.ReadLine()
|> KaleidoResponse.fromJsonString
if not keepRunning then stop kaleidoProcess
result
//Example usage:
//Path to kaleido executable.
let kaleidoPath = @"path/to/kaleido.cmd"
//Start a kaleido process
let proc = Kaleido.start kaleidoPath "plotly"
//Formats dont seem to work as I expected. Changing format here does nothing.
let input = Kaleido.KaleidoInput.ofChart("png",testFig)
//Render the chart with the started process. Save image using FAKE
input
|> Kaleido.render proc true
|> fun r ->
//save result as image
r.Result
|> Convert.FromBase64String
|> File.writeBytes (
sprintf
@"path/to/kaleido.cmd\testFigure.%s"
r.Format
)
@jonmmease
Copy link

Awesome! I think the problem with the format not working might have to do with needing a one extra level of nesting.

The outer JSON would look something like:

{"data": ..., "format": "svg", "width": 500}

At the outer level "data" is not Plotly specific, it's meaning is determined by the scope. For the Plotly scope, the value of "data" is a Plotly figure. The confusing thing is that, as you know, Plotly.js also uses "data" as a top-level key to refer to the list of traces.

So, filling in a simple figure the JSON would look like

{"data": {"data": [{"y": [1, 3, 2]}], "layout": {"title": {"text": "Figure Title"}}}, "format": "svg", "width": 500}

Does that help make sense of what you're seeing?

@jonmmease
Copy link

Here's a version that adds the extra level of nesting and works with different formats: https://gist.github.com/jonmmease/2616440ebea523b83f25100f5c6a9a7f.

@kMutagene
Copy link
Author

@jonmmease

I think the problem with the format not working might have to do with needing a one extra level of nesting.

Yes absolutely, i missed that one

Here's a version that adds the extra level of nesting and works with different formats:

Very cool, especially considering that you said that you don't have much first hand experience in .NET! So what do you think is the way to go from here, I think i could work out a PR for the kaleido repo that adds a F# project with all the build and package distribution scripts and proper typing but i think for integrating tests etc. into your CI i would need your help.

@jonmmease
Copy link

Very cool, especially considering that you said that you don't have much first hand experience in .NET!

I played with F# several years ago for a class project, and I love the language. But I never got deep enough into to it to understand how packaging works. And that was before .NET core, so I'm also not clear on the Linux packaging story right now.

I think i could work out a PR for the kaleido repo that adds a F# project with all the build and package distribution scripts and proper typing but i think for integrating tests etc. into your CI i would need your help.

Cool! If you could include bash/cmd scripts that build the packages, given a path to the executable directory, I think that would be enough for me to use to integrate it into CI. What I'd be doing would be to create a CI task that runs after the executable is created, builds the package, and then uploads it to circleci as an artifact. Then I'd look to you for help on how to publish these packages.

I haven't added the Python tests to CI yet, but that's something I'll work on soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment