mirror of
https://github.com/simon987/sist2.git
synced 2025-04-20 18:56:49 +00:00
822 lines
26 KiB
HTML
822 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>sist2 - Stats</title>
|
|
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'/>
|
|
<link href="css" rel="stylesheet" type="text/css">
|
|
</head>
|
|
<body>
|
|
|
|
<nav class="navbar navbar-expand-lg">
|
|
<a class="navbar-brand" href="/">sist2</a>
|
|
<span class="badge badge-pill version">2.9.0</span>
|
|
<span class="tagline">Lightning-fast file system indexer and search tool </span>
|
|
<a style="margin-left: auto" class="btn" href="/">Back</a>
|
|
<button class="btn" type="button" data-toggle="modal" data-target="#settings"
|
|
onclick="loadSettings()">Settings
|
|
</button>
|
|
<button class="btn" title="Toggle theme" onclick="toggleTheme()">Theme</button>
|
|
</nav>
|
|
|
|
<div class="container pb-3">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
|
|
<label for="indices">Index</label>
|
|
<select id="indices" onchange="updateStats()"></select>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="treemap-card" class="stats-card">
|
|
<button class="btn stats-btn" onclick="fullScreen('treemap-card')" id="treemap-card-enlarge">Enlarge</button>
|
|
<button class="btn stats-btn" onclick="exportTreemap()">Export</button>
|
|
<svg id="treemap"></svg>
|
|
</div>
|
|
|
|
<div id="graphs-card" class="stats-card">
|
|
<button class="btn stats-btn" onclick="fullScreen('graphs-card')" id="graphs-card-enlarge">Enlarge</button>
|
|
<div class="graph">
|
|
<svg id="agg_mime_size"></svg>
|
|
</div>
|
|
<div class="graph">
|
|
<svg id="agg_mime_count"></svg>
|
|
</div>
|
|
<div class="graph">
|
|
<svg id="date_histogram"></svg>
|
|
</div>
|
|
<div class="graph">
|
|
<svg id="size_histogram"></svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal" id="settings" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Settings</h5>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="custom-control custom-checkbox">
|
|
<input type="checkbox" class="custom-control-input" id="settingHighlight">
|
|
<label class="custom-control-label" for="settingHighlight">Enable highlighting</label>
|
|
</div>
|
|
|
|
<div class="custom-control custom-checkbox">
|
|
<input type="checkbox" class="custom-control-input" id="settingFuzzy">
|
|
<label class="custom-control-label" for="settingFuzzy">Set fuzzy search by default</label>
|
|
</div>
|
|
|
|
<div class="custom-control custom-checkbox">
|
|
<input type="checkbox" class="custom-control-input" id="settingSearchInPath">
|
|
<label class="custom-control-label" for="settingSearchInPath">Enable matching query against document
|
|
path</label>
|
|
</div>
|
|
|
|
<div class="custom-control custom-checkbox">
|
|
<input type="checkbox" class="custom-control-input" id="settingSuggestPath">
|
|
<label class="custom-control-label" for="settingSuggestPath">Enable auto-complete in path filter bar</label>
|
|
</div>
|
|
|
|
<br/>
|
|
<div class="form-group">
|
|
<input type="number" class="form-control" id="settingFragmentSize">
|
|
<label for="settingFragmentSize">Highlight context size in characters</label>
|
|
</div>
|
|
|
|
<label for="settingDisplay">Display</label>
|
|
<select id="settingDisplay" class="form-control form-control-sm">
|
|
<option value="grid">Grid</option>
|
|
<option value="list">List</option>
|
|
</select>
|
|
|
|
<div class="form-group">
|
|
<label for="settingColumns">Maximum column count</label>
|
|
<select id="settingColumns" class="form-control form-control-sm">
|
|
<option value="3">3</option>
|
|
<option value="4">4</option>
|
|
<option value="5">5</option>
|
|
<option value="6">6</option>
|
|
<option value="7">7</option>
|
|
<option value="8">8</option>
|
|
<option value="9">9</option>
|
|
</select>
|
|
</div>
|
|
|
|
<hr/>
|
|
<h4>Stats</h4>
|
|
|
|
<div class="form-group">
|
|
<label for="settingTreemapType">Treemap type</label>
|
|
<select id="settingTreemapType" class="form-control form-control-sm">
|
|
<option value="cascaded">Cascaded</option>
|
|
<option value="flat">Flat (compact)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="settingTreemapTiling">Treemap tiling</label>
|
|
<select id="settingTreemapTiling" class="form-control form-control-sm">
|
|
<option value="binary">Binary</option>
|
|
<option value="squarify">Squarify</option>
|
|
<option value="slice">Slice</option>
|
|
<option value="dice">Dice</option>
|
|
<option value="sliceDice">Slide & Dice</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="settingTreemapGroupingDepth">Treemap color grouping depth (flat)</label>
|
|
<input type="number" class="form-control" id="settingTreemapGroupingDepth" min="1" max="10">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="settingTreemapColor">Treemap color (cascaded)</label>
|
|
<select id="settingTreemapColor" class="form-control form-control-sm">
|
|
<option value="PuBuGn">Purple-Blue-Green</option>
|
|
<option value="PuRd">Purple-Red</option>
|
|
<option value="PuBu">Purple-Blue</option>
|
|
<option value="YlOrBr">Yellow-Orange-Brown</option>
|
|
<option value="YlOrRd">Yellow-Orange-Red</option>
|
|
<option value="YlGn">Yellow-Green</option>
|
|
<option value="YlGnBu">Yellow-Green-Blue</option>
|
|
<option value="Plasma">Plasma</option>
|
|
<option value="Magma">Magma</option>
|
|
<option value="Inferno">Inferno</option>
|
|
<option value="Viridis">Viridis</option>
|
|
<option value="Turbo">Turbo</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="settingTreemapSize">Treemap size</label>
|
|
<select id="settingTreemapSize" class="form-control form-control-sm">
|
|
<option value="small">Small</option>
|
|
<option value="medium">Medium</option>
|
|
<option value="large">Large</option>
|
|
<option value="x-large">X-Large</option>
|
|
<option value="xx-large">XX-Large</option>
|
|
</select>
|
|
</div>
|
|
|
|
<br>
|
|
<button class="btn btn-primary float-right" onclick="updateSettings()">Update settings</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="jslib" type="text/javascript"></script>
|
|
<script>
|
|
let width;
|
|
let height;
|
|
let indexMap = {};
|
|
|
|
const barHeight = 20;
|
|
const ordinalColor = d3.scaleOrdinal(d3.schemeCategory10);
|
|
|
|
const formatSI = d3.format("~s");
|
|
|
|
|
|
const TILING_MODES = {
|
|
"squarify": d3.treemapSquarify,
|
|
"binary": d3.treemapBinary,
|
|
"sliceDice": d3.treemapSliceDice,
|
|
"slice": d3.treemapSlice,
|
|
"dice": d3.treemapDice,
|
|
};
|
|
|
|
const COLORS = {
|
|
"PuBuGn": d3.interpolatePuBuGn,
|
|
"PuRd": d3.interpolatePuRd,
|
|
"PuBu": d3.interpolatePuBu,
|
|
"YlOrBr": d3.interpolateYlOrBr,
|
|
"YlOrRd": d3.interpolateYlOrRd,
|
|
"YlGn": d3.interpolateYlGn,
|
|
"YlGnBu": d3.interpolateYlGnBu,
|
|
"Plasma": d3.interpolatePlasma,
|
|
"Magma": d3.interpolateMagma,
|
|
"Inferno": d3.interpolateInferno,
|
|
"Viridis": d3.interpolateViridis,
|
|
"Turbo": d3.interpolateTurbo,
|
|
};
|
|
|
|
const SIZES = {
|
|
"small": [800, 600],
|
|
"medium": [1300, 750],
|
|
"large": [1900, 900],
|
|
"x-large": [2800, 1700],
|
|
"xx-large": [3600, 2000],
|
|
};
|
|
|
|
const fillOpacity = document.cookie.includes("sist") ? 0.9 : 0.6;
|
|
|
|
const uids = {};
|
|
|
|
function uid(name) {
|
|
let id = uids[name] || 0;
|
|
uids[name] = id + 1;
|
|
return name + id;
|
|
}
|
|
|
|
const burrow = function (table, addSelfDir) {
|
|
const root = {};
|
|
table.forEach(row => {
|
|
let layer = root;
|
|
|
|
row.taxonomy.forEach(key => {
|
|
layer[key] = key in layer ? layer[key] : {};
|
|
layer = layer[key];
|
|
});
|
|
if (Object.keys(layer).length === 0) {
|
|
layer["$size$"] = row.size;
|
|
} else if (addSelfDir) {
|
|
layer["."] = {
|
|
"$size$": row.size,
|
|
};
|
|
}
|
|
});
|
|
|
|
const descend = function (obj, depth) {
|
|
return Object.keys(obj).filter(k => k !== "$size$").map(k => {
|
|
const child = {
|
|
name: k,
|
|
depth: depth,
|
|
value: 0,
|
|
children: descend(obj[k], depth + 1)
|
|
};
|
|
if ("$size$" in obj[k]) {
|
|
child.value = obj[k]["$size$"];
|
|
}
|
|
return child;
|
|
});
|
|
};
|
|
|
|
return {
|
|
name: `[${indexMap[$("#indices").val()]}]`,
|
|
children: descend(root, 1),
|
|
value: 0,
|
|
depth: 0,
|
|
}
|
|
};
|
|
|
|
function flatTreemap(data, svg) {
|
|
const root = d3.treemap()
|
|
.tile(TILING_MODES[CONF.options.treemapTiling])
|
|
.size([width, height])
|
|
.padding(1)
|
|
.round(true)(
|
|
d3.hierarchy(data)
|
|
.sum(d => d.value)
|
|
.sort((a, b) => b.value - a.value)
|
|
);
|
|
|
|
const leaf = svg.selectAll("g")
|
|
.data(root.leaves())
|
|
.join("g")
|
|
.attr("transform", d => `translate(${d.x0},${d.y0})`);
|
|
|
|
leaf.append("title")
|
|
.text(d => `${d.ancestors().reverse().map(d => d.data.name).join("/")}\n${humanFileSize(d.value)}`);
|
|
|
|
leaf.append("rect")
|
|
.attr("id", d => (d.leafUid = uid("leaf")))
|
|
.attr("fill", d => {
|
|
while (d.depth > CONF.options.treemapGroupingDepth) d = d.parent;
|
|
return ordinalColor(d.data.name);
|
|
})
|
|
.attr("fill-opacity", fillOpacity)
|
|
.attr("width", d => d.x1 - d.x0)
|
|
.attr("height", d => d.y1 - d.y0);
|
|
|
|
leaf.append("clipPath")
|
|
.attr("id", d => (d.clipUid = uid("clip")))
|
|
.append("use")
|
|
.attr("href", d => `#${d.leafUid}`);
|
|
|
|
leaf.append("text")
|
|
.attr("clip-path", d => `url(#${d.clipUid})`)
|
|
.selectAll("tspan")
|
|
.data(d => {
|
|
if (d.data.name === ".") {
|
|
d = d.parent;
|
|
}
|
|
return [d.data.name, humanFileSize(d.value)]
|
|
})
|
|
.join("tspan")
|
|
.attr("x", 2)
|
|
.attr("y", (d, i, nodes) => `${i === 0 ? 1.1 : 2.3}em`)
|
|
.text(d => d);
|
|
}
|
|
|
|
function cascade(root, offset) {
|
|
const x = new Map;
|
|
const y = new Map;
|
|
return root.eachAfter(d => {
|
|
if (d.children && d.children.length !== 0) {
|
|
x.set(d, 1 + d3.max(d.children, c => c.x1 === d.x1 - offset ? x.get(c) : NaN));
|
|
y.set(d, 1 + d3.max(d.children, c => c.y1 === d.y1 - offset ? y.get(c) : NaN));
|
|
} else {
|
|
x.set(d, 0);
|
|
y.set(d, 0);
|
|
}
|
|
}).eachBefore(d => {
|
|
d.x1 -= 2 * offset * x.get(d);
|
|
d.y1 -= 2 * offset * y.get(d);
|
|
});
|
|
}
|
|
|
|
function cascadeTreemap(data, svg) {
|
|
|
|
const root = cascade(
|
|
d3.treemap()
|
|
.size([width, height])
|
|
.tile(TILING_MODES[CONF.options.treemapTiling])
|
|
.paddingOuter(3)
|
|
.paddingTop(16)
|
|
.paddingInner(1)
|
|
.round(true)(
|
|
d3.hierarchy(data)
|
|
.sum(d => d.value)
|
|
.sort((a, b) => b.value - a.value)
|
|
),
|
|
3 // treemap.paddingOuter
|
|
);
|
|
|
|
const maxDepth = Math.max(...root.descendants().map(d => d.depth));
|
|
const color = d3.scaleSequential([maxDepth, -1], COLORS[CONF.options.treemapColor]);
|
|
|
|
svg.append("filter")
|
|
.attr("id", "shadow")
|
|
.append("feDropShadow")
|
|
.attr("flood-opacity", 0.3)
|
|
.attr("dx", 0)
|
|
.attr("stdDeviation", 3);
|
|
|
|
const node = svg.selectAll("g")
|
|
.data(
|
|
d3.nest()
|
|
.key(d => d.depth).sortKeys(d3.ascending)
|
|
.entries(root.descendants())
|
|
)
|
|
.join("g")
|
|
.attr("filter", "url(#shadow)")
|
|
.selectAll("g")
|
|
.data(d => d.values)
|
|
.join("g")
|
|
.attr("transform", d => `translate(${d.x0},${d.y0})`);
|
|
|
|
node.append("title")
|
|
.text(d => `${d.ancestors().reverse().splice(1).map(d => d.data.name).join("/")}\n${humanFileSize(d.value)}`);
|
|
|
|
node.append("rect")
|
|
.attr("id", d => (d.nodeUid = uid("node")))
|
|
.attr("fill", d => color(d.depth))
|
|
.attr("width", d => d.x1 - d.x0)
|
|
.attr("height", d => d.y1 - d.y0);
|
|
|
|
node.append("clipPath")
|
|
.attr("id", d => (d.clipUid = uid("clip")))
|
|
.append("use")
|
|
.attr("href", d => `#${d.nodeUid}`);
|
|
|
|
node.append("text")
|
|
.attr("fill", d => d3.hsl(color(d.depth)).l > .5 ? "#333" : "#eee")
|
|
.attr("clip-path", d => `url(#${d.clipUid})`)
|
|
.selectAll("tspan")
|
|
.data(d => [d.data.name, humanFileSize(d.value)])
|
|
.join("tspan")
|
|
.text(d => d);
|
|
|
|
node.filter(d => d.children).selectAll("tspan")
|
|
.attr("dx", 3)
|
|
.attr("y", 13);
|
|
|
|
node.filter(d => !d.children).selectAll("tspan")
|
|
.attr("x", 3)
|
|
.attr("y", (d, i, nodes) => `${i === 0 ? 1.1 : 2.3}em`);
|
|
}
|
|
|
|
|
|
function mimeBarSize(data, svg) {
|
|
|
|
const margin = {
|
|
top: 50,
|
|
right: 0,
|
|
bottom: 10,
|
|
left: Math.max(
|
|
d3.max(data.sort((a, b) => b.count - a.count).slice(0, 15), d => d.mime.length) * 6,
|
|
d3.max(data.sort((a, b) => b.size - a.size).slice(0, 15), d => d.mime.length) * 6,
|
|
)
|
|
};
|
|
|
|
data.forEach(d => {
|
|
d.name = d.mime;
|
|
d.value = Number(d.size);
|
|
});
|
|
data = data.sort((a, b) => b.value - a.value).slice(0, 15);
|
|
|
|
const width = 550;
|
|
const height = Math.ceil((data.length + 0.1) * barHeight) + margin.top + margin.bottom;
|
|
|
|
svg.selectAll("*").remove();
|
|
svg.attr("viewBox", [0, 0, width, height]);
|
|
|
|
const y = d3.scaleBand()
|
|
.domain(d3.range(data.length))
|
|
.rangeRound([margin.top, height - margin.bottom]);
|
|
|
|
const x = d3.scaleLinear()
|
|
.domain([0, d3.max(data, d => d.value)])
|
|
.range([margin.left, width - margin.right]);
|
|
|
|
svg.append("g")
|
|
.attr("fill-opacity", fillOpacity)
|
|
.selectAll("rect")
|
|
.data(data)
|
|
.join("rect")
|
|
.attr("fill", d => ordinalColor(d.name))
|
|
.attr("x", x(0))
|
|
.attr("y", (d, i) => y(i))
|
|
.attr("width", d => x(d.value) - x(0))
|
|
.attr("height", y.bandwidth())
|
|
.append("title")
|
|
.text(d => formatSI(d.value));
|
|
|
|
svg.append("g")
|
|
.attr("transform", `translate(0,${margin.top})`)
|
|
.call(d3.axisTop(x).ticks(width / 80, data.format).tickFormat(formatSI))
|
|
.call(g => g.select(".domain").remove());
|
|
|
|
svg.append("g")
|
|
.attr("transform", `translate(${margin.left},0)`)
|
|
.call(d3.axisLeft(y).tickFormat(i => data[i].name).tickSizeOuter(0));
|
|
|
|
svg.append("text")
|
|
.attr("x", (width / 2))
|
|
.attr("y", (margin.top / 2))
|
|
.attr("text-anchor", "middle")
|
|
.style("font-size", "16px")
|
|
.text("Size distribution by MIME type");
|
|
}
|
|
|
|
function mimeBarCount(data, svg) {
|
|
|
|
const margin = {
|
|
top: 50,
|
|
right: 0,
|
|
bottom: 10,
|
|
left: Math.max(
|
|
d3.max(data.sort((a, b) => b.count - a.count).slice(0, 15), d => d.mime.length) * 6,
|
|
d3.max(data.sort((a, b) => b.size - a.size).slice(0, 15), d => d.mime.length) * 6,
|
|
)
|
|
};
|
|
|
|
data.forEach(d => {
|
|
d.name = d.mime;
|
|
d.value = Number(d.count);
|
|
});
|
|
|
|
data = data.sort((a, b) => b.value - a.value).slice(0, 15);
|
|
|
|
const width = 550;
|
|
const height = Math.ceil((data.length + 0.1) * barHeight) + margin.top + margin.bottom;
|
|
|
|
svg.selectAll("*").remove();
|
|
svg.attr("viewBox", [0, 0, width, height]);
|
|
|
|
const y = d3.scaleBand()
|
|
.domain(d3.range(data.length))
|
|
.rangeRound([margin.top, height - margin.bottom]);
|
|
|
|
const x = d3.scaleLinear()
|
|
.domain([0, d3.max(data, d => d.value)])
|
|
.range([margin.left, width - margin.right]);
|
|
|
|
svg.append("g")
|
|
.attr("fill-opacity", fillOpacity)
|
|
.selectAll("rect")
|
|
.data(data)
|
|
.join("rect")
|
|
.attr("fill", d => ordinalColor(d.name))
|
|
.attr("x", x(0))
|
|
.attr("y", (d, i) => y(i))
|
|
.attr("width", d => x(d.value) - x(0))
|
|
.attr("height", y.bandwidth())
|
|
.append("title")
|
|
.text(d => d3.format(",")(d.value));
|
|
|
|
svg.append("g")
|
|
.attr("transform", `translate(0,${margin.top})`)
|
|
.call(d3.axisTop(x).ticks(width / 80, data.format).tickFormat(formatSI))
|
|
.call(g => g.select(".domain").remove());
|
|
|
|
svg.append("g")
|
|
.attr("transform", `translate(${margin.left},0)`)
|
|
.call(d3.axisLeft(y).tickFormat(i => data[i].name).tickSizeOuter(0));
|
|
|
|
svg.append("text")
|
|
.attr("x", (width / 2))
|
|
.attr("y", (margin.top / 2))
|
|
.attr("text-anchor", "middle")
|
|
.style("font-size", "16px")
|
|
.text("File count distribution by MIME type");
|
|
}
|
|
|
|
function dateHistogram(data, svg) {
|
|
|
|
let bins = data.map(d => {
|
|
return {
|
|
length: Number(d.count),
|
|
x0: Number(d.bucket),
|
|
x1: Number(d.bucket) + 2629800
|
|
}
|
|
});
|
|
bins.sort((a, b) => a.length - b.length);
|
|
|
|
const margin = {
|
|
top: 50,
|
|
right: 20,
|
|
bottom: 70,
|
|
left: 40
|
|
};
|
|
|
|
const thresh = d3.quantile(bins, 0.9, d => d.length);
|
|
bins = bins.filter(d => d.length > thresh);
|
|
|
|
const width = 550;
|
|
const height = 450;
|
|
|
|
svg.selectAll("*").remove();
|
|
svg.attr("viewBox", [0, 0, width, height]);
|
|
|
|
const y = d3.scaleLinear()
|
|
.domain([0, d3.max(bins, d => d.length)]).nice()
|
|
.range([height - margin.bottom, margin.top]);
|
|
|
|
const x = d3.scaleLinear()
|
|
.domain(d3.extent(bins, d => d.x0)).nice()
|
|
.range([margin.left, width - margin.right]);
|
|
|
|
svg.append("g")
|
|
.attr("fill", "steelblue")
|
|
.selectAll("rect")
|
|
.data(bins)
|
|
.join("rect")
|
|
.attr("x", d => x(d.x0) + 1)
|
|
.attr("width", d => Math.max(1, x(d.x1) - x(d.x0) - 1))
|
|
.attr("y", d => y(d.length))
|
|
.attr("height", d => y(0) - y(d.length))
|
|
.call(g => g
|
|
.append("title")
|
|
.text(d => d.length)
|
|
);
|
|
|
|
svg.append("g")
|
|
.attr("transform", `translate(0,${height - margin.bottom})`)
|
|
.call(
|
|
d3.axisBottom(x)
|
|
.ticks(width / 30)
|
|
.tickSizeOuter(0)
|
|
.tickFormat(t => d3.timeFormat("%Y-%m-%d")(d3.utcParse("%s")(t)))
|
|
)
|
|
.call(g => g
|
|
.selectAll("text")
|
|
.style("text-anchor", "end")
|
|
.attr("dx", "-.8em")
|
|
.attr("dy", ".15em")
|
|
.attr("transform", "rotate(-65)")
|
|
)
|
|
.call(g => g.append("text")
|
|
.attr("x", width - margin.right)
|
|
.attr("y", -4)
|
|
.attr("fill", "currentColor")
|
|
.attr("font-weight", "bold")
|
|
.attr("text-anchor", "end")
|
|
.text("mtime")
|
|
);
|
|
|
|
svg.append("g")
|
|
.attr("transform", `translate(${margin.left},0)`)
|
|
.call(
|
|
d3.axisLeft(y)
|
|
.ticks(height / 40)
|
|
.tickFormat(t => formatSI(t))
|
|
)
|
|
.call(g => g.select(".domain").remove())
|
|
.call(g => g.select(".tick:last-of-type text").clone()
|
|
.attr("x", 4)
|
|
.attr("text-anchor", "start")
|
|
.attr("font-weight", "bold")
|
|
.text("File count"));
|
|
|
|
svg.append("text")
|
|
.attr("x", (width / 2))
|
|
.attr("y", (margin.top / 2))
|
|
.attr("text-anchor", "middle")
|
|
.style("font-size", "16px")
|
|
.text("File modification time distribution");
|
|
}
|
|
|
|
function sizeHistogram(data, svg) {
|
|
|
|
let bins = data.map(d => {
|
|
return {
|
|
length: Number(d.count),
|
|
x0: Number(d.bucket),
|
|
x1: Number(d.bucket) + (5 * 1024 * 1024)
|
|
}
|
|
});
|
|
bins = bins.sort((a, b) => b.length - a.length).slice(0, 25);
|
|
|
|
const margin = {
|
|
top: 50,
|
|
right: 20,
|
|
bottom: 70,
|
|
left: 40
|
|
};
|
|
|
|
const width = 550;
|
|
const height = 450;
|
|
|
|
svg.selectAll("*").remove();
|
|
svg.attr("viewBox", [0, 0, width, height]);
|
|
|
|
const y = d3.scaleLinear()
|
|
.domain([0, d3.max(bins, d => d.length)])
|
|
.range([height - margin.bottom, margin.top]);
|
|
|
|
const x = d3.scaleLinear()
|
|
.domain(d3.extent(bins, d => d.x0)).nice()
|
|
.range([margin.left, width - margin.right]);
|
|
|
|
svg.append("g")
|
|
.attr("fill", "steelblue")
|
|
.selectAll("rect")
|
|
.data(bins)
|
|
.join("rect")
|
|
.attr("x", d => x(d.x0) + 1)
|
|
.attr("width", d => Math.max(1, x(d.x1) - x(d.x0) - 1))
|
|
.attr("y", d => y(d.length))
|
|
.attr("height", d => y(0) - y(d.length))
|
|
.call(g => g
|
|
.append("title")
|
|
.text(d => d.length)
|
|
);
|
|
|
|
svg.append("g")
|
|
.attr("transform", `translate(0,${height - margin.bottom})`)
|
|
.call(
|
|
d3.axisBottom(x)
|
|
.ticks(width / 30)
|
|
.tickSizeOuter(0)
|
|
.tickFormat(formatSI)
|
|
)
|
|
.call(g => g
|
|
.selectAll("text")
|
|
.style("text-anchor", "end")
|
|
.attr("dx", "-.8em")
|
|
.attr("dy", ".15em")
|
|
.attr("transform", "rotate(-65)")
|
|
)
|
|
.call(g => g.append("text")
|
|
.attr("x", width - margin.right)
|
|
.attr("y", -4)
|
|
.attr("fill", "currentColor")
|
|
.attr("font-weight", "bold")
|
|
.attr("text-anchor", "end")
|
|
.text("size (bytes)")
|
|
);
|
|
|
|
svg.append("g")
|
|
.attr("transform", `translate(${margin.left},0)`)
|
|
.call(
|
|
d3.axisLeft(y)
|
|
.ticks(height / 40)
|
|
.tickFormat(t => formatSI(t))
|
|
)
|
|
.call(g => g.select(".domain").remove())
|
|
.call(g => g.select(".tick:last-of-type text").clone()
|
|
.attr("x", 4)
|
|
.attr("text-anchor", "start")
|
|
.attr("font-weight", "bold")
|
|
.text("File count"));
|
|
|
|
svg.append("text")
|
|
.attr("x", (width / 2))
|
|
.attr("y", (margin.top / 2))
|
|
.attr("text-anchor", "middle")
|
|
.style("font-size", "16px")
|
|
.text("File size distribution");
|
|
}
|
|
|
|
|
|
function updateStats() {
|
|
width = SIZES[CONF.options.treemapSize][0];
|
|
height = SIZES[CONF.options.treemapSize][1];
|
|
|
|
const treemapSvg = d3.select("#treemap");
|
|
const mimeSvgSize = d3.select("#agg_mime_size");
|
|
const mimeSvgCount = d3.select("#agg_mime_count");
|
|
const dateHistogramSvg = d3.select("#date_histogram");
|
|
const sizeHistogramSvg = d3.select("#size_histogram");
|
|
|
|
const indexId = $("#indices").val();
|
|
|
|
d3.csv(`./s/${indexId}/1`).then(tabularData => {
|
|
tabularData.forEach(row => {
|
|
row.taxonomy = row.path.split("/");
|
|
row.size = Number(row.size);
|
|
});
|
|
|
|
if (CONF.options.treemapType === "cascaded") {
|
|
const data = burrow(tabularData, false);
|
|
cascadeTreemap(data, treemapSvg);
|
|
} else {
|
|
const data = burrow(tabularData.sort((a, b) => b.taxonomy.length - a.taxonomy.length), true);
|
|
flatTreemap(data, treemapSvg);
|
|
}
|
|
});
|
|
|
|
d3.csv(`./s/${indexId}/2`).then(tabularData => {
|
|
mimeBarSize(tabularData.slice(), mimeSvgSize);
|
|
mimeBarCount(tabularData.slice(), mimeSvgCount);
|
|
});
|
|
|
|
d3.csv(`./s/${indexId}/3`).then(tabularData => {
|
|
sizeHistogram(tabularData, sizeHistogramSvg);
|
|
});
|
|
|
|
d3.csv(`./s/${indexId}/4`).then(tabularData => {
|
|
dateHistogram(tabularData, dateHistogramSvg);
|
|
});
|
|
|
|
treemapSvg.selectAll("*").remove();
|
|
treemapSvg.attr("viewBox", [0, 0, width, height])
|
|
.attr("xmlns", "http://www.w3.org/2000/svg")
|
|
.attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
|
|
.attr("version", "1.1")
|
|
.style("overflow", "visible")
|
|
.style("font", "10px sans-serif");
|
|
|
|
}
|
|
|
|
window.onload = function () {
|
|
CONF.load();
|
|
|
|
$.jsonPost("i").then(resp => {
|
|
const select = $("#indices");
|
|
|
|
const urlIndices = (new URLSearchParams(location.search)).get("i");
|
|
resp["indices"].forEach(idx => {
|
|
indexMap[idx.id] = idx.name;
|
|
select.append($("<option>")
|
|
.attr("value", idx.id)
|
|
.append(idx.name));
|
|
|
|
if (urlIndices && urlIndices.split(",").indexOf(idx.name) !== -1) {
|
|
select.select(idx.name);
|
|
}
|
|
});
|
|
|
|
updateStats();
|
|
});
|
|
};
|
|
|
|
function fullScreen(selector) {
|
|
const card = document.getElementById(selector);
|
|
const btn = document.getElementById(selector + "-enlarge");
|
|
|
|
card.classList.toggle("full-screen");
|
|
|
|
if (card.classList.contains("full-screen")) {
|
|
btn.innerText = "Shrink";
|
|
} else {
|
|
btn.innerText = "Enlarge";
|
|
}
|
|
}
|
|
|
|
function exportTreemap() {
|
|
domtoimage.toBlob(document.getElementById("treemap"), {width: width, height: height})
|
|
.then(function (blob) {
|
|
let a = document.createElement("a");
|
|
let url = URL.createObjectURL(blob);
|
|
|
|
a.href = url;
|
|
a.download = `${indexMap[$("#indices").val()]}_treemap.png`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
setTimeout(function() {
|
|
document.body.removeChild(a);
|
|
window.URL.revokeObjectURL(url);
|
|
}, 0);
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|