3  Owners and facility types

3.1 Owners and facility types

There are so many ownertype values unspecified that we can’t make any meaningful inferences. I have a hunch that most of the unspecified are either privately owned or “Non-Network”.

Show the code
dta_for_plot <- dta |>
  count(yr, owner_type_desc, name = "n_stations")

dta_labels_for_plot <- dta_for_plot |>
  filter(yr == 2025) |>
  slice_max(order_by = owner_type_desc, n = 6)

p1 <- dta_for_plot |>
  ggplot() +
  geom_line(aes(yr, n_stations, group = owner_type_desc),
            linewidth = 0.2,
            alpha = 0.2) +
  geom_text_repel(data = dta_labels_for_plot,
                  aes(yr, n_stations, group = owner_type_desc, label = owner_type_desc),
                  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())) +
  coord_cartesian(xlim = c(NA, 2028)) +
  labs(
    subtitle = "A. Counts",
    x = NULL,
    y = NULL
  )

dta_for_plot <- dta |>
  filter(owner_type_desc != "Unspecified") |>
  count(yr, owner_type_desc, name = "n_stations")

dta_labels_for_plot <- dta_for_plot |>
  filter(yr == 2025) |>
  slice_max(order_by = owner_type_desc, n = 6)

p2 <- dta_for_plot |>
  ggplot() +
  geom_line(aes(yr, n_stations, group = owner_type_desc),
            linewidth = 0.2,
            alpha = 0.2) +
  geom_text_repel(
    data = dta_labels_for_plot,
    aes(yr, n_stations, group = owner_type_desc, label = owner_type_desc),
    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())) +
  coord_cartesian(xlim = c(NA, 2028)) +
  labs(
    subtitle = "B. Without 'Unspecified'",
    x = NULL,
    y = NULL
  )

p1 + p2 +
  plot_annotation(
    title = "Who owns EV stations?",
    subtitle = "All types in USA",
    caption = my_caption
  )
Figure 3.1: Who owns EV stations?


There are 63 facility types in the data set. There are so many facility_type values unspecified that we can’t make any meaningful inferences.

Show the code
dta_for_plot <- dta |>
  replace_na(list(facility_type = "unspecified")) |>
  reframe(
    n = n(),
    unspecified = sum(facility_type == "unspecified"),
    specified = n - unspecified,
    pct_unspecified = unspecified / n,
    pct_specified = specified / n,
    .by = yr
  ) |>
  pivot_longer(
    cols = c(, "unspecified", "specified"),
    names_to = "category",
    values_to = "n_stations"
  ) |>
  mutate(category = factor(category, levels = c("unspecified", "specified")))

dta_for_plot |>
  ggplot(aes(yr, n_stations, fill = category)) +
  geom_col(alpha = 0.5) +
  geom_text(aes(x = yr,
                y = if_else(category == "unspecified",
                            n_stations,
                            NA),
                label = percent(pct_unspecified, accuracy = 1)),
            vjust = 0,
            na.rm = TRUE) +
  scale_x_discrete(labels = year_labels) +
  scale_y_continuous(labels = label_number(scale_cut = cut_short_scale())) +
  scale_fill_viridis_d(end = 0.9) +
  guides(fill = guide_legend(position = "inside")) +
  theme(legend.position.inside = c(0.15, 0.8)) +
  labs(
    title = "Nearly three-fourths of the records\ndo not specify facility type",
    subtitle = "And the proportion unspecified is growing",
    x = NULL,
    y = NULL,
    caption = my_caption,
    fill = "Facility type"
  )
Figure 3.2: Nearly three-fourths of the records do not specify facility type


Nonetheless, it’s interesting to see the facility types that are included in the data set. Figure 3.3 shows this by station, and Figure 3.4 shows it by number of chargers.

Show the code
dta_for_plot <- dta |>
  filter(yr == 2025) |>
  filter(!is.na(facility_type)) |>
  count(yr, facility_type, name = "n_stations", sort = TRUE) |>
  mutate(facility_type = str_to_lower(facility_type),
         pct_of_facilities = n_stations / sum(n_stations),
         pct_label = percent(pct_of_facilities, accuracy = .01),
         facility_type = fct_reorder(facility_type, n_stations),
         cum_sum_stations = cumsum(n_stations),
         my_facet = case_when(
           n_stations > 500               ~ "500+ stations",
           between(n_stations, 160, 499)  ~ "160-499 stations",
           between(n_stations, 50, 159)   ~ "50-159 stations",
           n_stations < 50                ~ "49 or fewer stations"
         ),
         my_facet = factor(
           my_facet, 
           levels = 
             c("500+ stations",
               "160-499 stations",
               "50-159 stations",
               "49 or fewer stations")
         )
         )

dta_for_plot |>
  ggplot(aes(n_stations, facility_type)) +
  geom_point(
    size = 0.5,
    alpha = 0.8) +
  scale_x_continuous(labels = label_number(scale_cut = cut_short_scale())) +
  facet_wrap( ~ my_facet, nrow = 2, scales = "free") +
  theme(plot.title.position = "plot") +
  labs(
    title = "Number of stations by facility type",
    subtitle = "All types in USA. As of end 2025. Excluding the 74% of the data with 'Unspecified' facility type.",
    x = NULL,
    y = NULL,
    caption = my_caption
  )
Figure 3.3: Number of stations by facility type (where this information is available)


Similarly for chargers:

Show the code
dta_for_plot <- dta |>
  filter(yr == 2025) |>
  filter(!is.na(facility_type)) |>
  count(yr, facility_type, wt = n_chargers,
        name = "n_chargers", sort = TRUE) |>
  mutate(facility_type = str_to_lower(facility_type),
         pct_of_chargers = n_chargers / sum(n_chargers),
         pct_label = percent(pct_of_chargers, accuracy = .01),
         facility_type = fct_reorder(facility_type, n_chargers),
         cum_sum_chargers = cumsum(n_chargers),
         my_facet = case_when(
           n_chargers > 2000                ~ "2000+ chargers",
           between(n_chargers, 300, 1999)   ~ "300-1999 chargers",
           n_chargers < 300                 ~ "299 or fewer chargers"
         ),
         my_facet = factor(
           my_facet,
           levels =
             c("2000+ chargers",
               "300-1999 chargers",
               "299 or fewer chargers")
         )
         )

dta_for_plot |>
  ggplot(aes(n_chargers, facility_type)) +
  geom_point(
    size = 0.5,
    alpha = 0.8) +
  scale_x_continuous(labels = label_number(scale_cut = cut_short_scale())) +
  facet_wrap( ~ my_facet, nrow = 1, scales = "free") +
  labs(
    title = "Number of chargers by facility type",
    subtitle = "All types in USA. As of end 2025. Excluding the 74% of the data with 'Unspecified' facility type.",
    x = NULL,
    y = NULL,
    caption = my_caption
  )
Figure 3.4: Number of chargers by facility type (where this information is available)


3.2 Private and public acccess to charging stations

Show the code
dta_for_plot <- dta |>
  filter(!is.na(access_code)) |>
  count(yr, access_code, sort = TRUE) |>
  mutate(pct = n / sum(n),
         .by = yr)

dta_for_plot |>
  ggplot(aes(yr, n, fill = access_code)) +
  geom_col(alpha = 0.5) +
  geom_text(aes(x = yr,
                y = if_else(access_code == "public",
                            n * 0.6,
                            NA),
                label = percent(pct, accuracy = 1)),
            vjust = 1,
            na.rm = TRUE) +
  scale_x_discrete(labels = year_labels) +
  scale_y_continuous(labels = label_number(scale_cut = cut_short_scale())) +
  scale_fill_viridis_d(end = 0.9) +
  guides(fill = guide_legend(position = "inside")) +
  theme(legend.position.inside = c(0.1, 0.8)) +
  labs(
    title = glue("Public station proportion grew", 
                 "\nwhile number of stations more than quadrupled"),
    x = NULL,
    y = NULL,
    caption = my_caption,
    fill = "Access"
  )
Figure 3.5: Public station proportion has been consistent since 2021 while number of stations more than doubled


Show the code
dta_for_plot <- dta |>
  count(yr, state, access_code, sort = TRUE) |>
  mutate(pct = n / sum(n),
         .by = c(yr, state)
  )

dta_for_plot |>
  filter(access_code == "public") |>
  ggplot(aes(yr, pct, group = state)) +
  geom_vline(xintercept = 2021,
             lty = 2,
             linewidth = 0.2,
             alpha = 0.5) +
  geom_line(
    linewidth = 0.2,
    alpha = 0.8,
  ) +
  scale_x_continuous(labels = year_labels,
                     breaks = year_labels,
                     expand = expansion(mult = c(0.01, 0.04))
                     ) +
  scale_y_continuous(labels = label_percent()) +
  theme(legend.position.inside = c(0.1, 0.8)) +
  labs(
    title = glue("Public station proportion has been broadly similar",
                 "\nacross states since 2021"),
    x = NULL,
    y = NULL,
    caption = my_caption,
    fill = "Access"
  )
Figure 3.6: Public station proportion has been broadly similar across states since 2021


3.3 Fleet EV charging stations

Some governmental and private organizations transitioned some or all of their vehicle fleet to electric vehicles, in which case it’s economic to install chargers for their fleet. Typically they are not available to the public.

Let’s start with the privately owned stations with “fleet” mentioned in access_days_time.

Show the code
dta_for_plot <- dta |>
  filter(str_detect(str_to_lower(access_days_time), "fleet"),
         owner_type_desc == "Private") |>
  mutate(station_name_simple = str_extract(station_name, "([[:alnum:]]( )?(&)?)+")) |>
  count(yr, station_name_simple, wt = n_chargers, sort = TRUE,
        name = "n_chargers",
        .by = station_name_simple)

p1 <- dta_for_plot |>
  ggplot(aes(yr, n_chargers, group = station_name_simple)) +
  geom_line(
    linewidth = 0.2,
    alpha = 0.8,
  ) +
  annotate("text", x = 2023.5, y = 280, 
           label = "Google",
           hjust = 1) +
  scale_x_continuous(labels = year_labels,
                     breaks = year_labels) +
  scale_y_continuous(labels = label_number(scale_cut = cut_short_scale())) +
  guides(fill = guide_legend(position = "inside")) +
  theme(legend.position.inside = c(0.1, 0.8)) +
  labs(
    subtitle = glue("A. All private fleet chargers"),
    x = NULL,
    y = NULL
  )

dta_labels_for_plot <- dta_for_plot |>
  filter(station_name_simple != "Google",
         yr == 2025) |>
  slice_max(
    order_by = n_chargers,
    n = 10
  )

p2 <- dta_for_plot |>
  filter(station_name_simple != "Google") |>
  ggplot(aes(yr, n_chargers, group = station_name_simple)) +
  geom_line(
    linewidth = 0.2,
    alpha = 0.8,
  ) +
  geom_text_repel(
    data = dta_labels_for_plot,
    aes(yr, n_chargers, label = station_name_simple),
    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())) +
  coord_cartesian(xlim = c(NA, 2032)) +
  guides(fill = guide_legend(position = "inside")) +
  theme(legend.position.inside = c(0.1, 0.8)) +
  labs(
    subtitle = glue("B. Next top 10",
                    "\n(Excluding Google)"),
    x = NULL,
    y = NULL
  )

my_layout <- c(
"AABBBB"
)

p1 + p2 +
  plot_annotation(
    title = glue("Private fleet chargers"),
    subtitle = "Google's significant commitment to EV charging starting showing up in the numbers in 2024.\n2020-2025",
    caption = my_caption
  ) +
  plot_layout(design = my_layout)
Figure 3.7: Private fleet chargers


Compare Figure 3.7 above with all private owners / private access Figure 3.8

Show the code
dta_for_plot <- dta |>
  filter(groups_with_access_code == "Private",
         owner_type_desc == "Private") |>
  count(yr, station_name, wt = n_chargers, 
        sort = TRUE, 
        name = "n_chargers",
        .by = station_name) |>
  mutate(station_name_simple = str_extract(station_name, "([[:alnum:]]( )?(&)?)+")) |>
  count(station_name_simple, yr, wt = n_chargers,
        name = "n_chargers",) |>
  filter(any(n_chargers > 50),
         .by = station_name_simple)

p1 <- dta_for_plot |>
  ggplot(aes(yr, n_chargers, group = station_name_simple)) +
  geom_line(
    linewidth = 0.2,
    alpha = 0.8,
  ) +
  annotate("text", x = 2023.5, y = 2173, 
           label = "Google",
           hjust = 1) +
  scale_x_discrete(labels = year_labels) +
  scale_y_continuous(labels = label_number(scale_cut = cut_short_scale())) +
  guides(fill = guide_legend(position = "inside")) +
  theme(legend.position.inside = c(0.1, 0.8)) +
  labs(
    subtitle = glue("A. All privately-owned, private-access chargers"),
    x = NULL,
    y = NULL
  )

dta_labels_for_plot <- dta_for_plot |>
  filter(station_name_simple != "Google",
         yr == 2025) |>
  slice_max(
    order_by = n_chargers,
    n = 10
  )

p2 <- dta_for_plot |>
  filter(station_name_simple != "Google") |>
  ggplot(aes(yr, n_chargers, group = station_name_simple)) +
  geom_line(
    linewidth = 0.2,
    alpha = 0.8,
  ) +
  geom_text_repel(
    data = dta_labels_for_plot,
    aes(yr, n_chargers, label = station_name_simple),
    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())) +
  coord_cartesian(xlim = c(NA, 2029)) +
  labs(
    subtitle = glue("B. Excluding Google\n(Next top 10 labelled)"),
    x = NULL,
    y = NULL
  )

my_layout <- c("AABBB")

p1 + p2 +
  plot_annotation(
    title = glue("Privately owned, private-access chargers"),
    subtitle = glue("Google made a significant commitment to EV charging starting in 2024.", 
                    "\nAll US locations. 2020-2025."),
    caption = my_caption
  ) +
  plot_layout(
    design = my_layout
  )
Figure 3.8: Privately owned, private-access chargers