[{"data":1,"prerenderedAt":3147},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-that-excel-charts-were-written-correctly":3,"surround:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-that-excel-charts-were-written-correctly":3113,"ancestors:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-that-excel-charts-were-written-correctly":3121,"children:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-that-excel-charts-were-written-correctly":3126,"related:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-that-excel-charts-were-written-correctly":3127},{"id":4,"title":5,"body":6,"dateModified":3080,"datePublished":3080,"description":3081,"extension":3082,"faq":3083,"keyword":3092,"kind":3093,"meta":3094,"navigation":112,"path":3102,"seo":3103,"slug":3106,"stem":3107,"topics":3108,"type":3111,"__hash__":3112},"docs\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-that-excel-charts-were-written-correctly\u002Findex.md","Test That Excel Charts Were Written Correctly",{"type":7,"value":8,"toc":3069},"minimark",[9,28,33,61,67,71,622,625,629,1943,1954,2092,2096,2503,2536,2548,2710,2714,2871,2881,2951,2955,2990,2994,3000,3004,3010,3020,3026,3032,3036,3065],[10,11,12,13,17,18,21,22,27],"p",{},"Tests for a report script usually check the numbers: the right rows, the right totals, the right formats. The charts get looked at once, when the script is first written, and never again — until someone changes the column order and the revenue chart quietly starts plotting costs, or a refactor drops a line and the axes vanish. Charts deserve tests too, and the obstacle is practical: openpyxl can write charts but does not read them back, so ",[14,15,16],"code",{},"load_workbook"," gives no way to inspect what a saved chart contains. The chart definitions are still in the file, though, as XML parts inside the ",[14,19,20],{},".xlsx"," package, and they are easy to read with the standard library. This page builds a small helper that extracts each chart's essentials — type, title, series and their ranges, axis settings, the sheet and cell it is anchored to — and uses it in pytest, both with explicit assertions and with a stored snapshot. It belongs to ",[23,24,26],"a",{"href":25},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002F","Testing and Packaging Excel Automation Scripts",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,40],"pre",{"className":36,"code":37,"language":38,"meta":39,"style":39},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pytest\n","bash","",[14,41,42],{"__ignoreMap":39},[43,44,47,51,55,58],"span",{"class":45,"line":46},"line",1,[43,48,50],{"class":49},"sMTad","pip",[43,52,54],{"class":53},"srMev"," install",[43,56,57],{"class":53}," openpyxl",[43,59,60],{"class":53}," pytest\n",[10,62,63,64,66],{},"The code under test is a small report function that writes a sheet of monthly figures and two charts. The helper reads any ",[14,65,20],{},", whichever library wrote it.",[29,68,70],{"id":69},"step-1-the-report-being-tested","Step 1 — The report being tested",[34,72,76],{"className":73,"code":74,"language":75,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\nfrom openpyxl.chart import BarChart, LineChart, Reference\n\ndef build_report(path):\n    wb = Workbook()\n    ws = wb.active\n    ws.title = \"Monthly\"\n    ws.append([\"Month\", \"Revenue\", \"Cost\", \"Margin %\"])\n    for i, (rev, cost) in enumerate([(120, 80), (135, 88), (128, 90), (150, 95)], start=2):\n        ws.append([f\"2026-0{i - 1}\", rev, cost, f\"=(B{i}-C{i})\u002FB{i}\"])\n\n    bars = BarChart()\n    bars.type = \"col\"\n    bars.title = \"Revenue and cost\"\n    bars.add_data(Reference(ws, min_col=2, max_col=3, min_row=1, max_row=5), titles_from_data=True)\n    bars.set_categories(Reference(ws, min_col=1, min_row=2, max_row=5))\n    bars.x_axis.delete = bars.y_axis.delete = False\n    ws.add_chart(bars, \"F2\")\n\n    line = LineChart()\n    line.title = \"Margin\"\n    line.add_data(Reference(ws, min_col=4, min_row=1, max_row=5), titles_from_data=True)\n    line.set_categories(Reference(ws, min_col=1, min_row=2, max_row=5))\n    line.y_axis.numFmt = \"0%\"\n    line.x_axis.delete = line.y_axis.delete = False\n    ws.add_chart(line, \"F20\")\n    wb.save(path)\n","python",[14,77,78,94,107,114,127,139,150,161,189,262,330,335,346,357,368,425,456,472,483,488,499,510,549,579,590,605,616],{"__ignoreMap":39},[43,79,80,84,88,91],{"class":45,"line":46},[43,81,83],{"class":82},"s-kum","from",[43,85,87],{"class":86},"skGVy"," openpyxl ",[43,89,90],{"class":82},"import",[43,92,93],{"class":86}," Workbook\n",[43,95,97,99,102,104],{"class":45,"line":96},2,[43,98,83],{"class":82},[43,100,101],{"class":86}," openpyxl.chart ",[43,103,90],{"class":82},[43,105,106],{"class":86}," BarChart, LineChart, Reference\n",[43,108,110],{"class":45,"line":109},3,[43,111,113],{"emptyLinePlaceholder":112},true,"\n",[43,115,117,120,124],{"class":45,"line":116},4,[43,118,119],{"class":82},"def",[43,121,123],{"class":122},"s_Opv"," build_report",[43,125,126],{"class":86},"(path):\n",[43,128,130,133,136],{"class":45,"line":129},5,[43,131,132],{"class":86},"    wb ",[43,134,135],{"class":82},"=",[43,137,138],{"class":86}," Workbook()\n",[43,140,142,145,147],{"class":45,"line":141},6,[43,143,144],{"class":86},"    ws ",[43,146,135],{"class":82},[43,148,149],{"class":86}," wb.active\n",[43,151,153,156,158],{"class":45,"line":152},7,[43,154,155],{"class":86},"    ws.title ",[43,157,135],{"class":82},[43,159,160],{"class":53}," \"Monthly\"\n",[43,162,164,167,170,173,176,178,181,183,186],{"class":45,"line":163},8,[43,165,166],{"class":86},"    ws.append([",[43,168,169],{"class":53},"\"Month\"",[43,171,172],{"class":86},", ",[43,174,175],{"class":53},"\"Revenue\"",[43,177,172],{"class":86},[43,179,180],{"class":53},"\"Cost\"",[43,182,172],{"class":86},[43,184,185],{"class":53},"\"Margin %\"",[43,187,188],{"class":86},"])\n",[43,190,192,195,198,201,205,208,211,213,216,219,222,224,227,229,232,234,237,239,242,244,247,250,254,256,259],{"class":45,"line":191},9,[43,193,194],{"class":82},"    for",[43,196,197],{"class":86}," i, (rev, cost) ",[43,199,200],{"class":82},"in",[43,202,204],{"class":203},"sP0c6"," enumerate",[43,206,207],{"class":86},"([(",[43,209,210],{"class":203},"120",[43,212,172],{"class":86},[43,214,215],{"class":203},"80",[43,217,218],{"class":86},"), (",[43,220,221],{"class":203},"135",[43,223,172],{"class":86},[43,225,226],{"class":203},"88",[43,228,218],{"class":86},[43,230,231],{"class":203},"128",[43,233,172],{"class":86},[43,235,236],{"class":203},"90",[43,238,218],{"class":86},[43,240,241],{"class":203},"150",[43,243,172],{"class":86},[43,245,246],{"class":203},"95",[43,248,249],{"class":86},")], ",[43,251,253],{"class":252},"sa561","start",[43,255,135],{"class":82},[43,257,258],{"class":203},"2",[43,260,261],{"class":86},"):\n",[43,263,265,268,271,274,278,281,284,287,290,293,296,298,301,303,306,308,311,313,315,317,320,322,324,326,328],{"class":45,"line":264},10,[43,266,267],{"class":86},"        ws.append([",[43,269,270],{"class":82},"f",[43,272,273],{"class":53},"\"2026-0",[43,275,277],{"class":276},"sSjpA","{",[43,279,280],{"class":86},"i ",[43,282,283],{"class":82},"-",[43,285,286],{"class":203}," 1",[43,288,289],{"class":276},"}",[43,291,292],{"class":53},"\"",[43,294,295],{"class":86},", rev, cost, ",[43,297,270],{"class":82},[43,299,300],{"class":53},"\"=(B",[43,302,277],{"class":276},[43,304,305],{"class":86},"i",[43,307,289],{"class":276},[43,309,310],{"class":53},"-C",[43,312,277],{"class":276},[43,314,305],{"class":86},[43,316,289],{"class":276},[43,318,319],{"class":53},")\u002FB",[43,321,277],{"class":276},[43,323,305],{"class":86},[43,325,289],{"class":276},[43,327,292],{"class":53},[43,329,188],{"class":86},[43,331,333],{"class":45,"line":332},11,[43,334,113],{"emptyLinePlaceholder":112},[43,336,338,341,343],{"class":45,"line":337},12,[43,339,340],{"class":86},"    bars ",[43,342,135],{"class":82},[43,344,345],{"class":86}," BarChart()\n",[43,347,349,352,354],{"class":45,"line":348},13,[43,350,351],{"class":86},"    bars.type ",[43,353,135],{"class":82},[43,355,356],{"class":53}," \"col\"\n",[43,358,360,363,365],{"class":45,"line":359},14,[43,361,362],{"class":86},"    bars.title ",[43,364,135],{"class":82},[43,366,367],{"class":53}," \"Revenue and cost\"\n",[43,369,371,374,377,379,381,383,386,388,391,393,396,398,401,403,406,408,411,414,417,419,422],{"class":45,"line":370},15,[43,372,373],{"class":86},"    bars.add_data(Reference(ws, ",[43,375,376],{"class":252},"min_col",[43,378,135],{"class":82},[43,380,258],{"class":203},[43,382,172],{"class":86},[43,384,385],{"class":252},"max_col",[43,387,135],{"class":82},[43,389,390],{"class":203},"3",[43,392,172],{"class":86},[43,394,395],{"class":252},"min_row",[43,397,135],{"class":82},[43,399,400],{"class":203},"1",[43,402,172],{"class":86},[43,404,405],{"class":252},"max_row",[43,407,135],{"class":82},[43,409,410],{"class":203},"5",[43,412,413],{"class":86},"), ",[43,415,416],{"class":252},"titles_from_data",[43,418,135],{"class":82},[43,420,421],{"class":203},"True",[43,423,424],{"class":86},")\n",[43,426,428,431,433,435,437,439,441,443,445,447,449,451,453],{"class":45,"line":427},16,[43,429,430],{"class":86},"    bars.set_categories(Reference(ws, ",[43,432,376],{"class":252},[43,434,135],{"class":82},[43,436,400],{"class":203},[43,438,172],{"class":86},[43,440,395],{"class":252},[43,442,135],{"class":82},[43,444,258],{"class":203},[43,446,172],{"class":86},[43,448,405],{"class":252},[43,450,135],{"class":82},[43,452,410],{"class":203},[43,454,455],{"class":86},"))\n",[43,457,459,462,464,467,469],{"class":45,"line":458},17,[43,460,461],{"class":86},"    bars.x_axis.delete ",[43,463,135],{"class":82},[43,465,466],{"class":86}," bars.y_axis.delete ",[43,468,135],{"class":82},[43,470,471],{"class":203}," False\n",[43,473,475,478,481],{"class":45,"line":474},18,[43,476,477],{"class":86},"    ws.add_chart(bars, ",[43,479,480],{"class":53},"\"F2\"",[43,482,424],{"class":86},[43,484,486],{"class":45,"line":485},19,[43,487,113],{"emptyLinePlaceholder":112},[43,489,491,494,496],{"class":45,"line":490},20,[43,492,493],{"class":86},"    line ",[43,495,135],{"class":82},[43,497,498],{"class":86}," LineChart()\n",[43,500,502,505,507],{"class":45,"line":501},21,[43,503,504],{"class":86},"    line.title ",[43,506,135],{"class":82},[43,508,509],{"class":53}," \"Margin\"\n",[43,511,513,516,518,520,523,525,527,529,531,533,535,537,539,541,543,545,547],{"class":45,"line":512},22,[43,514,515],{"class":86},"    line.add_data(Reference(ws, ",[43,517,376],{"class":252},[43,519,135],{"class":82},[43,521,522],{"class":203},"4",[43,524,172],{"class":86},[43,526,395],{"class":252},[43,528,135],{"class":82},[43,530,400],{"class":203},[43,532,172],{"class":86},[43,534,405],{"class":252},[43,536,135],{"class":82},[43,538,410],{"class":203},[43,540,413],{"class":86},[43,542,416],{"class":252},[43,544,135],{"class":82},[43,546,421],{"class":203},[43,548,424],{"class":86},[43,550,552,555,557,559,561,563,565,567,569,571,573,575,577],{"class":45,"line":551},23,[43,553,554],{"class":86},"    line.set_categories(Reference(ws, ",[43,556,376],{"class":252},[43,558,135],{"class":82},[43,560,400],{"class":203},[43,562,172],{"class":86},[43,564,395],{"class":252},[43,566,135],{"class":82},[43,568,258],{"class":203},[43,570,172],{"class":86},[43,572,405],{"class":252},[43,574,135],{"class":82},[43,576,410],{"class":203},[43,578,455],{"class":86},[43,580,582,585,587],{"class":45,"line":581},24,[43,583,584],{"class":86},"    line.y_axis.numFmt ",[43,586,135],{"class":82},[43,588,589],{"class":53}," \"0%\"\n",[43,591,593,596,598,601,603],{"class":45,"line":592},25,[43,594,595],{"class":86},"    line.x_axis.delete ",[43,597,135],{"class":82},[43,599,600],{"class":86}," line.y_axis.delete ",[43,602,135],{"class":82},[43,604,471],{"class":203},[43,606,608,611,614],{"class":45,"line":607},26,[43,609,610],{"class":86},"    ws.add_chart(line, ",[43,612,613],{"class":53},"\"F20\"",[43,615,424],{"class":86},[43,617,619],{"class":45,"line":618},27,[43,620,621],{"class":86},"    wb.save(path)\n",[10,623,624],{},"The report writes four months of revenue and cost, a margin formula, a column chart of revenue and cost, and a line chart of the margin. It is ordinary openpyxl code; nothing about it is special for testing.",[29,626,628],{"id":627},"step-2-read-the-charts-back-from-the-package","Step 2 — Read the charts back from the package",[34,630,632],{"className":73,"code":631,"language":75,"meta":39,"style":39},"import posixpath\nimport re\nimport zipfile\nimport xml.etree.ElementTree as ET\n\nC = \"{http:\u002F\u002Fschemas.openxmlformats.org\u002Fdrawingml\u002F2006\u002Fchart}\"\nA = \"{http:\u002F\u002Fschemas.openxmlformats.org\u002Fdrawingml\u002F2006\u002Fmain}\"\nXDR = \"{http:\u002F\u002Fschemas.openxmlformats.org\u002Fdrawingml\u002F2006\u002FspreadsheetDrawing}\"\nR = \"{http:\u002F\u002Fschemas.openxmlformats.org\u002FofficeDocument\u002F2006\u002Frelationships}\"\nPR = \"{http:\u002F\u002Fschemas.openxmlformats.org\u002Fpackage\u002F2006\u002Frelationships}\"\nM = \"{http:\u002F\u002Fschemas.openxmlformats.org\u002Fspreadsheetml\u002F2006\u002Fmain}\"\n\ndef _rels(z, part):\n    folder, name = posixpath.split(part)\n    rels_path = posixpath.join(folder, \"_rels\", name + \".rels\")\n    if rels_path not in z.namelist():\n        return {}\n    root = ET.fromstring(z.read(rels_path))\n    # targets may be relative (\"..\u002Fcharts\u002Fchart1.xml\") or absolute (\"\u002Fxl\u002Fworksheets\u002Fsheet1.xml\")\n    return {r.get(\"Id\"): posixpath.normpath(posixpath.join(folder, r.get(\"Target\"))).lstrip(\"\u002F\") for r in root.iter(PR + \"Relationship\")}\n\ndef _text(el):\n    return \"\".join(t.text or \"\" for t in el.iter(A + \"t\")) if el is not None else None\n\ndef read_charts(path):\n    \"\"\"Return one dict per chart: sheet, anchor cell, type, title, series and axes.\"\"\"\n    charts = []\n    with zipfile.ZipFile(path) as z:\n        book = ET.fromstring(z.read(\"xl\u002Fworkbook.xml\"))\n        book_rels = _rels(z, \"xl\u002Fworkbook.xml\")\n        for sheet in book.iter(M + \"sheet\"):\n            sheet_part = book_rels[sheet.get(R + \"id\")]\n            for drawing in _rels(z, sheet_part).values():\n                if \"\u002Fdrawings\u002F\" not in drawing:\n                    continue\n                d_rels = _rels(z, drawing)\n                for anchor in ET.fromstring(z.read(drawing)):\n                    frm = anchor.find(XDR + \"from\")\n                    chart_ref = anchor.find(f\".\u002F\u002F{C}chart\")\n                    if chart_ref is None:\n                        continue\n                    col, row = int(frm.find(XDR + \"col\").text), int(frm.find(XDR + \"row\").text)\n                    chart_xml = ET.fromstring(z.read(d_rels[chart_ref.get(R + \"id\")]))\n                    plot = chart_xml.find(f\"{C}chart\u002F{C}plotArea\")\n                    kind = next(el.tag.replace(C, \"\") for el in plot if el.tag.endswith(\"Chart\"))\n                    series = [{\"name\": _formula(s.find(f\"{C}tx\")), \"values\": _formula(s.find(f\"{C}val\")),\n                               \"categories\": _formula(s.find(f\"{C}cat\"))} for s in plot.iter(C + \"ser\")]\n                    axes = {ax.tag.replace(C, \"\"): {\"deleted\": ax.find(f\"{C}delete\") is not None and ax.find(f\"{C}delete\").get(\"val\") == \"1\",\n                                                    \"format\": ax.find(f\"{C}numFmt\").get(\"formatCode\") if ax.find(f\"{C}numFmt\") is not None else None}\n                            for ax in plot if ax.tag in (C + \"catAx\", C + \"valAx\", C + \"dateAx\")}\n                    charts.append({\"sheet\": sheet.get(\"name\"), \"anchor\": _cell(col, row), \"type\": kind,\n                                   \"title\": _text(chart_xml.find(f\"{C}chart\u002F{C}title\")), \"series\": series, \"axes\": axes})\n    return charts\n\ndef _formula(el):\n    if el is None:\n        return None\n    f = el.find(f\".\u002F\u002F{C}f\")\n    return f.text if f is not None else None\n\ndef _cell(col, row):\n    letters = \"\"\n    col += 1\n    while col:\n        col, rem = divmod(col - 1, 26)\n        letters = chr(65 + rem) + letters\n    return f\"{letters}{row + 1}\"\n",[14,633,634,641,648,655,668,672,694,712,731,749,767,785,789,799,809,833,850,858,871,877,925,929,939,994,998,1007,1012,1022,1036,1054,1069,1090,1109,1123,1139,1145,1156,1172,1192,1219,1235,1241,1280,1300,1335,1373,1427,1468,1546,1605,1650,1678,1722,1730,1735,1745,1758,1765,1791,1814,1819,1830,1841,1853,1862,1887,1914],{"__ignoreMap":39},[43,635,636,638],{"class":45,"line":46},[43,637,90],{"class":82},[43,639,640],{"class":86}," posixpath\n",[43,642,643,645],{"class":45,"line":96},[43,644,90],{"class":82},[43,646,647],{"class":86}," re\n",[43,649,650,652],{"class":45,"line":109},[43,651,90],{"class":82},[43,653,654],{"class":86}," zipfile\n",[43,656,657,659,662,665],{"class":45,"line":116},[43,658,90],{"class":82},[43,660,661],{"class":86}," xml.etree.ElementTree ",[43,663,664],{"class":82},"as",[43,666,667],{"class":203}," ET\n",[43,669,670],{"class":45,"line":129},[43,671,113],{"emptyLinePlaceholder":112},[43,673,674,677,679,682,685,688,691],{"class":45,"line":141},[43,675,676],{"class":86},"C ",[43,678,135],{"class":82},[43,680,681],{"class":53}," \"",[43,683,684],{"class":276},"{http",[43,686,687],{"class":82},":",[43,689,690],{"class":276},"\u002F\u002Fschemas.openxmlformats.org\u002Fdrawingml\u002F2006\u002Fchart}",[43,692,693],{"class":53},"\"\n",[43,695,696,699,701,703,705,707,710],{"class":45,"line":152},[43,697,698],{"class":86},"A ",[43,700,135],{"class":82},[43,702,681],{"class":53},[43,704,684],{"class":276},[43,706,687],{"class":82},[43,708,709],{"class":276},"\u002F\u002Fschemas.openxmlformats.org\u002Fdrawingml\u002F2006\u002Fmain}",[43,711,693],{"class":53},[43,713,714,717,720,722,724,726,729],{"class":45,"line":163},[43,715,716],{"class":203},"XDR",[43,718,719],{"class":82}," =",[43,721,681],{"class":53},[43,723,684],{"class":276},[43,725,687],{"class":82},[43,727,728],{"class":276},"\u002F\u002Fschemas.openxmlformats.org\u002Fdrawingml\u002F2006\u002FspreadsheetDrawing}",[43,730,693],{"class":53},[43,732,733,736,738,740,742,744,747],{"class":45,"line":191},[43,734,735],{"class":86},"R ",[43,737,135],{"class":82},[43,739,681],{"class":53},[43,741,684],{"class":276},[43,743,687],{"class":82},[43,745,746],{"class":276},"\u002F\u002Fschemas.openxmlformats.org\u002FofficeDocument\u002F2006\u002Frelationships}",[43,748,693],{"class":53},[43,750,751,754,756,758,760,762,765],{"class":45,"line":264},[43,752,753],{"class":203},"PR",[43,755,719],{"class":82},[43,757,681],{"class":53},[43,759,684],{"class":276},[43,761,687],{"class":82},[43,763,764],{"class":276},"\u002F\u002Fschemas.openxmlformats.org\u002Fpackage\u002F2006\u002Frelationships}",[43,766,693],{"class":53},[43,768,769,772,774,776,778,780,783],{"class":45,"line":332},[43,770,771],{"class":86},"M ",[43,773,135],{"class":82},[43,775,681],{"class":53},[43,777,684],{"class":276},[43,779,687],{"class":82},[43,781,782],{"class":276},"\u002F\u002Fschemas.openxmlformats.org\u002Fspreadsheetml\u002F2006\u002Fmain}",[43,784,693],{"class":53},[43,786,787],{"class":45,"line":337},[43,788,113],{"emptyLinePlaceholder":112},[43,790,791,793,796],{"class":45,"line":348},[43,792,119],{"class":82},[43,794,795],{"class":122}," _rels",[43,797,798],{"class":86},"(z, part):\n",[43,800,801,804,806],{"class":45,"line":359},[43,802,803],{"class":86},"    folder, name ",[43,805,135],{"class":82},[43,807,808],{"class":86}," posixpath.split(part)\n",[43,810,811,814,816,819,822,825,828,831],{"class":45,"line":370},[43,812,813],{"class":86},"    rels_path ",[43,815,135],{"class":82},[43,817,818],{"class":86}," posixpath.join(folder, ",[43,820,821],{"class":53},"\"_rels\"",[43,823,824],{"class":86},", name ",[43,826,827],{"class":82},"+",[43,829,830],{"class":53}," \".rels\"",[43,832,424],{"class":86},[43,834,835,838,841,844,847],{"class":45,"line":427},[43,836,837],{"class":82},"    if",[43,839,840],{"class":86}," rels_path ",[43,842,843],{"class":82},"not",[43,845,846],{"class":82}," in",[43,848,849],{"class":86}," z.namelist():\n",[43,851,852,855],{"class":45,"line":458},[43,853,854],{"class":82},"        return",[43,856,857],{"class":86}," {}\n",[43,859,860,863,865,868],{"class":45,"line":474},[43,861,862],{"class":86},"    root ",[43,864,135],{"class":82},[43,866,867],{"class":203}," ET",[43,869,870],{"class":86},".fromstring(z.read(rels_path))\n",[43,872,873],{"class":45,"line":485},[43,874,876],{"class":875},"s-wDw","    # targets may be relative (\"..\u002Fcharts\u002Fchart1.xml\") or absolute (\"\u002Fxl\u002Fworksheets\u002Fsheet1.xml\")\n",[43,878,879,882,885,888,891,894,897,900,903,906,909,911,914,916,919,922],{"class":45,"line":490},[43,880,881],{"class":82},"    return",[43,883,884],{"class":86}," {r.get(",[43,886,887],{"class":53},"\"Id\"",[43,889,890],{"class":86},"): posixpath.normpath(posixpath.join(folder, r.get(",[43,892,893],{"class":53},"\"Target\"",[43,895,896],{"class":86},"))).lstrip(",[43,898,899],{"class":53},"\"\u002F\"",[43,901,902],{"class":86},") ",[43,904,905],{"class":82},"for",[43,907,908],{"class":86}," r ",[43,910,200],{"class":82},[43,912,913],{"class":86}," root.iter(",[43,915,753],{"class":203},[43,917,918],{"class":82}," +",[43,920,921],{"class":53}," \"Relationship\"",[43,923,924],{"class":86},")}\n",[43,926,927],{"class":45,"line":501},[43,928,113],{"emptyLinePlaceholder":112},[43,930,931,933,936],{"class":45,"line":512},[43,932,119],{"class":82},[43,934,935],{"class":122}," _text",[43,937,938],{"class":86},"(el):\n",[43,940,941,943,946,949,952,954,957,960,962,965,967,970,973,976,979,982,985,988,991],{"class":45,"line":551},[43,942,881],{"class":82},[43,944,945],{"class":53}," \"\"",[43,947,948],{"class":86},".join(t.text ",[43,950,951],{"class":82},"or",[43,953,945],{"class":53},[43,955,956],{"class":82}," for",[43,958,959],{"class":86}," t ",[43,961,200],{"class":82},[43,963,964],{"class":86}," el.iter(A ",[43,966,827],{"class":82},[43,968,969],{"class":53}," \"t\"",[43,971,972],{"class":86},")) ",[43,974,975],{"class":82},"if",[43,977,978],{"class":86}," el ",[43,980,981],{"class":82},"is",[43,983,984],{"class":82}," not",[43,986,987],{"class":203}," None",[43,989,990],{"class":82}," else",[43,992,993],{"class":203}," None\n",[43,995,996],{"class":45,"line":581},[43,997,113],{"emptyLinePlaceholder":112},[43,999,1000,1002,1005],{"class":45,"line":592},[43,1001,119],{"class":82},[43,1003,1004],{"class":122}," read_charts",[43,1006,126],{"class":86},[43,1008,1009],{"class":45,"line":607},[43,1010,1011],{"class":53},"    \"\"\"Return one dict per chart: sheet, anchor cell, type, title, series and axes.\"\"\"\n",[43,1013,1014,1017,1019],{"class":45,"line":618},[43,1015,1016],{"class":86},"    charts ",[43,1018,135],{"class":82},[43,1020,1021],{"class":86}," []\n",[43,1023,1025,1028,1031,1033],{"class":45,"line":1024},28,[43,1026,1027],{"class":82},"    with",[43,1029,1030],{"class":86}," zipfile.ZipFile(path) ",[43,1032,664],{"class":82},[43,1034,1035],{"class":86}," z:\n",[43,1037,1039,1042,1044,1046,1049,1052],{"class":45,"line":1038},29,[43,1040,1041],{"class":86},"        book ",[43,1043,135],{"class":82},[43,1045,867],{"class":203},[43,1047,1048],{"class":86},".fromstring(z.read(",[43,1050,1051],{"class":53},"\"xl\u002Fworkbook.xml\"",[43,1053,455],{"class":86},[43,1055,1057,1060,1062,1065,1067],{"class":45,"line":1056},30,[43,1058,1059],{"class":86},"        book_rels ",[43,1061,135],{"class":82},[43,1063,1064],{"class":86}," _rels(z, ",[43,1066,1051],{"class":53},[43,1068,424],{"class":86},[43,1070,1072,1075,1078,1080,1083,1085,1088],{"class":45,"line":1071},31,[43,1073,1074],{"class":82},"        for",[43,1076,1077],{"class":86}," sheet ",[43,1079,200],{"class":82},[43,1081,1082],{"class":86}," book.iter(M ",[43,1084,827],{"class":82},[43,1086,1087],{"class":53}," \"sheet\"",[43,1089,261],{"class":86},[43,1091,1093,1096,1098,1101,1103,1106],{"class":45,"line":1092},32,[43,1094,1095],{"class":86},"            sheet_part ",[43,1097,135],{"class":82},[43,1099,1100],{"class":86}," book_rels[sheet.get(R ",[43,1102,827],{"class":82},[43,1104,1105],{"class":53}," \"id\"",[43,1107,1108],{"class":86},")]\n",[43,1110,1112,1115,1118,1120],{"class":45,"line":1111},33,[43,1113,1114],{"class":82},"            for",[43,1116,1117],{"class":86}," drawing ",[43,1119,200],{"class":82},[43,1121,1122],{"class":86}," _rels(z, sheet_part).values():\n",[43,1124,1126,1129,1132,1134,1136],{"class":45,"line":1125},34,[43,1127,1128],{"class":82},"                if",[43,1130,1131],{"class":53}," \"\u002Fdrawings\u002F\"",[43,1133,984],{"class":82},[43,1135,846],{"class":82},[43,1137,1138],{"class":86}," drawing:\n",[43,1140,1142],{"class":45,"line":1141},35,[43,1143,1144],{"class":82},"                    continue\n",[43,1146,1148,1151,1153],{"class":45,"line":1147},36,[43,1149,1150],{"class":86},"                d_rels ",[43,1152,135],{"class":82},[43,1154,1155],{"class":86}," _rels(z, drawing)\n",[43,1157,1159,1162,1165,1167,1169],{"class":45,"line":1158},37,[43,1160,1161],{"class":82},"                for",[43,1163,1164],{"class":86}," anchor ",[43,1166,200],{"class":82},[43,1168,867],{"class":203},[43,1170,1171],{"class":86},".fromstring(z.read(drawing)):\n",[43,1173,1175,1178,1180,1183,1185,1187,1190],{"class":45,"line":1174},38,[43,1176,1177],{"class":86},"                    frm ",[43,1179,135],{"class":82},[43,1181,1182],{"class":86}," anchor.find(",[43,1184,716],{"class":203},[43,1186,918],{"class":82},[43,1188,1189],{"class":53}," \"from\"",[43,1191,424],{"class":86},[43,1193,1195,1198,1200,1202,1204,1207,1209,1212,1214,1217],{"class":45,"line":1194},39,[43,1196,1197],{"class":86},"                    chart_ref ",[43,1199,135],{"class":82},[43,1201,1182],{"class":86},[43,1203,270],{"class":82},[43,1205,1206],{"class":53},"\".\u002F\u002F",[43,1208,277],{"class":276},[43,1210,1211],{"class":86},"C",[43,1213,289],{"class":276},[43,1215,1216],{"class":53},"chart\"",[43,1218,424],{"class":86},[43,1220,1222,1225,1228,1230,1232],{"class":45,"line":1221},40,[43,1223,1224],{"class":82},"                    if",[43,1226,1227],{"class":86}," chart_ref ",[43,1229,981],{"class":82},[43,1231,987],{"class":203},[43,1233,1234],{"class":86},":\n",[43,1236,1238],{"class":45,"line":1237},41,[43,1239,1240],{"class":82},"                        continue\n",[43,1242,1244,1247,1249,1252,1255,1257,1259,1262,1265,1268,1270,1272,1274,1277],{"class":45,"line":1243},42,[43,1245,1246],{"class":86},"                    col, row ",[43,1248,135],{"class":82},[43,1250,1251],{"class":203}," int",[43,1253,1254],{"class":86},"(frm.find(",[43,1256,716],{"class":203},[43,1258,918],{"class":82},[43,1260,1261],{"class":53}," \"col\"",[43,1263,1264],{"class":86},").text), ",[43,1266,1267],{"class":203},"int",[43,1269,1254],{"class":86},[43,1271,716],{"class":203},[43,1273,918],{"class":82},[43,1275,1276],{"class":53}," \"row\"",[43,1278,1279],{"class":86},").text)\n",[43,1281,1283,1286,1288,1290,1293,1295,1297],{"class":45,"line":1282},43,[43,1284,1285],{"class":86},"                    chart_xml ",[43,1287,135],{"class":82},[43,1289,867],{"class":203},[43,1291,1292],{"class":86},".fromstring(z.read(d_rels[chart_ref.get(R ",[43,1294,827],{"class":82},[43,1296,1105],{"class":53},[43,1298,1299],{"class":86},")]))\n",[43,1301,1303,1306,1308,1311,1313,1315,1317,1319,1321,1324,1326,1328,1330,1333],{"class":45,"line":1302},44,[43,1304,1305],{"class":86},"                    plot ",[43,1307,135],{"class":82},[43,1309,1310],{"class":86}," chart_xml.find(",[43,1312,270],{"class":82},[43,1314,292],{"class":53},[43,1316,277],{"class":276},[43,1318,1211],{"class":86},[43,1320,289],{"class":276},[43,1322,1323],{"class":53},"chart\u002F",[43,1325,277],{"class":276},[43,1327,1211],{"class":86},[43,1329,289],{"class":276},[43,1331,1332],{"class":53},"plotArea\"",[43,1334,424],{"class":86},[43,1336,1338,1341,1343,1346,1349,1352,1354,1356,1358,1360,1363,1365,1368,1371],{"class":45,"line":1337},45,[43,1339,1340],{"class":86},"                    kind ",[43,1342,135],{"class":82},[43,1344,1345],{"class":203}," next",[43,1347,1348],{"class":86},"(el.tag.replace(C, ",[43,1350,1351],{"class":53},"\"\"",[43,1353,902],{"class":86},[43,1355,905],{"class":82},[43,1357,978],{"class":86},[43,1359,200],{"class":82},[43,1361,1362],{"class":86}," plot ",[43,1364,975],{"class":82},[43,1366,1367],{"class":86}," el.tag.endswith(",[43,1369,1370],{"class":53},"\"Chart\"",[43,1372,455],{"class":86},[43,1374,1376,1379,1381,1384,1387,1390,1392,1394,1396,1398,1400,1403,1406,1409,1411,1413,1415,1417,1419,1421,1424],{"class":45,"line":1375},46,[43,1377,1378],{"class":86},"                    series ",[43,1380,135],{"class":82},[43,1382,1383],{"class":86}," [{",[43,1385,1386],{"class":53},"\"name\"",[43,1388,1389],{"class":86},": _formula(s.find(",[43,1391,270],{"class":82},[43,1393,292],{"class":53},[43,1395,277],{"class":276},[43,1397,1211],{"class":86},[43,1399,289],{"class":276},[43,1401,1402],{"class":53},"tx\"",[43,1404,1405],{"class":86},")), ",[43,1407,1408],{"class":53},"\"values\"",[43,1410,1389],{"class":86},[43,1412,270],{"class":82},[43,1414,292],{"class":53},[43,1416,277],{"class":276},[43,1418,1211],{"class":86},[43,1420,289],{"class":276},[43,1422,1423],{"class":53},"val\"",[43,1425,1426],{"class":86},")),\n",[43,1428,1430,1433,1435,1437,1439,1441,1443,1445,1448,1451,1453,1456,1458,1461,1463,1466],{"class":45,"line":1429},47,[43,1431,1432],{"class":53},"                               \"categories\"",[43,1434,1389],{"class":86},[43,1436,270],{"class":82},[43,1438,292],{"class":53},[43,1440,277],{"class":276},[43,1442,1211],{"class":86},[43,1444,289],{"class":276},[43,1446,1447],{"class":53},"cat\"",[43,1449,1450],{"class":86},"))} ",[43,1452,905],{"class":82},[43,1454,1455],{"class":86}," s ",[43,1457,200],{"class":82},[43,1459,1460],{"class":86}," plot.iter(C ",[43,1462,827],{"class":82},[43,1464,1465],{"class":53}," \"ser\"",[43,1467,1108],{"class":86},[43,1469,1471,1474,1476,1479,1481,1484,1487,1490,1492,1494,1496,1498,1500,1503,1505,1507,1509,1511,1514,1517,1519,1521,1523,1525,1527,1529,1532,1535,1537,1540,1543],{"class":45,"line":1470},48,[43,1472,1473],{"class":86},"                    axes ",[43,1475,135],{"class":82},[43,1477,1478],{"class":86}," {ax.tag.replace(C, ",[43,1480,1351],{"class":53},[43,1482,1483],{"class":86},"): {",[43,1485,1486],{"class":53},"\"deleted\"",[43,1488,1489],{"class":86},": ax.find(",[43,1491,270],{"class":82},[43,1493,292],{"class":53},[43,1495,277],{"class":276},[43,1497,1211],{"class":86},[43,1499,289],{"class":276},[43,1501,1502],{"class":53},"delete\"",[43,1504,902],{"class":86},[43,1506,981],{"class":82},[43,1508,984],{"class":82},[43,1510,987],{"class":203},[43,1512,1513],{"class":82}," and",[43,1515,1516],{"class":86}," ax.find(",[43,1518,270],{"class":82},[43,1520,292],{"class":53},[43,1522,277],{"class":276},[43,1524,1211],{"class":86},[43,1526,289],{"class":276},[43,1528,1502],{"class":53},[43,1530,1531],{"class":86},").get(",[43,1533,1534],{"class":53},"\"val\"",[43,1536,902],{"class":86},[43,1538,1539],{"class":82},"==",[43,1541,1542],{"class":53}," \"1\"",[43,1544,1545],{"class":86},",\n",[43,1547,1549,1552,1554,1556,1558,1560,1562,1564,1567,1569,1572,1574,1576,1578,1580,1582,1584,1586,1588,1590,1592,1594,1596,1598,1600,1602],{"class":45,"line":1548},49,[43,1550,1551],{"class":53},"                                                    \"format\"",[43,1553,1489],{"class":86},[43,1555,270],{"class":82},[43,1557,292],{"class":53},[43,1559,277],{"class":276},[43,1561,1211],{"class":86},[43,1563,289],{"class":276},[43,1565,1566],{"class":53},"numFmt\"",[43,1568,1531],{"class":86},[43,1570,1571],{"class":53},"\"formatCode\"",[43,1573,902],{"class":86},[43,1575,975],{"class":82},[43,1577,1516],{"class":86},[43,1579,270],{"class":82},[43,1581,292],{"class":53},[43,1583,277],{"class":276},[43,1585,1211],{"class":86},[43,1587,289],{"class":276},[43,1589,1566],{"class":53},[43,1591,902],{"class":86},[43,1593,981],{"class":82},[43,1595,984],{"class":82},[43,1597,987],{"class":203},[43,1599,990],{"class":82},[43,1601,987],{"class":203},[43,1603,1604],{"class":86},"}\n",[43,1606,1608,1611,1614,1616,1618,1620,1623,1625,1628,1630,1633,1636,1638,1641,1643,1645,1648],{"class":45,"line":1607},50,[43,1609,1610],{"class":82},"                            for",[43,1612,1613],{"class":86}," ax ",[43,1615,200],{"class":82},[43,1617,1362],{"class":86},[43,1619,975],{"class":82},[43,1621,1622],{"class":86}," ax.tag ",[43,1624,200],{"class":82},[43,1626,1627],{"class":86}," (C ",[43,1629,827],{"class":82},[43,1631,1632],{"class":53}," \"catAx\"",[43,1634,1635],{"class":86},", C ",[43,1637,827],{"class":82},[43,1639,1640],{"class":53}," \"valAx\"",[43,1642,1635],{"class":86},[43,1644,827],{"class":82},[43,1646,1647],{"class":53}," \"dateAx\"",[43,1649,924],{"class":86},[43,1651,1653,1656,1659,1662,1664,1666,1669,1672,1675],{"class":45,"line":1652},51,[43,1654,1655],{"class":86},"                    charts.append({",[43,1657,1658],{"class":53},"\"sheet\"",[43,1660,1661],{"class":86},": sheet.get(",[43,1663,1386],{"class":53},[43,1665,413],{"class":86},[43,1667,1668],{"class":53},"\"anchor\"",[43,1670,1671],{"class":86},": _cell(col, row), ",[43,1673,1674],{"class":53},"\"type\"",[43,1676,1677],{"class":86},": kind,\n",[43,1679,1681,1684,1687,1689,1691,1693,1695,1697,1699,1701,1703,1705,1708,1710,1713,1716,1719],{"class":45,"line":1680},52,[43,1682,1683],{"class":53},"                                   \"title\"",[43,1685,1686],{"class":86},": _text(chart_xml.find(",[43,1688,270],{"class":82},[43,1690,292],{"class":53},[43,1692,277],{"class":276},[43,1694,1211],{"class":86},[43,1696,289],{"class":276},[43,1698,1323],{"class":53},[43,1700,277],{"class":276},[43,1702,1211],{"class":86},[43,1704,289],{"class":276},[43,1706,1707],{"class":53},"title\"",[43,1709,1405],{"class":86},[43,1711,1712],{"class":53},"\"series\"",[43,1714,1715],{"class":86},": series, ",[43,1717,1718],{"class":53},"\"axes\"",[43,1720,1721],{"class":86},": axes})\n",[43,1723,1725,1727],{"class":45,"line":1724},53,[43,1726,881],{"class":82},[43,1728,1729],{"class":86}," charts\n",[43,1731,1733],{"class":45,"line":1732},54,[43,1734,113],{"emptyLinePlaceholder":112},[43,1736,1738,1740,1743],{"class":45,"line":1737},55,[43,1739,119],{"class":82},[43,1741,1742],{"class":122}," _formula",[43,1744,938],{"class":86},[43,1746,1748,1750,1752,1754,1756],{"class":45,"line":1747},56,[43,1749,837],{"class":82},[43,1751,978],{"class":86},[43,1753,981],{"class":82},[43,1755,987],{"class":203},[43,1757,1234],{"class":86},[43,1759,1761,1763],{"class":45,"line":1760},57,[43,1762,854],{"class":82},[43,1764,993],{"class":203},[43,1766,1768,1771,1773,1776,1778,1780,1782,1784,1786,1789],{"class":45,"line":1767},58,[43,1769,1770],{"class":86},"    f ",[43,1772,135],{"class":82},[43,1774,1775],{"class":86}," el.find(",[43,1777,270],{"class":82},[43,1779,1206],{"class":53},[43,1781,277],{"class":276},[43,1783,1211],{"class":86},[43,1785,289],{"class":276},[43,1787,1788],{"class":53},"f\"",[43,1790,424],{"class":86},[43,1792,1794,1796,1799,1801,1804,1806,1808,1810,1812],{"class":45,"line":1793},59,[43,1795,881],{"class":82},[43,1797,1798],{"class":86}," f.text ",[43,1800,975],{"class":82},[43,1802,1803],{"class":86}," f ",[43,1805,981],{"class":82},[43,1807,984],{"class":82},[43,1809,987],{"class":203},[43,1811,990],{"class":82},[43,1813,993],{"class":203},[43,1815,1817],{"class":45,"line":1816},60,[43,1818,113],{"emptyLinePlaceholder":112},[43,1820,1822,1824,1827],{"class":45,"line":1821},61,[43,1823,119],{"class":82},[43,1825,1826],{"class":122}," _cell",[43,1828,1829],{"class":86},"(col, row):\n",[43,1831,1833,1836,1838],{"class":45,"line":1832},62,[43,1834,1835],{"class":86},"    letters ",[43,1837,135],{"class":82},[43,1839,1840],{"class":53}," \"\"\n",[43,1842,1844,1847,1850],{"class":45,"line":1843},63,[43,1845,1846],{"class":86},"    col ",[43,1848,1849],{"class":82},"+=",[43,1851,1852],{"class":203}," 1\n",[43,1854,1856,1859],{"class":45,"line":1855},64,[43,1857,1858],{"class":82},"    while",[43,1860,1861],{"class":86}," col:\n",[43,1863,1865,1868,1870,1873,1876,1878,1880,1882,1885],{"class":45,"line":1864},65,[43,1866,1867],{"class":86},"        col, rem ",[43,1869,135],{"class":82},[43,1871,1872],{"class":203}," divmod",[43,1874,1875],{"class":86},"(col ",[43,1877,283],{"class":82},[43,1879,286],{"class":203},[43,1881,172],{"class":86},[43,1883,1884],{"class":203},"26",[43,1886,424],{"class":86},[43,1888,1890,1893,1895,1898,1901,1904,1906,1909,1911],{"class":45,"line":1889},66,[43,1891,1892],{"class":86},"        letters ",[43,1894,135],{"class":82},[43,1896,1897],{"class":203}," chr",[43,1899,1900],{"class":86},"(",[43,1902,1903],{"class":203},"65",[43,1905,918],{"class":82},[43,1907,1908],{"class":86}," rem) ",[43,1910,827],{"class":82},[43,1912,1913],{"class":86}," letters\n",[43,1915,1917,1919,1922,1924,1926,1929,1932,1935,1937,1939,1941],{"class":45,"line":1916},67,[43,1918,881],{"class":82},[43,1920,1921],{"class":82}," f",[43,1923,292],{"class":53},[43,1925,277],{"class":276},[43,1927,1928],{"class":86},"letters",[43,1930,1931],{"class":276},"}{",[43,1933,1934],{"class":86},"row ",[43,1936,827],{"class":82},[43,1938,286],{"class":203},[43,1940,289],{"class":276},[43,1942,693],{"class":53},[10,1944,1945,1946,1949,1950,1953],{},"The helper follows the same path Excel does. Relationship targets come in two styles — openpyxl writes absolute paths such as ",[14,1947,1948],{},"\u002Fxl\u002Fworksheets\u002Fsheet1.xml",", Excel usually writes relative ones such as ",[14,1951,1952],{},"..\u002Fcharts\u002Fchart1.xml"," — so each target is joined to the folder of the part that refers to it, normalised, and stripped of its leading slash, which handles both. The workbook part lists the sheets; each sheet's relationships point to its drawing; each drawing holds one anchor per chart or picture, with the cell the object starts in, and a relationship to the chart part; the chart part holds the type, the title and the series. For each series the useful facts are formulas: the cell that names it, the range of values and the range of categories. For each axis, whether it is deleted — the openpyxl default that hides axes in Excel — and its number format. The result is a plain list of dictionaries, which is exactly what a test wants to compare. Because it reads the XML rather than relying on a library's object model, it works on files written by openpyxl, XlsxWriter or Excel itself.",[1955,1956,1964,1965,1964,1969,1964,1973,1964,1980,1964,1990,1964,1997,1964,2002,1964,2008,1964,2013,1964,2017,1964,2022,1964,2025,1964,2029,1964,2032,1964,2035,1964,2040,1964,2044,1964,2048,1964,2052,1964,2055,1964,2060,1964,2065,1964,2069,1964,2072,1964,2076,1964,2079,1964,2084,1964,2089],"svg",{"viewBox":1957,"role":1958,"ariaLabelledBy":1959,"xmlns":1962,"style":1963},"3 0 753 108","img",[1960,1961],"chtest-path-t","chtest-path-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:753px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[1966,1967,1968],"title",{"id":1960},"Finding the charts inside an .xlsx",[1970,1971,1972],"desc",{"id":1961},"The workbook part lists the sheets. Each sheet's relationships lead to its drawing, the drawing has an anchor per chart with the cell it starts in, and each anchor's relationship leads to the chart part, which holds the type, title, series and axes.",[1974,1975],"rect",{"x":390,"y":1976,"width":1977,"height":1978,"fill":1979},"0","753","108","#ffffff",[1974,1981],{"x":1982,"y":1983,"width":1984,"height":1985,"rx":1986,"fill":1987,"stroke":1988,"style":1989},"20.0","16.0","115.2","76.5","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[1991,1992,1996],"text",{"x":1993,"y":1994,"style":1995},"77.6","50.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","xl\u002Fworkbook.xml",[1991,1998,2001],{"x":1993,"y":1999,"style":2000},"69.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","sheets in order",[45,2003],{"x1":2004,"y1":2005,"x2":2006,"y2":2005,"stroke":2007,"style":1989},"140.2","54.2","162.2","var(--brand,#5b5cf0)",[2009,2010],"polygon",{"points":2011,"fill":2012},"166.2,54.2 157.2,59.2 157.2,49.2","#5b5cf0",[1974,2014],{"x":2015,"y":1983,"width":1984,"height":1985,"rx":1986,"fill":2016,"stroke":2007,"style":1989},"171.2","#f0f4ff",[1991,2018,2021],{"x":2019,"y":1994,"style":2020},"228.8","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","sheet rels",[1991,2023,2024],{"x":2019,"y":1999,"style":2000},"→ drawing part",[45,2026],{"x1":2027,"y1":2005,"x2":2028,"y2":2005,"stroke":2007,"style":1989},"291.4","313.4",[2009,2030],{"points":2031,"fill":2012},"317.4,54.2 308.4,59.2 308.4,49.2",[1974,2033],{"x":2034,"y":1983,"width":1984,"height":1985,"rx":1986,"fill":2016,"stroke":2007,"style":1989},"322.4",[1991,2036,2039],{"x":2037,"y":2038,"style":2020},"380.0","41.5","drawing",[1991,2041,2043],{"x":2037,"y":2042,"style":2020},"59.0","anchors",[1991,2045,2047],{"x":2037,"y":2046,"style":2000},"78.5","cell + chart link",[45,2049],{"x1":2050,"y1":2005,"x2":2051,"y2":2005,"stroke":2007,"style":1989},"442.6","464.6",[2009,2053],{"points":2054,"fill":2012},"468.6,54.2 459.6,59.2 459.6,49.2",[1974,2056],{"x":2057,"y":1983,"width":1984,"height":1985,"rx":1986,"fill":2058,"stroke":2059,"style":1989},"473.6","#d9f4f1","var(--teal,#0f9488)",[1991,2061,2064],{"x":2062,"y":2038,"style":2063},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","chart part",[1991,2066,2068],{"x":2062,"y":2067,"style":2000},"61.0","type, title,",[1991,2070,2071],{"x":2062,"y":2046,"style":2000},"series",[45,2073],{"x1":2074,"y1":2005,"x2":2075,"y2":2005,"stroke":2007,"style":1989},"593.8","615.8",[2009,2077],{"points":2078,"fill":2012},"619.8,54.2 610.8,59.2 610.8,49.2",[1974,2080],{"x":2081,"y":1983,"width":1984,"height":1985,"rx":1986,"fill":2082,"stroke":2083,"style":1989},"624.8","#fdefd8","var(--gold,#b4740a)",[1991,2085,2088],{"x":2086,"y":1994,"style":2087},"682.4","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","list of dicts",[1991,2090,2091],{"x":2086,"y":1999,"style":2000},"ready to assert",[29,2093,2095],{"id":2094},"step-3-tests-with-explicit-expectations","Step 3 — Tests with explicit expectations",[34,2097,2099],{"className":73,"code":2098,"language":75,"meta":39,"style":39},"import pytest\nfrom chart_inspect import read_charts\nfrom report import build_report\n\n@pytest.fixture(scope=\"module\")\ndef charts(tmp_path_factory):\n    path = tmp_path_factory.mktemp(\"out\") \u002F \"report.xlsx\"\n    build_report(path)\n    return {c[\"title\"]: c for c in read_charts(path)}\n\ndef test_both_charts_exist(charts):\n    assert set(charts) == {\"Revenue and cost\", \"Margin\"}\n\ndef test_revenue_chart_plots_revenue_and_cost(charts):\n    c = charts[\"Revenue and cost\"]\n    assert c[\"type\"] == \"barChart\" and c[\"anchor\"] == \"F2\"\n    assert [s[\"name\"] for s in c[\"series\"]] == [\"'Monthly'!B1\", \"'Monthly'!C1\"]\n    assert c[\"series\"][0][\"values\"] == \"'Monthly'!$B$2:$B$5\"\n    assert all(s[\"categories\"] == \"'Monthly'!$A$2:$A$5\" for s in c[\"series\"])\n\ndef test_axes_are_visible(charts):\n    for c in charts.values():\n        assert not any(ax[\"deleted\"] for ax in c[\"axes\"].values()), c[\"title\"]\n\ndef test_margin_axis_is_percent(charts):\n    assert charts[\"Margin\"][\"axes\"][\"valAx\"][\"format\"] == \"0%\"\n",[14,2100,2101,2107,2119,2131,2135,2152,2162,2183,2188,2211,2215,2225,2251,2255,2264,2279,2309,2348,2372,2404,2408,2417,2428,2462,2466,2475],{"__ignoreMap":39},[43,2102,2103,2105],{"class":45,"line":46},[43,2104,90],{"class":82},[43,2106,60],{"class":86},[43,2108,2109,2111,2114,2116],{"class":45,"line":96},[43,2110,83],{"class":82},[43,2112,2113],{"class":86}," chart_inspect ",[43,2115,90],{"class":82},[43,2117,2118],{"class":86}," read_charts\n",[43,2120,2121,2123,2126,2128],{"class":45,"line":109},[43,2122,83],{"class":82},[43,2124,2125],{"class":86}," report ",[43,2127,90],{"class":82},[43,2129,2130],{"class":86}," build_report\n",[43,2132,2133],{"class":45,"line":116},[43,2134,113],{"emptyLinePlaceholder":112},[43,2136,2137,2140,2142,2145,2147,2150],{"class":45,"line":129},[43,2138,2139],{"class":122},"@pytest.fixture",[43,2141,1900],{"class":86},[43,2143,2144],{"class":252},"scope",[43,2146,135],{"class":82},[43,2148,2149],{"class":53},"\"module\"",[43,2151,424],{"class":86},[43,2153,2154,2156,2159],{"class":45,"line":141},[43,2155,119],{"class":82},[43,2157,2158],{"class":122}," charts",[43,2160,2161],{"class":86},"(tmp_path_factory):\n",[43,2163,2164,2167,2169,2172,2175,2177,2180],{"class":45,"line":152},[43,2165,2166],{"class":86},"    path ",[43,2168,135],{"class":82},[43,2170,2171],{"class":86}," tmp_path_factory.mktemp(",[43,2173,2174],{"class":53},"\"out\"",[43,2176,902],{"class":86},[43,2178,2179],{"class":82},"\u002F",[43,2181,2182],{"class":53}," \"report.xlsx\"\n",[43,2184,2185],{"class":45,"line":163},[43,2186,2187],{"class":86},"    build_report(path)\n",[43,2189,2190,2192,2195,2198,2201,2203,2206,2208],{"class":45,"line":191},[43,2191,881],{"class":82},[43,2193,2194],{"class":86}," {c[",[43,2196,2197],{"class":53},"\"title\"",[43,2199,2200],{"class":86},"]: c ",[43,2202,905],{"class":82},[43,2204,2205],{"class":86}," c ",[43,2207,200],{"class":82},[43,2209,2210],{"class":86}," read_charts(path)}\n",[43,2212,2213],{"class":45,"line":264},[43,2214,113],{"emptyLinePlaceholder":112},[43,2216,2217,2219,2222],{"class":45,"line":332},[43,2218,119],{"class":82},[43,2220,2221],{"class":122}," test_both_charts_exist",[43,2223,2224],{"class":86},"(charts):\n",[43,2226,2227,2230,2233,2236,2238,2241,2244,2246,2249],{"class":45,"line":337},[43,2228,2229],{"class":82},"    assert",[43,2231,2232],{"class":203}," set",[43,2234,2235],{"class":86},"(charts) ",[43,2237,1539],{"class":82},[43,2239,2240],{"class":86}," {",[43,2242,2243],{"class":53},"\"Revenue and cost\"",[43,2245,172],{"class":86},[43,2247,2248],{"class":53},"\"Margin\"",[43,2250,1604],{"class":86},[43,2252,2253],{"class":45,"line":348},[43,2254,113],{"emptyLinePlaceholder":112},[43,2256,2257,2259,2262],{"class":45,"line":359},[43,2258,119],{"class":82},[43,2260,2261],{"class":122}," test_revenue_chart_plots_revenue_and_cost",[43,2263,2224],{"class":86},[43,2265,2266,2269,2271,2274,2276],{"class":45,"line":370},[43,2267,2268],{"class":86},"    c ",[43,2270,135],{"class":82},[43,2272,2273],{"class":86}," charts[",[43,2275,2243],{"class":53},[43,2277,2278],{"class":86},"]\n",[43,2280,2281,2283,2286,2288,2291,2293,2296,2298,2300,2302,2304,2306],{"class":45,"line":427},[43,2282,2229],{"class":82},[43,2284,2285],{"class":86}," c[",[43,2287,1674],{"class":53},[43,2289,2290],{"class":86},"] ",[43,2292,1539],{"class":82},[43,2294,2295],{"class":53}," \"barChart\"",[43,2297,1513],{"class":82},[43,2299,2285],{"class":86},[43,2301,1668],{"class":53},[43,2303,2290],{"class":86},[43,2305,1539],{"class":82},[43,2307,2308],{"class":53}," \"F2\"\n",[43,2310,2311,2313,2316,2318,2320,2322,2324,2326,2328,2330,2333,2335,2338,2341,2343,2346],{"class":45,"line":458},[43,2312,2229],{"class":82},[43,2314,2315],{"class":86}," [s[",[43,2317,1386],{"class":53},[43,2319,2290],{"class":86},[43,2321,905],{"class":82},[43,2323,1455],{"class":86},[43,2325,200],{"class":82},[43,2327,2285],{"class":86},[43,2329,1712],{"class":53},[43,2331,2332],{"class":86},"]] ",[43,2334,1539],{"class":82},[43,2336,2337],{"class":86}," [",[43,2339,2340],{"class":53},"\"'Monthly'!B1\"",[43,2342,172],{"class":86},[43,2344,2345],{"class":53},"\"'Monthly'!C1\"",[43,2347,2278],{"class":86},[43,2349,2350,2352,2354,2356,2359,2361,2363,2365,2367,2369],{"class":45,"line":474},[43,2351,2229],{"class":82},[43,2353,2285],{"class":86},[43,2355,1712],{"class":53},[43,2357,2358],{"class":86},"][",[43,2360,1976],{"class":203},[43,2362,2358],{"class":86},[43,2364,1408],{"class":53},[43,2366,2290],{"class":86},[43,2368,1539],{"class":82},[43,2370,2371],{"class":53}," \"'Monthly'!$B$2:$B$5\"\n",[43,2373,2374,2376,2379,2382,2385,2387,2389,2392,2394,2396,2398,2400,2402],{"class":45,"line":485},[43,2375,2229],{"class":82},[43,2377,2378],{"class":203}," all",[43,2380,2381],{"class":86},"(s[",[43,2383,2384],{"class":53},"\"categories\"",[43,2386,2290],{"class":86},[43,2388,1539],{"class":82},[43,2390,2391],{"class":53}," \"'Monthly'!$A$2:$A$5\"",[43,2393,956],{"class":82},[43,2395,1455],{"class":86},[43,2397,200],{"class":82},[43,2399,2285],{"class":86},[43,2401,1712],{"class":53},[43,2403,188],{"class":86},[43,2405,2406],{"class":45,"line":490},[43,2407,113],{"emptyLinePlaceholder":112},[43,2409,2410,2412,2415],{"class":45,"line":501},[43,2411,119],{"class":82},[43,2413,2414],{"class":122}," test_axes_are_visible",[43,2416,2224],{"class":86},[43,2418,2419,2421,2423,2425],{"class":45,"line":512},[43,2420,194],{"class":82},[43,2422,2205],{"class":86},[43,2424,200],{"class":82},[43,2426,2427],{"class":86}," charts.values():\n",[43,2429,2430,2433,2435,2438,2441,2443,2445,2447,2449,2451,2453,2455,2458,2460],{"class":45,"line":551},[43,2431,2432],{"class":82},"        assert",[43,2434,984],{"class":82},[43,2436,2437],{"class":203}," any",[43,2439,2440],{"class":86},"(ax[",[43,2442,1486],{"class":53},[43,2444,2290],{"class":86},[43,2446,905],{"class":82},[43,2448,1613],{"class":86},[43,2450,200],{"class":82},[43,2452,2285],{"class":86},[43,2454,1718],{"class":53},[43,2456,2457],{"class":86},"].values()), c[",[43,2459,2197],{"class":53},[43,2461,2278],{"class":86},[43,2463,2464],{"class":45,"line":581},[43,2465,113],{"emptyLinePlaceholder":112},[43,2467,2468,2470,2473],{"class":45,"line":592},[43,2469,119],{"class":82},[43,2471,2472],{"class":122}," test_margin_axis_is_percent",[43,2474,2224],{"class":86},[43,2476,2477,2479,2481,2483,2485,2487,2489,2492,2494,2497,2499,2501],{"class":45,"line":607},[43,2478,2229],{"class":82},[43,2480,2273],{"class":86},[43,2482,2248],{"class":53},[43,2484,2358],{"class":86},[43,2486,1718],{"class":53},[43,2488,2358],{"class":86},[43,2490,2491],{"class":53},"\"valAx\"",[43,2493,2358],{"class":86},[43,2495,2496],{"class":53},"\"format\"",[43,2498,2290],{"class":86},[43,2500,1539],{"class":82},[43,2502,589],{"class":53},[34,2504,2506],{"className":36,"code":2505,"language":38,"meta":39,"style":39},"python -m pytest -q test_report_charts.py 2>&1 | tail -3\n",[14,2507,2508],{"__ignoreMap":39},[43,2509,2510,2512,2515,2518,2521,2524,2527,2530,2533],{"class":45,"line":46},[43,2511,75],{"class":49},[43,2513,2514],{"class":203}," -m",[43,2516,2517],{"class":53}," pytest",[43,2519,2520],{"class":203}," -q",[43,2522,2523],{"class":53}," test_report_charts.py",[43,2525,2526],{"class":82}," 2>&1",[43,2528,2529],{"class":82}," |",[43,2531,2532],{"class":49}," tail",[43,2534,2535],{"class":203}," -3\n",[10,2537,2538,2539,2543,2544,2547],{},"The fixture builds the report once into a temporary folder and indexes its charts by title. Each test then states one thing that must stay true: both charts exist; the revenue chart is a column chart at F2 whose two series are named from B1 and C1, plot B2:B5 and share the month categories; no axis is deleted; the margin axis is formatted as a percentage. These are exactly the properties that break silently. If someone inserts a column before ",[2540,2541,2542],"em",{},"Revenue",", the series reference test fails with the new range in the message; if the ",[14,2545,2546],{},"delete = False"," lines are lost in a refactor, the axis test fails and names the chart. All four tests pass against the current report.",[1955,2549,1964,2555,1964,2558,1964,2561,1964,2566,1964,2572,1964,2576,1964,2582,1964,2586,1964,2590,1964,2594,1964,2597,1964,2601,1964,2605,1964,2609,1964,2613,1964,2617,1964,2620,1964,2623,1964,2627,1964,2629,1964,2632,1964,2634,1964,2637,1964,2639,1964,2642,1964,2644,1964,2647,1964,2650,1964,2653,1964,2655,1964,2659,1964,2661,1964,2664,1964,2666,1964,2669,1964,2671,1964,2674,1964,2676,1964,2679,1964,2682,1964,2685,1964,2687,1964,2690,1964,2692,1964,2695,1964,2697,1964,2700,1964,2702,1964,2705,1964,2707],{"viewBox":2550,"role":1958,"ariaLabelledBy":2551,"xmlns":1962,"style":2554},"55 0 649 140",[2552,2553],"chtest-facts-t","chtest-facts-d","width:100%;max-width:649px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[1966,2556,2557],{"id":2552},"What the helper reads from the two charts",[1970,2559,2560],{"id":2553},"The first chart is a bar chart at F2 titled Revenue and cost with two series from columns B and C and month categories from column A. The second is a line chart at F20 titled Margin with one series from column D and a percent-formatted value axis. Neither has a deleted axis.",[1974,2562],{"x":2563,"y":1976,"width":2564,"height":2565,"fill":1979},"55","649","140",[1974,2567],{"x":2568,"y":1983,"width":2569,"height":2570,"rx":1976,"fill":1987,"stroke":1988,"style":2571},"71.9","32.0","24.0","stroke-width:1px",[1974,2573],{"x":2574,"y":1983,"width":2575,"height":2570,"rx":1976,"fill":1987,"stroke":1988,"style":2571},"103.9","133.9",[1991,2577,2581],{"x":2578,"y":2579,"style":2580},"170.9","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[1974,2583],{"x":2584,"y":1983,"width":2585,"height":2570,"rx":1976,"fill":1987,"stroke":1988,"style":2571},"237.9","80.4",[1991,2587,2589],{"x":2588,"y":2579,"style":2580},"278.1","B",[1974,2591],{"x":2592,"y":1983,"width":2593,"height":2570,"rx":1976,"fill":1987,"stroke":1988,"style":2571},"318.3","69.4",[1991,2595,1211],{"x":2596,"y":2579,"style":2580},"353.0",[1974,2598],{"x":2599,"y":1983,"width":2600,"height":2570,"rx":1976,"fill":1987,"stroke":1988,"style":2571},"387.7","171.0",[1991,2602,2604],{"x":2603,"y":2579,"style":2580},"473.2","D",[1974,2606],{"x":2607,"y":1983,"width":2608,"height":2570,"rx":1976,"fill":1987,"stroke":1988,"style":2571},"558.7","129.4",[1991,2610,2612],{"x":2611,"y":2579,"style":2580},"623.4","E",[1974,2614],{"x":2568,"y":2615,"width":2569,"height":2616,"rx":1976,"fill":1987,"stroke":1988,"style":2571},"40.0","28.0",[1991,2618,400],{"x":2619,"y":2042,"style":2580},"87.9",[1974,2621],{"x":2574,"y":2615,"width":2575,"height":2616,"rx":1976,"fill":2016,"stroke":2007,"style":2622},"stroke-width:1.5px",[1991,2624,2626],{"x":2578,"y":2042,"style":2625},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Title",[1974,2628],{"x":2584,"y":2615,"width":2585,"height":2616,"rx":1976,"fill":2016,"stroke":2007,"style":2622},[1991,2630,2631],{"x":2588,"y":2042,"style":2625},"Type",[1974,2633],{"x":2592,"y":2615,"width":2593,"height":2616,"rx":1976,"fill":2016,"stroke":2007,"style":2622},[1991,2635,2636],{"x":2596,"y":2042,"style":2625},"Anchor",[1974,2638],{"x":2599,"y":2615,"width":2600,"height":2616,"rx":1976,"fill":2016,"stroke":2007,"style":2622},[1991,2640,2641],{"x":2603,"y":2042,"style":2625},"Series",[1974,2643],{"x":2607,"y":2615,"width":2608,"height":2616,"rx":1976,"fill":2016,"stroke":2007,"style":2622},[1991,2645,2646],{"x":2611,"y":2042,"style":2625},"Axes",[1974,2648],{"x":2568,"y":2649,"width":2569,"height":2616,"rx":1976,"fill":1987,"stroke":1988,"style":2571},"68.0",[1991,2651,258],{"x":2619,"y":2652,"style":2580},"87.0",[1974,2654],{"x":2574,"y":2649,"width":2575,"height":2616,"rx":1976,"fill":1979,"stroke":1988,"style":2571},[1991,2656,2658],{"x":2578,"y":2652,"style":2657},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Revenue and cost",[1974,2660],{"x":2584,"y":2649,"width":2585,"height":2616,"rx":1976,"fill":1979,"stroke":1988,"style":2571},[1991,2662,2663],{"x":2588,"y":2652,"style":2657},"barChart",[1974,2665],{"x":2592,"y":2649,"width":2593,"height":2616,"rx":1976,"fill":1979,"stroke":1988,"style":2571},[1991,2667,2668],{"x":2596,"y":2652,"style":2657},"F2",[1974,2670],{"x":2599,"y":2649,"width":2600,"height":2616,"rx":1976,"fill":1979,"stroke":1988,"style":2571},[1991,2672,2673],{"x":2603,"y":2652,"style":2657},"B1 → B2:B5, C1 → C2:C5",[1974,2675],{"x":2607,"y":2649,"width":2608,"height":2616,"rx":1976,"fill":1979,"stroke":1988,"style":2571},[1991,2677,2678],{"x":2611,"y":2652,"style":2657},"visible",[1974,2680],{"x":2568,"y":2681,"width":2569,"height":2616,"rx":1976,"fill":1987,"stroke":1988,"style":2571},"96.0",[1991,2683,390],{"x":2619,"y":2684,"style":2580},"115.0",[1974,2686],{"x":2574,"y":2681,"width":2575,"height":2616,"rx":1976,"fill":1979,"stroke":1988,"style":2571},[1991,2688,2689],{"x":2578,"y":2684,"style":2657},"Margin",[1974,2691],{"x":2584,"y":2681,"width":2585,"height":2616,"rx":1976,"fill":1979,"stroke":1988,"style":2571},[1991,2693,2694],{"x":2588,"y":2684,"style":2657},"lineChart",[1974,2696],{"x":2592,"y":2681,"width":2593,"height":2616,"rx":1976,"fill":1979,"stroke":1988,"style":2571},[1991,2698,2699],{"x":2596,"y":2684,"style":2657},"F20",[1974,2701],{"x":2599,"y":2681,"width":2600,"height":2616,"rx":1976,"fill":1979,"stroke":1988,"style":2571},[1991,2703,2704],{"x":2603,"y":2684,"style":2657},"D1 → D2:D5",[1974,2706],{"x":2607,"y":2681,"width":2608,"height":2616,"rx":1976,"fill":1979,"stroke":1988,"style":2571},[1991,2708,2709],{"x":2611,"y":2684,"style":2657},"visible, value 0%",[29,2711,2713],{"id":2712},"step-4-a-snapshot-for-everything-else","Step 4 — A snapshot for everything else",[34,2715,2717],{"className":73,"code":2716,"language":75,"meta":39,"style":39},"import json\nfrom pathlib import Path\nfrom chart_inspect import read_charts\nfrom report import build_report\n\nbuild_report(\"report.xlsx\")\ncurrent = read_charts(\"report.xlsx\")\nsnapshot = Path(\"charts_snapshot.json\")\nif not snapshot.exists():\n    snapshot.write_text(json.dumps(current, indent=2))           # first run: record it, review it, commit it\nassert json.loads(snapshot.read_text()) == current, \"chart definitions changed — review and update the snapshot\"\nprint(json.dumps(current[0], indent=2)[:400])\n",[14,2718,2719,2726,2738,2748,2758,2762,2772,2786,2801,2810,2828,2844],{"__ignoreMap":39},[43,2720,2721,2723],{"class":45,"line":46},[43,2722,90],{"class":82},[43,2724,2725],{"class":86}," json\n",[43,2727,2728,2730,2733,2735],{"class":45,"line":96},[43,2729,83],{"class":82},[43,2731,2732],{"class":86}," pathlib ",[43,2734,90],{"class":82},[43,2736,2737],{"class":86}," Path\n",[43,2739,2740,2742,2744,2746],{"class":45,"line":109},[43,2741,83],{"class":82},[43,2743,2113],{"class":86},[43,2745,90],{"class":82},[43,2747,2118],{"class":86},[43,2749,2750,2752,2754,2756],{"class":45,"line":116},[43,2751,83],{"class":82},[43,2753,2125],{"class":86},[43,2755,90],{"class":82},[43,2757,2130],{"class":86},[43,2759,2760],{"class":45,"line":129},[43,2761,113],{"emptyLinePlaceholder":112},[43,2763,2764,2767,2770],{"class":45,"line":141},[43,2765,2766],{"class":86},"build_report(",[43,2768,2769],{"class":53},"\"report.xlsx\"",[43,2771,424],{"class":86},[43,2773,2774,2777,2779,2782,2784],{"class":45,"line":152},[43,2775,2776],{"class":86},"current ",[43,2778,135],{"class":82},[43,2780,2781],{"class":86}," read_charts(",[43,2783,2769],{"class":53},[43,2785,424],{"class":86},[43,2787,2788,2791,2793,2796,2799],{"class":45,"line":163},[43,2789,2790],{"class":86},"snapshot ",[43,2792,135],{"class":82},[43,2794,2795],{"class":86}," Path(",[43,2797,2798],{"class":53},"\"charts_snapshot.json\"",[43,2800,424],{"class":86},[43,2802,2803,2805,2807],{"class":45,"line":191},[43,2804,975],{"class":82},[43,2806,984],{"class":82},[43,2808,2809],{"class":86}," snapshot.exists():\n",[43,2811,2812,2815,2818,2820,2822,2825],{"class":45,"line":264},[43,2813,2814],{"class":86},"    snapshot.write_text(json.dumps(current, ",[43,2816,2817],{"class":252},"indent",[43,2819,135],{"class":82},[43,2821,258],{"class":203},[43,2823,2824],{"class":86},"))           ",[43,2826,2827],{"class":875},"# first run: record it, review it, commit it\n",[43,2829,2830,2833,2836,2838,2841],{"class":45,"line":332},[43,2831,2832],{"class":82},"assert",[43,2834,2835],{"class":86}," json.loads(snapshot.read_text()) ",[43,2837,1539],{"class":82},[43,2839,2840],{"class":86}," current, ",[43,2842,2843],{"class":53},"\"chart definitions changed — review and update the snapshot\"\n",[43,2845,2846,2849,2852,2854,2857,2859,2861,2863,2866,2869],{"class":45,"line":337},[43,2847,2848],{"class":203},"print",[43,2850,2851],{"class":86},"(json.dumps(current[",[43,2853,1976],{"class":203},[43,2855,2856],{"class":86},"], ",[43,2858,2817],{"class":252},[43,2860,135],{"class":82},[43,2862,258],{"class":203},[43,2864,2865],{"class":86},")[:",[43,2867,2868],{"class":203},"400",[43,2870,188],{"class":86},[10,2872,2873,2874,2876,2877,27],{},"Explicit assertions are best for the properties that matter most. For everything else — the exact set of series, every axis, every anchor — a snapshot is less work: record the helper's output as JSON once, review it, commit it next to the tests, and compare future runs against it. Any change to any chart then fails the test with both versions available to diff; if the change was intended, the snapshot is regenerated as part of the same commit. Because the helper returns plain data, the snapshot is readable and reviews well in a pull request, unlike a binary ",[14,2875,20],{},". The same idea for whole workbooks is covered in ",[23,2878,2880],{"href":2879},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests\u002F","Compare Excel Output to a Golden File in Tests",[1955,2882,1964,2888,1964,2891,1964,2894,1964,2898,1964,2903,1964,2909,1964,2914,1964,2918,1964,2922,1964,2926,1964,2929,1964,2934,1964,2938,1964,2941,1964,2944,1964,2947],{"viewBox":2883,"role":1958,"ariaLabelledBy":2884,"xmlns":1962,"style":2887},"4 0 752 199",[2885,2886],"chtest-styles-t","chtest-styles-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[1966,2889,2890],{"id":2885},"Explicit assertions or a snapshot",[1970,2892,2893],{"id":2886},"Explicit assertions check a few named properties, give clear failure messages and survive harmless changes. A snapshot checks every property of every chart with little effort, but fails on any change and has to be reviewed and updated deliberately.",[1974,2895],{"x":522,"y":1976,"width":2896,"height":2897,"fill":1979},"752","199",[1974,2899],{"x":1982,"y":1983,"width":2900,"height":2901,"rx":2902,"fill":2016,"stroke":2007,"style":1989},"351.0","167.0","14",[1991,2904,2908],{"x":2905,"y":2906,"style":2907},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Explicit assertions",[45,2910],{"x1":2911,"y1":2912,"x2":2913,"y2":2912,"stroke":2007,"style":2571},"36.0","57.0","355.0",[1991,2915,2917],{"x":2905,"y":2916,"style":2657},"77.0","a few properties that matter",[1991,2919,2921],{"x":2905,"y":2920,"style":2657},"103.0","clear failure messages",[1991,2923,2925],{"x":2905,"y":2924,"style":2657},"129.0","ignore harmless changes",[1974,2927],{"x":2928,"y":1983,"width":2900,"height":2901,"rx":2902,"fill":2058,"stroke":2059,"style":1989},"389.0",[1991,2930,2933],{"x":2931,"y":2906,"style":2932},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","JSON snapshot",[45,2935],{"x1":2936,"y1":2912,"x2":2937,"y2":2912,"stroke":2059,"style":2571},"405.0","724.0",[1991,2939,2940],{"x":2931,"y":2916,"style":2657},"every chart, every property",[1991,2942,2943],{"x":2931,"y":2920,"style":2657},"little code to write",[1991,2945,2946],{"x":2931,"y":2924,"style":2657},"fails on any change",[1991,2948,2950],{"x":2931,"y":2949,"style":2657},"155.0","review when updating",[29,2952,2954],{"id":2953},"common-pitfalls","Common pitfalls",[2956,2957,2958,2966,2972,2978,2984],"ul",{},[2959,2960,2961,2965],"li",{},[2962,2963,2964],"strong",{},"Expecting openpyxl to load charts."," It does not; read the chart XML from the package.",[2959,2967,2968,2971],{},[2962,2969,2970],{},"Testing only that a chart exists."," Assert its series ranges; the wrong column is the common failure.",[2959,2973,2974,2977],{},[2962,2975,2976],{},"Ignoring deleted axes."," openpyxl hides axes unless told otherwise; test for it.",[2959,2979,2980,2983],{},[2962,2981,2982],{},"Matching on file names like chart1.xml."," Follow the relationships so tests survive reordering.",[2959,2985,2986,2989],{},[2962,2987,2988],{},"Snapshots nobody reviews."," Read the first snapshot before committing it.",[29,2991,2993],{"id":2992},"conclusion","Conclusion",[10,2995,2996,2997,2999],{},"To test the charts a script writes, read them from the ",[14,2998,20],{}," package: follow the workbook's relationships to each sheet's drawing, each drawing anchor to its chart part, and pull out the chart type, title, series name, value and category formulas, axis settings and anchor cell. Assert the properties that matter in pytest, and keep a reviewed JSON snapshot for the rest, so a chart that starts plotting the wrong column or loses its axes fails a test instead of reaching a reader.",[29,3001,3003],{"id":3002},"frequently-asked-questions","Frequently asked questions",[10,3005,3006,3009],{},[2962,3007,3008],{},"Can openpyxl read charts from an existing workbook?","\nNo; charts are not loaded, and saving a loaded workbook drops them. Read the chart XML from the file directly.",[10,3011,3012,3015,3016,3019],{},[2962,3013,3014],{},"Where are charts stored in an .xlsx file?","\nIn ",[14,3017,3018],{},"xl\u002Fcharts\u002FchartN.xml",", linked from a drawing part that the sheet refers to; the drawing anchors give each chart's position.",[10,3021,3022,3025],{},[2962,3023,3024],{},"What should a chart test check?","\nAt least the chart type, the series value and category ranges, the series names and that the axes are not deleted.",[10,3027,3028,3031],{},[2962,3029,3030],{},"How do I snapshot-test Excel charts?","\nConvert each chart's definition to a dictionary, save it as JSON once, and compare later runs with the stored file.",[29,3033,3035],{"id":3034},"related","Related",[2956,3037,3038,3044,3051,3058],{},[2959,3039,3040,3041,3043],{},"Up one level: ",[23,3042,26],{"href":25}," — reliable, testable projects.",[2959,3045,3046,3050],{},[23,3047,3049],{"href":3048},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest"," — testing the cell values.",[2959,3052,3053,3057],{},[23,3054,3056],{"href":3055},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Finspect-the-xml-inside-an-xlsx-file-with-python\u002F","Inspect the XML Inside an .xlsx File with Python"," — the package structure.",[2959,3059,3060,3064],{},[23,3061,3063],{"href":3062},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fplot-a-chart-from-non-adjacent-columns\u002F","Plot a Chart from Non-Adjacent Columns"," — series references to test.",[3066,3067,3068],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}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":39,"searchDepth":96,"depth":96,"links":3070},[3071,3072,3073,3074,3075,3076,3077,3078,3079],{"id":31,"depth":96,"text":32},{"id":69,"depth":96,"text":70},{"id":627,"depth":96,"text":628},{"id":2094,"depth":96,"text":2095},{"id":2712,"depth":96,"text":2713},{"id":2953,"depth":96,"text":2954},{"id":2992,"depth":96,"text":2993},{"id":3002,"depth":96,"text":3003},{"id":3034,"depth":96,"text":3035},"2026-10-02","Write pytest tests for the charts a report script produces: since openpyxl does not load charts back, read them straight from the chart XML inside the .xlsx — chart type, title, series names, value and category ranges, axis settings and which sheet and cell each chart sits on — compare them with expectations or a stored snapshot, and catch the regressions that leave a chart empty, pointing at the wrong column, or with hidden axes.","md",[3084,3086,3088,3090],{"q":3008,"a":3085},"No; charts are not loaded, and saving a loaded workbook drops them. Read the chart XML from the file directly.",{"q":3014,"a":3087},"In xl\u002Fcharts\u002FchartN.xml, linked from a drawing part that the sheet refers to; the drawing anchors give each chart's position.",{"q":3024,"a":3089},"At least the chart type, the series value and category ranges, the series names and that the axes are not deleted.",{"q":3030,"a":3091},"Convert each chart's definition to a dictionary, save it as JSON once, and compare later runs with the stored file.","test excel charts pytest",null,{"breadcrumb":3095},[3096,3098,3101],{"name":3097,"item":2179},"Home",{"name":3099,"item":3100},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":26,"item":25},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-that-excel-charts-were-written-correctly",{"title":3104,"description":3105},"Test That Excel Charts Were Written Correctly with pytest","Test Excel charts in Python: read chart XML from the .xlsx because openpyxl does not load charts, extract type, title, series ranges, axes and anchors, and assert them in pytest or against a snapshot.","test-that-excel-charts-were-written-correctly","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-that-excel-charts-were-written-correctly\u002Findex",[3109,3110],"reliable-jobs","charts-and-images","how-to","jcrph2Ca9M1F0lE53gX2GKE8FuvXbLhlxpY6MVQXbLQ",[3114,3117],{"title":3049,"path":3115,"stem":3116,"children":-1},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002Findex",{"title":3118,"path":3119,"stem":3120,"children":-1},"Test win32com and xlwings Code Without Excel","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-win32com-and-xlwings-code-without-excel","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-win32com-and-xlwings-code-without-excel\u002Findex",[3122,3124],{"path":3123,"title":3099},"\u002Fautomating-reporting-workflows",{"path":3125,"title":26},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts",[],[3128,3132,3135,3138,3141,3144],{"path":3129,"title":3130,"kind":3131},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",{"path":3133,"title":3134,"kind":3111},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run","Detect a Scheduled Report That Did Not Run",{"path":3136,"title":3137,"kind":3111},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder","Ignore Excel Lock Files When Processing a Folder",{"path":3139,"title":3140,"kind":3111},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file","Log Python Excel Script Output to a File",{"path":3142,"title":3143,"kind":3111},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fmake-an-excel-report-job-safe-to-rerun","Make an Excel Report Job Safe to Rerun",{"path":3145,"title":3146,"kind":3111},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-formula-injection-in-excel-exports","Prevent Formula Injection in Excel Exports from Python",1790972142957]