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();
}
Muuttoliike
Kuntien välinen muuttoliike
Ympyrä kuvaa kuntien välistä muuttoliikettä. Vihreällä olevat kunnat ovat muuttovoittajien ja punaisella olevat häviäjiä. Siniset viivat ympyrän keskellä kuvaavat muuttovirtojen suuruutta. Voit zoomata ympyrää hiiren rullalla. Laittamalla hiiren osoitin kuvan päälle näet muuttojen suuruudet ja liikesuunnat.