16.5 Conclusion

This chapter walked a complete dashboard from blank page to deployable HTML — planning, layout, KPIs, charts, interactivity, and deployment. We followed the same spec-driven instinct introduced in Chapter 13 and applied in Chapter 14: write down the audience, the KPIs, and the layout before writing any code, and the rest of the project becomes implementation. Along the way, we:

  • Planned a dashboard from a five-line plan and a wireframe before opening any tool.
  • Built the dashboard in Quarto Dashboard with rows, columns, value boxes, a gauge, and two interactive charts.
  • Added client-side interactivity with crosstalk and noted when Shiny’s server-side reactivity is worth its added cost.
  • Deployed the dashboard via a single render command, with a pre-delivery design review checklist drawn from Chapter 15.

This is the last chapter of the book. From the first BI definition in Chapter 1, through the spec-driven workflow in Chapters 13–14, to this dashboard, the book has tried to make one argument: the tools matter much less than the discipline of pairing a clear question with an honest answer. The R code, the Quarto syntax, the dashboard libraries — all of these will continue to evolve. The discipline does not.

16.5.1 Complete Code Listing

Below is the complete .qmd source for the absenteeism dashboard as a single document. Save it as absenteeism-dashboard.qmd, install the required packages, and render it with quarto::quarto_render() to reproduce the dashboard. Two small adjustments are required when copying the listing into a real .qmd file:

  1. Replace the angle-bracket placeholders — <avg_hours>, <chronic_rate_pct>, <total_cost_dollars> — with inline R expressions in single-backtick form (the syntax was described in Section 2 above).
  2. Change the code-block opener from ```r to ```{r} (with curly braces) so Quarto recognises and executes each block as an R chunk. The textbook listing uses the unbraced form only because the textbook’s render pipeline would otherwise execute the chunks while typesetting this page.

Complete Code

---
title: "Absenteeism Dashboard"
format:
  dashboard:
    orientation: rows
    theme: cosmo
---

```r
#| include: false
library(tidyverse)
library(plotly)
library(crosstalk)
library(DT)

absenteeism <- read_delim(
  "https://ljkelly3141.github.io/datasets/bi-book/Absenteeism_at_work.csv",
  delim = ";"
) |>
  mutate(
    Chronic    = `Absenteeism time in hours` > 40,
    Month_name = month.abb[`Month of absence`]
  )

hourly_wage  <- 35
total_hours  <- sum(absenteeism$`Absenteeism time in hours`, na.rm = TRUE)
avg_hours    <- round(mean(absenteeism$`Absenteeism time in hours`, na.rm = TRUE), 1)
chronic_rate <- round(mean(absenteeism$Chronic, na.rm = TRUE) * 100, 1)
total_cost   <- total_hours * hourly_wage
```

## Row

### Average absence hours

::: {.valuebox icon="clock" color="primary"}
Avg absence hours

<avg_hours>
:::

### Chronic absence rate

::: {.valuebox icon="exclamation-triangle" color="warning"}
Chronic absence rate

<chronic_rate_pct>
:::

## Row

### Estimated absence cost

::: {.valuebox icon="cash-coin" color="primary"}
Estimated cost (annual)

<total_cost_dollars>
:::

### Attendance rate

```r
attendance_pct <- round((1 - mean(absenteeism$Chronic, na.rm = TRUE)) * 100, 1)

plotly::plot_ly(
  type  = "indicator",
  mode  = "gauge+number",
  value = attendance_pct,
  gauge = list(
    axis  = list(range = c(0, 100)),
    bar   = list(color = "steelblue"),
    steps = list(
      list(range = c(0, 75),   color = "#f8d7da"),
      list(range = c(75, 90),  color = "#fff3cd"),
      list(range = c(90, 100), color = "#d1e7dd")
    )
  )
)
```

## Row

### Top absence reasons

```r
reason_summary <- absenteeism |>
  filter(`Reason for absence` != 0) |>
  group_by(`Reason for absence`) |>
  summarise(`Total hours` = sum(`Absenteeism time in hours`, na.rm = TRUE)) |>
  slice_max(`Total hours`, n = 10)

p <- ggplot(reason_summary,
            aes(x = reorder(factor(`Reason for absence`), `Total hours`),
                y = `Total hours`)) +
  geom_col(fill = "steelblue") +
  coord_flip() +
  labs(x = "Reason code (ICD)", y = "Total hours") +
  theme_minimal()

ggplotly(p)
```

## Row

### Monthly trend

```r
monthly <- absenteeism |>
  filter(`Month of absence` > 0) |>
  group_by(`Month of absence`) |>
  summarise(`Avg hours` = mean(`Absenteeism time in hours`, na.rm = TRUE))

p2 <- ggplot(monthly,
             aes(x = `Month of absence`, y = `Avg hours`)) +
  geom_line(color = "steelblue", linewidth = 1) +
  geom_point(color = "steelblue", size = 2) +
  geom_hline(yintercept = mean(monthly$`Avg hours`),
             linetype = "dashed", color = "gray40") +
  scale_x_continuous(breaks = 1:12, labels = month.abb) +
  labs(x = NULL, y = "Average hours") +
  theme_minimal()

ggplotly(p2, tooltip = c("x", "y"))
```