Initial commit

This commit is contained in:
lowlighter
2020-09-08 18:01:22 +02:00
commit 22d732c3f8
21 changed files with 8567 additions and 0 deletions

99
src/app.mjs Normal file
View File

@@ -0,0 +1,99 @@
//Imports
import express from "express"
import fs from "fs"
import path from "path"
import octokit from "@octokit/graphql"
import cache from "memory-cache"
import ratelimit from "express-rate-limit"
import metrics from "./metrics.mjs"
//Load svg template, style and query
async function load() {
return await Promise.all(["template.svg", "style.css", "query.graphql"].map(async file => `${await fs.promises.readFile(path.join("src", file))}`))
}
//Setup
export default async function setup() {
//Load settings
const settings = JSON.parse((await fs.promises.readFile(path.join("settings.json"))).toString())
console.log(settings)
const {token, maxusers = 0, restricted = [], debug = false, cached = 30*60*1000, port = 3000, ratelimiter = null} = settings
//Load svg template, style and query
let [template, style, query] = await load()
const graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}})
//Setup server
const app = express()
const middlewares = []
//Rate limiter middleware
if (ratelimiter) {
app.set("trust proxy", 1)
middlewares.push(ratelimit({
skip(req, res) { return !!cache.get(req.params.login) },
message:"Too many requests",
...ratelimiter
}))
}
//Cache headers middleware
middlewares.push((req, res, next) => {
res.header("Cache-Control", cached ? `public, max-age=${cached}` : "no-store, no-cache")
next()
})
//Base routes
app.get("/", (req, res) => res.redirect("https://github.com/lowlighter/metrics"))
app.get("/favicon.ico", (req, res) => res.sendStatus(204))
//Metrics
app.get("/:login", ...middlewares, async (req, res) => {
//Request params
const {login} = req.params
if ((restricted.length)&&(!restricted.includes(login)))
return res.sendStatus(403)
//Read cached data if possible
if ((!debug)&&(cached)&&(cache.get(login))) {
res.header("Content-Type", "image/svg+xml")
res.send(cache.get(login))
return
}
//Maximum simultaneous users
if ((maxusers)&&(cache.size()+1 > maxusers))
return res.sendStatus(503)
//Compute rendering
try {
//Render
if (debug)
[template, style, query] = await load()
const rendered = await metrics({login}, {template, style, query, graphql})
//Cache
if ((!debug)&&(cached))
cache.put(login, rendered, cached)
//Send response
res.header("Content-Type", "image/svg+xml")
res.send(rendered)
}
//Internal error
catch (error) {
//Not found user
if ((error instanceof Error)&&(/^user not found$/.test(error.message)))
return res.sendStatus(404)
//General error
console.error(error)
res.sendStatus(500)
}
})
//Listen
app.listen(port, () => console.log([
`Listening on port | ${port}`,
`Debug mode | ${debug}`,
`Restricted to users | ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
`Cached time | ${cached} seconds`,
`Rate limiter | ${ratelimiter ? JSON.stringify(ratelimiter) : "(enabled)"}`,
`Max simultaneous users | ${maxusers ? `${maxusers} users` : "(unrestricted)"}`
].join("\n")))
}

67
src/metrics.mjs Normal file
View File

@@ -0,0 +1,67 @@
//Imports
import imgb64 from "image-to-base64"
//Setup
export default async function metrics({login}, {template, style, query, graphql}) {
//Compute rendering
try {
//Query data from GitHub API
const data = await graphql(query
.replace(/[$]login/, `"${login}"`)
.replace(/[$]calendar.to/, `"${(new Date()).toISOString()}"`)
.replace(/[$]calendar.from/, `"${(new Date(Date.now()-14*24*60*60*1000)).toISOString()}"`)
)
//Init
const languages = {colors:{}, total:0, stats:{}}
const computed = data.computed = {commits:0, languages, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0}}
const avatar = imgb64(data.user.avatarUrl)
//Iterate through user's repositories
for (const repository of data.user.repositories.nodes) {
//Simple properties with totalCount
for (const property of ["watchers", "stargazers", "issues_open", "issues_closed", "pr_open", "pr_merged"])
computed.repositories[property] += repository[property].totalCount
//Forks
computed.repositories.forks += repository.forkCount
//Languages
for (const {size, node:{color, name}} of Object.values(repository.languages.edges)) {
languages.stats[name] = (languages.stats[name] || 0) + size
languages.colors[name] = color || "#ededed"
languages.total += size
}
}
//Compute count for issues and pull requests
for (const property of ["issues", "pr"])
computed.repositories[`${property}_count`] = computed.repositories[`${property}_open`] + computed.repositories[`${property}_${property === "pr" ? "merged" : "closed"}`]
//Compute total commits and sponsorships
computed.commits = data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
computed.sponsorships = data.user.sponsorshipsAsSponsor.totalCount + data.user.sponsorshipsAsMaintainer.totalCount
//Compute registration date
const diff = (Date.now()-(new Date(data.user.createdAt)).getTime())/(365*24*60*60*1000)
const years = Math.floor(diff)
const months = Math.ceil((diff-years)*12)
computed.registration = years ? `${years} year${years > 1 ? "s" : ""} ago` : `${months} month${months > 1 ? "s" : ""} ago`
//Compute languages stats
Object.keys(languages.stats).map(name => languages.stats[name] /= languages.total)
languages.favorites = Object.entries(languages.stats).sort(([an, a], [bn, b]) => b - a).slice(0, 8).map(([name, value]) => ({name, value, color:languages.colors[name], x:0}))
for (let i = 1; i < languages.favorites.length; i++)
languages.favorites[i].x = languages.favorites[i-1].x + languages.favorites[i-1].value
//Compute calendar
computed.calendar = data.user.calendar.contributionCalendar.weeks.flatMap(({contributionDays}) => contributionDays).slice(0, 14).reverse()
//Avatar (base64)
computed.avatar = await avatar || "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
//Eval rendering and return
return eval(`\`${template}\``)
}
//Internal error
catch (error) { throw (((Array.isArray(error.errors))&&(error.errors[0].type === "NOT_FOUND")) ? new Error("user not found") : error) }
}

82
src/query.graphql Normal file
View File

@@ -0,0 +1,82 @@
query Metrics {
user(login: $login) {
name
login
createdAt
avatarUrl
repositories(last: 100, isFork: false, ownerAffiliations: OWNER) {
totalCount
nodes {
watchers {
totalCount
}
stargazers {
totalCount
}
languages(first: 4) {
edges {
size
node {
color
name
}
}
}
issues_open: issues(states: OPEN) {
totalCount
}
issues_closed: issues(states: CLOSED) {
totalCount
}
pr_open: pullRequests(states: OPEN) {
totalCount
}
pr_merged: pullRequests(states: MERGED) {
totalCount
}
forkCount
}
}
packages {
totalCount
}
starredRepositories {
totalCount
}
watching {
totalCount
}
sponsorshipsAsSponsor {
totalCount
}
sponsorshipsAsMaintainer {
totalCount
}
contributionsCollection {
totalRepositoriesWithContributedCommits
totalCommitContributions
restrictedContributionsCount
totalIssueContributions
totalPullRequestContributions
totalPullRequestReviewContributions
}
calendar:contributionsCollection(from: $calendar.from, to: $calendar.to) {
contributionCalendar {
weeks {
contributionDays {
color
}
}
}
}
repositoriesContributedTo {
totalCount
}
followers {
totalCount
}
following {
totalCount
}
}
}

86
src/style.css Normal file
View File

@@ -0,0 +1,86 @@
svg {
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
font-size: 14px;
color: #777777;
}
svg.bar {
margin: 4px 0;
}
h1, h2, h3 {
margin: 8px 0 2px;
padding: 0;
color: #0366d6;
font-weight: normal;
}
h1 {
font-size: 20px;
font-weight: bold;
}
h2 {
font-size: 16px;
}
h2 svg {
fill: #0366d6;
}
h3 {
font-size: 14px;
}
.field {
display: flex;
align-items: center;
margin-bottom: 2px;
}
section > .field {
margin-left: 5px;
margin-right: 5px;
}
.field svg {
margin: 0 8px;
fill: #959da5;
}
.column {
display: flex;
flex-direction: column;
align-items: center;
}
.center {
justify-content: center;
}
.horizontal {
justify-content: space-around;
}
.horizontal-wrap {
flex-wrap: wrap;
}
.horizontal .field {
flex: 1 1 0;
}
.row {
display: flex;
}
.row section {
flex: 1 1 0;
}
.no-wrap {
white-space: nowrap;
}
.avatar {
background-color: #000000;
border-radius: 50%;
margin: 0 6px;
}
.calendar.field {
margin: 4px 0;
margin-left: 7px;
}
.calendar .day {
outline: 1px solid rgba(27,31,35,.04);
outline-offset: -1px;
}
footer {
margin-top: 8px;
text-align: right;
font-size: 8px;
font-style: italic;
opacity: 0.5;
}

203
src/template.svg Normal file
View File

@@ -0,0 +1,203 @@
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="480">
<style>
${style}
</style>
<foreignObject x="0" y="0" width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml">
<section>
<h1 class="field">
<img class="avatar" src="data:image/png;base64,${data.computed.avatar}" width="20" height="20" />
<span>${data.user.name || data.user.login}</span>
</h1>
<div class="row">
<section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.5 4.75a.75.75 0 00-1.5 0v3.5a.75.75 0 00.471.696l2.5 1a.75.75 0 00.557-1.392L8.5 7.742V4.75z"></path></svg>
Joined GitHub ${data.computed.registration}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M5.5 3.5a2 2 0 100 4 2 2 0 000-4zM2 5.5a3.5 3.5 0 115.898 2.549 5.507 5.507 0 013.034 4.084.75.75 0 11-1.482.235 4.001 4.001 0 00-7.9 0 .75.75 0 01-1.482-.236A5.507 5.507 0 013.102 8.05 3.49 3.49 0 012 5.5zM11 4a.75.75 0 100 1.5 1.5 1.5 0 01.666 2.844.75.75 0 00-.416.672v.352a.75.75 0 00.574.73c1.2.289 2.162 1.2 2.522 2.372a.75.75 0 101.434-.44 5.01 5.01 0 00-2.56-3.012A3 3 0 0011 4z"></path></svg>
Followed by ${data.user.followers.totalCount} user${data.user.followers.totalCount > 1 ? "s" : ""}
</div>
</section>
<section>
<div class="field calendar">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${data.computed.calendar.length*15} 11" width="${data.computed.calendar.length*15}" height="16">
<g>
${data.computed.calendar.map(({color}, x) => `
<rect class="day" x="${x*15}" y="0" width="11" height="11" fill="${color}" rx="2" ry="2" />
`).join("")}
</g>
</svg>
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1 2.5A2.5 2.5 0 013.5 0h8.75a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0V1.5h-8a1 1 0 00-1 1v6.708A2.492 2.492 0 013.5 9h3.25a.75.75 0 010 1.5H3.5a1 1 0 100 2h5.75a.75.75 0 010 1.5H3.5A2.5 2.5 0 011 11.5v-9zm13.23 7.79a.75.75 0 001.06-1.06l-2.505-2.505a.75.75 0 00-1.06 0L9.22 9.229a.75.75 0 001.06 1.061l1.225-1.224v6.184a.75.75 0 001.5 0V9.066l1.224 1.224z"></path></svg>
Contributed to ${data.user.repositoriesContributedTo.totalCount} repositor${data.user.repositoriesContributedTo.totalCount > 1 ? "ies" : "y"}
</div>
</section>
</div>
</section>
<div class="row">
<section>
<h2 class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.5 1.75a.75.75 0 00-1.5 0v12.5c0 .414.336.75.75.75h14.5a.75.75 0 000-1.5H1.5V1.75zm14.28 2.53a.75.75 0 00-1.06-1.06L10 7.94 7.53 5.47a.75.75 0 00-1.06 0L3.22 8.72a.75.75 0 001.06 1.06L7 7.06l2.47 2.47a.75.75 0 001.06 0l5.25-5.25z"></path></svg>
Activity
</h2>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M10.5 7.75a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm1.43.75a4.002 4.002 0 01-7.86 0H.75a.75.75 0 110-1.5h3.32a4.001 4.001 0 017.86 0h3.32a.75.75 0 110 1.5h-3.32z"></path></svg>
${data.computed.commits} Commit${data.computed.commits > 1 ? "s" : ""}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M2.5 1.75a.25.25 0 01.25-.25h8.5a.25.25 0 01.25.25v7.736a.75.75 0 101.5 0V1.75A1.75 1.75 0 0011.25 0h-8.5A1.75 1.75 0 001 1.75v11.5c0 .966.784 1.75 1.75 1.75h3.17a.75.75 0 000-1.5H2.75a.25.25 0 01-.25-.25V1.75zM4.75 4a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5zM4 7.75A.75.75 0 014.75 7h2a.75.75 0 010 1.5h-2A.75.75 0 014 7.75zm11.774 3.537a.75.75 0 00-1.048-1.074L10.7 14.145 9.281 12.72a.75.75 0 00-1.062 1.058l1.943 1.95a.75.75 0 001.055.008l4.557-4.45z"></path></svg>
${data.user.contributionsCollection.totalPullRequestReviewContributions} Pull request${data.user.contributionsCollection.totalPullRequestReviewContributions > 1 ? "s" : ""} reviewed
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"></path></svg>
${data.user.contributionsCollection.totalPullRequestContributions} Pull request${data.user.contributionsCollection.totalPullRequestContributions > 1 ? "s" : ""} opened
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
${data.user.contributionsCollection.totalIssueContributions} Issue${data.user.contributionsCollection.totalIssueContributions > 1 ? "s" : ""} opened
</div>
</section>
<section>
<h2 class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.75 0A1.75 1.75 0 000 1.75v12.5C0 15.216.784 16 1.75 16h12.5A1.75 1.75 0 0016 14.25V1.75A1.75 1.75 0 0014.25 0H1.75zM1.5 1.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v12.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V1.75zM11.75 3a.75.75 0 00-.75.75v7.5a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75zm-8.25.75a.75.75 0 011.5 0v5.5a.75.75 0 01-1.5 0v-5.5zM8 3a.75.75 0 00-.75.75v3.5a.75.75 0 001.5 0v-3.5A.75.75 0 008 3z"></path></svg>
Community stats
</h2>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M5.5 3.5a2 2 0 100 4 2 2 0 000-4zM2 5.5a3.5 3.5 0 115.898 2.549 5.507 5.507 0 013.034 4.084.75.75 0 11-1.482.235 4.001 4.001 0 00-7.9 0 .75.75 0 01-1.482-.236A5.507 5.507 0 013.102 8.05 3.49 3.49 0 012 5.5zM11 4a.75.75 0 100 1.5 1.5 1.5 0 01.666 2.844.75.75 0 00-.416.672v.352a.75.75 0 00.574.73c1.2.289 2.162 1.2 2.522 2.372a.75.75 0 101.434-.44 5.01 5.01 0 00-2.56-3.012A3 3 0 0011 4z"></path></svg>
Following ${data.user.following.totalCount} user${data.user.followers.totalCount > 1 ? "s" : ""}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"></path></svg>
Sponsoring ${data.computed.sponsorships} repositor${data.computed.sponsorships > 1 ? "ies" : "y"}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg>
Starred ${data.user.starredRepositories.totalCount} repositor${data.user.starredRepositories.totalCount > 1 ? "ies" : "y"}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg>
Watching ${data.user.watching.totalCount} repositor${data.user.watching.totalCount > 1 ? "ies" : "y"}
</div>
</section>
</div>
<section>
<h2 class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path></svg>
${data.user.repositories.totalCount} Repositor${data.user.repositories.totalCount > 1 ? "ies" : "y"}
</h2>
<div class="row">
<section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg>
${data.computed.repositories.stargazers} Stargazer${data.computed.repositories.stargazers > 1 ? "s" : ""}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M8.878.392a1.75 1.75 0 00-1.756 0l-5.25 3.045A1.75 1.75 0 001 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 001.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514L8.878.392zM7.875 1.69a.25.25 0 01.25 0l4.63 2.685L8 7.133 3.245 4.375l4.63-2.685zM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432L2.5 5.677zm6.25 8.271l4.625-2.683a.25.25 0 00.125-.216V5.677L8.75 8.432v5.516z"></path></svg>
${data.user.packages.totalCount} Package${data.user.packages.totalCount > 1 ? "s" : ""}
</div>
</section>
<section>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
${data.computed.repositories.forks} Fork${data.computed.repositories.forks > 1 ? "s" : ""}
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg>
${data.computed.repositories.watchers} Watcher${data.computed.repositories.watchers > 1 ? "s" : ""}
</div>
</section>
</div>
</section>
<div class="row">
<section class="column">
<h3>Issues</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="issues-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#issues-bar)" x="0" y="0" width="${data.computed.repositories.issues_count ? 0 : 220}" height="8" fill="#d1d5da"/>
<rect mask="url(#issues-bar)" x="0" y="0" width="${(data.computed.repositories.issues_closed/data.computed.repositories.issues_count)*220 || 0}" height="8" fill="#d73a49"/>
<rect mask="url(#issues-bar)" x="${(data.computed.repositories.issues_closed/data.computed.repositories.issues_count)*220 || 0}" y="0" width="${(1-data.computed.repositories.issues_closed/data.computed.repositories.issues_count)*220 || 0}" height="8" fill="#28a745"/>
</svg>
<div class="field horizontal" style="width:220">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="#d73a49" fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 0110.65-5.003.75.75 0 00.959-1.153 8 8 0 102.592 8.33.75.75 0 10-1.444-.407A6.5 6.5 0 011.5 8zM8 12a1 1 0 100-2 1 1 0 000 2zm0-8a.75.75 0 01.75.75v3.5a.75.75 0 11-1.5 0v-3.5A.75.75 0 018 4zm4.78 4.28l3-3a.75.75 0 00-1.06-1.06l-2.47 2.47-.97-.97a.749.749 0 10-1.06 1.06l1.5 1.5a.75.75 0 001.06 0z"></path></svg>
${data.computed.repositories.issues_closed} Closed
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
${data.computed.repositories.issues_open} Open
</div>
</div>
</section>
<section class="column">
<h3>Pull requests</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="pr-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#pr-bar)" x="0" y="0" width="${data.computed.repositories.pr_count ? 0 : 220}" height="8" fill="#d1d5da"/>
<rect mask="url(#pr-bar)" x="0" y="0" width="${(data.computed.repositories.pr_merged/data.computed.repositories.pr_count)*220 || 0}" height="8" fill="#6f42c1"/>
<rect mask="url(#pr-bar)" x="${(data.computed.repositories.pr_merged/data.computed.repositories.pr_count)*220 || 0}" y="0" width="${(1-data.computed.repositories.pr_merged/data.computed.repositories.pr_count)*220 || 0}" height="8" fill="#28a745"/>
</svg>
<div class="field horizontal" style="width:220">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="#6f42c1" fill-rule="evenodd" d="M5 3.254V3.25v.005a.75.75 0 110-.005v.004zm.45 1.9a2.25 2.25 0 10-1.95.218v5.256a2.25 2.25 0 101.5 0V7.123A5.735 5.735 0 009.25 9h1.378a2.251 2.251 0 100-1.5H9.25a4.25 4.25 0 01-3.8-2.346zM12.75 9a.75.75 0 100-1.5.75.75 0 000 1.5zm-8.5 4.5a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
${data.computed.repositories.pr_merged} Merged
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"></path></svg>
${data.computed.repositories.pr_open} Open
</div>
</div>
</section>
</div>
<section class="column">
<h3>Most used languages</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="460" height="8">
<mask id="languages-bar">
<rect x="0" y="0" width="460" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#languages-bar)" x="0" y="0" width="${data.computed.languages.favorites.length ? 0 : 460}" height="8" fill="#d1d5da"/>
${data.computed.languages.favorites.map(({name, value, color, x}) => `
<rect mask="url(#languages-bar)" x="${x*460}" y="0" width="${value*460}" height="8" fill="${color}"/>
`).join("")}
</svg>
<div class="field horizontal horizontal-wrap" style="width:460">
${data.computed.languages.favorites.map(({name, color}) => `
<div class="field center no-wrap">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" version="1.1" width="16" height="16"><path fill="${color}" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"></path></svg>
${name}
</div>
`).join("")}
</div>
</section>
<footer>
Last updated ${new Date()}
</footer>
</div>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB