Skip to main content Skip to footer

Changing the Position of the FlexChart Legend Text

Changing the Position of the Text in the FlexChart Legend

Background:

It is not common, but some users may want to change the position of the text in the legend of their FlexChart.

Steps to Complete:

1. Create your chart and add the rendered method

2. Get the legend rect attributes

3. Create legend items

4. Position the legend items

5. Set the legend sizing

Getting Started:

Use the rendered event to reposition the rect and text elements. Rects represent rectangles. You can learn more about rects here.

Create your chart and add the rendered method

In vanilla JS, create your chart instance and add the rendered method to it. Get the legend elements by using querySelector:

const host = s.hostElement;
      const legend = host.querySelector('.wj-legend');
      const texts = Array.from(legend.querySelectorAll('text'));
      const rects = Array.from(legend.querySelectorAll('rect'));

 

Get the legend rect attributes

This is done by using the array we created from the querySelectorAll and we set the attributes with the forEach method:

const legendRectAttrs = ['x', 'y', 'width', 'height'];
      legendRectAttrs.forEach((attr) =>
        rects[0].setAttribute(attr, legend.getAttribute(attr))
      );

 

Create legend items

const itemsSourceMap = s.itemsSource.reduce((acc, item) => {
        acc[item[s.bindingName]] = item;
        return acc;
      }, {});

const legendItems = texts
        .map((legendTextLabel, i) => {
          const item = itemsSourceMap[legendTextLabel.textContent];
          if (!item) return null;

          const val = `${wjcCore.Globalize.format(
            item[s.binding] / sum,
            'p1'
          )}`;
          const valueTextLabel = legendTextLabel.cloneNode();
          valueTextLabel.classList.add('wj-value-label');
          valueTextLabel.textContent = val;

          const legendItem = document.createElementNS(
            'http://www.w3.org/2000/svg',
            'g'
          );
          legendItem.classList.add('wj-legendItem');
          legendItem.setAttribute('fill', 'transparent');
          legendItem.appendChild(legendTextLabel);
          legendItem.appendChild(rects[i + 1]);
          legendItem.appendChild(valueTextLabel);

          legend.appendChild(legendItem);
          return legendItem;
        })
        .filter(Boolean);

 

Position the legend items

const legendX = parseFloat(legend.getAttribute('x'));
      const legendY = parseFloat(legend.getAttribute('y'));
      const legendWidth = parseFloat(legend.getAttribute('width'));
      const legendHeight = parseFloat(legend.getAttribute('height'));
      const columnWidth =
        (legendWidth - padding.paddingLeft - padding.paddingRight) /
        legendColumnCount;
      var rowHeight = legendHeight / (legendItems.length / legendColumnCount);
      legendItems.forEach((item, i) => {
        const col = i % legendColumnCount;
        const row = Math.floor(i / legendColumnCount);

        const x = legendX + padding.paddingLeft + col * columnWidth;
        const y =
          legendY +
          row * rowHeight -
          padding.paddingTop -
          padding.paddingBottom;

        const textLabel = item.querySelector('text.wj-label');
        const valueLabel = item.querySelector('text.wj-value-label');
        const colorRect = item.querySelector('rect');

        item.setAttribute('x', x);
        item.setAttribute('y', y);
        colorRect.setAttribute('x', x);
        colorRect.setAttribute('y', y);
        textLabel.setAttribute(
          'x',
          x + 10 + parseFloat(colorRect.getAttribute('width')) // 10 is spacing
        );
        textLabel.setAttribute(
          'y',
          y + parseFloat(colorRect.getAttribute('height'))
        );

        valueLabel.setAttribute(
          'x',
          legendX +
            (col + 1) * columnWidth -
            valueLabel.getBoundingClientRect().width
        );
        valueLabel.setAttribute(
          'y',
          y + parseFloat(colorRect.getAttribute('height'))
        );
      });

 

Set the legend sizing

function setLegendMaximumSize(host, padding) {
    const hostEleRect = host.getBoundingClientRect();
    const hostRect = host.querySelector('svg').getBoundingClientRect();
    const footerRect = host.querySelector('.wj-footer').getBoundingClientRect();
    const legend = host.querySelector('.wj-legend');
    const plotRect = host
      .querySelector('.wj-slice-group')
      .getBoundingClientRect();

    const availableWidth =
      hostRect.width - padding.paddingLeft - padding.paddingRight;
    const availableHeight =
      footerRect.top -
      plotRect.bottom -
      padding.paddingTop -
      padding.paddingBottom;

    legend.setAttribute('x', padding.paddingLeft);
    legend.setAttribute('y', plotRect.bottom - hostEleRect.top);
    legend.setAttribute('width', availableWidth);
    legend.setAttribute('height', Math.max(0, availableHeight));
  }

  function getMaxLabelWidth(chart, engine, labelElements, sum) {
    return Math.max(
      ...Array.from(labelElements).map((label, i) => {
        const val = `${
          chart.itemsSource[i][chart.bindingName]
        } ${wjcCore.Globalize.format(
          chart.itemsSource[i][chart.binding] / sum,
          'p1'
        )}`;
        return engine.measureString(val).width;
      })
    );
  }

 

When combined, the rendered method should look like this:

rendered: function (s, e) {
      const host = s.hostElement;
      const legend = host.querySelector('.wj-legend');
      const texts = Array.from(legend.querySelectorAll('text'));
      const rects = Array.from(legend.querySelectorAll('rect'));

      const padding = {
        paddingTop: 5,
        paddingRight: 5,
        paddingBottom: 5,
        paddingLeft: 5,
      };

      // Get max width
      const maxWidth = getMaxLabelWidth(s, e.engine, texts, sum);
      setLegendMaximumSize(host, padding);

      const legendRectAttrs = ['x', 'y', 'width', 'height'];
      legendRectAttrs.forEach((attr) =>
        rects[0].setAttribute(attr, legend.getAttribute(attr))
      );

      // Create legend items
      const itemsSourceMap = s.itemsSource.reduce((acc, item) => {
        acc[item[s.bindingName]] = item;
        return acc;
      }, {});

      const legendItems = texts
        .map((legendTextLabel, i) => {
          const item = itemsSourceMap[legendTextLabel.textContent];
          if (!item) return null;

          const val = `${wjcCore.Globalize.format(
            item[s.binding] / sum,
            'p1'
          )}`;
          const valueTextLabel = legendTextLabel.cloneNode();
          valueTextLabel.classList.add('wj-value-label');
          valueTextLabel.textContent = val;

          const legendItem = document.createElementNS(
            'http://www.w3.org/2000/svg',
            'g'
          );
          legendItem.classList.add('wj-legendItem');
          legendItem.setAttribute('fill', 'transparent');
          legendItem.appendChild(legendTextLabel);
          legendItem.appendChild(rects[i + 1]);
          legendItem.appendChild(valueTextLabel);

          legend.appendChild(legendItem);
          return legendItem;
        })
        .filter(Boolean);

      // Position legend items
      const legendX = parseFloat(legend.getAttribute('x'));
      const legendY = parseFloat(legend.getAttribute('y'));
      const legendWidth = parseFloat(legend.getAttribute('width'));
      const legendHeight = parseFloat(legend.getAttribute('height'));
      const columnWidth =
        (legendWidth - padding.paddingLeft - padding.paddingRight) /
        legendColumnCount;
      var rowHeight = legendHeight / (legendItems.length / legendColumnCount);
      legendItems.forEach((item, i) => {
        const col = i % legendColumnCount;
        const row = Math.floor(i / legendColumnCount);

        const x = legendX + padding.paddingLeft + col * columnWidth;
        const y =
          legendY +
          row * rowHeight -
          padding.paddingTop -
          padding.paddingBottom;

        const textLabel = item.querySelector('text.wj-label');
        const valueLabel = item.querySelector('text.wj-value-label');
        const colorRect = item.querySelector('rect');

        item.setAttribute('x', x);
        item.setAttribute('y', y);
        colorRect.setAttribute('x', x);
        colorRect.setAttribute('y', y);
        textLabel.setAttribute(
          'x',
          x + 10 + parseFloat(colorRect.getAttribute('width')) // 10 is spacing
        );
        textLabel.setAttribute(
          'y',
          y + parseFloat(colorRect.getAttribute('height'))
        );

        valueLabel.setAttribute(
          'x',
          legendX +
            (col + 1) * columnWidth -
            valueLabel.getBoundingClientRect().width
        );
        valueLabel.setAttribute(
          'y',
          y + parseFloat(colorRect.getAttribute('height'))
        );
      });
    }

  

If followed correctly, your FlexChart legend should look something like this:

Source Code App Example Image

 

Adjustments will be needed depending on your applications needs, but this example should be a good starting point. If you would like to see the reference code, you can find it on StackBlitz.

Happy coding!

Andrew Peterson

Technical Engagement Engineer