mirror of
https://github.com/LukeHagar/usage-statistics.git
synced 2025-12-06 04:21:55 +00:00
chore: streamline release workflows by adding version bump and built files commit step; remove redundant asset upload steps
This commit is contained in:
388
dist/summaries/pypi.js
vendored
Normal file
388
dist/summaries/pypi.js
vendored
Normal file
@@ -0,0 +1,388 @@
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import { Canvas } from 'skia-canvas';
|
||||
Chart.register(...registerables);
|
||||
export function formatPypiSummary(summary, platformMetrics) {
|
||||
summary += `| Package | Total Downloads | Monthly Downloads | Weekly Downloads | Daily Downloads | Version |\n`;
|
||||
summary += `| --- | --- | --- | --- | --- | --- |\n`;
|
||||
for (const metric of platformMetrics) {
|
||||
summary += `| ${metric.name} | ${metric.metrics?.downloadsTotal?.toLocaleString() || 0} | ${metric.metrics?.downloadsMonthly?.toLocaleString() || 0} | ${metric.metrics?.downloadsWeekly?.toLocaleString() || 0} | ${metric.metrics?.downloadsDaily?.toLocaleString() || 0} | ${metric.metrics?.version || 'N/A'} |\n`;
|
||||
}
|
||||
summary += `| **Total** | **${platformMetrics.reduce((sum, m) => sum + (m.metrics?.downloadsTotal || 0), 0).toLocaleString()}** | **${platformMetrics.reduce((sum, m) => sum + (m.metrics?.downloadsMonthly || 0), 0).toLocaleString()}** | **${platformMetrics.reduce((sum, m) => sum + (m.metrics?.downloadsWeekly || 0), 0).toLocaleString()}** | **${platformMetrics.reduce((sum, m) => sum + (m.metrics?.downloadsDaily || 0), 0).toLocaleString()}** | | |\n`;
|
||||
return summary;
|
||||
}
|
||||
function toIsoMonth(dateStr) {
|
||||
// input expected YYYY-MM-DD; fallback to Date parse if needed
|
||||
const iso = dateStr?.slice(0, 7);
|
||||
if (iso && /\d{4}-\d{2}/.test(iso))
|
||||
return iso;
|
||||
const d = new Date(dateStr);
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
return `${y}-${m}`;
|
||||
}
|
||||
function displayMonthLabel(isoMonth) {
|
||||
const [y, m] = isoMonth.split('-');
|
||||
const d = new Date(Number(y), Number(m) - 1, 1);
|
||||
return d.toLocaleDateString('en-US', { month: 'short', year: '2-digit' });
|
||||
}
|
||||
function aggregateMonthlyTotals(points) {
|
||||
const totals = {};
|
||||
for (const p of points) {
|
||||
const iso = toIsoMonth(p.date);
|
||||
totals[iso] = (totals[iso] || 0) + p.downloads;
|
||||
}
|
||||
const labelsIso = Object.keys(totals).sort();
|
||||
const labels = labelsIso.map(displayMonthLabel);
|
||||
const data = labelsIso.map(l => totals[l]);
|
||||
return { labels, data };
|
||||
}
|
||||
function aggregateMonthlyByCategory(points) {
|
||||
const labelIsoSet = new Set();
|
||||
const categoryMap = {};
|
||||
for (const p of points) {
|
||||
const iso = toIsoMonth(p.date);
|
||||
labelIsoSet.add(iso);
|
||||
if (!categoryMap[p.category])
|
||||
categoryMap[p.category] = {};
|
||||
categoryMap[p.category][iso] = (categoryMap[p.category][iso] || 0) + p.downloads;
|
||||
}
|
||||
const labelsIso = Array.from(labelIsoSet).sort();
|
||||
const labels = labelsIso.map(displayMonthLabel);
|
||||
return { labelsIso, labels, categoryMap };
|
||||
}
|
||||
async function createOverallDownloadsChart(metric, outputPath) {
|
||||
// Prefer server-prepared chart JSON if present
|
||||
const server = metric.metrics?.overallChart;
|
||||
let labels;
|
||||
let datasets;
|
||||
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
|
||||
labels = server.labels;
|
||||
const colorFor = (label, idx) => {
|
||||
const l = (label || '').toLowerCase();
|
||||
if (l.includes('without'))
|
||||
return { stroke: '#2563eb', fill: '#2563eb33' }; // blue
|
||||
if (l.includes('with'))
|
||||
return { stroke: '#64748b', fill: '#64748b33' }; // slate
|
||||
const palette = ['#2563eb', '#16a34a', '#f59e0b', '#ef4444', '#7c3aed'];
|
||||
const i = idx ?? 0;
|
||||
return { stroke: palette[i % palette.length], fill: palette[i % palette.length] + '33' };
|
||||
};
|
||||
datasets = server.datasets.map((ds, i) => {
|
||||
const c = colorFor(ds.label, i);
|
||||
return {
|
||||
...ds,
|
||||
borderColor: c.stroke,
|
||||
backgroundColor: c.fill,
|
||||
borderWidth: 3,
|
||||
fill: true,
|
||||
tension: 0.1,
|
||||
};
|
||||
});
|
||||
}
|
||||
else {
|
||||
const series = metric.metrics?.overallSeries || [];
|
||||
const agg = aggregateMonthlyTotals(series.map(p => ({ date: p.date, downloads: p.downloads })));
|
||||
labels = agg.labels;
|
||||
datasets = [{
|
||||
label: `${metric.name} downloads per month`,
|
||||
data: agg.data,
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 3,
|
||||
fill: true,
|
||||
tension: 0.1
|
||||
}];
|
||||
}
|
||||
const canvas = new Canvas(1000, 800);
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: { labels, datasets },
|
||||
options: {
|
||||
plugins: {
|
||||
legend: { display: true, position: 'bottom' },
|
||||
title: { display: true, text: `${metric.name} overall downloads` }
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: 'Month' } },
|
||||
y: { title: { display: true, text: 'Downloads' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
|
||||
const svgPath = `${outputPath}/${metric.name}-pypi-overall.svg`;
|
||||
writeFileSync(svgPath, svgBuffer);
|
||||
chart.destroy();
|
||||
return svgPath;
|
||||
}
|
||||
// Time-series: Python major over time (line)
|
||||
async function createPythonMajorChart(metric, outputPath) {
|
||||
// Prefer server chart JSON if present
|
||||
const server = metric.metrics?.pythonMajorChart;
|
||||
let labels;
|
||||
let datasets;
|
||||
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
|
||||
const palette = ['#2563eb', '#16a34a', '#f59e0b', '#ef4444', '#7c3aed', '#0891b2', '#dc2626', '#0ea5e9'];
|
||||
labels = server.labels;
|
||||
datasets = server.datasets
|
||||
.filter(ds => !/unknown/i.test(ds.label))
|
||||
.map((ds, idx) => ({
|
||||
...ds,
|
||||
borderColor: palette[idx % palette.length],
|
||||
backgroundColor: palette[idx % palette.length] + '33',
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
}));
|
||||
}
|
||||
else {
|
||||
const points = metric.metrics?.pythonMajorSeries || [];
|
||||
const { labelsIso, labels: lbls, categoryMap } = aggregateMonthlyByCategory(points);
|
||||
labels = lbls;
|
||||
const sortedCategories = Object.keys(categoryMap).filter(k => !/unknown/i.test(k)).sort((a, b) => Number(a) - Number(b));
|
||||
const palette = ['#2563eb', '#16a34a', '#f59e0b', '#ef4444', '#7c3aed', '#0891b2', '#dc2626', '#0ea5e9'];
|
||||
datasets = sortedCategories.map((category, idx) => ({
|
||||
label: `Python ${category}`,
|
||||
data: labelsIso.map(l => categoryMap[category][l] || 0),
|
||||
borderColor: palette[idx % palette.length],
|
||||
backgroundColor: palette[idx % palette.length] + '33',
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
}));
|
||||
}
|
||||
const canvas = new Canvas(1000, 800);
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: { labels, datasets },
|
||||
options: {
|
||||
plugins: {
|
||||
legend: { position: 'bottom' },
|
||||
title: { display: true, text: `${metric.name} downloads by Python major version` }
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: 'Month' } },
|
||||
y: { title: { display: true, text: 'Downloads' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
|
||||
const svgPath = `${outputPath}/${metric.name}-pypi-python-major.svg`;
|
||||
writeFileSync(svgPath, svgBuffer);
|
||||
chart.destroy();
|
||||
return svgPath;
|
||||
}
|
||||
// Time-series: Python minor over time (line)
|
||||
async function createPythonMinorChart(metric, outputPath) {
|
||||
// Prefer server chart JSON if present
|
||||
const server = metric.metrics?.pythonMinorChart;
|
||||
let labels;
|
||||
let datasets;
|
||||
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
|
||||
const palette = ['#1d4ed8', '#059669', '#d97706', '#dc2626', '#6d28d9', '#0e7490', '#b91c1c', '#0284c7'];
|
||||
labels = server.labels;
|
||||
datasets = server.datasets
|
||||
.filter(ds => !/unknown/i.test(ds.label))
|
||||
.map((ds, idx) => ({
|
||||
...ds,
|
||||
borderColor: palette[idx % palette.length],
|
||||
backgroundColor: palette[idx % palette.length] + '33',
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
}));
|
||||
}
|
||||
else {
|
||||
const points = metric.metrics?.pythonMinorSeries || [];
|
||||
const { labelsIso, labels: lbls, categoryMap } = aggregateMonthlyByCategory(points);
|
||||
labels = lbls;
|
||||
const sortedCategories = Object.keys(categoryMap).filter(k => !/unknown/i.test(k)).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
const palette = ['#1d4ed8', '#059669', '#d97706', '#dc2626', '#6d28d9', '#0e7490', '#b91c1c', '#0284c7'];
|
||||
datasets = sortedCategories.map((category, idx) => ({
|
||||
label: `Python ${category}`,
|
||||
data: labelsIso.map(l => categoryMap[category][l] || 0),
|
||||
borderColor: palette[idx % palette.length],
|
||||
backgroundColor: palette[idx % palette.length] + '33',
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
}));
|
||||
}
|
||||
const canvas = new Canvas(1000, 800);
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: { labels, datasets },
|
||||
options: {
|
||||
plugins: {
|
||||
legend: { position: 'bottom' },
|
||||
title: { display: true, text: `${metric.name} downloads by Python minor version` }
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: 'Month' } },
|
||||
y: { title: { display: true, text: 'Downloads' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
|
||||
const svgPath = `${outputPath}/${metric.name}-pypi-python-minor.svg`;
|
||||
writeFileSync(svgPath, svgBuffer);
|
||||
chart.destroy();
|
||||
return svgPath;
|
||||
}
|
||||
// Time-series: Installer over time (line) - prefer server JSON
|
||||
async function createInstallerChart(metric, outputPath) {
|
||||
const server = metric.metrics?.installerChart;
|
||||
let labels;
|
||||
let datasets;
|
||||
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
|
||||
const palette = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#a78bfa', '#22d3ee'];
|
||||
labels = server.labels;
|
||||
datasets = server.datasets.map((ds, idx) => ({
|
||||
...ds,
|
||||
borderColor: palette[idx % palette.length],
|
||||
backgroundColor: palette[idx % palette.length] + '33',
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
}));
|
||||
}
|
||||
else {
|
||||
const points = metric.metrics?.installerSeries || [];
|
||||
const { labelsIso, labels: lbls, categoryMap } = aggregateMonthlyByCategory(points);
|
||||
labels = lbls;
|
||||
const categories = Object.keys(categoryMap);
|
||||
const palette = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#a78bfa', '#22d3ee'];
|
||||
datasets = categories.map((category, idx) => ({
|
||||
label: category,
|
||||
data: labelsIso.map(l => categoryMap[category][l] || 0),
|
||||
borderColor: palette[idx % palette.length],
|
||||
backgroundColor: palette[idx % palette.length] + '33',
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
}));
|
||||
}
|
||||
const canvas = new Canvas(1000, 800);
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: { labels, datasets },
|
||||
options: {
|
||||
plugins: {
|
||||
legend: { position: 'bottom' },
|
||||
title: { display: true, text: `${metric.name} downloads by installer` }
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: 'Month' } },
|
||||
y: { title: { display: true, text: 'Downloads' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
|
||||
const svgPath = `${outputPath}/${metric.name}-pypi-installer.svg`;
|
||||
writeFileSync(svgPath, svgBuffer);
|
||||
chart.destroy();
|
||||
return svgPath;
|
||||
}
|
||||
// Time-series: System over time (line) - prefer server JSON
|
||||
async function createSystemChart(metric, outputPath) {
|
||||
const server = metric.metrics?.systemChart;
|
||||
let labels;
|
||||
let datasets;
|
||||
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
|
||||
const palette = ['#0ea5e9', '#22c55e', '#f97316', '#e11d48', '#8b5cf6', '#06b6d4'];
|
||||
labels = server.labels;
|
||||
datasets = server.datasets.map((ds, idx) => ({
|
||||
...ds,
|
||||
borderColor: palette[idx % palette.length],
|
||||
backgroundColor: palette[idx % palette.length] + '33',
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
}));
|
||||
}
|
||||
else {
|
||||
const points = metric.metrics?.systemSeries || [];
|
||||
const { labelsIso, labels: lbls, categoryMap } = aggregateMonthlyByCategory(points);
|
||||
labels = lbls;
|
||||
const sortedCategories = Object.keys(categoryMap).sort();
|
||||
const palette = ['#0ea5e9', '#22c55e', '#f97316', '#e11d48', '#8b5cf6', '#06b6d4'];
|
||||
datasets = sortedCategories.map((category, idx) => ({
|
||||
label: category,
|
||||
data: labelsIso.map(l => categoryMap[category][l] || 0),
|
||||
borderColor: palette[idx % palette.length],
|
||||
backgroundColor: palette[idx % palette.length] + '33',
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
}));
|
||||
}
|
||||
const canvas = new Canvas(1000, 800);
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: { labels, datasets },
|
||||
options: {
|
||||
plugins: {
|
||||
legend: { position: 'bottom' },
|
||||
title: { display: true, text: `${metric.name} downloads by OS` }
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: 'Month' } },
|
||||
y: { title: { display: true, text: 'Downloads' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
|
||||
const svgPath = `${outputPath}/${metric.name}-pypi-system.svg`;
|
||||
writeFileSync(svgPath, svgBuffer);
|
||||
chart.destroy();
|
||||
return svgPath;
|
||||
}
|
||||
// Removed static bar chart generators per request
|
||||
async function createPypiCharts(metrics, basePath) {
|
||||
const outputPaths = [];
|
||||
for (const metric of metrics) {
|
||||
const packagePath = `${basePath}`;
|
||||
mkdirSync(packagePath, { recursive: true });
|
||||
const overall = await createOverallDownloadsChart(metric, packagePath);
|
||||
outputPaths.push(overall);
|
||||
const pythonMajor = await createPythonMajorChart(metric, packagePath);
|
||||
outputPaths.push(pythonMajor);
|
||||
const pythonMinor = await createPythonMinorChart(metric, packagePath);
|
||||
outputPaths.push(pythonMinor);
|
||||
const installer = await createInstallerChart(metric, packagePath);
|
||||
outputPaths.push(installer);
|
||||
const system = await createSystemChart(metric, packagePath);
|
||||
outputPaths.push(system);
|
||||
// static bar charts removed
|
||||
}
|
||||
return outputPaths;
|
||||
}
|
||||
export function addPypiDetails(summary, metrics) {
|
||||
summary += `#### Package Details:\n\n`;
|
||||
for (const metric of metrics) {
|
||||
summary += `**${metric.name}**:\n`;
|
||||
summary += `- Version: ${metric.metrics?.version || 'N/A'}\n`;
|
||||
if (metric.metrics?.latestReleaseDate)
|
||||
summary += `- Released: ${metric.metrics.latestReleaseDate}\n`;
|
||||
if (metric.metrics?.popularSystem)
|
||||
summary += `- Popular system: ${metric.metrics.popularSystem}\n`;
|
||||
if (metric.metrics?.popularInstaller)
|
||||
summary += `- Popular installer: ${metric.metrics.popularInstaller}\n`;
|
||||
summary += `- Releases: ${metric.metrics?.releases || 0}\n`;
|
||||
if (metric.metrics?.systemBreakdown) {
|
||||
summary += `- OS Usage Breakdown \n`;
|
||||
for (const [key, value] of Object.entries(metric.metrics?.systemBreakdown)) {
|
||||
summary += ` - ${key}: ${value}\n`;
|
||||
}
|
||||
}
|
||||
if (metric.metrics?.pythonVersionBreakdown) {
|
||||
summary += `- Python Version Breakdown \n`;
|
||||
for (const [key, value] of Object.entries(metric.metrics?.pythonVersionBreakdown)) {
|
||||
summary += ` - ${key}: ${value}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
export async function addPypiCharts(summary, platformMetrics) {
|
||||
const outputPath = './charts/pypi';
|
||||
mkdirSync(outputPath, { recursive: true });
|
||||
summary += `\n\n`;
|
||||
const svgPaths = await createPypiCharts(platformMetrics, outputPath);
|
||||
for (const p of svgPaths) {
|
||||
summary += `\n`;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
Reference in New Issue
Block a user