-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
331 additions
and
0 deletions.
There are no files selected for viewing
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: | ||
<button onclick="createRadialGraph(2 /* 3rd cell as input */, 'graph')">Radial Graph</button> | ||
<button onclick="createTreeMap(2 /* 3rd cell as input */, 'graph')">Tree Map</button> | ||
<div id='graph'/> | ||
# %%--- [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 "<path> <size in MB>" | ||
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; |
File renamed without changes.