FileAttachmentDynamic = function(filename) {
return new Function("FileAttachment", `return FileAttachment("${filename}")`)(FileAttachment)
}
// Vaihdetaan tekstin väri
color = function(vari) {
if(vari > 0){
return "green"
}else{
return "red"
}
}
// Pyöristys
pyoristys = function(n, places) {
if (!places) return Math.round(n);
const d = 10 ** places;
return Math.round(n * d) / d;
}
//Euromerkintä
eurot = function(number) {
return number.toLocaleString('fi-FI', { style: 'currency', currency: 'EUR',minimumFractionDigits: 0 });
}
// Euromuutoksen laskeminen kertoimella
muutoskerroin = function(x, y) {
return (1+x/100)*y;
}
// Muutosprosentti
muutosprosentti = function(x, y) {
return pyoristys(((x-y)/y)*100,1);
}
// Tuhatjakaja
tuhatjakaja = function(x) {
return x/1000;
}
// Euromuutoksen laskeminen kertoimella
palkkakerroin = function(x, y) {
return 100*((1+x/100)*(1+y/100)-1);
}
// Jako-osuuden asukaskerroin
asukaskerroin = function(x, y) {
return (x/y);
}
// Jako-osuuden veroprosenttikerroin miinus1/miinus3 vuotta miinus 1 vuoden kerroin
veroprosenttikerroin = function(x, y) {
return (x/y);
}
// Kunnan muok. kunnallisvero, alkuvuosi ja loppuvuosi
kunnallisvero_muokkaus = function(kunnallisvero,asukaskerroin,veroprosentti) {
return kunnallisvero*asukaskerroin*veroprosentti;
}
//Tekstimuutokset
fn_nousu_lasku_txt = function(arvo){
if(arvo > 0) {
return "kasvanut"
}
else {
return "laskenut";
}
}
//TARVITAAN KARTOISSA
serialize = {
const xmlns = "http://www.w3.org/2000/xmlns/";
const xlinkns = "http://www.w3.org/1999/xlink";
const svgns = "http://www.w3.org/2000/svg";
return function serialize(svg) {
svg = svg.cloneNode(true);
const fragment = window.location.href + "#";
const walker = document.createTreeWalker(svg, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
for (const attr of walker.currentNode.attributes) {
if (attr.value.includes(fragment)) {
attr.value = attr.value.replace(fragment, "#");
}
}
}
svg.setAttributeNS(xmlns, "xmlns", svgns);
svg.setAttributeNS(xmlns, "xmlns:xlink", xlinkns);
const serializer = new window.XMLSerializer;
const string = serializer.serializeToString(svg);
return new Blob([string], {type: "image/svg+xml"});
};
}
function toSVG(chart) {
if (chart.nodeName !== "FIGURE") {
return chart;
}
// the chart needs to be in the body if we want to read values, positions, sizes…
document.body.appendChild(chart);
const [x0, y0, width, height] = getBounds([chart]);
const nodes = [];
for (const node of d3
.select(chart)
.selectChildren("h1,h2,h3,div,figcaption,svg")) {
switch (node.nodeName.toLowerCase()) {
case "div":
{
const children = d3.select(node).selectChildren("div,span");
const height = getBounds([node, ...children])[3] + 2;
const svg = d3
.select(chart)
.append("svg")
.attr("width", width)
.attr("height", height);
nodes.push(svg.node());
const swatches = svg
.selectAll()
.data(
Array.from(children, (d) => {
const svg = d3.select(d).select("svg").node();
const bbox = svg.getBBox();
return {
style: window.getComputedStyle(d),
svg,
width: bbox.width,
height: bbox.height,
text: d.textContent,
bounds: getBounds([d])
};
})
)
.join("g")
.attr(
"transform",
(d) => `translate(${d.bounds[0] - x0},${10 + d.bounds[1] - y0})`
);
swatches
.append((d) => d.svg) // "rect")
.attr("width", (d) => d.width)
.attr("height", (d) => d.height)
.attr("y", (d) => `${-parseFloat(d.height) / 2}px`);
swatches
.append("text")
.text((d) => d.text)
.attr("x", (d) => d.width)
.attr("dx", 5)
.attr("dy", "0.38em")
.attr("font-family", (d) => d.style.fontFamily)
.attr("font-size", (d) => d.style.fontSize)
.attr("fill", (d) => d.style.color);
}
break;
case "figcaption":
case "h1":
case "h2":
case "h3":
{
const svg = d3
.select(chart)
.append("svg")
.attr("width", width)
.attr("overflow", "visible");
nodes.push(svg.node());
const children = d3.select(node).selectChildren();
let h = 0;
for (const d of children.size() > 0
? children.selectChildren()
: [node]) {
const style = window.getComputedStyle(d);
const t = svg
.append("g")
.attr("transform", `translate(0,${h})`)
.append(() =>
d3
.select(
Plot.text([d.textContent], {
text: (d) => d,
lineWidth:
(1.06 * parseFloat(style.width)) /
parseFloat(style.fontSize),
lineHeight: 1.2,
frameAnchor: "top-left"
}).plot()
)
.select("text")
.attr("font-family", style.fontFamily)
.attr("font-size", 1.08 * parseFloat(style.fontSize))
.attr("font-weight", style.fontWeight)
.attr("fill", style.color)
.node()
);
h += getBounds([t.node()])[3] + 4;
}
svg.attr("height", h);
}
break;
case "svg":
d3.select(chart).append(() => node);
nodes.push(node);
break;
}
}
return serializeAll(nodes)
.then((blob) => blob.text())
.then((c) => {
document.body.removeChild(chart);
return Object.assign(svg`${c}`, chart);
});
}
// Given an array of SVG elements, composites them into a single SVG element,
// and then serializes the result to a blob.
async function serializeAll(elements, {padding = 10} = {}) {
const fragment = location.href + "#";
let root;
if (elements.length === 1) {
root = elements[0].cloneNode(true); // optimize common case
} else {
const [ox, oy, dx, dy] = getBounds(elements);
root = document.createElementNS(svgns, "svg");
root.setAttribute("width", dx + 2 * padding);
root.setAttribute("height", dy + 2 * padding);
root.setAttribute("viewBox", [-padding, -padding, dx + 2 * padding, dy + 2 * padding]);
for (const element of elements) {
const svg = root.appendChild(element.cloneNode(true));
const { x, y, width, height } = element.getBoundingClientRect();
svg.setAttribute("x", x - ox);
svg.setAttribute("y", y - oy);
svg.setAttribute("width", width);
svg.setAttribute("height", height);
}
}
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
const node = walker.currentNode;
for (const attr of node.attributes) {
if (attr.value.includes(fragment)) {
attr.value = attr.value.replace(fragment, "#");
}
}
}
root.setAttributeNS(xmlns, "xmlns", svgns);
root.setAttributeNS(xmlns, "xmlns:xlink", xlinkns);
const serializer = new XMLSerializer();
const string = serializer.serializeToString(root);
return new Blob([string], { type: "image/svg+xml" });
}
function getBounds(elements) {
let x1 = Infinity;
let y1 = x1;
let x2 = -x1;
let y2 = x2;
for (const element of elements) {
const { x, y, width, height } = element.getBoundingClientRect();
if (x < x1) x1 = x;
if (x + width > x2) x2 = x + width;
if (y < y1) y1 = y;
if (y + height > y2) y2 = y + height;
}
return [x1, y1, x2 - x1, y2 - y1];
}
// based on https://observablehq.com/@gka/cheap-fit-text-to-circle
function lines(text, targetWidth) {
const CHAR_W = {
"a":7,"B":8,"C":8,"c":6,"D":9,"f":4,"G":9,"H":9,"I":3,"i":3,"J":5,"j":3,"K":8,"k":6,
"l":3,"M":11,"m":11,"N":9,"O":9,"P":8,"Q":9,"R":8,"r":4,"S":8,"s":6,"t":4,"U":9,"v":6,
"W":11,"w":9,"x":6,"y":6,"z":5,".":2,",":2,":":2,";":2
};
function measureWidth(text) { return d3.sum(text, char => CHAR_W[char] || CHAR_W["a"]) * 0.8; };
const words = text.split(" ");
let line;
let lineWidth0 = Infinity;
const lines = [];
for (let i = 0, n = words.length; i < n; ++i) {
let lineText1 = (line ? line.text + " " : "") + words[i];
let lineWidth1 = measureWidth(lineText1);
if ((lineWidth0 + lineWidth1) / 2 < targetWidth) {
line.width = lineWidth0 = lineWidth1;
line.text = lineText1;
} else {
lineWidth0 = measureWidth(words[i]);
line = {width: lineWidth0, text: words[i]};
lines.push(line);
}
}
return lines;
}
xmlns = "http://www.w3.org/2000/xmlns/"
xlinkns = "http://www.w3.org/1999/xlink"
svgns = "http://www.w3.org/2000/svg"
Avainsanat
Kuntien kuukausiraportti, väestö, yhteistyö, avoin lähdekoodi, Tilastokeskus, tietojohtaminen
d3 = require("d3@7")
//import {Legend} from "@d3/color-legend"
folderKunnat24TopoJSON = "./maps/Kunnat2024Topo.json"
kunnat2024topo = FileAttachmentDynamic(folderKunnat24TopoJSON).json()
mapTopoJSON = topojson.feature(kunnat2024topo, kunnat2024topo.objects.Kunnat2024_geo).features
folderMaakunnat24TopoJSON = "./maps/Maakunnat2024Topo.json"
maakunnat_2024_topo = FileAttachmentDynamic(folderMaakunnat24TopoJSON).json()
function Legend(color, {
title,
tickSize = 6,
width = 320,
height = 44 + tickSize,
marginTop = 18,
marginRight = 0,
marginBottom = 16 + tickSize,
marginLeft = 0,
ticks = width / 64,
tickFormat,
tickValues
} = {}) {
function ramp(color, n = 256) {
const canvas = document.createElement("canvas");
canvas.width = n;
canvas.height = 1;
const context = canvas.getContext("2d");
for (let i = 0; i < n; ++i) {
context.fillStyle = color(i / (n - 1));
context.fillRect(i, 0, 1, 1);
}
return canvas;
}
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.style("overflow", "visible")
.style("display", "block");
let tickAdjust = g => g.selectAll(".tick line").attr("y1", marginTop + marginBottom - height);
let x;
// Continuous
if (color.interpolate) {
const n = Math.min(color.domain().length, color.range().length);
x = color.copy().rangeRound(d3.quantize(d3.interpolate(marginLeft, width - marginRight), n));
svg.append("image")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom)
.attr("preserveAspectRatio", "none")
.attr("xlink:href", ramp(color.copy().domain(d3.quantize(d3.interpolate(0, 1), n))).toDataURL());
}
// Sequential
else if (color.interpolator) {
x = Object.assign(color.copy()
.interpolator(d3.interpolateRound(marginLeft, width - marginRight)),
{range() { return [marginLeft, width - marginRight]; }});
svg.append("image")
.attr("x", marginLeft)
.attr("y", marginTop)
.attr("width", width - marginLeft - marginRight)
.attr("height", height - marginTop - marginBottom)
.attr("preserveAspectRatio", "none")
.attr("xlink:href", ramp(color.interpolator()).toDataURL());
// scaleSequentialQuantile doesn’t implement ticks or tickFormat.
if (!x.ticks) {
if (tickValues === undefined) {
const n = Math.round(ticks + 1);
tickValues = d3.range(n).map(i => d3.quantile(color.domain(), i / (n - 1)));
}
if (typeof tickFormat !== "function") {
tickFormat = d3.format(tickFormat === undefined ? ",f" : tickFormat);
}
}
}
// Threshold
else if (color.invertExtent) {
const thresholds
= color.thresholds ? color.thresholds() // scaleQuantize
: color.quantiles ? color.quantiles() // scaleQuantile
: color.domain(); // scaleThreshold
const thresholdFormat
= tickFormat === undefined ? d => d
: typeof tickFormat === "string" ? d3.format(tickFormat)
: tickFormat;
x = d3.scaleLinear()
.domain([-1, color.range().length - 1])
.rangeRound([marginLeft, width - marginRight]);
svg.append("g")
.selectAll("rect")
.data(color.range())
.join("rect")
.attr("x", (d, i) => x(i - 1))
.attr("y", marginTop)
.attr("width", (d, i) => x(i) - x(i - 1))
.attr("height", height - marginTop - marginBottom)
.attr("fill", d => d);
tickValues = d3.range(thresholds.length);
tickFormat = i => thresholdFormat(thresholds[i], i);
}
// Ordinal
else {
x = d3.scaleBand()
.domain(color.domain())
.rangeRound([marginLeft, width - marginRight]);
svg.append("g")
.selectAll("rect")
.data(color.domain())
.join("rect")
.attr("x", x)
.attr("y", marginTop)
.attr("width", Math.max(0, x.bandwidth() - 1))
.attr("height", height - marginTop - marginBottom)
.attr("fill", color);
tickAdjust = () => {};
}
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x)
.ticks(ticks, typeof tickFormat === "string" ? tickFormat : undefined)
.tickFormat(typeof tickFormat === "function" ? tickFormat : undefined)
.tickSize(tickSize)
.tickValues(tickValues))
.call(tickAdjust)
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", marginLeft)
.attr("y", marginTop + marginBottom - height - 6)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.style("font-size", "12px")
.attr("class", "title")
.text(title));
return svg.node();
}
function legend({color, ...options}) {
return Legend(color, options);
}
// Copyright 2021, Observable Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/color-legend
function Swatches(color, {
columns = null,
format,
unknown: formatUnknown,
swatchSize = 15,
swatchWidth = swatchSize,
swatchHeight = swatchSize,
marginLeft = 0
} = {}) {
const id = `-swatches-${Math.random().toString(16).slice(2)}`;
const unknown = formatUnknown == null ? undefined : color.unknown();
const unknowns = unknown == null || unknown === d3.scaleImplicit ? [] : [unknown];
const domain = color.domain().concat(unknowns);
if (format === undefined) format = x => x === unknown ? formatUnknown : x;
function entity(character) {
return `&#${character.charCodeAt(0).toString()};`;
}
if (columns !== null) return htl.html`<div style="display: flex; align-items: center; margin-left: ${+marginLeft}px; min-height: 33px; font: 12px sans-serif;">
<style>
.${id}-item {
break-inside: avoid;
display: flex;
align-items: center;
padding-bottom: 1px;
}
.${id}-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - ${+swatchWidth}px - 0.5em);
}
.${id}-swatch {
width: ${+swatchWidth}px;
height: ${+swatchHeight}px;
margin: 0 0.5em 0 0;
}
</style>
<div style=${{width: "100%", columns}}>${domain.map(value => {
const label = `${format(value)}`;
return htl.html`<div class=${id}-item>
<div class=${id}-swatch style=${{background: color(value)}}></div>
<div class=${id}-label title=${label}>${label}</div>
</div>`;
})}
</div>
</div>`;
return htl.html`<div style="display: flex; align-items: center; min-height: 33px; margin-left: ${+marginLeft}px; font: 12px sans-serif;">
<style>
.${id} {
display: inline-flex;
align-items: center;
margin-right: 1em;
}
.${id}::before {
content: "";
width: ${+swatchWidth}px;
height: ${+swatchHeight}px;
margin-right: 0.5em;
background: var(--color);
}
</style>
<div>${domain.map(value => htl.html`<span class="${id}" style="--color: ${color(value)}">${format(value)}</span>`)}</div>`;
}
function swatches({color, ...options}) {
return Swatches(color, options);
}
chart_map_d3 = function(data, mapTopoJSON, color, colorBorder, txtTitle, chart_width, chart_height) {
const projection = d3.geoMercator()
.scale(1200)
//.center([40, 67.5])
.center([40, 67.3])
.translate([400, 220])
const path = d3.geoPath().projection(projection);
const format = d => `${d}%`;
//Määritellään tähän muuttujat CSV:stä!!!!!!
const valuemap = new Map(data.map(d => [d.id, d.muutos]));
//Määritellään maakunnat
const maakuntamesh = topojson.mesh(maakunnat_2024_topo, maakunnat_2024_topo.objects.maakunnat_2024, (a, b) => a !== b);
const zoom = d3.zoom()
.scaleExtent([1, 8])
.on("zoom", zoomed);
const svg = d3.create("svg")
.attr("width", 900)
.attr("height", 650)
.attr("viewBox", [0, 0, 900, 650])
.on("click", reset);
const g = svg.append("g")
.attr("transform", "translate(20,35)")
.append(() => Legend(color, {title: txtTitle, width: 260}));
g.append("g")
.selectAll("path")
.data(mapTopoJSON)
.join("path")
.attr("fill", d => color(valuemap.get(d.properties.kunta)))
.attr("d", path)
.append("title")
.text(d => `${d.properties.name},\n(${d.properties.nimi_2}),\n${valuemap.get(d.properties.kunta)}`);
g.append("path")
.datum(maakuntamesh)
.attr("fill", "none")
.attr("stroke", colorBorder)
.attr("stroke-linejoin", "round")
.attr("d", path);
svg.call(zoom);
function reset() {
// states.transition().style("fill", null);
svg.transition().duration(750).call(
zoom.transform,
d3.zoomIdentity,
d3.zoomTransform(svg.node()).invert([chart_width / 2, chart_height / 2])
);
}
function zoomed(event) {
const {transform} = event;
g.attr("transform", transform);
g.attr("stroke-width", 1 / transform.k);
}
return svg.node();
}
chart_chord_d3 = function(data, title_txt_chord, chart_width, chart_height) {
var sum_target;
var sum_source;
let vari;
var sum_yht;
const margin = ({top: 20, right: 20, bottom: 0, left: 50});
const width = 300;
const height = width;
const innerRadius = Math.min(width, height) * .3 - 40;
const outerRadius = innerRadius + 15;
const chord = d3.chordDirected()
.padAngle(0.5 / innerRadius)
.sortSubgroups(d3.ascending)
.sortChords(d3.ascending);
const ribbon = d3.ribbon()
.radius(innerRadius - 1)
.padAngle(0.5 / innerRadius);
const rename = name => name.substring(name.indexOf(".") + 1, name.lastIndexOf("."))
const names = Array.from(new Set(data.flatMap(d => [d.source, d.target]))).sort(d3.ascending);
function matriisi(names) {
const index = new Map(names.map((name, i) => [name, i]));
const matrix = Array.from(index, () => new Array(names.length).fill(0));
for (const {source, target, value} of data) matrix[index.get(source)][index.get(target)] += value;
return matrix;
}
const matrix = matriisi(names)
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
//TÄSTÄ ALKAA PIIRROS
const zoom = d3.zoom()
.scaleExtent([1, 8])
.on("zoom", zoomed);
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2.5, width, height]);
const chords = chord(matrix);
const group = svg.append("g")
.attr("font-size", 3)
.attr("font-family", "Century Gothic")
.selectAll("g")
.data(chords.groups)
.join("g");
group.append("path")
.attr("fill", d => {
sum_target = d3.sum(chords, c => (c.target.index === d.index) * c.target.value);
sum_source = d3.sum(chords, c => (c.source.index === d.index) * c.source.value);
sum_yht = sum_target-sum_source;
if (sum_yht > 0) {
vari = "#009651"
return vari
}
else if (sum_yht == 0) {
vari = "#28336C"
return vari
}
else {
vari = "#DE232F"
return vari
}
})
.attr("d", arc);
group.append("text")
.each(d => (d.angle = (d.startAngle + d.endAngle) / 2))
.attr("dy", "0.35em")
.attr("transform", d => `
rotate(${(d.angle * 180 / Math.PI - 90)})
translate(${outerRadius + 6})
${d.angle > Math.PI ? "rotate(180)" : ""}
`)
.attr("text-anchor", d => d.angle > Math.PI ? "end" : null)
//.text(d => names[d.index])
.text(d => (d.index >= 3) ? names[d.index] : "")
.attr("fill", d => {
sum_target = d3.sum(chords, c => (c.target.index === d.index) * c.target.value);
sum_source = d3.sum(chords, c => (c.source.index === d.index) * c.source.value);
sum_yht = sum_target-sum_source;
if (sum_yht > 0) {
vari = "#009651"
return vari
}
else if (sum_yht == 0) {
vari = "#28336C"
return vari
}
else {
vari = "#DE232F"
return vari
}
});
group.append("title")
.text(d => `${names[d.index]}
${d3.sum(chords, c => (c.source.index === d.index) * c.source.value)} lähtee →
${d3.sum(chords, c => (c.target.index === d.index) * c.source.value)} tulee ←
${d3.sum(chords, c => (c.target.index === d.index) * c.target.value)- d3.sum(chords, c => (c.source.index === d.index) * c.source.value)} netto =`);
svg.append("g")
.attr("fill-opacity", 0.75)
.selectAll("path")
.data(chords)
.join("path")
.style("mix-blend-mode", "multiply")
.attr("fill", d => {
sum_target = d3.sum(chords, c => (c.target.index === d.index) * c.target.value);
sum_source = d3.sum(chords, c => (c.source.index === d.index) * c.source.value);
sum_yht = sum_target-sum_source;
if (sum_yht > 0) {
vari = "#009651"
return vari
}
else if (sum_yht == 0) {
vari = "#28336C"
return vari
}
else {
vari = "#DE232F"
return vari
}
})
.attr("d", ribbon)
.append("title")
.text(d => `${names[d.source.index]} --> ${names[d.target.index]} ${d.source.value}`);
svg.call(zoom);
function reset() {
// states.transition().style("fill", null);
svg.transition().duration(750).call(
zoom.transform,
d3.zoomIdentity,
d3.zoomTransform(svg.node()).invert([chart_width / 2, chart_height / 2])
);
}
function zoomed(event) {
const {transform} = event;
svg.attr("transform", transform);
svg.attr("stroke-width", 1 / transform.k);
}
return svg.node();
}
Syntyvyys
Luonnollinen väestönkasvu
Luonnollinen väestönlisäys
Syntyneiden enemmyys eli luonnollinen väestönlisäys tarkoittaa elävänä syntyneiden ja kuolleiden erotusta.
Luonnollinen väestönlisäys ajanjaksolla 2024Q2* - 2024Q3* oli yhteensä 25 (2024Q2*: -1). Vuosina 2022 ja 2023 luonnollinen väestönlisäys oli 2022: -13 ja 2023: -20
Vuosi | Ennakkotieto |
---|---|
2012 | NA |
2013 | NA |
2014 | NA |
2015 | NA |
2016 | NA |
2017 | NA |
2018 | NA |
2019 | NA |
2020 | NA |
2021 | NA |
2022 | NA |
2023 | NA |
2024Q1* | 15 |
2024Q2* | -1 |
2024Q3* | 11 |
Syntyneet
Syntyvyys
Syntyvyys
Syntyvyys ajanjaksolla 2024Q2* - 2024Q3* oli yhteensä 227 (lukema 2024Q2* oli 76). Vuosina 2022 ja 2023 syntyvyys oli lukemissa (2022) 287 ja (2023) 292.
Vuosi | Ennakkotieto |
---|---|
2012 | NA |
2013 | NA |
2014 | NA |
2015 | NA |
2016 | NA |
2017 | NA |
2018 | NA |
2019 | NA |
2020 | NA |
2021 | NA |
2022 | NA |
2023 | NA |
2024Q1* | 87 |
2024Q2* | 76 |
2024Q3* | 64 |
Väestönmuutokset - Väestöpyramidi
pyramid_pop_data = FileAttachmentDynamic(pyramid_pop_csv).csv({typed: true})
translations = ({
v1990: "1990",
v2010: "2010",
v2024: valitut_kk_tahti
})
pyramid = toSVG(Plot.plot({
width: 600,
height: 600,
marginTop: 20,
marginRight: 20,
marginBottom: 30,
marginLeft: 40,
x: {
label: "← mies · väestö · nainen →",
labelAnchor: "center",
tickFormat: Math.abs,
//domain: [-300, 300]
},
y: {grid: true},
color: {
width: 300,
domain: ["v1990", "v2010", "v2024"],
range: ["#e41a1c", "#377eb8", "#4daf4a"],
legend: true,
className: 'large-font',
tickFormat: (d) => translations[d]
},
style: {
fontSize: 14
},
marks: [
Plot.lineX(pyramid_pop_data, {
x: (d) => d.population * (d.sex === "Miehet" ? -1 : 1),
y: "age",
z: (d) => [d.sex, d.category].join(","),
stroke: "category",
strokeWidth: 4,
fill: "category",
fillOpacity: 0
}),
Plot.ruleX([0]),
Plot.ruleY([0])
]
}))