Pakistan’s Exports: What Sells and Where It Goes

A self-updating report, pulled live from the SBP EasyData API at every render

Author

Zahid Asghar

Published

22 September 2026

NoteThe story so far (data to August 2026)

In August 2026, Pakistan exported goods worth $2.46 billion, down 1.1% on August 2025. Over the last twelve months goods exports reached $31.03 billion, down 5.3% on the year before. FY2025-26 closed at $30.82 billion (down 4.7%). The first 2 months of FY2026-27 brought in $5.44 billion, up 4.0% on the same months of FY2025-26. Services added another $10.43 billion over the last twelve months, 25.2% of total exports.

Every number above is calculated at render time from data downloaded from SBP EasyData on 22 September 2026. Hover over any chart for exact values; drag the slider under a chart to zoom into a period.

About the data

Goods and services exports come from SBP’s monthly balance of payments summary (BPM6), in million US dollars.

Goods exports

Figure 1
Figure 2

The bars swing with Eid, working days and shipment timing, so the rolling line is the better guide to direction.

Figure 3
Figure 4: Cumulative goods exports through the fiscal year

This chart puts the current fiscal year beside the three before it, month by month, so it shows at a glance whether the year is running ahead or behind.

Services exports

Figure 5
Figure 6
Figure 7

Fiscal year summary

Table 1: Growth is shown only between complete fiscal years
Fiscal year Goods (US$ m) Goods growth Services (US$ m) Services growth Services share of total
FY2013-14 25078 NA 5345 NA 17.6%
FY2014-15 24090 -3.9% 5872 9.9% 19.6%
FY2015-16 21972 -8.8% 5456 -7.1% 19.9%
FY2016-17 22003 0.1% 5915 8.4% 21.2%
FY2017-18 24768 12.6% 5851 -1.1% 19.1%
FY2018-19 24257 -2.1% 5966 2.0% 19.7%
FY2019-20 22536 -7.1% 5437 -8.9% 19.4%
FY2020-21 25639 13.8% 5945 9.3% 18.8%
FY2021-22 32493 26.7% 7102 19.5% 17.9%
FY2022-23 27876 -14.2% 7596 7.0% 21.4%
FY2023-24 30980 11.1% 7691 1.3% 19.9%
FY2024-25 32343 4.4% 8450 9.9% 20.7%
FY2025-26 30821 -4.7% 10020 18.6% 24.5%
FY2026-27 (to date) 5445 NA 1811 NA 25.0%

The last twelve months

Table 2
Month Goods (US$ m) Goods, vs a year earlier Services (US$ m) Services, vs a year earlier
Sep 2025 2609 -0.3% 807 23.8%
Oct 2025 2632 -12.4% 811 15.2%
Nov 2025 2277 -18.4% 797 11.6%
Dec 2025 2758 -11.3% 946 17.2%
Jan 2026 2749 -8.6% 868 28.8%
Feb 2026 2481 -4.9% 799 14.8%
Mar 2026 2528 -8.4% 903 16.2%
Apr 2026 2619 0.5% 904 20.4%
May 2026 2357 -3.6% 838 15.7%
Jun 2026 2573 -0.7% 942 35.0%
Jul 2026 2985 8.5% 939 29.0%
Aug 2026 2460 -1.1% 872 28.8%

Keeping it current

Each render downloads every series again, so quarto render exports_sbp.qmd after SBP’s monthly release updates every number, chart and sentence. SBP occasionally fails a request, so each series is tried up to four times. Destination and product sections appear automatically once their series codes are listed in sbp_export_series.csv (build it with -P rediscover:true); until then the report covers goods and services only.

Table 3

Appendix: code

Code
library(tidyverse)
library(httr2)
library(tsibble)
library(knitr)
library(plotly)
library(plotly)

if (!exists("params")) params <- list(rediscover = FALSE)
theme_set(theme_minimal(base_size = 12))

start_date <- "2001-07-01"
keys_file  <- "sbp_export_series.csv"   # series codes and names only, no data

# Headline exports, BPM6 monthly summary
headline_keys <- c(
  goods    = "TS_GP_BOP_BPM6SUM_M.P00030",   # Exports of goods FOB
  services = "TS_GP_BOP_BPM6SUM_M.P00060"    # Exports of services
)

# Breakdowns and candidate series-code prefixes (used only when scanning)
blocks <- tribble(
  ~block,       ~dataset,               ~prefixes,
  "country",    "TS_GP_BOP_XRECCOU_M",  list(c("EXPC", "EXPA")),   # EXPC mirrors the confirmed IMPC
  "product",    "TS_GP_BOP_XRECCOM_M",  list(c("EXPA", "EXPC"))    # EXPA confirmed: ...XRECCOM_M.EXPA00420
)

block_title <- c(country = "export destinations", product = "export products")

country_agg <- regex(str_c(
  "\\btotal\\b|countries|region|union|world|unspecified|not specified|n\\.e\\.s|",
  "^(asia|africa|europe|oceania|america)\\b|^(north|south|latin|central) america|",
  "^(east|west|south|central|north)(ern)? (asia|europe)|middle east|far east|",
  "\\bgcc\\b|saarc|asean|opec|commonwealth|^others?\\b"
), ignore_case = TRUE)

product_agg <- regex("\\btotal\\b|^others?\\b|\\bgroup\\b", ignore_case = TRUE)
sbp_key <- Sys.getenv("SBP_EASYDATA_KEY")
if (!nzchar(sbp_key)) {
  stop("SBP_EASYDATA_KEY not found. Add it with usethis::edit_r_environ() and restart R.")
}

sbp_req <- function(key, start = start_date, end = Sys.Date(), timeout = 30) {
  request("https://easydata.sbp.org.pk/api/v1/series") |>
    req_url_path_append(key, "data") |>
    req_url_query(api_key = sbp_key, format = "json",
                  start_date = as.character(as.Date(start)),
                  end_date   = as.character(as.Date(end))) |>
    req_headers(Accept = "application/json") |>
    req_user_agent("pakistan-export-report (R httr2)") |>
    req_timeout(timeout) |>
    req_error(is_error = \(r) FALSE)
}

# SBP JSON: { columns: [...], rows: [[dataset, key, name, date, value, unit, ...], ...] }
parse_sbp <- function(resp, key) {
  body <- resp_body_json(resp, simplifyVector = FALSE)
  if (is.null(body$rows) || length(body$rows) == 0) return(NULL)

  cols <- str_to_lower(unlist(body$columns))
  find <- function(pattern, fallback) {
    i <- which(str_detect(cols, pattern))
    if (length(i) == 0) fallback else i[1]
  }
  i_name <- find("series name|^series$", 3)
  i_date <- find("observation date", 4)
  i_val  <- find("observation value", 5)
  cell   <- function(r, i) if (length(r) >= i && !is.null(r[[i]])) as.character(r[[i]]) else NA_character_

  tibble(
    key         = key,
    series_name = map_chr(body$rows, cell, i = i_name),
    date        = map_chr(body$rows, cell, i = i_date),
    value       = map_chr(body$rows, cell, i = i_val)
  ) |>
    mutate(
      month = yearmonth(as.Date(parse_date_time(date, c("Ymd", "dbY", "dby", "dmY"), quiet = TRUE))),
      value = parse_number(value)
    ) |>
    filter(!is.na(month), !is.na(value)) |>
    select(key, series_name, month, value)
}
safe_parse <- possibly(parse_sbp, otherwise = NULL)

empty_pull <- tibble(key = character(), series_name = character(),
                     month = yearmonth(as.Date(character())), value = numeric())

# One series at a time, with patient retries. SBP sometimes fails a call and
# answers the same call a few seconds later, so each series gets up to
# `attempts` tries with growing waits.
pull_keys <- function(keys, start = start_date, timeout = 30, attempts = 4,
                      waits = c(2, 5, 10), pause = 0.8,
                      deadline = Inf, verbose = TRUE) {
  keys <- unique(keys)
  if (length(keys) == 0) return(empty_pull)
  out <- vector("list", length(keys))

  for (i in seq_along(keys)) {
    note <- ""
    for (a in seq_len(attempts)) {
      if (Sys.time() > deadline) break
      Sys.sleep(if (a == 1) pause else waits[min(a - 1, length(waits))])
      resp <- tryCatch(req_perform(sbp_req(keys[i], start = start, timeout = timeout)),
                       error = \(e) e)

      if (!inherits(resp, "httr2_response")) {
        note <- str_c("request failed: ", conditionMessage(resp))
      } else if (resp_status(resp) != 200) {
        note <- str_c("HTTP ", resp_status(resp))
        if (resp_status(resp) == 404) break          # wrong code, no point retrying
      } else {
        out[[i]] <- safe_parse(resp, keys[i])
        if (!is.null(out[[i]])) { note <- str_c("ok, ", nrow(out[[i]]), " rows"); break }
        note <- "HTTP 200 but no usable rows"
      }
    }
    if (verbose) message(sprintf("  [%d/%d] %s  %s (attempt %d)",
                                 i, length(keys), keys[i], note, a))
    if (Sys.time() > deadline) { message("  time limit reached, stopping"); break }
  }
  out <- list_rbind(out)
  if (is.null(out) || ncol(out) == 0) empty_pull else out
}
# Scanning is OFF by default. Run it once with -P rediscover:true, or write
# sbp_export_series.csv by hand with columns: block, key, series_name.

code_keys <- function(dataset, prefix, width, codes) {
  str_c(dataset, ".", prefix, str_pad(codes, width, pad = "0"))
}

# Walk the codes upward in steps of 10. Stop when there has been no hit for a
# long stretch, so the scan ends by itself once the list of series runs out.
scan_prefix <- function(dataset, prefix, deadline, width = 5, step = 10,
                        give_up_before_first = 80, give_up_after_last = 40) {
  recent <- Sys.Date() - 540
  hits <- list(); misses <- 0; code <- step; any_hit <- FALSE
  repeat {
    if (Sys.time() > deadline) { message("  time limit reached"); break }
    key <- code_keys(dataset, prefix, width, code)
    got <- pull_keys(key, start = recent, timeout = 10, attempts = 2,
                     waits = 2, pause = 0.3, verbose = FALSE)
    if (nrow(got) > 0) {
      any_hit <- TRUE; misses <- 0
      hits[[length(hits) + 1]] <- got |> summarise(series_name = last(series_name), .by = key)
      message("  ", key, "  ", last(got$series_name))
    } else {
      misses <- misses + 1
    }
    if (!any_hit && misses >= give_up_before_first) break
    if (any_hit && misses >= give_up_after_last) break
    code <- code + step
  }
  list_rbind(hits)
}

discover_block <- function(dataset, prefixes, deadline) {
  for (p in prefixes) {
    message("Scanning ", dataset, ".", p, "...")
    found <- scan_prefix(dataset, p, deadline)
    if (!is.null(found) && nrow(found) > 0) return(found)
  }
  NULL
}

if (isTRUE(params$rediscover)) {
  deadline <- Sys.time() + 20 * 60
  message("Scanning SBP for export series (max 20 minutes)...")
  found <- blocks |>
    mutate(found = map2(dataset, prefixes, \(d, p) discover_block(d, p, deadline))) |>
    select(block, found) |>
    unnest(found)
  if (nrow(found) > 0) write_csv(found, keys_file)
}

directory <- if (file.exists(keys_file)) {
  read_csv(keys_file, show_col_types = FALSE)
} else {
  tibble(block = character(), key = character(), series_name = character())
}
raw <- pull_keys(c(headline_keys, directory$key))
pulled_on <- format(Sys.Date(), "%d %B %Y")

if (!headline_keys[["goods"]] %in% raw$key) {
  stop("SBP did not return goods exports (", headline_keys[["goods"]], ") after several tries. ",
       "The messages above show what SBP answered; please render again in a few minutes.")
}

roll12 <- function(x) as.numeric(stats::filter(x, rep(1, 12), sides = 1))

# ---- Headline exports ----
exp_m <- raw |>
  filter(key %in% headline_keys) |>
  mutate(series = names(headline_keys)[match(key, headline_keys)], value = abs(value)) |>
  distinct(month, series, .keep_all = TRUE) |>
  select(month, series, value) |>
  pivot_wider(names_from = series, values_from = value) |>
  (\(d) { if (!"services" %in% names(d)) d$services <- NA_real_; d })() |>
  arrange(month) |>
  filter(!is.na(goods)) |>
  mutate(
    goods12  = roll12(goods),
    serv12   = roll12(services),
    goods_yoy   = goods / lag(goods, 12) - 1,
    goods12_yoy = goods12 / lag(goods12, 12) - 1,
    serv_yoy    = services / lag(services, 12) - 1,
    serv12_yoy  = serv12 / lag(serv12, 12) - 1,
    serv12   = roll12(services),
    fy       = year(as.Date(month)) + (month(as.Date(month)) >= 7),
    fy_month = (month(as.Date(month)) - 7) %% 12 + 1
  )

# ---- Destinations and products ----
detail <- raw |>
  inner_join(select(directory, key, block), by = join_by(key)) |>
  mutate(label = str_squish(series_name)) |>
  distinct(key, month, .keep_all = TRUE)

if (nrow(detail) > 0) {
  series_info <- detail |>
    summarise(mean_v = mean(value), .by = c(block, key, label)) |>
    mutate(
      named_total = str_detect(label, regex("\\btotal\\b", ignore_case = TRUE)),
      others_sum  = sum(mean_v) - mean_v,
      .by = block
    ) |>
    mutate(
      total_key = {
        cand <- key[named_total]
        if (length(cand) > 0) cand[which.max(mean_v[named_total])]
        else if (max(mean_v) > 0.5 * others_sum[which.max(mean_v)]) key[which.max(mean_v)]
        else NA_character_
      },
      total_mean = if (all(is.na(total_key))) NA_real_ else mean_v[key == first(total_key)],
      .by = block
    ) |>
    mutate(role = case_when(
      key == total_key ~ "Total",
      block == "country" & str_detect(label, country_agg) ~ "Aggregate",
      block == "product" & str_detect(label, product_agg) ~ "Aggregate",
      !is.na(total_mean) & mean_v > 0.5 * total_mean ~ "Aggregate",
      .default = "Item"
    ))

  items <- detail |>
    semi_join(filter(series_info, role == "Item"), by = join_by(key)) |>
    summarise(value = sum(value), .by = c(block, label, month))

  block_totals <- bind_rows(
    detail |>
      semi_join(filter(series_info, role == "Total"), by = join_by(key)) |>
      select(block, month, value),
    items |>
      filter(!block %in% series_info$block[series_info$role == "Total"]) |>
      summarise(value = sum(value), .by = c(block, month))
  )
} else {
  series_info  <- tibble(block = character(), key = character(), label = character(),
                         mean_v = numeric(), role = character())
  items        <- tibble(block = character(), label = character(),
                         month = yearmonth(as.Date(character())), value = numeric())
  block_totals <- tibble(block = character(), month = yearmonth(as.Date(character())), value = numeric())
}

has <- as.list(set_names(names(block_title) %in% unique(items$block), names(block_title)))
usd <- function(x) {
  x <- unname(x)
  if_else(abs(x) >= 1000,
          str_c("$", format(round(abs(x) / 1000, 2), nsmall = 2), " billion"),
          str_c("$", format(round(abs(x)), big.mark = ","), " million"))
}
pct   <- function(x, d = 1) str_c(format(round(100 * abs(unname(x)), d), nsmall = d), "%")
chg   <- function(x) if_else(is.na(x), "new", str_c(if_else(x >= 0, "up ", "down "), pct(x)))
mlab  <- function(m) format(as.Date(m), "%B %Y")
fylab <- function(fy) str_c("FY", fy - 1, "-", sprintf("%02d", fy %% 100))
list_and <- function(x) {
  if (length(x) <= 1) return(x)
  str_c(str_c(head(x, -1), collapse = ", "), " and ", tail(x, 1))
}

analyse_block <- function(blk, n = 10) {
  tot <- block_totals |> filter(block == blk)
  if (nrow(tot) == 0) return(NULL)
  latest_b <- max(tot$month)
  in_now   <- function(m) m > latest_b - 12 & m <= latest_b
  in_prev  <- function(m) m > latest_b - 24 & m <= latest_b - 12
  tot_now  <- sum(tot$value[in_now(tot$month)])
  tot_prev <- sum(tot$value[in_prev(tot$month)])

  tbl <- items |>
    filter(block == blk) |>
    summarise(now = sum(value[in_now(month)]), before = sum(value[in_prev(month)]), .by = label) |>
    mutate(
      growth       = if_else(before > 0, now / before - 1, NA_real_),
      share        = now / tot_now,
      share_before = before / tot_prev,
      rank_now     = min_rank(desc(now)),
      rank_before  = min_rank(desc(before))
    ) |>
    arrange(rank_now, label)

  top <- slice_head(tbl, n = n)
  list(
    block = blk, latest = latest_b, tot_now = tot_now,
    table = tbl, top = top,
    top_share = sum(top$share),
    top_share_before = sum(slice_min(tbl, rank_before, n = n, with_ties = FALSE)$share_before),
    newcomers = top$label[top$rank_before > n]
  )
}

A <- map(set_names(names(block_title)), \(b) if (isTRUE(has[[b]])) analyse_block(b) else NULL)

block_msg <- function(a) {
  if (is.null(a)) return("")
  what   <- block_title[[a$block]]
  ranked <- filter(a$top, !is.na(growth))
  fast   <- slice_max(ranked, growth, n = 1, with_ties = FALSE)
  slow   <- slice_min(ranked, growth, n = 1, with_ties = FALSE)
  str_c(
    str_glue(
      "In the twelve months to {mlab(a$latest)}, the top 10 {what} accounted for {pct(a$top_share)} ",
      "of the {usd(a$tot_now)} total ({if_else(a$top_share >= a$top_share_before, 'up', 'down')} from ",
      "{pct(a$top_share_before)} a year earlier). {a$top$label[1]} leads with {usd(a$top$now[1])}, ",
      "a {pct(a$top$share[1])} share, followed by {a$top$label[2]} ({pct(a$top$share[2])}) and ",
      "{a$top$label[3]} ({pct(a$top$share[3])}). Within the top 10, {fast$label} grew fastest ",
      "({chg(fast$growth)}) while {slow$label} ",
      "{if_else(slow$growth < 0, 'fell furthest', 'grew least')} ({chg(slow$growth)}). "
    ),
    if (length(a$newcomers) > 0) {
      str_glue("{list_and(a$newcomers)} {if (length(a$newcomers) > 1) 'are' else 'is'} new to the top 10 this year.")
    } else {
      "The top 10 line-up is unchanged from the previous year."
    }
  )
}

plot_top <- function(a) {
  if (is.null(a)) return(invisible(NULL))
  a$top |>
    mutate(label = fct_reorder(label, now),
           tag = str_c(pct(share), "  ·  ", chg(growth))) |>
    ggplot(aes(now / 1000, label,
               text = str_c(label, "<br>", usd(now), "<br>Share: ", pct(share),
                            "<br>Change: ", chg(growth)))) +
    geom_col(fill = "#1b5e20", width = 0.7) +
    geom_text(aes(label = tag), hjust = -0.05, size = 3.2, colour = "grey30") +
    scale_x_continuous(expand = expansion(mult = c(0, 0.35))) +
    labs(x = str_c("US$ billion, 12 months to ", mlab(a$latest)), y = NULL,
         caption = "Labels: share of total · change on the previous 12 months")
}

plot_trend <- function(a) {
  if (is.null(a)) return(invisible(NULL))
  items |>
    filter(block == a$block, label %in% a$top$label) |>
    select(label, month, value) |>
    complete(label, month = seq(min(month), max(month), by = 1), fill = list(value = 0)) |>
    arrange(label, month) |>
    mutate(r12 = roll12(value), .by = label) |>
    filter(!is.na(r12)) |>
    mutate(label = factor(label, levels = a$top$label)) |>
    ggplot(aes(as.Date(month), r12 / 1000)) +
    geom_line(colour = "#1b5e20", linewidth = 0.8) +
    facet_wrap(~label, ncol = 5, scales = "free_y") +
    labs(x = NULL, y = "US$ billion, rolling 12 months") +
    theme(strip.text = element_text(face = "bold", size = 9),
          axis.text.x = element_text(size = 7))
}

table_top <- function(a) {
  if (is.null(a)) return(invisible(NULL))
  a$top |>
    transmute(
      Rank = rank_now,
      Name = label,
      `12 months (US$ m)` = round(now),
      `Previous 12 months (US$ m)` = round(before),
      Change = if_else(is.na(growth), "new", scales::percent(growth, accuracy = 0.1)),
      Share = scales::percent(share, accuracy = 0.1),
      `Rank a year ago` = rank_before
    ) |>
    kable()
}
latest  <- max(exp_m$month)
lr      <- exp_m |> filter(month == latest)
ly      <- exp_m |> filter(month == latest - 12)
cur_fy  <- lr$fy
cur_fm  <- lr$fy_month
fytd    <- sum(exp_m$goods[exp_m$fy == cur_fy])
fytd_ly <- sum(exp_m$goods[exp_m$fy == cur_fy - 1 & exp_m$fy_month <= cur_fm])

fy_done <- exp_m |>
  summarise(goods = sum(goods), months = n(), .by = fy) |>
  filter(months == 12) |>
  arrange(fy) |>
  mutate(growth = goods / lag(goods) - 1)
last_fy <- slice_tail(fy_done, n = 1)

svc_ok  <- !all(is.na(exp_m$services))
svc12   <- if (svc_ok) sum(tail(exp_m$services, 12), na.rm = TRUE) else NA

msg_head <- str_c(
  str_glue(
    "In {mlab(latest)}, Pakistan exported goods worth {usd(lr$goods)}, ",
    "{chg(lr$goods / ly$goods - 1)} on {mlab(latest - 12)}. Over the last twelve months goods ",
    "exports reached {usd(lr$goods12)}, {chg(lr$goods12 / ly$goods12 - 1)} on the year before. ",
    "{fylab(last_fy$fy)} closed at {usd(last_fy$goods)} ({chg(last_fy$growth)})"
  ),
  if (last_fy$goods == max(fy_done$goods)) ", the highest annual total in this series." else ".",
  if (cur_fm < 12) {
    str_glue(" The first {cur_fm} month{if (cur_fm > 1) 's' else ''} of {fylab(cur_fy)} ",
             "brought in {usd(fytd)}, {chg(fytd / fytd_ly - 1)} on the same months of {fylab(cur_fy - 1)}.")
  } else "",
  if (svc_ok) {
    str_glue(" Services added another {usd(svc12)} over the last twelve months, ",
             "{pct(svc12 / (svc12 + lr$goods12))} of total exports.")
  } else ""
)
hover_m <- "%{x|%b %Y}<br>US$ %{y:,.0f} million<extra>%{fullData.name}</extra>"

monthly_chart <- function(df, value, avg, bar_col, line_col) {
  plot_ly(df, x = ~as.Date(month)) |>
    add_bars(y = value, name = "Monthly", marker = list(color = bar_col),
             hovertemplate = hover_m) |>
    add_lines(y = avg, name = "12-month average", line = list(color = line_col, width = 3),
              hovertemplate = hover_m) |>
    layout(xaxis = list(title = "", rangeslider = list(visible = TRUE)),
           yaxis = list(title = "US$ million"),
           legend = list(orientation = "h", y = 1.12), hovermode = "x unified")
}

growth_chart <- function(df, monthly, rolling, col) {
  plot_ly(df, x = ~as.Date(month)) |>
    add_bars(y = monthly, name = "Month vs same month last year",
             marker = list(color = "#c8e6c9"),
             hovertemplate = "%{x|%b %Y}<br>%{y:.1f}%<extra>%{fullData.name}</extra>") |>
    add_lines(y = rolling, name = "Rolling 12 months vs a year earlier",
              line = list(color = col, width = 3),
              hovertemplate = "%{x|%b %Y}<br>%{y:.1f}%<extra>%{fullData.name}</extra>") |>
    layout(xaxis = list(title = ""), yaxis = list(title = "Growth, %", zeroline = TRUE),
           legend = list(orientation = "h", y = 1.12), hovermode = "x unified")
}

fy_tbl <- exp_m |>
  summarise(goods = sum(goods), services = sum(services), months = n(), .by = fy) |>
  arrange(fy) |>
  mutate(
    complete  = months == 12,
    fy_l      = str_c(fylab(fy), if_else(complete, "", " (to date)")),
    goods_g   = if_else(complete & lag(complete), goods / lag(goods) - 1, NA_real_),
    serv_g    = if_else(complete & lag(complete), services / lag(services) - 1, NA_real_),
    serv_sh   = services / (goods + services)
  )

fy_chart <- function(value, col, title) {
  plot_ly(fy_tbl, x = ~fy_l, y = value, type = "bar",
          marker = list(color = if_else(fy_tbl$complete, col, "#bdbdbd")),
          hovertemplate = "%{x}<br>US$ %{y:.2f} billion<extra></extra>") |>
    layout(title = list(text = title, x = 0), xaxis = list(title = "", categoryorder = "array",
           categoryarray = fy_tbl$fy_l), yaxis = list(title = "US$ billion"))
}
monthly_chart(exp_m, ~goods, ~goods12 / 12, "#a5d6a7", "#1b5e20")
growth_chart(filter(exp_m, !is.na(goods_yoy)), ~goods_yoy * 100, ~goods12_yoy * 100, "#1b5e20")
fy_chart(~goods / 1000, "#1b5e20", "Goods exports by fiscal year (grey = year to date)")
exp_m |>
  filter(fy >= cur_fy - 3) |>
  arrange(month) |>
  mutate(cum = cumsum(goods) / 1000, fy_l = fylab(fy),
         mname = month.abb[month(as.Date(month))], .by = fy) |>
  plot_ly(x = ~fy_month, y = ~cum, color = ~fy_l, type = "scatter", mode = "lines+markers",
          text = ~mname,
          hovertemplate = "%{text}: US$ %{y:.2f} billion<extra>%{fullData.name}</extra>") |>
  layout(xaxis = list(title = "", tickvals = 1:12, ticktext = month.abb[c(7:12, 1:6)]),
         yaxis = list(title = "US$ billion, cumulative"),
         legend = list(orientation = "h", y = 1.12))
if (svc_ok) cat("\n## Services exports\n")
monthly_chart(exp_m, ~services, ~serv12 / 12, "#b3e5fc", "#01579b")
growth_chart(filter(exp_m, !is.na(serv_yoy)), ~serv_yoy * 100, ~serv12_yoy * 100, "#01579b")
fy_chart(~services / 1000, "#01579b", "Services exports by fiscal year (grey = year to date)")
fy_tbl |>
  transmute(
    `Fiscal year` = fy_l,
    `Goods (US$ m)` = round(goods),
    `Goods growth` = scales::percent(goods_g, accuracy = 0.1),
    `Services (US$ m)` = round(services),
    `Services growth` = scales::percent(serv_g, accuracy = 0.1),
    `Services share of total` = scales::percent(serv_sh, accuracy = 0.1)
  ) |>
  select(where(\(x) !all(is.na(x)))) |>
  kable(align = "lrrrrr", caption = "Growth is shown only between complete fiscal years")
exp_m |>
  slice_tail(n = 12) |>
  transmute(
    Month = format(as.Date(month), "%b %Y"),
    `Goods (US$ m)` = round(goods),
    `Goods, vs a year earlier` = scales::percent(goods_yoy, accuracy = 0.1),
    `Services (US$ m)` = round(services),
    `Services, vs a year earlier` = scales::percent(serv_yoy, accuracy = 0.1)
  ) |>
  select(where(\(x) !all(is.na(x)))) |>
  kable(align = "lrrrr")
for (b in names(block_title)) {
  a <- A[[b]]
  if (is.null(a)) next
  cat("\n\n## Top 10 ", block_title[[b]], "\n\n", sep = "")
  cat(block_msg(a), "\n\n")
  print(plot_top(a))
  cat("\n\n")
  print(plot_trend(a))
  cat("\n\n")
  cat(table_top(a), sep = "\n")
  cat("\n\n")
}
cat("\n## Appendix: destination and product series\n\n")
series_info |>
  arrange(block, role, desc(mean_v)) |>
  transmute(Breakdown = block_title[block], Code = key, Name = label, Role = role,
            `Average monthly flow (US$ m)` = round(mean_v, 1)) |>
  kable() |>
  cat(sep = "\n")
Back to top