[{"data":1,"prerenderedAt":1996},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-dumbbell-chart-in-excel-with-python":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-dumbbell-chart-in-excel-with-python":1962,"ancestors:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-dumbbell-chart-in-excel-with-python":1971,"children:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-dumbbell-chart-in-excel-with-python":1976,"related:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-dumbbell-chart-in-excel-with-python":1977},{"id":4,"title":5,"body":6,"dateModified":1928,"datePublished":1928,"description":1929,"extension":1930,"faq":1931,"keyword":1940,"kind":1941,"meta":1942,"navigation":241,"path":1951,"seo":1952,"slug":1955,"stem":1956,"topics":1957,"type":1960,"__hash__":1961},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-dumbbell-chart-in-excel-with-python\u002Findex.md","Create a Dumbbell Chart in Excel with Python",{"type":7,"value":8,"toc":1915},"minimark",[9,24,29,58,61,65,81,195,199,647,658,662,1127,1144,1147,1151,1343,1369,1373,1446,1449,1688,1692,1703,1777,1781,1832,1836,1839,1843,1849,1862,1872,1878,1882,1911],[10,11,12,13,17,18,23],"p",{},"When a report compares two values for each of several categories — this year against last year, before and after a change, target against actual — a clustered bar chart is the usual choice, and it is often a poor one. The eye has to compare the heights of pairs of bars and work out which pairs moved the most. A dumbbell chart shows the same data as two dots on a line for each category, joined by a bar: the length of the bar ",[14,15,16],"em",{},"is"," the change, its direction is obvious from which dot is on top, and categories that hardly moved shrink to two overlapping dots. Excel has no dumbbell chart type, but it can draw one from parts it does have, and openpyxl can write those parts. This page builds a native, editable dumbbell chart from a small table of satisfaction scores, sorts it by the size of the change, and renders it to check the result. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002F","Creating Charts in Excel with openpyxl",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas\n","bash","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,55],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," openpyxl",[40,56,57],{"class":50}," pandas\n",[10,59,60],{},"LibreOffice is used at the end to render the chart to an image; it is optional.",[25,62,64],{"id":63},"how-the-chart-is-built","How the chart is built",[10,66,67,68,72,73,76,77,80],{},"The trick is to make Excel draw three things it already knows how to draw. A ",[69,70,71],"strong",{},"line chart"," with two series — the earlier and the later value — gives the dots, once the lines are switched off and markers switched on. The ",[69,74,75],{},"connector"," between each pair of dots comes from ",[14,78,79],{},"error bars",": a third, invisible series sits at the lower of the two values, and a custom error bar extends upwards from it by the gap between them. Because both values are plotted on the same category axis, the error bar runs exactly from one dot to the other. Everything stays a normal chart that people can restyle in Excel, and it updates when the numbers in the sheet change.",[82,83,91,92,91,96,91,100,91,108,91,118,91,125,91,130,91,134,91,139,91,144,91,149,91,153,91,157,91,160,91,165,91,170,91,174,91,178,91,181,91,186,91,191],"svg",{"viewBox":84,"role":85,"ariaLabelledBy":86,"xmlns":89,"style":90},"84 0 592 376","img",[87,88],"db-parts-t","db-parts-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:592px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[93,94,95],"title",{"id":87},"Three layers make a dumbbell",[97,98,99],"desc",{"id":88},"An invisible helper series sits at the lower value of each pair and carries a custom error bar the length of the gap, which draws the connector. Two marker-only series draw the earlier value in grey and the later value in blue on top.",[101,102],"rect",{"x":103,"y":104,"width":105,"height":106,"fill":107},"84","0","592","376","#ffffff",[101,109],{"x":110,"y":111,"width":112,"height":113,"rx":114,"fill":115,"stroke":116,"style":117},"100.0","16.0","560.0","59.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[119,120,124],"text",{"x":121,"y":122,"style":123},"380.0","41.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Later value",[119,126,129],{"x":121,"y":127,"style":128},"61.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","blue markers, no line",[42,131],{"x1":121,"y1":132,"x2":121,"y2":133,"stroke":116,"style":117},"80.0","102.0",[135,136],"polygon",{"points":137,"fill":138},"380.0,106.0 375.0,97.0 385.0,97.0","#5b5cf0",[101,140],{"x":110,"y":141,"width":112,"height":113,"rx":114,"fill":142,"stroke":143,"style":117},"111.0","#e7ebef","var(--line,#cdd5e6)",[119,145,148],{"x":121,"y":146,"style":147},"136.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Earlier value",[119,150,152],{"x":121,"y":151,"style":128},"156.0","grey markers, no line",[42,154],{"x1":121,"y1":155,"x2":121,"y2":156,"stroke":116,"style":117},"175.0","197.0",[135,158],{"points":159,"fill":138},"380.0,201.0 375.0,192.0 385.0,192.0",[101,161],{"x":110,"y":162,"width":112,"height":113,"rx":114,"fill":163,"stroke":164,"style":117},"206.0","#fdefd8","var(--gold,#b4740a)",[119,166,169],{"x":121,"y":167,"style":168},"231.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Helper at the lower value",[119,171,173],{"x":121,"y":172,"style":128},"251.0","invisible",[42,175],{"x1":121,"y1":176,"x2":121,"y2":177,"stroke":116,"style":117},"270.0","292.0",[135,179],{"points":180,"fill":138},"380.0,296.0 375.0,287.0 385.0,287.0",[101,182],{"x":110,"y":183,"width":112,"height":113,"rx":114,"fill":184,"stroke":185,"style":117},"301.0","#d9f4f1","var(--teal,#0f9488)",[119,187,190],{"x":121,"y":188,"style":189},"326.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Error bar of the gap",[119,192,194],{"x":121,"y":193,"style":128},"346.0","draws the connector",[25,196,198],{"id":197},"step-1-the-data-sorted-by-change-with-two-helper-columns","Step 1 — The data, sorted by change, with two helper columns",[30,200,204],{"className":201,"code":202,"language":203,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\nfrom openpyxl import Workbook\n\nscores = pd.DataFrame({\n    \"Region\": [\"North\", \"South\", \"East\", \"West\", \"Export\", \"Online\"],\n    \"2025\": [62, 58, 44, 70, 35, 66],\n    \"2026\": [71, 54, 57, 73, 49, 64],\n})\nscores = scores.assign(change=scores[\"2026\"] - scores[\"2025\"]).sort_values(\"change\", ascending=False)\n\nwb = Workbook()\nws = wb.active\nws.title = \"Satisfaction\"\nws.append([\"Region\", \"2025\", \"2026\", \"Low\", \"Gap\"])\nfor i, row in enumerate(scores.itertuples(index=False), start=2):\n    ws.append([row.Region, row._1, row._2, f\"=MIN(B{i}:C{i})\", f\"=ABS(C{i}-B{i})\"])\nn = len(scores) + 1\nprint(scores.drop(columns=\"change\").to_string(index=False))\n","python",[37,205,206,222,236,243,255,296,335,373,379,432,437,448,459,470,500,539,599,619],{"__ignoreMap":35},[40,207,208,212,216,219],{"class":42,"line":43},[40,209,211],{"class":210},"s-kum","import",[40,213,215],{"class":214},"skGVy"," pandas ",[40,217,218],{"class":210},"as",[40,220,221],{"class":214}," pd\n",[40,223,225,228,231,233],{"class":42,"line":224},2,[40,226,227],{"class":210},"from",[40,229,230],{"class":214}," openpyxl ",[40,232,211],{"class":210},[40,234,235],{"class":214}," Workbook\n",[40,237,239],{"class":42,"line":238},3,[40,240,242],{"emptyLinePlaceholder":241},true,"\n",[40,244,246,249,252],{"class":42,"line":245},4,[40,247,248],{"class":214},"scores ",[40,250,251],{"class":210},"=",[40,253,254],{"class":214}," pd.DataFrame({\n",[40,256,258,261,264,267,270,273,275,278,280,283,285,288,290,293],{"class":42,"line":257},5,[40,259,260],{"class":50},"    \"Region\"",[40,262,263],{"class":214},": [",[40,265,266],{"class":50},"\"North\"",[40,268,269],{"class":214},", ",[40,271,272],{"class":50},"\"South\"",[40,274,269],{"class":214},[40,276,277],{"class":50},"\"East\"",[40,279,269],{"class":214},[40,281,282],{"class":50},"\"West\"",[40,284,269],{"class":214},[40,286,287],{"class":50},"\"Export\"",[40,289,269],{"class":214},[40,291,292],{"class":50},"\"Online\"",[40,294,295],{"class":214},"],\n",[40,297,299,302,304,308,310,313,315,318,320,323,325,328,330,333],{"class":42,"line":298},6,[40,300,301],{"class":50},"    \"2025\"",[40,303,263],{"class":214},[40,305,307],{"class":306},"sP0c6","62",[40,309,269],{"class":214},[40,311,312],{"class":306},"58",[40,314,269],{"class":214},[40,316,317],{"class":306},"44",[40,319,269],{"class":214},[40,321,322],{"class":306},"70",[40,324,269],{"class":214},[40,326,327],{"class":306},"35",[40,329,269],{"class":214},[40,331,332],{"class":306},"66",[40,334,295],{"class":214},[40,336,338,341,343,346,348,351,353,356,358,361,363,366,368,371],{"class":42,"line":337},7,[40,339,340],{"class":50},"    \"2026\"",[40,342,263],{"class":214},[40,344,345],{"class":306},"71",[40,347,269],{"class":214},[40,349,350],{"class":306},"54",[40,352,269],{"class":214},[40,354,355],{"class":306},"57",[40,357,269],{"class":214},[40,359,360],{"class":306},"73",[40,362,269],{"class":214},[40,364,365],{"class":306},"49",[40,367,269],{"class":214},[40,369,370],{"class":306},"64",[40,372,295],{"class":214},[40,374,376],{"class":42,"line":375},8,[40,377,378],{"class":214},"})\n",[40,380,382,384,386,389,393,395,398,401,404,407,410,413,416,419,421,424,426,429],{"class":42,"line":381},9,[40,383,248],{"class":214},[40,385,251],{"class":210},[40,387,388],{"class":214}," scores.assign(",[40,390,392],{"class":391},"sa561","change",[40,394,251],{"class":210},[40,396,397],{"class":214},"scores[",[40,399,400],{"class":50},"\"2026\"",[40,402,403],{"class":214},"] ",[40,405,406],{"class":210},"-",[40,408,409],{"class":214}," scores[",[40,411,412],{"class":50},"\"2025\"",[40,414,415],{"class":214},"]).sort_values(",[40,417,418],{"class":50},"\"change\"",[40,420,269],{"class":214},[40,422,423],{"class":391},"ascending",[40,425,251],{"class":210},[40,427,428],{"class":306},"False",[40,430,431],{"class":214},")\n",[40,433,435],{"class":42,"line":434},10,[40,436,242],{"emptyLinePlaceholder":241},[40,438,440,443,445],{"class":42,"line":439},11,[40,441,442],{"class":214},"wb ",[40,444,251],{"class":210},[40,446,447],{"class":214}," Workbook()\n",[40,449,451,454,456],{"class":42,"line":450},12,[40,452,453],{"class":214},"ws ",[40,455,251],{"class":210},[40,457,458],{"class":214}," wb.active\n",[40,460,462,465,467],{"class":42,"line":461},13,[40,463,464],{"class":214},"ws.title ",[40,466,251],{"class":210},[40,468,469],{"class":50}," \"Satisfaction\"\n",[40,471,473,476,479,481,483,485,487,489,492,494,497],{"class":42,"line":472},14,[40,474,475],{"class":214},"ws.append([",[40,477,478],{"class":50},"\"Region\"",[40,480,269],{"class":214},[40,482,412],{"class":50},[40,484,269],{"class":214},[40,486,400],{"class":50},[40,488,269],{"class":214},[40,490,491],{"class":50},"\"Low\"",[40,493,269],{"class":214},[40,495,496],{"class":50},"\"Gap\"",[40,498,499],{"class":214},"])\n",[40,501,503,506,509,512,515,518,521,523,525,528,531,533,536],{"class":42,"line":502},15,[40,504,505],{"class":210},"for",[40,507,508],{"class":214}," i, row ",[40,510,511],{"class":210},"in",[40,513,514],{"class":306}," enumerate",[40,516,517],{"class":214},"(scores.itertuples(",[40,519,520],{"class":391},"index",[40,522,251],{"class":210},[40,524,428],{"class":306},[40,526,527],{"class":214},"), ",[40,529,530],{"class":391},"start",[40,532,251],{"class":210},[40,534,535],{"class":306},"2",[40,537,538],{"class":214},"):\n",[40,540,542,545,548,551,555,558,561,564,566,568,570,573,575,577,580,582,584,586,589,591,593,595,597],{"class":42,"line":541},16,[40,543,544],{"class":214},"    ws.append([row.Region, row._1, row._2, ",[40,546,547],{"class":210},"f",[40,549,550],{"class":50},"\"=MIN(B",[40,552,554],{"class":553},"sSjpA","{",[40,556,557],{"class":214},"i",[40,559,560],{"class":553},"}",[40,562,563],{"class":50},":C",[40,565,554],{"class":553},[40,567,557],{"class":214},[40,569,560],{"class":553},[40,571,572],{"class":50},")\"",[40,574,269],{"class":214},[40,576,547],{"class":210},[40,578,579],{"class":50},"\"=ABS(C",[40,581,554],{"class":553},[40,583,557],{"class":214},[40,585,560],{"class":553},[40,587,588],{"class":50},"-B",[40,590,554],{"class":553},[40,592,557],{"class":214},[40,594,560],{"class":553},[40,596,572],{"class":50},[40,598,499],{"class":214},[40,600,602,605,607,610,613,616],{"class":42,"line":601},17,[40,603,604],{"class":214},"n ",[40,606,251],{"class":210},[40,608,609],{"class":306}," len",[40,611,612],{"class":214},"(scores) ",[40,614,615],{"class":210},"+",[40,617,618],{"class":306}," 1\n",[40,620,622,625,628,631,633,635,638,640,642,644],{"class":42,"line":621},18,[40,623,624],{"class":306},"print",[40,626,627],{"class":214},"(scores.drop(",[40,629,630],{"class":391},"columns",[40,632,251],{"class":210},[40,634,418],{"class":50},[40,636,637],{"class":214},").to_string(",[40,639,520],{"class":391},[40,641,251],{"class":210},[40,643,428],{"class":306},[40,645,646],{"class":214},"))\n",[10,648,649,650,653,654,657],{},"The table is sorted by the change from one year to the next, largest improvement first, so the chart reads from the best story to the worst — the order in which a reader wants to discuss it. The two helper columns are formulas, not values computed in Python: ",[14,651,652],{},"Low"," is the smaller of the two scores and ",[14,655,656],{},"Gap"," the distance between them. Keeping them as formulas means that if someone corrects a score in the sheet, the connector moves with it. The helper columns can be hidden or placed out of the way; the chart needs them, the reader does not.",[25,659,661],{"id":660},"step-2-the-marker-series-and-the-connector","Step 2 — The marker series and the connector",[30,663,665],{"className":201,"code":664,"language":203,"meta":35,"style":35},"from openpyxl.chart import LineChart, Reference\nfrom openpyxl.chart.data_source import NumDataSource, NumRef\nfrom openpyxl.chart.error_bar import ErrorBars\nfrom openpyxl.chart.legend import LegendEntry\nfrom openpyxl.chart.shapes import GraphicalProperties\nfrom openpyxl.drawing.line import LineProperties\n\nchart = LineChart()\nchart.add_data(Reference(ws, min_col=4, min_row=1, max_row=n), titles_from_data=True)        # Low (helper)\nchart.add_data(Reference(ws, min_col=2, max_col=3, min_row=1, max_row=n), titles_from_data=True)\nchart.set_categories(Reference(ws, min_col=1, min_row=2, max_row=n))\n\nhelper, earlier, later = chart.series\nhelper.graphicalProperties.line.noFill = True\nhelper.marker.symbol = \"none\"\nhelper.errBars = ErrorBars(errBarType=\"plus\", errValType=\"cust\", noEndCap=True,\n                           plus=NumDataSource(numRef=NumRef(f=f\"'{ws.title}'!$E$2:$E${n}\")),\n                           spPr=GraphicalProperties(ln=LineProperties(solidFill=\"BDBDBD\", w=50800)))\n\nfor series, colour in [(earlier, \"A5A5A5\"), (later, \"1F5FA8\")]:\n    series.graphicalProperties.line.noFill = True\n    series.marker.symbol = \"circle\"\n    series.marker.size = 11\n    series.marker.graphicalProperties = GraphicalProperties(solidFill=colour)\n    series.marker.graphicalProperties.line.solidFill = colour\n",[37,666,667,679,691,703,715,727,739,743,753,801,845,873,877,887,897,907,947,997,1036,1041,1066,1076,1087,1098,1116],{"__ignoreMap":35},[40,668,669,671,674,676],{"class":42,"line":43},[40,670,227],{"class":210},[40,672,673],{"class":214}," openpyxl.chart ",[40,675,211],{"class":210},[40,677,678],{"class":214}," LineChart, Reference\n",[40,680,681,683,686,688],{"class":42,"line":224},[40,682,227],{"class":210},[40,684,685],{"class":214}," openpyxl.chart.data_source ",[40,687,211],{"class":210},[40,689,690],{"class":214}," NumDataSource, NumRef\n",[40,692,693,695,698,700],{"class":42,"line":238},[40,694,227],{"class":210},[40,696,697],{"class":214}," openpyxl.chart.error_bar ",[40,699,211],{"class":210},[40,701,702],{"class":214}," ErrorBars\n",[40,704,705,707,710,712],{"class":42,"line":245},[40,706,227],{"class":210},[40,708,709],{"class":214}," openpyxl.chart.legend ",[40,711,211],{"class":210},[40,713,714],{"class":214}," LegendEntry\n",[40,716,717,719,722,724],{"class":42,"line":257},[40,718,227],{"class":210},[40,720,721],{"class":214}," openpyxl.chart.shapes ",[40,723,211],{"class":210},[40,725,726],{"class":214}," GraphicalProperties\n",[40,728,729,731,734,736],{"class":42,"line":298},[40,730,227],{"class":210},[40,732,733],{"class":214}," openpyxl.drawing.line ",[40,735,211],{"class":210},[40,737,738],{"class":214}," LineProperties\n",[40,740,741],{"class":42,"line":337},[40,742,242],{"emptyLinePlaceholder":241},[40,744,745,748,750],{"class":42,"line":375},[40,746,747],{"class":214},"chart ",[40,749,251],{"class":210},[40,751,752],{"class":214}," LineChart()\n",[40,754,755,758,761,763,766,768,771,773,776,778,781,783,786,789,791,794,797],{"class":42,"line":381},[40,756,757],{"class":214},"chart.add_data(Reference(ws, ",[40,759,760],{"class":391},"min_col",[40,762,251],{"class":210},[40,764,765],{"class":306},"4",[40,767,269],{"class":214},[40,769,770],{"class":391},"min_row",[40,772,251],{"class":210},[40,774,775],{"class":306},"1",[40,777,269],{"class":214},[40,779,780],{"class":391},"max_row",[40,782,251],{"class":210},[40,784,785],{"class":214},"n), ",[40,787,788],{"class":391},"titles_from_data",[40,790,251],{"class":210},[40,792,793],{"class":306},"True",[40,795,796],{"class":214},")        ",[40,798,800],{"class":799},"s-wDw","# Low (helper)\n",[40,802,803,805,807,809,811,813,816,818,821,823,825,827,829,831,833,835,837,839,841,843],{"class":42,"line":434},[40,804,757],{"class":214},[40,806,760],{"class":391},[40,808,251],{"class":210},[40,810,535],{"class":306},[40,812,269],{"class":214},[40,814,815],{"class":391},"max_col",[40,817,251],{"class":210},[40,819,820],{"class":306},"3",[40,822,269],{"class":214},[40,824,770],{"class":391},[40,826,251],{"class":210},[40,828,775],{"class":306},[40,830,269],{"class":214},[40,832,780],{"class":391},[40,834,251],{"class":210},[40,836,785],{"class":214},[40,838,788],{"class":391},[40,840,251],{"class":210},[40,842,793],{"class":306},[40,844,431],{"class":214},[40,846,847,850,852,854,856,858,860,862,864,866,868,870],{"class":42,"line":439},[40,848,849],{"class":214},"chart.set_categories(Reference(ws, ",[40,851,760],{"class":391},[40,853,251],{"class":210},[40,855,775],{"class":306},[40,857,269],{"class":214},[40,859,770],{"class":391},[40,861,251],{"class":210},[40,863,535],{"class":306},[40,865,269],{"class":214},[40,867,780],{"class":391},[40,869,251],{"class":210},[40,871,872],{"class":214},"n))\n",[40,874,875],{"class":42,"line":450},[40,876,242],{"emptyLinePlaceholder":241},[40,878,879,882,884],{"class":42,"line":461},[40,880,881],{"class":214},"helper, earlier, later ",[40,883,251],{"class":210},[40,885,886],{"class":214}," chart.series\n",[40,888,889,892,894],{"class":42,"line":472},[40,890,891],{"class":214},"helper.graphicalProperties.line.noFill ",[40,893,251],{"class":210},[40,895,896],{"class":306}," True\n",[40,898,899,902,904],{"class":42,"line":502},[40,900,901],{"class":214},"helper.marker.symbol ",[40,903,251],{"class":210},[40,905,906],{"class":50}," \"none\"\n",[40,908,909,912,914,917,920,922,925,927,930,932,935,937,940,942,944],{"class":42,"line":541},[40,910,911],{"class":214},"helper.errBars ",[40,913,251],{"class":210},[40,915,916],{"class":214}," ErrorBars(",[40,918,919],{"class":391},"errBarType",[40,921,251],{"class":210},[40,923,924],{"class":50},"\"plus\"",[40,926,269],{"class":214},[40,928,929],{"class":391},"errValType",[40,931,251],{"class":210},[40,933,934],{"class":50},"\"cust\"",[40,936,269],{"class":214},[40,938,939],{"class":391},"noEndCap",[40,941,251],{"class":210},[40,943,793],{"class":306},[40,945,946],{"class":214},",\n",[40,948,949,952,954,957,960,962,965,967,969,971,974,976,979,981,984,986,989,991,994],{"class":42,"line":601},[40,950,951],{"class":391},"                           plus",[40,953,251],{"class":210},[40,955,956],{"class":214},"NumDataSource(",[40,958,959],{"class":391},"numRef",[40,961,251],{"class":210},[40,963,964],{"class":214},"NumRef(",[40,966,547],{"class":391},[40,968,251],{"class":210},[40,970,547],{"class":210},[40,972,973],{"class":50},"\"'",[40,975,554],{"class":553},[40,977,978],{"class":214},"ws.title",[40,980,560],{"class":553},[40,982,983],{"class":50},"'!$E$2:$E$",[40,985,554],{"class":553},[40,987,988],{"class":214},"n",[40,990,560],{"class":553},[40,992,993],{"class":50},"\"",[40,995,996],{"class":214},")),\n",[40,998,999,1002,1004,1007,1010,1012,1015,1018,1020,1023,1025,1028,1030,1033],{"class":42,"line":621},[40,1000,1001],{"class":391},"                           spPr",[40,1003,251],{"class":210},[40,1005,1006],{"class":214},"GraphicalProperties(",[40,1008,1009],{"class":391},"ln",[40,1011,251],{"class":210},[40,1013,1014],{"class":214},"LineProperties(",[40,1016,1017],{"class":391},"solidFill",[40,1019,251],{"class":210},[40,1021,1022],{"class":50},"\"BDBDBD\"",[40,1024,269],{"class":214},[40,1026,1027],{"class":391},"w",[40,1029,251],{"class":210},[40,1031,1032],{"class":306},"50800",[40,1034,1035],{"class":214},")))\n",[40,1037,1039],{"class":42,"line":1038},19,[40,1040,242],{"emptyLinePlaceholder":241},[40,1042,1044,1046,1049,1051,1054,1057,1060,1063],{"class":42,"line":1043},20,[40,1045,505],{"class":210},[40,1047,1048],{"class":214}," series, colour ",[40,1050,511],{"class":210},[40,1052,1053],{"class":214}," [(earlier, ",[40,1055,1056],{"class":50},"\"A5A5A5\"",[40,1058,1059],{"class":214},"), (later, ",[40,1061,1062],{"class":50},"\"1F5FA8\"",[40,1064,1065],{"class":214},")]:\n",[40,1067,1069,1072,1074],{"class":42,"line":1068},21,[40,1070,1071],{"class":214},"    series.graphicalProperties.line.noFill ",[40,1073,251],{"class":210},[40,1075,896],{"class":306},[40,1077,1079,1082,1084],{"class":42,"line":1078},22,[40,1080,1081],{"class":214},"    series.marker.symbol ",[40,1083,251],{"class":210},[40,1085,1086],{"class":50}," \"circle\"\n",[40,1088,1090,1093,1095],{"class":42,"line":1089},23,[40,1091,1092],{"class":214},"    series.marker.size ",[40,1094,251],{"class":210},[40,1096,1097],{"class":306}," 11\n",[40,1099,1101,1104,1106,1109,1111,1113],{"class":42,"line":1100},24,[40,1102,1103],{"class":214},"    series.marker.graphicalProperties ",[40,1105,251],{"class":210},[40,1107,1108],{"class":214}," GraphicalProperties(",[40,1110,1017],{"class":391},[40,1112,251],{"class":210},[40,1114,1115],{"class":214},"colour)\n",[40,1117,1119,1122,1124],{"class":42,"line":1118},25,[40,1120,1121],{"class":214},"    series.marker.graphicalProperties.line.solidFill ",[40,1123,251],{"class":210},[40,1125,1126],{"class":214}," colour\n",[10,1128,1129,1130,1133,1134,1136,1137,1140,1141,1143],{},"The helper series is added first so it is drawn underneath, and its line and markers are both switched off — only its error bars are visible. The error bars are ",[14,1131,1132],{},"custom",": instead of a fixed amount or a percentage, each point's bar length comes from the ",[14,1135,656],{}," column through a cell reference, so every category gets its own connector. ",[37,1138,1139],{},"errBarType=\"plus\""," draws the bar upwards only, from the lower dot to the higher one, and ",[37,1142,939],{}," removes the little crossbar Excel puts at the end of error bars by default. A wide grey line (50,800 EMUs is 4 points) reads as a bar rather than a whisker.",[10,1145,1146],{},"The two score series keep their markers and lose their lines. A muted grey for the earlier year and a strong colour for the later one puts the visual weight on the current figure, which is usually the one being discussed.",[25,1148,1150],{"id":1149},"step-3-axes-legend-and-placement","Step 3 — Axes, legend and placement",[30,1152,1154],{"className":201,"code":1153,"language":203,"meta":35,"style":35},"chart.title = \"Customer satisfaction by region, 2025 → 2026\"\nchart.y_axis.title = \"Score\"\nchart.y_axis.scaling.min, chart.y_axis.scaling.max = 30, 80\nchart.y_axis.majorUnit = 10\nchart.x_axis.delete = chart.y_axis.delete = False\nchart.legend.position = \"b\"\nchart.legend.legendEntry = [LegendEntry(idx=0, delete=True)]       # hide \"Low\" from the legend\nchart.width, chart.height = 18, 9\nws.add_chart(chart, \"A10\")\nws.column_dimensions[\"D\"].hidden = ws.column_dimensions[\"E\"].hidden = True\nchart.visible_cells_only = False                                  # plot the hidden helper columns\nwb.save(\"satisfaction_dumbbell.xlsx\")\nprint(\"saved\")\n",[37,1155,1156,1166,1176,1191,1201,1216,1226,1258,1273,1283,1308,1321,1331],{"__ignoreMap":35},[40,1157,1158,1161,1163],{"class":42,"line":43},[40,1159,1160],{"class":214},"chart.title ",[40,1162,251],{"class":210},[40,1164,1165],{"class":50}," \"Customer satisfaction by region, 2025 → 2026\"\n",[40,1167,1168,1171,1173],{"class":42,"line":224},[40,1169,1170],{"class":214},"chart.y_axis.title ",[40,1172,251],{"class":210},[40,1174,1175],{"class":50}," \"Score\"\n",[40,1177,1178,1181,1183,1186,1188],{"class":42,"line":238},[40,1179,1180],{"class":214},"chart.y_axis.scaling.min, chart.y_axis.scaling.max ",[40,1182,251],{"class":210},[40,1184,1185],{"class":306}," 30",[40,1187,269],{"class":214},[40,1189,1190],{"class":306},"80\n",[40,1192,1193,1196,1198],{"class":42,"line":245},[40,1194,1195],{"class":214},"chart.y_axis.majorUnit ",[40,1197,251],{"class":210},[40,1199,1200],{"class":306}," 10\n",[40,1202,1203,1206,1208,1211,1213],{"class":42,"line":257},[40,1204,1205],{"class":214},"chart.x_axis.delete ",[40,1207,251],{"class":210},[40,1209,1210],{"class":214}," chart.y_axis.delete ",[40,1212,251],{"class":210},[40,1214,1215],{"class":306}," False\n",[40,1217,1218,1221,1223],{"class":42,"line":298},[40,1219,1220],{"class":214},"chart.legend.position ",[40,1222,251],{"class":210},[40,1224,1225],{"class":50}," \"b\"\n",[40,1227,1228,1231,1233,1236,1239,1241,1243,1245,1248,1250,1252,1255],{"class":42,"line":337},[40,1229,1230],{"class":214},"chart.legend.legendEntry ",[40,1232,251],{"class":210},[40,1234,1235],{"class":214}," [LegendEntry(",[40,1237,1238],{"class":391},"idx",[40,1240,251],{"class":210},[40,1242,104],{"class":306},[40,1244,269],{"class":214},[40,1246,1247],{"class":391},"delete",[40,1249,251],{"class":210},[40,1251,793],{"class":306},[40,1253,1254],{"class":214},")]       ",[40,1256,1257],{"class":799},"# hide \"Low\" from the legend\n",[40,1259,1260,1263,1265,1268,1270],{"class":42,"line":375},[40,1261,1262],{"class":214},"chart.width, chart.height ",[40,1264,251],{"class":210},[40,1266,1267],{"class":306}," 18",[40,1269,269],{"class":214},[40,1271,1272],{"class":306},"9\n",[40,1274,1275,1278,1281],{"class":42,"line":381},[40,1276,1277],{"class":214},"ws.add_chart(chart, ",[40,1279,1280],{"class":50},"\"A10\"",[40,1282,431],{"class":214},[40,1284,1285,1288,1291,1294,1296,1299,1302,1304,1306],{"class":42,"line":434},[40,1286,1287],{"class":214},"ws.column_dimensions[",[40,1289,1290],{"class":50},"\"D\"",[40,1292,1293],{"class":214},"].hidden ",[40,1295,251],{"class":210},[40,1297,1298],{"class":214}," ws.column_dimensions[",[40,1300,1301],{"class":50},"\"E\"",[40,1303,1293],{"class":214},[40,1305,251],{"class":210},[40,1307,896],{"class":306},[40,1309,1310,1313,1315,1318],{"class":42,"line":439},[40,1311,1312],{"class":214},"chart.visible_cells_only ",[40,1314,251],{"class":210},[40,1316,1317],{"class":306}," False",[40,1319,1320],{"class":799},"                                  # plot the hidden helper columns\n",[40,1322,1323,1326,1329],{"class":42,"line":450},[40,1324,1325],{"class":214},"wb.save(",[40,1327,1328],{"class":50},"\"satisfaction_dumbbell.xlsx\"",[40,1330,431],{"class":214},[40,1332,1333,1335,1338,1341],{"class":42,"line":461},[40,1334,624],{"class":306},[40,1336,1337],{"class":214},"(",[40,1339,1340],{"class":50},"\"saved\"",[40,1342,431],{"class":214},[10,1344,1345,1346,1348,1349,1352,1353,1356,1357,1360,1361,1364,1365,1368],{},"The vertical axis starts at 30 rather than zero. That is acceptable here, unlike for a bar chart: a dumbbell's message is in the distance between dots, not in the length from the axis, so a cropped axis makes the changes easier to see without misleading. The helper series would otherwise appear in the legend as ",[14,1347,652],{},"; a ",[37,1350,1351],{},"LegendEntry"," with ",[37,1354,1355],{},"delete=True"," removes it by its index. Both axes are explicitly kept, because openpyxl writes charts whose axes Excel hides unless told otherwise. Finally the helper columns are hidden, and that needs one more setting. By default openpyxl marks a chart to plot ",[14,1358,1359],{},"visible cells only",", which is also Excel's default, so hiding the helper columns would silently remove the connectors. Setting ",[37,1362,1363],{},"visible_cells_only = False"," is the code equivalent of ticking ",[14,1366,1367],{},"Show data in hidden rows and columns"," in Excel's chart settings. The first rendering of this chart, made before that line was added, showed the dots without a single connector — a good example of why a quick render is worth doing.",[25,1370,1372],{"id":1371},"step-4-render-it-to-check","Step 4 — Render it to check",[30,1374,1376],{"className":32,"code":1375,"language":34,"meta":35,"style":35},"soffice --headless --convert-to pdf satisfaction_dumbbell.xlsx > \u002Fdev\u002Fnull 2>&1\npdftoppm -r 70 -f 1 -l 1 -png satisfaction_dumbbell.pdf preview && ls preview*\n",[37,1377,1378,1404],{"__ignoreMap":35},[40,1379,1380,1383,1386,1389,1392,1395,1398,1401],{"class":42,"line":43},[40,1381,1382],{"class":46},"soffice",[40,1384,1385],{"class":306}," --headless",[40,1387,1388],{"class":306}," --convert-to",[40,1390,1391],{"class":50}," pdf",[40,1393,1394],{"class":50}," satisfaction_dumbbell.xlsx",[40,1396,1397],{"class":210}," >",[40,1399,1400],{"class":50}," \u002Fdev\u002Fnull",[40,1402,1403],{"class":210}," 2>&1\n",[40,1405,1406,1409,1412,1415,1418,1421,1424,1426,1429,1432,1435,1438,1441,1443],{"class":42,"line":224},[40,1407,1408],{"class":46},"pdftoppm",[40,1410,1411],{"class":306}," -r",[40,1413,1414],{"class":306}," 70",[40,1416,1417],{"class":306}," -f",[40,1419,1420],{"class":306}," 1",[40,1422,1423],{"class":306}," -l",[40,1425,1420],{"class":306},[40,1427,1428],{"class":306}," -png",[40,1430,1431],{"class":50}," satisfaction_dumbbell.pdf",[40,1433,1434],{"class":50}," preview",[40,1436,1437],{"class":214}," && ",[40,1439,1440],{"class":46},"ls",[40,1442,1434],{"class":50},[40,1444,1445],{"class":306},"*\n",[10,1447,1448],{},"Converting the workbook to PDF with LibreOffice and looking at the image is a quick way to check a chart built from parts, because a mistake — the helper series visible, error bars pointing the wrong way — is obvious at a glance. In the rendered chart, East and Export show the longest connectors, with the blue dot on top for an improvement; South and Online have short connectors with the grey dot on top, because their scores fell; and West is almost a single dot.",[82,1450,91,1456,91,1459,91,1462,91,1467,91,1473,91,1477,91,1483,91,1487,91,1491,91,1495,91,1499,91,1503,91,1507,91,1511,91,1514,91,1517,91,1521,91,1523,91,1526,91,1528,91,1531,91,1533,91,1536,91,1539,91,1542,91,1544,91,1548,91,1550,91,1552,91,1554,91,1556,91,1558,91,1562,91,1565,91,1568,91,1570,91,1573,91,1575,91,1577,91,1579,91,1581,91,1583,91,1586,91,1589,91,1592,91,1594,91,1597,91,1599,91,1601,91,1603,91,1605,91,1607,91,1610,91,1613,91,1617,91,1619,91,1622,91,1624,91,1626,91,1628,91,1630,91,1632,91,1635,91,1638,91,1642,91,1644,91,1647,91,1649,91,1651,91,1653,91,1655,91,1659,91,1663,91,1666,91,1670,91,1672,91,1675,91,1677,91,1679,91,1681,91,1683,91,1685],{"viewBox":1451,"role":85,"ariaLabelledBy":1452,"xmlns":89,"style":1455},"222 0 316 252",[1453,1454],"db-data-t","db-data-d","width:100%;max-width:316px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[93,1457,1458],{"id":1453},"The data behind the chart, sorted by change",[97,1460,1461],{"id":1454},"Export and East improved most, by 14 and 13 points. North rose 9, West 3. Online fell 2 and South fell 4, so their earlier dot sits above the later one.",[101,1463],{"x":1464,"y":104,"width":1465,"height":1466,"fill":107},"222","316","252",[101,1468],{"x":1469,"y":111,"width":1470,"height":1471,"rx":104,"fill":142,"stroke":143,"style":1472},"238.3","32.0","24.0","stroke-width:1px",[101,1474],{"x":1475,"y":111,"width":1476,"height":1471,"rx":104,"fill":142,"stroke":143,"style":1472},"270.3","66.7",[119,1478,1482],{"x":1479,"y":1480,"style":1481},"303.7","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[101,1484],{"x":1485,"y":111,"width":1486,"height":1471,"rx":104,"fill":142,"stroke":143,"style":1472},"337.1","56.2",[119,1488,1490],{"x":1489,"y":1480,"style":1481},"365.2","B",[101,1492],{"x":1493,"y":111,"width":1494,"height":1471,"rx":104,"fill":142,"stroke":143,"style":1472},"393.3","56.5",[119,1496,1498],{"x":1497,"y":1480,"style":1481},"421.6","C",[101,1500],{"x":1501,"y":111,"width":1502,"height":1471,"rx":104,"fill":142,"stroke":143,"style":1472},"449.8","71.8",[119,1504,1506],{"x":1505,"y":1480,"style":1481},"485.7","D",[101,1508],{"x":1469,"y":1509,"width":1470,"height":1510,"rx":104,"fill":142,"stroke":143,"style":1472},"40.0","28.0",[119,1512,775],{"x":1513,"y":113,"style":1481},"254.3",[101,1515],{"x":1475,"y":1509,"width":1476,"height":1510,"rx":104,"fill":115,"stroke":116,"style":1516},"stroke-width:1.5px",[119,1518,1520],{"x":1479,"y":113,"style":1519},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Region",[101,1522],{"x":1485,"y":1509,"width":1486,"height":1510,"rx":104,"fill":115,"stroke":116,"style":1516},[119,1524,1525],{"x":1489,"y":113,"style":1519},"2025",[101,1527],{"x":1493,"y":1509,"width":1494,"height":1510,"rx":104,"fill":115,"stroke":116,"style":1516},[119,1529,1530],{"x":1497,"y":113,"style":1519},"2026",[101,1532],{"x":1501,"y":1509,"width":1502,"height":1510,"rx":104,"fill":115,"stroke":116,"style":1516},[119,1534,1535],{"x":1505,"y":113,"style":1519},"Change",[101,1537],{"x":1469,"y":1538,"width":1470,"height":1510,"rx":104,"fill":142,"stroke":143,"style":1472},"68.0",[119,1540,535],{"x":1513,"y":1541,"style":1481},"87.0",[101,1543],{"x":1475,"y":1538,"width":1476,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1545,1547],{"x":1479,"y":1541,"style":1546},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Export",[101,1549],{"x":1485,"y":1538,"width":1486,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1551,327],{"x":1489,"y":1541,"style":1546},[101,1553],{"x":1493,"y":1538,"width":1494,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1555,365],{"x":1497,"y":1541,"style":1546},[101,1557],{"x":1501,"y":1538,"width":1502,"height":1510,"rx":104,"fill":184,"stroke":185,"style":1516},[119,1559,1561],{"x":1505,"y":1541,"style":1560},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","+14",[101,1563],{"x":1469,"y":1564,"width":1470,"height":1510,"rx":104,"fill":142,"stroke":143,"style":1472},"96.0",[119,1566,820],{"x":1513,"y":1567,"style":1481},"115.0",[101,1569],{"x":1475,"y":1564,"width":1476,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1571,1572],{"x":1479,"y":1567,"style":1546},"East",[101,1574],{"x":1485,"y":1564,"width":1486,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1576,317],{"x":1489,"y":1567,"style":1546},[101,1578],{"x":1493,"y":1564,"width":1494,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1580,355],{"x":1497,"y":1567,"style":1546},[101,1582],{"x":1501,"y":1564,"width":1502,"height":1510,"rx":104,"fill":184,"stroke":185,"style":1516},[119,1584,1585],{"x":1505,"y":1567,"style":1560},"+13",[101,1587],{"x":1469,"y":1588,"width":1470,"height":1510,"rx":104,"fill":142,"stroke":143,"style":1472},"124.0",[119,1590,765],{"x":1513,"y":1591,"style":1481},"143.0",[101,1593],{"x":1475,"y":1588,"width":1476,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1595,1596],{"x":1479,"y":1591,"style":1546},"North",[101,1598],{"x":1485,"y":1588,"width":1486,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1600,307],{"x":1489,"y":1591,"style":1546},[101,1602],{"x":1493,"y":1588,"width":1494,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1604,345],{"x":1497,"y":1591,"style":1546},[101,1606],{"x":1501,"y":1588,"width":1502,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1608,1609],{"x":1505,"y":1591,"style":1546},"+9",[101,1611],{"x":1469,"y":1612,"width":1470,"height":1510,"rx":104,"fill":142,"stroke":143,"style":1472},"152.0",[119,1614,1616],{"x":1513,"y":1615,"style":1481},"171.0","5",[101,1618],{"x":1475,"y":1612,"width":1476,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1620,1621],{"x":1479,"y":1615,"style":1546},"West",[101,1623],{"x":1485,"y":1612,"width":1486,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1625,322],{"x":1489,"y":1615,"style":1546},[101,1627],{"x":1493,"y":1612,"width":1494,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1629,360],{"x":1497,"y":1615,"style":1546},[101,1631],{"x":1501,"y":1612,"width":1502,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1633,1634],{"x":1505,"y":1615,"style":1546},"+3",[101,1636],{"x":1469,"y":1637,"width":1470,"height":1510,"rx":104,"fill":142,"stroke":143,"style":1472},"180.0",[119,1639,1641],{"x":1513,"y":1640,"style":1481},"199.0","6",[101,1643],{"x":1475,"y":1637,"width":1476,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1645,1646],{"x":1479,"y":1640,"style":1546},"Online",[101,1648],{"x":1485,"y":1637,"width":1486,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1650,332],{"x":1489,"y":1640,"style":1546},[101,1652],{"x":1493,"y":1637,"width":1494,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1654,370],{"x":1497,"y":1640,"style":1546},[101,1656],{"x":1501,"y":1637,"width":1502,"height":1510,"rx":104,"fill":1657,"stroke":1658,"style":1516},"#fce9e9","var(--danger,#dc2626)",[119,1660,1662],{"x":1505,"y":1640,"style":1661},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","−2",[101,1664],{"x":1469,"y":1665,"width":1470,"height":1510,"rx":104,"fill":142,"stroke":143,"style":1472},"208.0",[119,1667,1669],{"x":1513,"y":1668,"style":1481},"227.0","7",[101,1671],{"x":1475,"y":1665,"width":1476,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1673,1674],{"x":1479,"y":1668,"style":1546},"South",[101,1676],{"x":1485,"y":1665,"width":1486,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1678,312],{"x":1489,"y":1668,"style":1546},[101,1680],{"x":1493,"y":1665,"width":1494,"height":1510,"rx":104,"fill":107,"stroke":143,"style":1472},[119,1682,350],{"x":1497,"y":1668,"style":1546},[101,1684],{"x":1501,"y":1665,"width":1502,"height":1510,"rx":104,"fill":1657,"stroke":1658,"style":1516},[119,1686,1687],{"x":1505,"y":1668,"style":1661},"−4",[25,1689,1691],{"id":1690},"a-note-on-high-low-lines","A note on high-low lines",[10,1693,1694,1695,1698,1699,1702],{},"Excel offers another way to draw the connectors: ",[14,1696,1697],{},"high-low lines",", an option on line charts that joins the highest and lowest value in each category, which openpyxl exposes as ",[37,1700,1701],{},"chart.hiLowLines",". In Excel the result looks the same and needs no helper columns. The catch is portability — LibreOffice ignores high-low lines on ordinary line charts, so the connectors vanish when the file is opened there or converted to PDF on a server without Excel. Error bars are drawn by both, which is why this page uses them. If the workbook will only ever be opened in Excel, high-low lines are a simpler option worth knowing.",[82,1704,91,1710,91,1713,91,1716,91,1720,91,1726,91,1732,91,1737,91,1741,91,1745,91,1749,91,1753,91,1756,91,1761,91,1765,91,1768,91,1771,91,1774],{"viewBox":1705,"role":85,"ariaLabelledBy":1706,"xmlns":89,"style":1709},"4 0 752 199",[1707,1708],"db-methods-t","db-methods-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[93,1711,1712],{"id":1707},"Error bars or high-low lines for the connectors",[97,1714,1715],{"id":1708},"Custom error bars on a hidden helper series need two helper columns but render in both Excel and LibreOffice. High-low lines need no helpers but are only drawn by Excel.",[101,1717],{"x":765,"y":104,"width":1718,"height":1719,"fill":107},"752","199",[101,1721],{"x":1722,"y":111,"width":1723,"height":1724,"rx":1725,"fill":184,"stroke":185,"style":117},"20.0","351.0","167.0","14",[119,1727,1731],{"x":1728,"y":1729,"style":1730},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Custom error bars",[42,1733],{"x1":1734,"y1":1735,"x2":1736,"y2":1735,"stroke":185,"style":1472},"36.0","57.0","355.0",[119,1738,1740],{"x":1728,"y":1739,"style":1546},"77.0","two helper columns",[119,1742,1744],{"x":1728,"y":1743,"style":1546},"103.0","rendered by Excel and LibreOffice",[119,1746,1748],{"x":1728,"y":1747,"style":1546},"129.0","works in server-side PDF export",[119,1750,1752],{"x":1728,"y":1751,"style":1546},"155.0","used on this page",[101,1754],{"x":1755,"y":111,"width":1723,"height":1724,"rx":1725,"fill":163,"stroke":164,"style":117},"389.0",[119,1757,1760],{"x":1758,"y":1729,"style":1759},"564.5","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","High-low lines",[42,1762],{"x1":1763,"y1":1735,"x2":1764,"y2":1735,"stroke":164,"style":1472},"405.0","724.0",[119,1766,1767],{"x":1758,"y":1739,"style":1546},"no helper columns",[119,1769,1770],{"x":1758,"y":1743,"style":1546},"rendered by Excel only",[119,1772,1773],{"x":1758,"y":1747,"style":1546},"vanish in LibreOffice",[119,1775,1776],{"x":1758,"y":1751,"style":1546},"simpler if Excel is guaranteed",[25,1778,1780],{"id":1779},"common-pitfalls","Common pitfalls",[1782,1783,1784,1795,1804,1813,1819,1826],"ul",{},[1785,1786,1787,1790,1791,1794],"li",{},[69,1788,1789],{},"Lines left on."," Set ",[37,1792,1793],{},"line.noFill"," on every series or the dots are joined across categories.",[1785,1796,1797,1803],{},[69,1798,1799,1800,23],{},"Error bars without ",[37,1801,1802],{},"cust"," Fixed or percentage bars do not match each pair's gap.",[1785,1805,1806,1809,1810,23],{},[69,1807,1808],{},"The helper in the legend."," Remove it with ",[37,1811,1812],{},"LegendEntry(idx=0, delete=True)",[1785,1814,1815,1818],{},[69,1816,1817],{},"Unsorted categories."," Sort by change so the chart has a reading order.",[1785,1820,1821,1790,1824,23],{},[69,1822,1823],{},"Hidden helper columns, invisible connectors.",[37,1825,1363],{},[1785,1827,1828,1831],{},[69,1829,1830],{},"Gap computed in Python."," Use formulas so corrections in the sheet move the connectors.",[25,1833,1835],{"id":1834},"conclusion","Conclusion",[10,1837,1838],{},"A dumbbell chart shows two values per category and the change between them more clearly than paired bars. With openpyxl, build it from a line chart: two marker-only series for the values, and an invisible helper series at the lower value whose custom error bars, sized from a gap column, draw the connectors. Sort categories by change, hide the helper from the legend and the sheet while letting the chart plot hidden cells, keep the axes visible, and render the workbook once to check.",[25,1840,1842],{"id":1841},"frequently-asked-questions","Frequently asked questions",[10,1844,1845,1848],{},[69,1846,1847],{},"Does Excel have a dumbbell chart type?","\nNo; it is built from a line or scatter chart with markers only, plus error bars or high-low lines for the connectors.",[10,1850,1851,1854,1855,1858,1859,23],{},[69,1852,1853],{},"How do I add custom error bars in openpyxl?","\nCreate ",[37,1856,1857],{},"ErrorBars(errValType=\"cust\", plus=NumDataSource(numRef=NumRef(f=\"'Sheet'!$E$2:$E$7\")))"," and assign it to the series' ",[37,1860,1861],{},"errBars",[10,1863,1864,1867,1868,1871],{},[69,1865,1866],{},"How do I hide one series from the chart legend?","\nSet ",[37,1869,1870],{},"chart.legend.legendEntry = [LegendEntry(idx=0, delete=True)]",", using the series' index.",[10,1873,1874,1877],{},[69,1875,1876],{},"Can the value axis start above zero in a dumbbell chart?","\nYes; the message is the distance between the dots, so starting the axis near the data makes changes easier to see without distorting them.",[25,1879,1881],{"id":1880},"related","Related",[1782,1883,1884,1890,1897,1904],{},[1785,1885,1886,1887,1889],{},"Up one level: ",[19,1888,22],{"href":21}," — native Excel charts from Python.",[1785,1891,1892,1896],{},[19,1893,1895],{"href":1894},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fstyle-chart-colors-and-data-labels-with-openpyxl\u002F","Style Chart Colours and Data Labels with openpyxl"," — marker and line styling.",[1785,1898,1899,1903],{},[19,1900,1902],{"href":1901},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-error-bars-to-an-excel-chart-with-openpyxl\u002F","Add Error Bars to an Excel Chart with openpyxl"," — error bars in general.",[1785,1905,1906,1910],{},[19,1907,1909],{"href":1908},"\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fexport-an-excel-chart-as-an-image\u002F","Export an Excel Chart as an Image with Python"," — rendering charts.",[1912,1913,1914],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":35,"searchDepth":224,"depth":224,"links":1916},[1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927],{"id":27,"depth":224,"text":28},{"id":63,"depth":224,"text":64},{"id":197,"depth":224,"text":198},{"id":660,"depth":224,"text":661},{"id":1149,"depth":224,"text":1150},{"id":1371,"depth":224,"text":1372},{"id":1690,"depth":224,"text":1691},{"id":1779,"depth":224,"text":1780},{"id":1834,"depth":224,"text":1835},{"id":1841,"depth":224,"text":1842},{"id":1880,"depth":224,"text":1881},"2026-10-02","Build a dumbbell chart — two dots per category joined by a line, showing a before and after value — as a native Excel chart with openpyxl: a line chart with the lines switched off and markers on, the connecting bars drawn as custom error bars from a hidden helper series, categories sorted by the size of the change, the helper hidden from the legend, and the result rendered to check it.","md",[1932,1934,1936,1938],{"q":1847,"a":1933},"No; it is built from a line or scatter chart with markers only, plus error bars or high-low lines for the connectors.",{"q":1853,"a":1935},"Create ErrorBars(errValType=\"cust\", plus=NumDataSource(numRef=NumRef(f=\"'Sheet'!$E$2:$E$7\"))) and assign it to the series' errBars.",{"q":1866,"a":1937},"Set chart.legend.legendEntry = [LegendEntry(idx=0, delete=True)], using the series' index.",{"q":1876,"a":1939},"Yes; the message is the distance between the dots, so starting the axis near the data makes changes easier to see without distorting them.","dumbbell chart excel python",null,{"breadcrumb":1943},[1944,1947,1950],{"name":1945,"item":1946},"Home","\u002F",{"name":1948,"item":1949},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002F",{"name":22,"item":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-dumbbell-chart-in-excel-with-python",{"title":1953,"description":1954},"Create a Dumbbell Chart in Excel with Python and openpyxl","A native Excel dumbbell chart with openpyxl: marker-only line series, connectors from custom error bars on a hidden helper series, sorted categories, legend entry removed and rendering checked.","create-a-dumbbell-chart-in-excel-with-python","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-dumbbell-chart-in-excel-with-python\u002Findex",[1958,1959],"charts-and-images","openpyxl","how-to","XJv6Tz0qW8NLxE7ys7MMMnJ3d6PkAAnB0YndqJw9SEk",[1963,1967],{"title":1964,"path":1965,"stem":1966,"children":-1},"Create a Doughnut Chart with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-doughnut-chart-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-doughnut-chart-with-openpyxl\u002Findex",{"title":1968,"path":1969,"stem":1970,"children":-1},"Create a Funnel Chart in Excel with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-funnel-chart-in-excel-with-python","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-funnel-chart-in-excel-with-python\u002Findex",[1972,1974],{"path":1973,"title":22},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl",{"path":1975,"title":1948},"\u002Fformatting-and-charting-excel-reports-with-python",[],[1978,1981,1984,1987,1990,1993],{"path":1979,"title":1980,"kind":1960},"\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":1982,"title":1983,"kind":1960},"\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":1985,"title":1986,"kind":1960},"\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":1988,"title":1989,"kind":1960},"\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":1991,"title":1992,"kind":1960},"\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":1994,"title":1995,"kind":1960},"\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",1790972109066]