I play both computer and mobile games. This report looks at gaming in Australia, where I study.

Australia · 2024 · People aged 15+ in the survey. Video and mobile games are counted together.

Hover over a bar or point to see its values. Use the menu above each chart to change the view. Use the chart toolbar to zoom. Reset view keeps your chosen group and measure and undoes zoom. You can also use Tab, arrow keys and Enter with the menu and Reset view button. Choose the first menu item to return to the starting view.

1. A long session, a small daily share

In the All people view, those who played spent 170 minutes on gaming that day. Across everyone, including people who did not play, the average was 18 minutes. Only 10.4% recorded gaming that day. The base of an average changes its meaning.

Source: Australian Bureau of Statistics (ABS, 2025), 2024 Time Use Survey, Tables 1.1-1.2.

2. Younger people stand apart in both views

In the All people view, the 15-24 group had the largest share who played and the longest player time. Choose a sex group to see how its age pattern looks. Each point shows age, daily share and player time together.

Source: ABS (2025), 2024 Time Use Survey, Tables 3.1-3.6. Age groups do not overlap.

3. Days with paid work show less gaming

In the All people view, player time was 138 minutes on workdays and 188 minutes on days without paid work. Switch to share to compare how many people played. These are group estimates. They do not show that paid work caused the gap.

Circle: workday. Square: day without paid work. A workday has paid work recorded in the diary. A day without paid work does not mean the person was unemployed.

Source: ABS (2025), 2024 Time Use Survey, Tables 9.1-9.2.

4. Weekends show a smaller gap

In the All people view, player time was 162 minutes on weekdays and 189 minutes on weekends. The shares were closer: 10.1% and 11.1%. Choose All people | share to see their error ranges overlap. That overlap alone is not a test of a difference.

A weekend is not the same as a day without paid work. These are two ways of grouping the same survey days. They do not compare each person’s gaming on two types of day.

Source: ABS (2025), 2024 Time Use Survey, Tables 4.1-4.2.

Data notes and limits

Open the key values table
Group Sex group Share who played (%) Time among players (minutes) Time across everyone (minutes)
All ages 15+ Male 14.3 200 29
All ages 15+ Female 6.6 109 7
All ages 15+ All people 10.4 170 18
15-24 Male 32.8 236 78
15-24 Female 12.2 145 18
15-24 All people 22.8 212 49
25-64 Male 12.7 184 24
25-64 Female 5.6 102 6
25-64 All people 9.1 159 14
65+ Male 4.3 115 5
65+ Female 5.6 74 4
65+ All people 4.9 91 4
Weekday Male 14.0 191 27
Weekday Female 6.2 100 6
Weekday All people 10.1 162 16
Weekend Male 15.1 220 33
Weekend Female 7.3 128 9
Weekend All people 11.1 189 21
Workday Male 11.8 156 19
Workday Female 5.3 89 5
Workday All people 9.0 138 12
Not a workday Male 16.3 226 37
Not a workday Female 7.3 117 9
Not a workday All people 11.4 188 21

Acknowledgements

I used Codex (OpenAI, 2026) for planning, source searches, R code, chart controls, draft text, publishing help and checks. The starting gaming interest and wish to relate gaming to other factors came from the student. The title, story plan, report text and code used AI help. The exact model release was not recorded. Source values were checked against the original ABS tables. Python was used for a separate check, not for making the assignment charts. I will review the draft and the AI help before submitting. Appendix A records key prompts and the checked outputs.

The data comes from the Australian Bureau of Statistics (ABS). The tables, methods and reuse notes helped guide the work. The project uses one independent survey source. The data combines video and mobile games. It covers 2024 and people aged 15+ within the ABS survey scope.

References

Australian Bureau of Statistics. (2025). How Australians use their time, 2024 [Data set]. https://www.abs.gov.au/statistics/people/people-and-communities/how-australians-use-their-time/2024

OpenAI. (2026, October 3). Help with Time to Play: Gaming in Australian daily life [Generative AI chat]. Codex. https://openai.com/codex/. See Appendix A for prompts and outputs.

Appendix A: AI prompts and outputs

Codex helped during 30 September to 3 October 2026. The reference date is the date of this checked version. The chat title in the reference describes this work. The tool was Codex by OpenAI. This is a record of key requests and outputs kept for this report, not a copy of the full chat. Prompts are shown in their original Chinese. English notes give a short summary. Local file paths and private account details are left out. They are not needed to repeat the work.

Open the AI prompt record

  1. Plan and review request

Prompt extract: “請依照這份作業說明和評分標準 做一份五天計畫表(計畫表請包含所有文字內容除了專有名詞以外 難度不超過雅思5.5) 以及一份作業檢查表 以確保生成後的正確度”

English note: Use the supplied brief and rubric to make a five-day plan and a checklist. Keep new English simple. Review all earlier days after each new day.

Output kept: the plan, checklist and review workflow. They are local work notes, not extra data sources.

  1. Personal interest and source choice

Prompt: “電玩與手機遊戲 也可以 動機也差不多我平常也是兩個都會玩 如果有澳洲資料最好還是澳洲 但更重要的是選擇一個比較容易得高分的參考資料”

English note: The student plays both computer and mobile games. Australian data is preferred, but useful data matters most.

Output kept: the ABS 2024 Time Use Survey source choice and the story about gaming time across age groups and types of day. The student chose to use this topic.

  1. Source and plan check

Prompt: “ok那就確定這個 複查這份資料是否符合作業要求 以及是否能順利進行day1-5任務”

English note: Confirm the topic. Check that the data fits the assignment and can support Days 1-5.

Output kept: source scope notes, file checks and the four-chart outline. The combined video and mobile category is a stated limit.

  1. Local work and reviews

The student asked Codex to follow the saved plan and workflow, start Day 1 and review Day 1. The prompts named local file paths. Those paths are left out here.

Prompt: “我可以不說 你自動跟著Workflow工作嗎 可以的話繼續day2”

English note: Continue the workflow without a new request for every step. Start Day 2.

Output kept: R import and result code, checked values, chart code, local draft and daily review files.

  1. Draft copy request

Prompt: “忘記刪除Cadmus 紀錄 你生成md draft檔給我 我會自己複製貼上進去 檢查就檢查 這個作業draft檔”

English note: Use a local Markdown draft for review. The student will copy it into Cadmus.

Output kept: the five required draft headings and exact copies of the saved R and R Markdown code. Student platform actions are not claimed as done.

  1. Day 4 request

Prompt: “參考文獻就是 參考的遊戲資料集和codex 工作流繼續 應該複查完day1-3且開始day4”

English note: Keep references to the gaming data source and Codex. Finish the Day 1-3 review and continue Day 4.

Output kept: two reference entries, an AI use record, keyboard menus, design checks and the full report for free RPubs publishing.

  1. Author and two hosts

Prompt: “報告作者欄: Wentzu Lai”

Prompt extract: “我不確定報告中是否有Shiny 我想兩邊都上傳看看效果”

English note: Set the report author to Wentzu Lai. Prepare the same report for RPubs and shinyapps.io so the student can compare them. The student also asked for RStudio setup.

Output kept: the author detail, project settings and a small Shiny page around the same report. All four charts are still made in R with Plotly. Hosting setup notes and checks are kept in the local work files.

Checks of the outputs

R reads the original ABS files. A separate Python check compares all 144 selected numeric cells and the unchanged file hashes. Chart checks compare values, units, ranges and source notes with that data. Browser checks test the actual menus, zoom, reset, hover details and plain table. These checks are saved in the local day and review folders. They do not promise a mark or replace the student’s final review.

The outputs kept after checks are the title, notes, four R charts, controls and report text above. The code below is the saved output used to build them. It includes changes made after checks. The source data is from ABS. It was not made by AI. The local AI use log also records checks and fixes.

Open the checked R and report code

01_import.R

# Read real ABS Excel files. Keep source cells, raw values and format flags.
if (.Platform$OS.type == "windows") {
  Sys.setlocale("LC_ALL", "English_United States.utf8")
}
plan_root <- if (file.exists("../01_Five_Day_Plan.md")) {
  normalizePath("..", winslash = "/")
} else {
  normalizePath("hw3/five_day_plan", winslash = "/", mustWork = TRUE)
}
raw_dir <- file.path(plan_root, "data_feasibility", "abs")
out_dir <- file.path(plan_root, "project", "data")
day_dir <- file.path(plan_root, "day2")
dir.create(out_dir, recursive = TRUE, showWarnings = FALSE)
dir.create(day_dir, showWarnings = FALSE)

manifest <- jsonlite::fromJSON(file.path(raw_dir, "readiness", "source_integrity.json"))
hash_ok <- vapply(seq_len(nrow(manifest$files)), function(i) {
  digest::digest(file = file.path(raw_dir, manifest$files$file[i]), algo = "sha256") ==
    manifest$files$sha256[i]
}, logical(1))
stopifnot(length(hash_ok) == 14L, all(hash_ok))

read_zip_xml <- function(file, entry) {
  con <- unz(file, entry, open = "rb")
  on.exit(close(con))
  xml2::read_xml(con)
}
find_nodes <- function(doc, name) {
  xml2::xml_find_all(doc, paste0("//*[local-name()='", name, "']"))
}
cache <- new.env(parent = emptyenv())
read_cells <- function(file, sheet) {
  key <- paste(file, sheet, sep = "|")
  if (exists(key, cache, inherits = FALSE)) return(get(key, cache))
  path <- file.path(raw_dir, file)
  wb <- read_zip_xml(path, "xl/workbook.xml")
  sheets <- find_nodes(wb, "sheet")
  node <- sheets[xml2::xml_attr(sheets, "name") == sheet]
  stopifnot(length(node) == 1L)
  id <- xml2::xml_attr(node, "r:id",
    ns = c(r = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"))
  rel <- find_nodes(read_zip_xml(path, "xl/_rels/workbook.xml.rels"), "Relationship")
  target <- xml2::xml_attr(rel[xml2::xml_attr(rel, "Id") == id], "Target")
  entry <- if (startsWith(target, "/")) sub("^/", "", target) else paste0("xl/", target)
  styles <- read_zip_xml(path, "xl/styles.xml")
  fmt_nodes <- find_nodes(styles, "numFmt")
  formats <- setNames(xml2::xml_attr(fmt_nodes, "formatCode"),
    xml2::xml_attr(fmt_nodes, "numFmtId"))
  # Excel built-in formats used by these files.
  built_in <- c("0" = "General", "1" = "0", "2" = "0.00",
    "3" = "#,##0", "4" = "#,##0.00", "9" = "0%", "10" = "0.00%",
    "20" = "h:mm", "21" = "h:mm:ss", "22" = "m/d/yy h:mm",
    "45" = "mm:ss", "46" = "[h]:mm:ss", "47" = "mmss.0", "49" = "@")
  xfs <- xml2::xml_find_all(styles,
    "//*[local-name()='cellXfs']/*[local-name()='xf']")
  fmt_ids <- xml2::xml_attr(xfs, "numFmtId")
  style_formats <- formats[fmt_ids]
  missing <- is.na(style_formats)
  style_formats[missing] <- built_in[fmt_ids[missing]]
  style_formats[is.na(style_formats)] <- paste0("Excel built-in ",
    fmt_ids[is.na(style_formats)])
  zip_entries <- utils::unzip(path, list = TRUE)$Name
  strings <- character()
  if ("xl/sharedStrings.xml" %in% zip_entries) {
    si <- find_nodes(read_zip_xml(path, "xl/sharedStrings.xml"), "si")
    strings <- vapply(si, function(x) paste(xml2::xml_text(
      xml2::xml_find_all(x, ".//*[local-name()='t']")), collapse = ""), character(1))
  }
  cells <- find_nodes(read_zip_xml(path, entry), "c")
  types <- xml2::xml_attr(cells, "t")
  types[is.na(types)] <- "n"
  style_index <- as.integer(xml2::xml_attr(cells, "s"))
  style_index[is.na(style_index)] <- 0L
  raw <- xml2::xml_text(xml2::xml_find_first(cells, "./*[local-name()='v']"))
  raw[is.na(raw)] <- ""
  is_shared <- types == "s" & nzchar(raw)
  raw[is_shared] <- strings[as.integer(raw[is_shared]) + 1L]
  inline <- types == "inlineStr"
  raw[inline] <- vapply(cells[inline], function(x) paste(xml2::xml_text(
    xml2::xml_find_all(x, ".//*[local-name()='t']")), collapse = ""), character(1))
  number_format <- unname(style_formats[style_index + 1L])
  # A normal # digit placeholder is not an ABS reliability flag.
  flag <- ifelse(grepl('"#"', number_format, fixed = TRUE), "#",
    ifelse(grepl('"*"', number_format, fixed = TRUE), "*", ""))
  numeric <- rep(NA_real_, length(raw))
  is_number <- types == "n" & nzchar(raw)
  numeric[is_number] <- as.numeric(raw[is_number])
  result <- data.frame(cell = xml2::xml_attr(cells, "r"), type = types,
    raw = raw, numeric = numeric, number_format = number_format, flag = flag)
  assign(key, result, cache)
  result
}
get_cell <- function(file, sheet, cell) {
  values <- read_cells(file, sheet)
  hit <- values[values$cell == cell, , drop = FALSE]
  if (!nrow(hit)) hit <- data.frame(cell = cell, type = "missing", raw = "",
    numeric = NA_real_, number_format = "General", flag = "")
  stopifnot(nrow(hit) == 1L)
  hit
}
excel_col <- function(n) {
  stopifnot(n >= 1L, n <= 26L)
  LETTERS[n]
}
sex_groups <- c("Male", "Female", "All")
fields <- c("share", "player_minutes", "all_minutes", "share_moe",
  "player_time_rse", "all_time_rse")
audit_rows <- list()
data_rows <- list()
read_group <- function(view, group, sex_index, file_number, row, columns, sheets) {
  file <- sprintf("TUSDC%02d.xlsx", file_number)
  values <- numeric(length(fields))
  for (i in seq_along(fields)) {
    sheet <- paste0("Table ", file_number, ".", sheets[i])
    address <- paste0(excel_col(columns[i]), row)
    c <- get_cell(file, sheet, address)
    value <- c$numeric
    unit <- if (fields[i] %in% c("player_minutes", "all_minutes")) {
      value <- round(value * 1440)
      "minutes"
    } else if (fields[i] == "share_moe") "percentage points" else "percent"
    values[i] <- value
    audit_rows[[length(audit_rows) + 1L]] <<- data.frame(view = view,
      group = group, sex = sex_groups[sex_index], field = fields[i], file = file,
      sheet = sheet, cell = address, raw_value = c$raw, value = value, unit = unit,
      number_format = c$number_format, flag = c$flag,
      source_code = if (is.na(c$numeric)) c$raw else "")
  }
  r <- data.frame(view = view, group = group, sex = sex_groups[sex_index])
  for (i in seq_along(fields)) r[[fields[i]]] <- values[i]
  data_rows[[length(data_rows) + 1L]] <<- r
}
for (s in 1:3) {
  read_group("overall", "All ages 15+", s, 1, 57,
    rep(c(1 + s, 4 + s, 7 + s), 2), c(1, 1, 1, 2, 2, 2))
}
for (g in 1:3) for (s in 1:3) {
  # Skip the published 15-64 total, which overlaps the first two groups.
  col <- c(2L, 5L, 11L)[g] + s - 1L
  read_group("age", c("15-24", "25-64", "65+")[g], s, 3, 57,
    rep(col, 6), c(1, 3, 5, 2, 4, 6))
}
for (n in c(4L, 9L)) for (g in 1:2) for (s in 1:3) {
  j <- (g - 1L) * 3L + s - 1L
  labels <- if (n == 4L) c("Weekday", "Weekend") else c("Workday", "Not a workday")
  read_group(if (n == 4L) "week" else "work", labels[g], s, n, 58,
    rep(c(2L + j, 8L + j, 14L + j), 2), c(1, 1, 1, 2, 2, 2))
}
d <- do.call(rbind, data_rows)
audit <- do.call(rbind, audit_rows)
stopifnot(nrow(d) == 24L, nrow(audit) == 144L,
  !anyDuplicated(d[c("view", "group", "sex")]),
  !anyNA(d[fields]), all(audit$flag == ""), all(audit$source_code == ""),
  all(d$share >= 0 & d$share <= 100), all(d$share_moe >= 0),
  all(d$player_minutes > 0 & d$player_minutes <= 1440),
  all(d$all_minutes >= 0 & d$all_minutes <= d$player_minutes),
  all(d$player_time_rse >= 0 & d$player_time_rse < 25),
  all(d$all_time_rse >= 0 & d$all_time_rse < 25))
d$year <- 2024L
d$source_id <- "ABS_TUS_2024"
d$share_lower <- pmax(0, d$share - d$share_moe)
d$share_upper <- pmin(100, d$share + d$share_moe)
# Time ranges use the published RSE and a normal approximation.
# They are not new official ABS confidence intervals.
for (base in c("player", "all")) {
  se <- d[[paste0(base, "_minutes")]] * d[[paste0(base, "_time_rse")]] / 100
  d[[paste0(base, "_lower")]] <- pmax(0, d[[paste0(base, "_minutes")]] - 1.96 * se)
  d[[paste0(base, "_upper")]] <- pmin(1440, d[[paste0(base, "_minutes")]] + 1.96 * se)
}
# Check that a real out-of-scope warning flag and missing error are preserved.
flag_cell <- get_cell("TUSDC05.xlsx", "Table 5.1", "C45")
error_cell <- get_cell("TUSDC05.xlsx", "Table 5.2", "C45")
flag_example <- data.frame(file = "TUSDC05.xlsx", sheet = "Table 5.1", cell = "C45",
  value = flag_cell$numeric, flag = flag_cell$flag,
  number_format = flag_cell$number_format, error_value = error_cell$numeric,
  error_code = error_cell$raw, error_missing = is.na(error_cell$numeric),
  used_in_charts = FALSE)
stopifnot(flag_example$flag == "#", flag_example$error_missing)
write.csv(d, file.path(out_dir, "gaming_data.csv"), row.names = FALSE, na = "")
write.csv(audit, file.path(out_dir, "cell_audit.csv"), row.names = FALSE, na = "")
write.csv(flag_example, file.path(out_dir, "flag_example.csv"), row.names = FALSE, na = "")
jsonlite::write_json(list(groups = nrow(d), numeric_cells = nrow(audit),
  raw_files_unchanged = sum(hash_ok), selected_flags = sum(nzchar(audit$flag)),
  missing_selected_values = sum(is.na(audit$value)),
  positive_flag_check = flag_example$flag, missing_error_kept_missing = TRUE,
  source_count = 1, join_used = FALSE), file.path(day_dir, "import_check.json"),
  pretty = TRUE, auto_unbox = TRUE)
writeLines(c(R.version.string, capture.output(sessionInfo())),
  file.path(day_dir, "r_session.txt"))
cat("PASS: 24 real groups, 144 source cells, unchanged raw files and flags checked.\n")

02_results.R

# Read the final import; test source claims and compare real published groups.
if (.Platform$OS.type == "windows") Sys.setlocale("LC_ALL", "English_United States.utf8")
plan_root <- if (file.exists("../01_Five_Day_Plan.md")) {
  normalizePath("..", winslash = "/")
} else normalizePath("hw3/five_day_plan", winslash = "/", mustWork = TRUE)
data_dir <- file.path(plan_root, "project", "data")
day_dir <- file.path(plan_root, "day2")
d <- read.csv(file.path(data_dir, "gaming_data.csv"), stringsAsFactors = FALSE)
audit <- read.csv(file.path(data_dir, "cell_audit.csv"), stringsAsFactors = FALSE)
ref <- jsonlite::fromJSON(file.path(plan_root, "data_feasibility", "abs", "readiness", "checked_records.json"))
key <- function(x) paste(x$view, x$group, x$sex, sep = "|")
order <- match(key(d), key(ref))
stopifnot(!anyNA(order), !anyDuplicated(key(d)))
fields <- c("share", "player_minutes", "all_minutes", "share_moe",
  "player_time_rse", "all_time_rse")
for (f in fields) stopifnot(all(abs(d[[f]] - ref[[f]][order]) < 1e-8))

all_people <- d[d$sex == "All", , drop = FALSE]
claim_fields <- c("share", "player_minutes", "all_minutes")
claim_rows <- list()
for (i in seq_len(nrow(all_people))) for (f in claim_fields) {
  r <- all_people[i, ]
  a <- audit[audit$view == r$view & audit$group == r$group &
    audit$sex == r$sex & audit$field == f, , drop = FALSE]
  stopifnot(nrow(a) == 1L)
  claim_rows[[length(claim_rows) + 1L]] <- data.frame(view = r$view,
    group = r$group, sex = r$sex, measure = f, value = r[[f]], unit = a$unit,
    file = a$file, sheet = a$sheet, cell = a$cell)
}
claims <- do.call(rbind, claim_rows)
write.csv(claims, file.path(data_dir, "checked_claims.csv"), row.names = FALSE)

differences <- list()
for (v in c("work", "week")) for (s in c("All", "Male", "Female")) {
  a <- d[d$view == v & d$sex == s, , drop = FALSE]
  first_label <- if (v == "work") "Workday" else "Weekday"
  first <- a[a$group == first_label, , drop = FALSE]
  second <- a[a$group != first_label, , drop = FALSE]
  stopifnot(nrow(first) == 1L, nrow(second) == 1L)
  for (f in c("share", "player_minutes")) {
    lower <- if (f == "share") "share_lower" else "player_lower"
    upper <- if (f == "share") "share_upper" else "player_upper"
    differences[[length(differences) + 1L]] <- data.frame(view = v, sex = s,
      first_group = first$group, second_group = second$group, measure = f,
      first_value = first[[f]], second_value = second[[f]],
      difference = second[[f]] - first[[f]],
      unit = if (f == "share") "percentage points" else "minutes",
      first_lower = first[[lower]], first_upper = first[[upper]],
      second_lower = second[[lower]], second_upper = second[[upper]],
      ranges_overlap = max(first[[lower]], second[[lower]]) <=
        min(first[[upper]], second[[upper]]))
  }
}
differences <- do.call(rbind, differences)
write.csv(differences, file.path(data_dir, "day_comparisons.csv"), row.names = FALSE)

# Published percentages and minutes are rounded. Do not replace them with a
# number calculated from another rounded field.
rounding_gap <- d$all_minutes - d$share * d$player_minutes / 100
stopifnot(all(abs(rounding_gap) < 1))
jsonlite::write_json(list(source_benchmark_values_matched = 144,
  claim_rows = nrow(claims), day_comparisons = nrow(differences),
  maximum_rounding_gap_minutes = max(abs(rounding_gap)),
  chart_4_decision = "Keep: player time and gaming share give different views of a weekend.",
  caution = "Range overlap is descriptive, not a test of a difference. No paired covariance is available."),
  file.path(day_dir, "results_check.json"), pretty = TRUE, auto_unbox = TRUE)
cat("PASS: all 144 benchmarks match; 24 source claims and 12 day comparisons saved.\n")

04_charts.R

# Make all four assignment charts in R from the checked import.
if (.Platform$OS.type == "windows") Sys.setlocale("LC_ALL", "English_United States.utf8")
plan_root <- if (file.exists("../01_Five_Day_Plan.md")) {
  normalizePath("..", winslash = "/")
} else normalizePath("hw3/five_day_plan", winslash = "/", mustWork = TRUE)
project_dir <- file.path(plan_root, "project")
stage_args <- commandArgs(trailingOnly = TRUE)
stage <- if (length(stage_args)) stage_args[1] else "day3"
stopifnot(stage %in% c("day3", "day4", "day5"))
day_dir <- file.path(plan_root, stage)
dir.create(day_dir, showWarnings = FALSE)
library(plotly)
d <- read.csv(file.path(project_dir, "data", "gaming_data.csv"), stringsAsFactors = FALSE)
sexes <- c("All", "Male", "Female")
sex_name <- function(s) ifelse(s == "All", "All people", s)
blue <- "#245A81"
teal <- "#087E79"
ink <- "#233044"

pick <- function(view, sex, groups) {
  x <- d[d$view == view & d$sex == sex, , drop = FALSE]
  x <- x[match(groups, x$group), , drop = FALSE]
  stopifnot(nrow(x) == length(groups), !anyNA(x$group))
  x
}
measure_name <- function(m) {
  if (m == "share") "Share who played (%)" else "Time among players (minutes)"
}
time_axis <- list(title = list(text = "Time (minutes)"), range = c(0, 280),
  autorange = FALSE, zeroline = TRUE, gridcolor = "#e3e8ed", fixedrange = FALSE)
share_axis <- list(title = list(text = "Share who played (%)"), range = c(0, 40),
  autorange = FALSE, zeroline = TRUE, gridcolor = "#e3e8ed", fixedrange = FALSE)
error <- function(x, m) {
  lower <- if (m == "share") "share_lower" else if (m == "all_minutes") "all_lower" else "player_lower"
  upper <- if (m == "share") "share_upper" else if (m == "all_minutes") "all_upper" else "player_upper"
  list(type = "data", symmetric = FALSE, array = x[[upper]] - x[[m]],
    arrayminus = x[[m]] - x[[lower]], color = "#657386", thickness = 1.2, width = 4)
}
tip <- function(x, m, table) {
  label <- if (m == "all_minutes") "Time across everyone" else if (m == "share") "Share who played" else "Time among players"
  value <- if (m == "share") sprintf("%.1f%%", x[[m]]) else paste(round(x[[m]]), "minutes")
  lo <- if (m == "share") x$share_lower else if (m == "all_minutes") x$all_lower else x$player_lower
  hi <- if (m == "share") x$share_upper else if (m == "all_minutes") x$all_upper else x$player_upper
  kind <- if (m == "share") "Published 95% range" else "About 95% range (our estimate)"
  unit <- if (m == "share") "%" else "minutes"
  rse <- if (m == "all_minutes") x$all_time_rse else x$player_time_rse
  err_note <- if (m == "share") paste0("Margin of error: ", x$share_moe, " percentage points") else
    paste0("RSE: ", rse, "% (a measure of error)")
  paste0("<b>", sex_name(x$sex), " | ", x$group, "</b><br>", label, ": ", value,
    "<br>", kind, ": ", sprintf("%.1f", lo), " to ", sprintf("%.1f", hi), " ", unit,
    "<br>", err_note, "<br>Share who played: ", sprintf("%.1f", x$share), "%",
    "<br>Time among players: ", round(x$player_minutes), " minutes",
    "<br>Time across everyone: ", round(x$all_minutes), " minutes",
    "<br>Source: ABS 2024, ", table)
}
menu <- function(buttons) list(list(type = "dropdown", visible = FALSE, x = 0, y = 1.16,
  xanchor = "left", yanchor = "top", active = 0, buttons = buttons,
  bgcolor = "#ffffff", bordercolor = "#6b7786", font = list(size = 13, color = ink)))
finish <- function(p, buttons, xa, ya, height = 420, left = 105, control_label) {
  p$height <- height
  p <- layout(p, font = list(family = "Arial, sans-serif", size = 14, color = ink),
    paper_bgcolor = "#ffffff", plot_bgcolor = "#ffffff", showlegend = FALSE,
    margin = list(l = left, r = 30, t = 25, b = 65),
    xaxis = xa, yaxis = ya, updatemenus = menu(buttons), hovermode = "closest")
  # Plotly's built-in reset uses the first axis range, even after changing units.
  # This small widget control restores the currently selected menu view instead.
  js_views <- jsonlite::toJSON(buttons, auto_unbox = TRUE, digits = NA, null = "null")
  reset_button <- list(name = "Reset view", icon = list(width = 24, height = 24,
    path = "M12 4A8 8 0 1 0 20 12H17A5 5 0 1 1 12 7V10L18 5L12 0Z"),
    click = htmlwidgets::JS(paste0("function(gd) { var views = ", js_views,
      "; var i = gd.layout.updatemenus[0].active; var b = views[i >= 0 ? i : 0]; Plotly.update(gd, b.args[0], b.args[1] || {}); }")))
  p <- config(p, displaylogo = FALSE, responsive = TRUE, scrollZoom = FALSE, doubleClick = FALSE,
    modeBarButtons = list(list("zoomIn2d", "zoomOut2d", reset_button)))
  # Native HTML controls also work with a keyboard. Their view settings come
  # from R, with a fresh copy each time so zoom cannot change the saved ranges.
  htmlwidgets::onRender(p, paste0("function(el, x) {",
    "var old = document.getElementById(el.id + '-controls'); if(old) old.remove();",
    "var box = document.createElement('div'); box.id = el.id + '-controls'; box.className = 'chart-controls';",
    "var label = document.createElement('label'); label.htmlFor = el.id + '-view'; label.textContent = ",
    jsonlite::toJSON(control_label, auto_unbox = TRUE), ";",
    "var select = document.createElement('select'); select.id = el.id + '-view';",
    "var views = ", js_views, "; views.forEach(function(b, i) { var option = document.createElement('option');",
    "option.value = i; option.textContent = b.label; select.appendChild(option); });",
    "var reset = document.createElement('button'); reset.type = 'button'; reset.textContent = 'Reset view';",
    "reset.setAttribute('aria-label', ", jsonlite::toJSON(paste(control_label, "reset"), auto_unbox = TRUE), ");",
    "var note = document.createElement('span'); note.className = 'view-note'; note.setAttribute('aria-live', 'polite');",
    "function applyView() { var fresh = ", js_views, "; var i = Number(select.value); var b = fresh[i];",
    "var changes = b.args[1] || {}; changes['updatemenus[0].active'] = i;",
    "return Plotly.update(el, b.args[0], changes).then(function() { note.textContent = 'View: ' + b.label; }); }",
    "select.addEventListener('change', applyView); reset.addEventListener('click', applyView);",
    "box.appendChild(label); box.appendChild(select); box.appendChild(reset); box.appendChild(note);",
    "el.parentNode.insertBefore(box, el); note.textContent = 'View: ' + views[0].label; }"))
}
choose <- function(label, visible, changes = list()) {
  list(label = label, method = "update", execute = FALSE, args = list(list(visible = visible), changes))
}

# Chart 1: the same survey has two different bases for average time.
p1 <- plot_ly()
for (s in sexes) {
  x <- pick("overall", s, "All ages 15+")
  bases <- c("Time among players", "Time across everyone")
  values <- c(x$player_minutes, x$all_minutes)
  errs <- list(type = "data", symmetric = FALSE,
    array = c(x$player_upper - x$player_minutes, x$all_upper - x$all_minutes),
    arrayminus = c(x$player_minutes - x$player_lower, x$all_minutes - x$all_lower),
    color = "#657386", thickness = 1.2, width = 4)
  p1 <- add_trace(p1, type = "bar", orientation = "h", x = values, y = bases,
    marker = list(color = c(blue, teal)), error_x = errs, name = sex_name(s),
    text = c(tip(x, "player_minutes", "Tables 1.1-1.2"), tip(x, "all_minutes", "Tables 1.1-1.2")),
    textposition = "none", hoverinfo = "text", customdata = bases,
    meta = paste("overall", s, "time", sep = "|"), visible = s == "All")
}
b1 <- lapply(seq_along(sexes), function(i) choose(sex_name(sexes[i]), seq_along(sexes) == i,
  list("xaxis.range" = c(0, 280), "xaxis.autorange" = FALSE,
    "yaxis.range" = c(-0.5, 1.5), "yaxis.autorange" = FALSE)))
chart1 <- finish(p1, b1, time_axis,
  list(title = list(text = ""), range = c(-0.5, 1.5), autorange = FALSE,
    categoryorder = "array", categoryarray = rev(c("Time among players", "Time across everyone"))),
  height = 350, left = 170, control_label = "Chart 1 view")

# Chart 2: only the three non-overlapping age groups are shown.
p2 <- plot_ly()
ages <- c("15-24", "25-64", "65+")
for (s in sexes) {
  x <- pick("age", s, ages)
  p2 <- add_trace(p2, type = "scatter", mode = "markers+text", x = x$share, y = x$player_minutes,
    marker = list(size = 13, color = c(blue, teal, "#8A4D1E"), symbol = c("circle", "square", "diamond")),
    text = ages, hovertext = tip(x, "player_minutes", "Tables 3.1-3.6"), hoverinfo = "text",
    textposition = "top right", error_x = error(x, "share"), error_y = error(x, "player_minutes"),
    customdata = ages, meta = paste("age", s, "player_minutes", sep = "|"), visible = s == "All")
}
b2 <- lapply(seq_along(sexes), function(i) choose(sex_name(sexes[i]), seq_along(sexes) == i,
  list("xaxis.range" = c(0, 40), "yaxis.range" = c(0, 280),
    "xaxis.autorange" = FALSE, "yaxis.autorange" = FALSE)))
chart2 <- finish(p2, b2, share_axis,
  modifyList(time_axis, list(title = list(text = "Time among players (minutes)"))), height = 450, left = 90,
  control_label = "Chart 2 view")

# Chart 3: paired rows let readers compare all three source sex groups on one scale.
p3 <- plot_ly()
measures <- c("player_minutes", "share")
for (m in measures) for (i in seq_along(sexes)) {
  s <- sexes[i]
  x <- pick("work", s, c("Workday", "Not a workday"))
  p3 <- add_trace(p3, type = "scatter", mode = "lines+markers", x = x[[m]], y = rep(4 - i, 2),
    marker = list(size = 12, color = c(blue, teal), symbol = c("circle", "square")),
    line = list(color = "#a4aebb", width = 2), error_x = error(x, m),
    text = tip(x, m, "Tables 9.1-9.2"), hoverinfo = "text", customdata = x$group,
    meta = paste("work", s, m, sep = "|"), visible = m == "player_minutes")
}
b3 <- lapply(seq_along(measures), function(i) {
  m <- measures[i]
  changes <- list()
  changes[["xaxis.title.text"]] <- measure_name(m)
  changes[["xaxis.range"]] <- if (m == "share") c(0, 40) else c(0, 280)
  changes[["xaxis.autorange"]] <- FALSE
  changes[["yaxis.range"]] <- c(0.5, 3.5)
  choose(measure_name(m), rep(seq_along(measures) == i, each = 3), changes)
})
chart3 <- finish(p3, b3, modifyList(time_axis, list(title = list(text = measure_name("player_minutes")))),
  list(title = list(text = ""), range = c(0.5, 3.5), tickvals = c(3, 2, 1),
    ticktext = c("All people", "Male", "Female"), zeroline = FALSE, showgrid = FALSE), left = 100,
  control_label = "Chart 3 view")

# Chart 4: one menu controls group, unit, scale and error ranges together.
# One combined menu avoids losing the state of a second menu.
p4 <- plot_ly()
choices <- list()
for (s in sexes) for (m in measures) {
  x <- pick("week", s, c("Weekday", "Weekend"))
  p4 <- add_trace(p4, type = "scatter", mode = "lines+markers", x = c(0, 1), y = x[[m]],
    marker = list(size = 13, color = c(blue, teal), symbol = c("circle", "square")),
    line = list(color = "#a4aebb", width = 2), error_y = error(x, m),
    text = tip(x, m, "Tables 4.1-4.2"), hoverinfo = "text", customdata = x$group,
    meta = paste("week", s, m, sep = "|"), visible = s == "All" && m == "player_minutes")
  choices[[length(choices) + 1L]] <- list(sex = s, measure = m)
}
b4 <- lapply(seq_along(choices), function(i) {
  ch <- choices[[i]]
  changes <- list()
  changes[["yaxis.title.text"]] <- measure_name(ch$measure)
  changes[["yaxis.range"]] <- if (ch$measure == "share") c(0, 40) else c(0, 280)
  changes[["yaxis.autorange"]] <- FALSE
  changes[["xaxis.range"]] <- c(-0.3, 1.3)
  short <- if (ch$measure == "share") "share" else "player time"
  choose(paste0(sex_name(ch$sex), " | ", short), seq_along(choices) == i, changes)
})
chart4 <- finish(p4, b4,
  list(title = list(text = "Type of day"), range = c(-0.3, 1.3), tickvals = c(0, 1),
    ticktext = c("Weekday", "Weekend"), showgrid = FALSE, zeroline = FALSE),
  modifyList(time_axis, list(title = list(text = measure_name("player_minutes")))), left = 90,
  control_label = "Chart 4 view")

charts <- list(chart1 = chart1, chart2 = chart2, chart3 = chart3, chart4 = chart4)
saveRDS(charts, file.path(project_dir, "data", "charts.rds"))
specs <- lapply(names(charts), function(id) {
  built <- plotly_build(charts[[id]])
  # A full trace export lets the separate checker test real plotted values.
  list(id = id, data = built$x$data, layout = built$x$layout)
})
jsonlite::write_json(specs, file.path(day_dir, "chart_specs.json"), pretty = TRUE,
  auto_unbox = TRUE, null = "null", digits = NA)
rmarkdown::render(file.path(project_dir, "report.Rmd"), output_file = "report.html",
  envir = new.env(parent = globalenv()), quiet = TRUE)
writeLines(capture.output(sessionInfo()), file.path(day_dir, "r_session.txt"))
cat("Four R charts and one HTML report saved. Separate value and browser checks are next.\n")

report.Rmd

---
title: "Time to Play: Gaming in Australian Daily Life"
author: "Wentzu Lai"
output:
  html_document:
    self_contained: true
    theme: readable
    toc: false
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE, message = FALSE, warning = FALSE)
library(plotly)
charts <- readRDS("data/charts.rds")
data <- read.csv("data/gaming_data.csv", stringsAsFactors = FALSE)
```

<style>
body { color: #233044; font-size: 17px; line-height: 1.55; }
.main-container { max-width: 1050px; }
h1.title { font-size: 36px; line-height: 1.15; }
h2 { margin-top: 36px; font-size: 25px; }
.chart-source { color: #42536a; font-size: 14px; margin-top: 4px; }
.guide { border-left: 4px solid #245a81; padding: 10px 14px; background: #f2f6fa; }
table { width: 100%; font-size: 15px; }
.plotly { margin-top: 8px; }
.chart-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 16px; font-size: 15px; }
.chart-controls label { margin: 0; }
.chart-controls select, .chart-controls button { background: white; color: #233044; border: 1px solid #657386; border-radius: 4px; padding: 7px 10px; max-width: 100%; }
.chart-controls select:focus-visible, .chart-controls button:focus-visible, summary:focus-visible { outline: 3px solid #245a81; outline-offset: 3px; }
.view-note { color: #42536a; font-size: 14px; }
pre { overflow-x: auto; }
</style>

I play both computer and mobile games. This report looks at gaming in Australia, where I study.

**Australia · 2024 · People aged 15+ in the survey.** Video and mobile games are counted together.

<div class="guide">
Hover over a bar or point to see its values. Use the menu above each chart to change the view.
Use the chart toolbar to zoom. Reset view keeps your chosen group and measure and undoes zoom.
You can also use Tab, arrow keys and Enter with the menu and Reset view button.
Choose the first menu item to return to the starting view.
</div>

## 1. A long session, a small daily share

In the All people view, those who played spent **170 minutes** on gaming that day.
Across everyone, including people who did not play, the average was **18 minutes**.
Only **10.4%** recorded gaming that day. The base of an average changes its meaning.

```{r chart1}
charts$chart1
```

<p class="chart-source">Source: Australian Bureau of Statistics (ABS, 2025), 2024 Time Use Survey, Tables 1.1-1.2.</p>

## 2. Younger people stand apart in both views

In the All people view, the 15-24 group had the largest share who played and the longest player time.
Choose a sex group to see how its age pattern looks. Each point shows age, daily share and player time together.

```{r chart2}
charts$chart2
```

<p class="chart-source">Source: ABS (2025), 2024 Time Use Survey, Tables 3.1-3.6. Age groups do not overlap.</p>

## 3. Days with paid work show less gaming

In the All people view, player time was **138 minutes** on workdays and **188 minutes** on days without paid work.
Switch to share to compare how many people played. These are group estimates. They do not show that paid work caused the gap.

**Circle: workday. Square: day without paid work.** A workday has paid work recorded in the diary.
A day without paid work does not mean the person was unemployed.

```{r chart3}
charts$chart3
```

<p class="chart-source">Source: ABS (2025), 2024 Time Use Survey, Tables 9.1-9.2.</p>

## 4. Weekends show a smaller gap

In the All people view, player time was **162 minutes** on weekdays and **189 minutes** on weekends.
The shares were closer: **10.1%** and **11.1%**. Choose **All people | share** to see their error ranges overlap.
That overlap alone is not a test of a difference.

A weekend is not the same as a day without paid work. These are two ways of grouping the same survey days.
They do not compare each person's gaming on two types of day.

```{r chart4}
charts$chart4
```

<p class="chart-source">Source: ABS (2025), 2024 Time Use Survey, Tables 4.1-4.2.</p>

## Data notes and limits

- **Share who played:** the percent who recorded gaming as a main activity for at least five minutes on a diary day. It is not the share of regular gamers.
- **Time among players:** the average time for people who played that day.
- **Time across everyone:** the average for the full group, including people who did not play.
- **Error ranges:** share ranges use the ABS 95% margin of error. Time ranges are our rough 95% estimates: time plus or minus 1.96 × time × RSE / 100. RSE means relative standard error. It measures how much a survey estimate may vary. We keep ranges within possible values. These time ranges are not new official ABS ranges.
- **Sex groups:** ABS records sex at birth. All people is the published Persons total. It includes other sex terms too. It is not made by adding Male and Female.
- **Scope:** the survey covers people aged 15+ in private homes within its scope. Very remote areas and discrete Aboriginal and Torres Strait Islander communities are outside the scope. Other survey exclusions also apply. Read the [ABS methods](https://www.abs.gov.au/methodologies/how-australians-use-their-time-methodology/2024) for the full list. These notes belong to the same ABS data release.
- **Limits:** the data combines video and mobile games. It cannot compare computers with phones, Steam with other platforms, or single games. The separate tables cannot show new joint groups or prove causes. No comparison with 2020-21 is made because survey methods changed.
- **Access:** labels and shapes help show meaning without colour. The menus and Reset view buttons work with a keyboard. The plain table below gives all key values too.

<details><summary>Open the key values table</summary>

```{r plain_table, results='asis'}
plain <- data[, c("group", "sex", "share", "player_minutes", "all_minutes")]
plain$sex[plain$sex == "All"] <- "All people"
names(plain) <- c("Group", "Sex group", "Share who played (%)", "Time among players (minutes)", "Time across everyone (minutes)")
knitr::kable(plain, format = "html", escape = TRUE, row.names = FALSE)
```

</details>

## Acknowledgements

I used Codex (OpenAI, 2026) for planning, source searches, R code, chart controls, draft text, publishing help and checks.
The starting gaming interest and wish to relate gaming to other factors came from the student.
The title, story plan, report text and code used AI help. The exact model release was not recorded.
Source values were checked against the original ABS tables. Python was used for a separate check, not for making the assignment charts.
I will review the draft and the AI help before submitting. Appendix A records key prompts and the checked outputs.

The data comes from the Australian Bureau of Statistics (ABS).
The tables, methods and reuse notes helped guide the work. The project uses one independent survey source.
The data combines video and mobile games. It covers 2024 and people aged 15+ within the ABS survey scope.

## References

Australian Bureau of Statistics. (2025). *How Australians use their time, 2024* [Data set]. <https://www.abs.gov.au/statistics/people/people-and-communities/how-australians-use-their-time/2024>

OpenAI. (2026, October 3). *Help with Time to Play: Gaming in Australian daily life* [Generative AI chat]. Codex. <https://openai.com/codex/>. See Appendix A for prompts and outputs.

## Appendix A: AI prompts and outputs

Codex helped during 30 September to 3 October 2026. The reference date is the date of this checked version.
The chat title in the reference describes this work. The tool was Codex by OpenAI.
This is a record of key requests and outputs kept for this report, not a copy of the full chat.
Prompts are shown in their original Chinese. English notes give a short summary.
Local file paths and private account details are left out. They are not needed to repeat the work.

<details><summary>Open the AI prompt record</summary>

```{r ai_prompts, results='asis'}
prompt_text <- paste(readLines("ai_prompts.md", encoding = "UTF-8"), collapse = "\n")
for (part in strsplit(prompt_text, "\n\n", fixed = TRUE)[[1]]) {
  if (startsWith(part, "### ")) {
    cat("<h3>", htmltools::htmlEscape(sub("^### ", "", part)), "</h3>\n", sep = "")
  } else cat("<p>", htmltools::htmlEscape(part), "</p>\n", sep = "")
}
```

</details>

The outputs kept after checks are the title, notes, four R charts, controls and report text above.
The code below is the saved output used to build them. It includes changes made after checks.
The source data is from ABS. It was not made by AI. The local AI use log also records checks and fixes.

<details><summary>Open the checked R and report code</summary>

```{r ai_code_output, results='asis'}
for (f in c("R/01_import.R", "R/02_results.R", "R/04_charts.R", "report.Rmd")) {
  cat("<h3>", basename(f), "</h3><pre><code>", sep = "")
  cat(htmltools::htmlEscape(paste(readLines(f, encoding = "UTF-8"), collapse = "\n")))
  cat("</code></pre>\n")
}
```

</details>