Gantt charts in R

September 20, 2017

  R time management gantt chart data visualization
  graphics tidyverse RColorBrewer kableExtra

David Beauchesne

Kevin Cazelles

   

Gantt charts

Gantt charts are a very useful way to organize projects into milestones and tasks visually. They are also dead useful when comes the time to create a timeline for a project, whether it be for a research project, field work or grant applications. I recently wanted to create one, but was unsatisfied by what was available out there in terms of visuals (although there are neat R examples, see here, here and here). So I came up with my own!

For those who might be interested only in generating charts without all the code, jump to the end, copy the ganttR function I created and Gantt away!

Initiate R

1
2
3
4
5
# Packages required
library(knitr)
library(tidyverse)
library(RColorBrewer)
library(kableExtra)

The code!

I wanted to come up with something simple and visually appealing, so I created a figure that simply required the following elements: milestone, tasks, start date, due data and status.

Data

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Let's organize a fake manuscript project to generate the chart.
Manuscript <- c('Research & readings', 'Data preparation', 'Analyses', 'Plan', 'Introduction',
  'Methods', 'Results', 'Discussion', 'Conclusion', 'Preliminary version to co-authors')
startDate <- c('2017-09-01', '2017-09-14', '2017-09-28', '2017-09-21', '2017-09-28',
  '2017-09-14', '2017-10-28', '2017-11-14', '2017-11-28', '2017-12-11')
dueDate <- c('2017-11-28', '2017-09-28', '2017-10-28', '2017-09-28', '2017-10-28',
  '2017-10-28', '2017-11-14', '2017-11-28', '2017-12-04', '2017-12-11')
status <- c('I', 'C', 'I', 'C', 'I', 'I', 'I', 'I', 'I', 'I')
nTasks <- length(Manuscript)

# Create the data frame.
# In statuses, "I" and "C" would stand for "Incomplete" and "Complete", respectively
df <- data.frame(milestones = rep('Manuscript', nTasks),
                 tasks = Manuscript,
                 startDate = as.Date(startDate),
                 dueDate = as.Date(dueDate),
                 status = status,
                 stringsAsFactors = FALSE)

kable(df, "html") %>%
    kable_styling(full_width = FALSE)
milestones tasks startDate dueDate status
Manuscript Research & readings 2017-09-01 2017-11-28 I
Manuscript Data preparation 2017-09-14 2017-09-28 C
Manuscript Analyses 2017-09-28 2017-10-28 I
Manuscript Plan 2017-09-21 2017-09-28 C
Manuscript Introduction 2017-09-28 2017-10-28 I
Manuscript Methods 2017-09-14 2017-10-28 I
Manuscript Results 2017-10-28 2017-11-14 I
Manuscript Discussion 2017-11-14 2017-11-28 I
Manuscript Conclusion 2017-11-28 2017-12-04 I
Manuscript Preliminary version to co-authors 2017-12-11 2017-12-11 I

We can now create our Gantt chart!

The chart

 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
# Let's first organize our graph in order of tasks startDate
df <- arrange(df, desc(startDate))

# We need a date range for which we wish to create the graph.
# Let's select the duration of the milestone
dateRange <- c(min(df$startDate), max(df$dueDate))

# We also need the number of elements to add to our graph.
# It will be the number of tasks plus the number of milestones
nameMilestones <- unique(df$milestones)
nMilestones <- length(nameMilestones)
nLines <- nTasks + nMilestones

# We also need a date sequence that will be used as one of our axes
# We select the date range divided into 7 days periods
dateSeq <- seq.Date(dateRange[1], dateRange[2], by = 7)

# Finally, we need a color palette for the project
# We take three colors, the first will be for the milestones
# The second will be for incomplete tasks
# The third color will be for completed tasks
cols <- c('#4f739d', '#4f739dBB', '#4f739d33')

# Gantt chart
par(family = "serif", mar = c(6,9,2,0))
plot(x = 1, y = 1, col = 'transparent', xlim = dateRange, ylim = c(1, nLines), bty = "n",
  ann = FALSE, xaxt = "n", yaxt = "n", type = "n", bg = 'grey')

# Add axes, tasks and milestones
mtext(dateSeq, side = 1, at = dateSeq, las = 3, line = 1.5, cex = 0.75)
axis(1, dateSeq, labels = FALSE, line = 0.5)
mtext(df$tasks, side = 2, at = 1:nrow(df), las = 1, line = 0, cex = 0.75)
mtext(nameMilestones, side = 2, at = nrow(df)+1, las = 1, line = 8, font = 2, adj = 0, cex = 0.8)

# Add tasks
for(i in 1:nTasks) {
    lines(c(i,i) ~ c(df$startDate[i], df$dueDate[i]), lwd = 6, col = if(df$status[i] == 'C') cols[3] else cols[2])
}

# Add milestone
lines(c(nLines,nLines) ~ c(min(df$startDate), max(df$dueDate)), lwd = 8, col = cols[1])

# Add today's date
abline(v = as.Date(format(Sys.time(), format = "%Y-%m-%d")), lwd = 2, lty = 2)

Multiple milestones

Now Gantt charts are also useful to visualize and organize multiple projects together, so let’s make this chart multi-milestony.

 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
# We will simply duplicate the manuscript and do as though we are working on three consecutive manuscripts
df2 <- df3 <- df
df2$milestones <- 'Manuscript2'
df3$milestones <- 'Manuscript3'
df2[,c('startDate', 'dueDate')] <- df2[,c('startDate', 'dueDate')] + as.numeric(diff(dateRange))
df3[,c('startDate', 'dueDate')] <- df3[,c('startDate', 'dueDate')] + as.numeric(diff(dateRange))*2
df2$status <- df3$status <- rep('I', nTasks)
dfM <- rbind(df, df2, df3)

# ... and go through the process of setting up the elements needed for the chart
# Let's start with the colors this time
# We take three colors, the first will be for the milestones
# The second will be for incomplete tasks
# The third color will be for completed tasks
nameMilestones <- unique(dfM$milestones)
nMilestones <- length(nameMilestones)
cols <- data.frame(milestones = nameMilestones,
                   col = brewer.pal(nMilestones, "Dark2"),
                   stringsAsFactors = FALSE)

# Let's organize our dataset to produce the graph
dfMulti <- dfM %>%
      group_by(milestones) %>% # group by milestones
      summarise(startDate = min(startDate),
                dueDate = max(dueDate)) %>% # Determine the beginning and end date of milestones
      mutate(tasks = milestones, status = 'M') %>% # Give a name and a status
      bind_rows(dfM) %>% # Bind milestones with tasks
      mutate(lwd = ifelse(milestones == tasks, 8, 6)) %>% # Add line width for graph
      left_join(cols, by = 'milestones') %>% # add colors
      mutate(col = ifelse(status == 'I', paste0(col, 'BB'), col)) %>% # change colors according to status
      mutate(col = ifelse(status == 'C', paste0(col, '33'), col)) %>% # change colors according to status
      mutate(cex = ifelse(status == 'M', 0.8, 0.75)) %>%
      mutate(adj = ifelse(status == 'M', 0, 1)) %>%
      mutate(line = ifelse(status == 'M', 8, 0.5)) %>%
      mutate(font = ifelse(status == 'M', 2, 1)) %>%
      arrange(milestones,desc(startDate),dueDate) # sort table

# We need a date range for which we wish to create the graph.
# Let's select the duration of the milestone
dateRange <- c(min(dfMulti$startDate), max(dfMulti$dueDate))

# We also need a date sequence that will be used as one of our axes
# We select the date range divided into 7 days periods
dateSeq <- seq.Date(dateRange[1], dateRange[2], by = 7)


# Gantt chart
nLines <- nrow(dfMulti)
par(family = "serif", mar = c(6, 9, 2, 0))
plot(x = 1, y = 1, col = 'transparent', xlim = dateRange, ylim = c(1, nLines),
  bty = "n",ann = FALSE, xaxt = "n", yaxt = "n", type = "n", bg = 'grey')
mtext(dateSeq, side = 1, at = dateSeq, las = 3, line = 1.5, cex = 0.75)
axis(1, dateSeq, labels = F, line = 0.5)

for(i in 1:nLines) {
    lines(c(i,i) ~ c(dfMulti$startDate[i],dfMulti$dueDate[i]),
          lwd = dfMulti$lwd[i],
          col = dfMulti$col[i])
    mtext(dfMulti$tasks[i],
          side = 2,
          at = i,
          las = 1,
          adj = dfMulti$adj[i],
          line = dfMulti$line[i],
          cex = dfMulti$cex[i],
          font = dfMulti$font[i])
}

abline(h = which(dfMulti$status == 'M') + 0.5, col = '#634d42')
abline(v = as.Date(format(Sys.time(), format = "%Y-%m-%d")), lwd = 2, lty = 2)

ganttR()

Now that we have all the code, let’s simply transform it into a reusable function. Let’s also give it the choice to generate two types of graph: 1) all milestones and tasks or 2) only milestones.

  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
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
ganttR <- function(df, type = 'all') {
    nameMilestones <- unique(df$milestones)
    nMilestones <- length(nameMilestones)
    rbPal <- colorRampPalette(c("#3fb3b2", "#ffdd55", "#c7254e", "#1b95e0", "#8555b4")) # Color palette
    cols <- data.frame(milestones = nameMilestones,
                       col = rbPal(nMilestones),
                       stringsAsFactors = FALSE)
    cols <- cols[1:nMilestones, ]

    # Let's organize our dataset to produce the graph
    df <- df %>%
          group_by(milestones) %>% # group by milestones
          summarise(startDate = min(startDate),
                    dueDate = max(dueDate)) %>% # Determine the beginning and end date of milestones
          mutate(tasks = milestones, status = 'M') %>% # Give a name and a status
          bind_rows(df) %>% # Bind milestones with tasks
          mutate(lwd = ifelse(milestones == tasks, 8, 6)) %>% # Add line width for graph
          left_join(cols, by = 'milestones') %>% # add colors
          mutate(col = ifelse(status == 'I', paste0(col, 'BB'), col)) %>% # change colors according to status
          mutate(col = ifelse(status == 'C', paste0(col, '33'), col)) %>% # change colors according to status
          mutate(cex = ifelse(status == 'M', 0.8, 0.75)) %>%
          mutate(adj = ifelse(status == 'M', 0, 1)) %>%
          mutate(line = ifelse(status == 'M', 8, 0.5)) %>%
          mutate(font = ifelse(status == 'M', 2, 1)) %>%
          arrange(milestones,desc(startDate),dueDate) # sort table

    # We need a date range for which we wish to crete the graph.
    # Let's select the duration of the milestone
    dateRange <- c(min(df$startDate), max(df$dueDate))

    # We also need a date sequence that will be used as one of our axes
    # We select the date range divided into 7 days periods
    # dateSeq <- seq.Date(dateRange[1], dateRange[2], by = 7)
    forced_start <- as.Date(paste0(format(dateRange[1], "%Y-%m"), "-01"))
    yEnd <- format(dateRange[2], "%Y")
    mEnd <- as.numeric(format(dateRange[2], "%m")) + 1
    if (mEnd == 13) {
        yEnd <- as.numeric(yEnd) + 1
        mEnd <- 1
    }
    forced_end <- as.Date(paste0(yEnd, "-", mEnd,"-01"))
    dateSeq <- seq.Date(forced_start, forced_end, by = "month")
    lab <- format(dateSeq, "%B")

    # Gantt chart for 'all' type
    if(type == 'all') {
        nLines <- nrow(df)
        par(family = "serif", mar = c(6,9,2,0))
        plot(x = 1, y = 1, col = 'transparent',
          xlim = c(min(dateSeq), max(dateSeq)),
          ylim = c(1, nLines), bty = "n",
          ann = FALSE, xaxt = "n", yaxt = "n", type = "n", bg = 'grey')
        mtext(lab[-length(lab)], side = 1, at = dateSeq[-length(lab)], las = 0,
          line = 1.5, cex = 0.75, adj = 0)
        axis(1, dateSeq, labels = FALSE, line = 0.5)
        extra <- nLines * 0.03
        for(i in seq(1,length(dateSeq - 1), by = 2)) {
            polygon(x = c(dateSeq[i], dateSeq[i + 1], dateSeq[i + 1], dateSeq[i]),
                    y = c(1 - extra, 1 - extra, nLines + extra, nLines + extra),
                    border = 'transparent',
                    col = '#f1f1f155')
        }

        for(i in 1:nLines) {
            lines(c(i,i) ~ c(df$startDate[i], df$dueDate[i]),
                  lwd = df$lwd[i],
                  col = df$col[i])
            mtext(df$tasks[i],
                  side = 2,
                  at = i,
                  las = 1,
                  adj = df$adj[i],
                  line = df$line[i],
                  cex = df$cex[i],
                  font = df$font[i])
        }

        abline(h = which(df$status == 'M') + 0.5, col = '#634d42')
        abline(v = as.Date(format(Sys.time(), format = "%Y-%m-%d")), lwd = 2, lty = 2)
    }

    # Gantt chart for 'milestones' only
    if(type == 'milestones') {
        nLines <- nMilestones
        ms <- which(df$status == 'M')
        par(family = "serif", mar = c(6,9,2,0))
        plot(x = 1, y = 1, col = 'transparent', xlim = c(min(dateSeq), max(dateSeq)), ylim = c(1,nLines), bty = "n",ann = FALSE, xaxt = "n", yaxt = "n",type = "n",bg = 'grey')
        mtext(lab[-length(lab)], side = 1, at = dateSeq[-length(lab)], las = 0, line = 1.5, cex = 0.75, adj = 0)
        axis(1, dateSeq, labels = FALSE, line = 0.5)
        extra <- nLines * 0.03
        for(i in seq(1,length(dateSeq-1), by = 2)) {
            polygon(x = c(dateSeq[i], dateSeq[i + 1], dateSeq[i + 1], dateSeq[i]),
                    y = c(1 - extra, 1 - extra, nLines + extra, nLines + extra),
                    border = 'transparent',
                    col = '#f1f1f155')
        }

        for(i in 1:nLines) {
            lines(c(i,i) ~ c(df$startDate[ms[i]], df$dueDate[ms[i]]),
                  lwd = df$lwd[ms[i]],
                  col = df$col[ms[i]])
            mtext(df$tasks[ms[i]],
                  side = 2,
                  at = i,
                  las = 1,
                  adj = 1,
                  line = 0.5,
                  cex = df$cex[ms[i]],
                  font = df$font[ms[i]])
        }
        abline(v = as.Date(format(Sys.time(), format = "%Y-%m-%d")), lwd = 2, lty = 2)
    }
}

# Single milestone
ganttR(df)
1
2
3

# Multiple milestones
ganttR(dfM)
1
2
3

# Milestones only
ganttR(dfM, 'milestones')
Display information relative to the R session used to render this post.
 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
sessionInfo()
#R>  R version 4.4.2 (2024-10-31)
#R>  Platform: x86_64-pc-linux-gnu
#R>  Running under: Ubuntu 22.04.5 LTS
#R>  
#R>  Matrix products: default
#R>  BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#R>  LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.20.so;  LAPACK version 3.10.0
#R>  
#R>  locale:
#R>   [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8        LC_COLLATE=C.UTF-8    
#R>   [5] LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8    LC_PAPER=C.UTF-8       LC_NAME=C             
#R>   [9] LC_ADDRESS=C           LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   
#R>  
#R>  time zone: UTC
#R>  tzcode source: system (glibc)
#R>  
#R>  attached base packages:
#R>  [1] stats     graphics  grDevices utils     datasets  methods   base     
#R>  
#R>  other attached packages:
#R>   [1] kableExtra_1.4.0   RColorBrewer_1.1-3 lubridate_1.9.3    forcats_1.0.0      stringr_1.5.1     
#R>   [6] dplyr_1.1.4        purrr_1.0.2        readr_2.1.5        tidyr_1.3.1        tibble_3.2.1      
#R>  [11] ggplot2_3.5.1      tidyverse_2.0.0    knitr_1.49         inSilecoRef_0.1.1 
#R>  
#R>  loaded via a namespace (and not attached):
#R>   [1] gtable_0.3.6      xfun_0.49         bslib_0.8.0       htmlwidgets_1.6.4 tzdb_0.4.0       
#R>   [6] vctrs_0.6.5       tools_4.4.2       generics_0.1.3    curl_6.0.0        fansi_1.0.6      
#R>  [11] pkgconfig_2.0.3   lifecycle_1.0.4   compiler_4.4.2    munsell_0.5.1     httpuv_1.6.15    
#R>  [16] htmltools_0.5.8.1 sass_0.4.9        yaml_2.3.10       later_1.3.2       pillar_1.9.0     
#R>  [21] jquerylib_0.1.4   DT_0.33           cachem_1.1.0      mime_0.12         tidyselect_1.2.1 
#R>  [26] digest_0.6.37     stringi_1.8.4     bookdown_0.41     bibtex_0.5.1      fastmap_1.2.0    
#R>  [31] grid_4.4.2        colorspace_2.1-1  cli_3.6.3         magrittr_2.0.3    crul_1.5.0       
#R>  [36] utf8_1.2.4        withr_3.0.2       scales_1.3.0      promises_1.3.0    backports_1.5.0  
#R>  [41] timechange_0.3.0  rmarkdown_2.29    blogdown_1.19     hms_1.1.3         shiny_1.9.1      
#R>  [46] evaluate_1.0.1    viridisLite_0.4.2 miniUI_0.1.1.1    rlang_1.1.4       Rcpp_1.0.13-1    
#R>  [51] xtable_1.8-4      glue_1.8.0        httpcode_0.3.0    xml2_1.3.6        rstudioapi_0.17.1
#R>  [56] svglite_2.1.3     jsonlite_1.8.9    R6_2.5.1          rcrossref_1.2.0   plyr_1.8.9       
#R>  [61] systemfonts_1.1.0 fs_1.6.5

Edits

Apr 23, 2022 -- Improve code formatting.