Data in Motion - Earthquakes Map

Read Time: 5 minutes

Today’s “data in motion” post is a visualization of earthquakes over time. I’ll use seismic data from the National Science Foundation. Keeping with the theme, I’ll use F# and FFmpeg to convert the raw data into a video of the data over time.

The above video represents earthquakes from the last 20 years. The magnitude of seismic events are represented using bubble size and color. Lower magnitude events are smaller and blue/green. Higher magnitudes are larger and red. The static data is brought together using a combination of tools. The primary one being F#, along with the libraries Deedle and Plotly.NET for data manipulation and chart creation (respectively). The last step uses FFmpeg to transform a series of images into a final video.

Source Data: Incorporated Research Institutions for Seismology https://service.iris.edu/fdsnws/event/1/

For posterity sake, here are the package versions. Plotly.NET has made a lot of great progress lately.

1
2
3
4
dotnet add package Deedle --version 2.5.0
dotnet add package Newtonsoft.Json --version 13.0.1
dotnet add package Plotly.NET --version 2.0.0
dotnet add package Plotly.NET.ImageExport --version 2.0.0

And here is the code. As you can imagine, it is similar to the other posts in this series. The differences from previous posts are mostly a result of different data formats and using the BubbleGeo chart type for plotting data. The largest difference is the generation of intermediate charts combined into date-specific charts. This extra step is required because the current version of the BubbleGeo chart doesn’t support multiple marker colors for a single chart. To implement different colors, I create a single chart for each magnitude with it’s specific color. Then I use Plotly’s Chart.combine to combine them into a single chart image.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
open System
open System.Diagnostics
open Deedle
open Plotly.NET
open Plotly.NET.ImageExport

let imageDir = "../images/"

/// Execute command
let exec command args =
let startInfo = ProcessStartInfo(FileName = command, Arguments = args)
let p = new Process(StartInfo = startInfo)
let success = p.Start()
if not success then
printfn "Process Failed"
else
p.WaitForExit()

/// Build a video (mp4) using all pngs in the sourceDir
let buildVideo sourceDir dstFile =
exec "ffmpeg" $"-y -i {sourceDir}/image_%%04d.png -c:v libx264 -r 120 -pix_fmt yuv420p {dstFile}"

/// Convert an mp4 to a different file format (i.e. webm or .gif)
let convertVideo (inputFile: string) (outputFile: string) =
exec "ffmpeg" $"-y -i {inputFile} {outputFile}"

/// Create a chart from earthquake data
/// color - Based on magnitude
/// data - (long * lat * bubblesize) list
let makeChart (color: string) (data: (float * float * int) list) =
Chart.BubbleGeo(
lonlatsizes = data,
Name = "Magnitude",
ShowLegend = false,
Opacity = 0.5,
MarkerColor = Color.fromHex(color))
|> Chart.withGeoStyle(Scope = StyleParam.GeoScope.Usa)
|> Chart.withSize (800., 500.)

/// Convert magnitude to a color for the chart
let magnitudeToColor (magnitude: int) =
if magnitude < 1 then "000077"
else if magnitude < 2 then "007777"
else if magnitude < 3 then "007700"
else if magnitude < 4 then "777700"
else if magnitude < 5 then "770000"
else if magnitude < 6 then "550000"
else if magnitude < 7 then "330000"
else "110000"

[<EntryPoint>]
let main argv =
let data =
Frame.ReadCsv("../data/earthquakes-us.csv", true, separators = "|")
|> Frame.mapRowValues(fun row ->
let date = row.GetAs<DateTime>("Time").ToString("yyyy-MM-dd")
let longitude = row.GetAs<float>("Longitude")
let latitude = row.GetAs<float>("Latitude")
let magnitude = row.GetAs<int>("Magnitude")
(date, longitude, latitude, magnitude))
|> Series.values
|> Seq.groupBy (fun (date, _longitude, _latitude, _magnitude) -> date)
|> Seq.map (fun (date, data) ->
let dataByMagnitude =
data
|> Seq.groupBy (fun (_date, _longitude, _latitude, magnitude) -> magnitude)
(date, dataByMagnitude))
|> Seq.sortBy fst

// Create a chart for each date
// Each chart is a combination of separate magnitude-based charts for the date
data
|> Seq.iteri (fun i datum ->
let (date, dataForDate) = datum
let magnitudeCharts =
dataForDate
|> Seq.map (fun (m, dataForMagnitude) ->
let detailData =
dataForMagnitude
|> Seq.map (fun (_data: string, longitude: float, latitude: float, magnitude: int) -> (longitude, latitude, (magnitude + 1) * 4))
|> Seq.toList

makeChart (magnitudeToColor m) detailData)

// Create chart by combining magnitude-specific charts
let fileName = sprintf "%s/image_%04d" imageDir i
magnitudeCharts
|> Chart.combine
|> Chart.withTitle (title=date.Substring(0, 10),
TitleFont=Font.init(Family=StyleParam.FontFamily.Courier_New, Size=32.))
|> Chart.savePNG (path = fileName, Width = 800, Height = 500))

buildVideo "../images" "earthquake.mp4"
convertVideo "earthquake.mp4" "earthquake.webm"

0