[{"data":1,"prerenderedAt":2408},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph":3,"surround:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph":2380,"ancestors:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph":2388,"children:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph":2392,"related:\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph":2393},{"id":4,"title":5,"body":6,"dateModified":2347,"datePublished":2347,"description":2348,"extension":2349,"faq":2350,"keyword":2359,"kind":2360,"meta":2361,"navigation":117,"path":2369,"seo":2370,"slug":2373,"stem":2374,"topics":2375,"type":2378,"__hash__":2379},"docs\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph\u002Findex.md","Add a Chart to an Excel Online Workbook with Microsoft Graph",{"type":7,"value":8,"toc":2334},"minimark",[9,19,24,56,84,88,95,492,514,518,711,745,894,898,905,972,989,993,996,1179,1193,1300,1304,1307,1450,1465,1574,1578,1587,2159,2186,2190,2246,2250,2269,2273,2279,2285,2291,2297,2301,2330],[10,11,12,13,18],"p",{},"When a report workbook lives in OneDrive or SharePoint and people open it in the browser, the cleanest way to update it is in place, through Microsoft Graph's Excel API: write the new numbers into the range, and the charts that people look at should follow. Graph can create those charts too — a native Excel chart, not a picture — and later move, retitle and re-point them, or render one to a PNG for a summary email. This page builds a chart on a workbook's sales sheet, positions it, updates its data range when the table grows, and fetches an image of it. Because live Graph calls need a tenant and credentials, the code was tested against a mocked endpoint that records each request, which is shown in the last step. It belongs to ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002F","Automating Excel Online with Microsoft Graph",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install requests msal responses\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50,53],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," requests",[35,51,52],{"class":45}," msal",[35,54,55],{"class":45}," responses\n",[10,57,58,59,62,63,66,67,70,71,75,76,79,80,83],{},"You need an access token with ",[32,60,61],{},"Files.ReadWrite"," (delegated) or ",[32,64,65],{},"Files.ReadWrite.All"," \u002F ",[32,68,69],{},"Sites.ReadWrite.All"," (application) permission, obtained as described in ",[14,72,74],{"href":73},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation\u002F","Authenticate to Microsoft Graph with MSAL for Excel Automation",". The workbook's drive item ID and a sheet called ",[32,77,78],{},"Sales"," with month names in A2:A13 and two series in B and C are assumed. ",[32,81,82],{},"responses"," is only needed for the offline test at the end.",[20,85,87],{"id":86},"step-1-a-small-client-with-a-workbook-session","Step 1 — A small client with a workbook session",[10,89,90,91,94],{},"Every chart call goes to the workbook's ",[32,92,93],{},"\u002Fworkbook"," endpoint. A persistent session keeps the workbook open on the server between calls, which is faster and makes a series of changes behave like one editing session:",[25,96,100],{"className":97,"code":98,"language":99,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import requests\n\nGRAPH = \"https:\u002F\u002Fgraph.microsoft.com\u002Fv1.0\"\n\nclass Workbook:\n    def __init__(self, token, item_id, drive=\"me\u002Fdrive\"):\n        self.base = f\"{GRAPH}\u002F{drive}\u002Fitems\u002F{item_id}\u002Fworkbook\"\n        self.http = requests.Session()\n        self.http.headers[\"Authorization\"] = f\"Bearer {token}\"\n        r = self.http.post(f\"{self.base}\u002FcreateSession\", json={\"persistChanges\": True}, timeout=30)\n        r.raise_for_status()\n        self.http.headers[\"workbook-session-id\"] = r.json()[\"id\"]\n\n    def call(self, method, path, **kw):\n        r = self.http.request(method, f\"{self.base}\u002F{path}\", timeout=60, **kw)\n        r.raise_for_status()\n        return r.json() if r.content else None\n\n    def close(self):\n        self.call(\"POST\", \"closeSession\")\n","python",[32,101,102,112,119,132,137,149,170,219,232,263,329,335,358,363,381,432,437,458,463,474],{"__ignoreMap":30},[35,103,104,108],{"class":37,"line":38},[35,105,107],{"class":106},"s-kum","import",[35,109,111],{"class":110},"skGVy"," requests\n",[35,113,115],{"class":37,"line":114},2,[35,116,118],{"emptyLinePlaceholder":117},true,"\n",[35,120,122,126,129],{"class":37,"line":121},3,[35,123,125],{"class":124},"sP0c6","GRAPH",[35,127,128],{"class":106}," =",[35,130,131],{"class":45}," \"https:\u002F\u002Fgraph.microsoft.com\u002Fv1.0\"\n",[35,133,135],{"class":37,"line":134},4,[35,136,118],{"emptyLinePlaceholder":117},[35,138,140,143,146],{"class":37,"line":139},5,[35,141,142],{"class":106},"class",[35,144,145],{"class":41}," Workbook",[35,147,148],{"class":110},":\n",[35,150,152,155,158,161,164,167],{"class":37,"line":151},6,[35,153,154],{"class":106},"    def",[35,156,157],{"class":124}," __init__",[35,159,160],{"class":110},"(self, token, item_id, drive",[35,162,163],{"class":106},"=",[35,165,166],{"class":45},"\"me\u002Fdrive\"",[35,168,169],{"class":110},"):\n",[35,171,173,176,179,181,184,187,191,193,196,199,201,204,206,209,211,214,216],{"class":37,"line":172},7,[35,174,175],{"class":124},"        self",[35,177,178],{"class":110},".base ",[35,180,163],{"class":106},[35,182,183],{"class":106}," f",[35,185,186],{"class":45},"\"",[35,188,190],{"class":189},"sSjpA","{",[35,192,125],{"class":124},[35,194,195],{"class":189},"}",[35,197,198],{"class":45},"\u002F",[35,200,190],{"class":189},[35,202,203],{"class":110},"drive",[35,205,195],{"class":189},[35,207,208],{"class":45},"\u002Fitems\u002F",[35,210,190],{"class":189},[35,212,213],{"class":110},"item_id",[35,215,195],{"class":189},[35,217,218],{"class":45},"\u002Fworkbook\"\n",[35,220,222,224,227,229],{"class":37,"line":221},8,[35,223,175],{"class":124},[35,225,226],{"class":110},".http ",[35,228,163],{"class":106},[35,230,231],{"class":110}," requests.Session()\n",[35,233,235,237,240,243,246,248,250,253,255,258,260],{"class":37,"line":234},9,[35,236,175],{"class":124},[35,238,239],{"class":110},".http.headers[",[35,241,242],{"class":45},"\"Authorization\"",[35,244,245],{"class":110},"] ",[35,247,163],{"class":106},[35,249,183],{"class":106},[35,251,252],{"class":45},"\"Bearer ",[35,254,190],{"class":189},[35,256,257],{"class":110},"token",[35,259,195],{"class":189},[35,261,262],{"class":45},"\"\n",[35,264,266,269,271,274,277,280,282,284,287,290,292,295,298,302,304,306,309,312,315,318,321,323,326],{"class":37,"line":265},10,[35,267,268],{"class":110},"        r ",[35,270,163],{"class":106},[35,272,273],{"class":124}," self",[35,275,276],{"class":110},".http.post(",[35,278,279],{"class":106},"f",[35,281,186],{"class":45},[35,283,190],{"class":189},[35,285,286],{"class":124},"self",[35,288,289],{"class":110},".base",[35,291,195],{"class":189},[35,293,294],{"class":45},"\u002FcreateSession\"",[35,296,297],{"class":110},", ",[35,299,301],{"class":300},"sa561","json",[35,303,163],{"class":106},[35,305,190],{"class":110},[35,307,308],{"class":45},"\"persistChanges\"",[35,310,311],{"class":110},": ",[35,313,314],{"class":124},"True",[35,316,317],{"class":110},"}, ",[35,319,320],{"class":300},"timeout",[35,322,163],{"class":106},[35,324,325],{"class":124},"30",[35,327,328],{"class":110},")\n",[35,330,332],{"class":37,"line":331},11,[35,333,334],{"class":110},"        r.raise_for_status()\n",[35,336,338,340,342,345,347,349,352,355],{"class":37,"line":337},12,[35,339,175],{"class":124},[35,341,239],{"class":110},[35,343,344],{"class":45},"\"workbook-session-id\"",[35,346,245],{"class":110},[35,348,163],{"class":106},[35,350,351],{"class":110}," r.json()[",[35,353,354],{"class":45},"\"id\"",[35,356,357],{"class":110},"]\n",[35,359,361],{"class":37,"line":360},13,[35,362,118],{"emptyLinePlaceholder":117},[35,364,366,368,372,375,378],{"class":37,"line":365},14,[35,367,154],{"class":106},[35,369,371],{"class":370},"s_Opv"," call",[35,373,374],{"class":110},"(self, method, path, ",[35,376,377],{"class":106},"**",[35,379,380],{"class":110},"kw):\n",[35,382,384,386,388,390,393,395,397,399,401,403,405,407,409,412,414,416,418,420,422,425,427,429],{"class":37,"line":383},15,[35,385,268],{"class":110},[35,387,163],{"class":106},[35,389,273],{"class":124},[35,391,392],{"class":110},".http.request(method, ",[35,394,279],{"class":106},[35,396,186],{"class":45},[35,398,190],{"class":189},[35,400,286],{"class":124},[35,402,289],{"class":110},[35,404,195],{"class":189},[35,406,198],{"class":45},[35,408,190],{"class":189},[35,410,411],{"class":110},"path",[35,413,195],{"class":189},[35,415,186],{"class":45},[35,417,297],{"class":110},[35,419,320],{"class":300},[35,421,163],{"class":106},[35,423,424],{"class":124},"60",[35,426,297],{"class":110},[35,428,377],{"class":106},[35,430,431],{"class":110},"kw)\n",[35,433,435],{"class":37,"line":434},16,[35,436,334],{"class":110},[35,438,440,443,446,449,452,455],{"class":37,"line":439},17,[35,441,442],{"class":106},"        return",[35,444,445],{"class":110}," r.json() ",[35,447,448],{"class":106},"if",[35,450,451],{"class":110}," r.content ",[35,453,454],{"class":106},"else",[35,456,457],{"class":124}," None\n",[35,459,461],{"class":37,"line":460},18,[35,462,118],{"emptyLinePlaceholder":117},[35,464,466,468,471],{"class":37,"line":465},19,[35,467,154],{"class":106},[35,469,470],{"class":370}," close",[35,472,473],{"class":110},"(self):\n",[35,475,477,479,482,485,487,490],{"class":37,"line":476},20,[35,478,175],{"class":124},[35,480,481],{"class":110},".call(",[35,483,484],{"class":45},"\"POST\"",[35,486,297],{"class":110},[35,488,489],{"class":45},"\"closeSession\"",[35,491,328],{"class":110},[10,493,494,497,498,501,502,505,506,509,510,18],{},[32,495,496],{},"createSession"," with ",[32,499,500],{},"persistChanges: true"," returns a session ID that every later request sends in the ",[32,503,504],{},"workbook-session-id"," header; ",[32,507,508],{},"closeSession"," ends it. Without a session, each call opens and closes the workbook on the server, which works but is slower and can hit concurrency limits in busy files — the trade-offs are explained in ",[14,511,513],{"href":512},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Frecalculate-excel-online-workbooks-with-graph-sessions\u002F","Recalculate Excel Online Workbooks with Graph Sessions",[20,515,517],{"id":516},"step-2-add-the-chart","Step 2 — Add the chart",[25,519,521],{"className":97,"code":520,"language":99,"meta":30,"style":30},"def add_chart(wb, sheet, chart_type, source, name, title):\n    chart = wb.call(\"POST\", f\"worksheets\u002F{sheet}\u002Fcharts\u002Fadd\",\n                    json={\"type\": chart_type, \"sourceData\": source, \"seriesBy\": \"Columns\"})\n    wb.call(\"PATCH\", f\"worksheets\u002F{sheet}\u002Fcharts\u002F{chart['name']}\", json={\"name\": name})\n    wb.call(\"PATCH\", f\"worksheets\u002F{sheet}\u002Fcharts\u002F{name}\u002Ftitle\", json={\"text\": title, \"visible\": True})\n    return name\n",[32,522,523,534,566,598,650,703],{"__ignoreMap":30},[35,524,525,528,531],{"class":37,"line":38},[35,526,527],{"class":106},"def",[35,529,530],{"class":370}," add_chart",[35,532,533],{"class":110},"(wb, sheet, chart_type, source, name, title):\n",[35,535,536,539,541,544,546,548,550,553,555,558,560,563],{"class":37,"line":114},[35,537,538],{"class":110},"    chart ",[35,540,163],{"class":106},[35,542,543],{"class":110}," wb.call(",[35,545,484],{"class":45},[35,547,297],{"class":110},[35,549,279],{"class":106},[35,551,552],{"class":45},"\"worksheets\u002F",[35,554,190],{"class":189},[35,556,557],{"class":110},"sheet",[35,559,195],{"class":189},[35,561,562],{"class":45},"\u002Fcharts\u002Fadd\"",[35,564,565],{"class":110},",\n",[35,567,568,571,573,575,578,581,584,587,590,592,595],{"class":37,"line":121},[35,569,570],{"class":300},"                    json",[35,572,163],{"class":106},[35,574,190],{"class":110},[35,576,577],{"class":45},"\"type\"",[35,579,580],{"class":110},": chart_type, ",[35,582,583],{"class":45},"\"sourceData\"",[35,585,586],{"class":110},": source, ",[35,588,589],{"class":45},"\"seriesBy\"",[35,591,311],{"class":110},[35,593,594],{"class":45},"\"Columns\"",[35,596,597],{"class":110},"})\n",[35,599,600,603,606,608,610,612,614,616,618,621,623,626,629,632,634,636,638,640,642,644,647],{"class":37,"line":134},[35,601,602],{"class":110},"    wb.call(",[35,604,605],{"class":45},"\"PATCH\"",[35,607,297],{"class":110},[35,609,279],{"class":106},[35,611,552],{"class":45},[35,613,190],{"class":189},[35,615,557],{"class":110},[35,617,195],{"class":189},[35,619,620],{"class":45},"\u002Fcharts\u002F",[35,622,190],{"class":189},[35,624,625],{"class":110},"chart[",[35,627,628],{"class":45},"'name'",[35,630,631],{"class":110},"]",[35,633,195],{"class":189},[35,635,186],{"class":45},[35,637,297],{"class":110},[35,639,301],{"class":300},[35,641,163],{"class":106},[35,643,190],{"class":110},[35,645,646],{"class":45},"\"name\"",[35,648,649],{"class":110},": name})\n",[35,651,652,654,656,658,660,662,664,666,668,670,672,675,677,680,682,684,686,688,691,694,697,699,701],{"class":37,"line":139},[35,653,602],{"class":110},[35,655,605],{"class":45},[35,657,297],{"class":110},[35,659,279],{"class":106},[35,661,552],{"class":45},[35,663,190],{"class":189},[35,665,557],{"class":110},[35,667,195],{"class":189},[35,669,620],{"class":45},[35,671,190],{"class":189},[35,673,674],{"class":110},"name",[35,676,195],{"class":189},[35,678,679],{"class":45},"\u002Ftitle\"",[35,681,297],{"class":110},[35,683,301],{"class":300},[35,685,163],{"class":106},[35,687,190],{"class":110},[35,689,690],{"class":45},"\"text\"",[35,692,693],{"class":110},": title, ",[35,695,696],{"class":45},"\"visible\"",[35,698,311],{"class":110},[35,700,314],{"class":124},[35,702,597],{"class":110},[35,704,705,708],{"class":37,"line":151},[35,706,707],{"class":106},"    return",[35,709,710],{"class":110}," name\n",[10,712,713,716,717,297,720,297,723,297,726,729,730,733,734,737,738,741,742,18],{},[32,714,715],{},"charts\u002Fadd"," takes the chart type — ",[32,718,719],{},"ColumnClustered",[32,721,722],{},"Line",[32,724,725],{},"BarClustered",[32,727,728],{},"Pie"," and the other Excel chart types — and the source range in A1 notation including headers. ",[32,731,732],{},"seriesBy: \"Columns\""," makes each column a series (North, South) with the months as categories; ",[32,735,736],{},"\"Auto\""," lets Excel guess, which is usually but not always the same. The new chart gets a generated name such as ",[32,739,740],{},"Chart 1",", so the next call renames it to something stable: later runs address the chart by that name. The title is a sub-resource with its own ",[32,743,744],{},"PATCH",[746,747,755,756,755,760,755,764,755,772,755,782,755,789,755,794,755,799,755,804,755,806,755,811,755,816,755,824,755,830,755,833,755,836,755,841,755,846,755,849,755,852,755,857,755,861,755,864,755,867,755,874,755,879,755,882,755,885,755,890],"svg",{"viewBox":748,"role":749,"ariaLabelledBy":750,"xmlns":753,"style":754},"4 0 752 340","img",[751,752],"gc-seq-t","gc-seq-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[757,758,759],"title",{"id":751},"The Graph calls that create and set up a chart",[761,762,763],"desc",{"id":752},"The script creates a workbook session, posts charts\u002Fadd with the type and source range, renames the chart with a PATCH, sets its title with another PATCH, positions it with setPosition, and closes the session.",[765,766],"rect",{"x":767,"y":768,"width":769,"height":770,"fill":771},"4","0","752","340","#ffffff",[765,773],{"x":774,"y":775,"width":776,"height":777,"rx":778,"fill":779,"stroke":780,"style":781},"20.0","16.0","180.0","48.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[783,784,788],"text",{"x":785,"y":786,"style":787},"110.0","44.5","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Python script",[765,790],{"x":791,"y":775,"width":776,"height":777,"rx":778,"fill":792,"stroke":793,"style":781},"560.0","#e7ebef","var(--line,#cdd5e6)",[783,795,798],{"x":796,"y":786,"style":797},"650.0","font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Graph \u002Fworkbook",[37,800],{"x1":785,"y1":801,"x2":785,"y2":802,"stroke":793,"style":803},"64.0","324.0","stroke-width:1.5px;stroke-dasharray:5 4",[37,805],{"x1":796,"y1":801,"x2":796,"y2":802,"stroke":793,"style":803},[37,807],{"x1":808,"y1":809,"x2":810,"y2":809,"stroke":780,"style":781},"115.0","104.0","641.0",[812,813],"polygon",{"points":814,"fill":815},"645.0,104.0 636.0,109.0 636.0,99.0","#5b5cf0",[765,817],{"x":818,"y":819,"width":820,"height":821,"rx":822,"fill":771,"stroke":793,"style":823},"304.6","75.0","150.7","21.0","10","stroke-width:1.2px",[783,825,829],{"x":826,"y":827,"style":828},"380.0","89.5","font-size:11.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","1. POST createSession",[37,831],{"x1":808,"y1":832,"x2":810,"y2":832,"stroke":780,"style":781},"152.0",[812,834],{"points":835,"fill":815},"645.0,152.0 636.0,157.0 636.0,147.0",[765,837],{"x":838,"y":839,"width":840,"height":821,"rx":822,"fill":779,"stroke":780,"style":823},"255.6","123.0","248.8",[783,842,845],{"x":826,"y":843,"style":844},"137.5","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","2. POST charts\u002Fadd {type, sourceData}",[37,847],{"x1":808,"y1":848,"x2":810,"y2":848,"stroke":780,"style":781},"200.0",[812,850],{"points":851,"fill":815},"645.0,200.0 636.0,205.0 636.0,195.0",[765,853],{"x":854,"y":855,"width":856,"height":821,"rx":822,"fill":771,"stroke":793,"style":823},"279.3","171.0","201.4",[783,858,860],{"x":826,"y":859,"style":828},"185.5","3. PATCH charts\u002F{name} + title",[37,862],{"x1":808,"y1":863,"x2":810,"y2":863,"stroke":780,"style":781},"248.0",[812,865],{"points":866,"fill":815},"645.0,248.0 636.0,253.0 636.0,243.0",[765,868],{"x":869,"y":870,"width":871,"height":821,"rx":822,"fill":872,"stroke":873,"style":823},"254.2","219.0","251.6","#d9f4f1","var(--teal,#0f9488)",[783,875,878],{"x":826,"y":876,"style":877},"233.5","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","4. POST setPosition {startCell, endCell}",[37,880],{"x1":808,"y1":881,"x2":810,"y2":881,"stroke":780,"style":781},"296.0",[812,883],{"points":884,"fill":815},"645.0,296.0 636.0,301.0 636.0,291.0",[765,886],{"x":887,"y":888,"width":889,"height":821,"rx":822,"fill":771,"stroke":793,"style":823},"306.5","267.0","146.9",[783,891,893],{"x":826,"y":892,"style":828},"281.5","5. POST closeSession",[20,895,897],{"id":896},"step-3-put-it-where-people-expect-it","Step 3 — Put it where people expect it",[10,899,900,901,904],{},"A chart added through Graph appears at a default position, often on top of the data. ",[32,902,903],{},"setPosition"," places it over a block of cells, which keeps it aligned with the grid when column widths change:",[25,906,908],{"className":97,"code":907,"language":99,"meta":30,"style":30},"def place_chart(wb, sheet, name, start_cell, end_cell):\n    wb.call(\"POST\", f\"worksheets\u002F{sheet}\u002Fcharts\u002F{name}\u002FsetPosition\",\n            json={\"startCell\": start_cell, \"endCell\": end_cell})\n",[32,909,910,920,951],{"__ignoreMap":30},[35,911,912,914,917],{"class":37,"line":38},[35,913,527],{"class":106},[35,915,916],{"class":370}," place_chart",[35,918,919],{"class":110},"(wb, sheet, name, start_cell, end_cell):\n",[35,921,922,924,926,928,930,932,934,936,938,940,942,944,946,949],{"class":37,"line":114},[35,923,602],{"class":110},[35,925,484],{"class":45},[35,927,297],{"class":110},[35,929,279],{"class":106},[35,931,552],{"class":45},[35,933,190],{"class":189},[35,935,557],{"class":110},[35,937,195],{"class":189},[35,939,620],{"class":45},[35,941,190],{"class":189},[35,943,674],{"class":110},[35,945,195],{"class":189},[35,947,948],{"class":45},"\u002FsetPosition\"",[35,950,565],{"class":110},[35,952,953,956,958,960,963,966,969],{"class":37,"line":121},[35,954,955],{"class":300},"            json",[35,957,163],{"class":106},[35,959,190],{"class":110},[35,961,962],{"class":45},"\"startCell\"",[35,964,965],{"class":110},": start_cell, ",[35,967,968],{"class":45},"\"endCell\"",[35,970,971],{"class":110},": end_cell})\n",[10,973,974,975,297,978,297,981,984,985,988],{},"Placing the chart over E2:L18 puts it beside the A:C data, about eight columns wide and sixteen rows high. The chart also has ",[32,976,977],{},"top",[32,979,980],{},"left",[32,982,983],{},"width"," and ",[32,986,987],{},"height"," properties in points that can be patched directly, but cell-based placement is easier to reason about and survives zoom levels.",[20,990,992],{"id":991},"step-4-point-an-existing-chart-at-new-data","Step 4 — Point an existing chart at new data",[10,994,995],{},"Next month the table has a thirteenth row. Rather than deleting and re-creating the chart — which loses any formatting someone applied by hand — re-point it:",[25,997,999],{"className":97,"code":998,"language":99,"meta":30,"style":30},"def update_chart_source(wb, sheet, name, source):\n    wb.call(\"POST\", f\"worksheets\u002F{sheet}\u002Fcharts\u002F{name}\u002FsetData\",\n            json={\"sourceData\": source, \"seriesBy\": \"Columns\"})\n\ndef ensure_chart(wb, sheet, name, source, title):\n    existing = {c[\"name\"] for c in wb.call(\"GET\", f\"worksheets\u002F{sheet}\u002Fcharts\")[\"value\"]}\n    if name in existing:\n        update_chart_source(wb, sheet, name, source)\n    else:\n        add_chart(wb, sheet, \"ColumnClustered\", source, name, title)\n        place_chart(wb, sheet, name, \"E2\", \"L18\")\n",[32,1000,1001,1011,1042,1062,1066,1076,1128,1141,1146,1153,1164],{"__ignoreMap":30},[35,1002,1003,1005,1008],{"class":37,"line":38},[35,1004,527],{"class":106},[35,1006,1007],{"class":370}," update_chart_source",[35,1009,1010],{"class":110},"(wb, sheet, name, source):\n",[35,1012,1013,1015,1017,1019,1021,1023,1025,1027,1029,1031,1033,1035,1037,1040],{"class":37,"line":114},[35,1014,602],{"class":110},[35,1016,484],{"class":45},[35,1018,297],{"class":110},[35,1020,279],{"class":106},[35,1022,552],{"class":45},[35,1024,190],{"class":189},[35,1026,557],{"class":110},[35,1028,195],{"class":189},[35,1030,620],{"class":45},[35,1032,190],{"class":189},[35,1034,674],{"class":110},[35,1036,195],{"class":189},[35,1038,1039],{"class":45},"\u002FsetData\"",[35,1041,565],{"class":110},[35,1043,1044,1046,1048,1050,1052,1054,1056,1058,1060],{"class":37,"line":121},[35,1045,955],{"class":300},[35,1047,163],{"class":106},[35,1049,190],{"class":110},[35,1051,583],{"class":45},[35,1053,586],{"class":110},[35,1055,589],{"class":45},[35,1057,311],{"class":110},[35,1059,594],{"class":45},[35,1061,597],{"class":110},[35,1063,1064],{"class":37,"line":134},[35,1065,118],{"emptyLinePlaceholder":117},[35,1067,1068,1070,1073],{"class":37,"line":139},[35,1069,527],{"class":106},[35,1071,1072],{"class":370}," ensure_chart",[35,1074,1075],{"class":110},"(wb, sheet, name, source, title):\n",[35,1077,1078,1081,1083,1086,1088,1090,1093,1096,1099,1101,1104,1106,1108,1110,1112,1114,1116,1119,1122,1125],{"class":37,"line":151},[35,1079,1080],{"class":110},"    existing ",[35,1082,163],{"class":106},[35,1084,1085],{"class":110}," {c[",[35,1087,646],{"class":45},[35,1089,245],{"class":110},[35,1091,1092],{"class":106},"for",[35,1094,1095],{"class":110}," c ",[35,1097,1098],{"class":106},"in",[35,1100,543],{"class":110},[35,1102,1103],{"class":45},"\"GET\"",[35,1105,297],{"class":110},[35,1107,279],{"class":106},[35,1109,552],{"class":45},[35,1111,190],{"class":189},[35,1113,557],{"class":110},[35,1115,195],{"class":189},[35,1117,1118],{"class":45},"\u002Fcharts\"",[35,1120,1121],{"class":110},")[",[35,1123,1124],{"class":45},"\"value\"",[35,1126,1127],{"class":110},"]}\n",[35,1129,1130,1133,1136,1138],{"class":37,"line":172},[35,1131,1132],{"class":106},"    if",[35,1134,1135],{"class":110}," name ",[35,1137,1098],{"class":106},[35,1139,1140],{"class":110}," existing:\n",[35,1142,1143],{"class":37,"line":221},[35,1144,1145],{"class":110},"        update_chart_source(wb, sheet, name, source)\n",[35,1147,1148,1151],{"class":37,"line":234},[35,1149,1150],{"class":106},"    else",[35,1152,148],{"class":110},[35,1154,1155,1158,1161],{"class":37,"line":265},[35,1156,1157],{"class":110},"        add_chart(wb, sheet, ",[35,1159,1160],{"class":45},"\"ColumnClustered\"",[35,1162,1163],{"class":110},", source, name, title)\n",[35,1165,1166,1169,1172,1174,1177],{"class":37,"line":331},[35,1167,1168],{"class":110},"        place_chart(wb, sheet, name, ",[35,1170,1171],{"class":45},"\"E2\"",[35,1173,297],{"class":110},[35,1175,1176],{"class":45},"\"L18\"",[35,1178,328],{"class":110},[10,1180,1181,1184,1185,1188,1189,18],{},[32,1182,1183],{},"ensure_chart"," makes the job safe to run repeatedly: it lists the sheet's charts, updates the source range if the named chart exists, and creates and places it only on the first run. The size of the data range usually comes from the table itself — if the data is an Excel table, read its range first (",[32,1186,1187],{},"GET tables\u002F{name}\u002Frange",") and pass that address, so the chart always covers exactly the table's rows. Appending those rows through Graph is covered in ",[14,1190,1192],{"href":1191},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph\u002F","Append Rows to an Excel Online Table with Microsoft Graph",[746,1194,755,1199,755,1202,755,1205,755,1208,755,1213,755,1223,755,1227,755,1231,755,1235,755,1238,755,1241,755,1246,755,1251,755,1256,755,1263,755,1268,755,1273,755,1275,755,1278,755,1282,755,1285,755,1288,755,1294,755,1297],{"viewBox":1195,"role":749,"ariaLabelledBy":1196,"xmlns":753,"style":754},"4 0 752 272",[1197,1198],"gc-ensure-t","gc-ensure-d",[757,1200,1201],{"id":1197},"What ensure_chart does on each run",[761,1203,1204],{"id":1198},"The script lists the sheet's charts. If a chart with the stable name exists, it calls setData with the new source range and keeps its formatting. If not, it adds the chart, renames it, sets the title and positions it.",[765,1206],{"x":767,"y":768,"width":769,"height":1207,"fill":771},"272",[765,1209],{"x":1210,"y":775,"width":1211,"height":1212,"rx":778,"fill":779,"stroke":780,"style":781},"207.0","346.1","75.5",[1214,1215,1217],"g",{"transform":1216},"translate(368.0,29.0) scale(1.000)",[411,1218],{"d":1219,"fill":1220,"stroke":1221,"style":1222},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[783,1224,1226],{"x":826,"y":1212,"style":1225},"font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Does a chart named SalesByMonth exist?",[37,1228],{"x1":826,"y1":1229,"x2":826,"y2":1230,"stroke":780,"style":781},"91.5","111.5",[37,1232],{"x1":1233,"y1":1230,"x2":1234,"y2":1230,"stroke":780,"style":781},"196.0","564.0",[37,1236],{"x1":1233,"y1":1230,"x2":1233,"y2":1237,"stroke":780,"style":781},"157.5",[812,1239],{"points":1240,"fill":815},"196.0,161.5 191.0,152.5 201.0,152.5",[765,1242],{"x":1243,"y":1244,"width":1245,"height":821,"rx":822,"fill":771,"stroke":793,"style":823},"176.4","121.5","39.2",[783,1247,1250],{"x":1233,"y":1248,"style":1249},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","yes",[765,1252],{"x":774,"y":1253,"width":1254,"height":1255,"rx":778,"fill":872,"stroke":873,"style":781},"163.5","352.0","93.0",[1214,1257,1259],{"transform":1258},"translate(184.0,176.5) scale(1.000)",[411,1260],{"d":1261,"fill":1220,"stroke":1262,"style":1222},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7","var(--teal-ink,#0b6157)",[783,1264,1267],{"x":1233,"y":1265,"style":1266},"223.0","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","setData",[783,1269,1272],{"x":1233,"y":1270,"style":1271},"242.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","new range, formatting kept",[37,1274],{"x1":1234,"y1":1230,"x2":1234,"y2":1237,"stroke":780,"style":781},[812,1276],{"points":1277,"fill":815},"564.0,161.5 559.0,152.5 569.0,152.5",[765,1279],{"x":1280,"y":1244,"width":1281,"height":821,"rx":822,"fill":771,"stroke":793,"style":823},"547.5","32.9",[783,1283,1284],{"x":1234,"y":1248,"style":1249},"no",[765,1286],{"x":1287,"y":1253,"width":1254,"height":1255,"rx":778,"fill":779,"stroke":780,"style":781},"388.0",[1214,1289,1291],{"transform":1290},"translate(552.0,176.5) scale(1.000)",[411,1292],{"d":1293,"fill":1220,"stroke":1221,"style":1222},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8",[783,1295,1296],{"x":1234,"y":1265,"style":1225},"add + rename + title + setPosition",[783,1298,1299],{"x":1234,"y":1270,"style":1271},"first run only",[20,1301,1303],{"id":1302},"step-5-get-a-picture-of-the-chart","Step 5 — Get a picture of the chart",[10,1305,1306],{},"Graph can render a chart to an image, returned as base64 text — useful for putting the current chart into an email or a Teams message without screenshots:",[25,1308,1310],{"className":97,"code":1309,"language":99,"meta":30,"style":30},"import base64\n\ndef chart_png(wb, sheet, name, width=800, height=400, path=\"chart.png\"):\n    data = wb.call(\"GET\", f\"worksheets\u002F{sheet}\u002Fcharts\u002F{name}\u002Fimage(width={width},height={height},fittingMode='fit')\")\n    with open(path, \"wb\") as fh:\n        fh.write(base64.b64decode(data[\"value\"]))\n    return path\n",[32,1311,1312,1319,1323,1356,1410,1433,1443],{"__ignoreMap":30},[35,1313,1314,1316],{"class":37,"line":38},[35,1315,107],{"class":106},[35,1317,1318],{"class":110}," base64\n",[35,1320,1321],{"class":37,"line":114},[35,1322,118],{"emptyLinePlaceholder":117},[35,1324,1325,1327,1330,1333,1335,1338,1341,1343,1346,1349,1351,1354],{"class":37,"line":121},[35,1326,527],{"class":106},[35,1328,1329],{"class":370}," chart_png",[35,1331,1332],{"class":110},"(wb, sheet, name, width",[35,1334,163],{"class":106},[35,1336,1337],{"class":124},"800",[35,1339,1340],{"class":110},", height",[35,1342,163],{"class":106},[35,1344,1345],{"class":124},"400",[35,1347,1348],{"class":110},", path",[35,1350,163],{"class":106},[35,1352,1353],{"class":45},"\"chart.png\"",[35,1355,169],{"class":110},[35,1357,1358,1361,1363,1365,1367,1369,1371,1373,1375,1377,1379,1381,1383,1385,1387,1390,1392,1394,1396,1399,1401,1403,1405,1408],{"class":37,"line":134},[35,1359,1360],{"class":110},"    data ",[35,1362,163],{"class":106},[35,1364,543],{"class":110},[35,1366,1103],{"class":45},[35,1368,297],{"class":110},[35,1370,279],{"class":106},[35,1372,552],{"class":45},[35,1374,190],{"class":189},[35,1376,557],{"class":110},[35,1378,195],{"class":189},[35,1380,620],{"class":45},[35,1382,190],{"class":189},[35,1384,674],{"class":110},[35,1386,195],{"class":189},[35,1388,1389],{"class":45},"\u002Fimage(width=",[35,1391,190],{"class":189},[35,1393,983],{"class":110},[35,1395,195],{"class":189},[35,1397,1398],{"class":45},",height=",[35,1400,190],{"class":189},[35,1402,987],{"class":110},[35,1404,195],{"class":189},[35,1406,1407],{"class":45},",fittingMode='fit')\"",[35,1409,328],{"class":110},[35,1411,1412,1415,1418,1421,1424,1427,1430],{"class":37,"line":139},[35,1413,1414],{"class":106},"    with",[35,1416,1417],{"class":124}," open",[35,1419,1420],{"class":110},"(path, ",[35,1422,1423],{"class":45},"\"wb\"",[35,1425,1426],{"class":110},") ",[35,1428,1429],{"class":106},"as",[35,1431,1432],{"class":110}," fh:\n",[35,1434,1435,1438,1440],{"class":37,"line":151},[35,1436,1437],{"class":110},"        fh.write(base64.b64decode(data[",[35,1439,1124],{"class":45},[35,1441,1442],{"class":110},"]))\n",[35,1444,1445,1447],{"class":37,"line":172},[35,1446,707],{"class":106},[35,1448,1449],{"class":110}," path\n",[10,1451,1452,1453,1456,1457,1460,1461,18],{},"The ",[32,1454,1455],{},"image"," function takes the size in pixels and a fitting mode; the response's ",[32,1458,1459],{},"value"," is the PNG as base64, decoded and written to a file. Embedding it in an email body is shown in ",[14,1462,1464],{"href":1463},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fembed-a-chart-image-in-the-body-of-a-report-email\u002F","Embed a Chart Image in the Body of a Report Email",[746,1466,755,1471,755,1474,755,1477,755,1480,755,1484,755,1490,755,1496,755,1500,755,1504,755,1507,755,1510,755,1516,755,1520,755,1524,755,1528,755,1532,755,1535,755,1538,755,1544,755,1548,755,1551,755,1555,755,1558,755,1561,755,1567,755,1571],{"viewBox":1467,"role":749,"ariaLabelledBy":1468,"xmlns":753,"style":754},"4 0 752 142",[1469,1470],"gc-image-t","gc-image-d",[757,1472,1473],{"id":1469},"From a live chart to a PNG attachment",[761,1475,1476],{"id":1470},"The chart in the online workbook is rendered by Graph's image function, returned as base64 text in a JSON response, decoded into PNG bytes and saved, ready to embed in an email or a chat message.",[765,1478],{"x":767,"y":768,"width":769,"height":1479,"fill":771},"142",[765,1481],{"x":774,"y":775,"width":1482,"height":1483,"rx":778,"fill":792,"stroke":793,"style":781},"153.0","110.5",[1214,1485,1487],{"transform":1486},"translate(84.5,37.8) scale(1.000)",[411,1488],{"d":1293,"fill":1220,"stroke":1489,"style":1222},"var(--text,#172033)",[783,1491,1495],{"x":1492,"y":1493,"style":1494},"96.5","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Chart in OneDrive",[783,1497,1499],{"x":1492,"y":1498,"style":1271},"103.8","SalesByMonth",[37,1501],{"x1":1502,"y1":1503,"x2":848,"y2":1503,"stroke":780,"style":781},"178.0","71.2",[812,1505],{"points":1506,"fill":815},"204.0,71.2 195.0,76.2 195.0,66.2",[765,1508],{"x":1509,"y":775,"width":1482,"height":1483,"rx":778,"fill":779,"stroke":780,"style":781},"209.0",[1214,1511,1513],{"transform":1512},"translate(273.5,29.0) scale(1.000)",[411,1514],{"d":1515,"fill":1220,"stroke":1221,"style":1222},"M7 18h10a4 4 0 0 0 .6-8A6 6 0 0 0 6.1 11.4 3.4 3.4 0 0 0 7 18z",[783,1517,1519],{"x":1518,"y":1212,"style":1225},"285.5","GET …\u002Fimage(…)",[783,1521,1523],{"x":1518,"y":1522,"style":1271},"95.0","rendered by Excel",[783,1525,1527],{"x":1518,"y":1526,"style":1271},"112.5","Online",[37,1529],{"x1":1530,"y1":1503,"x2":1531,"y2":1503,"stroke":780,"style":781},"367.0","389.0",[812,1533],{"points":1534,"fill":815},"393.0,71.2 384.0,76.2 384.0,66.2",[765,1536],{"x":1537,"y":775,"width":1482,"height":1483,"rx":778,"fill":779,"stroke":780,"style":781},"398.0",[1214,1539,1541],{"transform":1540},"translate(462.5,37.8) scale(1.000)",[411,1542],{"d":1543,"fill":1220,"stroke":1221,"style":1222},"M9 8l-4 4 4 4 M15 8l4 4-4 4",[783,1545,1547],{"x":1546,"y":1493,"style":1225},"474.5","base64 in JSON",[783,1549,1550],{"x":1546,"y":1498,"style":1271},"data[\"value\"]",[37,1552],{"x1":1553,"y1":1503,"x2":1554,"y2":1503,"stroke":780,"style":781},"556.0","578.0",[812,1556],{"points":1557,"fill":815},"582.0,71.2 573.0,76.2 573.0,66.2",[765,1559],{"x":1560,"y":775,"width":1482,"height":1483,"rx":778,"fill":872,"stroke":873,"style":781},"587.0",[1214,1562,1564],{"transform":1563},"translate(651.5,37.8) scale(1.000)",[411,1565],{"d":1566,"fill":1220,"stroke":1262,"style":1222},"M4 5h16v14H4z M4 16l5-5 4 4 3-3 4 4 M15 9h.01",[783,1568,1570],{"x":1569,"y":1493,"style":1266},"663.5","chart.png",[783,1572,1573],{"x":1569,"y":1498,"style":1271},"ready for email",[20,1575,1577],{"id":1576},"step-6-test-the-calls-without-a-tenant","Step 6 — Test the calls without a tenant",[10,1579,1452,1580,1582,1583,1586],{},[32,1581,82],{}," library intercepts ",[32,1584,1585],{},"requests"," calls and returns canned answers, so the whole flow can run in a test and the requests can be inspected:",[25,1588,1590],{"className":97,"code":1589,"language":99,"meta":30,"style":30},"import json\nimport responses\n\nITEM = \"01ABCDEF\"\nBASE = f\"{GRAPH}\u002Fme\u002Fdrive\u002Fitems\u002F{ITEM}\u002Fworkbook\"\npng = base64.b64encode(b\"\\x89PNG fake image bytes\").decode()\n\nwith responses.RequestsMock(assert_all_requests_are_fired=False) as mock:\n    mock.post(f\"{BASE}\u002FcreateSession\", json={\"id\": \"sess-123\"})\n    mock.get(f\"{BASE}\u002Fworksheets\u002FSales\u002Fcharts\", json={\"value\": []})\n    mock.post(f\"{BASE}\u002Fworksheets\u002FSales\u002Fcharts\u002Fadd\", json={\"name\": \"Chart 1\", \"id\": \"{C1}\"})\n    mock.patch(f\"{BASE}\u002Fworksheets\u002FSales\u002Fcharts\u002FChart 1\", json={\"name\": \"SalesByMonth\"})\n    mock.patch(f\"{BASE}\u002Fworksheets\u002FSales\u002Fcharts\u002FSalesByMonth\u002Ftitle\", json={\"text\": \"Sales by month\"})\n    mock.post(f\"{BASE}\u002Fworksheets\u002FSales\u002Fcharts\u002FSalesByMonth\u002FsetPosition\", body=b\"\")\n    mock.get(f\"{BASE}\u002Fworksheets\u002FSales\u002Fcharts\u002FSalesByMonth\u002Fimage(width=800,height=400,fittingMode='fit')\",\n             json={\"value\": png})\n    mock.post(f\"{BASE}\u002FcloseSession\", body=b\"\")\n\n    wb = Workbook(\"test-token\", ITEM)\n    ensure_chart(wb, \"Sales\", \"SalesByMonth\", \"A1:C13\", \"Sales by month\")\n    out = chart_png(wb, \"Sales\", \"SalesByMonth\")\n    wb.close()\n\n    for call in mock.calls:\n        body = json.loads(call.request.body) if call.request.body else \"\"\n        print(call.request.method, call.request.url.split(\"\u002Fworkbook\u002F\")[1], body)\n    print(\"session header sent:\", mock.calls[1].request.headers.get(\"workbook-session-id\"))\nwith open(out, \"rb\") as fh:\n    print(fh.read(4))\n",[32,1591,1592,1599,1605,1609,1619,1647,1671,1675,1698,1732,1763,1810,1845,1879,1910,1929,1943,1972,1976,1995,2018,2037,2043,2048,2062,2083,2103,2128,2147],{"__ignoreMap":30},[35,1593,1594,1596],{"class":37,"line":38},[35,1595,107],{"class":106},[35,1597,1598],{"class":110}," json\n",[35,1600,1601,1603],{"class":37,"line":114},[35,1602,107],{"class":106},[35,1604,55],{"class":110},[35,1606,1607],{"class":37,"line":121},[35,1608,118],{"emptyLinePlaceholder":117},[35,1610,1611,1614,1616],{"class":37,"line":134},[35,1612,1613],{"class":124},"ITEM",[35,1615,128],{"class":106},[35,1617,1618],{"class":45}," \"01ABCDEF\"\n",[35,1620,1621,1624,1626,1628,1630,1632,1634,1636,1639,1641,1643,1645],{"class":37,"line":139},[35,1622,1623],{"class":124},"BASE",[35,1625,128],{"class":106},[35,1627,183],{"class":106},[35,1629,186],{"class":45},[35,1631,190],{"class":189},[35,1633,125],{"class":124},[35,1635,195],{"class":189},[35,1637,1638],{"class":45},"\u002Fme\u002Fdrive\u002Fitems\u002F",[35,1640,190],{"class":189},[35,1642,1613],{"class":124},[35,1644,195],{"class":189},[35,1646,218],{"class":45},[35,1648,1649,1652,1654,1657,1660,1662,1665,1668],{"class":37,"line":151},[35,1650,1651],{"class":110},"png ",[35,1653,163],{"class":106},[35,1655,1656],{"class":110}," base64.b64encode(",[35,1658,1659],{"class":106},"b",[35,1661,186],{"class":45},[35,1663,1664],{"class":189},"\\x89",[35,1666,1667],{"class":45},"PNG fake image bytes\"",[35,1669,1670],{"class":110},").decode()\n",[35,1672,1673],{"class":37,"line":172},[35,1674,118],{"emptyLinePlaceholder":117},[35,1676,1677,1680,1683,1686,1688,1691,1693,1695],{"class":37,"line":221},[35,1678,1679],{"class":106},"with",[35,1681,1682],{"class":110}," responses.RequestsMock(",[35,1684,1685],{"class":300},"assert_all_requests_are_fired",[35,1687,163],{"class":106},[35,1689,1690],{"class":124},"False",[35,1692,1426],{"class":110},[35,1694,1429],{"class":106},[35,1696,1697],{"class":110}," mock:\n",[35,1699,1700,1703,1705,1707,1709,1711,1713,1715,1717,1719,1721,1723,1725,1727,1730],{"class":37,"line":234},[35,1701,1702],{"class":110},"    mock.post(",[35,1704,279],{"class":106},[35,1706,186],{"class":45},[35,1708,190],{"class":189},[35,1710,1623],{"class":124},[35,1712,195],{"class":189},[35,1714,294],{"class":45},[35,1716,297],{"class":110},[35,1718,301],{"class":300},[35,1720,163],{"class":106},[35,1722,190],{"class":110},[35,1724,354],{"class":45},[35,1726,311],{"class":110},[35,1728,1729],{"class":45},"\"sess-123\"",[35,1731,597],{"class":110},[35,1733,1734,1737,1739,1741,1743,1745,1747,1750,1752,1754,1756,1758,1760],{"class":37,"line":265},[35,1735,1736],{"class":110},"    mock.get(",[35,1738,279],{"class":106},[35,1740,186],{"class":45},[35,1742,190],{"class":189},[35,1744,1623],{"class":124},[35,1746,195],{"class":189},[35,1748,1749],{"class":45},"\u002Fworksheets\u002FSales\u002Fcharts\"",[35,1751,297],{"class":110},[35,1753,301],{"class":300},[35,1755,163],{"class":106},[35,1757,190],{"class":110},[35,1759,1124],{"class":45},[35,1761,1762],{"class":110},": []})\n",[35,1764,1765,1767,1769,1771,1773,1775,1777,1780,1782,1784,1786,1788,1790,1792,1795,1797,1799,1801,1803,1806,1808],{"class":37,"line":331},[35,1766,1702],{"class":110},[35,1768,279],{"class":106},[35,1770,186],{"class":45},[35,1772,190],{"class":189},[35,1774,1623],{"class":124},[35,1776,195],{"class":189},[35,1778,1779],{"class":45},"\u002Fworksheets\u002FSales\u002Fcharts\u002Fadd\"",[35,1781,297],{"class":110},[35,1783,301],{"class":300},[35,1785,163],{"class":106},[35,1787,190],{"class":110},[35,1789,646],{"class":45},[35,1791,311],{"class":110},[35,1793,1794],{"class":45},"\"Chart 1\"",[35,1796,297],{"class":110},[35,1798,354],{"class":45},[35,1800,311],{"class":110},[35,1802,186],{"class":45},[35,1804,1805],{"class":189},"{C1}",[35,1807,186],{"class":45},[35,1809,597],{"class":110},[35,1811,1812,1815,1817,1819,1821,1823,1825,1828,1830,1832,1834,1836,1838,1840,1843],{"class":37,"line":337},[35,1813,1814],{"class":110},"    mock.patch(",[35,1816,279],{"class":106},[35,1818,186],{"class":45},[35,1820,190],{"class":189},[35,1822,1623],{"class":124},[35,1824,195],{"class":189},[35,1826,1827],{"class":45},"\u002Fworksheets\u002FSales\u002Fcharts\u002FChart 1\"",[35,1829,297],{"class":110},[35,1831,301],{"class":300},[35,1833,163],{"class":106},[35,1835,190],{"class":110},[35,1837,646],{"class":45},[35,1839,311],{"class":110},[35,1841,1842],{"class":45},"\"SalesByMonth\"",[35,1844,597],{"class":110},[35,1846,1847,1849,1851,1853,1855,1857,1859,1862,1864,1866,1868,1870,1872,1874,1877],{"class":37,"line":360},[35,1848,1814],{"class":110},[35,1850,279],{"class":106},[35,1852,186],{"class":45},[35,1854,190],{"class":189},[35,1856,1623],{"class":124},[35,1858,195],{"class":189},[35,1860,1861],{"class":45},"\u002Fworksheets\u002FSales\u002Fcharts\u002FSalesByMonth\u002Ftitle\"",[35,1863,297],{"class":110},[35,1865,301],{"class":300},[35,1867,163],{"class":106},[35,1869,190],{"class":110},[35,1871,690],{"class":45},[35,1873,311],{"class":110},[35,1875,1876],{"class":45},"\"Sales by month\"",[35,1878,597],{"class":110},[35,1880,1881,1883,1885,1887,1889,1891,1893,1896,1898,1901,1903,1905,1908],{"class":37,"line":365},[35,1882,1702],{"class":110},[35,1884,279],{"class":106},[35,1886,186],{"class":45},[35,1888,190],{"class":189},[35,1890,1623],{"class":124},[35,1892,195],{"class":189},[35,1894,1895],{"class":45},"\u002Fworksheets\u002FSales\u002Fcharts\u002FSalesByMonth\u002FsetPosition\"",[35,1897,297],{"class":110},[35,1899,1900],{"class":300},"body",[35,1902,163],{"class":106},[35,1904,1659],{"class":106},[35,1906,1907],{"class":45},"\"\"",[35,1909,328],{"class":110},[35,1911,1912,1914,1916,1918,1920,1922,1924,1927],{"class":37,"line":383},[35,1913,1736],{"class":110},[35,1915,279],{"class":106},[35,1917,186],{"class":45},[35,1919,190],{"class":189},[35,1921,1623],{"class":124},[35,1923,195],{"class":189},[35,1925,1926],{"class":45},"\u002Fworksheets\u002FSales\u002Fcharts\u002FSalesByMonth\u002Fimage(width=800,height=400,fittingMode='fit')\"",[35,1928,565],{"class":110},[35,1930,1931,1934,1936,1938,1940],{"class":37,"line":434},[35,1932,1933],{"class":300},"             json",[35,1935,163],{"class":106},[35,1937,190],{"class":110},[35,1939,1124],{"class":45},[35,1941,1942],{"class":110},": png})\n",[35,1944,1945,1947,1949,1951,1953,1955,1957,1960,1962,1964,1966,1968,1970],{"class":37,"line":439},[35,1946,1702],{"class":110},[35,1948,279],{"class":106},[35,1950,186],{"class":45},[35,1952,190],{"class":189},[35,1954,1623],{"class":124},[35,1956,195],{"class":189},[35,1958,1959],{"class":45},"\u002FcloseSession\"",[35,1961,297],{"class":110},[35,1963,1900],{"class":300},[35,1965,163],{"class":106},[35,1967,1659],{"class":106},[35,1969,1907],{"class":45},[35,1971,328],{"class":110},[35,1973,1974],{"class":37,"line":460},[35,1975,118],{"emptyLinePlaceholder":117},[35,1977,1978,1981,1983,1986,1989,1991,1993],{"class":37,"line":465},[35,1979,1980],{"class":110},"    wb ",[35,1982,163],{"class":106},[35,1984,1985],{"class":110}," Workbook(",[35,1987,1988],{"class":45},"\"test-token\"",[35,1990,297],{"class":110},[35,1992,1613],{"class":124},[35,1994,328],{"class":110},[35,1996,1997,2000,2003,2005,2007,2009,2012,2014,2016],{"class":37,"line":476},[35,1998,1999],{"class":110},"    ensure_chart(wb, ",[35,2001,2002],{"class":45},"\"Sales\"",[35,2004,297],{"class":110},[35,2006,1842],{"class":45},[35,2008,297],{"class":110},[35,2010,2011],{"class":45},"\"A1:C13\"",[35,2013,297],{"class":110},[35,2015,1876],{"class":45},[35,2017,328],{"class":110},[35,2019,2021,2024,2026,2029,2031,2033,2035],{"class":37,"line":2020},21,[35,2022,2023],{"class":110},"    out ",[35,2025,163],{"class":106},[35,2027,2028],{"class":110}," chart_png(wb, ",[35,2030,2002],{"class":45},[35,2032,297],{"class":110},[35,2034,1842],{"class":45},[35,2036,328],{"class":110},[35,2038,2040],{"class":37,"line":2039},22,[35,2041,2042],{"class":110},"    wb.close()\n",[35,2044,2046],{"class":37,"line":2045},23,[35,2047,118],{"emptyLinePlaceholder":117},[35,2049,2051,2054,2057,2059],{"class":37,"line":2050},24,[35,2052,2053],{"class":106},"    for",[35,2055,2056],{"class":110}," call ",[35,2058,1098],{"class":106},[35,2060,2061],{"class":110}," mock.calls:\n",[35,2063,2065,2068,2070,2073,2075,2078,2080],{"class":37,"line":2064},25,[35,2066,2067],{"class":110},"        body ",[35,2069,163],{"class":106},[35,2071,2072],{"class":110}," json.loads(call.request.body) ",[35,2074,448],{"class":106},[35,2076,2077],{"class":110}," call.request.body ",[35,2079,454],{"class":106},[35,2081,2082],{"class":45}," \"\"\n",[35,2084,2086,2089,2092,2095,2097,2100],{"class":37,"line":2085},26,[35,2087,2088],{"class":124},"        print",[35,2090,2091],{"class":110},"(call.request.method, call.request.url.split(",[35,2093,2094],{"class":45},"\"\u002Fworkbook\u002F\"",[35,2096,1121],{"class":110},[35,2098,2099],{"class":124},"1",[35,2101,2102],{"class":110},"], body)\n",[35,2104,2106,2109,2112,2115,2118,2120,2123,2125],{"class":37,"line":2105},27,[35,2107,2108],{"class":124},"    print",[35,2110,2111],{"class":110},"(",[35,2113,2114],{"class":45},"\"session header sent:\"",[35,2116,2117],{"class":110},", mock.calls[",[35,2119,2099],{"class":124},[35,2121,2122],{"class":110},"].request.headers.get(",[35,2124,344],{"class":45},[35,2126,2127],{"class":110},"))\n",[35,2129,2131,2133,2135,2138,2141,2143,2145],{"class":37,"line":2130},28,[35,2132,1679],{"class":106},[35,2134,1417],{"class":124},[35,2136,2137],{"class":110},"(out, ",[35,2139,2140],{"class":45},"\"rb\"",[35,2142,1426],{"class":110},[35,2144,1429],{"class":106},[35,2146,1432],{"class":110},[35,2148,2150,2152,2155,2157],{"class":37,"line":2149},29,[35,2151,2108],{"class":124},[35,2153,2154],{"class":110},"(fh.read(",[35,2156,767],{"class":124},[35,2158,2127],{"class":110},[10,2160,2161,2162,2164,2165,2168,2169,2171,2172,497,2174,984,2176,2179,2180,2182,2183,2185],{},"The printout also shows a detail of URL handling: the generated name ",[32,2163,740],{}," was sent as ",[32,2166,2167],{},"Chart%201",", because ",[32,2170,1585],{}," percent-encodes the space — another reason to rename charts to simple, stable names. The printout lists every request in order with its JSON body — ",[32,2173,715],{},[32,2175,719],{},[32,2177,2178],{},"A1:C13",", the rename, the title, ",[32,2181,903],{}," with E2 and L18 — and confirms that the session header was sent after ",[32,2184,496],{},". A test like this catches broken URLs and bodies without credentials; a final check against a real test workbook is still worth doing once, because a mock only knows what you told it.",[20,2187,2189],{"id":2188},"common-pitfalls","Common pitfalls",[2191,2192,2193,2204,2212,2227,2236],"ul",{},[2194,2195,2196,2200,2201,2203],"li",{},[2197,2198,2199],"strong",{},"Addressing charts by generated names."," ",[32,2202,740],{}," changes between files; rename the chart and use the stable name.",[2194,2205,2206,2209,2210,18],{},[2197,2207,2208],{},"Recreating instead of updating."," Deleting and re-adding loses manual formatting; use ",[32,2211,1267],{},[2194,2213,2214,2200,2217,2219,2220,2223,2224,2226],{},[2197,2215,2216],{},"Names with spaces in URLs.",[32,2218,1585],{}," encodes spaces, but special characters such as ",[32,2221,2222],{},"#"," or ",[32,2225,198],{}," in chart or sheet names break the path; keep names simple.",[2194,2228,2229,2232,2233,2235],{},[2197,2230,2231],{},"No session for many calls."," Use ",[32,2234,496],{}," for a series of edits and close it afterwards.",[2194,2237,2238,2241,2242,18],{},[2197,2239,2240],{},"Throttling."," Busy workbooks return 429 or 503; retry with back-off as in ",[14,2243,2245],{"href":2244},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fhandle-microsoft-graph-throttling-for-excel\u002F","Handle Microsoft Graph Throttling for Excel",[20,2247,2249],{"id":2248},"conclusion","Conclusion",[10,2251,2252,2253,2256,2257,2259,2260,2262,2263,2265,2266,2268],{},"Microsoft Graph can create native charts in a workbook stored online: open a session, ",[32,2254,2255],{},"POST charts\u002Fadd"," with a type and source range, rename the chart to a stable name, set its title, and place it over a cell block with ",[32,2258,903],{},". On later runs, update its range with ",[32,2261,1267],{}," rather than recreating it, fetch a PNG through the ",[32,2264,1455],{}," function when the chart needs to travel, and test the request flow offline with ",[32,2267,82],{}," before pointing it at a real workbook.",[20,2270,2272],{"id":2271},"frequently-asked-questions","Frequently asked questions",[10,2274,2275,2278],{},[2197,2276,2277],{},"How do I add a chart to an Excel file in OneDrive with Python?","\nSend a POST request to the worksheet's charts\u002Fadd endpoint under the workbook's drive item, with a JSON body containing type, such as ColumnClustered, sourceData, such as A1:C13, and seriesBy. The response contains the new chart's name and ID.",[10,2280,2281,2284],{},[2197,2282,2283],{},"How do I change the data range of an existing Excel Online chart?","\nCall POST ...\u002Fcharts\u002F{name}\u002FsetData with the new sourceData range and seriesBy. This keeps the chart and its formatting instead of deleting and re-creating it.",[10,2286,2287,2290],{},[2197,2288,2289],{},"How do I position a chart created with Microsoft Graph?","\nCall POST ...\u002Fcharts\u002F{name}\u002FsetPosition with startCell and endCell, for example E2 and L18, to place it over that block of cells. You can also patch top, left, width and height in points.",[10,2292,2293,2296],{},[2197,2294,2295],{},"Can Microsoft Graph return a chart as an image?","\nYes. Call the chart's image function with a width, a height and a fitting mode; the response's value field holds the chart rendered as a base64-encoded PNG.",[20,2298,2300],{"id":2299},"related","Related",[2191,2302,2303,2309,2316,2323],{},[2194,2304,2305,2306,2308],{},"Up one level: ",[14,2307,17],{"href":16}," — editing cloud workbooks in place.",[2194,2310,2311,2315],{},[14,2312,2314],{"href":2313},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fread-and-write-excel-online-ranges-with-microsoft-graph\u002F","Read and Write Excel Online Ranges with Microsoft Graph"," — updating the data the chart shows.",[2194,2317,2318,2322],{},[14,2319,2321],{"href":2320},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002F","Creating Charts in Excel with openpyxl"," — native charts in files.",[2194,2324,2325,2329],{},[14,2326,2328],{"href":2327},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Frun-a-report-when-an-excel-file-changes-in-onedrive\u002F","Run a Report When an Excel File Changes in OneDrive"," — triggering the refresh.",[2331,2332,2333],"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 .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_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":30,"searchDepth":114,"depth":114,"links":2335},[2336,2337,2338,2339,2340,2341,2342,2343,2344,2345,2346],{"id":22,"depth":114,"text":23},{"id":86,"depth":114,"text":87},{"id":516,"depth":114,"text":517},{"id":896,"depth":114,"text":897},{"id":991,"depth":114,"text":992},{"id":1302,"depth":114,"text":1303},{"id":1576,"depth":114,"text":1577},{"id":2188,"depth":114,"text":2189},{"id":2248,"depth":114,"text":2249},{"id":2271,"depth":114,"text":2272},{"id":2299,"depth":114,"text":2300},"2026-10-02","Create and maintain native charts in a workbook stored in OneDrive or SharePoint without downloading it: call the Graph charts\u002Fadd endpoint with a type and source range, set the title and position, point an existing chart at new data with setData, fetch a PNG of the chart for an email, and run it all inside a workbook session — with the request code tested against a mocked Graph endpoint.","md",[2351,2353,2355,2357],{"q":2277,"a":2352},"Send a POST request to the worksheet's charts\u002Fadd endpoint under the workbook's drive item, with a JSON body containing type, such as ColumnClustered, sourceData, such as A1:C13, and seriesBy. The response contains the new chart's name and ID.",{"q":2283,"a":2354},"Call POST ...\u002Fcharts\u002F{name}\u002FsetData with the new sourceData range and seriesBy. This keeps the chart and its formatting instead of deleting and re-creating it.",{"q":2289,"a":2356},"Call POST ...\u002Fcharts\u002F{name}\u002FsetPosition with startCell and endCell, for example E2 and L18, to place it over that block of cells. You can also patch top, left, width and height in points.",{"q":2295,"a":2358},"Yes. Call the chart's image function with a width, a height and a fitting mode; the response's value field holds the chart rendered as a base64-encoded PNG.","add a chart to an excel online workbook with microsoft graph",null,{"breadcrumb":2362},[2363,2365,2368],{"name":2364,"item":198},"Home",{"name":2366,"item":2367},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":17,"item":16},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph",{"title":2371,"description":2372},"Add a Chart to Excel Online with Microsoft Graph (Python)","Use Microsoft Graph from Python to add a chart to a OneDrive or SharePoint workbook: charts\u002Fadd with type and sourceData, PATCH title and size, setPosition, setData for new ranges, chart image as base64 PNG, workbook sessions, and testing with responses.","add-a-chart-to-an-excel-online-workbook-with-microsoft-graph","automating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-a-chart-to-an-excel-online-workbook-with-microsoft-graph\u002Findex",[2376,2377],"excel-online","charts-and-images","how-to","VyfeO0h6n5rrLXHFFD0D1ROY8vTqssseV-hrcZlstio",[2381,2384],{"title":17,"path":2382,"stem":2383,"children":-1},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph","automating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Findex",{"title":2385,"path":2386,"stem":2387,"children":-1},"Add, Rename and Delete Worksheets with Microsoft Graph","\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-rename-and-delete-worksheets-with-microsoft-graph","automating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fadd-rename-and-delete-worksheets-with-microsoft-graph\u002Findex",[2389,2390],{"path":2382,"title":17},{"path":2391,"title":2366},"\u002Fautomating-reporting-workflows",[],[2394,2397,2398,2400,2402,2405],{"path":2395,"title":2396,"kind":2378},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-microsoft-graph-api","Send an Excel Report with the Microsoft Graph API",{"path":2386,"title":2385,"kind":2378},{"path":2399,"title":1192,"kind":2378},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fappend-rows-to-an-excel-online-table-with-microsoft-graph",{"path":2401,"title":74,"kind":2378},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation",{"path":2403,"title":2404,"kind":2378},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcall-excel-worksheet-functions-through-microsoft-graph","Call Excel Worksheet Functions Through Microsoft Graph",{"path":2406,"title":2407,"kind":2378},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcopy-a-template-workbook-in-sharepoint-with-microsoft-graph","Copy a Template Workbook in SharePoint with Microsoft Graph",1790972118846]