//data = FileAttachment("Ovw.csv").csv({ typed: true })
data = FileAttachment("Ovw2.csv").csv({ typed: true })Overview of Macroeconomic Forecasts for Austria
Forecasts including revisions [Source: OeNB]. Revisions [former forecast → revised forecast]:
function createYAxisGrid(yMin, yMax) {
const tickCount = 5; // Anzahl der Gridlinien (kannst du anpassen)
const ticks = d3.ticks(yMin, yMax, tickCount);
return Plot.ruleY(ticks, {
stroke: "#999", // dunkleres Grau
strokeOpacity: 0.5,
strokeWidth: 1
});
}function createForecastPlot(itemName) {
// 1. Daten filtern
const filteredData = data.filter(d => d.item === itemName);
// 2. Revisionen & Offsets
const revsList = Array.from(new Set(filteredData.map(d => d.revs)));
const revsOffsetMap = Object.fromEntries(
revsList.map((r, i) => [r, (i - revsList.length / 2) * 0.2])
);
// 3. Forecast-Links berechnen
const forecastLinks = d3.groups(filteredData, d => d.item, d => d.source, d => d.period)
.flatMap(([item, sources]) =>
sources.flatMap(([source, periods]) =>
periods.flatMap(([period, records]) => {
const revGroups = d3.groups(records, d => d.revs).sort(
(a, b) => d3.min(a[1], d => d.id) - d3.min(b[1], d => d.id)
);
const links = [];
for (let i = 0; i < revGroups.length - 1; i++) {
const [revsFrom, fromGroup] = revGroups[i];
const [revsTo, toGroup] = revGroups[i + 1];
const from = d3.max(fromGroup, d => d);
const to = d3.max(toGroup, d => d);
const periodStr = String(period);
links.push({
item,
source,
period: periodStr,
revsFrom,
revsTo,
y1: +from.value,
y2: +to.value,
toId: to.id,
revLabel: `${revsFrom} → ${revsTo}`
});
}
return links;
})
)
);
// 4. Perioden pro Source
const sourcePeriodsSorted = Object.fromEntries(
d3.groups(forecastLinks, d => d.source).map(([source, values]) => [
source,
Array.from(new Set(values.map(d => d.period))).sort()
])
);
const sourcePeriodIndexMap = Object.fromEntries(
Object.entries(sourcePeriodsSorted).map(([source, periods]) => [
source,
Object.fromEntries(periods.map((p, i) => [p, i]))
])
);
// 5. xNumeric berechnen
const sortedLinks = d3.groups(forecastLinks, d => d.source, d => d.period)
.flatMap(([source, periods]) =>
periods.flatMap(([period, links]) => {
const periodIndex = sourcePeriodIndexMap[source]?.[period] ?? 0;
const sorted = links.sort((a, b) => a.toId - b.toId);
const spacing = 0.35;
const n = sorted.length;
sorted.forEach((link, i) => {
const localOffset = (i - (n - 1) / 2) * spacing;
link.xNumeric = periodIndex + localOffset;
});
return sorted;
})
);
// 6. Tick-Marks für X-Achse
const tickMarks = Object.entries(sourcePeriodIndexMap).flatMap(([source, periodMap]) =>
Object.entries(periodMap).map(([period, index]) => ({
source,
x: index,
label: period
}))
);
// 7. Y-Achsenbereich bestimmen
const allY = sortedLinks.flatMap(d => [d.y1, d.y2]);
const yMinData = d3.min(allY);
const yMaxData = d3.max(allY);
const yPadding = (yMaxData - yMinData) * 0.10 || 1;
const yMin = yMinData < 0 ? yMinData - yPadding : 0;
const yMax = yMaxData > 0 ? yMaxData + yPadding : 0;
// 8. x-Domain mit Puffer
const xVals = sortedLinks.map(d => d.xNumeric);
const xMin = d3.min(xVals);
const xMax = d3.max(xVals);
const xPadding = (xMax - xMin) * 0.10 || 0.2;
const xDomain = [xMin - xPadding, xMax + xPadding];
// 9. y-Position der X-Achsen-Beschriftung
const xAxisY = yMin - 0.05 * (yMax - yMin);
// 10. Manuelles Grid
function createYAxisGrid(yMin, yMax) {
const tickCount = 5;
const ticks = d3.ticks(yMin, yMax, tickCount);
return Plot.ruleY(ticks, {
stroke: "#666",
strokeOpacity: 0.6,
strokeWidth: 1
});
}
// 11. 0-Linie (optional)
const zeroLineMark = (yMin < 0 && yMax > 0)
? [Plot.ruleY([0], {
stroke: "#aaa",
strokeWidth: 1.5,
strokeDasharray: "4,2"
})]
: [];
// 12. Plot erzeugen
return Plot.plot({
width: 700,
height: 250,
marginLeft: 20,
marginRight: 20,
marginBottom: 25,
style: { backgroundColor: "black" },
fx: {
label: null
},
x: {
domain: xDomain,
type: "linear",
ticks: []
},
y: {
domain: [yMin, yMax]
},
color: {
label: "Revision Path",
legend: true,
scheme: "category10"
},
marks: [
Plot.frame({ fill: "#222", stroke: "none" }),
createYAxisGrid(yMin, yMax),
Plot.ruleY([xAxisY], { stroke: "white", strokeWidth: 1 }),
...zeroLineMark,
Plot.link(sortedLinks, {
fx: "source",
x1: "xNumeric",
x2: "xNumeric",
y1: "y1",
y2: "y2",
stroke: "revLabel",
strokeWidth: 2,
markerStart: "dot",
markerEnd: "arrow",
tip: true,
title: d => `${d.source}
${d.revLabel}
former value: ${d.y1.toFixed(2)}
revised value: ${d.y2.toFixed(2)}`
}),
Plot.text(tickMarks, {
fx: "source",
x: "x",
y: xAxisY,
dy: 8,
text: "label",
fontSize: 10,
textAnchor: "middle"
})
]
});
}