[{"data":1,"prerenderedAt":2021},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fgroup-and-outline-rows-in-excel-with-openpyxl":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fgroup-and-outline-rows-in-excel-with-openpyxl":2012},{"id":4,"title":5,"body":6,"dateModified":1989,"datePublished":1989,"description":1990,"extension":1991,"faq":1992,"meta":2003,"navigation":226,"path":2004,"seo":2005,"slug":2008,"stem":2009,"type":2010,"__hash__":2011},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fgroup-and-outline-rows-in-excel-with-openpyxl\u002Findex.md","Group and Outline Rows in Excel with openpyxl",{"type":7,"value":8,"toc":1973},"minimark",[9,24,156,161,188,192,198,423,429,433,436,873,886,890,893,966,973,1047,1051,1054,1078,1081,1085,1088,1128,1135,1139,1142,1334,1337,1341,1344,1554,1560,1618,1622,1625,1759,1765,1801,1804,1808,1853,1857,1864,1868,1882,1886,1892,1898,1907,1917,1923,1927,1969],[10,11,12,13,17,18,23],"p",{},"A report that shows every row is honest but overwhelming; one that shows only totals hides the workings. Excel's outlining gives you both — totals visible, detail one click away — and openpyxl writes it in a single call per group. It suits exactly the reports that generated workbooks tend to be: a region with its months, a month with its transactions, a total with the rows behind it. This guide groups rows and columns, nests levels, sets the collapsed state, and pairs the outline with ",[14,15,16],"code",{},"SUBTOTAL"," formulas that respond to it. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002F","Creating Excel Tables and Autofilters with Python",".",[25,26,34,35,34,39,34,43,34,50,34,57,34,62,34,70,34,78,34,84,34,89,34,92,34,95,34,98,34,101,34,104,34,107,34,112,34,118,34,123,34,126,34,129,34,132,34,135,34,138,34,141,34,144,34,148,34,151],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"0 0 760 240","img",[30,31],"gr-t","gr-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[36,37,38],"title",{"id":30},"An outlined sheet, collapsed and expanded",[40,41,42],"desc",{"id":31},"Collapsed, the sheet shows one total row per region; expanded, the same sheet reveals the detail rows beneath each total, controlled from the outline margin.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","240","#ffffff",[51,52,56],"text",{"x":53,"y":54,"style":55},"196","28","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","collapsed — what opens first",[51,58,61],{"x":59,"y":54,"style":60},"566","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","expanded — one click later",[63,64],"line",{"x1":65,"y1":66,"x2":65,"y2":67,"stroke":68,"style":69},"380","40","222","var(--line,#cdd5e6)","stroke-width:1px",[44,71],{"x":72,"y":73,"width":74,"height":75,"rx":76,"fill":77,"stroke":68},"34","48","322","30","5","#d9f4f1",[51,79,83],{"x":80,"y":81,"style":82},"60","68","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","+",[51,85,88],{"x":86,"y":81,"style":87},"200","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","North total          128,400",[44,90],{"x":72,"y":91,"width":74,"height":75,"rx":76,"fill":77,"stroke":68},"82",[51,93,83],{"x":80,"y":94,"style":82},"102",[51,96,97],{"x":86,"y":94,"style":87},"South total           96,220",[44,99],{"x":72,"y":100,"width":74,"height":75,"rx":76,"fill":77,"stroke":68},"116",[51,102,83],{"x":80,"y":103,"style":82},"136",[51,105,106],{"x":86,"y":103,"style":87},"East total            51,130",[44,108],{"x":109,"y":73,"width":74,"height":110,"rx":76,"fill":111,"stroke":68},"404","26","#ebebfd",[51,113,117],{"x":114,"y":115,"style":116},"430","66","font-size:11px;fill:var(--muted,#5b6780);text-anchor:start","|",[51,119,122],{"x":120,"y":115,"style":121},"570","font-size:11px;fill:var(--text,#172033);text-anchor:middle","Jan                  41,100",[44,124],{"x":109,"y":125,"width":74,"height":110,"rx":76,"fill":111,"stroke":68},"78",[51,127,117],{"x":114,"y":128,"style":116},"96",[51,130,131],{"x":120,"y":128,"style":121},"Feb                  43,200",[44,133],{"x":109,"y":134,"width":74,"height":110,"rx":76,"fill":111,"stroke":68},"108",[51,136,117],{"x":114,"y":137,"style":116},"126",[51,139,140],{"x":120,"y":137,"style":121},"Mar                  44,100",[44,142],{"x":109,"y":143,"width":74,"height":75,"rx":76,"fill":77,"stroke":68},"138",[51,145,147],{"x":114,"y":146,"style":82},"158","-",[51,149,150],{"x":120,"y":146,"style":87},"North total         128,400",[51,152,155],{"x":65,"y":153,"style":154},"212","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","The detail is present in both — only its visibility differs",[157,158,160],"h2",{"id":159},"prerequisites","Prerequisites",[162,163,168],"pre",{"className":164,"code":165,"language":166,"meta":167,"style":167},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas\n","bash","",[14,169,170],{"__ignoreMap":167},[171,172,174,178,182,185],"span",{"class":63,"line":173},1,[171,175,177],{"class":176},"sMTad","pip",[171,179,181],{"class":180},"srMev"," install",[171,183,184],{"class":180}," openpyxl",[171,186,187],{"class":180}," pandas\n",[157,189,191],{"id":190},"group-a-block-of-rows","Group a block of rows",[10,193,194,197],{},[14,195,196],{},"group_rows"," takes the first and last row of the detail — not the summary row — and an outline level:",[162,199,203],{"className":200,"code":201,"language":202,"meta":167,"style":167},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\n\nwb = Workbook()\nws = wb.active\nws.title = \"Sales\"\n\nws.append([\"Region\", \"Month\", \"Revenue\"])\nws.append([\"North\", \"Jan\", 41100])\nws.append([\"North\", \"Feb\", 43200])\nws.append([\"North\", \"Mar\", 44100])\nws.append([\"North total\", \"\", \"=SUBTOTAL(109,C2:C4)\"])\n\nws.group_rows(2, 4, outline_level=1, hidden=True)\nwb.save(\"outline.xlsx\")\n","python",[14,204,205,221,228,240,251,262,267,290,311,330,349,369,374,412],{"__ignoreMap":167},[171,206,207,211,215,218],{"class":63,"line":173},[171,208,210],{"class":209},"s-kum","from",[171,212,214],{"class":213},"skGVy"," openpyxl ",[171,216,217],{"class":209},"import",[171,219,220],{"class":213}," Workbook\n",[171,222,224],{"class":63,"line":223},2,[171,225,227],{"emptyLinePlaceholder":226},true,"\n",[171,229,231,234,237],{"class":63,"line":230},3,[171,232,233],{"class":213},"wb ",[171,235,236],{"class":209},"=",[171,238,239],{"class":213}," Workbook()\n",[171,241,243,246,248],{"class":63,"line":242},4,[171,244,245],{"class":213},"ws ",[171,247,236],{"class":209},[171,249,250],{"class":213}," wb.active\n",[171,252,254,257,259],{"class":63,"line":253},5,[171,255,256],{"class":213},"ws.title ",[171,258,236],{"class":209},[171,260,261],{"class":180}," \"Sales\"\n",[171,263,265],{"class":63,"line":264},6,[171,266,227],{"emptyLinePlaceholder":226},[171,268,270,273,276,279,282,284,287],{"class":63,"line":269},7,[171,271,272],{"class":213},"ws.append([",[171,274,275],{"class":180},"\"Region\"",[171,277,278],{"class":213},", ",[171,280,281],{"class":180},"\"Month\"",[171,283,278],{"class":213},[171,285,286],{"class":180},"\"Revenue\"",[171,288,289],{"class":213},"])\n",[171,291,293,295,298,300,303,305,309],{"class":63,"line":292},8,[171,294,272],{"class":213},[171,296,297],{"class":180},"\"North\"",[171,299,278],{"class":213},[171,301,302],{"class":180},"\"Jan\"",[171,304,278],{"class":213},[171,306,308],{"class":307},"sP0c6","41100",[171,310,289],{"class":213},[171,312,314,316,318,320,323,325,328],{"class":63,"line":313},9,[171,315,272],{"class":213},[171,317,297],{"class":180},[171,319,278],{"class":213},[171,321,322],{"class":180},"\"Feb\"",[171,324,278],{"class":213},[171,326,327],{"class":307},"43200",[171,329,289],{"class":213},[171,331,333,335,337,339,342,344,347],{"class":63,"line":332},10,[171,334,272],{"class":213},[171,336,297],{"class":180},[171,338,278],{"class":213},[171,340,341],{"class":180},"\"Mar\"",[171,343,278],{"class":213},[171,345,346],{"class":307},"44100",[171,348,289],{"class":213},[171,350,352,354,357,359,362,364,367],{"class":63,"line":351},11,[171,353,272],{"class":213},[171,355,356],{"class":180},"\"North total\"",[171,358,278],{"class":213},[171,360,361],{"class":180},"\"\"",[171,363,278],{"class":213},[171,365,366],{"class":180},"\"=SUBTOTAL(109,C2:C4)\"",[171,368,289],{"class":213},[171,370,372],{"class":63,"line":371},12,[171,373,227],{"emptyLinePlaceholder":226},[171,375,377,380,383,385,388,390,394,396,399,401,404,406,409],{"class":63,"line":376},13,[171,378,379],{"class":213},"ws.group_rows(",[171,381,382],{"class":307},"2",[171,384,278],{"class":213},[171,386,387],{"class":307},"4",[171,389,278],{"class":213},[171,391,393],{"class":392},"sa561","outline_level",[171,395,236],{"class":209},[171,397,398],{"class":307},"1",[171,400,278],{"class":213},[171,402,403],{"class":392},"hidden",[171,405,236],{"class":209},[171,407,408],{"class":307},"True",[171,410,411],{"class":213},")\n",[171,413,415,418,421],{"class":63,"line":414},14,[171,416,417],{"class":213},"wb.save(",[171,419,420],{"class":180},"\"outline.xlsx\"",[171,422,411],{"class":213},[10,424,425,428],{},[14,426,427],{},"hidden=True"," makes the group start collapsed, which is usually what a summary report wants — a reader sees the totals and opens the detail they care about. Leave it out and the outline controls appear with everything expanded.",[157,430,432],{"id":431},"build-the-outline-from-grouped-data","Build the outline from grouped data",[10,434,435],{},"In practice the groups come from the data, so write each block and record where it started:",[162,437,439],{"className":200,"code":438,"language":202,"meta":167,"style":167},"import pandas as pd\nfrom openpyxl import Workbook\n\ndf = pd.DataFrame({\n    \"region\": [\"North\"] * 3 + [\"South\"] * 2 + [\"East\"] * 3,\n    \"month\": [\"Jan\", \"Feb\", \"Mar\", \"Jan\", \"Feb\", \"Jan\", \"Feb\", \"Mar\"],\n    \"revenue\": [41100, 43200, 44100, 48000, 48220, 16100, 17300, 17730],\n})\n\nwb = Workbook()\nws = wb.active\nws.title = \"Sales\"\nws.append([\"Region\", \"Month\", \"Revenue\"])\n\nfor region, group in df.groupby(\"region\", sort=True):\n    first = ws.max_row + 1\n    for row in group.itertuples():\n        ws.append([region, row.month, float(row.revenue)])\n    last = ws.max_row\n    ws.append([f\"{region} total\", \"\", f\"=SUBTOTAL(109,C{first}:C{last})\"])\n    ws.group_rows(first, last, outline_level=1, hidden=True)\n\nws.append([\"Grand total\", \"\", f\"=SUBTOTAL(109,C2:C{ws.max_row})\"])\nwb.save(\"outline.xlsx\")\n",[14,440,441,454,464,468,478,529,569,613,618,622,630,638,646,662,666,696,712,726,738,749,807,829,834,864],{"__ignoreMap":167},[171,442,443,445,448,451],{"class":63,"line":173},[171,444,217],{"class":209},[171,446,447],{"class":213}," pandas ",[171,449,450],{"class":209},"as",[171,452,453],{"class":213}," pd\n",[171,455,456,458,460,462],{"class":63,"line":223},[171,457,210],{"class":209},[171,459,214],{"class":213},[171,461,217],{"class":209},[171,463,220],{"class":213},[171,465,466],{"class":63,"line":230},[171,467,227],{"emptyLinePlaceholder":226},[171,469,470,473,475],{"class":63,"line":242},[171,471,472],{"class":213},"df ",[171,474,236],{"class":209},[171,476,477],{"class":213}," pd.DataFrame({\n",[171,479,480,483,486,488,491,494,497,500,503,506,508,510,513,515,517,520,522,524,526],{"class":63,"line":253},[171,481,482],{"class":180},"    \"region\"",[171,484,485],{"class":213},": [",[171,487,297],{"class":180},[171,489,490],{"class":213},"] ",[171,492,493],{"class":209},"*",[171,495,496],{"class":307}," 3",[171,498,499],{"class":209}," +",[171,501,502],{"class":213}," [",[171,504,505],{"class":180},"\"South\"",[171,507,490],{"class":213},[171,509,493],{"class":209},[171,511,512],{"class":307}," 2",[171,514,499],{"class":209},[171,516,502],{"class":213},[171,518,519],{"class":180},"\"East\"",[171,521,490],{"class":213},[171,523,493],{"class":209},[171,525,496],{"class":307},[171,527,528],{"class":213},",\n",[171,530,531,534,536,538,540,542,544,546,548,550,552,554,556,558,560,562,564,566],{"class":63,"line":264},[171,532,533],{"class":180},"    \"month\"",[171,535,485],{"class":213},[171,537,302],{"class":180},[171,539,278],{"class":213},[171,541,322],{"class":180},[171,543,278],{"class":213},[171,545,341],{"class":180},[171,547,278],{"class":213},[171,549,302],{"class":180},[171,551,278],{"class":213},[171,553,322],{"class":180},[171,555,278],{"class":213},[171,557,302],{"class":180},[171,559,278],{"class":213},[171,561,322],{"class":180},[171,563,278],{"class":213},[171,565,341],{"class":180},[171,567,568],{"class":213},"],\n",[171,570,571,574,576,578,580,582,584,586,588,591,593,596,598,601,603,606,608,611],{"class":63,"line":269},[171,572,573],{"class":180},"    \"revenue\"",[171,575,485],{"class":213},[171,577,308],{"class":307},[171,579,278],{"class":213},[171,581,327],{"class":307},[171,583,278],{"class":213},[171,585,346],{"class":307},[171,587,278],{"class":213},[171,589,590],{"class":307},"48000",[171,592,278],{"class":213},[171,594,595],{"class":307},"48220",[171,597,278],{"class":213},[171,599,600],{"class":307},"16100",[171,602,278],{"class":213},[171,604,605],{"class":307},"17300",[171,607,278],{"class":213},[171,609,610],{"class":307},"17730",[171,612,568],{"class":213},[171,614,615],{"class":63,"line":292},[171,616,617],{"class":213},"})\n",[171,619,620],{"class":63,"line":313},[171,621,227],{"emptyLinePlaceholder":226},[171,623,624,626,628],{"class":63,"line":332},[171,625,233],{"class":213},[171,627,236],{"class":209},[171,629,239],{"class":213},[171,631,632,634,636],{"class":63,"line":351},[171,633,245],{"class":213},[171,635,236],{"class":209},[171,637,250],{"class":213},[171,639,640,642,644],{"class":63,"line":371},[171,641,256],{"class":213},[171,643,236],{"class":209},[171,645,261],{"class":180},[171,647,648,650,652,654,656,658,660],{"class":63,"line":376},[171,649,272],{"class":213},[171,651,275],{"class":180},[171,653,278],{"class":213},[171,655,281],{"class":180},[171,657,278],{"class":213},[171,659,286],{"class":180},[171,661,289],{"class":213},[171,663,664],{"class":63,"line":414},[171,665,227],{"emptyLinePlaceholder":226},[171,667,669,672,675,678,681,684,686,689,691,693],{"class":63,"line":668},15,[171,670,671],{"class":209},"for",[171,673,674],{"class":213}," region, group ",[171,676,677],{"class":209},"in",[171,679,680],{"class":213}," df.groupby(",[171,682,683],{"class":180},"\"region\"",[171,685,278],{"class":213},[171,687,688],{"class":392},"sort",[171,690,236],{"class":209},[171,692,408],{"class":307},[171,694,695],{"class":213},"):\n",[171,697,699,702,704,707,709],{"class":63,"line":698},16,[171,700,701],{"class":213},"    first ",[171,703,236],{"class":209},[171,705,706],{"class":213}," ws.max_row ",[171,708,83],{"class":209},[171,710,711],{"class":307}," 1\n",[171,713,715,718,721,723],{"class":63,"line":714},17,[171,716,717],{"class":209},"    for",[171,719,720],{"class":213}," row ",[171,722,677],{"class":209},[171,724,725],{"class":213}," group.itertuples():\n",[171,727,729,732,735],{"class":63,"line":728},18,[171,730,731],{"class":213},"        ws.append([region, row.month, ",[171,733,734],{"class":307},"float",[171,736,737],{"class":213},"(row.revenue)])\n",[171,739,741,744,746],{"class":63,"line":740},19,[171,742,743],{"class":213},"    last ",[171,745,236],{"class":209},[171,747,748],{"class":213}," ws.max_row\n",[171,750,752,755,758,761,765,768,771,774,776,778,780,782,785,787,790,792,795,797,800,802,805],{"class":63,"line":751},20,[171,753,754],{"class":213},"    ws.append([",[171,756,757],{"class":209},"f",[171,759,760],{"class":180},"\"",[171,762,764],{"class":763},"sSjpA","{",[171,766,767],{"class":213},"region",[171,769,770],{"class":763},"}",[171,772,773],{"class":180}," total\"",[171,775,278],{"class":213},[171,777,361],{"class":180},[171,779,278],{"class":213},[171,781,757],{"class":209},[171,783,784],{"class":180},"\"=SUBTOTAL(109,C",[171,786,764],{"class":763},[171,788,789],{"class":213},"first",[171,791,770],{"class":763},[171,793,794],{"class":180},":C",[171,796,764],{"class":763},[171,798,799],{"class":213},"last",[171,801,770],{"class":763},[171,803,804],{"class":180},")\"",[171,806,289],{"class":213},[171,808,810,813,815,817,819,821,823,825,827],{"class":63,"line":809},21,[171,811,812],{"class":213},"    ws.group_rows(first, last, ",[171,814,393],{"class":392},[171,816,236],{"class":209},[171,818,398],{"class":307},[171,820,278],{"class":213},[171,822,403],{"class":392},[171,824,236],{"class":209},[171,826,408],{"class":307},[171,828,411],{"class":213},[171,830,832],{"class":63,"line":831},22,[171,833,227],{"emptyLinePlaceholder":226},[171,835,837,839,842,844,846,848,850,853,855,858,860,862],{"class":63,"line":836},23,[171,838,272],{"class":213},[171,840,841],{"class":180},"\"Grand total\"",[171,843,278],{"class":213},[171,845,361],{"class":180},[171,847,278],{"class":213},[171,849,757],{"class":209},[171,851,852],{"class":180},"\"=SUBTOTAL(109,C2:C",[171,854,764],{"class":763},[171,856,857],{"class":213},"ws.max_row",[171,859,770],{"class":763},[171,861,804],{"class":180},[171,863,289],{"class":213},[171,865,867,869,871],{"class":63,"line":866},24,[171,868,417],{"class":213},[171,870,420],{"class":180},[171,872,411],{"class":213},[10,874,875,878,879,882,883,885],{},[14,876,877],{},"SUBTOTAL(109, …)"," rather than ",[14,880,881],{},"SUM"," is deliberate: function 109 ignores rows hidden by a filter, and — crucially — nested ",[14,884,16],{}," results, so the grand total does not double-count the per-region totals sitting inside its range.",[157,887,889],{"id":888},"nest-a-second-level","Nest a second level",[10,891,892],{},"Levels are just integers, and a deeper group inside a shallower one nests automatically:",[162,894,896],{"className":200,"code":895,"language":202,"meta":167,"style":167},"ws.group_rows(3, 6, outline_level=2, hidden=True)     # weeks inside a month\nws.group_rows(2, 12, outline_level=1, hidden=True)    # months inside a region\n",[14,897,898,933],{"__ignoreMap":167},[171,899,900,902,905,907,910,912,914,916,918,920,922,924,926,929],{"class":63,"line":173},[171,901,379],{"class":213},[171,903,904],{"class":307},"3",[171,906,278],{"class":213},[171,908,909],{"class":307},"6",[171,911,278],{"class":213},[171,913,393],{"class":392},[171,915,236],{"class":209},[171,917,382],{"class":307},[171,919,278],{"class":213},[171,921,403],{"class":392},[171,923,236],{"class":209},[171,925,408],{"class":307},[171,927,928],{"class":213},")     ",[171,930,932],{"class":931},"s-wDw","# weeks inside a month\n",[171,934,935,937,939,941,944,946,948,950,952,954,956,958,960,963],{"class":63,"line":223},[171,936,379],{"class":213},[171,938,382],{"class":307},[171,940,278],{"class":213},[171,942,943],{"class":307},"12",[171,945,278],{"class":213},[171,947,393],{"class":392},[171,949,236],{"class":209},[171,951,398],{"class":307},[171,953,278],{"class":213},[171,955,403],{"class":392},[171,957,236],{"class":209},[171,959,408],{"class":307},[171,961,962],{"class":213},")    ",[171,964,965],{"class":931},"# months inside a region\n",[10,967,968,969,972],{},"Excel then shows numbered buttons ",[14,970,971],{},"1 2 3"," in the margin, so a reader can jump the whole sheet to a level rather than opening groups one at a time. Two levels is usually the practical limit for comprehension; the eight Excel allows is a specification, not a recommendation.",[25,974,34,979,34,982,34,985,34,988,34,992,34,997,34,1003,34,1007,34,1010,34,1014,34,1017,34,1020,34,1024,34,1028,34,1030,34,1034,34,1037,34,1040,34,1044],{"viewBox":975,"role":28,"ariaLabelledBy":976,"xmlns":32,"style":33},"0 0 760 214",[977,978],"gr2-t","gr2-d",[36,980,981],{"id":977},"How outline levels map onto the rows",[40,983,984],{"id":978},"Level one groups the months within a region, level two groups the weeks within a month, and the summary row for each group sits below its detail by default.",[44,986],{"x":46,"y":46,"width":47,"height":987,"fill":49},"214",[51,989,991],{"x":65,"y":110,"style":990},"font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","Two nested levels, one summary row each",[44,993],{"x":80,"y":994,"width":995,"height":110,"rx":76,"fill":996,"stroke":68},"46","640","#f0f2f5",[51,998,1002],{"x":999,"y":1000,"style":1001},"90","64","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:start","row 2",[51,1004,1006],{"x":65,"y":1000,"style":1005},"font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","week 1 detail — level 2",[44,1008],{"x":80,"y":1009,"width":995,"height":110,"rx":76,"fill":996,"stroke":68},"76",[51,1011,1013],{"x":999,"y":1012,"style":1001},"94","row 3",[51,1015,1016],{"x":65,"y":1012,"style":1005},"week 2 detail — level 2",[44,1018],{"x":80,"y":1019,"width":995,"height":110,"rx":76,"fill":111,"stroke":68},"106",[51,1021,1023],{"x":999,"y":1022,"style":1001},"124","row 4",[51,1025,1027],{"x":65,"y":1022,"style":1026},"font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","January total — closes level 2",[44,1029],{"x":80,"y":103,"width":995,"height":110,"rx":76,"fill":996,"stroke":68},[51,1031,1033],{"x":999,"y":1032,"style":1001},"154","row 5",[51,1035,1036],{"x":65,"y":1032,"style":1005},"February detail — level 2",[44,1038],{"x":80,"y":1039,"width":995,"height":75,"rx":76,"fill":77,"stroke":68},"166",[51,1041,1043],{"x":999,"y":1042,"style":1001},"186","row 6",[51,1045,1046],{"x":65,"y":1042,"style":87},"North total — closes level 1",[157,1048,1050],{"id":1049},"put-the-summary-above-the-detail","Put the summary above the detail",[10,1052,1053],{},"Some report conventions put the total first. Excel assumes the opposite, so tell it:",[162,1055,1057],{"className":200,"code":1056,"language":202,"meta":167,"style":167},"ws.sheet_properties.outlinePr.summaryBelow = False\nws.sheet_properties.outlinePr.summaryRight = False\n",[14,1058,1059,1069],{"__ignoreMap":167},[171,1060,1061,1064,1066],{"class":63,"line":173},[171,1062,1063],{"class":213},"ws.sheet_properties.outlinePr.summaryBelow ",[171,1065,236],{"class":209},[171,1067,1068],{"class":307}," False\n",[171,1070,1071,1074,1076],{"class":63,"line":223},[171,1072,1073],{"class":213},"ws.sheet_properties.outlinePr.summaryRight ",[171,1075,236],{"class":209},[171,1077,1068],{"class":307},[10,1079,1080],{},"Set this once per sheet, before or after the grouping — it changes where Excel draws the outline bracket and which row the collapse control attaches to. Getting it wrong produces the confusing result where clicking the control hides the total instead of the detail.",[157,1082,1084],{"id":1083},"group-columns","Group columns",[10,1086,1087],{},"The same idea horizontally, using column letters, is how you hide twelve monthly columns behind a total:",[162,1089,1091],{"className":200,"code":1090,"language":202,"meta":167,"style":167},"ws.group_columns(\"B\", \"M\", outline_level=1, hidden=True)   # Jan–Dec\n",[14,1092,1093],{"__ignoreMap":167},[171,1094,1095,1098,1101,1103,1106,1108,1110,1112,1114,1116,1118,1120,1122,1125],{"class":63,"line":173},[171,1096,1097],{"class":213},"ws.group_columns(",[171,1099,1100],{"class":180},"\"B\"",[171,1102,278],{"class":213},[171,1104,1105],{"class":180},"\"M\"",[171,1107,278],{"class":213},[171,1109,393],{"class":392},[171,1111,236],{"class":209},[171,1113,398],{"class":307},[171,1115,278],{"class":213},[171,1117,403],{"class":392},[171,1119,236],{"class":209},[171,1121,408],{"class":307},[171,1123,1124],{"class":213},")   ",[171,1126,1127],{"class":931},"# Jan–Dec\n",[10,1129,1130,1131,23],{},"The column holding the annual total sits outside the group, so it stays visible when the months are collapsed. This pairs well with a sparkline column — see ",[19,1132,1134],{"href":1133},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fadd-sparklines-to-an-excel-report-with-xlsxwriter\u002F","Add sparklines to an Excel report with xlsxwriter",[157,1136,1138],{"id":1137},"apply-grouping-after-writing-with-pandas","Apply grouping after writing with pandas",[10,1140,1141],{},"pandas has no concept of outlines, so the sequence is: write with pandas, reopen with openpyxl, group:",[162,1143,1145],{"className":200,"code":1144,"language":202,"meta":167,"style":167},"import pandas as pd\nfrom openpyxl import load_workbook\n\ndf.to_excel(\"sales.xlsx\", index=False, sheet_name=\"Sales\")\n\nwb = load_workbook(\"sales.xlsx\")\nws = wb[\"Sales\"]\n\nstart = 2\nfor region, group in df.groupby(\"region\", sort=False):\n    end = start + len(group) - 1\n    ws.group_rows(start, end, outline_level=1, hidden=True)\n    start = end + 1\nwb.save(\"sales.xlsx\")\n",[14,1146,1147,1157,1168,1172,1202,1206,1219,1233,1237,1247,1269,1291,1312,1326],{"__ignoreMap":167},[171,1148,1149,1151,1153,1155],{"class":63,"line":173},[171,1150,217],{"class":209},[171,1152,447],{"class":213},[171,1154,450],{"class":209},[171,1156,453],{"class":213},[171,1158,1159,1161,1163,1165],{"class":63,"line":223},[171,1160,210],{"class":209},[171,1162,214],{"class":213},[171,1164,217],{"class":209},[171,1166,1167],{"class":213}," load_workbook\n",[171,1169,1170],{"class":63,"line":230},[171,1171,227],{"emptyLinePlaceholder":226},[171,1173,1174,1177,1180,1182,1185,1187,1190,1192,1195,1197,1200],{"class":63,"line":242},[171,1175,1176],{"class":213},"df.to_excel(",[171,1178,1179],{"class":180},"\"sales.xlsx\"",[171,1181,278],{"class":213},[171,1183,1184],{"class":392},"index",[171,1186,236],{"class":209},[171,1188,1189],{"class":307},"False",[171,1191,278],{"class":213},[171,1193,1194],{"class":392},"sheet_name",[171,1196,236],{"class":209},[171,1198,1199],{"class":180},"\"Sales\"",[171,1201,411],{"class":213},[171,1203,1204],{"class":63,"line":253},[171,1205,227],{"emptyLinePlaceholder":226},[171,1207,1208,1210,1212,1215,1217],{"class":63,"line":264},[171,1209,233],{"class":213},[171,1211,236],{"class":209},[171,1213,1214],{"class":213}," load_workbook(",[171,1216,1179],{"class":180},[171,1218,411],{"class":213},[171,1220,1221,1223,1225,1228,1230],{"class":63,"line":269},[171,1222,245],{"class":213},[171,1224,236],{"class":209},[171,1226,1227],{"class":213}," wb[",[171,1229,1199],{"class":180},[171,1231,1232],{"class":213},"]\n",[171,1234,1235],{"class":63,"line":292},[171,1236,227],{"emptyLinePlaceholder":226},[171,1238,1239,1242,1244],{"class":63,"line":313},[171,1240,1241],{"class":213},"start ",[171,1243,236],{"class":209},[171,1245,1246],{"class":307}," 2\n",[171,1248,1249,1251,1253,1255,1257,1259,1261,1263,1265,1267],{"class":63,"line":332},[171,1250,671],{"class":209},[171,1252,674],{"class":213},[171,1254,677],{"class":209},[171,1256,680],{"class":213},[171,1258,683],{"class":180},[171,1260,278],{"class":213},[171,1262,688],{"class":392},[171,1264,236],{"class":209},[171,1266,1189],{"class":307},[171,1268,695],{"class":213},[171,1270,1271,1274,1276,1279,1281,1284,1287,1289],{"class":63,"line":351},[171,1272,1273],{"class":213},"    end ",[171,1275,236],{"class":209},[171,1277,1278],{"class":213}," start ",[171,1280,83],{"class":209},[171,1282,1283],{"class":307}," len",[171,1285,1286],{"class":213},"(group) ",[171,1288,147],{"class":209},[171,1290,711],{"class":307},[171,1292,1293,1296,1298,1300,1302,1304,1306,1308,1310],{"class":63,"line":371},[171,1294,1295],{"class":213},"    ws.group_rows(start, end, ",[171,1297,393],{"class":392},[171,1299,236],{"class":209},[171,1301,398],{"class":307},[171,1303,278],{"class":213},[171,1305,403],{"class":392},[171,1307,236],{"class":209},[171,1309,408],{"class":307},[171,1311,411],{"class":213},[171,1313,1314,1317,1319,1322,1324],{"class":63,"line":376},[171,1315,1316],{"class":213},"    start ",[171,1318,236],{"class":209},[171,1320,1321],{"class":213}," end ",[171,1323,83],{"class":209},[171,1325,711],{"class":307},[171,1327,1328,1330,1332],{"class":63,"line":414},[171,1329,417],{"class":213},[171,1331,1179],{"class":180},[171,1333,411],{"class":213},[10,1335,1336],{},"Because the frame was written in its existing order, the row arithmetic is a simple running offset — but only if the DataFrame is sorted by the grouping column first. Sort it before writing, or the groups will interleave and the outline will be nonsense.",[157,1338,1340],{"id":1339},"style-the-summary-rows-so-the-structure-reads","Style the summary rows so the structure reads",[10,1342,1343],{},"An outline tells Excel which rows belong together; formatting tells the reader. Give each summary row a fill and a bold font, and indent the detail so the hierarchy is visible even when everything is expanded:",[162,1345,1347],{"className":200,"code":1346,"language":202,"meta":167,"style":167},"from openpyxl.styles import Alignment, Font, PatternFill\n\ntotal_fill = PatternFill(\"solid\", start_color=\"DDEBF7\")\nbold = Font(bold=True)\n\nfor row in ws.iter_rows(min_row=2, max_row=ws.max_row):\n    label = row[0].value or \"\"\n    is_total = str(label).endswith(\"total\")\n    for cell in row:\n        if is_total:\n            cell.fill = total_fill\n            cell.font = bold\n    if not is_total:\n        row[0].alignment = Alignment(indent=1)\n",[14,1348,1349,1361,1365,1390,1409,1413,1441,1462,1480,1492,1500,1510,1520,1530],{"__ignoreMap":167},[171,1350,1351,1353,1356,1358],{"class":63,"line":173},[171,1352,210],{"class":209},[171,1354,1355],{"class":213}," openpyxl.styles ",[171,1357,217],{"class":209},[171,1359,1360],{"class":213}," Alignment, Font, PatternFill\n",[171,1362,1363],{"class":63,"line":223},[171,1364,227],{"emptyLinePlaceholder":226},[171,1366,1367,1370,1372,1375,1378,1380,1383,1385,1388],{"class":63,"line":230},[171,1368,1369],{"class":213},"total_fill ",[171,1371,236],{"class":209},[171,1373,1374],{"class":213}," PatternFill(",[171,1376,1377],{"class":180},"\"solid\"",[171,1379,278],{"class":213},[171,1381,1382],{"class":392},"start_color",[171,1384,236],{"class":209},[171,1386,1387],{"class":180},"\"DDEBF7\"",[171,1389,411],{"class":213},[171,1391,1392,1395,1397,1400,1403,1405,1407],{"class":63,"line":242},[171,1393,1394],{"class":213},"bold ",[171,1396,236],{"class":209},[171,1398,1399],{"class":213}," Font(",[171,1401,1402],{"class":392},"bold",[171,1404,236],{"class":209},[171,1406,408],{"class":307},[171,1408,411],{"class":213},[171,1410,1411],{"class":63,"line":253},[171,1412,227],{"emptyLinePlaceholder":226},[171,1414,1415,1417,1419,1421,1424,1427,1429,1431,1433,1436,1438],{"class":63,"line":264},[171,1416,671],{"class":209},[171,1418,720],{"class":213},[171,1420,677],{"class":209},[171,1422,1423],{"class":213}," ws.iter_rows(",[171,1425,1426],{"class":392},"min_row",[171,1428,236],{"class":209},[171,1430,382],{"class":307},[171,1432,278],{"class":213},[171,1434,1435],{"class":392},"max_row",[171,1437,236],{"class":209},[171,1439,1440],{"class":213},"ws.max_row):\n",[171,1442,1443,1446,1448,1451,1453,1456,1459],{"class":63,"line":269},[171,1444,1445],{"class":213},"    label ",[171,1447,236],{"class":209},[171,1449,1450],{"class":213}," row[",[171,1452,46],{"class":307},[171,1454,1455],{"class":213},"].value ",[171,1457,1458],{"class":209},"or",[171,1460,1461],{"class":180}," \"\"\n",[171,1463,1464,1467,1469,1472,1475,1478],{"class":63,"line":292},[171,1465,1466],{"class":213},"    is_total ",[171,1468,236],{"class":209},[171,1470,1471],{"class":307}," str",[171,1473,1474],{"class":213},"(label).endswith(",[171,1476,1477],{"class":180},"\"total\"",[171,1479,411],{"class":213},[171,1481,1482,1484,1487,1489],{"class":63,"line":313},[171,1483,717],{"class":209},[171,1485,1486],{"class":213}," cell ",[171,1488,677],{"class":209},[171,1490,1491],{"class":213}," row:\n",[171,1493,1494,1497],{"class":63,"line":332},[171,1495,1496],{"class":209},"        if",[171,1498,1499],{"class":213}," is_total:\n",[171,1501,1502,1505,1507],{"class":63,"line":351},[171,1503,1504],{"class":213},"            cell.fill ",[171,1506,236],{"class":209},[171,1508,1509],{"class":213}," total_fill\n",[171,1511,1512,1515,1517],{"class":63,"line":371},[171,1513,1514],{"class":213},"            cell.font ",[171,1516,236],{"class":209},[171,1518,1519],{"class":213}," bold\n",[171,1521,1522,1525,1528],{"class":63,"line":376},[171,1523,1524],{"class":209},"    if",[171,1526,1527],{"class":209}," not",[171,1529,1499],{"class":213},[171,1531,1532,1535,1537,1540,1542,1545,1548,1550,1552],{"class":63,"line":414},[171,1533,1534],{"class":213},"        row[",[171,1536,46],{"class":307},[171,1538,1539],{"class":213},"].alignment ",[171,1541,236],{"class":209},[171,1543,1544],{"class":213}," Alignment(",[171,1546,1547],{"class":392},"indent",[171,1549,236],{"class":209},[171,1551,398],{"class":307},[171,1553,411],{"class":213},[10,1555,1556,1559],{},[14,1557,1558],{},"Alignment(indent=1)"," shifts the detail labels one step right, which does more for legibility than any amount of colour. Keep the indent small — two steps already looks like a mistake on a printed page.",[25,1561,34,1566,34,1569,34,1572,34,1575,34,1578,34,1580,34,1586,34,1591,34,1593,34,1597,34,1600,34,1604,34,1610,34,1614],{"viewBox":1562,"role":28,"ariaLabelledBy":1563,"xmlns":32,"style":33},"0 0 760 202",[1564,1565],"gr3-t","gr3-d",[36,1567,1568],{"id":1564},"Formatting that makes the outline legible on paper",[40,1570,1571],{"id":1565},"Indenting detail labels and filling summary rows keeps the hierarchy visible when the sheet is fully expanded or printed, where the outline controls do not appear.",[44,1573],{"x":46,"y":46,"width":47,"height":1574,"fill":49},"202",[51,1576,1577],{"x":65,"y":110,"style":990},"Outline controls do not print — formatting does",[44,1579],{"x":80,"y":994,"width":995,"height":54,"rx":76,"fill":996,"stroke":68},[51,1581,1585],{"x":1582,"y":1583,"style":1584},"110","65","font-size:11.5px;fill:var(--text,#172033);text-anchor:start","Jan",[51,1587,1590],{"x":1588,"y":1583,"style":1589},"660","font-size:11.5px;fill:var(--text,#172033);text-anchor:end","41,100",[44,1592],{"x":80,"y":125,"width":995,"height":54,"rx":76,"fill":996,"stroke":68},[51,1594,1596],{"x":1582,"y":1595,"style":1584},"97","Feb",[51,1598,1599],{"x":1588,"y":1595,"style":1589},"43,200",[44,1601],{"x":80,"y":1582,"width":995,"height":1602,"rx":76,"fill":1603,"stroke":68},"32","#DDEBF7",[51,1605,1609],{"x":1606,"y":1607,"style":1608},"86","131","font-size:12px;font-weight:700;fill:#172033;text-anchor:start","North total",[51,1611,1613],{"x":1588,"y":1607,"style":1612},"font-size:12px;font-weight:700;fill:#172033;text-anchor:end","128,400",[51,1615,1617],{"x":65,"y":1616,"style":154},"180","Indented detail, filled and bold totals — readable with no outline at all",[157,1619,1621],{"id":1620},"read-an-existing-outline-back","Read an existing outline back",[10,1623,1624],{},"When a workbook arrives already outlined — from a colleague, or from a previous run — you can inspect and rebuild the structure rather than guessing at it:",[162,1626,1628],{"className":200,"code":1627,"language":202,"meta":167,"style":167},"from openpyxl import load_workbook\n\nwb = load_workbook(\"sales.xlsx\")\nws = wb[\"Sales\"]\n\nfor row_index, dim in sorted(ws.row_dimensions.items()):\n    if dim.outlineLevel:\n        state = \"collapsed\" if dim.hidden else \"expanded\"\n        print(f\"row {row_index}: level {dim.outlineLevel}, {state}\")\n",[14,1629,1630,1640,1644,1656,1668,1672,1687,1694,1716],{"__ignoreMap":167},[171,1631,1632,1634,1636,1638],{"class":63,"line":173},[171,1633,210],{"class":209},[171,1635,214],{"class":213},[171,1637,217],{"class":209},[171,1639,1167],{"class":213},[171,1641,1642],{"class":63,"line":223},[171,1643,227],{"emptyLinePlaceholder":226},[171,1645,1646,1648,1650,1652,1654],{"class":63,"line":230},[171,1647,233],{"class":213},[171,1649,236],{"class":209},[171,1651,1214],{"class":213},[171,1653,1179],{"class":180},[171,1655,411],{"class":213},[171,1657,1658,1660,1662,1664,1666],{"class":63,"line":242},[171,1659,245],{"class":213},[171,1661,236],{"class":209},[171,1663,1227],{"class":213},[171,1665,1199],{"class":180},[171,1667,1232],{"class":213},[171,1669,1670],{"class":63,"line":253},[171,1671,227],{"emptyLinePlaceholder":226},[171,1673,1674,1676,1679,1681,1684],{"class":63,"line":264},[171,1675,671],{"class":209},[171,1677,1678],{"class":213}," row_index, dim ",[171,1680,677],{"class":209},[171,1682,1683],{"class":307}," sorted",[171,1685,1686],{"class":213},"(ws.row_dimensions.items()):\n",[171,1688,1689,1691],{"class":63,"line":269},[171,1690,1524],{"class":209},[171,1692,1693],{"class":213}," dim.outlineLevel:\n",[171,1695,1696,1699,1701,1704,1707,1710,1713],{"class":63,"line":292},[171,1697,1698],{"class":213},"        state ",[171,1700,236],{"class":209},[171,1702,1703],{"class":180}," \"collapsed\"",[171,1705,1706],{"class":209}," if",[171,1708,1709],{"class":213}," dim.hidden ",[171,1711,1712],{"class":209},"else",[171,1714,1715],{"class":180}," \"expanded\"\n",[171,1717,1718,1721,1724,1726,1729,1731,1734,1736,1739,1741,1744,1746,1748,1750,1753,1755,1757],{"class":63,"line":313},[171,1719,1720],{"class":307},"        print",[171,1722,1723],{"class":213},"(",[171,1725,757],{"class":209},[171,1727,1728],{"class":180},"\"row ",[171,1730,764],{"class":763},[171,1732,1733],{"class":213},"row_index",[171,1735,770],{"class":763},[171,1737,1738],{"class":180},": level ",[171,1740,764],{"class":763},[171,1742,1743],{"class":213},"dim.outlineLevel",[171,1745,770],{"class":763},[171,1747,278],{"class":180},[171,1749,764],{"class":763},[171,1751,1752],{"class":213},"state",[171,1754,770],{"class":763},[171,1756,760],{"class":180},[171,1758,411],{"class":213},[10,1760,1761,1764],{},[14,1762,1763],{},"row_dimensions"," only holds entries for rows that carry some property, so the loop is cheap even on a large sheet. Two practical uses: flipping every group to expanded before a PDF export, since collapsed rows do not print, and clearing an inherited outline before applying your own:",[162,1766,1768],{"className":200,"code":1767,"language":202,"meta":167,"style":167},"for dim in ws.row_dimensions.values():\n    dim.outlineLevel = 0\n    dim.hidden = False\n",[14,1769,1770,1782,1792],{"__ignoreMap":167},[171,1771,1772,1774,1777,1779],{"class":63,"line":173},[171,1773,671],{"class":209},[171,1775,1776],{"class":213}," dim ",[171,1778,677],{"class":209},[171,1780,1781],{"class":213}," ws.row_dimensions.values():\n",[171,1783,1784,1787,1789],{"class":63,"line":223},[171,1785,1786],{"class":213},"    dim.outlineLevel ",[171,1788,236],{"class":209},[171,1790,1791],{"class":307}," 0\n",[171,1793,1794,1797,1799],{"class":63,"line":230},[171,1795,1796],{"class":213},"    dim.hidden ",[171,1798,236],{"class":209},[171,1800,1068],{"class":307},[10,1802,1803],{},"The PDF point is worth emphasising — a report delivered collapsed prints as totals only, which is either exactly what you wanted or a missing appendix nobody notices until the meeting.",[157,1805,1807],{"id":1806},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[1809,1810,1811,1819,1825,1838,1847],"ul",{},[1812,1813,1814,1818],"li",{},[1815,1816,1817],"strong",{},"Including the summary row in the group."," Group the detail only, or collapsing hides the total too.",[1812,1820,1821,1824],{},[1815,1822,1823],{},"Unsorted data."," Groups must be contiguous rows; sort by the grouping column before writing.",[1812,1826,1827,1834,1835,1837],{},[1815,1828,1829,1831,1832,23],{},[14,1830,881],{}," instead of ",[14,1833,16],{}," A grand total using ",[14,1836,881],{}," over a range containing per-group totals double-counts everything.",[1812,1839,1840,1846],{},[1815,1841,1842,1843,23],{},"Forgetting ",[14,1844,1845],{},"summaryBelow"," If your totals are above the detail, the outline controls attach to the wrong rows.",[1812,1848,1849,1852],{},[1815,1850,1851],{},"Expecting the outline to survive pandas."," Reading the file back with pandas gives values only; re-writing it drops the outline.",[157,1854,1856],{"id":1855},"performance-and-scale-notes","Performance and scale notes",[10,1858,1859,1860,23],{},"Grouping is metadata — a level number per row — so it costs nothing at write time and nothing in file size. What it costs is Excel's rendering when a sheet has thousands of groups: opening such a file is noticeably slower, and the outline margin becomes a wall of brackets. Keep groups meaningful and few, typically one per business grouping rather than one per row. If a sheet needs more than a hundred or so, the report probably wants a summary sheet with links into detail sheets instead — the structure in ",[19,1861,1863],{"href":1862},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-table-of-contents-sheet-with-hyperlinks-in-excel\u002F","Add a table of contents sheet with hyperlinks in Excel",[157,1865,1867],{"id":1866},"conclusion","Conclusion",[10,1869,1870,1872,1873,1875,1876,1878,1879,1881],{},[14,1871,196],{}," turns a long report into a summary a reader can drill into, without removing anything. Group the detail rows and leave the summary row outside the group, start collapsed with ",[14,1874,427],{},", use ",[14,1877,877],{}," so nested totals do not double-count, and set ",[14,1880,1845],{}," to match your layout. Apply the grouping as a final openpyxl pass after pandas has written the data, and sort by the grouping column first so every group is contiguous.",[157,1883,1885],{"id":1884},"frequently-asked-questions","Frequently asked questions",[10,1887,1888,1891],{},[1815,1889,1890],{},"What is the difference between grouping and hiding rows?","\nHiding removes rows from view with no way back except unhiding them. Grouping adds an outline control in the margin, so a reader collapses and expands the detail themselves — the report stays complete but starts summarised.",[10,1893,1894,1897],{},[1815,1895,1896],{},"How many levels can I nest?","\nExcel supports up to eight outline levels. In practice two or three — region, then month, then detail — is as deep as a reader will follow.",[10,1899,1900,1903,1904,23],{},[1815,1901,1902],{},"Why do my collapse controls appear on the wrong side?","\nThe outline's summary row is assumed to be below the group by default. If your totals sit above the detail, set ",[14,1905,1906],{},"ws.sheet_properties.outlinePr.summaryBelow = False",[10,1908,1909,1912,1913,1916],{},[1815,1910,1911],{},"Can I group columns too?","\nYes, with ",[14,1914,1915],{},"group_columns",", using column letters. It is the usual way to hide a block of monthly columns behind a total.",[10,1918,1919,1922],{},[1815,1920,1921],{},"Do outlines survive a pandas round trip?","\nNo. Reading with pandas gives you values only, so apply grouping after writing, with openpyxl, as the final step.",[157,1924,1926],{"id":1925},"related","Related",[1809,1928,1929,1938,1948,1955,1962],{},[1812,1930,1931,1934,1935,1937],{},[1815,1932,1933],{},"Up:"," ",[19,1936,22],{"href":21}," — the other ways to make a long sheet navigable.",[1812,1939,1940,1944,1945,1947],{},[19,1941,1943],{"href":1942},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-an-autofilter-to-an-excel-sheet-with-openpyxl\u002F","Add an autofilter to an Excel sheet with openpyxl"," — filtering, which ",[14,1946,16],{}," also respects.",[1812,1949,1950,1954],{},[19,1951,1953],{"href":1952},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fhide-sheets-rows-and-columns-with-openpyxl\u002F","Hide sheets, rows and columns with openpyxl"," — the blunter alternative, and when it is right.",[1812,1956,1957,1961],{},[19,1958,1960],{"href":1959},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-summary-sheet-to-excel-report-python\u002F","Add a summary sheet to an Excel report with Python"," — summarising across sheets rather than within one.",[1812,1963,1964,1968],{},[19,1965,1967],{"href":1966},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Ffreeze-header-row-in-excel-with-openpyxl\u002F","Freeze the header row in Excel with openpyxl"," — keeping the headings visible while the outline is explored.",[1970,1971,1972],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":167,"searchDepth":223,"depth":223,"links":1974},[1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988],{"id":159,"depth":223,"text":160},{"id":190,"depth":223,"text":191},{"id":431,"depth":223,"text":432},{"id":888,"depth":223,"text":889},{"id":1049,"depth":223,"text":1050},{"id":1083,"depth":223,"text":1084},{"id":1137,"depth":223,"text":1138},{"id":1339,"depth":223,"text":1340},{"id":1620,"depth":223,"text":1621},{"id":1806,"depth":223,"text":1807},{"id":1855,"depth":223,"text":1856},{"id":1866,"depth":223,"text":1867},{"id":1884,"depth":223,"text":1885},{"id":1925,"depth":223,"text":1926},"2026-08-27","Add collapsible groups to a generated report: row and column outlines, nested levels, collapsed-by-default state, summary rows below or above, and subtotal formulas.","md",[1993,1995,1997,1999,2001],{"q":1890,"a":1994},"Hiding removes rows from view with no way back except unhiding them. Grouping adds an outline control in the margin, so a reader collapses and expands the detail themselves — the report stays complete but starts summarised.",{"q":1896,"a":1996},"Excel supports up to eight outline levels. In practice two or three — region, then month, then detail — is as deep as a reader will follow.",{"q":1902,"a":1998},"The outline's summary row is assumed to be below the group by default. If your totals sit above the detail, set ws.sheet_properties.outlinePr.summaryBelow = False.",{"q":1911,"a":2000},"Yes, with group_columns, using column letters. It is the usual way to hide a block of monthly columns behind a total.",{"q":1921,"a":2002},"No. Reading with pandas gives you values only, so apply grouping after writing, with openpyxl, as the final step.",{},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fgroup-and-outline-rows-in-excel-with-openpyxl",{"title":2006,"description":2007},"Group Rows in Excel with openpyxl","Create Excel outlines from Python: group_rows and group_columns, outline levels, hidden and collapsed state, summaryBelow, and SUBTOTAL formulas that follow the groups.","group-and-outline-rows-in-excel-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fgroup-and-outline-rows-in-excel-with-openpyxl\u002Findex","how-to","U6xr9dhpuqa995sQhKTZQoIh4h7jAxoPfgIqEO-BaKc",[2013,2017],{"title":2014,"path":2015,"stem":2016,"children":-1},"Create an Excel Table with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fcreate-an-excel-table-with-python","formatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fcreate-an-excel-table-with-python\u002Findex",{"title":2018,"path":2019,"stem":2020,"children":-1},"Sort Excel Rows with Python Before Writing","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fsort-excel-rows-with-python-before-writing","formatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fsort-excel-rows-with-python-before-writing\u002Findex",1788710159869]