[{"data":1,"prerenderedAt":2537},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-bullet-chart-in-excel-with-python":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-bullet-chart-in-excel-with-python":2503,"ancestors:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-bullet-chart-in-excel-with-python":2512,"children:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-bullet-chart-in-excel-with-python":2517,"related:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-bullet-chart-in-excel-with-python":2518},{"id":4,"title":5,"body":6,"dateModified":2469,"datePublished":2469,"description":2470,"extension":2471,"faq":2472,"keyword":2481,"kind":2482,"meta":2483,"navigation":102,"path":2492,"seo":2493,"slug":2496,"stem":2497,"topics":2498,"type":2501,"__hash__":2502},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-bullet-chart-in-excel-with-python\u002Findex.md","Create a Bullet Chart in Excel with Python",{"type":7,"value":8,"toc":2456},"minimark",[9,19,24,56,67,71,74,494,502,883,887,1179,1186,1190,1197,1369,1380,1480,1484,1487,1607,1613,1617,1810,1825,1829,1832,2192,2195,2345,2349,2384,2388,2391,2395,2401,2407,2413,2419,2423,2452],[10,11,12,13,18],"p",{},"A dashboard with four gauges takes up a lot of space to say little. The bullet chart, designed as a compact replacement, shows the same information in a strip: grey bands for \"poor\", \"satisfactory\" and \"good\", a dark bar for the actual value, and a short line for the target. Several KPIs sit side by side, and a reader sees at a glance which ones fall short. Excel has no bullet chart type, but it can be assembled from parts a Python script controls: a stacked column chart for the bands, plus a second series for the actual and one for the target. The construction details decide whether the chart renders correctly everywhere, so this page builds it step by step with XlsxWriter and checks the rendered output. It belongs to ",[14,15,17],"a",{"href":16},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002F","Creating Charts in Excel with openpyxl",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install xlsxwriter pandas pillow\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50,53],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," xlsxwriter",[35,51,52],{"class":45}," pandas",[35,54,55],{"class":45}," pillow\n",[10,57,58,59,62,63,66],{},"LibreOffice (",[32,60,61],{},"soffice",") and ",[32,64,65],{},"pdftoppm"," render the chart in Step 6 for a visual check; they are optional. The example tracks four KPIs with different units — revenue in thousands, margin in percent, a Net Promoter Score and on-time delivery — each with an actual value, a target and the upper limits of its \"poor\" and \"satisfactory\" bands.",[20,68,70],{"id":69},"step-1-put-the-kpis-on-one-scale","Step 1 — Put the KPIs on one scale",[10,72,73],{},"Revenue of 270 and a margin of 22 cannot share an axis. Express everything as a percentage of the KPI's \"good\" ceiling, so every column runs from 0 to 100:",[25,75,79],{"className":76,"code":77,"language":78,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nkpis = pd.DataFrame({\n    \"KPI\": [\"Revenue\", \"Margin\", \"NPS\", \"On-time\"],\n    \"Actual\": [270, 22, 48, 91],\n    \"Target\": [250, 25, 50, 95],\n    \"PoorUpTo\": [200, 15, 30, 80],\n    \"OkUpTo\": [260, 20, 45, 90],\n    \"Max\": [300, 30, 60, 100],\n})\nscale = 100 \u002F kpis[\"Max\"]\nchart_data = pd.DataFrame({\n    \"KPI\": kpis[\"KPI\"],\n    \"Actual %\": (kpis[\"Actual\"] * scale).round(1),\n    \"Target %\": (kpis[\"Target\"] * scale).round(1),\n    \"Poor\": (kpis[\"PoorUpTo\"] * scale).round(1),\n    \"Satisfactory\": ((kpis[\"OkUpTo\"] - kpis[\"PoorUpTo\"]) * scale).round(1),\n    \"Good\": ((kpis[\"Max\"] - kpis[\"OkUpTo\"]) * scale).round(1),\n})\nprint(chart_data.to_string(index=False))\n","python",[32,80,81,97,104,116,147,176,204,232,260,287,293,316,326,339,366,387,408,440,468,473],{"__ignoreMap":30},[35,82,83,87,91,94],{"class":37,"line":38},[35,84,86],{"class":85},"s-kum","import",[35,88,90],{"class":89},"skGVy"," pandas ",[35,92,93],{"class":85},"as",[35,95,96],{"class":89}," pd\n",[35,98,100],{"class":37,"line":99},2,[35,101,103],{"emptyLinePlaceholder":102},true,"\n",[35,105,107,110,113],{"class":37,"line":106},3,[35,108,109],{"class":89},"kpis ",[35,111,112],{"class":85},"=",[35,114,115],{"class":89}," pd.DataFrame({\n",[35,117,119,122,125,128,131,134,136,139,141,144],{"class":37,"line":118},4,[35,120,121],{"class":45},"    \"KPI\"",[35,123,124],{"class":89},": [",[35,126,127],{"class":45},"\"Revenue\"",[35,129,130],{"class":89},", ",[35,132,133],{"class":45},"\"Margin\"",[35,135,130],{"class":89},[35,137,138],{"class":45},"\"NPS\"",[35,140,130],{"class":89},[35,142,143],{"class":45},"\"On-time\"",[35,145,146],{"class":89},"],\n",[35,148,150,153,155,159,161,164,166,169,171,174],{"class":37,"line":149},5,[35,151,152],{"class":45},"    \"Actual\"",[35,154,124],{"class":89},[35,156,158],{"class":157},"sP0c6","270",[35,160,130],{"class":89},[35,162,163],{"class":157},"22",[35,165,130],{"class":89},[35,167,168],{"class":157},"48",[35,170,130],{"class":89},[35,172,173],{"class":157},"91",[35,175,146],{"class":89},[35,177,179,182,184,187,189,192,194,197,199,202],{"class":37,"line":178},6,[35,180,181],{"class":45},"    \"Target\"",[35,183,124],{"class":89},[35,185,186],{"class":157},"250",[35,188,130],{"class":89},[35,190,191],{"class":157},"25",[35,193,130],{"class":89},[35,195,196],{"class":157},"50",[35,198,130],{"class":89},[35,200,201],{"class":157},"95",[35,203,146],{"class":89},[35,205,207,210,212,215,217,220,222,225,227,230],{"class":37,"line":206},7,[35,208,209],{"class":45},"    \"PoorUpTo\"",[35,211,124],{"class":89},[35,213,214],{"class":157},"200",[35,216,130],{"class":89},[35,218,219],{"class":157},"15",[35,221,130],{"class":89},[35,223,224],{"class":157},"30",[35,226,130],{"class":89},[35,228,229],{"class":157},"80",[35,231,146],{"class":89},[35,233,235,238,240,243,245,248,250,253,255,258],{"class":37,"line":234},8,[35,236,237],{"class":45},"    \"OkUpTo\"",[35,239,124],{"class":89},[35,241,242],{"class":157},"260",[35,244,130],{"class":89},[35,246,247],{"class":157},"20",[35,249,130],{"class":89},[35,251,252],{"class":157},"45",[35,254,130],{"class":89},[35,256,257],{"class":157},"90",[35,259,146],{"class":89},[35,261,263,266,268,271,273,275,277,280,282,285],{"class":37,"line":262},9,[35,264,265],{"class":45},"    \"Max\"",[35,267,124],{"class":89},[35,269,270],{"class":157},"300",[35,272,130],{"class":89},[35,274,224],{"class":157},[35,276,130],{"class":89},[35,278,279],{"class":157},"60",[35,281,130],{"class":89},[35,283,284],{"class":157},"100",[35,286,146],{"class":89},[35,288,290],{"class":37,"line":289},10,[35,291,292],{"class":89},"})\n",[35,294,296,299,301,304,307,310,313],{"class":37,"line":295},11,[35,297,298],{"class":89},"scale ",[35,300,112],{"class":85},[35,302,303],{"class":157}," 100",[35,305,306],{"class":85}," \u002F",[35,308,309],{"class":89}," kpis[",[35,311,312],{"class":45},"\"Max\"",[35,314,315],{"class":89},"]\n",[35,317,319,322,324],{"class":37,"line":318},12,[35,320,321],{"class":89},"chart_data ",[35,323,112],{"class":85},[35,325,115],{"class":89},[35,327,329,331,334,337],{"class":37,"line":328},13,[35,330,121],{"class":45},[35,332,333],{"class":89},": kpis[",[35,335,336],{"class":45},"\"KPI\"",[35,338,146],{"class":89},[35,340,342,345,348,351,354,357,360,363],{"class":37,"line":341},14,[35,343,344],{"class":45},"    \"Actual %\"",[35,346,347],{"class":89},": (kpis[",[35,349,350],{"class":45},"\"Actual\"",[35,352,353],{"class":89},"] ",[35,355,356],{"class":85},"*",[35,358,359],{"class":89}," scale).round(",[35,361,362],{"class":157},"1",[35,364,365],{"class":89},"),\n",[35,367,369,372,374,377,379,381,383,385],{"class":37,"line":368},15,[35,370,371],{"class":45},"    \"Target %\"",[35,373,347],{"class":89},[35,375,376],{"class":45},"\"Target\"",[35,378,353],{"class":89},[35,380,356],{"class":85},[35,382,359],{"class":89},[35,384,362],{"class":157},[35,386,365],{"class":89},[35,388,390,393,395,398,400,402,404,406],{"class":37,"line":389},16,[35,391,392],{"class":45},"    \"Poor\"",[35,394,347],{"class":89},[35,396,397],{"class":45},"\"PoorUpTo\"",[35,399,353],{"class":89},[35,401,356],{"class":85},[35,403,359],{"class":89},[35,405,362],{"class":157},[35,407,365],{"class":89},[35,409,411,414,417,420,422,425,427,429,432,434,436,438],{"class":37,"line":410},17,[35,412,413],{"class":45},"    \"Satisfactory\"",[35,415,416],{"class":89},": ((kpis[",[35,418,419],{"class":45},"\"OkUpTo\"",[35,421,353],{"class":89},[35,423,424],{"class":85},"-",[35,426,309],{"class":89},[35,428,397],{"class":45},[35,430,431],{"class":89},"]) ",[35,433,356],{"class":85},[35,435,359],{"class":89},[35,437,362],{"class":157},[35,439,365],{"class":89},[35,441,443,446,448,450,452,454,456,458,460,462,464,466],{"class":37,"line":442},18,[35,444,445],{"class":45},"    \"Good\"",[35,447,416],{"class":89},[35,449,312],{"class":45},[35,451,353],{"class":89},[35,453,424],{"class":85},[35,455,309],{"class":89},[35,457,419],{"class":45},[35,459,431],{"class":89},[35,461,356],{"class":85},[35,463,359],{"class":89},[35,465,362],{"class":157},[35,467,365],{"class":89},[35,469,471],{"class":37,"line":470},19,[35,472,292],{"class":89},[35,474,476,479,482,486,488,491],{"class":37,"line":475},20,[35,477,478],{"class":157},"print",[35,480,481],{"class":89},"(chart_data.to_string(",[35,483,485],{"class":484},"sa561","index",[35,487,112],{"class":85},[35,489,490],{"class":157},"False",[35,492,493],{"class":89},"))\n",[10,495,496,497,501],{},"The band columns are the ",[498,499,500],"em",{},"heights"," of each band, not their upper limits, because a stacked chart adds them up: Poor 66.7 + Satisfactory 20 + Good 13.3 = 100 for revenue. Keeping the original values in their own columns lets a table next to the chart show real numbers while the chart uses the normalised ones.",[503,504,512,513,512,517,512,521,512,529,512,538,512,542,512,549,512,553,512,557,512,561,512,565,512,569,512,573,512,577,512,581,512,586,512,590,512,592,512,595,512,597,512,600,512,602,512,605,512,608,512,612,512,614,512,618,512,620,512,622,512,624,512,626,512,628,512,630,512,633,512,637,512,639,512,642,512,644,512,646,512,648,512,650,512,652,512,654,512,657,512,660,512,662,512,665,512,667,512,669,512,671,512,673,512,675,512,677,512,680,512,684,512,687,512,691,512,694,512,698,512,701,512,705,512,708,512,712,512,716,512,720,512,724,512,726,512,729,512,731,512,733,512,735,512,738,512,740,512,743,512,745,512,748,512,750,512,753,512,755,512,758,512,760,512,762,512,764,512,766,512,768,512,770,512,772,512,775,512,777,512,780,512,782,512,784,512,786,512,789,512,791,512,793,512,795,512,797,512,799,512,802,512,804,512,806,512,808,512,810,512,812,512,815,512,817,512,820,512,822,512,824,512,826,512,828,512,830,512,832,512,834,512,836,512,838,512,840,512,842,512,844,512,846,512,848,512,854,512,858,512,864,512,869,512,877],"svg",{"viewBox":505,"role":506,"ariaLabelledBy":507,"xmlns":510,"style":511},"4 0 752 192","img",[508,509],"bu-scale-t","bu-scale-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[514,515,516],"title",{"id":508},"KPIs in different units on one percentage scale",[518,519,520],"desc",{"id":509},"On the left the raw KPI values with revenue 270 against a ceiling of 300 and margin 22 against 30. On the right the same values as a percentage of each ceiling — 90 and 73.3 — with band heights that add up to 100.",[522,523],"rect",{"x":524,"y":525,"width":526,"height":527,"fill":528},"4","0","752","192","#ffffff",[522,530],{"x":531,"y":532,"width":533,"height":534,"rx":525,"fill":535,"stroke":536,"style":537},"20.0","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[522,539],{"x":540,"y":532,"width":541,"height":534,"rx":525,"fill":535,"stroke":536,"style":537},"52.0","57.7",[543,544,548],"text",{"x":545,"y":546,"style":547},"80.9","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[522,550],{"x":551,"y":532,"width":552,"height":534,"rx":525,"fill":535,"stroke":536,"style":537},"109.7","47.8",[543,554,556],{"x":555,"y":546,"style":547},"133.6","B",[522,558],{"x":559,"y":532,"width":560,"height":534,"rx":525,"fill":535,"stroke":536,"style":537},"157.5","48.4",[543,562,564],{"x":563,"y":546,"style":547},"181.7","C",[522,566],{"x":567,"y":532,"width":568,"height":534,"rx":525,"fill":535,"stroke":536,"style":537},"206.0","38.1",[543,570,572],{"x":571,"y":546,"style":547},"225.0","D",[522,574],{"x":531,"y":575,"width":533,"height":576,"rx":525,"fill":535,"stroke":536,"style":537},"64.0","28.0",[543,578,362],{"x":579,"y":580,"style":547},"36.0","83.0",[522,582],{"x":540,"y":575,"width":541,"height":576,"rx":525,"fill":583,"stroke":584,"style":585},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[543,587,589],{"x":545,"y":580,"style":588},"font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","KPI",[522,591],{"x":551,"y":575,"width":552,"height":576,"rx":525,"fill":583,"stroke":584,"style":585},[543,593,594],{"x":555,"y":580,"style":588},"Actual",[522,596],{"x":559,"y":575,"width":560,"height":576,"rx":525,"fill":583,"stroke":584,"style":585},[543,598,599],{"x":563,"y":580,"style":588},"Target",[522,601],{"x":567,"y":575,"width":568,"height":576,"rx":525,"fill":583,"stroke":584,"style":585},[543,603,604],{"x":571,"y":580,"style":588},"Max",[522,606],{"x":531,"y":607,"width":533,"height":576,"rx":525,"fill":535,"stroke":536,"style":537},"92.0",[543,609,611],{"x":579,"y":610,"style":547},"111.0","2",[522,613],{"x":540,"y":607,"width":541,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,615,617],{"x":545,"y":610,"style":616},"font-size:10.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Revenue",[522,619],{"x":551,"y":607,"width":552,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,621,158],{"x":555,"y":610,"style":616},[522,623],{"x":559,"y":607,"width":560,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,625,186],{"x":563,"y":610,"style":616},[522,627],{"x":567,"y":607,"width":568,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,629,270],{"x":571,"y":610,"style":616},[522,631],{"x":531,"y":632,"width":533,"height":576,"rx":525,"fill":535,"stroke":536,"style":537},"120.0",[543,634,636],{"x":579,"y":635,"style":547},"139.0","3",[522,638],{"x":540,"y":632,"width":541,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,640,641],{"x":545,"y":635,"style":616},"Margin",[522,643],{"x":551,"y":632,"width":552,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,645,163],{"x":555,"y":635,"style":616},[522,647],{"x":559,"y":632,"width":560,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,649,191],{"x":563,"y":635,"style":616},[522,651],{"x":567,"y":632,"width":568,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,653,224],{"x":571,"y":635,"style":616},[522,655],{"x":531,"y":656,"width":533,"height":576,"rx":525,"fill":535,"stroke":536,"style":537},"148.0",[543,658,524],{"x":579,"y":659,"style":547},"167.0",[522,661],{"x":540,"y":656,"width":541,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,663,664],{"x":545,"y":659,"style":616},"NPS",[522,666],{"x":551,"y":656,"width":552,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,668,168],{"x":555,"y":659,"style":616},[522,670],{"x":559,"y":656,"width":560,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,672,196],{"x":563,"y":659,"style":616},[522,674],{"x":567,"y":656,"width":568,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,676,279],{"x":571,"y":659,"style":616},[522,678],{"x":679,"y":532,"width":533,"height":534,"rx":525,"fill":535,"stroke":536,"style":537},"409.0",[522,681],{"x":682,"y":532,"width":683,"height":534,"rx":525,"fill":535,"stroke":536,"style":537},"441.0","58.3",[543,685,548],{"x":686,"y":546,"style":547},"470.2",[522,688],{"x":689,"y":532,"width":690,"height":534,"rx":525,"fill":535,"stroke":536,"style":537},"499.3","59.5",[543,692,556],{"x":693,"y":546,"style":547},"529.1",[522,695],{"x":696,"y":532,"width":697,"height":534,"rx":525,"fill":535,"stroke":536,"style":537},"558.9","60.2",[543,699,564],{"x":700,"y":546,"style":547},"589.0",[522,702],{"x":703,"y":532,"width":704,"height":534,"rx":525,"fill":535,"stroke":536,"style":537},"619.1","40.2",[543,706,572],{"x":707,"y":546,"style":547},"639.2",[522,709],{"x":710,"y":532,"width":711,"height":534,"rx":525,"fill":535,"stroke":536,"style":537},"659.3","37.6",[543,713,715],{"x":714,"y":546,"style":547},"678.1","E",[522,717],{"x":718,"y":532,"width":719,"height":534,"rx":525,"fill":535,"stroke":536,"style":537},"696.9","43.1",[543,721,723],{"x":722,"y":546,"style":547},"718.4","F",[522,725],{"x":679,"y":575,"width":533,"height":576,"rx":525,"fill":535,"stroke":536,"style":537},[543,727,362],{"x":728,"y":580,"style":547},"425.0",[522,730],{"x":682,"y":575,"width":683,"height":576,"rx":525,"fill":583,"stroke":584,"style":585},[543,732,589],{"x":686,"y":580,"style":588},[522,734],{"x":689,"y":575,"width":690,"height":576,"rx":525,"fill":583,"stroke":584,"style":585},[543,736,737],{"x":693,"y":580,"style":588},"Actual %",[522,739],{"x":696,"y":575,"width":697,"height":576,"rx":525,"fill":583,"stroke":584,"style":585},[543,741,742],{"x":700,"y":580,"style":588},"Target %",[522,744],{"x":703,"y":575,"width":704,"height":576,"rx":525,"fill":583,"stroke":584,"style":585},[543,746,747],{"x":707,"y":580,"style":588},"Poor",[522,749],{"x":710,"y":575,"width":711,"height":576,"rx":525,"fill":583,"stroke":584,"style":585},[543,751,752],{"x":714,"y":580,"style":588},"OK",[522,754],{"x":718,"y":575,"width":719,"height":576,"rx":525,"fill":583,"stroke":584,"style":585},[543,756,757],{"x":722,"y":580,"style":588},"Good",[522,759],{"x":679,"y":607,"width":533,"height":576,"rx":525,"fill":535,"stroke":536,"style":537},[543,761,611],{"x":728,"y":610,"style":547},[522,763],{"x":682,"y":607,"width":683,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,765,617],{"x":686,"y":610,"style":616},[522,767],{"x":689,"y":607,"width":690,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,769,257],{"x":693,"y":610,"style":616},[522,771],{"x":696,"y":607,"width":697,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,773,774],{"x":700,"y":610,"style":616},"83.3",[522,776],{"x":703,"y":607,"width":704,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,778,779],{"x":707,"y":610,"style":616},"66.7",[522,781],{"x":710,"y":607,"width":711,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,783,247],{"x":714,"y":610,"style":616},[522,785],{"x":718,"y":607,"width":719,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,787,788],{"x":722,"y":610,"style":616},"13.3",[522,790],{"x":679,"y":632,"width":533,"height":576,"rx":525,"fill":535,"stroke":536,"style":537},[543,792,636],{"x":728,"y":635,"style":547},[522,794],{"x":682,"y":632,"width":683,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,796,641],{"x":686,"y":635,"style":616},[522,798],{"x":689,"y":632,"width":690,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,800,801],{"x":693,"y":635,"style":616},"73.3",[522,803],{"x":696,"y":632,"width":697,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,805,774],{"x":700,"y":635,"style":616},[522,807],{"x":703,"y":632,"width":704,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,809,196],{"x":707,"y":635,"style":616},[522,811],{"x":710,"y":632,"width":711,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,813,814],{"x":714,"y":635,"style":616},"16.7",[522,816],{"x":718,"y":632,"width":719,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,818,819],{"x":722,"y":635,"style":616},"33.3",[522,821],{"x":679,"y":656,"width":533,"height":576,"rx":525,"fill":535,"stroke":536,"style":537},[543,823,524],{"x":728,"y":659,"style":547},[522,825],{"x":682,"y":656,"width":683,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,827,664],{"x":686,"y":659,"style":616},[522,829],{"x":689,"y":656,"width":690,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,831,229],{"x":693,"y":659,"style":616},[522,833],{"x":696,"y":656,"width":697,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,835,774],{"x":700,"y":659,"style":616},[522,837],{"x":703,"y":656,"width":704,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,839,196],{"x":707,"y":659,"style":616},[522,841],{"x":710,"y":656,"width":711,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,843,191],{"x":714,"y":659,"style":616},[522,845],{"x":718,"y":656,"width":719,"height":576,"rx":525,"fill":528,"stroke":536,"style":537},[543,847,191],{"x":722,"y":659,"style":616},[543,849,853],{"x":850,"y":851,"style":852},"132.0","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","raw values",[543,855,857],{"x":856,"y":851,"style":852},"574.5","% of the good ceiling",[37,859],{"x1":860,"y1":861,"x2":862,"y2":861,"stroke":584,"style":863},"254.1","108.0","395.0","stroke-width:2px",[865,866],"polygon",{"points":867,"fill":868},"399.0,108.0 390.0,113.0 390.0,103.0","#5b5cf0",[522,870],{"x":871,"y":872,"width":873,"height":874,"rx":875,"fill":528,"stroke":536,"style":876},"276.6","75.0","100.0","22.0","11","stroke-width:1.2px",[543,878,882],{"x":879,"y":880,"style":881},"326.5","90.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","× 100 \u002F Max",[20,884,886],{"id":885},"step-2-draw-the-bands-as-stacked-columns","Step 2 — Draw the bands as stacked columns",[25,888,890],{"className":76,"code":889,"language":78,"meta":30,"style":30},"import xlsxwriter\n\nbook = xlsxwriter.Workbook(\"kpi_bullets.xlsx\")\nsheet = book.add_worksheet(\"KPIs\")\nsheet.write_row(0, 0, list(chart_data.columns))\nfor r, row in enumerate(chart_data.itertuples(index=False), start=1):\n    sheet.write_row(r, 0, row)\nn = len(chart_data)\ncats = [\"KPIs\", 1, 0, n, 0]\n\nbands = book.add_chart({\"type\": \"column\", \"subtype\": \"stacked\"})\nfor col, colour in ((3, \"#BFC6D4\"), (4, \"#DDE2EA\"), (5, \"#EEF1F5\")):\n    bands.add_series({\"name\": [\"KPIs\", 0, col], \"categories\": cats,\n                      \"values\": [\"KPIs\", 1, col, n, col], \"fill\": {\"color\": colour}, \"gap\": 40})\n",[32,891,892,899,903,919,934,953,989,999,1012,1039,1043,1074,1116,1141],{"__ignoreMap":30},[35,893,894,896],{"class":37,"line":38},[35,895,86],{"class":85},[35,897,898],{"class":89}," xlsxwriter\n",[35,900,901],{"class":37,"line":99},[35,902,103],{"emptyLinePlaceholder":102},[35,904,905,908,910,913,916],{"class":37,"line":106},[35,906,907],{"class":89},"book ",[35,909,112],{"class":85},[35,911,912],{"class":89}," xlsxwriter.Workbook(",[35,914,915],{"class":45},"\"kpi_bullets.xlsx\"",[35,917,918],{"class":89},")\n",[35,920,921,924,926,929,932],{"class":37,"line":118},[35,922,923],{"class":89},"sheet ",[35,925,112],{"class":85},[35,927,928],{"class":89}," book.add_worksheet(",[35,930,931],{"class":45},"\"KPIs\"",[35,933,918],{"class":89},[35,935,936,939,941,943,945,947,950],{"class":37,"line":149},[35,937,938],{"class":89},"sheet.write_row(",[35,940,525],{"class":157},[35,942,130],{"class":89},[35,944,525],{"class":157},[35,946,130],{"class":89},[35,948,949],{"class":157},"list",[35,951,952],{"class":89},"(chart_data.columns))\n",[35,954,955,958,961,964,967,970,972,974,976,979,982,984,986],{"class":37,"line":178},[35,956,957],{"class":85},"for",[35,959,960],{"class":89}," r, row ",[35,962,963],{"class":85},"in",[35,965,966],{"class":157}," enumerate",[35,968,969],{"class":89},"(chart_data.itertuples(",[35,971,485],{"class":484},[35,973,112],{"class":85},[35,975,490],{"class":157},[35,977,978],{"class":89},"), ",[35,980,981],{"class":484},"start",[35,983,112],{"class":85},[35,985,362],{"class":157},[35,987,988],{"class":89},"):\n",[35,990,991,994,996],{"class":37,"line":206},[35,992,993],{"class":89},"    sheet.write_row(r, ",[35,995,525],{"class":157},[35,997,998],{"class":89},", row)\n",[35,1000,1001,1004,1006,1009],{"class":37,"line":234},[35,1002,1003],{"class":89},"n ",[35,1005,112],{"class":85},[35,1007,1008],{"class":157}," len",[35,1010,1011],{"class":89},"(chart_data)\n",[35,1013,1014,1017,1019,1022,1024,1026,1028,1030,1032,1035,1037],{"class":37,"line":262},[35,1015,1016],{"class":89},"cats ",[35,1018,112],{"class":85},[35,1020,1021],{"class":89}," [",[35,1023,931],{"class":45},[35,1025,130],{"class":89},[35,1027,362],{"class":157},[35,1029,130],{"class":89},[35,1031,525],{"class":157},[35,1033,1034],{"class":89},", n, ",[35,1036,525],{"class":157},[35,1038,315],{"class":89},[35,1040,1041],{"class":37,"line":289},[35,1042,103],{"emptyLinePlaceholder":102},[35,1044,1045,1048,1050,1053,1056,1059,1062,1064,1067,1069,1072],{"class":37,"line":295},[35,1046,1047],{"class":89},"bands ",[35,1049,112],{"class":85},[35,1051,1052],{"class":89}," book.add_chart({",[35,1054,1055],{"class":45},"\"type\"",[35,1057,1058],{"class":89},": ",[35,1060,1061],{"class":45},"\"column\"",[35,1063,130],{"class":89},[35,1065,1066],{"class":45},"\"subtype\"",[35,1068,1058],{"class":89},[35,1070,1071],{"class":45},"\"stacked\"",[35,1073,292],{"class":89},[35,1075,1076,1078,1081,1083,1086,1088,1090,1093,1096,1098,1100,1103,1105,1108,1110,1113],{"class":37,"line":318},[35,1077,957],{"class":85},[35,1079,1080],{"class":89}," col, colour ",[35,1082,963],{"class":85},[35,1084,1085],{"class":89}," ((",[35,1087,636],{"class":157},[35,1089,130],{"class":89},[35,1091,1092],{"class":45},"\"#BFC6D4\"",[35,1094,1095],{"class":89},"), (",[35,1097,524],{"class":157},[35,1099,130],{"class":89},[35,1101,1102],{"class":45},"\"#DDE2EA\"",[35,1104,1095],{"class":89},[35,1106,1107],{"class":157},"5",[35,1109,130],{"class":89},[35,1111,1112],{"class":45},"\"#EEF1F5\"",[35,1114,1115],{"class":89},")):\n",[35,1117,1118,1121,1124,1126,1128,1130,1132,1135,1138],{"class":37,"line":328},[35,1119,1120],{"class":89},"    bands.add_series({",[35,1122,1123],{"class":45},"\"name\"",[35,1125,124],{"class":89},[35,1127,931],{"class":45},[35,1129,130],{"class":89},[35,1131,525],{"class":157},[35,1133,1134],{"class":89},", col], ",[35,1136,1137],{"class":45},"\"categories\"",[35,1139,1140],{"class":89},": cats,\n",[35,1142,1143,1146,1148,1150,1152,1154,1157,1160,1163,1166,1169,1172,1174,1177],{"class":37,"line":341},[35,1144,1145],{"class":45},"                      \"values\"",[35,1147,124],{"class":89},[35,1149,931],{"class":45},[35,1151,130],{"class":89},[35,1153,362],{"class":157},[35,1155,1156],{"class":89},", col, n, col], ",[35,1158,1159],{"class":45},"\"fill\"",[35,1161,1162],{"class":89},": {",[35,1164,1165],{"class":45},"\"color\"",[35,1167,1168],{"class":89},": colour}, ",[35,1170,1171],{"class":45},"\"gap\"",[35,1173,1058],{"class":89},[35,1175,1176],{"class":157},"40",[35,1178,292],{"class":89},[10,1180,1181,1182,1185],{},"Three stacked series make the bands, darkest for \"poor\" at the bottom and lightest for \"good\" at the top — the conventional order, which makes the bands readable in greyscale print. A small gap (",[32,1183,1184],{},"\"gap\": 40",") makes each KPI's band wide, so the actual bar has room inside it.",[20,1187,1189],{"id":1188},"step-3-draw-the-actual-value-as-a-thick-error-bar","Step 3 — Draw the actual value as a thick error bar",[10,1191,1192,1193,1196],{},"The obvious recipe — a second, narrower column series on a secondary axis — is fragile: in testing with LibreOffice, the secondary column series was not drawn at all and the secondary axis ignored its fixed 0–100 scale. A sturdier construction keeps everything on one axis. Add the actual values as a line series with no line and no marker, and give it a vertical error bar that runs 100% ",[498,1194,1195],{},"down"," from each point to zero, drawn as a thick dark line:",[25,1198,1200],{"className":76,"code":1199,"language":78,"meta":30,"style":30},"lines = book.add_chart({\"type\": \"line\"})\nlines.add_series({\n    \"name\": \"Actual\", \"categories\": cats, \"values\": [\"KPIs\", 1, 1, n, 1],\n    \"line\": {\"none\": True}, \"marker\": {\"type\": \"none\"},\n    \"y_error_bars\": {\"type\": \"percentage\", \"value\": 100, \"direction\": \"minus\",\n                     \"end_style\": 0, \"line\": {\"color\": \"#1E293B\", \"width\": 14}},\n})\n",[32,1201,1202,1220,1225,1262,1294,1330,1365],{"__ignoreMap":30},[35,1203,1204,1207,1209,1211,1213,1215,1218],{"class":37,"line":38},[35,1205,1206],{"class":89},"lines ",[35,1208,112],{"class":85},[35,1210,1052],{"class":89},[35,1212,1055],{"class":45},[35,1214,1058],{"class":89},[35,1216,1217],{"class":45},"\"line\"",[35,1219,292],{"class":89},[35,1221,1222],{"class":37,"line":99},[35,1223,1224],{"class":89},"lines.add_series({\n",[35,1226,1227,1230,1232,1234,1236,1238,1241,1244,1246,1248,1250,1252,1254,1256,1258,1260],{"class":37,"line":106},[35,1228,1229],{"class":45},"    \"name\"",[35,1231,1058],{"class":89},[35,1233,350],{"class":45},[35,1235,130],{"class":89},[35,1237,1137],{"class":45},[35,1239,1240],{"class":89},": cats, ",[35,1242,1243],{"class":45},"\"values\"",[35,1245,124],{"class":89},[35,1247,931],{"class":45},[35,1249,130],{"class":89},[35,1251,362],{"class":157},[35,1253,130],{"class":89},[35,1255,362],{"class":157},[35,1257,1034],{"class":89},[35,1259,362],{"class":157},[35,1261,146],{"class":89},[35,1263,1264,1267,1269,1272,1274,1277,1280,1283,1285,1287,1289,1291],{"class":37,"line":118},[35,1265,1266],{"class":45},"    \"line\"",[35,1268,1162],{"class":89},[35,1270,1271],{"class":45},"\"none\"",[35,1273,1058],{"class":89},[35,1275,1276],{"class":157},"True",[35,1278,1279],{"class":89},"}, ",[35,1281,1282],{"class":45},"\"marker\"",[35,1284,1162],{"class":89},[35,1286,1055],{"class":45},[35,1288,1058],{"class":89},[35,1290,1271],{"class":45},[35,1292,1293],{"class":89},"},\n",[35,1295,1296,1299,1301,1303,1305,1308,1310,1313,1315,1317,1319,1322,1324,1327],{"class":37,"line":149},[35,1297,1298],{"class":45},"    \"y_error_bars\"",[35,1300,1162],{"class":89},[35,1302,1055],{"class":45},[35,1304,1058],{"class":89},[35,1306,1307],{"class":45},"\"percentage\"",[35,1309,130],{"class":89},[35,1311,1312],{"class":45},"\"value\"",[35,1314,1058],{"class":89},[35,1316,284],{"class":157},[35,1318,130],{"class":89},[35,1320,1321],{"class":45},"\"direction\"",[35,1323,1058],{"class":89},[35,1325,1326],{"class":45},"\"minus\"",[35,1328,1329],{"class":89},",\n",[35,1331,1332,1335,1337,1339,1341,1343,1345,1347,1349,1352,1354,1357,1359,1362],{"class":37,"line":178},[35,1333,1334],{"class":45},"                     \"end_style\"",[35,1336,1058],{"class":89},[35,1338,525],{"class":157},[35,1340,130],{"class":89},[35,1342,1217],{"class":45},[35,1344,1162],{"class":89},[35,1346,1165],{"class":45},[35,1348,1058],{"class":89},[35,1350,1351],{"class":45},"\"#1E293B\"",[35,1353,130],{"class":89},[35,1355,1356],{"class":45},"\"width\"",[35,1358,1058],{"class":89},[35,1360,1361],{"class":157},"14",[35,1363,1364],{"class":89},"}},\n",[35,1366,1367],{"class":37,"line":206},[35,1368,292],{"class":89},[10,1370,1371,1372,1375,1376,18],{},"An error bar of 100% in the minus direction always reaches exactly from the value to zero, so it behaves like a bar of the right height, centred in the category — independent of any axis settings. ",[32,1373,1374],{},"end_style: 0"," removes the end caps, and the 14-point line width makes it a slim column inside the band. Error bars from Python are covered in ",[14,1377,1379],{"href":1378},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-error-bars-to-an-excel-chart-with-openpyxl\u002F","Add Error Bars to an Excel Chart with openpyxl",[503,1381,512,1387,512,1390,512,1393,512,1398,512,1406,512,1417,512,1423,512,1428,512,1432,512,1435,512,1438,512,1445,512,1450,512,1454,512,1458,512,1461,512,1464,512,1471,512,1476],{"viewBox":1382,"role":506,"ariaLabelledBy":1383,"xmlns":510,"style":1386},"84 0 592 383",[1384,1385],"bu-layers-t","bu-layers-d","width:100%;max-width:592px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[514,1388,1389],{"id":1384},"The three layers of the bullet chart",[518,1391,1392],{"id":1385},"The bottom layer is a stacked column series for the poor, satisfactory and good bands. On top, a line series with no line carries a 100 percent minus error bar drawn as a thick dark line for the actual value. The top layer is a line series with long dash markers for the target.",[522,1394],{"x":1395,"y":525,"width":1396,"height":1397,"fill":528},"84","592","383",[522,1399],{"x":873,"y":1400,"width":1401,"height":1402,"rx":1403,"fill":1404,"stroke":1405,"style":863},"16.0","560.0","93.0","12","#fce9e9","var(--danger,#dc2626)",[1407,1408,1410],"g",{"transform":1409},"translate(368.0,29.0) scale(1.000)",[1411,1412],"path",{"d":1413,"fill":1414,"stroke":1415,"style":1416},"M5 12.5l4.5 4.5L19 7","none","var(--accent-ink,#be185d)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[543,1418,1422],{"x":1419,"y":1420,"style":1421},"380.0","75.5","font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Target · line series, long-dash markers",[543,1424,1427],{"x":1419,"y":1425,"style":1426},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","no line, red marker at the target height",[37,1429],{"x1":1419,"y1":1430,"x2":1419,"y2":1431,"stroke":584,"style":863},"114.0","136.0",[865,1433],{"points":1434,"fill":868},"380.0,140.0 375.0,131.0 385.0,131.0",[522,1436],{"x":873,"y":1437,"width":1401,"height":1402,"rx":1403,"fill":535,"stroke":536,"style":863},"145.0",[1407,1439,1441],{"transform":1440},"translate(368.0,158.0) scale(1.000)",[1411,1442],{"d":1443,"fill":1414,"stroke":1444,"style":1416},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8","var(--text,#172033)",[543,1446,1449],{"x":1419,"y":1447,"style":1448},"204.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Actual · line series with error bar",[543,1451,1453],{"x":1419,"y":1452,"style":1426},"224.0","100% minus error bar, 14 pt dark line",[37,1455],{"x1":1419,"y1":1456,"x2":1419,"y2":1457,"stroke":584,"style":863},"243.0","265.0",[865,1459],{"points":1460,"fill":868},"380.0,269.0 375.0,260.0 385.0,260.0",[522,1462],{"x":873,"y":1463,"width":1401,"height":1402,"rx":1403,"fill":583,"stroke":584,"style":863},"274.0",[1407,1465,1467],{"transform":1466},"translate(368.0,287.0) scale(1.000)",[1411,1468],{"d":1469,"fill":1414,"stroke":1470,"style":1416},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5","var(--brand-strong,#4338ca)",[543,1472,1475],{"x":1419,"y":1473,"style":1474},"333.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Bands · stacked columns",[543,1477,1479],{"x":1419,"y":1478,"style":1426},"353.0","Poor, Satisfactory, Good — light to dark greys",[20,1481,1483],{"id":1482},"step-4-add-the-target-marker","Step 4 — Add the target marker",[10,1485,1486],{},"The target is a second series in the same line chart: no line, and a long horizontal dash as its marker:",[25,1488,1490],{"className":76,"code":1489,"language":78,"meta":30,"style":30},"lines.add_series({\n    \"name\": \"Target\", \"categories\": cats, \"values\": [\"KPIs\", 1, 2, n, 2],\n    \"line\": {\"none\": True},\n    \"marker\": {\"type\": \"long_dash\", \"size\": 22,\n               \"fill\": {\"color\": \"#DC2626\"}, \"border\": {\"color\": \"#DC2626\"}},\n})\nbands.combine(lines)\n",[32,1491,1492,1496,1530,1544,1569,1598,1602],{"__ignoreMap":30},[35,1493,1494],{"class":37,"line":38},[35,1495,1224],{"class":89},[35,1497,1498,1500,1502,1504,1506,1508,1510,1512,1514,1516,1518,1520,1522,1524,1526,1528],{"class":37,"line":99},[35,1499,1229],{"class":45},[35,1501,1058],{"class":89},[35,1503,376],{"class":45},[35,1505,130],{"class":89},[35,1507,1137],{"class":45},[35,1509,1240],{"class":89},[35,1511,1243],{"class":45},[35,1513,124],{"class":89},[35,1515,931],{"class":45},[35,1517,130],{"class":89},[35,1519,362],{"class":157},[35,1521,130],{"class":89},[35,1523,611],{"class":157},[35,1525,1034],{"class":89},[35,1527,611],{"class":157},[35,1529,146],{"class":89},[35,1531,1532,1534,1536,1538,1540,1542],{"class":37,"line":106},[35,1533,1266],{"class":45},[35,1535,1162],{"class":89},[35,1537,1271],{"class":45},[35,1539,1058],{"class":89},[35,1541,1276],{"class":157},[35,1543,1293],{"class":89},[35,1545,1546,1549,1551,1553,1555,1558,1560,1563,1565,1567],{"class":37,"line":118},[35,1547,1548],{"class":45},"    \"marker\"",[35,1550,1162],{"class":89},[35,1552,1055],{"class":45},[35,1554,1058],{"class":89},[35,1556,1557],{"class":45},"\"long_dash\"",[35,1559,130],{"class":89},[35,1561,1562],{"class":45},"\"size\"",[35,1564,1058],{"class":89},[35,1566,163],{"class":157},[35,1568,1329],{"class":89},[35,1570,1571,1574,1576,1578,1580,1583,1585,1588,1590,1592,1594,1596],{"class":37,"line":149},[35,1572,1573],{"class":45},"               \"fill\"",[35,1575,1162],{"class":89},[35,1577,1165],{"class":45},[35,1579,1058],{"class":89},[35,1581,1582],{"class":45},"\"#DC2626\"",[35,1584,1279],{"class":89},[35,1586,1587],{"class":45},"\"border\"",[35,1589,1162],{"class":89},[35,1591,1165],{"class":45},[35,1593,1058],{"class":89},[35,1595,1582],{"class":45},[35,1597,1364],{"class":89},[35,1599,1600],{"class":37,"line":178},[35,1601,292],{"class":89},[35,1603,1604],{"class":37,"line":206},[35,1605,1606],{"class":89},"bands.combine(lines)\n",[10,1608,1609,1612],{},[32,1610,1611],{},"combine"," overlays the line chart's series on the column chart, sharing its axes. A red dash is the usual target symbol; size 22 makes it a little wider than the actual bar, so it stays visible when the actual is close to the target.",[20,1614,1616],{"id":1615},"step-5-axis-title-and-legend","Step 5 — Axis, title and legend",[25,1618,1620],{"className":76,"code":1619,"language":78,"meta":30,"style":30},"bands.set_y_axis({\"min\": 0, \"max\": 100, \"major_unit\": 20, \"num_format\": '0\"%\"',\n                  \"major_gridlines\": {\"visible\": True, \"line\": {\"color\": \"#E5E7EB\"}}})\nbands.set_title({\"name\": \"KPIs as % of the good threshold\"})\nbands.set_legend({\"position\": \"bottom\", \"delete_series\": [3]})\nbands.set_size({\"width\": 640, \"height\": 360})\nsheet.insert_chart(\"H2\", bands)\nsheet.set_column(\"A:A\", 12)\nbook.close()\nprint(\"chart written with\", n, \"KPIs\")\n",[32,1621,1622,1664,1694,1715,1740,1764,1775,1789,1794],{"__ignoreMap":30},[35,1623,1624,1627,1630,1632,1634,1636,1639,1641,1643,1645,1648,1650,1652,1654,1657,1659,1662],{"class":37,"line":38},[35,1625,1626],{"class":89},"bands.set_y_axis({",[35,1628,1629],{"class":45},"\"min\"",[35,1631,1058],{"class":89},[35,1633,525],{"class":157},[35,1635,130],{"class":89},[35,1637,1638],{"class":45},"\"max\"",[35,1640,1058],{"class":89},[35,1642,284],{"class":157},[35,1644,130],{"class":89},[35,1646,1647],{"class":45},"\"major_unit\"",[35,1649,1058],{"class":89},[35,1651,247],{"class":157},[35,1653,130],{"class":89},[35,1655,1656],{"class":45},"\"num_format\"",[35,1658,1058],{"class":89},[35,1660,1661],{"class":45},"'0\"%\"'",[35,1663,1329],{"class":89},[35,1665,1666,1669,1671,1674,1676,1678,1680,1682,1684,1686,1688,1691],{"class":37,"line":99},[35,1667,1668],{"class":45},"                  \"major_gridlines\"",[35,1670,1162],{"class":89},[35,1672,1673],{"class":45},"\"visible\"",[35,1675,1058],{"class":89},[35,1677,1276],{"class":157},[35,1679,130],{"class":89},[35,1681,1217],{"class":45},[35,1683,1162],{"class":89},[35,1685,1165],{"class":45},[35,1687,1058],{"class":89},[35,1689,1690],{"class":45},"\"#E5E7EB\"",[35,1692,1693],{"class":89},"}}})\n",[35,1695,1696,1699,1701,1703,1706,1710,1713],{"class":37,"line":106},[35,1697,1698],{"class":89},"bands.set_title({",[35,1700,1123],{"class":45},[35,1702,1058],{"class":89},[35,1704,1705],{"class":45},"\"KPIs as ",[35,1707,1709],{"class":1708},"sSjpA","% o",[35,1711,1712],{"class":45},"f the good threshold\"",[35,1714,292],{"class":89},[35,1716,1717,1720,1723,1725,1728,1730,1733,1735,1737],{"class":37,"line":118},[35,1718,1719],{"class":89},"bands.set_legend({",[35,1721,1722],{"class":45},"\"position\"",[35,1724,1058],{"class":89},[35,1726,1727],{"class":45},"\"bottom\"",[35,1729,130],{"class":89},[35,1731,1732],{"class":45},"\"delete_series\"",[35,1734,124],{"class":89},[35,1736,636],{"class":157},[35,1738,1739],{"class":89},"]})\n",[35,1741,1742,1745,1747,1749,1752,1754,1757,1759,1762],{"class":37,"line":149},[35,1743,1744],{"class":89},"bands.set_size({",[35,1746,1356],{"class":45},[35,1748,1058],{"class":89},[35,1750,1751],{"class":157},"640",[35,1753,130],{"class":89},[35,1755,1756],{"class":45},"\"height\"",[35,1758,1058],{"class":89},[35,1760,1761],{"class":157},"360",[35,1763,292],{"class":89},[35,1765,1766,1769,1772],{"class":37,"line":178},[35,1767,1768],{"class":89},"sheet.insert_chart(",[35,1770,1771],{"class":45},"\"H2\"",[35,1773,1774],{"class":89},", bands)\n",[35,1776,1777,1780,1783,1785,1787],{"class":37,"line":206},[35,1778,1779],{"class":89},"sheet.set_column(",[35,1781,1782],{"class":45},"\"A:A\"",[35,1784,130],{"class":89},[35,1786,1403],{"class":157},[35,1788,918],{"class":89},[35,1790,1791],{"class":37,"line":234},[35,1792,1793],{"class":89},"book.close()\n",[35,1795,1796,1798,1801,1804,1806,1808],{"class":37,"line":262},[35,1797,478],{"class":157},[35,1799,1800],{"class":89},"(",[35,1802,1803],{"class":45},"\"chart written with\"",[35,1805,1034],{"class":89},[35,1807,931],{"class":45},[35,1809,918],{"class":89},[10,1811,1812,1813,1816,1817,1820,1821,18],{},"Fixing the axis at 0–100 makes every band reach the top of the plot. The number format ",[32,1814,1815],{},"0\"%\""," shows the values as percentages without multiplying them by 100. ",[32,1818,1819],{},"delete_series: [3]"," removes the fourth legend entry — the invisible \"Actual\" line series, whose legend symbol would be empty — leaving Poor, Satisfactory, Good and Target. Placing a small table of the raw values beside the chart answers the inevitable \"what is 73%?\" question; see ",[14,1822,1824],{"href":1823},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-kpi-summary-block-to-an-excel-dashboard\u002F","Add a KPI Summary Block to an Excel Dashboard",[20,1826,1828],{"id":1827},"step-6-check-the-rendered-chart","Step 6 — Check the rendered chart",[10,1830,1831],{},"Charts built from several series are easy to break with one wrong index. Render the sheet and confirm that the actual bars and target markers really appear:",[25,1833,1835],{"className":76,"code":1834,"language":78,"meta":30,"style":30},"import subprocess\nfrom PIL import Image\n\nsubprocess.run([\"soffice\", \"--headless\", \"--convert-to\", \"pdf\", \"kpi_bullets.xlsx\"], check=True, capture_output=True)\nsubprocess.run([\"pdftoppm\", \"-png\", \"-r\", \"60\", \"-f\", \"1\", \"-l\", \"1\", \"kpi_bullets.pdf\", \"kpi_bullets\"], check=True)\nimg = Image.open(\"kpi_bullets-1.png\").convert(\"RGB\")\ncounts = {colour: n for n, colour in img.getcolors(maxcolors=1_000_000)}\nfor label, rgb in {\"actual bars\": (0x1E, 0x29, 0x3B), \"target markers\": (0xDC, 0x26, 0x26),\n                   \"poor band\": (0xBF, 0xC6, 0xD4)}.items():\n    print(f\"{label:15} {'present' if counts.get(rgb, 0) > 50 else 'MISSING'} ({counts.get(rgb, 0)} px)\")\n",[32,1836,1837,1844,1858,1862,1910,1971,1992,2023,2088,2117],{"__ignoreMap":30},[35,1838,1839,1841],{"class":37,"line":38},[35,1840,86],{"class":85},[35,1842,1843],{"class":89}," subprocess\n",[35,1845,1846,1849,1852,1855],{"class":37,"line":99},[35,1847,1848],{"class":85},"from",[35,1850,1851],{"class":157}," PIL",[35,1853,1854],{"class":85}," import",[35,1856,1857],{"class":89}," Image\n",[35,1859,1860],{"class":37,"line":106},[35,1861,103],{"emptyLinePlaceholder":102},[35,1863,1864,1867,1870,1872,1875,1877,1880,1882,1885,1887,1889,1892,1895,1897,1899,1901,1904,1906,1908],{"class":37,"line":118},[35,1865,1866],{"class":89},"subprocess.run([",[35,1868,1869],{"class":45},"\"soffice\"",[35,1871,130],{"class":89},[35,1873,1874],{"class":45},"\"--headless\"",[35,1876,130],{"class":89},[35,1878,1879],{"class":45},"\"--convert-to\"",[35,1881,130],{"class":89},[35,1883,1884],{"class":45},"\"pdf\"",[35,1886,130],{"class":89},[35,1888,915],{"class":45},[35,1890,1891],{"class":89},"], ",[35,1893,1894],{"class":484},"check",[35,1896,112],{"class":85},[35,1898,1276],{"class":157},[35,1900,130],{"class":89},[35,1902,1903],{"class":484},"capture_output",[35,1905,112],{"class":85},[35,1907,1276],{"class":157},[35,1909,918],{"class":89},[35,1911,1912,1914,1917,1919,1922,1924,1927,1929,1932,1934,1937,1939,1942,1944,1947,1949,1951,1953,1956,1958,1961,1963,1965,1967,1969],{"class":37,"line":149},[35,1913,1866],{"class":89},[35,1915,1916],{"class":45},"\"pdftoppm\"",[35,1918,130],{"class":89},[35,1920,1921],{"class":45},"\"-png\"",[35,1923,130],{"class":89},[35,1925,1926],{"class":45},"\"-r\"",[35,1928,130],{"class":89},[35,1930,1931],{"class":45},"\"60\"",[35,1933,130],{"class":89},[35,1935,1936],{"class":45},"\"-f\"",[35,1938,130],{"class":89},[35,1940,1941],{"class":45},"\"1\"",[35,1943,130],{"class":89},[35,1945,1946],{"class":45},"\"-l\"",[35,1948,130],{"class":89},[35,1950,1941],{"class":45},[35,1952,130],{"class":89},[35,1954,1955],{"class":45},"\"kpi_bullets.pdf\"",[35,1957,130],{"class":89},[35,1959,1960],{"class":45},"\"kpi_bullets\"",[35,1962,1891],{"class":89},[35,1964,1894],{"class":484},[35,1966,112],{"class":85},[35,1968,1276],{"class":157},[35,1970,918],{"class":89},[35,1972,1973,1976,1978,1981,1984,1987,1990],{"class":37,"line":178},[35,1974,1975],{"class":89},"img ",[35,1977,112],{"class":85},[35,1979,1980],{"class":89}," Image.open(",[35,1982,1983],{"class":45},"\"kpi_bullets-1.png\"",[35,1985,1986],{"class":89},").convert(",[35,1988,1989],{"class":45},"\"RGB\"",[35,1991,918],{"class":89},[35,1993,1994,1997,1999,2002,2004,2007,2009,2012,2015,2017,2020],{"class":37,"line":206},[35,1995,1996],{"class":89},"counts ",[35,1998,112],{"class":85},[35,2000,2001],{"class":89}," {colour: n ",[35,2003,957],{"class":85},[35,2005,2006],{"class":89}," n, colour ",[35,2008,963],{"class":85},[35,2010,2011],{"class":89}," img.getcolors(",[35,2013,2014],{"class":484},"maxcolors",[35,2016,112],{"class":85},[35,2018,2019],{"class":157},"1_000_000",[35,2021,2022],{"class":89},")}\n",[35,2024,2025,2027,2030,2032,2035,2038,2041,2044,2047,2049,2051,2054,2056,2058,2061,2063,2066,2068,2070,2073,2075,2077,2080,2082,2084,2086],{"class":37,"line":234},[35,2026,957],{"class":85},[35,2028,2029],{"class":89}," label, rgb ",[35,2031,963],{"class":85},[35,2033,2034],{"class":89}," {",[35,2036,2037],{"class":45},"\"actual bars\"",[35,2039,2040],{"class":89},": (",[35,2042,2043],{"class":85},"0x",[35,2045,2046],{"class":157},"1E",[35,2048,130],{"class":89},[35,2050,2043],{"class":85},[35,2052,2053],{"class":157},"29",[35,2055,130],{"class":89},[35,2057,2043],{"class":85},[35,2059,2060],{"class":157},"3B",[35,2062,978],{"class":89},[35,2064,2065],{"class":45},"\"target markers\"",[35,2067,2040],{"class":89},[35,2069,2043],{"class":85},[35,2071,2072],{"class":157},"DC",[35,2074,130],{"class":89},[35,2076,2043],{"class":85},[35,2078,2079],{"class":157},"26",[35,2081,130],{"class":89},[35,2083,2043],{"class":85},[35,2085,2079],{"class":157},[35,2087,365],{"class":89},[35,2089,2090,2093,2095,2097,2100,2102,2104,2107,2109,2111,2114],{"class":37,"line":262},[35,2091,2092],{"class":45},"                   \"poor band\"",[35,2094,2040],{"class":89},[35,2096,2043],{"class":85},[35,2098,2099],{"class":157},"BF",[35,2101,130],{"class":89},[35,2103,2043],{"class":85},[35,2105,2106],{"class":157},"C6",[35,2108,130],{"class":89},[35,2110,2043],{"class":85},[35,2112,2113],{"class":157},"D4",[35,2115,2116],{"class":89},")}.items():\n",[35,2118,2119,2122,2124,2127,2130,2133,2136,2139,2142,2144,2147,2150,2153,2155,2158,2161,2164,2167,2170,2172,2175,2177,2180,2182,2185,2187,2190],{"class":37,"line":289},[35,2120,2121],{"class":157},"    print",[35,2123,1800],{"class":89},[35,2125,2126],{"class":85},"f",[35,2128,2129],{"class":45},"\"",[35,2131,2132],{"class":1708},"{",[35,2134,2135],{"class":89},"label",[35,2137,2138],{"class":85},":15",[35,2140,2141],{"class":1708},"}",[35,2143,2034],{"class":1708},[35,2145,2146],{"class":45},"'present'",[35,2148,2149],{"class":85}," if",[35,2151,2152],{"class":89}," counts.get(rgb, ",[35,2154,525],{"class":157},[35,2156,2157],{"class":89},") ",[35,2159,2160],{"class":85},">",[35,2162,2163],{"class":157}," 50",[35,2165,2166],{"class":85}," else",[35,2168,2169],{"class":45}," 'MISSING'",[35,2171,2141],{"class":1708},[35,2173,2174],{"class":45}," (",[35,2176,2132],{"class":1708},[35,2178,2179],{"class":89},"counts.get(rgb, ",[35,2181,525],{"class":157},[35,2183,2184],{"class":89},")",[35,2186,2141],{"class":1708},[35,2188,2189],{"class":45}," px)\"",[35,2191,918],{"class":89},[10,2193,2194],{},"Counting pixels of each series colour proves that every layer was drawn — the check that would have caught the missing secondary-axis series. In the render, Revenue's bar rises past its target into the good band, Margin falls short in the satisfactory band, NPS sits just below target, and On-time is just below its 95% target in the good band.",[503,2196,512,2202,512,2205,512,2208,512,2212,512,2216,512,2221,512,2224,512,2227,512,2232,512,2237,512,2241,512,2244,512,2247,512,2250,512,2253,512,2257,512,2259,512,2262,512,2265,512,2269,512,2272,512,2275,512,2279,512,2284,512,2289,512,2294,512,2298,512,2302,512,2305,512,2310,512,2314,512,2319,512,2324,512,2327,512,2330,512,2333,512,2336,512,2341],{"viewBox":2197,"role":506,"ariaLabelledBy":2198,"xmlns":510,"style":2201},"4 0 748 280",[2199,2200],"bu-result-t","bu-result-d","width:100%;max-width:748px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[514,2203,2204],{"id":2199},"The four KPIs against their targets",[518,2206,2207],{"id":2200},"Actual values as a percentage of each KPI's good ceiling, with targets. Revenue 90 against a target of 83.3, Margin 73.3 against 83.3, NPS 80 against 83.3 and On-time 91 against 95.",[522,2209],{"x":524,"y":525,"width":2210,"height":2211,"fill":528},"748","280",[522,2213],{"x":575,"y":2214,"width":2215,"height":2215,"rx":636,"fill":535,"stroke":536,"style":585},"18.0","14.0",[543,2217,737],{"x":2218,"y":2219,"style":2220},"84.0","29.5","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start",[522,2222],{"x":2223,"y":2214,"width":2215,"height":2215,"rx":636,"fill":1404,"stroke":1405,"style":585},"167.8",[543,2225,742],{"x":2226,"y":2219,"style":2220},"187.8",[37,2228],{"x1":2229,"y1":2230,"x2":2231,"y2":2230,"stroke":536,"style":537},"51.3","242.0","736.0",[543,2233,525],{"x":2234,"y":2235,"style":2236},"29.6","246.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle",[37,2238],{"x1":2229,"y1":2239,"x2":2231,"y2":2239,"stroke":536,"style":2240},"194.5","stroke-width:1px;stroke-dasharray:5 4",[543,2242,191],{"x":2234,"y":2243,"style":2236},"198.5",[37,2245],{"x1":2229,"y1":2246,"x2":2231,"y2":2246,"stroke":536,"style":2240},"147.0",[543,2248,196],{"x":2234,"y":2249,"style":2236},"151.0",[37,2251],{"x1":2229,"y1":2252,"x2":2231,"y2":2252,"stroke":536,"style":2240},"99.5",[543,2254,2256],{"x":2234,"y":2255,"style":2236},"103.5","75",[37,2258],{"x1":2229,"y1":540,"x2":2231,"y2":540,"stroke":536,"style":2240},[543,2260,284],{"x":2234,"y":2261,"style":2236},"56.0",[37,2263],{"x1":2229,"y1":2230,"x2":2231,"y2":2230,"stroke":2264,"style":585},"var(--muted,#5b6780)",[543,2266,617],{"x":2267,"y":2268,"style":1426},"136.9","260.0",[543,2270,641],{"x":2271,"y":2268,"style":1426},"308.0",[543,2273,664],{"x":2274,"y":2268,"style":1426},"479.2",[543,2276,2278],{"x":2277,"y":2268,"style":1426},"650.4","On-time",[522,2280],{"x":2281,"y":2282,"width":579,"height":2283,"rx":636,"fill":535,"stroke":536,"style":585},"99.9","71.0","171.0",[543,2285,257],{"x":2286,"y":2287,"style":2288},"117.9","65.0","font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[522,2290],{"x":2291,"y":2292,"width":579,"height":2293,"rx":636,"fill":535,"stroke":536,"style":585},"271.0","102.7","139.3",[543,2295,801],{"x":2296,"y":2297,"style":2288},"289.0","96.7",[522,2299],{"x":2300,"y":880,"width":579,"height":2301,"rx":636,"fill":535,"stroke":536,"style":585},"442.2","152.0",[543,2303,229],{"x":2304,"y":2218,"style":2288},"460.2",[522,2306],{"x":2307,"y":2308,"width":579,"height":2309,"rx":636,"fill":535,"stroke":536,"style":585},"613.4","69.1","172.9",[543,2311,173],{"x":2312,"y":2313,"style":2288},"631.4","63.1",[522,2315],{"x":2316,"y":2317,"width":579,"height":2318,"rx":636,"fill":1404,"stroke":1405,"style":585},"137.9","83.7","158.3",[543,2320,774],{"x":2321,"y":2322,"style":2323},"155.9","77.7","font-size:10.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[522,2325],{"x":2326,"y":2317,"width":579,"height":2318,"rx":636,"fill":1404,"stroke":1405,"style":585},"309.0",[543,2328,774],{"x":2329,"y":2322,"style":2323},"327.0",[522,2331],{"x":2332,"y":2317,"width":579,"height":2318,"rx":636,"fill":1404,"stroke":1405,"style":585},"480.2",[543,2334,774],{"x":2335,"y":2322,"style":2323},"498.2",[522,2337],{"x":2338,"y":2339,"width":579,"height":2340,"rx":636,"fill":1404,"stroke":1405,"style":585},"651.4","61.5","180.5",[543,2342,201],{"x":2343,"y":2344,"style":2323},"669.4","55.5",[20,2346,2348],{"id":2347},"common-pitfalls","Common pitfalls",[2350,2351,2352,2360,2366,2372,2378],"ul",{},[2353,2354,2355,2359],"li",{},[2356,2357,2358],"strong",{},"Mixing units on one axis."," Normalise each KPI to its own ceiling first.",[2353,2361,2362,2365],{},[2356,2363,2364],{},"Band upper limits instead of heights."," Stacked columns add values; store each band's height.",[2353,2367,2368,2371],{},[2356,2369,2370],{},"Relying on a secondary axis."," It renders inconsistently across applications; the error-bar construction stays on one axis.",[2353,2373,2374,2377],{},[2356,2375,2376],{},"An empty legend entry."," Delete the invisible actual series from the legend.",[2353,2379,2380,2383],{},[2356,2381,2382],{},"Colour-only meaning."," Keep the bands in light-to-dark greys so the chart survives greyscale printing.",[20,2385,2387],{"id":2386},"conclusion","Conclusion",[10,2389,2390],{},"A bullet chart in Excel is three layers on one axis: stacked columns for the poor, satisfactory and good bands, an invisible line series whose 100% minus error bar draws the actual value as a thick bar, and a second line series with long-dash markers for the target. Normalise KPIs to a common percentage scale first, fix the axis at 0–100, remove the invisible series from the legend, and render the chart in a test to make sure every layer appears.",[20,2392,2394],{"id":2393},"frequently-asked-questions","Frequently asked questions",[10,2396,2397,2400],{},[2356,2398,2399],{},"Can Excel make a bullet chart?","\nNot as a built-in chart type, but you can build one by combining a stacked column chart for the bands with series for the actual value and the target. Python libraries such as XlsxWriter can create it in one script.",[10,2402,2403,2406],{},[2356,2404,2405],{},"How do I show the actual value as a bar inside the bands?","\nAdd the actual values as a line series with no line and no marker, and give it a y error bar of 100 percent in the minus direction with a thick line. The error bar runs from the value down to zero like a narrow column.",[10,2408,2409,2412],{},[2356,2410,2411],{},"How do I put KPIs with different units in one bullet chart?","\nExpress each KPI as a percentage of its own maximum or \"good\" threshold, so every KPI runs from 0 to 100, and show the raw values in a table beside the chart.",[10,2414,2415,2418],{},[2356,2416,2417],{},"Why does my secondary-axis bullet chart look wrong?","\nSecondary axes on combined column charts are rendered inconsistently: in testing, LibreOffice ignored the fixed axis scale and did not draw the secondary column series. Keeping all series on the primary axis with the error-bar technique avoids the problem.",[20,2420,2422],{"id":2421},"related","Related",[2350,2424,2425,2431,2438,2445],{},[2353,2426,2427,2428,2430],{},"Up one level: ",[14,2429,17],{"href":16}," — native charts from Python.",[2353,2432,2433,2437],{},[14,2434,2436],{"href":2435},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-gauge-chart-in-excel-with-python\u002F","Create a Gauge Chart in Excel with Python"," — the space-hungry alternative.",[2353,2439,2440,2444],{},[14,2441,2443],{"href":2442},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-a-combo-chart-with-a-secondary-axis-in-openpyxl\u002F","Add a Combo Chart with a Secondary Axis in openpyxl"," — when a secondary axis is the right tool.",[2353,2446,2447,2451],{},[14,2448,2450],{"href":2449},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets\u002F","Build a Monthly KPI Scorecard with Targets"," — a report that uses bullet charts.",[2453,2454,2455],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":30,"searchDepth":99,"depth":99,"links":2457},[2458,2459,2460,2461,2462,2463,2464,2465,2466,2467,2468],{"id":22,"depth":99,"text":23},{"id":69,"depth":99,"text":70},{"id":885,"depth":99,"text":886},{"id":1188,"depth":99,"text":1189},{"id":1482,"depth":99,"text":1483},{"id":1615,"depth":99,"text":1616},{"id":1827,"depth":99,"text":1828},{"id":2347,"depth":99,"text":2348},{"id":2386,"depth":99,"text":2387},{"id":2393,"depth":99,"text":2394},{"id":2421,"depth":99,"text":2422},"2026-10-02","Build a bullet chart — qualitative bands, an actual-value bar and a target marker — as a native Excel chart from Python: normalise KPIs with different units, draw the bands as stacked columns, draw the actual as a thick error bar and the target as a dash marker on the same axis, tidy the legend and axis, and check the rendered result. Includes why the common secondary-axis recipe is fragile.","md",[2473,2475,2477,2479],{"q":2399,"a":2474},"Not as a built-in chart type, but you can build one by combining a stacked column chart for the bands with series for the actual value and the target. Python libraries such as XlsxWriter can create it in one script.",{"q":2405,"a":2476},"Add the actual values as a line series with no line and no marker, and give it a y error bar of 100 percent in the minus direction with a thick line. The error bar runs from the value down to zero like a narrow column.",{"q":2411,"a":2478},"Express each KPI as a percentage of its own maximum or \"good\" threshold, so every KPI runs from 0 to 100, and show the raw values in a table beside the chart.",{"q":2417,"a":2480},"Secondary axes on combined column charts are rendered inconsistently: in testing, LibreOffice ignored the fixed axis scale and did not draw the secondary column series. Keeping all series on the primary axis with the error-bar technique avoids the problem.","create a bullet chart in excel with python",null,{"breadcrumb":2484},[2485,2488,2491],{"name":2486,"item":2487},"Home","\u002F",{"name":2489,"item":2490},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002F",{"name":17,"item":16},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-bullet-chart-in-excel-with-python",{"title":2494,"description":2495},"Create a Bullet Chart in Excel with Python (XlsxWriter)","A native Excel bullet chart from Python: KPIs normalised to % of target bands, stacked columns for poor\u002Fsatisfactory\u002Fgood, the actual value as a 100% minus error bar, a long-dash target marker, legend clean-up, and a rendered check.","create-a-bullet-chart-in-excel-with-python","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-bullet-chart-in-excel-with-python\u002Findex",[2499,2500],"charts-and-images","openpyxl","how-to","pXZ3So-nd_6lQmrHZSZt7KO8E7dcTsmP2Qok-Mg3QH0",[2504,2508],{"title":2505,"path":2506,"stem":2507,"children":-1},"Create a Bubble Chart in Excel with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-bubble-chart-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-bubble-chart-with-openpyxl\u002Findex",{"title":2509,"path":2510,"stem":2511,"children":-1},"Create a Butterfly Chart in Excel with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-butterfly-chart-in-excel-with-python","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-butterfly-chart-in-excel-with-python\u002Findex",[2513,2515],{"path":2514,"title":17},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl",{"path":2516,"title":2489},"\u002Fformatting-and-charting-excel-reports-with-python",[],[2519,2522,2525,2528,2531,2534],{"path":2520,"title":2521,"kind":2501},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fbuild-a-dashboard-sheet-with-charts-from-multiple-tabs","Build a Dashboard Sheet with Charts from Multiple Tabs",{"path":2523,"title":2524,"kind":2501},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fbuild-a-rolling-12-month-chart-on-an-excel-dashboard","Build a Rolling 12-Month Chart on an Excel Dashboard",{"path":2526,"title":2527,"kind":2501},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fkeep-charts-and-images-when-filling-an-excel-template","Keep Charts and Images When Filling an Excel Template",{"path":2529,"title":2530,"kind":2501},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fadd-a-chart-to-an-excel-file-with-xlsxwriter","Add a Chart to an Excel File with xlsxwriter",{"path":2532,"title":2533,"kind":2501},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fadd-sparklines-to-an-excel-report-with-xlsxwriter","Add Sparklines to an Excel Report with xlsxwriter",{"path":2535,"title":2536,"kind":2501},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fbuild-a-waterfall-chart-in-excel-with-python","Build a Waterfall Chart in Excel with Python",1790972108979]