4  North Carolina

It can be helpful to zoom in on one place to see local dynamics. Below I look at North Carolina and then zoom in further to the Triangle area, where I live.

Show the code
dta_nc <- dta |>
  filter(state == "NC")

dta_nc_2025 <- dta_nc |>
  filter(yr == 2025) |>
  reframe(n_stations = n(),
          n_chargers = sum(n_chargers),
          n_ev_network = n_distinct(ev_network)
          )

At the end of 2025, there were 38 EV charging networks operating in North Carolina with 2054 stations and 6321 chargers1

Show the code
ev_networks_nc <- dta_nc |>
  st_drop_geometry() |>
  filter(yr == 2025) |>
  reframe(n_chargers = sum(n_chargers),
          n_stations = n(),
          .by = ev_network) |>
  arrange(desc(n_chargers)) |>
  mutate(chargers_per_station = n_chargers / n_stations,
         rowid = row_number())

ev_networks_nc |>
  gt() |>
  tab_header(md("**EV Charging networks active in NC at end of 2025***")) |>
  fmt_number(columns = chargers_per_station,
             decimals = 1)
EV Charging networks active in NC at end of 2025*
ev_network n_chargers n_stations chargers_per_station rowid
ChargePoint Network 1828 1002 1.8 1
Non-Networked 1092 332 3.3 2
Tesla 1054 88 12.0 3
Blink Network 605 164 3.7 4
EV Connect 395 132 3.0 5
Tesla Destination 189 71 2.7 6
LOOP 134 32 4.2 7
AMPUP 131 20 6.5 8
Electrify America 121 21 5.8 9
eVgo Network 75 21 3.6 10
ENVIROSPARK 67 16 4.2 11
IONNA 64 7 9.1 12
CHARGEUP 63 17 3.7 13
FORD_CHARGE 58 17 3.4 14
SWTCH 56 13 4.3 15
NOODOE 55 5 11.0 16
CIRCLE_K 54 12 4.5 17
RED_E 51 9 5.7 18
RIVIAN_ADVENTURE 36 6 6.0 19
EVGATEWAY 33 8 4.1 20
CHARGELAB 32 8 4.0 21
SHELL_RECHARGE 24 10 2.4 22
UNIVERSAL 18 9 2.0 23
ZEFNET 15 3 5.0 24
MERCEDES_BENZ 10 6 1.7 25
STAY_N_CHARGE 10 5 2.0 26
AUTEL 8 1 8.0 27
AMPED_UP 6 1 6.0 28
FLO 6 3 2.0 29
RIVIAN_WAYPOINTS 6 3 2.0 30
OpConnect 4 2 2.0 31
VIALYNK 4 2 2.0 32
TURNONGREEN 4 2 2.0 33
EVMATCH 3 2 1.5 34
IN_CHARGE 3 1 3.0 35
EVOKE 3 1 3.0 36
EPIC_CHARGING 2 1 2.0 37
CHAEVI 2 1 2.0 38
Table 4.1


There were three networks (plus “Non-Networked”) with more than 500 chargers at end of 2025 (Figure 4.1 panel B).

Show the code
ev_network_top_10_stations_nc <- dta_nc |>
  count(ev_network, sort = TRUE) |>
  head(10)

ev_network_top_10_chargers_nc <- dta_nc |>
  count(ev_network, wt = n_chargers, sort = TRUE) |>
  head(10)

dta_for_plot <- dta_nc |>
  filter(ev_network %in% ev_network_top_10_stations_nc$ev_network) |>
  reframe(
    n_stations = n(),
    n_chargers = sum(n_chargers),
    chargers_per_station = n_chargers / n_stations,
    .by = c(yr, state, ev_network)
  ) |>
  mutate(ev_network = fct_reorder(ev_network, -n_stations, max))

dta_labels_for_plot <- dta_for_plot |>
  filter(yr == 2025)

p1 <- dta_for_plot |>
  ggplot() +
  geom_line(
    aes(yr, n_stations, group = ev_network, color = ev_network),
    linewidth = 0.2,
    alpha = 0.8) +
  geom_text_repel(
    data = dta_labels_for_plot,
    aes(x = yr,
        y = n_stations,
        label = ev_network,
        color = ev_network),
    nudge_x = 0.5,
    direction = "y",
    hjust = 0) +
  scale_x_discrete(labels = year_labels) +
  scale_y_continuous(labels = label_number(scale_cut = cut_short_scale())) +
  guides(color = "none") +
  coord_cartesian(xlim = c(NA, 2030)) +
  labs(
    subtitle = "A. Top 10 Networks by number of EV charging stations ",
    x = NULL,
    y = NULL
  )

dta_for_plot2 <- dta_nc |>
  filter(ev_network %in% ev_network_top_10_chargers_nc$ev_network) |>
  reframe(
    n_stations = n(),
    n_chargers = sum(n_chargers),
    chargers_per_station = n_chargers / n_stations,
    .by = c(yr, state, ev_network)
  ) |>
  mutate(ev_network = fct_reorder(ev_network, -n_stations, max))

dta_labels_for_plot2 <- dta_for_plot2 |>
  filter(yr == 2025)
  
p2 <- dta_for_plot2 |>
  ggplot() +
  geom_line(
    aes(yr, n_chargers, group = ev_network, color = ev_network),
    linewidth = 0.2,
    alpha = 0.8) +
  geom_text_repel(
    data = dta_labels_for_plot2,
    aes(x = yr,
        y = n_chargers,
        label = ev_network,
        color = ev_network),
    nudge_x = 0.5,
    direction = "y",
    hjust = 0) +
  scale_x_discrete(labels = year_labels) +
  scale_y_continuous(labels = label_number(scale_cut = cut_short_scale())) +
  guides(color = "none") +
  coord_cartesian(xlim = c(NA, 2030)) +
  labs(
    subtitle = "B. Top 10 Networks by number of EV chargers",
    x = NULL,
    y = NULL
  )

p1 + p2 +
  plot_annotation(
    title = "Top 10 Networks by number of EV charging stations in North Carolina",
    subtitle = "All types.",
    caption = my_caption
  )
Figure 4.1: Top 10 Networks by number of EV charging stations in North Carolina


4.1 NC map

Chargers are where the people are (as you would expect) plus on common travel routes (Figure 4.2). The counties making up the core of the Triangle are highlighted.

Show the code
nc_border <- state_boundaries_sf |>
  filter(state_abb == "NC")

dta_nc_sf <- dta_nc |>
  st_as_sf(coords = c("longitude", "latitude"),
           crs = "WGS84") |>
  st_transform(crs = my_proj) %>%
  # need to get rid of some bad points
  st_crop(st_bbox(nc_border)) # much faster
  # st_intersection(nc_border, .) # this works but is very slow

nc_counties <- us_counties(states = "NC") |>
  select(-state_name) |>
  st_transform(crs = my_proj)

nc_triangle <- nc_counties |>
  filter(name %in% c("Orange",
                     "Wake",
                     "Durham"))

nc_triangle_bbox <- st_bbox(nc_triangle)

dta_triangle_sf <- dta_nc_sf |>
  st_crop(nc_triangle_bbox)

dta_triangle_no_geom <- dta_triangle_sf |>
  st_drop_geometry()

ev_networks_triangle <- dta_triangle_no_geom |>
  st_drop_geometry() |>
  filter(yr == 2025) |>
  reframe(n_chargers = sum(n_chargers),
          n_stations = n(),
          .by = ev_network) |>
  arrange(desc(n_chargers)) |>
  mutate(chargers_per_station = n_chargers / n_stations,
         rowid = row_number())

facility_type_2025 <- dta_triangle_no_geom |>
  filter(yr == 2025) |>
  reframe(
    n_stations = n(),
    n_chargers = sum(n_chargers),
    .by = facility_type
  ) |>
  replace_na(list(facility_type = "Unspecified"))
  
facility_type_ev_network_2025 <- dta_triangle_no_geom |>
  filter(yr == 2025) |>
  reframe(
    n_stations = n(),
    n_chargers = sum(n_chargers),
    .by = c(ev_network, facility_type)
  ) |>
  replace_na(list(facility_type = "Unspecified"))


get_pop_2020_tract_tmp <-  function(proj) {
  get_decennial(
    geography = c("tract"),
    variables = c(pop = "P001001"),
    state = "NC",
    county = c("Orange", "Durham", "Wake"),
    # year = 2020,
    geometry = TRUE,
    # output = "wide"
  ) #|>
    clean_names() |>
    separate(name, into = c("tract_name", "county", "state"), sep = ", ") |>
    mutate(tract_name = str_extract(tract_name, "\\d+(\\.\\d+)?"),
           county = str_extract(county, "^.+(?= County)")
    ) |>
    select(-c(state)) |>
    mutate(year = 2000) |>
    st_transform(crs = proj)
}

v2020_dhc <- load_variables(2020, "dhc")

census_tract_triangle_2020 <- get_decennial(
    geography = c("tract"),
    variables = c("P1_001N"),
    state = "NC",
    county = c("Orange", "Durham", "Wake"),
    year = 2020,
    geometry = TRUE,
    cb = FALSE,
    cache_table = TRUE
  ) |>
  rename(pop = value) |>
  clean_names() |>
  mutate(county = str_extract(name, "Orange|Durham|Wake"),
         area_m2 = as.numeric(st_area(geometry)),
         area_mi2 = 3.861E-07 * area_m2,
         density_mi2 = pop / area_mi2)
  
####### OSM data: NC state roads
fname <- here("data/processed/nc-highways.rds")

if(!file.exists(fname)) {

  nc_highways_motorways <- st_bbox(nc_border) |>
    opq()%>%
    add_osm_feature(key = "highway",
                    value = c("motorway", "trunk")) %>%
    osmdata_sf()

  write_rds(nc_highways_motorways$osm_lines |>
      st_simplify(dTolerance = 100), 
      fname)

} else {

  nc_highways_motorways <- read_rds(fname)

}

# Is it faster to covert to spatVector, use mask(), and convert back to sf instead of st_intersection()? YES

# nc_highways_motorways_lines <- nc_highways_motorways$osm_lines |>
#   select(osm_id, highway, ref, geometry) |>
#   st_transform(crs = my_proj) |>
#   st_crop(nc_border) %>%
#   # TODO: do I need to do st_intersection() here? YES
#   st_intersection(nc_border, .) |> # this works but is very slow
#   mutate(highway = factor(highway, levels = c("motorway", "trunk"),
#                           ordered = TRUE))

nc_highways_motorways_lines <- nc_highways_motorways |>
  select(osm_id, highway, ref, geometry) |>
  st_transform(crs = my_proj) |>
  filter(!st_is_empty(geometry)) |>
  vect() |>
  crop(nc_border) %>%
  mask(vect(nc_border)) |>
  st_as_sf() |>
  mutate(highway = factor(highway, levels = c("motorway", "trunk"),
                          ordered = TRUE))

####### OSM data: Triangle roads
fname <- here("data/processed/triangle-highways.rds")

if(!file.exists(fname)) {

  triangle_highways_motorways <- nc_triangle_bbox |>
    opq()%>%
    add_osm_feature(key = "highway",
                    value = c("motorway", "trunk", "primary")) %>%
    osmdata_sf()

  write_rds(triangle_highways_motorways$osm_lines  |>
      st_simplify(dTolerance = 100), 
      fname)

} else {

  triangle_highways_motorways <- read_rds(fname)

}

triangle_highways_motorways_lines <- triangle_highways_motorways |>
  select(osm_id, highway, ref, geometry) |>
  st_transform(crs = my_proj) |>
  st_crop(nc_triangle_bbox) |>
  mutate(highway = factor(highway, levels = c("primary", "trunk", "motorway"),
                          ordered = TRUE))
Show the code
ggplot() +
  geom_sf(
    data = nc_border,
    linewidth = 0.5,
    color = "grey60",
    fill = "grey98") +
  geom_sf(
    data = nc_triangle,
    linewidth = 0.5,
    # lty = 2,
    color = "goldenrod" #"grey40"
    ) +
  geom_sf(
    data = nc_highways_motorways_lines,
    aes(linewidth = highway,
        color = highway),
    alpha = 0.6) +
  geom_sf(
    data = dta_nc_sf,
    aes(size = n_chargers),
    color = "firebrick",
    alpha = 0.2) +
  scale_linewidth_discrete(range = c(0.15, 0.35)) +
  scale_color_manual(values = c("black", "blue")) + # "darkslateblue")) +
  scale_size_continuous(breaks = c(10, 50, 100),
                        range = c(0.25, 2.0)) +
  guides(size = guide_legend(position = "inside"),
         linewidth = "none",
         color = "none") +
  theme(legend.position.inside = c(0.3, 0.3),
        axis.text = element_blank()) +
  labs(
    title = "EV charging stations in North Carolina 2025",
    subtitle = "Including interstate motorways and trunk roads. Triangle counties highlighted.",
    caption = my_caption
  )
Figure 4.2: EV charging stations in North Carolina 2025


4.2 Triangle area

There were 23 EV charging networks active in the Triangle area at the end of 2025. Among them they offer 626 stations and 1880 chargers.

Show the code
ev_networks_triangle |>
  gt() |>
  tab_header(md("**EV Charging networks active in NC Triangle in 2025***")) |>
  fmt_number(columns = chargers_per_station,
             decimals = 1)
EV Charging networks active in NC Triangle in 2025*
ev_network n_chargers n_stations chargers_per_station rowid
ChargePoint Network 713 373 1.9 1
Non-Networked 290 72 4.0 2
Blink Network 233 55 4.2 3
Tesla 149 11 13.5 4
EV Connect 96 29 3.3 5
LOOP 70 17 4.1 6
SWTCH 47 9 5.2 7
NOODOE 41 2 20.5 8
Electrify America 37 6 6.2 9
IONNA 36 5 7.2 10
eVgo Network 35 11 3.2 11
Tesla Destination 34 7 4.9 12
SHELL_RECHARGE 18 7 2.6 13
CHARGEUP 16 4 4.0 14
FORD_CHARGE 16 4 4.0 15
AMPUP 15 6 2.5 16
AUTEL 8 1 8.0 17
ZEFNET 8 1 8.0 18
AMPED_UP 6 1 6.0 19
UNIVERSAL 4 2 2.0 20
CHARGELAB 4 1 4.0 21
EVGATEWAY 2 1 2.0 22
RED_E 2 1 2.0 23
Table 4.2


Are the chargers located in the US Census tracts with the greatest density? Only partly (Figure 4.3):

Show the code
ggplot() +
  geom_sf(
    data = census_tract_triangle_2020,
    aes(fill = density_mi2),
    color = NA,
    ) +
  geom_sf(
    data = nc_triangle,
    # fill = "grey98",
    fill = NA,
    lty = 2,
    linewidth = 0.3,
    color = "grey70") +
  geom_sf(
    data = triangle_highways_motorways_lines,
    aes(linewidth = highway,
        color = highway),
    alpha = 0.6) +
  geom_sf(
    data = dta_triangle_sf,
    aes(size = n_chargers),
    color = "firebrick",
    alpha = 0.2) +
  scale_size_continuous(breaks = c(1, 10, 20),
                        range = c(0.25, 2.0)) +
  scale_linewidth_discrete(range = c(0.15, 0.5)) +
  scale_color_manual(values = c("grey", "black", "blue")) +  #"darkslateblue")) +
  scale_fill_gradient(low ="white",
                      high = "dodgerblue") +
  guides(size = guide_legend(position = "right"),
         fill = "none") +
  theme(axis.text = element_blank()) +
  labs(
    title = "EV charging stations in the NC Triangle in 2025",
    subtitle = "Orange, Durham, and Wake counties with major roads (motorways, trunk, primary)\nand population density of US Census tracts",
    caption = paste0(my_caption, ", US Census 2020")
  )
Figure 4.3: EV charging stations in the NC Triangle in 2025


Show the code
dta_for_plot_sf <- dta_triangle_sf |>
  left_join(ev_networks_nc |>
              rename(n_chargers_ev_network = n_chargers),
            by = "ev_network") |>
  filter(yr == 2025) |>
  mutate(n_stations = n(),
         n_chargers_sum = sum(n_chargers),
         ev_network_facet = glue("{ev_network} (n={n_chargers_sum})"),
         .by = ev_network) |>
  filter(n_chargers_sum > 50) |>
  mutate(ev_network_facet = fct_reorder(ev_network_facet, -n_chargers_sum))

ggplot() +
  geom_sf(
    data = nc_triangle,
    fill = NA,
    lty = 2,
    linewidth = 0.3,
    color = "grey70") +
  geom_sf(
    data = dta_for_plot_sf,
    aes(size = n_chargers_sum),
    color = "firebrick",
    alpha = 0.4) +
  scale_size_continuous(breaks = c(1, 10, 20),
                        range = c(0.25, 2.0)) +
  guides(size = guide_legend(position = "top")) +
  facet_wrap( ~ ev_network_facet) +
  theme(axis.text = element_blank()) +
  labs(
    title = "EV charging stations in the NC Triangle 2025",
    subtitle = "Networks with at least 50 chargers in the Triangle",
    caption = my_caption
  )
Figure 4.4: EV charging stations in the NC Triangle 2025


Show the code
dta_for_plot <- dta_triangle_sf |>
  st_drop_geometry() |>
  reframe(
    n_stations = n(),
    n_chargers = sum(n_chargers),
    chargers_per_station = n_chargers / n_stations,
    .by = c(yr, state, ev_network)
  ) |>
  group_by(yr) |>
  mutate(n_networks = n()) |>
  arrange(n_chargers) |>
  mutate(ordering = row_number()) |>
  ungroup()

max_x = max(dta_for_plot$n_chargers)

dta_for_plot |>
  ggplot(aes(n_chargers, ordering)) +
  geom_point(
    size = 1.5,
    color = "firebrick",
    alpha = 0.6) +
  geom_text(aes(label = ev_network),
            hjust = 0,
            nudge_x = 40) +
  guides(color = "none") +
  coord_cartesian(xlim = c(NA, max_x + 1000)) +
  facet_wrap(~ yr, nrow = 2) +
  labs(
    title = glue("EV charging networks active in the Triangle each year 2016-2025", 
                 "\nby number of chargers"),
    subtitle = "Ordered by number of chargers",
    x = "Numberof chargers",
    y = NULL
  )
Figure 4.5: EV charging networks active in the Triangle each year 2016-2025 by number of chargers


Focusing on the EV charging networks with the most chargers (Figure 4.6):

Show the code
dta_labels_for_plot <- dta_for_plot |>
  filter(yr == min(yr)) |>
  arrange(desc(n_chargers)) |>
  head(3)

dta_labels_for_plot_right <- dta_for_plot |>
  filter(yr == max(yr)) |>
  arrange(desc(n_chargers)) |>
  head(5)

dta_for_plot |>
  ggplot() +
  geom_text(
    data = dta_labels_for_plot,
    aes(x = yr,
        y = n_chargers,
        label = ev_network,
        color = ev_network),
    # direction = "y",
    nudge_x = -0.25,
    hjust = 1) +
  geom_text(
    data = dta_labels_for_plot_right,
    aes(x = yr,
        y = n_chargers,
        label = ev_network,
        color = ev_network),
    # direction = "y",
    nudge_x = 0.25,
    hjust = 0) +
  geom_point(
    aes(x = yr,
        y = n_chargers,
        color = ev_network
    )) +
  geom_bump(
    aes(x = yr,
        y = n_chargers,
        color = ev_network
    )) +
  guides(color = "none") +
  coord_cartesian(xlim = c(2012, 2029)) +
  labs(
    title = glue("In the triangle, (A) the number of EV networks", 
                 "\nand (B) the number of chargers within these networks", 
                 "\nboth increased over the ten-year period"),
    x = NULL,
    y = "Number of chargers",
    caption = my_caption
  )
Figure 4.6: In the triangle, (A) the number of EV networks and (B) the number of chargers within these networks both increased over the ten-year period


Looking at the smallest networks in 2016 and 2025 (Figure 4.7):

  • Two of the smallest in 2016 no longer offer chargers.
  • The five smallest in 2025 were new in NC 2023 or later.
Show the code
dta_labels_for_plot_left <- dta_for_plot |>
  filter(yr == min(yr)) |>
  slice_min(order_by = n_chargers, 
            n = 5)

dta_labels_for_plot_right <- dta_for_plot |>
  filter(yr == max(yr)) |>
  slice_min(order_by = n_chargers, 
            n = 5)

dta_labels_for_plot <- dta_for_plot |>
  filter(ev_network %in% union(dta_labels_for_plot_left$ev_network, dta_labels_for_plot_right$ev_network)) |>
  select(yr, ev_network, n_chargers) |>
  filter(yr == min(yr) | yr == max(yr))

dta_for_plot |>
  filter(ev_network %in% union(dta_labels_for_plot$ev_network, dta_labels_for_plot_right$ev_network)) |>
  ggplot() +
  geom_text_repel(
    data = dta_labels_for_plot,
    aes(x = if_else(yr == min(yr),
                    yr,
                    NA),
        y = n_chargers,
        label = ev_network,
        color = ev_network), 
    direction = "y",
    nudge_x = -0.25,
    hjust = 1,
    na.rm = TRUE) +
  geom_text_repel(
    data = dta_labels_for_plot,
    aes(x = if_else(yr == max(yr),
                    yr,
                    NA),
        y = n_chargers,
        label = ev_network,
        color = ev_network), 
    direction = "y",
    nudge_x = 0.25,
    hjust = 0,
    na.rm = TRUE) +
  geom_point(
    aes(x = yr,
        y = n_chargers,
        color = ev_network
    )) +
  geom_bump(
    aes(x = yr,
        y = n_chargers,
        color = ev_network
    )) +
  guides(color = "none") +
  coord_cartesian(xlim = c(2012, 2029)) +
  labs(
    title = glue("In the triangle, the number of (A) smaller EV networks", 
                 "\nand (B) number of chargers within these networks", 
                 "\nincreased over the last 10 years"),
    x = NULL,
    y = "Number of chargers (log2 scale)",
    caption = my_caption
  )
Figure 4.7


Three bump charts are based on number of chargers in the Triangle at end of 2025:

  1. 50 or more
  2. 10 or more
  3. Less than 10
Show the code
dta_labels_for_plot <- dta_for_plot |>
  filter(any(n_chargers >= 50),
         .by = ev_network)

dta_for_plot |>
  filter(ev_network %in% dta_labels_for_plot$ev_network) |>
  ggplot() +
  geom_text_repel(
    data = dta_labels_for_plot,
    aes(x = if_else(yr == min(yr),
                    yr,
                    NA),
        y = n_chargers,
        label = ev_network,
        color = ev_network), 
    direction = "y",
    nudge_x = -0.25,
    hjust = 1,
    na.rm = TRUE) +
  geom_text_repel(
    data = dta_labels_for_plot,
    aes(x = if_else(yr == max(yr),
                    yr,
                    NA),
        y = n_chargers,
        label = ev_network,
        color = ev_network), 
    direction = "y",
    nudge_x = 0.25,
    hjust = 0,
    na.rm = TRUE) +
  geom_point(
    aes(x = yr,
        y = n_chargers,
        color = ev_network
    )) +
  geom_bump(
    aes(x = yr,
        y = n_chargers,
        color = ev_network
    )) +
  guides(color = "none") +
  coord_cartesian(xlim = c(2012, 2029)) +
  labs(
    title = glue("Group 1: EV charging networks with 50+ chargers", 
                 "\nin the Triangle at end of 2025"),
    x = NULL,
    y = "Number of chargers",
    caption = my_caption
  )
Figure 4.8


Show the code
dta_labels_for_plot <- dta_for_plot |>
  filter(!any(n_chargers >= 50),
         .by = ev_network) |>
  filter(any(between(n_chargers, 10, 49)),
         .by = ev_network) |>
  filter(yr == min(yr) | yr == max(yr))

dta_for_plot |>
  filter(ev_network %in% dta_labels_for_plot$ev_network) |>
  ggplot() +
  geom_text_repel(
    data = dta_labels_for_plot,
    aes(x = if_else(yr == min(yr),
                    yr,
                    NA),
        y = n_chargers,
        label = ev_network,
        color = ev_network), 
    direction = "y",
    nudge_x = -0.25,
    hjust = 1,
    na.rm = TRUE) +
  geom_text_repel(
    data = dta_labels_for_plot,
    aes(x = if_else(yr == max(yr),
                    yr,
                    NA),
        y = n_chargers,
        label = ev_network,
        color = ev_network), 
    direction = "y",
    nudge_x = 0.25,
    hjust = 0,
    na.rm = TRUE) +
  geom_point(
    aes(x = yr,
        y = n_chargers,
        color = ev_network
    )) +
  geom_bump(
    aes(x = yr,
        y = n_chargers,
        color = ev_network
    )) +
  guides(color = "none") +
  coord_cartesian(xlim = c(2012, 2029)) +
  labs(
    title = glue("Group 2: EV charging networks with 10-49 chargers", 
                 "\nin the Triangle at end of 2025"),
    x = NULL,
    y = "Number of chargers",
    caption = my_caption
  )
Figure 4.9


Show the code
dta_labels_for_plot <- dta_for_plot |>
  filter(!any(n_chargers >= 10),
         .by = ev_network) |>
  filter(yr == min(yr) | yr == max(yr))

dta_for_plot |>
  filter(ev_network %in% dta_labels_for_plot$ev_network) |>
  ggplot() +
  geom_text_repel(
    data = dta_labels_for_plot,
    aes(x = if_else(yr == min(yr),
                    yr,
                    NA),
        y = n_chargers,
        label = ev_network,
        color = ev_network), 
    direction = "y",
    nudge_x = -0.25,
    hjust = 1,
    na.rm = TRUE) +
  geom_text_repel(
    data = dta_labels_for_plot,
    aes(x = if_else(yr == max(yr),
                    yr,
                    NA),
        y = n_chargers,
        label = ev_network,
        color = ev_network), 
    direction = "y",
    nudge_x = 0.25,
    hjust = 0,
    na.rm = TRUE) +
  geom_point(
    aes(x = yr,
        y = n_chargers,
        color = ev_network
    )) +
  geom_bump(
    aes(x = yr,
        y = n_chargers,
        color = ev_network
    )) +
  guides(color = "none") +
  coord_cartesian(xlim = c(2012, 2029)) +
  labs(
    title = glue("Group 3: EV charging networks with < 10 chargers", 
                 "\nin the Triangle at end of 2025"),
    x = NULL,
    y = "Number of chargers",
    caption = my_caption
  )
Figure 4.10


4.3 In what facilities are EV stations/chargers in the Triangle?

Show the code
n_unspecified_stations <- facility_type_2025$n_stations[facility_type_2025$facility_type == "Unspecified"]
n_unspecified_chargers <- facility_type_2025$n_chargers[facility_type_2025$facility_type == "Unspecified"]

pct_unspecified_stations <- n_unspecified_stations / sum(facility_type_2025$n_stations)
pct_unspecified_chargers <- n_unspecified_chargers / sum(facility_type_2025$n_chargers)

Unfortunately most facility_type values are unspecified: 81% of the stations and 71% chargers. And LOOP shows facility_type = “OTHER” for all their stations in NC (but not nationally). Nonetheless, it’s interesting to see what facility_type values are in the data set (Table 4.3):

Show the code
facility_type_2025 |>
  arrange(desc(n_chargers)) |>
  gt() |>
  tab_options(table.font.size = 11) |>
  tab_header(md("**Facility locations for EV charging stations in the Triangle**"))
Facility locations for EV charging stations in the Triangle
facility_type n_stations n_chargers
Unspecified 506 1331
OFFICE_BLDG 12 99
SHOPPING_CENTER 9 78
OTHER 17 70
MOTOR_POOL, FLEET GARAGE 7 42
PARKING_LOT 6 39
HOTEL, INN, B&B 13 35
MUNI_GOV 9 35
CAR_DEALER 9 20
PARKING_GARAGE 4 17
PUBLIC 4 16
GAS_STATION 2 14
GROCERY 2 14
FED_GOV 7 12
RESTAURANT 2 10
MULTI_UNIT_DWELLING 1 10
HOSPITAL 2 8
MUSEUM 2 8
COLLEGE_CAMPUS 2 5
STREET_PARKING 3 4
PARK 2 4
LIBRARY 2 4
REC_SPORTS_FACILITY 2 3
WORKPLACE 1 2
Table 4.3


Some EV Networks report more facility_type data than others. Some seems to avoid reporting this information altogether–perhaps for competitive reasons.

Show the code
##| column: page-right

facility_type_ev_network_2025 |>
  mutate(reporting = if_else(facility_type == "Unspecified",
                             "unspecified",
                             "reported")
  ) |>
  reframe(
    n_stations = sum(n_stations),
    n_chargers = sum(n_chargers),
    .by = c(ev_network, reporting)
  ) |>
  pivot_wider(
    names_from = reporting,
    values_from = c(n_stations, n_chargers),
    values_fill = 0) |>
  mutate(
    pct_stations_unspecified = n_stations_unspecified / (n_stations_reported + n_stations_unspecified),
    pct_chargers_unspecified = n_chargers_unspecified / (n_chargers_reported + n_chargers_unspecified)
  ) |>
  relocate(c(pct_stations_unspecified, pct_chargers_unspecified), 
           .before = n_stations_reported) |>
  arrange(desc(pct_stations_unspecified), desc(n_chargers_unspecified)) %>%
  set_names(., nm = str_replace_all(names(.), "_", " ")) |>
  gt() |>
  tab_options(table.font.size = 11) |>
  tab_header(md("**Availability of `facility_type` data for EV charging stations in the Triangle**")) |>
  fmt_percent(columns = starts_with("pct"),
              decimals = 0)
Availability of facility_type data for EV charging stations in the Triangle
ev network pct stations unspecified pct chargers unspecified n stations reported n stations unspecified n chargers reported n chargers unspecified
ChargePoint Network 100% 100% 0 373 0 713
Blink Network 100% 100% 0 55 0 233
EV Connect 100% 100% 0 29 0 96
NOODOE 100% 100% 0 2 0 41
Electrify America 100% 100% 0 6 0 37
IONNA 100% 100% 0 5 0 36
eVgo Network 100% 100% 0 11 0 35
SHELL_RECHARGE 100% 100% 0 7 0 18
CHARGEUP 100% 100% 0 4 0 16
ZEFNET 100% 100% 0 1 0 8
CHARGELAB 100% 100% 0 1 0 4
EVGATEWAY 100% 100% 0 1 0 2
Tesla Destination 71% 91% 2 5 3 31
Tesla 18% 24% 9 2 113 36
Non-Networked 6% 9% 68 4 265 25
AMPUP 0% 0% 6 0 15 0
AMPED_UP 0% 0% 1 0 6 0
UNIVERSAL 0% 0% 2 0 4 0
SWTCH 0% 0% 9 0 47 0
FORD_CHARGE 0% 0% 4 0 16 0
AUTEL 0% 0% 1 0 8 0
LOOP 0% 0% 17 0 70 0
RED_E 0% 0% 1 0 2 0
Table 4.4



  1. I am considering “Non-Networked one of the networks.↩︎