Changing the Position of the Text in the FlexChart Legend in React
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:
-
Render the chart and wire the rendered event.
-
Read the legend’s bounding rect attributes.
-
Create grouped legend items.
-
Position the legend items.
-
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:

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;
})
);
}
