feat(plugins/stargazers): add worldmap (#1137) [skip ci]
This commit is contained in:
1789
package-lock.json
generated
1789
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -32,14 +32,18 @@
|
|||||||
"@actions/core": "^1.9.0",
|
"@actions/core": "^1.9.0",
|
||||||
"@actions/github": "^5.0.3",
|
"@actions/github": "^5.0.3",
|
||||||
"@faker-js/faker": "^7.3.0",
|
"@faker-js/faker": "^7.3.0",
|
||||||
|
"@googlemaps/google-maps-services-js": "^3.3.16",
|
||||||
"@octokit/graphql": "^5.0.0",
|
"@octokit/graphql": "^5.0.0",
|
||||||
"@octokit/rest": "^19.0.3",
|
"@octokit/rest": "^19.0.3",
|
||||||
"@primer/css": "^20.2.4",
|
"@primer/css": "^20.2.4",
|
||||||
"@primer/octicons": "^17.3.0",
|
"@primer/octicons": "^17.3.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
|
"color": "^4.2.3",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"csso": "^5.0.3",
|
"csso": "^5.0.3",
|
||||||
|
"d3": "^7.6.1",
|
||||||
|
"d3-node": "^2.2.3",
|
||||||
"ejs": "^3.1.8",
|
"ejs": "^3.1.8",
|
||||||
"emoji-name-map": "^1.2.9",
|
"emoji-name-map": "^1.2.9",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
|
|||||||
@@ -860,10 +860,14 @@
|
|||||||
//Stargazers
|
//Stargazers
|
||||||
...(set.plugins.enabled.stargazers
|
...(set.plugins.enabled.stargazers
|
||||||
? ({
|
? ({
|
||||||
|
__stargazers: {
|
||||||
|
worldmap: await staticPlaceholder(options["stargazers.worldmap"], "stargazers.worldmap.svg"),
|
||||||
|
},
|
||||||
get stargazers() {
|
get stargazers() {
|
||||||
const dates = []
|
const dates = []
|
||||||
let total = faker.datatype.number(1000)
|
let total = faker.datatype.number(1000)
|
||||||
const result = {
|
const result = {
|
||||||
|
worldmap: this.__stargazers.worldmap,
|
||||||
total: {
|
total: {
|
||||||
dates: {},
|
dates: {},
|
||||||
get max() {
|
get max() {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 3.3 MiB |
@@ -24,6 +24,17 @@
|
|||||||
</table>
|
</table>
|
||||||
<!--/header-->
|
<!--/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
|
## ➡️ Available options
|
||||||
|
|
||||||
<!--options-->
|
<!--options-->
|
||||||
|
|||||||
@@ -13,4 +13,14 @@
|
|||||||
token: ${{ secrets.METRICS_TOKEN }}
|
token: ${{ secrets.METRICS_TOKEN }}
|
||||||
base: ""
|
base: ""
|
||||||
plugin_stargazers: yes
|
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 }}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//Setup
|
//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
|
//Plugin execution
|
||||||
try {
|
try {
|
||||||
//Check if plugin is enabled and requirements are met
|
//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
|
return null
|
||||||
|
|
||||||
//Load inputs
|
//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
|
//Retrieve stargazers from graphql api
|
||||||
console.debug(`metrics/compute/${login}/plugins > stargazers > querying 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 repositories = data.user.repositories.nodes.map(({name: repository, owner: {login: owner}}) => ({repository, owner})) ?? []
|
||||||
const dates = []
|
const dates = []
|
||||||
|
const locations = []
|
||||||
for (const {repository, owner} of repositories) {
|
for (const {repository, owner} of repositories) {
|
||||||
//Iterate through stargazers
|
//Iterate through stargazers
|
||||||
console.debug(`metrics/compute/${login}/plugins > stargazers > retrieving stargazers of ${repository}`)
|
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
|
let pushed = 0
|
||||||
do {
|
do {
|
||||||
console.debug(`metrics/compute/${login}/plugins > stargazers > retrieving stargazers of ${repository} after ${cursor}`)
|
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
|
cursor = edges?.[edges?.length - 1]?.cursor
|
||||||
dates.push(...edges.map(({starredAt}) => new Date(starredAt)))
|
dates.push(...edges.map(({starredAt}) => new Date(starredAt)))
|
||||||
|
locations.push(...edges.map(({node: {location}}) => location))
|
||||||
pushed = edges.length
|
pushed = edges.length
|
||||||
} while ((pushed) && (cursor))
|
} 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 for ${repository}`)
|
||||||
}
|
}
|
||||||
console.debug(`metrics/compute/${login}/plugins > stargazers > loaded ${dates.length} stargazers in total`)
|
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()})`)
|
}})(${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
|
//Results
|
||||||
return {total, increments, months, charts}
|
return {total, increments, months, charts, worldmap}
|
||||||
}
|
}
|
||||||
//Handle errors
|
//Handle errors
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ description: |
|
|||||||
examples:
|
examples:
|
||||||
+classic charts: https://github.com/lowlighter/metrics/blob/examples/metrics.plugin.stargazers.svg
|
+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
|
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
|
index: 10
|
||||||
supports:
|
supports:
|
||||||
- user
|
- user
|
||||||
@@ -32,4 +33,29 @@ inputs:
|
|||||||
- classic
|
- classic
|
||||||
- chartist
|
- chartist
|
||||||
extras:
|
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
|
||||||
@@ -4,6 +4,7 @@ query StargazersDefault {
|
|||||||
edges {
|
edges {
|
||||||
starredAt
|
starredAt
|
||||||
cursor
|
cursor
|
||||||
|
$location
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29912
source/plugins/stargazers/worldmap/atlas/50m_countries.geojson
Normal file
29912
source/plugins/stargazers/worldmap/atlas/50m_countries.geojson
Normal file
File diff suppressed because one or more lines are too long
1
source/plugins/stargazers/worldmap/atlas/LICENSE.md
Normal file
1
source/plugins/stargazers/worldmap/atlas/LICENSE.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
See license at [npmjs.com/package/visionscarto-world-atlas](https://www.npmjs.com/package/visionscarto-world-atlas)
|
||||||
60
source/plugins/stargazers/worldmap/index.mjs
Normal file
60
source/plugins/stargazers/worldmap/index.mjs
Normal 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()
|
||||||
|
}
|
||||||
@@ -50,6 +50,12 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
<% if (plugins.stargazers.worldmap) { %>
|
||||||
|
<div class="row margin-bottom">
|
||||||
|
<h3 class="margin-lr-auto">Stargazers origins</h3>
|
||||||
|
<%- plugins.stargazers.worldmap %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
<% } %>
|
<% } %>
|
||||||
</section>
|
</section>
|
||||||
<% } %>
|
<% } %>
|
||||||
@@ -141,6 +141,10 @@
|
|||||||
.no-margin-top {
|
.no-margin-top {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
|
.margin-lr-auto {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* User avatar */
|
/* User avatar */
|
||||||
.avatar {
|
.avatar {
|
||||||
|
|||||||
@@ -13,3 +13,12 @@
|
|||||||
plugin_stargazers_charts_type: chartist
|
plugin_stargazers_charts_type: chartist
|
||||||
use_mocked_data: 'yes'
|
use_mocked_data: 'yes'
|
||||||
verify: 'yes'
|
verify: 'yes'
|
||||||
|
- name: ✨ Stargazers over last weeks - With worldmap
|
||||||
|
uses: lowlighter/metrics@latest
|
||||||
|
with:
|
||||||
|
token: MOCKED_TOKEN
|
||||||
|
plugin_stargazers: 'yes'
|
||||||
|
plugin_stargazers_worldmap: 'yes'
|
||||||
|
plugin_stargazers_worldmap_token: MOCKED_TOKEN
|
||||||
|
use_mocked_data: 'yes'
|
||||||
|
verify: 'yes'
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export default function({faker, query, login = faker.internet.userName()}) {
|
|||||||
edges: new Array(faker.datatype.number({min: 50, max: 100})).fill(null).map(() => ({
|
edges: new Array(faker.datatype.number({min: 50, max: 100})).fill(null).map(() => ({
|
||||||
starredAt: `${faker.date.recent(30)}`,
|
starredAt: `${faker.date.recent(30)}`,
|
||||||
cursor: "MOCKED_CURSOR",
|
cursor: "MOCKED_CURSOR",
|
||||||
|
node:{
|
||||||
|
location: faker.address.city(),
|
||||||
|
}
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import fs from "fs/promises"
|
|||||||
import paths from "path"
|
import paths from "path"
|
||||||
import rss from "rss-parser"
|
import rss from "rss-parser"
|
||||||
import urls from "url"
|
import urls from "url"
|
||||||
|
import {Client as Gmap} from "@googlemaps/google-maps-services-js"
|
||||||
|
|
||||||
//Mocked state
|
//Mocked state
|
||||||
let mocked = false
|
let mocked = false
|
||||||
@@ -146,6 +147,56 @@ export default async function({graphql, rest}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Google API mocking
|
||||||
|
{
|
||||||
|
//Unmocked
|
||||||
|
console.debug("metrics/compute/mocks > mocking google-maps-services-js")
|
||||||
|
|
||||||
|
//Mock geocode API
|
||||||
|
Gmap.prototype.geocode = function() {
|
||||||
|
console.debug("metrics/compute/mocks > mocking google maps geocode result")
|
||||||
|
const lat = faker.address.latitude()
|
||||||
|
const lng = faker.address.longitude()
|
||||||
|
const city = faker.address.city()
|
||||||
|
const country = faker.address.country()
|
||||||
|
return {
|
||||||
|
data:{
|
||||||
|
results:[
|
||||||
|
{
|
||||||
|
address_components: [
|
||||||
|
{
|
||||||
|
long_name: city,
|
||||||
|
short_name: city,
|
||||||
|
types: [ "political" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
long_name: country,
|
||||||
|
short_name: faker.address.countryCode(),
|
||||||
|
types: [ "country", "political" ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
formatted_address: `${city}, ${country}`,
|
||||||
|
geometry: {
|
||||||
|
bounds: {
|
||||||
|
northeast: { lat, lng },
|
||||||
|
southwest: { lat, lng }
|
||||||
|
},
|
||||||
|
location: { lat, lng },
|
||||||
|
location_type: "APPROXIMATE",
|
||||||
|
viewport: {
|
||||||
|
northeast: { lat, lng },
|
||||||
|
southwest: { lat, lng }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
place_id: 'ChIJu9FC7RXupzsR26dsAapFLgg',
|
||||||
|
types: [ "locality", "political" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Return mocked elements
|
//Return mocked elements
|
||||||
return {graphql, rest}
|
return {graphql, rest}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"NIGHTSCOUT_URL":"https://testapp.herokuapp.com/",
|
"NIGHTSCOUT_URL":"https://testapp.herokuapp.com/",
|
||||||
"WAKATIME_TOKEN":"MOCKED_TOKEN",
|
"WAKATIME_TOKEN":"MOCKED_TOKEN",
|
||||||
"TWITTER_TOKEN":"MOCKED_TOKEN",
|
"TWITTER_TOKEN":"MOCKED_TOKEN",
|
||||||
|
"GOOGLE_MAP_TOKEN": "MOCKED_TOKEN",
|
||||||
"STOCK_TOKEN":"MOCKED_TOKEN",
|
"STOCK_TOKEN":"MOCKED_TOKEN",
|
||||||
"POOPMAP_TOKEN":"MOCKED_TOKEN"
|
"POOPMAP_TOKEN":"MOCKED_TOKEN"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user