[{"data":1,"prerenderedAt":2455},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-a-combo-chart-with-a-secondary-axis-in-openpyxl":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-a-combo-chart-with-a-secondary-axis-in-openpyxl":2448},{"id":4,"title":5,"body":6,"dateModified":2424,"datePublished":2424,"description":2425,"extension":2426,"faq":2427,"meta":2439,"navigation":259,"path":2440,"seo":2441,"slug":2444,"stem":2445,"type":2446,"__hash__":2447},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-a-combo-chart-with-a-secondary-axis-in-openpyxl\u002Findex.md","Add a Combo Chart with a Secondary Axis in openpyxl",{"type":7,"value":8,"toc":2412},"minimark",[9,19,195,200,228,231,420,423,427,430,659,669,673,676,821,824,874,888,892,991,996,1030,1041,1044,1578,1585,1589,1592,1671,1674,1681,1895,1902,1982,1989,1993,2129,2133,2136,2139,2246,2254,2269,2275,2285,2289,2305,2309,2318,2334,2348,2354,2368,2372,2408],[10,11,12,13,18],"p",{},"Revenue in thousands and margin as a percentage cannot share a scale: plot them on one axis and the margin line lies flat along the bottom. A secondary axis solves it — bars on the left scale, a line on the right — and openpyxl builds one by combining two chart objects with a couple of axis settings that are easy to get wrong. This guide covers the construction, the two settings that decide whether the second series appears at all, and the cases where a secondary axis actively misleads. It extends ",[14,15,17],"a",{"href":16},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002F","Creating Charts in Excel with openpyxl",".",[20,21,30,31,30,35,30,39,30,46,30,53,30,61,30,66,30,69,30,75,30,79,30,82,30,87,30,90,30,93,30,101,30,106,30,111,30,115,30,120,30,125,30,131,30,139,30,143,30,147,30,151,30,154,30,158,30,163,30,166,30,169,30,172,30,175,30,178,30,182,30,188,30,191],"svg",{"viewBox":22,"role":23,"ariaLabel":24,"ariaLabelledBy":25,"xmlns":28,"style":29},"0 0 800 264","img","A combo chart: monthly revenue as bars against a left axis in thousands, and margin as a line against a right axis in percent, so both series are readable.",[26,27],"combo-t","combo-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[32,33,34],"title",{"id":26},"Bars on the left scale, a line on the right",[36,37,38],"desc",{"id":27},"A combo chart over six months. Revenue is drawn as indigo bars measured against a left axis running from zero to six thousand. Margin is drawn as a teal line measured against a right axis running from zero to thirty per cent. Because each series has its own scale, both are legible; plotted on a single axis the margin line would sit flat along the bottom of the chart.",[40,41],"rect",{"x":42,"y":42,"width":43,"height":44,"fill":45},"0","800","264","#ffffff",[47,48,52],"text",{"x":49,"y":50,"style":51},"400","26","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","revenue and margin, each on its own scale",[54,55],"line",{"x1":56,"y1":57,"x2":58,"y2":57,"stroke":59,"style":60},"86","200","700","var(--line,#cdd5e6)","stroke-width:1.5px",[54,62],{"x1":56,"y1":57,"x2":56,"y2":63,"stroke":64,"style":65},"44","var(--brand,#5b5cf0)","stroke-width:2px",[54,67],{"x1":58,"y1":57,"x2":58,"y2":63,"stroke":68,"style":65},"var(--teal,#0f9488)",[47,70,74],{"x":71,"y":72,"style":73},"78","50","font-size:10px;fill:var(--brand-strong,#4338ca);text-anchor:end","6,000",[47,76,78],{"x":71,"y":77,"style":73},"128","3,000",[47,80,42],{"x":71,"y":81,"style":73},"204",[47,83,86],{"x":84,"y":72,"style":85},"710","font-size:10px;fill:var(--teal-ink,#0b6157)","30%",[47,88,89],{"x":84,"y":77,"style":85},"15%",[47,91,92],{"x":84,"y":81,"style":85},"0%",[40,94],{"x":95,"y":96,"width":97,"height":98,"rx":99,"fill":100},"110","66","60","134","3","#5b5cf0",[40,102],{"x":103,"y":104,"width":97,"height":105,"rx":99,"fill":100},"208","88","112",[40,107],{"x":108,"y":109,"width":97,"height":110,"rx":99,"fill":100},"306","108","92",[40,112],{"x":113,"y":71,"width":97,"height":114,"rx":99,"fill":100},"404","122",[40,116],{"x":117,"y":118,"width":97,"height":119,"rx":99,"fill":100},"502","56","144",[40,121],{"x":122,"y":123,"width":97,"height":124,"rx":99,"fill":100},"600","96","104",[126,127],"path",{"d":128,"fill":129,"stroke":68,"style":130},"M140 138 L 238 118 L 336 152 L 434 106 L 532 92 L 630 124","none","stroke-width:3px",[132,133],"circle",{"cx":134,"cy":135,"r":136,"fill":137,"stroke":138,"style":65},"140","138","5","var(--teal,#14b8a6)","#0f9488",[132,140],{"cx":141,"cy":142,"r":136,"fill":137,"stroke":138,"style":65},"238","118",[132,144],{"cx":145,"cy":146,"r":136,"fill":137,"stroke":138,"style":65},"336","152",[132,148],{"cx":149,"cy":150,"r":136,"fill":137,"stroke":138,"style":65},"434","106",[132,152],{"cx":153,"cy":110,"r":136,"fill":137,"stroke":138,"style":65},"532",[132,155],{"cx":156,"cy":157,"r":136,"fill":137,"stroke":138,"style":65},"630","124",[47,159,162],{"x":134,"y":160,"style":161},"218","font-size:9.5px;fill:var(--muted,#5b6780);text-anchor:middle","Mar",[47,164,165],{"x":141,"y":160,"style":161},"Apr",[47,167,168],{"x":145,"y":160,"style":161},"May",[47,170,171],{"x":149,"y":160,"style":161},"Jun",[47,173,174],{"x":153,"y":160,"style":161},"Jul",[47,176,177],{"x":156,"y":160,"style":161},"Aug",[40,179],{"x":180,"y":141,"width":181,"height":181,"rx":99,"fill":100},"250","14",[47,183,187],{"x":184,"y":185,"style":186},"272","249","font-size:11px;fill:var(--muted,#5b6780)","revenue (left)",[40,189],{"x":190,"y":141,"width":181,"height":181,"rx":99,"fill":137},"424",[47,192,194],{"x":193,"y":185,"style":186},"446","margin (right)",[196,197,199],"h2",{"id":198},"prerequisites","Prerequisites",[201,202,207],"pre",{"className":203,"code":204,"language":205,"meta":206,"style":206},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas\n","bash","",[208,209,210],"code",{"__ignoreMap":206},[211,212,214,218,222,225],"span",{"class":54,"line":213},1,[211,215,217],{"class":216},"sMTad","pip",[211,219,221],{"class":220},"srMev"," install",[211,223,224],{"class":220}," openpyxl",[211,226,227],{"class":220}," pandas\n",[10,229,230],{},"Data with two very different scales:",[201,232,236],{"className":233,"code":234,"language":235,"meta":206,"style":206},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nmonthly = pd.DataFrame({\n    \"month\": [\"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\"],\n    \"revenue\": [5150, 4820, 3980, 5402, 6100, 4560],\n    \"margin\": [0.152, 0.181, 0.118, 0.205, 0.228, 0.163],\n})\nmonthly.to_excel(\"monthly.xlsx\", index=False)\n","python",[208,237,238,254,261,273,314,353,391,397],{"__ignoreMap":206},[211,239,240,244,248,251],{"class":54,"line":213},[211,241,243],{"class":242},"s-kum","import",[211,245,247],{"class":246},"skGVy"," pandas ",[211,249,250],{"class":242},"as",[211,252,253],{"class":246}," pd\n",[211,255,257],{"class":54,"line":256},2,[211,258,260],{"emptyLinePlaceholder":259},true,"\n",[211,262,264,267,270],{"class":54,"line":263},3,[211,265,266],{"class":246},"monthly ",[211,268,269],{"class":242},"=",[211,271,272],{"class":246}," pd.DataFrame({\n",[211,274,276,279,282,285,288,291,293,296,298,301,303,306,308,311],{"class":54,"line":275},4,[211,277,278],{"class":220},"    \"month\"",[211,280,281],{"class":246},": [",[211,283,284],{"class":220},"\"Mar\"",[211,286,287],{"class":246},", ",[211,289,290],{"class":220},"\"Apr\"",[211,292,287],{"class":246},[211,294,295],{"class":220},"\"May\"",[211,297,287],{"class":246},[211,299,300],{"class":220},"\"Jun\"",[211,302,287],{"class":246},[211,304,305],{"class":220},"\"Jul\"",[211,307,287],{"class":246},[211,309,310],{"class":220},"\"Aug\"",[211,312,313],{"class":246},"],\n",[211,315,317,320,322,326,328,331,333,336,338,341,343,346,348,351],{"class":54,"line":316},5,[211,318,319],{"class":220},"    \"revenue\"",[211,321,281],{"class":246},[211,323,325],{"class":324},"sP0c6","5150",[211,327,287],{"class":246},[211,329,330],{"class":324},"4820",[211,332,287],{"class":246},[211,334,335],{"class":324},"3980",[211,337,287],{"class":246},[211,339,340],{"class":324},"5402",[211,342,287],{"class":246},[211,344,345],{"class":324},"6100",[211,347,287],{"class":246},[211,349,350],{"class":324},"4560",[211,352,313],{"class":246},[211,354,356,359,361,364,366,369,371,374,376,379,381,384,386,389],{"class":54,"line":355},6,[211,357,358],{"class":220},"    \"margin\"",[211,360,281],{"class":246},[211,362,363],{"class":324},"0.152",[211,365,287],{"class":246},[211,367,368],{"class":324},"0.181",[211,370,287],{"class":246},[211,372,373],{"class":324},"0.118",[211,375,287],{"class":246},[211,377,378],{"class":324},"0.205",[211,380,287],{"class":246},[211,382,383],{"class":324},"0.228",[211,385,287],{"class":246},[211,387,388],{"class":324},"0.163",[211,390,313],{"class":246},[211,392,394],{"class":54,"line":393},7,[211,395,396],{"class":246},"})\n",[211,398,400,403,406,408,412,414,417],{"class":54,"line":399},8,[211,401,402],{"class":246},"monthly.to_excel(",[211,404,405],{"class":220},"\"monthly.xlsx\"",[211,407,287],{"class":246},[211,409,411],{"class":410},"sa561","index",[211,413,269],{"class":242},[211,415,416],{"class":324},"False",[211,418,419],{"class":246},")\n",[10,421,422],{},"Revenue runs in the thousands, margin between 0.11 and 0.23. On one axis the margin series is invisible.",[196,424,426],{"id":425},"step-1-build-the-primary-chart","Step 1 — Build the primary chart",[10,428,429],{},"The bar chart owns the axes, the title and the plot area:",[201,431,433],{"className":233,"code":432,"language":235,"meta":206,"style":206},"from openpyxl import load_workbook\nfrom openpyxl.chart import BarChart, Reference\n\nwb = load_workbook(\"monthly.xlsx\")\nws = wb.active\n\ncategories = Reference(ws, min_col=1, min_row=2, max_row=7)\n\nbars = BarChart()\nbars.type = \"col\"\nbars.title = \"Revenue and margin by month\"\nbars.y_axis.title = \"Revenue\"\nbars.x_axis.title = \"Month\"\nbars.height, bars.width = 9, 18\n\nbars.add_data(Reference(ws, min_col=2, min_row=1, max_row=7), titles_from_data=True)\nbars.set_categories(categories)\n",[208,434,435,448,460,464,478,488,492,532,536,547,558,569,580,591,607,612,653],{"__ignoreMap":206},[211,436,437,440,443,445],{"class":54,"line":213},[211,438,439],{"class":242},"from",[211,441,442],{"class":246}," openpyxl ",[211,444,243],{"class":242},[211,446,447],{"class":246}," load_workbook\n",[211,449,450,452,455,457],{"class":54,"line":256},[211,451,439],{"class":242},[211,453,454],{"class":246}," openpyxl.chart ",[211,456,243],{"class":242},[211,458,459],{"class":246}," BarChart, Reference\n",[211,461,462],{"class":54,"line":263},[211,463,260],{"emptyLinePlaceholder":259},[211,465,466,469,471,474,476],{"class":54,"line":275},[211,467,468],{"class":246},"wb ",[211,470,269],{"class":242},[211,472,473],{"class":246}," load_workbook(",[211,475,405],{"class":220},[211,477,419],{"class":246},[211,479,480,483,485],{"class":54,"line":316},[211,481,482],{"class":246},"ws ",[211,484,269],{"class":242},[211,486,487],{"class":246}," wb.active\n",[211,489,490],{"class":54,"line":355},[211,491,260],{"emptyLinePlaceholder":259},[211,493,494,497,499,502,505,507,510,512,515,517,520,522,525,527,530],{"class":54,"line":393},[211,495,496],{"class":246},"categories ",[211,498,269],{"class":242},[211,500,501],{"class":246}," Reference(ws, ",[211,503,504],{"class":410},"min_col",[211,506,269],{"class":242},[211,508,509],{"class":324},"1",[211,511,287],{"class":246},[211,513,514],{"class":410},"min_row",[211,516,269],{"class":242},[211,518,519],{"class":324},"2",[211,521,287],{"class":246},[211,523,524],{"class":410},"max_row",[211,526,269],{"class":242},[211,528,529],{"class":324},"7",[211,531,419],{"class":246},[211,533,534],{"class":54,"line":399},[211,535,260],{"emptyLinePlaceholder":259},[211,537,539,542,544],{"class":54,"line":538},9,[211,540,541],{"class":246},"bars ",[211,543,269],{"class":242},[211,545,546],{"class":246}," BarChart()\n",[211,548,550,553,555],{"class":54,"line":549},10,[211,551,552],{"class":246},"bars.type ",[211,554,269],{"class":242},[211,556,557],{"class":220}," \"col\"\n",[211,559,561,564,566],{"class":54,"line":560},11,[211,562,563],{"class":246},"bars.title ",[211,565,269],{"class":242},[211,567,568],{"class":220}," \"Revenue and margin by month\"\n",[211,570,572,575,577],{"class":54,"line":571},12,[211,573,574],{"class":246},"bars.y_axis.title ",[211,576,269],{"class":242},[211,578,579],{"class":220}," \"Revenue\"\n",[211,581,583,586,588],{"class":54,"line":582},13,[211,584,585],{"class":246},"bars.x_axis.title ",[211,587,269],{"class":242},[211,589,590],{"class":220}," \"Month\"\n",[211,592,594,597,599,602,604],{"class":54,"line":593},14,[211,595,596],{"class":246},"bars.height, bars.width ",[211,598,269],{"class":242},[211,600,601],{"class":324}," 9",[211,603,287],{"class":246},[211,605,606],{"class":324},"18\n",[211,608,610],{"class":54,"line":609},15,[211,611,260],{"emptyLinePlaceholder":259},[211,613,615,618,620,622,624,626,628,630,632,634,636,638,640,643,646,648,651],{"class":54,"line":614},16,[211,616,617],{"class":246},"bars.add_data(Reference(ws, ",[211,619,504],{"class":410},[211,621,269],{"class":242},[211,623,519],{"class":324},[211,625,287],{"class":246},[211,627,514],{"class":410},[211,629,269],{"class":242},[211,631,509],{"class":324},[211,633,287],{"class":246},[211,635,524],{"class":410},[211,637,269],{"class":242},[211,639,529],{"class":324},[211,641,642],{"class":246},"), ",[211,644,645],{"class":410},"titles_from_data",[211,647,269],{"class":242},[211,649,650],{"class":324},"True",[211,652,419],{"class":246},[211,654,656],{"class":54,"line":655},17,[211,657,658],{"class":246},"bars.set_categories(categories)\n",[10,660,661,664,665,668],{},[208,662,663],{},"titles_from_data=True"," with ",[208,666,667],{},"min_row=1"," takes the series name from the header cell, which is why the legend reads \"revenue\" rather than \"Series 1\".",[196,670,672],{"id":671},"step-2-build-the-secondary-chart-and-set-its-axis","Step 2 — Build the secondary chart and set its axis",[10,674,675],{},"The line chart contributes only its series and a second scale. Three settings make that work:",[201,677,679],{"className":233,"code":678,"language":235,"meta":206,"style":206},"from openpyxl.chart import LineChart\n\nline = LineChart()\nline.add_data(Reference(ws, min_col=3, min_row=1, max_row=7), titles_from_data=True)\n\n# 1. A distinct axis id — this is what makes it a SECOND axis.\nline.y_axis.axId = 200\nline.y_axis.title = \"Margin\"\nline.y_axis.numFmt = \"0.0%\"\n\n# 2. Cross at the far end so it draws on the right.\nline.y_axis.crosses = \"max\"\n\n# 3. Suppress the duplicate category axis.\nline.x_axis.delete = True\n",[208,680,681,692,696,706,743,747,753,763,773,783,787,792,802,806,811],{"__ignoreMap":206},[211,682,683,685,687,689],{"class":54,"line":213},[211,684,439],{"class":242},[211,686,454],{"class":246},[211,688,243],{"class":242},[211,690,691],{"class":246}," LineChart\n",[211,693,694],{"class":54,"line":256},[211,695,260],{"emptyLinePlaceholder":259},[211,697,698,701,703],{"class":54,"line":263},[211,699,700],{"class":246},"line ",[211,702,269],{"class":242},[211,704,705],{"class":246}," LineChart()\n",[211,707,708,711,713,715,717,719,721,723,725,727,729,731,733,735,737,739,741],{"class":54,"line":275},[211,709,710],{"class":246},"line.add_data(Reference(ws, ",[211,712,504],{"class":410},[211,714,269],{"class":242},[211,716,99],{"class":324},[211,718,287],{"class":246},[211,720,514],{"class":410},[211,722,269],{"class":242},[211,724,509],{"class":324},[211,726,287],{"class":246},[211,728,524],{"class":410},[211,730,269],{"class":242},[211,732,529],{"class":324},[211,734,642],{"class":246},[211,736,645],{"class":410},[211,738,269],{"class":242},[211,740,650],{"class":324},[211,742,419],{"class":246},[211,744,745],{"class":54,"line":316},[211,746,260],{"emptyLinePlaceholder":259},[211,748,749],{"class":54,"line":355},[211,750,752],{"class":751},"s-wDw","# 1. A distinct axis id — this is what makes it a SECOND axis.\n",[211,754,755,758,760],{"class":54,"line":393},[211,756,757],{"class":246},"line.y_axis.axId ",[211,759,269],{"class":242},[211,761,762],{"class":324}," 200\n",[211,764,765,768,770],{"class":54,"line":399},[211,766,767],{"class":246},"line.y_axis.title ",[211,769,269],{"class":242},[211,771,772],{"class":220}," \"Margin\"\n",[211,774,775,778,780],{"class":54,"line":538},[211,776,777],{"class":246},"line.y_axis.numFmt ",[211,779,269],{"class":242},[211,781,782],{"class":220}," \"0.0%\"\n",[211,784,785],{"class":54,"line":549},[211,786,260],{"emptyLinePlaceholder":259},[211,788,789],{"class":54,"line":560},[211,790,791],{"class":751},"# 2. Cross at the far end so it draws on the right.\n",[211,793,794,797,799],{"class":54,"line":571},[211,795,796],{"class":246},"line.y_axis.crosses ",[211,798,269],{"class":242},[211,800,801],{"class":220}," \"max\"\n",[211,803,804],{"class":54,"line":582},[211,805,260],{"emptyLinePlaceholder":259},[211,807,808],{"class":54,"line":593},[211,809,810],{"class":751},"# 3. Suppress the duplicate category axis.\n",[211,812,813,816,818],{"class":54,"line":609},[211,814,815],{"class":246},"line.x_axis.delete ",[211,817,269],{"class":242},[211,819,820],{"class":324}," True\n",[10,822,823],{},"Each of the three is load-bearing:",[825,826,827,840],"table",{},[828,829,830],"thead",{},[831,832,833,837],"tr",{},[834,835,836],"th",{},"Setting",[834,838,839],{},"Without it",[841,842,843,854,864],"tbody",{},[831,844,845,851],{},[846,847,848],"td",{},[208,849,850],{},"y_axis.axId = 200",[846,852,853],{},"Both charts share one axis; the line flattens",[831,855,856,861],{},[846,857,858],{},[208,859,860],{},"y_axis.crosses = \"max\"",[846,862,863],{},"The second axis draws on the left, overlapping the first",[831,865,866,871],{},[846,867,868],{},[208,869,870],{},"x_axis.delete = True",[846,872,873],{},"Two category axes render on top of each other",[10,875,876,877,880,881,884,885,887],{},"The ",[208,878,879],{},"axId"," value is arbitrary — it just has to differ from the primary chart's, which defaults to ",[208,882,883],{},"100",". Using ",[208,886,57],{}," is a convention, not a requirement.",[196,889,891],{"id":890},"step-3-combine-and-anchor","Step 3 — Combine and anchor",[20,893,30,900,30,903,30,906,30,912,30,918,30,924,30,928,30,932,30,936,30,941,30,946,30,951,30,954,30,958,30,962,30,967,30,971,30,977,30,982,30,986],{"viewBox":894,"role":23,"ariaLabel":895,"ariaLabelledBy":896,"xmlns":28,"style":899},"-2 36 802 188","Combining two charts: the chart on the left of the operator keeps its title and axes, while the one on the right contributes only its series.",[897,898],"comb-t","comb-d","width:100%;max-width:802px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[32,901,902],{"id":897},"Which chart survives the combine",[36,904,905],{"id":898},"Two chart objects are merged. The bar chart on the left of the operator keeps its title, its axis titles, its plot area and its size — it owns the resulting chart. The line chart on the right contributes only its data series and its secondary axis; its own title and axis labels are discarded. Combining in the opposite order would silently lose the settings applied to the bar chart.",[40,907],{"x":908,"y":909,"width":910,"height":911,"fill":45},"-2","36","802","188",[40,913],{"x":181,"y":914,"width":915,"height":77,"rx":916,"fill":917,"stroke":64,"style":65},"52","234","13","#ebebfd",[47,919,923],{"x":920,"y":921,"style":922},"131","80","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","BarChart",[47,925,927],{"x":920,"y":150,"style":926},"font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","title · axis titles",[47,929,931],{"x":920,"y":930,"style":926},"126","size · plot area",[47,933,935],{"x":920,"y":146,"style":934},"font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","all of this survives",[47,937,940],{"x":938,"y":114,"style":939},"278","font-size:20px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","+=",[40,942],{"x":943,"y":914,"width":915,"height":77,"rx":916,"fill":944,"stroke":945,"style":65},"308","#fdefd8","var(--gold,#b4740a)",[47,947,950],{"x":948,"y":921,"style":949},"425","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","LineChart",[47,952,953],{"x":948,"y":150,"style":926},"series + secondary axis",[47,955,957],{"x":948,"y":930,"style":956},"font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","its own title discarded",[47,959,961],{"x":948,"y":146,"style":960},"font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","contributes data only",[54,963],{"x1":964,"y1":965,"x2":966,"y2":965,"stroke":68,"style":65},"546","116","578",[968,969],"polygon",{"points":970,"fill":138},"586,116 574,110 574,122",[40,972],{"x":973,"y":974,"width":975,"height":921,"rx":916,"fill":976,"stroke":68,"style":65},"594","76","190","#d9f4f1",[47,978,981],{"x":979,"y":150,"style":980},"689","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","one combo chart",[47,983,985],{"x":979,"y":984,"style":956},"130","bars, line, two axes",[47,987,990],{"x":49,"y":988,"style":989},"206","font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","reverse the operands and the title and axis labels you set are silently lost",[10,992,876,993,995],{},[208,994,940],{}," operator merges the second chart's series into the first:",[201,997,999],{"className":233,"code":998,"language":235,"meta":206,"style":206},"bars += line\nws.add_chart(bars, \"F2\")\nwb.save(\"monthly_combo.xlsx\")\n",[208,1000,1001,1010,1020],{"__ignoreMap":206},[211,1002,1003,1005,1007],{"class":54,"line":213},[211,1004,541],{"class":246},[211,1006,940],{"class":242},[211,1008,1009],{"class":246}," line\n",[211,1011,1012,1015,1018],{"class":54,"line":256},[211,1013,1014],{"class":246},"ws.add_chart(bars, ",[211,1016,1017],{"class":220},"\"F2\"",[211,1019,419],{"class":246},[211,1021,1022,1025,1028],{"class":54,"line":263},[211,1023,1024],{"class":246},"wb.save(",[211,1026,1027],{"class":220},"\"monthly_combo.xlsx\"",[211,1029,419],{"class":246},[10,1031,1032,1033,1037,1038,1040],{},"Order matters. The chart on the ",[1034,1035,1036],"strong",{},"left"," of the ",[208,1039,940],{}," keeps its title, axes and plot area; the one on the right contributes series only. Combining the other way round would give you a line chart with bars added, and the title and axis labels you set on the bar chart would be lost.",[10,1042,1043],{},"The complete function:",[201,1045,1047],{"className":233,"code":1046,"language":235,"meta":206,"style":206},"from openpyxl import load_workbook\nfrom openpyxl.chart import BarChart, LineChart, Reference\nfrom openpyxl.chart.marker import Marker\n\ndef add_combo_chart(ws, anchor=\"F2\", last_row=7,\n                    bar_col=2, line_col=3, category_col=1,\n                    title=\"Revenue and margin by month\",\n                    line_format=\"0.0%\"):\n    \"\"\"Bars on the primary axis, a line on a secondary axis.\"\"\"\n    categories = Reference(ws, min_col=category_col, min_row=2, max_row=last_row)\n\n    bars = BarChart()\n    bars.type = \"col\"\n    bars.title = title\n    bars.y_axis.title = ws.cell(row=1, column=bar_col).value\n    bars.x_axis.title = ws.cell(row=1, column=category_col).value\n    bars.y_axis.numFmt = \"#,##0\"\n    bars.height, bars.width = 9, 18\n    bars.add_data(Reference(ws, min_col=bar_col, min_row=1, max_row=last_row),\n                  titles_from_data=True)\n    bars.set_categories(categories)\n\n    line = LineChart()\n    line.add_data(Reference(ws, min_col=line_col, min_row=1, max_row=last_row),\n                  titles_from_data=True)\n    line.y_axis.axId = 200\n    line.y_axis.title = ws.cell(row=1, column=line_col).value\n    line.y_axis.numFmt = line_format\n    line.y_axis.crosses = \"max\"\n    line.x_axis.delete = True\n\n    series = line.series[0]\n    series.smooth = False                       # straight segments read honestly\n    series.marker = Marker(symbol=\"circle\", size=7)\n    series.graphicalProperties.line.width = 28_000   # EMU, roughly 2.2pt\n\n    bars += line\n    ws.add_chart(bars, anchor)\n    return bars\n",[208,1048,1049,1059,1070,1082,1086,1112,1137,1149,1162,1167,1198,1202,1211,1220,1230,1257,1281,1291,1305,1333,1345,1351,1356,1366,1393,1404,1414,1439,1450,1460,1470,1475,1491,1505,1535,1549,1554,1563,1569],{"__ignoreMap":206},[211,1050,1051,1053,1055,1057],{"class":54,"line":213},[211,1052,439],{"class":242},[211,1054,442],{"class":246},[211,1056,243],{"class":242},[211,1058,447],{"class":246},[211,1060,1061,1063,1065,1067],{"class":54,"line":256},[211,1062,439],{"class":242},[211,1064,454],{"class":246},[211,1066,243],{"class":242},[211,1068,1069],{"class":246}," BarChart, LineChart, Reference\n",[211,1071,1072,1074,1077,1079],{"class":54,"line":263},[211,1073,439],{"class":242},[211,1075,1076],{"class":246}," openpyxl.chart.marker ",[211,1078,243],{"class":242},[211,1080,1081],{"class":246}," Marker\n",[211,1083,1084],{"class":54,"line":275},[211,1085,260],{"emptyLinePlaceholder":259},[211,1087,1088,1091,1095,1098,1100,1102,1105,1107,1109],{"class":54,"line":316},[211,1089,1090],{"class":242},"def",[211,1092,1094],{"class":1093},"s_Opv"," add_combo_chart",[211,1096,1097],{"class":246},"(ws, anchor",[211,1099,269],{"class":242},[211,1101,1017],{"class":220},[211,1103,1104],{"class":246},", last_row",[211,1106,269],{"class":242},[211,1108,529],{"class":324},[211,1110,1111],{"class":246},",\n",[211,1113,1114,1117,1119,1121,1124,1126,1128,1131,1133,1135],{"class":54,"line":355},[211,1115,1116],{"class":246},"                    bar_col",[211,1118,269],{"class":242},[211,1120,519],{"class":324},[211,1122,1123],{"class":246},", line_col",[211,1125,269],{"class":242},[211,1127,99],{"class":324},[211,1129,1130],{"class":246},", category_col",[211,1132,269],{"class":242},[211,1134,509],{"class":324},[211,1136,1111],{"class":246},[211,1138,1139,1142,1144,1147],{"class":54,"line":393},[211,1140,1141],{"class":246},"                    title",[211,1143,269],{"class":242},[211,1145,1146],{"class":220},"\"Revenue and margin by month\"",[211,1148,1111],{"class":246},[211,1150,1151,1154,1156,1159],{"class":54,"line":399},[211,1152,1153],{"class":246},"                    line_format",[211,1155,269],{"class":242},[211,1157,1158],{"class":220},"\"0.0%\"",[211,1160,1161],{"class":246},"):\n",[211,1163,1164],{"class":54,"line":538},[211,1165,1166],{"class":220},"    \"\"\"Bars on the primary axis, a line on a secondary axis.\"\"\"\n",[211,1168,1169,1172,1174,1176,1178,1180,1183,1185,1187,1189,1191,1193,1195],{"class":54,"line":549},[211,1170,1171],{"class":246},"    categories ",[211,1173,269],{"class":242},[211,1175,501],{"class":246},[211,1177,504],{"class":410},[211,1179,269],{"class":242},[211,1181,1182],{"class":246},"category_col, ",[211,1184,514],{"class":410},[211,1186,269],{"class":242},[211,1188,519],{"class":324},[211,1190,287],{"class":246},[211,1192,524],{"class":410},[211,1194,269],{"class":242},[211,1196,1197],{"class":246},"last_row)\n",[211,1199,1200],{"class":54,"line":560},[211,1201,260],{"emptyLinePlaceholder":259},[211,1203,1204,1207,1209],{"class":54,"line":571},[211,1205,1206],{"class":246},"    bars ",[211,1208,269],{"class":242},[211,1210,546],{"class":246},[211,1212,1213,1216,1218],{"class":54,"line":582},[211,1214,1215],{"class":246},"    bars.type ",[211,1217,269],{"class":242},[211,1219,557],{"class":220},[211,1221,1222,1225,1227],{"class":54,"line":593},[211,1223,1224],{"class":246},"    bars.title ",[211,1226,269],{"class":242},[211,1228,1229],{"class":246}," title\n",[211,1231,1232,1235,1237,1240,1243,1245,1247,1249,1252,1254],{"class":54,"line":609},[211,1233,1234],{"class":246},"    bars.y_axis.title ",[211,1236,269],{"class":242},[211,1238,1239],{"class":246}," ws.cell(",[211,1241,1242],{"class":410},"row",[211,1244,269],{"class":242},[211,1246,509],{"class":324},[211,1248,287],{"class":246},[211,1250,1251],{"class":410},"column",[211,1253,269],{"class":242},[211,1255,1256],{"class":246},"bar_col).value\n",[211,1258,1259,1262,1264,1266,1268,1270,1272,1274,1276,1278],{"class":54,"line":614},[211,1260,1261],{"class":246},"    bars.x_axis.title ",[211,1263,269],{"class":242},[211,1265,1239],{"class":246},[211,1267,1242],{"class":410},[211,1269,269],{"class":242},[211,1271,509],{"class":324},[211,1273,287],{"class":246},[211,1275,1251],{"class":410},[211,1277,269],{"class":242},[211,1279,1280],{"class":246},"category_col).value\n",[211,1282,1283,1286,1288],{"class":54,"line":655},[211,1284,1285],{"class":246},"    bars.y_axis.numFmt ",[211,1287,269],{"class":242},[211,1289,1290],{"class":220}," \"#,##0\"\n",[211,1292,1294,1297,1299,1301,1303],{"class":54,"line":1293},18,[211,1295,1296],{"class":246},"    bars.height, bars.width ",[211,1298,269],{"class":242},[211,1300,601],{"class":324},[211,1302,287],{"class":246},[211,1304,606],{"class":324},[211,1306,1308,1311,1313,1315,1318,1320,1322,1324,1326,1328,1330],{"class":54,"line":1307},19,[211,1309,1310],{"class":246},"    bars.add_data(Reference(ws, ",[211,1312,504],{"class":410},[211,1314,269],{"class":242},[211,1316,1317],{"class":246},"bar_col, ",[211,1319,514],{"class":410},[211,1321,269],{"class":242},[211,1323,509],{"class":324},[211,1325,287],{"class":246},[211,1327,524],{"class":410},[211,1329,269],{"class":242},[211,1331,1332],{"class":246},"last_row),\n",[211,1334,1336,1339,1341,1343],{"class":54,"line":1335},20,[211,1337,1338],{"class":410},"                  titles_from_data",[211,1340,269],{"class":242},[211,1342,650],{"class":324},[211,1344,419],{"class":246},[211,1346,1348],{"class":54,"line":1347},21,[211,1349,1350],{"class":246},"    bars.set_categories(categories)\n",[211,1352,1354],{"class":54,"line":1353},22,[211,1355,260],{"emptyLinePlaceholder":259},[211,1357,1359,1362,1364],{"class":54,"line":1358},23,[211,1360,1361],{"class":246},"    line ",[211,1363,269],{"class":242},[211,1365,705],{"class":246},[211,1367,1369,1372,1374,1376,1379,1381,1383,1385,1387,1389,1391],{"class":54,"line":1368},24,[211,1370,1371],{"class":246},"    line.add_data(Reference(ws, ",[211,1373,504],{"class":410},[211,1375,269],{"class":242},[211,1377,1378],{"class":246},"line_col, ",[211,1380,514],{"class":410},[211,1382,269],{"class":242},[211,1384,509],{"class":324},[211,1386,287],{"class":246},[211,1388,524],{"class":410},[211,1390,269],{"class":242},[211,1392,1332],{"class":246},[211,1394,1396,1398,1400,1402],{"class":54,"line":1395},25,[211,1397,1338],{"class":410},[211,1399,269],{"class":242},[211,1401,650],{"class":324},[211,1403,419],{"class":246},[211,1405,1407,1410,1412],{"class":54,"line":1406},26,[211,1408,1409],{"class":246},"    line.y_axis.axId ",[211,1411,269],{"class":242},[211,1413,762],{"class":324},[211,1415,1417,1420,1422,1424,1426,1428,1430,1432,1434,1436],{"class":54,"line":1416},27,[211,1418,1419],{"class":246},"    line.y_axis.title ",[211,1421,269],{"class":242},[211,1423,1239],{"class":246},[211,1425,1242],{"class":410},[211,1427,269],{"class":242},[211,1429,509],{"class":324},[211,1431,287],{"class":246},[211,1433,1251],{"class":410},[211,1435,269],{"class":242},[211,1437,1438],{"class":246},"line_col).value\n",[211,1440,1442,1445,1447],{"class":54,"line":1441},28,[211,1443,1444],{"class":246},"    line.y_axis.numFmt ",[211,1446,269],{"class":242},[211,1448,1449],{"class":246}," line_format\n",[211,1451,1453,1456,1458],{"class":54,"line":1452},29,[211,1454,1455],{"class":246},"    line.y_axis.crosses ",[211,1457,269],{"class":242},[211,1459,801],{"class":220},[211,1461,1463,1466,1468],{"class":54,"line":1462},30,[211,1464,1465],{"class":246},"    line.x_axis.delete ",[211,1467,269],{"class":242},[211,1469,820],{"class":324},[211,1471,1473],{"class":54,"line":1472},31,[211,1474,260],{"emptyLinePlaceholder":259},[211,1476,1478,1481,1483,1486,1488],{"class":54,"line":1477},32,[211,1479,1480],{"class":246},"    series ",[211,1482,269],{"class":242},[211,1484,1485],{"class":246}," line.series[",[211,1487,42],{"class":324},[211,1489,1490],{"class":246},"]\n",[211,1492,1494,1497,1499,1502],{"class":54,"line":1493},33,[211,1495,1496],{"class":246},"    series.smooth ",[211,1498,269],{"class":242},[211,1500,1501],{"class":324}," False",[211,1503,1504],{"class":751},"                       # straight segments read honestly\n",[211,1506,1508,1511,1513,1516,1519,1521,1524,1526,1529,1531,1533],{"class":54,"line":1507},34,[211,1509,1510],{"class":246},"    series.marker ",[211,1512,269],{"class":242},[211,1514,1515],{"class":246}," Marker(",[211,1517,1518],{"class":410},"symbol",[211,1520,269],{"class":242},[211,1522,1523],{"class":220},"\"circle\"",[211,1525,287],{"class":246},[211,1527,1528],{"class":410},"size",[211,1530,269],{"class":242},[211,1532,529],{"class":324},[211,1534,419],{"class":246},[211,1536,1538,1541,1543,1546],{"class":54,"line":1537},35,[211,1539,1540],{"class":246},"    series.graphicalProperties.line.width ",[211,1542,269],{"class":242},[211,1544,1545],{"class":324}," 28_000",[211,1547,1548],{"class":751},"   # EMU, roughly 2.2pt\n",[211,1550,1552],{"class":54,"line":1551},36,[211,1553,260],{"emptyLinePlaceholder":259},[211,1555,1557,1559,1561],{"class":54,"line":1556},37,[211,1558,1206],{"class":246},[211,1560,940],{"class":242},[211,1562,1009],{"class":246},[211,1564,1566],{"class":54,"line":1565},38,[211,1567,1568],{"class":246},"    ws.add_chart(bars, anchor)\n",[211,1570,1572,1575],{"class":54,"line":1571},39,[211,1573,1574],{"class":242},"    return",[211,1576,1577],{"class":246}," bars\n",[10,1579,1580,1581,1584],{},"Two details in the series styling. ",[208,1582,1583],{},"smooth = False"," matters more than it looks — a smoothed line curves between points and implies values that were never measured, which is misleading on monthly data. And line width is set in EMU, English Metric Units, where 12,700 EMU is one point; 28,000 is a little over two points.",[196,1586,1588],{"id":1587},"step-4-know-when-a-second-axis-misleads","Step 4 — Know when a second axis misleads",[10,1590,1591],{},"A secondary axis is a genuine solution to a scale problem and a genuine way to mislead, because the relationship it appears to show depends entirely on where you put the scales.",[20,1593,30,1599,30,1602,30,1605,30,1608,30,1614,30,1618,30,1623,30,1627,30,1630,30,1633,30,1636,30,1639,30,1643,30,1648,30,1652,30,1655,30,1657,30,1659,30,1662,30,1665,30,1668],{"viewBox":1594,"role":23,"ariaLabel":1595,"ariaLabelledBy":1596,"xmlns":28,"style":29},"0 0 800 244","The same two series drawn with two different secondary-axis ranges, producing opposite visual impressions of whether the line leads or lags the bars.",[1597,1598],"mis-t","mis-d",[32,1600,1601],{"id":1597},"The same data, two secondary-axis ranges, two impressions",[36,1603,1604],{"id":1598},"The identical revenue bars and margin line drawn twice. On the left the secondary axis runs from zero to thirty per cent and the line sits above the bars, suggesting margin outpaces revenue. On the right the same data with the secondary axis running from zero to sixty per cent puts the line below the bar tops, suggesting the opposite. Nothing about the data changed; only the scale did. That is why a secondary axis should be used to make a series legible, never to imply a relationship.",[40,1606],{"x":42,"y":42,"width":43,"height":1607,"fill":45},"244",[47,1609,1613],{"x":1610,"y":1611,"style":1612},"198","28","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","right axis 0–30%",[47,1615,1617],{"x":1616,"y":1611,"style":1612},"602","right axis 0–60% · same data",[54,1619],{"x1":1620,"y1":1621,"x2":1622,"y2":1621,"stroke":59,"style":60},"46","170","350",[40,1624],{"x":96,"y":123,"width":1625,"height":1626,"rx":99,"fill":100},"40","74",[40,1628],{"x":77,"y":105,"width":1625,"height":1629,"rx":99,"fill":100},"58",[40,1631],{"x":975,"y":921,"width":1625,"height":1632,"rx":99,"fill":100},"90",[40,1634],{"x":1635,"y":96,"width":1625,"height":124,"rx":99,"fill":100},"252",[126,1637],{"d":1638,"fill":129,"stroke":68,"style":130},"M86 74 L 148 88 L 210 56 L 272 44",[47,1640,1642],{"x":1610,"y":57,"style":1641},"font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","the line rides above the bars",[47,1644,1647],{"x":1610,"y":1645,"style":1646},"222","font-size:10px;fill:var(--muted,#5b6780);text-anchor:middle","\"margin is outpacing revenue\"",[54,1649],{"x1":1650,"y1":1621,"x2":1651,"y2":1621,"stroke":59,"style":60},"450","754",[40,1653],{"x":1654,"y":123,"width":1625,"height":1626,"rx":99,"fill":100},"470",[40,1656],{"x":153,"y":105,"width":1625,"height":1629,"rx":99,"fill":100},[40,1658],{"x":973,"y":921,"width":1625,"height":1632,"rx":99,"fill":100},[40,1660],{"x":1661,"y":96,"width":1625,"height":124,"rx":99,"fill":100},"656",[126,1663],{"d":1664,"fill":129,"stroke":68,"style":130},"M490 122 L 552 129 L 614 113 L 676 107",[47,1666,1667],{"x":1616,"y":57,"style":1641},"the line sits inside the bars",[47,1669,1670],{"x":1616,"y":1645,"style":1646},"\"margin is lagging revenue\"",[10,1672,1673],{},"The defensible uses are narrow: the two series measure genuinely different quantities, and the reader needs to see them against the same time axis. The indefensible use is implying a correlation, because with two free scales you can make any two series appear to move together or apart.",[10,1675,1676,1677,1680],{},"Two alternatives are usually better when the goal is comparison. ",[1034,1678,1679],{},"Stack two charts"," sharing a category axis, so each has an honest zero-based scale:",[201,1682,1684],{"className":233,"code":1683,"language":235,"meta":206,"style":206},"from openpyxl.chart import BarChart, LineChart, Reference\n\nrevenue = BarChart()\nrevenue.type = \"col\"\nrevenue.title = \"Revenue\"\nrevenue.add_data(Reference(ws, min_col=2, min_row=1, max_row=7),\n                 titles_from_data=True)\nrevenue.set_categories(categories)\nrevenue.height, revenue.width = 6, 18\nws.add_chart(revenue, \"F2\")\n\nmargin = LineChart()\nmargin.title = \"Margin\"\nmargin.y_axis.numFmt = \"0.0%\"\nmargin.add_data(Reference(ws, min_col=3, min_row=1, max_row=7),\n                titles_from_data=True)\nmargin.set_categories(categories)\nmargin.height, margin.width = 6, 18\nws.add_chart(margin, \"F16\")\n",[208,1685,1686,1696,1700,1709,1718,1727,1757,1768,1773,1787,1796,1800,1809,1818,1827,1856,1867,1872,1885],{"__ignoreMap":206},[211,1687,1688,1690,1692,1694],{"class":54,"line":213},[211,1689,439],{"class":242},[211,1691,454],{"class":246},[211,1693,243],{"class":242},[211,1695,1069],{"class":246},[211,1697,1698],{"class":54,"line":256},[211,1699,260],{"emptyLinePlaceholder":259},[211,1701,1702,1705,1707],{"class":54,"line":263},[211,1703,1704],{"class":246},"revenue ",[211,1706,269],{"class":242},[211,1708,546],{"class":246},[211,1710,1711,1714,1716],{"class":54,"line":275},[211,1712,1713],{"class":246},"revenue.type ",[211,1715,269],{"class":242},[211,1717,557],{"class":220},[211,1719,1720,1723,1725],{"class":54,"line":316},[211,1721,1722],{"class":246},"revenue.title ",[211,1724,269],{"class":242},[211,1726,579],{"class":220},[211,1728,1729,1732,1734,1736,1738,1740,1742,1744,1746,1748,1750,1752,1754],{"class":54,"line":355},[211,1730,1731],{"class":246},"revenue.add_data(Reference(ws, ",[211,1733,504],{"class":410},[211,1735,269],{"class":242},[211,1737,519],{"class":324},[211,1739,287],{"class":246},[211,1741,514],{"class":410},[211,1743,269],{"class":242},[211,1745,509],{"class":324},[211,1747,287],{"class":246},[211,1749,524],{"class":410},[211,1751,269],{"class":242},[211,1753,529],{"class":324},[211,1755,1756],{"class":246},"),\n",[211,1758,1759,1762,1764,1766],{"class":54,"line":393},[211,1760,1761],{"class":410},"                 titles_from_data",[211,1763,269],{"class":242},[211,1765,650],{"class":324},[211,1767,419],{"class":246},[211,1769,1770],{"class":54,"line":399},[211,1771,1772],{"class":246},"revenue.set_categories(categories)\n",[211,1774,1775,1778,1780,1783,1785],{"class":54,"line":538},[211,1776,1777],{"class":246},"revenue.height, revenue.width ",[211,1779,269],{"class":242},[211,1781,1782],{"class":324}," 6",[211,1784,287],{"class":246},[211,1786,606],{"class":324},[211,1788,1789,1792,1794],{"class":54,"line":549},[211,1790,1791],{"class":246},"ws.add_chart(revenue, ",[211,1793,1017],{"class":220},[211,1795,419],{"class":246},[211,1797,1798],{"class":54,"line":560},[211,1799,260],{"emptyLinePlaceholder":259},[211,1801,1802,1805,1807],{"class":54,"line":571},[211,1803,1804],{"class":246},"margin ",[211,1806,269],{"class":242},[211,1808,705],{"class":246},[211,1810,1811,1814,1816],{"class":54,"line":582},[211,1812,1813],{"class":246},"margin.title ",[211,1815,269],{"class":242},[211,1817,772],{"class":220},[211,1819,1820,1823,1825],{"class":54,"line":593},[211,1821,1822],{"class":246},"margin.y_axis.numFmt ",[211,1824,269],{"class":242},[211,1826,782],{"class":220},[211,1828,1829,1832,1834,1836,1838,1840,1842,1844,1846,1848,1850,1852,1854],{"class":54,"line":609},[211,1830,1831],{"class":246},"margin.add_data(Reference(ws, ",[211,1833,504],{"class":410},[211,1835,269],{"class":242},[211,1837,99],{"class":324},[211,1839,287],{"class":246},[211,1841,514],{"class":410},[211,1843,269],{"class":242},[211,1845,509],{"class":324},[211,1847,287],{"class":246},[211,1849,524],{"class":410},[211,1851,269],{"class":242},[211,1853,529],{"class":324},[211,1855,1756],{"class":246},[211,1857,1858,1861,1863,1865],{"class":54,"line":614},[211,1859,1860],{"class":410},"                titles_from_data",[211,1862,269],{"class":242},[211,1864,650],{"class":324},[211,1866,419],{"class":246},[211,1868,1869],{"class":54,"line":655},[211,1870,1871],{"class":246},"margin.set_categories(categories)\n",[211,1873,1874,1877,1879,1881,1883],{"class":54,"line":1293},[211,1875,1876],{"class":246},"margin.height, margin.width ",[211,1878,269],{"class":242},[211,1880,1782],{"class":324},[211,1882,287],{"class":246},[211,1884,606],{"class":324},[211,1886,1887,1890,1893],{"class":54,"line":1307},[211,1888,1889],{"class":246},"ws.add_chart(margin, ",[211,1891,1892],{"class":220},"\"F16\"",[211,1894,419],{"class":246},[10,1896,1897,1898,1901],{},"Or ",[1034,1899,1900],{},"index both series to a common base",", so a single axis shows relative movement:",[201,1903,1905],{"className":233,"code":1904,"language":235,"meta":206,"style":206},"import pandas as pd\n\nindexed = monthly.copy()\nfor name in (\"revenue\", \"margin\"):\n    indexed[name] = indexed[name] \u002F indexed[name].iloc[0] * 100\n",[208,1906,1907,1917,1921,1931,1955],{"__ignoreMap":206},[211,1908,1909,1911,1913,1915],{"class":54,"line":213},[211,1910,243],{"class":242},[211,1912,247],{"class":246},[211,1914,250],{"class":242},[211,1916,253],{"class":246},[211,1918,1919],{"class":54,"line":256},[211,1920,260],{"emptyLinePlaceholder":259},[211,1922,1923,1926,1928],{"class":54,"line":263},[211,1924,1925],{"class":246},"indexed ",[211,1927,269],{"class":242},[211,1929,1930],{"class":246}," monthly.copy()\n",[211,1932,1933,1936,1939,1942,1945,1948,1950,1953],{"class":54,"line":275},[211,1934,1935],{"class":242},"for",[211,1937,1938],{"class":246}," name ",[211,1940,1941],{"class":242},"in",[211,1943,1944],{"class":246}," (",[211,1946,1947],{"class":220},"\"revenue\"",[211,1949,287],{"class":246},[211,1951,1952],{"class":220},"\"margin\"",[211,1954,1161],{"class":246},[211,1956,1957,1960,1962,1965,1968,1971,1973,1976,1979],{"class":54,"line":316},[211,1958,1959],{"class":246},"    indexed[name] ",[211,1961,269],{"class":242},[211,1963,1964],{"class":246}," indexed[name] ",[211,1966,1967],{"class":242},"\u002F",[211,1969,1970],{"class":246}," indexed[name].iloc[",[211,1972,42],{"class":324},[211,1974,1975],{"class":246},"] ",[211,1977,1978],{"class":242},"*",[211,1980,1981],{"class":324}," 100\n",[10,1983,1984,1985,18],{},"Then one axis reading \"index, first month = 100\" carries both series honestly. The line-chart mechanics are covered in ",[14,1986,1988],{"href":1987},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-line-chart-to-excel-report-with-python\u002F","adding a line chart to an Excel report",[196,1990,1992],{"id":1991},"common-pitfalls-and-fixes","Common pitfalls and fixes",[825,1994,1995,2008],{},[828,1996,1997],{},[831,1998,1999,2002,2005],{},[834,2000,2001],{},"Symptom",[834,2003,2004],{},"Cause",[834,2006,2007],{},"Fix",[841,2009,2010,2026,2042,2055,2068,2082,2098,2111],{},[831,2011,2012,2015,2020],{},[846,2013,2014],{},"Second series missing",[846,2016,2017,2018],{},"Shared ",[208,2019,879],{},[846,2021,2022,2023,18],{},"Set ",[208,2024,2025],{},"line.y_axis.axId = 200",[831,2027,2028,2031,2037],{},[846,2029,2030],{},"Both axes on the left",[846,2032,2033,2036],{},[208,2034,2035],{},"crosses"," not set",[846,2038,2039,18],{},[208,2040,2041],{},"line.y_axis.crosses = \"max\"",[831,2043,2044,2047,2050],{},[846,2045,2046],{},"Doubled category labels",[846,2048,2049],{},"Two x-axes drawn",[846,2051,2052,18],{},[208,2053,2054],{},"line.x_axis.delete = True",[831,2056,2057,2060,2063],{},[846,2058,2059],{},"Title and axis labels lost",[846,2061,2062],{},"Combined in the wrong order",[846,2064,2065,2066,18],{},"Put the chart owning the axes on the left of ",[208,2067,940],{},[831,2069,2070,2073,2076],{},[846,2071,2072],{},"Legend reads \"Series 1\"",[846,2074,2075],{},"Header not included",[846,2077,2078,664,2080,18],{},[208,2079,667],{},[208,2081,663],{},[831,2083,2084,2090,2093],{},[846,2085,2086,2087],{},"Margin axis shows ",[208,2088,2089],{},"0.15",[846,2091,2092],{},"No number format",[846,2094,2095,18],{},[208,2096,2097],{},"line.y_axis.numFmt = \"0.0%\"",[831,2099,2100,2103,2106],{},[846,2101,2102],{},"The line implies values between points",[846,2104,2105],{},"Smoothing on",[846,2107,2108,18],{},[208,2109,2110],{},"series.smooth = False",[831,2112,2113,2116,2119],{},[846,2114,2115],{},"Chart is tiny",[846,2117,2118],{},"Default size",[846,2120,2022,2121,2124,2125,2128],{},[208,2122,2123],{},"height"," and ",[208,2126,2127],{},"width"," in centimetres.",[196,2130,2132],{"id":2131},"performance-and-scale-notes","Performance and scale notes",[10,2134,2135],{},"A chart references cell ranges rather than copying values, so its cost is independent of the number of rows — the chart XML is the same size for a hundred points as for ten.",[10,2137,2138],{},"What does matter is how many points Excel has to draw. Beyond a few hundred, a line becomes an unreadable band and rendering slows noticeably; beyond a few thousand it is actively unpleasant to scroll past. Aggregate before charting rather than plotting raw transactions:",[201,2140,2142],{"className":233,"code":2141,"language":235,"meta":206,"style":206},"import pandas as pd\n\n# Chart the monthly summary, not the 400,000 underlying rows.\nmonthly = (\n    raw.groupby(pd.Grouper(key=\"date\", freq=\"MS\"))\n       .agg(revenue=(\"amount\", \"sum\"), margin=(\"margin\", \"mean\"))\n       .reset_index()\n)\n",[208,2143,2144,2154,2158,2163,2172,2198,2237,2242],{"__ignoreMap":206},[211,2145,2146,2148,2150,2152],{"class":54,"line":213},[211,2147,243],{"class":242},[211,2149,247],{"class":246},[211,2151,250],{"class":242},[211,2153,253],{"class":246},[211,2155,2156],{"class":54,"line":256},[211,2157,260],{"emptyLinePlaceholder":259},[211,2159,2160],{"class":54,"line":263},[211,2161,2162],{"class":751},"# Chart the monthly summary, not the 400,000 underlying rows.\n",[211,2164,2165,2167,2169],{"class":54,"line":275},[211,2166,266],{"class":246},[211,2168,269],{"class":242},[211,2170,2171],{"class":246}," (\n",[211,2173,2174,2177,2180,2182,2185,2187,2190,2192,2195],{"class":54,"line":316},[211,2175,2176],{"class":246},"    raw.groupby(pd.Grouper(",[211,2178,2179],{"class":410},"key",[211,2181,269],{"class":242},[211,2183,2184],{"class":220},"\"date\"",[211,2186,287],{"class":246},[211,2188,2189],{"class":410},"freq",[211,2191,269],{"class":242},[211,2193,2194],{"class":220},"\"MS\"",[211,2196,2197],{"class":246},"))\n",[211,2199,2200,2203,2206,2208,2211,2214,2216,2219,2221,2224,2226,2228,2230,2232,2235],{"class":54,"line":355},[211,2201,2202],{"class":246},"       .agg(",[211,2204,2205],{"class":410},"revenue",[211,2207,269],{"class":242},[211,2209,2210],{"class":246},"(",[211,2212,2213],{"class":220},"\"amount\"",[211,2215,287],{"class":246},[211,2217,2218],{"class":220},"\"sum\"",[211,2220,642],{"class":246},[211,2222,2223],{"class":410},"margin",[211,2225,269],{"class":242},[211,2227,2210],{"class":246},[211,2229,1952],{"class":220},[211,2231,287],{"class":246},[211,2233,2234],{"class":220},"\"mean\"",[211,2236,2197],{"class":246},[211,2238,2239],{"class":54,"line":393},[211,2240,2241],{"class":246},"       .reset_index()\n",[211,2243,2244],{"class":54,"line":399},[211,2245,419],{"class":246},[10,2247,2248,2249,2253],{},"That is the same aggregation described in ",[14,2250,2252],{"href":2251},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fgroup-excel-rows-by-month-and-quarter-with-pandas\u002F","grouping Excel rows by month and quarter",", and it improves the chart as well as the file.",[10,2255,2256,2257,2260,2261,2264,2265,18],{},"Three further notes. ",[1034,2258,2259],{},"Put the chart's source data on its own sheet"," when it is a summary — a hidden ",[208,2262,2263],{},"_chartdata"," sheet keeps the visible report clean while the chart still has a real range to reference, using the hiding technique from ",[14,2266,2268],{"href":2267},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fhide-sheets-rows-and-columns-with-openpyxl\u002F","hiding sheets, rows and columns",[10,2270,2271,2274],{},[1034,2272,2273],{},"openpyxl cannot preserve a chart it did not create."," Loading a workbook that already contains charts and saving it drops them, so a script that adds a combo chart to an existing report must recreate every chart in that workbook, not just the new one.",[10,2276,2277,2284],{},[1034,2278,2279,2280,2283],{},"Charts cannot be added in ",[208,2281,2282],{},"write_only"," mode."," A large report needing both streaming and a chart has to be written in two passes: stream the data, then re-open normally and add the chart. Since the chart only references ranges, that second pass touches almost nothing and is cheap.",[196,2286,2288],{"id":2287},"conclusion","Conclusion",[10,2290,2291,2292,2294,2295,287,2298,2301,2302,2304],{},"A combo chart is two openpyxl chart objects merged with ",[208,2293,940],{},", and the three settings on the secondary chart are what make it work: a distinct ",[208,2296,2297],{},"y_axis.axId",[208,2299,2300],{},"crosses = \"max\""," so the scale draws on the right, and ",[208,2303,870],{}," so the category axis is not drawn twice. Put the chart that owns the title and axes on the left of the operator. Then be deliberate about whether a second axis is the right answer at all — it makes a small-scale series legible, but with two free scales it can imply any relationship you like, so when the point is comparison, stack two honest charts or index both series to a common base instead.",[196,2306,2308],{"id":2307},"frequently-asked-questions","Frequently asked questions",[10,2310,2311,2314,2315,2317],{},[1034,2312,2313],{},"How do I combine two chart types in openpyxl?","\nBuild each chart separately, then add the second to the first with the ",[208,2316,940],{}," operator. The first chart owns the axes and the title; the second contributes only its series and, if configured, a secondary axis.",[10,2319,2320,2323,2324,2326,2327,2330,2331,2333],{},[1034,2321,2322],{},"Why does my secondary series not appear?","\nAlmost always the axis identifiers. The secondary chart's ",[208,2325,2297],{}," must differ from the primary's, and its ",[208,2328,2329],{},"x_axis.delete"," must be ",[208,2332,650],{}," so the two charts do not both draw a category axis over each other.",[10,2335,2336,2339,2340,2343,2344,2347],{},[1034,2337,2338],{},"How do I put the second axis on the right?","\nSet the secondary chart's ",[208,2341,2342],{},"y_axis.crosses"," to ",[208,2345,2346],{},"\"max\""," so it crosses the category axis at the far end. Without that it draws on the left, overlapping the primary scale.",[10,2349,2350,2353],{},[1034,2351,2352],{},"When is a secondary axis a bad idea?","\nWhenever the two scales have no relationship, because the reader can be led to any conclusion by the choice of scale. Prefer two stacked charts sharing a category axis, or index both series to a common base.",[10,2355,2356,2359,2360,2363,2364,2367],{},[1034,2357,2358],{},"Can I do this with xlsxwriter instead?","\nYes, and its API is arguably clearer: build both charts, call ",[208,2361,2362],{},"combine"," on the primary, and set ",[208,2365,2366],{},"y2_axis"," on the secondary series. The concepts map directly.",[196,2369,2371],{"id":2370},"related","Related",[2373,2374,2375,2382,2388,2395,2402],"ul",{},[2376,2377,2378,2379,2381],"li",{},"Up to the parent: ",[14,2380,17],{"href":16}," — the chart types this combines.",[2376,2383,2384,2387],{},[14,2385,2386],{"href":1987},"Add a Line Chart to an Excel Report with Python"," — the secondary series on its own.",[2376,2389,2390,2394],{},[14,2391,2393],{"href":2392},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-bar-chart-in-excel-with-openpyxl\u002F","Create a Bar Chart in Excel with openpyxl"," — the primary chart in depth.",[2376,2396,2397,2401],{},[14,2398,2400],{"href":2399},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fadd-a-chart-to-an-excel-file-with-xlsxwriter\u002F","Add a Chart to an Excel File with xlsxwriter"," — the other engine's charting API.",[2376,2403,2404,2407],{},[14,2405,2406],{"href":2251},"Group Excel Rows by Month and Quarter with pandas"," — producing the aggregate a chart should plot.",[2409,2410,2411],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":206,"searchDepth":256,"depth":256,"links":2413},[2414,2415,2416,2417,2418,2419,2420,2421,2422,2423],{"id":198,"depth":256,"text":199},{"id":425,"depth":256,"text":426},{"id":671,"depth":256,"text":672},{"id":890,"depth":256,"text":891},{"id":1587,"depth":256,"text":1588},{"id":1991,"depth":256,"text":1992},{"id":2131,"depth":256,"text":2132},{"id":2287,"depth":256,"text":2288},{"id":2307,"depth":256,"text":2308},{"id":2370,"depth":256,"text":2371},"2026-08-15","Plot revenue as bars and margin as a line on their own axis — combining chart objects, the y_axis.axId and crosses settings, and when a second axis misleads.","md",[2428,2430,2432,2434,2436],{"q":2313,"a":2429},"Build each chart separately, then add the second to the first with the plus operator. The first chart owns the axes and the title; the second contributes only its series and, if configured, a secondary axis.",{"q":2322,"a":2431},"Almost always the axis identifiers. The secondary chart's y_axis.axId must differ from the primary's, and its x_axis.delete must be True so the two charts do not both draw a category axis over each other.",{"q":2338,"a":2433},"Set the secondary chart's y_axis.crosses to \"max\" so it crosses the category axis at the far end. Without that it draws on the left, overlapping the primary scale.",{"q":2352,"a":2435},"Whenever the two scales have no relationship, because the reader can be led to any conclusion by the choice of scale. Prefer two stacked charts sharing a category axis, or index both series to a common base.",{"q":2358,"a":2437},{"Yes, and its API is arguably clearer":2438},"build both charts, call combine on the primary, and set y2_axis on the secondary series. The concepts map directly.",{},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-a-combo-chart-with-a-secondary-axis-in-openpyxl",{"title":2442,"description":2443},"Combo Chart with a Secondary Axis in openpyxl","Build a bar-and-line combo chart in Python: combine two openpyxl chart objects, set a distinct axId for the secondary axis, position it on the right, and label both scales.","add-a-combo-chart-with-a-secondary-axis-in-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-a-combo-chart-with-a-secondary-axis-in-openpyxl\u002Findex","how-to","O9jv5lJ7MRKKypb67IV_NgqohusBwOTTwdcBiv5KtlA",[2449,2452],{"title":17,"path":2450,"stem":2451,"children":-1},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Findex",{"title":2386,"path":2453,"stem":2454,"children":-1},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-line-chart-to-excel-report-with-python","formatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fadd-line-chart-to-excel-report-with-python\u002Findex",1786800028824]