Dash.NET is a .NET interface to Dash - the most downloaded framework for building ML & data science web apps - written in F#. Built on top of Plotly.js, React and asp.netcore (via Giraffe), Dash.NET ties modern UI elements like dropdowns, sliders, and graphs directly to your analytical .NET code.
This library is under heavy development. Things might break. However, Dash.NET has a stable core and has already been used for non trivial applications (example1, example2). The current development goal is to implement all targets set in the beta roadmap, where you can also see a summary of the state of the project.
The documentation is WIP as well.
Get the latest preview package via nuget:
Use the dotnet new
template:
dotnet new -i Dash.NET.Template::*
(watch out, this template might not use the latest Dash.NET package, take a look at the referenced version and update if needed )
In Dash.NET, everything is basically about constructing a DashApp
that holds all parts of your dash application, such as:
- the Layout
, which holds the UI components of your application
- Callbacks
that handle how different components in your Layout
interact with each other
- Various server and renderer configurations via DashConfig
- The IndexView
template that controls the html scaffold that holds the rendered application.
The most simple (and boring) Dash.NET application looks like this:
open Dash.NET
let myApp = DashApp.initDefault()
Which creates a DashApp
with all fields initialized with empty defaults. The http handler for a DashApp
can be accessed via the DashApp.toHttpHandler
function to plug it into your aps.netcore application configuration function via UseGiraffe
(for more info, check out Giraffe docs or take a look at the dev project in this repo)
To get actual content into the default application, it needs a Layout
. Layout
s can be created via Dash.NET's DSL for html components, where the first function parameter is always a list of properties (e.g. for setting css classes), and the second a list of children.
//Will create the following html:
//<div>
// <h1>"Hello world from Dash.NET!"</h1>
//</div>
//
open Dash.NET.Html
let myLayout =
Html.div [
Attr.children [
Html.h1 [
Attr.children "Hello world from Dash.NET!"
]
]
]
let test =
DashApp.initDefault()
|> DashApp.withLayout myLayout
You can include internal and external stylesheets and scripts via DashApp.appendCSSLinks
and DashApp.appendScripts
:
Let's say you have the following main.css
file in your wwwroot directory (served from /main.css
)
|
you can reference it from your DashApp
like this (using the basic example from above):
let test =
DashApp.initDefault()
|> DashApp.withLayout myLayout
|> DashApp.appendCSSLinks ["main.css"]
If you want to reference external content (e.g. a CSS framework like Bulma), you can do that as well. To use the classes defined there, set the ClassName
accordingly:
let myLayout3 =
Html.div [
Attr.children [
Html.h1 [
Attr.className "title is-1"
Attr.children "Hello world from Dash.NET!"
]
]
]
let test3 =
DashApp.initDefault()
|> DashApp.withLayout myLayout3
|> DashApp.appendCSSLinks [
"https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.1/css/bulma.min.css"
]
You can also use most dash core components. The following example uses the Plotly.NET to create a plotly graph component. Note that all core components must have a nunique id, and therefore have the mandatory id parameter:
open Dash.NET.Html
open Dash.NET.DCC
open Plotly.NET
let myGraph = Chart.Line([(1,1);(2,2)])
let myLayout =
Html.div [
Attr.children [
Html.h1 [ Attr.children "Hello world from Dash.NET!"]
Html.h2 [ Attr.children "Take a look at this graph:"]
Graph.graph "my-ghraph-id" [Graph.Figure (myGraph |> GenericChart.toFigure)] []
]
]
let test =
DashApp.initDefault()
|> DashApp.withLayout myLayout
Callbacks describe the interactive part of your DashApp
.
Callbacks with single outputs describe either a (1 -> 1) or (n -> 1) dependency between components.
In the most basic case, you have one input component, which updates one output component (1 -> 1). For both you need to assign the property of the component that will be part of the callback. Additionally, a function is needed that takes the input and returns the output. Lets first define the layout for this example:
open Dash.NET.Html
open Dash.NET.DCC
open ComponentPropTypes
let myLayout =
Html.div [
Attr.children [
Html.h1 [Attr.children "Hello world from Dash.NET!"]
Html.h2 [Attr.children "Tell us something!"]
Input.input "test-input" [Input.Type InputType.Text] []
Html.h2 [Attr.children "test-output"]
]
]
There are multiple ways of defining callbacks, all of which use the respective DashApp.singleOut
overloads for (1 -> 1), as well as (1 -> n) callbacks.
The most simple (1 -> 1) callback needs:
- 1 CallbackInput
, which is a representation of the input component and the property that triggers the callback
- 1 CallbackOutput
, which is a representation of the output component and the property that will be updated by the callback
- A function taking as many inputs as there are input components (1 in this case) that creates the callback output
// a 1 -> 1 callback
let testCallback =
Callback.singleOut(
CallbackInput.create("test-input","value"), // <- Input of the callback is the `value` property of the component with the id "test-input"
CallbackOutput.create("test-output","children"), // <- Output of the callback is the `children` property of the component with the id "test-output"
(fun (input:string) -> // this function takes a string as input and returns another message.
sprintf "You said : %s" input
)
)
While above usage of callbacks is the most similar to the original python library, we can leverage the type System of F# to make those calls a little bit safer.
To simplify the generation of CallbackInput
and CallbackOutput
, as well as making sure you are updating the right property in the callback handler function, you can use the custom operators provided by Dash.NET:
@.
defines CallbackInput
or CallbackOutput
(as well as CallbackState
, more on that later) in a typesafe way, so you don't need to write the properties in string from anymore.
=>
defines a CallbackResultBinding
, which is the representation of a function result bound to the given output property (this is more valuable when we come to multioutput callbacks later)
Here is the same simple callback using Dash.NET operators:
open Dash.NET.Operators
let testCallback =
Callback.singleOut(
"test-input" @. Value, // <- Input of the callback is the `value`property of the component with the id "test-input", now typesafe
"test-output" @. Children, // <- Output of the callback is the `children` property of the component with the id "test-output", now typesafe
(fun (input:string) -> // this function takes a string as input and returns another message, now bound to the output property
"test-output" @. Children => sprintf "You said : %s" input
)
)
If you don't like operators, you can still make your callbacks safer using the type safe create
overloads and CallbackResultBinding.create
:
let testCallback =
Callback.singleOut(
CallbackInput.create("test-input" ,Value), // <- Input of the callback is the `value` property of the component with the id "test-input"
CallbackOutput.create("test-output", Children), // <- Output of the callback is the `children` property of the component with the id "test-output"
(fun (input:string) -> // this function takes a string as input and returns another message.
CallbackResultBinding.bindResult
(CallbackOutput.create("test-output", Children))
(sprintf "You said : %s" input)
)
)
The following example shows how to define a (n -> n) multi output callback, again first with the most python-like way, and then in more type safe .NET manner.
let's first define the layout used in this example:
let testLayout =
Html.div [
Attr.children [
Html.h1 [Attr.children "Hello world from Dash.NET!"]
Input.input "test-input1" [Input.Type InputType.Number; Input.Value 2.] []
Input.input "test-input2" [Input.Type InputType.Number; Input.Value 3.] []
Html.h2 [Attr.children "first number times 2 is:"]
Html.div [Attr.children "test-output1"]
Html.h2 [Attr.children "first number times squared is:"]
Html.div [Attr.children "test-output2"]
Html.h2 [Attr.children "second number times 3 is:"]
Html.div [Attr.children "test-output3"]
Html.h2 [Attr.children "second number squared is:"]
Html.div [Attr.children "test-output4"]
]
]
We will now create a (2 -> 4) callback, where we multiply and square each number.
To define multi output callbacks, use Callback.multiOut
:
let multiOutCallbackExample =
Callback.multiOut(
[
CallbackInput.create("test-input1", "value")
CallbackInput.create("test-input2", "value")
],
[
CallbackInput.create("test-output1", "children")
CallbackInput.create("test-output2", "children")
CallbackInput.create("test-output3", "children")
CallbackInput.create("test-output4", "children")
],
(fun (input1:float) (input2:float) ->
[
input1 * 2.
input1 * input1
input2 * 3.
input2 * input2
]
)
)
As you might already see, multi outputs are a lot trickier to do right this way, mainly due to two problems:
The fact that the output must be an array, where the position of the result will be mapped to the same position in the CallbackOutput
array opens possibilities for a lot of mixups and bugs, while being hard to debug aswell.
Additionally to 1., when your outputs must be of different types, you must box all results to keep them in the same collection, therefore returning an object collection which is even more obstrusive because you loose the descriptive type annotation of the callback, as it will now be something along the lines of Callback<*A -> seq<obj>>
To tackle both issues, we recommend your Calllback functions to always return CallbackResultBindin(s), as they can be used internally to be always mapped to the correct output property, and internally box the result aswell. Here is a version of this for the initial example, also using Dash.NET custom operators:
let multiOutCallbackExample =
Callback.multiOut(
[
"test-input1" @. Value
"test-input2" @. Value
],
[
"test-output1" @. Children
"test-output2" @. Children
"test-output3" @. Children
"test-output4" @. Children
],
(fun (input1:float) (input2:float) ->
[
"test-output1" @. Children => input1 * 2.
"test-output3" @. Children => input2 * 3.
"test-output2" @. Children => input1 * input1
"test-output4" @. Children => input2 * input2
]
)
)
note that this callback still maps to the correct output components although the order is intentionally different from the input array.
Use states as non-triggering input for callbacks. You can use the optional State
constructor parameter of Callback
. Just keep in mind that the state will be used for your callback function parameters after the callback inputs:
let myLayout =
Html.div [
Attr.children [
Html.h1 [Attr.children "Hello world from Dash.NET!"]
Html.h2 [Attr.children "Tell us something!"]
Input.input "test-input" [Input.Type InputType.Text] []
Html.h3 [Attr.children "Input below will not trigger the callback"]
Input.input "test-input-state" [Input.Type InputType.Text] []
Html.h2 [Attr.children "test-output"]
]
]
let testCallback =
Callback.singleOut(
"test-input" @. Value,
"test-output" @. Children,
(fun (input:string) (state:string) ->
"test-output" @. Children => (
sprintf "You said : '%s' and we added the state: '%s'" input state)
),
State = ["test-input-state" @. Value]
)
let test =
DashApp.initDefault()
|> DashApp.withLayout myLayout
|> DashApp.addCallback testCallback
Note: The release
and prerelease
build targets assume that there is a NUGET_KEY
environment variable that contains a valid Nuget.org API key.
Check the build.fsx file to take a look at the build targets. Here are some examples:
|
The docs are contained in .fsx
and .md
files in the docs
folder. To develop docs on a local server with hot reload, run the following in the root of the project:
|
The dev server is useful to test new components/code. After a successful build you can start the dev server application by executing the following command in your terminal:
dotnet run -p ./dev/Dash.NET.Dev.fsproj
After the application has started visit https://localhost:5001/ or http://localhost:5000/ in your preferred browser.