mirror of
https://github.com/PrivateCaptcha/PrivateCaptcha.git
synced 2026-02-09 15:28:49 -06:00
275 lines
12 KiB
HTML
275 lines
12 KiB
HTML
<script type="text/javascript">
|
|
if (typeof ChartComponent === 'undefined') {
|
|
class ChartComponent {
|
|
constructor(usageLimit) {
|
|
this.usageLimit = usageLimit;
|
|
this.isLoading = false;
|
|
this.chart = null;
|
|
|
|
// Constants
|
|
this.yTicksCount = 7;
|
|
this.maxBarWidth = 7;
|
|
|
|
this.backgroundColor = '#e4e4e7';
|
|
this.requestedColor = '#188B8B'; // pcteal-600
|
|
this.verifiedColor = '#F45D5D'; // pcred-300
|
|
this.grayColor = "#6b7280";
|
|
|
|
this.monthlyFormat = d3.timeFormat("%b");
|
|
}
|
|
|
|
monthlyTicks(date, i) {
|
|
const day = date.getDate();
|
|
const month = date.getMonth();
|
|
if (day === 1 && month === 0) {
|
|
return date.getFullYear();
|
|
} else {
|
|
return this.monthlyFormat(date);
|
|
}
|
|
}
|
|
|
|
yTickFormat(d) {
|
|
if (d >= 1_000_000_000) return (d / 1_000_000_000) + 'B';
|
|
if (d >= 1_000_000) return (d / 1_000_000) + 'M';
|
|
if (d >= 1_000) return (d / 1_000) + 'K';
|
|
return d;
|
|
}
|
|
|
|
drawNoData(element, xTickFunction, periodLengthDays) {
|
|
const margin = { top: 20, right: 30, bottom: 60, left: 30 };
|
|
const rect = element.getBoundingClientRect();
|
|
const width = rect.width - margin.left - margin.right;
|
|
const height = rect.height - margin.top - margin.bottom;
|
|
|
|
const d3Selection = d3.select(element);
|
|
d3Selection.selectAll('svg').remove();
|
|
|
|
const svg = d3Selection.append('svg')
|
|
.attr('width', width + margin.left + margin.right)
|
|
.attr('height', height + margin.top + margin.bottom);
|
|
|
|
const chartElement = svg.append('g')
|
|
.attr('class', 'charts')
|
|
.attr("transform", `translate(${margin.left},${margin.top})`);
|
|
|
|
const x = d3.scaleTime().range([0, width]);
|
|
x.domain([d3.timeDay.offset(new Date(), -periodLengthDays), new Date()]);
|
|
|
|
const xTickValues = x.domain().filter((_, i) => !(i % 2));
|
|
|
|
const xAxis = d3.axisBottom(x)
|
|
.tickValues(xTickValues)
|
|
.tickFormat(xTickFunction);
|
|
|
|
const y = d3.scaleLinear().range([height, 0]);
|
|
|
|
chartElement.append('g')
|
|
.attr('transform', `translate(0,${height})`)
|
|
.call(xAxis)
|
|
.style("color", this.backgroundColor)
|
|
.style("stroke-width", 2)
|
|
.selectAll("text")
|
|
.style("text-anchor", "end")
|
|
.style("color", "#000")
|
|
.attr("dx", "-.8em")
|
|
.attr("dy", "-.55em")
|
|
.attr("transform", "rotate(-90)");
|
|
|
|
chartElement.append('g')
|
|
.call(d3.axisLeft(y).ticks(this.yTicksCount).tickSize(-width).tickFormat(''))
|
|
.style("color", this.backgroundColor)
|
|
.selectAll(".domain").remove();
|
|
|
|
chartElement.append("g")
|
|
.attr("transform", `translate(${width / 2 - 80},${height / 2 + 5})`)
|
|
.append("text")
|
|
.text("No data available")
|
|
.style("font-size", "20px")
|
|
.style("fill", this.grayColor);
|
|
}
|
|
|
|
setBarAttributes(bars, x, y, height, color) {
|
|
bars.enter().append("rect")
|
|
.attr("class", "bar")
|
|
.attr("x", d => x(d.x) + x.bandwidth() / 2)
|
|
.attr("width", () => Math.min(x.bandwidth(), this.maxBarWidth))
|
|
.attr("y", d => y(d.y))
|
|
.attr("height", d => (d.y > 1e-6 ? height - y(d.y) : 0))
|
|
.attr("rx", 3)
|
|
.attr("ry", 3)
|
|
.attr("fill", color)
|
|
.attr("opacity", 1)
|
|
.on("mouseover", function () { d3.select(this).attr("opacity", 0.8); })
|
|
.on("mouseout", function () { d3.select(this).attr("opacity", 1); })
|
|
.append("title").text(d => d.y);
|
|
}
|
|
|
|
setLegend(legend, text, color) {
|
|
legend.append("circle")
|
|
.attr("cx", -16)
|
|
.attr("cy", 0)
|
|
.attr("r", 6)
|
|
.style("fill", color);
|
|
|
|
legend.append("text")
|
|
.attr("x", 0)
|
|
.attr("y", 0)
|
|
.attr("dy", ".35em")
|
|
.text(text)
|
|
.attr("class", "textselected")
|
|
.style("text-anchor", "start")
|
|
.style("font-size", "14px");
|
|
}
|
|
|
|
setChartData(element, data, xTickFormat) {
|
|
const requested = data.data;
|
|
requested.forEach(d => { d.x = new Date(d.x * 1000); });
|
|
|
|
const legendHeight = 50;
|
|
const margin = { top: 20, right: 30, bottom: 30, left: 30 };
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
const width = rect.width - margin.left - margin.right;
|
|
const height = rect.height - legendHeight - margin.top - margin.bottom;
|
|
|
|
const d3Selection = d3.select(element);
|
|
d3Selection.selectAll('svg').remove();
|
|
|
|
const svg = d3Selection.append('svg')
|
|
.attr('width', width + margin.left + margin.right)
|
|
.attr('height', height + legendHeight + margin.top + margin.bottom);
|
|
|
|
const chartElement = svg.append('g')
|
|
.attr('class', 'charts')
|
|
.attr("transform", `translate(${margin.left},${margin.top})`);
|
|
|
|
const x = d3.scaleBand().rangeRound([0, width]).padding(0.1);
|
|
const y = d3.scaleLinear().range([height, 0]);
|
|
|
|
x.domain(requested.map(d => d.x));
|
|
y.domain([0, d3.max(requested, d => d.y) * 1.2]);
|
|
|
|
const xTickValues = x.domain();
|
|
|
|
const xAxis = d3.axisBottom(x)
|
|
.tickValues(xTickValues)
|
|
.tickFormat(xTickFormat);
|
|
|
|
const yAxis = d3.axisLeft(y)
|
|
.ticks(this.yTicksCount)
|
|
.tickFormat(this.yTickFormat)
|
|
.tickPadding(5);
|
|
|
|
const yGrid = chartElement.append("g")
|
|
.attr("class", "grid")
|
|
.call(yAxis.tickSize(-width))
|
|
.style("color", this.backgroundColor);
|
|
|
|
yGrid.selectAll("text").style("color", this.grayColor);
|
|
yGrid.selectAll(".domain").remove();
|
|
|
|
if (this.usageLimit) {
|
|
const closeToLimit = requested.some(d => (1.1 * d.y) >= this.usageLimit);
|
|
if (closeToLimit) {
|
|
svg.append('line')
|
|
.attr('x1', 0)
|
|
.attr('x2', width)
|
|
.attr('y1', y(this.usageLimit))
|
|
.attr('y2', y(this.usageLimit))
|
|
.attr("transform", `translate(${margin.left},${margin.top})`)
|
|
.attr('stroke', this.verifiedColor)
|
|
.attr('stroke-width', 1)
|
|
.attr('stroke-dasharray', '5,5');
|
|
}
|
|
}
|
|
|
|
const barsRequested = chartElement.selectAll("bar-requested").data(requested);
|
|
this.setBarAttributes(barsRequested, x, y, height, this.requestedColor);
|
|
|
|
chartElement.append("g")
|
|
.attr("class", "x axis")
|
|
.attr("transform", `translate(0,${height})`)
|
|
.call(xAxis)
|
|
.style("color", this.backgroundColor)
|
|
.style("stroke-width", 2)
|
|
.selectAll("text")
|
|
.style("text-anchor", "end")
|
|
.style("color", "#000")
|
|
.attr("dx", "-.8em")
|
|
.attr("dy", "-.55em")
|
|
.attr("transform", "rotate(-90)");
|
|
|
|
const legendParent = chartElement.append("g")
|
|
.attr("class", "legendParent")
|
|
.attr("transform", `translate(${width / 2},${height + 30 + legendHeight / 2})`);
|
|
|
|
const legend1 = legendParent.append("g")
|
|
.attr("class", "legend-requested");
|
|
|
|
this.setLegend(legend1, 'Requests', this.requestedColor);
|
|
}
|
|
|
|
async fetchChartData(spinnerElement, maxRetries = 3, baseDelay = 1000) {
|
|
if (spinnerElement) { spinnerElement.style.display = 'flex'; }
|
|
try {
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
const response = await fetch('{{ partsURL $.Const.UserEndpoint $.Const.Stats | safeJS }}');
|
|
if (response.ok) {
|
|
return await response.json();
|
|
}
|
|
|
|
if (response.status === 429 || response.status === 503) {
|
|
const retryDelay = baseDelay * Math.pow(2, attempt - 1);
|
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
continue;
|
|
}
|
|
|
|
const errorText = await response.text();
|
|
throw new Error(`Request failed (${response.status}): ${errorText || response.statusText}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching chart data:', error);
|
|
return null;
|
|
} finally {
|
|
if (spinnerElement) { spinnerElement.style.display = 'none'; }
|
|
}
|
|
}
|
|
|
|
async updateChart(chartElement, spinnerElement) {
|
|
const response = await this.fetchChartData(spinnerElement);
|
|
if (response && response.data && response.data.length > 0) {
|
|
this.setChartData(chartElement, response, this.monthlyTicks.bind(this));
|
|
|
|
const totalElement = document.getElementById("totalRequests");
|
|
if (totalElement) {
|
|
const sum = response.data.reduce((sum, item) => sum + item.y, 0);
|
|
const formatter = new Intl.NumberFormat('en', {
|
|
notation: 'compact',
|
|
compactDisplay: 'short',
|
|
});
|
|
totalElement.innerHTML = formatter.format(sum);;
|
|
}
|
|
} else {
|
|
this.drawNoData(chartElement, this.monthlyTicks.bind(this), 365);
|
|
totalElement.innerHTML = "N/A";
|
|
}
|
|
}
|
|
|
|
async init(chartElement, spinnerElement) {
|
|
await this.updateChart(chartElement, spinnerElement);
|
|
}
|
|
}
|
|
|
|
window.ChartComponent = ChartComponent;
|
|
}
|
|
|
|
(function(){
|
|
loadScript("{{$.Ctx.CDN}}/portal/js/d3.v7.min.js",
|
|
function() {
|
|
const chart = new ChartComponent({{ if $.Params.Limit }}{{$.Params.Limit}}{{else}}null{{end}}); // Pass usageLimit
|
|
chart.init(document.querySelector('#usage-chart'), document.querySelector('#usage-spinner')); // Chart container element
|
|
});
|
|
})()
|
|
|
|
</script>
|