Skip to content

Instantly share code, notes, and snippets.

@kevmal
Created October 2, 2023 17:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kevmal/38bdf460431a263d2cb8edb480867691 to your computer and use it in GitHub Desktop.
Save kevmal/38bdf460431a263d2cb8edb480867691 to your computer and use it in GitHub Desktop.
FSI plotting with Avalonia and ScottPlot
#r "nuget:Avalonia.Desktop"
#r "nuget:Avalonia.FuncUI"
#r "nuget:Avalonia.Themes.Fluent"
#r "nuget:ScottPlot.Avalonia"
// ScottPlot.Avalonia exists, without global open Avalonia.* failes after ScottPlot is opened.
// This really only matters if you want to rerun this entire script without resetting
open System
open global.Avalonia
open global.Avalonia.Controls
open global.Avalonia.Controls.ApplicationLifetimes
open global.Avalonia.Data
open global.Avalonia.FuncUI.Hosts
open global.Avalonia.Themes.Fluent
open global.Avalonia.FuncUI
open global.Avalonia.FuncUI.DSL
open global.Avalonia.Threading
open ScottPlot.Avalonia
open ScottPlot
module AvaPlot =
open global.Avalonia.FuncUI.Builder
open global.Avalonia.FuncUI.Types
open ScottPlot
open ScottPlot.Control
let create (attrs: IAttr<AvaPlot> list): IView<AvaPlot> =
ViewBuilder.Create<AvaPlot>(attrs)
// Using ints, if we used a DU then a new type is created every run and we can't check equality across runs
module AppState =
[<Literal>]
let NotStarted = 0
[<Literal>]
let Starting = 1
[<Literal>]
let Started = 2
// Store AppState in FSI thread so we can reexecute this entire script and not recreate the ui thread
let fsiObj =
let slot = System.Threading.Thread.GetNamedDataSlot("fsi obj")
match System.Threading.Thread.GetData(slot) with
| :? Ref<int> as x ->
x
| _ ->
let o = ref (AppState.NotStarted)
System.Threading.Thread.SetData(slot, o)
o
type App() =
inherit Application()
override this.Initialize() =
this.Styles.Add (FluentTheme())
this.RequestedThemeVariant <- Styling.ThemeVariant.Dark
this.Styles.Load "avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"
override this.OnFrameworkInitializationCompleted() =
match this.ApplicationLifetime with
| :? IClassicDesktopStyleApplicationLifetime as desktopLifetime ->
fsiObj.Value <- AppState.Started
| _ -> ()
do //Create ui thread only once
match fsiObj.Value with
| AppState.NotStarted ->
fsiObj.Value <- AppState.Starting
System.Threading.ThreadStart
(fun () ->
AppBuilder
.Configure<App>()
.UsePlatformDetect()
.UseSkia()
.StartWithClassicDesktopLifetime([||], ShutdownMode.OnExplicitShutdown)
|> ignore
)
|> System.Threading.Thread
|> (fun x -> x.Start())
| _ -> ()
let uido f =
let rec loop tries =
match fsiObj.Value with
| AppState.Started -> Dispatcher.UIThread.Invoke(fun () -> f())
| AppState.Starting ->
if tries < 100 then
do System.Threading.Thread.Sleep(100)
loop (tries + 1)
else
failwith "uido timeout"
| _ -> failwith "should be unreachable"
loop 0
let show c = uido <| fun () ->
let w = HostWindow()
w.Title <- "Ny Window"
w.Width <- 500.0
w.Height <- 500.0
w.Content <- c()
w.Show()
let showPlot f = show (fun () -> Component (fun ctx -> AvaPlot.create [ ScottPlot.Avalonia.AvaPlot.init f ] ) )
let showData (xs : 's []) = show (fun () -> Component (fun ctx -> DataGrid.create [ DataGrid.items xs; DataGrid.autoGeneratedColumns true ] ) )
/// Show plots stacked and with synced x axis
let showPlots l =
let rowDef = l |> List.map fst |> String.concat ","
let ps = ResizeArray()
let mutable currentX = 0.0
let mutable currentY = 0.0
let wrap f (p : AvaPlot) =
ps.Add(p)
f p
p.AxesChanged.Add
(fun e ->
let alim = p.Plot.GetAxisLimits()
if alim.XMin <> currentX || alim.XMax <> currentY then
currentX <- alim.XMin
currentY <- alim.XMax
for i in ps do
if not(LanguagePrimitives.PhysicalEquality i p) then
i.Plot.SetAxisLimits(alim.XMin,alim.XMax)
i.Render()
)
show
(fun () ->
Component
(fun ctx ->
Grid.create [
Grid.rowDefinitions rowDef
l
|> List.mapi
(fun i (_, f) ->
AvaPlot.create [
AvaPlot.column 0
AvaPlot.row i
AvaPlot.init (wrap f)
] :> Types.IView
)
|> Grid.children
]
)
)
/// Convinience version of showPlots which defaults all plots to DateTime axis
let showPlotsDt l =
l
|> List.map
(fun (a,b) ->
a, (fun (p : AvaPlot) -> b p; p.Plot.BottomAxis.DateTimeFormat(true))
)
|> showPlots
/// Combine plots on same axis
let cc l p = l |> List.iter (fun x -> x p)
// *******************************************
// some convinence functions for select plot types
// *******************************************
let line x y (p:AvaPlot) =
let s = p.Plot.AddScatter(x,y,markerSize = 0f)
s.OnNaN <- Plottable.ScatterPlot.NanBehavior.Gap
let candle x (p : AvaPlot) = p.Plot.AddCandlesticks(x) |> ignore
let signal x (p : AvaPlot) = p.Plot.AddSignal(x) |> ignore
// ********************************************
// Some arbitrary data
// ********************************************
let rng = Random()
let boxMuller () =
let u1 = rng.NextDouble()
let u2 = rng.NextDouble()
let r = sqrt (-2.0 * log u1)
let theta = 2.0 * Math.PI * u2
r * sin theta
let someDates = Seq.initInfinite (fun i -> System.DateTime(2000,01,01).AddDays(float i))
let toBars chunkSize (vs: float seq) =
vs
|> Seq.chunkBySize chunkSize
|> Seq.map
(fun xs ->
xs
|> Seq.map (fun x -> x,x,x,x)
|> Seq.reduce (fun (o1,h1,l1,c1) (o2,h2,l2,c2) -> o1,max h1 h2,min l1 l2,c2)
)
let someData = Seq.initInfinite (fun _ -> boxMuller()) |> Seq.map (fun x -> 1. + x * 0.003) |> Seq.scan ( *) 1.0
let n = 1000
let ds = someDates |> Seq.take n |> Seq.toArray
let bars = someData |> toBars 100 |> Seq.take n |> Seq.toArray
let ohlc = (ds,bars) ||> Array.map2 (fun d (o,h,l,c) -> OHLC(o,h,l,c,d,TimeSpan.FromDays(1.0)) :> IOHLC)
let m = 1000
let moreDates = someDates |> Seq.take m |> Seq.toArray |> Array.map (fun x -> x.ToOADate())
let x1 = someData |> Seq.take m |> Seq.toArray
let x2 = someData |> Seq.take m |> Seq.toArray
let x3 = someData |> Seq.take m |> Seq.toArray
// ********************************************
// Some plot examples
// ********************************************
//Stacked charts, first takes half the space and the other take a quarter
showPlotsDt
[
"2*", cc [
line moreDates x1
line moreDates x2
line moreDates x3
]
"1*", line moreDates x2
"1*", line moreDates x3
]
// signal plot
someData |> Seq.take 10000 |> Seq.toArray |> signal |> showPlot
// candleStick plot
showPlotsDt ["*", candle ohlc ]
// Show table of data
ohlc |> showData
// bunch of line charts
showPlotsDt
[
"*", List.init 100 (fun _ -> someData |> Seq.take m |> Seq.toArray |> line moreDates ) |> cc
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment