feat(plugins/stargazers): add worldmap (#1137) [skip ci]

This commit is contained in:
Simon Lecoq
2022-07-21 04:44:47 +02:00
committed by GitHub
parent 12372cbc84
commit 06e0550763
18 changed files with 31945 additions and 214 deletions

View File

@@ -24,6 +24,17 @@
</table>
<!--/header-->
## 🗝️ Obtaining a Google Maps API token
Some features like `plugin_stagazers_worldmap` require a Google Geocoding API token.
Follow instructions from their [documentation](https://developers.google.com/maps/documentation/geocoding/get-api-key) for more informations.
> 💳 A billing account is required to get a token. However a recurring [monthly credit](https://developers.google.com/maps/billing-credits#monthly) is offered which means you should not be charged if you don't exceed the free quota.
>
> It is advised to set the quota limit at 1200 requests per day
>
> Use at your own risk, *metrics* and its authors cannot be held responsible for anything charged.
## ➡️ Available options
<!--options-->

View File

@@ -13,4 +13,14 @@
token: ${{ secrets.METRICS_TOKEN }}
base: ""
plugin_stargazers: yes
plugin_stargazers_charts_type: chartist
plugin_stargazers_charts_type: chartist
- name: With worldmap
uses: lowlighter/metrics@latest
with:
filename: metrics.plugin.stargazers.worldmap.svg
token: ${{ secrets.METRICS_TOKEN }}
base: ""
plugin_stargazers: yes
plugin_stargazers_worldmap: yes
plugin_stargazers_worldmap_token: ${{ secrets.GOOGLE_MAP_TOKEN }}

View File

@@ -1,5 +1,5 @@
//Setup
export default async function({login, graphql, data, imports, q, queries, account}, {enabled = false, extras = false} = {}) {
export default async function({login, graphql, data, imports, q, queries, account}, {enabled = false, extras = false, "worldmap.token":_worldmap_token} = {}) {
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
@@ -7,12 +7,13 @@ export default async function({login, graphql, data, imports, q, queries, accoun
return null
//Load inputs
let {"charts.type": _charts} = imports.metadata.plugins.stargazers.inputs({data, account, q})
let {"charts.type": _charts, worldmap:_worldmap, "worldmap.sample":_worldmap_sample} = imports.metadata.plugins.stargazers.inputs({data, account, q})
//Retrieve stargazers from graphql api
console.debug(`metrics/compute/${login}/plugins > stargazers > querying api`)
const repositories = data.user.repositories.nodes.map(({name: repository, owner: {login: owner}}) => ({repository, owner})) ?? []
const dates = []
const locations = []
for (const {repository, owner} of repositories) {
//Iterate through stargazers
console.debug(`metrics/compute/${login}/plugins > stargazers > retrieving stargazers of ${repository}`)
@@ -20,12 +21,12 @@ export default async function({login, graphql, data, imports, q, queries, accoun
let pushed = 0
do {
console.debug(`metrics/compute/${login}/plugins > stargazers > retrieving stargazers of ${repository} after ${cursor}`)
const {repository: {stargazers: {edges}}} = await graphql(queries.stargazers({login: owner, repository, after: cursor ? `after: "${cursor}"` : ""}))
const {repository: {stargazers: {edges}}} = await graphql(queries.stargazers({login: owner, repository, after: cursor ? `after: "${cursor}"` : "", location: _worldmap ? "node { location }" : ""}))
cursor = edges?.[edges?.length - 1]?.cursor
dates.push(...edges.map(({starredAt}) => new Date(starredAt)))
locations.push(...edges.map(({node: {location}}) => location))
pushed = edges.length
} while ((pushed) && (cursor))
//Limit repositories
console.debug(`metrics/compute/${login}/plugins > stargazers > loaded ${dates.length} stargazers for ${repository}`)
}
console.debug(`metrics/compute/${login}/plugins > stargazers > loaded ${dates.length} stargazers in total`)
@@ -95,8 +96,15 @@ export default async function({login, graphql, data, imports, q, queries, accoun
}})(${imports.format.toString()})`)
}
//Generating worldmap
let worldmap = null
if ((_worldmap)&&(imports.metadata.plugins.stargazers.extras("worldmap", {extras}))) {
const {default:generate} = await import("./worldmap/index.mjs")
worldmap = await generate(login, {locations, imports, token:_worldmap_token, sample:_worldmap_sample})
}
//Results
return {total, increments, months, charts}
return {total, increments, months, charts, worldmap}
}
//Handle errors
catch (error) {

View File

@@ -5,6 +5,7 @@ description: |
examples:
+classic charts: https://github.com/lowlighter/metrics/blob/examples/metrics.plugin.stargazers.svg
chartist charts: https://github.com/lowlighter/metrics/blob/examples/metrics.plugin.stargazers.chartist.svg
+worldmap: https://github.com/lowlighter/metrics/blob/examples/metrics.plugin.stargazers.worldmap.svg
index: 10
supports:
- user
@@ -32,4 +33,29 @@ inputs:
- classic
- chartist
extras:
- metrics.npm.optional.chartist
- metrics.npm.optional.chartist
plugin_stargazers_worldmap:
description: |
Stargazers worldmap
type: boolean
default: no
extras:
- metrics.api.google.maps
plugin_stargazers_worldmap_token:
description: |
Stargazers worldmap token
type: token
default: ""
plugin_stargazers_worldmap_sample:
description: |
Stargazers worldmap sample
Use this setting to randomly sample and limit your stargazers.
Helps to avoid consuming too much Google Geocoding API requests while still being representative.
type: number
default: 0
min: 0
zero: disable

View File

@@ -4,6 +4,7 @@ query StargazersDefault {
edges {
starredAt
cursor
$location
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
See license at [npmjs.com/package/visionscarto-world-atlas](https://www.npmjs.com/package/visionscarto-world-atlas)

View File

@@ -0,0 +1,60 @@
//Imports
import * as d3 from "d3"
import D3Node from "d3-node"
import color from "color"
import {Client as Gmap} from "@googlemaps/google-maps-services-js"
/**
* Worldmap
* Mostly ported from https://github.com/dyatko/worldstar
* License: https://raw.githubusercontent.com/dyatko/worldstar/master/LICENSE
*/
export default async function (login, {locations, sample, imports, token}) {
//Parse geocodes
let stars = new Map()
if (token) {
const cache = new Map()
const get = new Gmap()
locations = imports.shuffle(locations.filter(string => string).map(string => string.toLocaleLowerCase())).slice(0, sample || Infinity)
for (const location of locations) {
console.debug(`metrics/compute/${login}/plugins > stargazers > worldmap > looking for ${location}`)
if (!cache.has(location)) {
try {
const {data:{results}} = await get.geocode({params:{address:location, key:token}})
const country = results.at(0).address_components.find(({types}) => types.includes("country"))
cache.set(location, country.short_name ?? country.long_name)
console.debug(`metrics/compute/${login}/plugins > stargazers > worldmap > ${location} resolved to ${cache.get(location)}`)
}
catch (error) {
console.debug(`metrics/compute/${login}/plugins > stargazers > worldmap > failed to resolve ${location}: ${error}`)
}
}
const code = cache.get(location)
stars.set(code, (stars.get(code) ?? 0) + 1)
}
}
else throw {error:{message:"Google Maps API token is not set"}}
//Generate SVG
const d3n = new D3Node()
const svg = d3n.createSVG(480, 315)
const countries = JSON.parse(await imports.fs.readFile(imports.paths.join(imports.__module(import.meta.url), "atlas/50m_countries.geojson")))
const geopath = d3.geoPath(d3.geoMercator().fitWidth(svg.attr("width"), countries))
const splits = [...new Set(stars.values())].sort((a, b) => a - b)
svg
.append("g")
.selectAll("path")
.data(countries.features)
.join("path")
.attr("id", ({id}) => id)
.style("fill", ({properties:{iso_a2, wb_a2, sov_a3}}) => {
const code = iso_a2?.match(/[A-Z]{2}/) ? iso_a2 : wb_a2?.match(/[A-Z]{2}/) ? wb_a2 : sov_a3?.substr(0, 2) ?? ""
const value = stars.get(code)
return color("#216e39").mix(color("#ffffff"), 1 - Math.max(0, splits.indexOf(value))/splits.length).hex()
})
.style("stroke", "#afafaf")
.style("stroke-width", "0.6px")
.attr("d", geopath)
return d3n.svgString()
}