function Range(range, options = {}) {
const [min, max] = range;
const {
className = "Range",
vertical = false,
label = null,
format = (x) => +x,
step = 1,
value = (min + max) / 2,
style = "",
labelStyle = "",
rangeStyle = "",
valueStyle = ""
} = options;
const rangeWrap = htl.html`<div class=${className} style="${style}"></div>`;
Object.assign(rangeWrap.style, {
display: "inline-flex",
position: "relative",
userSelect: "none"
});
const valueDisplay = htl.html`<output style="${valueStyle}">`;
Object.assign(valueDisplay.style, {
display: "inline-block"
});
const rangeInput = htl.html`<input type=range min=${min} max=${max} step=${step} value=${value} style=${rangeStyle}>`;
Object.assign(rangeInput.style, {
display: "inline-block"
});
if (vertical) {
rangeInput.setAttribute("orient", "vertical");
rangeInput.style.writingMode = "bt-lr"; /* IE */
rangeInput.style["-webkit-appearance"] = "slider-vertical"; /* WebKit */
rangeInput.style.width = "8px";
}
rangeWrap.append(rangeInput, valueDisplay);
if (label) rangeWrap.prepend(htl.html`<label style=${labelStyle}>${label}`);
rangeInput.oninput = () => {
valueDisplay.innerHTML = format(rangeInput.valueAsNumber);
rangeWrap.value = rangeWrap.valueAsNumber = +rangeInput.valueAsNumber;
rangeWrap.dispatchEvent(new CustomEvent("input"));
};
rangeInput.oninput();
return rangeWrap;
}
function extractDataForYear(data, year) {
if (!data || typeof data !== 'object') return null;
// Recursive function to search through the children
function recursiveSearch(node) {
if (!node || typeof node !== 'object') return null;
// If the node has children, process them
if (node.children) {
let filteredChildren = node.children.map(child => recursiveSearch(child)).filter(child => child !== null);
// If filtered children are found, return them within the same structure
if (filteredChildren.length > 0) {
return { ...node, children: filteredChildren };
}
}
// If the node itself contains the year we're looking for, return it
if (node.year === year) {
return node;
}
// If neither children nor the node itself match, return null
return null;
}
return recursiveSearch(data);
}
function highlighter(text, view, highlight) {
const el =
typeof text === 'string'
? html`<span style="color: #f54000; border-bottom: dotted 1px currentColor; cursor: pointer;${
highlight ? 'background: rgba(245, 64, 0, .2);' : ''
}">${text}</span>`
: text;
el.addEventListener('mouseover', () => {
view.value = true;
});
el.addEventListener('mouseout', () => {
view.value = false;
});
return el;
}
scaleValue = 4000000
function getBarValues(title) {
const value = "EUR " + d3.format(",.0~f")(dataGalaxy.filter(d => d.title == title).map(d => d.value/1000000)[0]) + " m"
const percent = d3.format(",.1~f")(dataGalaxy.filter(d => d.title == title).map(d => d.percent)[0]) + "% of total revenue"
const percentOther = d3.format(",.1~f")(
dataGalaxy.filter(d => d.title == title).map(d => d.value/1000000)[0] /
d3.sum(dataGalaxy.filter(d => d.title != 11 && d.title != 13 && d.title != 14 && d.title != 17).map(d => d.value/1000000)) *100
) + "% of other revenue"
const changeValue = dataGalaxy.filter(d => d.title == title).map(d => d.changeValue)
const change = (changeValue === undefined || isNaN(changeValue) || changeValue === null || changeValue > 10000) ?
"<span style='color:gray'>no data in previous year</span>" :
changeValue > 0 ?
"change to previous year " + "<span style='color:green'>" + "↗ " + d3.format("+,.1~f")(changeValue) + "%" + "</span>" :
"change to previous year " + "<span style='color:red'>" + "↘ " + d3.format("+,.1~f")(changeValue) + "%" + "</span>"
const temp = "<br>" + value + "<br>" + percent + "<br>" + percentOther + "<br>" + change
return temp
}
function getBarValuesAssigned() {
const value = "EUR " + d3.format(",.0~f")(
d3.sum(dataGalaxy.filter(d => d.title != 11 && d.title != 13 && d.title != 14 && d.title != 17).map(d => d.assigned/1000000))
) + " m"
const percent = d3.format(",.1~f")(
d3.sum(dataGalaxy.filter(d => d.title != 11 && d.title != 13 && d.title != 14 && d.title != 17).map(d => d.assigned/1000000)) /
d3.sum(dataGalaxy.map(d => d.value/1000000)) * 100
) + "% of total revenue"
const percentOther = d3.format(",.1~f")(
d3.sum(dataGalaxy.filter(d => d.title != 11 && d.title != 13 && d.title != 14 && d.title != 17).map(d => d.assigned/1000000)) /
d3.sum(dataGalaxy.filter(d => d.title != 11 && d.title != 13 && d.title != 14 && d.title != 17).map(d => d.value/1000000)) * 100
) + "% of other revenue"
const temp = "<br>" + value + "<br>" + percent + "<br>" + percentOther
return temp
}
function getBarValuesNonAssigned() {
const value = "EUR " + d3.format(",.0~f")(
d3.sum(dataGalaxy.filter(d => d.title != 11 && d.title != 13 && d.title != 14 && d.title != 17).map(d => d.nonAssigned/1000000))
) + " m"
const percent = d3.format(",.1~f")(
d3.sum(dataGalaxy.filter(d => d.title != 11 && d.title != 13 && d.title != 14 && d.title != 17).map(d => d.nonAssigned/1000000)) /
d3.sum(dataGalaxy.map(d => d.value/1000000)) * 100
) + "% of total revenue"
const percentOther = d3.format(",.1~f")(
d3.sum(dataGalaxy.filter(d => d.title != 11 && d.title != 13 && d.title != 14 && d.title != 17).map(d => d.nonAssigned/1000000)) /
d3.sum(dataGalaxy.filter(d => d.title != 11 && d.title != 13 && d.title != 14 && d.title != 17).map(d => d.value/1000000)) * 100
) + "% of other revenue"
const temp = "<br>" + value + "<br>" + percent + "<br>" + percentOther
return temp
}
function getBarValuesOwn(title) {
const value = "EUR " + d3.format(",.0~f")(
d3.sum(dataGalaxy.filter(d => d.title == title).map(d => d.value/1000000))
) + " m"
const percent = d3.format(",.1~f")(
dataGalaxy.filter(d => d.title == title).map(d => d.value/1000000) /
d3.sum(dataGalaxy.map(d => d.value/1000000)) * 100
) + "% of total revenue"
const changeValue = dataGalaxy.filter(d => d.title == title).map(d => d.changeValue)
const change = (changeValue === undefined || isNaN(changeValue) || changeValue === null || changeValue > 10000) ?
"<span style='color:gray'>no data in previous year</span>" :
changeValue > 0 ?
"change to previous year " + "<span style='color:green'>" + "↗ " + d3.format("+,.1~f")(changeValue) + "%" + "</span>" :
"change to previous year " + "<span style='color:red'>" + "↘ " + d3.format("+,.1~f")(changeValue) + "%" + "</span>"
const temp = "<br>" + value + "<br>" + percent + "<br>" + change
return temp
}
function getBarValuesOwnSum() {
const value = "EUR " + d3.format(",.0~f")(
d3.sum(dataGalaxy.filter(d => d.title == 11 || d.title == 13 || d.title == 14 || d.title == 17).map(d => d.value/1000000))
) + " m"
const percent = d3.format(",.1~f")(
d3.sum(dataGalaxy.filter(d => d.title == 11 || d.title == 13 || d.title == 14 || d.title == 17).map(d => d.value/1000000)) /
d3.sum(dataGalaxy.map(d => d.value/1000000)) * 100
) + "%"
const temp = "<br>" + value + "<br>" + percent
return temp
}
function getRadius(title) {
const temp = Math.sqrt(dataGalaxy.filter(d => d.title == title).map(d => d.value/scaleValue)[0])
return temp
}
function getRadiusAssigned(title) {
const temp = Math.sqrt(d3.sum(dataGalaxy.filter(d => d.title != 11 && d.title != 13 && d.title != 14 && d.title != 17).map(d => d[title]/scaleValue)))
return temp
}
function progressBar(percentage) {
const totalBars = 20; // Total number of bars in the progress bar
const filledBars = Math.round((percentage / 100) * totalBars);
const emptyBars = totalBars - filledBars;
const filled = "▓".repeat(filledBars); // Lighter shade for the filled part
const empty = "░".repeat(emptyBars); // Light shade for the empty part
// return `[${filled}${empty}] ${d3.format(",.1~f")(percentage)}%`;
return `${filled}${empty}`;
}
colorsWIFO = [
"#72bb6f",
"#559bd5",
"#f3d039",
"#c3423f",
"#12a7e7",
"#96b428",
"#559e8f",
"#f3d039",
"#559e8f",
"#2d5b5e"
]
colors = [
"#0472c3", // Existing blue
"#ed8a04", // Existing orange
"#08818f", // Existing teal
"#f54000", // Existing red
"#82b74b", // Complementary green
"#ffdd44", // Complementary yellow
"#7a42f4", // Complementary purple
"#e91e63", // Complementary pink
]
colorsShift = [
"#ed8a04", // Existing orange
"#08818f", // Existing teal
"#f54000", // Existing red
"#82b74b", // Complementary green
"#ffdd44", // Complementary yellow
"#7a42f4", // Complementary purple
"#e91e63", // Complementary pink
]
colorsBlue = [
"#82b74b",
"#024e82", // Darker Shade 2
"#0472c3", // Original Color
"#3995d3", // Lighter Shade 1
"#6ea9e3", // Lighter Shade 2
"#a3c7f3", // Lighter Shade 3
"#0459c3", // Analogous Color 1
"#0460c3", // Analogous Color 2
"#0483c3" // Analogous Color 3
]
config = ({
view: {stroke: null},
padding: {
left: 5,
top: 5,
right: 5,
bottom: 20
},
font: "Noto Sans",
fontSize: 14,
title: {
offset: 0,
fontSize: 16,
subtitleFontSize: 14,
},
facet: {
fontSize: 14,
labelFontSize: 14,
titleFontSize: 14,
titleFontWeight: "normal",
},
axis: {
labelFontSize: 14,
titleFontSize: 14,
titleFontWeight: "normal",
},
legend: {
labelFontSize: 14,
titleFontSize: 14,
titleFontWeight: "normal",
},
mark: {
fontSize: 14
},
locale: {
number: {
currency: ["EUR ", " m"],
decimal: ".",
thousands: ",",
grouping: [3]
},
time: {
dateTime: "%A %e %B %Y, %X",
date: "%d/%m/%Y",
time: "%H:%M:%S",
periods: ["AM", "PM"],
days: [
"Montag",
"Dienstag",
"Mittwoch",
"Donnerstag",
"Freitag",
"Samstag",
"Sonntag"
],
shortDays: [
"Mo",
"Di",
"Mi",
"Do",
"Fr",
"Sa",
"So"
],
months: [
"Jänner",
"Februar",
"März",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Dezember"
],
shortMonths: [
"Jan",
"Feb",
"Mar",
"Apr",
"Mai",
"Jun",
"Jul",
"Aug",
"Sep",
"Okt",
"Nov",
"Dez"
]
}
},
})
import { View } from '@mbostock/synchronized-views'
dataGalaxyRaw = FileAttachment("data/dataGalaxy.csv").csv({ typed: true })
dataGalaxy = dataGalaxyRaw.filter(d => d.year == range)
dataRaw = FileAttachment("data/nestedData.json").json()
dataRawSmall = FileAttachment("data/nestedDataSmall.json").json()
data = extractDataForYear(radios == "Total Revenue" ? dataRaw : dataRawSmall, range.toString())
radios === "Total Revenue" ?
mermaid`
%%{init: {'theme': 'base', 'themeVariables': {'fontFamily': 'Noto Sans', 'fontSize': '30px', 'primaryBorderColor': 'black','primaryColor': 'white'}}}%%
graph TB
subgraph container[" "]
member[/"Member States"\] o-- % of the estimated <br>value added tax --> T13["VAT-based"]
member o-- uniform % of GNI --> T14["GNI-based"]
member o-- 25% of customs duties --> T11["Traditional Resources"]
member o-- "0.80 per kilogram or lump sum" --> T17["Plastic based"]
subgraph own["<b>OWN RESOURCES"]
subgraph T11["Traditional Resources"]
T11bar(("⠀"))
T11box["${getBarValuesOwn(11)}</b>"]
end
subgraph T13["VAT-based"]
T13bar(("⠀"))
T13box["${getBarValuesOwn(13)}"]
end
subgraph T14["GNI-based"]
T14bar(("⠀"))
T14box["${getBarValuesOwn(14)}"]
end
subgraph T17["Plastic based"]
T17bar(("⠀"))
T17box["${getBarValuesOwn(17)}"]
end
end
T11 --> B
T13 --> B
T14 --> B
T17 --> B
style own fill:transparent
style T11bar r:${getRadius(11)}px,fill:${colors[0]}
style T13bar r:${getRadius(13)}px,fill:${colors[0]}
style T14bar r:${getRadius(14)}px,fill:${colors[0]}
style T17bar r:${getRadius(17)}px,fill:${colors[0]}
subgraph other["<b>OTHER REVENUE</b> "]
subgraph T2["Balances and Adjustments"]
T2bar((" "))
T2box["${getBarValues(2)}"]
end
subgraph T3["Administrative Revenue"]
T3bar(("⠀"))
T3box["${getBarValues(3)}"]
end
subgraph T4["Financial Revenue, Default Interest, Fines"]
T4bar(("⠀"))
T4box["${getBarValues(4)}"]
end
subgraph T5["Budgetary Guarantees, Borrowing-and-Lending Operations"]
T5bar(("⠀"))
T5box["${getBarValues(5)}"]
end
subgraph T6["Revenue, Contributions and Refunds Related to Union Policies"]
T6bar(("⠀"))
T6box["${getBarValues(6)}"]
end
end
style T2bar r:${getRadius(2)}px,fill:${colors[1]}
style T3bar r:${getRadius(3)}px,fill:${colors[2]}
style T4bar r:${getRadius(4)}px,fill:${colors[3]}
style T5bar r:${getRadius(5)}px,fill:${colors[4]}
style T6bar r:${getRadius(6)}px,fill:${colors[5]}
companies[/"Companies"\]
last(("Previous<br>Financial Year"))
member[/"Member States"\]
staff[/"EU Staff"\]
property[/"EU Property"\]
third[/"Third Countries"\]
project(("EU Projects<br>and Funds"))
fin[/"Financial Institutions"\]
companies -- fines --> T4
member -- penalties --> T4
companies -- purchase of emission allowances --> T6
member -- contributions --> T6
project -- adjustments --> T6
third -- contributions --> T6
last -- surplus and adjustments --> T2
staff -- taxes and pension contributions --> T3
property -- rent and sale --> T3
fin -- lend --> T5
subgraph A
subgraph A1["Assigned Revenue"]
spacer((" "))
A1bar(("⠀"))
A1box["${getBarValuesAssigned()}"]
end
subgraph A2["Non-Assigned Revenue"]
A2bar(("⠀"))
A2box["${getBarValuesNonAssigned()}"]
end
end
style spacer r:1px,display:none;
T2 -- "${Math.round(dataGalaxy.filter(d => d.title == 2).map(d => d.percentNonAssigned)[0])}%" --> A2
T3 -- "${Math.round(dataGalaxy.filter(d => d.title == 3).map(d => d.percentNonAssigned)[0])}%" --> A2
T4 -- "${Math.round(dataGalaxy.filter(d => d.title == 4).map(d => d.percentNonAssigned)[0])}%" --> A2
T6 -- "${Math.round(dataGalaxy.filter(d => d.title == 6).map(d => d.percentNonAssigned)[0])}%" --> A2
T3 -- "${Math.round(dataGalaxy.filter(d => d.title == 3).map(d => d.percentAssigned)[0])}%" --> A1
T5 -- "${Math.round(dataGalaxy.filter(d => d.title == 5).map(d => d.percentAssigned)[0])}%" --> A1
T6 -- "${Math.round(dataGalaxy.filter(d => d.title == 6).map(d => d.percentAssigned)[0])}%" --> A1
B(("
<span style='color:#FFDD00'>fa:fa-star</span> EU Revenue <span style='color:#FFDD00'>fa:fa-star</span>⠀
${
"<br>" + "EUR " + d3.format(",.0~f")(d3.sum(dataGalaxy.map(d => d.value/1000000))) + " m" + "<br>" +
d3.format(",.1~f")(d3.sum(dataGalaxy.map(d => d.percent))) + "%"
}
"))
A2 --> B
A1 --> B
A1 -.-> SP(["Special Instruments"])
A1 -.-> P1
A1 -.-> P2
A1 -.-> P3
A1 -.-> P4
A1 -.-> P5
A1 -.-> P6
A1 -.-> P7
A1 -.-> OP(["Off-Budget Funds"])
classDef T stroke:transparent,fill:transparent;
class T2,T3,T4,T5,T6,T11,T13,T14,T17,A1,A2 T;
classDef bar stroke:black,stroke-width:2px,fill-opacity:0.7;
class T2bar,T3bar,T4bar,T5bar,T6bar,T11bar,T13bar,T14bar,T17bar,A1bar,A2bar bar;
classDef box fill:transparent,stroke:transparent;
class T2box,T3box,T4box,T5box,T6box,T11box,T13box,T14box,T17box,A1box,A2box box;
style A1bar r:${getRadiusAssigned("assigned")}px,fill:${colors[6]},stroke:black
style A2bar r:${getRadiusAssigned("nonAssigned")}px,fill:${colors[7]},stroke:black
style B r:${Math.sqrt(d3.sum(dataGalaxy.map(d => d.value/scaleValue)))}px,fill:${colors[0]},fill-opacity:0.7,stroke:black
subgraph policy[" "]
P1(["Single Market, Innovation and Digital"])
P2(["Cohesion, Resilience and Values"])
P3(["Natural Resources and Environment"])
P4(["Migration and Border Management"])
P5(["Security and Defense"])
P6(["Neighbourhood and the World"])
P7(["European Public Administration"])
end
style policy fill:transparent,stroke:black,stroke-width:1px
style A fill:transparent,stroke:transparent,color:transparent
style container fill:transparent,stroke:transparent
style other fill:#ededed,fill-opacity:0.7,stroke:black,stroke-width:1px
style own fill:#ededed,fill-opacity:0.7,stroke:black,stroke-width:1px
end
${highlight1 ? "style B stroke:#333,stroke-width:3px,fill-opacity:0.4" : ""}
`
:
mermaid`
%%{init: {'theme': 'base', 'themeVariables': {'fontFamily': 'Noto Sans', 'fontSize': '30px', 'primaryBorderColor': 'black','primaryColor': 'white'}, 'flowchart':{'nodeSpacing': 6, 'rankSpacing':'6'}}}%%
graph TB
subgraph container[" "]
subgraph other["<b>OTHER REVENUE</b> "]
subgraph T2["Balances and Adjustments"]
T2bar((" "))
T2box["${getBarValues(2)}"]
end
subgraph T3["Administrative Revenue"]
T3bar(("⠀"))
T3box["${getBarValues(3)}"]
end
subgraph T4["Financial Revenue, Default Interest, Fines"]
T4bar(("⠀"))
T4box["${getBarValues(4)}"]
end
subgraph T5["Budgetary Guarantees, Borrowing-and-Lending Operations"]
T5bar(("⠀"))
T5box["${getBarValues(5)}"]
end
subgraph T6["Revenue, Contributions and Refunds Related to Union Policies"]
T6bar(("⠀"))
T6box["${getBarValues(6)}"]
end
end
style T2bar r:${getRadius(2)}px,fill:${colors[1]}
style T3bar r:${getRadius(3)}px,fill:${colors[2]}
style T4bar r:${getRadius(4)}px,fill:${colors[3]}
style T5bar r:${getRadius(5)}px,fill:${colors[4]}
style T6bar r:${getRadius(6)}px,fill:${colors[5]}
companies[/"Companies"\]
last(("Previous<br>Financial Year"))
member[/"Member States"\]
staff[/"EU Staff"\]
property[/"EU Property"\]
third[/"Third Countries"\]
project(("EU Projects<br>and Funds"))
fin[/"Financial Institutions"\]
member -- "<b>OWN RESOURCES ${getBarValuesOwnSum()}</b>" --> boxB
companies -- fines --> T4
member -- penalties --> T4
companies -- purchase of emission allowances --> T6
member -- contributions --> T6
project -- adjustments --> T6
third -- contributions --> T6
last -- surplus and adjustments --> T2
staff -- taxes and pension contributions --> T3
property -- rent and sale --> T3
fin -- lend --> T5
subgraph A
subgraph A1["Assigned Revenue"]
spacer((" "))
A1bar(("⠀"))
A1box["${getBarValuesAssigned()}"]
end
subgraph A2["Non-Assigned Revenue"]
A2bar(("⠀"))
A2box["${getBarValuesNonAssigned()}"]
end
end
style spacer r:1px,display:none;
T2 -- "${Math.round(dataGalaxy.filter(d => d.title == 2).map(d => d.percentNonAssigned)[0])}%" --> A2
T3 -- "${Math.round(dataGalaxy.filter(d => d.title == 3).map(d => d.percentNonAssigned)[0])}%" --> A2
T4 -- "${Math.round(dataGalaxy.filter(d => d.title == 4).map(d => d.percentNonAssigned)[0])}%" --> A2
T6 -- "${Math.round(dataGalaxy.filter(d => d.title == 6).map(d => d.percentNonAssigned)[0])}%" --> A2
T3 -- "${Math.round(dataGalaxy.filter(d => d.title == 3).map(d => d.percentAssigned)[0])}%" --> A1
T5 -- "${Math.round(dataGalaxy.filter(d => d.title == 5).map(d => d.percentAssigned)[0])}%" --> A1
T6 -- "${Math.round(dataGalaxy.filter(d => d.title == 6).map(d => d.percentAssigned)[0])}%" --> A1
subgraph boxB[" "]
B(("
<span style='color:#FFDD00'>fa:fa-star</span> EU Revenue <span style='color:#FFDD00'>fa:fa-star</span>⠀
${
"<br>" + "EUR " + d3.format(",.0~f")(d3.sum(dataGalaxy.map(d => d.value/1000000))) + " m" + "<br>" +
d3.format(",.1~f")(d3.sum(dataGalaxy.map(d => d.percent))) + "%"
}
"))
end
A2 --> boxB
A1 --> boxB
A1 -.-> SP(["Special Instruments"])
A1 -.-> P1
A1 -.-> P2
A1 -.-> P3
A1 -.-> P4
A1 -.-> P5
A1 -.-> P6
A1 -.-> P7
A1 -.-> OP(["Off-Budget Funds"])
classDef T stroke:transparent,fill:transparent;
class boxB,T2,T3,T4,T5,T6,T11,T13,T14,T17,A1,A2 T;
classDef bar stroke:black,stroke-width:2px,fill-opacity:0.7;
class T2bar,T3bar,T4bar,T5bar,T6bar,T11bar,T13bar,T14bar,T17bar,A1bar,A2bar bar;
classDef box fill:transparent,stroke:transparent;
class T2box,T3box,T4box,T5box,T6box,T11box,T13box,T14box,T17box,A1box,A2box box;
style A1bar r:${getRadiusAssigned("assigned")}px,fill:${colors[6]},stroke:black
style A2bar r:${getRadiusAssigned("nonAssigned")}px,fill:${colors[7]},stroke:black
style B r:${Math.sqrt(d3.sum(dataGalaxy.map(d => d.value/scaleValue)))}px,fill:${colors[0]},fill-opacity:0.7,stroke:black
subgraph policy[" "]
P1(["Single Market, Innovation and Digital"])
P2(["Cohesion, Resilience and Values"])
P3(["Natural Resources and Environment"])
P4(["Migration and Border Management"])
P5(["Security and Defense"])
P6(["Neighbourhood and the World"])
P7(["European Public Administration"])
end
style policy fill:transparent,stroke:black,stroke-width:1px
style A fill:transparent,stroke:transparent,color:transparent,margin-left:100px
style container fill:transparent,stroke:transparent
style other fill:#ededed,fill-opacity:0.7,stroke:black,stroke-width:1px
end
${highlight1 ? "style B stroke:#333,stroke-width:3px,fill-opacity:0.4" : ""}
`
treemap = {
const width = catchWidth >= 2000 ?
catchWidth * 0.60 :
catchWidth >= 1600 ?
catchWidth * 0.72 :
catchWidth * 0.88;
const height = width * 0.33 ;
const color = d3.scaleOrdinal(
radios == "Total Revenue" ? colors : colorsShift // ["#72bb6f", "#559bd5", "#f3d039", "#c3423f", "#12a7e7"]
);
function tile(node, x0, y0, x1, y1) {
d3.treemapSquarify(node, 0, 0, width, height);
for (const child of node.children) {
child.x0 = x0 + child.x0 / width * (x1 - x0);
child.x1 = x0 + child.x1 / width * (x1 - x0);
child.y0 = y0 + child.y0 / height * (y1 - y0);
child.y1 = y0 + child.y1 / height * (y1 - y0);
}
}
const hierarchy = d3.hierarchy(data)
.sum(d => d.value)
// .sort((a, b) => b.value - a.value);
hierarchy.eachAfter(function(node) {
var sumPercent = +node.data.percent || 0,
sumPercentAssigned = +node.data.percentAssigned || 0,
sumValueAssigned = +node.data.valueAssigned || 0,
children = node.children,
i = children && children.length;
while (--i >= 0)
sumPercent += children[i].percent,
sumPercentAssigned += children[i].percentAssigned,
sumValueAssigned += children[i].valueAssigned;
node.percent = sumPercent;
node.percentAssigned = sumPercentAssigned;
node.valueAssigned = sumValueAssigned;
});
const root = d3.treemap().tile(tile)(hierarchy);
const x = d3.scaleLinear().rangeRound([0, width]);
const y = d3.scaleLinear().rangeRound([0, height]);
const format = d3.format(",d");
const name = d => d.ancestors().reverse().map(d => d.data.name).join(" → ");
const svg = d3.create("svg")
.attr("viewBox", [0.5, -56.5, width, height + 56])
.attr("width", width)
.attr("class", "treemap")
.attr("height", height + 56)
.attr("style", "max-width: 100%; height: auto;")
.style("font", "Noto Sans sans-serif"); // font size adjustment does not work here see below
let group = svg.append("g")
.call(render, root);
function render(group, root) {
const node = group
.selectAll("g")
.data(root.children.concat(root))
.join("g");
node.filter(d => d === root ? d.parent : d.children)
.attr("cursor", "pointer")
.on("click", (event, d) => d === root ? zoomout(root) : zoomin(d));
// node.append("title")
// .text(d => `${name(d)}\n${format(d.value)}`);
node.append("rect")
.attr("id", d => (d.leafUid = DOM.uid("leaf")).id)
.attr("fill", d => d === root ? "#ededed" : d.children ? "#ccc" : "#ddd")
.attr("stroke", "black")
.attr("stroke-width", "1px")
.attr("fill-opacity", 0.7)
.attr("fill", d => {
if (d === root) return "#ededed";
while (d.depth > 1) d = d.parent;
return color(d.data.name);
});
node.append("clipPath")
.attr("id", d => (d.clipUid = DOM.uid("clip")).id)
.append("use")
.attr("xlink:href", d => d.leafUid.href);
node.append("text")
.attr("clip-path", d => d.clipUid)
.attr("font-weight", d => d === root ? "bold" : null)
.attr("font-size", "0.9em")
.selectAll("tspan")
.data(
d => d === root ?
d.data.name // name(d)
.split(/(?=\/)/g)
.concat(
"EUR " + format(d.value / 1000000) + " m" + " / " + d3.format(",.1~f")(d.percent) + "%"
)
:
d.data.name
.split(/(?<=\d+\s)/g)
.concat("EUR " + format(d.value / 1000000) + " m")
.concat(
radios == "Total Revenue" ?
d3.format(",.1~f")(d.percent) + "% of total revenue" :
d3.format(",.1~f")(d.percent) + "% of other revenue"
)
.concat("Assigned Revenue: ") // → a
.concat("EUR " + format(d.valueAssigned / 1000000) + " m")
.concat(d3.format(",.1~f")(d.valueAssigned / d.value * 100) + "% of " + d.data.name.split(' ').slice(0, 2).join(' '))
// .concat(progressBar(d.valueAssigned / d.value * 100))
)
.join("tspan")
.attr("x", 5)
.attr("y", (d, i, nodes) => `${(i >= nodes.length - 3) * 0.55 + 1.33 + (i > 3 ? i * 1.15 : i) * 1.2}em`)
// .attr("fill-opacity", (d, i, nodes) => i >= nodes.length - 4 ? 0.7 : null) // 0.7
.attr("font-style", (d, i, nodes) => i >= 4 ? "italic" : null) // 0.7
.attr("font-weight", (d, i, nodes) => i >= nodes.length - 3 ? "normal" : null)
.text(d => d);
group.call(position, root);
}
function position(group, root) {
group.selectAll("g")
.attr("transform", d => d === root ? `translate(0,-56)` : `translate(${x(d.x0)},${y(d.y0)})`)
.select("rect")
.attr("width", d => d === root ? width : x(d.x1) - x(d.x0))
.attr("height", d => d === root ? 56 : y(d.y1) - y(d.y0));
}
function zoomin(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.append("g").call(render, d);
x.domain([d.x0, d.x1]);
y.domain([d.y0, d.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.call(position, d.parent))
.call(t => group1.transition(t)
.attrTween("opacity", () => d3.interpolate(0, 1))
.call(position, d));
}
function zoomout(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.insert("g", "*").call(render, d.parent);
x.domain([d.parent.x0, d.parent.x1]);
y.domain([d.parent.y0, d.parent.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.attrTween("opacity", () => d3.interpolate(1, 0))
.call(position, d))
.call(t => group1.transition(t)
.call(position, d.parent));
}
return svg.node();
}