Brand Your Docs, Apps, and ggplots Using LLMs

Umair Durrani (Data Scientist at Presage Group)

Powering Branding with LLMs

  • Tool Calling
  • Retrieval-Augmented Generation (RAG)
  • Structured Output

You’ve Just Finished Your Analysis πŸŽ‰

  • Days worth of complex analysis
  • Carefully crafted visualizations

  • No personality! πŸ˜•

Where to start theming?

  • Think long and hard about colors, fonts, and styles
  • Pick stuff that makes sense in the context

Iris flowers

Add some colors

We want these outputs:

Add some colors

You need to know CSS or SCSS!

Don’t forget revealjs

// Import fonts
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@400;700&display=swap');

/*-- scss:defaults --*/
$body-bg: #EFEFF8;
$navbar-bg: #6E2DAB;

// Base font for body text
$font-family-sans-serif: "Quicksand", system-ui, -apple-system, sans-serif;

// Font for headings
$headings-font-family: "Quicksand", Georgia, serif;

// Reveal.js specific heading
$presentation-heading-font: "Quicksand", Georgia, serif;
$presentation-heading-color: #6E2DAB;

/*-- scss:rules --*/
h1, h2, h3, h4, h5, h6 {
  color: $navbar-bg
}

.reveal h1, 
.reveal h2, 
.reveal h3, 
.reveal h4, 
.reveal h5, 
.reveal h6 {
  color: $navbar-bg !important;
}

Current Output

_brand.yml makes it easy

_brand.yml makes it easy

meta:
  name: Iris Dataset
logo: 
  medium: logo.png
color:
  palette:
    forest-green: '#1B2D0B'
    green: '#49752E'
    lavender: '#9B79CD'
    purple: '#6E2DAB'
    lilac: '#EFEFF8'
    white: '#FFFFFF'
    black: '#1B2D0B'
  background: lilac
  foreground: '#1B2D0B'
  primary: '#6E2DAB'
  secondary: '#49752E'
  success: '#49752E'
  info: '#9B79CD'
  warning: '#FFB300'
  danger: '#D7263D'
  light: '#FFFFFF'
  dark: '#1B2D0B'
typography:
  fonts:
    - family: Quicksand
      source: google
      weight:
        - 400
        - 700
      style:
        - normal
        - italic
    - family: Fira Code
      source: bunny
      weight:
        - 400
        - 500
        - 600
      style:
        - normal
        - italic
  base:
    family: Quicksand
    weight: 400
    size: 16px
    line-height: 1.5
  headings:
    family: Quicksand
    weight: 700
    style: normal
    line-height: 1.2
    color: '#6E2DAB'
  monospace:
    family: Fira Code
    weight: 400
    size: 0.9em

The Traditional Solution

Manual Branding

  • Read branding guidelines PDF
  • Extract colors, fonts, logos
  • Manually create _brand.yml
  • Design ggplot2 themes
  • Create custom color scales

⏰ Hours of tedious work

πŸ”„ Lots of trial and error

🎨 Requires design expertise

Introducing {brandthis}

AI-powered branding for docs and apps

πŸ€– LLM-Powered

Uses Google Gemini or other LLMs via {ellmer}

⚑ Fast

Generate complete branding in a minute

createBranding app

brandthis::run_brand_app()

Package Features

Function Description
brandthis::create_brand Creates _brand.yml
brandthis::suggest_color_scales Suggests suitable color palettes and scales
brandthis::create_color_palette Creates color palettes based on _brand.yml

Generate a _brand.yml

brandthis::create_brand

personal_brand <- brandthis::create_brand(
  prompt = "My name is John Doe",
  img = "Iris_virginica.jpg",
  type = "personal",
  chat_fn = ellmer::chat_google_gemini
)

company_brand <- brandthis::create_brand(
  prompt = "Company name is Walmart",
  img = c(
    "walmart-font.png",
    "walmart-palette.jpeg",
    "walmart-logo.png"
  ),
  type = "company",
  chat_fn = ellmer::chat_google_gemini
)

How create_brand works?

Tool Calling

How create_brand works?

# Make an ellmer chat object
client_brand <- make_chat(
  chat_fn,
  system_prompt = system_prompt_brand,
  ...
)

# Register tools for fonts and contrast
client_brand$register_tool(get_fonts_combination_tool)
client_brand$register_tool(check_contrast_tool)

# Image paths
img <- get_image_paths(img = img, browse = browse, type = type)

# Chat
if (type == "personal"){
  # Either img is NULL
  prompt <- paste0("Create a personal brand.yml. ", prompt)
  # Or img has a single URL or a single path
  if (!is.null(img)){
    vec_color <- create_palette_from_image(img = img, show = FALSE)
    prompt <- paste0(
      prompt,
      ". The initial color palette is: ",
      paste(vec_color, collapse = ", ")
    )
  }
  res <- client_brand$chat(prompt)
} else if (type == "company"){
  prompt <- paste0("Create a company brand.yml. ", prompt)
  # Either img has one or more paths
  if (!is.null(img)){
    img_ellmer <- lapply(img, ellmer::content_image_file)
    res <- do.call(client_brand$chat, c(prompt, img_ellmer))
  }
}

# Make a brand.yml object
brand.yml::as_brand_yml(as.character(res))

Suggest Color Scales

created with hexsession

Retrieval-Augmented Generation (RAG)

  • Store information - Documents, articles, or data are broken into chunks and stored in a searchable database
  • Retrieve relevant info - When you ask a question, the system searches for the most relevant chunks
  • Generate an answer - The LLM reads those chunks and uses them to write an accurate, informed response

How suggest_color_scales works?

Create a knowledge store for RAG

library(ragnar)

# Find all the relevant pages
base_url_paletteer <- "https://emilhvitfeldt.github.io/paletteer/index.html"
pages_paletteer <- ragnar_find_links(base_url_paletteer)
pp <- pages_paletteer[c(3, 5, 7, 8, 9)]

## Embeddings model
store_location <- "paletteer.ragnar.duckdb"
store <- ragnar_store_create(
  store_location,
  embed = \(x) ragnar::embed_ollama(x, model = "embeddinggemma")
)

## Insert into knowledge store
ragnar_store_insert(store, markdown_chunk(paletteer::palettes_c_names))
ragnar_store_insert(store, markdown_chunk(paletteer::palettes_d_names))
ragnar_store_insert(store, markdown_chunk(paletteer::palettes_dynamic_names))

for (page in pp) {
  message("ingesting: ", page)
  chunks <- page |> read_as_markdown() |> markdown_chunk()
  ragnar_store_insert(store, chunks)
}

How suggest_color_scales works?

Retrieve from the store

suggest_color_scales <- function(brand_yml,
                                 pkg = c("paletteer", "ggsci"),
                                 top_k = 8L,
                                 chat_fn = ellmer::chat_google_gemini,
                                 ...){
  pkg = match.arg(pkg)
  if (pkg == "ggsci"){
    system_prompt_scs <- system_prompt_scs_ggsci
  }
  if (pkg == "paletteer"){
    system_prompt_scs <- system_prompt_scs_paletteer
  }
  semantic_colors_list <- semantic_colors_as_hex_codes(brand_yml$color)
  # Make one string
  semantic_colors <- paste(names(semantic_colors_list), semantic_colors_list, sep = "=", collapse = ", ")

  client_scs <- make_chat(
    chat_fn,
    system_prompt = system_prompt_scs,
    ...
  )

  ragnar::ragnar_register_tool_retrieve(client_scs, brandthis_ragnar_store(pkg), top_k = top_k)

  client_scs$chat(semantic_colors)
}

Suggested scales

Create color palettes

Structured Output

brandthis::create_color_palette(brand.yml::read_brand_yml())

Instead of:

Here are your requested color palettes: Discrete: β€œ#1F407A” β€œ#78D32D” β€œ#FDD60A” β€œ#9A4665” β€œ#8A2BE2” β€œ#008080” Sequential: β€œ#F0F8FF” β€œ#B0C4DE” β€œ#8DA0C8” β€œ#6C7BAF” β€œ#4B5696” β€œ#2F367F” …

Structured output:

cp <- list(
  discrete1 = c("#1F407A", "#78D32D", "#FDD60A", "#9A4665", "#8A2BE2", "#008080"), 
  sequential1 = c("#F0F8FF", "#B0C4DE", "#8DA0C8", "#6C7BAF", "#4B5696", 
                "#2F367F", "#1F407A", "#15305C", "#0B203D"),  
  diverging1 = c("#1F407A", "#4B6B9B", "#7A96BC", "#A9C1DD", "#F0F8FF", 
                "#DBC5CE", "#BF97A4", "#9A4665")
)

Using color palettes

How create_color_palette works?

create_color_palette <- function(brand_yml,
                                chat_fn = ellmer::chat_google_gemini,
                                 ...){

  semantic_colors_list <- semantic_colors_as_hex_codes(brand_yml$color)
  # Make one string
  semantic_colors <- paste(names(semantic_colors_list),
                           semantic_colors_list,
                           sep = "=",
                           collapse = ", ")

  client_ccp <- make_chat(
    chat_fn,
    system_prompt = system_prompt_ccp,
    ...
  )

  palette_type <- ellmer::type_object(
    "A collection of color palettes for ggplot2",
    discrete1 = ellmer::type_array(
      "First discrete palette: a list of color hex codes without names or comments.",
      items = ellmer::type_string()
    ),
    discrete2 = ellmer::type_array(
      "Second discrete palette: a list of color hex codes without names or comments.",
      items = ellmer::type_string()
    ),
    sequential1 = ellmer::type_array(
      "First sequential palette: a list of color hex codes without names or comments.",
      items = ellmer::type_string()
    ),
    sequential2 = ellmer::type_array(
      "Second sequential palette: a list of color hex codes without names or comments.",
      items = ellmer::type_string()
    ),
    sequential3 = ellmer::type_array(
      "Third sequential palette: a list of color hex codes without names or comments.",
      items = ellmer::type_string()
    ),
    diverging1 = ellmer::type_array(
      "First diverging palette: a list of color hex codes without names or comments.",
      items = ellmer::type_string()
    ),
    diverging2 = ellmer::type_array(
      "Second diverging palette: a list of color hex codes without names or comments.",
      items = ellmer::type_string()
    )
  )

  client_ccp$chat_structured(
    semantic_colors,
    type = palette_type,
    echo = FALSE
  )

}

Benefits Summary

Why Use {brandthis}?

  • ⚑ Speed: Minutes instead of hours
  • 🎨 Professional: LLM-designed color palettes
  • πŸ”„ Consistent: One file for all projects
  • πŸ€– Smart: AI understands design principles

And done!

Thank You!

Questions?

Connect

  • πŸ“§ GitHub: @durraniu
  • πŸ¦‹ Bluesky: @transport-talk.bsky.social
  • πŸ“¦ Package: github.com/durraniu/brandthis
  • 🌐 More: Check out the createBranding app (brandthis::run_brand_app())