[{"data":1,"prerenderedAt":2327},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-error-bars-to-an-excel-chart-with-openpyxl":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-error-bars-to-an-excel-chart-with-openpyxl":2294,"ancestors:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-error-bars-to-an-excel-chart-with-openpyxl":2302,"children:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-error-bars-to-an-excel-chart-with-openpyxl":2307,"related:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-error-bars-to-an-excel-chart-with-openpyxl":2308},{"id":4,"title":5,"body":6,"dateModified":2261,"datePublished":2261,"description":2262,"extension":2263,"faq":2264,"keyword":2273,"kind":2274,"meta":2275,"navigation":254,"path":2283,"seo":2284,"slug":2287,"stem":2288,"topics":2289,"type":2292,"__hash__":2293},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-error-bars-to-an-excel-chart-with-openpyxl\u002Findex.md","Add Error Bars to an Excel Chart with openpyxl",{"type":7,"value":8,"toc":2249},"minimark",[9,19,24,56,66,200,204,212,475,491,495,498,987,1005,1009,1090,1093,1183,1187,1190,1823,1826,1981,1985,1988,2093,2106,2110,2154,2158,2176,2180,2194,2200,2206,2212,2216,2245],[10,11,12,13,18],"p",{},"A column chart of average satisfaction by region says the East scores 7.8 and the West 6.9. It does not say that the West's average comes from 30 responses and could be more than half a point higher or lower, or that the East's is based on 400 and is solid. Error bars add that: a line through the top of each column showing the range the true value probably lies in. They also show ranges that are not about statistics at all — the lowest and highest daily value around a monthly average, or the spread of a forecast. openpyxl writes them as part of the chart, reading their lengths from cells so the workbook stays traceable. This page is part of ",[14,15,17],"a",{"href":16},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002F","Creating Charts in Excel with openpyxl",".",[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 openpyxl pandas scipy\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}," openpyxl",[35,51,52],{"class":45}," pandas",[35,54,55],{"class":45}," scipy\n",[10,57,58,61,62,65],{},[32,59,60],{},"survey_responses.xlsx"," holds individual satisfaction scores from 0 to 10, one row per response, for four regions with very different numbers of responses — from 400 in the East to 30 in the West. ",[32,63,64],{},"daily_load.xlsx"," has a daily electricity load figure for eight months, used for the min\u002Fmax example.",[67,68,76,77,76,81,76,85,76,93,76,103,76,114,76,121,76,127,76,132,76,136,76,140,76,145,76,152,76,157,76,161,76,164,76,167,76,170,76,175,76,182,76,187,76,191,76,194,76,197],"svg",{"viewBox":69,"role":70,"ariaLabelledBy":71,"xmlns":74,"style":75},"4 0 752 207","img",[72,73],"eb-kinds-t","eb-kinds-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  ",[78,79,80],"title",{"id":72},"What an error bar can mean",[82,83,84],"desc",{"id":73},"Three panels. A confidence interval shows where the true average probably lies. A min–max range shows the spread of the underlying values. A fixed or percentage bar shows a tolerance that applies to every point.",[86,87],"rect",{"x":88,"y":89,"width":90,"height":91,"fill":92},"4","0","752","207","#ffffff",[86,94],{"x":95,"y":96,"width":97,"height":98,"rx":99,"fill":100,"stroke":101,"style":102},"20.0","16.0","228.0","175.0","14","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[104,105,107],"g",{"transform":106},"translate(121.0,29.0) scale(1.083)",[108,109],"path",{"d":110,"fill":111,"stroke":112,"style":113},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8","none","var(--brand-strong,#4338ca)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[115,116,120],"text",{"x":117,"y":118,"style":119},"134.0","78.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Confidence interval",[37,122],{"x1":123,"y1":124,"x2":125,"y2":124,"stroke":101,"style":126},"36.0","91.0","232.0","stroke-width:1px",[115,128,131],{"x":117,"y":129,"style":130},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","± t × standard error",[115,133,135],{"x":117,"y":134,"style":130},"137.0","depends on n",[115,137,139],{"x":117,"y":138,"style":130},"163.0","survey and sample data",[86,141],{"x":142,"y":96,"width":97,"height":98,"rx":99,"fill":143,"stroke":144,"style":102},"266.0","#d9f4f1","var(--teal,#0f9488)",[104,146,148],{"transform":147},"translate(367.0,29.0) scale(1.083)",[108,149],{"d":150,"fill":111,"stroke":151,"style":113},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5","var(--teal-ink,#0b6157)",[115,153,156],{"x":154,"y":118,"style":155},"380.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Min–max range",[37,158],{"x1":159,"y1":124,"x2":160,"y2":124,"stroke":144,"style":126},"282.0","478.0",[115,162,163],{"x":154,"y":129,"style":130},"asymmetric",[115,165,166],{"x":154,"y":134,"style":130},"lowest and highest value",[115,168,169],{"x":154,"y":138,"style":130},"daily values around a month",[86,171],{"x":172,"y":96,"width":97,"height":98,"rx":99,"fill":173,"stroke":174,"style":102},"512.0","#fdefd8","var(--gold,#b4740a)",[104,176,178],{"transform":177},"translate(613.0,29.0) scale(1.083)",[108,179],{"d":180,"fill":111,"stroke":181,"style":113},"M12 9a3 3 0 1 0 .01 0z M12 2.5v3 M12 18.5v3 M2.5 12h3 M18.5 12h3 M5.3 5.3l2.1 2.1 M16.6 16.6l2.1 2.1 M5.3 18.7l2.1-2.1 M16.6 7.4l2.1-2.1","var(--gold-ink,#7a4e06)",[115,183,186],{"x":184,"y":118,"style":185},"626.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Fixed or percentage",[37,188],{"x1":189,"y1":124,"x2":190,"y2":124,"stroke":174,"style":126},"528.0","724.0",[115,192,193],{"x":184,"y":129,"style":130},"same for every point",[115,195,196],{"x":184,"y":134,"style":130},"e.g. ±5% tolerance",[115,198,199],{"x":184,"y":138,"style":130},"measurement precision",[20,201,203],{"id":202},"step-1-compute-the-intervals-from-the-raw-data","Step 1 — Compute the intervals from the raw data",[10,205,206,207,211],{},"A 95% confidence interval for a mean is the mean plus or minus a ",[208,209,210],"em",{},"margin",": the t critical value for the sample size times the standard error, which is the standard deviation divided by the square root of n. Compute it per region:",[25,213,217],{"className":214,"code":215,"language":216,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\nfrom scipy import stats\n\nresponses = pd.read_excel(\"survey_responses.xlsx\")\nsummary = responses.groupby(\"Region\", sort=False)[\"Score\"].agg(n=\"count\", mean=\"mean\", sd=\"std\")\nsummary[\"t\"] = stats.t.ppf(0.975, summary[\"n\"] - 1)\nsummary[\"margin\"] = summary[\"t\"] * summary[\"sd\"] \u002F summary[\"n\"] ** 0.5\nsummary = summary.round({\"mean\": 2, \"sd\": 2, \"t\": 3, \"margin\": 2})\nprint(summary)\n","python",[32,218,219,235,249,256,274,340,376,420,466],{"__ignoreMap":30},[35,220,221,225,229,232],{"class":37,"line":38},[35,222,224],{"class":223},"s-kum","import",[35,226,228],{"class":227},"skGVy"," pandas ",[35,230,231],{"class":223},"as",[35,233,234],{"class":227}," pd\n",[35,236,238,241,244,246],{"class":37,"line":237},2,[35,239,240],{"class":223},"from",[35,242,243],{"class":227}," scipy ",[35,245,224],{"class":223},[35,247,248],{"class":227}," stats\n",[35,250,252],{"class":37,"line":251},3,[35,253,255],{"emptyLinePlaceholder":254},true,"\n",[35,257,259,262,265,268,271],{"class":37,"line":258},4,[35,260,261],{"class":227},"responses ",[35,263,264],{"class":223},"=",[35,266,267],{"class":227}," pd.read_excel(",[35,269,270],{"class":45},"\"survey_responses.xlsx\"",[35,272,273],{"class":227},")\n",[35,275,277,280,282,285,288,291,295,297,301,304,307,310,313,315,318,320,323,325,328,330,333,335,338],{"class":37,"line":276},5,[35,278,279],{"class":227},"summary ",[35,281,264],{"class":223},[35,283,284],{"class":227}," responses.groupby(",[35,286,287],{"class":45},"\"Region\"",[35,289,290],{"class":227},", ",[35,292,294],{"class":293},"sa561","sort",[35,296,264],{"class":223},[35,298,300],{"class":299},"sP0c6","False",[35,302,303],{"class":227},")[",[35,305,306],{"class":45},"\"Score\"",[35,308,309],{"class":227},"].agg(",[35,311,312],{"class":293},"n",[35,314,264],{"class":223},[35,316,317],{"class":45},"\"count\"",[35,319,290],{"class":227},[35,321,322],{"class":293},"mean",[35,324,264],{"class":223},[35,326,327],{"class":45},"\"mean\"",[35,329,290],{"class":227},[35,331,332],{"class":293},"sd",[35,334,264],{"class":223},[35,336,337],{"class":45},"\"std\"",[35,339,273],{"class":227},[35,341,343,346,349,352,354,357,360,363,366,368,371,374],{"class":37,"line":342},6,[35,344,345],{"class":227},"summary[",[35,347,348],{"class":45},"\"t\"",[35,350,351],{"class":227},"] ",[35,353,264],{"class":223},[35,355,356],{"class":227}," stats.t.ppf(",[35,358,359],{"class":299},"0.975",[35,361,362],{"class":227},", summary[",[35,364,365],{"class":45},"\"n\"",[35,367,351],{"class":227},[35,369,370],{"class":223},"-",[35,372,373],{"class":299}," 1",[35,375,273],{"class":227},[35,377,379,381,384,386,388,391,393,395,398,400,403,405,408,410,412,414,417],{"class":37,"line":378},7,[35,380,345],{"class":227},[35,382,383],{"class":45},"\"margin\"",[35,385,351],{"class":227},[35,387,264],{"class":223},[35,389,390],{"class":227}," summary[",[35,392,348],{"class":45},[35,394,351],{"class":227},[35,396,397],{"class":223},"*",[35,399,390],{"class":227},[35,401,402],{"class":45},"\"sd\"",[35,404,351],{"class":227},[35,406,407],{"class":223},"\u002F",[35,409,390],{"class":227},[35,411,365],{"class":45},[35,413,351],{"class":227},[35,415,416],{"class":223},"**",[35,418,419],{"class":299}," 0.5\n",[35,421,423,425,427,430,432,435,438,440,442,444,446,448,450,452,455,457,459,461,463],{"class":37,"line":422},8,[35,424,279],{"class":227},[35,426,264],{"class":223},[35,428,429],{"class":227}," summary.round({",[35,431,327],{"class":45},[35,433,434],{"class":227},": ",[35,436,437],{"class":299},"2",[35,439,290],{"class":227},[35,441,402],{"class":45},[35,443,434],{"class":227},[35,445,437],{"class":299},[35,447,290],{"class":227},[35,449,348],{"class":45},[35,451,434],{"class":227},[35,453,454],{"class":299},"3",[35,456,290],{"class":227},[35,458,383],{"class":45},[35,460,434],{"class":227},[35,462,437],{"class":299},[35,464,465],{"class":227},"})\n",[35,467,469,472],{"class":37,"line":468},9,[35,470,471],{"class":299},"print",[35,473,474],{"class":227},"(summary)\n",[10,476,477,478,481,482,486,487,490],{},"The margin shrinks with the square root of the number of responses: the East, with 400 answers, has a margin of under two tenths of a point, while the West, with 30, has more than half a point. ",[32,479,480],{},"stats.t.ppf(0.975, n - 1)"," is the two-sided 95% critical value — close to 1.96 for large samples and noticeably larger for small ones, which is why using 1.96 everywhere understates the uncertainty of small groups. ",[14,483,485],{"href":484},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fstdev-and-var-equivalents-in-pandas\u002F","STDEV and VAR Equivalents in pandas"," covers the sample standard deviation ",[32,488,489],{},"std"," uses.",[20,492,494],{"id":493},"step-2-write-the-summary-and-a-chart-with-custom-error-bars","Step 2 — Write the summary and a chart with custom error bars",[10,496,497],{},"Write the summary to a sheet and build a column chart of the means. Custom error bars read their plus and minus lengths from cell ranges — here the margin column, for both directions:",[25,499,501],{"className":214,"code":500,"language":216,"meta":30,"style":30},"from openpyxl import Workbook\nfrom openpyxl.chart import BarChart, Reference\nfrom openpyxl.chart.data_source import NumDataSource, NumRef\nfrom openpyxl.chart.error_bar import ErrorBars\n\nwb = Workbook()\nws = wb.active\nws.title = \"Survey\"\nws.append([\"Region\", \"Responses\", \"Mean score\", \"Margin (95%)\"])\nfor region, row in summary.iterrows():\n    ws.append([region, int(row[\"n\"]), row[\"mean\"], row[\"margin\"]])\n\nchart = BarChart()\nchart.type = \"col\"\nchart.title = \"Satisfaction by region, mean with 95% interval\"\nchart.add_data(Reference(ws, min_col=3, min_row=1, max_row=5), titles_from_data=True)\nchart.set_categories(Reference(ws, min_col=1, min_row=2, max_row=5))\nmargin = NumDataSource(numRef=NumRef(f=\"'Survey'!$D$2:$D$5\"))\nchart.series[0].errBars = ErrorBars(errDir=\"y\", errBarType=\"both\", errValType=\"cust\",\n                                    plus=margin, minus=margin, noEndCap=False)\nchart.series[0].graphicalProperties.solidFill = \"4338CA\"\nchart.y_axis.scaling.min, chart.y_axis.scaling.max = 0, 10\nchart.x_axis.delete = chart.y_axis.delete = False\nchart.legend = None\nchart.width, chart.height = 15, 8\nws.add_chart(chart, \"A8\")\nwb.save(\"survey_chart.xlsx\")\nprint(\"error bars read from\", margin.numRef.f)\n",[32,502,503,515,527,539,551,555,565,575,585,610,625,652,657,668,679,697,743,774,803,850,877,892,908,924,935,951,962,973],{"__ignoreMap":30},[35,504,505,507,510,512],{"class":37,"line":38},[35,506,240],{"class":223},[35,508,509],{"class":227}," openpyxl ",[35,511,224],{"class":223},[35,513,514],{"class":227}," Workbook\n",[35,516,517,519,522,524],{"class":37,"line":237},[35,518,240],{"class":223},[35,520,521],{"class":227}," openpyxl.chart ",[35,523,224],{"class":223},[35,525,526],{"class":227}," BarChart, Reference\n",[35,528,529,531,534,536],{"class":37,"line":251},[35,530,240],{"class":223},[35,532,533],{"class":227}," openpyxl.chart.data_source ",[35,535,224],{"class":223},[35,537,538],{"class":227}," NumDataSource, NumRef\n",[35,540,541,543,546,548],{"class":37,"line":258},[35,542,240],{"class":223},[35,544,545],{"class":227}," openpyxl.chart.error_bar ",[35,547,224],{"class":223},[35,549,550],{"class":227}," ErrorBars\n",[35,552,553],{"class":37,"line":276},[35,554,255],{"emptyLinePlaceholder":254},[35,556,557,560,562],{"class":37,"line":342},[35,558,559],{"class":227},"wb ",[35,561,264],{"class":223},[35,563,564],{"class":227}," Workbook()\n",[35,566,567,570,572],{"class":37,"line":378},[35,568,569],{"class":227},"ws ",[35,571,264],{"class":223},[35,573,574],{"class":227}," wb.active\n",[35,576,577,580,582],{"class":37,"line":422},[35,578,579],{"class":227},"ws.title ",[35,581,264],{"class":223},[35,583,584],{"class":45}," \"Survey\"\n",[35,586,587,590,592,594,597,599,602,604,607],{"class":37,"line":468},[35,588,589],{"class":227},"ws.append([",[35,591,287],{"class":45},[35,593,290],{"class":227},[35,595,596],{"class":45},"\"Responses\"",[35,598,290],{"class":227},[35,600,601],{"class":45},"\"Mean score\"",[35,603,290],{"class":227},[35,605,606],{"class":45},"\"Margin (95%)\"",[35,608,609],{"class":227},"])\n",[35,611,613,616,619,622],{"class":37,"line":612},10,[35,614,615],{"class":223},"for",[35,617,618],{"class":227}," region, row ",[35,620,621],{"class":223},"in",[35,623,624],{"class":227}," summary.iterrows():\n",[35,626,628,631,634,637,639,642,644,647,649],{"class":37,"line":627},11,[35,629,630],{"class":227},"    ws.append([region, ",[35,632,633],{"class":299},"int",[35,635,636],{"class":227},"(row[",[35,638,365],{"class":45},[35,640,641],{"class":227},"]), row[",[35,643,327],{"class":45},[35,645,646],{"class":227},"], row[",[35,648,383],{"class":45},[35,650,651],{"class":227},"]])\n",[35,653,655],{"class":37,"line":654},12,[35,656,255],{"emptyLinePlaceholder":254},[35,658,660,663,665],{"class":37,"line":659},13,[35,661,662],{"class":227},"chart ",[35,664,264],{"class":223},[35,666,667],{"class":227}," BarChart()\n",[35,669,671,674,676],{"class":37,"line":670},14,[35,672,673],{"class":227},"chart.type ",[35,675,264],{"class":223},[35,677,678],{"class":45}," \"col\"\n",[35,680,682,685,687,690,694],{"class":37,"line":681},15,[35,683,684],{"class":227},"chart.title ",[35,686,264],{"class":223},[35,688,689],{"class":45}," \"Satisfaction by region, mean with 95",[35,691,693],{"class":692},"sSjpA","% i",[35,695,696],{"class":45},"nterval\"\n",[35,698,700,703,706,708,710,712,715,717,720,722,725,727,730,733,736,738,741],{"class":37,"line":699},16,[35,701,702],{"class":227},"chart.add_data(Reference(ws, ",[35,704,705],{"class":293},"min_col",[35,707,264],{"class":223},[35,709,454],{"class":299},[35,711,290],{"class":227},[35,713,714],{"class":293},"min_row",[35,716,264],{"class":223},[35,718,719],{"class":299},"1",[35,721,290],{"class":227},[35,723,724],{"class":293},"max_row",[35,726,264],{"class":223},[35,728,729],{"class":299},"5",[35,731,732],{"class":227},"), ",[35,734,735],{"class":293},"titles_from_data",[35,737,264],{"class":223},[35,739,740],{"class":299},"True",[35,742,273],{"class":227},[35,744,746,749,751,753,755,757,759,761,763,765,767,769,771],{"class":37,"line":745},17,[35,747,748],{"class":227},"chart.set_categories(Reference(ws, ",[35,750,705],{"class":293},[35,752,264],{"class":223},[35,754,719],{"class":299},[35,756,290],{"class":227},[35,758,714],{"class":293},[35,760,264],{"class":223},[35,762,437],{"class":299},[35,764,290],{"class":227},[35,766,724],{"class":293},[35,768,264],{"class":223},[35,770,729],{"class":299},[35,772,773],{"class":227},"))\n",[35,775,777,780,782,785,788,790,793,796,798,801],{"class":37,"line":776},18,[35,778,779],{"class":227},"margin ",[35,781,264],{"class":223},[35,783,784],{"class":227}," NumDataSource(",[35,786,787],{"class":293},"numRef",[35,789,264],{"class":223},[35,791,792],{"class":227},"NumRef(",[35,794,795],{"class":293},"f",[35,797,264],{"class":223},[35,799,800],{"class":45},"\"'Survey'!$D$2:$D$5\"",[35,802,773],{"class":227},[35,804,806,809,811,814,816,819,822,824,827,829,832,834,837,839,842,844,847],{"class":37,"line":805},19,[35,807,808],{"class":227},"chart.series[",[35,810,89],{"class":299},[35,812,813],{"class":227},"].errBars ",[35,815,264],{"class":223},[35,817,818],{"class":227}," ErrorBars(",[35,820,821],{"class":293},"errDir",[35,823,264],{"class":223},[35,825,826],{"class":45},"\"y\"",[35,828,290],{"class":227},[35,830,831],{"class":293},"errBarType",[35,833,264],{"class":223},[35,835,836],{"class":45},"\"both\"",[35,838,290],{"class":227},[35,840,841],{"class":293},"errValType",[35,843,264],{"class":223},[35,845,846],{"class":45},"\"cust\"",[35,848,849],{"class":227},",\n",[35,851,853,856,858,861,864,866,868,871,873,875],{"class":37,"line":852},20,[35,854,855],{"class":293},"                                    plus",[35,857,264],{"class":223},[35,859,860],{"class":227},"margin, ",[35,862,863],{"class":293},"minus",[35,865,264],{"class":223},[35,867,860],{"class":227},[35,869,870],{"class":293},"noEndCap",[35,872,264],{"class":223},[35,874,300],{"class":299},[35,876,273],{"class":227},[35,878,880,882,884,887,889],{"class":37,"line":879},21,[35,881,808],{"class":227},[35,883,89],{"class":299},[35,885,886],{"class":227},"].graphicalProperties.solidFill ",[35,888,264],{"class":223},[35,890,891],{"class":45}," \"4338CA\"\n",[35,893,895,898,900,903,905],{"class":37,"line":894},22,[35,896,897],{"class":227},"chart.y_axis.scaling.min, chart.y_axis.scaling.max ",[35,899,264],{"class":223},[35,901,902],{"class":299}," 0",[35,904,290],{"class":227},[35,906,907],{"class":299},"10\n",[35,909,911,914,916,919,921],{"class":37,"line":910},23,[35,912,913],{"class":227},"chart.x_axis.delete ",[35,915,264],{"class":223},[35,917,918],{"class":227}," chart.y_axis.delete ",[35,920,264],{"class":223},[35,922,923],{"class":299}," False\n",[35,925,927,930,932],{"class":37,"line":926},24,[35,928,929],{"class":227},"chart.legend ",[35,931,264],{"class":223},[35,933,934],{"class":299}," None\n",[35,936,938,941,943,946,948],{"class":37,"line":937},25,[35,939,940],{"class":227},"chart.width, chart.height ",[35,942,264],{"class":223},[35,944,945],{"class":299}," 15",[35,947,290],{"class":227},[35,949,950],{"class":299},"8\n",[35,952,954,957,960],{"class":37,"line":953},26,[35,955,956],{"class":227},"ws.add_chart(chart, ",[35,958,959],{"class":45},"\"A8\"",[35,961,273],{"class":227},[35,963,965,968,971],{"class":37,"line":964},27,[35,966,967],{"class":227},"wb.save(",[35,969,970],{"class":45},"\"survey_chart.xlsx\"",[35,972,273],{"class":227},[35,974,976,978,981,984],{"class":37,"line":975},28,[35,977,471],{"class":299},[35,979,980],{"class":227},"(",[35,982,983],{"class":45},"\"error bars read from\"",[35,985,986],{"class":227},", margin.numRef.f)\n",[10,988,989,992,993,996,997,1000,1001,1004],{},[32,990,991],{},"errValType=\"cust\""," means custom values from cells; ",[32,994,995],{},"errBarType=\"both\""," draws above and below the point; ",[32,998,999],{},"errDir=\"y\""," makes them vertical. Because the lengths are cell references, anyone who opens the workbook can see exactly what each bar represents, and changing the margin cell changes the bar. The value axis is fixed to the 0–10 scoring range so the bars are drawn in proportion. As with every openpyxl chart, ",[32,1002,1003],{},"delete = False"," on the axes makes Excel show them.",[20,1006,1008],{"id":1007},"step-3-check-the-rendering","Step 3 — Check the rendering",[25,1010,1012],{"className":27,"code":1011,"language":29,"meta":30,"style":30},"soffice -env:UserInstallation=file:\u002F\u002F$PWD\u002Flo_profile --headless --convert-to pdf survey_chart.xlsx > \u002Fdev\u002Fnull 2>&1\npdftoppm -png -r 60 -f 1 -l 1 survey_chart.pdf preview && ls preview*\n",[32,1013,1014,1049],{"__ignoreMap":30},[35,1015,1016,1019,1022,1025,1028,1031,1034,1037,1040,1043,1046],{"class":37,"line":38},[35,1017,1018],{"class":41},"soffice",[35,1020,1021],{"class":299}," -env:UserInstallation=file:\u002F\u002F",[35,1023,1024],{"class":227},"$PWD",[35,1026,1027],{"class":299},"\u002Flo_profile",[35,1029,1030],{"class":299}," --headless",[35,1032,1033],{"class":299}," --convert-to",[35,1035,1036],{"class":45}," pdf",[35,1038,1039],{"class":45}," survey_chart.xlsx",[35,1041,1042],{"class":223}," >",[35,1044,1045],{"class":45}," \u002Fdev\u002Fnull",[35,1047,1048],{"class":223}," 2>&1\n",[35,1050,1051,1054,1057,1060,1063,1066,1068,1071,1073,1076,1079,1082,1085,1087],{"class":37,"line":237},[35,1052,1053],{"class":41},"pdftoppm",[35,1055,1056],{"class":299}," -png",[35,1058,1059],{"class":299}," -r",[35,1061,1062],{"class":299}," 60",[35,1064,1065],{"class":299}," -f",[35,1067,373],{"class":299},[35,1069,1070],{"class":299}," -l",[35,1072,373],{"class":299},[35,1074,1075],{"class":45}," survey_chart.pdf",[35,1077,1078],{"class":45}," preview",[35,1080,1081],{"class":227}," && ",[35,1083,1084],{"class":41},"ls",[35,1086,1078],{"class":45},[35,1088,1089],{"class":299},"*\n",[10,1091,1092],{},"In the rendered chart, the East's bar is a short tick above its column and the West's a long whisker — the picture now says what the table of margins said. The West's interval, from about 6.3 to 7.5, overlaps both the North's and the South's, so this survey cannot say where the West really ranks between them. The North and the South, on the other hand, are separated by more than their intervals combined: that difference is real.",[67,1094,76,1100,76,1103,76,1106,76,1110,76,1117,76,1123,76,1128,76,1133,76,1136,76,1140,76,1143,76,1147,76,1150,76,1154,76,1157,76,1162,76,1165,76,1169,76,1173,76,1178],{"viewBox":1095,"role":70,"ariaLabelledBy":1096,"xmlns":74,"style":1099},"4 0 744 206",[1097,1098],"eb-margins-t","eb-margins-d","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[78,1101,1102],{"id":1097},"The 95% margin shrinks as responses grow",[82,1104,1105],{"id":1098},"Horizontal bars of the 95 percent margin for each region. The East, with 400 responses, has a margin of 0.16 points; the North 0.23 with 220; the South 0.35 with 90; the West 0.60 with 30.",[86,1107],{"x":88,"y":89,"width":1108,"height":1109,"fill":92},"744","206",[86,1111],{"x":95,"y":96,"width":1112,"height":1113,"rx":1114,"fill":1115,"stroke":1116,"style":126},"126.1","28.0","7","#e7ebef","var(--line,#cdd5e6)",[115,1118,1122],{"x":1119,"y":1120,"style":1121},"83.0","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","East (n = 400)",[86,1124],{"x":1125,"y":96,"width":1126,"height":1113,"rx":1114,"fill":143,"stroke":144,"style":1127},"156.1","140.8","stroke-width:1.5px",[115,1129,1132],{"x":1130,"y":1120,"style":1131},"304.9","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","± 0.16",[86,1134],{"x":95,"y":1135,"width":1112,"height":1113,"rx":1114,"fill":1115,"stroke":1116,"style":126},"56.0",[115,1137,1139],{"x":1119,"y":1138,"style":1121},"74.5","North (n = 220)",[86,1141],{"x":1125,"y":1135,"width":1142,"height":1113,"rx":1114,"fill":143,"stroke":144,"style":1127},"202.4",[115,1144,1146],{"x":1145,"y":1138,"style":1131},"366.5","± 0.23",[86,1148],{"x":95,"y":1149,"width":1112,"height":1113,"rx":1114,"fill":1115,"stroke":1116,"style":126},"96.0",[115,1151,1153],{"x":1119,"y":1152,"style":1121},"114.5","South (n = 90)",[86,1155],{"x":1125,"y":1149,"width":1156,"height":1113,"rx":1114,"fill":173,"stroke":174,"style":1127},"308.0",[115,1158,1161],{"x":1159,"y":1152,"style":1160},"472.1","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","± 0.35",[86,1163],{"x":95,"y":1164,"width":1112,"height":1113,"rx":1114,"fill":1115,"stroke":1116,"style":126},"136.0",[115,1166,1168],{"x":1119,"y":1167,"style":1121},"154.5","West (n = 30)",[86,1170],{"x":1125,"y":1164,"width":189,"height":1113,"rx":1114,"fill":1171,"stroke":1172,"style":1127},"#fce9e9","var(--danger,#dc2626)",[115,1174,1177],{"x":1175,"y":1167,"style":1176},"692.1","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","± 0.60",[115,1179,1182],{"x":154,"y":1180,"style":1181},"186.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","The same numbers the error bars read from the Margin column.",[20,1184,1186],{"id":1185},"step-4-asymmetric-ranges-min-and-max-around-a-monthly-average","Step 4 — Asymmetric ranges: min and max around a monthly average",[10,1188,1189],{},"Error bars do not have to be symmetric or statistical. For a monthly average of daily values, bars from the month's lowest to highest day show the spread. The plus length is max minus mean, the minus length mean minus min — two different ranges:",[25,1191,1193],{"className":214,"code":1192,"language":216,"meta":30,"style":30},"load = pd.read_excel(\"daily_load.xlsx\", parse_dates=[\"Date\"])\nmonthly = load.groupby(load[\"Date\"].dt.to_period(\"M\"))[\"Load_MW\"].agg([\"mean\", \"min\", \"max\"]).round(1)\nmonthly[\"up\"] = (monthly[\"max\"] - monthly[\"mean\"]).round(1)\nmonthly[\"down\"] = (monthly[\"mean\"] - monthly[\"min\"]).round(1)\nprint(monthly)\n\nfrom openpyxl.chart import LineChart\nfrom openpyxl.chart.shapes import GraphicalProperties\nfrom openpyxl.drawing.line import LineProperties\n\nws2 = wb.create_sheet(\"Load\")\nws2.append([\"Month\", \"Average MW\", \"Up to max\", \"Down to min\"])\nfor month, row in monthly.iterrows():\n    ws2.append([month.strftime(\"%b\"), row[\"mean\"], row[\"up\"], row[\"down\"]])\nline = LineChart()\nline.title = \"Daily load: monthly average with min–max range\"\nline.add_data(Reference(ws2, min_col=2, min_row=1, max_row=len(monthly) + 1), titles_from_data=True)\nline.set_categories(Reference(ws2, min_col=1, min_row=2, max_row=len(monthly) + 1))\ns = line.series[0]\ns.errBars = ErrorBars(errDir=\"y\", errBarType=\"both\", errValType=\"cust\",\n                      plus=NumDataSource(numRef=NumRef(f=f\"'Load'!$C$2:$C${len(monthly) + 1}\")),\n                      minus=NumDataSource(numRef=NumRef(f=f\"'Load'!$D$2:$D${len(monthly) + 1}\")))\ns.errBars.spPr = GraphicalProperties(ln=LineProperties(solidFill=\"0F9488\", w=19050))\ns.graphicalProperties.line.solidFill = \"4338CA\"\ns.marker.symbol = \"circle\"\nline.x_axis.delete = line.y_axis.delete = False\nline.legend = None\nline.width, line.height = 15, 8\nws2.add_chart(line, \"A11\")\nwb.save(\"survey_chart.xlsx\")\nprint(len(monthly), \"months\")\n",[32,1194,1195,1222,1268,1300,1329,1336,1340,1351,1363,1375,1379,1394,1419,1431,1459,1469,1479,1525,1560,1575,1608,1652,1693,1731,1740,1750,1764,1773,1786,1797,1806],{"__ignoreMap":30},[35,1196,1197,1200,1202,1204,1207,1209,1212,1214,1217,1220],{"class":37,"line":38},[35,1198,1199],{"class":227},"load ",[35,1201,264],{"class":223},[35,1203,267],{"class":227},[35,1205,1206],{"class":45},"\"daily_load.xlsx\"",[35,1208,290],{"class":227},[35,1210,1211],{"class":293},"parse_dates",[35,1213,264],{"class":223},[35,1215,1216],{"class":227},"[",[35,1218,1219],{"class":45},"\"Date\"",[35,1221,609],{"class":227},[35,1223,1224,1227,1229,1232,1234,1237,1240,1243,1246,1249,1251,1253,1256,1258,1261,1264,1266],{"class":37,"line":237},[35,1225,1226],{"class":227},"monthly ",[35,1228,264],{"class":223},[35,1230,1231],{"class":227}," load.groupby(load[",[35,1233,1219],{"class":45},[35,1235,1236],{"class":227},"].dt.to_period(",[35,1238,1239],{"class":45},"\"M\"",[35,1241,1242],{"class":227},"))[",[35,1244,1245],{"class":45},"\"Load_MW\"",[35,1247,1248],{"class":227},"].agg([",[35,1250,327],{"class":45},[35,1252,290],{"class":227},[35,1254,1255],{"class":45},"\"min\"",[35,1257,290],{"class":227},[35,1259,1260],{"class":45},"\"max\"",[35,1262,1263],{"class":227},"]).round(",[35,1265,719],{"class":299},[35,1267,273],{"class":227},[35,1269,1270,1273,1276,1278,1280,1283,1285,1287,1289,1292,1294,1296,1298],{"class":37,"line":251},[35,1271,1272],{"class":227},"monthly[",[35,1274,1275],{"class":45},"\"up\"",[35,1277,351],{"class":227},[35,1279,264],{"class":223},[35,1281,1282],{"class":227}," (monthly[",[35,1284,1260],{"class":45},[35,1286,351],{"class":227},[35,1288,370],{"class":223},[35,1290,1291],{"class":227}," monthly[",[35,1293,327],{"class":45},[35,1295,1263],{"class":227},[35,1297,719],{"class":299},[35,1299,273],{"class":227},[35,1301,1302,1304,1307,1309,1311,1313,1315,1317,1319,1321,1323,1325,1327],{"class":37,"line":258},[35,1303,1272],{"class":227},[35,1305,1306],{"class":45},"\"down\"",[35,1308,351],{"class":227},[35,1310,264],{"class":223},[35,1312,1282],{"class":227},[35,1314,327],{"class":45},[35,1316,351],{"class":227},[35,1318,370],{"class":223},[35,1320,1291],{"class":227},[35,1322,1255],{"class":45},[35,1324,1263],{"class":227},[35,1326,719],{"class":299},[35,1328,273],{"class":227},[35,1330,1331,1333],{"class":37,"line":276},[35,1332,471],{"class":299},[35,1334,1335],{"class":227},"(monthly)\n",[35,1337,1338],{"class":37,"line":342},[35,1339,255],{"emptyLinePlaceholder":254},[35,1341,1342,1344,1346,1348],{"class":37,"line":378},[35,1343,240],{"class":223},[35,1345,521],{"class":227},[35,1347,224],{"class":223},[35,1349,1350],{"class":227}," LineChart\n",[35,1352,1353,1355,1358,1360],{"class":37,"line":422},[35,1354,240],{"class":223},[35,1356,1357],{"class":227}," openpyxl.chart.shapes ",[35,1359,224],{"class":223},[35,1361,1362],{"class":227}," GraphicalProperties\n",[35,1364,1365,1367,1370,1372],{"class":37,"line":468},[35,1366,240],{"class":223},[35,1368,1369],{"class":227}," openpyxl.drawing.line ",[35,1371,224],{"class":223},[35,1373,1374],{"class":227}," LineProperties\n",[35,1376,1377],{"class":37,"line":612},[35,1378,255],{"emptyLinePlaceholder":254},[35,1380,1381,1384,1386,1389,1392],{"class":37,"line":627},[35,1382,1383],{"class":227},"ws2 ",[35,1385,264],{"class":223},[35,1387,1388],{"class":227}," wb.create_sheet(",[35,1390,1391],{"class":45},"\"Load\"",[35,1393,273],{"class":227},[35,1395,1396,1399,1402,1404,1407,1409,1412,1414,1417],{"class":37,"line":654},[35,1397,1398],{"class":227},"ws2.append([",[35,1400,1401],{"class":45},"\"Month\"",[35,1403,290],{"class":227},[35,1405,1406],{"class":45},"\"Average MW\"",[35,1408,290],{"class":227},[35,1410,1411],{"class":45},"\"Up to max\"",[35,1413,290],{"class":227},[35,1415,1416],{"class":45},"\"Down to min\"",[35,1418,609],{"class":227},[35,1420,1421,1423,1426,1428],{"class":37,"line":659},[35,1422,615],{"class":223},[35,1424,1425],{"class":227}," month, row ",[35,1427,621],{"class":223},[35,1429,1430],{"class":227}," monthly.iterrows():\n",[35,1432,1433,1436,1439,1442,1444,1447,1449,1451,1453,1455,1457],{"class":37,"line":670},[35,1434,1435],{"class":227},"    ws2.append([month.strftime(",[35,1437,1438],{"class":45},"\"",[35,1440,1441],{"class":692},"%b",[35,1443,1438],{"class":45},[35,1445,1446],{"class":227},"), row[",[35,1448,327],{"class":45},[35,1450,646],{"class":227},[35,1452,1275],{"class":45},[35,1454,646],{"class":227},[35,1456,1306],{"class":45},[35,1458,651],{"class":227},[35,1460,1461,1464,1466],{"class":37,"line":681},[35,1462,1463],{"class":227},"line ",[35,1465,264],{"class":223},[35,1467,1468],{"class":227}," LineChart()\n",[35,1470,1471,1474,1476],{"class":37,"line":699},[35,1472,1473],{"class":227},"line.title ",[35,1475,264],{"class":223},[35,1477,1478],{"class":45}," \"Daily load: monthly average with min–max range\"\n",[35,1480,1481,1484,1486,1488,1490,1492,1494,1496,1498,1500,1502,1504,1507,1510,1513,1515,1517,1519,1521,1523],{"class":37,"line":745},[35,1482,1483],{"class":227},"line.add_data(Reference(ws2, ",[35,1485,705],{"class":293},[35,1487,264],{"class":223},[35,1489,437],{"class":299},[35,1491,290],{"class":227},[35,1493,714],{"class":293},[35,1495,264],{"class":223},[35,1497,719],{"class":299},[35,1499,290],{"class":227},[35,1501,724],{"class":293},[35,1503,264],{"class":223},[35,1505,1506],{"class":299},"len",[35,1508,1509],{"class":227},"(monthly) ",[35,1511,1512],{"class":223},"+",[35,1514,373],{"class":299},[35,1516,732],{"class":227},[35,1518,735],{"class":293},[35,1520,264],{"class":223},[35,1522,740],{"class":299},[35,1524,273],{"class":227},[35,1526,1527,1530,1532,1534,1536,1538,1540,1542,1544,1546,1548,1550,1552,1554,1556,1558],{"class":37,"line":776},[35,1528,1529],{"class":227},"line.set_categories(Reference(ws2, ",[35,1531,705],{"class":293},[35,1533,264],{"class":223},[35,1535,719],{"class":299},[35,1537,290],{"class":227},[35,1539,714],{"class":293},[35,1541,264],{"class":223},[35,1543,437],{"class":299},[35,1545,290],{"class":227},[35,1547,724],{"class":293},[35,1549,264],{"class":223},[35,1551,1506],{"class":299},[35,1553,1509],{"class":227},[35,1555,1512],{"class":223},[35,1557,373],{"class":299},[35,1559,773],{"class":227},[35,1561,1562,1565,1567,1570,1572],{"class":37,"line":805},[35,1563,1564],{"class":227},"s ",[35,1566,264],{"class":223},[35,1568,1569],{"class":227}," line.series[",[35,1571,89],{"class":299},[35,1573,1574],{"class":227},"]\n",[35,1576,1577,1580,1582,1584,1586,1588,1590,1592,1594,1596,1598,1600,1602,1604,1606],{"class":37,"line":852},[35,1578,1579],{"class":227},"s.errBars ",[35,1581,264],{"class":223},[35,1583,818],{"class":227},[35,1585,821],{"class":293},[35,1587,264],{"class":223},[35,1589,826],{"class":45},[35,1591,290],{"class":227},[35,1593,831],{"class":293},[35,1595,264],{"class":223},[35,1597,836],{"class":45},[35,1599,290],{"class":227},[35,1601,841],{"class":293},[35,1603,264],{"class":223},[35,1605,846],{"class":45},[35,1607,849],{"class":227},[35,1609,1610,1613,1615,1618,1620,1622,1624,1626,1628,1630,1633,1636,1638,1640,1642,1644,1647,1649],{"class":37,"line":879},[35,1611,1612],{"class":293},"                      plus",[35,1614,264],{"class":223},[35,1616,1617],{"class":227},"NumDataSource(",[35,1619,787],{"class":293},[35,1621,264],{"class":223},[35,1623,792],{"class":227},[35,1625,795],{"class":293},[35,1627,264],{"class":223},[35,1629,795],{"class":223},[35,1631,1632],{"class":45},"\"'Load'!$C$2:$C$",[35,1634,1635],{"class":692},"{",[35,1637,1506],{"class":299},[35,1639,1509],{"class":227},[35,1641,1512],{"class":223},[35,1643,373],{"class":299},[35,1645,1646],{"class":692},"}",[35,1648,1438],{"class":45},[35,1650,1651],{"class":227},")),\n",[35,1653,1654,1657,1659,1661,1663,1665,1667,1669,1671,1673,1676,1678,1680,1682,1684,1686,1688,1690],{"class":37,"line":894},[35,1655,1656],{"class":293},"                      minus",[35,1658,264],{"class":223},[35,1660,1617],{"class":227},[35,1662,787],{"class":293},[35,1664,264],{"class":223},[35,1666,792],{"class":227},[35,1668,795],{"class":293},[35,1670,264],{"class":223},[35,1672,795],{"class":223},[35,1674,1675],{"class":45},"\"'Load'!$D$2:$D$",[35,1677,1635],{"class":692},[35,1679,1506],{"class":299},[35,1681,1509],{"class":227},[35,1683,1512],{"class":223},[35,1685,373],{"class":299},[35,1687,1646],{"class":692},[35,1689,1438],{"class":45},[35,1691,1692],{"class":227},")))\n",[35,1694,1695,1698,1700,1703,1706,1708,1711,1714,1716,1719,1721,1724,1726,1729],{"class":37,"line":910},[35,1696,1697],{"class":227},"s.errBars.spPr ",[35,1699,264],{"class":223},[35,1701,1702],{"class":227}," GraphicalProperties(",[35,1704,1705],{"class":293},"ln",[35,1707,264],{"class":223},[35,1709,1710],{"class":227},"LineProperties(",[35,1712,1713],{"class":293},"solidFill",[35,1715,264],{"class":223},[35,1717,1718],{"class":45},"\"0F9488\"",[35,1720,290],{"class":227},[35,1722,1723],{"class":293},"w",[35,1725,264],{"class":223},[35,1727,1728],{"class":299},"19050",[35,1730,773],{"class":227},[35,1732,1733,1736,1738],{"class":37,"line":926},[35,1734,1735],{"class":227},"s.graphicalProperties.line.solidFill ",[35,1737,264],{"class":223},[35,1739,891],{"class":45},[35,1741,1742,1745,1747],{"class":37,"line":937},[35,1743,1744],{"class":227},"s.marker.symbol ",[35,1746,264],{"class":223},[35,1748,1749],{"class":45}," \"circle\"\n",[35,1751,1752,1755,1757,1760,1762],{"class":37,"line":953},[35,1753,1754],{"class":227},"line.x_axis.delete ",[35,1756,264],{"class":223},[35,1758,1759],{"class":227}," line.y_axis.delete ",[35,1761,264],{"class":223},[35,1763,923],{"class":299},[35,1765,1766,1769,1771],{"class":37,"line":964},[35,1767,1768],{"class":227},"line.legend ",[35,1770,264],{"class":223},[35,1772,934],{"class":299},[35,1774,1775,1778,1780,1782,1784],{"class":37,"line":975},[35,1776,1777],{"class":227},"line.width, line.height ",[35,1779,264],{"class":223},[35,1781,945],{"class":299},[35,1783,290],{"class":227},[35,1785,950],{"class":299},[35,1787,1789,1792,1795],{"class":37,"line":1788},29,[35,1790,1791],{"class":227},"ws2.add_chart(line, ",[35,1793,1794],{"class":45},"\"A11\"",[35,1796,273],{"class":227},[35,1798,1800,1802,1804],{"class":37,"line":1799},30,[35,1801,967],{"class":227},[35,1803,970],{"class":45},[35,1805,273],{"class":227},[35,1807,1809,1811,1813,1815,1818,1821],{"class":37,"line":1808},31,[35,1810,471],{"class":299},[35,1812,980],{"class":227},[35,1814,1506],{"class":299},[35,1816,1817],{"class":227},"(monthly), ",[35,1819,1820],{"class":45},"\"months\"",[35,1822,273],{"class":227},[10,1824,1825],{},"Load climbs from about 450 MW in January to nearly 780 MW in July and August — a cooling-driven pattern — and July's downward whisker is the longest, reaching 637 MW on one unusually cool day.",[67,1827,76,1833,76,1836,76,1839,76,1843,76,1848,76,1853,76,1857,76,1861,76,1864,76,1868,76,1871,76,1875,76,1878,76,1881,76,1884,76,1890,76,1894,76,1898,76,1902,76,1906,76,1910,76,1914,76,1918,76,1922,76,1927,76,1930,76,1933,76,1936,76,1939,76,1942,76,1945,76,1948,76,1953,76,1957,76,1961,76,1965,76,1969,76,1973,76,1977],{"viewBox":1828,"role":70,"ariaLabelledBy":1829,"xmlns":74,"style":1832},"4 0 748 222",[1830,1831],"eb-load-t","eb-load-d","width:100%;max-width:748px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[78,1834,1835],{"id":1830},"Monthly average load, the series the whiskers sit on",[82,1837,1838],{"id":1831},"Line chart of average daily load by month from January to August, rising from about 449 megawatts to about 783 in July and 777 in August.",[86,1840],{"x":88,"y":89,"width":1841,"height":1842,"fill":92},"748","222",[37,1844],{"x1":1845,"y1":1846,"x2":1847,"y2":1846,"stroke":1116,"style":126},"54.0","184.0","736.0",[115,1849,89],{"x":1850,"y":1851,"style":1852},"31.0","188.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle",[37,1854],{"x1":1845,"y1":1855,"x2":1847,"y2":1855,"stroke":1116,"style":1856},"144.0","stroke-width:1px;stroke-dasharray:5 4",[115,1858,1860],{"x":1850,"y":1859,"style":1852},"148.0","200",[37,1862],{"x1":1845,"y1":1863,"x2":1847,"y2":1863,"stroke":1116,"style":1856},"104.0",[115,1865,1867],{"x":1850,"y":1866,"style":1852},"108.0","400",[37,1869],{"x1":1845,"y1":1870,"x2":1847,"y2":1870,"stroke":1116,"style":1856},"64.0",[115,1872,1874],{"x":1850,"y":1873,"style":1852},"68.0","600",[37,1876],{"x1":1845,"y1":1877,"x2":1847,"y2":1877,"stroke":1116,"style":1856},"24.0",[115,1879,1880],{"x":1850,"y":1113,"style":1852},"800",[37,1882],{"x1":1845,"y1":1846,"x2":1847,"y2":1846,"stroke":1883,"style":1127},"var(--muted,#5b6780)",[115,1885,1889],{"x":1886,"y":1887,"style":1888},"96.7","202.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Jan",[115,1891,1893],{"x":1892,"y":1887,"style":1888},"181.9","Feb",[115,1895,1897],{"x":1896,"y":1887,"style":1888},"267.2","Mar",[115,1899,1901],{"x":1900,"y":1887,"style":1888},"352.4","Apr",[115,1903,1905],{"x":1904,"y":1887,"style":1888},"437.6","May",[115,1907,1909],{"x":1908,"y":1887,"style":1888},"522.9","Jun",[115,1911,1913],{"x":1912,"y":1887,"style":1888},"608.1","Jul",[115,1915,1917],{"x":1916,"y":1887,"style":1888},"693.4","Aug",[108,1919],{"d":1920,"fill":111,"stroke":101,"style":1921},"M96.7,94.2 L181.9,88.2 L267.2,77.8 L352.4,60.6 L437.6,47.8 L522.9,34.6 L608.1,27.4 L693.4,28.6","stroke-width:2.5px;stroke-linejoin:round;stroke-linecap:round",[1923,1924],"circle",{"cx":1886,"cy":1925,"r":1926,"fill":92,"stroke":101,"style":102},"94.2","4.0",[1923,1928],{"cx":1892,"cy":1929,"r":1926,"fill":92,"stroke":101,"style":102},"88.2",[1923,1931],{"cx":1896,"cy":1932,"r":1926,"fill":92,"stroke":101,"style":102},"77.8",[1923,1934],{"cx":1900,"cy":1935,"r":1926,"fill":92,"stroke":101,"style":102},"60.6",[1923,1937],{"cx":1904,"cy":1938,"r":1926,"fill":92,"stroke":101,"style":102},"47.8",[1923,1940],{"cx":1908,"cy":1941,"r":1926,"fill":92,"stroke":101,"style":102},"34.6",[1923,1943],{"cx":1912,"cy":1944,"r":1926,"fill":92,"stroke":101,"style":102},"27.4",[1923,1946],{"cx":1916,"cy":1947,"r":1926,"fill":92,"stroke":101,"style":102},"28.6",[115,1949,1952],{"x":1886,"y":1950,"style":1951},"84.2","font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","449",[115,1954,1956],{"x":1892,"y":1955,"style":1951},"78.2","479",[115,1958,1960],{"x":1896,"y":1959,"style":1951},"67.8","531",[115,1962,1964],{"x":1900,"y":1963,"style":1951},"50.6","617",[115,1966,1968],{"x":1904,"y":1967,"style":1951},"37.8","681",[115,1970,1972],{"x":1908,"y":1971,"style":1951},"24.6","747",[115,1974,1976],{"x":1912,"y":1975,"style":1951},"17.4","783",[115,1978,1980],{"x":1916,"y":1979,"style":1951},"18.6","777",[20,1982,1984],{"id":1983},"step-5-fixed-and-percentage-error-bars","Step 5 — Fixed and percentage error bars",[10,1986,1987],{},"When the same tolerance applies to every point — a measurement accurate to ±5%, a target band of ±20 units — no cell ranges are needed:",[25,1989,1991],{"className":214,"code":1990,"language":216,"meta":30,"style":30},"pct = ErrorBars(errDir=\"y\", errBarType=\"both\", errValType=\"percentage\", val=5)\nfixed = ErrorBars(errDir=\"y\", errBarType=\"plus\", errValType=\"fixedVal\", val=20)\nprint(pct.errValType, pct.val, \"|\", fixed.errValType, fixed.errBarType, fixed.val)\n",[32,1992,1993,2036,2080],{"__ignoreMap":30},[35,1994,1995,1998,2000,2002,2004,2006,2008,2010,2012,2014,2016,2018,2020,2022,2025,2027,2030,2032,2034],{"class":37,"line":38},[35,1996,1997],{"class":227},"pct ",[35,1999,264],{"class":223},[35,2001,818],{"class":227},[35,2003,821],{"class":293},[35,2005,264],{"class":223},[35,2007,826],{"class":45},[35,2009,290],{"class":227},[35,2011,831],{"class":293},[35,2013,264],{"class":223},[35,2015,836],{"class":45},[35,2017,290],{"class":227},[35,2019,841],{"class":293},[35,2021,264],{"class":223},[35,2023,2024],{"class":45},"\"percentage\"",[35,2026,290],{"class":227},[35,2028,2029],{"class":293},"val",[35,2031,264],{"class":223},[35,2033,729],{"class":299},[35,2035,273],{"class":227},[35,2037,2038,2041,2043,2045,2047,2049,2051,2053,2055,2057,2060,2062,2064,2066,2069,2071,2073,2075,2078],{"class":37,"line":237},[35,2039,2040],{"class":227},"fixed ",[35,2042,264],{"class":223},[35,2044,818],{"class":227},[35,2046,821],{"class":293},[35,2048,264],{"class":223},[35,2050,826],{"class":45},[35,2052,290],{"class":227},[35,2054,831],{"class":293},[35,2056,264],{"class":223},[35,2058,2059],{"class":45},"\"plus\"",[35,2061,290],{"class":227},[35,2063,841],{"class":293},[35,2065,264],{"class":223},[35,2067,2068],{"class":45},"\"fixedVal\"",[35,2070,290],{"class":227},[35,2072,2029],{"class":293},[35,2074,264],{"class":223},[35,2076,2077],{"class":299},"20",[35,2079,273],{"class":227},[35,2081,2082,2084,2087,2090],{"class":37,"line":251},[35,2083,471],{"class":299},[35,2085,2086],{"class":227},"(pct.errValType, pct.val, ",[35,2088,2089],{"class":45},"\"|\"",[35,2091,2092],{"class":227},", fixed.errValType, fixed.errBarType, fixed.val)\n",[10,2094,2095,2097,2098,2101,2102,2105],{},[32,2096,841],{}," can also be ",[32,2099,2100],{},"\"stdErr\""," or ",[32,2103,2104],{},"\"stdDev\"",", which make Excel compute the bars from the plotted values themselves. Those are rarely what a report means — they describe the spread of the plotted points, not the uncertainty of each one — so compute intervals from the raw data as in Step 1 and use custom bars.",[20,2107,2109],{"id":2108},"common-pitfalls","Common pitfalls",[2111,2112,2113,2121,2127,2139,2145],"ul",{},[2114,2115,2116,2120],"li",{},[2117,2118,2119],"strong",{},"Bars without a stated meaning."," Always say in the title or a note whether bars are a confidence interval, a range or a tolerance.",[2114,2122,2123,2126],{},[2117,2124,2125],{},"Using 1.96 for small samples."," Use the t distribution; for 30 responses the critical value is about 2.05.",[2114,2128,2129,2138],{},[2117,2130,2131,2132,407,2135,18],{},"Built-in ",[32,2133,2134],{},"stdErr",[32,2136,2137],{},"stdDev"," They are computed from the plotted points, not from the data behind each point.",[2114,2140,2141,2144],{},[2117,2142,2143],{},"Axis ranges that clip bars."," Fix the axis so the longest whisker fits.",[2114,2146,2147,2150,2151,2153],{},[2117,2148,2149],{},"Hidden axes in Excel."," Set ",[32,2152,1003],{}," on both axes.",[20,2155,2157],{"id":2156},"conclusion","Conclusion",[10,2159,2160,2161,2164,2165,2168,2169,2171,2172,2175],{},"Compute what the bars should show from the raw data — a t-based confidence interval per group, or the min and max around an average — and write those numbers to cells. Attach ",[32,2162,2163],{},"ErrorBars(errValType=\"cust\", ...)"," to the chart series with ",[32,2166,2167],{},"plus"," and ",[32,2170,863],{}," pointing at those cells, symmetric or not, style them with ",[32,2173,2174],{},"spPr",", and fix the axis so they fit. Use fixed or percentage bars for uniform tolerances, and always say what the bars mean.",[20,2177,2179],{"id":2178},"frequently-asked-questions","Frequently asked questions",[10,2181,2182,2185,2186,2189,2190,2193],{},[2117,2183,2184],{},"How do I add error bars to a chart with openpyxl?","\nCreate an ErrorBars object and assign it to the series' errBars attribute. For values from cells use errValType=\"cust\" and pass a ",[32,2187,2188],{},"NumDataSource"," wrapping a ",[32,2191,2192],{},"NumRef"," to the cell range as both plus and minus.",[10,2195,2196,2199],{},[2117,2197,2198],{},"How do I make asymmetric error bars?","\nPoint plus and minus at two different cell ranges, for example max minus mean and mean minus min. Excel draws each side with its own length.",[10,2201,2202,2205],{},[2117,2203,2204],{},"How do I calculate a 95% confidence interval in pandas?","\nGroup the raw values, compute count, mean and standard deviation, and set the margin to scipy.stats.t.ppf(0.975, n - 1) × sd \u002F √n. The interval is the mean plus or minus the margin.",[10,2207,2208,2211],{},[2117,2209,2210],{},"What do Excel's standard error and standard deviation error bars show?","\nThey are computed from the plotted values themselves, not from the data behind each point, so they rarely match what a report means. Calculate the intervals from the raw data and use custom error bars instead.",[20,2213,2215],{"id":2214},"related","Related",[2111,2217,2218,2224,2231,2238],{},[2114,2219,2220,2221,2223],{},"Up one level: ",[14,2222,17],{"href":16}," — every chart type openpyxl supports.",[2114,2225,2226,2230],{},[14,2227,2229],{"href":2228},"\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 chart these bars sit on.",[2114,2232,2233,2237],{},[14,2234,2236],{"href":2235},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-a-trendline-to-an-excel-chart-with-python\u002F","Add a Trendline to an Excel Chart with Python"," — another analytical overlay on a series.",[2114,2239,2240,2244],{},[14,2241,2243],{"href":2242},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fdetect-outliers-in-excel-data-with-pandas\u002F","Detect Outliers in Excel Data with pandas"," — when a wide range means bad data rather than real spread.",[2246,2247,2248],"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 .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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":30,"searchDepth":237,"depth":237,"links":2250},[2251,2252,2253,2254,2255,2256,2257,2258,2259,2260],{"id":22,"depth":237,"text":23},{"id":202,"depth":237,"text":203},{"id":493,"depth":237,"text":494},{"id":1007,"depth":237,"text":1008},{"id":1185,"depth":237,"text":1186},{"id":1983,"depth":237,"text":1984},{"id":2108,"depth":237,"text":2109},{"id":2156,"depth":237,"text":2157},{"id":2178,"depth":237,"text":2179},{"id":2214,"depth":237,"text":2215},"2026-09-29","Show uncertainty and ranges on Excel charts written from Python: compute confidence intervals from raw data with pandas and SciPy, attach custom error bars that read their lengths from cells, draw asymmetric min\u002Fmax ranges, use fixed and percentage error bars, style the bars, and check the result by rendering the workbook.","md",[2265,2267,2269,2271],{"q":2184,"a":2266},"Create an ErrorBars object and assign it to the series' errBars attribute. For values from cells use errValType=\"cust\" and pass a NumDataSource wrapping a NumRef to the cell range as both plus and minus.",{"q":2198,"a":2268},"Point plus and minus at two different cell ranges, for example max minus mean and mean minus min. Excel draws each side with its own length.",{"q":2204,"a":2270},"Group the raw values, compute count, mean and standard deviation, and set the margin to scipy.stats.t.ppf(0.975, n - 1) × sd \u002F √n. The interval is the mean plus or minus the margin.",{"q":2210,"a":2272},"They are computed from the plotted values themselves, not from the data behind each point, so they rarely match what a report means. Calculate the intervals from the raw data and use custom error bars instead.","add error bars to an excel chart with openpyxl",null,{"breadcrumb":2276},[2277,2279,2282],{"name":2278,"item":407},"Home",{"name":2280,"item":2281},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002F",{"name":17,"item":16},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-error-bars-to-an-excel-chart-with-openpyxl",{"title":2285,"description":2286},"Excel Chart Error Bars from Cells with openpyxl","Add error bars to Excel column and line charts with openpyxl ErrorBars: custom plus\u002Fminus values from cell ranges, 95% confidence intervals from pandas and scipy.stats.t, asymmetric min–max ranges, fixed and percentage bars, colours and end caps.","add-error-bars-to-an-excel-chart-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-error-bars-to-an-excel-chart-with-openpyxl\u002Findex",[2290,2291],"charts-and-images","openpyxl","how-to","wn_DuJGSNJUXeihaI1Bxvmp8aecPaK3j1sDwufnOXn4",[2295,2298],{"title":2236,"path":2296,"stem":2297,"children":-1},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-a-trendline-to-an-excel-chart-with-python","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-a-trendline-to-an-excel-chart-with-python\u002Findex",{"title":2299,"path":2300,"stem":2301,"children":-1},"Add a Line Chart to an Excel Report with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-line-chart-to-excel-report-with-python","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-line-chart-to-excel-report-with-python\u002Findex",[2303,2305],{"path":2304,"title":17},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl",{"path":2306,"title":2280},"\u002Fformatting-and-charting-excel-reports-with-python",[],[2309,2312,2315,2318,2321,2324],{"path":2310,"title":2311,"kind":2292},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fbuild-a-dashboard-sheet-with-charts-from-multiple-tabs","Build a Dashboard Sheet with Charts from Multiple Tabs",{"path":2313,"title":2314,"kind":2292},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fbuild-a-rolling-12-month-chart-on-an-excel-dashboard","Build a Rolling 12-Month Chart on an Excel Dashboard",{"path":2316,"title":2317,"kind":2292},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fkeep-charts-and-images-when-filling-an-excel-template","Keep Charts and Images When Filling an Excel Template",{"path":2319,"title":2320,"kind":2292},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fadd-a-chart-to-an-excel-file-with-xlsxwriter","Add a Chart to an Excel File with xlsxwriter",{"path":2322,"title":2323,"kind":2292},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fadd-sparklines-to-an-excel-report-with-xlsxwriter","Add Sparklines to an Excel Report with xlsxwriter",{"path":2325,"title":2326,"kind":2292},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fbuild-a-waterfall-chart-in-excel-with-python","Build a Waterfall Chart in Excel with Python",1790972108885]