Skip to main content Skip to footer

Changing the Position of the Text in the FlexChart Legend in React

Before and After Moving Legend Text

Background:

Occasionally you need custom legend layout in a Wijmo FlexChart. The idea is to hook the rendered event, grab the legend SVG, create your own grouped legend items, and position them yourself. This version shows exactly how to do it in React.

Steps to Complete:

  1. Render the chart and wire the rendered event.

  2. Read the legend’s bounding rect attributes.

  3. Create grouped legend items.

  4. Position the legend items.

  5. Constrain the legend size.

Getting Started:

Use the chart’s rendered event to reposition the rect and text elements. Rects are SVG rectangles.

 

Render the chart and wire the rendered event

We use Wijmo’s React wrappers and the rendered event so we can manipulate the legend after the chart draws.

const onRendered = React.useCallback((s: any, e: any) => {
    const host: HTMLElement = s?.hostElement;
    const legend: SVGGElement | null = host?.querySelector('.wj-legend');
    if (!host || !legend) return;

    const texts = Array.from(legend.querySelectorAll('text'));
    const rects = Array.from(legend.querySelectorAll('rect'));

  

Read the legend’s bounding rect attributes

We mirror the legend’s x/y/width/height into the first <rect> so our custom items fit inside the same region.

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

 

Create grouped legend items

We build <g class="wj-legendItem"> elements containing the text, color swatch <rect>, and a cloned “value label”.

const itemsSourceMap = (s.itemsSource || []).reduce((acc: Record<string, any>, item: any) => {
      acc[item[s.bindingName]] = item;
      return acc;
    }, {});

 

Position the legend items

We compute a grid (columns/rows) and set x/y on each item, its swatch, and both text labels.

const legendX = parseFloat(legend.getAttribute('x') || '0');
const legendY = parseFloat(legend.getAttribute('y') || '0');
const legendWidth = parseFloat(legend.getAttribute('width') || '0');
const legendHeight = parseFloat(legend.getAttribute('height') || '0');

 

Constrain the legend size

We measure available space under the plot to set the legend’s x/y/width/height so it never overlaps the chart.

function setLegendMaximumSize(host: HTMLElement, padding: Padding) {
  const hostEleRect = host.getBoundingClientRect();
  const hostRect = (host.querySelector('svg') as SVGElement)?.getBoundingClientRect();
  const footerRect = (host.querySelector('.wj-footer') as HTMLElement)?.getBoundingClientRect();
  // For pie/donut; change selector for other chart types if needed
  const plotRect = (host.querySelector('.wj-slice-group') as SVGGElement)?.getBoundingClientRect();

  if (!hostRect || !plotRect) return;

  const availableWidth = hostRect.width - padding.paddingLeft - padding.paddingRight;
  const availableHeight = Math.max(
    0,
    ((footerRect?.top ?? hostRect.bottom) - plotRect.bottom) - padding.paddingTop - padding.paddingBottom
  );

  const legend = host.querySelector('.wj-legend') as SVGGElement;
  if (!legend) return;

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

 

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

Legend Text on the Bottom

 

Adjustments will be needed depending on your applications’ needs, but this example should be a good starting point. I hope you find this article helpful.

Happy coding!

 

Combined file for reference:

// FlexChartLegendCustom.tsx

import * as React from 'react';
import { FlexChart, FlexChartLegend } from '@mescius/wijmo.react.chart';
import * as wjcCore from '@mescius/wijmo';

type Padding = { paddingTop: number; paddingRight: number; paddingBottom: number; paddingLeft: number };

export default function FlexChartLegendCustom() {
  const data = React.useMemo(
    () => [
      { name: 'A', value: 10 },
      { name: 'B', value: 25 },
      { name: 'C', value: 15 },
      { name: 'D', value: 50 },
    ],
    []
  );

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

  const onRendered = React.useCallback((s: any, e: any) => {
    const host: HTMLElement = s?.hostElement;
    const legend: SVGGElement | null = host?.querySelector('.wj-legend');
    if (!host || !legend) return;

    const texts = Array.from(legend.querySelectorAll('text'));
    const rects = Array.from(legend.querySelectorAll('rect'));

    // total for percentages
    const sum = (s.itemsSource || []).reduce((acc: number, it: any) => acc + (+it[s.binding] || 0), 0);

    // optional: helps sizing decisions if you need it
    const _maxWidth = getMaxLabelWidth(s, e?.engine, texts, sum);

    // Step 5: set available legend area
    setLegendMaximumSize(host, padding);

    // Step 2: sync outer rect with legend bbox
    const legendRectAttrs = ['x', 'y', 'width', 'height'] as const;
    legendRectAttrs.forEach((attr) => rects[0]?.setAttribute(attr, legend.getAttribute(attr) || '0'));

    // Step 3: create grouped legend items
    const itemsSourceMap = (s.itemsSource || []).reduce((acc: Record<string, any>, item: any) => {
      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] || 0) / (sum || 1), 'p1')}`;
        const valueTextLabel = legendTextLabel.cloneNode() as SVGTextElement;
        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');

        // keep order: text label, color rect, value label
        legendItem.appendChild(legendTextLabel);
        if (rects[i + 1]) legendItem.appendChild(rects[i + 1]);
        legendItem.appendChild(valueTextLabel);

        legend.appendChild(legendItem);
        return legendItem;
      })
      .filter(Boolean) as SVGGElement[];

    // Step 4: position items in a grid
    const legendX = parseFloat(legend.getAttribute('x') || '0');
    const legendY = parseFloat(legend.getAttribute('y') || '0');
    const legendWidth = parseFloat(legend.getAttribute('width') || '0');
    const legendHeight = parseFloat(legend.getAttribute('height') || '0');

    const columnWidth =
      (legendWidth - padding.paddingLeft - padding.paddingRight) / legendColumnCount;

    const rows = Math.ceil((legendItems.length || 1) / legendColumnCount);
    const rowHeight = rows ? legendHeight / rows : legendHeight;

    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') as SVGTextElement) ||
        (item.querySelector('text') as SVGTextElement);

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

      item.setAttribute('x', String(x));
      item.setAttribute('y', String(y));

      if (colorRect) {
        colorRect.setAttribute('x', String(x));
        colorRect.setAttribute('y', String(y));
      }

      if (textLabel && colorRect) {
        const cw = parseFloat(colorRect.getAttribute('width') || '0');
        const ch = parseFloat(colorRect.getAttribute('height') || '0');
        textLabel.setAttribute('x', String(x + 10 + cw)); // 10px spacing
        textLabel.setAttribute('y', String(y + ch));
      }

      if (valueLabel) {
        const valueLabelWidth = valueLabel.getBoundingClientRect().width || 0;
        valueLabel.setAttribute('x', String(legendX + (col + 1) * columnWidth - valueLabelWidth));
        if (colorRect) {
          const ch = parseFloat(colorRect.getAttribute('height') || '0');
          valueLabel.setAttribute('y', String(y + ch));
        }
      }
    });
  }, []);

  return (
    <div style={{ width: 600, height: 400 }}>
      <FlexChart
        itemsSource={data}
        bindingName="name"
        binding="value"
        chartType="Pie"
        rendered={onRendered}
      >
        <FlexChartLegend position="Right" />
      </FlexChart>
    </div>
  );
}

/** Step 5 helper: constrain legend to available area below the plot */
function setLegendMaximumSize(host: HTMLElement, padding: Padding) {
  const hostEleRect = host.getBoundingClientRect();
  const hostRect = (host.querySelector('svg') as SVGElement)?.getBoundingClientRect();
  const footerRect = (host.querySelector('.wj-footer') as HTMLElement)?.getBoundingClientRect();
  // For pie/donut; change selector for other chart types if needed
  const plotRect = (host.querySelector('.wj-slice-group') as SVGGElement)?.getBoundingClientRect();

  if (!hostRect || !plotRect) return;

  const availableWidth = hostRect.width - padding.paddingLeft - padding.paddingRight;
  const availableHeight = Math.max(
    0,
    ((footerRect?.top ?? hostRect.bottom) - plotRect.bottom) - padding.paddingTop - padding.paddingBottom
  );

  const legend = host.querySelector('.wj-legend') as SVGGElement;
  if (!legend) return;

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

/** Optional helper: widest potential label (“name + percent”) */
function getMaxLabelWidth(chart: any, engine: any, labelElements: Element[], sum: number) {
  if (!engine || !labelElements?.length) return 0;
  return Math.max(
    ...Array.from(labelElements).map((_, i) => {
      const name = chart.itemsSource?.[i]?.[chart.bindingName] ?? '';
      const frac = (chart.itemsSource?.[i]?.[chart.binding] ?? 0) / (sum || 1);
      const val = `${name} ${wjcCore.Globalize.format(frac, 'p1')}`;
      return engine.measureString(val).width;
    })
  );
}

Andrew Peterson

Technical Engagement Engineer