337 lines
10 KiB
JavaScript
337 lines
10 KiB
JavaScript
(function () {
|
|
const colours = [
|
|
'#D32F2F',
|
|
'#1976D2',
|
|
'#FBC02D',
|
|
'#388E3C',
|
|
'#7B1FA2',
|
|
'#F57C00',
|
|
'#5D4037',
|
|
'#455A64',
|
|
'#00796B',
|
|
'#E64A19',
|
|
'#C2185B',
|
|
'#512DA8',
|
|
'#0097A7',
|
|
];
|
|
|
|
const options = {
|
|
plugins: {
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function (context) {
|
|
let total = context.dataset.data.reduce((total, next) => total + next);
|
|
let percentage = (context.raw / total * 100).toFixed(2);
|
|
|
|
return `${context.label} (${context.raw}, ${percentage}%)`;
|
|
}
|
|
}
|
|
},
|
|
legend: {
|
|
display: false,
|
|
},
|
|
},
|
|
};
|
|
|
|
function combineUnderMedian(data) {
|
|
let midpoint = Math.trunc(data.length / 2);
|
|
let wasOdd = data.length % 2 === 1;
|
|
let median;
|
|
if (wasOdd) {
|
|
median = (data[midpoint].y + data[midpoint + 1].y) / 2;
|
|
} else {
|
|
median = data[midpoint].y;
|
|
}
|
|
|
|
let newData = [];
|
|
let other = {
|
|
x: 'Other',
|
|
y: 0,
|
|
};
|
|
|
|
for (let entry of data) {
|
|
if (entry.y <= median) {
|
|
other.y += 1;
|
|
continue;
|
|
}
|
|
|
|
newData.push(entry);
|
|
}
|
|
|
|
newData.push(other);
|
|
|
|
return newData;
|
|
}
|
|
|
|
function combineTopN(data, n) {
|
|
let newData = [];
|
|
|
|
let other = {
|
|
x: 'Other',
|
|
y: 0,
|
|
};
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
if (i < n) {
|
|
newData.push(data[i]);
|
|
continue;
|
|
}
|
|
|
|
other.y += data[i].y;
|
|
}
|
|
|
|
newData.push(other);
|
|
|
|
return newData;
|
|
}
|
|
|
|
function extractData(tableId, extractor = null) {
|
|
if (extractor === null) {
|
|
extractor = (cols) => {
|
|
return {
|
|
label: cols[0].innerHTML,
|
|
value: Number(cols[1].innerHTML),
|
|
};
|
|
};
|
|
}
|
|
|
|
let table = document.getElementById(tableId);
|
|
let data = [];
|
|
for (let row of table.querySelectorAll('tbody > tr')) {
|
|
let cols = row.querySelectorAll('td');
|
|
data.push(extractor(cols));
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
function wrap(text) {
|
|
text.each(function () {
|
|
var text = d3.select(this),
|
|
words = text.text().split(/\s+/).reverse(),
|
|
word,
|
|
line = [],
|
|
lineNumber = 0,
|
|
lineHeight = 1.1, // ems
|
|
x = text.attr("x"),
|
|
y = text.attr("y"),
|
|
dy = 0, //parseFloat(text.attr("dy")),
|
|
tspan = text.text(null)
|
|
.append("tspan")
|
|
.attr("x", x)
|
|
.attr("y", y)
|
|
.attr("dy", dy + "em");
|
|
while (word = words.pop()) {
|
|
line.push(word);
|
|
tspan.text(line.join(" "));
|
|
if (tspan.node().getComputedTextLength() > text.attr('width') - 6) {
|
|
line.pop();
|
|
tspan.text(line.join(" "));
|
|
line = [word];
|
|
tspan = text.append("tspan")
|
|
.attr("x", x)
|
|
.attr("y", y)
|
|
.attr("dy", ++lineNumber * lineHeight + dy + "em")
|
|
.text(word);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function makeTreeMap(data, graphId, opts = {}) {
|
|
let drawLabels = opts.drawLabels === undefined ? true : opts.drawLabels;
|
|
let grouped = opts.grouped === undefined ? false : opts.grouped;
|
|
|
|
let elem = document.getElementById(graphId);
|
|
const [width, height] = [elem.offsetWidth, elem.offsetHeight];
|
|
|
|
let svg = d3.select(`#${graphId}`)
|
|
.append('svg')
|
|
.attr('viewBox', `0 0 ${width} ${height}`);
|
|
|
|
if (grouped) {
|
|
d3.treemap()
|
|
.size([width, height])
|
|
.paddingInner(2)
|
|
.paddingTop(4)
|
|
.paddingRight(4)
|
|
.paddingLeft(4)
|
|
.paddingBottom(4)
|
|
(data);
|
|
} else {
|
|
d3.treemap()
|
|
.size([width, height])
|
|
.paddingInner(2)
|
|
(data);
|
|
}
|
|
|
|
let a = 0;
|
|
let group = svg.selectAll('g')
|
|
.data(data.leaves())
|
|
.enter()
|
|
.append('g');
|
|
|
|
let title = (d) => `${d.data.label} (${(d.data.value / d.parent.value * 100).toFixed(2)}% - ${d.data.value.toLocaleString()})`;
|
|
|
|
group
|
|
.append('title')
|
|
.text(title);
|
|
|
|
group
|
|
.append('rect')
|
|
.attr('x', d => d.x0)
|
|
.attr('y', d => d.y0)
|
|
.attr('width', d => d.x1 - d.x0)
|
|
.attr('height', d => d.y1 - d.y0)
|
|
.style('fill', d => {
|
|
let colour = colours[a];
|
|
a += 1;
|
|
a %= colours.length;
|
|
return colour;
|
|
});
|
|
|
|
if (drawLabels) {
|
|
svg.selectAll('text')
|
|
.data(data.leaves().filter(d => d.data.value / d.parent.value >= .05 ))
|
|
.enter()
|
|
.append('text')
|
|
.attr('x', d => d.x0 + 5)
|
|
.attr('y', d => d.y0 + 20)
|
|
.attr('width', d => d.x1 - d.x0)
|
|
.text(title)
|
|
.attr('font-size', '1em')
|
|
.attr('fill', 'white')
|
|
.call(wrap);
|
|
}
|
|
|
|
if (grouped) {
|
|
svg.selectAll('borders')
|
|
.data(data.descendants().filter(d => d.depth === 1))
|
|
.enter()
|
|
.append('rect')
|
|
.attr('x', d => d.x0)
|
|
.attr('y', d => d.y0)
|
|
.attr('width', d => d.x1 - d.x0)
|
|
.attr('height', d => d.y1 - d.y0)
|
|
.attr('fill', 'none')
|
|
.attr('stroke', '#374956');
|
|
|
|
svg.selectAll('titles')
|
|
.data(data.descendants().filter(d => d.depth === 1))
|
|
.enter()
|
|
.append('text')
|
|
.attr('x', d => d.x1 - 2)
|
|
.attr('y', d => d.y1 - 2)
|
|
.attr('text-anchor', 'end')
|
|
.style('transform', d => {
|
|
if ((d.x1 - d.x0) < (d.y1 - d.y0)) {
|
|
return 'rotate(90deg)';
|
|
}
|
|
|
|
return null;
|
|
})
|
|
.style('transform-box', 'fill-box')
|
|
.style('transform-origin', '95%')
|
|
.text(d => d.data[0])
|
|
.attr("font-size", d => {
|
|
if (d === data) {
|
|
return "1em";
|
|
}
|
|
let width = d.x1 - d.x0, height = d.y1 - d.y0;
|
|
return Math.max(Math.min(width/5, height/2, Math.sqrt((width*width + height*height))/10), 9)
|
|
})
|
|
.attr('fill', 'white');
|
|
}
|
|
}
|
|
|
|
function makeBarPlot(data, graphId) {
|
|
let elem = document.getElementById(graphId);
|
|
const [marginLeft, marginRight, marginTop, marginBottom] = [100, 0, 16, 50];
|
|
const [width, height] = [elem.offsetWidth - marginLeft - marginRight, elem.offsetHeight - marginTop - marginBottom];
|
|
|
|
let svg = d3.select(`#${graphId}`)
|
|
.append('svg')
|
|
.attr('viewBox', `0 0 ${width + marginLeft + marginRight} ${height + marginTop + marginBottom}`)
|
|
.append('g')
|
|
.attr('transform', `translate(${marginLeft}, ${marginTop})`);
|
|
|
|
let x = d3.scaleBand()
|
|
.range([0, width])
|
|
.domain(data.map(d => d.label))
|
|
.padding(0.2);
|
|
svg.append('g')
|
|
.attr('transform', `translate(0, ${height})`)
|
|
.call(d3.axisBottom(x))
|
|
.attr('font-size', '1em')
|
|
.selectAll('text')
|
|
.style('text-anchor', 'middle')
|
|
.attr('font-size', '1em');
|
|
|
|
let y = d3.scaleLinear()
|
|
.domain([0, data.map(d => d.value).reduce((max, a) => Math.max(max, a))])
|
|
.range([height, 0]);
|
|
svg.append('g')
|
|
.call(d3.axisLeft(y))
|
|
.attr('font-size', '1em')
|
|
.selectAll('text')
|
|
.attr('font-size', '1em');
|
|
|
|
let sum = data.map(d => d.value).reduce((total, a) => total + a);
|
|
let colourIdx = 0;
|
|
let group = svg.selectAll('mybar')
|
|
.data(data)
|
|
.enter()
|
|
.append('g');
|
|
group.append('title')
|
|
.text(d => `${d.value} (${(d.value / sum * 100).toFixed(2)}%)`);
|
|
group.append('rect')
|
|
.attr('x', d => x(d.label))
|
|
.attr('y', d => y(d.value))
|
|
.attr('width', x.bandwidth())
|
|
.attr('height', d => height - y(d.value))
|
|
.attr('fill', d => {
|
|
let colour = colours[colourIdx];
|
|
colourIdx += 1;
|
|
colourIdx %= colours.length;
|
|
return colour;
|
|
});
|
|
}
|
|
|
|
makeTreeMap(
|
|
d3.hierarchy({
|
|
children: extractData('duties'),
|
|
}).sum(d => d.value),
|
|
'dutiesChart',
|
|
);
|
|
makeTreeMap(
|
|
d3.hierarchy(
|
|
d3.group(
|
|
extractData(
|
|
'hosts',
|
|
(cols) => {
|
|
return {
|
|
label: cols[1].innerHTML,
|
|
world: cols[0].innerHTML,
|
|
value: Number(cols[2].innerHTML),
|
|
};
|
|
},
|
|
),
|
|
d => d.world,
|
|
)
|
|
).sum(d => d.value),
|
|
'hostsChart',
|
|
{
|
|
drawLabels: false,
|
|
grouped: true,
|
|
},
|
|
);
|
|
makeBarPlot(
|
|
extractData('hours'),
|
|
'hoursChart',
|
|
);
|
|
makeBarPlot(
|
|
extractData('days'),
|
|
'daysChart',
|
|
);
|
|
})();
|