Note: Dash.NET is currently considered experimental. If your organization is interested in sponsoring Dash.NET, please get in touch.

Interactive Visualizations

This is the 3rd chapter of the Dash Fundamentals. The previous chapter covered basic callback usage. The next chapter describes how to share data between callbacks. Just getting started? Make sure to install the necessary dependencies.

The Dash.NET.DCC module includes a Graph component called DCC.Graph.graph.

DCC.Graph.graph renders interactive data visualizations using the open source plotly.js JavaScript graphing library. Plotly.js supports over 35 chart types and renders charts in both vector-quality SVG and high-performance WebGL.

The figure attribute in the DCC.Graph.graph component is the same figure argument that is used by Plotly.NET. Check out the Plotly.NET documentation and gallery to learn more.

As we already saw, Dash components are described by a set of attributes. Any of these attributes can be updated by callback functions, but only a subset of these attributes are updated through user interaction, such as typing inside a DCC.Input.input component or clicking an option in a DCC.Dropdown.dropdown component.

The DCC.Graph.graph component has four attributes that can change through user-interaction: hoverData, clickData, selectedData, relayoutData. These properties update when you hover over points, click on points, or select regions of points in a graph.

Here's an example that prints these attributes to the screen.

Show code

open Dash.NET
open Dash.NET.DCC
open Plotly.NET
open Newtonsoft.Json

let xDataA = [1;2]
let yDataA = [1;2]
let fruitA = ["apple"; "apple"]
let xDataB = [1;2]
let yDataB = [3;4]
let fruitB = ["orange"; "orange"]

let fig = 
    [ Chart.Scatter(xDataA, yDataA, StyleParam.Mode.Markers, Color = Color.fromKeyword Red, Labels = fruitA)
      Chart.Scatter(xDataB, yDataB, StyleParam.Mode.Markers, Color = Color.fromKeyword Orange, Labels = fruitB) ]
    |> Chart.combine
    |> GenericChart.toFigure

let dslLayout = 
    Html.div [
        Attr.children [
            Graph.graph "basic-interactions" [
                Graph.Attr.figure fig
            ]

            Html.div [
                Attr.className "row"
                Attr.children [
                    Html.div [
                        Attr.className "three columns"
                        Attr.children [
                            Markdown.markdown "hover-data-head" [
                                Markdown.Attr.children "
                                    **Hover Data**

                                    Mouse over values in the graph.
                                "
                            ]
                            Html.pre [
                                Attr.id "hover-data"
                                Attr.style [ 
                                    Css.border ("thin", Feliz.borderStyle.solid, "lightgrey")
                                    Css.overflowXScroll
                                ]
                            ]
                        ]
                    ]
                    Html.div [
                        Attr.className "three columns"
                        Attr.children [
                            Markdown.markdown "click-data-head" [
                                Markdown.Attr.children "
                                    **Click Data**

                                    Click on points in the graph.
                                "
                            ]
                            Html.pre [
                                Attr.id "click-data"
                                Attr.style [ 
                                    Css.border ("thin", Feliz.borderStyle.solid, "lightgrey")
                                    Css.overflowXScroll
                                ]
                            ]
                        ]
                    ]
                    Html.div [
                        Attr.className "three columns"
                        Attr.children [
                            Markdown.markdown "selected-data-head" [
                                Markdown.Attr.children "
                                    **Selection Data**

                                    Choose the lasso or rectangle tool in the graph's menu
                                    bar and then select points in the graph.

                                    Note that if `layout.clickmode = 'event+select'`, selection data also
                                    accumulates (or un-accumulates) selected data if you hold down the shift
                                    button while clicking.
                                "
                            ]
                            Html.pre [
                                Attr.id "selected-data"
                                Attr.style [ 
                                    Css.border ("thin", Feliz.borderStyle.solid, "lightgrey")
                                    Css.overflowXScroll
                                ]
                            ]
                        ]
                    ]
                    Html.div [
                        Attr.className "three columns"
                        Attr.children [
                            Markdown.markdown "relayout-data-head" [
                                Markdown.Attr.children "
                                    **Zoom and Relayout Data**

                                    Click and drag on the graph to zoom or click on the zoom
                                    buttons in the graph's menu bar.
                                    Clicking on legend items will also fire
                                    this event.
                                "
                            ]
                            Html.pre [
                                Attr.id "relayout-data"
                                Attr.style [ 
                                    Css.border ("thin", Feliz.borderStyle.solid, "lightgrey")
                                    Css.overflowXScroll
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ]

let hoverDataCallback =
    Callback.singleOut(
        "basic-interactions" @. (CustomProperty "hoverData"),
        "hover-data" @. Children,
        fun (data: obj) -> 
            "hover-data" @. Children => JsonConvert.SerializeObject(data, Formatting.Indented)

    )

let clickDataCallback =
    Callback.singleOut(
        "basic-interactions" @. (CustomProperty "clickData"),
        "click-data" @. Children,
        fun (data: obj) -> 
            "click-data" @. Children => JsonConvert.SerializeObject(data, Formatting.Indented)
    )

let selectedDataCallback =
    Callback.singleOut(
        "basic-interactions" @. (CustomProperty "selectedData"),
        "selected-data" @. Children,
        fun (data: obj) -> 
            "selected-data" @. Children => JsonConvert.SerializeObject(data, Formatting.Indented)
    )

let relayoutDataCallback =
    Callback.singleOut(
        "basic-interactions" @. (CustomProperty "relayoutData"),
        "relayout-data" @. Children,
        fun (data: obj) -> 
            "relayout-data" @. Children => JsonConvert.SerializeObject(data, Formatting.Indented)
    )

[<EntryPoint>]
let main args =
    Giraffe.DashApp.initDefault()
    |> Giraffe.DashApp.withLayout dslLayout
    |> Giraffe.DashApp.appendCSSLinks ["https://codepen.io/chriddyp/pen/bWLwgP.css"]
    |> Giraffe.DashApp.addCallbacks [
        hoverDataCallback
        clickDataCallback
        selectedDataCallback
        relayoutDataCallback
    ]
    |> Giraffe.DashApp.run args (Giraffe.DashGiraffeConfig.initDebug "localhost")

Hover Data

Mouse over values in the graph.

null

Click Data

Click on points in the graph.

null

Selection Data

Choose the lasso or rectangle tool in the graph's menu bar and then select points in the graph.

Note that if layout.clickmode = 'event+select', selection data also accumulates (or un-accumulates) selected data if you hold down the shift button while clicking.

null

Zoom and Relayout Data

Click and drag on the graph to zoom or click on the zoom buttons in the graph's menu bar.

{
  "autosize": true
}

For optimal user interaction and chart loading performance, Dash apps in production should consider the Job Queue, HPC, Datashader, and horizontal scaling capabilities of Dash Enterprise.

Update Graphs on Hover

Let's update our world indicators example from the previous chapter by updating the time series when we hover over points in our scatter plot.

Show code

open Dash.NET
open Dash.NET.DCC
open Plotly.NET
open FSharp.Data

let df = CsvFile.Load("https://plotly.github.io/datasets/country_indicators.csv").Rows |> List.ofSeq

let available_indicators = df |> List.map (fun r -> r.Item "Indicator Name") |> List.distinct

let dslLayout = 
    Html.div [
        Attr.children [
            Html.div [
                Attr.children [
                    Html.div [
                        Attr.children [
                            Dropdown.dropdown "crossfilter-xaxis-column" [
                                Dropdown.Attr.options <| List.map (fun ai -> DropdownOption.init(label = ai, value = ai)) available_indicators
                                Dropdown.Attr.value "Fertility rate, total (births per woman)"
                            ]
                            RadioItems.radioItems "crossfilter-xaxis-type" [
                                RadioItems.Attr.options [
                                    RadioItemsOption.init(label = "Linear", value = "Linear")
                                    RadioItemsOption.init(label = "Log", value = "Log")
                                ]
                                RadioItems.Attr.value "Linear"
                                RadioItems.Attr.labelStyle [
                                    Css.displayInlineBlock
                                    Css.marginTop (Feliz.length.px 5)
                                ]
                            ]
                        ]
                        Attr.style [
                            Css.displayInlineBlock
                            Css.width (Feliz.length.perc 49)
                        ]
                    ]

                    Html.div [
                        Attr.children [
                            Dropdown.dropdown "crossfilter-yaxis-column" [
                                Dropdown.Attr.options <| List.map (fun ai -> DropdownOption.init(label = ai, value = ai)) available_indicators
                                Dropdown.Attr.value "Life expectancy at birth, total (years)"
                            ]
                            RadioItems.radioItems "crossfilter-yaxis-type" [
                                RadioItems.Attr.options [
                                    RadioItemsOption.init(label = "Linear", value = "Linear")
                                    RadioItemsOption.init(label = "Log", value = "Log")
                                ]
                                RadioItems.Attr.value "Linear"
                                RadioItems.Attr.labelStyle [
                                    Css.displayInlineBlock
                                    Css.marginTop (Feliz.length.px 5)
                                ]
                            ]
                        ]
                        Attr.style [
                            Css.displayInlineBlock
                            Css.width (Feliz.length.perc 49)
                        ]
                    ]
                ]
                Attr.style [
                    Css.padding (Feliz.length.px 10, Feliz.length.px 5)
                ]
            ]

            Html.div [
                Attr.children [
                    Graph.graph "crossfilter-indicator-scatter" [
                        Graph.Attr.hoverData {| points = [{| text = "Japan" |}] |}
                    ]
                ]
                Attr.style [
                    Css.width (Feliz.length.perc 49)
                    Css.displayInlineBlock
                    Css.padding (Feliz.length.px 0, Feliz.length.px 20)
                ]
            ]

            Html.div [
                Attr.children [
                    Graph.graph "x-time-series" []
                    Graph.graph "y-time-series" []
                ]
                Attr.style [
                    Css.width (Feliz.length.perc 49)
                    Css.displayInlineBlock
                ]
            ]

            Html.div [
                Attr.children [
                    Slider.slider "crossfilter-year-slider" [
                        let years = df |> List.map (fun r -> r.Item "Year")
                        yield Slider.Attr.min (years |> List.map int |> List.min)
                        yield Slider.Attr.max (years |> List.map int |> List.max)
                        yield Slider.Attr.value (years |> List.map int |> List.max)
                        yield Slider.Attr.marks ( 
                            years
                            |> List.map (fun y -> (y, Slider.MarkValue.String y))
                            |> Map.ofList
                            |> Slider.MarksType
                        )
                        yield Slider.Attr.step null
                    ]
                ]
                Attr.style [
                    Css.width (Feliz.length.perc 49)
                    Css.padding (Feliz.length.px 0, Feliz.length.px 20, Feliz.length.px 20, Feliz.length.px 20)
                ]
            ]
        ]
    ]

let updateGraph =
    Callback.multiOut(
        [ "crossfilter-xaxis-column" @. Value
          "crossfilter-yaxis-column" @. Value
          "crossfilter-xaxis-type" @. Value
          "crossfilter-yaxis-type" @. Value
          "crossfilter-year-slider" @. Value ],
        [ "crossfilter-indicator-scatter" @. (CustomProperty "figure") ],
        fun xAxisColumnName yAxisColumnName xAxisType yAxisType yearValue -> 
            let dff = df |> List.filter (fun r -> r.Item "Year" = yearValue)

            let xRows = dff |> List.filter (fun r -> r.Item "Indicator Name" = xAxisColumnName)
            let yRows = dff |> List.filter (fun r -> r.Item "Indicator Name" = yAxisColumnName)

            let xData = xRows |> List.map (fun r -> r.Item "Value")
            let yData = yRows |> List.map (fun r -> r.Item "Value")

            let yLabels = yRows |> List.map (fun r -> r.Item "Country Name")

            let fig = 
                Chart.Scatter(xData, yData, StyleParam.Mode.Markers, Labels = yLabels)
                |> Chart.withXAxis(
                    LayoutObjects.LinearAxis.init(
                        AxisType = (if xAxisType = "Linear" then StyleParam.AxisType.Linear else StyleParam.AxisType.Log),
                        Title = Title.init xAxisColumnName
                    )
                )
                |> Chart.withYAxis(
                    LayoutObjects.LinearAxis.init(
                        AxisType = (if yAxisType = "Linear" then StyleParam.AxisType.Linear else StyleParam.AxisType.Log),
                        Title = Title.init yAxisColumnName
                    )
                )
                |> Chart.withLayout (
                    Layout.init(
                        Margin = LayoutObjects.Margin.init(Left = 40, Right = 0, Top = 10, Bottom = 40),
                        HoverMode = StyleParam.HoverMode.Closest
                    )
                )
                |> GenericChart.toFigure

            [ "crossfilter-indicator-scatter" @. (CustomProperty "figure") => fig ]
        , PreventInitialCall = false
    )

let createTimeSeries xData yearData axisType title =
    Chart.Scatter(xData, yearData, StyleParam.Mode.Lines_Markers, Name = title)
    |> Chart.withXAxis(
        LayoutObjects.LinearAxis.init(
            ShowGrid = false,
            Title = Title.init "Value"
        )
    )
    |> Chart.withYAxis(
        LayoutObjects.LinearAxis.init(
            AxisType = (if axisType = "Linear" then StyleParam.AxisType.Linear else StyleParam.AxisType.Log),
            Title = Title.init "Year"
        )
    )
    |> Chart.withLayout (
        Layout.init(
            Margin = LayoutObjects.Margin.init(Left = 40, Right = 0, Top = 10, Bottom = 40),
            HoverMode = StyleParam.HoverMode.Closest
        )
    )
    |> Chart.withAnnotations [
        LayoutObjects.Annotation.init(
            X = 0, 
            Y = 0.85, 
            XRef = "paper", 
            YRef = "paper", 
            ShowArrow = false, 
            XAnchor = StyleParam.XAnchorPosition.Left, 
            YAnchor = StyleParam.YAnchorPosition.Bottom
        )
    ]
    |> GenericChart.toFigure

type PointData =
    { text: string }
type HoverData = 
    { points: PointData list}

let updateXTimeseries =
    Callback.multiOut(
        [ "crossfilter-xaxis-column" @. Value
          "crossfilter-xaxis-type" @. Value
          "crossfilter-indicator-scatter" @. (CustomProperty "hoverData") ],
        [ "x-time-series" @. (CustomProperty "figure") ],
        fun xAxisColumnName xAxisType (hoverData: HoverData) ->
            match hoverData.points |> List.tryHead |> Option.map (fun h -> h.text) with
            | None -> []
            | Some countryName -> 
                let dff = df |> List.filter (fun r -> r.Item "Country Name" = countryName)
                let rows = dff |> List.filter (fun r -> r.Item "Indicator Name" = xAxisColumnName)

                let yData = rows |> List.map (fun r -> r.Item "Value")
                let xData = rows |> List.map (fun r -> r.Item "Year")

                let title = sprintf "<b>%s</b><br>%s" countryName xAxisColumnName

                [ "x-time-series" @. (CustomProperty "figure") => createTimeSeries xData yData xAxisType title ]
        , PreventInitialCall = false
    )

let updateYTimeseries =
    Callback.multiOut(
        [ "crossfilter-yaxis-column" @. Value
          "crossfilter-yaxis-type" @. Value
          "crossfilter-indicator-scatter" @. (CustomProperty "hoverData") ],
        [ "y-time-series" @. (CustomProperty "figure") ],
        fun yAxisColumnName yAxisType (hoverData: HoverData) ->
            match hoverData.points |> List.tryHead |> Option.map (fun h -> h.text) with
            | None -> []
            | Some countryName -> 
                let dff = df |> List.filter (fun r -> r.Item "Country Name" = countryName)
                let rows = dff |> List.filter (fun r -> r.Item "Indicator Name" = yAxisColumnName)

                let yData = rows |> List.map (fun r -> r.Item "Value")
                let xData = rows |> List.map (fun r -> r.Item "Year")

                let title = sprintf "<b>%s</b><br>%s" countryName yAxisColumnName

                [ "y-time-series" @. (CustomProperty "figure") => createTimeSeries xData yData yAxisType title ]
        , PreventInitialCall = false
    )

[<EntryPoint>]
let main args =
    Giraffe.DashApp.initDefault()
    |> Giraffe.DashApp.withLayout dslLayout
    |> Giraffe.DashApp.appendCSSLinks ["https://codepen.io/chriddyp/pen/bWLwgP.css"]
    |> Giraffe.DashApp.addCallback updateGraph
    |> Giraffe.DashApp.addCallbacks [
        updateXTimeseries
        updateYTimeseries
    ]
    |> Giraffe.DashApp.run args (Giraffe.DashGiraffeConfig.initDebug "localhost")
Fertility rate, total (births per woman)
×
Life expectancy at birth, total (years)
×
2468455055606570758085
Fertility rate, total (births per woman)Life expectancy at birth, total (years)
1962196719721977198219871992199720022007

Try moving the mouse over the points in the scatter plot on the left. Notice how the line graphs on the right update based on the point that you are hovering over.

Sign up for Dash Club → Two free cheat sheets plus updates from Chris Parmer and Adam Schroeder delivered to your inbox every two months. Includes tips and tricks, community apps, and deep dives into the Dash architecture. Join now.

As an aside, if you find yourself filtering and visualizing highly-dimensional datasets, you should consider checking out the parallel coordinates chart type.


Current Limitations

There are a few limitations in graph interactions right now.

There's a lot that you can do with these interactive plotting features. If you need help exploring your use case, open up a thread in the Dash Community Forum.


The next chapter of the Dash Fundamentals explains how to share data between callbacks. Dash Fundamentals Part 4. Sharing Data Between Callbacks