remote-party-finder/server/assets/stats.js

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',
);
})();