Blog Post

Use Case Gallery
5 MIN READ

A Guide to Creating Bullet Charts

pmoney's avatar
pmoney
Sisense Employee
07-09-2025

Transform a standard bar chart into a bullet chart, using scripts.

 Instructions:

  1. Create a Bar Chart

  2. Add your main Category (ex. 'Company')

  3. 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)

  4.  Navigate to the widget's script editor:

     

     

  5. 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
    

     

  6. 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.';
                }
            }
        };
    });
    
  7. Save script and refresh the widget. You're done!

 

Updated 08-04-2025
Version 2.0

1 Comment

Related Content

Related Content