[{"data":1,"prerenderedAt":1954},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fmake-an-excel-chart-grow-with-its-data":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fmake-an-excel-chart-grow-with-its-data":1920,"ancestors:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fmake-an-excel-chart-grow-with-its-data":1929,"children:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fmake-an-excel-chart-grow-with-its-data":1934,"related:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fmake-an-excel-chart-grow-with-its-data":1935},{"id":4,"title":5,"body":6,"dateModified":1886,"datePublished":1886,"description":1887,"extension":1888,"faq":1889,"keyword":1898,"kind":1899,"meta":1900,"navigation":103,"path":1909,"seo":1910,"slug":1913,"stem":1914,"topics":1915,"type":1918,"__hash__":1919},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fmake-an-excel-chart-grow-with-its-data\u002Findex.md","Make an Excel Chart Grow with Its Data",{"type":7,"value":8,"toc":1873},"minimark",[9,29,34,65,71,75,303,306,570,574,589,840,863,867,870,1199,1214,1351,1355,1362,1480,1492,1496,1518,1522,1525,1689,1692,1753,1757,1798,1802,1808,1812,1818,1824,1830,1836,1840,1869],[10,11,12,13,17,18,22,23,28],"p",{},"A monthly sales sheet has a line chart beside it. A script appends October's row — and the chart still stops at September. Charts in Excel do not point at \"the data\"; each series stores fixed cell ranges such as ",[14,15,16],"code",{},"Data!$B$2:$B$10",", and appending a row below those ranges does not change them. Inside Excel, people are used to charts that expand on their own, because a chart built on an Excel ",[19,20,21],"em",{},"table"," is adjusted by Excel when rows are added to the table through the interface. A file edited by a script gets none of that help. This page shows how to see the problem, how to extend an existing chart's ranges from Python, why rebuilding the chart is often simpler, and what to do for workbooks that people edit live. It belongs to ",[24,25,27],"a",{"href":26},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002F","Creating Charts in Excel with openpyxl",".",[30,31,33],"h2",{"id":32},"prerequisites","Prerequisites",[35,36,41],"pre",{"className":37,"code":38,"language":39,"meta":40,"style":40},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl xlsxwriter pandas\n","bash","",[14,42,43],{"__ignoreMap":40},[44,45,48,52,56,59,62],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sMTad","pip",[44,53,55],{"class":54},"srMev"," install",[44,57,58],{"class":54}," openpyxl",[44,60,61],{"class":54}," xlsxwriter",[44,63,64],{"class":54}," pandas\n",[10,66,67,70],{},[14,68,69],{},"sales_chart.xlsx"," has six months of sales and targets and a line chart covering rows 2–7. The task: append July to September and have the chart include them.",[30,72,74],{"id":73},"step-1-see-the-problem","Step 1 — See the problem",[35,76,80],{"className":77,"code":78,"language":79,"meta":40,"style":40},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import load_workbook\n\nwb = load_workbook(\"sales_chart.xlsx\")\nws = wb[\"Data\"]\nfor row in [(\"Jul\", 172, 140), (\"Aug\", 169, 145), (\"Sep\", 181, 150)]:\n    ws.append(row)\nwb.save(\"sales_appended.xlsx\")\n\ncheck = load_workbook(\"sales_appended.xlsx\")[\"Data\"]\nchart = check._charts[0]\nprint(\"data now ends at row\", check.max_row)\nfor s in chart.series:\n    print(\"series range:\", s.val.numRef.f)\n","python",[14,81,82,98,105,123,140,204,210,221,226,245,261,276,289],{"__ignoreMap":40},[44,83,84,88,92,95],{"class":46,"line":47},[44,85,87],{"class":86},"s-kum","from",[44,89,91],{"class":90},"skGVy"," openpyxl ",[44,93,94],{"class":86},"import",[44,96,97],{"class":90}," load_workbook\n",[44,99,101],{"class":46,"line":100},2,[44,102,104],{"emptyLinePlaceholder":103},true,"\n",[44,106,108,111,114,117,120],{"class":46,"line":107},3,[44,109,110],{"class":90},"wb ",[44,112,113],{"class":86},"=",[44,115,116],{"class":90}," load_workbook(",[44,118,119],{"class":54},"\"sales_chart.xlsx\"",[44,121,122],{"class":90},")\n",[44,124,126,129,131,134,137],{"class":46,"line":125},4,[44,127,128],{"class":90},"ws ",[44,130,113],{"class":86},[44,132,133],{"class":90}," wb[",[44,135,136],{"class":54},"\"Data\"",[44,138,139],{"class":90},"]\n",[44,141,143,146,149,152,155,158,161,165,167,170,173,176,178,181,183,186,188,191,193,196,198,201],{"class":46,"line":142},5,[44,144,145],{"class":86},"for",[44,147,148],{"class":90}," row ",[44,150,151],{"class":86},"in",[44,153,154],{"class":90}," [(",[44,156,157],{"class":54},"\"Jul\"",[44,159,160],{"class":90},", ",[44,162,164],{"class":163},"sP0c6","172",[44,166,160],{"class":90},[44,168,169],{"class":163},"140",[44,171,172],{"class":90},"), (",[44,174,175],{"class":54},"\"Aug\"",[44,177,160],{"class":90},[44,179,180],{"class":163},"169",[44,182,160],{"class":90},[44,184,185],{"class":163},"145",[44,187,172],{"class":90},[44,189,190],{"class":54},"\"Sep\"",[44,192,160],{"class":90},[44,194,195],{"class":163},"181",[44,197,160],{"class":90},[44,199,200],{"class":163},"150",[44,202,203],{"class":90},")]:\n",[44,205,207],{"class":46,"line":206},6,[44,208,209],{"class":90},"    ws.append(row)\n",[44,211,213,216,219],{"class":46,"line":212},7,[44,214,215],{"class":90},"wb.save(",[44,217,218],{"class":54},"\"sales_appended.xlsx\"",[44,220,122],{"class":90},[44,222,224],{"class":46,"line":223},8,[44,225,104],{"emptyLinePlaceholder":103},[44,227,229,232,234,236,238,241,243],{"class":46,"line":228},9,[44,230,231],{"class":90},"check ",[44,233,113],{"class":86},[44,235,116],{"class":90},[44,237,218],{"class":54},[44,239,240],{"class":90},")[",[44,242,136],{"class":54},[44,244,139],{"class":90},[44,246,248,251,253,256,259],{"class":46,"line":247},10,[44,249,250],{"class":90},"chart ",[44,252,113],{"class":86},[44,254,255],{"class":90}," check._charts[",[44,257,258],{"class":163},"0",[44,260,139],{"class":90},[44,262,264,267,270,273],{"class":46,"line":263},11,[44,265,266],{"class":163},"print",[44,268,269],{"class":90},"(",[44,271,272],{"class":54},"\"data now ends at row\"",[44,274,275],{"class":90},", check.max_row)\n",[44,277,279,281,284,286],{"class":46,"line":278},12,[44,280,145],{"class":86},[44,282,283],{"class":90}," s ",[44,285,151],{"class":86},[44,287,288],{"class":90}," chart.series:\n",[44,290,292,295,297,300],{"class":46,"line":291},13,[44,293,294],{"class":163},"    print",[44,296,269],{"class":90},[44,298,299],{"class":54},"\"series range:\"",[44,301,302],{"class":90},", s.val.numRef.f)\n",[10,304,305],{},"The data ends at row 10, but both series still read rows 2–7. openpyxl 3.1 keeps the chart when it loads and saves the workbook — it reads charts back and rewrites them — but it does not, and cannot know to, change their ranges. Anyone opening the file sees a chart that stops in June.",[307,308,316,317,316,321,316,325,316,332,316,341,316,345,316,352,316,356,316,360,316,364,316,368,316,372,316,377,316,382,316,386,316,388,316,391,316,393,316,396,316,399,316,403,316,407,316,411,316,413,316,416,316,418,316,421,316,424,316,428,316,430,316,433,316,435,316,437,316,439,316,441,316,444,316,448,316,450,316,453,316,455,316,458,316,460,316,463,316,466,316,470,316,474,316,478,316,480,316,482,316,484,316,486,316,489,316,493,316,495,316,498,316,500,316,502,316,504,316,506,316,509,316,513,316,515,316,518,316,520,316,522,316,524,316,526,316,532,316,537,316,544,316,555,316,561,316,566],"svg",{"viewBox":309,"role":310,"ariaLabelledBy":311,"xmlns":314,"style":315},"142 0 476 252","img",[312,313],"gr-problem-t","gr-problem-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:476px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[318,319,320],"title",{"id":312},"Rows appended below a chart's fixed range",[322,323,324],"desc",{"id":313},"The sales sheet with months January to September. The chart's series cover rows 2 to 7, January to June; the appended rows 8 to 10, July to September, fall outside the range.",[326,327],"rect",{"x":328,"y":258,"width":329,"height":330,"fill":331},"142","476","252","#ffffff",[326,333],{"x":334,"y":335,"width":336,"height":337,"rx":258,"fill":338,"stroke":339,"style":340},"158.2","16.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[326,342],{"x":343,"y":335,"width":344,"height":337,"rx":258,"fill":338,"stroke":339,"style":340},"190.2","58.8",[346,347,351],"text",{"x":348,"y":349,"style":350},"219.6","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[326,353],{"x":354,"y":335,"width":355,"height":337,"rx":258,"fill":338,"stroke":339,"style":340},"249.0","53.4",[346,357,359],{"x":358,"y":349,"style":350},"275.6","B",[326,361],{"x":362,"y":335,"width":363,"height":337,"rx":258,"fill":338,"stroke":339,"style":340},"302.3","59.8",[346,365,367],{"x":366,"y":349,"style":350},"332.2","C",[326,369],{"x":334,"y":370,"width":336,"height":371,"rx":258,"fill":338,"stroke":339,"style":340},"40.0","28.0",[346,373,376],{"x":374,"y":375,"style":350},"174.2","59.0","1",[326,378],{"x":343,"y":370,"width":344,"height":371,"rx":258,"fill":379,"stroke":380,"style":381},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[346,383,385],{"x":348,"y":375,"style":384},"font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Month",[326,387],{"x":354,"y":370,"width":355,"height":371,"rx":258,"fill":379,"stroke":380,"style":381},[346,389,390],{"x":358,"y":375,"style":384},"Sales",[326,392],{"x":362,"y":370,"width":363,"height":371,"rx":258,"fill":379,"stroke":380,"style":381},[346,394,395],{"x":366,"y":375,"style":384},"Target",[326,397],{"x":334,"y":398,"width":336,"height":371,"rx":258,"fill":338,"stroke":339,"style":340},"68.0",[346,400,402],{"x":374,"y":401,"style":350},"87.0","2",[326,404],{"x":343,"y":398,"width":344,"height":371,"rx":258,"fill":405,"stroke":406,"style":381},"#d9f4f1","var(--teal,#0f9488)",[346,408,410],{"x":348,"y":401,"style":409},"font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Jan",[326,412],{"x":354,"y":398,"width":355,"height":371,"rx":258,"fill":405,"stroke":406,"style":381},[346,414,415],{"x":358,"y":401,"style":409},"120",[326,417],{"x":362,"y":398,"width":363,"height":371,"rx":258,"fill":405,"stroke":406,"style":381},[346,419,420],{"x":366,"y":401,"style":409},"110",[326,422],{"x":334,"y":423,"width":336,"height":371,"rx":258,"fill":338,"stroke":339,"style":340},"96.0",[346,425,427],{"x":374,"y":426,"style":350},"115.0","3",[326,429],{"x":343,"y":423,"width":344,"height":371,"rx":258,"fill":405,"stroke":406,"style":381},[346,431,432],{"x":348,"y":426,"style":409},"…",[326,434],{"x":354,"y":423,"width":355,"height":371,"rx":258,"fill":405,"stroke":406,"style":381},[346,436,432],{"x":358,"y":426,"style":409},[326,438],{"x":362,"y":423,"width":363,"height":371,"rx":258,"fill":405,"stroke":406,"style":381},[346,440,432],{"x":366,"y":426,"style":409},[326,442],{"x":334,"y":443,"width":336,"height":371,"rx":258,"fill":338,"stroke":339,"style":340},"124.0",[346,445,447],{"x":374,"y":446,"style":350},"143.0","4",[326,449],{"x":343,"y":443,"width":344,"height":371,"rx":258,"fill":405,"stroke":406,"style":381},[346,451,452],{"x":348,"y":446,"style":409},"Jun",[326,454],{"x":354,"y":443,"width":355,"height":371,"rx":258,"fill":405,"stroke":406,"style":381},[346,456,457],{"x":358,"y":446,"style":409},"158",[326,459],{"x":362,"y":443,"width":363,"height":371,"rx":258,"fill":405,"stroke":406,"style":381},[346,461,462],{"x":366,"y":446,"style":409},"135",[326,464],{"x":334,"y":465,"width":336,"height":371,"rx":258,"fill":338,"stroke":339,"style":340},"152.0",[346,467,469],{"x":374,"y":468,"style":350},"171.0","5",[326,471],{"x":343,"y":465,"width":344,"height":371,"rx":258,"fill":472,"stroke":473,"style":381},"#fdefd8","var(--gold,#b4740a)",[346,475,477],{"x":348,"y":468,"style":476},"font-size:10.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Jul",[326,479],{"x":354,"y":465,"width":355,"height":371,"rx":258,"fill":472,"stroke":473,"style":381},[346,481,164],{"x":358,"y":468,"style":476},[326,483],{"x":362,"y":465,"width":363,"height":371,"rx":258,"fill":472,"stroke":473,"style":381},[346,485,169],{"x":366,"y":468,"style":476},[326,487],{"x":334,"y":488,"width":336,"height":371,"rx":258,"fill":338,"stroke":339,"style":340},"180.0",[346,490,492],{"x":374,"y":491,"style":350},"199.0","6",[326,494],{"x":343,"y":488,"width":344,"height":371,"rx":258,"fill":472,"stroke":473,"style":381},[346,496,497],{"x":348,"y":491,"style":476},"Aug",[326,499],{"x":354,"y":488,"width":355,"height":371,"rx":258,"fill":472,"stroke":473,"style":381},[346,501,180],{"x":358,"y":491,"style":476},[326,503],{"x":362,"y":488,"width":363,"height":371,"rx":258,"fill":472,"stroke":473,"style":381},[346,505,185],{"x":366,"y":491,"style":476},[326,507],{"x":334,"y":508,"width":336,"height":371,"rx":258,"fill":338,"stroke":339,"style":340},"208.0",[346,510,512],{"x":374,"y":511,"style":350},"227.0","7",[326,514],{"x":343,"y":508,"width":344,"height":371,"rx":258,"fill":472,"stroke":473,"style":381},[346,516,517],{"x":348,"y":511,"style":476},"Sep",[326,519],{"x":354,"y":508,"width":355,"height":371,"rx":258,"fill":472,"stroke":473,"style":381},[346,521,195],{"x":358,"y":511,"style":476},[326,523],{"x":362,"y":508,"width":363,"height":371,"rx":258,"fill":472,"stroke":473,"style":381},[346,525,200],{"x":366,"y":511,"style":476},[46,527],{"x1":528,"y1":529,"x2":530,"y2":529,"stroke":380,"style":531},"370.1","126.0","402.1","stroke-width:2px",[533,534],"polygon",{"points":535,"fill":536},"406.1,126.0 397.1,131.0 397.1,121.0","#5b5cf0",[326,538],{"x":539,"y":540,"width":541,"height":542,"rx":543,"fill":472,"stroke":473,"style":531},"412.1","70.8","189.7","110.5","12",[545,546,548],"g",{"transform":547},"translate(495.0,83.8) scale(1.000)",[549,550],"path",{"d":551,"fill":552,"stroke":553,"style":554},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","none","var(--gold-ink,#7a4e06)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[346,556,560],{"x":557,"y":558,"style":559},"507.0","130.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Series: $B$2:$B$7",[346,562,565],{"x":557,"y":563,"style":564},"149.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","July–September are outside",[346,567,569],{"x":557,"y":568,"style":564},"167.2","the chart stops in June",[30,571,573],{"id":572},"step-2-extend-the-existing-charts-ranges","Step 2 — Extend the existing chart's ranges",[10,575,576,577,580,581,584,585,588],{},"Each series of a loaded openpyxl chart exposes its range formulas — ",[14,578,579],{},"val.numRef.f"," for values and ",[14,582,583],{},"cat.numRef.f"," or ",[14,586,587],{},"cat.strRef.f"," for categories — and they can be rewritten:",[35,590,592],{"className":77,"code":591,"language":79,"meta":40,"style":40},"import re\n\ndef extend_series(chart, last_row):\n    \"\"\"Point every series' values and categories at rows up to last_row.\"\"\"\n    def grow(ref):\n        return re.sub(r\"\\$(\\d+)$\", f\"${last_row}\", ref)\n    for s in chart.series:\n        s.val.numRef.f = grow(s.val.numRef.f)\n        if s.cat is not None:\n            target = s.cat.numRef or s.cat.strRef\n            target.f = grow(target.f)\n\nwb = load_workbook(\"sales_appended.xlsx\")\nws = wb[\"Data\"]\nfor chart in ws._charts:\n    extend_series(chart, ws.max_row)\nwb.save(\"sales_extended.xlsx\")\nprint([s.val.numRef.f for s in load_workbook(\"sales_extended.xlsx\")[\"Data\"]._charts[0].series])\n",[14,593,594,601,605,617,622,633,685,696,706,726,742,752,756,768,781,794,800,810],{"__ignoreMap":40},[44,595,596,598],{"class":46,"line":47},[44,597,94],{"class":86},[44,599,600],{"class":90}," re\n",[44,602,603],{"class":46,"line":100},[44,604,104],{"emptyLinePlaceholder":103},[44,606,607,610,614],{"class":46,"line":107},[44,608,609],{"class":86},"def",[44,611,613],{"class":612},"s_Opv"," extend_series",[44,615,616],{"class":90},"(chart, last_row):\n",[44,618,619],{"class":46,"line":125},[44,620,621],{"class":54},"    \"\"\"Point every series' values and categories at rows up to last_row.\"\"\"\n",[44,623,624,627,630],{"class":46,"line":142},[44,625,626],{"class":86},"    def",[44,628,629],{"class":612}," grow",[44,631,632],{"class":90},"(ref):\n",[44,634,635,638,641,644,647,651,654,657,660,662,664,667,670,674,677,680,682],{"class":46,"line":206},[44,636,637],{"class":86},"        return",[44,639,640],{"class":90}," re.sub(",[44,642,643],{"class":86},"r",[44,645,646],{"class":54},"\"",[44,648,650],{"class":649},"s_b0D","\\$",[44,652,653],{"class":163},"(\\d",[44,655,656],{"class":86},"+",[44,658,659],{"class":163},")$",[44,661,646],{"class":54},[44,663,160],{"class":90},[44,665,666],{"class":86},"f",[44,668,669],{"class":54},"\"$",[44,671,673],{"class":672},"sSjpA","{",[44,675,676],{"class":90},"last_row",[44,678,679],{"class":672},"}",[44,681,646],{"class":54},[44,683,684],{"class":90},", ref)\n",[44,686,687,690,692,694],{"class":46,"line":212},[44,688,689],{"class":86},"    for",[44,691,283],{"class":90},[44,693,151],{"class":86},[44,695,288],{"class":90},[44,697,698,701,703],{"class":46,"line":223},[44,699,700],{"class":90},"        s.val.numRef.f ",[44,702,113],{"class":86},[44,704,705],{"class":90}," grow(s.val.numRef.f)\n",[44,707,708,711,714,717,720,723],{"class":46,"line":228},[44,709,710],{"class":86},"        if",[44,712,713],{"class":90}," s.cat ",[44,715,716],{"class":86},"is",[44,718,719],{"class":86}," not",[44,721,722],{"class":163}," None",[44,724,725],{"class":90},":\n",[44,727,728,731,733,736,739],{"class":46,"line":247},[44,729,730],{"class":90},"            target ",[44,732,113],{"class":86},[44,734,735],{"class":90}," s.cat.numRef ",[44,737,738],{"class":86},"or",[44,740,741],{"class":90}," s.cat.strRef\n",[44,743,744,747,749],{"class":46,"line":263},[44,745,746],{"class":90},"            target.f ",[44,748,113],{"class":86},[44,750,751],{"class":90}," grow(target.f)\n",[44,753,754],{"class":46,"line":278},[44,755,104],{"emptyLinePlaceholder":103},[44,757,758,760,762,764,766],{"class":46,"line":291},[44,759,110],{"class":90},[44,761,113],{"class":86},[44,763,116],{"class":90},[44,765,218],{"class":54},[44,767,122],{"class":90},[44,769,771,773,775,777,779],{"class":46,"line":770},14,[44,772,128],{"class":90},[44,774,113],{"class":86},[44,776,133],{"class":90},[44,778,136],{"class":54},[44,780,139],{"class":90},[44,782,784,786,789,791],{"class":46,"line":783},15,[44,785,145],{"class":86},[44,787,788],{"class":90}," chart ",[44,790,151],{"class":86},[44,792,793],{"class":90}," ws._charts:\n",[44,795,797],{"class":46,"line":796},16,[44,798,799],{"class":90},"    extend_series(chart, ws.max_row)\n",[44,801,803,805,808],{"class":46,"line":802},17,[44,804,215],{"class":90},[44,806,807],{"class":54},"\"sales_extended.xlsx\"",[44,809,122],{"class":90},[44,811,813,815,818,820,822,824,826,828,830,832,835,837],{"class":46,"line":812},18,[44,814,266],{"class":163},[44,816,817],{"class":90},"([s.val.numRef.f ",[44,819,145],{"class":86},[44,821,283],{"class":90},[44,823,151],{"class":86},[44,825,116],{"class":90},[44,827,807],{"class":54},[44,829,240],{"class":90},[44,831,136],{"class":54},[44,833,834],{"class":90},"]._charts[",[44,836,258],{"class":163},[44,838,839],{"class":90},"].series])\n",[10,841,842,843,846,847,850,851,854,855,858,859,28],{},"The helper replaces the end row of each range — the digits after the last ",[14,844,845],{},"$"," — with the sheet's new last row, so ",[14,848,849],{},"'Data'!$B$2:$B$7"," becomes ",[14,852,853],{},"'Data'!$B$2:$B$10"," for both series and the category range follows. ",[14,856,857],{},"_charts"," is openpyxl's list of charts on a worksheet; it carries a leading underscore, so treat it as an implementation detail and pin your openpyxl version. One more caution: because openpyxl rebuilds charts from what it understands, a chart designed in Excel with formatting openpyxl does not model can lose some of it in the round trip — try it on a copy of the real workbook first, as described in ",[24,860,862],{"href":861},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fwhat-openpyxl-loses-when-saving-an-existing-workbook\u002F","What openpyxl Loses When Saving an Existing Workbook",[30,864,866],{"id":865},"step-3-or-rebuild-the-chart-from-the-datas-size","Step 3 — Or rebuild the chart from the data's size",[10,868,869],{},"For reports a script generates from scratch, the more robust habit is to create the chart after writing the data, with ranges computed from the data itself:",[35,871,873],{"className":77,"code":872,"language":79,"meta":40,"style":40},"import pandas as pd\n\ndf = pd.read_excel(\"sales_appended.xlsx\", sheet_name=\"Data\")\nwith pd.ExcelWriter(\"sales_rebuilt.xlsx\", engine=\"xlsxwriter\") as xw:\n    df.to_excel(xw, sheet_name=\"Data\", index=False)\n    book = xw.book\n    chart = book.add_chart({\"type\": \"line\"})\n    last = len(df)                                   # last data row, zero-based (header is row 0)\n    for col, colour in ((1, \"#4338CA\"), (2, \"#0F9488\")):\n        chart.add_series({\"name\": [\"Data\", 0, col], \"categories\": [\"Data\", 1, 0, last, 0],\n                          \"values\": [\"Data\", 1, col, last, col], \"line\": {\"color\": colour}})\n    chart.set_title({\"name\": \"Sales vs target\"})\n    xw.sheets[\"Data\"].insert_chart(\"E2\", chart)\nprint(\"chart covers\", len(df), \"months:\", df[\"Month\"].tolist()[0], \"to\", df[\"Month\"].tolist()[-1])\n",[14,874,875,888,892,916,945,968,978,1000,1017,1048,1091,1118,1132,1148],{"__ignoreMap":40},[44,876,877,879,882,885],{"class":46,"line":47},[44,878,94],{"class":86},[44,880,881],{"class":90}," pandas ",[44,883,884],{"class":86},"as",[44,886,887],{"class":90}," pd\n",[44,889,890],{"class":46,"line":100},[44,891,104],{"emptyLinePlaceholder":103},[44,893,894,897,899,902,904,906,910,912,914],{"class":46,"line":107},[44,895,896],{"class":90},"df ",[44,898,113],{"class":86},[44,900,901],{"class":90}," pd.read_excel(",[44,903,218],{"class":54},[44,905,160],{"class":90},[44,907,909],{"class":908},"sa561","sheet_name",[44,911,113],{"class":86},[44,913,136],{"class":54},[44,915,122],{"class":90},[44,917,918,921,924,927,929,932,934,937,940,942],{"class":46,"line":125},[44,919,920],{"class":86},"with",[44,922,923],{"class":90}," pd.ExcelWriter(",[44,925,926],{"class":54},"\"sales_rebuilt.xlsx\"",[44,928,160],{"class":90},[44,930,931],{"class":908},"engine",[44,933,113],{"class":86},[44,935,936],{"class":54},"\"xlsxwriter\"",[44,938,939],{"class":90},") ",[44,941,884],{"class":86},[44,943,944],{"class":90}," xw:\n",[44,946,947,950,952,954,956,958,961,963,966],{"class":46,"line":142},[44,948,949],{"class":90},"    df.to_excel(xw, ",[44,951,909],{"class":908},[44,953,113],{"class":86},[44,955,136],{"class":54},[44,957,160],{"class":90},[44,959,960],{"class":908},"index",[44,962,113],{"class":86},[44,964,965],{"class":163},"False",[44,967,122],{"class":90},[44,969,970,973,975],{"class":46,"line":206},[44,971,972],{"class":90},"    book ",[44,974,113],{"class":86},[44,976,977],{"class":90}," xw.book\n",[44,979,980,983,985,988,991,994,997],{"class":46,"line":212},[44,981,982],{"class":90},"    chart ",[44,984,113],{"class":86},[44,986,987],{"class":90}," book.add_chart({",[44,989,990],{"class":54},"\"type\"",[44,992,993],{"class":90},": ",[44,995,996],{"class":54},"\"line\"",[44,998,999],{"class":90},"})\n",[44,1001,1002,1005,1007,1010,1013],{"class":46,"line":223},[44,1003,1004],{"class":90},"    last ",[44,1006,113],{"class":86},[44,1008,1009],{"class":163}," len",[44,1011,1012],{"class":90},"(df)                                   ",[44,1014,1016],{"class":1015},"s-wDw","# last data row, zero-based (header is row 0)\n",[44,1018,1019,1021,1024,1026,1029,1031,1033,1036,1038,1040,1042,1045],{"class":46,"line":228},[44,1020,689],{"class":86},[44,1022,1023],{"class":90}," col, colour ",[44,1025,151],{"class":86},[44,1027,1028],{"class":90}," ((",[44,1030,376],{"class":163},[44,1032,160],{"class":90},[44,1034,1035],{"class":54},"\"#4338CA\"",[44,1037,172],{"class":90},[44,1039,402],{"class":163},[44,1041,160],{"class":90},[44,1043,1044],{"class":54},"\"#0F9488\"",[44,1046,1047],{"class":90},")):\n",[44,1049,1050,1053,1056,1059,1061,1063,1065,1068,1071,1073,1075,1077,1079,1081,1083,1086,1088],{"class":46,"line":247},[44,1051,1052],{"class":90},"        chart.add_series({",[44,1054,1055],{"class":54},"\"name\"",[44,1057,1058],{"class":90},": [",[44,1060,136],{"class":54},[44,1062,160],{"class":90},[44,1064,258],{"class":163},[44,1066,1067],{"class":90},", col], ",[44,1069,1070],{"class":54},"\"categories\"",[44,1072,1058],{"class":90},[44,1074,136],{"class":54},[44,1076,160],{"class":90},[44,1078,376],{"class":163},[44,1080,160],{"class":90},[44,1082,258],{"class":163},[44,1084,1085],{"class":90},", last, ",[44,1087,258],{"class":163},[44,1089,1090],{"class":90},"],\n",[44,1092,1093,1096,1098,1100,1102,1104,1107,1109,1112,1115],{"class":46,"line":263},[44,1094,1095],{"class":54},"                          \"values\"",[44,1097,1058],{"class":90},[44,1099,136],{"class":54},[44,1101,160],{"class":90},[44,1103,376],{"class":163},[44,1105,1106],{"class":90},", col, last, col], ",[44,1108,996],{"class":54},[44,1110,1111],{"class":90},": {",[44,1113,1114],{"class":54},"\"color\"",[44,1116,1117],{"class":90},": colour}})\n",[44,1119,1120,1123,1125,1127,1130],{"class":46,"line":278},[44,1121,1122],{"class":90},"    chart.set_title({",[44,1124,1055],{"class":54},[44,1126,993],{"class":90},[44,1128,1129],{"class":54},"\"Sales vs target\"",[44,1131,999],{"class":90},[44,1133,1134,1137,1139,1142,1145],{"class":46,"line":291},[44,1135,1136],{"class":90},"    xw.sheets[",[44,1138,136],{"class":54},[44,1140,1141],{"class":90},"].insert_chart(",[44,1143,1144],{"class":54},"\"E2\"",[44,1146,1147],{"class":90},", chart)\n",[44,1149,1150,1152,1154,1157,1159,1162,1165,1168,1171,1174,1177,1179,1182,1185,1187,1189,1191,1194,1196],{"class":46,"line":770},[44,1151,266],{"class":163},[44,1153,269],{"class":90},[44,1155,1156],{"class":54},"\"chart covers\"",[44,1158,160],{"class":90},[44,1160,1161],{"class":163},"len",[44,1163,1164],{"class":90},"(df), ",[44,1166,1167],{"class":54},"\"months:\"",[44,1169,1170],{"class":90},", df[",[44,1172,1173],{"class":54},"\"Month\"",[44,1175,1176],{"class":90},"].tolist()[",[44,1178,258],{"class":163},[44,1180,1181],{"class":90},"], ",[44,1183,1184],{"class":54},"\"to\"",[44,1186,1170],{"class":90},[44,1188,1173],{"class":54},[44,1190,1176],{"class":90},[44,1192,1193],{"class":86},"-",[44,1195,376],{"class":163},[44,1197,1198],{"class":90},"])\n",[10,1200,1201,1202,1205,1206,1209,1210,28],{},"The ranges come from ",[14,1203,1204],{},"len(df)",", so the chart always covers exactly the rows written — nine months now, twelve in December, and never a stale range. There is no round trip through a reader, so nothing is lost. This is the approach to prefer whenever the workbook is a ",[19,1207,1208],{},"generated report"," rather than a file people maintain by hand. Building line charts is covered in ",[24,1211,1213],{"href":1212},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-line-chart-to-excel-report-with-python\u002F","Add a Line Chart to an Excel Report with Python",[307,1215,316,1221,316,1224,316,1227,316,1231,316,1236,316,1243,316,1248,316,1252,316,1256,316,1259,316,1262,316,1270,316,1275,316,1281,316,1288,316,1293,316,1297,316,1299,316,1302,316,1306,316,1309,316,1312,316,1318,316,1321,316,1324,316,1326,316,1329,316,1333,316,1336,316,1339,316,1345,316,1348],{"viewBox":1216,"role":310,"ariaLabelledBy":1217,"xmlns":314,"style":1220},"4 0 752 272",[1218,1219],"gr-choose-t","gr-choose-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[318,1222,1223],{"id":1218},"Extend the chart or rebuild it?",[322,1225,1226],{"id":1219},"If the workbook is generated by the script, rebuild the chart from the data size every run. If people maintain the workbook and its chart formatting must be kept, extend the existing chart's ranges with openpyxl after testing the round trip. If people add rows live in Excel, base the chart on an Excel table.",[326,1228],{"x":447,"y":258,"width":1229,"height":1230,"fill":331},"752","272",[326,1232],{"x":1233,"y":335,"width":1234,"height":1235,"rx":543,"fill":379,"stroke":380,"style":531},"240.0","280.0","75.5",[545,1237,1239],{"transform":1238},"translate(368.0,29.0) scale(1.000)",[549,1240],{"d":1241,"fill":552,"stroke":1242,"style":554},"M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z M2.5 20c0-3.6 2.9-6 6.5-6s6.5 2.4 6.5 6 M16 4.5a3.3 3.3 0 0 1 0 6.3 M18 14.3c2.2.7 3.5 2.8 3.5 5.7","var(--brand-strong,#4338ca)",[346,1244,1247],{"x":1245,"y":1235,"style":1246},"380.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Who owns the workbook?",[46,1249],{"x1":1245,"y1":1250,"x2":1245,"y2":1251,"stroke":380,"style":531},"91.5","111.5",[46,1253],{"x1":1254,"y1":1251,"x2":1255,"y2":1251,"stroke":380,"style":531},"134.7","625.3",[46,1257],{"x1":1254,"y1":1251,"x2":1254,"y2":1258,"stroke":380,"style":531},"157.5",[533,1260],{"points":1261,"fill":536},"134.7,161.5 129.7,152.5 139.7,152.5",[326,1263],{"x":1264,"y":1265,"width":1266,"height":1267,"rx":1268,"fill":331,"stroke":339,"style":1269},"97.6","121.5","74.1","21.0","10","stroke-width:1.2px",[346,1271,1274],{"x":1254,"y":1272,"style":1273},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","the script",[326,1276],{"x":1277,"y":1278,"width":1279,"height":1280,"rx":543,"fill":405,"stroke":406,"style":531},"20.0","163.5","229.3","93.0",[545,1282,1284],{"transform":1283},"translate(122.7,176.5) scale(1.000)",[549,1285],{"d":1286,"fill":552,"stroke":1287,"style":554},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7","var(--teal-ink,#0b6157)",[346,1289,1292],{"x":1254,"y":1290,"style":1291},"223.0","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Rebuild every run",[346,1294,1296],{"x":1254,"y":1295,"style":564},"242.5","ranges from len(df)",[46,1298],{"x1":1245,"y1":1251,"x2":1245,"y2":1258,"stroke":380,"style":531},[533,1300],{"points":1301,"fill":536},"380.0,161.5 375.0,152.5 385.0,152.5",[326,1303],{"x":1304,"y":1265,"width":1305,"height":1267,"rx":1268,"fill":331,"stroke":339,"style":1269},"326.9","106.3",[346,1307,1308],{"x":1245,"y":1272,"style":1273},"people + script",[326,1310],{"x":1311,"y":1278,"width":1279,"height":1280,"rx":543,"fill":379,"stroke":380,"style":531},"265.3",[545,1313,1315],{"transform":1314},"translate(368.0,176.5) scale(1.000)",[549,1316],{"d":1317,"fill":552,"stroke":1242,"style":554},"M4 20h16 M4 20V4 M6 16l4-5 4 3 6-7",[346,1319,1320],{"x":1245,"y":1290,"style":1246},"Extend existing ranges",[346,1322,1323],{"x":1245,"y":1295,"style":564},"openpyxl series refs",[46,1325],{"x1":1255,"y1":1251,"x2":1255,"y2":1258,"stroke":380,"style":531},[533,1327],{"points":1328,"fill":536},"625.3,161.5 620.3,152.5 630.3,152.5",[326,1330],{"x":1331,"y":1265,"width":1332,"height":1267,"rx":1268,"fill":331,"stroke":339,"style":1269},"572.0","106.7",[346,1334,1335],{"x":1255,"y":1272,"style":1273},"people in Excel",[326,1337],{"x":1338,"y":1278,"width":1279,"height":1280,"rx":543,"fill":472,"stroke":473,"style":531},"510.7",[545,1340,1342],{"transform":1341},"translate(613.3,176.5) scale(1.000)",[549,1343],{"d":1344,"fill":552,"stroke":553,"style":554},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14",[346,1346,1347],{"x":1255,"y":1290,"style":559},"Chart on an Excel table",[346,1349,1350],{"x":1255,"y":1295,"style":564},"Excel adjusts it",[30,1352,1354],{"id":1353},"step-4-workbooks-people-edit-live-put-the-data-in-a-table","Step 4 — Workbooks people edit live: put the data in a table",[10,1356,1357,1358,1361],{},"When people add rows in Excel, the most reliable mechanism is Excel's own: make the data an Excel table and build the chart on it. When a row is added to the table in Excel, Excel extends the table and adjusts charts based on it. A script that adds rows ",[19,1359,1360],{},"through Excel"," — with xlwings, for example — gets the same behaviour:",[35,1363,1365],{"className":77,"code":1364,"language":79,"meta":40,"style":40},"import xlwings as xw\n\nwb = xw.Book(\"sales_live.xlsx\")\ntable = wb.sheets[\"Data\"].tables[\"Sales\"]\nnext_row = table.range.last_cell.offset(1, 0)\nnext_row.resize(1, 3).value = [\"Oct\", 176, 155]      # typed just below the table: Excel extends it\nprint(table.range.address)\n",[14,1366,1367,1379,1383,1397,1417,1435,1473],{"__ignoreMap":40},[44,1368,1369,1371,1374,1376],{"class":46,"line":47},[44,1370,94],{"class":86},[44,1372,1373],{"class":90}," xlwings ",[44,1375,884],{"class":86},[44,1377,1378],{"class":90}," xw\n",[44,1380,1381],{"class":46,"line":100},[44,1382,104],{"emptyLinePlaceholder":103},[44,1384,1385,1387,1389,1392,1395],{"class":46,"line":107},[44,1386,110],{"class":90},[44,1388,113],{"class":86},[44,1390,1391],{"class":90}," xw.Book(",[44,1393,1394],{"class":54},"\"sales_live.xlsx\"",[44,1396,122],{"class":90},[44,1398,1399,1402,1404,1407,1409,1412,1415],{"class":46,"line":125},[44,1400,1401],{"class":90},"table ",[44,1403,113],{"class":86},[44,1405,1406],{"class":90}," wb.sheets[",[44,1408,136],{"class":54},[44,1410,1411],{"class":90},"].tables[",[44,1413,1414],{"class":54},"\"Sales\"",[44,1416,139],{"class":90},[44,1418,1419,1422,1424,1427,1429,1431,1433],{"class":46,"line":142},[44,1420,1421],{"class":90},"next_row ",[44,1423,113],{"class":86},[44,1425,1426],{"class":90}," table.range.last_cell.offset(",[44,1428,376],{"class":163},[44,1430,160],{"class":90},[44,1432,258],{"class":163},[44,1434,122],{"class":90},[44,1436,1437,1440,1442,1444,1446,1449,1451,1454,1457,1459,1462,1464,1467,1470],{"class":46,"line":206},[44,1438,1439],{"class":90},"next_row.resize(",[44,1441,376],{"class":163},[44,1443,160],{"class":90},[44,1445,427],{"class":163},[44,1447,1448],{"class":90},").value ",[44,1450,113],{"class":86},[44,1452,1453],{"class":90}," [",[44,1455,1456],{"class":54},"\"Oct\"",[44,1458,160],{"class":90},[44,1460,1461],{"class":163},"176",[44,1463,160],{"class":90},[44,1465,1466],{"class":163},"155",[44,1468,1469],{"class":90},"]      ",[44,1471,1472],{"class":1015},"# typed just below the table: Excel extends it\n",[44,1474,1475,1477],{"class":46,"line":212},[44,1476,266],{"class":163},[44,1478,1479],{"class":90},"(table.range.address)\n",[10,1481,1482,1483,1486,1487,1491],{},"Writing directly below a table in a live Excel session triggers Excel's automatic table expansion, and the chart follows. A script that edits the ",[19,1484,1485],{},"file"," with openpyxl does not get this: it has to resize the table itself, as in ",[24,1488,1490],{"href":1489},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fappend-rows-to-an-excel-table-and-resize-it\u002F","Append Rows to an Excel Table and Resize It",", and then still update the chart's ranges as in Step 2, because the chart stores plain cell ranges in the file.",[30,1493,1495],{"id":1494},"step-5-why-dynamic-named-ranges-are-a-poor-fit-for-generated-files","Step 5 — Why dynamic named ranges are a poor fit for generated files",[10,1497,1498,1499,1502,1503,1506,1507,1510,1511,1514,1515,1517],{},"Classic Excel advice is a named range defined with ",[14,1500,1501],{},"OFFSET"," and ",[14,1504,1505],{},"COUNTA"," — ",[14,1508,1509],{},"=OFFSET(Data!$B$2,0,0,COUNTA(Data!$A:$A)-1,1)"," — used as a series' values. Created in Excel, it works there. Generated from Python it is fragile: chart series that reference workbook-level names are displayed by Excel as ",[14,1512,1513],{},"='Workbook.xlsx'!Name",", tying them to the file name, XlsxWriter warns that it cannot validate such references, and ",[14,1516,1501],{}," is volatile, recalculating on every edit. Updating plain ranges from Python, or rebuilding the chart, gives the same result without these side effects.",[30,1519,1521],{"id":1520},"step-6-test-that-the-chart-covers-the-data","Step 6 — Test that the chart covers the data",[10,1523,1524],{},"Whichever approach you use, a two-line test prevents a stale chart from shipping:",[35,1526,1528],{"className":77,"code":1527,"language":79,"meta":40,"style":40},"def chart_covers_data(path, sheet=\"Data\"):\n    ws = load_workbook(path)[sheet]\n    last = ws.max_row\n    ends = {int(re.search(r\"\\$(\\d+)$\", s.val.numRef.f).group(1)) for c in ws._charts for s in c.series}\n    return ends == {last}, last, ends\n\nfor f in (\"sales_appended.xlsx\", \"sales_extended.xlsx\", \"sales_rebuilt.xlsx\"):\n    print(f\"{f:22}\", chart_covers_data(f))\n",[14,1529,1530,1547,1557,1566,1623,1637,1641,1665],{"__ignoreMap":40},[44,1531,1532,1534,1537,1540,1542,1544],{"class":46,"line":47},[44,1533,609],{"class":86},[44,1535,1536],{"class":612}," chart_covers_data",[44,1538,1539],{"class":90},"(path, sheet",[44,1541,113],{"class":86},[44,1543,136],{"class":54},[44,1545,1546],{"class":90},"):\n",[44,1548,1549,1552,1554],{"class":46,"line":100},[44,1550,1551],{"class":90},"    ws ",[44,1553,113],{"class":86},[44,1555,1556],{"class":90}," load_workbook(path)[sheet]\n",[44,1558,1559,1561,1563],{"class":46,"line":107},[44,1560,1004],{"class":90},[44,1562,113],{"class":86},[44,1564,1565],{"class":90}," ws.max_row\n",[44,1567,1568,1571,1573,1576,1579,1582,1584,1586,1588,1590,1592,1594,1596,1599,1601,1604,1606,1609,1611,1614,1616,1618,1620],{"class":46,"line":125},[44,1569,1570],{"class":90},"    ends ",[44,1572,113],{"class":86},[44,1574,1575],{"class":90}," {",[44,1577,1578],{"class":163},"int",[44,1580,1581],{"class":90},"(re.search(",[44,1583,643],{"class":86},[44,1585,646],{"class":54},[44,1587,650],{"class":649},[44,1589,653],{"class":163},[44,1591,656],{"class":86},[44,1593,659],{"class":163},[44,1595,646],{"class":54},[44,1597,1598],{"class":90},", s.val.numRef.f).group(",[44,1600,376],{"class":163},[44,1602,1603],{"class":90},")) ",[44,1605,145],{"class":86},[44,1607,1608],{"class":90}," c ",[44,1610,151],{"class":86},[44,1612,1613],{"class":90}," ws._charts ",[44,1615,145],{"class":86},[44,1617,283],{"class":90},[44,1619,151],{"class":86},[44,1621,1622],{"class":90}," c.series}\n",[44,1624,1625,1628,1631,1634],{"class":46,"line":142},[44,1626,1627],{"class":86},"    return",[44,1629,1630],{"class":90}," ends ",[44,1632,1633],{"class":86},"==",[44,1635,1636],{"class":90}," {last}, last, ends\n",[44,1638,1639],{"class":46,"line":206},[44,1640,104],{"emptyLinePlaceholder":103},[44,1642,1643,1645,1648,1650,1653,1655,1657,1659,1661,1663],{"class":46,"line":212},[44,1644,145],{"class":86},[44,1646,1647],{"class":90}," f ",[44,1649,151],{"class":86},[44,1651,1652],{"class":90}," (",[44,1654,218],{"class":54},[44,1656,160],{"class":90},[44,1658,807],{"class":54},[44,1660,160],{"class":90},[44,1662,926],{"class":54},[44,1664,1546],{"class":90},[44,1666,1667,1669,1671,1673,1675,1677,1679,1682,1684,1686],{"class":46,"line":223},[44,1668,294],{"class":163},[44,1670,269],{"class":90},[44,1672,666],{"class":86},[44,1674,646],{"class":54},[44,1676,673],{"class":672},[44,1678,666],{"class":90},[44,1680,1681],{"class":86},":22",[44,1683,679],{"class":672},[44,1685,646],{"class":54},[44,1687,1688],{"class":90},", chart_covers_data(f))\n",[10,1690,1691],{},"The appended-only file fails — its series end at row 7 while the data ends at row 10 — and both fixed files pass. Run a check like this after every update; it costs milliseconds and catches the chart that quietly stopped growing three months ago.",[307,1693,316,1699,316,1702,316,1705,316,1708,316,1711,316,1717,316,1723,316,1728,316,1731,316,1735,316,1738,316,1743,316,1745,316,1749,316,1751],{"viewBox":1694,"role":310,"ariaLabelledBy":1695,"xmlns":314,"style":1698},"4 0 684 140",[1696,1697],"gr-test-t","gr-test-d","width:100%;max-width:684px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[318,1700,1701],{"id":1696},"Last row covered by the chart versus last row of data",[322,1703,1704],{"id":1697},"In the appended file the chart's series end at row 7 while the data ends at row 10. In the extended and rebuilt files the series end at row 10, matching the data.",[326,1706],{"x":447,"y":258,"width":1707,"height":169,"fill":331},"684",[326,1709],{"x":1277,"y":335,"width":1710,"height":371,"rx":512,"fill":338,"stroke":339,"style":340},"131.0",[346,1712,1716],{"x":1713,"y":1714,"style":1715},"85.5","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","appended only",[326,1718],{"x":1719,"y":335,"width":1720,"height":371,"rx":512,"fill":1721,"stroke":1722,"style":381},"161.0","280.1","#fce9e9","var(--danger,#dc2626)",[346,1724,1727],{"x":1725,"y":1714,"style":1726},"449.0","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","ends at row 7 · data row 10",[326,1729],{"x":1277,"y":1730,"width":1710,"height":371,"rx":512,"fill":338,"stroke":339,"style":340},"56.0",[346,1732,1734],{"x":1713,"y":1733,"style":1715},"74.5","series extended",[326,1736],{"x":1719,"y":1730,"width":1737,"height":371,"rx":512,"fill":405,"stroke":406,"style":381},"400.1",[346,1739,1742],{"x":1740,"y":1733,"style":1741},"569.0","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","ends at row 10 ✓",[326,1744],{"x":1277,"y":423,"width":1710,"height":371,"rx":512,"fill":338,"stroke":339,"style":340},[346,1746,1748],{"x":1713,"y":1747,"style":1715},"114.5","chart rebuilt",[326,1750],{"x":1719,"y":423,"width":1737,"height":371,"rx":512,"fill":405,"stroke":406,"style":381},[346,1752,1742],{"x":1740,"y":1747,"style":1741},[30,1754,1756],{"id":1755},"common-pitfalls","Common pitfalls",[1758,1759,1760,1768,1774,1780,1786,1792],"ul",{},[1761,1762,1763,1767],"li",{},[1764,1765,1766],"strong",{},"Assuming charts follow appended rows."," Ranges are fixed in the file; update or rebuild them.",[1761,1769,1770,1773],{},[1764,1771,1772],{},"Updating values but not categories."," Extend the category range too, or new points get no labels.",[1761,1775,1776,1779],{},[1764,1777,1778],{},"Relying on table auto-expansion from a script."," That is an Excel interface feature; scripts must resize tables and charts themselves.",[1761,1781,1782,1785],{},[1764,1783,1784],{},"Round-tripping hand-made charts."," openpyxl may drop formatting it does not model; test on a copy.",[1761,1787,1788,1791],{},[1764,1789,1790],{},"Hard-coding row numbers."," Derive the last row from the data each run.",[1761,1793,1794,1797],{},[1764,1795,1796],{},"Trailing blank or total rows."," If the sheet ends with a total row, stop the series one row above it, or the total becomes a spike at the end of the line.",[30,1799,1801],{"id":1800},"conclusion","Conclusion",[10,1803,1804,1805,1807],{},"Excel charts store fixed ranges, so rows appended by a script fall outside them. For generated reports, rebuild the chart every run with ranges computed from the data's size; for workbooks people maintain, extend each series' value and category ranges with openpyxl after checking the round trip keeps the chart's formatting; for live editing, keep the data in an Excel table and let Excel extend it. Avoid ",[14,1806,1501],{}," named ranges in generated files, and test that the chart's last row matches the data's.",[30,1809,1811],{"id":1810},"frequently-asked-questions","Frequently asked questions",[10,1813,1814,1817],{},[1764,1815,1816],{},"Why doesn't my Excel chart include rows added by Python?","\nEach chart series stores a fixed cell range such as Data!$B$2:$B$7. Appending rows below it does not change the range, so the chart has to be updated or rebuilt by the script.",[10,1819,1820,1823],{},[1764,1821,1822],{},"How do I extend an existing chart's range with openpyxl?","\nLoad the workbook, go through the worksheet's charts and their series, and rewrite val.numRef.f and the category reference so their end row is the sheet's new last row, then save.",[10,1825,1826,1829],{},[1764,1827,1828],{},"Do charts based on Excel tables update automatically?","\nWhen rows are added to the table inside Excel, Excel extends the table and adjusts charts that use it. Files edited by scripts do not get this automatically; resize the table and update the chart ranges in the script.",[10,1831,1832,1835],{},[1764,1833,1834],{},"Should I use OFFSET named ranges for dynamic charts in generated files?","\nThey work when built in Excel, but from Python they are brittle: name references in chart series are tied to the file name, libraries cannot validate them, and OFFSET is volatile. Updating ranges or rebuilding the chart is more reliable.",[30,1837,1839],{"id":1838},"related","Related",[1758,1841,1842,1848,1855,1862],{},[1761,1843,1844,1845,1847],{},"Up one level: ",[24,1846,27],{"href":26}," — native charts from Python.",[1761,1849,1850,1854],{},[24,1851,1853],{"href":1852},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-one-chart-per-group-with-openpyxl\u002F","Create One Chart per Group with openpyxl"," — generating many charts from data.",[1761,1856,1857,1861],{},[24,1858,1860],{"href":1859},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fopenpyxl-append-data-to-existing-excel-sheet\u002F","openpyxl: Append Data to an Existing Excel Sheet"," — appending rows safely.",[1761,1863,1864,1868],{},[24,1865,1867],{"href":1866},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph\u002F","Add a Chart to an Excel Online Workbook with Microsoft Graph"," — setData for cloud workbooks.",[1870,1871,1872],"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 .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}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":40,"searchDepth":100,"depth":100,"links":1874},[1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885],{"id":32,"depth":100,"text":33},{"id":73,"depth":100,"text":74},{"id":572,"depth":100,"text":573},{"id":865,"depth":100,"text":866},{"id":1353,"depth":100,"text":1354},{"id":1494,"depth":100,"text":1495},{"id":1520,"depth":100,"text":1521},{"id":1755,"depth":100,"text":1756},{"id":1800,"depth":100,"text":1801},{"id":1810,"depth":100,"text":1811},{"id":1838,"depth":100,"text":1839},"2026-10-02","A chart points at fixed cell ranges, so rows appended by a script fall outside it. See the problem in a saved file, extend every series of an existing chart to the new last row with openpyxl, rebuild the chart from the data's current size instead, let Excel tables carry the chart when rows are added inside Excel, and know why dynamic named ranges are brittle in generated files.","md",[1890,1892,1894,1896],{"q":1816,"a":1891},"Each chart series stores a fixed cell range such as Data!$B$2:$B$7. Appending rows below it does not change the range, so the chart has to be updated or rebuilt by the script.",{"q":1822,"a":1893},"Load the workbook, go through the worksheet's charts and their series, and rewrite val.numRef.f and the category reference so their end row is the sheet's new last row, then save.",{"q":1828,"a":1895},"When rows are added to the table inside Excel, Excel extends the table and adjusts charts that use it. Files edited by scripts do not get this automatically; resize the table and update the chart ranges in the script.",{"q":1834,"a":1897},"They work when built in Excel, but from Python they are brittle: name references in chart series are tied to the file name, libraries cannot validate them, and OFFSET is volatile. Updating ranges or rebuilding the chart is more reliable.","make an excel chart grow with its data",null,{"breadcrumb":1901},[1902,1905,1908],{"name":1903,"item":1904},"Home","\u002F",{"name":1906,"item":1907},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002F",{"name":27,"item":26},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fmake-an-excel-chart-grow-with-its-data",{"title":1911,"description":1912},"Make an Excel Chart Expand When Rows Are Added (Python)","Keep Excel charts in step with appended data from Python: why chart ranges stay fixed, updating series numRef\u002FstrRef ranges with openpyxl, rebuilding charts from the DataFrame size, tables and xlwings for live workbooks, and the limits of OFFSET named ranges.","make-an-excel-chart-grow-with-its-data","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fmake-an-excel-chart-grow-with-its-data\u002Findex",[1916,1917],"charts-and-images","tables-and-filters","how-to","tmAlneq_RJzGlpM-eCY5gwlFyOkkOZ3m86Eqo8lzNwo",[1921,1925],{"title":1922,"path":1923,"stem":1924,"children":-1},"Format Chart Axes with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fformat-chart-axes-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fformat-chart-axes-with-openpyxl\u002Findex",{"title":1926,"path":1927,"stem":1928,"children":-1},"Plot a Chart from Non-Adjacent Columns","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fplot-a-chart-from-non-adjacent-columns","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fplot-a-chart-from-non-adjacent-columns\u002Findex",[1930,1932],{"path":1931,"title":27},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl",{"path":1933,"title":1906},"\u002Fformatting-and-charting-excel-reports-with-python",[],[1936,1939,1942,1945,1948,1951],{"path":1937,"title":1938,"kind":1918},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fbuild-a-dashboard-sheet-with-charts-from-multiple-tabs","Build a Dashboard Sheet with Charts from Multiple Tabs",{"path":1940,"title":1941,"kind":1918},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fbuild-a-rolling-12-month-chart-on-an-excel-dashboard","Build a Rolling 12-Month Chart on an Excel Dashboard",{"path":1943,"title":1944,"kind":1918},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fkeep-charts-and-images-when-filling-an-excel-template","Keep Charts and Images When Filling an Excel Template",{"path":1946,"title":1947,"kind":1918},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fadd-a-chart-to-an-excel-file-with-xlsxwriter","Add a Chart to an Excel File with xlsxwriter",{"path":1949,"title":1950,"kind":1918},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fadd-sparklines-to-an-excel-report-with-xlsxwriter","Add Sparklines to an Excel Report with xlsxwriter",{"path":1952,"title":1953,"kind":1918},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fbuild-a-waterfall-chart-in-excel-with-python","Build a Waterfall Chart in Excel with Python",1790972111345]