Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
rousbound opened this issue Feb 22, 2024 · 20 comments
Assignees
Labels
🎁 feature New feature or request

Comments

@rousbound
Copy link

I mean something like this:
highchart

@rousbound rousbound 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? Feb 22, 2024
@johannes-wolf
Copy link
Member

This is currently not implemented, no.

@rousbound
Copy link
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.

@johannes-wolf
Copy link
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.

@rousbound
Copy link
Author

rousbound commented Feb 25, 2024

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?

@johannes-wolf
Copy link
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.

@rousbound
Copy link
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

@johannes-wolf
Copy link
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.

@rousbound
Copy link
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?

@johannes-wolf
Copy link
Member

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

@rousbound
Copy link
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?

@rousbound
Copy link
Author

rousbound commented Feb 26, 2024

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.

@rousbound
Copy link
Author

rousbound commented Feb 26, 2024

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

@johannes-wolf
Copy link
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.

@rousbound
Copy link
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.

@johannes-wolf johannes-wolf transferred this issue from cetz-package/cetz Jun 12, 2024
@wlievens
Copy link

Is this planned to be merged into cetz-plot soon? I don't see a PR for it in this project.

@johannes-wolf
Copy link
Member

The cetz-plot repo moved, therefore this PR is in the wrong repo.

@johannes-wolf
Copy link
Member

Original PR is here: cetz-package/cetz#516

@johannes-wolf
Copy link
Member

johannes-wolf commented Jul 23, 2024

It is not easy right now to implement this in a good way, because of how plot scaling works, see #4.

@ramkumarkb
Copy link

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,

@johannes-wolf
Copy link
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.

@johannes-wolf johannes-wolf self-assigned this Nov 27, 2024
@johannes-wolf johannes-wolf added the 🎁 feature New feature or request label Nov 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🎁 feature New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants