Skip to contents

Global Top 50


Source: Spotify Charts, Spotify via spotifyr

Raw data: top_tracks.json, get-top-tracks.R

Source Code

library(reactable)
library(htmltools)

top_tracks <- jsonlite::fromJSON("top_tracks.json")
top_tracks <- top_tracks[, c("position", "trend", "name", "explicit", "artists", "daily_plays", "url")]

# artists is a list-column of data frames containing artist metadata (one artist per row).
# Convert this to a character string of artist names so it can be searched,
# and attach the metadata as an attribute so we can insert links later.
top_tracks$artists <- lapply(top_tracks$artists, function(x) {
  structure(paste(x$name, collapse = ", "), metadata = x)
})

tracks_table <- function(data) {
  reactable(
    data,
    searchable = TRUE,
    highlight = TRUE,
    wrap = FALSE,
    paginationType = "simple",
    minRows = 10,
    columns = list(
      position = colDef(
        header = tagList(
          span("#", "aria-hidden" = "true", title = "Position"),
          # Column label for screen readers (sr-only comes from Bootstrap)
          span("Position", class = "sr-only")
        ),
        width = 40
      ),
      trend = colDef(
        header = span("Trend", class = "sr-only"),
        sortable = FALSE,
        align = "center",
        width = 40,
        cell = function(value) trend_indicator(value)
      ),
      name = colDef(
        name = "Title",
        resizable = TRUE,
        cell = function(value, index) {
          title <- tags$a(href = data[index, "url"], value)
          if (data[index, "explicit"]) {
            explicit_tag <- div(style = list(float = "right"), span(class = "tag", "Explicit"))
            title <- tagList(title, explicit_tag)
          }
          title
        },
        minWidth = 200
      ),
      artists = colDef(
        name = "Artist",
        resizable = TRUE,
        html = TRUE,
        cell = function(value) {
          # Create comma-separated list of artist links
          metadata <- attr(value, "metadata")
          links <- apply(metadata, 1, function(artist) {
            sprintf('<a href="%s">%s</a>', artist["external_urls.spotify"], artist["name"])
          })
          paste(links, collapse = ", ")
        },
        minWidth = 100
      ),
      daily_plays = colDef(
        name = "Daily Plays",
        defaultSortOrder = "desc",
        format = colFormat(separators = TRUE),
        width = 120,
        class = "number"
      ),
      explicit = colDef(show = FALSE),
      url = colDef(show = FALSE)
    ),
    language = reactableLang(
      searchPlaceholder = "Filter tracks",
      noData = "No tracks found",
      pageInfo = "{rowStart}\u2013{rowEnd} of {rows} tracks",
      pagePrevious = "\u276e",
      pageNext = "\u276f",
    ),
    theme = spotify_theme()
  )
}

# Icon to indicate trend: unchanged, up, down, or new
trend_indicator <- function(value = c("unchanged", "up", "down", "new")) {
  value <- match.arg(value)
  label <- switch(value,
                  unchanged = "Unchanged", up = "Trending up",
                  down = "Trending down", new = "New")

  # Add img role and tooltip/label for accessibility
  args <- list(role = "img", title = label)

  if (value == "unchanged") {
    args <- c(args, list("–", style = "color: #666; font-weight: 700"))
  } else if (value == "up") {
    args <- c(args, list(shiny::icon("caret-up"), style = "color: #1ed760"))
  } else if (value == "down") {
    args <- c(args, list(shiny::icon("caret-down"), style = "color: #cd1a2b"))
  } else {
    args <- c(args, list(shiny::icon("circle"), style = "color: #2e77d0; font-size: 0.6rem"))
  }
  do.call(span, args)
}

spotify_theme <- function() {
  search_icon <- function(fill = "none") {
    # Icon from https://boxicons.com
    svg <- sprintf('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="%s" d="M10 18c1.85 0 3.54-.64 4.9-1.69l4.4 4.4 1.4-1.42-4.39-4.4A8 8 0 102 10a8 8 0 008 8.01zm0-14a6 6 0 11-.01 12.01A6 6 0 0110 4z"/></svg>', fill)
    sprintf("url('data:image/svg+xml;charset=utf-8,%s')", URLencode(svg))
  }

  text_color <- "hsl(0, 0%, 95%)"
  text_color_light <- "hsl(0, 0%, 70%)"
  text_color_lighter <- "hsl(0, 0%, 55%)"
  bg_color <- "hsl(0, 0%, 10%)"

  reactableTheme(
    color = text_color,
    backgroundColor = bg_color,
    borderColor = "hsl(0, 0%, 16%)",
    borderWidth = "1px",
    highlightColor = "rgba(255, 255, 255, 0.1)",
    cellPadding = "10px 8px",
    style = list(
      fontFamily = "Work Sans, Helvetica Neue, Helvetica, Arial, sans-serif",
      fontSize = "0.875rem",
      "a" = list(
        color = text_color,
        textDecoration = "none",
        "&:hover, &:focus" = list(
          textDecoration = "underline",
          textDecorationThickness = "1px"
        )
      ),
      ".number" = list(
        color = text_color_light,
        fontFamily = "Source Code Pro, Consolas, Monaco, monospace"
      ),
      ".tag" = list(
        padding = "0.125rem 0.25rem",
        color = "hsl(0, 0%, 40%)",
        fontSize = "0.75rem",
        border = "1px solid hsl(0, 0%, 24%)",
        borderRadius = "2px",
        textTransform = "uppercase"
      )
    ),
    headerStyle = list(
      color = text_color_light,
      fontWeight = 400,
      fontSize = "0.75rem",
      letterSpacing = "1px",
      textTransform = "uppercase",
      "&:hover, &:focus" = list(color = text_color)
    ),
    rowHighlightStyle = list(
      ".tag" = list(color = text_color, borderColor = text_color_lighter)
    ),
    # Full-width search bar with search icon
    searchInputStyle = list(
      paddingLeft = "1.9rem",
      paddingTop = "0.5rem",
      paddingBottom = "0.5rem",
      width = "100%",
      border = "none",
      backgroundColor = bg_color,
      backgroundImage = search_icon(text_color_light),
      backgroundSize = "1rem",
      backgroundPosition = "left 0.5rem center",
      backgroundRepeat = "no-repeat",
      "&:focus" = list(backgroundColor = "rgba(255, 255, 255, 0.1)", border = "none"),
      "&:hover, &:focus" = list(backgroundImage = search_icon(text_color)),
      "::placeholder" = list(color = text_color_lighter),
      "&:hover::placeholder, &:focus::placeholder" = list(color = text_color)
    ),
    paginationStyle = list(color = text_color_light),
    pageButtonHoverStyle = list(backgroundColor = "hsl(0, 0%, 20%)"),
    pageButtonActiveStyle = list(backgroundColor = "hsl(0, 0%, 24%)")
  )
}


div(class = "spotify-charts",
  h2(class = "title", "Global Top 50"),
  tracks_table(top_tracks)
)
tags$link(href = "https://fonts.googleapis.com/css?family=Work+Sans:400,600,700|Source+Code+Pro:400&display=fallback",
          rel = "stylesheet")
.spotify-charts {
  padding: 0.75rem 0.75rem 0;
  font-family: "Work Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
  color: hsl(0, 0%, 95%);
  background-color: hsl(0, 0%, 10%);
}

.title {
  margin: 0.75rem 0.375rem 1.5rem;
  padding: 0;
  font-size: 1.5rem;
  font-weight: 600;
}