Public
Edited
Oct 19, 2024
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, config.width, config.height])
.attr("width", config.width)
.attr("height", config.height)
.attr("style", "max-width: 100%; height: auto; font: 13px sans-serif;");
const defs = svg.append("defs");
const filter = defs
.append("filter")
.attr("id", "drop-shadow")
.attr("height", "150%");

filter
.append("feGaussianBlur")
.attr("in", "SourceAlpha")
.attr("stdDeviation", 5); // Increase blur strength

filter
.append("feOffset")
.attr("dx", 8) // Increase shadow horizontal offset
.attr("dy", 8) // Increase shadow vertical offset
.attr("result", "offsetblur");

filter
.append("feComponentTransfer")
.append("feFuncA")
.attr("type", "linear")
.attr("slope", 1); // Increase shadow opacity (1 makes it fully opaque)

filter
.append("feMerge")
.selectAll("feMergeNode")
.data(["offsetblur", "SourceGraphic"])
.enter()
.append("feMergeNode")
.attr("in", (d) => d);

// Add a cell for each leaf of the hierarchy.
const leaf = svg
.selectAll("g")
.data(leaves)
.join("g")
.attr("transform", (d) => `translate(${d.x0},${d.y0})`);

// A unique identifier for clip paths (to avoid conflicts).
const uid = `O-${Math.random().toString(16).slice(2)}`;

leaf
.append("clipPath")
.attr("id", (d) => (d.clipUid = `${uid}-${d.data.id}`))
.append("rect")
.attr("width", (d) => d.x1 - d.x0)
.attr("height", (d) => d.y1 - d.y0);

// Append a tooltip.
const format = d3.format(",d");
leaf.append("title").text((d) => d.data.kapittel);

// Append a color rectangle.
leaf
.append("rect")
.attr("fill", (d) => colorScale(d.data.data.diff))
.attr("fill-opacity", 0.6)
.attr("opacity", 0.4)
.attr("width", (d) => d.x1 - d.x0)
.attr("height", (d) => d.y1 - d.y0)
.attr("filter", (d) =>
d.data.data.kapittel === "Sikt" && highlightSikt
? "url(#drop-shadow)"
: null
); // Apply the filter conditionally

// leaf
// .append("text")
// //.attr("clip-path", (d) => d.clipUid)
// .attr(
// "font-size",
// (d) => tsizes[d.size] - calcSizeMinus(d.data.data.kapittel.length)
// )
// .attr("x", (d) => d.midx)
// .attr("y", (d) => d.midy)
// .attr("text-anchor", "middle")
// .attr("dominant-baseline", "middle")
// .attr("opacity", 0.8)
// .attr("transform", (d) =>
// d.rotate ? `rotate(90, ${d.midx}, ${d.midy})` : ""
// )
// .text((d) => `${d.data.data.kapittel} ${d.data.data.post}`);

const ytpos = (d, i) => {
return ttsizes[d.size] * (i + 1.4);
};
leaf
.append("text")
.attr("clip-path", (d) => `url(#${d.clipUid})`)
.attr("x", (d) => 4)
.attr("y", (d) => ytpos(d, 0))
.attr("font-size", (d) => ttsizes[d.size])
.attr("opacity", (d) => (d.h > 20 ? 1 : 0))
.attr("font-weight", "bold")
// .attr("text-anchor", "end")
.text((d) => d.data.data.kapittel);
leaf
.append("text")
.attr("clip-path", (d) => `url(#${d.clipUid})`)
.attr("x", (d) => 4)
.attr("y", (d) => ytpos(d, 1))
.attr("font-size", (d) => ttsizes[d.size])
.attr("opacity", (d) => (d.h > 30 ? 1 : 0))
// .attr("text-anchor", "end")
.text((d) => d.data.data.post);
leaf
.append("text")
.attr("clip-path", (d) => `url(#${d.clipUid})`)
.attr("x", (d) => 4)
.attr("y", (d) => ytpos(d, 2))
.attr("font-size", (d) => ttsizes[d.size])
.attr("opacity", (d) => (d.h > 36 ? 1 : 0))
// .attr("text-anchor", "end")
.text((d) => numFmt(d.data.data.sum25));
leaf
.append("text")
.attr("clip-path", (d) => `url(#${d.clipUid})`)
.attr("x", (d) => 4)
.attr("y", (d) => ytpos(d, 3))
.attr("font-size", (d) => ttsizes[d.size])
.attr("opacity", (d) => d.h > ytpos(d, 4) + 2)
// .attr("text-anchor", "end")
.text((d) => (d.data.data.diff ? diffFmt(d.data.data.diff) : "Nytt 2025"));

return svg.node();
}
Insert cell
Insert cell
diffFmt = (input) => {
const formatter = new Intl.NumberFormat("no-NO", {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
const formattedNumber = formatter.format(Math.abs((input - 1) * 100));
return (input > 1 ? "+ " : "- ") + formattedNumber + " %";
}
Insert cell
numFmt = (input) => {
if (input > 1e9) {
return (
(input / 1e9).toLocaleString("no-NO", {
minimumFractionDigits: 1,
maximumFractionDigits: 1
}) + " mrd."
);
} else if (input > 1e6) {
return (
(input / 1e6).toLocaleString("no-NO", {
minimumFractionDigits: 1,
maximumFractionDigits: 1
}) + " mill."
);
}
return input.toLocaleString("no-NO", {
minimumFractionDigits: 1,
maximumFractionDigits: 1
});
}
Insert cell
colorScale = (input) => {
if (!input) return "green";
return colorScaleL(input);
}
Insert cell
colorWidth = 0.1
Insert cell
colorCenter = 1.03
Insert cell
colorScaleL = d3
.scaleLinear()
.domain([colorCenter - colorWidth, colorCenter, colorCenter + colorWidth]) // The domain with midpoint at 1.05
.range(["red", "white", "green"])
.clamp(true)
Insert cell
// Append multiline text. The last line shows the value and has a specific formatting.
tsizes = ({
l: 28,
m: 18,
s: 10,
xs: 6
})
Insert cell
ttsizes = ({
l: 14,
m: 11,
s: 7,
xs: 5
})
Insert cell
calcSizeMinus = (textlength) => {
//return textlength;
return 0;
return Math.max(0, Math.floor(textlength - 8 / 2));
}
Insert cell
Insert cell
leaves = {
let leaves = root.leaves();
leaves.forEach((d) => {
d.midx = (d.x1 - d.x0) / 2;
d.midy = (d.y1 - d.y0) / 2;
d.w = d.x1 - d.x0;
d.h = d.y1 - d.y0;
d.hratio = d.h / d.w;
d.rotate = d.hratio > 1.5;

let textlength = d.w;
d.size = "s";
if (textlength > 300) {
d.size = "l";
} else if (textlength > 200) {
d.size = "m";
} else if (textlength > 100) {
d.size = "s";
} else if (textlength > 50) {
d.size = "s";
} else {
d.size = "xs";
}
if (
d.data.data.kapittel === "Kunnskapssektorens tjenesteleverandør - Sikt"
) {
d.data.data.kapittel = "Sikt";
} else if (
d.data.data.kapittel === "Direktoratet for høyere utdanning og kompetanse"
) {
d.data.data.kapittel = "HK-dir";
}
});
return leaves;
}
Insert cell
leaves.map((d) => d.data.data.kapittel)
Insert cell
Insert cell
Insert cell
config = ({
height: 3000,
width: 1000
})
Insert cell
Insert cell
Insert cell
root = d3
.treemap()
.tile(tile) // e.g., d3.treemapSquarify. Fetched from the dropdown in the configuration section
.size([config.width, config.height])
.padding(1)
.round(true)(
d3
.hierarchy(dataStratified)
.sum((d) => d.data.sum25)
.sort((a, b) => b.data.sum25 - a.data.sum25)
)
Insert cell
Insert cell
dataStratified = d3
.stratify()
.id((d) => d.id)
.parentId((d) => d.parent)(combinedData)
Insert cell
Insert cell
combinedData = {
let root = { Område: "root", level: 0, id: "root", sum25: null };
let topData = kd_kategori25_aggregated
.objects()
.filter((d) => !d.kapittel.includes("lånekasse"))
.sort((a, b) => a.sum25 - b.sum25)
.map((d, i) => ({
...d,
id: createBase64Id(d.kapittel),
sum25: 0,
parent: "root",
level: 1
}));
let data = kd_kategori25
.objects()
.filter((d) => !d.kapittel.includes("lånekasse") && d.sum25 > 10e7)
.sort((a, b) => a.sum25 - b.sum25)
.map((d, i) => ({
...d,
id: "2." + i,
parent: createBase64Id(d.kapittel),
level: 2
}));
return [root, ...topData, ...data];
}
Insert cell
kd_kategori25.objects()[60]
Insert cell
createBase64Id = (text) => {
// Convert the input text to Base64 encoding
let base64Encoded = btoa(text);

// Replace invalid characters for an HTML ID
// '=' is removed, '/' and '+' are replaced with valid characters
let safeBase64Id = base64Encoded
.replace(/=+$/, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");

return safeBase64Id;
}
Insert cell
Insert cell
import {
kd_kategori25,
kd_kategori25_aggregated,
kd_kategori24_only
} from "b7c388bdeecd8fde"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more