Skip to content

Is there any way to add "DataLabels" like Highcharts. That is, to provide above/next to the bar/column the value of the y axis?  #2

Open

Description

@rousbound

I mean something like this:
highchart

Activity

  1. changed the title [-]Is there any way to add a DataLabel like Highcharts. That is, to provide above/next to the bar/column the value of the y axis? [/-] [+]Is there any way to add "DataLabels" like Highcharts. That is, to provide above/next to the bar/column the value of the y axis? [/+] on Feb 22, 2024
  2. johannes-wolf commented on Feb 22, 2024

    @johannes-wolf
    Member

    This is currently not implemented, no.

  3. rousbound commented on Feb 22, 2024

    @rousbound
    Author

    If it is a desirable feature, I can look into it and suggest an implementation.

    If that's the case, could you @johannes-wolf or someone give me some pointers on how to start thinking about this?

    I'm not familiar with CeTZ the source code yet.

  4. johannes-wolf commented on Feb 22, 2024

    @johannes-wolf
    Member

    If it is a desirable feature, I can look into it and suggest an implementation.

    If that's the case, could you @johannes-wolf or someone give me some pointers on how to start thinking about this?

    I'm not familiar with CeTZ the source code yet.

    Sure. The barchart implementation is here: https://github.com/johannes-wolf/cetz/blob/master/src/lib/plot/bar.typ.

    You can do the drawing in the _stroke function: https://github.com/johannes-wolf/cetz/blob/bf3ec2f6894ccd9255243e1fa40d2b56d6ddcd5b/src/lib/plot/bar.typ#L72

    If those numbers are drawn (and where) should be styleable self.style gives you the style dictionary. Measuring the content and deciding where to draw it could get tricky, though.

  5. rousbound commented on Feb 25, 2024

    @rousbound
    Author

    Well, actually I think that it makes more sense to draw it here while iterating through each bar, doesn't it?

    For example:

    #let _draw-rects(self, ctx, ..args) = {
      let x-axis = ctx.x
      let y-axis = ctx.y
    
      let w = self.bar-width
      for d in self.data {
        let (x, n, len, y-min, y-max) = d
    
        let x-offset = _get-x-offset(self.bar-position, self.bar-width)
        let left  = x - x-offset
        let right = left + w
        let width = (right - left) / len
    
       
    
        if self.mode in ("basic", "clustered") {
          left = left + width * n
          right = left + width
        }
    
        if (left <= x-axis.max and right >= x-axis.min and
            y-min <= y-axis.max and y-max >= y-axis.min) {
          left = calc.max(left, x-axis.min)
          right = calc.min(right, x-axis.max)
          y-min = calc.max(y-min, y-axis.min)
          y-max = calc.min(y-max, y-axis.max)
    ---------------------------------------------------------------------------------------------------------------
          draw.rect((left, y-min), (right, y-max))
            // new attribute data-label
            if self.data-label { 
               // Draw data-label here with an offset relative to the top of the bar
            }
    ---------------------------------------------------------------------------------------------------------------
        }
        }

    I couldn't understand how to draw text on that context though.

    I'm on the right track?

    If that's the case, can you give me some insight into how to draw text in that "if self.data-label" line?

  6. johannes-wolf commented on Feb 25, 2024

    @johannes-wolf
    Member

    You are on the right track. You can just use the normal draw.* commands here. The ctx is not a canvas.ctx object, but a plot.ctx object, which contains the axes you are drawing on.

    Note, that scaling is already set-up when this function is called.

  7. rousbound commented on Feb 25, 2024

    @rousbound
    Author

    Now I got it. I missed the draw.content function.

    I arrived on this:

     if self.data-label != none {
            let offset = self.data-label.at("offset")
            let size = self.data-label.at("text-size")
            draw.content((((left) + right)/2, y-max+offset), text(size:size)[#y-max])     
     }

    Then I pass this to the columnchart parameters:

    data-label:(offset: 0.4, text-size: 7pt)

    The offset doesn't work in a fixed manner because it scales, so you need to adjust it.

    It kinda works for me right now.

    If you wish we can see how to exactly make this follow the API style.

    Example:

    test

  8. johannes-wolf commented on Feb 25, 2024

    @johannes-wolf
    Member

    Looks good. You should use relative coordinates to mix canvas units with absolute units:
    draw.content(rel: (0, offset), to: ((left + right) / 2, y-max))

    You should also use add: anchor: "south" to use the south anchor as origin.

    Also, the offset and content must come from the style dict.
    If we want to add this a functionality to add-bar, we have to consider horizontal bars (check the value of y-axis.horizontal, and if so, either rotate the text or use a different anchor.

  9. rousbound commented on Feb 26, 2024

    @rousbound
    Author

    I tried to follow the draw.content function call that you suggested, but couldn't manage to make it work:

            let data_label = text(size:size)[#y-max]
            draw.content(rel: (0, offset), to: ((left + right) / 2, y-max), anchor:"south", data_label) 

    It gives this error:

    error: panicked with: "Expected 2 or 3 positional arguments, got 1"
        ┌─ src/draw/shapes.typ:758:9
        │
    758 │     panic("Expected 2 or 3 positional arguments, got " + str(args.len()))
    

    I checked the docs on code and pdf but couldn't manage to understand how to call it correctly. The docs don't mention these 'rel' and and 'to' parameters. Am I missing something?

  10. johannes-wolf commented on Feb 26, 2024

    @johannes-wolf
    Member

    You are missing parentheses arround your coordinate. rel: and to: are not arguments to content.

  11. rousbound commented on Feb 26, 2024

    @rousbound
    Author

    Oh! Right. Now I got it.

    What I have now is the following:

          draw.rect((left, y-min), (right, y-max))
          if self.style.data-label != none {
            let offset = self.data-label.at("offset")
            let size = self.data-label.at("text-size")
            let data_label = text(size:size)[#y-max]
            if y-axis.horizontal {
              draw.content((rel: (offset, 0), to: (right, (y-min + y-max) / 2)), anchor:"west", data_label)
            } else {
              draw.content((rel: (0, offset), to: ((left + right) / 2, y-max)), anchor:"south", data_label)          
            }
          }

    It is working for columnchart but not for barchart. I tried to mess up with parameters but the data-labels didn't appeared at all in the barchart case. Don't know what I'm missing.

    Also I couldn't understand how to propagate the 'data-label' from:

      set-style(
          data-label: data-label,
      )
    

    To the _draw-rects(self, ctx) function above where I need to use the data-label from style dict.

    Can you give more pointers how to continue?

  12. rousbound commented on Feb 26, 2024

    @rousbound
    Author

    Nevermind! I got it. Found on the source code that you needed to use 'draw.group(ctx => {})'.

          draw.rect((left, y-min), (right, y-max))
          draw.group(ctx => {
            if ctx.style.data-label != none {
              let offset = ctx.style.data-label.at("offset")
              let size = ctx.style.data-label.at("text-size")
              let data_label = text(size:size)[#y-max]
              if y-axis.horizontal {
                draw.content((rel: (offset, 0), to: (right, (y-min + y-max) / 2)), anchor:"west", data_label)
              } else {
                draw.content((rel: (0, offset), to: ((left + right) / 2, y-max)), anchor:"south", data_label)
              
              }
            }     
          })

    I just need to know why the data-labels aren't appearing on the barchart.

  13. rousbound commented on Feb 26, 2024

    @rousbound
    Author

    Done!

    bar.typ:

          draw.group(ctx => {
            if ctx.style.data-label != none {
              let offset = ctx.style.data-label.at("offset")
              let size = ctx.style.data-label.at("text-size")
              let data_label = text(size:size)[#y-max]
              let anchor = if y-axis.horizontal {"west"} else {"south"}
              draw.content((rel: (0, offset), to: ((left + right) / 2, y-max)), anchor:"south", data_label)
            }
          })

    I needed to make some changes to columchart.typ and barchart.typ to include these:
    columnchart.typ

    #let columnchart-default-style = (
      axes: (tick: (length: 0), grid: (stroke: (dash: "dotted"))),
      bar-width: .9,
      x-inset: 0.6,
      data-label: (offset: 0.20, text-size: 8pt)
    )

    barchart.typ

    #let barchart-default-style = (
      axes: (tick: (length: 0), grid: (stroke: (dash: "dotted"))),
      bar-width: .9,
      y-inset: 1,
      data-label: (offset: 0.10, text-size: 8pt)
    )

    The only problem that persists that I've noticed is that the style values aren't overriding the defaults, that applies not only for data-label, but for other custom values like "bar-width" and "x-inset".

    I mean:

     canvas({
      draw.set-style(
      x-inset: 0,
      legend: (fill: white),
      padding: 1.5pt,
      data-label: data-label,
      )
      chart.columnchart(
        data,
        label-key: 0,
        value-key: (..range(1, labels.len() + 1)),
        mode: "clustered",
        size: size,
        y-label: smallcaps[Escala de Likert],
        x-label: smallcaps[Perguntas],
        y-tick-step: 0.5,
        y-min: 1,
        y-max: 5,
        labels: labels,
        legend: "legend.north-east",
        bar-style: p
      )
      } )
    }

    set-style isn't working for 'x-inset', 'barwidth' and 'data-label'. I suspect the merge of the set-style dict and the bar defaults isn't working properly. Maybe it is a bug?

    Current state of the charts:

    graf1
    graf2

  14. johannes-wolf commented on Feb 27, 2024

    @johannes-wolf
    Member

    Nice! You must not use ctx.style directly but styles.resolve(ctx.style, ...) (search for uses or look up the documentation). I would also suggest adding a callback to add-bar that allows returning the content to show as label: this allows for custom formatting.

    Do you want to open up a PR with your changes? I can also add some additions myself. If we add this feature I would like to have it more configurable: overriding the content anchor, specifying the side of the bar that is used as anchor etc.

  15. rousbound commented on Feb 27, 2024

    @rousbound
    Author

    Ok! I've created the PR, take a look!

    I looked into using the styles.resolve but couldn't see exactly how to use it. I would need to check which default-style to use inside the draw-rects.

    Also the 'add-bar' callback you mentioned I didn't understand how to implement it exactly.

    If you need anything just comment on the PR and I can try to help.

  16. 6 remaining items

  17. johannes-wolf commented on Nov 24, 2024

    @johannes-wolf
    Member

    Would it be possible to have a basic version of data-labels for bar charts - i.e. w/o scaling (users usually preview and can manually adjust?) ? Thank you,

    Yes. I'll try to add something after refactoring the plot environment to make sub-plots possible.

  18. LevitatingBusinessMan commented on Jun 20, 2025

    @LevitatingBusinessMan

    Is there currently any quick way to get this to work? Does the old PR still merge cleanly?

  19. LevitatingBusinessMan commented on Jun 23, 2025

    @LevitatingBusinessMan

    The code still appears to work although the placement of the labels seems a bit off.

  20. johannes-wolf commented on Jun 23, 2025

    @johannes-wolf
    Member

    With cetz:0.4.0 a better implementation is possible, but cetz-plot needs some rework to support than (custom coordinate systems). This is a feature I am planning to implement.

  21. LevitatingBusinessMan commented on Jun 23, 2025

    @LevitatingBusinessMan

    I'd love to see this implemented properly, but for now I am looking for a quick and dirty solution that I can use before my thesis deadline this sunday. So I am currently trying to hack on the codebase trying to get it to work, any pointers?

  22. johannes-wolf commented on Jun 23, 2025

    @johannes-wolf
    Member

    Maybe I will push a branch later today that helps implementing it.

  23. LevitatingBusinessMan commented on Jun 24, 2025

    @LevitatingBusinessMan

    Were you able to make some changes?

  24. johannes-wolf commented on Jun 24, 2025

    @johannes-wolf
    Member

    With this branch:
    #135

    And this code:

    #let data = (
      (0, 4),
      (1, 13),
      (2, 12),
      (3, 3),
      (4, 7),
      (5, 9),
    )
    
    #cetz.canvas({
      import cetz.draw: *
      import cetz-plot: *
    
      plot.plot(size: (4,4), x-tick-step: none, y-tick-step: none, {
        for (x, y) in data {
          plot.annotate({
            content((plot: (x, y)), [Test], anchor: "north", padding: 0.1em)
          })
        }
    
        plot.add-bar(data,
          x-key: 0,
          y-key: 1)
      })
    })
    

    You can place labels:
    Image

  25. wlievens commented on Jun 25, 2025

    @wlievens

    Very interesting. Is this going to be released soon? I really need it in a report.

  26. johannes-wolf commented on Jun 25, 2025

    @johannes-wolf
    Member

    Very interesting. Is this going to be released soon? I really need it in a report.

    Sorry, but I do not think I will have the time to release this soon. The PR is a hack that I do not want to release it. Plan is to do a rewrite of how cetz-plot works internally.

    Is using a local checkout a problem?

  27. wlievens commented on Jun 26, 2025

    @wlievens

    I could use a local checkout, yes. It's part of an automated flow in a python module so no reason why that would not work. Thanks for the tip.

  28. LevitatingBusinessMan commented on Jun 29, 2025

    @LevitatingBusinessMan

    I applied the following patch for src/chart/barchart.typ which works for my needs.

    @johannes-wolf I am incredibly thankful that you could cook something up this quickly, this is exactly what I was hoping to be possible.

    diff --git a/src/chart/barchart.typ b/src/chart/barchart.typ
    index 5a10ed4..571efd8 100644
    --- a/src/chart/barchart.typ
    +++ b/src/chart/barchart.typ
    @@ -73,6 +73,8 @@
                   x-format: auto,
                   y-label: none,
                   labels: none,
    +              draw-val: false,
    +              draw-val-padding: 0.2em,
                   ..plot-args
                   ) = {
       assert(type(label-key) in (int, str))
    @@ -131,6 +133,14 @@
                   plot-style: bar-style,
                   ..plot-args,
         {
    +      if draw-val {
    +        for (i, xs, _) in data {
    +          plot.annotate({
    +            draw.content((plot: (xs.at(0), i)), str(xs.at(0)), anchor: "west", padding: draw-val-padding)
    +          })
    +        }
    +      }
    +
           plot.add-bar(data,
             x-key: 0,
             y-key: 1,
  29. johannes-wolf commented on Mar 7, 2026

    @johannes-wolf
    Member

    Link: #170

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions