[{"data":1,"prerenderedAt":2731},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fmake-an-excel-report-accessible":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fmake-an-excel-report-accessible":2697,"ancestors:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fmake-an-excel-report-accessible":2706,"children:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fmake-an-excel-report-accessible":2711,"related:\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fmake-an-excel-report-accessible":2712},{"id":4,"title":5,"body":6,"dateModified":2664,"datePublished":2664,"description":2665,"extension":2666,"faq":2667,"keyword":2676,"kind":2677,"meta":2678,"navigation":194,"path":2686,"seo":2687,"slug":2690,"stem":2691,"topics":2692,"type":2695,"__hash__":2696},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fmake-an-excel-report-accessible\u002Findex.md","Make an Excel Report Accessible with Python",{"type":7,"value":8,"toc":2651},"minimark",[9,24,29,60,63,165,169,172,623,630,634,641,928,940,944,947,1105,1113,1229,1233,1240,1406,1409,1413,1416,1814,1817,1908,1912,1919,2526,2534,2538,2576,2580,2586,2590,2596,2602,2608,2614,2618,2647],[10,11,12,13,17,18,23],"p",{},"Reports go to everyone on a distribution list, including colleagues who use a screen reader, magnification or high-contrast settings. Most accessibility problems in spreadsheets are introduced by the tool that generated them, not by the data: sheets called ",[14,15,16],"code",{},"Sheet1",", charts with no description, titles merged across eight columns, status shown only as red or green fill, pale grey text on white. Because a Python script builds the same report every week, fixing it once in the script fixes every future copy — and a short audit function can check any workbook, generated or not. This page is part of ",[19,20,22],"a",{"href":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002F","Protecting and Sharing Excel Workbooks",". It follows the checks in Excel's own Accessibility Checker; it is not a legal compliance review.",[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 xlsxwriter openpyxl pandas\n","bash","",[14,37,38],{"__ignoreMap":35},[39,40,43,47,51,54,57],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sMTad","pip",[39,48,50],{"class":49},"srMev"," install",[39,52,53],{"class":49}," xlsxwriter",[39,55,56],{"class":49}," openpyxl",[39,58,59],{"class":49}," pandas\n",[10,61,62],{},"The example is a small regional sales report: a summary sheet with a status per region, a chart and a logo. It is built twice — once the typical way and once accessibly — and the audit in Step 6 compares them.",[64,65,73,74,73,78,73,82,73,90,73,100,73,111,73,118,73,123,73,128,73,135,73,140,73,144,73,149,73,156,73,161],"svg",{"viewBox":66,"role":67,"ariaLabelledBy":68,"xmlns":71,"style":72},"44 0 672 331","img",[69,70],"ax-layers-t","ax-layers-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:672px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[75,76,77],"title",{"id":69},"What an accessible report gets right",[79,80,81],"desc",{"id":70},"Layers of an accessible workbook. Structure — meaningful sheet names, a title and data in tables with header rows. Content — alternative text on charts and images and no merged cells. Presentation — meaning carried by text as well as colour, and colours with enough contrast.",[83,84],"rect",{"x":85,"y":86,"width":87,"height":88,"fill":89},"44","0","672","331","#ffffff",[83,91],{"x":92,"y":93,"width":94,"height":95,"rx":96,"fill":97,"stroke":98,"style":99},"60.0","16.0","640.0","93.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[101,102,104],"g",{"transform":103},"translate(368.0,29.0) scale(1.000)",[105,106],"path",{"d":107,"fill":108,"stroke":109,"style":110},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[112,113,117],"text",{"x":114,"y":115,"style":116},"380.0","75.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Structure",[112,119,122],{"x":114,"y":120,"style":121},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","sheet names, title in A1, tables with header rows, document title",[83,124],{"x":92,"y":125,"width":94,"height":95,"rx":96,"fill":126,"stroke":127,"style":99},"119.0","#d9f4f1","var(--teal,#0f9488)",[101,129,131],{"transform":130},"translate(368.0,132.0) scale(1.000)",[105,132],{"d":133,"fill":108,"stroke":134,"style":110},"M4 5h16v14H4z M4 16l5-5 4 4 3-3 4 4 M15 9h.01","var(--teal-ink,#0b6157)",[112,136,139],{"x":114,"y":137,"style":138},"178.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Content",[112,141,143],{"x":114,"y":142,"style":121},"198.0","alt text on charts and images, no merged cells, descriptive links",[83,145],{"x":92,"y":146,"width":94,"height":95,"rx":96,"fill":147,"stroke":148,"style":99},"222.0","#fdefd8","var(--gold,#b4740a)",[101,150,152],{"transform":151},"translate(368.0,235.0) scale(1.000)",[105,153],{"d":154,"fill":108,"stroke":155,"style":110},"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(--gold-ink,#7a4e06)",[112,157,160],{"x":114,"y":158,"style":159},"281.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Presentation",[112,162,164],{"x":114,"y":163,"style":121},"301.0","status as text + colour, contrast of at least 4.5:1",[25,166,168],{"id":167},"step-1-the-typical-version","Step 1 — The typical version",[10,170,171],{},"This is how many generated reports look: default sheet name, a merged title, status as coloured fill only, pale header text, a chart and a logo without descriptions:",[30,173,177],{"className":174,"code":175,"language":176,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import xlsxwriter\n\nDATA = [(\"North\", 482100, 450000), (\"South\", 391250, 400000), (\"East\", 297800, 280000), (\"West\", 259000, 315000)]\n\nwb = xlsxwriter.Workbook(\"sales_typical.xlsx\")\nws = wb.add_worksheet()\nws.merge_range(\"A1:D1\", \"Regional sales — September\", wb.add_format({\"bold\": True, \"align\": \"center\"}))\nhead = wb.add_format({\"bold\": True, \"font_color\": \"#BBBBBB\", \"bg_color\": \"#FFFFFF\"})\ngreen, red = wb.add_format({\"bg_color\": \"#C6EFCE\"}), wb.add_format({\"bg_color\": \"#FFC7CE\"})\nws.write_row(2, 0, [\"Region\", \"Sales\", \"Target\", \"\"], head)\nfor r, (region, sales, target) in enumerate(DATA, start=3):\n    ws.write_row(r, 0, [region, sales, target])\n    ws.write(r, 3, \"\", green if sales >= target else red)\nchart = wb.add_chart({\"type\": \"column\"})\nchart.add_series({\"categories\": \"=Sheet1!$A$4:$A$7\", \"values\": \"=Sheet1!$B$4:$B$7\"})\nws.insert_chart(\"F2\", chart)\nws.insert_image(\"F18\", \"logo.png\")\nwb.close()\nprint(\"written\")\n","python",[14,178,179,189,196,272,277,295,306,345,385,414,451,485,496,529,550,576,588,604,610],{"__ignoreMap":35},[39,180,181,185],{"class":41,"line":42},[39,182,184],{"class":183},"s-kum","import",[39,186,188],{"class":187},"skGVy"," xlsxwriter\n",[39,190,192],{"class":41,"line":191},2,[39,193,195],{"emptyLinePlaceholder":194},true,"\n",[39,197,199,203,206,209,212,215,218,220,223,226,229,231,234,236,239,241,244,246,249,251,254,256,259,261,264,266,269],{"class":41,"line":198},3,[39,200,202],{"class":201},"sP0c6","DATA",[39,204,205],{"class":183}," =",[39,207,208],{"class":187}," [(",[39,210,211],{"class":49},"\"North\"",[39,213,214],{"class":187},", ",[39,216,217],{"class":201},"482100",[39,219,214],{"class":187},[39,221,222],{"class":201},"450000",[39,224,225],{"class":187},"), (",[39,227,228],{"class":49},"\"South\"",[39,230,214],{"class":187},[39,232,233],{"class":201},"391250",[39,235,214],{"class":187},[39,237,238],{"class":201},"400000",[39,240,225],{"class":187},[39,242,243],{"class":49},"\"East\"",[39,245,214],{"class":187},[39,247,248],{"class":201},"297800",[39,250,214],{"class":187},[39,252,253],{"class":201},"280000",[39,255,225],{"class":187},[39,257,258],{"class":49},"\"West\"",[39,260,214],{"class":187},[39,262,263],{"class":201},"259000",[39,265,214],{"class":187},[39,267,268],{"class":201},"315000",[39,270,271],{"class":187},")]\n",[39,273,275],{"class":41,"line":274},4,[39,276,195],{"emptyLinePlaceholder":194},[39,278,280,283,286,289,292],{"class":41,"line":279},5,[39,281,282],{"class":187},"wb ",[39,284,285],{"class":183},"=",[39,287,288],{"class":187}," xlsxwriter.Workbook(",[39,290,291],{"class":49},"\"sales_typical.xlsx\"",[39,293,294],{"class":187},")\n",[39,296,298,301,303],{"class":41,"line":297},6,[39,299,300],{"class":187},"ws ",[39,302,285],{"class":183},[39,304,305],{"class":187}," wb.add_worksheet()\n",[39,307,309,312,315,317,320,323,326,329,332,334,337,339,342],{"class":41,"line":308},7,[39,310,311],{"class":187},"ws.merge_range(",[39,313,314],{"class":49},"\"A1:D1\"",[39,316,214],{"class":187},[39,318,319],{"class":49},"\"Regional sales — September\"",[39,321,322],{"class":187},", wb.add_format({",[39,324,325],{"class":49},"\"bold\"",[39,327,328],{"class":187},": ",[39,330,331],{"class":201},"True",[39,333,214],{"class":187},[39,335,336],{"class":49},"\"align\"",[39,338,328],{"class":187},[39,340,341],{"class":49},"\"center\"",[39,343,344],{"class":187},"}))\n",[39,346,348,351,353,356,358,360,362,364,367,369,372,374,377,379,382],{"class":41,"line":347},8,[39,349,350],{"class":187},"head ",[39,352,285],{"class":183},[39,354,355],{"class":187}," wb.add_format({",[39,357,325],{"class":49},[39,359,328],{"class":187},[39,361,331],{"class":201},[39,363,214],{"class":187},[39,365,366],{"class":49},"\"font_color\"",[39,368,328],{"class":187},[39,370,371],{"class":49},"\"#BBBBBB\"",[39,373,214],{"class":187},[39,375,376],{"class":49},"\"bg_color\"",[39,378,328],{"class":187},[39,380,381],{"class":49},"\"#FFFFFF\"",[39,383,384],{"class":187},"})\n",[39,386,388,391,393,395,397,399,402,405,407,409,412],{"class":41,"line":387},9,[39,389,390],{"class":187},"green, red ",[39,392,285],{"class":183},[39,394,355],{"class":187},[39,396,376],{"class":49},[39,398,328],{"class":187},[39,400,401],{"class":49},"\"#C6EFCE\"",[39,403,404],{"class":187},"}), wb.add_format({",[39,406,376],{"class":49},[39,408,328],{"class":187},[39,410,411],{"class":49},"\"#FFC7CE\"",[39,413,384],{"class":187},[39,415,417,420,423,425,427,430,433,435,438,440,443,445,448],{"class":41,"line":416},10,[39,418,419],{"class":187},"ws.write_row(",[39,421,422],{"class":201},"2",[39,424,214],{"class":187},[39,426,86],{"class":201},[39,428,429],{"class":187},", [",[39,431,432],{"class":49},"\"Region\"",[39,434,214],{"class":187},[39,436,437],{"class":49},"\"Sales\"",[39,439,214],{"class":187},[39,441,442],{"class":49},"\"Target\"",[39,444,214],{"class":187},[39,446,447],{"class":49},"\"\"",[39,449,450],{"class":187},"], head)\n",[39,452,454,457,460,463,466,469,471,473,477,479,482],{"class":41,"line":453},11,[39,455,456],{"class":183},"for",[39,458,459],{"class":187}," r, (region, sales, target) ",[39,461,462],{"class":183},"in",[39,464,465],{"class":201}," enumerate",[39,467,468],{"class":187},"(",[39,470,202],{"class":201},[39,472,214],{"class":187},[39,474,476],{"class":475},"sa561","start",[39,478,285],{"class":183},[39,480,481],{"class":201},"3",[39,483,484],{"class":187},"):\n",[39,486,488,491,493],{"class":41,"line":487},12,[39,489,490],{"class":187},"    ws.write_row(r, ",[39,492,86],{"class":201},[39,494,495],{"class":187},", [region, sales, target])\n",[39,497,499,502,504,506,508,511,514,517,520,523,526],{"class":41,"line":498},13,[39,500,501],{"class":187},"    ws.write(r, ",[39,503,481],{"class":201},[39,505,214],{"class":187},[39,507,447],{"class":49},[39,509,510],{"class":187},", green ",[39,512,513],{"class":183},"if",[39,515,516],{"class":187}," sales ",[39,518,519],{"class":183},">=",[39,521,522],{"class":187}," target ",[39,524,525],{"class":183},"else",[39,527,528],{"class":187}," red)\n",[39,530,532,535,537,540,543,545,548],{"class":41,"line":531},14,[39,533,534],{"class":187},"chart ",[39,536,285],{"class":183},[39,538,539],{"class":187}," wb.add_chart({",[39,541,542],{"class":49},"\"type\"",[39,544,328],{"class":187},[39,546,547],{"class":49},"\"column\"",[39,549,384],{"class":187},[39,551,553,556,559,561,564,566,569,571,574],{"class":41,"line":552},15,[39,554,555],{"class":187},"chart.add_series({",[39,557,558],{"class":49},"\"categories\"",[39,560,328],{"class":187},[39,562,563],{"class":49},"\"=Sheet1!$A$4:$A$7\"",[39,565,214],{"class":187},[39,567,568],{"class":49},"\"values\"",[39,570,328],{"class":187},[39,572,573],{"class":49},"\"=Sheet1!$B$4:$B$7\"",[39,575,384],{"class":187},[39,577,579,582,585],{"class":41,"line":578},16,[39,580,581],{"class":187},"ws.insert_chart(",[39,583,584],{"class":49},"\"F2\"",[39,586,587],{"class":187},", chart)\n",[39,589,591,594,597,599,602],{"class":41,"line":590},17,[39,592,593],{"class":187},"ws.insert_image(",[39,595,596],{"class":49},"\"F18\"",[39,598,214],{"class":187},[39,600,601],{"class":49},"\"logo.png\"",[39,603,294],{"class":187},[39,605,607],{"class":41,"line":606},18,[39,608,609],{"class":187},"wb.close()\n",[39,611,613,616,618,621],{"class":41,"line":612},19,[39,614,615],{"class":201},"print",[39,617,468],{"class":187},[39,619,620],{"class":49},"\"written\"",[39,622,294],{"class":187},[10,624,625,626,629],{},"To a sighted reader it looks fine. To a screen reader, the status column is four empty cells, the chart has no description at all, and the logo is announced as ",[14,627,628],{},"logo.png"," — xlsxwriter uses an image's file name as its alternative text unless told otherwise, which is no better than nothing; the merged title spans cells that navigation lands in unpredictably; and the header text is so pale it fails contrast checks for everyone else.",[25,631,633],{"id":632},"step-2-structure-names-title-tables","Step 2 — Structure: names, title, tables",[10,635,636,637,640],{},"Give each sheet a name that says what it contains, put the title in ",[14,638,639],{},"A1"," without merging, and put the data in an Excel table so its first row is a real header row that screen readers announce as column names:",[30,642,644],{"className":174,"code":643,"language":176,"meta":35,"style":35},"wb = xlsxwriter.Workbook(\"sales_accessible.xlsx\")\nwb.set_properties({\"title\": \"Regional sales, September 2026\", \"subject\": \"Sales against target by region\",\n                   \"author\": \"Sales reporting\"})\nws = wb.add_worksheet(\"Sales by region\")\nws.write(\"A1\", \"Regional sales, September 2026\", wb.add_format({\"bold\": True, \"font_size\": 14}))\nrows = [[region, sales, target, \"On target\" if sales >= target else \"Below target\"] for region, sales, target in DATA]\nws.add_table(\"A3:D7\", {\"name\": \"RegionalSales\", \"style\": \"Table Style Medium 2\",\n                       \"columns\": [{\"header\": h} for h in (\"Region\", \"Sales\", \"Target\", \"Status\")], \"data\": rows})\nws.set_column(\"A:A\", 14)\nws.set_column(\"B:C\", 12, wb.add_format({\"num_format\": \"#,##0\"}))\nws.set_column(\"D:D\", 14)\nws.freeze_panes(3, 0)\n",[14,645,646,659,685,697,711,743,786,817,865,879,902,915],{"__ignoreMap":35},[39,647,648,650,652,654,657],{"class":41,"line":42},[39,649,282],{"class":187},[39,651,285],{"class":183},[39,653,288],{"class":187},[39,655,656],{"class":49},"\"sales_accessible.xlsx\"",[39,658,294],{"class":187},[39,660,661,664,667,669,672,674,677,679,682],{"class":41,"line":191},[39,662,663],{"class":187},"wb.set_properties({",[39,665,666],{"class":49},"\"title\"",[39,668,328],{"class":187},[39,670,671],{"class":49},"\"Regional sales, September 2026\"",[39,673,214],{"class":187},[39,675,676],{"class":49},"\"subject\"",[39,678,328],{"class":187},[39,680,681],{"class":49},"\"Sales against target by region\"",[39,683,684],{"class":187},",\n",[39,686,687,690,692,695],{"class":41,"line":198},[39,688,689],{"class":49},"                   \"author\"",[39,691,328],{"class":187},[39,693,694],{"class":49},"\"Sales reporting\"",[39,696,384],{"class":187},[39,698,699,701,703,706,709],{"class":41,"line":274},[39,700,300],{"class":187},[39,702,285],{"class":183},[39,704,705],{"class":187}," wb.add_worksheet(",[39,707,708],{"class":49},"\"Sales by region\"",[39,710,294],{"class":187},[39,712,713,716,719,721,723,725,727,729,731,733,736,738,741],{"class":41,"line":279},[39,714,715],{"class":187},"ws.write(",[39,717,718],{"class":49},"\"A1\"",[39,720,214],{"class":187},[39,722,671],{"class":49},[39,724,322],{"class":187},[39,726,325],{"class":49},[39,728,328],{"class":187},[39,730,331],{"class":201},[39,732,214],{"class":187},[39,734,735],{"class":49},"\"font_size\"",[39,737,328],{"class":187},[39,739,740],{"class":201},"14",[39,742,344],{"class":187},[39,744,745,748,750,753,756,759,761,763,765,767,770,773,775,778,780,783],{"class":41,"line":297},[39,746,747],{"class":187},"rows ",[39,749,285],{"class":183},[39,751,752],{"class":187}," [[region, sales, target, ",[39,754,755],{"class":49},"\"On target\"",[39,757,758],{"class":183}," if",[39,760,516],{"class":187},[39,762,519],{"class":183},[39,764,522],{"class":187},[39,766,525],{"class":183},[39,768,769],{"class":49}," \"Below target\"",[39,771,772],{"class":187},"] ",[39,774,456],{"class":183},[39,776,777],{"class":187}," region, sales, target ",[39,779,462],{"class":183},[39,781,782],{"class":201}," DATA",[39,784,785],{"class":187},"]\n",[39,787,788,791,794,797,800,802,805,807,810,812,815],{"class":41,"line":308},[39,789,790],{"class":187},"ws.add_table(",[39,792,793],{"class":49},"\"A3:D7\"",[39,795,796],{"class":187},", {",[39,798,799],{"class":49},"\"name\"",[39,801,328],{"class":187},[39,803,804],{"class":49},"\"RegionalSales\"",[39,806,214],{"class":187},[39,808,809],{"class":49},"\"style\"",[39,811,328],{"class":187},[39,813,814],{"class":49},"\"Table Style Medium 2\"",[39,816,684],{"class":187},[39,818,819,822,825,828,831,833,836,838,841,843,845,847,849,851,853,856,859,862],{"class":41,"line":347},[39,820,821],{"class":49},"                       \"columns\"",[39,823,824],{"class":187},": [{",[39,826,827],{"class":49},"\"header\"",[39,829,830],{"class":187},": h} ",[39,832,456],{"class":183},[39,834,835],{"class":187}," h ",[39,837,462],{"class":183},[39,839,840],{"class":187}," (",[39,842,432],{"class":49},[39,844,214],{"class":187},[39,846,437],{"class":49},[39,848,214],{"class":187},[39,850,442],{"class":49},[39,852,214],{"class":187},[39,854,855],{"class":49},"\"Status\"",[39,857,858],{"class":187},")], ",[39,860,861],{"class":49},"\"data\"",[39,863,864],{"class":187},": rows})\n",[39,866,867,870,873,875,877],{"class":41,"line":387},[39,868,869],{"class":187},"ws.set_column(",[39,871,872],{"class":49},"\"A:A\"",[39,874,214],{"class":187},[39,876,740],{"class":201},[39,878,294],{"class":187},[39,880,881,883,886,888,890,892,895,897,900],{"class":41,"line":416},[39,882,869],{"class":187},[39,884,885],{"class":49},"\"B:C\"",[39,887,214],{"class":187},[39,889,96],{"class":201},[39,891,322],{"class":187},[39,893,894],{"class":49},"\"num_format\"",[39,896,328],{"class":187},[39,898,899],{"class":49},"\"#,##0\"",[39,901,344],{"class":187},[39,903,904,906,909,911,913],{"class":41,"line":453},[39,905,869],{"class":187},[39,907,908],{"class":49},"\"D:D\"",[39,910,214],{"class":187},[39,912,740],{"class":201},[39,914,294],{"class":187},[39,916,917,920,922,924,926],{"class":41,"line":487},[39,918,919],{"class":187},"ws.freeze_panes(",[39,921,481],{"class":201},[39,923,214],{"class":187},[39,925,86],{"class":201},[39,927,294],{"class":187},[10,929,930,931,934,935,939],{},"The document title is what assistive technology announces when the file opens, and what file dialogs and search show. The table name, ",[14,932,933],{},"RegionalSales",", gives the range a meaningful handle too. ",[19,936,938],{"href":937},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fcreate-an-excel-table-with-python\u002F","Create an Excel Table with Python"," covers tables in general.",[25,941,943],{"id":942},"step-3-status-as-text-colour-as-a-second-signal","Step 3 — Status as text, colour as a second signal",[10,945,946],{},"The status column now contains words, so its meaning survives a screen reader, a black-and-white printer and colour blindness. Colour can still help sighted readers scan — as a second signal on top of the text, with a dark text colour on a light fill:",[30,948,950],{"className":174,"code":949,"language":176,"meta":35,"style":35},"ok = wb.add_format({\"bg_color\": \"#E6F4EA\", \"font_color\": \"#0B6157\", \"bold\": True})\nbad = wb.add_format({\"bg_color\": \"#FCE9E9\", \"font_color\": \"#9C0006\", \"bold\": True})\nws.conditional_format(\"D4:D7\", {\"type\": \"text\", \"criteria\": \"begins with\", \"value\": \"On\", \"format\": ok})\nws.conditional_format(\"D4:D7\", {\"type\": \"text\", \"criteria\": \"begins with\", \"value\": \"Below\", \"format\": bad})\n",[14,951,952,987,1022,1067],{"__ignoreMap":35},[39,953,954,957,959,961,963,965,968,970,972,974,977,979,981,983,985],{"class":41,"line":42},[39,955,956],{"class":187},"ok ",[39,958,285],{"class":183},[39,960,355],{"class":187},[39,962,376],{"class":49},[39,964,328],{"class":187},[39,966,967],{"class":49},"\"#E6F4EA\"",[39,969,214],{"class":187},[39,971,366],{"class":49},[39,973,328],{"class":187},[39,975,976],{"class":49},"\"#0B6157\"",[39,978,214],{"class":187},[39,980,325],{"class":49},[39,982,328],{"class":187},[39,984,331],{"class":201},[39,986,384],{"class":187},[39,988,989,992,994,996,998,1000,1003,1005,1007,1009,1012,1014,1016,1018,1020],{"class":41,"line":191},[39,990,991],{"class":187},"bad ",[39,993,285],{"class":183},[39,995,355],{"class":187},[39,997,376],{"class":49},[39,999,328],{"class":187},[39,1001,1002],{"class":49},"\"#FCE9E9\"",[39,1004,214],{"class":187},[39,1006,366],{"class":49},[39,1008,328],{"class":187},[39,1010,1011],{"class":49},"\"#9C0006\"",[39,1013,214],{"class":187},[39,1015,325],{"class":49},[39,1017,328],{"class":187},[39,1019,331],{"class":201},[39,1021,384],{"class":187},[39,1023,1024,1027,1030,1032,1034,1036,1039,1041,1044,1046,1049,1051,1054,1056,1059,1061,1064],{"class":41,"line":198},[39,1025,1026],{"class":187},"ws.conditional_format(",[39,1028,1029],{"class":49},"\"D4:D7\"",[39,1031,796],{"class":187},[39,1033,542],{"class":49},[39,1035,328],{"class":187},[39,1037,1038],{"class":49},"\"text\"",[39,1040,214],{"class":187},[39,1042,1043],{"class":49},"\"criteria\"",[39,1045,328],{"class":187},[39,1047,1048],{"class":49},"\"begins with\"",[39,1050,214],{"class":187},[39,1052,1053],{"class":49},"\"value\"",[39,1055,328],{"class":187},[39,1057,1058],{"class":49},"\"On\"",[39,1060,214],{"class":187},[39,1062,1063],{"class":49},"\"format\"",[39,1065,1066],{"class":187},": ok})\n",[39,1068,1069,1071,1073,1075,1077,1079,1081,1083,1085,1087,1089,1091,1093,1095,1098,1100,1102],{"class":41,"line":274},[39,1070,1026],{"class":187},[39,1072,1029],{"class":49},[39,1074,796],{"class":187},[39,1076,542],{"class":49},[39,1078,328],{"class":187},[39,1080,1038],{"class":49},[39,1082,214],{"class":187},[39,1084,1043],{"class":49},[39,1086,328],{"class":187},[39,1088,1048],{"class":49},[39,1090,214],{"class":187},[39,1092,1053],{"class":49},[39,1094,328],{"class":187},[39,1096,1097],{"class":49},"\"Below\"",[39,1099,214],{"class":187},[39,1101,1063],{"class":49},[39,1103,1104],{"class":187},": bad})\n",[10,1106,1107,1108,1112],{},"The conditional formats key off the text, so the colour always agrees with the words. ",[19,1109,1111],{"href":1110},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fapplying-number-and-date-formats-in-excel\u002Fshow-negative-numbers-in-red-with-parentheses\u002F","Show Negative Numbers in Red with Parentheses"," applies the same principle to numbers: brackets carry the meaning, red reinforces it.",[64,1114,73,1120,73,1123,73,1126,73,1131,73,1136,73,1143,73,1149,73,1155,73,1160,73,1168,73,1172,73,1177,73,1181,73,1186,73,1189,73,1195,73,1200,73,1204,73,1208,73,1213,73,1216,73,1221,73,1224],{"viewBox":1115,"role":67,"ariaLabelledBy":1116,"xmlns":71,"style":1119},"4 0 752 207",[1117,1118],"ax-colour-t","ax-colour-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[75,1121,1122],{"id":1117},"Colour alone versus colour plus text",[79,1124,1125],{"id":1118},"Two panels. A status column shown only as red and green fill is empty to a screen reader and ambiguous in print and for colour-blind readers. A status column with the words On target and Below target plus a matching colour works for everyone.",[83,1127],{"x":1128,"y":86,"width":1129,"height":1130,"fill":89},"4","752","207",[83,1132],{"x":1133,"y":93,"width":1134,"height":1135,"rx":740,"fill":147,"stroke":148,"style":99},"20.0","351.0","175.0",[101,1137,1139],{"transform":1138},"translate(182.5,29.0) scale(1.083)",[105,1140],{"d":1141,"fill":108,"stroke":155,"style":1142},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[112,1144,1148],{"x":1145,"y":1146,"style":1147},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Colour only",[41,1150],{"x1":1151,"y1":1152,"x2":1153,"y2":1152,"stroke":148,"style":1154},"36.0","91.0","355.0","stroke-width:1px",[112,1156,1159],{"x":92,"y":1157,"style":1158},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","empty cells to a screen reader",[101,1161,1163],{"transform":1162},"translate(38.5,99.5) scale(0.625)",[105,1164],{"d":1165,"fill":108,"stroke":1166,"style":1167},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[112,1169,1171],{"x":92,"y":1170,"style":1158},"137.0","lost in black-and-white print",[101,1173,1175],{"transform":1174},"translate(38.5,125.5) scale(0.625)",[105,1176],{"d":1165,"fill":108,"stroke":1166,"style":1167},[112,1178,1180],{"x":92,"y":1179,"style":1158},"163.0","red\u002Fgreen hard to tell apart",[101,1182,1184],{"transform":1183},"translate(38.5,151.5) scale(0.625)",[105,1185],{"d":1165,"fill":108,"stroke":1166,"style":1167},[83,1187],{"x":1188,"y":93,"width":1134,"height":1135,"rx":740,"fill":126,"stroke":127,"style":99},"389.0",[101,1190,1192],{"transform":1191},"translate(551.5,29.0) scale(1.083)",[105,1193],{"d":1194,"fill":108,"stroke":134,"style":1142},"M5 12.5l4.5 4.5L19 7",[112,1196,1199],{"x":1197,"y":1146,"style":1198},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Text + colour",[41,1201],{"x1":1202,"y1":1152,"x2":1203,"y2":1152,"stroke":127,"style":1154},"405.0","724.0",[112,1205,1207],{"x":1206,"y":1157,"style":1158},"429.0","'Below target' is read aloud",[101,1209,1211],{"transform":1210},"translate(407.5,99.5) scale(0.625)",[105,1212],{"d":1194,"fill":108,"stroke":127,"style":1167},[112,1214,1215],{"x":1206,"y":1170,"style":1158},"survives printing",[101,1217,1219],{"transform":1218},"translate(407.5,125.5) scale(0.625)",[105,1220],{"d":1194,"fill":108,"stroke":127,"style":1167},[112,1222,1223],{"x":1206,"y":1179,"style":1158},"colour is a second signal",[101,1225,1227],{"transform":1226},"translate(407.5,151.5) scale(0.625)",[105,1228],{"d":1194,"fill":108,"stroke":127,"style":1167},[25,1230,1232],{"id":1231},"step-4-alternative-text-on-charts-and-images","Step 4 — Alternative text on charts and images",[10,1234,1235,1236,1239],{},"Charts and pictures need a text alternative. xlsxwriter's ",[14,1237,1238],{},"description"," option writes it into the file; for a chart, say what it shows and what the reader should take from it, not just its type. Purely decorative images can be marked as such so screen readers skip them:",[30,1241,1243],{"className":174,"code":1242,"language":176,"meta":35,"style":35},"chart = wb.add_chart({\"type\": \"column\"})\nchart.add_series({\"name\": \"Sales\", \"categories\": \"='Sales by region'!$A$4:$A$7\",\n                  \"values\": \"='Sales by region'!$B$4:$B$7\"})\nchart.add_series({\"name\": \"Target\", \"categories\": \"='Sales by region'!$A$4:$A$7\",\n                  \"values\": \"='Sales by region'!$C$4:$C$7\"})\nchart.set_title({\"name\": \"Sales against target by region\"})\nws.insert_chart(\"F3\", chart, {\"description\": \"Column chart of sales against target for four regions. \"\n                              \"North and East are above target; South is slightly below; West is 18% below.\"})\nws.insert_image(\"F19\", \"logo.png\", {\"decorative\": True})\nwb.close()\nprint(\"written\")\n",[14,1244,1245,1261,1282,1294,1314,1325,1338,1356,1370,1392,1396],{"__ignoreMap":35},[39,1246,1247,1249,1251,1253,1255,1257,1259],{"class":41,"line":42},[39,1248,534],{"class":187},[39,1250,285],{"class":183},[39,1252,539],{"class":187},[39,1254,542],{"class":49},[39,1256,328],{"class":187},[39,1258,547],{"class":49},[39,1260,384],{"class":187},[39,1262,1263,1265,1267,1269,1271,1273,1275,1277,1280],{"class":41,"line":191},[39,1264,555],{"class":187},[39,1266,799],{"class":49},[39,1268,328],{"class":187},[39,1270,437],{"class":49},[39,1272,214],{"class":187},[39,1274,558],{"class":49},[39,1276,328],{"class":187},[39,1278,1279],{"class":49},"\"='Sales by region'!$A$4:$A$7\"",[39,1281,684],{"class":187},[39,1283,1284,1287,1289,1292],{"class":41,"line":198},[39,1285,1286],{"class":49},"                  \"values\"",[39,1288,328],{"class":187},[39,1290,1291],{"class":49},"\"='Sales by region'!$B$4:$B$7\"",[39,1293,384],{"class":187},[39,1295,1296,1298,1300,1302,1304,1306,1308,1310,1312],{"class":41,"line":274},[39,1297,555],{"class":187},[39,1299,799],{"class":49},[39,1301,328],{"class":187},[39,1303,442],{"class":49},[39,1305,214],{"class":187},[39,1307,558],{"class":49},[39,1309,328],{"class":187},[39,1311,1279],{"class":49},[39,1313,684],{"class":187},[39,1315,1316,1318,1320,1323],{"class":41,"line":279},[39,1317,1286],{"class":49},[39,1319,328],{"class":187},[39,1321,1322],{"class":49},"\"='Sales by region'!$C$4:$C$7\"",[39,1324,384],{"class":187},[39,1326,1327,1330,1332,1334,1336],{"class":41,"line":297},[39,1328,1329],{"class":187},"chart.set_title({",[39,1331,799],{"class":49},[39,1333,328],{"class":187},[39,1335,681],{"class":49},[39,1337,384],{"class":187},[39,1339,1340,1342,1345,1348,1351,1353],{"class":41,"line":308},[39,1341,581],{"class":187},[39,1343,1344],{"class":49},"\"F3\"",[39,1346,1347],{"class":187},", chart, {",[39,1349,1350],{"class":49},"\"description\"",[39,1352,328],{"class":187},[39,1354,1355],{"class":49},"\"Column chart of sales against target for four regions. \"\n",[39,1357,1358,1361,1365,1368],{"class":41,"line":347},[39,1359,1360],{"class":49},"                              \"North and East are above target; South is slightly below; West is 18",[39,1362,1364],{"class":1363},"sSjpA","% b",[39,1366,1367],{"class":49},"elow.\"",[39,1369,384],{"class":187},[39,1371,1372,1374,1377,1379,1381,1383,1386,1388,1390],{"class":41,"line":387},[39,1373,593],{"class":187},[39,1375,1376],{"class":49},"\"F19\"",[39,1378,214],{"class":187},[39,1380,601],{"class":49},[39,1382,796],{"class":187},[39,1384,1385],{"class":49},"\"decorative\"",[39,1387,328],{"class":187},[39,1389,331],{"class":201},[39,1391,384],{"class":187},[39,1393,1394],{"class":41,"line":416},[39,1395,609],{"class":187},[39,1397,1398,1400,1402,1404],{"class":41,"line":453},[39,1399,615],{"class":201},[39,1401,468],{"class":187},[39,1403,620],{"class":49},[39,1405,294],{"class":187},[10,1407,1408],{},"The chart's alt text summarises the finding, which is what a sighted reader gets at a glance. Numbers in the alt text should match the data — build the sentence from the same variables as the chart when it changes weekly. The chart also gets a title and named series, so its legend reads \"Sales\" and \"Target\" rather than \"Series1\".",[25,1410,1412],{"id":1411},"step-5-colours-with-enough-contrast","Step 5 — Colours with enough contrast",[10,1414,1415],{},"Text needs a contrast ratio of at least 4.5:1 against its background under WCAG AA — the standard most accessibility policies refer to. The ratio is a formula on the two colours' relative luminance, so a report script can check every font-on-fill combination it uses:",[30,1417,1419],{"className":174,"code":1418,"language":176,"meta":35,"style":35},"def luminance(hex_colour):\n    rgb = [int(hex_colour.lstrip(\"#\")[i:i + 2], 16) \u002F 255 for i in (0, 2, 4)]\n    lin = [c \u002F 12.92 if c \u003C= 0.03928 else ((c + 0.055) \u002F 1.055) ** 2.4 for c in rgb]\n    return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]\n\ndef contrast(fg, bg):\n    hi, lo = sorted((luminance(fg), luminance(bg)), reverse=True)\n    return (hi + 0.05) \u002F (lo + 0.05)\n\nfor fg, bg, where in [(\"#BBBBBB\", \"#FFFFFF\", \"typical header\"), (\"#0B6157\", \"#E6F4EA\", \"On target\"),\n                      (\"#9C0006\", \"#FCE9E9\", \"Below target\"), (\"#FFFFFF\", \"#4F81BD\", \"white on mid blue\")]:\n    ratio = contrast(fg, bg)\n    print(f\"{where:18} {ratio:5.2f}:1  {'pass' if ratio >= 4.5 else 'FAIL'}\")\n",[14,1420,1421,1433,1498,1559,1604,1608,1618,1640,1665,1669,1706,1739,1749],{"__ignoreMap":35},[39,1422,1423,1426,1430],{"class":41,"line":42},[39,1424,1425],{"class":183},"def",[39,1427,1429],{"class":1428},"s_Opv"," luminance",[39,1431,1432],{"class":187},"(hex_colour):\n",[39,1434,1435,1438,1440,1443,1446,1449,1452,1455,1458,1461,1464,1467,1470,1473,1476,1479,1482,1484,1486,1488,1490,1492,1494,1496],{"class":41,"line":191},[39,1436,1437],{"class":187},"    rgb ",[39,1439,285],{"class":183},[39,1441,1442],{"class":187}," [",[39,1444,1445],{"class":201},"int",[39,1447,1448],{"class":187},"(hex_colour.lstrip(",[39,1450,1451],{"class":49},"\"#\"",[39,1453,1454],{"class":187},")[i:i ",[39,1456,1457],{"class":183},"+",[39,1459,1460],{"class":201}," 2",[39,1462,1463],{"class":187},"], ",[39,1465,1466],{"class":201},"16",[39,1468,1469],{"class":187},") ",[39,1471,1472],{"class":183},"\u002F",[39,1474,1475],{"class":201}," 255",[39,1477,1478],{"class":183}," for",[39,1480,1481],{"class":187}," i ",[39,1483,462],{"class":183},[39,1485,840],{"class":187},[39,1487,86],{"class":201},[39,1489,214],{"class":187},[39,1491,422],{"class":201},[39,1493,214],{"class":187},[39,1495,1128],{"class":201},[39,1497,271],{"class":187},[39,1499,1500,1503,1505,1508,1510,1513,1515,1518,1521,1524,1527,1530,1532,1535,1537,1539,1542,1544,1547,1550,1552,1554,1556],{"class":41,"line":198},[39,1501,1502],{"class":187},"    lin ",[39,1504,285],{"class":183},[39,1506,1507],{"class":187}," [c ",[39,1509,1472],{"class":183},[39,1511,1512],{"class":201}," 12.92",[39,1514,758],{"class":183},[39,1516,1517],{"class":187}," c ",[39,1519,1520],{"class":183},"\u003C=",[39,1522,1523],{"class":201}," 0.03928",[39,1525,1526],{"class":183}," else",[39,1528,1529],{"class":187}," ((c ",[39,1531,1457],{"class":183},[39,1533,1534],{"class":201}," 0.055",[39,1536,1469],{"class":187},[39,1538,1472],{"class":183},[39,1540,1541],{"class":201}," 1.055",[39,1543,1469],{"class":187},[39,1545,1546],{"class":183},"**",[39,1548,1549],{"class":201}," 2.4",[39,1551,1478],{"class":183},[39,1553,1517],{"class":187},[39,1555,462],{"class":183},[39,1557,1558],{"class":187}," rgb]\n",[39,1560,1561,1564,1567,1570,1573,1575,1577,1579,1582,1584,1586,1589,1591,1593,1596,1598,1600,1602],{"class":41,"line":274},[39,1562,1563],{"class":183},"    return",[39,1565,1566],{"class":201}," 0.2126",[39,1568,1569],{"class":183}," *",[39,1571,1572],{"class":187}," lin[",[39,1574,86],{"class":201},[39,1576,772],{"class":187},[39,1578,1457],{"class":183},[39,1580,1581],{"class":201}," 0.7152",[39,1583,1569],{"class":183},[39,1585,1572],{"class":187},[39,1587,1588],{"class":201},"1",[39,1590,772],{"class":187},[39,1592,1457],{"class":183},[39,1594,1595],{"class":201}," 0.0722",[39,1597,1569],{"class":183},[39,1599,1572],{"class":187},[39,1601,422],{"class":201},[39,1603,785],{"class":187},[39,1605,1606],{"class":41,"line":279},[39,1607,195],{"emptyLinePlaceholder":194},[39,1609,1610,1612,1615],{"class":41,"line":297},[39,1611,1425],{"class":183},[39,1613,1614],{"class":1428}," contrast",[39,1616,1617],{"class":187},"(fg, bg):\n",[39,1619,1620,1623,1625,1628,1631,1634,1636,1638],{"class":41,"line":308},[39,1621,1622],{"class":187},"    hi, lo ",[39,1624,285],{"class":183},[39,1626,1627],{"class":201}," sorted",[39,1629,1630],{"class":187},"((luminance(fg), luminance(bg)), ",[39,1632,1633],{"class":475},"reverse",[39,1635,285],{"class":183},[39,1637,331],{"class":201},[39,1639,294],{"class":187},[39,1641,1642,1644,1647,1649,1652,1654,1656,1659,1661,1663],{"class":41,"line":347},[39,1643,1563],{"class":183},[39,1645,1646],{"class":187}," (hi ",[39,1648,1457],{"class":183},[39,1650,1651],{"class":201}," 0.05",[39,1653,1469],{"class":187},[39,1655,1472],{"class":183},[39,1657,1658],{"class":187}," (lo ",[39,1660,1457],{"class":183},[39,1662,1651],{"class":201},[39,1664,294],{"class":187},[39,1666,1667],{"class":41,"line":387},[39,1668,195],{"emptyLinePlaceholder":194},[39,1670,1671,1673,1676,1678,1680,1682,1684,1686,1688,1691,1693,1695,1697,1699,1701,1703],{"class":41,"line":416},[39,1672,456],{"class":183},[39,1674,1675],{"class":187}," fg, bg, where ",[39,1677,462],{"class":183},[39,1679,208],{"class":187},[39,1681,371],{"class":49},[39,1683,214],{"class":187},[39,1685,381],{"class":49},[39,1687,214],{"class":187},[39,1689,1690],{"class":49},"\"typical header\"",[39,1692,225],{"class":187},[39,1694,976],{"class":49},[39,1696,214],{"class":187},[39,1698,967],{"class":49},[39,1700,214],{"class":187},[39,1702,755],{"class":49},[39,1704,1705],{"class":187},"),\n",[39,1707,1708,1711,1713,1715,1717,1719,1722,1724,1726,1728,1731,1733,1736],{"class":41,"line":453},[39,1709,1710],{"class":187},"                      (",[39,1712,1011],{"class":49},[39,1714,214],{"class":187},[39,1716,1002],{"class":49},[39,1718,214],{"class":187},[39,1720,1721],{"class":49},"\"Below target\"",[39,1723,225],{"class":187},[39,1725,381],{"class":49},[39,1727,214],{"class":187},[39,1729,1730],{"class":49},"\"#4F81BD\"",[39,1732,214],{"class":187},[39,1734,1735],{"class":49},"\"white on mid blue\"",[39,1737,1738],{"class":187},")]:\n",[39,1740,1741,1744,1746],{"class":41,"line":487},[39,1742,1743],{"class":187},"    ratio ",[39,1745,285],{"class":183},[39,1747,1748],{"class":187}," contrast(fg, bg)\n",[39,1750,1751,1754,1756,1759,1762,1765,1768,1771,1774,1777,1780,1783,1785,1788,1790,1793,1795,1798,1800,1803,1805,1808,1810,1812],{"class":41,"line":498},[39,1752,1753],{"class":201},"    print",[39,1755,468],{"class":187},[39,1757,1758],{"class":183},"f",[39,1760,1761],{"class":49},"\"",[39,1763,1764],{"class":1363},"{",[39,1766,1767],{"class":187},"where",[39,1769,1770],{"class":183},":18",[39,1772,1773],{"class":1363},"}",[39,1775,1776],{"class":1363}," {",[39,1778,1779],{"class":187},"ratio",[39,1781,1782],{"class":183},":5.2f",[39,1784,1773],{"class":1363},[39,1786,1787],{"class":49},":1  ",[39,1789,1764],{"class":1363},[39,1791,1792],{"class":49},"'pass'",[39,1794,758],{"class":183},[39,1796,1797],{"class":187}," ratio ",[39,1799,519],{"class":183},[39,1801,1802],{"class":201}," 4.5",[39,1804,1526],{"class":183},[39,1806,1807],{"class":49}," 'FAIL'",[39,1809,1773],{"class":1363},[39,1811,1761],{"class":49},[39,1813,294],{"class":187},[10,1815,1816],{},"The pale grey header of the typical version fails badly and the dark-on-light status colours pass comfortably. The instructive one is white text on a mid blue, a very common header style: it looks crisp but reaches only about 4:1, short of 4.5:1 for normal text. WCAG allows 3:1 for large or bold text, so it is acceptable for a bold 14-point header and not for ordinary body text — darken the blue if it is used behind normal-sized text. Running a check like this over the palette a report uses — once, when the palette is chosen — prevents the most common readability complaint.",[64,1818,73,1824,73,1827,73,1830,73,1834,73,1841,73,1847,73,1854,73,1859,73,1862,73,1866,73,1869,73,1874,73,1877,73,1881,73,1884,73,1889,73,1892,73,1896,73,1899,73,1903],{"viewBox":1819,"role":67,"ariaLabelledBy":1820,"xmlns":71,"style":1823},"4 0 684 206",[1821,1822],"ax-contrast-t","ax-contrast-d","width:100%;max-width:684px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[75,1825,1826],{"id":1821},"Contrast ratios of the report's colour pairs",[79,1828,1829],{"id":1822},"Horizontal bars of contrast ratios. The pale grey header on white reaches 1.9 to 1 and white on mid blue 4.0 to 1, both below the 4.5 to 1 threshold for normal text; dark red on pale pink reaches 7.4 and dark teal on pale green 6.5.",[83,1831],{"x":1128,"y":86,"width":1832,"height":1833,"fill":89},"684","206",[83,1835],{"x":1133,"y":93,"width":1836,"height":1837,"rx":1838,"fill":1839,"stroke":1840,"style":1154},"171.8","28.0","7","#e7ebef","var(--line,#cdd5e6)",[112,1842,1846],{"x":1843,"y":1844,"style":1845},"105.9","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","grey on white (typical)",[83,1848],{"x":1849,"y":93,"width":1850,"height":1837,"rx":1838,"fill":1851,"stroke":1852,"style":1853},"201.8","99.3","#fce9e9","var(--danger,#dc2626)","stroke-width:1.5px",[112,1855,1858],{"x":1856,"y":1844,"style":1857},"309.1","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","1.9:1 — fail",[83,1860],{"x":1133,"y":1861,"width":1836,"height":1837,"rx":1838,"fill":1839,"stroke":1840,"style":1154},"56.0",[112,1863,1865],{"x":1843,"y":1864,"style":1845},"74.5","white on mid blue",[83,1867],{"x":1849,"y":1861,"width":1868,"height":1837,"rx":1838,"fill":147,"stroke":148,"style":1853},"208.5",[112,1870,1873],{"x":1871,"y":1864,"style":1872},"418.2","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","4.0:1 — large text only",[83,1875],{"x":1133,"y":1876,"width":1836,"height":1837,"rx":1838,"fill":1839,"stroke":1840,"style":1154},"96.0",[112,1878,1880],{"x":1843,"y":1879,"style":1845},"114.5","dark red on pink",[83,1882],{"x":1849,"y":1876,"width":1883,"height":1837,"rx":1838,"fill":126,"stroke":127,"style":1853},"384.9",[112,1885,1888],{"x":1886,"y":1879,"style":1887},"594.6","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","7.4:1 — pass",[83,1890],{"x":1133,"y":1891,"width":1836,"height":1837,"rx":1838,"fill":1839,"stroke":1840,"style":1154},"136.0",[112,1893,1895],{"x":1843,"y":1894,"style":1845},"154.5","dark teal on green",[83,1897],{"x":1849,"y":1891,"width":1898,"height":1837,"rx":1838,"fill":126,"stroke":127,"style":1853},"334.2",[112,1900,1902],{"x":1901,"y":1894,"style":1887},"543.9","6.5:1 — pass",[112,1904,1907],{"x":114,"y":1905,"style":1906},"186.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","WCAG AA asks for at least 4.5:1 for normal text and 3:1 for large or bold text.",[25,1909,1911],{"id":1910},"step-6-audit-any-workbook","Step 6 — Audit any workbook",[10,1913,1914,1915,1918],{},"The same rules make an audit that runs on any ",[14,1916,1917],{},".xlsx",", generated or hand-made: default sheet names, merged cells, drawings without alt text, a missing document title:",[30,1920,1922],{"className":174,"code":1921,"language":176,"meta":35,"style":35},"import re\nimport zipfile\nfrom openpyxl import load_workbook\n\ndef audit(path):\n    issues = []\n    wb = load_workbook(path)\n    if not wb.properties.title:\n        issues.append(\"no document title\")\n    for ws in wb.worksheets:\n        if re.fullmatch(r\"Sheet\\d*\", ws.title):\n            issues.append(f\"default sheet name '{ws.title}'\")\n        if ws.merged_cells.ranges:\n            issues.append(f\"{ws.title}: merged cells {[str(r) for r in ws.merged_cells.ranges]}\")\n    with zipfile.ZipFile(path) as z:\n        for part in (n for n in z.namelist() if n.startswith(\"xl\u002Fdrawings\u002Fdrawing\")):\n            xml = z.read(part).decode()\n            for tag in re.findall(r\"\u003Cxdr:cNvPr [^>]*>\", xml):\n                decorative = \"decorative\" in xml[xml.find(tag):xml.find(tag) + 600]\n                name = re.search(r'name=\"([^\"]*)\"', tag).group(1)\n                descr = re.search(r'descr=\"([^\"]*)\"', tag)\n                if decorative:\n                    continue\n                if not descr:\n                    issues.append(f\"{part}: '{name}' has no alt text\")\n                elif re.fullmatch(r\"[\\w .-]+\\.(png|jpe?g|gif|bmp|svg)\", descr.group(1), re.I):\n                    issues.append(f\"{part}: '{name}' alt text is just a file name\")\n    return issues\n\nfor path in (\"sales_typical.xlsx\", \"sales_accessible.xlsx\"):\n    found = audit(path)\n    print(path, \"->\", found or \"no issues found\")\n",[14,1923,1924,1931,1938,1951,1955,1965,1975,1985,1996,2006,2019,2044,2066,2073,2117,2131,2165,2175,2212,2235,2275,2307,2316,2322,2332,2364,2432,2460,2468,2473,2493,2504],{"__ignoreMap":35},[39,1925,1926,1928],{"class":41,"line":42},[39,1927,184],{"class":183},[39,1929,1930],{"class":187}," re\n",[39,1932,1933,1935],{"class":41,"line":191},[39,1934,184],{"class":183},[39,1936,1937],{"class":187}," zipfile\n",[39,1939,1940,1943,1946,1948],{"class":41,"line":198},[39,1941,1942],{"class":183},"from",[39,1944,1945],{"class":187}," openpyxl ",[39,1947,184],{"class":183},[39,1949,1950],{"class":187}," load_workbook\n",[39,1952,1953],{"class":41,"line":274},[39,1954,195],{"emptyLinePlaceholder":194},[39,1956,1957,1959,1962],{"class":41,"line":279},[39,1958,1425],{"class":183},[39,1960,1961],{"class":1428}," audit",[39,1963,1964],{"class":187},"(path):\n",[39,1966,1967,1970,1972],{"class":41,"line":297},[39,1968,1969],{"class":187},"    issues ",[39,1971,285],{"class":183},[39,1973,1974],{"class":187}," []\n",[39,1976,1977,1980,1982],{"class":41,"line":308},[39,1978,1979],{"class":187},"    wb ",[39,1981,285],{"class":183},[39,1983,1984],{"class":187}," load_workbook(path)\n",[39,1986,1987,1990,1993],{"class":41,"line":347},[39,1988,1989],{"class":183},"    if",[39,1991,1992],{"class":183}," not",[39,1994,1995],{"class":187}," wb.properties.title:\n",[39,1997,1998,2001,2004],{"class":41,"line":387},[39,1999,2000],{"class":187},"        issues.append(",[39,2002,2003],{"class":49},"\"no document title\"",[39,2005,294],{"class":187},[39,2007,2008,2011,2014,2016],{"class":41,"line":416},[39,2009,2010],{"class":183},"    for",[39,2012,2013],{"class":187}," ws ",[39,2015,462],{"class":183},[39,2017,2018],{"class":187}," wb.worksheets:\n",[39,2020,2021,2024,2027,2030,2033,2036,2039,2041],{"class":41,"line":453},[39,2022,2023],{"class":183},"        if",[39,2025,2026],{"class":187}," re.fullmatch(",[39,2028,2029],{"class":183},"r",[39,2031,2032],{"class":49},"\"Sheet",[39,2034,2035],{"class":201},"\\d",[39,2037,2038],{"class":183},"*",[39,2040,1761],{"class":49},[39,2042,2043],{"class":187},", ws.title):\n",[39,2045,2046,2049,2051,2054,2056,2059,2061,2064],{"class":41,"line":487},[39,2047,2048],{"class":187},"            issues.append(",[39,2050,1758],{"class":183},[39,2052,2053],{"class":49},"\"default sheet name '",[39,2055,1764],{"class":1363},[39,2057,2058],{"class":187},"ws.title",[39,2060,1773],{"class":1363},[39,2062,2063],{"class":49},"'\"",[39,2065,294],{"class":187},[39,2067,2068,2070],{"class":41,"line":498},[39,2069,2023],{"class":183},[39,2071,2072],{"class":187}," ws.merged_cells.ranges:\n",[39,2074,2075,2077,2079,2081,2083,2085,2087,2090,2092,2095,2098,2101,2103,2106,2108,2111,2113,2115],{"class":41,"line":531},[39,2076,2048],{"class":187},[39,2078,1758],{"class":183},[39,2080,1761],{"class":49},[39,2082,1764],{"class":1363},[39,2084,2058],{"class":187},[39,2086,1773],{"class":1363},[39,2088,2089],{"class":49},": merged cells ",[39,2091,1764],{"class":1363},[39,2093,2094],{"class":187},"[",[39,2096,2097],{"class":201},"str",[39,2099,2100],{"class":187},"(r) ",[39,2102,456],{"class":183},[39,2104,2105],{"class":187}," r ",[39,2107,462],{"class":183},[39,2109,2110],{"class":187}," ws.merged_cells.ranges]",[39,2112,1773],{"class":1363},[39,2114,1761],{"class":49},[39,2116,294],{"class":187},[39,2118,2119,2122,2125,2128],{"class":41,"line":552},[39,2120,2121],{"class":183},"    with",[39,2123,2124],{"class":187}," zipfile.ZipFile(path) ",[39,2126,2127],{"class":183},"as",[39,2129,2130],{"class":187}," z:\n",[39,2132,2133,2136,2139,2141,2144,2146,2149,2151,2154,2156,2159,2162],{"class":41,"line":578},[39,2134,2135],{"class":183},"        for",[39,2137,2138],{"class":187}," part ",[39,2140,462],{"class":183},[39,2142,2143],{"class":187}," (n ",[39,2145,456],{"class":183},[39,2147,2148],{"class":187}," n ",[39,2150,462],{"class":183},[39,2152,2153],{"class":187}," z.namelist() ",[39,2155,513],{"class":183},[39,2157,2158],{"class":187}," n.startswith(",[39,2160,2161],{"class":49},"\"xl\u002Fdrawings\u002Fdrawing\"",[39,2163,2164],{"class":187},")):\n",[39,2166,2167,2170,2172],{"class":41,"line":590},[39,2168,2169],{"class":187},"            xml ",[39,2171,285],{"class":183},[39,2173,2174],{"class":187}," z.read(part).decode()\n",[39,2176,2177,2180,2183,2185,2188,2190,2193,2195,2198,2201,2204,2206,2209],{"class":41,"line":606},[39,2178,2179],{"class":183},"            for",[39,2181,2182],{"class":187}," tag ",[39,2184,462],{"class":183},[39,2186,2187],{"class":187}," re.findall(",[39,2189,2029],{"class":183},[39,2191,2192],{"class":49},"\"\u003Cxdr:cNvPr ",[39,2194,2094],{"class":201},[39,2196,2197],{"class":183},"^",[39,2199,2200],{"class":1363},">",[39,2202,2203],{"class":201},"]",[39,2205,2038],{"class":183},[39,2207,2208],{"class":49},">\"",[39,2210,2211],{"class":187},", xml):\n",[39,2213,2214,2217,2219,2222,2225,2228,2230,2233],{"class":41,"line":612},[39,2215,2216],{"class":187},"                decorative ",[39,2218,285],{"class":183},[39,2220,2221],{"class":49}," \"decorative\"",[39,2223,2224],{"class":183}," in",[39,2226,2227],{"class":187}," xml[xml.find(tag):xml.find(tag) ",[39,2229,1457],{"class":183},[39,2231,2232],{"class":201}," 600",[39,2234,785],{"class":187},[39,2236,2238,2241,2243,2246,2248,2251,2254,2256,2258,2260,2262,2265,2268,2271,2273],{"class":41,"line":2237},20,[39,2239,2240],{"class":187},"                name ",[39,2242,285],{"class":183},[39,2244,2245],{"class":187}," re.search(",[39,2247,2029],{"class":183},[39,2249,2250],{"class":49},"'name=\"",[39,2252,2253],{"class":201},"([",[39,2255,2197],{"class":183},[39,2257,1761],{"class":1363},[39,2259,2203],{"class":201},[39,2261,2038],{"class":183},[39,2263,2264],{"class":201},")",[39,2266,2267],{"class":49},"\"'",[39,2269,2270],{"class":187},", tag).group(",[39,2272,1588],{"class":201},[39,2274,294],{"class":187},[39,2276,2278,2281,2283,2285,2287,2290,2292,2294,2296,2298,2300,2302,2304],{"class":41,"line":2277},21,[39,2279,2280],{"class":187},"                descr ",[39,2282,285],{"class":183},[39,2284,2245],{"class":187},[39,2286,2029],{"class":183},[39,2288,2289],{"class":49},"'descr=\"",[39,2291,2253],{"class":201},[39,2293,2197],{"class":183},[39,2295,1761],{"class":1363},[39,2297,2203],{"class":201},[39,2299,2038],{"class":183},[39,2301,2264],{"class":201},[39,2303,2267],{"class":49},[39,2305,2306],{"class":187},", tag)\n",[39,2308,2310,2313],{"class":41,"line":2309},22,[39,2311,2312],{"class":183},"                if",[39,2314,2315],{"class":187}," decorative:\n",[39,2317,2319],{"class":41,"line":2318},23,[39,2320,2321],{"class":183},"                    continue\n",[39,2323,2325,2327,2329],{"class":41,"line":2324},24,[39,2326,2312],{"class":183},[39,2328,1992],{"class":183},[39,2330,2331],{"class":187}," descr:\n",[39,2333,2335,2338,2340,2342,2344,2347,2349,2352,2354,2357,2359,2362],{"class":41,"line":2334},25,[39,2336,2337],{"class":187},"                    issues.append(",[39,2339,1758],{"class":183},[39,2341,1761],{"class":49},[39,2343,1764],{"class":1363},[39,2345,2346],{"class":187},"part",[39,2348,1773],{"class":1363},[39,2350,2351],{"class":49},": '",[39,2353,1764],{"class":1363},[39,2355,2356],{"class":187},"name",[39,2358,1773],{"class":1363},[39,2360,2361],{"class":49},"' has no alt text\"",[39,2363,294],{"class":187},[39,2365,2367,2370,2372,2374,2376,2379,2382,2384,2386,2390,2392,2395,2398,2401,2404,2406,2408,2411,2413,2416,2418,2420,2422,2424,2427,2429],{"class":41,"line":2366},26,[39,2368,2369],{"class":183},"                elif",[39,2371,2026],{"class":187},[39,2373,2029],{"class":183},[39,2375,1761],{"class":49},[39,2377,2378],{"class":201},"[\\w",[39,2380,2381],{"class":1363}," .-",[39,2383,2203],{"class":201},[39,2385,1457],{"class":183},[39,2387,2389],{"class":2388},"s_b0D","\\.",[39,2391,468],{"class":201},[39,2393,2394],{"class":49},"png",[39,2396,2397],{"class":183},"|",[39,2399,2400],{"class":49},"jpe",[39,2402,2403],{"class":183},"?",[39,2405,101],{"class":49},[39,2407,2397],{"class":183},[39,2409,2410],{"class":49},"gif",[39,2412,2397],{"class":183},[39,2414,2415],{"class":49},"bmp",[39,2417,2397],{"class":183},[39,2419,64],{"class":49},[39,2421,2264],{"class":201},[39,2423,1761],{"class":49},[39,2425,2426],{"class":187},", descr.group(",[39,2428,1588],{"class":201},[39,2430,2431],{"class":187},"), re.I):\n",[39,2433,2435,2437,2439,2441,2443,2445,2447,2449,2451,2453,2455,2458],{"class":41,"line":2434},27,[39,2436,2337],{"class":187},[39,2438,1758],{"class":183},[39,2440,1761],{"class":49},[39,2442,1764],{"class":1363},[39,2444,2346],{"class":187},[39,2446,1773],{"class":1363},[39,2448,2351],{"class":49},[39,2450,1764],{"class":1363},[39,2452,2356],{"class":187},[39,2454,1773],{"class":1363},[39,2456,2457],{"class":49},"' alt text is just a file name\"",[39,2459,294],{"class":187},[39,2461,2463,2465],{"class":41,"line":2462},28,[39,2464,1563],{"class":183},[39,2466,2467],{"class":187}," issues\n",[39,2469,2471],{"class":41,"line":2470},29,[39,2472,195],{"emptyLinePlaceholder":194},[39,2474,2476,2478,2481,2483,2485,2487,2489,2491],{"class":41,"line":2475},30,[39,2477,456],{"class":183},[39,2479,2480],{"class":187}," path ",[39,2482,462],{"class":183},[39,2484,840],{"class":187},[39,2486,291],{"class":49},[39,2488,214],{"class":187},[39,2490,656],{"class":49},[39,2492,484],{"class":187},[39,2494,2496,2499,2501],{"class":41,"line":2495},31,[39,2497,2498],{"class":187},"    found ",[39,2500,285],{"class":183},[39,2502,2503],{"class":187}," audit(path)\n",[39,2505,2507,2509,2512,2515,2518,2521,2524],{"class":41,"line":2506},32,[39,2508,1753],{"class":201},[39,2510,2511],{"class":187},"(path, ",[39,2513,2514],{"class":49},"\"->\"",[39,2516,2517],{"class":187},", found ",[39,2519,2520],{"class":183},"or",[39,2522,2523],{"class":49}," \"no issues found\"",[39,2525,294],{"class":187},[10,2527,2528,2529,2533],{},"The typical workbook fails on every count — including the logo, whose only alternative text is its file name; the accessible one passes. Alt text is read from the drawing XML because openpyxl does not expose it. Add the audit to the report's tests — ",[19,2530,2532],{"href":2531},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest"," — so a later change cannot quietly reintroduce a merged title or an undescribed chart.",[25,2535,2537],{"id":2536},"common-pitfalls","Common pitfalls",[2539,2540,2541,2549,2555,2561,2570],"ul",{},[2542,2543,2544,2548],"li",{},[2545,2546,2547],"strong",{},"Merged cells for titles and headers."," They confuse navigation; write the title in one cell and use tables for headers.",[2542,2550,2551,2554],{},[2545,2552,2553],{},"Colour as the only signal."," Always carry the meaning in text too.",[2542,2556,2557,2560],{},[2545,2558,2559],{},"Alt text that describes the chart type."," \"Column chart\" helps nobody; state what it shows.",[2542,2562,2563,2566,2567,2569],{},[2545,2564,2565],{},"Default sheet names."," ",[14,2568,16],{}," says nothing; name sheets for their content.",[2542,2571,2572,2575],{},[2545,2573,2574],{},"Blank rows and columns inside data."," Screen readers treat them as the end of the table; keep data contiguous.",[25,2577,2579],{"id":2578},"conclusion","Conclusion",[10,2581,2582,2583,2585],{},"Name sheets for their content, set a document title, write the title in ",[14,2584,639],{}," without merging, and put data in Excel tables so header rows are real headers. Carry status in words with colour as a second signal, give charts alternative text that states the finding and mark decorative images as decorative, check every text colour pair for a contrast ratio of at least 4.5:1, and run an audit over the output so the report stays accessible as it evolves.",[25,2587,2589],{"id":2588},"frequently-asked-questions","Frequently asked questions",[10,2591,2592,2595],{},[2545,2593,2594],{},"How do I add alt text to a chart with Python?","\nWith xlsxwriter, pass {\"description\": \"…\"} as the options of worksheet.insert_chart or insert_image. The text is stored with the drawing and read by screen readers.",[10,2597,2598,2601],{},[2545,2599,2600],{},"Why are merged cells bad for accessibility?","\nScreen readers and keyboard navigation move cell by cell, and merged ranges make positions and headers ambiguous. Use a single cell for titles and centre text across columns with alignment instead if needed.",[10,2603,2604,2607],{},[2545,2605,2606],{},"How do I check colour contrast in an Excel report?","\nCompute the WCAG contrast ratio from the relative luminance of the text and background colours; normal text should reach at least 4.5:1. The function on this page does it for any pair of hex colours.",[10,2609,2610,2613],{},[2545,2611,2612],{},"Does Excel have an accessibility checker?","\nYes, Review → Check Accessibility in desktop Excel. Building the same checks into the report script means every generated copy passes without anyone running the checker by hand.",[25,2615,2617],{"id":2616},"related","Related",[2539,2619,2620,2626,2633,2640],{},[2542,2621,2622,2623,2625],{},"Up one level: ",[19,2624,22],{"href":21}," — preparing workbooks for other people.",[2542,2627,2628,2632],{},[19,2629,2631],{"href":2630},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fset-workbook-properties-with-python\u002F","Set Workbook Properties with Python"," — title, subject and other document properties.",[2542,2634,2635,2639],{},[19,2636,2638],{"href":2637},"\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"," — and the alternatives to merging.",[2542,2641,2642,2646],{},[19,2643,2645],{"href":2644},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fstyle-chart-colors-and-data-labels-with-openpyxl\u002F","Style Chart Colours and Data Labels with openpyxl"," — choosing chart colours with contrast in mind.",[2648,2649,2650],"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_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .s_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}",{"title":35,"searchDepth":191,"depth":191,"links":2652},[2653,2654,2655,2656,2657,2658,2659,2660,2661,2662,2663],{"id":27,"depth":191,"text":28},{"id":167,"depth":191,"text":168},{"id":632,"depth":191,"text":633},{"id":942,"depth":191,"text":943},{"id":1231,"depth":191,"text":1232},{"id":1411,"depth":191,"text":1412},{"id":1910,"depth":191,"text":1911},{"id":2536,"depth":191,"text":2537},{"id":2578,"depth":191,"text":2579},{"id":2588,"depth":191,"text":2589},{"id":2616,"depth":191,"text":2617},"2026-09-29","Generate workbooks that screen-reader users and people with low vision can use: meaningful sheet names and a title, data in real tables with header rows, alternative text on charts and images, no merged cells, meaning that does not rely on colour alone, colours that pass contrast checks, and an automated audit that flags the problems in any workbook.","md",[2668,2670,2672,2674],{"q":2594,"a":2669},"With xlsxwriter, pass {\"description\": \"…\"} as the options of worksheet.insert_chart or insert_image. The text is stored with the drawing and read by screen readers.",{"q":2600,"a":2671},"Screen readers and keyboard navigation move cell by cell, and merged ranges make positions and headers ambiguous. Use a single cell for titles and centre text across columns with alignment instead if needed.",{"q":2606,"a":2673},"Compute the WCAG contrast ratio from the relative luminance of the text and background colours; normal text should reach at least 4.5:1. The function on this page does it for any pair of hex colours.",{"q":2612,"a":2675},"Yes, Review → Check Accessibility in desktop Excel. Building the same checks into the report script means every generated copy passes without anyone running the checker by hand.","make an excel report accessible",null,{"breadcrumb":2679},[2680,2682,2685],{"name":2681,"item":1472},"Home",{"name":2683,"item":2684},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002F",{"name":22,"item":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fmake-an-excel-report-accessible",{"title":2688,"description":2689},"Accessible Excel Reports with Python — Alt Text, Tables, Contrast","Make Excel reports accessible from Python: descriptive sheet names, document title, xlsxwriter tables with headers, alt text (description) for charts and images, no merged cells, text alongside colour, WCAG contrast ratios for fills, and an openpyxl audit script.","make-an-excel-report-accessible","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fmake-an-excel-report-accessible\u002Findex",[2693,2694],"protection-and-sharing","report-layouts","how-to","oZW4tzIzb9PD8uq2zSG07w_eJqYjP9rdVYIkB67cXAI",[2698,2702],{"title":2699,"path":2700,"stem":2701,"children":-1},"Make an Excel File Open Read-Only Recommended","\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fmake-an-excel-file-open-read-only-recommended","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fmake-an-excel-file-open-read-only-recommended\u002Findex",{"title":2703,"path":2704,"stem":2705,"children":-1},"Open a Password-Protected Excel File with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fopen-a-password-protected-excel-file-with-python","formatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fopen-a-password-protected-excel-file-with-python\u002Findex",[2707,2709],{"path":2708,"title":2683},"\u002Fformatting-and-charting-excel-reports-with-python",{"path":2710,"title":22},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks",[],[2713,2716,2719,2722,2725,2728],{"path":2714,"title":2715,"kind":2695},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fanonymise-personal-data-in-an-excel-file-with-python","Anonymise Personal Data in an Excel File with Python",{"path":2717,"title":2718,"kind":2695},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fpassword-protect-a-pdf-exported-from-excel","Password-Protect a PDF Exported from Excel",{"path":2720,"title":2721,"kind":2695},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fsign-a-pdf-report-digitally-with-python","Sign a PDF Report Digitally with Python",{"path":2723,"title":2724,"kind":2695},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fdefine-names-and-protect-sheets-with-xlsxwriter","Define Names and Protect Sheets with XlsxWriter",{"path":2726,"title":2727,"kind":2695},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fadd-comments-and-notes-to-excel-cells-with-python","Add Comments and Notes to Excel Cells with Python",{"path":2729,"title":2730,"kind":2695},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fhide-sheets-rows-and-columns-with-openpyxl","Hide Sheets, Rows and Columns with openpyxl",1790972116962]