[{"data":1,"prerenderedAt":2070},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fembed-a-matplotlib-chart-in-an-excel-report":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fembed-a-matplotlib-chart-in-an-excel-report":2062},{"id":4,"title":5,"body":6,"dateModified":2031,"datePublished":2031,"description":2032,"extension":2033,"faq":2034,"meta":2043,"navigation":228,"path":2054,"seo":2055,"slug":2058,"stem":2059,"type":2060,"__hash__":2061},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fembed-a-matplotlib-chart-in-an-excel-report\u002Findex.md","Embed a matplotlib Chart in an Excel Report",{"type":7,"value":8,"toc":2017},"minimark",[9,19,27,155,160,191,199,203,206,906,912,915,919,1148,1163,1170,1174,1181,1264,1271,1274,1326,1404,1408,1420,1547,1553,1559,1563,1566,1658,1675,1679,1682,1771,1774,1778,1895,1899,1902,1905,1909,1915,1919,1926,1945,1958,1964,1968,1971,1980,1983,2013],[10,11,12,13,18],"p",{},"Excel's own charts cover the common shapes well, and where they do, a ",[14,15,17],"a",{"href":16},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002F","native chart"," is the better answer because it recalculates when a reader edits the numbers. But some plots have no Excel equivalent — a heatmap, a distribution, a regression fit, a grid of small multiples — and for those, rendering with matplotlib and embedding the result is the practical route.",[10,20,21,22,26],{},"This guide covers the mechanics that matter: getting the figure into the workbook without touching the disk, sizing it so it is sharp rather than blurry, and keeping the numbers on a sheet so the picture is not the only record. It is part of ",[14,23,25],{"href":24},"\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002F","Inserting Images and Logos into Excel",".",[28,29,37,38,37,42,37,46,37,54,37,64,37,71,37,76,37,80,37,84,37,87,37,92,37,97,37,100,37,106,37,111,37,114,37,121,37,126,37,130,37,133,37,137,37,140,37,146,37,150],"svg",{"viewBox":30,"role":31,"ariaLabelledBy":32,"xmlns":35,"style":36},"0 44 740 172","img",[33,34],"mpl-path-t","mpl-path-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:740px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[39,40,41],"title",{"id":33},"From DataFrame to an image anchored in a worksheet",[43,44,45],"desc",{"id":34},"The DataFrame is plotted with matplotlib using the Agg backend, saved as a PNG into an in-memory buffer, wrapped as an openpyxl image and anchored at a cell. The same DataFrame is also written to a data sheet, so the workbook still contains the numbers behind the picture.",[47,48],"rect",{"x":49,"y":50,"width":51,"height":52,"fill":53},"0","44","740","172","#ffffff",[47,55],{"x":56,"y":57,"width":58,"height":59,"rx":60,"fill":61,"stroke":62,"style":63},"16","60","140","70","12","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[65,66,70],"text",{"x":67,"y":68,"style":69},"86","90","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","DataFrame",[65,72,75],{"x":67,"y":73,"style":74},"112","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","the numbers",[47,77],{"x":78,"y":57,"width":58,"height":59,"rx":60,"fill":79,"stroke":62,"style":63},"186","#f0f4ff",[65,81,83],{"x":82,"y":68,"style":69},"256","matplotlib",[65,85,86],{"x":82,"y":73,"style":74},"Agg backend",[47,88],{"x":89,"y":57,"width":58,"height":59,"rx":60,"fill":90,"stroke":91,"style":63},"356","#fdefd8","var(--gold,#b4740a)",[65,93,96],{"x":94,"y":68,"style":95},"426","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","BytesIO",[65,98,99],{"x":94,"y":73,"style":74},"PNG in memory",[47,101],{"x":102,"y":57,"width":103,"height":59,"rx":60,"fill":104,"stroke":105,"style":63},"526","198","#d9f4f1","var(--teal,#0f9488)",[65,107,110],{"x":108,"y":68,"style":109},"625","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","anchored at a cell",[65,112,113],{"x":108,"y":73,"style":74},"ws.add_image(img, \"E2\")",[115,116],"line",{"x1":117,"y1":118,"x2":119,"y2":118,"stroke":120,"style":63},"160","95","180","var(--muted,#5b6780)",[122,123],"polygon",{"points":124,"fill":125},"184,95 174,90 174,100","#5b6780",[115,127],{"x1":128,"y1":118,"x2":129,"y2":118,"stroke":120,"style":63},"330","350",[122,131],{"points":132,"fill":125},"354,95 344,90 344,100",[115,134],{"x1":135,"y1":118,"x2":136,"y2":118,"stroke":120,"style":63},"500","520",[122,138],{"points":139,"fill":125},"524,95 514,90 514,100",[141,142],"path",{"d":143,"stroke":105,"style":144,"fill":145},"M 86 134 L 86 172 L 625 172 L 625 136","stroke-width:2px;stroke-dasharray:5 4","none",[122,147],{"points":148,"fill":149},"625,132 619,146 631,146","#0f766e",[65,151,154],{"x":89,"y":152,"style":153},"196","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","the same numbers also go onto a data sheet — an image alone is not a record",[156,157,159],"h2",{"id":158},"prerequisites","Prerequisites",[161,162,167],"pre",{"className":163,"code":164,"language":165,"meta":166,"style":166},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install matplotlib openpyxl pandas\n","bash","",[168,169,170],"code",{"__ignoreMap":166},[171,172,174,178,182,185,188],"span",{"class":115,"line":173},1,[171,175,177],{"class":176},"sMTad","pip",[171,179,181],{"class":180},"srMev"," install",[171,183,184],{"class":180}," matplotlib",[171,186,187],{"class":180}," openpyxl",[171,189,190],{"class":180}," pandas\n",[10,192,193,194,198],{},"matplotlib pulls in a plotting stack of a few tens of megabytes, which is worth noting if the script is destined for a ",[14,195,197],{"href":196},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fpackage-a-python-excel-script-as-an-exe-with-pyinstaller\u002F","packaged executable"," — it is one of the biggest single contributors to bundle size.",[156,200,202],{"id":201},"step-1-render-without-a-display","Step 1: Render without a display",[10,204,205],{},"On a server or in a scheduled job there is no window system, and matplotlib's default backend will try to find one. Select the file-only Agg backend before importing pyplot:",[161,207,211],{"className":208,"code":209,"language":210,"meta":166,"style":166},"language-python shiki shiki-themes github-light github-dark-high-contrast","import io\n\nimport matplotlib\nmatplotlib.use(\"Agg\")                    # must come before pyplot is imported\nimport matplotlib.pyplot as plt\nimport pandas as pd\n\ndf = pd.DataFrame({\n    \"month\": pd.period_range(\"2026-01\", periods=12, freq=\"M\").astype(str),\n    \"actual\": [412, 388, 501, 466, 590, 623, 641, 598, 655, 702, 688, 731],\n    \"budget\": [400, 400, 480, 480, 560, 600, 620, 620, 660, 680, 700, 720],\n})\n\n\ndef render_chart(frame, width_in=8.0, height_in=4.0, dpi=150):\n    \"\"\"Return a PNG of the figure in an in-memory buffer.\"\"\"\n    fig, ax = plt.subplots(figsize=(width_in, height_in), dpi=dpi)\n    ax.plot(frame[\"month\"], frame[\"actual\"], marker=\"o\", linewidth=2,\n            color=\"#5B5CF0\", label=\"Actual\")\n    ax.plot(frame[\"month\"], frame[\"budget\"], linestyle=\"--\", linewidth=2,\n            color=\"#0F766E\", label=\"Budget\")\n    ax.fill_between(frame[\"month\"], frame[\"actual\"], frame[\"budget\"],\n                    where=frame[\"actual\"] >= frame[\"budget\"],\n                    color=\"#0F766E\", alpha=0.12)\n    ax.set_ylabel(\"Revenue (£000)\")\n    ax.legend(frameon=False)\n    ax.grid(axis=\"y\", color=\"#E4E8F2\")\n    ax.spines[[\"top\", \"right\"]].set_visible(False)\n    fig.autofmt_xdate(rotation=45)\n    fig.tight_layout()\n\n    buffer = io.BytesIO()\n    fig.savefig(buffer, format=\"png\", dpi=dpi)\n    plt.close(fig)                       # release the figure, or memory climbs\n    buffer.seek(0)\n    return buffer\n","python",[168,212,213,223,230,238,254,268,281,286,298,341,411,476,482,487,492,529,535,562,601,625,657,678,696,722,744,755,771,797,818,834,840,845,856,878,887,897],{"__ignoreMap":166},[171,214,215,219],{"class":115,"line":173},[171,216,218],{"class":217},"s-kum","import",[171,220,222],{"class":221},"skGVy"," io\n",[171,224,226],{"class":115,"line":225},2,[171,227,229],{"emptyLinePlaceholder":228},true,"\n",[171,231,233,235],{"class":115,"line":232},3,[171,234,218],{"class":217},[171,236,237],{"class":221}," matplotlib\n",[171,239,241,244,247,250],{"class":115,"line":240},4,[171,242,243],{"class":221},"matplotlib.use(",[171,245,246],{"class":180},"\"Agg\"",[171,248,249],{"class":221},")                    ",[171,251,253],{"class":252},"s-wDw","# must come before pyplot is imported\n",[171,255,257,259,262,265],{"class":115,"line":256},5,[171,258,218],{"class":217},[171,260,261],{"class":221}," matplotlib.pyplot ",[171,263,264],{"class":217},"as",[171,266,267],{"class":221}," plt\n",[171,269,271,273,276,278],{"class":115,"line":270},6,[171,272,218],{"class":217},[171,274,275],{"class":221}," pandas ",[171,277,264],{"class":217},[171,279,280],{"class":221}," pd\n",[171,282,284],{"class":115,"line":283},7,[171,285,229],{"emptyLinePlaceholder":228},[171,287,289,292,295],{"class":115,"line":288},8,[171,290,291],{"class":221},"df ",[171,293,294],{"class":217},"=",[171,296,297],{"class":221}," pd.DataFrame({\n",[171,299,301,304,307,310,313,317,319,322,324,327,329,332,335,338],{"class":115,"line":300},9,[171,302,303],{"class":180},"    \"month\"",[171,305,306],{"class":221},": pd.period_range(",[171,308,309],{"class":180},"\"2026-01\"",[171,311,312],{"class":221},", ",[171,314,316],{"class":315},"sa561","periods",[171,318,294],{"class":217},[171,320,60],{"class":321},"sP0c6",[171,323,312],{"class":221},[171,325,326],{"class":315},"freq",[171,328,294],{"class":217},[171,330,331],{"class":180},"\"M\"",[171,333,334],{"class":221},").astype(",[171,336,337],{"class":321},"str",[171,339,340],{"class":221},"),\n",[171,342,344,347,350,353,355,358,360,363,365,368,370,373,375,378,380,383,385,388,390,393,395,398,400,403,405,408],{"class":115,"line":343},10,[171,345,346],{"class":180},"    \"actual\"",[171,348,349],{"class":221},": [",[171,351,352],{"class":321},"412",[171,354,312],{"class":221},[171,356,357],{"class":321},"388",[171,359,312],{"class":221},[171,361,362],{"class":321},"501",[171,364,312],{"class":221},[171,366,367],{"class":321},"466",[171,369,312],{"class":221},[171,371,372],{"class":321},"590",[171,374,312],{"class":221},[171,376,377],{"class":321},"623",[171,379,312],{"class":221},[171,381,382],{"class":321},"641",[171,384,312],{"class":221},[171,386,387],{"class":321},"598",[171,389,312],{"class":221},[171,391,392],{"class":321},"655",[171,394,312],{"class":221},[171,396,397],{"class":321},"702",[171,399,312],{"class":221},[171,401,402],{"class":321},"688",[171,404,312],{"class":221},[171,406,407],{"class":321},"731",[171,409,410],{"class":221},"],\n",[171,412,414,417,419,422,424,426,428,431,433,435,437,440,442,445,447,450,452,454,456,459,461,464,466,469,471,474],{"class":115,"line":413},11,[171,415,416],{"class":180},"    \"budget\"",[171,418,349],{"class":221},[171,420,421],{"class":321},"400",[171,423,312],{"class":221},[171,425,421],{"class":321},[171,427,312],{"class":221},[171,429,430],{"class":321},"480",[171,432,312],{"class":221},[171,434,430],{"class":321},[171,436,312],{"class":221},[171,438,439],{"class":321},"560",[171,441,312],{"class":221},[171,443,444],{"class":321},"600",[171,446,312],{"class":221},[171,448,449],{"class":321},"620",[171,451,312],{"class":221},[171,453,449],{"class":321},[171,455,312],{"class":221},[171,457,458],{"class":321},"660",[171,460,312],{"class":221},[171,462,463],{"class":321},"680",[171,465,312],{"class":221},[171,467,468],{"class":321},"700",[171,470,312],{"class":221},[171,472,473],{"class":321},"720",[171,475,410],{"class":221},[171,477,479],{"class":115,"line":478},12,[171,480,481],{"class":221},"})\n",[171,483,485],{"class":115,"line":484},13,[171,486,229],{"emptyLinePlaceholder":228},[171,488,490],{"class":115,"line":489},14,[171,491,229],{"emptyLinePlaceholder":228},[171,493,495,498,502,505,507,510,513,515,518,521,523,526],{"class":115,"line":494},15,[171,496,497],{"class":217},"def",[171,499,501],{"class":500},"s_Opv"," render_chart",[171,503,504],{"class":221},"(frame, width_in",[171,506,294],{"class":217},[171,508,509],{"class":321},"8.0",[171,511,512],{"class":221},", height_in",[171,514,294],{"class":217},[171,516,517],{"class":321},"4.0",[171,519,520],{"class":221},", dpi",[171,522,294],{"class":217},[171,524,525],{"class":321},"150",[171,527,528],{"class":221},"):\n",[171,530,532],{"class":115,"line":531},16,[171,533,534],{"class":180},"    \"\"\"Return a PNG of the figure in an in-memory buffer.\"\"\"\n",[171,536,538,541,543,546,549,551,554,557,559],{"class":115,"line":537},17,[171,539,540],{"class":221},"    fig, ax ",[171,542,294],{"class":217},[171,544,545],{"class":221}," plt.subplots(",[171,547,548],{"class":315},"figsize",[171,550,294],{"class":217},[171,552,553],{"class":221},"(width_in, height_in), ",[171,555,556],{"class":315},"dpi",[171,558,294],{"class":217},[171,560,561],{"class":221},"dpi)\n",[171,563,565,568,571,574,577,580,583,585,588,590,593,595,598],{"class":115,"line":564},18,[171,566,567],{"class":221},"    ax.plot(frame[",[171,569,570],{"class":180},"\"month\"",[171,572,573],{"class":221},"], frame[",[171,575,576],{"class":180},"\"actual\"",[171,578,579],{"class":221},"], ",[171,581,582],{"class":315},"marker",[171,584,294],{"class":217},[171,586,587],{"class":180},"\"o\"",[171,589,312],{"class":221},[171,591,592],{"class":315},"linewidth",[171,594,294],{"class":217},[171,596,597],{"class":321},"2",[171,599,600],{"class":221},",\n",[171,602,604,607,609,612,614,617,619,622],{"class":115,"line":603},19,[171,605,606],{"class":315},"            color",[171,608,294],{"class":217},[171,610,611],{"class":180},"\"#5B5CF0\"",[171,613,312],{"class":221},[171,615,616],{"class":315},"label",[171,618,294],{"class":217},[171,620,621],{"class":180},"\"Actual\"",[171,623,624],{"class":221},")\n",[171,626,628,630,632,634,637,639,642,644,647,649,651,653,655],{"class":115,"line":627},20,[171,629,567],{"class":221},[171,631,570],{"class":180},[171,633,573],{"class":221},[171,635,636],{"class":180},"\"budget\"",[171,638,579],{"class":221},[171,640,641],{"class":315},"linestyle",[171,643,294],{"class":217},[171,645,646],{"class":180},"\"--\"",[171,648,312],{"class":221},[171,650,592],{"class":315},[171,652,294],{"class":217},[171,654,597],{"class":321},[171,656,600],{"class":221},[171,658,660,662,664,667,669,671,673,676],{"class":115,"line":659},21,[171,661,606],{"class":315},[171,663,294],{"class":217},[171,665,666],{"class":180},"\"#0F766E\"",[171,668,312],{"class":221},[171,670,616],{"class":315},[171,672,294],{"class":217},[171,674,675],{"class":180},"\"Budget\"",[171,677,624],{"class":221},[171,679,681,684,686,688,690,692,694],{"class":115,"line":680},22,[171,682,683],{"class":221},"    ax.fill_between(frame[",[171,685,570],{"class":180},[171,687,573],{"class":221},[171,689,576],{"class":180},[171,691,573],{"class":221},[171,693,636],{"class":180},[171,695,410],{"class":221},[171,697,699,702,704,707,709,712,715,718,720],{"class":115,"line":698},23,[171,700,701],{"class":315},"                    where",[171,703,294],{"class":217},[171,705,706],{"class":221},"frame[",[171,708,576],{"class":180},[171,710,711],{"class":221},"] ",[171,713,714],{"class":217},">=",[171,716,717],{"class":221}," frame[",[171,719,636],{"class":180},[171,721,410],{"class":221},[171,723,725,728,730,732,734,737,739,742],{"class":115,"line":724},24,[171,726,727],{"class":315},"                    color",[171,729,294],{"class":217},[171,731,666],{"class":180},[171,733,312],{"class":221},[171,735,736],{"class":315},"alpha",[171,738,294],{"class":217},[171,740,741],{"class":321},"0.12",[171,743,624],{"class":221},[171,745,747,750,753],{"class":115,"line":746},25,[171,748,749],{"class":221},"    ax.set_ylabel(",[171,751,752],{"class":180},"\"Revenue (£000)\"",[171,754,624],{"class":221},[171,756,758,761,764,766,769],{"class":115,"line":757},26,[171,759,760],{"class":221},"    ax.legend(",[171,762,763],{"class":315},"frameon",[171,765,294],{"class":217},[171,767,768],{"class":321},"False",[171,770,624],{"class":221},[171,772,774,777,780,782,785,787,790,792,795],{"class":115,"line":773},27,[171,775,776],{"class":221},"    ax.grid(",[171,778,779],{"class":315},"axis",[171,781,294],{"class":217},[171,783,784],{"class":180},"\"y\"",[171,786,312],{"class":221},[171,788,789],{"class":315},"color",[171,791,294],{"class":217},[171,793,794],{"class":180},"\"#E4E8F2\"",[171,796,624],{"class":221},[171,798,800,803,806,808,811,814,816],{"class":115,"line":799},28,[171,801,802],{"class":221},"    ax.spines[[",[171,804,805],{"class":180},"\"top\"",[171,807,312],{"class":221},[171,809,810],{"class":180},"\"right\"",[171,812,813],{"class":221},"]].set_visible(",[171,815,768],{"class":321},[171,817,624],{"class":221},[171,819,821,824,827,829,832],{"class":115,"line":820},29,[171,822,823],{"class":221},"    fig.autofmt_xdate(",[171,825,826],{"class":315},"rotation",[171,828,294],{"class":217},[171,830,831],{"class":321},"45",[171,833,624],{"class":221},[171,835,837],{"class":115,"line":836},30,[171,838,839],{"class":221},"    fig.tight_layout()\n",[171,841,843],{"class":115,"line":842},31,[171,844,229],{"emptyLinePlaceholder":228},[171,846,848,851,853],{"class":115,"line":847},32,[171,849,850],{"class":221},"    buffer ",[171,852,294],{"class":217},[171,854,855],{"class":221}," io.BytesIO()\n",[171,857,859,862,865,867,870,872,874,876],{"class":115,"line":858},33,[171,860,861],{"class":221},"    fig.savefig(buffer, ",[171,863,864],{"class":315},"format",[171,866,294],{"class":217},[171,868,869],{"class":180},"\"png\"",[171,871,312],{"class":221},[171,873,556],{"class":315},[171,875,294],{"class":217},[171,877,561],{"class":221},[171,879,881,884],{"class":115,"line":880},34,[171,882,883],{"class":221},"    plt.close(fig)                       ",[171,885,886],{"class":252},"# release the figure, or memory climbs\n",[171,888,890,893,895],{"class":115,"line":889},35,[171,891,892],{"class":221},"    buffer.seek(",[171,894,49],{"class":321},[171,896,624],{"class":221},[171,898,900,903],{"class":115,"line":899},36,[171,901,902],{"class":217},"    return",[171,904,905],{"class":221}," buffer\n",[10,907,908,911],{},[168,909,910],{},"plt.close(fig)"," is the line that matters in a loop. matplotlib keeps every figure alive until it is closed, so a job generating one chart per region leaks them all and eventually warns about too many open figures — shortly before it runs out of memory.",[10,913,914],{},"Returning a buffer rather than a path keeps the whole operation in memory: no temporary files to clean up, no permissions to worry about, nothing left behind if the job fails halfway.",[156,916,918],{"id":917},"step-2-anchor-the-image-with-openpyxl","Step 2: Anchor the image with openpyxl",[161,920,922],{"className":208,"code":921,"language":210,"meta":166,"style":166},"from openpyxl import Workbook\nfrom openpyxl.drawing.image import Image as XLImage\n\nwb = Workbook()\ndata_ws = wb.active\ndata_ws.title = \"Data\"\ndata_ws.append(list(df.columns))\nfor row in df.itertuples(index=False):\n    data_ws.append(list(row))\n\nchart_ws = wb.create_sheet(\"Chart\", 0)          # first tab\nbuffer = render_chart(df)\n\nimg = XLImage(buffer)\nimg.anchor = \"B3\"\nchart_ws.add_image(img, \"B3\")\nchart_ws[\"B1\"] = \"Actual against budget, 2026\"\nchart_ws.column_dimensions[\"A\"].width = 3\n\nwb.save(\"with-chart.xlsx\")\n",[168,923,924,937,954,958,968,978,988,999,1022,1032,1036,1059,1069,1073,1083,1093,1103,1118,1134,1138],{"__ignoreMap":166},[171,925,926,929,932,934],{"class":115,"line":173},[171,927,928],{"class":217},"from",[171,930,931],{"class":221}," openpyxl ",[171,933,218],{"class":217},[171,935,936],{"class":221}," Workbook\n",[171,938,939,941,944,946,949,951],{"class":115,"line":225},[171,940,928],{"class":217},[171,942,943],{"class":221}," openpyxl.drawing.image ",[171,945,218],{"class":217},[171,947,948],{"class":221}," Image ",[171,950,264],{"class":217},[171,952,953],{"class":221}," XLImage\n",[171,955,956],{"class":115,"line":232},[171,957,229],{"emptyLinePlaceholder":228},[171,959,960,963,965],{"class":115,"line":240},[171,961,962],{"class":221},"wb ",[171,964,294],{"class":217},[171,966,967],{"class":221}," Workbook()\n",[171,969,970,973,975],{"class":115,"line":256},[171,971,972],{"class":221},"data_ws ",[171,974,294],{"class":217},[171,976,977],{"class":221}," wb.active\n",[171,979,980,983,985],{"class":115,"line":270},[171,981,982],{"class":221},"data_ws.title ",[171,984,294],{"class":217},[171,986,987],{"class":180}," \"Data\"\n",[171,989,990,993,996],{"class":115,"line":283},[171,991,992],{"class":221},"data_ws.append(",[171,994,995],{"class":321},"list",[171,997,998],{"class":221},"(df.columns))\n",[171,1000,1001,1004,1007,1010,1013,1016,1018,1020],{"class":115,"line":288},[171,1002,1003],{"class":217},"for",[171,1005,1006],{"class":221}," row ",[171,1008,1009],{"class":217},"in",[171,1011,1012],{"class":221}," df.itertuples(",[171,1014,1015],{"class":315},"index",[171,1017,294],{"class":217},[171,1019,768],{"class":321},[171,1021,528],{"class":221},[171,1023,1024,1027,1029],{"class":115,"line":300},[171,1025,1026],{"class":221},"    data_ws.append(",[171,1028,995],{"class":321},[171,1030,1031],{"class":221},"(row))\n",[171,1033,1034],{"class":115,"line":343},[171,1035,229],{"emptyLinePlaceholder":228},[171,1037,1038,1041,1043,1046,1049,1051,1053,1056],{"class":115,"line":413},[171,1039,1040],{"class":221},"chart_ws ",[171,1042,294],{"class":217},[171,1044,1045],{"class":221}," wb.create_sheet(",[171,1047,1048],{"class":180},"\"Chart\"",[171,1050,312],{"class":221},[171,1052,49],{"class":321},[171,1054,1055],{"class":221},")          ",[171,1057,1058],{"class":252},"# first tab\n",[171,1060,1061,1064,1066],{"class":115,"line":478},[171,1062,1063],{"class":221},"buffer ",[171,1065,294],{"class":217},[171,1067,1068],{"class":221}," render_chart(df)\n",[171,1070,1071],{"class":115,"line":484},[171,1072,229],{"emptyLinePlaceholder":228},[171,1074,1075,1078,1080],{"class":115,"line":489},[171,1076,1077],{"class":221},"img ",[171,1079,294],{"class":217},[171,1081,1082],{"class":221}," XLImage(buffer)\n",[171,1084,1085,1088,1090],{"class":115,"line":494},[171,1086,1087],{"class":221},"img.anchor ",[171,1089,294],{"class":217},[171,1091,1092],{"class":180}," \"B3\"\n",[171,1094,1095,1098,1101],{"class":115,"line":531},[171,1096,1097],{"class":221},"chart_ws.add_image(img, ",[171,1099,1100],{"class":180},"\"B3\"",[171,1102,624],{"class":221},[171,1104,1105,1108,1111,1113,1115],{"class":115,"line":537},[171,1106,1107],{"class":221},"chart_ws[",[171,1109,1110],{"class":180},"\"B1\"",[171,1112,711],{"class":221},[171,1114,294],{"class":217},[171,1116,1117],{"class":180}," \"Actual against budget, 2026\"\n",[171,1119,1120,1123,1126,1129,1131],{"class":115,"line":564},[171,1121,1122],{"class":221},"chart_ws.column_dimensions[",[171,1124,1125],{"class":180},"\"A\"",[171,1127,1128],{"class":221},"].width ",[171,1130,294],{"class":217},[171,1132,1133],{"class":321}," 3\n",[171,1135,1136],{"class":115,"line":603},[171,1137,229],{"emptyLinePlaceholder":228},[171,1139,1140,1143,1146],{"class":115,"line":627},[171,1141,1142],{"class":221},"wb.save(",[171,1144,1145],{"class":180},"\"with-chart.xlsx\"",[171,1147,624],{"class":221},[10,1149,1150,1151,1153,1154,1158,1159,1162],{},"openpyxl accepts a file-like object, so the ",[168,1152,96],{}," buffer goes straight in. One caveat with in-memory images: openpyxl reads the buffer when the workbook is ",[1155,1156,1157],"em",{},"saved",", not when ",[168,1160,1161],{},"add_image"," is called, so the buffer must still be open at save time — do not close it in between, and do not reuse one buffer for two images.",[10,1164,1165,1166,1169],{},"Writing the numbers to a ",[168,1167,1168],{},"Data"," sheet alongside is not optional in practice. An image is a picture; the first question anyone asks about a chart is \"what was the number for June\", and a workbook that cannot answer it sends them back to you.",[156,1171,1173],{"id":1172},"step-3-size-it-so-it-stays-sharp","Step 3: Size it so it stays sharp",[10,1175,1176,1177,1180],{},"An image inserted into Excel is placed at its pixel dimensions. Those come from ",[168,1178,1179],{},"figsize × dpi",", so a figure at 8×4 inches and 150 DPI is 1200×600 pixels:",[161,1182,1184],{"className":208,"code":1183,"language":210,"meta":166,"style":166},"buffer = render_chart(df, width_in=8.0, height_in=4.0, dpi=150)   # 1200 x 600 px\nimg = XLImage(buffer)\nimg.width = 800          # displayed size in pixels\nimg.height = 400\nchart_ws.add_image(img, \"B3\")\n",[168,1185,1186,1225,1233,1246,1256],{"__ignoreMap":166},[171,1187,1188,1190,1192,1195,1198,1200,1202,1204,1207,1209,1211,1213,1215,1217,1219,1222],{"class":115,"line":173},[171,1189,1063],{"class":221},[171,1191,294],{"class":217},[171,1193,1194],{"class":221}," render_chart(df, ",[171,1196,1197],{"class":315},"width_in",[171,1199,294],{"class":217},[171,1201,509],{"class":321},[171,1203,312],{"class":221},[171,1205,1206],{"class":315},"height_in",[171,1208,294],{"class":217},[171,1210,517],{"class":321},[171,1212,312],{"class":221},[171,1214,556],{"class":315},[171,1216,294],{"class":217},[171,1218,525],{"class":321},[171,1220,1221],{"class":221},")   ",[171,1223,1224],{"class":252},"# 1200 x 600 px\n",[171,1226,1227,1229,1231],{"class":115,"line":225},[171,1228,1077],{"class":221},[171,1230,294],{"class":217},[171,1232,1082],{"class":221},[171,1234,1235,1238,1240,1243],{"class":115,"line":232},[171,1236,1237],{"class":221},"img.width ",[171,1239,294],{"class":217},[171,1241,1242],{"class":321}," 800",[171,1244,1245],{"class":252},"          # displayed size in pixels\n",[171,1247,1248,1251,1253],{"class":115,"line":240},[171,1249,1250],{"class":221},"img.height ",[171,1252,294],{"class":217},[171,1254,1255],{"class":321}," 400\n",[171,1257,1258,1260,1262],{"class":115,"line":256},[171,1259,1097],{"class":221},[171,1261,1100],{"class":180},[171,1263,624],{"class":221},[10,1265,1266,1267,1270],{},"Rendering larger than the display size and scaling down keeps the chart crisp on a high-DPI screen and when printed; scaling ",[1155,1268,1269],{},"up"," is what produces the soft, blurry chart people complain about. A 1.5× ratio between rendered and displayed pixels is a good default.",[10,1272,1273],{},"Set the figure's font sizes explicitly if the chart will be shown small — matplotlib's defaults are sized for a full-screen figure and become unreadable at 400 pixels wide:",[161,1275,1277],{"className":208,"code":1276,"language":210,"meta":166,"style":166},"plt.rcParams.update({\"font.size\": 11, \"axes.titlesize\": 13,\n                     \"axes.labelsize\": 11, \"legend.fontsize\": 10})\n",[168,1278,1279,1305],{"__ignoreMap":166},[171,1280,1281,1284,1287,1290,1293,1295,1298,1300,1303],{"class":115,"line":173},[171,1282,1283],{"class":221},"plt.rcParams.update({",[171,1285,1286],{"class":180},"\"font.size\"",[171,1288,1289],{"class":221},": ",[171,1291,1292],{"class":321},"11",[171,1294,312],{"class":221},[171,1296,1297],{"class":180},"\"axes.titlesize\"",[171,1299,1289],{"class":221},[171,1301,1302],{"class":321},"13",[171,1304,600],{"class":221},[171,1306,1307,1310,1312,1314,1316,1319,1321,1324],{"class":115,"line":225},[171,1308,1309],{"class":180},"                     \"axes.labelsize\"",[171,1311,1289],{"class":221},[171,1313,1292],{"class":321},[171,1315,312],{"class":221},[171,1317,1318],{"class":180},"\"legend.fontsize\"",[171,1320,1289],{"class":221},[171,1322,1323],{"class":321},"10",[171,1325,481],{"class":221},[28,1327,37,1332,37,1335,37,1338,37,1341,37,1347,37,1354,37,1360,37,1364,37,1368,37,1371,37,1375,37,1380,37,1387,37,1390,37,1394,37,1398,37,1401],{"viewBox":1328,"role":31,"ariaLabelledBy":1329,"xmlns":35,"style":36},"0 0 740 246",[1330,1331],"mpl-dpi-t","mpl-dpi-d",[39,1333,1334],{"id":1330},"Rendering above the display size versus below it",[43,1336,1337],{"id":1331},"A figure rendered at 1200 by 600 pixels and displayed at 800 by 400 has more pixels than it needs, so it stays sharp on a high-resolution screen and in print. A figure rendered at 600 by 300 and displayed at 800 by 400 is scaled up, and the text and lines soften.",[47,1339],{"x":49,"y":49,"width":51,"height":1340,"fill":53},"246",[65,1342,1346],{"x":1343,"y":1344,"style":1345},"184","32","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","render 1200×600, display 800×400",[47,1348],{"x":1349,"y":1350,"width":1351,"height":1352,"rx":1353,"fill":104,"stroke":105,"style":63},"34","52","300","120","6",[47,1355],{"x":1356,"y":67,"width":1357,"height":1358,"rx":1359,"fill":53,"stroke":105},"84","200","76","4",[65,1361,1363],{"x":1343,"y":1358,"style":1362},"font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","rendered pixels",[65,1365,1367],{"x":1343,"y":1366,"style":153},"128","displayed area",[65,1369,1370],{"x":1343,"y":1357,"style":153},"detail to spare — sharp on screen and in print",[65,1372,1374],{"x":1343,"y":1373,"style":74},"224","costs a slightly larger file, and nothing else",[65,1376,1379],{"x":1377,"y":1344,"style":1378},"556","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","render 600×300, display 800×400",[47,1381],{"x":1382,"y":1383,"width":1357,"height":1384,"rx":1353,"fill":1385,"stroke":1386,"style":63},"456","82","80","#fce9e9","var(--accent-ink,#be185d)",[65,1388,1363],{"x":1377,"y":1366,"style":1389},"font-size:11.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[47,1391],{"x":1392,"y":1350,"width":1351,"height":1352,"rx":1359,"fill":145,"stroke":1386,"style":1393},"406","stroke-width:1.5px;stroke-dasharray:5 4",[65,1395,1397],{"x":1377,"y":1396,"style":74},"68","displayed area, larger than what was drawn",[65,1399,1400],{"x":1377,"y":1357,"style":153},"scaled up — text and thin lines soften",[65,1402,1403],{"x":1377,"y":1373,"style":74},"this is what \"the chart looks blurry\" means",[156,1405,1407],{"id":1406},"step-4-the-same-thing-with-xlsxwriter","Step 4: The same thing with xlsxwriter",[10,1409,1410,1411,1415,1416,1419],{},"If the report is being generated with ",[14,1412,1414],{"href":1413},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002F","xlsxwriter",", the in-memory route uses ",[168,1417,1418],{},"image_data",":",[161,1421,1423],{"className":208,"code":1422,"language":210,"meta":166,"style":166},"import xlsxwriter\n\nwb = xlsxwriter.Workbook(\"with-chart.xlsx\")\nws = wb.add_worksheet(\"Chart\")\nbuffer = render_chart(df)\n\nws.insert_image(\"B3\", \"trend.png\", {\n    \"image_data\": buffer,           # the buffer, not a file on disk\n    \"x_scale\": 0.67, \"y_scale\": 0.67,\n    \"object_position\": 1,           # move and size with cells\n})\nwb.close()\n",[168,1424,1425,1432,1436,1449,1463,1471,1475,1490,1501,1522,1538,1542],{"__ignoreMap":166},[171,1426,1427,1429],{"class":115,"line":173},[171,1428,218],{"class":217},[171,1430,1431],{"class":221}," xlsxwriter\n",[171,1433,1434],{"class":115,"line":225},[171,1435,229],{"emptyLinePlaceholder":228},[171,1437,1438,1440,1442,1445,1447],{"class":115,"line":232},[171,1439,962],{"class":221},[171,1441,294],{"class":217},[171,1443,1444],{"class":221}," xlsxwriter.Workbook(",[171,1446,1145],{"class":180},[171,1448,624],{"class":221},[171,1450,1451,1454,1456,1459,1461],{"class":115,"line":240},[171,1452,1453],{"class":221},"ws ",[171,1455,294],{"class":217},[171,1457,1458],{"class":221}," wb.add_worksheet(",[171,1460,1048],{"class":180},[171,1462,624],{"class":221},[171,1464,1465,1467,1469],{"class":115,"line":256},[171,1466,1063],{"class":221},[171,1468,294],{"class":217},[171,1470,1068],{"class":221},[171,1472,1473],{"class":115,"line":270},[171,1474,229],{"emptyLinePlaceholder":228},[171,1476,1477,1480,1482,1484,1487],{"class":115,"line":283},[171,1478,1479],{"class":221},"ws.insert_image(",[171,1481,1100],{"class":180},[171,1483,312],{"class":221},[171,1485,1486],{"class":180},"\"trend.png\"",[171,1488,1489],{"class":221},", {\n",[171,1491,1492,1495,1498],{"class":115,"line":288},[171,1493,1494],{"class":180},"    \"image_data\"",[171,1496,1497],{"class":221},": buffer,           ",[171,1499,1500],{"class":252},"# the buffer, not a file on disk\n",[171,1502,1503,1506,1508,1511,1513,1516,1518,1520],{"class":115,"line":300},[171,1504,1505],{"class":180},"    \"x_scale\"",[171,1507,1289],{"class":221},[171,1509,1510],{"class":321},"0.67",[171,1512,312],{"class":221},[171,1514,1515],{"class":180},"\"y_scale\"",[171,1517,1289],{"class":221},[171,1519,1510],{"class":321},[171,1521,600],{"class":221},[171,1523,1524,1527,1529,1532,1535],{"class":115,"line":343},[171,1525,1526],{"class":180},"    \"object_position\"",[171,1528,1289],{"class":221},[171,1530,1531],{"class":321},"1",[171,1533,1534],{"class":221},",           ",[171,1536,1537],{"class":252},"# move and size with cells\n",[171,1539,1540],{"class":115,"line":413},[171,1541,481],{"class":221},[171,1543,1544],{"class":115,"line":478},[171,1545,1546],{"class":221},"wb.close()\n",[10,1548,1549,1550,1552],{},"The filename argument is still required even with ",[168,1551,1418],{}," — xlsxwriter uses it only to name the part inside the archive and to infer the format, so it can be any sensible name.",[10,1554,1555,1558],{},[168,1556,1557],{},"object_position"," controls what happens when rows and columns are resized: 1 moves and sizes with cells, 2 moves but does not size, 3 does neither. For a chart beside a table whose columns a reader might widen, 2 is usually the least surprising.",[156,1560,1562],{"id":1561},"step-4b-one-figure-many-sheets","Step 4b: One figure, many sheets",[10,1564,1565],{},"Where the same chart belongs on several sheets — a trend line repeated at the top of each regional tab — render it once and insert the same bytes repeatedly. Rendering per sheet costs a tenth of a second each and produces a larger file, because every insertion carries its own copy of the image:",[28,1567,37,1572,37,1575,37,1578,37,1581,37,1585,37,1590,37,1594,37,1597,37,1600,37,1604,37,1606,37,1609,37,1612,37,1615,37,1618,37,1622,37,1625,37,1628,37,1631,37,1633,37,1636,37,1639,37,1642,37,1647,37,1650,37,1655],{"viewBox":1568,"role":31,"ariaLabelledBy":1569,"xmlns":35,"style":36},"0 0 740 210",[1570,1571],"mpl-reuse-t","mpl-reuse-d",[39,1573,1574],{"id":1570},"Rendering once against rendering per sheet",[43,1576,1577],{"id":1571},"Rendering the figure once and reusing the PNG bytes across six sheets costs one render and one stored image. Calling the render function inside the loop costs six renders and six copies of the same picture in the workbook.",[47,1579],{"x":49,"y":49,"width":51,"height":1580,"fill":53},"210",[65,1582,1584],{"x":1343,"y":1583,"style":1345},"30","render once, insert six times",[47,1586],{"x":68,"y":1587,"width":1588,"height":1349,"rx":1589,"fill":104,"stroke":105,"style":63},"46","188","8",[65,1591,1593],{"x":1343,"y":1396,"style":1592},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","one render · one PNG",[115,1595],{"x1":1343,"y1":1356,"x2":1343,"y2":1596,"stroke":105,"style":63},"100",[122,1598],{"points":1599,"fill":149},"184,104 178,92 190,92",[47,1601],{"x":1349,"y":1602,"width":1587,"height":1583,"rx":1603,"fill":53,"stroke":105},"108","5",[47,1605],{"x":67,"y":1602,"width":1587,"height":1583,"rx":1603,"fill":53,"stroke":105},[47,1607],{"x":1608,"y":1602,"width":1587,"height":1583,"rx":1603,"fill":53,"stroke":105},"138",[47,1610],{"x":1611,"y":1602,"width":1587,"height":1583,"rx":1603,"fill":53,"stroke":105},"190",[47,1613],{"x":1614,"y":1602,"width":1587,"height":1583,"rx":1603,"fill":53,"stroke":105},"242",[47,1616],{"x":1617,"y":1602,"width":1587,"height":1583,"rx":1603,"fill":53,"stroke":105},"294",[65,1619,1621],{"x":1343,"y":1620,"style":74},"166","0.1 s and one stored image",[65,1623,1624],{"x":1377,"y":1583,"style":1378},"render inside the loop",[47,1626],{"x":1627,"y":1587,"width":1587,"height":1583,"rx":1603,"fill":1385,"stroke":1386},"416",[47,1629],{"x":1630,"y":1587,"width":1587,"height":1583,"rx":1603,"fill":1385,"stroke":1386},"468",[47,1632],{"x":136,"y":1587,"width":1587,"height":1583,"rx":1603,"fill":1385,"stroke":1386},[47,1634],{"x":1635,"y":1587,"width":1587,"height":1583,"rx":1603,"fill":1385,"stroke":1386},"572",[47,1637],{"x":1638,"y":1587,"width":1587,"height":1583,"rx":1603,"fill":1385,"stroke":1386},"624",[47,1640],{"x":1641,"y":1587,"width":1587,"height":1583,"rx":1603,"fill":1385,"stroke":1386},"676",[65,1643,1646],{"x":1377,"y":1644,"style":1645},"98","font-size:11px;fill:var(--text,#172033);text-anchor:middle","six identical renders",[47,1648],{"x":1627,"y":1602,"width":1649,"height":1583,"rx":1603,"fill":1385,"stroke":1386,"style":63},"306",[65,1651,1654],{"x":1652,"y":1366,"style":1653},"569","font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","six copies of the same picture in the file",[65,1656,1657],{"x":1377,"y":1620,"style":74},"0.6 s, and a workbook several times larger",[10,1659,1660,1661,1664,1665,1667,1668,1670,1671,1674],{},"With openpyxl, give each insertion its own ",[168,1662,1663],{},"XLImage"," wrapping a fresh ",[168,1666,96],{}," built from the same bytes — one buffer cannot be read twice — while xlsxwriter deduplicates identical images itself, so passing the same ",[168,1669,1418],{}," to several ",[168,1672,1673],{},"insert_image"," calls stores it once.",[156,1676,1678],{"id":1677},"step-5-choose-the-image-only-when-it-earns-it","Step 5: Choose the image only when it earns it",[10,1680,1681],{},"A rendered image cannot be restyled, does not recalculate, and does not tell the reader which cells it came from. Those are real costs, so use it where matplotlib does something Excel cannot:",[1683,1684,1685,1701],"table",{},[1686,1687,1688],"thead",{},[1689,1690,1691,1695,1698],"tr",{},[1692,1693,1694],"th",{},"Plot",[1692,1696,1697],{},"Native Excel chart?",[1692,1699,1700],{},"Verdict",[1702,1703,1704,1716,1732,1743,1752,1761],"tbody",{},[1689,1705,1706,1710,1713],{},[1707,1708,1709],"td",{},"Column, bar, line, pie, scatter",[1707,1711,1712],{},"Yes",[1707,1714,1715],{},"Use a native chart — it recalculates",[1689,1717,1718,1721,1724],{},[1707,1719,1720],{},"Heatmap of a matrix",[1707,1722,1723],{},"No",[1707,1725,1726,1727,1731],{},"Image, or ",[14,1728,1730],{"href":1729},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002F","conditional formatting"," on the cells",[1689,1733,1734,1737,1740],{},[1707,1735,1736],{},"Distribution, histogram with a fitted curve",[1707,1738,1739],{},"Partly",[1707,1741,1742],{},"Image",[1689,1744,1745,1748,1750],{},[1707,1746,1747],{},"Small multiples, a grid of facets",[1707,1749,1723],{},[1707,1751,1742],{},[1689,1753,1754,1757,1759],{},[1707,1755,1756],{},"Regression fit with a confidence band",[1707,1758,1723],{},[1707,1760,1742],{},[1689,1762,1763,1766,1768],{},[1707,1764,1765],{},"Anything the reader will edit",[1707,1767,1712],{},[1707,1769,1770],{},"Native chart, always",[10,1772,1773],{},"The middle ground worth knowing about: for a matrix heatmap, applying a colour scale to the actual cells gives you a heatmap made of real numbers, which is better than a picture of one on every count.",[156,1775,1777],{"id":1776},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[1683,1779,1780,1793],{},[1686,1781,1782],{},[1689,1783,1784,1787,1790],{},[1692,1785,1786],{},"Symptom",[1692,1788,1789],{},"Cause",[1692,1791,1792],{},"Fix",[1702,1794,1795,1809,1822,1836,1847,1858,1873,1884],{},[1689,1796,1797,1800,1803],{},[1707,1798,1799],{},"Crash or hang on a server",[1707,1801,1802],{},"Interactive backend, no display",[1707,1804,1805,1808],{},[168,1806,1807],{},"matplotlib.use(\"Agg\")"," before importing pyplot",[1689,1810,1811,1814,1817],{},[1707,1812,1813],{},"Memory grows across a batch",[1707,1815,1816],{},"Figures never closed",[1707,1818,1819,1821],{},[168,1820,910],{}," after each save",[1689,1823,1824,1827,1833],{},[1707,1825,1826],{},"Image missing from the saved file",[1707,1828,1829,1830],{},"Buffer closed before ",[168,1831,1832],{},"wb.save()",[1707,1834,1835],{},"Keep the buffer open until after the save",[1689,1837,1838,1841,1844],{},[1707,1839,1840],{},"The same image appears twice",[1707,1842,1843],{},"One buffer reused for two insertions",[1707,1845,1846],{},"Render into a fresh buffer per image",[1689,1848,1849,1852,1855],{},[1707,1850,1851],{},"Blurry chart",[1707,1853,1854],{},"Rendered smaller than displayed",[1707,1856,1857],{},"Render at 1.5× the display size",[1689,1859,1860,1863,1866],{},[1707,1861,1862],{},"Labels unreadable",[1707,1864,1865],{},"Default font sizes on a small figure",[1707,1867,1868,1869,1872],{},"Set ",[168,1870,1871],{},"rcParams"," sizes explicitly",[1689,1874,1875,1878,1881],{},[1707,1876,1877],{},"Image covers the table",[1707,1879,1880],{},"Anchored inside the data range",[1707,1882,1883],{},"Anchor to the right of the last column",[1689,1885,1886,1889,1892],{},[1707,1887,1888],{},"Workbook is huge",[1707,1890,1891],{},"Several high-DPI PNGs",[1707,1893,1894],{},"Lower the DPI, or reuse one chart per sheet",[156,1896,1898],{"id":1897},"performance-and-scale-notes","Performance and scale notes",[10,1900,1901],{},"Rendering is the slow part: a simple matplotlib figure takes on the order of a hundred milliseconds, so a report with a chart per region across forty regions spends several seconds purely in rendering. If the same chart appears on several sheets, render it once and insert the same PNG bytes repeatedly rather than re-plotting.",[10,1903,1904],{},"File size grows with DPI squared, so a 300-DPI chart is four times the bytes of a 150-DPI one for a difference nobody sees on screen. 150 is a good default; go to 200 only when the workbook is genuinely going to be printed.",[156,1906,1908],{"id":1907},"conclusion","Conclusion",[10,1910,1911,1912,1914],{},"Render with the Agg backend, save into a ",[168,1913,96],{}," buffer, keep that buffer open until the workbook is saved, and close every figure so a batch does not leak. Render at about 1.5 times the display size so the result is sharp rather than soft, set the font sizes for the size it will actually appear at, and write the underlying numbers to a data sheet so the picture is not the only record. Then check the plot is one Excel could not have drawn — if it is a column chart, a native chart serves the reader better.",[156,1916,1918],{"id":1917},"frequently-asked-questions","Frequently asked questions",[10,1920,1921,1925],{},[1922,1923,1924],"strong",{},"Should I embed an image or build a native Excel chart?","\nA native chart when the reader may edit the numbers or wants to restyle it — it recalculates. An image when the plot is something Excel cannot draw, such as a heatmap, a regression fit or a small-multiples grid.",[10,1927,1928,1931,1932,1935,1936,1938,1939,1941,1942,1944],{},[1922,1929,1930],{},"Do I need to write the PNG to disk first?","\nNo. Save the figure into an ",[168,1933,1934],{},"io.BytesIO"," buffer and pass that to openpyxl's ",[168,1937,1742],{}," or xlsxwriter's ",[168,1940,1673],{}," with the ",[168,1943,1418],{}," argument. Nothing touches the filesystem.",[10,1946,1947,1950,1951,1954,1955,1957],{},[1922,1948,1949],{},"Why does the image look blurry in Excel?","\nIt was rendered at screen DPI and then scaled up. Render at ",[168,1952,1953],{},"dpi=150"," or ",[168,1956,1357],{}," and set the figure size in inches so the pixel dimensions match the space it will occupy.",[10,1959,1960,1963],{},[1922,1961,1962],{},"Does the image resize when someone changes a column width?","\nWith openpyxl the anchor is a cell reference and the image keeps its size. Excel's own move-and-size behaviour is not something openpyxl exposes, so treat the placement as fixed.",[156,1965,1967],{"id":1966},"related","Related",[10,1969,1970],{},"Up to the parent guide:",[1972,1973,1974],"ul",{},[1975,1976,1977,1979],"li",{},[14,1978,25],{"href":24}," — anchoring, sizing and the image formats Excel accepts.",[10,1981,1982],{},"Related guides:",[1972,1984,1985,1992,1999,2006],{},[1975,1986,1987,1991],{},[14,1988,1990],{"href":1989},"\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fadd-logo-image-to-excel-report-with-openpyxl\u002F","Add a Logo Image to an Excel Report with openpyxl"," — the same anchoring for branding rather than data.",[1975,1993,1994,1998],{},[14,1995,1997],{"href":1996},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-bar-chart-in-excel-with-openpyxl\u002F","Create a Bar Chart in Excel with openpyxl"," — the native chart to prefer where it fits.",[1975,2000,2001,2005],{},[14,2002,2004],{"href":2003},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fadd-data-bars-and-colour-scales-with-openpyxl\u002F","Add Data Bars and Colour Scales with openpyxl"," — a heatmap made of real cells instead of a picture.",[1975,2007,2008,2012],{},[14,2009,2011],{"href":2010},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-summary-sheet-to-excel-report-python\u002F","Add a Summary Sheet to an Excel Report in Python"," — where an embedded figure usually belongs.",[2014,2015,2016],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s-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 .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":166,"searchDepth":225,"depth":225,"links":2018},[2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030],{"id":158,"depth":225,"text":159},{"id":201,"depth":225,"text":202},{"id":917,"depth":225,"text":918},{"id":1172,"depth":225,"text":1173},{"id":1406,"depth":225,"text":1407},{"id":1561,"depth":225,"text":1562},{"id":1677,"depth":225,"text":1678},{"id":1776,"depth":225,"text":1777},{"id":1897,"depth":225,"text":1898},{"id":1907,"depth":225,"text":1908},{"id":1917,"depth":225,"text":1918},{"id":1966,"depth":225,"text":1967},"2026-08-11","Render a figure to PNG and anchor it in a worksheet: sizing and DPI that stay sharp, in-memory buffers so nothing hits the disk, keeping the source data on a sheet, and when a native chart is the better answer.","md",[2035,2037,2039,2041],{"q":1924,"a":2036},"A native chart when the reader may edit the numbers or wants to restyle it — it recalculates. An image when the plot is something Excel cannot draw, such as a heatmap, a regression fit or a small-multiples grid.",{"q":1930,"a":2038},"No. Save the figure into an io.BytesIO buffer and pass that to openpyxl's Image or xlsxwriter's insert_image with the image_data argument. Nothing touches the filesystem.",{"q":1949,"a":2040},"It was rendered at screen DPI and then scaled up. Render at dpi=150 or 200 and set the figure size in inches so the pixel dimensions match the space it will occupy.",{"q":1962,"a":2042},"With openpyxl the anchor is a cell reference and the image keeps its size. Excel's own move-and-size behaviour is not something openpyxl exposes, so treat the placement as fixed.",{"breadcrumb":2044},[2045,2048,2051,2052],{"name":2046,"item":2047},"Home","\u002F",{"name":2049,"item":2050},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002F",{"name":25,"item":24},{"name":5,"item":2053},"\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fembed-a-matplotlib-chart-in-an-excel-report\u002F","\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fembed-a-matplotlib-chart-in-an-excel-report",{"title":2056,"description":2057},"Embed a matplotlib Chart in Excel with Python","Insert matplotlib figures into .xlsx from Python — save to a BytesIO buffer, anchor with openpyxl or xlsxwriter, control DPI and cell sizing, and choose between an image and a native chart.","embed-a-matplotlib-chart-in-an-excel-report","formatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fembed-a-matplotlib-chart-in-an-excel-report\u002Findex","how-to","uQFir-2KrEwtVACXeXODzQeHjhvOqYoMMTdWR_pZ8dU",[2063,2066],{"title":1990,"path":2064,"stem":2065,"children":-1},"\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fadd-logo-image-to-excel-report-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fadd-logo-image-to-excel-report-with-openpyxl\u002Findex",{"title":2067,"path":2068,"stem":2069,"children":-1},"Resize and Position Images in Excel with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fresize-and-position-images-in-excel-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fresize-and-position-images-in-excel-with-openpyxl\u002Findex",1786800028804]