Reproducing the D3 zoomable map example with F# and WebSharper

Today’s post will be the last one associated with DSP 2017. Almost 3 months have passed since the beginning of the blogging contest and here we are, 21 posts later! I will continue to blog after the contest ends, but probably not as frequently as of now.

Today we are going to try out the D3 extension for WebSharper. WebSharper comes with quite a lot of extensions that allow developers to use some of the most JavaScript libraries from F#. The full list of extensions is available here.

What is D3.js?

If you are not familiar with D3,js, it’s a JavaScript library that allows to create and embed awesome data visualizations into your website. You can browse the official list of examples to have an idea of the capabilities of the tool. The sample that I am going to port to WebSharper displays a map of the US and allows users to click and zoom in on the different states. You can check the JavaScript source code and try out the original sample in your browser by visiting this page.

The D3 extension for WebSharper

The WebSharper’s extension that allows to interact with D3 from F# is available here. It basically binds every D3.js function to an equivalent F# function. D3.js was written using a rather functional style, so the F# syntax is actually quite similar to the JavaScript one, strong types aside. Without further ado, here’s the result I managed to obtain:

(click on the .gif to enlarge)

I managed to reproduce all functionalities present in the original JavaScript sample, except for one. For some unknown reason, I wasn’t able to retrieve the information about the borders from the JSON file containing all the geographical data. This is currently preventing me from drawing thicker borders between the states on the map. I need to investigate further to find out why.

Quick analysis of the F# code

Interestingly, the JavaScript code was much shorter than the F# equivalent:

  • the JavaScript file contains 64 lines of logic.
  • The F# file contains 142 lines of logic (around 100 lines of pure logic, the rest being helper / integration code around D3.js).

This contrasts with F# / C# comparisons where F# is most of the time much shorter.

Here is the core logic that I wrote to port the example (the full example is available on my GitHub repo under ZoomableMap.fs):

[<JavaScript>]
module ZoomableMap =
    
    type CanvasDimensions = { Width: float; Height: float }

    let private Render (canvas: Dom.Element) =
        let dimensions = { Width = 960.; Height = 500. }
        
        /// Store the index of the currently selected state. '-1' means that no state is currently selected.
        let noStateSelectedIndex = -1
        let mutable currentStateIndex = noStateSelectedIndex
        
        let resetCurrentState () =
            currentStateIndex <- noStateSelectedIndex

        let projection =
            D3.Geo.AlbersUsa()
                .Scale(1070.)
                .Translate(dimensions.Width / 2., dimensions.Height / 2.)

        let path = 
            projection |> D3.Geo.Path().Projection

        let svg = 
            D3.Select(canvas)
                .Append("svg")
                .Attr("width", dimensions.Width)
                .Attr("height", dimensions.Height)

        /// Function called when the user clicks on the map
        let onClicked (eventData:obj * int) =

            let mutable x : float = 0.
            let mutable y : float = 0.
            let mutable k : float = 0.
                        
            let stateClicked, stateClickedIndex = eventData

            // If the user clicks on a state different than the currently selected state
            if sprintf "%A" stateClicked <> "undefined" && stateClickedIndex <> noStateSelectedIndex && currentStateIndex <> stateClickedIndex then
                let cX, cY = path.Centroid(stateClicked :?> Feature)
                x <- float cX
                y <- float cY
                k <- 4.
                currentStateIndex <- stateClickedIndex
            // If the user clicks outside of the map or on the currently selected state
            else
                x <- dimensions.Width / 2.
                y <- dimensions.Height / 2.
                k <- 1.
                resetCurrentState()

            let svgG = D3.Select("#svg-group")

            svgG.SelectAll("path")
                .Classed("active", fun (_, i) -> currentStateIndex <> noStateSelectedIndex && currentStateIndex = i)
                |> ignore

            svgG.Transition()
                .Duration(750)
                .Attr("transform", SvgTransform.Translate(dimensions.Width / 2., dimensions.Height / 2.) + SvgTransform.Scale(k) + SvgTransform.Translate(-x, -y))
                |> ignore

        svg.Append("rect")
            .Attr("class", "background")
            .Attr("width", dimensions.Width)
            .Attr("height", dimensions.Height)
            .On("click", onClicked)
            |> ignore

        let svgGroup =
            svg.Append("g").Attr("id", "svg-group")

        // Download the JSON representing the map and draw it on the screen
        async {    
            let! map = D3.Json("Content/us.json")
            let states : obj[] = topojson.feature(map, map?objects?``states``)?features
            let borders = topojson.mesh(map, map?objects?``states``, fun (a, b) -> a !==. b)

            // Draw states
            svgGroup
                .Append("g")
                .Attr("id", "states")
                .SelectAll("path")
                .Data(states)
                .Enter().Append("path")
                .Attr("d", path)
                .On("click", onClicked)
                |> ignore

            // Draw borders between states
            svgGroup
                .Append("path")
                .Datum(borders) // For some reason, 'borders' doesn't contain any data here.
                .Attr("id", "state-borders")    
                .Attr("d", path)
                |> ignore
        }
        |> Async.Start

    // Render everything under the 'main' div
    let Main =
        let canvas = JS.Document.GetElementById("main")
        Render canvas

As I mentioned above, the F# code is actually pretty similar to the JavaScript one, aside from a few exceptions:

  • Due to the sequential nature of F# (every element must be defined before being used) I had to define the onClicked function at the top of the file. This forced me to add a unique svg-group Id to the SVG group element:
let svgGroup =
    svg.Append("g").Attr("id", "svg-group")

so I can retrieve it later in the onClicked function:

let svgG = D3.Select("#svg-group")

svgG.SelectAll("path")
    .Classed("active", fun (_, i) -> currentStateIndex <> noStateSelectedIndex && currentStateIndex = i)
    |> ignore

svgG.Transition()
    .Duration(750)
    .Attr("transform", SvgTransform.Translate(dimensions.Width / 2., dimensions.Height / 2.) + SvgTransform.Scale(k) + SvgTransform.Translate(-x, -y))
    |> ignore
  • The following F# code was actually quite ugly compared to the JavaScript version:
// F#
let states : obj[] = topojson.feature(map, map?objects?``states``)?features
let borders = topojson.mesh(map, map?objects?``states``, fun (a, b) -> a !==. b)
// JavaScript
.data(topojson.feature(us, us.objects.states).features)
...
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))

I also had to look at the source code of WebSharper on GitHub to check how to handle JavaScript objects from F#. For instance, the specific WebSharper operator !==. is equivalent to !== in JavaScript.

How to get quickly started with D3 under WebSharper

I actually spent a few hours trying to figure out how to properly set up a WebSharper project for my experiment. The instructions below will help you get started quickly and effortlessly!

  1. Create a new project using the UI.Next Single-Page Application template:
  2. Add the WebSharper.Html package to the project using the following NuGet command:
    Install-Package WebSharper.Html
  3. Do the same with the WebSharper.D3 package:
    Install-Package WebSharper.D3
  4. Edit the index.html file and add the following script to it:
    <script type=”text/javascript” src=”http://d3js.org/topojson.v1.min.js”></script>

With that configured, you should be able to run your D3 samples without problems. Once again, the full sources are available on my GitHub repo.

Wrapping up

That’s it for today. Porting this D3 sample to F# and WebSharper was not as easy as I thought it would be but it was fun nonetheless! The JavaScript API of D3.js is really neat and translates nicely into F#. I will most certainly try out new things with D3 in the future!

Cheers

One thought on “Reproducing the D3 zoomable map example with F# and WebSharper

  1. Pingback: dotnetomaniak.pl

Leave a Reply

Your email address will not be published. Required fields are marked *