Changing the Position of the Text in the FlexChart Legend in Vue
Background:
Sometimes you need a custom legend layout in Wijmo FlexChart. This functionality isn't supported out of the box but can be achieved with some custom code. To do this, hook the rendered event, grab the legend SVG, build your own legend items, and position them.
Steps to Complete:
- Render the chart and wire rendered.
- Read the legend’s bounding attributes.
- Create grouped legend items.
- Position the legend items.
- Constrain the legend size.
Getting Started:
Use the chart’s rendered event to reposition the legend’s rect and text elements.
Render the chart and wire rendered
Use Wijmo’s Vue wrappers; the rendered event fires after the chart draws so you can safely manipulate the legend DOM.
<template>
<div style="width: 600px; height: 400px">
<wj-flex-chart
:itemsSource="data"
bindingName="name"
binding="value"
chartType="Pie"
@rendered="onRendered"
>
<wj-flex-chart-legend position="Right" />
</wj-flex-chart>
</div>
</template>
Read the legend’s bounding attributes
Mirror the legend group’s x/y/width/height into the first <rect> so your custom items fit inside the same box.
const legend: SVGGElement | null = host?.querySelector('.wj-legend');
const rects = Array.from(legend.querySelectorAll('rect'));
['x', 'y', 'width', 'height'].forEach((attr) =>
rects[0]?.setAttribute(attr, legend.getAttribute(attr) || '0')
);
Create grouped legend items
Build <g class="wj-legendItem"> with the text label, the color swatch <rect>, and a cloned “value label”.
const texts = Array.from(legend.querySelectorAll('text'));
const sum = (s.itemsSource || []).reduce((acc: number, it: any) => acc + (+it[s.binding] || 0), 0);
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');
legendItem.appendChild(legendTextLabel);
const colorRect = legend.querySelectorAll('rect')[i + 1];
if (colorRect) legendItem.appendChild(colorRect);
legendItem.appendChild(valueTextLabel);
legend.appendChild(legendItem);
return legendItem;
})
.filter(Boolean) as SVGGElement[];
Position the legend items
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');
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));
}
}
});
Constrain the legend size
Measure the space under the plot and set the legend’s x/y/width/height so it doesn’t collide with the chart.
function setLegendMaximumSize(
host: HTMLElement,
padding: { paddingTop: number; paddingRight: number; paddingBottom: number; paddingLeft: number }
) {
const hostEleRect = host.getBoundingClientRect();
const hostRect = (host.querySelector('svg') as SVGElement)?.getBoundingClientRect();
const footerRect = (host.querySelector('.wj-footer') as HTMLElement)?.getBoundingClientRect();
// For pie/donut charts; replace 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 may 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.vue -->
<template>
<div style="width: 600px; height: 400px">
<wj-flex-chart
:itemsSource="data"
bindingName="name"
binding="value"
chartType="Pie"
@rendered="onRendered"
>
<wj-flex-chart-legend position="Right" />
</wj-flex-chart>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { WjFlexChart, WjFlexChartLegend } from '@mescius/wijmo.vue3.chart';
import * as wjcCore from '@mescius/wijmo';
const data = ref([
{ name: 'A', value: 10 },
{ name: 'B', value: 25 },
{ name: 'C', value: 15 },
{ name: 'D', value: 50 },
]);
const legendColumnCount = 2;
const padding = { paddingTop: 5, paddingRight: 5, paddingBottom: 5, paddingLeft: 5 };
function onRendered(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'));
const sum = (s.itemsSource || []).reduce((acc: number, it: any) => acc + (+it[s.binding] || 0), 0);
// (Optional) can guide sizing decisions
const _maxWidth = getMaxLabelWidth(s, e?.engine, texts, sum);
// Step 5: size the legend region first
setLegendMaximumSize(host, padding);
// Step 2: sync first legend rect with legend bbox
['x', 'y', 'width', 'height'].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');
// Order matters: text, 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
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));
}
}
});
}
// Step 5 helper: bound the legend to available area
function setLegendMaximumSize(
host: HTMLElement,
padding: { paddingTop: number; paddingRight: number; paddingBottom: number; paddingLeft: number }
) {
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 “name + percent” label
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;
})
);
}
</script>
