[{"data":1,"prerenderedAt":2327},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-variance-arrows-and-trend-indicators-to-a-dashboard":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-variance-arrows-and-trend-indicators-to-a-dashboard":2291,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-variance-arrows-and-trend-indicators-to-a-dashboard":2300,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-variance-arrows-and-trend-indicators-to-a-dashboard":2305,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-variance-arrows-and-trend-indicators-to-a-dashboard":2306},{"id":4,"title":5,"body":6,"dateModified":2257,"datePublished":2257,"description":2258,"extension":2259,"faq":2260,"keyword":2269,"kind":2270,"meta":2271,"navigation":128,"path":2280,"seo":2281,"slug":2284,"stem":2285,"topics":2286,"type":2289,"__hash__":2290},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-variance-arrows-and-trend-indicators-to-a-dashboard\u002Findex.md","Add Variance Arrows and Trend Indicators to a Dashboard",{"type":7,"value":8,"toc":2245},"minimark",[9,24,29,58,69,73,629,640,644,647,771,794,805,918,922,925,1055,1058,1062,1069,1511,1522,1646,1650,1657,1845,1857,2112,2116,2156,2160,2167,2171,2185,2196,2202,2208,2212,2241],[10,11,12,13,17,18,23],"p",{},"A dashboard figure on its own invites the question \"is that good?\". A small arrow beside it — green and pointing up when revenue rose, red and pointing down when it fell — answers before anyone reaches for last month's report. The details are where most implementations go wrong: arrows made by concatenating text break the numbers underneath, so the variance can no longer be summed or charted; the same green \"up\" is applied to costs and complaint counts, where up is bad; and a change of 0.1% gets the same bold arrow as a change of 30%. This page adds arrows with custom number formats, which change how a value ",[14,15,16],"em",{},"looks"," but leave it a number, gives measures where a fall is good their own format, adds a dead band so trivial changes show as flat, builds a trend indicator over the last three periods, and checks the displayed result. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002F","Building Multi-Sheet Excel Dashboards",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas\n","bash","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,55],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," openpyxl",[40,56,57],{"class":50}," pandas\n",[10,59,60,61,64,65,68],{},"LibreOffice (",[37,62,63],{},"soffice",") is used to recalculate the saved workbook and export what each cell ",[14,66,67],{},"displays",", so the formats can be checked without opening Excel. The data is six KPIs with this month's and the three previous months' values; two of them — operating cost and complaints — are measures where a fall is the good direction.",[25,70,72],{"id":71},"step-1-the-figures-and-the-variance-formula","Step 1 — The figures and the variance formula",[30,74,78],{"className":75,"code":76,"language":77,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\nfrom openpyxl import Workbook\nfrom openpyxl.styles import Font\n\nkpis = pd.DataFrame({\n    \"Measure\": [\"Revenue\", \"Orders\", \"Conversion\", \"Operating cost\", \"Complaints\", \"Avg order value\"],\n    \"Good when\": [\"up\", \"up\", \"up\", \"down\", \"down\", \"up\"],\n    \"Sep\": [1284500, 3412, 0.0312, 412800, 37, 376.5],\n    \"Aug\": [1215200, 3389, 0.0298, 398900, 52, 358.6],\n    \"Jul\": [1180400, 3350, 0.0301, 401200, 49, 352.4],\n    \"Jun\": [1150900, 3302, 0.0295, 405100, 44, 348.5],\n})\n\nwb = Workbook()\nws = wb.active\nws.title = \"Dashboard\"\nws.append([\"Measure\", \"Sep\", \"Aug\", \"Jul\", \"Jun\", \"vs last month\", \"Trend\"])\nfor c in ws[1]:\n    c.font = Font(bold=True)\nfor i, row in enumerate(kpis.itertuples(index=False), start=2):\n    ws.append([row.Measure, row.Sep, row.Aug, row.Jul, row.Jun])\n    ws[f\"F{i}\"] = f\"=IF(C{i}=0,NA(),B{i}\u002FC{i}-1)\"\nprint(ws.max_row - 1, \"measures; variance in column F as a formula\")\n","python",[37,79,80,96,110,123,130,142,183,217,256,294,332,370,376,381,392,403,414,456,477,500,538,544,607],{"__ignoreMap":35},[40,81,82,86,90,93],{"class":42,"line":43},[40,83,85],{"class":84},"s-kum","import",[40,87,89],{"class":88},"skGVy"," pandas ",[40,91,92],{"class":84},"as",[40,94,95],{"class":88}," pd\n",[40,97,99,102,105,107],{"class":42,"line":98},2,[40,100,101],{"class":84},"from",[40,103,104],{"class":88}," openpyxl ",[40,106,85],{"class":84},[40,108,109],{"class":88}," Workbook\n",[40,111,113,115,118,120],{"class":42,"line":112},3,[40,114,101],{"class":84},[40,116,117],{"class":88}," openpyxl.styles ",[40,119,85],{"class":84},[40,121,122],{"class":88}," Font\n",[40,124,126],{"class":42,"line":125},4,[40,127,129],{"emptyLinePlaceholder":128},true,"\n",[40,131,133,136,139],{"class":42,"line":132},5,[40,134,135],{"class":88},"kpis ",[40,137,138],{"class":84},"=",[40,140,141],{"class":88}," pd.DataFrame({\n",[40,143,145,148,151,154,157,160,162,165,167,170,172,175,177,180],{"class":42,"line":144},6,[40,146,147],{"class":50},"    \"Measure\"",[40,149,150],{"class":88},": [",[40,152,153],{"class":50},"\"Revenue\"",[40,155,156],{"class":88},", ",[40,158,159],{"class":50},"\"Orders\"",[40,161,156],{"class":88},[40,163,164],{"class":50},"\"Conversion\"",[40,166,156],{"class":88},[40,168,169],{"class":50},"\"Operating cost\"",[40,171,156],{"class":88},[40,173,174],{"class":50},"\"Complaints\"",[40,176,156],{"class":88},[40,178,179],{"class":50},"\"Avg order value\"",[40,181,182],{"class":88},"],\n",[40,184,186,189,191,194,196,198,200,202,204,207,209,211,213,215],{"class":42,"line":185},7,[40,187,188],{"class":50},"    \"Good when\"",[40,190,150],{"class":88},[40,192,193],{"class":50},"\"up\"",[40,195,156],{"class":88},[40,197,193],{"class":50},[40,199,156],{"class":88},[40,201,193],{"class":50},[40,203,156],{"class":88},[40,205,206],{"class":50},"\"down\"",[40,208,156],{"class":88},[40,210,206],{"class":50},[40,212,156],{"class":88},[40,214,193],{"class":50},[40,216,182],{"class":88},[40,218,220,223,225,229,231,234,236,239,241,244,246,249,251,254],{"class":42,"line":219},8,[40,221,222],{"class":50},"    \"Sep\"",[40,224,150],{"class":88},[40,226,228],{"class":227},"sP0c6","1284500",[40,230,156],{"class":88},[40,232,233],{"class":227},"3412",[40,235,156],{"class":88},[40,237,238],{"class":227},"0.0312",[40,240,156],{"class":88},[40,242,243],{"class":227},"412800",[40,245,156],{"class":88},[40,247,248],{"class":227},"37",[40,250,156],{"class":88},[40,252,253],{"class":227},"376.5",[40,255,182],{"class":88},[40,257,259,262,264,267,269,272,274,277,279,282,284,287,289,292],{"class":42,"line":258},9,[40,260,261],{"class":50},"    \"Aug\"",[40,263,150],{"class":88},[40,265,266],{"class":227},"1215200",[40,268,156],{"class":88},[40,270,271],{"class":227},"3389",[40,273,156],{"class":88},[40,275,276],{"class":227},"0.0298",[40,278,156],{"class":88},[40,280,281],{"class":227},"398900",[40,283,156],{"class":88},[40,285,286],{"class":227},"52",[40,288,156],{"class":88},[40,290,291],{"class":227},"358.6",[40,293,182],{"class":88},[40,295,297,300,302,305,307,310,312,315,317,320,322,325,327,330],{"class":42,"line":296},10,[40,298,299],{"class":50},"    \"Jul\"",[40,301,150],{"class":88},[40,303,304],{"class":227},"1180400",[40,306,156],{"class":88},[40,308,309],{"class":227},"3350",[40,311,156],{"class":88},[40,313,314],{"class":227},"0.0301",[40,316,156],{"class":88},[40,318,319],{"class":227},"401200",[40,321,156],{"class":88},[40,323,324],{"class":227},"49",[40,326,156],{"class":88},[40,328,329],{"class":227},"352.4",[40,331,182],{"class":88},[40,333,335,338,340,343,345,348,350,353,355,358,360,363,365,368],{"class":42,"line":334},11,[40,336,337],{"class":50},"    \"Jun\"",[40,339,150],{"class":88},[40,341,342],{"class":227},"1150900",[40,344,156],{"class":88},[40,346,347],{"class":227},"3302",[40,349,156],{"class":88},[40,351,352],{"class":227},"0.0295",[40,354,156],{"class":88},[40,356,357],{"class":227},"405100",[40,359,156],{"class":88},[40,361,362],{"class":227},"44",[40,364,156],{"class":88},[40,366,367],{"class":227},"348.5",[40,369,182],{"class":88},[40,371,373],{"class":42,"line":372},12,[40,374,375],{"class":88},"})\n",[40,377,379],{"class":42,"line":378},13,[40,380,129],{"emptyLinePlaceholder":128},[40,382,384,387,389],{"class":42,"line":383},14,[40,385,386],{"class":88},"wb ",[40,388,138],{"class":84},[40,390,391],{"class":88}," Workbook()\n",[40,393,395,398,400],{"class":42,"line":394},15,[40,396,397],{"class":88},"ws ",[40,399,138],{"class":84},[40,401,402],{"class":88}," wb.active\n",[40,404,406,409,411],{"class":42,"line":405},16,[40,407,408],{"class":88},"ws.title ",[40,410,138],{"class":84},[40,412,413],{"class":50}," \"Dashboard\"\n",[40,415,417,420,423,425,428,430,433,435,438,440,443,445,448,450,453],{"class":42,"line":416},17,[40,418,419],{"class":88},"ws.append([",[40,421,422],{"class":50},"\"Measure\"",[40,424,156],{"class":88},[40,426,427],{"class":50},"\"Sep\"",[40,429,156],{"class":88},[40,431,432],{"class":50},"\"Aug\"",[40,434,156],{"class":88},[40,436,437],{"class":50},"\"Jul\"",[40,439,156],{"class":88},[40,441,442],{"class":50},"\"Jun\"",[40,444,156],{"class":88},[40,446,447],{"class":50},"\"vs last month\"",[40,449,156],{"class":88},[40,451,452],{"class":50},"\"Trend\"",[40,454,455],{"class":88},"])\n",[40,457,459,462,465,468,471,474],{"class":42,"line":458},18,[40,460,461],{"class":84},"for",[40,463,464],{"class":88}," c ",[40,466,467],{"class":84},"in",[40,469,470],{"class":88}," ws[",[40,472,473],{"class":227},"1",[40,475,476],{"class":88},"]:\n",[40,478,480,483,485,488,492,494,497],{"class":42,"line":479},19,[40,481,482],{"class":88},"    c.font ",[40,484,138],{"class":84},[40,486,487],{"class":88}," Font(",[40,489,491],{"class":490},"sa561","bold",[40,493,138],{"class":84},[40,495,496],{"class":227},"True",[40,498,499],{"class":88},")\n",[40,501,503,505,508,510,513,516,519,521,524,527,530,532,535],{"class":42,"line":502},20,[40,504,461],{"class":84},[40,506,507],{"class":88}," i, row ",[40,509,467],{"class":84},[40,511,512],{"class":227}," enumerate",[40,514,515],{"class":88},"(kpis.itertuples(",[40,517,518],{"class":490},"index",[40,520,138],{"class":84},[40,522,523],{"class":227},"False",[40,525,526],{"class":88},"), ",[40,528,529],{"class":490},"start",[40,531,138],{"class":84},[40,533,534],{"class":227},"2",[40,536,537],{"class":88},"):\n",[40,539,541],{"class":42,"line":540},21,[40,542,543],{"class":88},"    ws.append([row.Measure, row.Sep, row.Aug, row.Jul, row.Jun])\n",[40,545,547,550,553,556,560,563,566,569,572,574,577,580,582,584,586,589,591,593,595,598,600,602,604],{"class":42,"line":546},22,[40,548,549],{"class":88},"    ws[",[40,551,552],{"class":84},"f",[40,554,555],{"class":50},"\"F",[40,557,559],{"class":558},"sSjpA","{",[40,561,562],{"class":88},"i",[40,564,565],{"class":558},"}",[40,567,568],{"class":50},"\"",[40,570,571],{"class":88},"] ",[40,573,138],{"class":84},[40,575,576],{"class":84}," f",[40,578,579],{"class":50},"\"=IF(C",[40,581,559],{"class":558},[40,583,562],{"class":88},[40,585,565],{"class":558},[40,587,588],{"class":50},"=0,NA(),B",[40,590,559],{"class":558},[40,592,562],{"class":88},[40,594,565],{"class":558},[40,596,597],{"class":50},"\u002FC",[40,599,559],{"class":558},[40,601,562],{"class":88},[40,603,565],{"class":558},[40,605,606],{"class":50},"-1)\"\n",[40,608,610,613,616,619,622,624,627],{"class":42,"line":609},23,[40,611,612],{"class":227},"print",[40,614,615],{"class":88},"(ws.max_row ",[40,617,618],{"class":84},"-",[40,620,621],{"class":227}," 1",[40,623,156],{"class":88},[40,625,626],{"class":50},"\"measures; variance in column F as a formula\"",[40,628,499],{"class":88},[10,630,631,632,635,636,639],{},"The variance is a formula — this month over last month, minus one — so the workbook stays live if someone corrects a figure, and it returns ",[37,633,634],{},"#N\u002FA"," rather than a division error when last month was zero. Most importantly it is a ",[14,637,638],{},"number",". Everything that follows changes only how it is displayed.",[25,641,643],{"id":642},"step-2-arrows-from-a-custom-number-format","Step 2 — Arrows from a custom number format",[10,645,646],{},"A number format can have up to four sections separated by semicolons — positive; negative; zero; text — each with its own colour and literal characters. Put the arrow in the format:",[30,648,650],{"className":75,"code":649,"language":77,"meta":35,"style":35},"UP_GOOD = '[Color10]▲ 0.0%;[Red]▼ 0.0%;[Color55]► 0.0%'\nDOWN_GOOD = '[Red]▲ 0.0%;[Color10]▼ 0.0%;[Color55]► 0.0%'\n\nfor i, good in enumerate(kpis[\"Good when\"], start=2):\n    ws[f\"F{i}\"].number_format = UP_GOOD if good == \"up\" else DOWN_GOOD\nprint(ws[\"F2\"].number_format)\nprint(ws[\"F5\"].number_format)\n",[37,651,652,663,673,677,705,747,760],{"__ignoreMap":35},[40,653,654,657,660],{"class":42,"line":43},[40,655,656],{"class":227},"UP_GOOD",[40,658,659],{"class":84}," =",[40,661,662],{"class":50}," '[Color10]▲ 0.0%;[Red]▼ 0.0%;[Color55]► 0.0%'\n",[40,664,665,668,670],{"class":42,"line":98},[40,666,667],{"class":227},"DOWN_GOOD",[40,669,659],{"class":84},[40,671,672],{"class":50}," '[Red]▲ 0.0%;[Color10]▼ 0.0%;[Color55]► 0.0%'\n",[40,674,675],{"class":42,"line":112},[40,676,129],{"emptyLinePlaceholder":128},[40,678,679,681,684,686,688,691,694,697,699,701,703],{"class":42,"line":125},[40,680,461],{"class":84},[40,682,683],{"class":88}," i, good ",[40,685,467],{"class":84},[40,687,512],{"class":227},[40,689,690],{"class":88},"(kpis[",[40,692,693],{"class":50},"\"Good when\"",[40,695,696],{"class":88},"], ",[40,698,529],{"class":490},[40,700,138],{"class":84},[40,702,534],{"class":227},[40,704,537],{"class":88},[40,706,707,709,711,713,715,717,719,721,724,726,729,732,735,738,741,744],{"class":42,"line":132},[40,708,549],{"class":88},[40,710,552],{"class":84},[40,712,555],{"class":50},[40,714,559],{"class":558},[40,716,562],{"class":88},[40,718,565],{"class":558},[40,720,568],{"class":50},[40,722,723],{"class":88},"].number_format ",[40,725,138],{"class":84},[40,727,728],{"class":227}," UP_GOOD",[40,730,731],{"class":84}," if",[40,733,734],{"class":88}," good ",[40,736,737],{"class":84},"==",[40,739,740],{"class":50}," \"up\"",[40,742,743],{"class":84}," else",[40,745,746],{"class":227}," DOWN_GOOD\n",[40,748,749,751,754,757],{"class":42,"line":144},[40,750,612],{"class":227},[40,752,753],{"class":88},"(ws[",[40,755,756],{"class":50},"\"F2\"",[40,758,759],{"class":88},"].number_format)\n",[40,761,762,764,766,769],{"class":42,"line":185},[40,763,612],{"class":227},[40,765,753],{"class":88},[40,767,768],{"class":50},"\"F5\"",[40,770,759],{"class":88},[10,772,773,774,777,778,781,782,785,786,789,790,23],{},"The first section applies to positive values and shows a green up arrow; the second applies to negative values and shows a red down arrow — and because a negative section's format has no minus sign of its own, the value is shown without one, so a fall of 3.5% reads \"▼ 3.5%\" rather than \"▼ -3.5%\". The third section handles exactly zero with a neutral sideways arrow. ",[37,775,776],{},"[Color10]"," is a dark green from Excel's standard 56-colour palette — easier to read than the bright ",[37,779,780],{},"[Green]"," — and ",[37,783,784],{},"[Color55]"," a grey; ",[37,787,788],{},"[Red]"," is one of the eight named colours. The cell's value remains −0.035, so totals, charts and further formulas still work. Number format syntax in general is covered in ",[19,791,793],{"href":792},"\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",[10,795,796,797,800,801,804],{},"For measures where down is good — costs, complaints, days to pay, error rates — the ",[14,798,799],{},"arrow"," still shows the direction of change (up means it went up) but the ",[14,802,803],{},"colours"," swap. Keeping the arrow honest and moving the judgement into the colour avoids the confusing alternative of a down arrow for a cost that rose.",[806,807,815,816,815,820,815,824,815,832,815,840,815,847,815,854,815,857,815,861,815,864,815,868,815,873,815,879,815,881,815,886,815,888,815,891,815,893,815,896,815,899,815,903,815,905,815,909,815,911,815,914,815,916],"svg",{"viewBox":808,"role":809,"ariaLabelledBy":810,"xmlns":813,"style":814},"4 0 752 140","img",[811,812],"va-formats-t","va-formats-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[817,818,819],"title",{"id":811},"Same arrow, different colour depending on the good direction",[821,822,823],"desc",{"id":812},"For measures where up is good, a rise shows a green up arrow and a fall a red down arrow. For measures where down is good, such as costs and complaints, the arrows still show the direction of change but the colours swap. No change shows a grey sideways arrow in both.",[825,826],"rect",{"x":827,"y":828,"width":829,"height":830,"fill":831},"4","0","752","140","#ffffff",[825,833],{"x":834,"y":835,"width":836,"height":837,"rx":828,"fill":831,"stroke":838,"style":839},"20.0","16.0","161.7","40.0","var(--line,#cdd5e6)","stroke-width:1px",[825,841],{"x":842,"y":835,"width":843,"height":837,"rx":828,"fill":844,"stroke":845,"style":846},"181.7","186.1","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.2px",[848,849,853],"text",{"x":850,"y":851,"style":852},"274.8","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","rise",[825,855],{"x":856,"y":835,"width":843,"height":837,"rx":828,"fill":844,"stroke":845,"style":846},"367.8",[848,858,860],{"x":859,"y":851,"style":852},"460.9","fall",[825,862],{"x":863,"y":835,"width":843,"height":837,"rx":828,"fill":844,"stroke":845,"style":846},"553.9",[848,865,867],{"x":866,"y":851,"style":852},"647.0","no change",[825,869],{"x":834,"y":870,"width":836,"height":871,"rx":828,"fill":872,"stroke":838,"style":839},"56.0","34.0","#e7ebef",[848,874,878],{"x":875,"y":876,"style":877},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","up is good (revenue)",[825,880],{"x":842,"y":870,"width":843,"height":871,"rx":828,"fill":831,"stroke":838,"style":839},[848,882,885],{"x":850,"y":883,"style":884},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","green ▲",[825,887],{"x":856,"y":870,"width":843,"height":871,"rx":828,"fill":831,"stroke":838,"style":839},[848,889,890],{"x":859,"y":883,"style":884},"red ▼",[825,892],{"x":863,"y":870,"width":843,"height":871,"rx":828,"fill":831,"stroke":838,"style":839},[848,894,895],{"x":866,"y":883,"style":884},"grey ►",[825,897],{"x":834,"y":898,"width":836,"height":871,"rx":828,"fill":872,"stroke":838,"style":839},"90.0",[848,900,902],{"x":875,"y":901,"style":877},"112.0","down is good (costs)",[825,904],{"x":842,"y":898,"width":843,"height":871,"rx":828,"fill":831,"stroke":838,"style":839},[848,906,908],{"x":850,"y":907,"style":884},"111.0","red ▲",[825,910],{"x":856,"y":898,"width":843,"height":871,"rx":828,"fill":831,"stroke":838,"style":839},[848,912,913],{"x":859,"y":907,"style":884},"green ▼",[825,915],{"x":863,"y":898,"width":843,"height":871,"rx":828,"fill":831,"stroke":838,"style":839},[848,917,895],{"x":866,"y":907,"style":884},[25,919,921],{"id":920},"step-3-a-dead-band-so-small-changes-look-flat","Step 3 — A dead band so small changes look flat",[10,923,924],{},"An arrow on a change of 0.1% suggests news where there is none. Number formats can test conditions, but only two at a time; the cleaner way to add a threshold is to round the variance into the band inside the formula, so values within ±1% become exactly zero and pick up the neutral section:",[30,926,928],{"className":75,"code":927,"language":77,"meta":35,"style":35},"BAND = 0.01\nfor i in range(2, ws.max_row + 1):\n    ws[f\"F{i}\"] = f\"=IF(C{i}=0,NA(),IF(ABS(B{i}\u002FC{i}-1)\u003C{BAND},0,B{i}\u002FC{i}-1))\"\nprint(ws[\"F3\"].value)\n",[37,929,930,940,967,1043],{"__ignoreMap":35},[40,931,932,935,937],{"class":42,"line":43},[40,933,934],{"class":227},"BAND",[40,936,659],{"class":84},[40,938,939],{"class":227}," 0.01\n",[40,941,942,944,947,949,952,955,957,960,963,965],{"class":42,"line":98},[40,943,461],{"class":84},[40,945,946],{"class":88}," i ",[40,948,467],{"class":84},[40,950,951],{"class":227}," range",[40,953,954],{"class":88},"(",[40,956,534],{"class":227},[40,958,959],{"class":88},", ws.max_row ",[40,961,962],{"class":84},"+",[40,964,621],{"class":227},[40,966,537],{"class":88},[40,968,969,971,973,975,977,979,981,983,985,987,989,991,993,995,997,1000,1002,1004,1006,1008,1010,1012,1014,1017,1019,1021,1023,1026,1028,1030,1032,1034,1036,1038,1040],{"class":42,"line":112},[40,970,549],{"class":88},[40,972,552],{"class":84},[40,974,555],{"class":50},[40,976,559],{"class":558},[40,978,562],{"class":88},[40,980,565],{"class":558},[40,982,568],{"class":50},[40,984,571],{"class":88},[40,986,138],{"class":84},[40,988,576],{"class":84},[40,990,579],{"class":50},[40,992,559],{"class":558},[40,994,562],{"class":88},[40,996,565],{"class":558},[40,998,999],{"class":50},"=0,NA(),IF(ABS(B",[40,1001,559],{"class":558},[40,1003,562],{"class":88},[40,1005,565],{"class":558},[40,1007,597],{"class":50},[40,1009,559],{"class":558},[40,1011,562],{"class":88},[40,1013,565],{"class":558},[40,1015,1016],{"class":50},"-1)\u003C",[40,1018,559],{"class":558},[40,1020,934],{"class":227},[40,1022,565],{"class":558},[40,1024,1025],{"class":50},",0,B",[40,1027,559],{"class":558},[40,1029,562],{"class":88},[40,1031,565],{"class":558},[40,1033,597],{"class":50},[40,1035,559],{"class":558},[40,1037,562],{"class":88},[40,1039,565],{"class":558},[40,1041,1042],{"class":50},"-1))\"\n",[40,1044,1045,1047,1049,1052],{"class":42,"line":125},[40,1046,612],{"class":227},[40,1048,753],{"class":88},[40,1050,1051],{"class":50},"\"F3\"",[40,1053,1054],{"class":88},"].value)\n",[10,1056,1057],{},"Orders rose from 3,389 to 3,412 — 0.7% — which is within the band and now shows as flat. Where the exact variance must still be available, keep it in a hidden column and put the banded version next to it for display. The width of the band is a business decision: 1% suits revenue, but a measure that normally moves by 0.1% a month, such as a conversion rate measured in basis points, needs a band of its own, so in practice the band is often a per-measure column in the data sheet rather than one constant.",[25,1059,1061],{"id":1060},"step-4-a-trend-indicator-over-three-periods","Step 4 — A trend indicator over three periods",[10,1063,1064,1065,1068],{},"Month-on-month arrows are noisy. A trend column compares this month with the ",[14,1066,1067],{},"average"," of the previous three, which smooths out one-off swings, and shows a symbol chosen by formula:",[30,1070,1072],{"className":75,"code":1071,"language":77,"meta":35,"style":35},"from openpyxl.formatting.rule import FormulaRule\n\nfor i, good in enumerate(kpis[\"Good when\"], start=2):\n    ws[f\"G{i}\"] = (f'=IF(B{i}>AVERAGE(C{i}:E{i})*(1+{BAND}),\"↑\",'\n                   f'IF(B{i}\u003CAVERAGE(C{i}:E{i})*(1-{BAND}),\"↓\",\"→\"))')\ngreen, red = Font(color=\"1A7F37\", bold=True), Font(color=\"CF222E\", bold=True)\nlast = ws.max_row\nfor good, up_font, down_font in [(\"up\", green, red), (\"down\", red, green)]:\n    rows = [i for i, g in enumerate(kpis[\"Good when\"], start=2) if g == good]\n    rng = \" \".join(f\"G{i}\" for i in rows)\n    ws.conditional_formatting.add(rng, FormulaRule(formula=[f'G{rows[0]}=\"↑\"'], font=up_font))\n    ws.conditional_formatting.add(rng, FormulaRule(formula=[f'G{rows[0]}=\"↓\"'], font=down_font))\nws.column_dimensions[\"A\"].width = 18\nfor col in \"BCDEFG\":\n    ws.column_dimensions[col].width = 13\nwb.save(\"variance.xlsx\")\n",[37,1073,1074,1086,1090,1114,1179,1224,1269,1279,1301,1346,1381,1424,1460,1476,1491,1501],{"__ignoreMap":35},[40,1075,1076,1078,1081,1083],{"class":42,"line":43},[40,1077,101],{"class":84},[40,1079,1080],{"class":88}," openpyxl.formatting.rule ",[40,1082,85],{"class":84},[40,1084,1085],{"class":88}," FormulaRule\n",[40,1087,1088],{"class":42,"line":98},[40,1089,129],{"emptyLinePlaceholder":128},[40,1091,1092,1094,1096,1098,1100,1102,1104,1106,1108,1110,1112],{"class":42,"line":112},[40,1093,461],{"class":84},[40,1095,683],{"class":88},[40,1097,467],{"class":84},[40,1099,512],{"class":227},[40,1101,690],{"class":88},[40,1103,693],{"class":50},[40,1105,696],{"class":88},[40,1107,529],{"class":490},[40,1109,138],{"class":84},[40,1111,534],{"class":227},[40,1113,537],{"class":88},[40,1115,1116,1118,1120,1123,1125,1127,1129,1131,1133,1135,1138,1140,1143,1145,1147,1149,1152,1154,1156,1158,1161,1163,1165,1167,1170,1172,1174,1176],{"class":42,"line":125},[40,1117,549],{"class":88},[40,1119,552],{"class":84},[40,1121,1122],{"class":50},"\"G",[40,1124,559],{"class":558},[40,1126,562],{"class":88},[40,1128,565],{"class":558},[40,1130,568],{"class":50},[40,1132,571],{"class":88},[40,1134,138],{"class":84},[40,1136,1137],{"class":88}," (",[40,1139,552],{"class":84},[40,1141,1142],{"class":50},"'=IF(B",[40,1144,559],{"class":558},[40,1146,562],{"class":88},[40,1148,565],{"class":558},[40,1150,1151],{"class":50},">AVERAGE(C",[40,1153,559],{"class":558},[40,1155,562],{"class":88},[40,1157,565],{"class":558},[40,1159,1160],{"class":50},":E",[40,1162,559],{"class":558},[40,1164,562],{"class":88},[40,1166,565],{"class":558},[40,1168,1169],{"class":50},")*(1+",[40,1171,559],{"class":558},[40,1173,934],{"class":227},[40,1175,565],{"class":558},[40,1177,1178],{"class":50},"),\"↑\",'\n",[40,1180,1181,1184,1187,1189,1191,1193,1196,1198,1200,1202,1204,1206,1208,1210,1213,1215,1217,1219,1222],{"class":42,"line":132},[40,1182,1183],{"class":84},"                   f",[40,1185,1186],{"class":50},"'IF(B",[40,1188,559],{"class":558},[40,1190,562],{"class":88},[40,1192,565],{"class":558},[40,1194,1195],{"class":50},"\u003CAVERAGE(C",[40,1197,559],{"class":558},[40,1199,562],{"class":88},[40,1201,565],{"class":558},[40,1203,1160],{"class":50},[40,1205,559],{"class":558},[40,1207,562],{"class":88},[40,1209,565],{"class":558},[40,1211,1212],{"class":50},")*(1-",[40,1214,559],{"class":558},[40,1216,934],{"class":227},[40,1218,565],{"class":558},[40,1220,1221],{"class":50},"),\"↓\",\"→\"))'",[40,1223,499],{"class":88},[40,1225,1226,1229,1231,1233,1236,1238,1241,1243,1245,1247,1249,1252,1254,1256,1259,1261,1263,1265,1267],{"class":42,"line":144},[40,1227,1228],{"class":88},"green, red ",[40,1230,138],{"class":84},[40,1232,487],{"class":88},[40,1234,1235],{"class":490},"color",[40,1237,138],{"class":84},[40,1239,1240],{"class":50},"\"1A7F37\"",[40,1242,156],{"class":88},[40,1244,491],{"class":490},[40,1246,138],{"class":84},[40,1248,496],{"class":227},[40,1250,1251],{"class":88},"), Font(",[40,1253,1235],{"class":490},[40,1255,138],{"class":84},[40,1257,1258],{"class":50},"\"CF222E\"",[40,1260,156],{"class":88},[40,1262,491],{"class":490},[40,1264,138],{"class":84},[40,1266,496],{"class":227},[40,1268,499],{"class":88},[40,1270,1271,1274,1276],{"class":42,"line":185},[40,1272,1273],{"class":88},"last ",[40,1275,138],{"class":84},[40,1277,1278],{"class":88}," ws.max_row\n",[40,1280,1281,1283,1286,1288,1291,1293,1296,1298],{"class":42,"line":219},[40,1282,461],{"class":84},[40,1284,1285],{"class":88}," good, up_font, down_font ",[40,1287,467],{"class":84},[40,1289,1290],{"class":88}," [(",[40,1292,193],{"class":50},[40,1294,1295],{"class":88},", green, red), (",[40,1297,206],{"class":50},[40,1299,1300],{"class":88},", red, green)]:\n",[40,1302,1303,1306,1308,1311,1313,1316,1318,1320,1322,1324,1326,1328,1330,1332,1335,1338,1341,1343],{"class":42,"line":258},[40,1304,1305],{"class":88},"    rows ",[40,1307,138],{"class":84},[40,1309,1310],{"class":88}," [i ",[40,1312,461],{"class":84},[40,1314,1315],{"class":88}," i, g ",[40,1317,467],{"class":84},[40,1319,512],{"class":227},[40,1321,690],{"class":88},[40,1323,693],{"class":50},[40,1325,696],{"class":88},[40,1327,529],{"class":490},[40,1329,138],{"class":84},[40,1331,534],{"class":227},[40,1333,1334],{"class":88},") ",[40,1336,1337],{"class":84},"if",[40,1339,1340],{"class":88}," g ",[40,1342,737],{"class":84},[40,1344,1345],{"class":88}," good]\n",[40,1347,1348,1351,1353,1356,1359,1361,1363,1365,1367,1369,1371,1374,1376,1378],{"class":42,"line":296},[40,1349,1350],{"class":88},"    rng ",[40,1352,138],{"class":84},[40,1354,1355],{"class":50}," \" \"",[40,1357,1358],{"class":88},".join(",[40,1360,552],{"class":84},[40,1362,1122],{"class":50},[40,1364,559],{"class":558},[40,1366,562],{"class":88},[40,1368,565],{"class":558},[40,1370,568],{"class":50},[40,1372,1373],{"class":84}," for",[40,1375,946],{"class":88},[40,1377,467],{"class":84},[40,1379,1380],{"class":88}," rows)\n",[40,1382,1383,1386,1389,1391,1394,1396,1399,1401,1404,1406,1409,1411,1414,1416,1419,1421],{"class":42,"line":334},[40,1384,1385],{"class":88},"    ws.conditional_formatting.add(rng, FormulaRule(",[40,1387,1388],{"class":490},"formula",[40,1390,138],{"class":84},[40,1392,1393],{"class":88},"[",[40,1395,552],{"class":84},[40,1397,1398],{"class":50},"'G",[40,1400,559],{"class":558},[40,1402,1403],{"class":88},"rows[",[40,1405,828],{"class":227},[40,1407,1408],{"class":88},"]",[40,1410,565],{"class":558},[40,1412,1413],{"class":50},"=\"↑\"'",[40,1415,696],{"class":88},[40,1417,1418],{"class":490},"font",[40,1420,138],{"class":84},[40,1422,1423],{"class":88},"up_font))\n",[40,1425,1426,1428,1430,1432,1434,1436,1438,1440,1442,1444,1446,1448,1451,1453,1455,1457],{"class":42,"line":372},[40,1427,1385],{"class":88},[40,1429,1388],{"class":490},[40,1431,138],{"class":84},[40,1433,1393],{"class":88},[40,1435,552],{"class":84},[40,1437,1398],{"class":50},[40,1439,559],{"class":558},[40,1441,1403],{"class":88},[40,1443,828],{"class":227},[40,1445,1408],{"class":88},[40,1447,565],{"class":558},[40,1449,1450],{"class":50},"=\"↓\"'",[40,1452,696],{"class":88},[40,1454,1418],{"class":490},[40,1456,138],{"class":84},[40,1458,1459],{"class":88},"down_font))\n",[40,1461,1462,1465,1468,1471,1473],{"class":42,"line":378},[40,1463,1464],{"class":88},"ws.column_dimensions[",[40,1466,1467],{"class":50},"\"A\"",[40,1469,1470],{"class":88},"].width ",[40,1472,138],{"class":84},[40,1474,1475],{"class":227}," 18\n",[40,1477,1478,1480,1483,1485,1488],{"class":42,"line":383},[40,1479,461],{"class":84},[40,1481,1482],{"class":88}," col ",[40,1484,467],{"class":84},[40,1486,1487],{"class":50}," \"BCDEFG\"",[40,1489,1490],{"class":88},":\n",[40,1492,1493,1496,1498],{"class":42,"line":394},[40,1494,1495],{"class":88},"    ws.column_dimensions[col].width ",[40,1497,138],{"class":84},[40,1499,1500],{"class":227}," 13\n",[40,1502,1503,1506,1509],{"class":42,"line":405},[40,1504,1505],{"class":88},"wb.save(",[40,1507,1508],{"class":50},"\"variance.xlsx\"",[40,1510,499],{"class":88},[10,1512,1513,1514,1517,1518,23],{},"The trend arrows are text, so they are coloured with conditional formatting rather than a number format; the rule ranges list the up-is-good and down-is-good rows separately so each gets the right colours, and the relative reference ",[37,1515,1516],{},"G{rows[0]}"," is adjusted by Excel for every cell in the range. Using different glyphs for the two columns — triangles for the monthly variance, thin arrows for the trend — helps readers tell them apart. Icon sets are another way to show direction, with their own thresholds; they are covered in ",[19,1519,1521],{"href":1520},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fadd-icon-sets-with-openpyxl\u002F","Add Icon Sets to Excel Cells with openpyxl",[806,1523,815,1528,815,1531,815,1534,815,1537,815,1543,815,1554,815,1560,815,1564,815,1569,815,1574,815,1579,815,1586,815,1591,815,1594,815,1598,815,1601,815,1604,815,1611,815,1616,815,1619,815,1623,815,1626,815,1631,815,1638,815,1643],{"viewBox":1524,"role":809,"ariaLabelledBy":1525,"xmlns":813,"style":814},"4 0 752 125",[1526,1527],"va-flow-t","va-flow-d",[817,1529,1530],{"id":1526},"From two numbers to an arrow",[821,1532,1533],{"id":1527},"The variance is calculated as a number by formula. A dead band turns changes within one percent into zero. The custom number format chooses an arrow and colour by sign, using swapped colours where down is good. A separate trend column compares this month with the previous three-month average.",[825,1535],{"x":827,"y":828,"width":829,"height":1536,"fill":831},"125",[825,1538],{"x":834,"y":835,"width":1539,"height":1540,"rx":1541,"fill":872,"stroke":838,"style":1542},"153.0","93.0","12","stroke-width:2px",[1544,1545,1547],"g",{"transform":1546},"translate(84.5,29.0) scale(1.000)",[1548,1549],"path",{"d":1550,"fill":1551,"stroke":1552,"style":1553},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[848,1555,1559],{"x":1556,"y":1557,"style":1558},"96.5","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Variance formula",[848,1561,1563],{"x":1556,"y":1562,"style":884},"95.0","this \u002F last - 1",[42,1565],{"x1":1566,"y1":1567,"x2":1568,"y2":1567,"stroke":845,"style":1542},"178.0","62.5","200.0",[1570,1571],"polygon",{"points":1572,"fill":1573},"204.0,62.5 195.0,67.5 195.0,57.5","#5b5cf0",[825,1575],{"x":1576,"y":835,"width":1539,"height":1540,"rx":1541,"fill":1577,"stroke":1578,"style":1542},"209.0","#fdefd8","var(--gold,#b4740a)",[1544,1580,1582],{"transform":1581},"translate(273.5,29.0) scale(1.000)",[1548,1583],{"d":1584,"fill":1551,"stroke":1585,"style":1553},"M3 5h18l-7 8v6l-4 2v-8z","var(--gold-ink,#7a4e06)",[848,1587,1590],{"x":1588,"y":1557,"style":1589},"285.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Dead band",[848,1592,1593],{"x":1588,"y":1562,"style":884},"±1% becomes 0",[42,1595],{"x1":1596,"y1":1567,"x2":1597,"y2":1567,"stroke":845,"style":1542},"367.0","389.0",[1570,1599],{"points":1600,"fill":1573},"393.0,62.5 384.0,67.5 384.0,57.5",[825,1602],{"x":1603,"y":835,"width":1539,"height":1540,"rx":1541,"fill":844,"stroke":845,"style":1542},"398.0",[1544,1605,1607],{"transform":1606},"translate(462.5,29.0) scale(1.000)",[1548,1608],{"d":1609,"fill":1551,"stroke":1610,"style":1553},"M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z M12 9a3 3 0 1 0 .01 0z","var(--brand-strong,#4338ca)",[848,1612,1615],{"x":1613,"y":1557,"style":1614},"474.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Number format",[848,1617,1618],{"x":1613,"y":1562,"style":884},"▲ ▼ ► by sign",[42,1620],{"x1":1621,"y1":1567,"x2":1622,"y2":1567,"stroke":845,"style":1542},"556.0","578.0",[1570,1624],{"points":1625,"fill":1573},"582.0,62.5 573.0,67.5 573.0,57.5",[825,1627],{"x":1628,"y":835,"width":1539,"height":1540,"rx":1541,"fill":1629,"stroke":1630,"style":1542},"587.0","#d9f4f1","var(--teal,#0f9488)",[1544,1632,1634],{"transform":1633},"translate(651.5,29.0) scale(1.000)",[1548,1635],{"d":1636,"fill":1551,"stroke":1637,"style":1553},"M4 20h16 M4 20V4 M6 16l4-5 4 3 6-7","var(--teal-ink,#0b6157)",[848,1639,1642],{"x":1640,"y":1557,"style":1641},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Trend column",[848,1644,1645],{"x":1640,"y":1562,"style":884},"vs 3-month average",[25,1647,1649],{"id":1648},"step-5-check-what-the-cells-display","Step 5 — Check what the cells display",[10,1651,1652,1653,1656],{},"Recalculate with LibreOffice and export to CSV with the \"save cell contents as shown\" option, which writes each cell as ",[14,1654,1655],{},"displayed"," — arrows and all — so the formats can be checked in a test:",[30,1658,1660],{"className":75,"code":1659,"language":77,"meta":35,"style":35},"import csv, subprocess\n\nAS_SHOWN = \"csv:Text - txt - csv (StarCalc):44,34,76,1,,0,false,true,true\"     # 9th token: save as shown\nsubprocess.run([\"soffice\", \"--headless\", \"--convert-to\", AS_SHOWN, \"--outdir\", \"calc\", \"variance.xlsx\"], check=True, capture_output=True)\nwith open(\"calc\u002Fvariance.csv\", encoding=\"utf-8\") as f:\n    for row in csv.reader(f):\n        print(f\"{row[0]:\u003C16} {row[5]:>13}  {row[6]:>5}\")\n",[37,1661,1662,1669,1673,1687,1743,1773,1786],{"__ignoreMap":35},[40,1663,1664,1666],{"class":42,"line":43},[40,1665,85],{"class":84},[40,1667,1668],{"class":88}," csv, subprocess\n",[40,1670,1671],{"class":42,"line":98},[40,1672,129],{"emptyLinePlaceholder":128},[40,1674,1675,1678,1680,1683],{"class":42,"line":112},[40,1676,1677],{"class":227},"AS_SHOWN",[40,1679,659],{"class":84},[40,1681,1682],{"class":50}," \"csv:Text - txt - csv (StarCalc):44,34,76,1,,0,false,true,true\"",[40,1684,1686],{"class":1685},"s-wDw","     # 9th token: save as shown\n",[40,1688,1689,1692,1695,1697,1700,1702,1705,1707,1709,1711,1714,1716,1719,1721,1723,1725,1728,1730,1732,1734,1737,1739,1741],{"class":42,"line":125},[40,1690,1691],{"class":88},"subprocess.run([",[40,1693,1694],{"class":50},"\"soffice\"",[40,1696,156],{"class":88},[40,1698,1699],{"class":50},"\"--headless\"",[40,1701,156],{"class":88},[40,1703,1704],{"class":50},"\"--convert-to\"",[40,1706,156],{"class":88},[40,1708,1677],{"class":227},[40,1710,156],{"class":88},[40,1712,1713],{"class":50},"\"--outdir\"",[40,1715,156],{"class":88},[40,1717,1718],{"class":50},"\"calc\"",[40,1720,156],{"class":88},[40,1722,1508],{"class":50},[40,1724,696],{"class":88},[40,1726,1727],{"class":490},"check",[40,1729,138],{"class":84},[40,1731,496],{"class":227},[40,1733,156],{"class":88},[40,1735,1736],{"class":490},"capture_output",[40,1738,138],{"class":84},[40,1740,496],{"class":227},[40,1742,499],{"class":88},[40,1744,1745,1748,1751,1753,1756,1758,1761,1763,1766,1768,1770],{"class":42,"line":132},[40,1746,1747],{"class":84},"with",[40,1749,1750],{"class":227}," open",[40,1752,954],{"class":88},[40,1754,1755],{"class":50},"\"calc\u002Fvariance.csv\"",[40,1757,156],{"class":88},[40,1759,1760],{"class":490},"encoding",[40,1762,138],{"class":84},[40,1764,1765],{"class":50},"\"utf-8\"",[40,1767,1334],{"class":88},[40,1769,92],{"class":84},[40,1771,1772],{"class":88}," f:\n",[40,1774,1775,1778,1781,1783],{"class":42,"line":144},[40,1776,1777],{"class":84},"    for",[40,1779,1780],{"class":88}," row ",[40,1782,467],{"class":84},[40,1784,1785],{"class":88}," csv.reader(f):\n",[40,1787,1788,1791,1793,1795,1797,1799,1802,1804,1806,1809,1811,1814,1816,1819,1821,1824,1826,1829,1831,1834,1836,1839,1841,1843],{"class":42,"line":185},[40,1789,1790],{"class":227},"        print",[40,1792,954],{"class":88},[40,1794,552],{"class":84},[40,1796,568],{"class":50},[40,1798,559],{"class":558},[40,1800,1801],{"class":88},"row[",[40,1803,828],{"class":227},[40,1805,1408],{"class":88},[40,1807,1808],{"class":84},":\u003C16",[40,1810,565],{"class":558},[40,1812,1813],{"class":558}," {",[40,1815,1801],{"class":88},[40,1817,1818],{"class":227},"5",[40,1820,1408],{"class":88},[40,1822,1823],{"class":84},":>13",[40,1825,565],{"class":558},[40,1827,1828],{"class":558},"  {",[40,1830,1801],{"class":88},[40,1832,1833],{"class":227},"6",[40,1835,1408],{"class":88},[40,1837,1838],{"class":84},":>5",[40,1840,565],{"class":558},[40,1842,568],{"class":50},[40,1844,499],{"class":88},[10,1846,1847,1848,1851,1852,1856],{},"Revenue shows \"▲ 5.7%\" and a rising trend. Orders, within the dead band month on month, show \"► 0.0%\" — yet the trend column still shows a rise, because 3,412 is 1.9% above the average of the three previous months: steady small gains add up, which is exactly what the trend column is for. Operating cost rose 3.5% — an up arrow, red in Excel because the format for costs swaps the colours; complaints fell 28.8%, a down arrow that is green for the same reason. Without the filter option LibreOffice writes the underlying values instead (5.70276…%), so the option matters. The CSV carries the characters but not the colours, so a test can assert both the text and, separately, the ",[37,1849,1850],{},"number_format"," string on each cell. Exporting the dashboard to PDF and looking at it remains the final check of colours; ",[19,1853,1855],{"href":1854},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-to-pdf-on-linux-with-libreoffice\u002F","Convert Excel to PDF on Linux with LibreOffice"," shows how.",[806,1858,815,1864,815,1867,815,1870,815,1875,815,1879,815,1883,815,1889,815,1893,815,1897,815,1901,815,1905,815,1909,815,1913,815,1917,815,1921,815,1924,815,1928,815,1931,815,1935,815,1937,815,1940,815,1942,815,1945,815,1947,815,1950,815,1952,815,1955,815,1958,815,1961,815,1963,815,1967,815,1969,815,1972,815,1974,815,1977,815,1979,815,1983,815,1985,815,1988,815,1991,815,1995,815,1997,815,2000,815,2002,815,2005,815,2007,815,2010,815,2012,815,2016,815,2018,815,2020,815,2023,815,2026,815,2028,815,2031,815,2033,815,2036,815,2038,815,2041,815,2043,815,2046,815,2048,815,2050,815,2053,815,2056,815,2058,815,2061,815,2063,815,2066,815,2068,815,2071,815,2075,815,2079,815,2081,815,2083,815,2086,815,2089,815,2091,815,2094,815,2096,815,2098,815,2100,815,2102,815,2104,815,2107,815,2109],{"viewBox":1859,"role":809,"ariaLabelledBy":1860,"xmlns":813,"style":1863},"121 0 518 224",[1861,1862],"va-result-t","va-result-d","width:100%;max-width:518px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[817,1865,1866],{"id":1861},"The finished variance and trend columns",[821,1868,1869],{"id":1862},"Revenue up 5.7 percent with a rising trend, orders flat month on month inside the dead band but rising against the three-month average, conversion up 4.7 percent, operating cost up 3.5 percent shown in red because costs should fall, complaints down 28.8 percent shown in green, and average order value up 5.0 percent.",[825,1871],{"x":1872,"y":828,"width":1873,"height":1874,"fill":831},"121","518","224",[825,1876],{"x":1877,"y":835,"width":875,"height":1878,"rx":828,"fill":872,"stroke":838,"style":839},"137.1","24.0",[825,1880],{"x":1881,"y":835,"width":1882,"height":1878,"rx":828,"fill":872,"stroke":838,"style":839},"169.1","115.2",[848,1884,1888],{"x":1885,"y":1886,"style":1887},"226.7","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[825,1890],{"x":1891,"y":835,"width":1892,"height":1878,"rx":828,"fill":872,"stroke":838,"style":839},"284.3","87.3",[848,1894,1896],{"x":1895,"y":1886,"style":1887},"328.0","B",[825,1898],{"x":1899,"y":835,"width":1900,"height":1878,"rx":828,"fill":872,"stroke":838,"style":839},"371.6","84.0",[848,1902,1904],{"x":1903,"y":1886,"style":1887},"413.6","C",[825,1906],{"x":1907,"y":835,"width":1908,"height":1878,"rx":828,"fill":872,"stroke":838,"style":839},"455.6","106.5",[848,1910,1912],{"x":1911,"y":1886,"style":1887},"508.9","D",[825,1914],{"x":1915,"y":835,"width":1916,"height":1878,"rx":828,"fill":872,"stroke":838,"style":839},"562.1","60.8",[848,1918,1920],{"x":1919,"y":1886,"style":1887},"592.5","E",[825,1922],{"x":1877,"y":837,"width":875,"height":1923,"rx":828,"fill":872,"stroke":838,"style":839},"28.0",[848,1925,473],{"x":1926,"y":1927,"style":1887},"153.1","59.0",[825,1929],{"x":1881,"y":837,"width":1882,"height":1923,"rx":828,"fill":844,"stroke":845,"style":1930},"stroke-width:1.5px",[848,1932,1934],{"x":1885,"y":1927,"style":1933},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Measure",[825,1936],{"x":1891,"y":837,"width":1892,"height":1923,"rx":828,"fill":844,"stroke":845,"style":1930},[848,1938,1939],{"x":1895,"y":1927,"style":1933},"Sep",[825,1941],{"x":1899,"y":837,"width":1900,"height":1923,"rx":828,"fill":844,"stroke":845,"style":1930},[848,1943,1944],{"x":1903,"y":1927,"style":1933},"Aug",[825,1946],{"x":1907,"y":837,"width":1908,"height":1923,"rx":828,"fill":844,"stroke":845,"style":1930},[848,1948,1949],{"x":1911,"y":1927,"style":1933},"vs last month",[825,1951],{"x":1915,"y":837,"width":1916,"height":1923,"rx":828,"fill":844,"stroke":845,"style":1930},[848,1953,1954],{"x":1919,"y":1927,"style":1933},"Trend",[825,1956],{"x":1877,"y":1957,"width":875,"height":1923,"rx":828,"fill":872,"stroke":838,"style":839},"68.0",[848,1959,534],{"x":1926,"y":1960,"style":1887},"87.0",[825,1962],{"x":1881,"y":1957,"width":1882,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,1964,1966],{"x":1885,"y":1960,"style":1965},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Revenue",[825,1968],{"x":1891,"y":1957,"width":1892,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,1970,1971],{"x":1895,"y":1960,"style":1965},"1,284,500",[825,1973],{"x":1899,"y":1957,"width":1900,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,1975,1976],{"x":1903,"y":1960,"style":1965},"1,215,200",[825,1978],{"x":1907,"y":1957,"width":1908,"height":1923,"rx":828,"fill":1629,"stroke":1630,"style":1930},[848,1980,1982],{"x":1911,"y":1960,"style":1981},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","▲ 5.7%",[825,1984],{"x":1915,"y":1957,"width":1916,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,1986,1987],{"x":1919,"y":1960,"style":1965},"↑",[825,1989],{"x":1877,"y":1990,"width":875,"height":1923,"rx":828,"fill":872,"stroke":838,"style":839},"96.0",[848,1992,1994],{"x":1926,"y":1993,"style":1887},"115.0","3",[825,1996],{"x":1881,"y":1990,"width":1882,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,1998,1999],{"x":1885,"y":1993,"style":1965},"Orders",[825,2001],{"x":1891,"y":1990,"width":1892,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2003,2004],{"x":1895,"y":1993,"style":1965},"3,412",[825,2006],{"x":1899,"y":1990,"width":1900,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2008,2009],{"x":1903,"y":1993,"style":1965},"3,389",[825,2011],{"x":1907,"y":1990,"width":1908,"height":1923,"rx":828,"fill":872,"stroke":838,"style":1930},[848,2013,2015],{"x":1911,"y":1993,"style":2014},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","► 0.0%",[825,2017],{"x":1915,"y":1990,"width":1916,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2019,1987],{"x":1919,"y":1993,"style":1965},[825,2021],{"x":1877,"y":2022,"width":875,"height":1923,"rx":828,"fill":872,"stroke":838,"style":839},"124.0",[848,2024,827],{"x":1926,"y":2025,"style":1887},"143.0",[825,2027],{"x":1881,"y":2022,"width":1882,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2029,2030],{"x":1885,"y":2025,"style":1965},"Conversion",[825,2032],{"x":1891,"y":2022,"width":1892,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2034,2035],{"x":1895,"y":2025,"style":1965},"3.12%",[825,2037],{"x":1899,"y":2022,"width":1900,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2039,2040],{"x":1903,"y":2025,"style":1965},"2.98%",[825,2042],{"x":1907,"y":2022,"width":1908,"height":1923,"rx":828,"fill":1629,"stroke":1630,"style":1930},[848,2044,2045],{"x":1911,"y":2025,"style":1981},"▲ 4.7%",[825,2047],{"x":1915,"y":2022,"width":1916,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2049,1987],{"x":1919,"y":2025,"style":1965},[825,2051],{"x":1877,"y":2052,"width":875,"height":1923,"rx":828,"fill":872,"stroke":838,"style":839},"152.0",[848,2054,1818],{"x":1926,"y":2055,"style":1887},"171.0",[825,2057],{"x":1881,"y":2052,"width":1882,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2059,2060],{"x":1885,"y":2055,"style":1965},"Operating cost",[825,2062],{"x":1891,"y":2052,"width":1892,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2064,2065],{"x":1895,"y":2055,"style":1965},"412,800",[825,2067],{"x":1899,"y":2052,"width":1900,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2069,2070],{"x":1903,"y":2055,"style":1965},"398,900",[825,2072],{"x":1907,"y":2052,"width":1908,"height":1923,"rx":828,"fill":2073,"stroke":2074,"style":1930},"#fce9e9","var(--danger,#dc2626)",[848,2076,2078],{"x":1911,"y":2055,"style":2077},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","▲ 3.5%",[825,2080],{"x":1915,"y":2052,"width":1916,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2082,1987],{"x":1919,"y":2055,"style":1965},[825,2084],{"x":1877,"y":2085,"width":875,"height":1923,"rx":828,"fill":872,"stroke":838,"style":839},"180.0",[848,2087,1833],{"x":1926,"y":2088,"style":1887},"199.0",[825,2090],{"x":1881,"y":2085,"width":1882,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2092,2093],{"x":1885,"y":2088,"style":1965},"Complaints",[825,2095],{"x":1891,"y":2085,"width":1892,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2097,248],{"x":1895,"y":2088,"style":1965},[825,2099],{"x":1899,"y":2085,"width":1900,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2101,286],{"x":1903,"y":2088,"style":1965},[825,2103],{"x":1907,"y":2085,"width":1908,"height":1923,"rx":828,"fill":1629,"stroke":1630,"style":1930},[848,2105,2106],{"x":1911,"y":2088,"style":1981},"▼ 28.8%",[825,2108],{"x":1915,"y":2085,"width":1916,"height":1923,"rx":828,"fill":831,"stroke":838,"style":839},[848,2110,2111],{"x":1919,"y":2088,"style":1965},"↓",[25,2113,2115],{"id":2114},"common-pitfalls","Common pitfalls",[2117,2118,2119,2127,2133,2139,2145],"ul",{},[2120,2121,2122,2126],"li",{},[2123,2124,2125],"strong",{},"Arrows concatenated into text."," The variance stops being a number; use a number format.",[2120,2128,2129,2132],{},[2123,2130,2131],{},"Green for every rise."," Swap colours for measures where down is good.",[2120,2134,2135,2138],{},[2123,2136,2137],{},"Arrows on noise."," Add a dead band so tiny changes show as flat.",[2120,2140,2141,2144],{},[2123,2142,2143],{},"Minus signs after arrows."," Give the negative section its own format without a sign.",[2120,2146,2147,2152,2153,2155],{},[2123,2148,2149,2150,23],{},"Bright ",[37,2151,780],{}," Use a palette colour such as ",[37,2154,776],{}," for legibility.",[25,2157,2159],{"id":2158},"conclusion","Conclusion",[10,2161,2162,2163,2166],{},"Keep variances as numbers and let a custom number format add the arrow: ",[37,2164,2165],{},"[Color10]▲ 0.0%;[Red]▼ 0.0%;[Color55]► 0.0%"," for measures where up is good, with the colours swapped where down is good. Fold a dead band into the formula so small changes display as flat, add a trend column that compares with the average of recent periods, and check the displayed text by exporting to CSV with LibreOffice.",[25,2168,2170],{"id":2169},"frequently-asked-questions","Frequently asked questions",[10,2172,2173,2176,2177,2180,2181,2184],{},[2123,2174,2175],{},"How do I show an up or down arrow next to a percentage in Excel?","\nUse a custom number format with sections for positive, negative and zero values, such as ",[40,2178,2179],{},"Color10","▲ 0.0%;",[40,2182,2183],{},"Red","▼ 0.0%;► 0.0%. The value stays a number.",[10,2186,2187,2190,2191,2180,2193,2195],{},[2123,2188,2189],{},"How do I show red for an increase in costs?","\nUse a second format with the colours swapped, ",[40,2192,2183],{},[40,2194,2179],{},"▼ 0.0%, and apply it to measures where a decrease is good.",[10,2197,2198,2201],{},[2123,2199,2200],{},"How do I stop tiny changes showing an arrow?","\nReturn 0 from the variance formula when the absolute change is below a threshold, so the zero section of the format shows a neutral symbol.",[10,2203,2204,2207],{},[2123,2205,2206],{},"Can I check number formats without opening Excel?","\nRecalculate and export the workbook to CSV with LibreOffice using the option to save cell contents as shown; the CSV then contains the displayed text of each cell, including arrows.",[25,2209,2211],{"id":2210},"related","Related",[2117,2213,2214,2220,2227,2234],{},[2120,2215,2216,2217,2219],{},"Up one level: ",[19,2218,22],{"href":21}," — dashboards from Python.",[2120,2221,2222,2226],{},[19,2223,2225],{"href":2224},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-kpi-summary-block-to-an-excel-dashboard\u002F","Add a KPI Summary Block to an Excel Dashboard"," — headline tiles to put arrows on.",[2120,2228,2229,2233],{},[19,2230,2232],{"href":2231},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets\u002F","Build a Monthly KPI Scorecard with Targets"," — comparing against targets.",[2120,2235,2236,2240],{},[19,2237,2239],{"href":2238},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Flay-out-a-dashboard-sheet-on-a-grid\u002F","Lay Out a Dashboard Sheet on a Grid"," — arranging the dashboard.",[2242,2243,2244],"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":35,"searchDepth":98,"depth":98,"links":2246},[2247,2248,2249,2250,2251,2252,2253,2254,2255,2256],{"id":27,"depth":98,"text":28},{"id":71,"depth":98,"text":72},{"id":642,"depth":98,"text":643},{"id":920,"depth":98,"text":921},{"id":1060,"depth":98,"text":1061},{"id":1648,"depth":98,"text":1649},{"id":2114,"depth":98,"text":2115},{"id":2158,"depth":98,"text":2159},{"id":2169,"depth":98,"text":2170},{"id":2210,"depth":98,"text":2211},"2026-10-02","Show at a glance whether each dashboard figure moved the right way: custom number formats that put a coloured up or down arrow next to a variance without changing the value, separate formats for measures where down is good, a dead band so tiny changes show as flat, a three-period trend indicator built with formulas, and a check of the displayed text with LibreOffice.","md",[2261,2263,2265,2267],{"q":2175,"a":2262},"Use a custom number format with sections for positive, negative and zero values, such as [Color10]▲ 0.0%;[Red]▼ 0.0%;► 0.0%. The value stays a number.",{"q":2189,"a":2264},"Use a second format with the colours swapped, [Red]▲ 0.0%;[Color10]▼ 0.0%, and apply it to measures where a decrease is good.",{"q":2200,"a":2266},"Return 0 from the variance formula when the absolute change is below a threshold, so the zero section of the format shows a neutral symbol.",{"q":2206,"a":2268},"Recalculate and export the workbook to CSV with LibreOffice using the option to save cell contents as shown; the CSV then contains the displayed text of each cell, including arrows.","variance arrows and trend indicators excel dashboard",null,{"breadcrumb":2272},[2273,2276,2279],{"name":2274,"item":2275},"Home","\u002F",{"name":2277,"item":2278},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-variance-arrows-and-trend-indicators-to-a-dashboard",{"title":2282,"description":2283},"Add Variance Arrows and Trend Indicators to an Excel Dashboard","Coloured ▲▼ variance arrows from custom number formats with openpyxl, inverted colours for costs, a dead band for flat changes, formula-based trend indicators and checking the displayed text with LibreOffice.","add-variance-arrows-and-trend-indicators-to-a-dashboard","automating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-variance-arrows-and-trend-indicators-to-a-dashboard\u002Findex",[2287,2288],"report-layouts","number-formats","how-to","vPwgHgB9o4wO7wJLqJDJIAaRlfmOZAwAxdXvHwNokiI",[2292,2296],{"title":2293,"path":2294,"stem":2295,"children":-1},"Add a Summary Sheet to an Excel Report with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-summary-sheet-to-excel-report-python","automating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-summary-sheet-to-excel-report-python\u002Findex",{"title":2297,"path":2298,"stem":2299,"children":-1},"Build a Dashboard Sheet with Charts from Multiple Tabs","\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fbuild-a-dashboard-sheet-with-charts-from-multiple-tabs","automating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fbuild-a-dashboard-sheet-with-charts-from-multiple-tabs\u002Findex",[2301,2303],{"path":2302,"title":22},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards",{"path":2304,"title":2277},"\u002Fautomating-reporting-workflows",[],[2307,2311,2314,2317,2320,2323],{"path":2308,"title":2309,"kind":2310},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports","Building Finance and Operations Reports in Excel with Python","guide",{"path":2312,"title":2313,"kind":2289},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fallocate-shared-costs-across-departments","Allocate Shared Costs Across Departments with Python",{"path":2315,"title":2316,"kind":2289},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bank-reconciliation-report","Build a Bank Reconciliation Report with Python and Excel",{"path":2318,"title":2319,"kind":2289},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bill-of-materials-cost-rollup","Build a Bill of Materials Cost Rollup",{"path":2321,"title":2322,"kind":2289},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-break-even-analysis-in-excel-with-python","Build a Break-Even Analysis in Excel with Python",{"path":2324,"title":2325,"kind":2326},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-budget-vs-actual-variance-report","Build a Budget vs Actual Variance Report in Excel with Python","compare",1790972129378]