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
crosstalkand 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:
- 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). - Change the code-block opener from
```rto```{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"))
```