How would I normalise and stack plots in lilaq

Bit of background - I have just submitted my PhD in Chemistry which I wrote in Latex (via Overleaf) and was reasonably happy with that. All of the graphs in my thesis were generated in OriginPro and exported as .png for compilation into Latex, but I have since discovered the world of typst and lilaq and am attempting to switch all of my future research reports/journal publication drafting etc. to typst. My use case is pretty simple but I’m relatively new to the language of typst and I guess coding in general, but am keen to learn.

I have been able to get lilaq to work in reading data from two csv files containing the data I want to plot and it produces a nice overlay graph (attached) using this code. What I would like to produce is is a graph similar to the attached image which plots multiple datasets which are normalised (i.e., the y-values in column 2 of Dataset 1 are scaled such that they are divided by the highest value in that column, resulting in y-values between [0,1], and then same for Dataset 2. I would then like to be able to stack them by a constant value (i.e. offset the y-values for Dataset 2 by 1). It doesn’t seem to let me upload the csv files as I’m a new user, but am sure an example could be made with any kind of curve to illustrate the point.

#import "@preview/lilaq:0.5.0" as lq

#let (x, y) = lq.load-txt(read("PXP2291_RW211g_Cuthz-R-3m_DMF_12min.csv"))
#let (q, r) = lq.load-txt(read("PXP2290_RW211f_Cuthz-R-3m_DMF_12min.csv"))

#lq.diagram(
  width: 10cm,height: 8cm,
  xlim: (3, 30),
  ylim: (0, 9000),
  legend: (position: top + right),
  margin: (top: 20%),
  xaxis: (subticks: 4, mirror: (ticks:false)),
  yaxis: (subticks: none),
  title: [PXRD Pattern],
  xlabel: [2θ (degrees)],
  ylabel: [Intensity (a.u.)],
  lq.plot(x, y, 
    label: [Experiment 1],
    mark-size: 2pt,
    color: red,
    smooth:false),

    lq.plot(q, r, 
    label: [Experiment 2],
    mark-size: 2pt,
    color: blue,
    smooth:false),
    
)

Any help would be greatly appreciated!

Hi @robbo911 and welcome to Typst and Lilaq!

In Typst, it is quite easy to do some scripting. For example, you can write a function for normalizing your data

#let normalize(values) = {
  let max = calc.max(..values)
  values.map(value => value / max)
}

so you can reuse this function for both your y and r arrays. Do you want to also rescale the bottom of your data, I mean bring the minimum down to 0? In this case, you’ll want this as your normalization function:

#let normalize(values) = {
  let min = calc.min(..values)
  let range = calc.max(..values) - min
  values.map(value => (value - min) / range)
}

After normalization, all that remains is to add 1 to the second array because now you know that both arrays will cover the range [0, 1].

Two tips for your diagram:

  • smooth: false is already the default, so you can omit it. Smoothing will usually only be useful very few scenarios.
  • If you have more than one plot like this, you can define a color cycle with one line of code instead of defining the colors within lq.plot: #show: lq.set-diagram(cycle: (red, blue)), see Using style cycles − Lilaq.
#let normalize(values) = {
  let min = calc.min(..values)
  let range = calc.max(..values) - min
  values.map(value => (value - min) / range)
}

#show: lq.set-diagram(cycle: (red, blue))

#lq.diagram(
  width: 10cm, height: 8cm,
  xlim: (3, 30),
  ylim: (0, 9000),
  legend: (position: top + right),
  margin: (top: 20%),
  xaxis: (subticks: 4, mirror: (ticks:false)),
  yaxis: (subticks: none),
  title: [PXRD Pattern],
  xlabel: [2θ (degrees)],
  ylabel: [Intensity (a.u.)],
  lq.plot(
    x, 
    normalize(y),
    label: [Experiment 1],
    mark-size: 2pt,
  ),
  lq.plot(
    q, 
    normalize(r).map(v => v + 1),
    label: [Experiment 2],
    mark-size: 2pt,
  )
)

3 Likes

Thank you very much @Mc-Zen that was very helpful, really appreciate the prompt and detailed reply. It was in fact your video on lilaq on the typst youtube channel that converted me to trying it :slightly_smiling_face:

Thanks for the help with that normalize function (which you were correct it is much better to have it normalize 0 to 1) and the styles too; the style cycles will be very helpful to ensure consistency (especially in other types of diagrams that might have different scatter symbols that rotate).

I had one final additional question on this topic, which is how you would go about doing this if you had say 5 plots that you wanted stacked; of course I could do your method with the normalize(r).map(v => v + 1)

but wanted to check whether you thought there might be a more robust yet hopefully still simple way to do this dynamically based on the number of plots (e.g. if there were 5 plots, I don’t want to have to write v => v+4) and manually edit each stack offset.

And if its not too much to ask, just wanted to check if you had any high level advice on how to better load in the data (particularly if I end up with lots of datasets in a project). I had used the “#let (x, y) =lq.load-txt(read())” but can see that will probably get unmanageable in bigger projects with multiple datasets.

Thanks a lot for your help!

You’re welcome!

Sure this should be possible. Say you have a bunch of data. Then you can just enumerate the pairs of x and y coordinates (or just y if the xs are always the same) and add the index (0,1,2,…) to them like this:

#let data = (
  (x1, y1),
  (x2, y2),
  ..
).enumerate().map(((i, (x, y)) => {
  (x, normalize(y).map(v => v + i)
})

2 Likes

Regarding the datasets, there are multiple approaches. One option might be to switch to JSON format. It allows you to store more complex data and it is even typed (int, float, array, dictionaries, strings at least). This way you could store all data sets in one JSON file, load it into Typst and iterate over the entries programmatically.

{
  "Set 1": { "x": [...], "y": [...] },
  "Set 2": { "x": [...], "y": [...] },
}
#{
  let data = json("data.json")
  for (name, set) in data.pairs() {
    let (x, y) = set // dict destructuring
    ...
  }
}

Of course, if you need to stick to CSV for some reason, you could also just name your files in the form of some series like set1, set2, set3 and load it via load-txt in a loop.

However, I think the JSON approach will be cleaner in most cases.

1 Like