[{"data":1,"prerenderedAt":2128},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-waffle-or-progress-chart-in-excel":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-waffle-or-progress-chart-in-excel":2094,"ancestors:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-waffle-or-progress-chart-in-excel":2103,"children:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-waffle-or-progress-chart-in-excel":2108,"related:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-waffle-or-progress-chart-in-excel":2109},{"id":4,"title":5,"body":6,"dateModified":2060,"datePublished":2060,"description":2061,"extension":2062,"faq":2063,"keyword":2072,"kind":2073,"meta":2074,"navigation":249,"path":2083,"seo":2084,"slug":2087,"stem":2088,"topics":2089,"type":2092,"__hash__":2093},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-waffle-or-progress-chart-in-excel\u002Findex.md","Create a Waffle or Progress Chart in Excel",{"type":7,"value":8,"toc":2047},"minimark",[9,28,33,59,62,66,73,185,189,915,930,934,1164,1187,1191,1570,1573,1577,1649,1652,1840,1844,1851,1921,1925,1963,1967,1973,1977,1983,1992,2005,2011,2015,2044],[10,11,12,13,17,18,21,22,27],"p",{},"Some figures are percentages of a whole that people want to feel rather than read: 73% of new starters onboarded, 41% of the budget spent, 88% of the survey returned. A pie or doughnut chart can show them, but small differences in angle are hard to judge. A ",[14,15,16],"em",{},"waffle chart"," is a 10 × 10 grid of squares in which one square is one percent, so 73% is 73 coloured squares out of 100 — easy to count, easy to compare across several measures side by side. A ",[14,19,20],{},"progress bar"," is the one-dimensional version: a bar that fills from left to right. Excel has neither as a chart type, but both are simple to build from worksheet cells with conditional formatting, which means they update instantly when the percentage changes, print crisply and need no chart object at all. This page builds a row of waffles and a set of progress bars with openpyxl and renders the result. It belongs to ",[23,24,26],"a",{"href":25},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002F","Creating Charts in Excel with openpyxl",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,40],"pre",{"className":36,"code":37,"language":38,"meta":39,"style":39},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash","",[41,42,43],"code",{"__ignoreMap":39},[44,45,48,52,56],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sMTad","pip",[44,53,55],{"class":54},"srMev"," install",[44,57,58],{"class":54}," openpyxl\n",[10,60,61],{},"LibreOffice is used to render the sheet for checking; Excel draws the same conditional formatting.",[29,63,65],{"id":64},"how-the-waffle-works","How the waffle works",[10,67,68,69,72],{},"Each of the 100 cells in the grid holds a number from 1 to 100 — its position in the filling order. One conditional formatting rule colours a cell when its number is less than or equal to the percentage times 100. If the percentage cell holds 73%, cells 1 to 73 are coloured and 74 to 100 stay grey. The numbers themselves are hidden with the number format ",[41,70,71],{},";;;",", which displays nothing for any value, so only the colours show. Changing the percentage — by hand or through a formula linked to the data — recolours the grid immediately.",[74,75,83,84,83,88,83,92,83,100,83,110,83,117,83,122,83,127,83,132,83,137,83,142,83,145,83,149,83,152,83,157,83,162,83,165,83,169,83,172,83,177,83,182],"svg",{"viewBox":76,"role":77,"ariaLabelledBy":78,"xmlns":81,"style":82},"4 0 752 98","img",[79,80],"waf-how-t","waf-how-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  ",[85,86,87],"title",{"id":79},"From a percentage to coloured squares",[89,90,91],"desc",{"id":80},"Each cell in the 10 by 10 grid holds its position from 1 to 100, hidden by a blank number format. A conditional formatting rule compares the position with the percentage times 100 and colours the cell when it is within it.",[93,94],"rect",{"x":95,"y":96,"width":97,"height":98,"fill":99},"4","0","752","98","#ffffff",[93,101],{"x":102,"y":103,"width":104,"height":105,"rx":106,"fill":107,"stroke":108,"style":109},"20.0","16.0","153.0","66.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[111,112,116],"text",{"x":113,"y":114,"style":115},"96.5","45.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Percentage cell",[111,118,121],{"x":113,"y":119,"style":120},"64.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","e.g. 73%",[46,123],{"x1":124,"y1":125,"x2":126,"y2":125,"stroke":108,"style":109},"178.0","49.0","200.0",[128,129],"polygon",{"points":130,"fill":131},"204.0,49.0 195.0,54.0 195.0,44.0","#5b5cf0",[93,133],{"x":134,"y":103,"width":104,"height":105,"rx":106,"fill":135,"stroke":136,"style":109},"209.0","#e7ebef","var(--line,#cdd5e6)",[111,138,141],{"x":139,"y":114,"style":140},"285.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Grid cells 1–100",[111,143,144],{"x":139,"y":119,"style":120},"position in fill order",[46,146],{"x1":147,"y1":125,"x2":148,"y2":125,"stroke":108,"style":109},"367.0","389.0",[128,150],{"points":151,"fill":131},"393.0,49.0 384.0,54.0 384.0,44.0",[93,153],{"x":154,"y":103,"width":104,"height":105,"rx":106,"fill":155,"stroke":156,"style":109},"398.0","#fdefd8","var(--gold,#b4740a)",[111,158,161],{"x":159,"y":114,"style":160},"474.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Rule: n ≤ 73",[111,163,164],{"x":159,"y":119,"style":120},"one formula per grid",[46,166],{"x1":167,"y1":125,"x2":168,"y2":125,"stroke":108,"style":109},"556.0","578.0",[128,170],{"points":171,"fill":131},"582.0,49.0 573.0,54.0 573.0,44.0",[93,173],{"x":174,"y":103,"width":104,"height":105,"rx":106,"fill":175,"stroke":176,"style":109},"587.0","#d9f4f1","var(--teal,#0f9488)",[111,178,181],{"x":179,"y":114,"style":180},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Coloured squares",[111,183,184],{"x":179,"y":119,"style":120},"numbers hidden",[29,186,188],{"id":187},"step-1-a-numbered-grid-for-each-measure","Step 1 — A numbered grid for each measure",[34,190,194],{"className":191,"code":192,"language":193,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\nfrom openpyxl.styles import Alignment, Border, Font, PatternFill, Side\nfrom openpyxl.utils import get_column_letter as col\n\nmeasures = [(\"Onboarding complete\", 0.73), (\"Budget spent\", 0.41), (\"Survey returned\", 0.88), (\"Training booked\", 0.25)]\n\nwb = Workbook()\nws = wb.active\nws.title = \"Progress\"\ngap = Side(style=\"thin\", color=\"FFFFFF\")\ngrey = PatternFill(\"solid\", fgColor=\"E7E6E6\")\nGRID_TOP, BLOCK = 5, 11                                   # grid starts on row 5; each waffle is 10 columns + 1 gap\n\nfor k, (name, pct) in enumerate(measures):\n    left = 2 + k * BLOCK\n    ws.merge_cells(start_row=2, start_column=left, end_row=2, end_column=left + 9)\n    ws.merge_cells(start_row=3, start_column=left, end_row=3, end_column=left + 9)\n    ws.cell(2, left, name).font = Font(bold=True, size=10)\n    value = ws.cell(3, left, pct)\n    value.number_format, value.font = \"0%\", Font(bold=True, size=18, color=\"1F5FA8\")\n    for r in range(10):\n        for c in range(10):\n            cell = ws.cell(GRID_TOP + r, left + c, (9 - r) * 10 + c + 1)   # 1 is bottom-left, 100 top-right\n            cell.number_format = \";;;\"\n            cell.fill = grey\n            cell.border = Border(left=gap, right=gap, top=gap, bottom=gap)\n    for c in range(11):\n        ws.column_dimensions[col(left + c)].width = 2.4\nfor r in range(GRID_TOP, GRID_TOP + 10):\n    ws.row_dimensions[r].height = 14\nprint(ws.cell(GRID_TOP + 9, 2).value, ws.cell(GRID_TOP, 11).value)   # bottom-left and top-right of the first grid\n","python",[41,195,196,212,225,244,251,307,312,323,334,345,378,404,430,435,453,476,525,565,601,617,657,679,698,749,760,771,812,829,845,870,881],{"__ignoreMap":39},[44,197,198,202,206,209],{"class":46,"line":47},[44,199,201],{"class":200},"s-kum","from",[44,203,205],{"class":204},"skGVy"," openpyxl ",[44,207,208],{"class":200},"import",[44,210,211],{"class":204}," Workbook\n",[44,213,215,217,220,222],{"class":46,"line":214},2,[44,216,201],{"class":200},[44,218,219],{"class":204}," openpyxl.styles ",[44,221,208],{"class":200},[44,223,224],{"class":204}," Alignment, Border, Font, PatternFill, Side\n",[44,226,228,230,233,235,238,241],{"class":46,"line":227},3,[44,229,201],{"class":200},[44,231,232],{"class":204}," openpyxl.utils ",[44,234,208],{"class":200},[44,236,237],{"class":204}," get_column_letter ",[44,239,240],{"class":200},"as",[44,242,243],{"class":204}," col\n",[44,245,247],{"class":46,"line":246},4,[44,248,250],{"emptyLinePlaceholder":249},true,"\n",[44,252,254,257,260,263,266,269,273,276,279,281,284,286,289,291,294,296,299,301,304],{"class":46,"line":253},5,[44,255,256],{"class":204},"measures ",[44,258,259],{"class":200},"=",[44,261,262],{"class":204}," [(",[44,264,265],{"class":54},"\"Onboarding complete\"",[44,267,268],{"class":204},", ",[44,270,272],{"class":271},"sP0c6","0.73",[44,274,275],{"class":204},"), (",[44,277,278],{"class":54},"\"Budget spent\"",[44,280,268],{"class":204},[44,282,283],{"class":271},"0.41",[44,285,275],{"class":204},[44,287,288],{"class":54},"\"Survey returned\"",[44,290,268],{"class":204},[44,292,293],{"class":271},"0.88",[44,295,275],{"class":204},[44,297,298],{"class":54},"\"Training booked\"",[44,300,268],{"class":204},[44,302,303],{"class":271},"0.25",[44,305,306],{"class":204},")]\n",[44,308,310],{"class":46,"line":309},6,[44,311,250],{"emptyLinePlaceholder":249},[44,313,315,318,320],{"class":46,"line":314},7,[44,316,317],{"class":204},"wb ",[44,319,259],{"class":200},[44,321,322],{"class":204}," Workbook()\n",[44,324,326,329,331],{"class":46,"line":325},8,[44,327,328],{"class":204},"ws ",[44,330,259],{"class":200},[44,332,333],{"class":204}," wb.active\n",[44,335,337,340,342],{"class":46,"line":336},9,[44,338,339],{"class":204},"ws.title ",[44,341,259],{"class":200},[44,343,344],{"class":54}," \"Progress\"\n",[44,346,348,351,353,356,360,362,365,367,370,372,375],{"class":46,"line":347},10,[44,349,350],{"class":204},"gap ",[44,352,259],{"class":200},[44,354,355],{"class":204}," Side(",[44,357,359],{"class":358},"sa561","style",[44,361,259],{"class":200},[44,363,364],{"class":54},"\"thin\"",[44,366,268],{"class":204},[44,368,369],{"class":358},"color",[44,371,259],{"class":200},[44,373,374],{"class":54},"\"FFFFFF\"",[44,376,377],{"class":204},")\n",[44,379,381,384,386,389,392,394,397,399,402],{"class":46,"line":380},11,[44,382,383],{"class":204},"grey ",[44,385,259],{"class":200},[44,387,388],{"class":204}," PatternFill(",[44,390,391],{"class":54},"\"solid\"",[44,393,268],{"class":204},[44,395,396],{"class":358},"fgColor",[44,398,259],{"class":200},[44,400,401],{"class":54},"\"E7E6E6\"",[44,403,377],{"class":204},[44,405,407,410,412,415,418,421,423,426],{"class":46,"line":406},12,[44,408,409],{"class":271},"GRID_TOP",[44,411,268],{"class":204},[44,413,414],{"class":271},"BLOCK",[44,416,417],{"class":200}," =",[44,419,420],{"class":271}," 5",[44,422,268],{"class":204},[44,424,425],{"class":271},"11",[44,427,429],{"class":428},"s-wDw","                                   # grid starts on row 5; each waffle is 10 columns + 1 gap\n",[44,431,433],{"class":46,"line":432},13,[44,434,250],{"emptyLinePlaceholder":249},[44,436,438,441,444,447,450],{"class":46,"line":437},14,[44,439,440],{"class":200},"for",[44,442,443],{"class":204}," k, (name, pct) ",[44,445,446],{"class":200},"in",[44,448,449],{"class":271}," enumerate",[44,451,452],{"class":204},"(measures):\n",[44,454,456,459,461,464,467,470,473],{"class":46,"line":455},15,[44,457,458],{"class":204},"    left ",[44,460,259],{"class":200},[44,462,463],{"class":271}," 2",[44,465,466],{"class":200}," +",[44,468,469],{"class":204}," k ",[44,471,472],{"class":200},"*",[44,474,475],{"class":271}," BLOCK\n",[44,477,479,482,485,487,490,492,495,497,500,503,505,507,509,512,514,517,520,523],{"class":46,"line":478},16,[44,480,481],{"class":204},"    ws.merge_cells(",[44,483,484],{"class":358},"start_row",[44,486,259],{"class":200},[44,488,489],{"class":271},"2",[44,491,268],{"class":204},[44,493,494],{"class":358},"start_column",[44,496,259],{"class":200},[44,498,499],{"class":204},"left, ",[44,501,502],{"class":358},"end_row",[44,504,259],{"class":200},[44,506,489],{"class":271},[44,508,268],{"class":204},[44,510,511],{"class":358},"end_column",[44,513,259],{"class":200},[44,515,516],{"class":204},"left ",[44,518,519],{"class":200},"+",[44,521,522],{"class":271}," 9",[44,524,377],{"class":204},[44,526,528,530,532,534,537,539,541,543,545,547,549,551,553,555,557,559,561,563],{"class":46,"line":527},17,[44,529,481],{"class":204},[44,531,484],{"class":358},[44,533,259],{"class":200},[44,535,536],{"class":271},"3",[44,538,268],{"class":204},[44,540,494],{"class":358},[44,542,259],{"class":200},[44,544,499],{"class":204},[44,546,502],{"class":358},[44,548,259],{"class":200},[44,550,536],{"class":271},[44,552,268],{"class":204},[44,554,511],{"class":358},[44,556,259],{"class":200},[44,558,516],{"class":204},[44,560,519],{"class":200},[44,562,522],{"class":271},[44,564,377],{"class":204},[44,566,568,571,573,576,578,581,584,586,589,591,594,596,599],{"class":46,"line":567},18,[44,569,570],{"class":204},"    ws.cell(",[44,572,489],{"class":271},[44,574,575],{"class":204},", left, name).font ",[44,577,259],{"class":200},[44,579,580],{"class":204}," Font(",[44,582,583],{"class":358},"bold",[44,585,259],{"class":200},[44,587,588],{"class":271},"True",[44,590,268],{"class":204},[44,592,593],{"class":358},"size",[44,595,259],{"class":200},[44,597,598],{"class":271},"10",[44,600,377],{"class":204},[44,602,604,607,609,612,614],{"class":46,"line":603},19,[44,605,606],{"class":204},"    value ",[44,608,259],{"class":200},[44,610,611],{"class":204}," ws.cell(",[44,613,536],{"class":271},[44,615,616],{"class":204},", left, pct)\n",[44,618,620,623,625,628,631,633,635,637,639,641,643,646,648,650,652,655],{"class":46,"line":619},20,[44,621,622],{"class":204},"    value.number_format, value.font ",[44,624,259],{"class":200},[44,626,627],{"class":54}," \"0%\"",[44,629,630],{"class":204},", Font(",[44,632,583],{"class":358},[44,634,259],{"class":200},[44,636,588],{"class":271},[44,638,268],{"class":204},[44,640,593],{"class":358},[44,642,259],{"class":200},[44,644,645],{"class":271},"18",[44,647,268],{"class":204},[44,649,369],{"class":358},[44,651,259],{"class":200},[44,653,654],{"class":54},"\"1F5FA8\"",[44,656,377],{"class":204},[44,658,660,663,666,668,671,674,676],{"class":46,"line":659},21,[44,661,662],{"class":200},"    for",[44,664,665],{"class":204}," r ",[44,667,446],{"class":200},[44,669,670],{"class":271}," range",[44,672,673],{"class":204},"(",[44,675,598],{"class":271},[44,677,678],{"class":204},"):\n",[44,680,682,685,688,690,692,694,696],{"class":46,"line":681},22,[44,683,684],{"class":200},"        for",[44,686,687],{"class":204}," c ",[44,689,446],{"class":200},[44,691,670],{"class":271},[44,693,673],{"class":204},[44,695,598],{"class":271},[44,697,678],{"class":204},[44,699,701,704,706,708,710,712,715,717,720,723,726,729,731,734,736,738,740,743,746],{"class":46,"line":700},23,[44,702,703],{"class":204},"            cell ",[44,705,259],{"class":200},[44,707,611],{"class":204},[44,709,409],{"class":271},[44,711,466],{"class":200},[44,713,714],{"class":204}," r, left ",[44,716,519],{"class":200},[44,718,719],{"class":204}," c, (",[44,721,722],{"class":271},"9",[44,724,725],{"class":200}," -",[44,727,728],{"class":204}," r) ",[44,730,472],{"class":200},[44,732,733],{"class":271}," 10",[44,735,466],{"class":200},[44,737,687],{"class":204},[44,739,519],{"class":200},[44,741,742],{"class":271}," 1",[44,744,745],{"class":204},")   ",[44,747,748],{"class":428},"# 1 is bottom-left, 100 top-right\n",[44,750,752,755,757],{"class":46,"line":751},24,[44,753,754],{"class":204},"            cell.number_format ",[44,756,259],{"class":200},[44,758,759],{"class":54}," \";;;\"\n",[44,761,763,766,768],{"class":46,"line":762},25,[44,764,765],{"class":204},"            cell.fill ",[44,767,259],{"class":200},[44,769,770],{"class":204}," grey\n",[44,772,774,777,779,782,785,787,790,793,795,797,800,802,804,807,809],{"class":46,"line":773},26,[44,775,776],{"class":204},"            cell.border ",[44,778,259],{"class":200},[44,780,781],{"class":204}," Border(",[44,783,784],{"class":358},"left",[44,786,259],{"class":200},[44,788,789],{"class":204},"gap, ",[44,791,792],{"class":358},"right",[44,794,259],{"class":200},[44,796,789],{"class":204},[44,798,799],{"class":358},"top",[44,801,259],{"class":200},[44,803,789],{"class":204},[44,805,806],{"class":358},"bottom",[44,808,259],{"class":200},[44,810,811],{"class":204},"gap)\n",[44,813,815,817,819,821,823,825,827],{"class":46,"line":814},27,[44,816,662],{"class":200},[44,818,687],{"class":204},[44,820,446],{"class":200},[44,822,670],{"class":271},[44,824,673],{"class":204},[44,826,425],{"class":271},[44,828,678],{"class":204},[44,830,832,835,837,840,842],{"class":46,"line":831},28,[44,833,834],{"class":204},"        ws.column_dimensions[col(left ",[44,836,519],{"class":200},[44,838,839],{"class":204}," c)].width ",[44,841,259],{"class":200},[44,843,844],{"class":271}," 2.4\n",[44,846,848,850,852,854,856,858,860,862,864,866,868],{"class":46,"line":847},29,[44,849,440],{"class":200},[44,851,665],{"class":204},[44,853,446],{"class":200},[44,855,670],{"class":271},[44,857,673],{"class":204},[44,859,409],{"class":271},[44,861,268],{"class":204},[44,863,409],{"class":271},[44,865,466],{"class":200},[44,867,733],{"class":271},[44,869,678],{"class":204},[44,871,873,876,878],{"class":46,"line":872},30,[44,874,875],{"class":204},"    ws.row_dimensions[r].height ",[44,877,259],{"class":200},[44,879,880],{"class":271}," 14\n",[44,882,884,887,890,892,894,896,898,900,903,905,907,909,912],{"class":46,"line":883},31,[44,885,886],{"class":271},"print",[44,888,889],{"class":204},"(ws.cell(",[44,891,409],{"class":271},[44,893,466],{"class":200},[44,895,522],{"class":271},[44,897,268],{"class":204},[44,899,489],{"class":271},[44,901,902],{"class":204},").value, ws.cell(",[44,904,409],{"class":271},[44,906,268],{"class":204},[44,908,425],{"class":271},[44,910,911],{"class":204},").value)   ",[44,913,914],{"class":428},"# bottom-left and top-right of the first grid\n",[10,916,917,918,921,922,925,926,929],{},"The grid is numbered so that it fills from the bottom-left corner, row by row upwards, the way a glass fills: position 1 is the bottom-left square and 100 the top-right. The formula ",[41,919,920],{},"(9 - r) * 10 + c + 1"," does that for row ",[41,923,924],{},"r"," and column ",[41,927,928],{},"c"," within the grid. Filling left to right, top to bottom, is just as valid if that suits the report better; only the numbering changes. Columns 2.4 characters wide and rows 14 points high make the cells roughly square at normal zoom. A thin white border on every cell creates the gaps between the squares, and the grey fill is the \"empty\" colour that the conditional format overrides.",[29,931,933],{"id":932},"step-2-one-rule-colours-each-grid","Step 2 — One rule colours each grid",[34,935,937],{"className":191,"code":936,"language":193,"meta":39,"style":39},"from openpyxl.formatting.rule import FormulaRule\n\nblue = PatternFill(\"solid\", fgColor=\"1F5FA8\", bgColor=\"1F5FA8\")\nfor k in range(len(measures)):\n    left = 2 + k * BLOCK\n    grid = f\"{col(left)}{GRID_TOP}:{col(left + 9)}{GRID_TOP + 9}\"\n    pct_cell = f\"${col(left)}$3\"\n    ws.conditional_formatting.add(grid, FormulaRule(formula=[f\"{col(left)}{GRID_TOP}\u003C=ROUND({pct_cell}*100,0)\"], fill=blue))\nprint(len(ws.conditional_formatting), \"rules\")\n",[41,938,939,951,955,985,1003,1019,1075,1096,1148],{"__ignoreMap":39},[44,940,941,943,946,948],{"class":46,"line":47},[44,942,201],{"class":200},[44,944,945],{"class":204}," openpyxl.formatting.rule ",[44,947,208],{"class":200},[44,949,950],{"class":204}," FormulaRule\n",[44,952,953],{"class":46,"line":214},[44,954,250],{"emptyLinePlaceholder":249},[44,956,957,960,962,964,966,968,970,972,974,976,979,981,983],{"class":46,"line":227},[44,958,959],{"class":204},"blue ",[44,961,259],{"class":200},[44,963,388],{"class":204},[44,965,391],{"class":54},[44,967,268],{"class":204},[44,969,396],{"class":358},[44,971,259],{"class":200},[44,973,654],{"class":54},[44,975,268],{"class":204},[44,977,978],{"class":358},"bgColor",[44,980,259],{"class":200},[44,982,654],{"class":54},[44,984,377],{"class":204},[44,986,987,989,991,993,995,997,1000],{"class":46,"line":246},[44,988,440],{"class":200},[44,990,469],{"class":204},[44,992,446],{"class":200},[44,994,670],{"class":271},[44,996,673],{"class":204},[44,998,999],{"class":271},"len",[44,1001,1002],{"class":204},"(measures)):\n",[44,1004,1005,1007,1009,1011,1013,1015,1017],{"class":46,"line":253},[44,1006,458],{"class":204},[44,1008,259],{"class":200},[44,1010,463],{"class":271},[44,1012,466],{"class":200},[44,1014,469],{"class":204},[44,1016,472],{"class":200},[44,1018,475],{"class":271},[44,1020,1021,1024,1026,1029,1032,1036,1039,1042,1044,1047,1050,1052,1055,1057,1059,1062,1064,1066,1068,1070,1072],{"class":46,"line":309},[44,1022,1023],{"class":204},"    grid ",[44,1025,259],{"class":200},[44,1027,1028],{"class":200}," f",[44,1030,1031],{"class":54},"\"",[44,1033,1035],{"class":1034},"sSjpA","{",[44,1037,1038],{"class":204},"col(left)",[44,1040,1041],{"class":1034},"}{",[44,1043,409],{"class":271},[44,1045,1046],{"class":1034},"}",[44,1048,1049],{"class":54},":",[44,1051,1035],{"class":1034},[44,1053,1054],{"class":204},"col(left ",[44,1056,519],{"class":200},[44,1058,522],{"class":271},[44,1060,1061],{"class":204},")",[44,1063,1041],{"class":1034},[44,1065,409],{"class":271},[44,1067,466],{"class":200},[44,1069,522],{"class":271},[44,1071,1046],{"class":1034},[44,1073,1074],{"class":54},"\"\n",[44,1076,1077,1080,1082,1084,1087,1089,1091,1093],{"class":46,"line":314},[44,1078,1079],{"class":204},"    pct_cell ",[44,1081,259],{"class":200},[44,1083,1028],{"class":200},[44,1085,1086],{"class":54},"\"$",[44,1088,1035],{"class":1034},[44,1090,1038],{"class":204},[44,1092,1046],{"class":1034},[44,1094,1095],{"class":54},"$3\"\n",[44,1097,1098,1101,1104,1106,1109,1112,1114,1116,1118,1120,1122,1124,1127,1129,1132,1134,1137,1140,1143,1145],{"class":46,"line":325},[44,1099,1100],{"class":204},"    ws.conditional_formatting.add(grid, FormulaRule(",[44,1102,1103],{"class":358},"formula",[44,1105,259],{"class":200},[44,1107,1108],{"class":204},"[",[44,1110,1111],{"class":200},"f",[44,1113,1031],{"class":54},[44,1115,1035],{"class":1034},[44,1117,1038],{"class":204},[44,1119,1041],{"class":1034},[44,1121,409],{"class":271},[44,1123,1046],{"class":1034},[44,1125,1126],{"class":54},"\u003C=ROUND(",[44,1128,1035],{"class":1034},[44,1130,1131],{"class":204},"pct_cell",[44,1133,1046],{"class":1034},[44,1135,1136],{"class":54},"*100,0)\"",[44,1138,1139],{"class":204},"], ",[44,1141,1142],{"class":358},"fill",[44,1144,259],{"class":200},[44,1146,1147],{"class":204},"blue))\n",[44,1149,1150,1152,1154,1156,1159,1162],{"class":46,"line":336},[44,1151,886],{"class":271},[44,1153,673],{"class":204},[44,1155,999],{"class":271},[44,1157,1158],{"class":204},"(ws.conditional_formatting), ",[44,1160,1161],{"class":54},"\"rules\"",[44,1163,377],{"class":204},[10,1165,1166,1167,1170,1171,1174,1175,1178,1179,1182,1183,1186],{},"The rule's formula is written for the top-left cell of the range, with a ",[14,1168,1169],{},"relative"," reference to it (",[41,1172,1173],{},"B5",") and an ",[14,1176,1177],{},"absolute"," reference to the percentage (",[41,1180,1181],{},"$B$3","). Excel shifts the relative part for every other cell in the range, so each cell compares its own number with the same percentage. ",[41,1184,1185],{},"ROUND(... * 100, 0)"," turns 73% into 73 squares and avoids a 72.99999% leaving the last square empty. The fill is given as both foreground and background colour, because conditional formatting fills use the background colour in Excel and the foreground in some other applications.",[29,1188,1190],{"id":1189},"step-3-progress-bars-from-data-bars","Step 3 — Progress bars from data bars",[34,1192,1194],{"className":191,"code":1193,"language":193,"meta":39,"style":39},"from openpyxl.formatting.rule import DataBarRule\n\ntop = GRID_TOP + 12\nws.cell(top, 2, \"Progress bars\").font = Font(bold=True)\nfor i, (name, pct) in enumerate(measures, start=1):\n    r = top + i\n    ws.merge_cells(start_row=r, start_column=2, end_row=r, end_column=10)\n    ws.cell(r, 2, name)\n    ws.merge_cells(start_row=r, start_column=13, end_row=r, end_column=30)\n    bar = ws.cell(r, 13, f\"={col(2 + (i - 1) * BLOCK)}3\")      # linked to the waffle's percentage\n    bar.number_format = \"0%\"\n    bar.alignment = Alignment(horizontal=\"left\")\nws.conditional_formatting.add(f\"M{top + 1}:M{top + len(measures)}\",\n                              DataBarRule(start_type=\"num\", start_value=0, end_type=\"num\", end_value=1, color=\"1F5FA8\"))\nwb.save(\"progress.xlsx\")\nprint(\"saved\")\n",[41,1195,1196,1207,1211,1226,1253,1277,1292,1325,1335,1369,1426,1436,1456,1498,1549,1559],{"__ignoreMap":39},[44,1197,1198,1200,1202,1204],{"class":46,"line":47},[44,1199,201],{"class":200},[44,1201,945],{"class":204},[44,1203,208],{"class":200},[44,1205,1206],{"class":204}," DataBarRule\n",[44,1208,1209],{"class":46,"line":214},[44,1210,250],{"emptyLinePlaceholder":249},[44,1212,1213,1216,1218,1221,1223],{"class":46,"line":227},[44,1214,1215],{"class":204},"top ",[44,1217,259],{"class":200},[44,1219,1220],{"class":271}," GRID_TOP",[44,1222,466],{"class":200},[44,1224,1225],{"class":271}," 12\n",[44,1227,1228,1231,1233,1235,1238,1241,1243,1245,1247,1249,1251],{"class":46,"line":246},[44,1229,1230],{"class":204},"ws.cell(top, ",[44,1232,489],{"class":271},[44,1234,268],{"class":204},[44,1236,1237],{"class":54},"\"Progress bars\"",[44,1239,1240],{"class":204},").font ",[44,1242,259],{"class":200},[44,1244,580],{"class":204},[44,1246,583],{"class":358},[44,1248,259],{"class":200},[44,1250,588],{"class":271},[44,1252,377],{"class":204},[44,1254,1255,1257,1260,1262,1264,1267,1270,1272,1275],{"class":46,"line":253},[44,1256,440],{"class":200},[44,1258,1259],{"class":204}," i, (name, pct) ",[44,1261,446],{"class":200},[44,1263,449],{"class":271},[44,1265,1266],{"class":204},"(measures, ",[44,1268,1269],{"class":358},"start",[44,1271,259],{"class":200},[44,1273,1274],{"class":271},"1",[44,1276,678],{"class":204},[44,1278,1279,1282,1284,1287,1289],{"class":46,"line":309},[44,1280,1281],{"class":204},"    r ",[44,1283,259],{"class":200},[44,1285,1286],{"class":204}," top ",[44,1288,519],{"class":200},[44,1290,1291],{"class":204}," i\n",[44,1293,1294,1296,1298,1300,1303,1305,1307,1309,1311,1313,1315,1317,1319,1321,1323],{"class":46,"line":314},[44,1295,481],{"class":204},[44,1297,484],{"class":358},[44,1299,259],{"class":200},[44,1301,1302],{"class":204},"r, ",[44,1304,494],{"class":358},[44,1306,259],{"class":200},[44,1308,489],{"class":271},[44,1310,268],{"class":204},[44,1312,502],{"class":358},[44,1314,259],{"class":200},[44,1316,1302],{"class":204},[44,1318,511],{"class":358},[44,1320,259],{"class":200},[44,1322,598],{"class":271},[44,1324,377],{"class":204},[44,1326,1327,1330,1332],{"class":46,"line":325},[44,1328,1329],{"class":204},"    ws.cell(r, ",[44,1331,489],{"class":271},[44,1333,1334],{"class":204},", name)\n",[44,1336,1337,1339,1341,1343,1345,1347,1349,1352,1354,1356,1358,1360,1362,1364,1367],{"class":46,"line":336},[44,1338,481],{"class":204},[44,1340,484],{"class":358},[44,1342,259],{"class":200},[44,1344,1302],{"class":204},[44,1346,494],{"class":358},[44,1348,259],{"class":200},[44,1350,1351],{"class":271},"13",[44,1353,268],{"class":204},[44,1355,502],{"class":358},[44,1357,259],{"class":200},[44,1359,1302],{"class":204},[44,1361,511],{"class":358},[44,1363,259],{"class":200},[44,1365,1366],{"class":271},"30",[44,1368,377],{"class":204},[44,1370,1371,1374,1376,1379,1381,1383,1385,1388,1390,1393,1395,1397,1400,1403,1405,1408,1410,1413,1415,1417,1420,1423],{"class":46,"line":347},[44,1372,1373],{"class":204},"    bar ",[44,1375,259],{"class":200},[44,1377,1378],{"class":204}," ws.cell(r, ",[44,1380,1351],{"class":271},[44,1382,268],{"class":204},[44,1384,1111],{"class":200},[44,1386,1387],{"class":54},"\"=",[44,1389,1035],{"class":1034},[44,1391,1392],{"class":204},"col(",[44,1394,489],{"class":271},[44,1396,466],{"class":200},[44,1398,1399],{"class":204}," (i ",[44,1401,1402],{"class":200},"-",[44,1404,742],{"class":271},[44,1406,1407],{"class":204},") ",[44,1409,472],{"class":200},[44,1411,1412],{"class":271}," BLOCK",[44,1414,1061],{"class":204},[44,1416,1046],{"class":1034},[44,1418,1419],{"class":54},"3\"",[44,1421,1422],{"class":204},")      ",[44,1424,1425],{"class":428},"# linked to the waffle's percentage\n",[44,1427,1428,1431,1433],{"class":46,"line":380},[44,1429,1430],{"class":204},"    bar.number_format ",[44,1432,259],{"class":200},[44,1434,1435],{"class":54}," \"0%\"\n",[44,1437,1438,1441,1443,1446,1449,1451,1454],{"class":46,"line":406},[44,1439,1440],{"class":204},"    bar.alignment ",[44,1442,259],{"class":200},[44,1444,1445],{"class":204}," Alignment(",[44,1447,1448],{"class":358},"horizontal",[44,1450,259],{"class":200},[44,1452,1453],{"class":54},"\"left\"",[44,1455,377],{"class":204},[44,1457,1458,1461,1463,1466,1468,1470,1472,1474,1476,1479,1481,1483,1485,1488,1491,1493,1495],{"class":46,"line":432},[44,1459,1460],{"class":204},"ws.conditional_formatting.add(",[44,1462,1111],{"class":200},[44,1464,1465],{"class":54},"\"M",[44,1467,1035],{"class":1034},[44,1469,1215],{"class":204},[44,1471,519],{"class":200},[44,1473,742],{"class":271},[44,1475,1046],{"class":1034},[44,1477,1478],{"class":54},":M",[44,1480,1035],{"class":1034},[44,1482,1215],{"class":204},[44,1484,519],{"class":200},[44,1486,1487],{"class":271}," len",[44,1489,1490],{"class":204},"(measures)",[44,1492,1046],{"class":1034},[44,1494,1031],{"class":54},[44,1496,1497],{"class":204},",\n",[44,1499,1500,1503,1506,1508,1511,1513,1516,1518,1520,1522,1525,1527,1529,1531,1534,1536,1538,1540,1542,1544,1546],{"class":46,"line":437},[44,1501,1502],{"class":204},"                              DataBarRule(",[44,1504,1505],{"class":358},"start_type",[44,1507,259],{"class":200},[44,1509,1510],{"class":54},"\"num\"",[44,1512,268],{"class":204},[44,1514,1515],{"class":358},"start_value",[44,1517,259],{"class":200},[44,1519,96],{"class":271},[44,1521,268],{"class":204},[44,1523,1524],{"class":358},"end_type",[44,1526,259],{"class":200},[44,1528,1510],{"class":54},[44,1530,268],{"class":204},[44,1532,1533],{"class":358},"end_value",[44,1535,259],{"class":200},[44,1537,1274],{"class":271},[44,1539,268],{"class":204},[44,1541,369],{"class":358},[44,1543,259],{"class":200},[44,1545,654],{"class":54},[44,1547,1548],{"class":204},"))\n",[44,1550,1551,1554,1557],{"class":46,"line":455},[44,1552,1553],{"class":204},"wb.save(",[44,1555,1556],{"class":54},"\"progress.xlsx\"",[44,1558,377],{"class":204},[44,1560,1561,1563,1565,1568],{"class":46,"line":478},[44,1562,886],{"class":271},[44,1564,673],{"class":204},[44,1566,1567],{"class":54},"\"saved\"",[44,1569,377],{"class":204},[10,1571,1572],{},"A data bar is conditional formatting that draws a bar inside the cell in proportion to its value. Left to itself, Excel scales data bars to the smallest and largest values in the range, so the biggest of four percentages always fills the whole cell — misleading for progress, where 88% should look clearly short of complete. Setting the start to the number 0 and the end to the number 1 fixes the scale at 0–100%, so each bar's length is its true share. The bar cells are formulas linked to the waffles' percentage cells, so updating a percentage updates both visuals. Merging each bar cell across many narrow columns gives it room to be a proper bar.",[29,1574,1576],{"id":1575},"step-4-render-and-check","Step 4 — Render and check",[34,1578,1580],{"className":36,"code":1579,"language":38,"meta":39,"style":39},"soffice --headless --convert-to pdf progress.xlsx > \u002Fdev\u002Fnull 2>&1\npdftoppm -r 70 -f 1 -l 1 -png progress.pdf preview && ls preview*\n",[41,1581,1582,1608],{"__ignoreMap":39},[44,1583,1584,1587,1590,1593,1596,1599,1602,1605],{"class":46,"line":47},[44,1585,1586],{"class":50},"soffice",[44,1588,1589],{"class":271}," --headless",[44,1591,1592],{"class":271}," --convert-to",[44,1594,1595],{"class":54}," pdf",[44,1597,1598],{"class":54}," progress.xlsx",[44,1600,1601],{"class":200}," >",[44,1603,1604],{"class":54}," \u002Fdev\u002Fnull",[44,1606,1607],{"class":200}," 2>&1\n",[44,1609,1610,1613,1616,1619,1622,1624,1627,1629,1632,1635,1638,1641,1644,1646],{"class":46,"line":214},[44,1611,1612],{"class":50},"pdftoppm",[44,1614,1615],{"class":271}," -r",[44,1617,1618],{"class":271}," 70",[44,1620,1621],{"class":271}," -f",[44,1623,742],{"class":271},[44,1625,1626],{"class":271}," -l",[44,1628,742],{"class":271},[44,1630,1631],{"class":271}," -png",[44,1633,1634],{"class":54}," progress.pdf",[44,1636,1637],{"class":54}," preview",[44,1639,1640],{"class":204}," && ",[44,1642,1643],{"class":50},"ls",[44,1645,1637],{"class":54},[44,1647,1648],{"class":271},"*\n",[10,1650,1651],{},"The rendered page shows four waffles side by side — 73, 41, 88 and 25 blue squares filling from the bottom — with each percentage above its grid, and below them four progress bars whose lengths are directly comparable because they share the fixed 0–100% scale. Counting is easy where it matters: the onboarding waffle has seven full rows and three squares of the eighth. Two details are worth knowing from the render. Data bars are drawn with a gradient fill by default, fading towards the end of the bar; a solid fill needs the newer data bar extension that openpyxl does not write, so accept the gradient or use a REPT-based text bar if a solid look matters. And four waffles side by side are wider than a portrait page — the fourth is cut off in the default print layout — so set the sheet to landscape and fit-to-width before printing or exporting to PDF.",[74,1653,83,1659,83,1662,83,1665,83,1670,83,1676,83,1680,83,1686,83,1690,83,1694,83,1698,83,1702,83,1706,83,1710,83,1714,83,1718,83,1721,83,1725,83,1727,83,1730,83,1732,83,1735,83,1737,83,1740,83,1743,83,1746,83,1748,83,1752,83,1754,83,1757,83,1759,83,1762,83,1764,83,1766,83,1769,83,1772,83,1774,83,1777,83,1779,83,1782,83,1784,83,1786,83,1788,83,1790,83,1793,83,1796,83,1798,83,1801,83,1803,83,1806,83,1808,83,1811,83,1813,83,1815,83,1818,83,1822,83,1824,83,1827,83,1829,83,1832,83,1834,83,1836,83,1838],{"viewBox":1654,"role":77,"ariaLabelledBy":1655,"xmlns":81,"style":1658},"127 0 505 196",[1656,1657],"waf-result-t","waf-result-d","width:100%;max-width:505px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[85,1660,1661],{"id":1656},"What the four waffles show",[89,1663,1664],{"id":1657},"Onboarding complete fills 73 squares, seven full rows and three more. Budget spent fills 41, survey returned 88 and training booked 25. Each progress bar below has the same length as its percentage of a full bar.",[93,1666],{"x":1667,"y":96,"width":1668,"height":1669,"fill":99},"127","505","196",[93,1671],{"x":1672,"y":103,"width":1673,"height":1674,"rx":96,"fill":135,"stroke":136,"style":1675},"143.5","32.0","24.0","stroke-width:1px",[93,1677],{"x":1678,"y":103,"width":1679,"height":1674,"rx":96,"fill":135,"stroke":136,"style":1675},"175.5","157.4",[111,1681,1685],{"x":1682,"y":1683,"style":1684},"254.2","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[93,1687],{"x":1688,"y":103,"width":1689,"height":1674,"rx":96,"fill":135,"stroke":136,"style":1675},"333.0","95.0",[111,1691,1693],{"x":1692,"y":1683,"style":1684},"380.5","B",[93,1695],{"x":1696,"y":103,"width":1697,"height":1674,"rx":96,"fill":135,"stroke":136,"style":1675},"427.9","79.7",[111,1699,1701],{"x":1700,"y":1683,"style":1684},"467.8","C",[93,1703],{"x":1704,"y":103,"width":1705,"height":1674,"rx":96,"fill":135,"stroke":136,"style":1675},"507.6","108.9",[111,1707,1709],{"x":1708,"y":1683,"style":1684},"562.1","D",[93,1711],{"x":1672,"y":1712,"width":1673,"height":1713,"rx":96,"fill":135,"stroke":136,"style":1675},"40.0","28.0",[111,1715,1274],{"x":1716,"y":1717,"style":1684},"159.5","59.0",[93,1719],{"x":1678,"y":1712,"width":1679,"height":1713,"rx":96,"fill":107,"stroke":108,"style":1720},"stroke-width:1.5px",[111,1722,1724],{"x":1682,"y":1717,"style":1723},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Measure",[93,1726],{"x":1688,"y":1712,"width":1689,"height":1713,"rx":96,"fill":107,"stroke":108,"style":1720},[111,1728,1729],{"x":1692,"y":1717,"style":1723},"Percentage",[93,1731],{"x":1696,"y":1712,"width":1697,"height":1713,"rx":96,"fill":107,"stroke":108,"style":1720},[111,1733,1734],{"x":1700,"y":1717,"style":1723},"Full rows",[93,1736],{"x":1704,"y":1712,"width":1705,"height":1713,"rx":96,"fill":107,"stroke":108,"style":1720},[111,1738,1739],{"x":1708,"y":1717,"style":1723},"Extra squares",[93,1741],{"x":1672,"y":1742,"width":1673,"height":1713,"rx":96,"fill":135,"stroke":136,"style":1675},"68.0",[111,1744,489],{"x":1716,"y":1745,"style":1684},"87.0",[93,1747],{"x":1678,"y":1742,"width":1679,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1749,1751],{"x":1682,"y":1745,"style":1750},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Onboarding complete",[93,1753],{"x":1688,"y":1742,"width":1689,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1755,1756],{"x":1692,"y":1745,"style":1750},"73%",[93,1758],{"x":1696,"y":1742,"width":1697,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1760,1761],{"x":1700,"y":1745,"style":1750},"7",[93,1763],{"x":1704,"y":1742,"width":1705,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1765,536],{"x":1708,"y":1745,"style":1750},[93,1767],{"x":1672,"y":1768,"width":1673,"height":1713,"rx":96,"fill":135,"stroke":136,"style":1675},"96.0",[111,1770,536],{"x":1716,"y":1771,"style":1684},"115.0",[93,1773],{"x":1678,"y":1768,"width":1679,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1775,1776],{"x":1682,"y":1771,"style":1750},"Budget spent",[93,1778],{"x":1688,"y":1768,"width":1689,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1780,1781],{"x":1692,"y":1771,"style":1750},"41%",[93,1783],{"x":1696,"y":1768,"width":1697,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1785,95],{"x":1700,"y":1771,"style":1750},[93,1787],{"x":1704,"y":1768,"width":1705,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1789,1274],{"x":1708,"y":1771,"style":1750},[93,1791],{"x":1672,"y":1792,"width":1673,"height":1713,"rx":96,"fill":135,"stroke":136,"style":1675},"124.0",[111,1794,95],{"x":1716,"y":1795,"style":1684},"143.0",[93,1797],{"x":1678,"y":1792,"width":1679,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1799,1800],{"x":1682,"y":1795,"style":1750},"Survey returned",[93,1802],{"x":1688,"y":1792,"width":1689,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1804,1805],{"x":1692,"y":1795,"style":1750},"88%",[93,1807],{"x":1696,"y":1792,"width":1697,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1809,1810],{"x":1700,"y":1795,"style":1750},"8",[93,1812],{"x":1704,"y":1792,"width":1705,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1814,1810],{"x":1708,"y":1795,"style":1750},[93,1816],{"x":1672,"y":1817,"width":1673,"height":1713,"rx":96,"fill":135,"stroke":136,"style":1675},"152.0",[111,1819,1821],{"x":1716,"y":1820,"style":1684},"171.0","5",[93,1823],{"x":1678,"y":1817,"width":1679,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1825,1826],{"x":1682,"y":1820,"style":1750},"Training booked",[93,1828],{"x":1688,"y":1817,"width":1689,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1830,1831],{"x":1692,"y":1820,"style":1750},"25%",[93,1833],{"x":1696,"y":1817,"width":1697,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1835,489],{"x":1700,"y":1820,"style":1750},[93,1837],{"x":1704,"y":1817,"width":1705,"height":1713,"rx":96,"fill":99,"stroke":136,"style":1675},[111,1839,1821],{"x":1708,"y":1820,"style":1750},[29,1841,1843],{"id":1842},"choosing-between-them","Choosing between them",[10,1845,1846,1847,27],{},"A waffle takes a lot of space for one number, so it suits a summary page with a handful of headline percentages, where its countable squares make the numbers concrete. Progress bars are compact and line up well in a table — one per project, region or person — and the fixed scale makes them easy to compare. Neither is a good choice for values above 100% (a budget overspent by 12% does not fit in a 100-square grid) or for small differences between similar percentages, where the numbers themselves are clearer. For a measure that must be read against a target rather than against 100%, a bullet chart is better; see ",[23,1848,1850],{"href":1849},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-bullet-chart-in-excel-with-python\u002F","Create a Bullet Chart in Excel with Python",[74,1852,83,1857,83,1860,83,1863,83,1866,83,1871,83,1877,83,1882,83,1886,83,1890,83,1894,83,1898,83,1900,83,1905,83,1909,83,1912,83,1915,83,1918],{"viewBox":1853,"role":77,"ariaLabelledBy":1854,"xmlns":81,"style":82},"4 0 752 199",[1855,1856],"waf-choice-t","waf-choice-d",[85,1858,1859],{"id":1855},"Waffle or progress bar",[89,1861,1862],{"id":1856},"A waffle uses 100 squares for one percentage, is easy to count and suits a few headline figures. A progress bar fits in one row, lines up in tables and suits comparing many items on a fixed scale.",[93,1864],{"x":95,"y":96,"width":97,"height":1865,"fill":99},"199",[93,1867],{"x":102,"y":103,"width":1868,"height":1869,"rx":1870,"fill":107,"stroke":108,"style":109},"351.0","167.0","14",[111,1872,1876],{"x":1873,"y":1874,"style":1875},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Waffle chart",[46,1878],{"x1":1879,"y1":1880,"x2":1881,"y2":1880,"stroke":108,"style":1675},"36.0","57.0","355.0",[111,1883,1885],{"x":1873,"y":1884,"style":1750},"77.0","100 squares, 1% each",[111,1887,1889],{"x":1873,"y":1888,"style":1750},"103.0","easy to count",[111,1891,1893],{"x":1873,"y":1892,"style":1750},"129.0","a few headline figures",[111,1895,1897],{"x":1873,"y":1896,"style":1750},"155.0","takes space",[93,1899],{"x":148,"y":103,"width":1868,"height":1869,"rx":1870,"fill":175,"stroke":176,"style":109},[111,1901,1904],{"x":1902,"y":1874,"style":1903},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Progress bar",[46,1906],{"x1":1907,"y1":1880,"x2":1908,"y2":1880,"stroke":176,"style":1675},"405.0","724.0",[111,1910,1911],{"x":1902,"y":1884,"style":1750},"one bar per row",[111,1913,1914],{"x":1902,"y":1888,"style":1750},"fixed 0–100% scale",[111,1916,1917],{"x":1902,"y":1892,"style":1750},"many items in a table",[111,1919,1920],{"x":1902,"y":1896,"style":1750},"compact",[29,1922,1924],{"id":1923},"common-pitfalls","Common pitfalls",[1926,1927,1928,1939,1945,1951,1957],"ul",{},[1929,1930,1931,1935,1936,1938],"li",{},[1932,1933,1934],"strong",{},"Visible numbers in the grid."," Apply the ",[41,1937,71],{}," number format to hide them.",[1929,1940,1941,1944],{},[1932,1942,1943],{},"Absolute reference to the grid cell."," The rule needs a relative cell and an absolute percentage.",[1929,1946,1947,1950],{},[1932,1948,1949],{},"Automatic data bar scaling."," Fix the bar's start at 0 and end at 1.",[1929,1952,1953,1956],{},[1932,1954,1955],{},"Rectangular squares."," Adjust column width and row height together until the cells look square.",[1929,1958,1959,1962],{},[1932,1960,1961],{},"Values over 100%."," Waffles and progress bars cannot show them; use a different chart.",[29,1964,1966],{"id":1965},"conclusion","Conclusion",[10,1968,1969,1970,1972],{},"To build a waffle chart in Excel with openpyxl, number a 10 × 10 grid of cells from 1 to 100 in the order they should fill, hide the numbers with ",[41,1971,71],{},", and add one conditional formatting rule that colours cells whose number is at most the rounded percentage times 100. For progress bars, use data bars with a fixed 0–1 scale on cells linked to the same percentages. Both update as soon as the percentage changes and render in Excel and LibreOffice alike.",[29,1974,1976],{"id":1975},"frequently-asked-questions","Frequently asked questions",[10,1978,1979,1982],{},[1932,1980,1981],{},"Can Excel make a waffle chart?","\nThere is no waffle chart type, but a 10 × 10 grid of cells coloured by a conditional formatting rule does the job and updates automatically.",[10,1984,1985,1988,1989,1991],{},[1932,1986,1987],{},"How do I hide numbers in Excel cells but keep conditional formatting?","\nApply the number format ",[41,1990,71],{},", which displays nothing for positive, negative and zero values while the values still drive the formatting.",[10,1993,1994,1997,1998,2001,2002,27],{},[1932,1995,1996],{},"How do I make data bars use a fixed 0 to 100% scale?","\nCreate the ",[41,1999,2000],{},"DataBarRule"," with ",[41,2003,2004],{},"start_type=\"num\", start_value=0, end_type=\"num\", end_value=1",[10,2006,2007,2010],{},[1932,2008,2009],{},"Should the waffle fill from the top or the bottom?","\nEither works; number the cells in the order they should fill. Filling from the bottom up reads naturally as \"filling up\".",[29,2012,2014],{"id":2013},"related","Related",[1926,2016,2017,2023,2030,2037],{},[1929,2018,2019,2020,2022],{},"Up one level: ",[23,2021,26],{"href":25}," — native Excel charts from Python.",[1929,2024,2025,2029],{},[23,2026,2028],{"href":2027},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fadd-data-bars-and-colour-scales-with-openpyxl\u002F","Add Data Bars and Colour Scales with openpyxl"," — data bars in depth.",[1929,2031,2032,2036],{},[23,2033,2035],{"href":2034},"\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"," — another single-value visual.",[1929,2038,2039,2043],{},[23,2040,2042],{"href":2041},"\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"," — headline figures.",[359,2045,2046],{},"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 .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":39,"searchDepth":214,"depth":214,"links":2048},[2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059],{"id":31,"depth":214,"text":32},{"id":64,"depth":214,"text":65},{"id":187,"depth":214,"text":188},{"id":932,"depth":214,"text":933},{"id":1189,"depth":214,"text":1190},{"id":1575,"depth":214,"text":1576},{"id":1842,"depth":214,"text":1843},{"id":1923,"depth":214,"text":1924},{"id":1965,"depth":214,"text":1966},{"id":1975,"depth":214,"text":1976},{"id":2013,"depth":214,"text":2014},"2026-10-02","Show progress towards 100% — share of a target reached, budget used, survey completion — as a waffle chart of 100 cells or as a progress bar, built from worksheet cells and conditional formatting with openpyxl rather than a chart object: number the grid so it fills from the bottom, colour cells with one formula rule driven by the percentage cell, hide the numbers, add data-bar progress bars with fixed 0–100% limits, and render the sheet to check.","md",[2064,2066,2068,2070],{"q":1981,"a":2065},"There is no waffle chart type, but a 10 × 10 grid of cells coloured by a conditional formatting rule does the job and updates automatically.",{"q":1987,"a":2067},"Apply the number format ;;;, which displays nothing for positive, negative and zero values while the values still drive the formatting.",{"q":1996,"a":2069},"Create the DataBarRule with start_type=\"num\", start_value=0, end_type=\"num\", end_value=1.",{"q":2009,"a":2071},"Either works; number the cells in the order they should fill. Filling from the bottom up reads naturally as \"filling up\".","waffle chart excel python",null,{"breadcrumb":2075},[2076,2079,2082],{"name":2077,"item":2078},"Home","\u002F",{"name":2080,"item":2081},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002F",{"name":26,"item":25},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-waffle-or-progress-chart-in-excel",{"title":2085,"description":2086},"Create a Waffle or Progress Chart in Excel with Python","Waffle charts and progress bars from cells with openpyxl: a 10×10 grid numbered from the bottom, one conditional formatting rule per waffle, hidden numbers, data bars with fixed limits and a render check.","create-a-waffle-or-progress-chart-in-excel","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-waffle-or-progress-chart-in-excel\u002Findex",[2090,2091],"charts-and-images","openpyxl","how-to","NR7Ys5yCca4KKgOPQjO_rJEXPVxNPcO2HeHjZ6P3-GU",[2095,2099],{"title":2096,"path":2097,"stem":2098,"children":-1},"Create a Stock Chart with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-stock-chart-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-stock-chart-with-openpyxl\u002Findex",{"title":2100,"path":2101,"stem":2102,"children":-1},"Create an Area Chart in Excel with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-an-area-chart-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-an-area-chart-with-openpyxl\u002Findex",[2104,2106],{"path":2105,"title":26},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl",{"path":2107,"title":2080},"\u002Fformatting-and-charting-excel-reports-with-python",[],[2110,2113,2116,2119,2122,2125],{"path":2111,"title":2112,"kind":2092},"\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":2114,"title":2115,"kind":2092},"\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":2117,"title":2118,"kind":2092},"\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":2120,"title":2121,"kind":2092},"\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":2123,"title":2124,"kind":2092},"\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":2126,"title":2127,"kind":2092},"\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",1790972111215]