[{"data":1,"prerenderedAt":2002},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fplot-a-chart-from-non-adjacent-columns":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fplot-a-chart-from-non-adjacent-columns":1969,"ancestors:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fplot-a-chart-from-non-adjacent-columns":1977,"children:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fplot-a-chart-from-non-adjacent-columns":1982,"related:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fplot-a-chart-from-non-adjacent-columns":1983},{"id":4,"title":5,"body":6,"dateModified":1936,"datePublished":1936,"description":1937,"extension":1938,"faq":1939,"keyword":1948,"kind":1949,"meta":1950,"navigation":141,"path":1959,"seo":1960,"slug":1963,"stem":1964,"topics":1965,"type":1967,"__hash__":1968},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fplot-a-chart-from-non-adjacent-columns\u002Findex.md","Plot a Chart from Non-Adjacent Columns",{"type":7,"value":8,"toc":1924},"minimark",[9,24,29,60,96,100,427,457,479,682,686,878,885,889,1186,1205,1276,1280,1283,1441,1448,1580,1584,1732,1750,1753,1757,1801,1805,1824,1828,1845,1859,1871,1887,1891,1920],[10,11,12,13,17,18,23],"p",{},"Report sheets are rarely laid out for charting. The sheet has a date column, then units, revenue, cost, margin, forecast, budget and variance, and the chart needs only three of them: the date along the axis, and actual revenue and forecast as two lines. In Excel you would hold Ctrl and select the columns. In Python the obvious approach — one ",[14,15,16],"code",{},"Reference"," covering a block of columns — takes everything in between too. The fix is to build the chart one series at a time, each from its own column. This page does that with openpyxl and with XlsxWriter, takes each series' name from its header cell, finds the columns by their headings so the code survives a new column being inserted, deals with hidden columns between the ones being charted, and checks the result by reading the series references out of the chart. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002F","Creating Charts in Excel with openpyxl",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl xlsxwriter pandas\n","bash","",[14,37,38],{"__ignoreMap":35},[39,40,43,47,51,54,57],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sMTad","pip",[39,48,50],{"class":49},"srMev"," install",[39,52,53],{"class":49}," openpyxl",[39,55,56],{"class":49}," xlsxwriter",[39,58,59],{"class":49}," pandas\n",[10,61,62,63,67,68,71,72,75,76,79,80,83,84,87,88,91,92,95],{},"The workbook has one sheet with eight columns: ",[64,65,66],"em",{},"Month"," (A), ",[64,69,70],{},"Units"," (B), ",[64,73,74],{},"Revenue"," (C), ",[64,77,78],{},"Cost"," (D), ",[64,81,82],{},"Margin"," (E), ",[64,85,86],{},"Forecast"," (F), ",[64,89,90],{},"Budget"," (G) and ",[64,93,94],{},"Variance"," (H), for twelve months. The chart should show revenue and forecast by month, plus budget as a third line — columns A, C, F and G.",[25,97,99],{"id":98},"step-1-one-series-per-column-with-openpyxl","Step 1 — One series per column with openpyxl",[30,101,105],{"className":102,"code":103,"language":104,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import load_workbook\nfrom openpyxl.chart import LineChart, Reference, Series\n\nwb = load_workbook(\"monthly_report.xlsx\")\nws = wb.active\nlast = ws.max_row\n\nchart = LineChart()\nfor col in (3, 6, 7):                                       # Revenue, Forecast, Budget\n    values = Reference(ws, min_col=col, min_row=1, max_row=last)\n    chart.series.append(Series(values, title_from_data=True))\nchart.set_categories(Reference(ws, min_col=1, min_row=2, max_row=last))\nchart.title = \"Revenue against forecast and budget\"\nchart.y_axis.numFmt = \"£#,##0\"\nchart.x_axis.number_format = \"mmm yy\"\nchart.x_axis.delete = chart.y_axis.delete = False\nchart.width, chart.height = 18, 9\nws.add_chart(chart, \"J2\")\nwb.save(\"report_with_chart.xlsx\")\nprint([s.tx.strRef.f for s in chart.series])\n","python",[14,106,107,123,136,143,161,172,183,188,199,236,274,291,321,332,343,354,370,386,397,408],{"__ignoreMap":35},[39,108,109,113,117,120],{"class":41,"line":42},[39,110,112],{"class":111},"s-kum","from",[39,114,116],{"class":115},"skGVy"," openpyxl ",[39,118,119],{"class":111},"import",[39,121,122],{"class":115}," load_workbook\n",[39,124,126,128,131,133],{"class":41,"line":125},2,[39,127,112],{"class":111},[39,129,130],{"class":115}," openpyxl.chart ",[39,132,119],{"class":111},[39,134,135],{"class":115}," LineChart, Reference, Series\n",[39,137,139],{"class":41,"line":138},3,[39,140,142],{"emptyLinePlaceholder":141},true,"\n",[39,144,146,149,152,155,158],{"class":41,"line":145},4,[39,147,148],{"class":115},"wb ",[39,150,151],{"class":111},"=",[39,153,154],{"class":115}," load_workbook(",[39,156,157],{"class":49},"\"monthly_report.xlsx\"",[39,159,160],{"class":115},")\n",[39,162,164,167,169],{"class":41,"line":163},5,[39,165,166],{"class":115},"ws ",[39,168,151],{"class":111},[39,170,171],{"class":115}," wb.active\n",[39,173,175,178,180],{"class":41,"line":174},6,[39,176,177],{"class":115},"last ",[39,179,151],{"class":111},[39,181,182],{"class":115}," ws.max_row\n",[39,184,186],{"class":41,"line":185},7,[39,187,142],{"emptyLinePlaceholder":141},[39,189,191,194,196],{"class":41,"line":190},8,[39,192,193],{"class":115},"chart ",[39,195,151],{"class":111},[39,197,198],{"class":115}," LineChart()\n",[39,200,202,205,208,211,214,218,221,224,226,229,232],{"class":41,"line":201},9,[39,203,204],{"class":111},"for",[39,206,207],{"class":115}," col ",[39,209,210],{"class":111},"in",[39,212,213],{"class":115}," (",[39,215,217],{"class":216},"sP0c6","3",[39,219,220],{"class":115},", ",[39,222,223],{"class":216},"6",[39,225,220],{"class":115},[39,227,228],{"class":216},"7",[39,230,231],{"class":115},"):                                       ",[39,233,235],{"class":234},"s-wDw","# Revenue, Forecast, Budget\n",[39,237,239,242,244,247,251,253,256,259,261,264,266,269,271],{"class":41,"line":238},10,[39,240,241],{"class":115},"    values ",[39,243,151],{"class":111},[39,245,246],{"class":115}," Reference(ws, ",[39,248,250],{"class":249},"sa561","min_col",[39,252,151],{"class":111},[39,254,255],{"class":115},"col, ",[39,257,258],{"class":249},"min_row",[39,260,151],{"class":111},[39,262,263],{"class":216},"1",[39,265,220],{"class":115},[39,267,268],{"class":249},"max_row",[39,270,151],{"class":111},[39,272,273],{"class":115},"last)\n",[39,275,277,280,283,285,288],{"class":41,"line":276},11,[39,278,279],{"class":115},"    chart.series.append(Series(values, ",[39,281,282],{"class":249},"title_from_data",[39,284,151],{"class":111},[39,286,287],{"class":216},"True",[39,289,290],{"class":115},"))\n",[39,292,294,297,299,301,303,305,307,309,312,314,316,318],{"class":41,"line":293},12,[39,295,296],{"class":115},"chart.set_categories(Reference(ws, ",[39,298,250],{"class":249},[39,300,151],{"class":111},[39,302,263],{"class":216},[39,304,220],{"class":115},[39,306,258],{"class":249},[39,308,151],{"class":111},[39,310,311],{"class":216},"2",[39,313,220],{"class":115},[39,315,268],{"class":249},[39,317,151],{"class":111},[39,319,320],{"class":115},"last))\n",[39,322,324,327,329],{"class":41,"line":323},13,[39,325,326],{"class":115},"chart.title ",[39,328,151],{"class":111},[39,330,331],{"class":49}," \"Revenue against forecast and budget\"\n",[39,333,335,338,340],{"class":41,"line":334},14,[39,336,337],{"class":115},"chart.y_axis.numFmt ",[39,339,151],{"class":111},[39,341,342],{"class":49}," \"£#,##0\"\n",[39,344,346,349,351],{"class":41,"line":345},15,[39,347,348],{"class":115},"chart.x_axis.number_format ",[39,350,151],{"class":111},[39,352,353],{"class":49}," \"mmm yy\"\n",[39,355,357,360,362,365,367],{"class":41,"line":356},16,[39,358,359],{"class":115},"chart.x_axis.delete ",[39,361,151],{"class":111},[39,363,364],{"class":115}," chart.y_axis.delete ",[39,366,151],{"class":111},[39,368,369],{"class":216}," False\n",[39,371,373,376,378,381,383],{"class":41,"line":372},17,[39,374,375],{"class":115},"chart.width, chart.height ",[39,377,151],{"class":111},[39,379,380],{"class":216}," 18",[39,382,220],{"class":115},[39,384,385],{"class":216},"9\n",[39,387,389,392,395],{"class":41,"line":388},18,[39,390,391],{"class":115},"ws.add_chart(chart, ",[39,393,394],{"class":49},"\"J2\"",[39,396,160],{"class":115},[39,398,400,403,406],{"class":41,"line":399},19,[39,401,402],{"class":115},"wb.save(",[39,404,405],{"class":49},"\"report_with_chart.xlsx\"",[39,407,160],{"class":115},[39,409,411,414,417,419,422,424],{"class":41,"line":410},20,[39,412,413],{"class":216},"print",[39,415,416],{"class":115},"([s.tx.strRef.f ",[39,418,204],{"class":111},[39,420,421],{"class":115}," s ",[39,423,210],{"class":111},[39,425,426],{"class":115}," chart.series])\n",[10,428,429,430,433,434,436,437,440,441,444,445,220,447,449,450,452,453,456],{},"Instead of ",[14,431,432],{},"chart.add_data()"," with a block of columns, each series is created separately from a one-column ",[14,435,16],{}," and appended to ",[14,438,439],{},"chart.series",". Starting each reference at the header row and passing ",[14,442,443],{},"title_from_data=True"," makes the header cell the series name, so the legend reads ",[64,446,74],{},[64,448,86],{}," and ",[64,451,90],{}," and stays right if someone renames a heading in the sheet. The categories — the months — come from column A with ",[14,454,455],{},"set_categories",", which applies them to every series already in the chart, so it is called after the series are added. The printed references show each series pointing at its own column header: C1, F1 and G1.",[10,458,459,460,463,464,467,468,471,472,220,475,478],{},"For comparison, ",[14,461,462],{},"chart.add_data(Reference(ws, min_col=3, max_col=7, ...))"," would have produced five series — revenue, cost, margin, forecast and budget — because a multi-column reference becomes one series per column. Deleting the unwanted ones afterwards with ",[14,465,466],{},"del chart.series[1:3]"," works, but it ties the code to the current column order in a way that is easy to break, whereas building only the series you want states the intent directly. The months on the axis are real dates, so the axis number format ",[14,469,470],{},"mmm yy"," shows them as ",[64,473,474],{},"Oct 25",[64,476,477],{},"Nov 25"," and so on.",[480,481,489,490,489,494,489,498,489,506,489,515,489,519,489,526,489,530,489,534,489,538,489,542,489,546,489,550,489,554,489,558,489,562,489,566,489,570,489,574,489,578,489,582,489,586,489,590,489,595,489,599,489,601,489,605,489,609,489,613,489,615,489,618,489,620,489,623,489,625,489,628,489,630,489,633,489,635,489,638,489,641,489,644,489,646,489,649,489,651,489,655,489,657,489,660,489,662,489,664,489,666,489,668,489,670,489,673,489,675,489,678,489,680],"svg",{"viewBox":482,"role":483,"ariaLabelledBy":484,"xmlns":487,"style":488},"22 0 716 112","img",[485,486],"nac-columns-t","nac-columns-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:716px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[491,492,493],"title",{"id":485},"The columns the chart uses",[495,496,497],"desc",{"id":486},"Of the eight report columns the chart takes Month from column A as categories, and Revenue, Forecast and Budget from columns C, F and G as three series. Units, Cost, Margin and Variance are left out.",[499,500],"rect",{"x":501,"y":502,"width":503,"height":504,"fill":505},"22","0","716","112","#ffffff",[499,507],{"x":508,"y":509,"width":510,"height":511,"rx":502,"fill":512,"stroke":513,"style":514},"38.3","16.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[499,516],{"x":517,"y":509,"width":518,"height":511,"rx":502,"fill":512,"stroke":513,"style":514},"70.3","89.6",[520,521,525],"text",{"x":522,"y":523,"style":524},"115.2","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[499,527],{"x":528,"y":509,"width":529,"height":511,"rx":502,"fill":512,"stroke":513,"style":514},"160.0","67.5",[520,531,533],{"x":532,"y":523,"style":524},"193.7","B",[499,535],{"x":536,"y":509,"width":537,"height":511,"rx":502,"fill":512,"stroke":513,"style":514},"227.5","90.4",[520,539,541],{"x":540,"y":523,"style":524},"272.7","C",[499,543],{"x":544,"y":509,"width":545,"height":511,"rx":502,"fill":512,"stroke":513,"style":514},"317.9","64.8",[520,547,549],{"x":548,"y":523,"style":524},"350.3","D",[499,551],{"x":552,"y":509,"width":553,"height":511,"rx":502,"fill":512,"stroke":513,"style":514},"382.7","78.1",[520,555,557],{"x":556,"y":523,"style":524},"421.7","E",[499,559],{"x":560,"y":509,"width":561,"height":511,"rx":502,"fill":512,"stroke":513,"style":514},"460.7","88.5",[520,563,565],{"x":564,"y":523,"style":524},"505.0","F",[499,567],{"x":568,"y":509,"width":569,"height":511,"rx":502,"fill":512,"stroke":513,"style":514},"549.2","80.6",[520,571,573],{"x":572,"y":523,"style":524},"589.6","G",[499,575],{"x":576,"y":509,"width":577,"height":511,"rx":502,"fill":512,"stroke":513,"style":514},"629.9","91.8",[520,579,581],{"x":580,"y":523,"style":524},"675.8","H",[499,583],{"x":508,"y":584,"width":510,"height":585,"rx":502,"fill":512,"stroke":513,"style":514},"40.0","28.0",[520,587,263],{"x":588,"y":589,"style":524},"54.3","59.0",[499,591],{"x":517,"y":584,"width":518,"height":585,"rx":502,"fill":592,"stroke":593,"style":594},"#fdefd8","var(--gold,#b4740a)","stroke-width:1.5px",[520,596,598],{"x":522,"y":589,"style":597},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","A Month",[499,600],{"x":528,"y":584,"width":529,"height":585,"rx":502,"fill":505,"stroke":513,"style":514},[520,602,604],{"x":532,"y":589,"style":603},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","B Units",[499,606],{"x":536,"y":584,"width":537,"height":585,"rx":502,"fill":607,"stroke":608,"style":594},"#f0f4ff","var(--brand,#5b5cf0)",[520,610,612],{"x":540,"y":589,"style":611},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","C Revenue",[499,614],{"x":544,"y":584,"width":545,"height":585,"rx":502,"fill":505,"stroke":513,"style":514},[520,616,617],{"x":548,"y":589,"style":603},"D Cost",[499,619],{"x":552,"y":584,"width":553,"height":585,"rx":502,"fill":505,"stroke":513,"style":514},[520,621,622],{"x":556,"y":589,"style":603},"E Margin",[499,624],{"x":560,"y":584,"width":561,"height":585,"rx":502,"fill":607,"stroke":608,"style":594},[520,626,627],{"x":564,"y":589,"style":611},"F Forecast",[499,629],{"x":568,"y":584,"width":569,"height":585,"rx":502,"fill":607,"stroke":608,"style":594},[520,631,632],{"x":572,"y":589,"style":611},"G Budget",[499,634],{"x":576,"y":584,"width":577,"height":585,"rx":502,"fill":505,"stroke":513,"style":514},[520,636,637],{"x":580,"y":589,"style":603},"H Variance",[499,639],{"x":508,"y":640,"width":510,"height":585,"rx":502,"fill":512,"stroke":513,"style":514},"68.0",[520,642,311],{"x":588,"y":643,"style":524},"87.0",[499,645],{"x":517,"y":640,"width":518,"height":585,"rx":502,"fill":592,"stroke":593,"style":594},[520,647,648],{"x":522,"y":643,"style":597},"categories",[499,650],{"x":528,"y":640,"width":529,"height":585,"rx":502,"fill":505,"stroke":513,"style":514},[520,652,654],{"x":532,"y":643,"style":653},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","—",[499,656],{"x":536,"y":640,"width":537,"height":585,"rx":502,"fill":607,"stroke":608,"style":594},[520,658,659],{"x":540,"y":643,"style":611},"series 1",[499,661],{"x":544,"y":640,"width":545,"height":585,"rx":502,"fill":505,"stroke":513,"style":514},[520,663,654],{"x":548,"y":643,"style":653},[499,665],{"x":552,"y":640,"width":553,"height":585,"rx":502,"fill":505,"stroke":513,"style":514},[520,667,654],{"x":556,"y":643,"style":653},[499,669],{"x":560,"y":640,"width":561,"height":585,"rx":502,"fill":607,"stroke":608,"style":594},[520,671,672],{"x":564,"y":643,"style":611},"series 2",[499,674],{"x":568,"y":640,"width":569,"height":585,"rx":502,"fill":607,"stroke":608,"style":594},[520,676,677],{"x":572,"y":643,"style":611},"series 3",[499,679],{"x":576,"y":640,"width":577,"height":585,"rx":502,"fill":505,"stroke":513,"style":514},[520,681,654],{"x":580,"y":643,"style":653},[25,683,685],{"id":684},"step-2-find-the-columns-by-heading-not-by-letter","Step 2 — Find the columns by heading, not by letter",[30,687,689],{"className":102,"code":688,"language":104,"meta":35,"style":35},"def col_of(ws, heading, row=1):\n    for cell in ws[row]:\n        if cell.value == heading:\n            return cell.column\n    raise KeyError(f\"no column headed {heading!r}; headings are {[c.value for c in ws[row]]}\")\n\nwanted = [\"Revenue\", \"Forecast\", \"Budget\"]\ncols = [col_of(ws, h) for h in wanted]\nprint(dict(zip(wanted, cols)), \"| categories in column\", col_of(ws, \"Month\"))\n",[14,690,691,710,723,737,745,800,804,830,850],{"__ignoreMap":35},[39,692,693,696,700,703,705,707],{"class":41,"line":42},[39,694,695],{"class":111},"def",[39,697,699],{"class":698},"s_Opv"," col_of",[39,701,702],{"class":115},"(ws, heading, row",[39,704,151],{"class":111},[39,706,263],{"class":216},[39,708,709],{"class":115},"):\n",[39,711,712,715,718,720],{"class":41,"line":125},[39,713,714],{"class":111},"    for",[39,716,717],{"class":115}," cell ",[39,719,210],{"class":111},[39,721,722],{"class":115}," ws[row]:\n",[39,724,725,728,731,734],{"class":41,"line":138},[39,726,727],{"class":111},"        if",[39,729,730],{"class":115}," cell.value ",[39,732,733],{"class":111},"==",[39,735,736],{"class":115}," heading:\n",[39,738,739,742],{"class":41,"line":145},[39,740,741],{"class":111},"            return",[39,743,744],{"class":115}," cell.column\n",[39,746,747,750,753,756,759,762,766,769,772,775,778,780,783,785,788,790,793,795,798],{"class":41,"line":163},[39,748,749],{"class":111},"    raise",[39,751,752],{"class":216}," KeyError",[39,754,755],{"class":115},"(",[39,757,758],{"class":111},"f",[39,760,761],{"class":49},"\"no column headed ",[39,763,765],{"class":764},"sSjpA","{",[39,767,768],{"class":115},"heading",[39,770,771],{"class":111},"!r",[39,773,774],{"class":764},"}",[39,776,777],{"class":49},"; headings are ",[39,779,765],{"class":764},[39,781,782],{"class":115},"[c.value ",[39,784,204],{"class":111},[39,786,787],{"class":115}," c ",[39,789,210],{"class":111},[39,791,792],{"class":115}," ws[row]]",[39,794,774],{"class":764},[39,796,797],{"class":49},"\"",[39,799,160],{"class":115},[39,801,802],{"class":41,"line":174},[39,803,142],{"emptyLinePlaceholder":141},[39,805,806,809,811,814,817,819,822,824,827],{"class":41,"line":185},[39,807,808],{"class":115},"wanted ",[39,810,151],{"class":111},[39,812,813],{"class":115}," [",[39,815,816],{"class":49},"\"Revenue\"",[39,818,220],{"class":115},[39,820,821],{"class":49},"\"Forecast\"",[39,823,220],{"class":115},[39,825,826],{"class":49},"\"Budget\"",[39,828,829],{"class":115},"]\n",[39,831,832,835,837,840,842,845,847],{"class":41,"line":190},[39,833,834],{"class":115},"cols ",[39,836,151],{"class":111},[39,838,839],{"class":115}," [col_of(ws, h) ",[39,841,204],{"class":111},[39,843,844],{"class":115}," h ",[39,846,210],{"class":111},[39,848,849],{"class":115}," wanted]\n",[39,851,852,854,856,859,861,864,867,870,873,876],{"class":41,"line":201},[39,853,413],{"class":216},[39,855,755],{"class":115},[39,857,858],{"class":216},"dict",[39,860,755],{"class":115},[39,862,863],{"class":216},"zip",[39,865,866],{"class":115},"(wanted, cols)), ",[39,868,869],{"class":49},"\"| categories in column\"",[39,871,872],{"class":115},", col_of(ws, ",[39,874,875],{"class":49},"\"Month\"",[39,877,290],{"class":115},[10,879,880,881,23],{},"Column numbers written into code break the first time someone inserts a column into the report. Looking each column up by its heading keeps the chart correct whatever the order, and the error message lists the available headings if one is missing, which makes a renamed column obvious at a glance. In practice the list of headings to chart is the only thing that changes from one chart to the next, so it belongs at the top of the script or in a configuration file, as in ",[19,882,884],{"href":883},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file\u002F","Keep Excel Report Settings in a Config File",[25,886,888],{"id":887},"step-3-the-same-with-xlsxwriter","Step 3 — The same with XlsxWriter",[30,890,892],{"className":102,"code":891,"language":104,"meta":35,"style":35},"import pandas as pd\nimport xlsxwriter\n\ndf = pd.read_excel(\"monthly_report.xlsx\")\nwith pd.ExcelWriter(\"report_xlsxwriter.xlsx\", engine=\"xlsxwriter\", datetime_format=\"mmm yy\") as xw:\n    df.to_excel(xw, sheet_name=\"Report\", index=False)\n    book, sheet = xw.book, xw.sheets[\"Report\"]\n    n = len(df)\n    line = book.add_chart({\"type\": \"line\"})\n    for heading in [\"Revenue\", \"Forecast\", \"Budget\"]:\n        c = df.columns.get_loc(heading)\n        line.add_series({\"name\": [\"Report\", 0, c], \"categories\": [\"Report\", 1, 0, n, 0], \"values\": [\"Report\", 1, c, n, c]})\n    line.set_title({\"name\": \"Revenue against forecast and budget\"})\n    line.set_y_axis({\"num_format\": \"£#,##0\"})\n    sheet.insert_chart(\"J2\", line)\nprint(\"saved\")\n",[14,893,894,907,914,918,932,971,996,1010,1023,1045,1069,1079,1136,1150,1165,1175],{"__ignoreMap":35},[39,895,896,898,901,904],{"class":41,"line":42},[39,897,119],{"class":111},[39,899,900],{"class":115}," pandas ",[39,902,903],{"class":111},"as",[39,905,906],{"class":115}," pd\n",[39,908,909,911],{"class":41,"line":125},[39,910,119],{"class":111},[39,912,913],{"class":115}," xlsxwriter\n",[39,915,916],{"class":41,"line":138},[39,917,142],{"emptyLinePlaceholder":141},[39,919,920,923,925,928,930],{"class":41,"line":145},[39,921,922],{"class":115},"df ",[39,924,151],{"class":111},[39,926,927],{"class":115}," pd.read_excel(",[39,929,157],{"class":49},[39,931,160],{"class":115},[39,933,934,937,940,943,945,948,950,953,955,958,960,963,966,968],{"class":41,"line":163},[39,935,936],{"class":111},"with",[39,938,939],{"class":115}," pd.ExcelWriter(",[39,941,942],{"class":49},"\"report_xlsxwriter.xlsx\"",[39,944,220],{"class":115},[39,946,947],{"class":249},"engine",[39,949,151],{"class":111},[39,951,952],{"class":49},"\"xlsxwriter\"",[39,954,220],{"class":115},[39,956,957],{"class":249},"datetime_format",[39,959,151],{"class":111},[39,961,962],{"class":49},"\"mmm yy\"",[39,964,965],{"class":115},") ",[39,967,903],{"class":111},[39,969,970],{"class":115}," xw:\n",[39,972,973,976,979,981,984,986,989,991,994],{"class":41,"line":174},[39,974,975],{"class":115},"    df.to_excel(xw, ",[39,977,978],{"class":249},"sheet_name",[39,980,151],{"class":111},[39,982,983],{"class":49},"\"Report\"",[39,985,220],{"class":115},[39,987,988],{"class":249},"index",[39,990,151],{"class":111},[39,992,993],{"class":216},"False",[39,995,160],{"class":115},[39,997,998,1001,1003,1006,1008],{"class":41,"line":185},[39,999,1000],{"class":115},"    book, sheet ",[39,1002,151],{"class":111},[39,1004,1005],{"class":115}," xw.book, xw.sheets[",[39,1007,983],{"class":49},[39,1009,829],{"class":115},[39,1011,1012,1015,1017,1020],{"class":41,"line":190},[39,1013,1014],{"class":115},"    n ",[39,1016,151],{"class":111},[39,1018,1019],{"class":216}," len",[39,1021,1022],{"class":115},"(df)\n",[39,1024,1025,1028,1030,1033,1036,1039,1042],{"class":41,"line":201},[39,1026,1027],{"class":115},"    line ",[39,1029,151],{"class":111},[39,1031,1032],{"class":115}," book.add_chart({",[39,1034,1035],{"class":49},"\"type\"",[39,1037,1038],{"class":115},": ",[39,1040,1041],{"class":49},"\"line\"",[39,1043,1044],{"class":115},"})\n",[39,1046,1047,1049,1052,1054,1056,1058,1060,1062,1064,1066],{"class":41,"line":238},[39,1048,714],{"class":111},[39,1050,1051],{"class":115}," heading ",[39,1053,210],{"class":111},[39,1055,813],{"class":115},[39,1057,816],{"class":49},[39,1059,220],{"class":115},[39,1061,821],{"class":49},[39,1063,220],{"class":115},[39,1065,826],{"class":49},[39,1067,1068],{"class":115},"]:\n",[39,1070,1071,1074,1076],{"class":41,"line":276},[39,1072,1073],{"class":115},"        c ",[39,1075,151],{"class":111},[39,1077,1078],{"class":115}," df.columns.get_loc(heading)\n",[39,1080,1081,1084,1087,1090,1092,1094,1096,1099,1102,1104,1106,1108,1110,1112,1114,1117,1119,1122,1125,1127,1129,1131,1133],{"class":41,"line":293},[39,1082,1083],{"class":115},"        line.add_series({",[39,1085,1086],{"class":49},"\"name\"",[39,1088,1089],{"class":115},": [",[39,1091,983],{"class":49},[39,1093,220],{"class":115},[39,1095,502],{"class":216},[39,1097,1098],{"class":115},", c], ",[39,1100,1101],{"class":49},"\"categories\"",[39,1103,1089],{"class":115},[39,1105,983],{"class":49},[39,1107,220],{"class":115},[39,1109,263],{"class":216},[39,1111,220],{"class":115},[39,1113,502],{"class":216},[39,1115,1116],{"class":115},", n, ",[39,1118,502],{"class":216},[39,1120,1121],{"class":115},"], ",[39,1123,1124],{"class":49},"\"values\"",[39,1126,1089],{"class":115},[39,1128,983],{"class":49},[39,1130,220],{"class":115},[39,1132,263],{"class":216},[39,1134,1135],{"class":115},", c, n, c]})\n",[39,1137,1138,1141,1143,1145,1148],{"class":41,"line":323},[39,1139,1140],{"class":115},"    line.set_title({",[39,1142,1086],{"class":49},[39,1144,1038],{"class":115},[39,1146,1147],{"class":49},"\"Revenue against forecast and budget\"",[39,1149,1044],{"class":115},[39,1151,1152,1155,1158,1160,1163],{"class":41,"line":334},[39,1153,1154],{"class":115},"    line.set_y_axis({",[39,1156,1157],{"class":49},"\"num_format\"",[39,1159,1038],{"class":115},[39,1161,1162],{"class":49},"\"£#,##0\"",[39,1164,1044],{"class":115},[39,1166,1167,1170,1172],{"class":41,"line":345},[39,1168,1169],{"class":115},"    sheet.insert_chart(",[39,1171,394],{"class":49},[39,1173,1174],{"class":115},", line)\n",[39,1176,1177,1179,1181,1184],{"class":41,"line":356},[39,1178,413],{"class":216},[39,1180,755],{"class":115},[39,1182,1183],{"class":49},"\"saved\"",[39,1185,160],{"class":115},[10,1187,1188,1189,1192,1193,1196,1197,1200,1201,1204],{},"XlsxWriter has always worked series by series: each ",[14,1190,1191],{},"add_series"," call names its own categories and values, so non-adjacent columns need nothing special. Passing row and column numbers as lists — ",[14,1194,1195],{},"[\"Report\", 1, c, n, c]"," — rather than A1 strings fits naturally with pandas, where ",[14,1198,1199],{},"df.columns.get_loc(heading)"," gives the zero-based column of each heading directly. The ",[14,1202,1203],{},"name"," points at the header cell, so the legend follows the sheet's headings here too, exactly as in the openpyxl version.",[480,1206,489,1212,489,1215,489,1218,489,1223,489,1230,489,1236,489,1241,489,1245,489,1249,489,1253,489,1258,489,1263,489,1267,489,1270,489,1273],{"viewBox":1207,"role":483,"ariaLabelledBy":1208,"xmlns":487,"style":1211},"4 0 752 173",[1209,1210],"nac-libs-t","nac-libs-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[491,1213,1214],{"id":1209},"Non-adjacent series in each library",[495,1216,1217],{"id":1210},"In openpyxl each series is a Series built from its own one-column Reference with title_from_data, appended to chart.series, with categories set afterwards. In XlsxWriter each add_series call gives its own name, categories and values ranges.",[499,1219],{"x":1220,"y":502,"width":1221,"height":1222,"fill":505},"4","752","173",[499,1224],{"x":1225,"y":509,"width":1226,"height":1227,"rx":1228,"fill":607,"stroke":608,"style":1229},"20.0","351.0","141.0","14","stroke-width:2px",[520,1231,1235],{"x":1232,"y":1233,"style":1234},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","openpyxl",[41,1237],{"x1":1238,"y1":1239,"x2":1240,"y2":1239,"stroke":608,"style":514},"36.0","57.0","355.0",[520,1242,1244],{"x":1232,"y":1243,"style":653},"77.0","Series(Reference(one column), title_from_data=True)",[520,1246,1248],{"x":1232,"y":1247,"style":653},"103.0","chart.series.append(...)",[520,1250,1252],{"x":1232,"y":1251,"style":653},"129.0","set_categories after the series",[499,1254],{"x":1255,"y":509,"width":1226,"height":1227,"rx":1228,"fill":1256,"stroke":1257,"style":1229},"389.0","#d9f4f1","var(--teal,#0f9488)",[520,1259,1262],{"x":1260,"y":1233,"style":1261},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","XlsxWriter",[41,1264],{"x1":1265,"y1":1239,"x2":1266,"y2":1239,"stroke":1257,"style":514},"405.0","724.0",[520,1268,1269],{"x":1260,"y":1243,"style":653},"chart.add_series({...}) per column",[520,1271,1272],{"x":1260,"y":1247,"style":653},"name, categories, values each",[520,1274,1275],{"x":1260,"y":1251,"style":653},"row\u002Fcolumn lists suit pandas",[25,1277,1279],{"id":1278},"styling-each-series-for-its-role","Styling each series for its role",[10,1281,1282],{},"Building series one at a time has a second benefit: each one can be styled for what it represents. Actual revenue is the main story and deserves a solid, heavier line; the forecast is an expectation and reads naturally as a dashed line; the budget is a reference level and can be a thin grey line in the background.",[30,1284,1286],{"className":102,"code":1285,"language":104,"meta":35,"style":35},"styles = {\"Revenue\": (\"1F5FA8\", 28575, \"solid\"), \"Forecast\": (\"1F5FA8\", 19050, \"dash\"), \"Budget\": (\"7F7F7F\", 12700, \"solid\")}\nfor series, heading in zip(chart.series, [\"Revenue\", \"Forecast\", \"Budget\"]):\n    colour, width, dash = styles[heading]\n    series.graphicalProperties.line.solidFill = colour\n    series.graphicalProperties.line.width = width\n    series.graphicalProperties.line.dashStyle = dash\n    series.smooth = False\nwb.save(\"report_with_chart.xlsx\")\n",[14,1287,1288,1356,1384,1394,1404,1414,1424,1433],{"__ignoreMap":35},[39,1289,1290,1293,1295,1298,1300,1303,1306,1308,1311,1313,1316,1319,1321,1323,1325,1327,1330,1332,1335,1337,1339,1341,1344,1346,1349,1351,1353],{"class":41,"line":42},[39,1291,1292],{"class":115},"styles ",[39,1294,151],{"class":111},[39,1296,1297],{"class":115}," {",[39,1299,816],{"class":49},[39,1301,1302],{"class":115},": (",[39,1304,1305],{"class":49},"\"1F5FA8\"",[39,1307,220],{"class":115},[39,1309,1310],{"class":216},"28575",[39,1312,220],{"class":115},[39,1314,1315],{"class":49},"\"solid\"",[39,1317,1318],{"class":115},"), ",[39,1320,821],{"class":49},[39,1322,1302],{"class":115},[39,1324,1305],{"class":49},[39,1326,220],{"class":115},[39,1328,1329],{"class":216},"19050",[39,1331,220],{"class":115},[39,1333,1334],{"class":49},"\"dash\"",[39,1336,1318],{"class":115},[39,1338,826],{"class":49},[39,1340,1302],{"class":115},[39,1342,1343],{"class":49},"\"7F7F7F\"",[39,1345,220],{"class":115},[39,1347,1348],{"class":216},"12700",[39,1350,220],{"class":115},[39,1352,1315],{"class":49},[39,1354,1355],{"class":115},")}\n",[39,1357,1358,1360,1363,1365,1368,1371,1373,1375,1377,1379,1381],{"class":41,"line":125},[39,1359,204],{"class":111},[39,1361,1362],{"class":115}," series, heading ",[39,1364,210],{"class":111},[39,1366,1367],{"class":216}," zip",[39,1369,1370],{"class":115},"(chart.series, [",[39,1372,816],{"class":49},[39,1374,220],{"class":115},[39,1376,821],{"class":49},[39,1378,220],{"class":115},[39,1380,826],{"class":49},[39,1382,1383],{"class":115},"]):\n",[39,1385,1386,1389,1391],{"class":41,"line":138},[39,1387,1388],{"class":115},"    colour, width, dash ",[39,1390,151],{"class":111},[39,1392,1393],{"class":115}," styles[heading]\n",[39,1395,1396,1399,1401],{"class":41,"line":145},[39,1397,1398],{"class":115},"    series.graphicalProperties.line.solidFill ",[39,1400,151],{"class":111},[39,1402,1403],{"class":115}," colour\n",[39,1405,1406,1409,1411],{"class":41,"line":163},[39,1407,1408],{"class":115},"    series.graphicalProperties.line.width ",[39,1410,151],{"class":111},[39,1412,1413],{"class":115}," width\n",[39,1415,1416,1419,1421],{"class":41,"line":174},[39,1417,1418],{"class":115},"    series.graphicalProperties.line.dashStyle ",[39,1420,151],{"class":111},[39,1422,1423],{"class":115}," dash\n",[39,1425,1426,1429,1431],{"class":41,"line":185},[39,1427,1428],{"class":115},"    series.smooth ",[39,1430,151],{"class":111},[39,1432,369],{"class":216},[39,1434,1435,1437,1439],{"class":41,"line":190},[39,1436,402],{"class":115},[39,1438,405],{"class":49},[39,1440,160],{"class":115},[10,1442,1443,1444,23],{},"Line widths are in EMUs — 12,700 per point — so the three lines are 2.25, 1.5 and 1 point wide. Keying the styles by heading rather than by position means the styling follows the data if the list of charted columns changes. The same approach extends to combination charts, where one column is shown as bars and another as a line on the same axes; the series are simply added to two chart objects that are then combined, as described in ",[19,1445,1447],{"href":1446},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-a-combo-chart-with-a-secondary-axis-in-openpyxl\u002F","Add a Combo Chart with a Secondary Axis in openpyxl",[480,1449,489,1454,489,1457,489,1460,489,1463,489,1467,489,1473,489,1476,489,1481,489,1485,489,1488,489,1493,489,1498,489,1501,489,1506,489,1510,489,1513,489,1517,489,1520,489,1523,489,1528,489,1532,489,1536,489,1539,489,1543,489,1546,489,1549,489,1554,489,1557,489,1560,489,1564,489,1567,489,1570,489,1575,489,1577],{"viewBox":1450,"role":483,"ariaLabelledBy":1451,"xmlns":487,"style":1211},"4 0 752 144",[1452,1453],"nac-build-t","nac-build-d",[491,1455,1456],{"id":1452},"Building a chart from chosen columns",[495,1458,1459],{"id":1453},"The headings to chart are listed, each is looked up to find its column, a one-column series is built from the header row down with the header as its name, each series is styled for its role, and the shared month categories are set last.",[499,1461],{"x":1220,"y":502,"width":1221,"height":1462,"fill":505},"144",[499,1464],{"x":1225,"y":509,"width":522,"height":1465,"rx":1466,"fill":512,"stroke":513,"style":1229},"111.5","12",[520,1468,1472],{"x":1469,"y":1470,"style":1471},"77.6","41.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Headings to",[520,1474,1475],{"x":1469,"y":589,"style":1471},"chart",[520,1477,1480],{"x":1469,"y":1478,"style":1479},"78.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Revenue,",[520,1482,1484],{"x":1469,"y":1483,"style":1479},"96.0","Forecast,",[520,1486,90],{"x":1469,"y":1487,"style":1479},"113.5",[41,1489],{"x1":1490,"y1":1491,"x2":1492,"y2":1491,"stroke":608,"style":1229},"140.2","71.8","162.2",[1494,1495],"polygon",{"points":1496,"fill":1497},"166.2,71.8 157.2,76.8 157.2,66.8","#5b5cf0",[499,1499],{"x":1500,"y":509,"width":522,"height":1465,"rx":1466,"fill":607,"stroke":608,"style":1229},"171.2",[520,1502,1505],{"x":1503,"y":589,"style":1504},"228.8","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Look up",[520,1507,1509],{"x":1503,"y":1508,"style":1504},"76.5","columns",[520,1511,1512],{"x":1503,"y":1483,"style":1479},"by heading",[41,1514],{"x1":1515,"y1":1491,"x2":1516,"y2":1491,"stroke":608,"style":1229},"291.4","313.4",[1494,1518],{"points":1519,"fill":1497},"317.4,71.8 308.4,76.8 308.4,66.8",[499,1521],{"x":1522,"y":509,"width":522,"height":1465,"rx":1466,"fill":607,"stroke":608,"style":1229},"322.4",[520,1524,1527],{"x":1525,"y":1526,"style":1504},"380.0","50.2","One series",[520,1529,1531],{"x":1525,"y":1530,"style":1504},"67.8","each",[520,1533,1535],{"x":1525,"y":1534,"style":1479},"87.2","header row as",[520,1537,1203],{"x":1525,"y":1538,"style":1479},"104.8",[41,1540],{"x1":1541,"y1":1491,"x2":1542,"y2":1491,"stroke":608,"style":1229},"442.6","464.6",[1494,1544],{"points":1545,"fill":1497},"468.6,71.8 459.6,76.8 459.6,66.8",[499,1547],{"x":1548,"y":509,"width":522,"height":1465,"rx":1466,"fill":592,"stroke":593,"style":1229},"473.6",[520,1550,1553],{"x":1551,"y":589,"style":1552},"531.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Style per role",[520,1555,1556],{"x":1551,"y":1478,"style":1479},"solid, dashed,",[520,1558,1559],{"x":1551,"y":1483,"style":1479},"grey",[41,1561],{"x1":1562,"y1":1491,"x2":1563,"y2":1491,"stroke":608,"style":1229},"593.8","615.8",[1494,1565],{"points":1566,"fill":1497},"619.8,71.8 610.8,76.8 610.8,66.8",[499,1568],{"x":1569,"y":509,"width":522,"height":1465,"rx":1466,"fill":1256,"stroke":1257,"style":1229},"624.8",[520,1571,1574],{"x":1572,"y":589,"style":1573},"682.4","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Set",[520,1576,648],{"x":1572,"y":1508,"style":1573},[520,1578,1579],{"x":1572,"y":1483,"style":1479},"month column",[25,1581,1583],{"id":1582},"step-4-hidden-columns-and-checking-the-result","Step 4 — Hidden columns, and checking the result",[30,1585,1587],{"className":102,"code":1586,"language":104,"meta":35,"style":35},"ws.column_dimensions[\"D\"].hidden = ws.column_dimensions[\"E\"].hidden = True    # hide Cost and Margin\nchart.visible_cells_only = True                                               # Excel's default\nwb.save(\"report_with_chart.xlsx\")\n\nimport re, zipfile\nxml = zipfile.ZipFile(\"report_with_chart.xlsx\").read(\"xl\u002Fcharts\u002Fchart1.xml\").decode()\nprint(re.findall(r\"\u003C(?:c:)?f>([^\u003C]+)\u003C\u002F(?:c:)?f>\", xml))\n",[14,1588,1589,1618,1630,1638,1642,1649,1670],{"__ignoreMap":35},[39,1590,1591,1594,1597,1600,1602,1605,1608,1610,1612,1615],{"class":41,"line":42},[39,1592,1593],{"class":115},"ws.column_dimensions[",[39,1595,1596],{"class":49},"\"D\"",[39,1598,1599],{"class":115},"].hidden ",[39,1601,151],{"class":111},[39,1603,1604],{"class":115}," ws.column_dimensions[",[39,1606,1607],{"class":49},"\"E\"",[39,1609,1599],{"class":115},[39,1611,151],{"class":111},[39,1613,1614],{"class":216}," True",[39,1616,1617],{"class":234},"    # hide Cost and Margin\n",[39,1619,1620,1623,1625,1627],{"class":41,"line":125},[39,1621,1622],{"class":115},"chart.visible_cells_only ",[39,1624,151],{"class":111},[39,1626,1614],{"class":216},[39,1628,1629],{"class":234},"                                               # Excel's default\n",[39,1631,1632,1634,1636],{"class":41,"line":138},[39,1633,402],{"class":115},[39,1635,405],{"class":49},[39,1637,160],{"class":115},[39,1639,1640],{"class":41,"line":145},[39,1641,142],{"emptyLinePlaceholder":141},[39,1643,1644,1646],{"class":41,"line":163},[39,1645,119],{"class":111},[39,1647,1648],{"class":115}," re, zipfile\n",[39,1650,1651,1654,1656,1659,1661,1664,1667],{"class":41,"line":174},[39,1652,1653],{"class":115},"xml ",[39,1655,151],{"class":111},[39,1657,1658],{"class":115}," zipfile.ZipFile(",[39,1660,405],{"class":49},[39,1662,1663],{"class":115},").read(",[39,1665,1666],{"class":49},"\"xl\u002Fcharts\u002Fchart1.xml\"",[39,1668,1669],{"class":115},").decode()\n",[39,1671,1672,1674,1677,1680,1683,1686,1689,1692,1695,1698,1701,1704,1707,1710,1713,1715,1718,1720,1722,1724,1726,1729],{"class":41,"line":185},[39,1673,413],{"class":216},[39,1675,1676],{"class":115},"(re.findall(",[39,1678,1679],{"class":111},"r",[39,1681,1682],{"class":49},"\"\u003C",[39,1684,1685],{"class":216},"(?:",[39,1687,1688],{"class":49},"c:",[39,1690,1691],{"class":216},")",[39,1693,1694],{"class":111},"?",[39,1696,1697],{"class":49},"f>",[39,1699,1700],{"class":216},"([",[39,1702,1703],{"class":111},"^",[39,1705,1706],{"class":764},"\u003C",[39,1708,1709],{"class":216},"]",[39,1711,1712],{"class":111},"+",[39,1714,1691],{"class":216},[39,1716,1717],{"class":49},"\u003C\u002F",[39,1719,1685],{"class":216},[39,1721,1688],{"class":49},[39,1723,1691],{"class":216},[39,1725,1694],{"class":111},[39,1727,1728],{"class":49},"f>\"",[39,1730,1731],{"class":115},", xml))\n",[10,1733,1734,1735,1738,1739,1741,1742,1745,1746,1749],{},"Hiding the columns ",[64,1736,1737],{},"between"," the charted ones does not affect the chart, because each series refers only to its own column. Hiding a charted column is different: by default Excel plots only visible cells, so a hidden ",[64,1740,86],{}," column would make its line disappear. When a report hides helper columns that a chart needs, set ",[14,1743,1744],{},"chart.visible_cells_only = False"," in openpyxl — XlsxWriter's equivalent is ",[14,1747,1748],{},"chart.show_hidden_data()"," — so the chart keeps reading them.",[10,1751,1752],{},"Reading the formulas out of the chart XML lists every range the chart uses: the three header cells for the series names, the three value ranges in columns C, F and G, and the month range in column A, once per series. That list is a precise test for a chart built from scattered columns — it fails loudly if a heading lookup ever picks the wrong column.",[25,1754,1756],{"id":1755},"common-pitfalls","Common pitfalls",[1758,1759,1760,1768,1774,1782,1791],"ul",{},[1761,1762,1763,1767],"li",{},[1764,1765,1766],"strong",{},"One Reference over a block."," It charts every column in between; build one series per column.",[1761,1769,1770,1773],{},[1764,1771,1772],{},"Hard-coded column numbers."," Look columns up by heading so inserted columns do not break the chart.",[1761,1775,1776,1779,1780,23],{},[1764,1777,1778],{},"Series without names."," Start each reference at the header row and use ",[14,1781,443],{},[1761,1783,1784,1787,1788,1790],{},[1764,1785,1786],{},"Categories set too early."," In openpyxl, call ",[14,1789,455],{}," after appending the series.",[1761,1792,1793,1796,1797,1800],{},[1764,1794,1795],{},"Hidden charted columns."," Turn off ",[64,1798,1799],{},"visible cells only"," if the chart must read them.",[25,1802,1804],{"id":1803},"conclusion","Conclusion",[10,1806,1807,1808,1811,1812,1814,1815,1817,1818,1820,1821,1823],{},"To chart non-adjacent columns, build the chart one series at a time: in openpyxl, append a ",[14,1809,1810],{},"Series"," made from a one-column ",[14,1813,16],{}," that starts at the header row with ",[14,1816,443],{},", then set the categories; in XlsxWriter, call ",[14,1819,1191],{}," once per column. Find the columns by heading rather than letter, keep charted columns visible or switch off ",[64,1822,1799],{},", and check the series ranges in the chart XML.",[25,1825,1827],{"id":1826},"frequently-asked-questions","Frequently asked questions",[10,1829,1830,1833,1834,1836,1837,1839,1840,1842,1843,23],{},[1764,1831,1832],{},"How do I chart columns that are not next to each other with openpyxl?","\nCreate a ",[14,1835,1810],{}," from a separate one-column ",[14,1838,16],{}," for each column and append it to ",[14,1841,439],{},", then call ",[14,1844,455],{},[10,1846,1847,1850,1851,1853,1854,1856,1857,23],{},[1764,1848,1849],{},"How do I take a series name from the header cell?","\nStart the ",[14,1852,16],{}," at the header row and pass ",[14,1855,443],{}," when creating the ",[14,1858,1810],{},[10,1860,1861,1864,1865,1867,1868,1870],{},[1764,1862,1863],{},"Why does my chart line disappear when I hide its column?","\nCharts plot visible cells only by default; set ",[14,1866,1744],{}," in openpyxl or ",[14,1869,1748],{}," in XlsxWriter.",[10,1872,1873,1876,1877,1879,1880,1883,1884,1886],{},[1764,1874,1875],{},"How do I chart non-adjacent columns with XlsxWriter?","\nCall ",[14,1878,1191],{}," once for each column, giving each its own ",[14,1881,1882],{},"values"," range and the shared ",[14,1885,648],{}," range.",[25,1888,1890],{"id":1889},"related","Related",[1758,1892,1893,1899,1906,1913],{},[1761,1894,1895,1896,1898],{},"Up one level: ",[19,1897,22],{"href":21}," — native Excel charts from Python.",[1761,1900,1901,1905],{},[19,1902,1904],{"href":1903},"\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"," — line chart basics.",[1761,1907,1908,1912],{},[19,1909,1911],{"href":1910},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fmake-an-excel-chart-grow-with-its-data\u002F","Make an Excel Chart Grow with Its Data"," — ranges that extend.",[1761,1914,1915,1919],{},[19,1916,1918],{"href":1917},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fuse-a-date-axis-in-an-excel-chart-with-openpyxl\u002F","Use a Date Axis in an Excel Chart with openpyxl"," — month categories.",[1921,1922,1923],"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-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":35,"searchDepth":125,"depth":125,"links":1925},[1926,1927,1928,1929,1930,1931,1932,1933,1934,1935],{"id":27,"depth":125,"text":28},{"id":98,"depth":125,"text":99},{"id":684,"depth":125,"text":685},{"id":887,"depth":125,"text":888},{"id":1278,"depth":125,"text":1279},{"id":1582,"depth":125,"text":1583},{"id":1755,"depth":125,"text":1756},{"id":1803,"depth":125,"text":1804},{"id":1826,"depth":125,"text":1827},{"id":1889,"depth":125,"text":1890},"2026-10-02","Chart only some of a sheet's columns — dates from column A, actual from D and forecast from G — without copying data around: add each series from its own Reference with openpyxl or its own add_series call in XlsxWriter, take series names from the header cells, find columns by heading instead of letter, keep a chart working when columns in between are hidden, and check the series references in the chart XML.","md",[1940,1942,1944,1946],{"q":1832,"a":1941},"Create a Series from a separate one-column Reference for each column and append it to chart.series, then call set_categories.",{"q":1849,"a":1943},"Start the Reference at the header row and pass title_from_data=True when creating the Series.",{"q":1863,"a":1945},"Charts plot visible cells only by default; set chart.visible_cells_only = False in openpyxl or chart.show_hidden_data() in XlsxWriter.",{"q":1875,"a":1947},"Call add_series once for each column, giving each its own values range and the shared categories range.","chart non adjacent columns openpyxl",null,{"breadcrumb":1951},[1952,1955,1958],{"name":1953,"item":1954},"Home","\u002F",{"name":1956,"item":1957},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002F",{"name":22,"item":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fplot-a-chart-from-non-adjacent-columns",{"title":1961,"description":1962},"Plot an Excel Chart from Non-Adjacent Columns with Python","Excel charts from non-adjacent columns with openpyxl and XlsxWriter: one Reference or add_series per column, series titles from headers, columns found by name, hidden columns in between and checking the chart XML.","plot-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",[1966,1235],"charts-and-images","how-to","2dN5WSaF0rJqhmb1GHre4BeZ_54E24z9DgjNRaJpARM",[1970,1973],{"title":1911,"path":1971,"stem":1972,"children":-1},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fmake-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",{"title":1974,"path":1975,"stem":1976,"children":-1},"Put a Chart on Its Own Chartsheet with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fput-a-chart-on-its-own-chartsheet-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fput-a-chart-on-its-own-chartsheet-with-openpyxl\u002Findex",[1978,1980],{"path":1979,"title":22},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl",{"path":1981,"title":1956},"\u002Fformatting-and-charting-excel-reports-with-python",[],[1984,1987,1990,1993,1996,1999],{"path":1985,"title":1986,"kind":1967},"\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":1988,"title":1989,"kind":1967},"\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":1991,"title":1992,"kind":1967},"\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":1994,"title":1995,"kind":1967},"\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":1997,"title":1998,"kind":1967},"\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":2000,"title":2001,"kind":1967},"\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",1790972111375]