Opbakningen til de fire gamle partier, 1953-2022

Dec 7, 2022

For et par år siden skrev jeg et indlæg, hvor jeg viste opbakningen til de fire gamle partier i meningsmålingerne fra 1957 til 2020. Jeg har ovenpå folketingsvalget forsøgt at arbejde med forskellige måder at visualisere opbakningen til de fire gamle partier ved de seneste folketingsvalg (og altså ikke meningsmålingerne). Jeg fandt det især interessant at kigge på en dynamisk visualisering af opbakningen over tid.

I nedenstående figur viser jeg således opbakningen til Socialdemokratiet, Venstre, Konservative og de Radikale ved folketingsvalgene fra 1953 til 2022. Hvert valgresultat vises i ét sekund før næste valg vises - og den begynder forfra, når den rammer 2022.

{"x":{"data":{"year":[1953,1953,1953,1953,1957,1957,1957,1957,1960,1960,1960,1960,1964,1964,1964,1964,1966,1966,1966,1966,1968,1968,1968,1968,1971,1971,1971,1971,1973,1973,1973,1973,1975,1975,1975,1975,1977,1977,1977,1977,1979,1979,1979,1979,1981,1981,1981,1981,1984,1984,1984,1984,1987,1987,1987,1987,1988,1988,1988,1988,1990,1990,1990,1990,1994,1994,1994,1994,1998,1998,1998,1998,2001,2001,2001,2001,2005,2005,2005,2005,2007,2007,2007,2007,2011,2011,2011,2011,2015,2015,2015,2015,2019,2019,2019,2019,2022,2022,2022,2022],"name":["party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v","party_s","party_rv","party_k","party_v"],"value":[894913,169295,364960,499656,910170,179822,383843,578932,1023794,140979,435764,512041,1103667,139702,527798,547770,1068911,203858,522028,539027,974833,427304,581051,530167,1074777,413620,481335,450904,783145,343117,279391,374283,913155,216553,168164,711298,1150355,113330,263262,371728,1213456,172365,395653,396484,1026726,160053,451478,353280,1062561,184642,788224,405737,985906,209086,700886,354291,992682,185707,642048,394190,1211121,114888,517293,511643,1150048,152701,499845,775176,1223620,131254,303965,817894,1003323,179023,312770,1077858,867349,308212,344886,974636,881037,177161,359404,908472,879615,336698,175047,947725,924940,161009,118003,685188,914882,304714,233865,826161,971995,133931,194820,470546],"total_valid":[2166391,2166391,2166391,2166391,2310175,2310175,2310175,2310175,2431947,2431947,2431947,2431947,2631384,2631384,2631384,2631384,2794007,2794007,2794007,2794007,2854647,2854647,2854647,2854647,2883900,2883900,2883900,2883900,3053203,3053203,3053203,3053203,3045452,3045452,3045452,3045452,3106297,3106297,3106297,3106297,3171002,3171002,3171002,3171002,3123563,3123563,3123563,3123563,3361830,3361830,3361830,3361830,3362557,3362557,3362557,3362557,3329129,3329129,3329129,3329129,3239662,3239662,3239662,3239662,3327597,3327597,3327597,3327597,3405997,3405997,3405997,3405997,3449668,3449668,3449668,3449668,3357212,3357212,3357212,3357212,3459420,3459420,3459420,3459420,3545368,3545368,3545368,3545368,3518987,3518987,3518987,3518987,3531720,3531720,3531720,3531720,3533951,3533951,3533951,3533951],"navn":["Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre","Socialdemokratiet","Det Radikale Venstre","Det Konservative Folkeparti","Venstre"],"procent":[41.3089326903592,7.81460964341156,16.8464510792373,23.0639806018396,39.3983139805426,7.78391247416321,16.6153213501142,25.0600928501088,42.0977101885855,5.79696021335991,17.918318121242,21.0547762759632,41.9424530969254,5.30906929585344,20.0578098825561,20.8168021087002,38.2572770934361,7.29625945819033,18.6838472487721,19.2922566049405,34.1489858465863,14.9687159217935,20.3545657308942,18.5720686305522,37.26817850827,14.342383577794,16.6904192239675,15.6352162002843,25.6499485949673,11.2379360298021,9.15075086720405,12.2587001257368,29.9842190912876,7.11070146566093,5.5218076003168,23.3560732528373,37.0330010298436,3.64839550113849,8.47510717745277,11.9669175226966,38.2672732467529,5.43566355366537,12.477223287781,12.5034295153393,32.8703471004107,5.12405224418397,14.4539425009196,11.3101608643719,31.6066249631897,5.49230627366643,23.4462777713329,12.0689326943956,29.3201275101061,6.21806559710363,20.8438399705938,10.5363567071131,29.8180695310996,5.57824584147986,19.2857651355655,11.8406345924114,37.3841777321214,3.54629587901454,15.9674990786076,15.793098168883,34.5609158801381,4.58892708461992,15.0211999830508,23.295369000513,35.9254573624111,3.85361466848033,8.92440598156722,24.0133505695983,29.0846249552131,5.18957186604624,9.06666960414741,31.2452676605401,25.8353955603638,9.18059389755547,10.2729884201534,29.0311127209125,25.4677662729591,5.12111856900868,10.3891403761324,26.2608182874586,24.8102594709491,9.49684207676044,4.93734359874631,26.7313576474995,26.284268739839,4.57543605588767,3.35332298755295,19.4711716752577,25.9047149830677,8.62792067321305,6.62184431381876,23.3925962420577,27.504484357593,3.78983749350231,5.51280988332889,13.3150120078066]},"type":"data.frame","container":"div","options":{"x":"procent","y":"navn","time":"year","ease":"Linear","frameDur":1000,"transitionDur":500,"colorCategory":"Set1","sort":"descending","paddingWidth":0.1,"xFontSize":10,"yFontSize":10,"xticks":10,"xtitle":"Opbakning (%)","ytitle":null,"xtitleFontSize":16,"ytitleFontSize":14,"title":null,"titleFontSize":22,"stroke":"black","strokeWidth":null,"font":"Verdana, Geneva, Tahoma, sans-serif","bgcol":"white","panelcol":"#f6f6f6","xgridlinecol":"white","opacity":1,"timeLabel":true,"timeLabelOpts":{"size":32,"prefix":"","suffix":"","xOffset":0.5,"yOffset":1}},"script":"var d3Script = function(d3, r2d3, data, div, width, height, options, theme, console) {\nthis.d3 = d3;\n\ndiv = d3.select(div.node());\n/* R2D3 Source File: /Library/Frameworks/R.framework/Versions/4.2/Resources/library/ddplot/d3/scatterplot/barchartrace.js */\n// set plot dimensions\nconst dims = {\n width: width,\n height: height,\n margin: { t: 40, r: 30, b: 50, l: 0 },\n};\n\n// draw svg canvas\nconst svg = div\n .append(\"svg\")\n .datum(data)\n .attr(\"width\", dims.width)\n .attr(\"height\", dims.height)\n .call((svg) => {\n svg\n .append(\"rect\")\n .attr(\"width\", dims.width)\n .attr(\"height\", dims.height)\n .attr(\"fill\", options.bgcol);\n });\n\n// panel\n\n//// adjust left plot margin to fit y-axis text (TODO this doesnt work)\nconst longestLabel = data\n .map((d) => d[options.y])\n .sort((a, b) => b.length - a.length);\nconst longestText = svg.append(\"text\").text(longestLabel[0]);\ndims.margin.l = longestText.node().getComputedTextLength() * 1.1;\nlongestText.remove();\n\n//// compute panel dims\ndims.panel = {\n width: width - dims.margin.r - dims.margin.l,\n height: dims.height - dims.margin.t - dims.margin.b,\n};\n\n//// draw panel\nconst panel = svg\n .append(\"g\")\n .attr(\"transform\", translate(${dims.margin.l}, ${dims.margin.t}))\n .call((g) => {\n g.append(\"rect\")\n .attr(\"width\", dims.panel.width)\n .attr(\"height\", dims.panel.height)\n .attr(\"fill\", options.panelcol);\n });\n\n// init static elements\n\nconst title = svg\n .append(\"text\")\n .attr(\"x\", dims.width / 2)\n .attr(\"y\", dims.margin.t / 1.5)\n .style(\"text-anchor\", \"middle\")\n .style(\"font-size\", options.titleFontSize)\n .style(\"font-family\", options.font)\n .text(options.title);\n\nconst xGridLines = panel\n .append(\"g\")\n .attr(\"class\", \"x-gridlines\")\n .attr(\"transform\", translate(0, ${dims.panel.height}));\n\nconst layerBar = panel.append(\"g\").attr(\"class\", \"layer-bar\");\n\nlet xAxis = panel\n .append(\"g\")\n .attr(\"class\", \"x-axis\")\n .attr(\"transform\", translate(0, ${dims.panel.height}))\n .style(\"font-size\", options.xFontSize)\n .style(\"font-family\", options.font);\n\nlet yAxis = panel\n .append(\"g\")\n .attr(\"class\", \"y-axis\")\n .style(\"font-size\", options.yFontSize)\n .style(\"font-family\", options.font);\n\nconst xAxisTitle = xAxis\n .append(\"text\")\n .attr(\"x\", dims.panel.width * 0.5)\n .attr(\"y\", dims.margin.b * 0.8)\n .style(\"text-anchor\", \"middle\")\n .style(\"fill\", \"black\")\n .style(\"font-size\", options.xtitleFontSize)\n .style(\"font-family\", options.font)\n .text(options.xtitle);\n\nconst yAxisTitle = yAxis\n .append(\"text\")\n .attr(\"y\", -dims.margin.t * 0.3)\n .style(\"text-anchor\", \"middle\")\n .style(\"fill\", \"black\")\n .style(\"font-size\", options.ytitleFontSize)\n .style(\"font-family\", options.font)\n .text(options.ytitle);\n\nconst timeLabel = panel.append(\"g\").attr(\"class\", \"layer-timelabel\");\nconst timeLabelText = timeLabel\n .append(\"text\")\n .style(\"text-anchor\", \"end\")\n .style(\"font-size\", options.timeLabelOpts.size)\n .style(\"font-family\", options.font);\n\n// initialize scales\n\nconst xScale = d3.scaleLinear().range([0, dims.panel.width]);\n\nconst yScale = d3.scaleBand().range([dims.panel.height, 0]);\n\nconst colorScale = d3.scaleOrdinal();\n\n// animating function\nfunction update(frame, init = false) {\n const t = svg\n .transition()\n .ease(d3[\"ease\" + options.ease])\n .duration(options.transitionDur);\n\n // setup data\n const frameData = data.filter((d) => d[options.time] == frame);\n\n frameData.sort(function (a, b) {\n return d3[options.sort](a[options.x], b[options.x]);\n });\n\n // update scales\n xScale.domain([0, d3.max(data, (d) => d[options.x]) * 1.05]).nice();\n //// y and color scale updated here for now for the\n //// future possibility of bars entering from out-of-frame\n const yLabels = [...new Set(frameData.map((d) => d[options.y]))];\n yScale\n .domain(d3.reverse(yLabels))\n .paddingInner(options.paddingWidth)\n .paddingOuter(options.paddingWidth);\n colorScale\n .domain(yLabels)\n .range(d3[\"scheme\" + options.colorCategory].slice(0, yLabels.length));\n\n // draw bars\n\n layerBar\n .selectAll(\".bar\")\n .data(frameData, (d) => d[options.y])\n .join(\n (enter) => {\n enter\n .append(\"rect\")\n .attr(\"class\", \"bar\")\n .attr(\"width\", (d) => xScale(d[options.x]))\n .attr(\"height\", yScale.bandwidth())\n .attr(\"y\", (d) => yScale(d[options.y]))\n .attr(\"fill\", (d) => colorScale(d[options.y]))\n .style(\"opacity\", options.opacity)\n .style(\"stroke\", options.stroke)\n .style(\"stroke-width\", options.strokeWidth);\n },\n (update) => {\n update.call((update) =>\n update\n .transition(t)\n .attr(\"width\", (d) => xScale(d[options.x]))\n .attr(\"y\", (d) => yScale(d[options.y]))\n );\n }\n );\n\n // draw axes\n xGridLines\n .call(\n d3.axisBottom(xScale).ticks(options.xticks).tickSize(-dims.panel.height)\n )\n .call((g) =>\n g\n .selectAll(\"line\")\n .style(\"stroke\", options.xgridlinecol)\n .style(\"stroke-width\", 2)\n )\n .call((g) => {\n g.selectAll(\"path\").remove();\n g.selectAll(\"text\").remove();\n });\n xAxis.call(d3.axisBottom(xScale).ticks(options.xticks));\n\n if (init) {\n yAxis.call(d3.axisLeft(yScale));\n } else {\n yAxis.call((g) => g.transition(t).call(d3.axisLeft(yScale)));\n }\n\n // label the frame\n if (options.timeLabel) {\n timeLabelText.text(\n options.timeLabelOpts.prefix + frame + options.timeLabelOpts.suffix\n );\n\n const timeLabelDims = timeLabelText.node().getBBox();\n\n timeLabel.attr(\n \"transform\",\n translate(\\n ${\\n dims.panel.width - timeLabelDims.width \* options.timeLabelOpts.xOffset\\n },\\n ${\\n dims.panel.height -\\n timeLabelDims.height \* options.timeLabelOpts.yOffset\\n }\\n )\n );\n }\n}\n\n// TODO format time and sort + expose method as parameter\nconst timeSet = [...new Set(data.map((d) => d[options.time]))];\n\nupdate(timeSet[0], (init = true));\nvar counter = 1;\nsetInterval(() => {\n update(timeSet[counter % timeSet.length]);\n counter++;\n}, options.frameDur + options.transitionDur);\n};","style":null,"version":6,"theme":{"default":{"background":"#FFFFFF","foreground":"#000000"},"runtime":{"background":"#FFFFFF","foreground":"#000000"}},"useShadow":true},"evals":[],"jsHooks":[]}{"viewer":{"width":550,"height":350,"padding":15,"fill":false},"browser":{"width":560,"height":300,"padding":40,"fill":false}}Det er ikke nødvendigvis en bedre visualisering end en simpel tidsseriegraf, men som med alle visualiseringer handler det om, hvilken pointe man ønsker at visualisere (og til hvilket publikum). En dynamisk figur kan forhåbentlig virke mere engagerende for nogle læsere end fire linjer på en figur med opbakningen på y-aksen og en række årstal på x-aksen.

Data er taget fra Simon Straubingers GitHub repository. Jeg har brugt pakken ddplot i R til at lave figuren. Koden til at lave figuren kan findes på GitHub.

Erik Gahner Larsen
RSS
https://erikgahner.github.io/posts/feed.xml