portal_plot <- portal |> ggplot2::ggplot(ggplot2::aes(x = rating, order = rating, fill = window)) + ggdist::geom_dots(layout ="bar", group =NA, color =NA) + ggplot2::scale_fill_manual(values =c("Spring"="#e4572e", "Winter"="#6a7fd1") ) + ggplot2::scale_x_continuous(breaks =seq(0.75, 1.00, 0.05), limits =c(0.75, 1.00)) + ggplot2::labs(title ="College Football Transfer Portal \nRatings by Window \n<span style='color:#e4572e;'>Spring</span> and <span style='color:#6a7fd1; font-weight:bold'>Winter</span>",subtitle ="Each dot represents players that entered the portal ahead of the 2025 season. \nDots are ordered by 247Sports Rating and grouped by the window when the player entered.",x ="<--- Lower Rating | Higher Rating --->",y ="",caption ="Only shows players with a 247Sports rating \nViz by Chris at Bless your chart | data via cfbfastR and 247Sports.com | April 21, 2025" ) + hrbrthemes::theme_ipsum_rc() + ggplot2::theme(plot.title = ggtext::element_markdown(hjust =0.5,size =20,family ="Roboto Condensed" ),plot.subtitle = ggtext::element_markdown(hjust =0.5,size =9.5,lineheight =1.5,family ="Roboto Condensed" ),legend.position ="none",axis.text.x = ggplot2::element_text(size =11.5, family ="Roboto Condensed"),axis.title.x = ggplot2::element_text(size =12.5, hjust =0.5),axis.text.y = ggplot2::element_blank(),plot.caption = ggplot2::element_text(face ="plain", size =8, "Roboto Condensed") ) + ggplot2::annotate(geom ="label",x =0.985,y = .47,color ="#333333",fill ="#e4572e",label ="Nico Iamaleava \nTennessee QB \nApril 12, 2025",size =3.5,fontface ='bold',family ='Roboto condensed',alpha =0.3 ) + ggplot2::annotate(geom ="curve",color ="#e4572e",x =0.978,y = .41,xend =0.98,yend = .02,curvature = .3,linewidth =0.8,arrow = ggplot2::arrow(length = ggplot2::unit(2, "mm")),alpha =0.3 )ggplot2::ggsave("portal_plot.png", portal_plot,w =7,h =8.5,dpi =600,bg ="white",type ='cairo')
Table by position and window
Code
portal_summary <- portal |> dplyr::mutate(position = dplyr::if_else(position =="DT", "DL", position), position = dplyr::if_else(position %in%c("P", "K", "LS"), "ST", position)) |> dplyr::mutate(rating =as.numeric(rating)) |> dplyr::filter(!is.na(rating)) |> dplyr::group_by(window, position) |> dplyr::summarise(Best =max(rating),Median =median(rating),Worst =min(rating),Total = dplyr::n(),.groups ="drop" )position_order <- portal_summary |> dplyr::group_by(window) |> dplyr::arrange(desc(Total), .by_group =TRUE) |> dplyr::mutate(position =factor(position, levels =unique(position))) portal_header <-glue::glue("<div style='display: flex; justify-content: space-between; align-items: center;'> <div> <img src='https://a.espncdn.com/combiner/i?img=/redesign/assets/img/icons/ESPN-icon-football-college.png' style='height: 40px; width: auto; vertical-align: middle;'> </div> <div style='flex-grow:1; margin-left: 30px; margin-right: 30px'> <span style='display: block; font-weight: bold; text-align: center; font-size: 24px;'>Transfer Portal: 247Sports Ratings by Position</span> <span style='font-size: 14px; font-weight: normal; display: block; text-align: center;'>Best, worst, and median ratings for players in the portal ahead of the 2025 season.</span> </div> <div> <img src='https://a.espncdn.com/combiner/i?img=/redesign/assets/img/icons/ESPN-icon-football-college.png' style='height: 40px; width: auto; vertical-align: middle;'> </div> </div> <br>")portal_table <- position_order |> tidyr::pivot_longer(cols =c(Best, Median, Worst, Total),names_to ="stat",values_to ="value" ) |> tidyr::pivot_wider(names_from = position,values_from = value ) |> dplyr::arrange(window, match(stat, c("Best", "Median", "Worst", "Total"))) |> gt::gt(groupname_col ="window") |> gtUtils::gt_theme_gtutils() |> gt::cols_label(stat ="" ) |> gtUtils::gt_border_grid(color ="black",weight =0.5,include_labels =FALSE) |> gtExtras::gt_add_divider(columns =c(stat),sides ="right",color ="black" ) |> gt::data_color(columns =c(-stat),rows =c(1:3, 5:7), direction =c("column"),method =c("numeric"),palette =c("#d7191c", "#fdae61", "#ffffbf", "#a6d96a", "#1a9641"),alpha =0.6 ) |> gt::cols_align(stat, align ="left") |> gt::fmt_number(columns =c(-stat),rows =c(1:3, 5:7), decimals =2 ) |> gt::fmt_number(columns =c(-stat),rows =c(4, 8), decimals =0 ) |> gt::tab_header(title = gt::html(portal_header)) |> gt::tab_source_note(source_note = gt::html("<hr>Data via cfbfastR | theme via {gtUtils} <br> Ratings provided by 247Sports and only surfaces players with an actual rating<br> Groups specialists together (K, P, LS) into one position group (ST)<br> Orders positions by the total number of players that entered the portal from the position<br> <hr><b>Table by Chris at Bless your chart | data from April 21, 2025</b>" ) ) |> gtUtils::gt_border_bars_bottom(c("#636363", "#969696", "#cccccc")) |> gt::tab_options(table.width = gt::px(675)) |> gt::tab_style(locations = gt::cells_source_notes(),style = gt::cell_text(font = gt::google_font("Signika Negative"),size = gt::px(11.5),weight =250 ) ) |> gt::tab_style(style =list( gt::cell_text(font = gt::google_font("Signika Negative"),size = gt::px(14) ) ),locations = gt::cells_body(rows = gt::everything(),columns = gt::everything() ) ) |> gt::tab_style(locations = gt::cells_row_groups(),style =list( gt::cell_text(font = gt::google_font("Signika Negative"),weight =850,size = gt::px(16),color ="black",align ="left" ), gt::cell_fill(color ="#C5C5FF" ) ) ) gtUtils::gt_save_crop( portal_table,file ="portal_tbl.png",whitespace =40,bg ="#FFFDF5")portal_table
Transfer Portal: 247Sports Ratings by PositionBest, worst, and median ratings for players in the portal ahead of the 2025 season.
WR
CB
OT
EDGE
RB
DL
IOL
LB
QB
S
TE
ST
Spring
Best
0.92
0.92
0.91
0.92
0.92
0.95
0.88
0.90
0.98
0.88
0.92
0.81
Median
0.86
0.86
0.86
0.86
0.85
0.86
0.86
0.86
0.86
0.85
0.86
0.80
Worst
0.83
0.83
0.83
0.82
0.81
0.83
0.80
0.81
0.82
0.83
0.81
0.79
Total
26
20
20
19
19
17
16
16
11
10
10
2
Winter
Best
0.96
0.93
0.92
0.98
0.92
0.93
0.94
0.91
0.96
0.90
0.94
0.82
Median
0.85
0.85
0.85
0.85
0.85
0.86
0.85
0.85
0.85
0.86
0.85
0.80
Worst
0.79
0.80
0.80
0.80
0.79
0.82
0.78
0.81
0.79
0.81
0.79
0.78
Total
286
169
124
151
131
130
170
115
127
125
102
17
Data via cfbfastR | theme via {gtUtils}
Ratings provided by 247Sports and only surfaces players with an actual rating
Groups specialists together (K, P, LS) into one position group (ST)
Orders positions by the total number of players that entered the portal from the position Table by Chris at Bless your chart | data from April 21, 2025