[{"data":1,"prerenderedAt":3109},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-kpi-summary-block-to-an-excel-dashboard":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-kpi-summary-block-to-an-excel-dashboard":3101},{"id":4,"title":5,"body":6,"dateModified":3074,"datePublished":3074,"description":3075,"extension":3076,"faq":3077,"meta":3086,"navigation":189,"path":3094,"seo":3095,"slug":3097,"stem":3098,"type":3099,"__hash__":3100},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-kpi-summary-block-to-an-excel-dashboard\u002Findex.md","Add a KPI Summary Block to an Excel Dashboard",{"type":7,"value":8,"toc":3060},"minimark",[9,24,126,131,161,373,377,380,670,677,681,764,771,1468,1479,1483,1847,1853,1857,1860,2411,2414,2418,2527,2531,2541,2620,2631,2635,2638,2762,2769,2773,2872,2875,2978,2981,2985,2988,2992,2999,3005,3011,3017,3021,3057],[10,11,12,13,17,18,23],"p",{},"A dashboard's top strip — four or five numbers in boxes, each with a label and a comparison — is the\npart everybody reads and the part that is most fiddly to generate. It is not a table, so ",[14,15,16],"code",{},"to_excel","\ndoes not help; it is a small grid of merged cells with their own formats. This guide, part of\n",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002F","Building Multi-Sheet Excel Dashboards",",\nbuilds one from a DataFrame with xlsxwriter, then shows the openpyxl equivalent for workbooks that\nalready exist.",[25,26,34,35,34,39,34,43,34,50,34,57,34,67,34,73,34,78,34,85,34,90,34,94,34,98,34,101,34,105,34,108,34,113,34,118,34,121],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"0 0 760 232","img",[30,31],"kpi-parts-t","kpi-parts-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},"A KPI tile is three cells, not a table",[40,41,42],"desc",{"id":31},"Each tile stacks a label, a large value and a signed comparison in a merged block, which is why a DataFrame export cannot produce one and a small drawing function can.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","232","#ffffff",[51,52,56],"text",{"x":53,"y":54,"style":55},"380.0","32","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","anatomy of a tile",[44,58],{"x":59,"y":60,"width":61,"height":62,"rx":63,"fill":64,"stroke":65,"style":66},"24.0","74","208.0","96","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[51,68,72],{"x":69,"y":70,"style":71},"128.0","114","font-size:14px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","label row",[51,74,77],{"x":69,"y":75,"style":76},"136","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","what the number is",[79,80],"line",{"x1":81,"y1":82,"x2":83,"y2":82,"stroke":84,"style":66},"237.0","122.0","269.0","var(--brand,#5b5cf0)",[86,87],"polygon",{"points":88,"fill":89},"269.0,122.0 260.0,117.0 260.0,127.0","#5b5cf0",[44,91],{"x":92,"y":60,"width":61,"height":62,"rx":63,"fill":93,"stroke":84,"style":66},"276.0","#f0f4ff",[51,95,97],{"x":53,"y":70,"style":96},"font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","value row",[51,99,100],{"x":53,"y":75,"style":76},"large, number-formatted",[79,102],{"x1":103,"y1":82,"x2":104,"y2":82,"stroke":84,"style":66},"489.0","521.0",[86,106],{"points":107,"fill":89},"521.0,122.0 512.0,117.0 512.0,127.0",[44,109],{"x":110,"y":60,"width":61,"height":62,"rx":63,"fill":111,"stroke":112,"style":66},"528.0","#d9f4f1","var(--teal,#0f9488)",[51,114,117],{"x":115,"y":70,"style":116},"632.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","delta row",[51,119,120],{"x":115,"y":75,"style":76},"signed, arrow by format",[51,122,125],{"x":53,"y":123,"style":124},"210","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","three merged ranges per tile, repeated across the strip",[127,128,130],"h2",{"id":129},"prerequisites","Prerequisites",[132,133,138],"pre",{"className":134,"code":135,"language":136,"meta":137,"style":137},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas xlsxwriter openpyxl\n","bash","",[14,139,140],{"__ignoreMap":137},[141,142,144,148,152,155,158],"span",{"class":79,"line":143},1,[141,145,147],{"class":146},"sMTad","pip",[141,149,151],{"class":150},"srMev"," install",[141,153,154],{"class":150}," pandas",[141,156,157],{"class":150}," xlsxwriter",[141,159,160],{"class":150}," openpyxl\n",[132,162,166],{"className":163,"code":164,"language":165,"meta":137,"style":137},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\norders = pd.DataFrame({\n    \"Region\": [\"North\", \"South\", \"North\", \"West\", \"South\", \"North\"],\n    \"Revenue\": [24500.0, 15320.0, 31200.0, 9800.0, 18400.0, 27600.0],\n    \"Units\": [245, 153, 312, 98, 184, 276],\n    \"Ordered\": pd.to_datetime([\n        \"2026-08-04\", \"2026-08-11\", \"2026-08-19\", \"2026-08-22\", \"2026-08-27\", \"2026-08-30\",\n    ]),\n})\n","python",[14,167,168,184,191,203,241,280,318,327,361,367],{"__ignoreMap":137},[141,169,170,174,178,181],{"class":79,"line":143},[141,171,173],{"class":172},"s-kum","import",[141,175,177],{"class":176},"skGVy"," pandas ",[141,179,180],{"class":172},"as",[141,182,183],{"class":176}," pd\n",[141,185,187],{"class":79,"line":186},2,[141,188,190],{"emptyLinePlaceholder":189},true,"\n",[141,192,194,197,200],{"class":79,"line":193},3,[141,195,196],{"class":176},"orders ",[141,198,199],{"class":172},"=",[141,201,202],{"class":176}," pd.DataFrame({\n",[141,204,206,209,212,215,218,221,223,225,227,230,232,234,236,238],{"class":79,"line":205},4,[141,207,208],{"class":150},"    \"Region\"",[141,210,211],{"class":176},": [",[141,213,214],{"class":150},"\"North\"",[141,216,217],{"class":176},", ",[141,219,220],{"class":150},"\"South\"",[141,222,217],{"class":176},[141,224,214],{"class":150},[141,226,217],{"class":176},[141,228,229],{"class":150},"\"West\"",[141,231,217],{"class":176},[141,233,220],{"class":150},[141,235,217],{"class":176},[141,237,214],{"class":150},[141,239,240],{"class":176},"],\n",[141,242,244,247,249,253,255,258,260,263,265,268,270,273,275,278],{"class":79,"line":243},5,[141,245,246],{"class":150},"    \"Revenue\"",[141,248,211],{"class":176},[141,250,252],{"class":251},"sP0c6","24500.0",[141,254,217],{"class":176},[141,256,257],{"class":251},"15320.0",[141,259,217],{"class":176},[141,261,262],{"class":251},"31200.0",[141,264,217],{"class":176},[141,266,267],{"class":251},"9800.0",[141,269,217],{"class":176},[141,271,272],{"class":251},"18400.0",[141,274,217],{"class":176},[141,276,277],{"class":251},"27600.0",[141,279,240],{"class":176},[141,281,283,286,288,291,293,296,298,301,303,306,308,311,313,316],{"class":79,"line":282},6,[141,284,285],{"class":150},"    \"Units\"",[141,287,211],{"class":176},[141,289,290],{"class":251},"245",[141,292,217],{"class":176},[141,294,295],{"class":251},"153",[141,297,217],{"class":176},[141,299,300],{"class":251},"312",[141,302,217],{"class":176},[141,304,305],{"class":251},"98",[141,307,217],{"class":176},[141,309,310],{"class":251},"184",[141,312,217],{"class":176},[141,314,315],{"class":251},"276",[141,317,240],{"class":176},[141,319,321,324],{"class":79,"line":320},7,[141,322,323],{"class":150},"    \"Ordered\"",[141,325,326],{"class":176},": pd.to_datetime([\n",[141,328,330,333,335,338,340,343,345,348,350,353,355,358],{"class":79,"line":329},8,[141,331,332],{"class":150},"        \"2026-08-04\"",[141,334,217],{"class":176},[141,336,337],{"class":150},"\"2026-08-11\"",[141,339,217],{"class":176},[141,341,342],{"class":150},"\"2026-08-19\"",[141,344,217],{"class":176},[141,346,347],{"class":150},"\"2026-08-22\"",[141,349,217],{"class":176},[141,351,352],{"class":150},"\"2026-08-27\"",[141,354,217],{"class":176},[141,356,357],{"class":150},"\"2026-08-30\"",[141,359,360],{"class":176},",\n",[141,362,364],{"class":79,"line":363},9,[141,365,366],{"class":176},"    ]),\n",[141,368,370],{"class":79,"line":369},10,[141,371,372],{"class":176},"})\n",[127,374,376],{"id":375},"decide-the-numbers-before-you-draw-anything","Decide the numbers before you draw anything",[10,378,379],{},"A KPI tile has three parts: a label, a value and a comparison. Computing all of them first — as\nplain Python values — keeps the drawing code free of business logic and makes the figures testable\nwithout opening a workbook.",[132,381,383],{"className":163,"code":382,"language":165,"meta":137,"style":137},"def kpis(frame: pd.DataFrame, prior: pd.DataFrame) -> list[dict]:\n    def delta(now, before):\n        return None if not before else (now - before) \u002F before\n\n    return [\n        {\"label\": \"Revenue\", \"value\": frame[\"Revenue\"].sum(),\n         \"format\": \"money\", \"delta\": delta(frame[\"Revenue\"].sum(), prior[\"Revenue\"].sum())},\n        {\"label\": \"Orders\", \"value\": len(frame),\n         \"format\": \"count\", \"delta\": delta(len(frame), len(prior))},\n        {\"label\": \"Units\", \"value\": int(frame[\"Units\"].sum()),\n         \"format\": \"count\", \"delta\": delta(frame[\"Units\"].sum(), prior[\"Units\"].sum())},\n        {\"label\": \"Average order\", \"value\": frame[\"Revenue\"].mean(),\n         \"format\": \"money\", \"delta\": delta(frame[\"Revenue\"].mean(), prior[\"Revenue\"].mean())},\n    ]\n",[14,384,385,403,414,449,453,461,488,516,539,565,593,616,639,664],{"__ignoreMap":137},[141,386,387,390,394,397,400],{"class":79,"line":143},[141,388,389],{"class":172},"def",[141,391,393],{"class":392},"s_Opv"," kpis",[141,395,396],{"class":176},"(frame: pd.DataFrame, prior: pd.DataFrame) -> list[",[141,398,399],{"class":251},"dict",[141,401,402],{"class":176},"]:\n",[141,404,405,408,411],{"class":79,"line":186},[141,406,407],{"class":172},"    def",[141,409,410],{"class":392}," delta",[141,412,413],{"class":176},"(now, before):\n",[141,415,416,419,422,425,428,431,434,437,440,443,446],{"class":79,"line":193},[141,417,418],{"class":172},"        return",[141,420,421],{"class":251}," None",[141,423,424],{"class":172}," if",[141,426,427],{"class":172}," not",[141,429,430],{"class":176}," before ",[141,432,433],{"class":172},"else",[141,435,436],{"class":176}," (now ",[141,438,439],{"class":172},"-",[141,441,442],{"class":176}," before) ",[141,444,445],{"class":172},"\u002F",[141,447,448],{"class":176}," before\n",[141,450,451],{"class":79,"line":205},[141,452,190],{"emptyLinePlaceholder":189},[141,454,455,458],{"class":79,"line":243},[141,456,457],{"class":172},"    return",[141,459,460],{"class":176}," [\n",[141,462,463,466,469,472,475,477,480,483,485],{"class":79,"line":282},[141,464,465],{"class":176},"        {",[141,467,468],{"class":150},"\"label\"",[141,470,471],{"class":176},": ",[141,473,474],{"class":150},"\"Revenue\"",[141,476,217],{"class":176},[141,478,479],{"class":150},"\"value\"",[141,481,482],{"class":176},": frame[",[141,484,474],{"class":150},[141,486,487],{"class":176},"].sum(),\n",[141,489,490,493,495,498,500,503,506,508,511,513],{"class":79,"line":320},[141,491,492],{"class":150},"         \"format\"",[141,494,471],{"class":176},[141,496,497],{"class":150},"\"money\"",[141,499,217],{"class":176},[141,501,502],{"class":150},"\"delta\"",[141,504,505],{"class":176},": delta(frame[",[141,507,474],{"class":150},[141,509,510],{"class":176},"].sum(), prior[",[141,512,474],{"class":150},[141,514,515],{"class":176},"].sum())},\n",[141,517,518,520,522,524,527,529,531,533,536],{"class":79,"line":329},[141,519,465],{"class":176},[141,521,468],{"class":150},[141,523,471],{"class":176},[141,525,526],{"class":150},"\"Orders\"",[141,528,217],{"class":176},[141,530,479],{"class":150},[141,532,471],{"class":176},[141,534,535],{"class":251},"len",[141,537,538],{"class":176},"(frame),\n",[141,540,541,543,545,548,550,552,555,557,560,562],{"class":79,"line":363},[141,542,492],{"class":150},[141,544,471],{"class":176},[141,546,547],{"class":150},"\"count\"",[141,549,217],{"class":176},[141,551,502],{"class":150},[141,553,554],{"class":176},": delta(",[141,556,535],{"class":251},[141,558,559],{"class":176},"(frame), ",[141,561,535],{"class":251},[141,563,564],{"class":176},"(prior))},\n",[141,566,567,569,571,573,576,578,580,582,585,588,590],{"class":79,"line":369},[141,568,465],{"class":176},[141,570,468],{"class":150},[141,572,471],{"class":176},[141,574,575],{"class":150},"\"Units\"",[141,577,217],{"class":176},[141,579,479],{"class":150},[141,581,471],{"class":176},[141,583,584],{"class":251},"int",[141,586,587],{"class":176},"(frame[",[141,589,575],{"class":150},[141,591,592],{"class":176},"].sum()),\n",[141,594,596,598,600,602,604,606,608,610,612,614],{"class":79,"line":595},11,[141,597,492],{"class":150},[141,599,471],{"class":176},[141,601,547],{"class":150},[141,603,217],{"class":176},[141,605,502],{"class":150},[141,607,505],{"class":176},[141,609,575],{"class":150},[141,611,510],{"class":176},[141,613,575],{"class":150},[141,615,515],{"class":176},[141,617,619,621,623,625,628,630,632,634,636],{"class":79,"line":618},12,[141,620,465],{"class":176},[141,622,468],{"class":150},[141,624,471],{"class":176},[141,626,627],{"class":150},"\"Average order\"",[141,629,217],{"class":176},[141,631,479],{"class":150},[141,633,482],{"class":176},[141,635,474],{"class":150},[141,637,638],{"class":176},"].mean(),\n",[141,640,642,644,646,648,650,652,654,656,659,661],{"class":79,"line":641},13,[141,643,492],{"class":150},[141,645,471],{"class":176},[141,647,497],{"class":150},[141,649,217],{"class":176},[141,651,502],{"class":150},[141,653,505],{"class":176},[141,655,474],{"class":150},[141,657,658],{"class":176},"].mean(), prior[",[141,660,474],{"class":150},[141,662,663],{"class":176},"].mean())},\n",[141,665,667],{"class":79,"line":666},14,[141,668,669],{"class":176},"    ]\n",[10,671,672,673,676],{},"Returning ",[14,674,675],{},"None"," for the delta when the prior period is zero avoids the division that would otherwise\nproduce infinity, and lets the drawing code render a dash rather than a nonsense percentage.",[127,678,680],{"id":679},"draw-the-tiles-with-xlsxwriter","Draw the tiles with xlsxwriter",[25,682,34,687,34,690,34,693,34,696,34,703,34,709,34,713,34,717,34,720,34,725,34,729,34,732,34,737,34,741,34,744,34,747,34,750,34,754,34,757,34,760],{"viewBox":683,"role":28,"ariaLabelledBy":684,"xmlns":32,"style":33},"0 0 760 216",[685,686],"kpi-fmt-t","kpi-fmt-d",[36,688,689],{"id":685},"One number format handles the whole comparison row",[40,691,692],{"id":686},"Excel's three-section number format applies different text to positive, negative and zero values, so the arrow and colour follow the sign without any conditional formatting rule.",[44,694],{"x":46,"y":46,"width":47,"height":695,"fill":49},"216",[44,697],{"x":698,"y":699,"width":700,"height":701,"rx":702,"fill":64,"stroke":65},"18","30","724","34","8",[51,704,708],{"x":705,"y":706,"style":707},"126.6","52","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","Section",[51,710,712],{"x":711,"y":706,"style":707},"361.9","Applies to",[51,714,716],{"x":715,"y":706,"style":707},"615.3000000000001","Renders",[44,718],{"x":698,"y":719,"width":700,"height":701,"rx":702,"fill":111,"stroke":112},"67",[51,721,724],{"x":705,"y":722,"style":723},"88.0","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","first",[51,726,728],{"x":711,"y":722,"style":727},"font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","positive values",[51,730,731],{"x":715,"y":722,"style":727},"▲ 12.4%",[44,733],{"x":698,"y":734,"width":700,"height":701,"rx":702,"fill":735,"stroke":736},"107","#fee8f2","var(--accent,#d81b73)",[51,738,740],{"x":705,"y":69,"style":739},"font-size:12px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:middle","second",[51,742,743],{"x":711,"y":69,"style":727},"negative values",[51,745,746],{"x":715,"y":69,"style":727},"▼ 3.1%",[44,748],{"x":698,"y":749,"width":700,"height":701,"rx":702,"fill":64,"stroke":65},"147",[51,751,753],{"x":705,"y":752,"style":707},"168.0","third",[51,755,756],{"x":711,"y":752,"style":727},"zero",[51,758,759],{"x":715,"y":752,"style":727},"a dash",[51,761,763],{"x":53,"y":762,"style":124},"204","semicolons separate the sections; no rules needed",[10,765,766,767,770],{},"Each tile spans three columns and three rows: a label row, a value row and a comparison row.\n",[14,768,769],{},"merge_range"," writes the value and applies the format to the whole merged block in one call, which is\nwhat makes this tolerable to write.",[132,772,774],{"className":163,"code":773,"language":165,"meta":137,"style":137},"import xlsxwriter\n\ndef write_kpi_block(book, sheet, tiles, first_row=1, first_col=1, width=3):\n    label = book.add_format({\n        \"font_size\": 10, \"font_color\": \"#5B6780\", \"align\": \"center\",\n        \"valign\": \"vcenter\", \"bg_color\": \"#F0F4FF\", \"top\": 1, \"left\": 1, \"right\": 1,\n        \"border_color\": \"#CDD5E6\",\n    })\n    money = book.add_format({\n        \"font_size\": 18, \"bold\": True, \"num_format\": \"#,##0\", \"align\": \"center\",\n        \"valign\": \"vcenter\", \"bg_color\": \"#F0F4FF\", \"left\": 1, \"right\": 1,\n        \"border_color\": \"#CDD5E6\",\n    })\n    count = book.add_format({\n        \"font_size\": 18, \"bold\": True, \"num_format\": \"#,##0\", \"align\": \"center\",\n        \"valign\": \"vcenter\", \"bg_color\": \"#F0F4FF\", \"left\": 1, \"right\": 1,\n        \"border_color\": \"#CDD5E6\",\n    })\n    up = book.add_format({\n        \"font_size\": 10, \"font_color\": \"#0B6157\", \"num_format\": '▲ 0.0%;▼ 0.0%;–',\n        \"align\": \"center\", \"valign\": \"vcenter\", \"bg_color\": \"#F0F4FF\",\n        \"bottom\": 1, \"left\": 1, \"right\": 1, \"border_color\": \"#CDD5E6\",\n    })\n\n    for index, tile in enumerate(tiles):\n        col = first_col + index * (width + 1)\n        sheet.merge_range(first_row, col, first_row, col + width - 1, tile[\"label\"], label)\n        sheet.merge_range(\n            first_row + 1, col, first_row + 1, col + width - 1,\n            tile[\"value\"], money if tile[\"format\"] == \"money\" else count,\n        )\n        sheet.merge_range(\n            first_row + 2, col, first_row + 2, col + width - 1,\n            tile[\"delta\"] if tile[\"delta\"] is not None else \"–\", up,\n        )\n    sheet.set_row(first_row + 1, 34)\n",[14,775,776,783,787,820,830,862,911,923,928,937,975,1009,1019,1023,1032,1067,1102,1113,1118,1128,1157,1186,1223,1228,1233,1251,1282,1305,1311,1341,1376,1382,1387,1415,1447,1452],{"__ignoreMap":137},[141,777,778,780],{"class":79,"line":143},[141,779,173],{"class":172},[141,781,782],{"class":176}," xlsxwriter\n",[141,784,785],{"class":79,"line":186},[141,786,190],{"emptyLinePlaceholder":189},[141,788,789,791,794,797,799,802,805,807,809,812,814,817],{"class":79,"line":193},[141,790,389],{"class":172},[141,792,793],{"class":392}," write_kpi_block",[141,795,796],{"class":176},"(book, sheet, tiles, first_row",[141,798,199],{"class":172},[141,800,801],{"class":251},"1",[141,803,804],{"class":176},", first_col",[141,806,199],{"class":172},[141,808,801],{"class":251},[141,810,811],{"class":176},", width",[141,813,199],{"class":172},[141,815,816],{"class":251},"3",[141,818,819],{"class":176},"):\n",[141,821,822,825,827],{"class":79,"line":205},[141,823,824],{"class":176},"    label ",[141,826,199],{"class":172},[141,828,829],{"class":176}," book.add_format({\n",[141,831,832,835,837,840,842,845,847,850,852,855,857,860],{"class":79,"line":243},[141,833,834],{"class":150},"        \"font_size\"",[141,836,471],{"class":176},[141,838,839],{"class":251},"10",[141,841,217],{"class":176},[141,843,844],{"class":150},"\"font_color\"",[141,846,471],{"class":176},[141,848,849],{"class":150},"\"#5B6780\"",[141,851,217],{"class":176},[141,853,854],{"class":150},"\"align\"",[141,856,471],{"class":176},[141,858,859],{"class":150},"\"center\"",[141,861,360],{"class":176},[141,863,864,867,869,872,874,877,879,882,884,887,889,891,893,896,898,900,902,905,907,909],{"class":79,"line":282},[141,865,866],{"class":150},"        \"valign\"",[141,868,471],{"class":176},[141,870,871],{"class":150},"\"vcenter\"",[141,873,217],{"class":176},[141,875,876],{"class":150},"\"bg_color\"",[141,878,471],{"class":176},[141,880,881],{"class":150},"\"#F0F4FF\"",[141,883,217],{"class":176},[141,885,886],{"class":150},"\"top\"",[141,888,471],{"class":176},[141,890,801],{"class":251},[141,892,217],{"class":176},[141,894,895],{"class":150},"\"left\"",[141,897,471],{"class":176},[141,899,801],{"class":251},[141,901,217],{"class":176},[141,903,904],{"class":150},"\"right\"",[141,906,471],{"class":176},[141,908,801],{"class":251},[141,910,360],{"class":176},[141,912,913,916,918,921],{"class":79,"line":320},[141,914,915],{"class":150},"        \"border_color\"",[141,917,471],{"class":176},[141,919,920],{"class":150},"\"#CDD5E6\"",[141,922,360],{"class":176},[141,924,925],{"class":79,"line":329},[141,926,927],{"class":176},"    })\n",[141,929,930,933,935],{"class":79,"line":363},[141,931,932],{"class":176},"    money ",[141,934,199],{"class":172},[141,936,829],{"class":176},[141,938,939,941,943,945,947,950,952,955,957,960,962,965,967,969,971,973],{"class":79,"line":369},[141,940,834],{"class":150},[141,942,471],{"class":176},[141,944,698],{"class":251},[141,946,217],{"class":176},[141,948,949],{"class":150},"\"bold\"",[141,951,471],{"class":176},[141,953,954],{"class":251},"True",[141,956,217],{"class":176},[141,958,959],{"class":150},"\"num_format\"",[141,961,471],{"class":176},[141,963,964],{"class":150},"\"#,##0\"",[141,966,217],{"class":176},[141,968,854],{"class":150},[141,970,471],{"class":176},[141,972,859],{"class":150},[141,974,360],{"class":176},[141,976,977,979,981,983,985,987,989,991,993,995,997,999,1001,1003,1005,1007],{"class":79,"line":595},[141,978,866],{"class":150},[141,980,471],{"class":176},[141,982,871],{"class":150},[141,984,217],{"class":176},[141,986,876],{"class":150},[141,988,471],{"class":176},[141,990,881],{"class":150},[141,992,217],{"class":176},[141,994,895],{"class":150},[141,996,471],{"class":176},[141,998,801],{"class":251},[141,1000,217],{"class":176},[141,1002,904],{"class":150},[141,1004,471],{"class":176},[141,1006,801],{"class":251},[141,1008,360],{"class":176},[141,1010,1011,1013,1015,1017],{"class":79,"line":618},[141,1012,915],{"class":150},[141,1014,471],{"class":176},[141,1016,920],{"class":150},[141,1018,360],{"class":176},[141,1020,1021],{"class":79,"line":641},[141,1022,927],{"class":176},[141,1024,1025,1028,1030],{"class":79,"line":666},[141,1026,1027],{"class":176},"    count ",[141,1029,199],{"class":172},[141,1031,829],{"class":176},[141,1033,1035,1037,1039,1041,1043,1045,1047,1049,1051,1053,1055,1057,1059,1061,1063,1065],{"class":79,"line":1034},15,[141,1036,834],{"class":150},[141,1038,471],{"class":176},[141,1040,698],{"class":251},[141,1042,217],{"class":176},[141,1044,949],{"class":150},[141,1046,471],{"class":176},[141,1048,954],{"class":251},[141,1050,217],{"class":176},[141,1052,959],{"class":150},[141,1054,471],{"class":176},[141,1056,964],{"class":150},[141,1058,217],{"class":176},[141,1060,854],{"class":150},[141,1062,471],{"class":176},[141,1064,859],{"class":150},[141,1066,360],{"class":176},[141,1068,1070,1072,1074,1076,1078,1080,1082,1084,1086,1088,1090,1092,1094,1096,1098,1100],{"class":79,"line":1069},16,[141,1071,866],{"class":150},[141,1073,471],{"class":176},[141,1075,871],{"class":150},[141,1077,217],{"class":176},[141,1079,876],{"class":150},[141,1081,471],{"class":176},[141,1083,881],{"class":150},[141,1085,217],{"class":176},[141,1087,895],{"class":150},[141,1089,471],{"class":176},[141,1091,801],{"class":251},[141,1093,217],{"class":176},[141,1095,904],{"class":150},[141,1097,471],{"class":176},[141,1099,801],{"class":251},[141,1101,360],{"class":176},[141,1103,1105,1107,1109,1111],{"class":79,"line":1104},17,[141,1106,915],{"class":150},[141,1108,471],{"class":176},[141,1110,920],{"class":150},[141,1112,360],{"class":176},[141,1114,1116],{"class":79,"line":1115},18,[141,1117,927],{"class":176},[141,1119,1121,1124,1126],{"class":79,"line":1120},19,[141,1122,1123],{"class":176},"    up ",[141,1125,199],{"class":172},[141,1127,829],{"class":176},[141,1129,1131,1133,1135,1137,1139,1141,1143,1146,1148,1150,1152,1155],{"class":79,"line":1130},20,[141,1132,834],{"class":150},[141,1134,471],{"class":176},[141,1136,839],{"class":251},[141,1138,217],{"class":176},[141,1140,844],{"class":150},[141,1142,471],{"class":176},[141,1144,1145],{"class":150},"\"#0B6157\"",[141,1147,217],{"class":176},[141,1149,959],{"class":150},[141,1151,471],{"class":176},[141,1153,1154],{"class":150},"'▲ 0.0%;▼ 0.0%;–'",[141,1156,360],{"class":176},[141,1158,1160,1163,1165,1167,1169,1172,1174,1176,1178,1180,1182,1184],{"class":79,"line":1159},21,[141,1161,1162],{"class":150},"        \"align\"",[141,1164,471],{"class":176},[141,1166,859],{"class":150},[141,1168,217],{"class":176},[141,1170,1171],{"class":150},"\"valign\"",[141,1173,471],{"class":176},[141,1175,871],{"class":150},[141,1177,217],{"class":176},[141,1179,876],{"class":150},[141,1181,471],{"class":176},[141,1183,881],{"class":150},[141,1185,360],{"class":176},[141,1187,1189,1192,1194,1196,1198,1200,1202,1204,1206,1208,1210,1212,1214,1217,1219,1221],{"class":79,"line":1188},22,[141,1190,1191],{"class":150},"        \"bottom\"",[141,1193,471],{"class":176},[141,1195,801],{"class":251},[141,1197,217],{"class":176},[141,1199,895],{"class":150},[141,1201,471],{"class":176},[141,1203,801],{"class":251},[141,1205,217],{"class":176},[141,1207,904],{"class":150},[141,1209,471],{"class":176},[141,1211,801],{"class":251},[141,1213,217],{"class":176},[141,1215,1216],{"class":150},"\"border_color\"",[141,1218,471],{"class":176},[141,1220,920],{"class":150},[141,1222,360],{"class":176},[141,1224,1226],{"class":79,"line":1225},23,[141,1227,927],{"class":176},[141,1229,1231],{"class":79,"line":1230},24,[141,1232,190],{"emptyLinePlaceholder":189},[141,1234,1236,1239,1242,1245,1248],{"class":79,"line":1235},25,[141,1237,1238],{"class":172},"    for",[141,1240,1241],{"class":176}," index, tile ",[141,1243,1244],{"class":172},"in",[141,1246,1247],{"class":251}," enumerate",[141,1249,1250],{"class":176},"(tiles):\n",[141,1252,1254,1257,1259,1262,1265,1268,1271,1274,1276,1279],{"class":79,"line":1253},26,[141,1255,1256],{"class":176},"        col ",[141,1258,199],{"class":172},[141,1260,1261],{"class":176}," first_col ",[141,1263,1264],{"class":172},"+",[141,1266,1267],{"class":176}," index ",[141,1269,1270],{"class":172},"*",[141,1272,1273],{"class":176}," (width ",[141,1275,1264],{"class":172},[141,1277,1278],{"class":251}," 1",[141,1280,1281],{"class":176},")\n",[141,1283,1285,1288,1290,1293,1295,1297,1300,1302],{"class":79,"line":1284},27,[141,1286,1287],{"class":176},"        sheet.merge_range(first_row, col, first_row, col ",[141,1289,1264],{"class":172},[141,1291,1292],{"class":176}," width ",[141,1294,439],{"class":172},[141,1296,1278],{"class":251},[141,1298,1299],{"class":176},", tile[",[141,1301,468],{"class":150},[141,1303,1304],{"class":176},"], label)\n",[141,1306,1308],{"class":79,"line":1307},28,[141,1309,1310],{"class":176},"        sheet.merge_range(\n",[141,1312,1314,1317,1319,1321,1324,1326,1328,1331,1333,1335,1337,1339],{"class":79,"line":1313},29,[141,1315,1316],{"class":176},"            first_row ",[141,1318,1264],{"class":172},[141,1320,1278],{"class":251},[141,1322,1323],{"class":176},", col, first_row ",[141,1325,1264],{"class":172},[141,1327,1278],{"class":251},[141,1329,1330],{"class":176},", col ",[141,1332,1264],{"class":172},[141,1334,1292],{"class":176},[141,1336,439],{"class":172},[141,1338,1278],{"class":251},[141,1340,360],{"class":176},[141,1342,1344,1347,1349,1352,1355,1358,1361,1364,1367,1370,1373],{"class":79,"line":1343},30,[141,1345,1346],{"class":176},"            tile[",[141,1348,479],{"class":150},[141,1350,1351],{"class":176},"], money ",[141,1353,1354],{"class":172},"if",[141,1356,1357],{"class":176}," tile[",[141,1359,1360],{"class":150},"\"format\"",[141,1362,1363],{"class":176},"] ",[141,1365,1366],{"class":172},"==",[141,1368,1369],{"class":150}," \"money\"",[141,1371,1372],{"class":172}," else",[141,1374,1375],{"class":176}," count,\n",[141,1377,1379],{"class":79,"line":1378},31,[141,1380,1381],{"class":176},"        )\n",[141,1383,1385],{"class":79,"line":1384},32,[141,1386,1310],{"class":176},[141,1388,1390,1392,1394,1397,1399,1401,1403,1405,1407,1409,1411,1413],{"class":79,"line":1389},33,[141,1391,1316],{"class":176},[141,1393,1264],{"class":172},[141,1395,1396],{"class":251}," 2",[141,1398,1323],{"class":176},[141,1400,1264],{"class":172},[141,1402,1396],{"class":251},[141,1404,1330],{"class":176},[141,1406,1264],{"class":172},[141,1408,1292],{"class":176},[141,1410,439],{"class":172},[141,1412,1278],{"class":251},[141,1414,360],{"class":176},[141,1416,1418,1420,1422,1424,1426,1428,1430,1432,1435,1437,1439,1441,1444],{"class":79,"line":1417},34,[141,1419,1346],{"class":176},[141,1421,502],{"class":150},[141,1423,1363],{"class":176},[141,1425,1354],{"class":172},[141,1427,1357],{"class":176},[141,1429,502],{"class":150},[141,1431,1363],{"class":176},[141,1433,1434],{"class":172},"is",[141,1436,427],{"class":172},[141,1438,421],{"class":251},[141,1440,1372],{"class":172},[141,1442,1443],{"class":150}," \"–\"",[141,1445,1446],{"class":176},", up,\n",[141,1448,1450],{"class":79,"line":1449},35,[141,1451,1381],{"class":176},[141,1453,1455,1458,1460,1462,1464,1466],{"class":79,"line":1454},36,[141,1456,1457],{"class":176},"    sheet.set_row(first_row ",[141,1459,1264],{"class":172},[141,1461,1278],{"class":251},[141,1463,217],{"class":176},[141,1465,701],{"class":251},[141,1467,1281],{"class":176},[10,1469,1470,1471,1473,1474,1478],{},"The number format ",[14,1472,1154],{}," is doing real work: Excel's three-section format applies the\nfirst part to positive numbers, the second to negatives and the third to zero, so the arrow follows\nthe sign without any conditional formatting. The details of that syntax are in\n",[19,1475,1477],{"href":1476},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fapplying-number-and-date-formats-in-excel\u002Fwrite-custom-number-format-codes-in-excel-with-python\u002F","Write Custom Number Format Codes in Excel with Python",".",[127,1480,1482],{"id":1481},"assemble-the-dashboard-sheet","Assemble the dashboard sheet",[132,1484,1486],{"className":163,"code":1485,"language":165,"meta":137,"style":137},"prior = orders.assign(Revenue=orders[\"Revenue\"] * 0.86, Units=(orders[\"Units\"] * 0.9).astype(int))\n\nwith pd.ExcelWriter(\"dashboard.xlsx\", engine=\"xlsxwriter\") as writer:\n    by_region = orders.groupby(\"Region\", as_index=False).agg(\n        Revenue=(\"Revenue\", \"sum\"), Orders=(\"Revenue\", \"size\"), Units=(\"Units\", \"sum\"),\n    )\n    by_region.to_excel(writer, sheet_name=\"Dashboard\", index=False, startrow=6, startcol=1)\n    orders.to_excel(writer, sheet_name=\"Detail\", index=False)\n\n    book, sheet = writer.book, writer.sheets[\"Dashboard\"]\n    sheet.hide_gridlines(2)\n    sheet.set_column(\"A:A\", 2)\n    sheet.set_column(\"B:M\", 11)\n    title = book.add_format({\"font_size\": 16, \"bold\": True, \"font_color\": \"#172033\"})\n    sheet.write(\"B1\", \"August 2026 — regional performance\", title)\n    write_kpi_block(book, sheet, kpis(orders, prior), first_row=2)\n",[14,1487,1488,1543,1547,1576,1602,1653,1658,1701,1723,1727,1742,1752,1766,1780,1817,1833],{"__ignoreMap":137},[141,1489,1490,1493,1495,1498,1502,1504,1507,1509,1511,1513,1516,1518,1521,1523,1526,1528,1530,1532,1535,1538,1540],{"class":79,"line":143},[141,1491,1492],{"class":176},"prior ",[141,1494,199],{"class":172},[141,1496,1497],{"class":176}," orders.assign(",[141,1499,1501],{"class":1500},"sa561","Revenue",[141,1503,199],{"class":172},[141,1505,1506],{"class":176},"orders[",[141,1508,474],{"class":150},[141,1510,1363],{"class":176},[141,1512,1270],{"class":172},[141,1514,1515],{"class":251}," 0.86",[141,1517,217],{"class":176},[141,1519,1520],{"class":1500},"Units",[141,1522,199],{"class":172},[141,1524,1525],{"class":176},"(orders[",[141,1527,575],{"class":150},[141,1529,1363],{"class":176},[141,1531,1270],{"class":172},[141,1533,1534],{"class":251}," 0.9",[141,1536,1537],{"class":176},").astype(",[141,1539,584],{"class":251},[141,1541,1542],{"class":176},"))\n",[141,1544,1545],{"class":79,"line":186},[141,1546,190],{"emptyLinePlaceholder":189},[141,1548,1549,1552,1555,1558,1560,1563,1565,1568,1571,1573],{"class":79,"line":193},[141,1550,1551],{"class":172},"with",[141,1553,1554],{"class":176}," pd.ExcelWriter(",[141,1556,1557],{"class":150},"\"dashboard.xlsx\"",[141,1559,217],{"class":176},[141,1561,1562],{"class":1500},"engine",[141,1564,199],{"class":172},[141,1566,1567],{"class":150},"\"xlsxwriter\"",[141,1569,1570],{"class":176},") ",[141,1572,180],{"class":172},[141,1574,1575],{"class":176}," writer:\n",[141,1577,1578,1581,1583,1586,1589,1591,1594,1596,1599],{"class":79,"line":205},[141,1579,1580],{"class":176},"    by_region ",[141,1582,199],{"class":172},[141,1584,1585],{"class":176}," orders.groupby(",[141,1587,1588],{"class":150},"\"Region\"",[141,1590,217],{"class":176},[141,1592,1593],{"class":1500},"as_index",[141,1595,199],{"class":172},[141,1597,1598],{"class":251},"False",[141,1600,1601],{"class":176},").agg(\n",[141,1603,1604,1607,1609,1612,1614,1616,1619,1622,1625,1627,1629,1631,1633,1636,1638,1640,1642,1644,1646,1648,1650],{"class":79,"line":243},[141,1605,1606],{"class":1500},"        Revenue",[141,1608,199],{"class":172},[141,1610,1611],{"class":176},"(",[141,1613,474],{"class":150},[141,1615,217],{"class":176},[141,1617,1618],{"class":150},"\"sum\"",[141,1620,1621],{"class":176},"), ",[141,1623,1624],{"class":1500},"Orders",[141,1626,199],{"class":172},[141,1628,1611],{"class":176},[141,1630,474],{"class":150},[141,1632,217],{"class":176},[141,1634,1635],{"class":150},"\"size\"",[141,1637,1621],{"class":176},[141,1639,1520],{"class":1500},[141,1641,199],{"class":172},[141,1643,1611],{"class":176},[141,1645,575],{"class":150},[141,1647,217],{"class":176},[141,1649,1618],{"class":150},[141,1651,1652],{"class":176},"),\n",[141,1654,1655],{"class":79,"line":282},[141,1656,1657],{"class":176},"    )\n",[141,1659,1660,1663,1666,1668,1671,1673,1676,1678,1680,1682,1685,1687,1690,1692,1695,1697,1699],{"class":79,"line":320},[141,1661,1662],{"class":176},"    by_region.to_excel(writer, ",[141,1664,1665],{"class":1500},"sheet_name",[141,1667,199],{"class":172},[141,1669,1670],{"class":150},"\"Dashboard\"",[141,1672,217],{"class":176},[141,1674,1675],{"class":1500},"index",[141,1677,199],{"class":172},[141,1679,1598],{"class":251},[141,1681,217],{"class":176},[141,1683,1684],{"class":1500},"startrow",[141,1686,199],{"class":172},[141,1688,1689],{"class":251},"6",[141,1691,217],{"class":176},[141,1693,1694],{"class":1500},"startcol",[141,1696,199],{"class":172},[141,1698,801],{"class":251},[141,1700,1281],{"class":176},[141,1702,1703,1706,1708,1710,1713,1715,1717,1719,1721],{"class":79,"line":329},[141,1704,1705],{"class":176},"    orders.to_excel(writer, ",[141,1707,1665],{"class":1500},[141,1709,199],{"class":172},[141,1711,1712],{"class":150},"\"Detail\"",[141,1714,217],{"class":176},[141,1716,1675],{"class":1500},[141,1718,199],{"class":172},[141,1720,1598],{"class":251},[141,1722,1281],{"class":176},[141,1724,1725],{"class":79,"line":363},[141,1726,190],{"emptyLinePlaceholder":189},[141,1728,1729,1732,1734,1737,1739],{"class":79,"line":369},[141,1730,1731],{"class":176},"    book, sheet ",[141,1733,199],{"class":172},[141,1735,1736],{"class":176}," writer.book, writer.sheets[",[141,1738,1670],{"class":150},[141,1740,1741],{"class":176},"]\n",[141,1743,1744,1747,1750],{"class":79,"line":595},[141,1745,1746],{"class":176},"    sheet.hide_gridlines(",[141,1748,1749],{"class":251},"2",[141,1751,1281],{"class":176},[141,1753,1754,1757,1760,1762,1764],{"class":79,"line":618},[141,1755,1756],{"class":176},"    sheet.set_column(",[141,1758,1759],{"class":150},"\"A:A\"",[141,1761,217],{"class":176},[141,1763,1749],{"class":251},[141,1765,1281],{"class":176},[141,1767,1768,1770,1773,1775,1778],{"class":79,"line":641},[141,1769,1756],{"class":176},[141,1771,1772],{"class":150},"\"B:M\"",[141,1774,217],{"class":176},[141,1776,1777],{"class":251},"11",[141,1779,1281],{"class":176},[141,1781,1782,1785,1787,1790,1793,1795,1798,1800,1802,1804,1806,1808,1810,1812,1815],{"class":79,"line":666},[141,1783,1784],{"class":176},"    title ",[141,1786,199],{"class":172},[141,1788,1789],{"class":176}," book.add_format({",[141,1791,1792],{"class":150},"\"font_size\"",[141,1794,471],{"class":176},[141,1796,1797],{"class":251},"16",[141,1799,217],{"class":176},[141,1801,949],{"class":150},[141,1803,471],{"class":176},[141,1805,954],{"class":251},[141,1807,217],{"class":176},[141,1809,844],{"class":150},[141,1811,471],{"class":176},[141,1813,1814],{"class":150},"\"#172033\"",[141,1816,372],{"class":176},[141,1818,1819,1822,1825,1827,1830],{"class":79,"line":1034},[141,1820,1821],{"class":176},"    sheet.write(",[141,1823,1824],{"class":150},"\"B1\"",[141,1826,217],{"class":176},[141,1828,1829],{"class":150},"\"August 2026 — regional performance\"",[141,1831,1832],{"class":176},", title)\n",[141,1834,1835,1838,1841,1843,1845],{"class":79,"line":1069},[141,1836,1837],{"class":176},"    write_kpi_block(book, sheet, kpis(orders, prior), ",[141,1839,1840],{"class":1500},"first_row",[141,1842,199],{"class":172},[141,1844,1749],{"class":251},[141,1846,1281],{"class":176},[10,1848,1849,1852],{},[14,1850,1851],{},"hide_gridlines(2)"," is the single change that makes a generated sheet look designed rather than\nexported. Leaving column A narrow and empty gives the block a left margin, which is the other half of\nthe same effect.",[127,1854,1856],{"id":1855},"the-openpyxl-version-for-an-existing-workbook","The openpyxl version, for an existing workbook",[10,1858,1859],{},"xlsxwriter cannot open a file, so a dashboard added to a workbook that already exists has to go\nthrough openpyxl. The same tiles, expressed as merged ranges with styles:",[132,1861,1863],{"className":163,"code":1862,"language":165,"meta":137,"style":137},"from openpyxl import load_workbook\nfrom openpyxl.styles import Alignment, Border, Font, PatternFill, Side\n\nbook = load_workbook(\"existing-report.xlsx\")\nsheet = book[\"Dashboard\"] if \"Dashboard\" in book.sheetnames else book.create_sheet(\"Dashboard\", 0)\n\nfill = PatternFill(\"solid\", fgColor=\"F0F4FF\")\nedge = Side(style=\"thin\", color=\"CDD5E6\")\nbox = Border(left=edge, right=edge, top=edge, bottom=edge)\n\nfor index, tile in enumerate(kpis(orders, prior)):\n    col = 2 + index * 4\n    sheet.merge_cells(start_row=3, start_column=col, end_row=3, end_column=col + 2)\n    sheet.merge_cells(start_row=4, start_column=col, end_row=4, end_column=col + 2)\n    sheet.cell(row=3, column=col, value=tile[\"label\"])\n    value_cell = sheet.cell(row=4, column=col, value=tile[\"value\"])\n    value_cell.number_format = \"#,##0\"\n    value_cell.font = Font(size=18, bold=True)\n    for row in (3, 4):\n        for offset in range(3):\n            cell = sheet.cell(row=row, column=col + offset)\n            cell.fill = fill\n            cell.border = box\n            cell.alignment = Alignment(horizontal=\"center\", vertical=\"center\")\n\nsheet.sheet_view.showGridLines = False\nbook.save(\"existing-report-with-dashboard.xlsx\")\n",[14,1864,1865,1878,1890,1894,1909,1947,1951,1976,2006,2046,2050,2064,2083,2128,2167,2201,2235,2245,2273,2293,2312,2339,2349,2359,2387,2391,2401],{"__ignoreMap":137},[141,1866,1867,1870,1873,1875],{"class":79,"line":143},[141,1868,1869],{"class":172},"from",[141,1871,1872],{"class":176}," openpyxl ",[141,1874,173],{"class":172},[141,1876,1877],{"class":176}," load_workbook\n",[141,1879,1880,1882,1885,1887],{"class":79,"line":186},[141,1881,1869],{"class":172},[141,1883,1884],{"class":176}," openpyxl.styles ",[141,1886,173],{"class":172},[141,1888,1889],{"class":176}," Alignment, Border, Font, PatternFill, Side\n",[141,1891,1892],{"class":79,"line":193},[141,1893,190],{"emptyLinePlaceholder":189},[141,1895,1896,1899,1901,1904,1907],{"class":79,"line":205},[141,1897,1898],{"class":176},"book ",[141,1900,199],{"class":172},[141,1902,1903],{"class":176}," load_workbook(",[141,1905,1906],{"class":150},"\"existing-report.xlsx\"",[141,1908,1281],{"class":176},[141,1910,1911,1914,1916,1919,1921,1923,1925,1928,1931,1934,1936,1939,1941,1943,1945],{"class":79,"line":243},[141,1912,1913],{"class":176},"sheet ",[141,1915,199],{"class":172},[141,1917,1918],{"class":176}," book[",[141,1920,1670],{"class":150},[141,1922,1363],{"class":176},[141,1924,1354],{"class":172},[141,1926,1927],{"class":150}," \"Dashboard\"",[141,1929,1930],{"class":172}," in",[141,1932,1933],{"class":176}," book.sheetnames ",[141,1935,433],{"class":172},[141,1937,1938],{"class":176}," book.create_sheet(",[141,1940,1670],{"class":150},[141,1942,217],{"class":176},[141,1944,46],{"class":251},[141,1946,1281],{"class":176},[141,1948,1949],{"class":79,"line":282},[141,1950,190],{"emptyLinePlaceholder":189},[141,1952,1953,1956,1958,1961,1964,1966,1969,1971,1974],{"class":79,"line":320},[141,1954,1955],{"class":176},"fill ",[141,1957,199],{"class":172},[141,1959,1960],{"class":176}," PatternFill(",[141,1962,1963],{"class":150},"\"solid\"",[141,1965,217],{"class":176},[141,1967,1968],{"class":1500},"fgColor",[141,1970,199],{"class":172},[141,1972,1973],{"class":150},"\"F0F4FF\"",[141,1975,1281],{"class":176},[141,1977,1978,1981,1983,1986,1989,1991,1994,1996,1999,2001,2004],{"class":79,"line":329},[141,1979,1980],{"class":176},"edge ",[141,1982,199],{"class":172},[141,1984,1985],{"class":176}," Side(",[141,1987,1988],{"class":1500},"style",[141,1990,199],{"class":172},[141,1992,1993],{"class":150},"\"thin\"",[141,1995,217],{"class":176},[141,1997,1998],{"class":1500},"color",[141,2000,199],{"class":172},[141,2002,2003],{"class":150},"\"CDD5E6\"",[141,2005,1281],{"class":176},[141,2007,2008,2011,2013,2016,2019,2021,2024,2027,2029,2031,2034,2036,2038,2041,2043],{"class":79,"line":363},[141,2009,2010],{"class":176},"box ",[141,2012,199],{"class":172},[141,2014,2015],{"class":176}," Border(",[141,2017,2018],{"class":1500},"left",[141,2020,199],{"class":172},[141,2022,2023],{"class":176},"edge, ",[141,2025,2026],{"class":1500},"right",[141,2028,199],{"class":172},[141,2030,2023],{"class":176},[141,2032,2033],{"class":1500},"top",[141,2035,199],{"class":172},[141,2037,2023],{"class":176},[141,2039,2040],{"class":1500},"bottom",[141,2042,199],{"class":172},[141,2044,2045],{"class":176},"edge)\n",[141,2047,2048],{"class":79,"line":369},[141,2049,190],{"emptyLinePlaceholder":189},[141,2051,2052,2055,2057,2059,2061],{"class":79,"line":595},[141,2053,2054],{"class":172},"for",[141,2056,1241],{"class":176},[141,2058,1244],{"class":172},[141,2060,1247],{"class":251},[141,2062,2063],{"class":176},"(kpis(orders, prior)):\n",[141,2065,2066,2069,2071,2073,2076,2078,2080],{"class":79,"line":618},[141,2067,2068],{"class":176},"    col ",[141,2070,199],{"class":172},[141,2072,1396],{"class":251},[141,2074,2075],{"class":172}," +",[141,2077,1267],{"class":176},[141,2079,1270],{"class":172},[141,2081,2082],{"class":251}," 4\n",[141,2084,2085,2088,2091,2093,2095,2097,2100,2102,2105,2108,2110,2112,2114,2117,2119,2122,2124,2126],{"class":79,"line":641},[141,2086,2087],{"class":176},"    sheet.merge_cells(",[141,2089,2090],{"class":1500},"start_row",[141,2092,199],{"class":172},[141,2094,816],{"class":251},[141,2096,217],{"class":176},[141,2098,2099],{"class":1500},"start_column",[141,2101,199],{"class":172},[141,2103,2104],{"class":176},"col, ",[141,2106,2107],{"class":1500},"end_row",[141,2109,199],{"class":172},[141,2111,816],{"class":251},[141,2113,217],{"class":176},[141,2115,2116],{"class":1500},"end_column",[141,2118,199],{"class":172},[141,2120,2121],{"class":176},"col ",[141,2123,1264],{"class":172},[141,2125,1396],{"class":251},[141,2127,1281],{"class":176},[141,2129,2130,2132,2134,2136,2139,2141,2143,2145,2147,2149,2151,2153,2155,2157,2159,2161,2163,2165],{"class":79,"line":666},[141,2131,2087],{"class":176},[141,2133,2090],{"class":1500},[141,2135,199],{"class":172},[141,2137,2138],{"class":251},"4",[141,2140,217],{"class":176},[141,2142,2099],{"class":1500},[141,2144,199],{"class":172},[141,2146,2104],{"class":176},[141,2148,2107],{"class":1500},[141,2150,199],{"class":172},[141,2152,2138],{"class":251},[141,2154,217],{"class":176},[141,2156,2116],{"class":1500},[141,2158,199],{"class":172},[141,2160,2121],{"class":176},[141,2162,1264],{"class":172},[141,2164,1396],{"class":251},[141,2166,1281],{"class":176},[141,2168,2169,2172,2175,2177,2179,2181,2184,2186,2188,2191,2193,2196,2198],{"class":79,"line":1034},[141,2170,2171],{"class":176},"    sheet.cell(",[141,2173,2174],{"class":1500},"row",[141,2176,199],{"class":172},[141,2178,816],{"class":251},[141,2180,217],{"class":176},[141,2182,2183],{"class":1500},"column",[141,2185,199],{"class":172},[141,2187,2104],{"class":176},[141,2189,2190],{"class":1500},"value",[141,2192,199],{"class":172},[141,2194,2195],{"class":176},"tile[",[141,2197,468],{"class":150},[141,2199,2200],{"class":176},"])\n",[141,2202,2203,2206,2208,2211,2213,2215,2217,2219,2221,2223,2225,2227,2229,2231,2233],{"class":79,"line":1069},[141,2204,2205],{"class":176},"    value_cell ",[141,2207,199],{"class":172},[141,2209,2210],{"class":176}," sheet.cell(",[141,2212,2174],{"class":1500},[141,2214,199],{"class":172},[141,2216,2138],{"class":251},[141,2218,217],{"class":176},[141,2220,2183],{"class":1500},[141,2222,199],{"class":172},[141,2224,2104],{"class":176},[141,2226,2190],{"class":1500},[141,2228,199],{"class":172},[141,2230,2195],{"class":176},[141,2232,479],{"class":150},[141,2234,2200],{"class":176},[141,2236,2237,2240,2242],{"class":79,"line":1104},[141,2238,2239],{"class":176},"    value_cell.number_format ",[141,2241,199],{"class":172},[141,2243,2244],{"class":150}," \"#,##0\"\n",[141,2246,2247,2250,2252,2255,2258,2260,2262,2264,2267,2269,2271],{"class":79,"line":1115},[141,2248,2249],{"class":176},"    value_cell.font ",[141,2251,199],{"class":172},[141,2253,2254],{"class":176}," Font(",[141,2256,2257],{"class":1500},"size",[141,2259,199],{"class":172},[141,2261,698],{"class":251},[141,2263,217],{"class":176},[141,2265,2266],{"class":1500},"bold",[141,2268,199],{"class":172},[141,2270,954],{"class":251},[141,2272,1281],{"class":176},[141,2274,2275,2277,2280,2282,2285,2287,2289,2291],{"class":79,"line":1120},[141,2276,1238],{"class":172},[141,2278,2279],{"class":176}," row ",[141,2281,1244],{"class":172},[141,2283,2284],{"class":176}," (",[141,2286,816],{"class":251},[141,2288,217],{"class":176},[141,2290,2138],{"class":251},[141,2292,819],{"class":176},[141,2294,2295,2298,2301,2303,2306,2308,2310],{"class":79,"line":1130},[141,2296,2297],{"class":172},"        for",[141,2299,2300],{"class":176}," offset ",[141,2302,1244],{"class":172},[141,2304,2305],{"class":251}," range",[141,2307,1611],{"class":176},[141,2309,816],{"class":251},[141,2311,819],{"class":176},[141,2313,2314,2317,2319,2321,2323,2325,2328,2330,2332,2334,2336],{"class":79,"line":1159},[141,2315,2316],{"class":176},"            cell ",[141,2318,199],{"class":172},[141,2320,2210],{"class":176},[141,2322,2174],{"class":1500},[141,2324,199],{"class":172},[141,2326,2327],{"class":176},"row, ",[141,2329,2183],{"class":1500},[141,2331,199],{"class":172},[141,2333,2121],{"class":176},[141,2335,1264],{"class":172},[141,2337,2338],{"class":176}," offset)\n",[141,2340,2341,2344,2346],{"class":79,"line":1188},[141,2342,2343],{"class":176},"            cell.fill ",[141,2345,199],{"class":172},[141,2347,2348],{"class":176}," fill\n",[141,2350,2351,2354,2356],{"class":79,"line":1225},[141,2352,2353],{"class":176},"            cell.border ",[141,2355,199],{"class":172},[141,2357,2358],{"class":176}," box\n",[141,2360,2361,2364,2366,2369,2372,2374,2376,2378,2381,2383,2385],{"class":79,"line":1230},[141,2362,2363],{"class":176},"            cell.alignment ",[141,2365,199],{"class":172},[141,2367,2368],{"class":176}," Alignment(",[141,2370,2371],{"class":1500},"horizontal",[141,2373,199],{"class":172},[141,2375,859],{"class":150},[141,2377,217],{"class":176},[141,2379,2380],{"class":1500},"vertical",[141,2382,199],{"class":172},[141,2384,859],{"class":150},[141,2386,1281],{"class":176},[141,2388,2389],{"class":79,"line":1235},[141,2390,190],{"emptyLinePlaceholder":189},[141,2392,2393,2396,2398],{"class":79,"line":1253},[141,2394,2395],{"class":176},"sheet.sheet_view.showGridLines ",[141,2397,199],{"class":172},[141,2399,2400],{"class":251}," False\n",[141,2402,2403,2406,2409],{"class":79,"line":1284},[141,2404,2405],{"class":176},"book.save(",[141,2407,2408],{"class":150},"\"existing-report-with-dashboard.xlsx\"",[141,2410,1281],{"class":176},[10,2412,2413],{},"The inner loop applying the border to every cell of the merged range is the part people leave out.\nOnly the top-left cell holds the value, but a merged range with a border on one cell draws one edge,\nnot a box.",[127,2415,2417],{"id":2416},"common-pitfalls","Common pitfalls",[2419,2420,2421,2437],"table",{},[2422,2423,2424],"thead",{},[2425,2426,2427,2431,2434],"tr",{},[2428,2429,2430],"th",{},"Symptom",[2428,2432,2433],{},"Cause",[2428,2435,2436],{},"Fix",[2438,2439,2440,2452,2469,2484,2495,2510],"tbody",{},[2425,2441,2442,2446,2449],{},[2443,2444,2445],"td",{},"The KPI box has a partial border",[2443,2447,2448],{},"Border applied only to the anchor cell",[2443,2450,2451],{},"Apply it to every cell in the merged range",[2425,2453,2454,2457,2460],{},[2443,2455,2456],{},"Values overflow the tile",[2443,2458,2459],{},"Row height left at the default",[2443,2461,2462,2465,2466],{},[14,2463,2464],{},"set_row(..., 34)"," or set ",[14,2467,2468],{},"row_dimensions[...].height",[2425,2470,2471,2474,2477],{},[2443,2472,2473],{},"Percentages show as 1240%",[2443,2475,2476],{},"A proportion written where a percentage was formatted",[2443,2478,2479,2480,2483],{},"Write ",[14,2481,2482],{},"0.124",", and let the number format add the sign",[2425,2485,2486,2489,2492],{},[2443,2487,2488],{},"Merged cells lose their fill",[2443,2490,2491],{},"Fill applied before merging in openpyxl",[2443,2493,2494],{},"Merge first, then style every cell in the range",[2425,2496,2497,2500,2503],{},[2443,2498,2499],{},"The block shifts when a column is inserted",[2443,2501,2502],{},"Absolute column positions",[2443,2504,2505,2506,2509],{},"Compute positions from one ",[14,2507,2508],{},"first_col"," variable",[2425,2511,2512,2518,2521],{},[2443,2513,2514,2515],{},"A delta shows as ",[14,2516,2517],{},"inf",[2443,2519,2520],{},"Prior period was zero",[2443,2522,2523,2524,2526],{},"Return ",[14,2525,675],{}," and render a dash",[127,2528,2530],{"id":2529},"keeping-the-block-aligned-with-the-table-below","Keeping the block aligned with the table below",[10,2532,2533,2534,2536,2537,2540],{},"A dashboard reads badly when the tiles and the table beneath them use different column widths. The\nfix is to pick one grid — twelve narrow columns, say — and express both in terms of it: each tile\nspans three, the table below spans all twelve. Because the tile-drawing function already takes\n",[14,2535,2508],{}," and ",[14,2538,2539],{},"width",", changing the layout is two arguments rather than a rewrite.",[132,2542,2544],{"className":163,"code":2543,"language":165,"meta":137,"style":137},"write_kpi_block(book, sheet, tiles[:4], first_row=2, first_col=1, width=3)   # 4 across\nwrite_kpi_block(book, sheet, tiles[:3], first_row=2, first_col=1, width=4)   # 3 across\n",[14,2545,2546,2585],{"__ignoreMap":137},[141,2547,2548,2551,2553,2556,2558,2560,2562,2564,2566,2568,2570,2572,2574,2576,2578,2581],{"class":79,"line":143},[141,2549,2550],{"class":176},"write_kpi_block(book, sheet, tiles[:",[141,2552,2138],{"class":251},[141,2554,2555],{"class":176},"], ",[141,2557,1840],{"class":1500},[141,2559,199],{"class":172},[141,2561,1749],{"class":251},[141,2563,217],{"class":176},[141,2565,2508],{"class":1500},[141,2567,199],{"class":172},[141,2569,801],{"class":251},[141,2571,217],{"class":176},[141,2573,2539],{"class":1500},[141,2575,199],{"class":172},[141,2577,816],{"class":251},[141,2579,2580],{"class":176},")   ",[141,2582,2584],{"class":2583},"s-wDw","# 4 across\n",[141,2586,2587,2589,2591,2593,2595,2597,2599,2601,2603,2605,2607,2609,2611,2613,2615,2617],{"class":79,"line":186},[141,2588,2550],{"class":176},[141,2590,816],{"class":251},[141,2592,2555],{"class":176},[141,2594,1840],{"class":1500},[141,2596,199],{"class":172},[141,2598,1749],{"class":251},[141,2600,217],{"class":176},[141,2602,2508],{"class":1500},[141,2604,199],{"class":172},[141,2606,801],{"class":251},[141,2608,217],{"class":176},[141,2610,2539],{"class":1500},[141,2612,199],{"class":172},[141,2614,2138],{"class":251},[141,2616,2580],{"class":176},[141,2618,2619],{"class":2583},"# 3 across\n",[10,2621,2622,2623,2626,2627,1478],{},"Fixing the widths once with ",[14,2624,2625],{},"set_column(\"B:M\", 11)"," and then never setting individual widths keeps\nthat grid intact. It is also what makes the sheet print sensibly, since a uniform grid fits a page\nwidth predictably — the setup covered in\n",[19,2628,2630],{"href":2629},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fset-print-area-and-page-setup-with-openpyxl\u002F","Set the Print Area and Page Setup with openpyxl",[127,2632,2634],{"id":2633},"testing-the-block-without-opening-excel","Testing the block without opening Excel",[10,2636,2637],{},"Because the tiles are computed as data before anything is drawn, the numbers can be asserted in a\ntest that never touches a workbook — which is the main practical reason to keep that separation.",[132,2639,2641],{"className":163,"code":2640,"language":165,"meta":137,"style":137},"def test_kpis_handle_an_empty_prior_period():\n    frame = pd.DataFrame({\"Revenue\": [100.0, 200.0], \"Units\": [1, 2]})\n    empty = frame.iloc[0:0]\n    tiles = kpis(frame, empty)\n    assert tiles[0][\"value\"] == 300.0\n    assert tiles[0][\"delta\"] is None       # no prior period, no growth figure\n",[14,2642,2643,2653,2690,2709,2719,2741],{"__ignoreMap":137},[141,2644,2645,2647,2650],{"class":79,"line":143},[141,2646,389],{"class":172},[141,2648,2649],{"class":392}," test_kpis_handle_an_empty_prior_period",[141,2651,2652],{"class":176},"():\n",[141,2654,2655,2658,2660,2663,2665,2667,2670,2672,2675,2677,2679,2681,2683,2685,2687],{"class":79,"line":186},[141,2656,2657],{"class":176},"    frame ",[141,2659,199],{"class":172},[141,2661,2662],{"class":176}," pd.DataFrame({",[141,2664,474],{"class":150},[141,2666,211],{"class":176},[141,2668,2669],{"class":251},"100.0",[141,2671,217],{"class":176},[141,2673,2674],{"class":251},"200.0",[141,2676,2555],{"class":176},[141,2678,575],{"class":150},[141,2680,211],{"class":176},[141,2682,801],{"class":251},[141,2684,217],{"class":176},[141,2686,1749],{"class":251},[141,2688,2689],{"class":176},"]})\n",[141,2691,2692,2695,2697,2700,2702,2705,2707],{"class":79,"line":193},[141,2693,2694],{"class":176},"    empty ",[141,2696,199],{"class":172},[141,2698,2699],{"class":176}," frame.iloc[",[141,2701,46],{"class":251},[141,2703,2704],{"class":176},":",[141,2706,46],{"class":251},[141,2708,1741],{"class":176},[141,2710,2711,2714,2716],{"class":79,"line":205},[141,2712,2713],{"class":176},"    tiles ",[141,2715,199],{"class":172},[141,2717,2718],{"class":176}," kpis(frame, empty)\n",[141,2720,2721,2724,2727,2729,2732,2734,2736,2738],{"class":79,"line":243},[141,2722,2723],{"class":172},"    assert",[141,2725,2726],{"class":176}," tiles[",[141,2728,46],{"class":251},[141,2730,2731],{"class":176},"][",[141,2733,479],{"class":150},[141,2735,1363],{"class":176},[141,2737,1366],{"class":172},[141,2739,2740],{"class":251}," 300.0\n",[141,2742,2743,2745,2747,2749,2751,2753,2755,2757,2759],{"class":79,"line":282},[141,2744,2723],{"class":172},[141,2746,2726],{"class":176},[141,2748,46],{"class":251},[141,2750,2731],{"class":176},[141,2752,502],{"class":150},[141,2754,1363],{"class":176},[141,2756,1434],{"class":172},[141,2758,421],{"class":251},[141,2760,2761],{"class":2583},"       # no prior period, no growth figure\n",[10,2763,2764,2765,1478],{},"The empty-prior case is the one worth testing first, because it is what happens on the first run of a\nnew report and it is where a division by zero would otherwise surface in front of the audience. Once\nthe values are covered, a second test can open the generated workbook and assert on a handful of\ncells — the approach in\n",[19,2766,2768],{"href":2767},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest",[127,2770,2772],{"id":2771},"performance-and-scale","Performance and scale",[25,2774,34,2779,34,2782,34,2785,34,2788,34,2795,34,2801,34,2807,34,2811,34,2815,34,2819,34,2822,34,2827,34,2831,34,2834,34,2837,34,2840,34,2848,34,2853,34,2858,34,2861,34,2865,34,2868],{"viewBox":2775,"role":28,"ariaLabelledBy":2776,"xmlns":32,"style":33},"0 0 760 201",[2777,2778],"kpi-pass-t","kpi-pass-d",[36,2780,2781],{"id":2777},"Where the tile numbers come from",[40,2783,2784],{"id":2778},"Computing each headline figure with its own pass over the frame scans the data once per tile, while a single aggregation produces all of them in one pass.",[44,2786],{"x":46,"y":46,"width":47,"height":2787,"fill":49},"201",[44,2789],{"x":2790,"y":2791,"width":2792,"height":2793,"rx":2794,"fill":735,"stroke":736,"style":66},"20","28","270.0","139","14",[51,2796,2800],{"x":2797,"y":2798,"style":2799},"155.0","54","font-size:13px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:middle","a pass per tile",[79,2802],{"x1":2803,"y1":2804,"x2":2805,"y2":2804,"stroke":736,"style":2806},"36","64","274.0","stroke-width:1px",[51,2808,2810],{"x":2797,"y":2809,"style":727},"86","8 scans for 8 tiles",[51,2812,2814],{"x":2797,"y":2813,"style":727},"109","re-reads the archive",[51,2816,2818],{"x":2797,"y":2817,"style":727},"132","grows with the strip",[44,2820],{"x":2821,"y":2791,"width":2792,"height":2793,"rx":2794,"fill":111,"stroke":112,"style":66},"470.0",[51,2823,2826],{"x":2824,"y":2798,"style":2825},"605.0","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","one aggregation",[79,2828],{"x1":2829,"y1":2804,"x2":2830,"y2":2804,"stroke":112,"style":2806},"486.0","724.0",[51,2832,2833],{"x":2824,"y":2809,"style":727},"1 scan total",[51,2835,2836],{"x":2824,"y":2813,"style":727},"prior period read once",[51,2838,2839],{"x":2824,"y":2817,"style":727},"constant in tile count",[44,2841],{"x":2842,"y":2843,"width":2844,"height":2845,"rx":2846,"fill":2847,"stroke":84},"316.0","78.5","128","38","19","#ebebfd",[51,2849,2852],{"x":53,"y":2850,"style":2851},"102.5","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","aggregate",[79,2854],{"x1":2855,"y1":2856,"x2":2857,"y2":2856,"stroke":84,"style":66},"295.0","97.5","309.0",[86,2859],{"points":2860,"fill":89},"309.0,97.5 300.0,92.5 300.0,102.5",[79,2862],{"x1":2863,"y1":2856,"x2":2864,"y2":2856,"stroke":84,"style":66},"449.0","463.0",[86,2866],{"points":2867,"fill":89},"463.0,97.5 454.0,92.5 454.0,102.5",[51,2869,2871],{"x":53,"y":2870,"style":124},"187","the drawing is free; the scanning is not",[10,2873,2874],{},"A KPI block is a few dozen cells, so its own cost is nothing. What does matter is where the numbers\ncome from: computing each tile with a separate pass over a large frame means one scan per tile, and\ncomputing them from a single grouped aggregation means one scan in total.",[132,2876,2878],{"className":163,"code":2877,"language":165,"meta":137,"style":137},"# One pass over the frame, all the headline numbers\ntotals = orders[[\"Revenue\", \"Units\"]].sum()\nsummary = {\n    \"Revenue\": float(totals[\"Revenue\"]),\n    \"Units\": int(totals[\"Units\"]),\n    \"Orders\": len(orders),\n    \"Average order\": float(orders[\"Revenue\"].mean()),\n}\n",[14,2879,2880,2885,2904,2914,2931,2945,2957,2973],{"__ignoreMap":137},[141,2881,2882],{"class":79,"line":143},[141,2883,2884],{"class":2583},"# One pass over the frame, all the headline numbers\n",[141,2886,2887,2890,2892,2895,2897,2899,2901],{"class":79,"line":186},[141,2888,2889],{"class":176},"totals ",[141,2891,199],{"class":172},[141,2893,2894],{"class":176}," orders[[",[141,2896,474],{"class":150},[141,2898,217],{"class":176},[141,2900,575],{"class":150},[141,2902,2903],{"class":176},"]].sum()\n",[141,2905,2906,2909,2911],{"class":79,"line":193},[141,2907,2908],{"class":176},"summary ",[141,2910,199],{"class":172},[141,2912,2913],{"class":176}," {\n",[141,2915,2916,2918,2920,2923,2926,2928],{"class":79,"line":205},[141,2917,246],{"class":150},[141,2919,471],{"class":176},[141,2921,2922],{"class":251},"float",[141,2924,2925],{"class":176},"(totals[",[141,2927,474],{"class":150},[141,2929,2930],{"class":176},"]),\n",[141,2932,2933,2935,2937,2939,2941,2943],{"class":79,"line":243},[141,2934,285],{"class":150},[141,2936,471],{"class":176},[141,2938,584],{"class":251},[141,2940,2925],{"class":176},[141,2942,575],{"class":150},[141,2944,2930],{"class":176},[141,2946,2947,2950,2952,2954],{"class":79,"line":282},[141,2948,2949],{"class":150},"    \"Orders\"",[141,2951,471],{"class":176},[141,2953,535],{"class":251},[141,2955,2956],{"class":176},"(orders),\n",[141,2958,2959,2962,2964,2966,2968,2970],{"class":79,"line":320},[141,2960,2961],{"class":150},"    \"Average order\"",[141,2963,471],{"class":176},[141,2965,2922],{"class":251},[141,2967,1525],{"class":176},[141,2969,474],{"class":150},[141,2971,2972],{"class":176},"].mean()),\n",[141,2974,2975],{"class":79,"line":329},[141,2976,2977],{"class":176},"}\n",[10,2979,2980],{},"The same argument applies to the prior period: read it once and hold it, rather than re-reading the\narchive workbook for each comparison. On a dashboard with eight tiles that is the difference between\none read and eight of the same file.",[127,2982,2984],{"id":2983},"conclusion","Conclusion",[10,2986,2987],{},"Compute the tiles as data first, then draw them from a single function that takes a starting column\nand a width — that keeps the layout adjustable and the numbers testable. Use a three-section number\nformat so the arrow follows the sign without conditional formatting, apply borders to every cell of\neach merged range, and hide the gridlines. For a workbook that already exists, the same structure\ngoes through openpyxl with merged ranges and style objects.",[127,2989,2991],{"id":2990},"frequently-asked-questions","Frequently asked questions",[10,2993,2994,2998],{},[2995,2996,2997],"strong",{},"Should the KPI values be formulas or literals?","\nWrite literals when the workbook is a snapshot that will be emailed and archived, and formulas when recipients will filter or edit the detail sheets and expect the headline to follow. Mixing them in one workbook is the arrangement that confuses people.",[10,3000,3001,3004],{},[2995,3002,3003],{},"How do I show a change against the previous period?","\nCompute both values in pandas and write the delta as its own cell, formatted with a custom number format that shows an up or down arrow. Excel's conditional formatting icon sets are the alternative and are harder to control from code.",[10,3006,3007,3010],{},[2995,3008,3009],{},"Why do my merged KPI cells lose their border?","\nOnly the top-left cell of a merged range carries the value, but every cell in the range needs its own border to draw a complete box. Loop over the range and apply the border to each cell, or use xlsxwriter's merge_range, which handles it.",[10,3012,3013,3016],{},[2995,3014,3015],{},"How wide should the KPI block be?","\nMatch it to the table below so the columns line up. Setting each tile to span two or three columns of a twelve-column grid keeps the dashboard aligned however many tiles there are.",[127,3018,3020],{"id":3019},"related","Related",[3022,3023,3024,3031,3038,3045,3050],"ul",{},[3025,3026,3027,3028,3030],"li",{},"Up one level: ",[19,3029,22],{"href":21}," — the wider dashboard workbook this strip sits on top of.",[3025,3032,3033,3037],{},[19,3034,3036],{"href":3035},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fbuild-a-dashboard-sheet-with-charts-from-multiple-tabs\u002F","Build a Dashboard Sheet with Charts from Multiple Tabs"," — the charts that go below the KPI block.",[3025,3039,3040,3044],{},[19,3041,3043],{"href":3042},"\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"," — the aggregation feeding these numbers.",[3025,3046,3047,3049],{},[19,3048,1477],{"href":1476}," — the three-section format behind the arrows.",[3025,3051,3052,3056],{},[19,3053,3055],{"href":3054},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fmerge-cells-and-centre-a-report-title-with-openpyxl\u002F","Merge Cells and Centre a Report Title with openpyxl"," — merging and styling in the openpyxl idiom.",[1988,3058,3059],{},"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}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}",{"title":137,"searchDepth":186,"depth":186,"links":3061},[3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073],{"id":129,"depth":186,"text":130},{"id":375,"depth":186,"text":376},{"id":679,"depth":186,"text":680},{"id":1481,"depth":186,"text":1482},{"id":1855,"depth":186,"text":1856},{"id":2416,"depth":186,"text":2417},{"id":2529,"depth":186,"text":2530},{"id":2633,"depth":186,"text":2634},{"id":2771,"depth":186,"text":2772},{"id":2983,"depth":186,"text":2984},{"id":2990,"depth":186,"text":2991},{"id":3019,"depth":186,"text":3020},"2026-09-04","Generate the top strip of a dashboard from Python: merged tiles with label, value and signed comparison, drawn by one reusable function in xlsxwriter or openpyxl.","md",[3078,3080,3082,3084],{"q":2997,"a":3079},"Write literals when the workbook is a snapshot that will be emailed and archived, and formulas when recipients will filter or edit the detail sheets and expect the headline to follow. Mixing them in one workbook is the arrangement that confuses people.",{"q":3003,"a":3081},"Compute both values in pandas and write the delta as its own cell, formatted with a custom number format that shows an up or down arrow. Excel's conditional formatting icon sets are the alternative and are harder to control from code.",{"q":3009,"a":3083},"Only the top-left cell of a merged range carries the value, but every cell in the range needs its own border to draw a complete box. Loop over the range and apply the border to each cell, or use xlsxwriter's merge_range, which handles it.",{"q":3015,"a":3085},"Match it to the table below so the columns line up. Setting each tile to span two or three columns of a twelve-column grid keeps the dashboard aligned however many tiles there are.",{"breadcrumb":3087},[3088,3090,3093],{"name":3089,"item":445},"Home",{"name":3091,"item":3092},"Automating Reporting Workflows","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-kpi-summary-block-to-an-excel-dashboard",{"title":5,"description":3096},"Build a dashboard KPI strip with xlsxwriter or openpyxl — merged tiles, three-section number formats for up and down arrows, hidden gridlines and a consistent column grid.","add-a-kpi-summary-block-to-an-excel-dashboard","automating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-kpi-summary-block-to-an-excel-dashboard\u002Findex","how-to","4ALjQheXYd96o_byG9nD0pO8rvA8yTPko8sH3j9YWSo",[3102,3105],{"title":22,"path":3103,"stem":3104,"children":-1},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","automating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Findex",{"title":3106,"path":3107,"stem":3108,"children":-1},"Add a Table of Contents Sheet with Hyperlinks in Excel","\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-table-of-contents-sheet-with-hyperlinks-in-excel","automating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-table-of-contents-sheet-with-hyperlinks-in-excel\u002Findex",1788710163099]