chartjs / chartjs-plugin-annotation

Annotation plugin for Chart.js
MIT License
603 stars 325 forks source link

Apply different font to each line in label annotation options #739

Closed antbankdata closed 1 year ago

antbankdata commented 2 years ago

Hi,

I am trying to create the following label in my graph: image

I am, however, unable to make the second line bold, unless i either create 2 labels or create a canvas (which doesn't scale very well).

Would it be possible to change the font parameter to accept a list of FontSpec? Or possibly change content, such that you can pass text styling options alongside the text?

stockiNail commented 2 years ago

@antbankdata as you have seen, you can do it by a canvas or image and not configuring the plugin and not sure if this could be an enhancement to implement as common use case.

Nevertheless I have prepared a codepen where, leveraging on the canvas, you could have a multi-line content with different font for each row.

The getLabelContent function can be improved (it's just a sample and I didn't spend too much time to improve that) and the canvas should be scalable, based on the content and fonts passed as arguments.

https://codepen.io/stockinail/pen/KKQWzzR

image

stockiNail commented 2 years ago

@antbankdata FYI, I have created another codepen, https://codepen.io/stockinail/pen/MWQpZKb, changing the function to create the label by a canvas. It improves a little bit, invoking toFont only once and adds colors and textAlign options.

Hopefully this solves your issue.

image

// checks and gets the index
// when colors and fonts, passed as arguments are not the same length of the content length
const checkAndGetIndex = (i, array) => Math.max(0, Math.min(i, array.length - 1));

/**
 * @param {string|string[]} content - text of the label
 * @param {FontSpec|FontSpec[]} pfonts - the fonts to apply to each row of the content.
 *     If less than the content length, the last font object is used for the rest of the lines 
 * @param {Color|Color[]} [pcolors = ['black']] - the colors to apply to each row of the content.
 *     If less than the content length, the last color is used for the rest of the lines 
 * @param {LabelTextAlign} [textAlign = 'center'] - text alignment inside the canvas
 * @returns {HTMLCanvasElement} the canvas to add to annotation label as content
 */
const createTextLabel = function(content, pfonts, pcolors = ['black'], textAlign = 'center') {
  // check and normalize the arguments
  const lines = Chart.helpers.isArray(content) ? content : [content];
  const fonts = Chart.helpers.isArray(pfonts) ? pfonts : [pfonts];
  const colors = Chart.helpers.isArray(pcolors) ? pcolors : [pcolors];
  const toFonts = [];
  fonts.forEach((el) => toFonts.push(Chart.helpers.toFont(el)));
  // create canvas to return
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  // calculate the size of the canvas
  // based on content and fonts 
  const count = lines.length;
  let width = 0;
  let height = 0;
  for (let i = 0; i < count; i++) {
    const font = toFonts[checkAndGetIndex(i, toFonts)];
    const text = lines[i];
    ctx.font = font.string;
    width = Math.max(width, ctx.measureText(text).width);
    height += font.lineHeight;
  }
  canvas.width = width;
  canvas.height = height;
  // draw the text content on the canvas
  ctx.save();
  ctx.textBaseline = 'middle';
  ctx.textAlign = textAlign;
  let x = 0;
  if (textAlign === 'center') {
    x = width / 2;
  } else if (textAlign === 'end' || textAlign === 'right') {
    x = width;
  }
  let y = 0;
  for (let i = 0; i < count; i++) {
    const font = toFonts[checkAndGetIndex(i, toFonts)];
    const lh = font.lineHeight;
    y += lh / 2;
    const text = lines[i];
    ctx.fillStyle = colors[checkAndGetIndex(i, colors)];
    ctx.font = font.string;
    ctx.fillText(text, x, y);
    y += lh / 2;
  }
  ctx.restore();
  return canvas;
};

@kurkle @LeeLenaleee I was thinking to add a sample about this use case. Make sense to you?

LeeLenaleee commented 2 years ago

Feels a bit to complex to me to be used in an example.

stockiNail commented 2 years ago

Feels a bit to complex to me to be used in an example.

Anyway the code is written here therefore whoever can take and use it if wants.

antbankdata commented 2 years ago

@stockiNail Thank you very much for the quick and detailed answer. I will likely implement some variation of this createTextLabel function.

I do however still want to emphasize that i, as a consumer of the chartjs-plugin-annotation, would very much prefer to be able to implement it more elegantly, by just passing it as a property.

stockiNail commented 2 years ago

I do however still want to emphasize that i, as a consumer of the chartjs-plugin-annotation, would very much prefer to be able to implement it more elegantly, by just passing it as a property.

@antbankdata yes, I just proposed a work-around so you are not stuck, if urgently needed. I have tagged this issue as enhancement because this shall be evaluated, before closing it.

In my opinion, going to a multiple fonts (and colors) for the labels, we could create a different way to manage labels comparing with Chart.js where a text can have only 1 font and color. It is only a doubt as it can be implemented externally.

@kurkle @LeeLenaleee what do you think?

LeeLenaleee commented 2 years ago

I am bit scared for the rabit hole you can open with this. Like for this use case you only need to be able to style individual lines. But what happens if someone wants to style indevidual words or even individual letters. Need to give the current text render implementation a good look but I think it will be a pain in the ass to make it work nicely

antbankdata commented 2 years ago

@stockiNail Hi.

When i try to implement the canvas solution, the text becomes a little blurry (at font size 12). I have tried several solutions online to make the text less blurry (using devicePixelRatio), however, it remains blurry. Do you know if it is possible to fix the blur issue, or if that is just the downside of using canvas?

You can see the issue in your newest codepen if you change the font size to 12 and size of the chart to 300x200.

stockiNail commented 2 years ago

@antbankdata I think that it may be due to the width and height values which are set to canvas dimension (and automatically rounded to an integer).

Can you try the following?

  ...
  canvas.width = Math.ceil(width);
  canvas.height = Math.ceil(height);
  ...

I have changed the the codepen and, if I'm not wrong, sounds a bit better.