Changing the Position of the FlexChart Legend Text
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:
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!