[{"data":1,"prerenderedAt":2378},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-step-chart-in-excel-with-python":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-step-chart-in-excel-with-python":2344,"ancestors:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-step-chart-in-excel-with-python":2353,"children:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-step-chart-in-excel-with-python":2358,"related:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-step-chart-in-excel-with-python":2359},{"id":4,"title":5,"body":6,"dateModified":2310,"datePublished":2310,"description":2311,"extension":2312,"faq":2313,"keyword":2322,"kind":2323,"meta":2324,"navigation":108,"path":2333,"seo":2334,"slug":2337,"stem":2338,"topics":2339,"type":2342,"__hash__":2343},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-step-chart-in-excel-with-python\u002Findex.md","Create a Step Chart in Excel with Python",{"type":7,"value":8,"toc":2298},"minimark",[9,28,33,65,76,80,379,382,493,497,500,686,689,929,933,940,1679,1698,1702,2017,2020,2164,2168,2187,2191,2226,2230,2233,2237,2243,2249,2255,2261,2265,2294],[10,11,12,13,17,18,21,22,27],"p",{},"Some numbers do not drift from one value to the next; they ",[14,15,16],"em",{},"jump",". A list price is 4.50 until 15 March and 4.80 from then on. A base rate holds for months and then moves a quarter point overnight. Headcount, contract value and stock on a shelf after a delivery behave the same way. A normal line chart joins each point to the next with a sloping line, which suggests the price was 4.65 on 1 March — a value it never had. A ",[14,19,20],{},"step chart"," draws such data honestly: flat lines while the value holds and vertical jumps where it changes. Excel has no step chart type, but a scatter chart with straight lines drawn through doubled-up points produces one exactly. This page prepares the data with pandas, builds the chart with openpyxl on a real date axis, extends the final value to the end of the period, compares two series, and renders the result to check it. 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"," are used only to render the chart for checking. The data is a price history: the dates on which a product's list price changed and the new price from each date, plus the same for a competitor.",[29,77,79],{"id":78},"step-1-why-a-line-chart-misleads","Step 1 — Why a line chart misleads",[34,81,85],{"className":82,"code":83,"language":84,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nours = pd.DataFrame({\"date\": pd.to_datetime([\"2026-01-01\", \"2026-03-15\", \"2026-06-01\", \"2026-08-20\"]),\n                     \"price\": [4.50, 4.80, 4.65, 5.10]})\ntheirs = pd.DataFrame({\"date\": pd.to_datetime([\"2026-01-01\", \"2026-02-10\", \"2026-05-05\", \"2026-09-01\"]),\n                       \"price\": [4.70, 4.60, 4.95, 4.85]})\nPERIOD_END = pd.Timestamp(\"2026-09-30\")\n\non_1_march = ours.set_index(\"date\")[\"price\"].reindex(pd.date_range(\"2026-01-01\", \"2026-09-30\")).ffill()[\"2026-03-01\"]\ninterp = ours.set_index(\"date\")[\"price\"].reindex(pd.date_range(\"2026-01-01\", \"2026-09-30\")).interpolate()[\"2026-03-01\"]\nprint(f\"price on 1 March: actual {on_1_march:.2f}, what a line chart implies {interp:.3f}\")\n","python",[41,86,87,103,110,150,181,214,242,260,265,302,333],{"__ignoreMap":39},[44,88,89,93,97,100],{"class":46,"line":47},[44,90,92],{"class":91},"s-kum","import",[44,94,96],{"class":95},"skGVy"," pandas ",[44,98,99],{"class":91},"as",[44,101,102],{"class":95}," pd\n",[44,104,106],{"class":46,"line":105},2,[44,107,109],{"emptyLinePlaceholder":108},true,"\n",[44,111,113,116,119,122,125,128,131,134,137,139,142,144,147],{"class":46,"line":112},3,[44,114,115],{"class":95},"ours ",[44,117,118],{"class":91},"=",[44,120,121],{"class":95}," pd.DataFrame({",[44,123,124],{"class":54},"\"date\"",[44,126,127],{"class":95},": pd.to_datetime([",[44,129,130],{"class":54},"\"2026-01-01\"",[44,132,133],{"class":95},", ",[44,135,136],{"class":54},"\"2026-03-15\"",[44,138,133],{"class":95},[44,140,141],{"class":54},"\"2026-06-01\"",[44,143,133],{"class":95},[44,145,146],{"class":54},"\"2026-08-20\"",[44,148,149],{"class":95},"]),\n",[44,151,153,156,159,163,165,168,170,173,175,178],{"class":46,"line":152},4,[44,154,155],{"class":54},"                     \"price\"",[44,157,158],{"class":95},": [",[44,160,162],{"class":161},"sP0c6","4.50",[44,164,133],{"class":95},[44,166,167],{"class":161},"4.80",[44,169,133],{"class":95},[44,171,172],{"class":161},"4.65",[44,174,133],{"class":95},[44,176,177],{"class":161},"5.10",[44,179,180],{"class":95},"]})\n",[44,182,184,187,189,191,193,195,197,199,202,204,207,209,212],{"class":46,"line":183},5,[44,185,186],{"class":95},"theirs ",[44,188,118],{"class":91},[44,190,121],{"class":95},[44,192,124],{"class":54},[44,194,127],{"class":95},[44,196,130],{"class":54},[44,198,133],{"class":95},[44,200,201],{"class":54},"\"2026-02-10\"",[44,203,133],{"class":95},[44,205,206],{"class":54},"\"2026-05-05\"",[44,208,133],{"class":95},[44,210,211],{"class":54},"\"2026-09-01\"",[44,213,149],{"class":95},[44,215,217,220,222,225,227,230,232,235,237,240],{"class":46,"line":216},6,[44,218,219],{"class":54},"                       \"price\"",[44,221,158],{"class":95},[44,223,224],{"class":161},"4.70",[44,226,133],{"class":95},[44,228,229],{"class":161},"4.60",[44,231,133],{"class":95},[44,233,234],{"class":161},"4.95",[44,236,133],{"class":95},[44,238,239],{"class":161},"4.85",[44,241,180],{"class":95},[44,243,245,248,251,254,257],{"class":46,"line":244},7,[44,246,247],{"class":161},"PERIOD_END",[44,249,250],{"class":91}," =",[44,252,253],{"class":95}," pd.Timestamp(",[44,255,256],{"class":54},"\"2026-09-30\"",[44,258,259],{"class":95},")\n",[44,261,263],{"class":46,"line":262},8,[44,264,109],{"emptyLinePlaceholder":108},[44,266,268,271,273,276,278,281,284,287,289,291,293,296,299],{"class":46,"line":267},9,[44,269,270],{"class":95},"on_1_march ",[44,272,118],{"class":91},[44,274,275],{"class":95}," ours.set_index(",[44,277,124],{"class":54},[44,279,280],{"class":95},")[",[44,282,283],{"class":54},"\"price\"",[44,285,286],{"class":95},"].reindex(pd.date_range(",[44,288,130],{"class":54},[44,290,133],{"class":95},[44,292,256],{"class":54},[44,294,295],{"class":95},")).ffill()[",[44,297,298],{"class":54},"\"2026-03-01\"",[44,300,301],{"class":95},"]\n",[44,303,305,308,310,312,314,316,318,320,322,324,326,329,331],{"class":46,"line":304},10,[44,306,307],{"class":95},"interp ",[44,309,118],{"class":91},[44,311,275],{"class":95},[44,313,124],{"class":54},[44,315,280],{"class":95},[44,317,283],{"class":54},[44,319,286],{"class":95},[44,321,130],{"class":54},[44,323,133],{"class":95},[44,325,256],{"class":54},[44,327,328],{"class":95},")).interpolate()[",[44,330,298],{"class":54},[44,332,301],{"class":95},[44,334,336,339,342,345,348,352,355,358,361,364,366,369,372,374,377],{"class":46,"line":335},11,[44,337,338],{"class":161},"print",[44,340,341],{"class":95},"(",[44,343,344],{"class":91},"f",[44,346,347],{"class":54},"\"price on 1 March: actual ",[44,349,351],{"class":350},"sSjpA","{",[44,353,354],{"class":95},"on_1_march",[44,356,357],{"class":91},":.2f",[44,359,360],{"class":350},"}",[44,362,363],{"class":54},", what a line chart implies ",[44,365,351],{"class":350},[44,367,368],{"class":95},"interp",[44,370,371],{"class":91},":.3f",[44,373,360],{"class":350},[44,375,376],{"class":54},"\"",[44,378,259],{"class":95},[10,380,381],{},"On 1 March the price was still 4.50 — the change came on the 15th. A line chart joining 1 January at 4.50 to 15 March at 4.80 passes through 4.74 on 1 March, which never happened. For prices, rates and levels the question readers ask is \"what was it on date X?\", and only a step chart answers it correctly.",[383,384,392,393,392,397,392,401,392,409,392,419,392,430,392,437,392,443,392,448,392,452,392,456,392,460,392,465,392,472,392,477,392,481,392,484,392,487,392,490],"svg",{"viewBox":385,"role":386,"ariaLabelledBy":387,"xmlns":390,"style":391},"4 0 752 233","img",[388,389],"st-vs-t","st-vs-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[394,395,396],"title",{"id":388},"Line chart or step chart for a price history",[398,399,400],"desc",{"id":389},"A line chart joins the change points with sloping lines, implying intermediate prices that never existed, such as 4.74 on 1 March. A step chart holds each price flat until the next change and jumps vertically, so the value on any date can be read directly.",[402,403],"rect",{"x":404,"y":405,"width":406,"height":407,"fill":408},"4","0","752","233","#ffffff",[402,410],{"x":411,"y":412,"width":413,"height":414,"rx":415,"fill":416,"stroke":417,"style":418},"20.0","16.0","351.0","201.0","14","#fdefd8","var(--gold,#b4740a)","stroke-width:2px",[420,421,423],"g",{"transform":422},"translate(182.5,29.0) scale(1.083)",[424,425],"path",{"d":426,"fill":427,"stroke":428,"style":429},"M4 20h16 M4 20V4 M6 16l4-5 4 3 6-7","none","var(--gold-ink,#7a4e06)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[431,432,436],"text",{"x":433,"y":434,"style":435},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Line chart",[46,438],{"x1":439,"y1":440,"x2":441,"y2":440,"stroke":417,"style":442},"36.0","91.0","355.0","stroke-width:1px",[431,444,447],{"x":433,"y":445,"style":446},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","slopes between changes",[431,449,451],{"x":433,"y":450,"style":446},"137.0","implies 4.74 on 1 March",[431,453,455],{"x":433,"y":454,"style":446},"163.0","last value ends at last change",[431,457,459],{"x":433,"y":458,"style":446},"189.0","misleading for levels",[402,461],{"x":462,"y":412,"width":413,"height":414,"rx":415,"fill":463,"stroke":464,"style":418},"389.0","#d9f4f1","var(--teal,#0f9488)",[420,466,468],{"transform":467},"translate(551.5,29.0) scale(1.083)",[424,469],{"d":470,"fill":427,"stroke":471,"style":429},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8","var(--teal-ink,#0b6157)",[431,473,476],{"x":474,"y":434,"style":475},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Step chart",[46,478],{"x1":479,"y1":440,"x2":480,"y2":440,"stroke":464,"style":442},"405.0","724.0",[431,482,483],{"x":474,"y":445,"style":446},"flat while the value holds",[431,485,486],{"x":474,"y":450,"style":446},"vertical jump at each change",[431,488,489],{"x":474,"y":454,"style":446},"extended to period end",[431,491,492],{"x":474,"y":458,"style":446},"reads 4.50 on 1 March",[29,494,496],{"id":495},"step-2-double-up-the-change-points","Step 2 — Double up the change points",[10,498,499],{},"A step is two points at the same date: the old value and the new value. Build the step series by repeating each change date with the previous price, then add a final point at the end of the period so the last price is drawn for as long as it applied:",[34,501,503],{"className":82,"code":502,"language":84,"meta":39,"style":39},"def steps(df, end):\n    rows = []\n    for i, (date, price) in enumerate(zip(df[\"date\"], df[\"price\"])):\n        if i:\n            rows.append((date, df[\"price\"].iloc[i - 1]))      # the old value up to the change\n        rows.append((date, price))                              # the new value from the change\n    rows.append((end, df[\"price\"].iloc[-1]))                    # hold the last value to the period end\n    return pd.DataFrame(rows, columns=[\"date\", \"price\"])\n\nours_steps, theirs_steps = steps(ours, PERIOD_END), steps(theirs, PERIOD_END)\nprint(ours_steps.to_string(index=False))\n",[41,504,505,517,527,559,567,590,598,619,645,649,668],{"__ignoreMap":39},[44,506,507,510,514],{"class":46,"line":47},[44,508,509],{"class":91},"def",[44,511,513],{"class":512},"s_Opv"," steps",[44,515,516],{"class":95},"(df, end):\n",[44,518,519,522,524],{"class":46,"line":105},[44,520,521],{"class":95},"    rows ",[44,523,118],{"class":91},[44,525,526],{"class":95}," []\n",[44,528,529,532,535,538,541,543,546,549,551,554,556],{"class":46,"line":112},[44,530,531],{"class":91},"    for",[44,533,534],{"class":95}," i, (date, price) ",[44,536,537],{"class":91},"in",[44,539,540],{"class":161}," enumerate",[44,542,341],{"class":95},[44,544,545],{"class":161},"zip",[44,547,548],{"class":95},"(df[",[44,550,124],{"class":54},[44,552,553],{"class":95},"], df[",[44,555,283],{"class":54},[44,557,558],{"class":95},"])):\n",[44,560,561,564],{"class":46,"line":152},[44,562,563],{"class":91},"        if",[44,565,566],{"class":95}," i:\n",[44,568,569,572,574,577,580,583,586],{"class":46,"line":183},[44,570,571],{"class":95},"            rows.append((date, df[",[44,573,283],{"class":54},[44,575,576],{"class":95},"].iloc[i ",[44,578,579],{"class":91},"-",[44,581,582],{"class":161}," 1",[44,584,585],{"class":95},"]))      ",[44,587,589],{"class":588},"s-wDw","# the old value up to the change\n",[44,591,592,595],{"class":46,"line":216},[44,593,594],{"class":95},"        rows.append((date, price))                              ",[44,596,597],{"class":588},"# the new value from the change\n",[44,599,600,603,605,608,610,613,616],{"class":46,"line":244},[44,601,602],{"class":95},"    rows.append((end, df[",[44,604,283],{"class":54},[44,606,607],{"class":95},"].iloc[",[44,609,579],{"class":91},[44,611,612],{"class":161},"1",[44,614,615],{"class":95},"]))                    ",[44,617,618],{"class":588},"# hold the last value to the period end\n",[44,620,621,624,627,631,633,636,638,640,642],{"class":46,"line":262},[44,622,623],{"class":91},"    return",[44,625,626],{"class":95}," pd.DataFrame(rows, ",[44,628,630],{"class":629},"sa561","columns",[44,632,118],{"class":91},[44,634,635],{"class":95},"[",[44,637,124],{"class":54},[44,639,133],{"class":95},[44,641,283],{"class":54},[44,643,644],{"class":95},"])\n",[44,646,647],{"class":46,"line":267},[44,648,109],{"emptyLinePlaceholder":108},[44,650,651,654,656,659,661,664,666],{"class":46,"line":304},[44,652,653],{"class":95},"ours_steps, theirs_steps ",[44,655,118],{"class":91},[44,657,658],{"class":95}," steps(ours, ",[44,660,247],{"class":161},[44,662,663],{"class":95},"), steps(theirs, ",[44,665,247],{"class":161},[44,667,259],{"class":95},[44,669,670,672,675,678,680,683],{"class":46,"line":335},[44,671,338],{"class":161},[44,673,674],{"class":95},"(ours_steps.to_string(",[44,676,677],{"class":629},"index",[44,679,118],{"class":91},[44,681,682],{"class":161},"False",[44,684,685],{"class":95},"))\n",[10,687,688],{},"Four price changes become eight points: the first price, then for each later change one point with the old price and one with the new price on the same date, and a final point on 30 September. Joined with straight lines, consecutive points with the same date draw the vertical jump and consecutive points with the same price draw the flat segment. Forgetting the last point is the most common mistake — the chart then stops at the last change, as if the price did not exist afterwards.",[383,690,392,696,392,699,392,702,392,707,392,715,392,719,392,725,392,729,392,733,392,737,392,741,392,743,392,747,392,749,392,752,392,755,392,758,392,760,392,763,392,765,392,767,392,770,392,774,392,776,392,779,392,781,392,783,392,786,392,789,392,791,392,794,392,796,392,798,392,801,392,804,392,807,392,810,392,813,392,815,392,818,392,820,392,822,392,824,392,826,392,828,392,830,392,832,392,834,392,836,392,838,392,840,392,842,392,845,392,848,392,850,392,852,392,854,392,856,392,858,392,860,392,862,392,864,392,867,392,871,392,873,392,875,392,877,392,879,392,882,392,886,392,888,392,890,392,892,392,894,392,900,392,904,392,910,392,915,392,923],{"viewBox":691,"role":386,"ariaLabelledBy":692,"xmlns":390,"style":695},"147 0 465 248",[693,694],"st-points-t","st-points-d","width:100%;max-width:465px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[394,697,698],{"id":693},"From change points to step points",[398,700,701],{"id":694},"Four change dates with their new prices become eight chart points. Each change after the first adds a point with the old price on the change date before the new price, and a final point holds the last price until 30 September.",[402,703],{"x":704,"y":405,"width":705,"height":706,"fill":408},"147","465","248",[402,708],{"x":709,"y":710,"width":711,"height":712,"rx":405,"fill":713,"stroke":714,"style":442},"163.9","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)",[402,716],{"x":717,"y":710,"width":718,"height":712,"rx":405,"fill":713,"stroke":714,"style":442},"195.9","64.4",[431,720,724],{"x":721,"y":722,"style":723},"228.1","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[402,726],{"x":727,"y":710,"width":728,"height":712,"rx":405,"fill":713,"stroke":714,"style":442},"260.3","55.4",[431,730,732],{"x":731,"y":722,"style":723},"288.0","B",[402,734],{"x":709,"y":735,"width":711,"height":736,"rx":405,"fill":713,"stroke":714,"style":442},"64.0","28.0",[431,738,612],{"x":739,"y":740,"style":723},"179.9","83.0",[402,742],{"x":717,"y":735,"width":718,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,744,746],{"x":721,"y":740,"style":745},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","date",[402,748],{"x":727,"y":735,"width":728,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,750,751],{"x":731,"y":740,"style":745},"price",[402,753],{"x":709,"y":754,"width":711,"height":736,"rx":405,"fill":713,"stroke":714,"style":442},"92.0",[431,756,757],{"x":739,"y":445,"style":723},"2",[402,759],{"x":717,"y":754,"width":718,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,761,762],{"x":721,"y":445,"style":446},"01 Jan",[402,764],{"x":727,"y":754,"width":728,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,766,162],{"x":731,"y":445,"style":446},[402,768],{"x":709,"y":769,"width":711,"height":736,"rx":405,"fill":713,"stroke":714,"style":442},"120.0",[431,771,773],{"x":739,"y":772,"style":723},"139.0","3",[402,775],{"x":717,"y":769,"width":718,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,777,778],{"x":721,"y":772,"style":446},"15 Mar",[402,780],{"x":727,"y":769,"width":728,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,782,167],{"x":731,"y":772,"style":446},[402,784],{"x":709,"y":785,"width":711,"height":736,"rx":405,"fill":713,"stroke":714,"style":442},"148.0",[431,787,404],{"x":739,"y":788,"style":723},"167.0",[402,790],{"x":717,"y":785,"width":718,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,792,793],{"x":721,"y":788,"style":446},"01 Jun",[402,795],{"x":727,"y":785,"width":728,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,797,172],{"x":731,"y":788,"style":446},[402,799],{"x":800,"y":710,"width":711,"height":712,"rx":405,"fill":713,"stroke":714,"style":442},"444.3",[402,802],{"x":803,"y":710,"width":718,"height":712,"rx":405,"fill":713,"stroke":714,"style":442},"476.3",[431,805,724],{"x":806,"y":722,"style":723},"508.5",[402,808],{"x":809,"y":710,"width":728,"height":712,"rx":405,"fill":713,"stroke":714,"style":442},"540.7",[431,811,732],{"x":812,"y":722,"style":723},"568.4",[402,814],{"x":800,"y":735,"width":711,"height":736,"rx":405,"fill":713,"stroke":714,"style":442},[431,816,612],{"x":817,"y":740,"style":723},"460.3",[402,819],{"x":803,"y":735,"width":718,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,821,746],{"x":806,"y":740,"style":745},[402,823],{"x":809,"y":735,"width":728,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,825,751],{"x":812,"y":740,"style":745},[402,827],{"x":800,"y":754,"width":711,"height":736,"rx":405,"fill":713,"stroke":714,"style":442},[431,829,757],{"x":817,"y":445,"style":723},[402,831],{"x":803,"y":754,"width":718,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,833,762],{"x":806,"y":445,"style":446},[402,835],{"x":809,"y":754,"width":728,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,837,162],{"x":812,"y":445,"style":446},[402,839],{"x":800,"y":769,"width":711,"height":736,"rx":405,"fill":713,"stroke":714,"style":442},[431,841,773],{"x":817,"y":772,"style":723},[402,843],{"x":803,"y":769,"width":718,"height":736,"rx":405,"fill":416,"stroke":417,"style":844},"stroke-width:1.5px",[431,846,778],{"x":806,"y":772,"style":847},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[402,849],{"x":809,"y":769,"width":728,"height":736,"rx":405,"fill":416,"stroke":417,"style":844},[431,851,162],{"x":812,"y":772,"style":847},[402,853],{"x":800,"y":785,"width":711,"height":736,"rx":405,"fill":713,"stroke":714,"style":442},[431,855,404],{"x":817,"y":788,"style":723},[402,857],{"x":803,"y":785,"width":718,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,859,778],{"x":806,"y":788,"style":446},[402,861],{"x":809,"y":785,"width":728,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,863,167],{"x":812,"y":788,"style":446},[402,865],{"x":800,"y":866,"width":711,"height":736,"rx":405,"fill":713,"stroke":714,"style":442},"176.0",[431,868,870],{"x":817,"y":869,"style":723},"195.0","5",[402,872],{"x":803,"y":866,"width":718,"height":736,"rx":405,"fill":416,"stroke":417,"style":844},[431,874,793],{"x":806,"y":869,"style":847},[402,876],{"x":809,"y":866,"width":728,"height":736,"rx":405,"fill":416,"stroke":417,"style":844},[431,878,167],{"x":812,"y":869,"style":847},[402,880],{"x":800,"y":881,"width":711,"height":736,"rx":405,"fill":713,"stroke":714,"style":442},"204.0",[431,883,885],{"x":817,"y":884,"style":723},"223.0","6",[402,887],{"x":803,"y":881,"width":718,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,889,793],{"x":806,"y":884,"style":446},[402,891],{"x":809,"y":881,"width":728,"height":736,"rx":405,"fill":408,"stroke":714,"style":442},[431,893,172],{"x":812,"y":884,"style":446},[431,895,899],{"x":896,"y":897,"style":898},"239.8","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before",[431,901,903],{"x":902,"y":897,"style":898},"520.2","after",[46,905],{"x1":906,"y1":907,"x2":908,"y2":907,"stroke":909,"style":418},"325.7","108.0","430.3","var(--brand,#5b5cf0)",[911,912],"polygon",{"points":913,"fill":914},"434.3,108.0 425.3,113.0 425.3,103.0","#5b5cf0",[402,916],{"x":917,"y":918,"width":919,"height":920,"rx":921,"fill":408,"stroke":714,"style":922},"344.6","75.0","70.9","22.0","11","stroke-width:1.2px",[431,924,928],{"x":925,"y":926,"style":927},"380.0","90.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","steps()",[29,930,932],{"id":931},"step-3-a-scatter-chart-on-a-date-axis","Step 3 — A scatter chart on a date axis",[10,934,935,936,939],{},"A line chart in Excel treats its categories as evenly spaced labels, so two points on the same date would sit side by side. A ",[14,937,938],{},"scatter"," chart places points by their x value, so both points of a step land on the same date. Write the step points and draw them with straight lines and no markers:",[34,941,943],{"className":82,"code":942,"language":84,"meta":39,"style":39},"from openpyxl import Workbook\nfrom openpyxl.chart import Reference, ScatterChart, Series\nfrom openpyxl.drawing.line import LineProperties\n\nwb = Workbook()\nws = wb.active\nws.title = \"Prices\"\nws.append([\"Our date\", \"Our price\", \"\", \"Their date\", \"Their price\"])\nfor i in range(max(len(ours_steps), len(theirs_steps))):\n    row = [None] * 5\n    if i \u003C len(ours_steps):\n        row[0], row[1] = ours_steps[\"date\"][i].to_pydatetime(), ours_steps[\"price\"][i]\n    if i \u003C len(theirs_steps):\n        row[3], row[4] = theirs_steps[\"date\"][i].to_pydatetime(), theirs_steps[\"price\"][i]\n    ws.append(row)\nfor col in \"AD\":\n    for cell in ws[col][1:]:\n        cell.number_format = \"dd mmm\"\n\nchart = ScatterChart()\nchart.title = \"List price, ours and competitor's\"\nchart.style = 13\nfor x_col, y_col, colour, name in [(1, 2, \"1F6FEB\", \"Ours\"), (4, 5, \"D4A72C\", \"Competitor\")]:\n    count = len(ours_steps) if x_col == 1 else len(theirs_steps)\n    xs = Reference(ws, min_col=x_col, min_row=2, max_row=1 + count)\n    ys = Reference(ws, min_col=y_col, min_row=2, max_row=1 + count)\n    s = Series(ys, xs, title=name)\n    s.marker.symbol = \"none\"\n    s.smooth = False\n    s.graphicalProperties.line = LineProperties(solidFill=colour, w=31750)\n    chart.series.append(s)\nchart.x_axis.number_format = \"mmm\"\nchart.x_axis.scaling.min = pd.Timestamp(\"2026-01-01\").to_julian_date() - 2415018.5\nchart.x_axis.scaling.max = PERIOD_END.to_julian_date() - 2415018.5\nchart.x_axis.majorUnit = 31\nchart.y_axis.scaling.min, chart.y_axis.scaling.max = 4.4, 5.2\nchart.y_axis.number_format = \"0.00\"\nchart.x_axis.delete = chart.y_axis.delete = False\nchart.legend.position = \"b\"\nchart.width, chart.height = 18, 9\nws.add_chart(chart, \"A12\")\nwb.save(\"step_chart.xlsx\")\nprint(\"x axis from\", chart.x_axis.scaling.min, \"to\", chart.x_axis.scaling.max)\n",[41,944,945,958,970,982,986,996,1006,1016,1046,1077,1099,1115,1145,1159,1186,1192,1208,1226,1237,1242,1253,1264,1275,1326,1358,1399,1434,1452,1463,1474,1503,1509,1520,1540,1558,1569,1585,1596,1611,1622,1638,1649,1660],{"__ignoreMap":39},[44,946,947,950,953,955],{"class":46,"line":47},[44,948,949],{"class":91},"from",[44,951,952],{"class":95}," openpyxl ",[44,954,92],{"class":91},[44,956,957],{"class":95}," Workbook\n",[44,959,960,962,965,967],{"class":46,"line":105},[44,961,949],{"class":91},[44,963,964],{"class":95}," openpyxl.chart ",[44,966,92],{"class":91},[44,968,969],{"class":95}," Reference, ScatterChart, Series\n",[44,971,972,974,977,979],{"class":46,"line":112},[44,973,949],{"class":91},[44,975,976],{"class":95}," openpyxl.drawing.line ",[44,978,92],{"class":91},[44,980,981],{"class":95}," LineProperties\n",[44,983,984],{"class":46,"line":152},[44,985,109],{"emptyLinePlaceholder":108},[44,987,988,991,993],{"class":46,"line":183},[44,989,990],{"class":95},"wb ",[44,992,118],{"class":91},[44,994,995],{"class":95}," Workbook()\n",[44,997,998,1001,1003],{"class":46,"line":216},[44,999,1000],{"class":95},"ws ",[44,1002,118],{"class":91},[44,1004,1005],{"class":95}," wb.active\n",[44,1007,1008,1011,1013],{"class":46,"line":244},[44,1009,1010],{"class":95},"ws.title ",[44,1012,118],{"class":91},[44,1014,1015],{"class":54}," \"Prices\"\n",[44,1017,1018,1021,1024,1026,1029,1031,1034,1036,1039,1041,1044],{"class":46,"line":262},[44,1019,1020],{"class":95},"ws.append([",[44,1022,1023],{"class":54},"\"Our date\"",[44,1025,133],{"class":95},[44,1027,1028],{"class":54},"\"Our price\"",[44,1030,133],{"class":95},[44,1032,1033],{"class":54},"\"\"",[44,1035,133],{"class":95},[44,1037,1038],{"class":54},"\"Their date\"",[44,1040,133],{"class":95},[44,1042,1043],{"class":54},"\"Their price\"",[44,1045,644],{"class":95},[44,1047,1048,1051,1054,1056,1059,1061,1064,1066,1069,1072,1074],{"class":46,"line":267},[44,1049,1050],{"class":91},"for",[44,1052,1053],{"class":95}," i ",[44,1055,537],{"class":91},[44,1057,1058],{"class":161}," range",[44,1060,341],{"class":95},[44,1062,1063],{"class":161},"max",[44,1065,341],{"class":95},[44,1067,1068],{"class":161},"len",[44,1070,1071],{"class":95},"(ours_steps), ",[44,1073,1068],{"class":161},[44,1075,1076],{"class":95},"(theirs_steps))):\n",[44,1078,1079,1082,1084,1087,1090,1093,1096],{"class":46,"line":304},[44,1080,1081],{"class":95},"    row ",[44,1083,118],{"class":91},[44,1085,1086],{"class":95}," [",[44,1088,1089],{"class":161},"None",[44,1091,1092],{"class":95},"] ",[44,1094,1095],{"class":91},"*",[44,1097,1098],{"class":161}," 5\n",[44,1100,1101,1104,1106,1109,1112],{"class":46,"line":335},[44,1102,1103],{"class":91},"    if",[44,1105,1053],{"class":95},[44,1107,1108],{"class":91},"\u003C",[44,1110,1111],{"class":161}," len",[44,1113,1114],{"class":95},"(ours_steps):\n",[44,1116,1118,1121,1123,1126,1128,1130,1132,1135,1137,1140,1142],{"class":46,"line":1117},12,[44,1119,1120],{"class":95},"        row[",[44,1122,405],{"class":161},[44,1124,1125],{"class":95},"], row[",[44,1127,612],{"class":161},[44,1129,1092],{"class":95},[44,1131,118],{"class":91},[44,1133,1134],{"class":95}," ours_steps[",[44,1136,124],{"class":54},[44,1138,1139],{"class":95},"][i].to_pydatetime(), ours_steps[",[44,1141,283],{"class":54},[44,1143,1144],{"class":95},"][i]\n",[44,1146,1148,1150,1152,1154,1156],{"class":46,"line":1147},13,[44,1149,1103],{"class":91},[44,1151,1053],{"class":95},[44,1153,1108],{"class":91},[44,1155,1111],{"class":161},[44,1157,1158],{"class":95},"(theirs_steps):\n",[44,1160,1162,1164,1166,1168,1170,1172,1174,1177,1179,1182,1184],{"class":46,"line":1161},14,[44,1163,1120],{"class":95},[44,1165,773],{"class":161},[44,1167,1125],{"class":95},[44,1169,404],{"class":161},[44,1171,1092],{"class":95},[44,1173,118],{"class":91},[44,1175,1176],{"class":95}," theirs_steps[",[44,1178,124],{"class":54},[44,1180,1181],{"class":95},"][i].to_pydatetime(), theirs_steps[",[44,1183,283],{"class":54},[44,1185,1144],{"class":95},[44,1187,1189],{"class":46,"line":1188},15,[44,1190,1191],{"class":95},"    ws.append(row)\n",[44,1193,1195,1197,1200,1202,1205],{"class":46,"line":1194},16,[44,1196,1050],{"class":91},[44,1198,1199],{"class":95}," col ",[44,1201,537],{"class":91},[44,1203,1204],{"class":54}," \"AD\"",[44,1206,1207],{"class":95},":\n",[44,1209,1211,1213,1216,1218,1221,1223],{"class":46,"line":1210},17,[44,1212,531],{"class":91},[44,1214,1215],{"class":95}," cell ",[44,1217,537],{"class":91},[44,1219,1220],{"class":95}," ws[col][",[44,1222,612],{"class":161},[44,1224,1225],{"class":95},":]:\n",[44,1227,1229,1232,1234],{"class":46,"line":1228},18,[44,1230,1231],{"class":95},"        cell.number_format ",[44,1233,118],{"class":91},[44,1235,1236],{"class":54}," \"dd mmm\"\n",[44,1238,1240],{"class":46,"line":1239},19,[44,1241,109],{"emptyLinePlaceholder":108},[44,1243,1245,1248,1250],{"class":46,"line":1244},20,[44,1246,1247],{"class":95},"chart ",[44,1249,118],{"class":91},[44,1251,1252],{"class":95}," ScatterChart()\n",[44,1254,1256,1259,1261],{"class":46,"line":1255},21,[44,1257,1258],{"class":95},"chart.title ",[44,1260,118],{"class":91},[44,1262,1263],{"class":54}," \"List price, ours and competitor's\"\n",[44,1265,1267,1270,1272],{"class":46,"line":1266},22,[44,1268,1269],{"class":95},"chart.style ",[44,1271,118],{"class":91},[44,1273,1274],{"class":161}," 13\n",[44,1276,1278,1280,1283,1285,1288,1290,1292,1294,1296,1299,1301,1304,1307,1309,1311,1313,1315,1318,1320,1323],{"class":46,"line":1277},23,[44,1279,1050],{"class":91},[44,1281,1282],{"class":95}," x_col, y_col, colour, name ",[44,1284,537],{"class":91},[44,1286,1287],{"class":95}," [(",[44,1289,612],{"class":161},[44,1291,133],{"class":95},[44,1293,757],{"class":161},[44,1295,133],{"class":95},[44,1297,1298],{"class":54},"\"1F6FEB\"",[44,1300,133],{"class":95},[44,1302,1303],{"class":54},"\"Ours\"",[44,1305,1306],{"class":95},"), (",[44,1308,404],{"class":161},[44,1310,133],{"class":95},[44,1312,870],{"class":161},[44,1314,133],{"class":95},[44,1316,1317],{"class":54},"\"D4A72C\"",[44,1319,133],{"class":95},[44,1321,1322],{"class":54},"\"Competitor\"",[44,1324,1325],{"class":95},")]:\n",[44,1327,1329,1332,1334,1336,1339,1342,1345,1348,1350,1353,1355],{"class":46,"line":1328},24,[44,1330,1331],{"class":95},"    count ",[44,1333,118],{"class":91},[44,1335,1111],{"class":161},[44,1337,1338],{"class":95},"(ours_steps) ",[44,1340,1341],{"class":91},"if",[44,1343,1344],{"class":95}," x_col ",[44,1346,1347],{"class":91},"==",[44,1349,582],{"class":161},[44,1351,1352],{"class":91}," else",[44,1354,1111],{"class":161},[44,1356,1357],{"class":95},"(theirs_steps)\n",[44,1359,1361,1364,1366,1369,1372,1374,1377,1380,1382,1384,1386,1389,1391,1393,1396],{"class":46,"line":1360},25,[44,1362,1363],{"class":95},"    xs ",[44,1365,118],{"class":91},[44,1367,1368],{"class":95}," Reference(ws, ",[44,1370,1371],{"class":629},"min_col",[44,1373,118],{"class":91},[44,1375,1376],{"class":95},"x_col, ",[44,1378,1379],{"class":629},"min_row",[44,1381,118],{"class":91},[44,1383,757],{"class":161},[44,1385,133],{"class":95},[44,1387,1388],{"class":629},"max_row",[44,1390,118],{"class":91},[44,1392,612],{"class":161},[44,1394,1395],{"class":91}," +",[44,1397,1398],{"class":95}," count)\n",[44,1400,1402,1405,1407,1409,1411,1413,1416,1418,1420,1422,1424,1426,1428,1430,1432],{"class":46,"line":1401},26,[44,1403,1404],{"class":95},"    ys ",[44,1406,118],{"class":91},[44,1408,1368],{"class":95},[44,1410,1371],{"class":629},[44,1412,118],{"class":91},[44,1414,1415],{"class":95},"y_col, ",[44,1417,1379],{"class":629},[44,1419,118],{"class":91},[44,1421,757],{"class":161},[44,1423,133],{"class":95},[44,1425,1388],{"class":629},[44,1427,118],{"class":91},[44,1429,612],{"class":161},[44,1431,1395],{"class":91},[44,1433,1398],{"class":95},[44,1435,1437,1440,1442,1445,1447,1449],{"class":46,"line":1436},27,[44,1438,1439],{"class":95},"    s ",[44,1441,118],{"class":91},[44,1443,1444],{"class":95}," Series(ys, xs, ",[44,1446,394],{"class":629},[44,1448,118],{"class":91},[44,1450,1451],{"class":95},"name)\n",[44,1453,1455,1458,1460],{"class":46,"line":1454},28,[44,1456,1457],{"class":95},"    s.marker.symbol ",[44,1459,118],{"class":91},[44,1461,1462],{"class":54}," \"none\"\n",[44,1464,1466,1469,1471],{"class":46,"line":1465},29,[44,1467,1468],{"class":95},"    s.smooth ",[44,1470,118],{"class":91},[44,1472,1473],{"class":161}," False\n",[44,1475,1477,1480,1482,1485,1488,1490,1493,1496,1498,1501],{"class":46,"line":1476},30,[44,1478,1479],{"class":95},"    s.graphicalProperties.line ",[44,1481,118],{"class":91},[44,1483,1484],{"class":95}," LineProperties(",[44,1486,1487],{"class":629},"solidFill",[44,1489,118],{"class":91},[44,1491,1492],{"class":95},"colour, ",[44,1494,1495],{"class":629},"w",[44,1497,118],{"class":91},[44,1499,1500],{"class":161},"31750",[44,1502,259],{"class":95},[44,1504,1506],{"class":46,"line":1505},31,[44,1507,1508],{"class":95},"    chart.series.append(s)\n",[44,1510,1512,1515,1517],{"class":46,"line":1511},32,[44,1513,1514],{"class":95},"chart.x_axis.number_format ",[44,1516,118],{"class":91},[44,1518,1519],{"class":54}," \"mmm\"\n",[44,1521,1523,1526,1528,1530,1532,1535,1537],{"class":46,"line":1522},33,[44,1524,1525],{"class":95},"chart.x_axis.scaling.min ",[44,1527,118],{"class":91},[44,1529,253],{"class":95},[44,1531,130],{"class":54},[44,1533,1534],{"class":95},").to_julian_date() ",[44,1536,579],{"class":91},[44,1538,1539],{"class":161}," 2415018.5\n",[44,1541,1543,1546,1548,1551,1554,1556],{"class":46,"line":1542},34,[44,1544,1545],{"class":95},"chart.x_axis.scaling.max ",[44,1547,118],{"class":91},[44,1549,1550],{"class":161}," PERIOD_END",[44,1552,1553],{"class":95},".to_julian_date() ",[44,1555,579],{"class":91},[44,1557,1539],{"class":161},[44,1559,1561,1564,1566],{"class":46,"line":1560},35,[44,1562,1563],{"class":95},"chart.x_axis.majorUnit ",[44,1565,118],{"class":91},[44,1567,1568],{"class":161}," 31\n",[44,1570,1572,1575,1577,1580,1582],{"class":46,"line":1571},36,[44,1573,1574],{"class":95},"chart.y_axis.scaling.min, chart.y_axis.scaling.max ",[44,1576,118],{"class":91},[44,1578,1579],{"class":161}," 4.4",[44,1581,133],{"class":95},[44,1583,1584],{"class":161},"5.2\n",[44,1586,1588,1591,1593],{"class":46,"line":1587},37,[44,1589,1590],{"class":95},"chart.y_axis.number_format ",[44,1592,118],{"class":91},[44,1594,1595],{"class":54}," \"0.00\"\n",[44,1597,1599,1602,1604,1607,1609],{"class":46,"line":1598},38,[44,1600,1601],{"class":95},"chart.x_axis.delete ",[44,1603,118],{"class":91},[44,1605,1606],{"class":95}," chart.y_axis.delete ",[44,1608,118],{"class":91},[44,1610,1473],{"class":161},[44,1612,1614,1617,1619],{"class":46,"line":1613},39,[44,1615,1616],{"class":95},"chart.legend.position ",[44,1618,118],{"class":91},[44,1620,1621],{"class":54}," \"b\"\n",[44,1623,1625,1628,1630,1633,1635],{"class":46,"line":1624},40,[44,1626,1627],{"class":95},"chart.width, chart.height ",[44,1629,118],{"class":91},[44,1631,1632],{"class":161}," 18",[44,1634,133],{"class":95},[44,1636,1637],{"class":161},"9\n",[44,1639,1641,1644,1647],{"class":46,"line":1640},41,[44,1642,1643],{"class":95},"ws.add_chart(chart, ",[44,1645,1646],{"class":54},"\"A12\"",[44,1648,259],{"class":95},[44,1650,1652,1655,1658],{"class":46,"line":1651},42,[44,1653,1654],{"class":95},"wb.save(",[44,1656,1657],{"class":54},"\"step_chart.xlsx\"",[44,1659,259],{"class":95},[44,1661,1663,1665,1667,1670,1673,1676],{"class":46,"line":1662},43,[44,1664,338],{"class":161},[44,1666,341],{"class":95},[44,1668,1669],{"class":54},"\"x axis from\"",[44,1671,1672],{"class":95},", chart.x_axis.scaling.min, ",[44,1674,1675],{"class":54},"\"to\"",[44,1677,1678],{"class":95},", chart.x_axis.scaling.max)\n",[10,1680,1681,1682,1685,1686,1689,1690,1693,1694,27],{},"The x values are real dates, written as Excel date serials with a month format, so the axis spaces them by time: the step on 15 March sits halfway through the month. The axis bounds are set as serial numbers — 46023 for 1 January 2026 and 46295 for 30 September — computed here from pandas timestamps, and a major unit of 31 days gives one tick, labelled with its month, per month — a unit of 30 would label two ticks ",[14,1683,1684],{},"Jan",", because 31 January is still January. ",[41,1687,1688],{},"smooth = False"," and ",[41,1691,1692],{},"marker.symbol = \"none\""," keep the lines straight and clean. Each series has its own x and y columns, so the two price histories can change on different dates. Dates on chart axes in general are covered in ",[23,1695,1697],{"href":1696},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fuse-a-date-axis-in-an-excel-chart-with-openpyxl\u002F","Use a Date Axis in an Excel Chart with openpyxl",[29,1699,1701],{"id":1700},"step-4-render-and-check","Step 4 — Render and check",[34,1703,1705],{"className":82,"code":1704,"language":84,"meta":39,"style":39},"import subprocess\nfrom PIL import Image\n\nsubprocess.run([\"soffice\", \"--headless\", \"--convert-to\", \"pdf\", \"step_chart.xlsx\"], check=True, capture_output=True)\nsubprocess.run([\"pdftoppm\", \"-png\", \"-r\", \"80\", \"-f\", \"1\", \"-l\", \"1\", \"step_chart.pdf\", \"step\"], check=True)\nimg = Image.open(\"step-1.png\").convert(\"RGB\")\ncounts = {colour: n for n, colour in img.getcolors(maxcolors=2_000_000)}\nfor label, hexcode in [(\"ours (blue)\", \"1F6FEB\"), (\"competitor (gold)\", \"D4A72C\")]:\n    rgb = tuple(int(hexcode[i:i + 2], 16) for i in (0, 2, 4))\n    print(f\"{label:18} {counts.get(rgb, 0):>6} px\")\n",[41,1706,1707,1714,1727,1731,1780,1841,1862,1893,1922,1975],{"__ignoreMap":39},[44,1708,1709,1711],{"class":46,"line":47},[44,1710,92],{"class":91},[44,1712,1713],{"class":95}," subprocess\n",[44,1715,1716,1718,1721,1724],{"class":46,"line":105},[44,1717,949],{"class":91},[44,1719,1720],{"class":161}," PIL",[44,1722,1723],{"class":91}," import",[44,1725,1726],{"class":95}," Image\n",[44,1728,1729],{"class":46,"line":112},[44,1730,109],{"emptyLinePlaceholder":108},[44,1732,1733,1736,1739,1741,1744,1746,1749,1751,1754,1756,1758,1761,1764,1766,1769,1771,1774,1776,1778],{"class":46,"line":152},[44,1734,1735],{"class":95},"subprocess.run([",[44,1737,1738],{"class":54},"\"soffice\"",[44,1740,133],{"class":95},[44,1742,1743],{"class":54},"\"--headless\"",[44,1745,133],{"class":95},[44,1747,1748],{"class":54},"\"--convert-to\"",[44,1750,133],{"class":95},[44,1752,1753],{"class":54},"\"pdf\"",[44,1755,133],{"class":95},[44,1757,1657],{"class":54},[44,1759,1760],{"class":95},"], ",[44,1762,1763],{"class":629},"check",[44,1765,118],{"class":91},[44,1767,1768],{"class":161},"True",[44,1770,133],{"class":95},[44,1772,1773],{"class":629},"capture_output",[44,1775,118],{"class":91},[44,1777,1768],{"class":161},[44,1779,259],{"class":95},[44,1781,1782,1784,1787,1789,1792,1794,1797,1799,1802,1804,1807,1809,1812,1814,1817,1819,1821,1823,1826,1828,1831,1833,1835,1837,1839],{"class":46,"line":183},[44,1783,1735],{"class":95},[44,1785,1786],{"class":54},"\"pdftoppm\"",[44,1788,133],{"class":95},[44,1790,1791],{"class":54},"\"-png\"",[44,1793,133],{"class":95},[44,1795,1796],{"class":54},"\"-r\"",[44,1798,133],{"class":95},[44,1800,1801],{"class":54},"\"80\"",[44,1803,133],{"class":95},[44,1805,1806],{"class":54},"\"-f\"",[44,1808,133],{"class":95},[44,1810,1811],{"class":54},"\"1\"",[44,1813,133],{"class":95},[44,1815,1816],{"class":54},"\"-l\"",[44,1818,133],{"class":95},[44,1820,1811],{"class":54},[44,1822,133],{"class":95},[44,1824,1825],{"class":54},"\"step_chart.pdf\"",[44,1827,133],{"class":95},[44,1829,1830],{"class":54},"\"step\"",[44,1832,1760],{"class":95},[44,1834,1763],{"class":629},[44,1836,118],{"class":91},[44,1838,1768],{"class":161},[44,1840,259],{"class":95},[44,1842,1843,1846,1848,1851,1854,1857,1860],{"class":46,"line":216},[44,1844,1845],{"class":95},"img ",[44,1847,118],{"class":91},[44,1849,1850],{"class":95}," Image.open(",[44,1852,1853],{"class":54},"\"step-1.png\"",[44,1855,1856],{"class":95},").convert(",[44,1858,1859],{"class":54},"\"RGB\"",[44,1861,259],{"class":95},[44,1863,1864,1867,1869,1872,1874,1877,1879,1882,1885,1887,1890],{"class":46,"line":244},[44,1865,1866],{"class":95},"counts ",[44,1868,118],{"class":91},[44,1870,1871],{"class":95}," {colour: n ",[44,1873,1050],{"class":91},[44,1875,1876],{"class":95}," n, colour ",[44,1878,537],{"class":91},[44,1880,1881],{"class":95}," img.getcolors(",[44,1883,1884],{"class":629},"maxcolors",[44,1886,118],{"class":91},[44,1888,1889],{"class":161},"2_000_000",[44,1891,1892],{"class":95},")}\n",[44,1894,1895,1897,1900,1902,1904,1907,1909,1911,1913,1916,1918,1920],{"class":46,"line":262},[44,1896,1050],{"class":91},[44,1898,1899],{"class":95}," label, hexcode ",[44,1901,537],{"class":91},[44,1903,1287],{"class":95},[44,1905,1906],{"class":54},"\"ours (blue)\"",[44,1908,133],{"class":95},[44,1910,1298],{"class":54},[44,1912,1306],{"class":95},[44,1914,1915],{"class":54},"\"competitor (gold)\"",[44,1917,133],{"class":95},[44,1919,1317],{"class":54},[44,1921,1325],{"class":95},[44,1923,1924,1927,1929,1932,1934,1937,1940,1943,1946,1948,1951,1954,1956,1958,1960,1963,1965,1967,1969,1971,1973],{"class":46,"line":267},[44,1925,1926],{"class":95},"    rgb ",[44,1928,118],{"class":91},[44,1930,1931],{"class":161}," tuple",[44,1933,341],{"class":95},[44,1935,1936],{"class":161},"int",[44,1938,1939],{"class":95},"(hexcode[i:i ",[44,1941,1942],{"class":91},"+",[44,1944,1945],{"class":161}," 2",[44,1947,1760],{"class":95},[44,1949,1950],{"class":161},"16",[44,1952,1953],{"class":95},") ",[44,1955,1050],{"class":91},[44,1957,1053],{"class":95},[44,1959,537],{"class":91},[44,1961,1962],{"class":95}," (",[44,1964,405],{"class":161},[44,1966,133],{"class":95},[44,1968,757],{"class":161},[44,1970,133],{"class":95},[44,1972,404],{"class":161},[44,1974,685],{"class":95},[44,1976,1977,1980,1982,1984,1986,1988,1991,1994,1996,1999,2002,2004,2007,2010,2012,2015],{"class":46,"line":304},[44,1978,1979],{"class":161},"    print",[44,1981,341],{"class":95},[44,1983,344],{"class":91},[44,1985,376],{"class":54},[44,1987,351],{"class":350},[44,1989,1990],{"class":95},"label",[44,1992,1993],{"class":91},":18",[44,1995,360],{"class":350},[44,1997,1998],{"class":350}," {",[44,2000,2001],{"class":95},"counts.get(rgb, ",[44,2003,405],{"class":161},[44,2005,2006],{"class":95},")",[44,2008,2009],{"class":91},":>6",[44,2011,360],{"class":350},[44,2013,2014],{"class":54}," px\"",[44,2016,259],{"class":95},[10,2018,2019],{},"Both series appear in the render, and the chart sits below the data at A12 so it prints whole on the first page rather than being cut at the page edge. Looked at as an image, the blue line holds at 4.50 until mid-March, jumps to 4.80, steps down to 4.65 in June and up to 5.10 in late August, and runs flat to the end of September; the competitor's line steps at its own dates, and the chart shows at a glance that they were cheaper in February and dearer from May until our August increase.",[383,2021,392,2026,392,2029,392,2032,392,2035,392,2041,392,2046,392,2051,392,2056,392,2060,392,2063,392,2067,392,2071,392,2077,392,2081,392,2084,392,2086,392,2088,392,2092,392,2097,392,2101,392,2103,392,2105,392,2108,392,2111,392,2113,392,2115,392,2117,392,2119,392,2122,392,2126,392,2129,392,2131,392,2133,392,2136,392,2141,392,2145,392,2147,392,2149,392,2152,392,2156,392,2158,392,2160,392,2162],{"viewBox":2022,"role":386,"ariaLabelledBy":2023,"xmlns":390,"style":391},"4 0 752 245",[2024,2025],"st-story-t","st-story-d",[394,2027,2028],{"id":2024},"The two price histories",[398,2030,2031],{"id":2025},"Our price is 4.50 from January, 4.80 from 15 March, 4.65 from 1 June and 5.10 from 20 August. The competitor's is 4.70 from January, 4.60 from 10 February, 4.95 from 5 May and 4.85 from 1 September.",[402,2033],{"x":404,"y":405,"width":406,"height":2034,"fill":408},"245",[46,2036],{"x1":728,"y1":2037,"x2":2038,"y2":2037,"stroke":2039,"style":2040},"122.5","704.6","var(--muted,#5b6780)","stroke-width:2.5px",[402,2042],{"x":411,"y":412,"width":2043,"height":2044,"rx":2045,"fill":713,"stroke":714,"style":418},"94.9","74.5","12",[431,2047,1684],{"x":2048,"y":2049,"style":2050},"67.4","40.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[431,2052,2055],{"x":2048,"y":2053,"style":2054},"59.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","ours 4.50 ·",[431,2057,2059],{"x":2048,"y":2058,"style":2054},"76.0","theirs 4.70",[46,2061],{"x1":2048,"y1":2062,"x2":2048,"y2":2037,"stroke":2039,"style":844},"90.5",[2064,2065],"circle",{"cx":2048,"cy":2037,"r":2066,"fill":2039,"stroke":408,"style":418},"7.0",[402,2068],{"x":2069,"y":2070,"width":2043,"height":2044,"rx":2045,"fill":416,"stroke":417,"style":418},"124.2","154.5",[431,2072,2076],{"x":2073,"y":2074,"style":2075},"171.6","179.0","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","10 Feb",[431,2078,2080],{"x":2073,"y":2079,"style":2054},"197.5","theirs →",[431,2082,229],{"x":2073,"y":2083,"style":2054},"214.5",[46,2085],{"x1":2073,"y1":2037,"x2":2073,"y2":2070,"stroke":2039,"style":844},[2064,2087],{"cx":2073,"cy":2037,"r":2066,"fill":417,"stroke":408,"style":418},[402,2089],{"x":2090,"y":412,"width":2043,"height":2044,"rx":2045,"fill":2091,"stroke":909,"style":418},"228.4","#f0f4ff",[431,2093,778],{"x":2094,"y":2095,"style":2096},"275.8","49.0","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[431,2098,2100],{"x":2094,"y":2099,"style":2054},"67.5","ours → 4.80",[46,2102],{"x1":2094,"y1":2062,"x2":2094,"y2":2037,"stroke":2039,"style":844},[2064,2104],{"cx":2094,"cy":2037,"r":2066,"fill":909,"stroke":408,"style":418},[402,2106],{"x":2107,"y":2070,"width":2043,"height":2044,"rx":2045,"fill":416,"stroke":417,"style":418},"332.6",[431,2109,2110],{"x":925,"y":2074,"style":2075},"5 May",[431,2112,2080],{"x":925,"y":2079,"style":2054},[431,2114,234],{"x":925,"y":2083,"style":2054},[46,2116],{"x1":925,"y1":2037,"x2":925,"y2":2070,"stroke":2039,"style":844},[2064,2118],{"cx":925,"cy":2037,"r":2066,"fill":417,"stroke":408,"style":418},[402,2120],{"x":2121,"y":412,"width":2043,"height":2044,"rx":2045,"fill":2091,"stroke":909,"style":418},"436.8",[431,2123,2125],{"x":2124,"y":2095,"style":2096},"484.2","1 Jun",[431,2127,2128],{"x":2124,"y":2099,"style":2054},"ours → 4.65",[46,2130],{"x1":2124,"y1":2062,"x2":2124,"y2":2037,"stroke":2039,"style":844},[2064,2132],{"cx":2124,"cy":2037,"r":2066,"fill":909,"stroke":408,"style":418},[402,2134],{"x":2135,"y":2070,"width":2043,"height":2044,"rx":2045,"fill":2091,"stroke":909,"style":418},"541.0",[431,2137,2140],{"x":2138,"y":2139,"style":2096},"588.4","187.5","20 Aug",[431,2142,2144],{"x":2138,"y":2143,"style":2054},"206.0","ours → 5.10",[46,2146],{"x1":2138,"y1":2037,"x2":2138,"y2":2070,"stroke":2039,"style":844},[2064,2148],{"cx":2138,"cy":2037,"r":2066,"fill":909,"stroke":408,"style":418},[402,2150],{"x":2151,"y":412,"width":2043,"height":2044,"rx":2045,"fill":416,"stroke":417,"style":418},"645.1",[431,2153,2155],{"x":2154,"y":2049,"style":2075},"692.6","1 Sep",[431,2157,2080],{"x":2154,"y":2053,"style":2054},[431,2159,239],{"x":2154,"y":2058,"style":2054},[46,2161],{"x1":2154,"y1":2062,"x2":2154,"y2":2037,"stroke":2039,"style":844},[2064,2163],{"cx":2154,"cy":2037,"r":2066,"fill":417,"stroke":408,"style":418},[29,2165,2167],{"id":2166},"variations","Variations",[10,2169,2170,2171,2174,2175,2179,2180,1689,2183,2186],{},"The same doubled-points technique covers related charts. If the history does not start at the beginning of the period — the first recorded change is in March, but the price before it is known — add a first point at the period start with that opening value, just as the last point extends to the period end. For many items, such as the stock level of each product, build the step points per item with a ",[41,2172,2173],{},"groupby"," and give each its own pair of columns, or one chart per item as in ",[23,2176,2178],{"href":2177},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-one-chart-per-group-with-openpyxl\u002F","Create One Chart per Group with openpyxl",". And when the analysis needs the value on every day — to average a price over a quarter, say — expand the change points with a daily ",[41,2181,2182],{},"reindex",[41,2184,2185],{},"ffill()",", as the first code block did to look up 1 March.",[29,2188,2190],{"id":2189},"common-pitfalls","Common pitfalls",[2192,2193,2194,2202,2208,2214,2220],"ul",{},[2195,2196,2197,2201],"li",{},[2198,2199,2200],"strong",{},"Using a line chart."," Its sloping lines imply values that never existed.",[2195,2203,2204,2207],{},[2198,2205,2206],{},"Category axes."," Two points on one date need a scatter chart's value axis.",[2195,2209,2210,2213],{},[2198,2211,2212],{},"No final point."," Add the period end so the last value is drawn.",[2195,2215,2216,2219],{},[2198,2217,2218],{},"Smoothed lines or markers."," Turn both off for clean steps.",[2195,2221,2222,2225],{},[2198,2223,2224],{},"Axis bounds as dates."," Set them as Excel serial numbers.",[29,2227,2229],{"id":2228},"conclusion","Conclusion",[10,2231,2232],{},"For values that hold until they change, build a step chart: duplicate each change date with the previous value and the new value, add a point at the end of the period with the last value, and draw the points as a scatter chart with straight lines, no markers and no smoothing, on a date axis with bounds set as Excel serial numbers. Give each series its own x and y columns so histories with different change dates can share the chart.",[29,2234,2236],{"id":2235},"frequently-asked-questions","Frequently asked questions",[10,2238,2239,2242],{},[2198,2240,2241],{},"How do I make a step chart in Excel?","\nDuplicate each change point so the old and new values share the change date, then plot the points as a scatter chart with straight lines.",[10,2244,2245,2248],{},[2198,2246,2247],{},"Why use a scatter chart rather than a line chart for steps?","\nA line chart spaces categories evenly, so two points on the same date sit apart; a scatter chart places them at the same x value, drawing a vertical jump.",[10,2250,2251,2254],{},[2198,2252,2253],{},"Why does my step chart stop at the last change?","\nAdd a final point at the end of the period with the last value so the line continues.",[10,2256,2257,2260],{},[2198,2258,2259],{},"How do I set date axis limits on an openpyxl scatter chart?","\nSet x_axis.scaling.min and max to Excel date serial numbers, for example 46023 for 1 January 2026, and use a date number format on the axis.",[29,2262,2264],{"id":2263},"related","Related",[2192,2266,2267,2273,2280,2287],{},[2195,2268,2269,2270,2272],{},"Up one level: ",[23,2271,26],{"href":25}," — charts from Python.",[2195,2274,2275,2279],{},[23,2276,2278],{"href":2277},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-scatter-chart-in-excel-with-openpyxl\u002F","Create a Scatter Chart in Excel with openpyxl"," — scatter chart basics.",[2195,2281,2282,2286],{},[23,2283,2285],{"href":2284},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-slope-chart-in-excel-with-python\u002F","Create a Slope Chart in Excel with Python"," — another line-based chart.",[2195,2288,2289,2293],{},[23,2290,2292],{"href":2291},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Ffill-gaps-in-per-item-time-series-with-a-staleness-limit\u002F","Fill Gaps in Per-Item Time Series with a Staleness Limit"," — forward-filling values that hold.",[2295,2296,2297],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}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}",{"title":39,"searchDepth":105,"depth":105,"links":2299},[2300,2301,2302,2303,2304,2305,2306,2307,2308,2309],{"id":31,"depth":105,"text":32},{"id":78,"depth":105,"text":79},{"id":495,"depth":105,"text":496},{"id":931,"depth":105,"text":932},{"id":1700,"depth":105,"text":1701},{"id":2166,"depth":105,"text":2167},{"id":2189,"depth":105,"text":2190},{"id":2228,"depth":105,"text":2229},{"id":2235,"depth":105,"text":2236},{"id":2263,"depth":105,"text":2264},"2026-10-02","Draw values that change at specific moments and then hold — prices, interest rates, headcount, stock levels — as a step chart in Excel from Python: why a normal line chart misleads with diagonal ramps, how to double up the change points with pandas, build the chart as an openpyxl scatter chart on a real date axis, carry the last value to the end of the period, and compare several series.","md",[2314,2316,2318,2320],{"q":2241,"a":2315},"Duplicate each change point so the old and new values share the change date, then plot the points as a scatter chart with straight lines.",{"q":2247,"a":2317},"A line chart spaces categories evenly, so two points on the same date sit apart; a scatter chart places them at the same x value, drawing a vertical jump.",{"q":2253,"a":2319},"Add a final point at the end of the period with the last value so the line continues.",{"q":2259,"a":2321},"Set x_axis.scaling.min and max to Excel date serial numbers, for example 46023 for 1 January 2026, and use a date number format on the axis.","create a step chart in excel with python",null,{"breadcrumb":2325},[2326,2329,2332],{"name":2327,"item":2328},"Home","\u002F",{"name":2330,"item":2331},"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-step-chart-in-excel-with-python",{"title":2335,"description":2336},"Create a Step Chart in Excel with Python and openpyxl","Step charts for prices, rates and levels with pandas and openpyxl: duplicating change points, a scatter chart with straight lines on a date axis, extending to the period end, multiple series, and checking the render.","create-a-step-chart-in-excel-with-python","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-step-chart-in-excel-with-python\u002Findex",[2340,2341],"charts-and-images","openpyxl","how-to","tCJxL7Itt__qsFc60JVNN5kaIRgwVZcWgKr55CmMagU",[2345,2349],{"title":2346,"path":2347,"stem":2348,"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",{"title":2350,"path":2351,"stem":2352,"children":-1},"Create a Stock Chart with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-stock-chart-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-stock-chart-with-openpyxl\u002Findex",[2354,2356],{"path":2355,"title":26},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl",{"path":2357,"title":2330},"\u002Fformatting-and-charting-excel-reports-with-python",[],[2360,2363,2366,2369,2372,2375],{"path":2361,"title":2362,"kind":2342},"\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":2364,"title":2365,"kind":2342},"\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":2367,"title":2368,"kind":2342},"\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":2370,"title":2371,"kind":2342},"\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":2373,"title":2374,"kind":2342},"\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":2376,"title":2377,"kind":2342},"\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",1790972111165]