D3 world tour

Looping through countries of the world

This demo is a Fable port of Mike Bostock's World Tour D3 demo. It uses the D3 library to create a visualization that loops through all countries of the world and shows them on the globe one by one. You can find the full source code on GitHub.

On the technical side, the demo shows some of the more interesting aspects of calling JavaScript libraries from Fable. You'll learn how to define mappings for imported scripts, how to pass lambdas to JS code and the ? operator.

Using D3 and TopoJSON

JavaScript helpers and imports

Fable comes with an F# mapping for the D3 library, which defines all the types and functions for D3 that we'll need in this example. In addition to D3, this demo uses d3-queue and topojson which we'll just import and use dynamically:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
open System
open Fable.Core
open Fable.Core.JsInterop
open Fable.Import
open Fable.Import.Browser

let queue = importDefault<unit->obj> "queue"
let topojson = importAll<obj> "topojson"

The Import attribute on the two values is used to import the code and make it available. We write the arguments as if we were writing EcmaScript 2015 modules like import defaultMember from 'queue', then Fable/Babel will transform the modules as needed. In this case, we use amd as a target so we can load them with Require.js.

Setting up the canvas and projection

We will be using D3 together with HTML5 canvas, so the first step is to get the context object and set the size of the canvas to 500x500:

1: 
2: 
3: 
4: 
5: 
let width, height = 500., 500.
let canvas =  document.getElementsByTagName_canvas().[0]
canvas.width <- width
canvas.height <- height
let ctx = canvas.getContext_2d()

Next, we setup the D3 orthographic projection for the globe. The projection object will be used later for rotating the globe. The path value is used for transforming paths that we want to render to match with the projection.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
let projection =
  D3.Geo.Globals.orthographic()
    .translate((width / 2., height / 2.))
    .scale(width / 2. - 20.)
    .clipAngle(90.)
    .precision(0.6)

let path =
  D3.Geo.Globals.path()
    .projection(unbox<D3.Geo.Transform> projection)
    .context(ctx)

let title = D3.Globals.select(".country-name")

Finally, the title value is the HTML element in the middle of the globe that shows the current country name. This is just an ordinary HTML element and we will set its body text during the animation.

Generating the visualization

The main part of the code is defined inside a dataLoaded function. This gets called after D3 loads the country names and locations. The structure of the code looks as follows:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
let dataLoaded world names =
  // (more definitions and setup)

  // Generate next transition
  let rec transition (i) =
    // (...)

  // Start the first transition
  transition (0)

The dataLoaded function will be called by D3 with a world value loaded from the world-110m.json file that represents individual country areas; names loads country names from world-country-names.tsv.

After some setup, the code defines a recursive transition function, which performs one transition and then calls itself to setup the next transition step. In each step, it increments the index of the current country, which is stored in i. The transition 0 call then starts the animation.

Preparing the data

Once the data is loaded, we need to do some pre-processing. First, we create a number of D3 objects that are used to render the map - this includes globe for the border around the globe, landFeatures (for filling land) and borders for rendering borders between couuntries. We also find all countries for which we have both name and map:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
  // Create globe object (to render the border)
  let globe = createObj [ "type" ==> "Sphere" ]
  // Create land feature (fill the world)
  let landFeature = topojson?feature(world, world?objects?``land``)

  // Used to render country borders, specify filter to
  // prune overlapping borders (shared by 2 countries)
  let borders =
    topojson?mesh(world, world?objects?countries, (<>))

  // Get countries for which we have a name and set
  // their name property using the `?` operator
  let countries =
    topojson?feature(world, world?objects?countries)?features
    |> unbox<obj[]>
    |> Array.filter (fun d ->
        names |> Seq.exists (fun n ->
          if (string d?id) = (string n?id)
          then d?name <- n?name; true
          else false))
    |> Array.sortWith (fun a b ->
          compare (string a?name) (string b?name))

Rendering the map

Now we have all we need to render the map! Given a selected country country and a rotation angle, the following function renders the map:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
  /// Helper that draws or fills a line
  let draw color width line fill =
    if fill then ctx.fillStyle <- U3.Case1 color
    else ctx.strokeStyle <- U3.Case1 color
    ctx.lineWidth <- width
    ctx.beginPath()
    path.Invoke(line) |> ignore
    if fill then ctx.fill() else ctx.stroke()

  /// Render background, current country, borders & globe
  let render country angle =
      projection.rotate(unbox angle) |> ignore
      ctx.clearRect(0., 0., width, height)
      draw "#ACA2AD" 0.0 landFeature true
      draw "#9E4078" 0.0 country true
      draw "#EAF1F7" 0.5 borders false
      draw "#726B72" 2.0 globe false
      box ()

Creating the transition

Perhaps the most interesting part of the demo is the next one. Here, we use D3 to create the animated transition. This is done by calling d3.transition() and then setting up a number of parameters:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
  let rec transition i =
    D3.Globals.transition()
      .duration(1250.)
      .each("start", fun _ _ ->
        // Set the text of the HTML element
        let name = unbox<D3.Primitive> countries.[i]?name
        title.text(name) |> box )
      .tween("rotate", fun _ ->
        // Interpolate the rotation & return function
        // that renders everything at a given time 't'
        let p1, p2 = D3.Geo.Globals.centroid(countries.[i])
        let r = D3.Globals.interpolate(projection.rotate(), (-p1, -p2))
        Func<_,_>(fun t -> render countries.[i] (r.Invoke(t))) )
      .transition()
      .each("end", fun _ _ ->
        // At the end, start the transition again!
        transition ((i + 1) % countries.Length) ) |> box

Loading the data

The last thing that we need to do in order to put everything together is to trigger the loading of data. This is done by calling queue().defer(...), which specifies that a file should be (eventually) loaded. When the loading is done, we check for potential errors and call the dataLoaded function, which then starts the first transition.

1: 
2: 
3: 
4: 
5: 
6: 
queue()
  ?defer((fun url callback -> D3.Globals.json(url, callback)), "data/world-110m.json")
  ?defer(D3.Globals.tsv, "data/world-country-names.tsv")
  ?await(fun error world names ->
    if error then error |> unbox |> raise
    dataLoaded world names)
namespace System
namespace Fable
namespace Fable.Core
module JsInterop

from Fable.Core
namespace Fable.Import
module Browser

from Fable.Import
val queue : (unit -> obj)

Full name: D3map.queue
val importDefault : path:string -> 'T

Full name: Fable.Core.JsInterop.importDefault
type unit = Unit

Full name: Microsoft.FSharp.Core.unit
type obj = Object

Full name: Microsoft.FSharp.Core.obj
val topojson : obj

Full name: D3map.topojson
val importAll : path:string -> 'T

Full name: Fable.Core.JsInterop.importAll
val width : float

Full name: D3map.width
val height : float

Full name: D3map.height
val canvas : HTMLCanvasElement

Full name: D3map.canvas
val document : Document

Full name: Fable.Import.Browser.document
abstract member Document.getElementsByTagName_canvas : unit -> NodeListOf<HTMLCanvasElement>
property HTMLCanvasElement.width: float
property HTMLCanvasElement.height: float
val ctx : CanvasRenderingContext2D

Full name: D3map.ctx
abstract member HTMLCanvasElement.getContext_2d : unit -> CanvasRenderingContext2D
val projection : D3.Geo.Projection

Full name: D3map.projection
module D3

from Fable.Import
module Geo

from Fable.Import.D3
type Globals =
  static member albers : unit -> ConicProjection
  static member albersUsa : unit -> ConicProjection
  static member area : feature:obj -> float
  static member azimuthalEqualArea : unit -> InvertibleProjection
  static member azimuthalEquidistant : unit -> InvertibleProjection
  static member bounds : feature:obj -> float * float * float * float
  static member centroid : feature:obj -> float * float
  static member circle : unit -> Circle
  static member clipExtent : unit -> ClipExtent
  static member conicConformal : unit -> ConicProjection
  ...

Full name: Fable.Import.D3.Geo.Globals
static member D3.Geo.Globals.orthographic : unit -> D3.Geo.InvertibleProjection
val path : D3.Geo.Path

Full name: D3map.path
static member D3.Geo.Globals.path : unit -> D3.Geo.Path
val unbox : value:obj -> 'T

Full name: Microsoft.FSharp.Core.Operators.unbox
type Transform =
  interface
    abstract member stream : stream:Listener -> Listener
  end

Full name: Fable.Import.D3.Geo.Transform
val title : D3.Selection<obj>

Full name: D3map.title
type Globals =
  static member ascending : a:Primitive * b:Primitive -> float
  static member bisectLeft : array:float [] * x:float * ?lo:float * ?hi:float -> float
  static member bisectLeft : array:string [] * x:string * ?lo:float * ?hi:float -> float
  static member bisectRight : array:'T [] * x:'T * ?lo:float * ?hi:float -> float
  static member bisector : accessor:Func<'T,'U> -> obj
  static member bisector : comparator:Func<'T,'U,float> -> obj
  static member descending : a:Primitive * b:Primitive -> float
  static member deviation : array:float [] -> float
  static member deviation : array:'T [] * accessor:Func<'T,float,float> -> float
  static member dispatch : [<ParamArray>] names:string [] -> Dispatch
  ...

Full name: Fable.Import.D3.Globals
static member D3.Globals.select : selector:string -> D3.Selection<obj>
static member D3.Globals.select : node:EventTarget -> D3.Selection<obj>
val dataLoaded : world:'a -> names:'b -> 'c

Full name: dmap.dataLoaded
val world : 'a
val names : 'b
val transition : (int -> 'd)
val i : int
val dataLoaded : world:'a -> names:seq<'b> -> obj

Full name: D3map.dataLoaded
val names : seq<'b>
val globe : obj
val createObj : fields:#seq<string * obj> -> obj

Full name: Fable.Core.JsInterop.createObj
val landFeature : obj
val borders : obj
val countries : obj []
type Array =
  member Clone : unit -> obj
  member CopyTo : array:Array * index:int -> unit + 1 overload
  member GetEnumerator : unit -> IEnumerator
  member GetLength : dimension:int -> int
  member GetLongLength : dimension:int -> int64
  member GetLowerBound : dimension:int -> int
  member GetUpperBound : dimension:int -> int
  member GetValue : [<ParamArray>] indices:int[] -> obj + 7 overloads
  member Initialize : unit -> unit
  member IsFixedSize : bool
  ...

Full name: System.Array
val filter : predicate:('T -> bool) -> array:'T [] -> 'T []

Full name: Microsoft.FSharp.Collections.Array.filter
val d : obj
module Seq

from Microsoft.FSharp.Collections
val exists : predicate:('T -> bool) -> source:seq<'T> -> bool

Full name: Microsoft.FSharp.Collections.Seq.exists
val n : 'b
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = String

Full name: Microsoft.FSharp.Core.string
val id : x:'T -> 'T

Full name: Microsoft.FSharp.Core.Operators.id
val name : string

Full name: Fable.Import.Browser.name
val sortWith : comparer:('T -> 'T -> int) -> array:'T [] -> 'T []

Full name: Microsoft.FSharp.Collections.Array.sortWith
val a : obj
val b : obj
val compare : e1:'T -> e2:'T -> int (requires comparison)

Full name: Microsoft.FSharp.Core.Operators.compare
val draw : (string -> float -> 'c -> bool -> unit)


 Helper that draws or fills a line
val color : string
val width : float
val line : 'c
val fill : bool
property CanvasRenderingContext2D.fillStyle: U3<string,CanvasGradient,CanvasPattern>
type U3<'a,'b,'c> =
  | Case1 of 'a
  | Case2 of 'b
  | Case3 of 'c

Full name: Fable.Core.U3<_,_,_>
union case U3.Case1: 'a -> U3<'a,'b,'c>
property CanvasRenderingContext2D.strokeStyle: U3<string,CanvasGradient,CanvasPattern>
property CanvasRenderingContext2D.lineWidth: float
abstract member CanvasRenderingContext2D.beginPath : unit -> unit
abstract member D3.Geo.Path.Invoke : feature:obj * ?index:float -> string
val ignore : value:'T -> unit

Full name: Microsoft.FSharp.Core.Operators.ignore
abstract member CanvasRenderingContext2D.fill : ?fillRule:string -> unit
abstract member CanvasRenderingContext2D.stroke : unit -> unit
val render : ('c -> 'd -> obj)


 Render background, current country, borders & globe
val country : 'c
val angle : 'd
abstract member D3.Geo.Projection.rotate : unit -> float * float * float
abstract member D3.Geo.Projection.rotate : rotation:(float * float * float) -> D3.Geo.Projection
abstract member CanvasRenderingContext2D.clearRect : x:float * y:float * w:float * h:float -> unit
val box : value:'T -> obj

Full name: Microsoft.FSharp.Core.Operators.box
val transition : (int -> obj)
static member D3.Globals.transition : unit -> D3.Transition<obj>
val name : D3.Primitive
type Primitive = U3<float,string,bool>

Full name: Fable.Import.D3.Primitive
abstract member D3.Selection.text : unit -> string
abstract member D3.Selection.text : value:D3.Primitive -> D3.Selection<'Datum>
abstract member D3.Selection.text : value:Func<'Datum,float,float,D3.Primitive> -> D3.Selection<'Datum>
val p1 : float
val p2 : float
static member D3.Geo.Globals.centroid : feature:obj -> float * float
val r : Func<float,obj>
static member D3.Globals.interpolate : a:float * b:float -> Func<float,float>
static member D3.Globals.interpolate : a:string * b:string -> Func<float,string>
static member D3.Globals.interpolate : a:U2<string,D3.Color> * b:D3.Color -> Func<float,string>
static member D3.Globals.interpolate : a:U2<string,D3.Color []> * b:D3.Color [] -> Func<float,string>
static member D3.Globals.interpolate : a:'Range [] * b:'Output [] -> Func<float,'Output []>
static member D3.Globals.interpolate : a:'Range [] * b:'Range [] -> Func<float,'Output []>
static member D3.Globals.interpolate : a:obj * b:obj -> Func<float,obj>
Multiple items
type Func<'TResult> =
  delegate of unit -> 'TResult

Full name: System.Func<_>

--------------------
type Func<'T,'TResult> =
  delegate of 'T -> 'TResult

Full name: System.Func<_,_>

--------------------
type Func<'T1,'T2,'TResult> =
  delegate of 'T1 * 'T2 -> 'TResult

Full name: System.Func<_,_,_>

--------------------
type Func<'T1,'T2,'T3,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 -> 'TResult

Full name: System.Func<_,_,_,_>

--------------------
type Func<'T1,'T2,'T3,'T4,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 * 'T4 -> 'TResult

Full name: System.Func<_,_,_,_,_>

--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 -> 'TResult

Full name: System.Func<_,_,_,_,_,_>

--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 -> 'TResult

Full name: System.Func<_,_,_,_,_,_,_>

--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 -> 'TResult

Full name: System.Func<_,_,_,_,_,_,_,_>

--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 -> 'TResult

Full name: System.Func<_,_,_,_,_,_,_,_,_>

--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 -> 'TResult

Full name: System.Func<_,_,_,_,_,_,_,_,_,_>

--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 -> 'TResult

Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_>

--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 -> 'TResult

Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_,_>

--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 -> 'TResult

Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_,_,_>

--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'T13,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 * 'T13 -> 'TResult

Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_,_,_,_>

--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'T13,'T14,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 * 'T13 * 'T14 -> 'TResult

Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_,_,_,_,_>

--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'T13,'T14,'T15,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 * 'T13 * 'T14 * 'T15 -> 'TResult

Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_>

--------------------
type Func<'T1,'T2,'T3,'T4,'T5,'T6,'T7,'T8,'T9,'T10,'T11,'T12,'T13,'T14,'T15,'T16,'TResult> =
  delegate of 'T1 * 'T2 * 'T3 * 'T4 * 'T5 * 'T6 * 'T7 * 'T8 * 'T9 * 'T10 * 'T11 * 'T12 * 'T13 * 'T14 * 'T15 * 'T16 -> 'TResult

Full name: System.Func<_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_>
val t : float
Func.Invoke(arg: float) : obj
property Array.Length: int
val url : string
val callback : Func<obj,obj,unit>
static member D3.Globals.json : url:string * ?callback:Func<obj,obj,unit> -> D3.Xhr
property D3.Globals.tsv: D3.Dsv
val error : bool
val world : obj
val names : seq<obj>
val raise : exn:Exception -> 'T

Full name: Microsoft.FSharp.Core.Operators.raise
Fork me on GitHub