How can I automatically assign a different color to each heading?

Hey all,
this is my first question here and I would appreciate your assistance.

I’m trying to write a template, where:

  1. Count all level 1 headings and store the total in a variable called X.
  2. Sample X different colors from a gradient and store them in an array, A, with X entries.
  3. Assign each of the X headings one of the entries from array A.

I’ve tried quite a few approaches, but I seem to be struggling with the last part. It prints each heading X times for each color instead of just once.
Also everything has to remain in the context which is not very elegant.

#set heading(numbering: "1")

#context[
  
  //get the number of level-one headings
  #let counter = counter(heading).final().at(0)
  #counter
  
  
  //get array of ratios
  #let ratios = ()
  #for i in range(1, counter+1) {
    ratios.push(i*(100/counter)*1%)
  }
  #ratios

  
  //generate colors
  #let grad = gradient.linear(..color.map.rainbow).samples(..ratios)
  #grad     

  #show heading: it => for i in range(counter){
    //if i+1 == counter(heading).at(i){
      align(center, text(font: "Poppins", fill: grad.at(i), it.body))
    } 

]

= one
= two
= three

Thank you in advance!

Are you trying to make each heading a different color? if so, the best way would be to use a show rule that applies to headings:

#show heading: it => context [
  
  //get the number of level-one headings
  #let finalcounter = counter(heading).final().at(0)
  #let currentcounter = counter(heading).at(here()).at(0)

  #let ratio = currentcounter / finalcounter * 100%
  
  //generate color
  #let grad = gradient.linear(..color.map.rainbow).sample(ratio)
  
  #align(center, text(font: "Poppins", fill: grad, it.body))
]

= one
= two
= three

If instead you are trying to get a sort of outline of all headings at the beginning, you can do that like so

#context [
  
  //get all level-one headings
  #let allheadings = query(heading.where(level: 1))

  #for (i, h) in allheadings.enumerate() {
    let ratio = i / allheadings.len() * 100%
    
    //generate color
    let grad = gradient.linear(..color.map.rainbow).sample(ratio)
    
    align(center, text(font: "Poppins", fill: grad, h.body))
    
  }
]

= one
= two
= three

If you are curious about why your code didn’t work, it is because you are doing a for loop for each heading, so each heading gets displayed as the output of the for loop, leaving you with X copies.

2 Likes

Oh my gosh, it works! Instead of dealing with multiple values and attempting to apply the show rule to each one individually, you can simply consolidate everything into a single show rule that will be applied to each heading separately. It’s such a clever solution!

No problem. Though do note that taking the value from the counter can lead to unwanted behavior if it’s manually set in the document:

= one
= two

#counter(heading).update(1)
= three
Summary

image

So if this is a problem, you should use queries instead of relying on counters :)

I haven’t looked at queries yet. Would you mind elaborating a bit?

Sure. All you need to do is replace the lines up to calculating the ratio with

  //get all level-one headings
  #let allheadings = query(heading.where(level: 1))

  #let currentheading = query(heading.where(level: 1).before(here()))

  #let ratio = currentheading.len() / allheadings.len() * 100%

This will calculate the ratio using the number of headings in the document, instead of relying on the counter whose value can be changed arbitrarily