[{"data":1,"prerenderedAt":2181},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-slope-chart-in-excel-with-python":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-slope-chart-in-excel-with-python":2147,"ancestors:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-slope-chart-in-excel-with-python":2156,"children:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-slope-chart-in-excel-with-python":2161,"related:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-slope-chart-in-excel-with-python":2162},{"id":4,"title":5,"body":6,"dateModified":2113,"datePublished":2113,"description":2114,"extension":2115,"faq":2116,"keyword":2125,"kind":2126,"meta":2127,"navigation":133,"path":2136,"seo":2137,"slug":2140,"stem":2141,"topics":2142,"type":2145,"__hash__":2146},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-slope-chart-in-excel-with-python\u002Findex.md","Create a Slope Chart in Excel with Python",{"type":7,"value":8,"toc":2102},"minimark",[9,28,33,65,76,80,91,382,624,627,631,1003,1017,1188,1192,1195,1524,1534,1624,1628,1631,1962,1981,1985,2023,2027,2037,2041,2047,2053,2059,2065,2069,2098],[10,11,12,13,17,18,21,22,27],"p",{},"When the question is \"what changed between last year and this year?\" for a handful of items — regions, products, departments — a clustered column chart forces the reader to compare pairs of bars and remember which colour is which year. A ",[14,15,16],"em",{},"slope chart"," answers it directly: two vertical positions, one for each period, and a straight line per item joining its old value to its new one. Rising lines slope up, falling lines slope down, steep lines changed a lot, and crossing lines show items that swapped places. Excel has no slope chart type, but a line chart with only two category points ",[14,19,20],{},"is"," a slope chart, and openpyxl can build one with the styling that makes it readable: colours that separate risers from fallers, labels at the ends of each line instead of a legend, and a stripped-back axis. This page builds it, then renders the workbook to check the chart. It belongs to ",[23,24,26],"a",{"href":25},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002F","Creating Charts in Excel with openpyxl",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,40],"pre",{"className":36,"code":37,"language":38,"meta":39,"style":39},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas pillow\n","bash","",[41,42,43],"code",{"__ignoreMap":39},[44,45,48,52,56,59,62],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sMTad","pip",[44,53,55],{"class":54},"srMev"," install",[44,57,58],{"class":54}," openpyxl",[44,60,61],{"class":54}," pandas",[44,63,64],{"class":54}," pillow\n",[10,66,67,68,71,72,75],{},"LibreOffice (",[41,69,70],{},"soffice",") and ",[41,73,74],{},"pdftoppm"," (from Poppler) are used only for the final check, which renders the chart to an image. The data is customer satisfaction scores for six regions in 2025 and 2026.",[29,77,79],{"id":78},"step-1-one-series-per-item","Step 1 — One series per item",[10,81,82,83,86,87,90],{},"A slope chart needs the ",[14,84,85],{},"items"," as series and the ",[14,88,89],{},"periods"," as categories, the transpose of how such data usually arrives. Pivot it in pandas and write it with the periods down the rows:",[34,92,96],{"className":93,"code":94,"language":95,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\nfrom openpyxl import Workbook\n\nscores = pd.DataFrame({\"Region\": [\"North\", \"South\", \"East\", \"West\", \"Central\", \"Islands\"],\n                       \"2025\": [72, 81, 64, 77, 69, 58], \"2026\": [79, 76, 71, 78, 61, 70]})\nwide = scores.set_index(\"Region\").T                      # rows: years, columns: regions\nwide.index.name = \"Year\"\n\nwb = Workbook()\nws = wb.active\nws.title = \"Slope\"\nws.append([\"Year\", *wide.columns])\nfor year, row in wide.iterrows():\n    ws.append([year, *row.tolist()])\nprint(wide.to_string())\n","python",[41,97,98,114,128,135,185,261,281,292,297,308,319,330,347,362,373],{"__ignoreMap":39},[44,99,100,104,108,111],{"class":46,"line":47},[44,101,103],{"class":102},"s-kum","import",[44,105,107],{"class":106},"skGVy"," pandas ",[44,109,110],{"class":102},"as",[44,112,113],{"class":106}," pd\n",[44,115,117,120,123,125],{"class":46,"line":116},2,[44,118,119],{"class":102},"from",[44,121,122],{"class":106}," openpyxl ",[44,124,103],{"class":102},[44,126,127],{"class":106}," Workbook\n",[44,129,131],{"class":46,"line":130},3,[44,132,134],{"emptyLinePlaceholder":133},true,"\n",[44,136,138,141,144,147,150,153,156,159,162,164,167,169,172,174,177,179,182],{"class":46,"line":137},4,[44,139,140],{"class":106},"scores ",[44,142,143],{"class":102},"=",[44,145,146],{"class":106}," pd.DataFrame({",[44,148,149],{"class":54},"\"Region\"",[44,151,152],{"class":106},": [",[44,154,155],{"class":54},"\"North\"",[44,157,158],{"class":106},", ",[44,160,161],{"class":54},"\"South\"",[44,163,158],{"class":106},[44,165,166],{"class":54},"\"East\"",[44,168,158],{"class":106},[44,170,171],{"class":54},"\"West\"",[44,173,158],{"class":106},[44,175,176],{"class":54},"\"Central\"",[44,178,158],{"class":106},[44,180,181],{"class":54},"\"Islands\"",[44,183,184],{"class":106},"],\n",[44,186,188,191,193,197,199,202,204,207,209,212,214,217,219,222,225,228,230,233,235,238,240,243,245,248,250,253,255,258],{"class":46,"line":187},5,[44,189,190],{"class":54},"                       \"2025\"",[44,192,152],{"class":106},[44,194,196],{"class":195},"sP0c6","72",[44,198,158],{"class":106},[44,200,201],{"class":195},"81",[44,203,158],{"class":106},[44,205,206],{"class":195},"64",[44,208,158],{"class":106},[44,210,211],{"class":195},"77",[44,213,158],{"class":106},[44,215,216],{"class":195},"69",[44,218,158],{"class":106},[44,220,221],{"class":195},"58",[44,223,224],{"class":106},"], ",[44,226,227],{"class":54},"\"2026\"",[44,229,152],{"class":106},[44,231,232],{"class":195},"79",[44,234,158],{"class":106},[44,236,237],{"class":195},"76",[44,239,158],{"class":106},[44,241,242],{"class":195},"71",[44,244,158],{"class":106},[44,246,247],{"class":195},"78",[44,249,158],{"class":106},[44,251,252],{"class":195},"61",[44,254,158],{"class":106},[44,256,257],{"class":195},"70",[44,259,260],{"class":106},"]})\n",[44,262,264,267,269,272,274,277],{"class":46,"line":263},6,[44,265,266],{"class":106},"wide ",[44,268,143],{"class":102},[44,270,271],{"class":106}," scores.set_index(",[44,273,149],{"class":54},[44,275,276],{"class":106},").T                      ",[44,278,280],{"class":279},"s-wDw","# rows: years, columns: regions\n",[44,282,284,287,289],{"class":46,"line":283},7,[44,285,286],{"class":106},"wide.index.name ",[44,288,143],{"class":102},[44,290,291],{"class":54}," \"Year\"\n",[44,293,295],{"class":46,"line":294},8,[44,296,134],{"emptyLinePlaceholder":133},[44,298,300,303,305],{"class":46,"line":299},9,[44,301,302],{"class":106},"wb ",[44,304,143],{"class":102},[44,306,307],{"class":106}," Workbook()\n",[44,309,311,314,316],{"class":46,"line":310},10,[44,312,313],{"class":106},"ws ",[44,315,143],{"class":102},[44,317,318],{"class":106}," wb.active\n",[44,320,322,325,327],{"class":46,"line":321},11,[44,323,324],{"class":106},"ws.title ",[44,326,143],{"class":102},[44,328,329],{"class":54}," \"Slope\"\n",[44,331,333,336,339,341,344],{"class":46,"line":332},12,[44,334,335],{"class":106},"ws.append([",[44,337,338],{"class":54},"\"Year\"",[44,340,158],{"class":106},[44,342,343],{"class":102},"*",[44,345,346],{"class":106},"wide.columns])\n",[44,348,350,353,356,359],{"class":46,"line":349},13,[44,351,352],{"class":102},"for",[44,354,355],{"class":106}," year, row ",[44,357,358],{"class":102},"in",[44,360,361],{"class":106}," wide.iterrows():\n",[44,363,365,368,370],{"class":46,"line":364},14,[44,366,367],{"class":106},"    ws.append([year, ",[44,369,343],{"class":102},[44,371,372],{"class":106},"row.tolist()])\n",[44,374,376,379],{"class":46,"line":375},15,[44,377,378],{"class":195},"print",[44,380,381],{"class":106},"(wide.to_string())\n",[383,384,392,393,392,397,392,401,392,409,392,418,392,422,392,429,392,433,392,437,392,441,392,445,392,449,392,454,392,456,392,460,392,462,392,465,392,467,392,470,392,473,392,477,392,479,392,483,392,485,392,487,392,489,392,491,392,494,392,498,392,500,392,503,392,505,392,507,392,509,392,511,392,514,392,517,392,520,392,524,392,527,392,531,392,534,392,536,392,539,392,544,392,548,392,550,392,552,392,554,392,556,392,558,392,560,392,562,392,564,392,566,392,568,392,570,392,572,392,574,392,576,392,578,392,580,392,582,392,584,392,586,392,588,392,594,392,598,392,605,392,610,392,618],"svg",{"viewBox":385,"role":386,"ariaLabelledBy":387,"xmlns":390,"style":391},"99 0 562 164","img",[388,389],"sl-shape-t","sl-shape-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:562px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[394,395,396],"title",{"id":388},"Turning the data round for a slope chart",[398,399,400],"desc",{"id":389},"The scores arrive with one row per region and a column per year. Transposed, there is one row per year and one column per region, so each region becomes a chart series with two points.",[402,403],"rect",{"x":404,"y":405,"width":406,"height":407,"fill":408},"99","0","562","164","#ffffff",[402,410],{"x":411,"y":412,"width":413,"height":414,"rx":405,"fill":415,"stroke":416,"style":417},"115.4","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[402,419],{"x":420,"y":412,"width":421,"height":414,"rx":405,"fill":415,"stroke":416,"style":417},"147.4","66.7",[423,424,428],"text",{"x":425,"y":426,"style":427},"180.8","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[402,430],{"x":431,"y":412,"width":432,"height":414,"rx":405,"fill":415,"stroke":416,"style":417},"214.1","56.2",[423,434,436],{"x":435,"y":426,"style":427},"242.2","B",[402,438],{"x":439,"y":412,"width":440,"height":414,"rx":405,"fill":415,"stroke":416,"style":417},"270.3","56.5",[423,442,444],{"x":443,"y":426,"style":427},"298.6","C",[402,446],{"x":411,"y":447,"width":413,"height":448,"rx":405,"fill":415,"stroke":416,"style":417},"64.0","28.0",[423,450,453],{"x":451,"y":452,"style":427},"131.4","83.0","1",[402,455],{"x":420,"y":447,"width":421,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,457,459],{"x":425,"y":452,"style":458},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Region",[402,461],{"x":431,"y":447,"width":432,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,463,464],{"x":435,"y":452,"style":458},"2025",[402,466],{"x":439,"y":447,"width":440,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,468,469],{"x":443,"y":452,"style":458},"2026",[402,471],{"x":411,"y":472,"width":413,"height":448,"rx":405,"fill":415,"stroke":416,"style":417},"92.0",[423,474,476],{"x":451,"y":475,"style":427},"111.0","2",[402,478],{"x":420,"y":472,"width":421,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,480,482],{"x":425,"y":475,"style":481},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","North",[402,484],{"x":431,"y":472,"width":432,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,486,196],{"x":435,"y":475,"style":481},[402,488],{"x":439,"y":472,"width":440,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,490,232],{"x":443,"y":475,"style":481},[402,492],{"x":411,"y":493,"width":413,"height":448,"rx":405,"fill":415,"stroke":416,"style":417},"120.0",[423,495,497],{"x":451,"y":496,"style":427},"139.0","3",[402,499],{"x":420,"y":493,"width":421,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,501,502],{"x":425,"y":496,"style":481},"South",[402,504],{"x":431,"y":493,"width":432,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,506,201],{"x":435,"y":496,"style":481},[402,508],{"x":439,"y":493,"width":440,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,510,237],{"x":443,"y":496,"style":481},[402,512],{"x":513,"y":412,"width":413,"height":414,"rx":405,"fill":415,"stroke":416,"style":417},"436.9",[402,515],{"x":516,"y":412,"width":440,"height":414,"rx":405,"fill":415,"stroke":416,"style":417},"468.9",[423,518,428],{"x":519,"y":426,"style":427},"497.1",[402,521],{"x":522,"y":412,"width":523,"height":414,"rx":405,"fill":415,"stroke":416,"style":417},"525.4","58.9",[423,525,436],{"x":526,"y":426,"style":427},"554.9",[402,528],{"x":529,"y":412,"width":530,"height":414,"rx":405,"fill":415,"stroke":416,"style":417},"584.3","60.3",[423,532,444],{"x":533,"y":426,"style":427},"614.5",[402,535],{"x":513,"y":447,"width":413,"height":448,"rx":405,"fill":415,"stroke":416,"style":417},[423,537,453],{"x":538,"y":452,"style":427},"452.9",[402,540],{"x":516,"y":447,"width":440,"height":448,"rx":405,"fill":541,"stroke":542,"style":543},"#d9f4f1","var(--teal,#0f9488)","stroke-width:1.5px",[423,545,547],{"x":519,"y":452,"style":546},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Year",[402,549],{"x":522,"y":447,"width":523,"height":448,"rx":405,"fill":541,"stroke":542,"style":543},[423,551,482],{"x":526,"y":452,"style":546},[402,553],{"x":529,"y":447,"width":530,"height":448,"rx":405,"fill":541,"stroke":542,"style":543},[423,555,502],{"x":533,"y":452,"style":546},[402,557],{"x":513,"y":472,"width":413,"height":448,"rx":405,"fill":415,"stroke":416,"style":417},[423,559,476],{"x":538,"y":475,"style":427},[402,561],{"x":516,"y":472,"width":440,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,563,464],{"x":519,"y":475,"style":481},[402,565],{"x":522,"y":472,"width":523,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,567,196],{"x":526,"y":475,"style":481},[402,569],{"x":529,"y":472,"width":530,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,571,201],{"x":533,"y":475,"style":481},[402,573],{"x":513,"y":493,"width":413,"height":448,"rx":405,"fill":415,"stroke":416,"style":417},[423,575,497],{"x":538,"y":496,"style":427},[402,577],{"x":516,"y":493,"width":440,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,579,469],{"x":519,"y":496,"style":481},[402,581],{"x":522,"y":493,"width":523,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,583,232],{"x":526,"y":496,"style":481},[402,585],{"x":529,"y":493,"width":530,"height":448,"rx":405,"fill":408,"stroke":416,"style":417},[423,587,237],{"x":533,"y":496,"style":481},[423,589,593],{"x":590,"y":591,"style":592},"221.1","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before",[423,595,597],{"x":596,"y":591,"style":592},"540.7","after",[46,599],{"x1":600,"y1":601,"x2":602,"y2":601,"stroke":603,"style":604},"336.9","94.0","422.9","var(--brand,#5b5cf0)","stroke-width:2px",[606,607],"polygon",{"points":608,"fill":609},"426.9,94.0 417.9,99.0 417.9,89.0","#5b5cf0",[402,611],{"x":612,"y":613,"width":614,"height":615,"rx":616,"fill":408,"stroke":416,"style":617},"364.6","61.0","34.5","22.0","11","stroke-width:1.2px",[423,619,623],{"x":620,"y":621,"style":622},"381.9","76.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",".T",[10,625,626],{},"Each column, B to G, is now one region with two values, and column A holds the two category labels. The years are written as text so the chart treats them as two evenly spaced labels rather than as numbers on a scale. Slope charts work best with up to about ten items; beyond that the lines tangle, and a sorted table of changes or a small-multiples layout reads better.",[29,628,630],{"id":629},"step-2-a-two-point-line-chart","Step 2 — A two-point line chart",[34,632,634],{"className":93,"code":633,"language":95,"meta":39,"style":39},"from openpyxl.chart import LineChart, Reference\nfrom openpyxl.chart.label import DataLabelList, DataLabel\nfrom openpyxl.chart.shapes import GraphicalProperties\nfrom openpyxl.drawing.line import LineProperties\n\nchart = LineChart()\nchart.add_data(Reference(ws, min_col=2, max_col=1 + len(wide.columns), min_row=1, max_row=3), titles_from_data=True)\nchart.set_categories(Reference(ws, min_col=1, min_row=2, max_row=3))\n\nUP, DOWN, FLAT = \"1A7F37\", \"CF222E\", \"8C959F\"\nfor series, region in zip(chart.series, wide.columns):\n    change = wide.loc[\"2026\", region] - wide.loc[\"2025\", region]\n    colour = UP if change >= 3 else DOWN if change \u003C= -3 else FLAT\n    series.graphicalProperties.line = LineProperties(solidFill=colour, w=28575)     # 2.25 pt\n    series.marker.symbol = \"circle\"\n    series.marker.size = 8\n    series.marker.graphicalProperties = GraphicalProperties(solidFill=colour)\n    series.marker.graphicalProperties.line.solidFill = colour\n    series.smooth = False\n",[41,635,636,648,660,672,684,688,698,759,789,793,824,839,865,910,942,952,963,981,992],{"__ignoreMap":39},[44,637,638,640,643,645],{"class":46,"line":47},[44,639,119],{"class":102},[44,641,642],{"class":106}," openpyxl.chart ",[44,644,103],{"class":102},[44,646,647],{"class":106}," LineChart, Reference\n",[44,649,650,652,655,657],{"class":46,"line":116},[44,651,119],{"class":102},[44,653,654],{"class":106}," openpyxl.chart.label ",[44,656,103],{"class":102},[44,658,659],{"class":106}," DataLabelList, DataLabel\n",[44,661,662,664,667,669],{"class":46,"line":130},[44,663,119],{"class":102},[44,665,666],{"class":106}," openpyxl.chart.shapes ",[44,668,103],{"class":102},[44,670,671],{"class":106}," GraphicalProperties\n",[44,673,674,676,679,681],{"class":46,"line":137},[44,675,119],{"class":102},[44,677,678],{"class":106}," openpyxl.drawing.line ",[44,680,103],{"class":102},[44,682,683],{"class":106}," LineProperties\n",[44,685,686],{"class":46,"line":187},[44,687,134],{"emptyLinePlaceholder":133},[44,689,690,693,695],{"class":46,"line":263},[44,691,692],{"class":106},"chart ",[44,694,143],{"class":102},[44,696,697],{"class":106}," LineChart()\n",[44,699,700,703,707,709,711,713,716,718,720,723,726,729,732,734,736,738,741,743,745,748,751,753,756],{"class":46,"line":283},[44,701,702],{"class":106},"chart.add_data(Reference(ws, ",[44,704,706],{"class":705},"sa561","min_col",[44,708,143],{"class":102},[44,710,476],{"class":195},[44,712,158],{"class":106},[44,714,715],{"class":705},"max_col",[44,717,143],{"class":102},[44,719,453],{"class":195},[44,721,722],{"class":102}," +",[44,724,725],{"class":195}," len",[44,727,728],{"class":106},"(wide.columns), ",[44,730,731],{"class":705},"min_row",[44,733,143],{"class":102},[44,735,453],{"class":195},[44,737,158],{"class":106},[44,739,740],{"class":705},"max_row",[44,742,143],{"class":102},[44,744,497],{"class":195},[44,746,747],{"class":106},"), ",[44,749,750],{"class":705},"titles_from_data",[44,752,143],{"class":102},[44,754,755],{"class":195},"True",[44,757,758],{"class":106},")\n",[44,760,761,764,766,768,770,772,774,776,778,780,782,784,786],{"class":46,"line":294},[44,762,763],{"class":106},"chart.set_categories(Reference(ws, ",[44,765,706],{"class":705},[44,767,143],{"class":102},[44,769,453],{"class":195},[44,771,158],{"class":106},[44,773,731],{"class":705},[44,775,143],{"class":102},[44,777,476],{"class":195},[44,779,158],{"class":106},[44,781,740],{"class":705},[44,783,143],{"class":102},[44,785,497],{"class":195},[44,787,788],{"class":106},"))\n",[44,790,791],{"class":46,"line":299},[44,792,134],{"emptyLinePlaceholder":133},[44,794,795,798,800,803,805,808,811,814,816,819,821],{"class":46,"line":310},[44,796,797],{"class":195},"UP",[44,799,158],{"class":106},[44,801,802],{"class":195},"DOWN",[44,804,158],{"class":106},[44,806,807],{"class":195},"FLAT",[44,809,810],{"class":102}," =",[44,812,813],{"class":54}," \"1A7F37\"",[44,815,158],{"class":106},[44,817,818],{"class":54},"\"CF222E\"",[44,820,158],{"class":106},[44,822,823],{"class":54},"\"8C959F\"\n",[44,825,826,828,831,833,836],{"class":46,"line":321},[44,827,352],{"class":102},[44,829,830],{"class":106}," series, region ",[44,832,358],{"class":102},[44,834,835],{"class":195}," zip",[44,837,838],{"class":106},"(chart.series, wide.columns):\n",[44,840,841,844,846,849,851,854,857,859,862],{"class":46,"line":332},[44,842,843],{"class":106},"    change ",[44,845,143],{"class":102},[44,847,848],{"class":106}," wide.loc[",[44,850,227],{"class":54},[44,852,853],{"class":106},", region] ",[44,855,856],{"class":102},"-",[44,858,848],{"class":106},[44,860,861],{"class":54},"\"2025\"",[44,863,864],{"class":106},", region]\n",[44,866,867,870,872,875,878,881,884,887,890,893,895,897,900,903,905,907],{"class":46,"line":349},[44,868,869],{"class":106},"    colour ",[44,871,143],{"class":102},[44,873,874],{"class":195}," UP",[44,876,877],{"class":102}," if",[44,879,880],{"class":106}," change ",[44,882,883],{"class":102},">=",[44,885,886],{"class":195}," 3",[44,888,889],{"class":102}," else",[44,891,892],{"class":195}," DOWN",[44,894,877],{"class":102},[44,896,880],{"class":106},[44,898,899],{"class":102},"\u003C=",[44,901,902],{"class":102}," -",[44,904,497],{"class":195},[44,906,889],{"class":102},[44,908,909],{"class":195}," FLAT\n",[44,911,912,915,917,920,923,925,928,931,933,936,939],{"class":46,"line":364},[44,913,914],{"class":106},"    series.graphicalProperties.line ",[44,916,143],{"class":102},[44,918,919],{"class":106}," LineProperties(",[44,921,922],{"class":705},"solidFill",[44,924,143],{"class":102},[44,926,927],{"class":106},"colour, ",[44,929,930],{"class":705},"w",[44,932,143],{"class":102},[44,934,935],{"class":195},"28575",[44,937,938],{"class":106},")     ",[44,940,941],{"class":279},"# 2.25 pt\n",[44,943,944,947,949],{"class":46,"line":375},[44,945,946],{"class":106},"    series.marker.symbol ",[44,948,143],{"class":102},[44,950,951],{"class":54}," \"circle\"\n",[44,953,955,958,960],{"class":46,"line":954},16,[44,956,957],{"class":106},"    series.marker.size ",[44,959,143],{"class":102},[44,961,962],{"class":195}," 8\n",[44,964,966,969,971,974,976,978],{"class":46,"line":965},17,[44,967,968],{"class":106},"    series.marker.graphicalProperties ",[44,970,143],{"class":102},[44,972,973],{"class":106}," GraphicalProperties(",[44,975,922],{"class":705},[44,977,143],{"class":102},[44,979,980],{"class":106},"colour)\n",[44,982,984,987,989],{"class":46,"line":983},18,[44,985,986],{"class":106},"    series.marker.graphicalProperties.line.solidFill ",[44,988,143],{"class":102},[44,990,991],{"class":106}," colour\n",[44,993,995,998,1000],{"class":46,"line":994},19,[44,996,997],{"class":106},"    series.smooth ",[44,999,143],{"class":102},[44,1001,1002],{"class":195}," False\n",[10,1004,1005,1008,1009,1012,1013,1016],{},[41,1006,1007],{},"add_data"," with ",[41,1010,1011],{},"titles_from_data=True"," makes each column a series named after its header. Colour is the chart's main message, so it is chosen from the change itself: green for regions that rose by 3 points or more, red for those that fell by 3 or more, grey for the rest. A threshold rather than \"any change\" keeps noise from being dressed up as news. Line widths in openpyxl are in EMUs — 12,700 per point — so 28,575 is 2.25 pt, thick enough for a few lines to stand out. ",[41,1014,1015],{},"smooth = False"," matters: openpyxl line charts otherwise write a smoothing flag that some Excel versions honour, and a curved slope line misrepresents a two-point comparison.",[383,1018,392,1024,392,1027,392,1030,392,1035,392,1040,392,1045,392,1048,392,1051,392,1054,392,1058,392,1061,392,1065,392,1068,392,1072,392,1075,392,1079,392,1084,392,1089,392,1093,392,1097,392,1100,392,1104,392,1107,392,1111,392,1114,392,1118,392,1121,392,1126,392,1129,392,1135,392,1140,392,1143,392,1146,392,1149,392,1152,392,1155,392,1158,392,1161,392,1164,392,1167,392,1170,392,1173,392,1176,392,1179,392,1182,392,1185],{"viewBox":1019,"role":386,"ariaLabelledBy":1020,"xmlns":390,"style":1023},"4 0 748 280",[1021,1022],"sl-chart-t","sl-chart-d","width:100%;max-width:748px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[394,1025,1026],{"id":1021},"Satisfaction by region, 2025 to 2026",[398,1028,1029],{"id":1022},"North rises from 72 to 79, East from 64 to 71 and Islands from 58 to 70, shown in green. South falls from 81 to 76 and Central from 69 to 61, shown in red. West is flat, 77 to 78, in grey. South and North swap places at the top.",[402,1031],{"x":1032,"y":405,"width":1033,"height":1034,"fill":408},"4","748","280",[402,1036],{"x":447,"y":1037,"width":1038,"height":1038,"rx":497,"fill":1039,"stroke":603,"style":543},"18.0","14.0","#f0f4ff",[423,1041,482],{"x":1042,"y":1043,"style":1044},"84.0","29.5","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start",[402,1046],{"x":1047,"y":1037,"width":1038,"height":1038,"rx":497,"fill":1039,"stroke":603,"style":543},"146.9",[423,1049,502],{"x":1050,"y":1043,"style":1044},"166.9",[402,1052],{"x":1053,"y":1037,"width":1038,"height":1038,"rx":497,"fill":1039,"stroke":603,"style":543},"231.2",[423,1055,1057],{"x":1056,"y":1043,"style":1044},"251.2","East",[402,1059],{"x":1060,"y":1037,"width":1038,"height":1038,"rx":497,"fill":1039,"stroke":603,"style":543},"305.8",[423,1062,1064],{"x":1063,"y":1043,"style":1044},"325.8","West",[402,1066],{"x":1067,"y":1037,"width":1038,"height":1038,"rx":497,"fill":1039,"stroke":603,"style":543},"386.1",[423,1069,1071],{"x":1070,"y":1043,"style":1044},"406.1","Central",[402,1073],{"x":1074,"y":1037,"width":1038,"height":1038,"rx":497,"fill":1039,"stroke":603,"style":543},"479.3",[423,1076,1078],{"x":1077,"y":1043,"style":1044},"499.3","Islands",[46,1080],{"x1":1081,"y1":1082,"x2":1083,"y2":1082,"stroke":416,"style":417},"51.3","242.0","736.0",[423,1085,405],{"x":1086,"y":1087,"style":1088},"29.6","246.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle",[46,1090],{"x1":1081,"y1":1091,"x2":1083,"y2":1091,"stroke":416,"style":1092},"194.5","stroke-width:1px;stroke-dasharray:5 4",[423,1094,1096],{"x":1086,"y":1095,"style":1088},"198.5","25",[46,1098],{"x1":1081,"y1":1099,"x2":1083,"y2":1099,"stroke":416,"style":1092},"147.0",[423,1101,1103],{"x":1086,"y":1102,"style":1088},"151.0","50",[46,1105],{"x1":1081,"y1":1106,"x2":1083,"y2":1106,"stroke":416,"style":1092},"99.5",[423,1108,1110],{"x":1086,"y":1109,"style":1088},"103.5","75",[46,1112],{"x1":1081,"y1":1113,"x2":1083,"y2":1113,"stroke":416,"style":1092},"52.0",[423,1115,1117],{"x":1086,"y":1116,"style":1088},"56.0","100",[46,1119],{"x1":1081,"y1":1082,"x2":1083,"y2":1082,"stroke":1120,"style":543},"var(--muted,#5b6780)",[423,1122,464],{"x":1123,"y":1124,"style":1125},"222.5","260.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[423,1127,469],{"x":1128,"y":1124,"style":1125},"564.8",[1130,1131],"path",{"d":1132,"fill":1133,"stroke":603,"style":1134},"M222.5,105.2 L564.8,91.9","none","stroke-width:2.5px;stroke-linejoin:round;stroke-linecap:round",[1136,1137],"circle",{"cx":1123,"cy":1138,"r":1139,"fill":408,"stroke":603,"style":604},"105.2","4.0",[1136,1141],{"cx":1128,"cy":1142,"r":1139,"fill":408,"stroke":603,"style":604},"91.9",[1130,1144],{"d":1145,"fill":1133,"stroke":603,"style":1134},"M222.5,88.1 L564.8,97.6",[1136,1147],{"cx":1123,"cy":1148,"r":1139,"fill":408,"stroke":603,"style":604},"88.1",[1136,1150],{"cx":1128,"cy":1151,"r":1139,"fill":408,"stroke":603,"style":604},"97.6",[1130,1153],{"d":1154,"fill":1133,"stroke":603,"style":1134},"M222.5,120.4 L564.8,107.1",[1136,1156],{"cx":1123,"cy":1157,"r":1139,"fill":408,"stroke":603,"style":604},"120.4",[1136,1159],{"cx":1128,"cy":1160,"r":1139,"fill":408,"stroke":603,"style":604},"107.1",[1130,1162],{"d":1163,"fill":1133,"stroke":603,"style":1134},"M222.5,95.7 L564.8,93.8",[1136,1165],{"cx":1123,"cy":1166,"r":1139,"fill":408,"stroke":603,"style":604},"95.7",[1136,1168],{"cx":1128,"cy":1169,"r":1139,"fill":408,"stroke":603,"style":604},"93.8",[1130,1171],{"d":1172,"fill":1133,"stroke":603,"style":1134},"M222.5,110.9 L564.8,126.1",[1136,1174],{"cx":1123,"cy":1175,"r":1139,"fill":408,"stroke":603,"style":604},"110.9",[1136,1177],{"cx":1128,"cy":1178,"r":1139,"fill":408,"stroke":603,"style":604},"126.1",[1130,1180],{"d":1181,"fill":1133,"stroke":603,"style":1134},"M222.5,131.8 L564.8,109.0",[1136,1183],{"cx":1123,"cy":1184,"r":1139,"fill":408,"stroke":603,"style":604},"131.8",[1136,1186],{"cx":1128,"cy":1187,"r":1139,"fill":408,"stroke":603,"style":604},"109.0",[29,1189,1191],{"id":1190},"step-3-labels-at-the-ends-instead-of-a-legend","Step 3 — Labels at the ends instead of a legend",[10,1193,1194],{},"A legend makes the reader match six colours to six names. Labelling each line directly — the region and its value at the right-hand end, the value alone at the left — removes that work:",[34,1196,1198],{"className":93,"code":1197,"language":95,"meta":39,"style":39},"for series in chart.series:\n    labels = DataLabelList()\n    hide = dict(showCatName=False, showLegendKey=False, showPercent=False, showBubbleSize=False)\n    labels.dLbl = [DataLabel(idx=0, showVal=True, showSerName=False, dLblPos=\"l\", **hide),\n                   DataLabel(idx=1, showVal=True, showSerName=True, dLblPos=\"r\", separator=\" \", **hide)]\n    labels.showVal = False\n    for flag in hide:\n        setattr(labels, flag, False)\n    series.dLbls = labels\n\nchart.legend = None\nchart.title = \"Customer satisfaction by region\"\nchart.y_axis.scaling.min, chart.y_axis.scaling.max = 50, 90\nchart.y_axis.majorGridlines = None\nchart.y_axis.delete = True\nchart.x_axis.delete = False\nchart.width, chart.height = 14, 12\nws.add_chart(chart, \"A5\")\nwb.save(\"slope_chart.xlsx\")\n",[41,1199,1200,1212,1222,1272,1325,1378,1387,1400,1412,1422,1426,1436,1446,1461,1470,1480,1489,1504,1514],{"__ignoreMap":39},[44,1201,1202,1204,1207,1209],{"class":46,"line":47},[44,1203,352],{"class":102},[44,1205,1206],{"class":106}," series ",[44,1208,358],{"class":102},[44,1210,1211],{"class":106}," chart.series:\n",[44,1213,1214,1217,1219],{"class":46,"line":116},[44,1215,1216],{"class":106},"    labels ",[44,1218,143],{"class":102},[44,1220,1221],{"class":106}," DataLabelList()\n",[44,1223,1224,1227,1229,1232,1235,1238,1240,1243,1245,1248,1250,1252,1254,1257,1259,1261,1263,1266,1268,1270],{"class":46,"line":130},[44,1225,1226],{"class":106},"    hide ",[44,1228,143],{"class":102},[44,1230,1231],{"class":195}," dict",[44,1233,1234],{"class":106},"(",[44,1236,1237],{"class":705},"showCatName",[44,1239,143],{"class":102},[44,1241,1242],{"class":195},"False",[44,1244,158],{"class":106},[44,1246,1247],{"class":705},"showLegendKey",[44,1249,143],{"class":102},[44,1251,1242],{"class":195},[44,1253,158],{"class":106},[44,1255,1256],{"class":705},"showPercent",[44,1258,143],{"class":102},[44,1260,1242],{"class":195},[44,1262,158],{"class":106},[44,1264,1265],{"class":705},"showBubbleSize",[44,1267,143],{"class":102},[44,1269,1242],{"class":195},[44,1271,758],{"class":106},[44,1273,1274,1277,1279,1282,1285,1287,1289,1291,1294,1296,1298,1300,1303,1305,1307,1309,1312,1314,1317,1319,1322],{"class":46,"line":137},[44,1275,1276],{"class":106},"    labels.dLbl ",[44,1278,143],{"class":102},[44,1280,1281],{"class":106}," [DataLabel(",[44,1283,1284],{"class":705},"idx",[44,1286,143],{"class":102},[44,1288,405],{"class":195},[44,1290,158],{"class":106},[44,1292,1293],{"class":705},"showVal",[44,1295,143],{"class":102},[44,1297,755],{"class":195},[44,1299,158],{"class":106},[44,1301,1302],{"class":705},"showSerName",[44,1304,143],{"class":102},[44,1306,1242],{"class":195},[44,1308,158],{"class":106},[44,1310,1311],{"class":705},"dLblPos",[44,1313,143],{"class":102},[44,1315,1316],{"class":54},"\"l\"",[44,1318,158],{"class":106},[44,1320,1321],{"class":102},"**",[44,1323,1324],{"class":106},"hide),\n",[44,1326,1327,1330,1332,1334,1336,1338,1340,1342,1344,1346,1348,1350,1352,1354,1356,1358,1361,1363,1366,1368,1371,1373,1375],{"class":46,"line":187},[44,1328,1329],{"class":106},"                   DataLabel(",[44,1331,1284],{"class":705},[44,1333,143],{"class":102},[44,1335,453],{"class":195},[44,1337,158],{"class":106},[44,1339,1293],{"class":705},[44,1341,143],{"class":102},[44,1343,755],{"class":195},[44,1345,158],{"class":106},[44,1347,1302],{"class":705},[44,1349,143],{"class":102},[44,1351,755],{"class":195},[44,1353,158],{"class":106},[44,1355,1311],{"class":705},[44,1357,143],{"class":102},[44,1359,1360],{"class":54},"\"r\"",[44,1362,158],{"class":106},[44,1364,1365],{"class":705},"separator",[44,1367,143],{"class":102},[44,1369,1370],{"class":54},"\" \"",[44,1372,158],{"class":106},[44,1374,1321],{"class":102},[44,1376,1377],{"class":106},"hide)]\n",[44,1379,1380,1383,1385],{"class":46,"line":263},[44,1381,1382],{"class":106},"    labels.showVal ",[44,1384,143],{"class":102},[44,1386,1002],{"class":195},[44,1388,1389,1392,1395,1397],{"class":46,"line":283},[44,1390,1391],{"class":102},"    for",[44,1393,1394],{"class":106}," flag ",[44,1396,358],{"class":102},[44,1398,1399],{"class":106}," hide:\n",[44,1401,1402,1405,1408,1410],{"class":46,"line":294},[44,1403,1404],{"class":195},"        setattr",[44,1406,1407],{"class":106},"(labels, flag, ",[44,1409,1242],{"class":195},[44,1411,758],{"class":106},[44,1413,1414,1417,1419],{"class":46,"line":299},[44,1415,1416],{"class":106},"    series.dLbls ",[44,1418,143],{"class":102},[44,1420,1421],{"class":106}," labels\n",[44,1423,1424],{"class":46,"line":310},[44,1425,134],{"emptyLinePlaceholder":133},[44,1427,1428,1431,1433],{"class":46,"line":321},[44,1429,1430],{"class":106},"chart.legend ",[44,1432,143],{"class":102},[44,1434,1435],{"class":195}," None\n",[44,1437,1438,1441,1443],{"class":46,"line":332},[44,1439,1440],{"class":106},"chart.title ",[44,1442,143],{"class":102},[44,1444,1445],{"class":54}," \"Customer satisfaction by region\"\n",[44,1447,1448,1451,1453,1456,1458],{"class":46,"line":349},[44,1449,1450],{"class":106},"chart.y_axis.scaling.min, chart.y_axis.scaling.max ",[44,1452,143],{"class":102},[44,1454,1455],{"class":195}," 50",[44,1457,158],{"class":106},[44,1459,1460],{"class":195},"90\n",[44,1462,1463,1466,1468],{"class":46,"line":364},[44,1464,1465],{"class":106},"chart.y_axis.majorGridlines ",[44,1467,143],{"class":102},[44,1469,1435],{"class":195},[44,1471,1472,1475,1477],{"class":46,"line":375},[44,1473,1474],{"class":106},"chart.y_axis.delete ",[44,1476,143],{"class":102},[44,1478,1479],{"class":195}," True\n",[44,1481,1482,1485,1487],{"class":46,"line":954},[44,1483,1484],{"class":106},"chart.x_axis.delete ",[44,1486,143],{"class":102},[44,1488,1002],{"class":195},[44,1490,1491,1494,1496,1499,1501],{"class":46,"line":965},[44,1492,1493],{"class":106},"chart.width, chart.height ",[44,1495,143],{"class":102},[44,1497,1498],{"class":195}," 14",[44,1500,158],{"class":106},[44,1502,1503],{"class":195},"12\n",[44,1505,1506,1509,1512],{"class":46,"line":983},[44,1507,1508],{"class":106},"ws.add_chart(chart, ",[44,1510,1511],{"class":54},"\"A5\"",[44,1513,758],{"class":106},[44,1515,1516,1519,1522],{"class":46,"line":994},[44,1517,1518],{"class":106},"wb.save(",[44,1520,1521],{"class":54},"\"slope_chart.xlsx\"",[44,1523,758],{"class":106},[10,1525,1526,1527,1529,1530,27],{},"Each series gets two individual labels: point 0 (2025) shows its value to the left of the marker, point 1 (2026) shows the series name and value to the right, separated by a space — \"North 79\". The series-wide ",[41,1528,1293],{}," is off, so only these two labels appear. With every line labelled, the legend is dropped; the value axis is deleted too, because the labels carry the numbers and the gridlines would only add clutter. The axis range of 50 to 90 keeps the lines from crowding into the top of the plot, which an axis starting at zero would do. Labels of lines that end close together can overlap — North and West at 79 and 78, East and Islands at 71 and 70 here — so for denser data, make the chart taller, round the values, or leave the right-hand labels off the less important lines. More styling options are in ",[23,1531,1533],{"href":1532},"\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",[383,1535,392,1541,392,1544,392,1547,392,1551,392,1558,392,1567,392,1573,392,1578,392,1581,392,1585,392,1589,392,1593,392,1596,392,1603,392,1608,392,1612,392,1615,392,1618,392,1621],{"viewBox":1536,"role":386,"ariaLabelledBy":1537,"xmlns":390,"style":1540},"4 0 752 233",[1538,1539],"sl-vs-t","sl-vs-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[394,1542,1543],{"id":1538},"Clustered columns or a slope chart",[398,1545,1546],{"id":1539},"Clustered columns place two bars per region and need a legend to tell the years apart; changes have to be judged by comparing bar heights. A slope chart draws one line per region from 2025 to 2026, so direction and size of change and rank swaps are visible at once and labels replace the legend.",[402,1548],{"x":1032,"y":405,"width":1549,"height":1550,"fill":408},"752","233",[402,1552],{"x":1553,"y":1554,"width":1555,"height":1556,"rx":1557,"fill":415,"stroke":416,"style":604},"20.0","16.0","351.0","201.0","14",[1559,1560,1562],"g",{"transform":1561},"translate(182.5,29.0) scale(1.083)",[1130,1563],{"d":1564,"fill":1133,"stroke":1565,"style":1566},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8","var(--text,#172033)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[423,1568,1572],{"x":1569,"y":1570,"style":1571},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Clustered columns",[46,1574],{"x1":1575,"y1":1576,"x2":1577,"y2":1576,"stroke":416,"style":417},"36.0","91.0","355.0",[423,1579,1580],{"x":1569,"y":475,"style":481},"two bars per region",[423,1582,1584],{"x":1569,"y":1583,"style":481},"137.0","legend for the years",[423,1586,1588],{"x":1569,"y":1587,"style":481},"163.0","change judged bar by bar",[423,1590,1592],{"x":1569,"y":1591,"style":481},"189.0","rank swaps hard to see",[402,1594],{"x":1595,"y":1554,"width":1555,"height":1556,"rx":1557,"fill":541,"stroke":542,"style":604},"389.0",[1559,1597,1599],{"transform":1598},"translate(551.5,29.0) scale(1.083)",[1130,1600],{"d":1601,"fill":1133,"stroke":1602,"style":1566},"M4 20h16 M4 20V4 M6 16l4-5 4 3 6-7","var(--teal-ink,#0b6157)",[423,1604,1607],{"x":1605,"y":1570,"style":1606},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Slope chart",[46,1609],{"x1":1610,"y1":1576,"x2":1611,"y2":1576,"stroke":542,"style":417},"405.0","724.0",[423,1613,1614],{"x":1605,"y":475,"style":481},"one line per region",[423,1616,1617],{"x":1605,"y":1583,"style":481},"labels at the line ends",[423,1619,1620],{"x":1605,"y":1587,"style":481},"slope shows direction and size",[423,1622,1623],{"x":1605,"y":1591,"style":481},"crossings show rank swaps",[29,1625,1627],{"id":1626},"step-4-render-and-check","Step 4 — Render and check",[10,1629,1630],{},"openpyxl writes the chart definition; nothing in Python draws it. Render the workbook and check that the coloured lines are actually there:",[34,1632,1634],{"className":93,"code":1633,"language":95,"meta":39,"style":39},"import subprocess\nfrom PIL import Image\n\nsubprocess.run([\"soffice\", \"--headless\", \"--convert-to\", \"pdf\", \"slope_chart.xlsx\"], check=True, capture_output=True)\nsubprocess.run([\"pdftoppm\", \"-png\", \"-r\", \"80\", \"-f\", \"1\", \"-l\", \"1\", \"slope_chart.pdf\", \"slope\"], check=True)\nimg = Image.open(\"slope-1.png\").convert(\"RGB\")\ncounts = {colour: n for n, colour in img.getcolors(maxcolors=2_000_000)}\nfor label, hexcode in [(\"rising (green)\", UP), (\"falling (red)\", DOWN), (\"flat (grey)\", FLAT)]:\n    rgb = tuple(int(hexcode[i:i + 2], 16) for i in (0, 2, 4))\n    print(f\"{label:16} {counts.get(rgb, 0):>6} px\")\n",[41,1635,1636,1643,1656,1660,1707,1768,1789,1820,1861,1915],{"__ignoreMap":39},[44,1637,1638,1640],{"class":46,"line":47},[44,1639,103],{"class":102},[44,1641,1642],{"class":106}," subprocess\n",[44,1644,1645,1647,1650,1653],{"class":46,"line":116},[44,1646,119],{"class":102},[44,1648,1649],{"class":195}," PIL",[44,1651,1652],{"class":102}," import",[44,1654,1655],{"class":106}," Image\n",[44,1657,1658],{"class":46,"line":130},[44,1659,134],{"emptyLinePlaceholder":133},[44,1661,1662,1665,1668,1670,1673,1675,1678,1680,1683,1685,1687,1689,1692,1694,1696,1698,1701,1703,1705],{"class":46,"line":137},[44,1663,1664],{"class":106},"subprocess.run([",[44,1666,1667],{"class":54},"\"soffice\"",[44,1669,158],{"class":106},[44,1671,1672],{"class":54},"\"--headless\"",[44,1674,158],{"class":106},[44,1676,1677],{"class":54},"\"--convert-to\"",[44,1679,158],{"class":106},[44,1681,1682],{"class":54},"\"pdf\"",[44,1684,158],{"class":106},[44,1686,1521],{"class":54},[44,1688,224],{"class":106},[44,1690,1691],{"class":705},"check",[44,1693,143],{"class":102},[44,1695,755],{"class":195},[44,1697,158],{"class":106},[44,1699,1700],{"class":705},"capture_output",[44,1702,143],{"class":102},[44,1704,755],{"class":195},[44,1706,758],{"class":106},[44,1708,1709,1711,1714,1716,1719,1721,1724,1726,1729,1731,1734,1736,1739,1741,1744,1746,1748,1750,1753,1755,1758,1760,1762,1764,1766],{"class":46,"line":187},[44,1710,1664],{"class":106},[44,1712,1713],{"class":54},"\"pdftoppm\"",[44,1715,158],{"class":106},[44,1717,1718],{"class":54},"\"-png\"",[44,1720,158],{"class":106},[44,1722,1723],{"class":54},"\"-r\"",[44,1725,158],{"class":106},[44,1727,1728],{"class":54},"\"80\"",[44,1730,158],{"class":106},[44,1732,1733],{"class":54},"\"-f\"",[44,1735,158],{"class":106},[44,1737,1738],{"class":54},"\"1\"",[44,1740,158],{"class":106},[44,1742,1743],{"class":54},"\"-l\"",[44,1745,158],{"class":106},[44,1747,1738],{"class":54},[44,1749,158],{"class":106},[44,1751,1752],{"class":54},"\"slope_chart.pdf\"",[44,1754,158],{"class":106},[44,1756,1757],{"class":54},"\"slope\"",[44,1759,224],{"class":106},[44,1761,1691],{"class":705},[44,1763,143],{"class":102},[44,1765,755],{"class":195},[44,1767,758],{"class":106},[44,1769,1770,1773,1775,1778,1781,1784,1787],{"class":46,"line":263},[44,1771,1772],{"class":106},"img ",[44,1774,143],{"class":102},[44,1776,1777],{"class":106}," Image.open(",[44,1779,1780],{"class":54},"\"slope-1.png\"",[44,1782,1783],{"class":106},").convert(",[44,1785,1786],{"class":54},"\"RGB\"",[44,1788,758],{"class":106},[44,1790,1791,1794,1796,1799,1801,1804,1806,1809,1812,1814,1817],{"class":46,"line":283},[44,1792,1793],{"class":106},"counts ",[44,1795,143],{"class":102},[44,1797,1798],{"class":106}," {colour: n ",[44,1800,352],{"class":102},[44,1802,1803],{"class":106}," n, colour ",[44,1805,358],{"class":102},[44,1807,1808],{"class":106}," img.getcolors(",[44,1810,1811],{"class":705},"maxcolors",[44,1813,143],{"class":102},[44,1815,1816],{"class":195},"2_000_000",[44,1818,1819],{"class":106},")}\n",[44,1821,1822,1824,1827,1829,1832,1835,1837,1839,1842,1845,1847,1849,1851,1854,1856,1858],{"class":46,"line":294},[44,1823,352],{"class":102},[44,1825,1826],{"class":106}," label, hexcode ",[44,1828,358],{"class":102},[44,1830,1831],{"class":106}," [(",[44,1833,1834],{"class":54},"\"rising (green)\"",[44,1836,158],{"class":106},[44,1838,797],{"class":195},[44,1840,1841],{"class":106},"), (",[44,1843,1844],{"class":54},"\"falling (red)\"",[44,1846,158],{"class":106},[44,1848,802],{"class":195},[44,1850,1841],{"class":106},[44,1852,1853],{"class":54},"\"flat (grey)\"",[44,1855,158],{"class":106},[44,1857,807],{"class":195},[44,1859,1860],{"class":106},")]:\n",[44,1862,1863,1866,1868,1871,1873,1876,1879,1882,1885,1887,1890,1893,1895,1898,1900,1903,1905,1907,1909,1911,1913],{"class":46,"line":299},[44,1864,1865],{"class":106},"    rgb ",[44,1867,143],{"class":102},[44,1869,1870],{"class":195}," tuple",[44,1872,1234],{"class":106},[44,1874,1875],{"class":195},"int",[44,1877,1878],{"class":106},"(hexcode[i:i ",[44,1880,1881],{"class":102},"+",[44,1883,1884],{"class":195}," 2",[44,1886,224],{"class":106},[44,1888,1889],{"class":195},"16",[44,1891,1892],{"class":106},") ",[44,1894,352],{"class":102},[44,1896,1897],{"class":106}," i ",[44,1899,358],{"class":102},[44,1901,1902],{"class":106}," (",[44,1904,405],{"class":195},[44,1906,158],{"class":106},[44,1908,476],{"class":195},[44,1910,158],{"class":106},[44,1912,1032],{"class":195},[44,1914,788],{"class":106},[44,1916,1917,1920,1922,1925,1928,1932,1935,1938,1941,1944,1947,1949,1952,1955,1957,1960],{"class":46,"line":310},[44,1918,1919],{"class":195},"    print",[44,1921,1234],{"class":106},[44,1923,1924],{"class":102},"f",[44,1926,1927],{"class":54},"\"",[44,1929,1931],{"class":1930},"sSjpA","{",[44,1933,1934],{"class":106},"label",[44,1936,1937],{"class":102},":16",[44,1939,1940],{"class":1930},"}",[44,1942,1943],{"class":1930}," {",[44,1945,1946],{"class":106},"counts.get(rgb, ",[44,1948,405],{"class":195},[44,1950,1951],{"class":106},")",[44,1953,1954],{"class":102},":>6",[44,1956,1940],{"class":1930},[44,1958,1959],{"class":54}," px\"",[44,1961,758],{"class":106},[10,1963,1964,1965,1968,1969,1971,1972,1974,1975,1977,1978,1980],{},"All three colours appear in the rendered page — three rising regions in green, two falling in red and West in grey — which confirms the series, their colours and the markers survived. Looking at the image itself shows the chart reading as intended: South and North cross, Islands climbs most steeply, Central falls furthest. Two rendering details are worth knowing. The individual data-label flags matter: when a ",[41,1966,1967],{},"DataLabel"," leaves ",[41,1970,1237],{}," and ",[41,1973,1247],{}," unset, LibreOffice shows the category and a legend swatch in every label (\"2025; 81\" with a coloured line), so the code sets them explicitly to ",[41,1976,1242],{},". And LibreOffice joins series name and value with \"; \" regardless of the ",[41,1979,1365],{}," set, so expect \"North; 79\" there and \"North 79\" where the separator is honoured. Placing the chart below the data, at A5, keeps it on the first printed page; anchored far to the right it would be split across pages in a PDF export. Counting pixels of known colours is a cheap automated test for any chart where the styling carries the meaning; a missing colour means a series was dropped or styled wrongly. For a visual check during development, open the PNG.",[29,1982,1984],{"id":1983},"common-pitfalls","Common pitfalls",[1986,1987,1988,1996,2002,2011,2017],"ul",{},[1989,1990,1991,1995],"li",{},[1992,1993,1994],"strong",{},"Periods as series."," Items must be the series and the two periods the categories.",[1989,1997,1998,2001],{},[1992,1999,2000],{},"Numeric category labels."," Write the years as text so they sit evenly.",[1989,2003,2004,2007,2008,2010],{},[1992,2005,2006],{},"Smoothed lines."," Set ",[41,2009,1015],{}," on every series.",[1989,2012,2013,2016],{},[1992,2014,2015],{},"A legend for six colours."," Label the line ends with series names instead.",[1989,2018,2019,2022],{},[1992,2020,2021],{},"An axis from zero."," Narrow the range so the slopes are visible.",[29,2024,2026],{"id":2025},"conclusion","Conclusion",[10,2028,2029,2030,2033,2034,2036],{},"A slope chart in Excel is a line chart with two categories and one series per item. Write the data with periods down the rows and items across, build a ",[41,2031,2032],{},"LineChart"," from it, colour each series by the size and direction of its change, set ",[41,2035,1015],{},", label point 0 with its value and point 1 with the series name and value, drop the legend and the value axis, and narrow the axis range. Render the result to confirm the lines and colours are there.",[29,2038,2040],{"id":2039},"frequently-asked-questions","Frequently asked questions",[10,2042,2043,2046],{},[1992,2044,2045],{},"How do I make a slope chart in Excel with Python?","\nWrite one column per item with two rows, one per period, and build an openpyxl LineChart with those columns as series and the two periods as categories.",[10,2048,2049,2052],{},[1992,2050,2051],{},"How do I label the end of each line instead of using a legend?","\nGive each series a DataLabelList with a DataLabel for the last point that shows the series name and value, and remove the chart legend.",[10,2054,2055,2058],{},[1992,2056,2057],{},"How do I colour rising and falling lines differently?","\nCompute each item's change in Python and set the series line and marker colour from it, for example green for increases and red for decreases.",[10,2060,2061,2064],{},[1992,2062,2063],{},"Why are my openpyxl line chart lines curved?","\nopenpyxl can write a smoothing flag on line series; set series.smooth = False on each series.",[29,2066,2068],{"id":2067},"related","Related",[1986,2070,2071,2077,2084,2091],{},[1989,2072,2073,2074,2076],{},"Up one level: ",[23,2075,26],{"href":25}," — charts from Python.",[1989,2078,2079,2083],{},[23,2080,2082],{"href":2081},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-line-chart-to-excel-report-with-python\u002F","Add a Line Chart to an Excel Report with Python"," — line chart basics.",[1989,2085,2086,2090],{},[23,2087,2089],{"href":2088},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-butterfly-chart-in-excel-with-python\u002F","Create a Butterfly Chart in Excel with Python"," — another comparison chart.",[1989,2092,2093,2097],{},[23,2094,2096],{"href":2095},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fformat-chart-axes-with-openpyxl\u002F","Format Chart Axes with openpyxl"," — axis settings.",[2099,2100,2101],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":39,"searchDepth":116,"depth":116,"links":2103},[2104,2105,2106,2107,2108,2109,2110,2111,2112],{"id":31,"depth":116,"text":32},{"id":78,"depth":116,"text":79},{"id":629,"depth":116,"text":630},{"id":1190,"depth":116,"text":1191},{"id":1626,"depth":116,"text":1627},{"id":1983,"depth":116,"text":1984},{"id":2025,"depth":116,"text":2026},{"id":2039,"depth":116,"text":2040},{"id":2067,"depth":116,"text":2068},"2026-10-02","Build a slope chart in Excel with openpyxl to compare each item between two periods: lay the data out with one series per item, draw a line chart with only two category points, colour rising and falling items differently, label each line at its ends instead of using a legend, tidy the axes, and check the rendered chart.","md",[2117,2119,2121,2123],{"q":2045,"a":2118},"Write one column per item with two rows, one per period, and build an openpyxl LineChart with those columns as series and the two periods as categories.",{"q":2051,"a":2120},"Give each series a DataLabelList with a DataLabel for the last point that shows the series name and value, and remove the chart legend.",{"q":2057,"a":2122},"Compute each item's change in Python and set the series line and marker colour from it, for example green for increases and red for decreases.",{"q":2063,"a":2124},"openpyxl can write a smoothing flag on line series; set series.smooth = False on each series.","create a slope chart in excel with python",null,{"breadcrumb":2128},[2129,2132,2135],{"name":2130,"item":2131},"Home","\u002F",{"name":2133,"item":2134},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002F",{"name":26,"item":25},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-slope-chart-in-excel-with-python",{"title":2138,"description":2139},"Create a Slope Chart in Excel with Python and openpyxl","Slope charts with openpyxl: data with one series per item, a two-point LineChart, colours for rising and falling items, end-of-line data labels with series names, axis settings, and rendering to check the result.","create-a-slope-chart-in-excel-with-python","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-slope-chart-in-excel-with-python\u002Findex",[2143,2144],"charts-and-images","openpyxl","how-to","1qRDzULyVeb8y1k7SL_1eqUTDYXtuiKdz2WkQxEJqOs",[2148,2152],{"title":2149,"path":2150,"stem":2151,"children":-1},"Create a Scatter Chart in Excel with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-scatter-chart-in-excel-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-scatter-chart-in-excel-with-openpyxl\u002Findex",{"title":2153,"path":2154,"stem":2155,"children":-1},"Create a Stacked Column Chart with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-stacked-column-chart-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-stacked-column-chart-with-openpyxl\u002Findex",[2157,2159],{"path":2158,"title":26},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl",{"path":2160,"title":2133},"\u002Fformatting-and-charting-excel-reports-with-python",[],[2163,2166,2169,2172,2175,2178],{"path":2164,"title":2165,"kind":2145},"\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":2167,"title":2168,"kind":2145},"\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":2170,"title":2171,"kind":2145},"\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":2173,"title":2174,"kind":2145},"\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":2176,"title":2177,"kind":2145},"\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":2179,"title":2180,"kind":2145},"\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",1790972111114]