function format_filesize(size, decimals) {
    /*var labels = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];*/
    var labels = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
        label = 0;

    while (size > 2048.0 && label < labels.length - 1) {
        label++;
        size /= 1024.0;
    }
    if (decimals === undefined) {
        decimals = 2;
    }

    return size.toFixed(decimals) + ' ' + labels[label];
}

function packages_treemap(chart_id, orderings, default_order) {
    const div = document.querySelector(chart_id);
    const color = d3.scale.category20();
    var key_func = function(d) { return d.key; };
    var value_package_count = function(d) { return d.count; },
        value_flagged_count = function(d) { return d.flagged; },
        value_compressed_size = function(d) { return d.csize; },
        value_installed_size = function(d) { return d.isize; };

    /* tag the function so when we display, we can format filesizes */
    value_package_count.is_size = value_flagged_count.is_size = false;
    value_compressed_size.is_size = value_installed_size.is_size = true;

    const rects = div.getBoundingClientRect();
    var treemap = d3.layout.treemap()
        .size([rects.width, rects.height])
        /*.sticky(true)*/
        .value(value_package_count)
        .sort(function(a, b) { return a.key < b.key; })
        .children(function(d) { return d.data; });

    var cell_html = function(d) {
        if (d.children) {
            return "";
        }
        var valuefunc = treemap.value();
        var value = valuefunc(d);
        if (valuefunc.is_size && value !== undefined) {
            value = format_filesize(value);
        }
        return "<span>" + d.name + ": " + value + "</span>";
    };

    var d3_div = d3.select(div);

    var prop_px = function(prop, offset) {
        return function(d) {
            var dist = d[prop] + offset;
            if (dist > 0) {
                return dist + "px";
            }
            else {
                return "0px";
            }
        };
    };

    var cell = function() {
        /* the -1 offset comes from the border width we use in the CSS */
        this.style("left", prop_px("x", 0)).style("top", prop_px("y", 0))
            .style("width", prop_px("dx", -1)).style("height", prop_px("dy", -1));
    };

    var fetch_for_ordering = function(order) {
        d3.json(order.url, function(json) {
            var nodes = d3_div.data([json]).selectAll("div")
                .data(treemap.nodes, key_func);
            /* start out new nodes in the center of the picture area */
            const rects = div.getBoundingClientRect();
            const w_center = rects.width / 2;
            const h_center = rects.height / 2;
            nodes.enter().append("div")
                .attr("class", "treemap-cell")
                .attr("title", function(d) { return d.name; })
                .style("left", w_center + "px").style("top", h_center + "px")
                .style("width", "0px").style("height", "0px")
                .style("display", function(d) { return d.children ? "none" : null; })
                .html(cell_html);
            nodes.transition().duration(1500)
                .style("background-color", function(d) {
                    return d.children ? null : color(d[order.color_attr]);
                })
                .call(cell);
            nodes.exit().transition().duration(1500).remove();
        });
    };

    /* start the callback for the default order */
    fetch_for_ordering(orderings[default_order]);

    var make_scale_button = function(name, valuefunc) {
        var button_id = chart_id + "-" + name;
        /* upon button click, attach new value function and redraw all boxes
         * accordingly */
        d3.select(button_id).on("click", function() {
            d3_div.selectAll("div")
                .data(treemap.value(valuefunc), key_func)
                .html(cell_html)
                .transition().duration(1500).call(cell);

            /* drop off the '#' sign to convert id to a class prefix */
            d3.selectAll("." + chart_id.substring(1) + "-scaleby")
                .classed("active", false);
            d3.select(button_id).classed("active", true);
        });
    };

    /* each scale button tweaks our value, e.g. net size function */
    make_scale_button("count", value_package_count);
    make_scale_button("flagged", value_flagged_count);
    make_scale_button("csize", value_compressed_size);
    make_scale_button("isize", value_installed_size);

    var make_group_button = function(name, order) {
        var button_id = chart_id + "-" + name;
        d3.select(button_id).on("click", function() {
            fetch_for_ordering(order);

            /* drop off the '#' sign to convert id to a class prefix */
            d3.selectAll("." + chart_id.substring(1) + "-groupby")
                .classed("active", false);
            d3.select(button_id).classed("active", true);
        });
    };

    for (const [k, v] of Object.entries(orderings)) {
        make_group_button(k, v);
    };

    /* adapt the chart size when the browser resizes */
    var resize_timeout = null;
    var real_resize = function() {
        resize_timeout = null;
        const rects = div.getBoundingClientRect();
        d3_div.selectAll("div")
            .data(treemap.size([rects.width, rects.height]), key_func)
            .call(cell);
    };
    window.addEventListener('resize', function() {
        if (resize_timeout) {
            clearTimeout(resize_timeout);
        }
        resize_timeout = setTimeout(real_resize, 200);
    });
}

function developer_keys(chart_id, data_url) {
    const div = document.querySelector(chart_id);
    const r = 10;
    const rect = div.getBoundingClientRect();

    var force = d3.layout.force()
        .friction(0.5)
        .gravity(0.1)
        .charge(-500)
        .size([rect.width, rect.height]);

    var svg = d3.select(chart_id)
        .append("svg");

    d3.json(data_url, function(json) {
        var fill = d3.scale.category20();

        var index_for_key = function(key) {
            var i;
            key = key.slice(-8);
            for (i = 0; i < json.nodes.length; i++) {
                var node_key = json.nodes[i].key;
                if (node_key && node_key.slice(-8) === key) {
                    return i;
                }
            }
        };

        /* filter edges to only include those that we have two nodes for */
        var edges = json.edges.filter(function(d, i) {
            d.source = index_for_key(d.signer);
            d.target = index_for_key(d.signee);
            return d.source >= 0 && d.target >= 0;
        });

        json.nodes.map(function(d, i) { d.master_sigs = 0; d.other_sigs = 0; });
        edges.map(function(d, i) {
            /* only the target gets credit in either case, as it is their key that was signed */
            if (json.nodes[d.source].group === "master") {
                json.nodes[d.target].master_sigs += 1;
            } else {
                json.nodes[d.target].other_sigs += 1;
            }
        });
        json.nodes.map(function(d, i) {
            if (d.group === "packager") {
                d.approved = d.master_sigs >= 3;
            } else {
                d.approved = null;
            }
        });

        var link = svg.selectAll("line")
            .data(edges)
            .enter()
            .append("line")
            .style("stroke", "#888");

        /* anyone with more than 7 - 1 == 6 signatures gets the top value */
        var stroke_color_scale = d3.scale.log().domain([1, 7]).range(["white", "green"]).clamp(true);

        var node = svg.selectAll("circle")
            .data(json.nodes)
            .enter().append("circle")
            .attr("r", function(d) {
                switch (d.group) {
                case "master":
                    return r * 1.6 - 0.75;
                case "packager":
                default:
                    return r - 0.75;
                }
            })
            .style("fill", function(d) { return fill(d.group); })
            .style("stroke", function(d) {
                if (d.approved === null) {
                    return d3.rgb(fill(d.group)).darker();
                } else if (d.approved) {
                    /* add 1 so we don't blow up the logarithm-based scale */
                    return stroke_color_scale(d.other_sigs + 1);
                } else {
                    return "red";
                }
            })
            .style("stroke-width", "1.5px")
            .call(force.drag);
        node.append("title").text(function(d) { return d.name; });

        var nodeover = function(d, i) {
            d3.select(this).transition().duration(500).style("stroke-width", "3px");
            link.filter(function(d_link, i) {
                return d_link.source === d || d_link.target === d;
            }).transition().duration(500).style("stroke", "#800");
        };
        var nodeout = function(d, i) {
            d3.select(this).transition().duration(500).style("stroke-width", "1.5px");
            link.transition().duration(500).style("stroke", "#888");
        };

        node.on("mouseover", nodeover)
            .on("mouseout", nodeout);

        var distance = function(d, i) {
            /* place a long line between all master keys and other keys.
             * however, other connected clusters should be close together. */
            if (d.source.group === "master" || d.target.group === "master") {
                return 200;
            } else {
                return 40;
            }
        };

        var strength = function(d, i) {
            if (d.source.group === "master" || d.target.group === "master") {
                return 0.2;
            } else {
                return 0.8;
            }
        };

        var tick = function() {
            const offset = r * 2;
            const rects = div.getBoundingClientRect();
            node.attr("cx", function(d) { return (d.x = Math.max(offset, Math.min(rects.width - offset, d.x))); })
                .attr("cy", function(d) { return (d.y = Math.max(offset, Math.min(rects.height - offset, d.y))); });

            link.attr("x1", function(d) { return d.source.x; })
                .attr("y1", function(d) { return d.source.y; })
                .attr("x2", function(d) { return d.target.x; })
                .attr("y2", function(d) { return d.target.y; });
        };

        force.nodes(json.nodes)
            .links(edges)
            .linkDistance(distance)
            .linkStrength(strength)
            .on("tick", tick)
            .start();
    });

    /* adapt the chart size when the browser resizes */
    var resize_timeout = null;
    var real_resize = function() {
        resize_timeout = null;
        const rect = div.getBoundingClientRect();
        force.size([rect.width, rect.height]);
    };

    window.addEventListener('resize', function() {
        if (resize_timeout) {
            clearTimeout(resize_timeout);
        }
        resize_timeout = setTimeout(real_resize, 200);
    });
}
