Code for producing the SoJSM Employment Outlook.

# ============================================================
# Communication & Media BA-level Jobs — BLS Projections
# ============================================================

library(tidyverse)
library(plotly)
library(scales)
options(scipen = 999)

# ------------------------------------------------------------
# 1) Read & filter data
# ------------------------------------------------------------
AllData <- read.csv("Employment Projections.csv")

BAjobs <- AllData %>%
  filter(
    Typical.Entry.Level.Education == "Bachelor's degree",
    # Include "5 years or more" so those occupations can be flagged
    Work.Experience.in.a.Related.Occupation %in% c("None", "Less than 5 years", "5 years or more")
  )

# ------------------------------------------------------------
# 2) Clean fields
# ------------------------------------------------------------
BAjobs <- BAjobs %>%
  mutate(
    Pay = parse_number(Median.Annual.Wage.2024),
    
    # BLS annual openings already in *thousands*
    Openings_thousands = as.numeric(Occupational.Openings..2024.2034.Annual.Average),
    
    # SOC cleanup
    Occupation.Code = str_replace_all(Occupation.Code, "[^0-9-]", ""),
    
    # Flag occupations requiring 5+ years in a related occupation
    requires_5plus = Work.Experience.in.a.Related.Occupation == "5 years or more"
  )

MedianPay      <- median(BAjobs$Pay, na.rm = TRUE)
MedianOpenings <- median(BAjobs$Openings_thousands, na.rm = TRUE)

# ------------------------------------------------------------
# 3) SOC list for SoJSM media-oriented jobs
# ------------------------------------------------------------
soc_media <- c(
  "11-2032",
  "11-2033",
  "11-2011",
  "27-1011",
  "27-3041",
  "27-3042",
  "27-3043",
  "27-3031",
  "27-3023",
  "27-1024",
  "15-1254",
  "15-1255",
  "27-2012",
  "27-3011",
  "27-4032",
  "27-4031"
)

Mediajobs <- BAjobs %>%
  filter(Occupation.Code %in% soc_media) %>%
  mutate(
    Job_base = str_remove(Occupation.Title, "[ ]{2,}.*$"),
    Job = if_else(requires_5plus, paste0(Job_base, " *"), Job_base)
  )

# ------------------------------------------------------------
# 4) Create tables for each metric
# ------------------------------------------------------------
PayChart <- Mediajobs %>%
  select(Job, Pay) %>%
  add_row(Job = "Median", Pay = MedianPay) %>%
  arrange(desc(Pay))

OpeningsChart <- Mediajobs %>%
  select(Job, Openings_thousands) %>%
  add_row(Job = "Median", Openings_thousands = MedianOpenings) %>%
  arrange(desc(Openings_thousands))

# ============================================================
# 5) PAY CHART (smart inside/outside labels)
# ============================================================

pay_max  <- max(PayChart$Pay, na.rm = TRUE)
pay_xlim <- c(0, pay_max * 1.10)
switch_thr_pay <- pay_max * 0.20

PayFig <- PayChart %>%
  mutate(
    Job       = fct_reorder(Job, Pay),
    too_small = Pay < switch_thr_pay,
    label_x     = if_else(too_small, Pay + 0.02 * pay_max, Pay - 0.02 * pay_max),
    label_hjust = if_else(too_small, 0, 1),
    label_color = if_else(too_small, "#384B70", "white")
  ) %>%
  ggplot(aes(x = Pay, y = Job)) +
  geom_col(aes(fill = Job != "Median")) +
  scale_fill_manual(values = c("#FF7F3E", "#384B70"), guide = "none") +
  geom_text(aes(x = label_x, label = dollar(Pay),
                hjust = label_hjust, color = label_color),
            size = 3.8) +
  scale_color_identity() +
  scale_x_continuous(
    name = "Median annual pay",
    limits = pay_xlim,
    labels = label_dollar(big.mark = ",", accuracy = 1),
    breaks = pretty_breaks(6),
    expand = expansion(mult = c(0, 0.03))
  ) +
  labs(caption = "* Requires 5+ years of related work experience") +
  theme_minimal(base_size = 13) +
  theme(
    axis.title.y = element_blank(),
    axis.text.x = element_blank(),
    axis.ticks.x = element_blank(),
    plot.caption = element_text(hjust = 0)
  )

# ============================================================
# 6) OPENINGS CHART 
# ============================================================

OpeningsChart <- OpeningsChart %>%
  mutate(Openings_actual = Openings_thousands * 1000)

open_max  <- max(OpeningsChart$Openings_actual, na.rm = TRUE)
open_xlim <- c(0, open_max * 1.10)
switch_thr_open <- open_max * 0.20

OpeningsFig <- OpeningsChart %>%
  mutate(
    Job       = fct_reorder(Job, Openings_actual),
    too_small = Openings_actual < switch_thr_open,
    label_x     = if_else(too_small, Openings_actual + 0.02 * open_max,
                          Openings_actual - 0.02 * open_max),
    label_hjust = if_else(too_small, 0, 1),
    label_color = if_else(too_small, "#384B70", "white")
  ) %>%
  ggplot(aes(x = Openings_actual, y = Job)) +
  geom_col(aes(fill = Job != "Median")) +
  scale_fill_manual(values = c("#FF7F3E", "#384B70"), guide = "none") +
  geom_text(aes(x = label_x, label = comma(Openings_actual),
                hjust = label_hjust, color = label_color),
            size = 3.8) +
  scale_color_identity() +
  scale_x_continuous(
    name = "Annual openings (count)",
    limits = open_xlim,
    labels = comma,
    breaks = pretty_breaks(6),
    expand = expansion(mult = c(0, 0.03))
  ) +
  labs(caption = "* Requires 5+ years of related work experience") +
  theme_minimal(base_size = 13) +
  theme(
    axis.title.y = element_blank(),
    axis.text.x = element_blank(),
    axis.ticks.x = element_blank(),
    plot.caption = element_text(hjust = 0)
  )

# ------------------------------------------------------------
# 8) Output
# ------------------------------------------------------------
PayFig
OpeningsFig