mirror of
				https://github.com/simon987/sist2.git
				synced 2025-11-04 09:36:53 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			814 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			814 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.8.5</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')">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')">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);
 | 
						|
    card.classList.toggle("full-screen");
 | 
						|
}
 | 
						|
 | 
						|
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>
 |