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();
}
Liikenne
Liikennemäärät
Kartta LAM-antureista
Fintraffic kerää tietoa tieliikenteestä liikenteen automaattisten mittausasemien (LAM) avulla. Klikkaamalla kartan nuppineulaa saat auki lähimpien LAM-anturien tiedot alueellasi. Mittausajankohtia verrataan neljän vuoden ajalta samaan päivämäärään (2024-10-21) Lähde: (Fintraffic.fi 2024)
Liikennemäärä yhteensä
Liikennettä oli ajanjaksolla 2024-10-21 suunnassa vt24 Kuhmoinen -> Jämsä yhteensä 812. Vastaavasti suunnassa vt24 Kuhmoinen -> Lahti yhteensä 1762.
Henkilö- tai pakettiauto
Henkilö- tai pakettiautoliikennettä oli suunnassa vt24 Kuhmoinen -> Jämsä yhteensä 755. Suunnan vt24 Kuhmoinen -> Lahti liikennemäärä oli yhteensä 1689.
Kuorma-autot
Kuorma-autojen liikennemäärä oli suunnassa vt24 Kuhmoinen -> Jämsä yhteensä 40. Suunnan vt24 Kuhmoinen -> Lahti kuorma-automäärä oli yhteensä 47.
Linja-autot
Linja-autoja liikkui suunnassa vt24 Kuhmoinen -> Jämsä yhteensä 3. Vastaavasti suunnassa vt24 Kuhmoinen -> Lahti linja-autoja oli yhteensä 3.
Henkilöauto ja asuntovaunu
Asuntovaunujen kanssa liikkuvia henkilöautoja oli suunnassa vt24 Kuhmoinen -> Jämsä yhteensä 14 ja suunnassa vt24 Kuhmoinen -> Lahti yhteensä 23.
Moottoripyörät ja mopot
HCT (High Capacity Truck)
Lyhenne HCT tulee sanoista High Capacity Transport. Lyhenne on kansainvälisesti vakiintunut termi sallittua pidemmille tai raskaammille yhdistelmille tieliikenteessä, joita ei kuitenkaan pidetä erikoiskuljetuksina.
Lähteet
Fintraffic.fi. 2024. Liikenteen automaattiset mittaustiedot. Fintraffic.fi. https://www.digitraffic.fi/tieliikenne/lam/.