From 1b503c72b417fcbdbed3888a6a2367417500584e Mon Sep 17 00:00:00 2001 From: Lars Windolf Date: Thu, 7 Sep 2023 00:03:17 +0200 Subject: [PATCH] Add df notebook --- frontend/notebooks/{arp.md => arp.nb} | 0 frontend/notebooks/{default.md => default.nb} | 0 frontend/notebooks/df.nb | 331 ++++++++++++++++++ frontend/notebooks/{systemd.md => systemd.nb} | 0 4 files changed, 331 insertions(+) rename frontend/notebooks/{arp.md => arp.nb} (100%) rename frontend/notebooks/{default.md => default.nb} (100%) create mode 100644 frontend/notebooks/df.nb rename frontend/notebooks/{systemd.md => systemd.nb} (100%) diff --git a/frontend/notebooks/arp.md b/frontend/notebooks/arp.nb similarity index 100% rename from frontend/notebooks/arp.md rename to frontend/notebooks/arp.nb diff --git a/frontend/notebooks/default.md b/frontend/notebooks/default.nb similarity index 100% rename from frontend/notebooks/default.md rename to frontend/notebooks/default.nb diff --git a/frontend/notebooks/df.nb b/frontend/notebooks/df.nb new file mode 100644 index 0000000..992ff3f --- /dev/null +++ b/frontend/notebooks/df.nb @@ -0,0 +1,331 @@ +# %% [markdown] +## Disk Usage Debugging +# %% [markdown] +### Directory Usage Overview + +The following command lists directory sorted by size. You might want to modify the `-maxdepth 2` parameter for different details. **Note that this command can run for quite some time!** +# %%--- [shell] +# properties: +# bottom_hidden: true +# ---%% +cd /usr/share; (find . -maxdepth 3 -type d ! -name "." -print0 | xargs -0 -n1 du -sm | sort -nr ) 2>/dev/null +# %%--- [html] +# properties: +# run_on_load: true +# top_hidden: true +# ---%% +Visualize: + + +
+# %%--- [esm] +# properties: +# run_on_load: true +# ---%% +import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm"; + +function createTreeMap(nr, id) { + const data = getData(nr); + + // Specify the chart’s dimensions. + const width = 928; + const height = 924; + + // This custom tiling function adapts the built-in binary tiling function + // for the appropriate aspect ratio when the treemap is zoomed-in. + function tile(node, x0, y0, x1, y1) { + d3.treemapBinary(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); + } + } + + // Compute the layout. + const hierarchy = d3.hierarchy(data) + .sum(d => d.value) + .sort((a, b) => b.value - a.value); + const root = d3.treemap().tile(tile)(hierarchy); + + // Create the scales. + const x = d3.scaleLinear().rangeRound([0, width]); + const y = d3.scaleLinear().rangeRound([0, height]); + + // Formatting utilities. + const format = d3.format(",d"); + const name = d => d.ancestors().reverse().map(d => d.data.name).join("/"); + + // Create the SVG container. + const svg = d3.create("svg") + .attr("viewBox", [0.5, -30.5, width, height + 30]) + .attr("width", width) + .attr("height", height + 30) + .attr("style", "max-width: 100%; height: auto;") + .style("font", "10px sans-serif"); + + // Display the root. + 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 = uid("leaf")).id) + .attr("fill", d => d === root ? "#fff" : d.children ? "#ccc" : "#ddd") + .attr("stroke", "#fff"); + + node.append("clipPath") + .attr("id", d => (d.clipUid = 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) + .selectAll("tspan") + .data(d => (d === root ? name(d) : d.data.name).split(/(?=[A-Z][^A-Z])/g).concat(format(d.value))) + .join("tspan") + .attr("x", 3) + .attr("y", (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`) + .attr("fill-opacity", (d, i, nodes) => i === nodes.length - 1 ? 0.7 : null) + .attr("font-weight", (d, i, nodes) => i === nodes.length - 1 ? "normal" : null) + .text(d => d); + + group.call(position, root); + } + + function position(group, root) { + group.selectAll("g") + .attr("transform", d => d === root ? `translate(0,-30)` : `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 ? 30 : y(d.y1) - y(d.y0)); + } + + // When zooming in, draw the new nodes on top, and fade them in. + 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)); + } + + // When zooming out, draw the old nodes on top, and fade them out. + 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)); + } + + document.getElementById(id).replaceChildren(svg.node()); +} + +function createRadialGraph(nr, id) { + const data = getData(nr); + + // Specify the chart’s dimensions. + const width = 928; + const height = width; + const radius = width / 6; + + // Create the color scale. + const color = d3.scaleOrdinal(d3.quantize(d3.interpolateRainbow, data.children.length + 1)); + + // Compute the layout. + const hierarchy = d3.hierarchy(data) + .sum(d => d.value) + .sort((a, b) => b.value - a.value); + const root = d3.partition() + .size([2 * Math.PI, hierarchy.height + 1]) + (hierarchy); + root.each(d => d.current = d); + + // Create the arc generator. + const arc = d3.arc() + .startAngle(d => d.x0) + .endAngle(d => d.x1) + .padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005)) + .padRadius(radius * 1.5) + .innerRadius(d => d.y0 * radius) + .outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1)) + + // Create the SVG container. + const svg = d3.create("svg") + .attr("viewBox", [-width / 2, -height / 2, width, width]) + .style("font", "10px sans-serif"); + + // Append the arcs. + const path = svg.append("g") + .selectAll("path") + .data(root.descendants().slice(1)) + .join("path") + .attr("fill", d => { while (d.depth > 1) d = d.parent; return color(d.data.name); }) + .attr("fill-opacity", d => arcVisible(d.current) ? (d.children ? 0.6 : 0.4) : 0) + .attr("pointer-events", d => arcVisible(d.current) ? "auto" : "none") + + .attr("d", d => arc(d.current)); + + // Make them clickable if they have children. + path.filter(d => d.children) + .style("cursor", "pointer") + .on("click", clicked); + + const format = d3.format(",d"); + path.append("title") + .text(d => `${d.ancestors().map(d => d.data.name).reverse().join("/")}\n${format(d.value)}`); + + const label = svg.append("g") + .attr("pointer-events", "none") + .attr("text-anchor", "middle") + .style("user-select", "none") + .selectAll("text") + .data(root.descendants().slice(1)) + .join("text") + .attr("dy", "0.35em") + .attr("fill-opacity", d => +labelVisible(d.current)) + .attr("transform", d => labelTransform(d.current)) + .text(d => d.data.name); + + const parent = svg.append("circle") + .datum(root) + .attr("r", radius) + .attr("fill", "none") + .attr("pointer-events", "all") + .on("click", clicked); + + // Handle zoom on click. + function clicked(event, p) { + parent.datum(p.parent || root); + + root.each(d => d.target = { + x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI, + x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI, + y0: Math.max(0, d.y0 - p.depth), + y1: Math.max(0, d.y1 - p.depth) + }); + + const t = svg.transition().duration(750); + + // Transition the data on all arcs, even the ones that aren’t visible, + // so that if this transition is interrupted, entering arcs will start + // the next transition from the desired position. + path.transition(t) + .tween("data", d => { + const i = d3.interpolate(d.current, d.target); + return t => d.current = i(t); + }) + .filter(function(d) { + return +this.getAttribute("fill-opacity") || arcVisible(d.target); + }) + .attr("fill-opacity", d => arcVisible(d.target) ? (d.children ? 0.6 : 0.4) : 0) + .attr("pointer-events", d => arcVisible(d.target) ? "auto" : "none") + + .attrTween("d", d => () => arc(d.current)); + + label.filter(function(d) { + return +this.getAttribute("fill-opacity") || labelVisible(d.target); + }).transition(t) + .attr("fill-opacity", d => +labelVisible(d.target)) + .attrTween("transform", d => () => labelTransform(d.current)); + } + + function arcVisible(d) { + return d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0; + } + + function labelVisible(d) { + return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03; + } + + function labelTransform(d) { + const x = (d.x0 + d.x1) / 2 * 180 / Math.PI; + const y = (d.y0 + d.y1) / 2 * radius; + return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`; + } + + document.getElementById(id).replaceChildren(svg.node()); +}; + +function uid(prefix) { + return { id: prefix + Date.now().toString(36) + Math.random().toString(36).substring(2) }; +} + +function getData(nr) { + // input data will be lines of " " + let data = {name: ".", children: []}; + let pathByName = {}; // lookup hash for insertion + getCellOutput(nr).split(/\n/).forEach((line) => { + function createParents(dir) { + let d, name; + + if(pathByName[dir]) + return pathByName[dir]; + + if(-1 == dir.indexOf('/')) { + d = data; + name = dir; + } else { + let m, parentDir; + [m, parentDir, name] = dir.match(/(.+)\/([^\/]+)/); + d = createParents(parentDir); + } + + return pathByName[dir] = d.children[d.children.push({name: name, children: []}) - 1]; + } + + try { + let path = line.split(/\s+/, 2)[1]; + if("" !== path) { + let d, name, m = path.match(/\.\/(.+)\/([^\/]+)/); + if(m) { + d = createParents(m[1]); + name = m[2]; + } else { + d = data; + name = path.splice(2, path.length); // FIXME: broken + } + + d.children.push({ name: name, value: line.split(/\s+/, 2)[0]}); + } + } catch(e) { + console.error(`Invalid line: ${line} (${e})`); + }; + }); + + return data; +} + +window.createRadialGraph = createRadialGraph; +window.createTreeMap = createTreeMap; \ No newline at end of file diff --git a/frontend/notebooks/systemd.md b/frontend/notebooks/systemd.nb similarity index 100% rename from frontend/notebooks/systemd.md rename to frontend/notebooks/systemd.nb