Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Discussion options

Hi friends, I had an Idea for label overlay but I couldnt find the list of annotation elements where i can use x, x2, y, y2.

Is There some way to get ALL elements ? So i can use the x/y coordenates to know If one element is overlapping another one.

Thanks for ur time

You must be logged in to vote

Interesting! In this case have a look to #869
If Pr will be accepted and merged, new ‘elements’ field of the context will be available with all visibile elements.
This will solve the missing API and you could implement your logic for label overlapping.
What do you think?

Replies: 3 comments · 5 replies

Comment options

@jlindoso see the discussion: #750 (comment)

The "state" of the plugin is containing all annotation elements (both visible and not ones).

Nevertheless, can you share your idea? Maybe there could be another way to do it?

You must be logged in to vote
4 replies
@jlindoso
Comment options

My idea is to get event "afterDraw" and the element x, x2, y, y2 and compare to all annotations edges, so I can adjust my element yValue to avoid overlap

@stockiNail
Comment options

Interesting! In this case have a look to #869
If Pr will be accepted and merged, new ‘elements’ field of the context will be available with all visibile elements.
This will solve the missing API and you could implement your logic for label overlapping.
What do you think?

Answer selected by jlindoso
@jlindoso
Comment options

@stockiNail thank you Man, It Will help, regards

@jlindoso
Comment options

I gonna refactory it futher but for now, works!

Thank u again
`

function afterDrawElement(context: any) {
  let actualElement = context?.element;
  let ann = annotationPlugin as any
  const elementList = ann?._getState(context.chart)?.elements.filter((i: any) => i?.options?.type == "label")
  let changed = false
  if (actualElement?.options?.display == true)
    elementList.map((e: any) => {
      if (actualElement?.options?.id == 0 || actualElement?.options?.id == 1)
        if (e?.y < Number(context.chart?.chartArea?.top)) {
          e.y = Number(context.chart?.chartArea?.top)
          changed = true
        }
   if (e?.x < Number(context.chart?.chartArea?.left)) {
        e.x = Number(context.chart?.chartArea?.left)
        changed = true
      }
 if (e?.x2 > Number(context.chart?.chartArea?.right)) {
        e.x2 = Number(context.chart?.chartArea?.right)
        changed = true
      }

      if (actualElement?.options?.id != e?.options.id) {
        if (Number(actualElement?.x2) >= Number(e?.x) && Number(actualElement?.x2) <= Number(e?.x2)) {
          if (Number(actualElement?.y2) >= Number(e?.y) && Number(actualElement?.y2) <= Number(e?.y2)) {
            if (Number(actualElement?.options?.id) % 2 == 0) {
              actualElement.y2 > 0 ?
                e.y = Number(actualElement.y2) + (Number(actualElement.y2 * 0.01)) :
                e.y = Number(actualElement.y2) - (Number(actualElement.y2 * 0.01))
              changed = true
            }
          }
        }
      }
      if (changed) {
        context.chart.draw();
      }
    })
}

`

Comment options

For anyone dealing with this in react.... this solution works across the board. Probably also works in non react envs

Put it in your plugins.

<Bar data={data} options={options} plugins=[ ...object defined below ] />

{
                id: 'annotations_draw',
                afterUpdate: (chart) => {
                  let ann = annotationPlugin as any;
                  const elementList = ann
                    ?._getState(chart)
                    ?.elements.filter((i: any) => i?.options?.type == 'label');
                  const map = new Map();
                  let changed = false;

                  for (var i = 0; i < elementList.length; i++) {
                    const currentElement = elementList[i];

                    const currentElementWidth =
                      currentElement.x2 - currentElement.x;
                    if (currentElement.x <= chart?.chartArea?.left) {
                      currentElement.x = chart?.chartArea?.left;
                      currentElement.x2 =
                        currentElement.x + currentElementWidth;
                      elementList[i] = currentElement;
                    }
                    if (currentElement?.x2 > chart?.chartArea?.right) {
                      currentElement.x2 = chart?.chartArea?.right;
                      currentElement.x =
                        currentElement.x2 - currentElementWidth;
                      elementList[i] = currentElement;
                    }
                    for (var j = i + 1; j < elementList.length; j++) {
                      const nextElement = elementList[j];
                      if (
                        currentElement.x2 >= nextElement.x &&
                        currentElement.y === nextElement.y
                      ) {
                        const elementHeight = nextElement.y2 - nextElement.y;
                        if (map.has(currentElement.options.id)) {
                          const currentElementList = map.get(
                            currentElement.options.id
                          );
                          nextElement.y =
                            currentElementList[
                              currentElementList.length - 1
                            ].y2;
                          nextElement.y2 = nextElement.y + elementHeight;
                          elementList[j] = nextElement;
                          currentElementList.push(nextElement);
                          map.set(
                            currentElement.options.id,
                            currentElementList
                          );
                        } else {
                          nextElement.y = currentElement.y2;
                          nextElement.y2 = nextElement.y + elementHeight;
                          elementList[j] = nextElement;
                          map.set(currentElement.options.id, [nextElement]);
                        }
                        elementList[j] = nextElement;
                        changed = true;
                      }
                    }
                  }

                  if (changed && draw) {
                    chart.draw();
                  }
                },
              },

Checks all labels for overlap and updates x/y coordinates as needed. At the end, it draws the chart. The afterUpdate hook is SUPER important here. If you use after or before draw you will get stuck in a maximum stack error. afterLayout does not work.

I think this algo can be optimized so bear with it lol.

You must be logged in to vote
1 reply
@PaulSender
Comment options

Small edit. With version 3.1.0 the _getState method has been removed in favor of getAnnotations

                    ?._getState(chart)
const annotations = annotationPlugin.getAnnotations(chart);
Comment options

If any annotation labels overlap, you can use the following plugin I created. It adjusts the positions of annotation labels to ensure they do not overlap and also modifies the z-index of the labels so that the callout lines do not pass over them:

const preventLabelOverlapPlugin = {
  id: "preventLabelOverlap",
  beforeInit(chart) {
    chart._labeloverlap = {
      updatingAnnotations: false,
    };
  },
  afterLayout(chart) {
    // Use afterLayout instead of afterUpdate
    if (chart._labeloverlap.updatingAnnotations) {
      return;
    }

    const annotations = chart.options.plugins.annotation.annotations;

    if (!annotations) return;

    const annotationsArray = Array.isArray(annotations)
      ? annotations
      : Object.values(annotations);

    const positionedLabels = [];
    const chartLeftBoundary = chart.chartArea.left;
    const chartRightBoundary = chart.chartArea.right;
    const chartTopBoundary = chart.chartArea.top;
    const chartBottomBoundary = chart.chartArea.bottom;

    annotationsArray.forEach((annotation) => {
      if (annotation.type === "label" && annotation.display !== false) {
        const xScale = chart.scales[annotation.xScaleID || "x"];
        const yScale = chart.scales[annotation.yScaleID || "y"];

        if (!xScale || !yScale) {
          console.warn("Scale not found for annotation", annotation);
          return;
        }

        const xValue = annotation.xValue;
        let yValue = annotation.yValue;

        let xPixel =
          xScale.getPixelForValue(xValue) + (annotation.xAdjust || 0);
        let yPixel =
          yScale.getPixelForValue(yValue) + (annotation.yAdjust || 0);

        const width = annotation.width || 120;
        const height = annotation.height || 0;

        let adjustedYPixel = yPixel;
        let adjustedXPixel = xPixel;

        const spacing = 15;

        // Adjust for left and right boundaries
        if (adjustedXPixel + width > chartRightBoundary) {
          adjustedXPixel = chartRightBoundary - width - 10;
        }

        if (adjustedXPixel < chartLeftBoundary + width) {
          adjustedXPixel = chartLeftBoundary + width / 2 + 10;
        }

        let safetyCounter = 0;
        let overlapFound;
        do {
          overlapFound = false;

          // Check for overlap with other labels
          for (let posLabel of positionedLabels) {
            if (
              adjustedXPixel < posLabel.x + posLabel.width + spacing &&
              adjustedXPixel + width + spacing > posLabel.x &&
              adjustedYPixel < posLabel.y + posLabel.height + spacing &&
              adjustedYPixel + height + spacing > posLabel.y
            ) {
              adjustedYPixel = posLabel.y - height - spacing;
              overlapFound = true;

              // If adjustedYPixel goes above the chart boundary, adjust X position instead
              if (adjustedYPixel < chartTopBoundary) {
                adjustedYPixel = chartTopBoundary + 10;
                adjustedXPixel += width + spacing;

                // Ensure X stays within the boundaries
                if (adjustedXPixel + width > chartRightBoundary) {
                  adjustedXPixel = chartRightBoundary - width - 10;
                }

                overlapFound = true;
              }
            }
          }

          safetyCounter++;
          if (safetyCounter >= 50) {
            console.warn(
              "Safety counter triggered to prevent infinite loop during overlap adjustments"
            );
            break;
          }
        } while (overlapFound);

        // Final enforcement for bottom boundary
        if (adjustedYPixel + height > chartBottomBoundary) {
          adjustedYPixel = chartBottomBoundary - height - 10;
        }

        const newYAdjust = adjustedYPixel - yPixel;
        const newXAdjust = adjustedXPixel - xPixel;
        annotation.yAdjust = (annotation.yAdjust || 0) + newYAdjust;
        annotation.xAdjust = (annotation.xAdjust || 0) + newXAdjust;

        positionedLabels.push({
          id: annotation.id,
          x: adjustedXPixel,
          y: adjustedYPixel,
          width: width,
          height: height,
        });
      }
    });

    // Assign z-index based on vertical position
    positionedLabels.sort((a, b) => a.y - b.y);
    positionedLabels.forEach((label, index) => {
      const annotation = annotationsArray.find((ann) => ann.id === label.id);
      if (annotation) {
        annotation.z = index + 1;
      }
    });

    chart._labeloverlap.updatingAnnotations = true;
    try {
      chart.update();
    } catch (error) {
      console.error("Error updating the chart:", error);
    }
    chart._labeloverlap.updatingAnnotations = false;
  },
};

Then register the plugin:

ChartJS.register( preventLabelOverlapPlugin);

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
🙏
Q&A
Labels
None yet
4 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.