[{"data":1,"prerenderedAt":1846},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-doughnut-chart-with-openpyxl":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-doughnut-chart-with-openpyxl":1812,"ancestors:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-doughnut-chart-with-openpyxl":1821,"children:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-doughnut-chart-with-openpyxl":1826,"related:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-doughnut-chart-with-openpyxl":1827},{"id":4,"title":5,"body":6,"dateModified":1778,"datePublished":1778,"description":1779,"extension":1780,"faq":1781,"keyword":1790,"kind":1791,"meta":1792,"navigation":337,"path":1801,"seo":1802,"slug":1805,"stem":1806,"topics":1807,"type":1810,"__hash__":1811},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-doughnut-chart-with-openpyxl\u002Findex.md","Create a Doughnut Chart with openpyxl",{"type":7,"value":8,"toc":1764},"minimark",[9,19,24,50,60,285,289,296,547,561,565,572,728,733,851,855,858,1094,1106,1110,1113,1222,1229,1233,1236,1429,1437,1521,1525,1528,1610,1613,1617,1625,1629,1677,1681,1699,1703,1709,1715,1721,1727,1731,1760],[10,11,12,13,18],"p",{},"A doughnut chart is a pie chart with the middle removed: it shows how a whole splits into parts — revenue by channel, budget by department, tickets by category — with the ring leaving space that makes the slices easier to compare by arc length. It is a common request for dashboard sheets, and openpyxl builds it natively, as a real Excel chart that updates when the data changes. The defaults, though, give a thin ring with legend-key squares on every label and dark text on dark colours. This page, part of ",[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",", builds the chart and fixes each of those.",[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 openpyxl\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47],"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}," openpyxl\n",[10,51,52,55,56,59],{},[32,53,54],{},"channel_mix.xlsx"," has revenue for four sales channels in 2025 and 2026 on a sheet called ",[32,57,58],{},"Mix",". The charts below are added to that workbook. To look at a chart without Excel, convert the file to PDF with LibreOffice, as the last step shows.",[61,62,70,71,70,75,70,79,70,87,70,97,70,102,70,113,70,119,70,125,70,128,70,134,70,138,70,142,70,146,70,150,70,154,70,159,70,163,70,167,70,169,70,172,70,174,70,177,70,180,70,184,70,186,70,190,70,192,70,195,70,199,70,203,70,206,70,210,70,212,70,215,70,217,70,220,70,222,70,225,70,228,70,232,70,234,70,237,70,239,70,242,70,244,70,247,70,250,70,254,70,256,70,259,70,261,70,264,70,266,70,269,70,274,70,280],"svg",{"viewBox":63,"role":64,"ariaLabelledBy":65,"xmlns":68,"style":69},"225 0 309 250","img",[66,67],"dn-data-t","dn-data-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:309px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[72,73,74],"title",{"id":66},"The data behind the chart",[76,77,78],"desc",{"id":67},"A small table with channels down the side and revenue for 2025 and 2026. The 2026 column feeds the single-ring chart; both columns feed the two-ring chart.",[80,81],"rect",{"x":82,"y":83,"width":84,"height":85,"fill":86},"225","0","309","250","#ffffff",[80,88],{"x":89,"y":90,"width":91,"height":92,"rx":93,"fill":94,"stroke":95,"style":96},"241.8","16.0","276.4","30.0","8","#e7ebef","var(--line,#cdd5e6)","stroke-width:1.5px",[80,98],{"x":89,"y":99,"width":91,"height":100,"rx":83,"fill":94,"stroke":95,"style":101},"38.0","8.0","stroke-width:0px",[103,104,106],"g",{"transform":105},"translate(251.8,23.0) scale(0.667)",[107,108],"path",{"d":109,"fill":110,"stroke":111,"style":112},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","none","var(--teal-ink,#0b6157)","stroke-width:2.70px;stroke-linecap:round;stroke-linejoin:round",[114,115,54],"text",{"x":116,"y":117,"style":118},"273.8","36.0","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:start",[80,120],{"x":89,"y":121,"width":122,"height":123,"rx":83,"fill":94,"stroke":95,"style":124},"46.0","32.0","24.0","stroke-width:1px",[80,126],{"x":116,"y":121,"width":127,"height":123,"rx":83,"fill":94,"stroke":95,"style":124},"88.9",[114,129,133],{"x":130,"y":131,"style":132},"318.2","63.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[80,135],{"x":136,"y":121,"width":137,"height":123,"rx":83,"fill":94,"stroke":95,"style":124},"362.7","77.5",[114,139,141],{"x":140,"y":131,"style":132},"401.4","B",[80,143],{"x":144,"y":121,"width":145,"height":123,"rx":83,"fill":94,"stroke":95,"style":124},"440.2","78.0",[114,147,149],{"x":148,"y":131,"style":132},"479.2","C",[80,151],{"x":89,"y":152,"width":122,"height":153,"rx":83,"fill":94,"stroke":95,"style":124},"70.0","28.0",[114,155,158],{"x":156,"y":157,"style":132},"257.8","89.0","1",[80,160],{"x":116,"y":152,"width":127,"height":153,"rx":83,"fill":161,"stroke":162,"style":96},"#f0f4ff","var(--brand,#5b5cf0)",[114,164,166],{"x":130,"y":157,"style":165},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Channel",[80,168],{"x":136,"y":152,"width":137,"height":153,"rx":83,"fill":161,"stroke":162,"style":96},[114,170,171],{"x":140,"y":157,"style":165},"2025",[80,173],{"x":144,"y":152,"width":145,"height":153,"rx":83,"fill":161,"stroke":162,"style":96},[114,175,176],{"x":148,"y":157,"style":165},"2026",[80,178],{"x":89,"y":179,"width":122,"height":153,"rx":83,"fill":94,"stroke":95,"style":124},"98.0",[114,181,183],{"x":156,"y":182,"style":132},"117.0","2",[80,185],{"x":116,"y":179,"width":127,"height":153,"rx":83,"fill":86,"stroke":95,"style":124},[114,187,189],{"x":130,"y":182,"style":188},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Online",[80,191],{"x":136,"y":179,"width":137,"height":153,"rx":83,"fill":86,"stroke":95,"style":124},[114,193,194],{"x":140,"y":182,"style":188},"355,000",[80,196],{"x":144,"y":179,"width":145,"height":153,"rx":83,"fill":197,"stroke":198,"style":96},"#d9f4f1","var(--teal,#0f9488)",[114,200,202],{"x":148,"y":182,"style":201},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","412,000",[80,204],{"x":89,"y":205,"width":122,"height":153,"rx":83,"fill":94,"stroke":95,"style":124},"126.0",[114,207,209],{"x":156,"y":208,"style":132},"145.0","3",[80,211],{"x":116,"y":205,"width":127,"height":153,"rx":83,"fill":86,"stroke":95,"style":124},[114,213,214],{"x":130,"y":208,"style":188},"Retail",[80,216],{"x":136,"y":205,"width":137,"height":153,"rx":83,"fill":86,"stroke":95,"style":124},[114,218,219],{"x":140,"y":208,"style":188},"291,000",[80,221],{"x":144,"y":205,"width":145,"height":153,"rx":83,"fill":197,"stroke":198,"style":96},[114,223,224],{"x":148,"y":208,"style":201},"268,000",[80,226],{"x":89,"y":227,"width":122,"height":153,"rx":83,"fill":94,"stroke":95,"style":124},"154.0",[114,229,231],{"x":156,"y":230,"style":132},"173.0","4",[80,233],{"x":116,"y":227,"width":127,"height":153,"rx":83,"fill":86,"stroke":95,"style":124},[114,235,236],{"x":130,"y":230,"style":188},"Wholesale",[80,238],{"x":136,"y":227,"width":137,"height":153,"rx":83,"fill":86,"stroke":95,"style":124},[114,240,241],{"x":140,"y":230,"style":188},"160,000",[80,243],{"x":144,"y":227,"width":145,"height":153,"rx":83,"fill":197,"stroke":198,"style":96},[114,245,246],{"x":148,"y":230,"style":201},"149,000",[80,248],{"x":89,"y":249,"width":122,"height":153,"rx":83,"fill":94,"stroke":95,"style":124},"182.0",[114,251,253],{"x":156,"y":252,"style":132},"201.0","5",[80,255],{"x":116,"y":249,"width":127,"height":153,"rx":83,"fill":86,"stroke":95,"style":124},[114,257,258],{"x":130,"y":252,"style":188},"Partners",[80,260],{"x":136,"y":249,"width":137,"height":153,"rx":83,"fill":86,"stroke":95,"style":124},[114,262,263],{"x":140,"y":252,"style":188},"52,000",[80,265],{"x":144,"y":249,"width":145,"height":153,"rx":83,"fill":197,"stroke":198,"style":96},[114,267,268],{"x":148,"y":252,"style":201},"71,000",[80,270],{"x":271,"y":272,"width":273,"height":123,"rx":83,"fill":86,"stroke":95,"style":124},"249.8","210.0","45.5",[37,275],{"x1":276,"y1":277,"x2":278,"y2":277,"stroke":198,"style":279},"251.8","233.0","293.3","stroke-width:3px",[114,281,58],{"x":282,"y":283,"style":284},"272.6","226.5","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[20,286,288],{"id":287},"step-1-a-basic-doughnut","Step 1 — A basic doughnut",[10,290,291,292,295],{},"Create a ",[32,293,294],{},"DoughnutChart",", add the values with their header as the series title, and the channel names as categories:",[25,297,301],{"className":298,"code":299,"language":300,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import load_workbook\nfrom openpyxl.chart import DoughnutChart, Reference\n\nwb = load_workbook(\"channel_mix.xlsx\")\nws = wb[\"Mix\"]\nchart = DoughnutChart()\nchart.title = \"Revenue by channel, 2026\"\ndata = Reference(ws, min_col=3, min_row=1, max_row=5)\nchart.add_data(data, titles_from_data=True)\nchart.set_categories(Reference(ws, min_col=1, min_row=2, max_row=5))\nchart.holeSize = 55\nchart.width, chart.height = 13, 9\nws.add_chart(chart, \"A8\")\nwb.save(\"channel_mix.xlsx\")\nprint(chart.holeSize, len(chart.series))\n","python",[32,302,303,319,332,339,357,374,385,396,437,453,484,495,511,522,532],{"__ignoreMap":30},[35,304,305,309,313,316],{"class":37,"line":38},[35,306,308],{"class":307},"s-kum","from",[35,310,312],{"class":311},"skGVy"," openpyxl ",[35,314,315],{"class":307},"import",[35,317,318],{"class":311}," load_workbook\n",[35,320,322,324,327,329],{"class":37,"line":321},2,[35,323,308],{"class":307},[35,325,326],{"class":311}," openpyxl.chart ",[35,328,315],{"class":307},[35,330,331],{"class":311}," DoughnutChart, Reference\n",[35,333,335],{"class":37,"line":334},3,[35,336,338],{"emptyLinePlaceholder":337},true,"\n",[35,340,342,345,348,351,354],{"class":37,"line":341},4,[35,343,344],{"class":311},"wb ",[35,346,347],{"class":307},"=",[35,349,350],{"class":311}," load_workbook(",[35,352,353],{"class":45},"\"channel_mix.xlsx\"",[35,355,356],{"class":311},")\n",[35,358,360,363,365,368,371],{"class":37,"line":359},5,[35,361,362],{"class":311},"ws ",[35,364,347],{"class":307},[35,366,367],{"class":311}," wb[",[35,369,370],{"class":45},"\"Mix\"",[35,372,373],{"class":311},"]\n",[35,375,377,380,382],{"class":37,"line":376},6,[35,378,379],{"class":311},"chart ",[35,381,347],{"class":307},[35,383,384],{"class":311}," DoughnutChart()\n",[35,386,388,391,393],{"class":37,"line":387},7,[35,389,390],{"class":311},"chart.title ",[35,392,347],{"class":307},[35,394,395],{"class":45}," \"Revenue by channel, 2026\"\n",[35,397,399,402,404,407,411,413,416,419,422,424,426,428,431,433,435],{"class":37,"line":398},8,[35,400,401],{"class":311},"data ",[35,403,347],{"class":307},[35,405,406],{"class":311}," Reference(ws, ",[35,408,410],{"class":409},"sa561","min_col",[35,412,347],{"class":307},[35,414,209],{"class":415},"sP0c6",[35,417,418],{"class":311},", ",[35,420,421],{"class":409},"min_row",[35,423,347],{"class":307},[35,425,158],{"class":415},[35,427,418],{"class":311},[35,429,430],{"class":409},"max_row",[35,432,347],{"class":307},[35,434,253],{"class":415},[35,436,356],{"class":311},[35,438,440,443,446,448,451],{"class":37,"line":439},9,[35,441,442],{"class":311},"chart.add_data(data, ",[35,444,445],{"class":409},"titles_from_data",[35,447,347],{"class":307},[35,449,450],{"class":415},"True",[35,452,356],{"class":311},[35,454,456,459,461,463,465,467,469,471,473,475,477,479,481],{"class":37,"line":455},10,[35,457,458],{"class":311},"chart.set_categories(Reference(ws, ",[35,460,410],{"class":409},[35,462,347],{"class":307},[35,464,158],{"class":415},[35,466,418],{"class":311},[35,468,421],{"class":409},[35,470,347],{"class":307},[35,472,183],{"class":415},[35,474,418],{"class":311},[35,476,430],{"class":409},[35,478,347],{"class":307},[35,480,253],{"class":415},[35,482,483],{"class":311},"))\n",[35,485,487,490,492],{"class":37,"line":486},11,[35,488,489],{"class":311},"chart.holeSize ",[35,491,347],{"class":307},[35,493,494],{"class":415}," 55\n",[35,496,498,501,503,506,508],{"class":37,"line":497},12,[35,499,500],{"class":311},"chart.width, chart.height ",[35,502,347],{"class":307},[35,504,505],{"class":415}," 13",[35,507,418],{"class":311},[35,509,510],{"class":415},"9\n",[35,512,514,517,520],{"class":37,"line":513},13,[35,515,516],{"class":311},"ws.add_chart(chart, ",[35,518,519],{"class":45},"\"A8\"",[35,521,356],{"class":311},[35,523,525,528,530],{"class":37,"line":524},14,[35,526,527],{"class":311},"wb.save(",[35,529,353],{"class":45},[35,531,356],{"class":311},[35,533,535,538,541,544],{"class":37,"line":534},15,[35,536,537],{"class":415},"print",[35,539,540],{"class":311},"(chart.holeSize, ",[35,542,543],{"class":415},"len",[35,545,546],{"class":311},"(chart.series))\n",[10,548,549,552,553,556,557,560],{},[32,550,551],{},"holeSize"," is the hole's diameter as a percentage of the chart — Excel accepts 10 to 90, and the default of 50 gives a ring that is thinner than most people expect; 55–65 reads well on dashboards. ",[32,554,555],{},"width"," and ",[32,558,559],{},"height"," are in centimetres. The chart references the cells rather than copying the numbers, so changing a value on the sheet changes the slice when the workbook is opened in Excel.",[20,562,564],{"id":563},"step-2-colour-each-slice","Step 2 — Colour each slice",[10,566,567,568,571],{},"By default Excel uses its theme colours. To match a report's palette — or to keep \"Online\" the same colour in every chart — set each slice's fill with a ",[32,569,570],{},"DataPoint",":",[25,573,575],{"className":298,"code":574,"language":300,"meta":30,"style":30},"from openpyxl.chart.series import DataPoint\n\nPALETTE = [\"4338CA\", \"0F9488\", \"B4740A\", \"BE185D\"]\n\ndef colour_slices(series, colours):\n    series.dPt = []\n    for i, colour in enumerate(colours):\n        point = DataPoint(idx=i)\n        point.graphicalProperties.solidFill = colour\n        point.graphicalProperties.line.solidFill = \"FFFFFF\"\n        series.dPt.append(point)\n\ncolour_slices(chart.series[0], PALETTE)\n",[32,576,577,589,593,624,628,640,650,667,685,695,705,710,714],{"__ignoreMap":30},[35,578,579,581,584,586],{"class":37,"line":38},[35,580,308],{"class":307},[35,582,583],{"class":311}," openpyxl.chart.series ",[35,585,315],{"class":307},[35,587,588],{"class":311}," DataPoint\n",[35,590,591],{"class":37,"line":321},[35,592,338],{"emptyLinePlaceholder":337},[35,594,595,598,601,604,607,609,612,614,617,619,622],{"class":37,"line":334},[35,596,597],{"class":415},"PALETTE",[35,599,600],{"class":307}," =",[35,602,603],{"class":311}," [",[35,605,606],{"class":45},"\"4338CA\"",[35,608,418],{"class":311},[35,610,611],{"class":45},"\"0F9488\"",[35,613,418],{"class":311},[35,615,616],{"class":45},"\"B4740A\"",[35,618,418],{"class":311},[35,620,621],{"class":45},"\"BE185D\"",[35,623,373],{"class":311},[35,625,626],{"class":37,"line":341},[35,627,338],{"emptyLinePlaceholder":337},[35,629,630,633,637],{"class":37,"line":359},[35,631,632],{"class":307},"def",[35,634,636],{"class":635},"s_Opv"," colour_slices",[35,638,639],{"class":311},"(series, colours):\n",[35,641,642,645,647],{"class":37,"line":376},[35,643,644],{"class":311},"    series.dPt ",[35,646,347],{"class":307},[35,648,649],{"class":311}," []\n",[35,651,652,655,658,661,664],{"class":37,"line":387},[35,653,654],{"class":307},"    for",[35,656,657],{"class":311}," i, colour ",[35,659,660],{"class":307},"in",[35,662,663],{"class":415}," enumerate",[35,665,666],{"class":311},"(colours):\n",[35,668,669,672,674,677,680,682],{"class":37,"line":398},[35,670,671],{"class":311},"        point ",[35,673,347],{"class":307},[35,675,676],{"class":311}," DataPoint(",[35,678,679],{"class":409},"idx",[35,681,347],{"class":307},[35,683,684],{"class":311},"i)\n",[35,686,687,690,692],{"class":37,"line":439},[35,688,689],{"class":311},"        point.graphicalProperties.solidFill ",[35,691,347],{"class":307},[35,693,694],{"class":311}," colour\n",[35,696,697,700,702],{"class":37,"line":455},[35,698,699],{"class":311},"        point.graphicalProperties.line.solidFill ",[35,701,347],{"class":307},[35,703,704],{"class":45}," \"FFFFFF\"\n",[35,706,707],{"class":37,"line":486},[35,708,709],{"class":311},"        series.dPt.append(point)\n",[35,711,712],{"class":37,"line":497},[35,713,338],{"emptyLinePlaceholder":337},[35,715,716,719,721,724,726],{"class":37,"line":513},[35,717,718],{"class":311},"colour_slices(chart.series[",[35,720,83],{"class":415},[35,722,723],{"class":311},"], ",[35,725,597],{"class":415},[35,727,356],{"class":311},[10,729,730,732],{},[32,731,679],{}," is the position of the slice in the data, starting at 0. The white outline separates adjacent slices cleanly. Keep a fixed mapping from category to colour across every chart in a report, ideally in one dictionary shared by all the chart code; a reader who learns that Online is indigo on one chart will read the next one wrong if it is teal there.",[61,734,70,740,70,743,70,746,70,750,70,759,70,767,70,772,70,776,70,782,70,790,70,794,70,799,70,803,70,808,70,811,70,817,70,822,70,826,70,830,70,835,70,838,70,843,70,846],{"viewBox":735,"role":64,"ariaLabelledBy":736,"xmlns":68,"style":739},"4 0 752 207",[737,738],"dn-defaults-t","dn-defaults-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[72,741,742],{"id":737},"Default labels and fixed labels",[76,744,745],{"id":738},"Two panels comparing the default percentage labels, with legend-key squares and dark text on dark slices, and the fixed labels, with the squares removed and white bold text.",[80,747],{"x":231,"y":83,"width":748,"height":749,"fill":86},"752","207",[80,751],{"x":752,"y":90,"width":753,"height":754,"rx":755,"fill":756,"stroke":757,"style":758},"20.0","351.0","175.0","14","#fdefd8","var(--gold,#b4740a)","stroke-width:2px",[103,760,762],{"transform":761},"translate(182.5,29.0) scale(1.083)",[107,763],{"d":764,"fill":110,"stroke":765,"style":766},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[114,768,771],{"x":769,"y":145,"style":770},"195.5","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Defaults",[37,773],{"x1":117,"y1":774,"x2":775,"y2":774,"stroke":757,"style":124},"91.0","355.0",[114,777,781],{"x":778,"y":779,"style":780},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","small legend-key square on every label",[103,783,785],{"transform":784},"translate(38.5,99.5) scale(0.625)",[107,786],{"d":787,"fill":110,"stroke":788,"style":789},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[114,791,793],{"x":778,"y":792,"style":780},"137.0","dark text on indigo and teal slices",[103,795,797],{"transform":796},"translate(38.5,125.5) scale(0.625)",[107,798],{"d":787,"fill":110,"stroke":788,"style":789},[114,800,802],{"x":778,"y":801,"style":780},"163.0","value and category shown together",[103,804,806],{"transform":805},"translate(38.5,151.5) scale(0.625)",[107,807],{"d":764,"fill":110,"stroke":757,"style":789},[80,809],{"x":810,"y":90,"width":753,"height":754,"rx":755,"fill":197,"stroke":198,"style":758},"389.0",[103,812,814],{"transform":813},"translate(551.5,29.0) scale(1.083)",[107,815],{"d":816,"fill":110,"stroke":111,"style":766},"M5 12.5l4.5 4.5L19 7",[114,818,821],{"x":819,"y":145,"style":820},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","After Step 3",[37,823],{"x1":824,"y1":774,"x2":825,"y2":774,"stroke":198,"style":124},"405.0","724.0",[114,827,829],{"x":828,"y":779,"style":780},"429.0","percentage only",[103,831,833],{"transform":832},"translate(407.5,99.5) scale(0.625)",[107,834],{"d":816,"fill":110,"stroke":198,"style":789},[114,836,837],{"x":828,"y":792,"style":780},"white bold text on every slice",[103,839,841],{"transform":840},"translate(407.5,125.5) scale(0.625)",[107,842],{"d":816,"fill":110,"stroke":198,"style":789},[114,844,845],{"x":828,"y":801,"style":780},"categories left to the legend",[103,847,849],{"transform":848},"translate(407.5,151.5) scale(0.625)",[107,850],{"d":816,"fill":110,"stroke":198,"style":789},[20,852,854],{"id":853},"step-3-percentage-labels-that-can-be-read","Step 3 — Percentage labels that can be read",[10,856,857],{},"Show each slice's share as a percentage, without the legend-key square Excel adds by default, in white bold text so it stays legible on dark colours:",[25,859,861],{"className":298,"code":860,"language":300,"meta":30,"style":30},"from openpyxl.chart.label import DataLabelList\nfrom openpyxl.chart.text import RichText\nfrom openpyxl.drawing.text import CharacterProperties, Paragraph, ParagraphProperties\n\ndef percent_labels(chart, colour=\"FFFFFF\", size=1100):\n    labels = DataLabelList()\n    labels.showPercent = True\n    for flag in (\"showVal\", \"showCatName\", \"showSerName\", \"showLegendKey\", \"showLeaderLines\"):\n        setattr(labels, flag, False)\n    font = CharacterProperties(sz=size, b=True, solidFill=colour)\n    labels.txPr = RichText(p=[Paragraph(pPr=ParagraphProperties(defRPr=font), endParaRPr=font)])\n    chart.dataLabels = labels\n\npercent_labels(chart)\n",[32,862,863,875,887,899,903,929,939,949,986,999,1034,1075,1085,1089],{"__ignoreMap":30},[35,864,865,867,870,872],{"class":37,"line":38},[35,866,308],{"class":307},[35,868,869],{"class":311}," openpyxl.chart.label ",[35,871,315],{"class":307},[35,873,874],{"class":311}," DataLabelList\n",[35,876,877,879,882,884],{"class":37,"line":321},[35,878,308],{"class":307},[35,880,881],{"class":311}," openpyxl.chart.text ",[35,883,315],{"class":307},[35,885,886],{"class":311}," RichText\n",[35,888,889,891,894,896],{"class":37,"line":334},[35,890,308],{"class":307},[35,892,893],{"class":311}," openpyxl.drawing.text ",[35,895,315],{"class":307},[35,897,898],{"class":311}," CharacterProperties, Paragraph, ParagraphProperties\n",[35,900,901],{"class":37,"line":341},[35,902,338],{"emptyLinePlaceholder":337},[35,904,905,907,910,913,915,918,921,923,926],{"class":37,"line":359},[35,906,632],{"class":307},[35,908,909],{"class":635}," percent_labels",[35,911,912],{"class":311},"(chart, colour",[35,914,347],{"class":307},[35,916,917],{"class":45},"\"FFFFFF\"",[35,919,920],{"class":311},", size",[35,922,347],{"class":307},[35,924,925],{"class":415},"1100",[35,927,928],{"class":311},"):\n",[35,930,931,934,936],{"class":37,"line":376},[35,932,933],{"class":311},"    labels ",[35,935,347],{"class":307},[35,937,938],{"class":311}," DataLabelList()\n",[35,940,941,944,946],{"class":37,"line":387},[35,942,943],{"class":311},"    labels.showPercent ",[35,945,347],{"class":307},[35,947,948],{"class":415}," True\n",[35,950,951,953,956,958,961,964,966,969,971,974,976,979,981,984],{"class":37,"line":398},[35,952,654],{"class":307},[35,954,955],{"class":311}," flag ",[35,957,660],{"class":307},[35,959,960],{"class":311}," (",[35,962,963],{"class":45},"\"showVal\"",[35,965,418],{"class":311},[35,967,968],{"class":45},"\"showCatName\"",[35,970,418],{"class":311},[35,972,973],{"class":45},"\"showSerName\"",[35,975,418],{"class":311},[35,977,978],{"class":45},"\"showLegendKey\"",[35,980,418],{"class":311},[35,982,983],{"class":45},"\"showLeaderLines\"",[35,985,928],{"class":311},[35,987,988,991,994,997],{"class":37,"line":439},[35,989,990],{"class":415},"        setattr",[35,992,993],{"class":311},"(labels, flag, ",[35,995,996],{"class":415},"False",[35,998,356],{"class":311},[35,1000,1001,1004,1006,1009,1012,1014,1017,1020,1022,1024,1026,1029,1031],{"class":37,"line":455},[35,1002,1003],{"class":311},"    font ",[35,1005,347],{"class":307},[35,1007,1008],{"class":311}," CharacterProperties(",[35,1010,1011],{"class":409},"sz",[35,1013,347],{"class":307},[35,1015,1016],{"class":311},"size, ",[35,1018,1019],{"class":409},"b",[35,1021,347],{"class":307},[35,1023,450],{"class":415},[35,1025,418],{"class":311},[35,1027,1028],{"class":409},"solidFill",[35,1030,347],{"class":307},[35,1032,1033],{"class":311},"colour)\n",[35,1035,1036,1039,1041,1044,1046,1048,1051,1054,1056,1059,1062,1064,1067,1070,1072],{"class":37,"line":486},[35,1037,1038],{"class":311},"    labels.txPr ",[35,1040,347],{"class":307},[35,1042,1043],{"class":311}," RichText(",[35,1045,10],{"class":409},[35,1047,347],{"class":307},[35,1049,1050],{"class":311},"[Paragraph(",[35,1052,1053],{"class":409},"pPr",[35,1055,347],{"class":307},[35,1057,1058],{"class":311},"ParagraphProperties(",[35,1060,1061],{"class":409},"defRPr",[35,1063,347],{"class":307},[35,1065,1066],{"class":311},"font), ",[35,1068,1069],{"class":409},"endParaRPr",[35,1071,347],{"class":307},[35,1073,1074],{"class":311},"font)])\n",[35,1076,1077,1080,1082],{"class":37,"line":497},[35,1078,1079],{"class":311},"    chart.dataLabels ",[35,1081,347],{"class":307},[35,1083,1084],{"class":311}," labels\n",[35,1086,1087],{"class":37,"line":513},[35,1088,338],{"emptyLinePlaceholder":337},[35,1090,1091],{"class":37,"line":524},[35,1092,1093],{"class":311},"percent_labels(chart)\n",[10,1095,1096,1097,1100,1101,1105],{},"Excel works out the percentages from the values, so they always add up to 100% of what is plotted. Each ",[32,1098,1099],{},"show..."," flag controls one part of the label; turning off everything but the percentage keeps the ring uncluttered, with the category names in the legend. The font size is in hundredths of a point — 1100 is 11pt. ",[14,1102,1104],{"href":1103},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fstyle-chart-colors-and-data-labels-with-openpyxl\u002F","Style Chart Colours and Data Labels with openpyxl"," covers label options in general.",[20,1107,1109],{"id":1108},"step-4-put-the-total-in-the-title","Step 4 — Put the total in the title",[10,1111,1112],{},"A doughnut's hole is the obvious place for the total, but Excel charts have no built-in text in the hole, and a floating text box drawn by code does not move with the chart. The robust alternative is to compute the total and put it in the title:",[25,1114,1116],{"className":298,"code":1115,"language":300,"meta":30,"style":30},"total = sum(ws.cell(r, 3).value for r in range(2, 6))\nchart.title = f\"Revenue by channel, 2026 — total {total:,.0f}\"\nchart.firstSliceAng = 0\nwb.save(\"channel_mix.xlsx\")\nprint(chart.title.tx.rich.p[0].r[0].t)\n",[32,1117,1118,1159,1187,1197,1205],{"__ignoreMap":30},[35,1119,1120,1123,1125,1128,1131,1133,1136,1139,1142,1144,1147,1150,1152,1154,1157],{"class":37,"line":38},[35,1121,1122],{"class":311},"total ",[35,1124,347],{"class":307},[35,1126,1127],{"class":415}," sum",[35,1129,1130],{"class":311},"(ws.cell(r, ",[35,1132,209],{"class":415},[35,1134,1135],{"class":311},").value ",[35,1137,1138],{"class":307},"for",[35,1140,1141],{"class":311}," r ",[35,1143,660],{"class":307},[35,1145,1146],{"class":415}," range",[35,1148,1149],{"class":311},"(",[35,1151,183],{"class":415},[35,1153,418],{"class":311},[35,1155,1156],{"class":415},"6",[35,1158,483],{"class":311},[35,1160,1161,1163,1165,1168,1171,1175,1178,1181,1184],{"class":37,"line":321},[35,1162,390],{"class":311},[35,1164,347],{"class":307},[35,1166,1167],{"class":307}," f",[35,1169,1170],{"class":45},"\"Revenue by channel, 2026 — total ",[35,1172,1174],{"class":1173},"sSjpA","{",[35,1176,1177],{"class":311},"total",[35,1179,1180],{"class":307},":,.0f",[35,1182,1183],{"class":1173},"}",[35,1185,1186],{"class":45},"\"\n",[35,1188,1189,1192,1194],{"class":37,"line":334},[35,1190,1191],{"class":311},"chart.firstSliceAng ",[35,1193,347],{"class":307},[35,1195,1196],{"class":415}," 0\n",[35,1198,1199,1201,1203],{"class":37,"line":341},[35,1200,527],{"class":311},[35,1202,353],{"class":45},[35,1204,356],{"class":311},[35,1206,1207,1209,1212,1214,1217,1219],{"class":37,"line":359},[35,1208,537],{"class":415},[35,1210,1211],{"class":311},"(chart.title.tx.rich.p[",[35,1213,83],{"class":415},[35,1215,1216],{"class":311},"].r[",[35,1218,83],{"class":415},[35,1220,1221],{"class":311},"].t)\n",[10,1223,1224,1225,1228],{},"Because the title is text, it does not update when the data changes; regenerate the workbook when the numbers change, which is what a scheduled report does anyway. ",[32,1226,1227],{},"firstSliceAng"," rotates the ring — 0 starts the first slice at 12 o'clock, which is where readers look first, so put the category you want noticed first in the data.",[20,1230,1232],{"id":1231},"step-5-two-rings-to-compare-two-periods","Step 5 — Two rings to compare two periods",[10,1234,1235],{},"Adding a second series draws a second ring. With 2025 on the inside and 2026 outside, a reader can see Partners growing and Retail shrinking:",[25,1237,1239],{"className":298,"code":1238,"language":300,"meta":30,"style":30},"two = DoughnutChart()\ntwo.title = \"Channel mix: 2025 inside, 2026 outside\"\ntwo.add_data(Reference(ws, min_col=2, max_col=3, min_row=1, max_row=5), titles_from_data=True)\ntwo.set_categories(Reference(ws, min_col=1, min_row=2, max_row=5))\ntwo.holeSize = 40\nfor s in two.series:\n    colour_slices(s, PALETTE)\npercent_labels(two, size=900)\ntwo.width, two.height = 13, 9\nws.add_chart(two, \"A27\")\nwb.save(\"channel_mix.xlsx\")\nprint([s.tx.strRef.f for s in two.series])\n",[32,1240,1241,1250,1260,1307,1336,1346,1358,1367,1382,1395,1405,1413],{"__ignoreMap":30},[35,1242,1243,1246,1248],{"class":37,"line":38},[35,1244,1245],{"class":311},"two ",[35,1247,347],{"class":307},[35,1249,384],{"class":311},[35,1251,1252,1255,1257],{"class":37,"line":321},[35,1253,1254],{"class":311},"two.title ",[35,1256,347],{"class":307},[35,1258,1259],{"class":45}," \"Channel mix: 2025 inside, 2026 outside\"\n",[35,1261,1262,1265,1267,1269,1271,1273,1276,1278,1280,1282,1284,1286,1288,1290,1292,1294,1296,1299,1301,1303,1305],{"class":37,"line":334},[35,1263,1264],{"class":311},"two.add_data(Reference(ws, ",[35,1266,410],{"class":409},[35,1268,347],{"class":307},[35,1270,183],{"class":415},[35,1272,418],{"class":311},[35,1274,1275],{"class":409},"max_col",[35,1277,347],{"class":307},[35,1279,209],{"class":415},[35,1281,418],{"class":311},[35,1283,421],{"class":409},[35,1285,347],{"class":307},[35,1287,158],{"class":415},[35,1289,418],{"class":311},[35,1291,430],{"class":409},[35,1293,347],{"class":307},[35,1295,253],{"class":415},[35,1297,1298],{"class":311},"), ",[35,1300,445],{"class":409},[35,1302,347],{"class":307},[35,1304,450],{"class":415},[35,1306,356],{"class":311},[35,1308,1309,1312,1314,1316,1318,1320,1322,1324,1326,1328,1330,1332,1334],{"class":37,"line":341},[35,1310,1311],{"class":311},"two.set_categories(Reference(ws, ",[35,1313,410],{"class":409},[35,1315,347],{"class":307},[35,1317,158],{"class":415},[35,1319,418],{"class":311},[35,1321,421],{"class":409},[35,1323,347],{"class":307},[35,1325,183],{"class":415},[35,1327,418],{"class":311},[35,1329,430],{"class":409},[35,1331,347],{"class":307},[35,1333,253],{"class":415},[35,1335,483],{"class":311},[35,1337,1338,1341,1343],{"class":37,"line":359},[35,1339,1340],{"class":311},"two.holeSize ",[35,1342,347],{"class":307},[35,1344,1345],{"class":415}," 40\n",[35,1347,1348,1350,1353,1355],{"class":37,"line":376},[35,1349,1138],{"class":307},[35,1351,1352],{"class":311}," s ",[35,1354,660],{"class":307},[35,1356,1357],{"class":311}," two.series:\n",[35,1359,1360,1363,1365],{"class":37,"line":387},[35,1361,1362],{"class":311},"    colour_slices(s, ",[35,1364,597],{"class":415},[35,1366,356],{"class":311},[35,1368,1369,1372,1375,1377,1380],{"class":37,"line":398},[35,1370,1371],{"class":311},"percent_labels(two, ",[35,1373,1374],{"class":409},"size",[35,1376,347],{"class":307},[35,1378,1379],{"class":415},"900",[35,1381,356],{"class":311},[35,1383,1384,1387,1389,1391,1393],{"class":37,"line":439},[35,1385,1386],{"class":311},"two.width, two.height ",[35,1388,347],{"class":307},[35,1390,505],{"class":415},[35,1392,418],{"class":311},[35,1394,510],{"class":415},[35,1396,1397,1400,1403],{"class":37,"line":455},[35,1398,1399],{"class":311},"ws.add_chart(two, ",[35,1401,1402],{"class":45},"\"A27\"",[35,1404,356],{"class":311},[35,1406,1407,1409,1411],{"class":37,"line":486},[35,1408,527],{"class":311},[35,1410,353],{"class":45},[35,1412,356],{"class":311},[35,1414,1415,1417,1420,1422,1424,1426],{"class":37,"line":497},[35,1416,537],{"class":415},[35,1418,1419],{"class":311},"([s.tx.strRef.f ",[35,1421,1138],{"class":307},[35,1423,1352],{"class":311},[35,1425,660],{"class":307},[35,1427,1428],{"class":311}," two.series])\n",[10,1430,1431,1432,1436],{},"The first series becomes the inner ring. In the rendered chart, Online's share rises from 41% to 46% and Partners' from 6% to 8%, while Retail falls from 34% to 30% — the labels on the two rings sit next to each other, so the change reads without any arithmetic. Two rings are the most a doughnut can carry before it turns into a puzzle; for more periods, a stacked 100% column chart — ",[14,1433,1435],{"href":1434},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-stacked-column-chart-with-openpyxl\u002F","Create a Stacked Column Chart with openpyxl"," — shows the same shares side by side and is easier to compare.",[61,1438,70,1444,70,1447,70,1450,70,1454,70,1458,70,1463,70,1467,70,1472,70,1475,70,1478,70,1481,70,1486,70,1489,70,1492,70,1495,70,1500,70,1503,70,1506,70,1510,70,1515],{"viewBox":1439,"role":64,"ariaLabelledBy":1440,"xmlns":68,"style":1443},"4 0 744 206",[1441,1442],"dn-shares-t","dn-shares-d","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[72,1445,1446],{"id":1441},"The shares the chart shows, 2026",[76,1448,1449],{"id":1442},"Horizontal bars of each channel's share of 2026 revenue — Online 46 percent, Retail 30 percent, Wholesale 17 percent, Partners 8 percent — the same numbers the doughnut labels display.",[80,1451],{"x":231,"y":83,"width":1452,"height":1453,"fill":86},"744","206",[80,1455],{"x":752,"y":90,"width":1456,"height":153,"rx":1457,"fill":94,"stroke":95,"style":124},"95.6","7",[114,1459,189],{"x":1460,"y":1461,"style":1462},"67.8","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[80,1464],{"x":1465,"y":90,"width":1466,"height":153,"rx":1457,"fill":161,"stroke":162,"style":96},"125.6","568.9",[114,1468,1471],{"x":1469,"y":1461,"style":1470},"702.5","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start","46%",[80,1473],{"x":752,"y":1474,"width":1456,"height":153,"rx":1457,"fill":94,"stroke":95,"style":124},"56.0",[114,1476,214],{"x":1460,"y":1477,"style":1462},"74.5",[80,1479],{"x":1465,"y":1474,"width":1480,"height":153,"rx":1457,"fill":197,"stroke":198,"style":96},"370.2",[114,1482,1485],{"x":1483,"y":1477,"style":1484},"503.8","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","30%",[80,1487],{"x":752,"y":1488,"width":1456,"height":153,"rx":1457,"fill":94,"stroke":95,"style":124},"96.0",[114,1490,236],{"x":1460,"y":1491,"style":1462},"114.5",[80,1493],{"x":1465,"y":1488,"width":1494,"height":153,"rx":1457,"fill":756,"stroke":757,"style":96},"206.2",[114,1496,1499],{"x":1497,"y":1491,"style":1498},"339.8","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","17%",[80,1501],{"x":752,"y":1502,"width":1456,"height":153,"rx":1457,"fill":94,"stroke":95,"style":124},"136.0",[114,1504,258],{"x":1460,"y":1505,"style":1462},"154.5",[80,1507],{"x":1465,"y":1502,"width":1508,"height":153,"rx":1457,"fill":1509,"stroke":788,"style":96},"98.1","#fee8f2",[114,1511,1514],{"x":1512,"y":1505,"style":1513},"231.7","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","8%",[114,1516,1520],{"x":1517,"y":1518,"style":1519},"380.0","186.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","Shares of a whole are often clearer as bars; the doughnut earns its place on a dashboard with few categories.",[20,1522,1524],{"id":1523},"step-6-check-the-chart-without-excel","Step 6 — Check the chart without Excel",[10,1526,1527],{},"Charts written by openpyxl are only XML until an application draws them. Render the workbook to PDF with LibreOffice to check colours, labels and placement:",[25,1529,1531],{"className":27,"code":1530,"language":29,"meta":30,"style":30},"soffice -env:UserInstallation=file:\u002F\u002F$PWD\u002Flo_profile --headless --convert-to pdf channel_mix.xlsx > \u002Fdev\u002Fnull 2>&1\npdftoppm -png -r 60 -f 1 -l 1 channel_mix.pdf preview && ls preview*\n",[32,1532,1533,1568],{"__ignoreMap":30},[35,1534,1535,1538,1541,1544,1547,1550,1553,1556,1559,1562,1565],{"class":37,"line":38},[35,1536,1537],{"class":41},"soffice",[35,1539,1540],{"class":415}," -env:UserInstallation=file:\u002F\u002F",[35,1542,1543],{"class":311},"$PWD",[35,1545,1546],{"class":415},"\u002Flo_profile",[35,1548,1549],{"class":415}," --headless",[35,1551,1552],{"class":415}," --convert-to",[35,1554,1555],{"class":45}," pdf",[35,1557,1558],{"class":45}," channel_mix.xlsx",[35,1560,1561],{"class":307}," >",[35,1563,1564],{"class":45}," \u002Fdev\u002Fnull",[35,1566,1567],{"class":307}," 2>&1\n",[35,1569,1570,1573,1576,1579,1582,1585,1588,1591,1593,1596,1599,1602,1605,1607],{"class":37,"line":321},[35,1571,1572],{"class":41},"pdftoppm",[35,1574,1575],{"class":415}," -png",[35,1577,1578],{"class":415}," -r",[35,1580,1581],{"class":415}," 60",[35,1583,1584],{"class":415}," -f",[35,1586,1587],{"class":415}," 1",[35,1589,1590],{"class":415}," -l",[35,1592,1587],{"class":415},[35,1594,1595],{"class":45}," channel_mix.pdf",[35,1597,1598],{"class":45}," preview",[35,1600,1601],{"class":311}," && ",[35,1603,1604],{"class":41},"ls",[35,1606,1598],{"class":45},[35,1608,1609],{"class":415},"*\n",[10,1611,1612],{},"The rendered page shows both charts with the four brand colours, white percentages on each slice and the legend on the right. LibreOffice's rendering is close to Excel's for doughnut charts; label positions can differ by a few pixels. Where the chart is anchored matters for printing: a chart anchored at column E with a width of 13 cm runs past the edge of a portrait page and gets cut off, legend first. Anchoring below the data, as here, or setting the sheet to landscape and fit-to-width keeps the whole chart on the page — which is also how the PDF check catches a layout problem before a reader does.",[20,1614,1616],{"id":1615},"when-not-to-use-a-doughnut","When not to use a doughnut",[10,1618,1619,1620,1624],{},"Doughnuts compare parts of ",[1621,1622,1623],"em",{},"one"," whole, with few parts. Beyond five or six slices the small ones become unreadable, and comparing slices across two doughnuts is hard for everyone. If the question is \"which channel is biggest?\" or \"how did each channel change?\", a sorted bar chart answers it faster. Use the doughnut where its strength — \"this is the whole, and here is its split\" — is the point, such as a single KPI block on a dashboard.",[20,1626,1628],{"id":1627},"common-pitfalls","Common pitfalls",[1630,1631,1632,1643,1653,1663,1671],"ul",{},[1633,1634,1635,1639,1640,1642],"li",{},[1636,1637,1638],"strong",{},"Default hole size."," 50% gives a thin ring; set ",[32,1641,551],{}," to 55–65.",[1633,1644,1645,1648,1649,1652],{},[1636,1646,1647],{},"Legend-key squares in labels."," Turn off ",[32,1650,1651],{},"showLegendKey"," on the data labels.",[1633,1654,1655,1658,1659,1662],{},[1636,1656,1657],{},"Dark text on dark slices."," Set a label font colour with ",[32,1660,1661],{},"txPr",".",[1633,1664,1665,1668,1669,1662],{},[1636,1666,1667],{},"Inconsistent colours across charts."," Set slice colours explicitly with ",[32,1670,570],{},[1633,1672,1673,1676],{},[1636,1674,1675],{},"Negative or zero values."," Slices cannot be negative; a zero slice gets a label at the edge. Filter them out before charting.",[20,1678,1680],{"id":1679},"conclusion","Conclusion",[10,1682,1683,1684,1686,1687,1689,1690,1692,1693,1695,1696,1698],{},"Build a ",[32,1685,294],{}," from a value column and a category column, set ",[32,1688,551],{}," to 55–65, colour each slice with a ",[32,1691,570],{}," from a fixed palette, and add percentage-only data labels with a white bold font through ",[32,1694,1661],{},". Put the total in the title, use ",[32,1697,1227],{}," to start at the top, add a second series for a two-ring comparison, and render the workbook with LibreOffice to check it. Reach for a bar chart when there are many categories or several periods.",[20,1700,1702],{"id":1701},"frequently-asked-questions","Frequently asked questions",[10,1704,1705,1708],{},[1636,1706,1707],{},"How do I create a doughnut chart in Excel with Python?","\nWith openpyxl, create DoughnutChart(), add the values with add_data(Reference(...), titles_from_data=True), set the categories with set_categories, and place it with ws.add_chart(chart, \"A8\").",[10,1710,1711,1714],{},[1636,1712,1713],{},"How do I change the hole size of a doughnut chart?","\nSet chart.holeSize to a percentage between 10 and 90. The default is 50; values around 55 to 65 give a more readable ring.",[10,1716,1717,1720],{},[1636,1718,1719],{},"How do I show percentages on a doughnut chart?","\nAssign a DataLabelList with showPercent = True and the other show flags set to False to chart.dataLabels. Excel calculates the percentages from the plotted values.",[10,1722,1723,1726],{},[1636,1724,1725],{},"Can I put the total in the middle of the doughnut?","\nExcel has no built-in text for the hole. Put the total in the chart title, or place it in a cell next to the chart, so it stays attached when the chart is moved.",[20,1728,1730],{"id":1729},"related","Related",[1630,1732,1733,1739,1746,1753],{},[1633,1734,1735,1736,1738],{},"Up one level: ",[14,1737,17],{"href":16}," — every chart type openpyxl supports.",[1633,1740,1741,1745],{},[14,1742,1744],{"href":1743},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-pie-chart-in-excel-with-openpyxl\u002F","Create a Pie Chart in Excel with openpyxl"," — the doughnut's closest relative.",[1633,1747,1748,1752],{},[14,1749,1751],{"href":1750},"\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"," — where doughnuts usually live.",[1633,1754,1755,1759],{},[14,1756,1758],{"href":1757},"\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fexport-an-excel-chart-as-an-image\u002F","Export an Excel Chart as an Image"," — using the chart outside the workbook.",[1761,1762,1763],"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":30,"searchDepth":321,"depth":321,"links":1765},[1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777],{"id":22,"depth":321,"text":23},{"id":287,"depth":321,"text":288},{"id":563,"depth":321,"text":564},{"id":853,"depth":321,"text":854},{"id":1108,"depth":321,"text":1109},{"id":1231,"depth":321,"text":1232},{"id":1523,"depth":321,"text":1524},{"id":1615,"depth":321,"text":1616},{"id":1627,"depth":321,"text":1628},{"id":1679,"depth":321,"text":1680},{"id":1701,"depth":321,"text":1702},{"id":1729,"depth":321,"text":1730},"2026-09-29","Build an Excel doughnut chart from Python: data and categories from a sheet, the hole size, one colour per slice, percentage labels that are readable on dark slices, a start angle, two rings to compare periods, the total shown in the title, and when a bar chart would say it better.","md",[1782,1784,1786,1788],{"q":1707,"a":1783},"With openpyxl, create DoughnutChart(), add the values with add_data(Reference(...), titles_from_data=True), set the categories with set_categories, and place it with ws.add_chart(chart, \"A8\").",{"q":1713,"a":1785},"Set chart.holeSize to a percentage between 10 and 90. The default is 50; values around 55 to 65 give a more readable ring.",{"q":1719,"a":1787},"Assign a DataLabelList with showPercent = True and the other show flags set to False to chart.dataLabels. Excel calculates the percentages from the plotted values.",{"q":1725,"a":1789},"Excel has no built-in text for the hole. Put the total in the chart title, or place it in a cell next to the chart, so it stays attached when the chart is moved.","create a doughnut chart with openpyxl",null,{"breadcrumb":1793},[1794,1797,1800],{"name":1795,"item":1796},"Home","\u002F",{"name":1798,"item":1799},"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-doughnut-chart-with-openpyxl",{"title":1803,"description":1804},"Excel Doughnut Chart with openpyxl — Colours, Labels, Rings","Create a doughnut chart with openpyxl DoughnutChart: holeSize, per-slice colours with DataPoint, white percentage data labels via txPr, firstSliceAng, a two-ring year-on-year comparison and a total in the title, checked by rendering the workbook.","create-a-doughnut-chart-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-doughnut-chart-with-openpyxl\u002Findex",[1808,1809],"charts-and-images","openpyxl","how-to","6Qu2r06K3pVP6kAziYgLEz905F401wyl9ruyz6J4jac",[1813,1817],{"title":1814,"path":1815,"stem":1816,"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",{"title":1818,"path":1819,"stem":1820,"children":-1},"Create a Dumbbell Chart in Excel with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-dumbbell-chart-in-excel-with-python","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-dumbbell-chart-in-excel-with-python\u002Findex",[1822,1824],{"path":1823,"title":17},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl",{"path":1825,"title":1798},"\u002Fformatting-and-charting-excel-reports-with-python",[],[1828,1831,1834,1837,1840,1843],{"path":1829,"title":1830,"kind":1810},"\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":1832,"title":1833,"kind":1810},"\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":1835,"title":1836,"kind":1810},"\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":1838,"title":1839,"kind":1810},"\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":1841,"title":1842,"kind":1810},"\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":1844,"title":1845,"kind":1810},"\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",1790972109039]