Transform a standard bar chart into a bullet chart, using scripts.
Instructions:
-
Create a Bar Chart
-
Add your main Category (ex. 'Company')
-
Add your three required measures:
- Actual Value: This is the primary metric you are measuring. It's the "what happened" value. (ex. YTD Revenue, Actual Spend, Units Sold)
- Target Value: This is the goal or benchmark you are comparing against. It's the "what should have happened" value. (ex. Sales Quota, Budget, Last Year's Revenue)
- Qualitative Value (Forecast): This is a secondary comparative point, often used to show a projection. It's the "what we think will happen" value. (ex. Forecasted Spend, Projected Sales, Pipeline Value)
- Actual Value: This is the primary metric you are measuring. It's the "what happened" value. (ex. YTD Revenue, Actual Spend, Units Sold)
-
Navigate to the widget's script editor:
-
Paste the script into the widget's script editor and follow instructions:
*/ widget.on('processresult', function(widget, args) { // ======================================================== // CONFIGURATION // ======================================================== // --- Step 1: Field Names --- // These names MUST EXACTLY MATCH the names in your widget's Values panel. const actualValue_FieldName = 'Actual Value'; const targetValue_FieldName = 'Target'; const qualitative_FieldName = 'Qualitative'; // --- Step 2: Chart Labels --- // These are the clean names that will show up in the legend and tooltips. const actualValue_DisplayName = 'Actual Value'; const targetValue_DisplayName = 'Target'; const qualitative_DisplayName = 'Qualitative'; // Used for the marker line // --- Step 3: Color Palette --- // Define the colors for your chart elements. const color_ActualValue = '#3682ff'; // Blue for the main 'Actual Value' bar. const color_TargetValue_Fill = '#d3d3d3'; // Light gray for the background 'Target' bar. const color_TargetValue_Border = '#a9a9a9'; // Darker gray for the border. // --- EDIT MARKER COLORS HERE --- // Control the colors for the forecast marker based on your goal. // By default, OVER target is red and UNDER is green (good for tracking costs). // To make OVER target green (good for tracking revenue), just swap the color hex codes below. const color_Marker_Over = '#e45c5c'; // Color for when Forecast > Target const color_Marker_Under = '#4dbd33'; // Color for when Forecast < Target
- Add this directly underneath the script you just customized:
if (!args.result || !args.result.series || args.result.series.length === 0) { return; } const result = args.result; const categories = result.categories; // Helper function to read data from your widget. const getDataBySeriesName = (name) => { for (let i = 0; i < result.series.length; i++) { if (result.series[i].name === name) { return result.series[i].data.map(point => typeof point === 'object' ? point.y : point); } } console.warn('Field not found in widget data:', name); return new Array(categories.length).fill(0); }; // --- DATA RETRIEVAL --- const actualValueData = getDataBySeriesName(actualValue_FieldName); const targetValueData = getDataBySeriesName(targetValue_FieldName); const qualitativeData = getDataBySeriesName(qualitative_FieldName); // Clear the default chart series to build our custom one. result.series = []; // --- PLOT OPTIONS --- result.plotOptions = { bar: { stacking: null, grouping: false, pointPadding: 0, groupPadding: 0.25, // Space between each bullet chart. borderWidth: 1, borderRadius: 3, // Rounded corners for the bars. dataLabels: { enabled: false } } }; // --- SERIES CONFIGURATION --- // 1. A hidden series to hold the raw qualitative data for the tooltip. result.series.push({ name: qualitative_FieldName, type: 'bar', data: qualitativeData.map((val, index) => ({ x: index, y: val })), visible: false, showInLegend: false }); // 2. Legend item for "Over Target". result.series.push({ name: 'Over Target', type: 'line', color: color_Marker_Over, marker: { symbol: 'line', lineWidth: 4, radius: 5 }, data: [], showInLegend: true }); // 3. Legend item for "Under Target". result.series.push({ name: 'Under Target', type: 'line', color: color_Marker_Under, marker: { symbol: 'line', lineWidth: 4, radius: 5 }, data: [], showInLegend: true }); // 4. The wide, background bar representing the Target value. result.series.push({ name: targetValue_DisplayName, type: 'bar', data: targetValueData.map((val, index) => ({ x: index, y: val })), color: color_TargetValue_Fill, borderColor: color_TargetValue_Border, pointWidth: 30, // Thickness of the background target bar. zIndex: 0, states: { hover: { enabled: false } }, showInLegend: true }); // 5. The narrower, foreground bar representing the Actual value. result.series.push({ name: actualValue_DisplayName, type: 'bar', data: actualValueData.map((val, index) => ({ x: index, y: val })), color: color_ActualValue, pointWidth: 15, // Thickness of the main actual value bar. zIndex: 1, showInLegend: true }); // 6. The thin vertical line that marks the Qualitative/Forecast value. result.series.push({ name: 'Qualitative Marker', type: 'bar', data: qualitativeData.map((val, i) => ({ y: val, color: (val - targetValueData[i]) >= 0 ? color_Marker_Over : color_Marker_Under })), pointWidth: 3, // Thickness of the marker line. zIndex: 2, showInLegend: false, enableMouseTracking: false }); // --- CHART & AXIS CONFIGURATION --- if (!result.chart) result.chart = {}; result.chart.inverted = true; // Flips chart to be horizontal. if (result.yAxis.title) result.yAxis.title.text = ''; // Hides Y-axis title. if (result.xAxis.title) result.xAxis.title.text = ''; // Hides X-axis title. // --- LEGEND & TOOLTIP --- result.legend = { ...result.legend, enabled: true, reversed: true }; result.tooltip = { enabled: true, shared: true, useHTML: true, backgroundColor: 'rgba(255, 255, 255, 1)', borderWidth: 1, borderColor: '#E0E0E0', formatter: function() { try { if (!this.points || this.points.length === 0) return false; const pointIndex = this.points[0].point.index; const categoryName = this.x; const chart = this.points[0].series.chart; const actualVal = chart.series.find(s => s.name === actualValue_DisplayName).data[pointIndex].y; const targetVal = chart.series.find(s => s.name === targetValue_DisplayName).data[pointIndex].y; const qualitativeVal = chart.series.find(s => s.name === qualitative_FieldName).data[pointIndex].y; const percentOfTarget = (targetVal === 0) ? 0 : (actualVal / targetVal); const varianceVal = qualitativeVal - targetVal; const varianceColor = varianceVal >= 0 ? color_Marker_Over : color_Marker_Under; let s = `<div style="padding: 10px; font-family: 'lato', sans-serif; font-size: 13px;">`; s += `<div style="font-size: 14px; margin-bottom: 10px; font-weight: 700;">${categoryName}</div>`; s += `<table style="width: 100%;">`; s += `<tr><td style="padding: 4px 2px;"><span style="background-color:${color_ActualValue}; width: 12px; height: 12px; border-radius: 2px; display: inline-block; margin-right: 8px;"></span>${actualValue_DisplayName}</td><td style="text-align: right; font-weight: 700;">${Highcharts.numberFormat(actualVal, 0, '.', ',')}</td></tr>`; s += `<tr><td style="padding: 4px 2px;"><span style="background-color:${color_TargetValue_Fill}; border: 1px solid ${color_TargetValue_Border}; width: 12px; height: 12px; border-radius: 2px; display: inline-block; margin-right: 8px; box-sizing: border-box;"></span>${targetValue_DisplayName}</td><td style="text-align: right; font-weight: 700;">${Highcharts.numberFormat(targetVal, 0, '.', ',')}</td></tr>`; s += `<tr><td style="padding: 4px 2px; padding-left: 24px;">% of Target</td><td style="text-align: right; font-weight: 700;">${Highcharts.numberFormat(percentOfTarget * 100, 0)}%</td></tr>`; s += `<tr><td style="padding: 4px 2px;"><span style="background-color:${varianceColor}; width: 3px; height: 12px; border-radius: 2px; display: inline-block; margin-right: 8px; margin-left: 4px;"></span>${qualitative_DisplayName}</td><td style="text-align: right; font-weight: 700;">${Highcharts.numberFormat(qualitativeVal, 0, '.', ',')}</td></tr>`; s += `<tr><td style="padding: 4px 2px; padding-left: 24px;">Variance</td><td style="text-align: right; font-weight: 700; color: ${varianceColor};">${Highcharts.numberFormat(varianceVal, 0, '.', ',')}</td></tr>`; s += `</table></div>`; return s; } catch (e) { return 'Error creating tooltip.'; } } }; });
-
Save script and refresh the widget. You're done!
Updated 08-04-2025
Version 2.0pmoney
Sisense Employee
Joined November 13, 2023
Use Case Gallery
Find inspiration for your visualizations, or show off your best work and tell us how you did it!