Created
October 2, 2023 17:02
-
-
Save kevmal/38bdf460431a263d2cb8edb480867691 to your computer and use it in GitHub Desktop.
FSI plotting with Avalonia and ScottPlot
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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