Sisense
Display a Centered Total Label on a Donut Chart
This guide explains a widget script that renders a centered total/subtotal label inside a donut chart and dynamically colors a slice based on its share of the total.
Overview
Pie chart widgets do not natively support a label rendered inside the donut hole. A common requirement is to surface a key figure, such as a running total, or a specific category's value directly in the center of the chart, along with conditional formatting that visually flags when that category exceeds an acceptable threshold.
This article describes a script that renders such a label using Highcharts' floating title configuration and that recolors a target slice based on its percentage of the chart's total. This approach does not rely on DOM manipulation or a ready event handler, which can produce inconsistent placement or cause the label to disappear on data refresh.

Use Cases
Apply this script when:
- A donut chart needs a center label showing the total value across all slices, plus a secondary figure (e.g., a specific category's value)
- A specific slice (for example, an "Overdue" or "At Risk" category) needs to change color automatically based on what percentage of the total it represents
- A widget previously used a
readyevent-based approach to add a center label and experienced placement issues or the label disappearing after data refreshes
Prerequisites
- Edit access to the widget script (Widget Options menu > Edit Script)
- A Pie or Donut chart widget
- Category (slice) names in the underlying data that match the labels referenced in the script (in this example,
'Overdue'and'Ongoing')
- If a previous script used
widget.on("ready", ...)to add a center label, remove it before adding this script. Running both can cause conflicting or duplicate behavior.
Script
/*
Robust donut centre label + overdue colouring
No DOM manipulation, no 'ready' handler needed.
*/
widget.on('processresult', function (se, ev) {
var labelColor = '#5B6372';
var labelFontSize = '16px';
var labelFont = 'Open Sans';
var overdueLabel = 'Overdue';
var ongoingLabel = 'Ongoing';
var sum = 0;
// Defensive panel/mask lookup
var panelItem = (ev.widget.metadata.panels[1] && ev.widget.metadata.panels[1].items)
? ev.widget.metadata.panels[1].items[0]
: null;
var itemMask = panelItem ? $$get(panelItem, "format.mask", {}) : {};
var numberFormatter = prism.$injector.get('$filter')('numeric');
var seriesData = (ev.result.series && ev.result.series[0] && ev.result.series[0].data)
? ev.result.series[0].data
: [];
var overduePoint = null;
var ongoingPoint = null;
$.each(seriesData, function (index, point) {
var y = (point && typeof point.y === 'number') ? point.y : 0;
sum += y;
if (point && point.name === overdueLabel) overduePoint = point;
if (point && point.name === ongoingLabel) ongoingPoint = point;
});
var overdueY = (overduePoint && typeof overduePoint.y === 'number') ? overduePoint.y : 0;
var ongoingY = (ongoingPoint && typeof ongoingPoint.y === 'number') ? ongoingPoint.y : 0;
var overduePct = sum > 0 ? (overdueY / sum) : 0;
// Colour overdue slice based on percentage
if (overduePoint) {
if (overduePct >= 0 && overduePct < 0.15) overduePoint.color = '#00a808';
else if (overduePct >= 0.15 && overduePct < 0.25) overduePoint.color = '#ffab03';
else overduePoint.color = '#ff0000';
}
// Donut sizing
ev.result.plotOptions = ev.result.plotOptions || {};
ev.result.plotOptions.pie = ev.result.plotOptions.pie || {};
ev.result.plotOptions.pie.size = '110%';
ev.result.plotOptions.pie.innerSize = '70%';
// Centre text (Highcharts title, floated in the middle)
var titleText =
'Total: ' + numberFormatter(sum, itemMask) +
'<br/>Overdue: ' + numberFormatter(overdueY, itemMask);
ev.result.title = ev.result.title || {};
ev.result.title.text = titleText;
ev.result.title.useHTML = true;
ev.result.title.floating = true;
ev.result.title.align = 'center';
ev.result.title.verticalAlign = 'middle';
ev.result.title.y = 16;
ev.result.title.style = {
color: labelColor,
fontSize: labelFontSize,
fontFamily: labelFont,
fontWeight: 'normal',
textAlign: 'center'
};
});
// IMPORTANT: remove any existing widget.on("ready", ...) script.
// Combining it with this script causes the label to render in the wrong place or disappear.How It Works
Step | Description |
Trigger | widget.on('processresult', ...) runs after the widget's data is processed and before rendering, allowing the chart configuration to be modified in place. |
Locate the value format mask | ev.widget.metadata.panels[1].items[0] retrieves the first item in the widget's Values panel, and $$get(...) safely reads its number format mask (format.mask) so totals can be formatted consistently with the rest of the widget. |
Calculate the total and target values | The script loops through the chart's series data, summing all slice values into sum, and capturing the data points matching the 'Overdue' and 'Ongoing' labels for later use. |
Apply conditional slice coloring | The 'Overdue' slice's percentage of the total (overduePct) determines its color: green below 15%, amber between 15–25%, red at 25% or above. |
Configure donut sizing | plotOptions.pie.size and plotOptions.pie.innerSize control the outer size and inner hole size of the donut, creating the space needed for the center label. |
Render the center label | ev.result.title is repurposed as a floating, centered label rather than a standard chart title — floating: true and verticalAlign: 'middle' place it inside the donut hole. useHTML: true allows the <br/> line break in the label text. |
Customization
Property | Description |
overdueLabel / ongoingLabel | The exact category (slice) names referenced in the calculation. Update these to match the category names in your data. |
Percentage thresholds ( 0.15, 0.25) | The breakpoints used to determine slice color. Adjust to match the risk tolerance or business rule for your use case. |
Slice colors ( '#00a808', '#ffab03', '#ff0000') | Hex colors applied at each threshold. |
titleText | The label content shown in the center. Add, remove, or reorder lines as needed (each line requires a <br/> separator since useHTML is enabled). |
labelColor, labelFontSize, labelFont | Typography for the center label. |
plotOptions.pie.size / innerSize | Controls the outer diameter and hole size of the donut. Larger innerSize values create more room for longer label text. |
ev.result.title.y | Fine-tunes the vertical offset of the label from the exact center, useful if the label appears slightly off-center for a given widget size. |
Limitations and Considerations
- The script assumes a single-series pie/donut chart (
ev.result.series[0]). It will need adjustment for multi-series configurations.
- Category matching is done by exact string comparison against
point.name. If category names vary (e.g., due to localization or trailing whitespace in the data), the corresponding point will not be found and will default to a value of0.
- This script must not be combined with a separate
widget.on("ready", ...)script that also manipulates the center label or DOM elements directly — doing so was the original cause of inconsistent placement and disappearing labels.
- The number formatter relies on
prism.$injectorand the widget's existing value format mask; if the Values panel structure changes, the panel index (panels[1].items[0]) may need to be updated.
