diff --git a/.claude/skills/linkedin-stats/fast/page/geo-monthly.py b/.claude/skills/linkedin-stats/fast/page/geo-monthly.py new file mode 100644 index 0000000..6432814 --- /dev/null +++ b/.claude/skills/linkedin-stats/fast/page/geo-monthly.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Build a MONTHLY ICP-geography trend from per-month Visitors XLS exports. + +Why separate from parse-page-xls.py: LinkedIn's demographic sheets carry NO +date column — one export yields a single aggregate distribution for its whole +range. To get a monthly geography trend you must export ONE range per month. +This reads a directory of files named YYYY-MM.xls (each a Visitors export whose +Time range was that calendar month) and classifies each month's Location sheet +into ICP buckets via geo_classify. + +Going forward the pipeline appends one month per run; for the initial backfill +the months were exported by hand. + +Usage: python3 geo-monthly.py +""" +import sys, os, json, glob +import xlrd +from geo_classify import classify_rows + + +def build(xls_dir): + out = {} + for f in sorted(glob.glob(os.path.join(xls_dir, "20??-??.xls"))): + month = os.path.basename(f)[:7] + sh = xlrd.open_workbook(f).sheet_by_name("Location") + rows = [(sh.cell_value(r, 0), sh.cell_value(r, 1)) for r in range(1, sh.nrows)] + c = classify_rows(rows) + out[month] = { + "us": c["buckets"]["US"], "team": c["buckets"]["TEAM"], + "anti": c["buckets"]["ANTI"], "other": c["buckets"]["OTHER"], + "icp_pct": c["icp_pct"], "anti_pct": c["anti_pct"], "total": c["total"], + } + return out + + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("usage: geo-monthly.py ", file=sys.stderr); sys.exit(2) + data = build(sys.argv[1]) + os.makedirs(os.path.dirname(sys.argv[2]) or ".", exist_ok=True) + json.dump({"months": data}, open(sys.argv[2], "w"), indent=2) + print("wrote", sys.argv[2], "months:", ", ".join(data), file=sys.stderr) diff --git a/.claude/skills/linkedin-stats/fast/page/geo_classify.py b/.claude/skills/linkedin-stats/fast/page/geo_classify.py new file mode 100644 index 0000000..183d5a6 --- /dev/null +++ b/.claude/skills/linkedin-stats/fast/page/geo_classify.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Classify LinkedIn location strings into ICP geography buckets. + +S&F ICP = US customers. Buckets: + US -> ICP (the target market) + Ukraine -> TEAM (S&F is UA-based; internal network, not a customer signal) + India / China -> ANTI (explicitly off-ICP) + everything else -> OTHER (off-target market) + +LinkedIn convention: US metros carry NO country suffix ("Greater Boston", +"San Francisco Bay Area"); international locations end with ", ". +So: a location with no trailing country is treated as US. + +Pure functions — no I/O. Imported by parse-page-xls.py. +""" + +COUNTRY_BUCKET = { + "ukraine": "TEAM", + "india": "ANTI", + "china": "ANTI", +} +BUCKETS = ("US", "TEAM", "ANTI", "OTHER") + + +def country_of(loc: str) -> str: + """Best-effort country from a LinkedIn location string.""" + parts = [p.strip() for p in str(loc).split(",") if p.strip()] + if len(parts) <= 1: + return "United States" # US convention: metro with no country suffix + return parts[-1] # trailing token is the country (doubled tails collapse to same value) + + +def bucket_of(loc: str) -> str: + c = country_of(loc).lower() + if c in ("united states", "usa", "us"): + return "US" + return COUNTRY_BUCKET.get(c, "OTHER") + + +def classify_rows(rows): + """rows: iterable of (location, count). Returns {buckets, countries, total, icp_pct, anti_pct}.""" + buckets = {b: 0 for b in BUCKETS} + countries = {} + for loc, n in rows: + if not isinstance(n, (int, float)): + continue + buckets[bucket_of(loc)] += n + c = country_of(loc) + countries[c] = countries.get(c, 0) + n + total = sum(buckets.values()) + return { + "buckets": {b: round(buckets[b]) for b in BUCKETS}, + "total": round(total), + "icp_pct": round(100 * buckets["US"] / total, 1) if total else 0.0, + "anti_pct": round(100 * buckets["ANTI"] / total, 1) if total else 0.0, + "countries": dict(sorted(countries.items(), key=lambda x: -x[1])), + } diff --git a/.claude/skills/linkedin-stats/fast/page/parse-page-xls.py b/.claude/skills/linkedin-stats/fast/page/parse-page-xls.py index 16be933..84d2bd3 100644 --- a/.claude/skills/linkedin-stats/fast/page/parse-page-xls.py +++ b/.claude/skills/linkedin-stats/fast/page/parse-page-xls.py @@ -20,6 +20,7 @@ """ import sys, os, json, datetime, collections import xlrd +from geo_classify import classify_rows def _date(v): @@ -58,6 +59,14 @@ def _demo(path, sheet_name, topn=8): return [[n, int(v)] for n, v in rows[:topn] if isinstance(v, (int, float))] +def _geo(path): + """Classify the full Location sheet into ICP geography buckets (US/TEAM/ANTI/OTHER).""" + wb = xlrd.open_workbook(path) + sh = wb.sheet_by_name("Location") + rows = [(sh.cell_value(r, 0), sh.cell_value(r, 1)) for r in range(1, sh.nrows)] + return classify_rows(rows) + + def last_months(n): today = datetime.date.today() out = [] @@ -111,6 +120,10 @@ def parse(xls_dir, months): "months": out_months, "visitor_demographics": {s: _demo(os.path.join(xls_dir, "visitors.xls"), s) for s in demo_sheets}, "follower_demographics": {s: _demo(os.path.join(xls_dir, "followers.xls"), s) for s in demo_sheets}, + "geography": { + "visitors": _geo(os.path.join(xls_dir, "visitors.xls")), + "followers": _geo(os.path.join(xls_dir, "followers.xls")), + }, } diff --git a/.github/scripts/build-page-dashboard.mjs b/.github/scripts/build-page-dashboard.mjs index 4938d25..a846382 100644 --- a/.github/scripts/build-page-dashboard.mjs +++ b/.github/scripts/build-page-dashboard.mjs @@ -17,10 +17,23 @@ import fs from 'node:fs'; const args = Object.fromEntries(process.argv.slice(2).map((a, i, arr) => a.startsWith('--') ? [a.slice(2), arr[i + 1]] : [null, null]).filter(([k]) => k)); const IN = args.in || 'dashboards/li-stats/page/monthly.json'; +const GEO_IN = args.geo || 'dashboards/li-stats/page/geo-monthly.json'; const OUT = args.out || 'dashboards/grafana/linkedin-page.json'; const src = JSON.parse(fs.readFileSync(IN, 'utf8')); const months = Object.entries(src.months).map(([month, m]) => ({ month, ...m })); +// Monthly ICP-geography trend (one Visitors export per calendar month). +const geoMonthly = fs.existsSync(GEO_IN) + ? Object.entries(JSON.parse(fs.readFileSync(GEO_IN, 'utf8')).months).map(([month, g]) => ({ month, ...g })) + : []; +// 6-month VISITOR geography = sum of the monthly exports (Location is additive +// "total views"), so the headline matches the rest of the 6-month dashboard. +const vis6 = (() => { + const b = { US: 0, TEAM: 0, ANTI: 0, OTHER: 0 }; + for (const r of geoMonthly) { b.US += r.us || 0; b.TEAM += r.team || 0; b.ANTI += r.anti || 0; b.OTHER += r.other || 0; } + const total = b.US + b.TEAM + b.ANTI + b.OTHER; + return { buckets: b, icp_pct: total ? Math.round((1000 * b.US) / total) / 10 : 0 }; +})(); const DS = { type: 'yesoreyeram-infinity-datasource', uid: 'grafanacloud-infinity' }; @@ -39,6 +52,18 @@ function inlineTarget(rows, columns, refId = 'A') { filters: [], }; } +// Backend-parser inline target that honours a filterExpression on the `month` +// column — the ONLY way an inline panel responds to the $month variable +// (parser:'simple' ignores filterExpression; parser:'backend' applies it). +function filteredTarget(rows, keys, filterExpression, refId = 'A') { + return { + refId, datasource: DS, type: 'json', source: 'inline', format: 'table', + parser: 'backend', root_selector: '', data: JSON.stringify(rows), + columns: [{ selector: 'month', text: 'month', type: 'string' }, + ...keys.map((k) => ({ selector: k, text: k, type: 'number' }))], + filters: [], filterExpression, + }; +} const numCols = (keys) => [{ selector: 'month', text: 'month', type: 'string' }, ...keys.map((k) => ({ selector: k, text: k, type: 'number' }))]; const demoCols = [{ selector: 'name', text: 'name', type: 'string' }, @@ -70,22 +95,85 @@ function bargauge(title, gridPos, rows) { targets: [inlineTarget(rows, demoCols)], }; } +// ICP share as a coloured stat: green above target, red below. +function icpStat(title, gridPos, pct) { + return { + id: nid(), type: 'stat', title, datasource: DS, gridPos, + fieldConfig: { defaults: { unit: 'percent', decimals: 1, min: 0, max: 100, thresholds: { + mode: 'absolute', steps: [{ color: 'red', value: null }, { color: 'orange', value: 30 }, { color: 'green', value: 50 }] } }, overrides: [] }, + options: { reduceOptions: { values: false, calcs: ['lastNotNull'], fields: '/^v$/' }, textMode: 'value', colorMode: 'value', graphMode: 'none' }, + targets: [inlineTarget([{ v: pct }], [{ selector: 'v', text: 'v', type: 'number' }])], + }; +} + +// Stat for the $month-selected value (filtered inline via backend parser). +function monthStat(title, gridPos, field, unit, colored = false) { + const defaults = { unit, decimals: unit === 'percent' ? 1 : 0 }; + if (colored) defaults.thresholds = { mode: 'absolute', + steps: [{ color: 'red', value: null }, { color: 'orange', value: 30 }, { color: 'green', value: 50 }] }; + return { + id: nid(), type: 'stat', title, datasource: DS, gridPos, + fieldConfig: { defaults, overrides: [] }, + options: { reduceOptions: { values: false, calcs: ['lastNotNull'], fields: `/^${field}$/` }, + textMode: 'value', colorMode: colored ? 'value' : 'none', graphMode: 'none' }, + targets: [filteredTarget(geoMonthly, [field], 'month == "${month}"')], + }; +} const demoRows = (obj, cat) => (obj[cat] || []).map(([name, value]) => ({ name, value })); +const geo = src.geography || { visitors: { buckets: {}, icp_pct: 0 }, followers: { buckets: {}, icp_pct: 0 } }; +const BUCKET_LABEL = { US: 'US · ICP', TEAM: 'Ukraine · team', ANTI: 'India / China · off-ICP', OTHER: 'Other · off-target' }; +const geoRows = (b) => ['US', 'TEAM', 'ANTI', 'OTHER'].map((k) => ({ name: BUCKET_LABEL[k], value: (b || {})[k] || 0 })); + +// Generic barchart over an explicit data array (used for the monthly geo trend). +function barchartData(title, gridPos, data, keys, unit = 'short', stacking = 'none') { + return { + id: nid(), type: 'barchart', title, datasource: DS, gridPos, + fieldConfig: { defaults: { unit, custom: { lineWidth: 1, fillOpacity: 80 } }, overrides: [] }, + options: { orientation: 'auto', xTickLabelRotation: 0, showValue: 'auto', stacking, legend: { showLegend: true, placement: 'bottom' } }, + targets: [inlineTarget(data, [{ selector: 'month', text: 'month', type: 'string' }, ...keys.map((k) => ({ selector: k, text: k, type: 'number' }))])], + }; +} const panels = [ - { id: nid(), type: 'row', title: 'Company Page — monthly', gridPos: { h: 1, w: 24, x: 0, y: 0 }, collapsed: false, panels: [] }, - barchart('Page views & unique visitors', { h: 8, w: 12, x: 0, y: 1 }, ['page_views', 'unique_visitors']), - barchart('New followers', { h: 8, w: 6, x: 12, y: 1 }, ['new_followers']), - barchart('Post impressions', { h: 8, w: 6, x: 18, y: 1 }, ['post_impressions']), - table('Monthly metrics', { h: 8, w: 24, x: 0, y: 9 }), - { id: nid(), type: 'row', title: 'Audience (12-month snapshot)', gridPos: { h: 1, w: 24, x: 0, y: 17 }, collapsed: false, panels: [] }, - bargauge('Visitors by seniority', { h: 8, w: 12, x: 0, y: 18 }, demoRows(src.visitor_demographics, 'Seniority')), - bargauge('Visitors by industry', { h: 8, w: 12, x: 12, y: 18 }, demoRows(src.visitor_demographics, 'Industry')), - bargauge('Followers by seniority', { h: 8, w: 12, x: 0, y: 26 }, demoRows(src.follower_demographics, 'Seniority')), - bargauge('Followers by industry', { h: 8, w: 12, x: 12, y: 26 }, demoRows(src.follower_demographics, 'Industry')), + { id: nid(), type: 'row', title: 'ICP geography (visitors · last 6 months)', gridPos: { h: 1, w: 24, x: 0, y: 0 }, collapsed: false, panels: [] }, + icpStat('US · ICP share — visitors (6 mo)', { h: 6, w: 6, x: 0, y: 1 }, vis6.icp_pct), + icpStat('US · ICP share — followers (base)', { h: 6, w: 6, x: 6, y: 1 }, geo.followers.icp_pct), + bargauge('Visitors by ICP bucket (6 mo)', { h: 6, w: 6, x: 12, y: 1 }, geoRows(vis6.buckets)), + bargauge('Followers by ICP bucket (base)', { h: 6, w: 6, x: 18, y: 1 }, geoRows(geo.followers.buckets)), + barchartData('US · ICP share of visitors by month', { h: 8, w: 12, x: 0, y: 7 }, geoMonthly, ['icp_pct'], 'percent'), + barchartData('Visitor geography by month', { h: 8, w: 12, x: 12, y: 7 }, geoMonthly, ['us', 'team', 'anti', 'other'], 'short', 'normal'), + + { id: nid(), type: 'row', title: 'Selected month — pick $month above', gridPos: { h: 1, w: 24, x: 0, y: 15 }, collapsed: false, panels: [] }, + monthStat('US · ICP share ($month)', { h: 5, w: 6, x: 0, y: 16 }, 'icp_pct', 'percent', true), + monthStat('India / China share ($month)', { h: 5, w: 6, x: 6, y: 16 }, 'anti_pct', 'percent'), + monthStat('US visitors ($month)', { h: 5, w: 6, x: 12, y: 16 }, 'us', 'short'), + monthStat('India / China visitors ($month)', { h: 5, w: 6, x: 18, y: 16 }, 'anti', 'short'), + + { id: nid(), type: 'row', title: 'Company Page — monthly', gridPos: { h: 1, w: 24, x: 0, y: 21 }, collapsed: false, panels: [] }, + barchart('Page views & unique visitors', { h: 8, w: 12, x: 0, y: 22 }, ['page_views', 'unique_visitors']), + barchart('New followers', { h: 8, w: 6, x: 12, y: 22 }, ['new_followers']), + barchart('Post impressions', { h: 8, w: 6, x: 18, y: 22 }, ['post_impressions']), + table('Monthly metrics', { h: 8, w: 24, x: 0, y: 30 }), + + { id: nid(), type: 'row', title: 'Audience (12-month snapshot)', gridPos: { h: 1, w: 24, x: 0, y: 38 }, collapsed: false, panels: [] }, + bargauge('Visitors by seniority', { h: 8, w: 12, x: 0, y: 39 }, demoRows(src.visitor_demographics, 'Seniority')), + bargauge('Visitors by industry', { h: 8, w: 12, x: 12, y: 39 }, demoRows(src.visitor_demographics, 'Industry')), + bargauge('Followers by seniority', { h: 8, w: 12, x: 0, y: 47 }, demoRows(src.follower_demographics, 'Seniority')), + bargauge('Followers by industry', { h: 8, w: 12, x: 12, y: 47 }, demoRows(src.follower_demographics, 'Industry')), ]; +// $month picker — Custom variable in "display : value" format (Query type +// can't read Infinity in v0alpha1 dashboards, so the list is embedded). +const monthValues = geoMonthly.map((r) => r.month); +const monthVar = { + name: 'month', type: 'custom', label: 'Month', + query: monthValues.map((m) => `${m} : ${m}`).join(', '), + current: monthValues.length ? { text: monthValues[monthValues.length - 1], value: monthValues[monthValues.length - 1] } : {}, + options: monthValues.map((m, i) => ({ text: m, value: m, selected: i === monthValues.length - 1 })), + includeAll: false, multi: false, +}; + const dashboard = { uid: 'linkedin-page', title: 'LinkedIn Stats — Company Page', @@ -95,7 +183,7 @@ const dashboard = { version: 1, refresh: '', time: { from: 'now-1y', to: 'now' }, - templating: { list: [] }, + templating: { list: monthValues.length ? [monthVar] : [] }, annotations: { list: [] }, panels, }; diff --git a/dashboards/grafana/linkedin-page.json b/dashboards/grafana/linkedin-page.json index 9fa931f..c769a34 100644 --- a/dashboards/grafana/linkedin-page.json +++ b/dashboards/grafana/linkedin-page.json @@ -14,27 +14,782 @@ "to": "now" }, "templating": { - "list": [] + "list": [ + { + "name": "month", + "type": "custom", + "label": "Month", + "query": "2026-03 : 2026-03, 2026-04 : 2026-04, 2026-05 : 2026-05, 2026-06 : 2026-06, 2026-07 : 2026-07, 2026-08 : 2026-08", + "current": { + "text": "2026-08", + "value": "2026-08" + }, + "options": [ + { + "text": "2026-03", + "value": "2026-03", + "selected": false + }, + { + "text": "2026-04", + "value": "2026-04", + "selected": false + }, + { + "text": "2026-05", + "value": "2026-05", + "selected": false + }, + { + "text": "2026-06", + "value": "2026-06", + "selected": false + }, + { + "text": "2026-07", + "value": "2026-07", + "selected": false + }, + { + "text": "2026-08", + "value": "2026-08", + "selected": true + } + ], + "includeAll": false, + "multi": false + } + ] }, "annotations": { "list": [] }, "panels": [ { - "id": 1, + "id": 1, + "type": "row", + "title": "ICP geography (visitors · last 6 months)", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "collapsed": false, + "panels": [] + }, + { + "id": 2, + "type": "stat", + "title": "US · ICP share — visitors (6 mo)", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "decimals": 1, + "min": 0, + "max": 100, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 30 + }, + { + "color": "green", + "value": 50 + } + ] + } + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "values": false, + "calcs": [ + "lastNotNull" + ], + "fields": "/^v$/" + }, + "textMode": "value", + "colorMode": "value", + "graphMode": "none" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "type": "json", + "source": "inline", + "format": "table", + "parser": "simple", + "root_selector": "", + "data": "[{\"v\":29.4}]", + "columns": [ + { + "selector": "v", + "text": "v", + "type": "number" + } + ], + "filters": [] + } + ] + }, + { + "id": 3, + "type": "stat", + "title": "US · ICP share — followers (base)", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "decimals": 1, + "min": 0, + "max": 100, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 30 + }, + { + "color": "green", + "value": 50 + } + ] + } + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "values": false, + "calcs": [ + "lastNotNull" + ], + "fields": "/^v$/" + }, + "textMode": "value", + "colorMode": "value", + "graphMode": "none" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "type": "json", + "source": "inline", + "format": "table", + "parser": "simple", + "root_selector": "", + "data": "[{\"v\":42.5}]", + "columns": [ + { + "selector": "v", + "text": "v", + "type": "number" + } + ], + "filters": [] + } + ] + }, + { + "id": 4, + "type": "bargauge", + "title": "Visitors by ICP bucket (6 mo)", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 12, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-BlPu" + } + }, + "overrides": [] + }, + "options": { + "orientation": "horizontal", + "displayMode": "gradient", + "reduceOptions": { + "values": true, + "calcs": [], + "fields": "/^value$/" + }, + "showUnfilled": true + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "type": "json", + "source": "inline", + "format": "table", + "parser": "simple", + "root_selector": "", + "data": "[{\"name\":\"US · ICP\",\"value\":647},{\"name\":\"Ukraine · team\",\"value\":653},{\"name\":\"India / China · off-ICP\",\"value\":184},{\"name\":\"Other · off-target\",\"value\":717}]", + "columns": [ + { + "selector": "name", + "text": "name", + "type": "string" + }, + { + "selector": "value", + "text": "value", + "type": "number" + } + ], + "filters": [] + } + ] + }, + { + "id": 5, + "type": "bargauge", + "title": "Followers by ICP bucket (base)", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 1 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-BlPu" + } + }, + "overrides": [] + }, + "options": { + "orientation": "horizontal", + "displayMode": "gradient", + "reduceOptions": { + "values": true, + "calcs": [], + "fields": "/^value$/" + }, + "showUnfilled": true + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "type": "json", + "source": "inline", + "format": "table", + "parser": "simple", + "root_selector": "", + "data": "[{\"name\":\"US · ICP\",\"value\":356},{\"name\":\"Ukraine · team\",\"value\":154},{\"name\":\"India / China · off-ICP\",\"value\":57},{\"name\":\"Other · off-target\",\"value\":270}]", + "columns": [ + { + "selector": "name", + "text": "name", + "type": "string" + }, + { + "selector": "value", + "text": "value", + "type": "number" + } + ], + "filters": [] + } + ] + }, + { + "id": 6, + "type": "barchart", + "title": "US · ICP share of visitors by month", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 7 + }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "custom": { + "lineWidth": 1, + "fillOpacity": 80 + } + }, + "overrides": [] + }, + "options": { + "orientation": "auto", + "xTickLabelRotation": 0, + "showValue": "auto", + "stacking": "none", + "legend": { + "showLegend": true, + "placement": "bottom" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "type": "json", + "source": "inline", + "format": "table", + "parser": "simple", + "root_selector": "", + "data": "[{\"month\":\"2026-03\",\"us\":65,\"team\":205,\"anti\":96,\"other\":207,\"icp_pct\":11.3,\"anti_pct\":16.8,\"total\":573},{\"month\":\"2026-04\",\"us\":68,\"team\":73,\"anti\":24,\"other\":83,\"icp_pct\":27.4,\"anti_pct\":9.7,\"total\":248},{\"month\":\"2026-05\",\"us\":113,\"team\":98,\"anti\":0,\"other\":55,\"icp_pct\":42.5,\"anti_pct\":0,\"total\":266},{\"month\":\"2026-06\",\"us\":326,\"team\":123,\"anti\":27,\"other\":136,\"icp_pct\":53.3,\"anti_pct\":4.4,\"total\":612},{\"month\":\"2026-07\",\"us\":75,\"team\":127,\"anti\":37,\"other\":154,\"icp_pct\":19.1,\"anti_pct\":9.4,\"total\":393},{\"month\":\"2026-08\",\"us\":0,\"team\":27,\"anti\":0,\"other\":82,\"icp_pct\":0,\"anti_pct\":0,\"total\":109}]", + "columns": [ + { + "selector": "month", + "text": "month", + "type": "string" + }, + { + "selector": "icp_pct", + "text": "icp_pct", + "type": "number" + } + ], + "filters": [] + } + ] + }, + { + "id": 7, + "type": "barchart", + "title": "Visitor geography by month", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 7 + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "custom": { + "lineWidth": 1, + "fillOpacity": 80 + } + }, + "overrides": [] + }, + "options": { + "orientation": "auto", + "xTickLabelRotation": 0, + "showValue": "auto", + "stacking": "normal", + "legend": { + "showLegend": true, + "placement": "bottom" + } + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "type": "json", + "source": "inline", + "format": "table", + "parser": "simple", + "root_selector": "", + "data": "[{\"month\":\"2026-03\",\"us\":65,\"team\":205,\"anti\":96,\"other\":207,\"icp_pct\":11.3,\"anti_pct\":16.8,\"total\":573},{\"month\":\"2026-04\",\"us\":68,\"team\":73,\"anti\":24,\"other\":83,\"icp_pct\":27.4,\"anti_pct\":9.7,\"total\":248},{\"month\":\"2026-05\",\"us\":113,\"team\":98,\"anti\":0,\"other\":55,\"icp_pct\":42.5,\"anti_pct\":0,\"total\":266},{\"month\":\"2026-06\",\"us\":326,\"team\":123,\"anti\":27,\"other\":136,\"icp_pct\":53.3,\"anti_pct\":4.4,\"total\":612},{\"month\":\"2026-07\",\"us\":75,\"team\":127,\"anti\":37,\"other\":154,\"icp_pct\":19.1,\"anti_pct\":9.4,\"total\":393},{\"month\":\"2026-08\",\"us\":0,\"team\":27,\"anti\":0,\"other\":82,\"icp_pct\":0,\"anti_pct\":0,\"total\":109}]", + "columns": [ + { + "selector": "month", + "text": "month", + "type": "string" + }, + { + "selector": "us", + "text": "us", + "type": "number" + }, + { + "selector": "team", + "text": "team", + "type": "number" + }, + { + "selector": "anti", + "text": "anti", + "type": "number" + }, + { + "selector": "other", + "text": "other", + "type": "number" + } + ], + "filters": [] + } + ] + }, + { + "id": 8, + "type": "row", + "title": "Selected month — pick $month above", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "collapsed": false, + "panels": [] + }, + { + "id": 9, + "type": "stat", + "title": "US · ICP share ($month)", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "gridPos": { + "h": 5, + "w": 6, + "x": 0, + "y": 16 + }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "decimals": 1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 30 + }, + { + "color": "green", + "value": 50 + } + ] + } + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "values": false, + "calcs": [ + "lastNotNull" + ], + "fields": "/^icp_pct$/" + }, + "textMode": "value", + "colorMode": "value", + "graphMode": "none" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "type": "json", + "source": "inline", + "format": "table", + "parser": "backend", + "root_selector": "", + "data": "[{\"month\":\"2026-03\",\"us\":65,\"team\":205,\"anti\":96,\"other\":207,\"icp_pct\":11.3,\"anti_pct\":16.8,\"total\":573},{\"month\":\"2026-04\",\"us\":68,\"team\":73,\"anti\":24,\"other\":83,\"icp_pct\":27.4,\"anti_pct\":9.7,\"total\":248},{\"month\":\"2026-05\",\"us\":113,\"team\":98,\"anti\":0,\"other\":55,\"icp_pct\":42.5,\"anti_pct\":0,\"total\":266},{\"month\":\"2026-06\",\"us\":326,\"team\":123,\"anti\":27,\"other\":136,\"icp_pct\":53.3,\"anti_pct\":4.4,\"total\":612},{\"month\":\"2026-07\",\"us\":75,\"team\":127,\"anti\":37,\"other\":154,\"icp_pct\":19.1,\"anti_pct\":9.4,\"total\":393},{\"month\":\"2026-08\",\"us\":0,\"team\":27,\"anti\":0,\"other\":82,\"icp_pct\":0,\"anti_pct\":0,\"total\":109}]", + "columns": [ + { + "selector": "month", + "text": "month", + "type": "string" + }, + { + "selector": "icp_pct", + "text": "icp_pct", + "type": "number" + } + ], + "filters": [], + "filterExpression": "month == \"${month}\"" + } + ] + }, + { + "id": 10, + "type": "stat", + "title": "India / China share ($month)", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "gridPos": { + "h": 5, + "w": 6, + "x": 6, + "y": 16 + }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "decimals": 1 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "values": false, + "calcs": [ + "lastNotNull" + ], + "fields": "/^anti_pct$/" + }, + "textMode": "value", + "colorMode": "none", + "graphMode": "none" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "type": "json", + "source": "inline", + "format": "table", + "parser": "backend", + "root_selector": "", + "data": "[{\"month\":\"2026-03\",\"us\":65,\"team\":205,\"anti\":96,\"other\":207,\"icp_pct\":11.3,\"anti_pct\":16.8,\"total\":573},{\"month\":\"2026-04\",\"us\":68,\"team\":73,\"anti\":24,\"other\":83,\"icp_pct\":27.4,\"anti_pct\":9.7,\"total\":248},{\"month\":\"2026-05\",\"us\":113,\"team\":98,\"anti\":0,\"other\":55,\"icp_pct\":42.5,\"anti_pct\":0,\"total\":266},{\"month\":\"2026-06\",\"us\":326,\"team\":123,\"anti\":27,\"other\":136,\"icp_pct\":53.3,\"anti_pct\":4.4,\"total\":612},{\"month\":\"2026-07\",\"us\":75,\"team\":127,\"anti\":37,\"other\":154,\"icp_pct\":19.1,\"anti_pct\":9.4,\"total\":393},{\"month\":\"2026-08\",\"us\":0,\"team\":27,\"anti\":0,\"other\":82,\"icp_pct\":0,\"anti_pct\":0,\"total\":109}]", + "columns": [ + { + "selector": "month", + "text": "month", + "type": "string" + }, + { + "selector": "anti_pct", + "text": "anti_pct", + "type": "number" + } + ], + "filters": [], + "filterExpression": "month == \"${month}\"" + } + ] + }, + { + "id": 11, + "type": "stat", + "title": "US visitors ($month)", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "gridPos": { + "h": 5, + "w": 6, + "x": 12, + "y": 16 + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "values": false, + "calcs": [ + "lastNotNull" + ], + "fields": "/^us$/" + }, + "textMode": "value", + "colorMode": "none", + "graphMode": "none" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "type": "json", + "source": "inline", + "format": "table", + "parser": "backend", + "root_selector": "", + "data": "[{\"month\":\"2026-03\",\"us\":65,\"team\":205,\"anti\":96,\"other\":207,\"icp_pct\":11.3,\"anti_pct\":16.8,\"total\":573},{\"month\":\"2026-04\",\"us\":68,\"team\":73,\"anti\":24,\"other\":83,\"icp_pct\":27.4,\"anti_pct\":9.7,\"total\":248},{\"month\":\"2026-05\",\"us\":113,\"team\":98,\"anti\":0,\"other\":55,\"icp_pct\":42.5,\"anti_pct\":0,\"total\":266},{\"month\":\"2026-06\",\"us\":326,\"team\":123,\"anti\":27,\"other\":136,\"icp_pct\":53.3,\"anti_pct\":4.4,\"total\":612},{\"month\":\"2026-07\",\"us\":75,\"team\":127,\"anti\":37,\"other\":154,\"icp_pct\":19.1,\"anti_pct\":9.4,\"total\":393},{\"month\":\"2026-08\",\"us\":0,\"team\":27,\"anti\":0,\"other\":82,\"icp_pct\":0,\"anti_pct\":0,\"total\":109}]", + "columns": [ + { + "selector": "month", + "text": "month", + "type": "string" + }, + { + "selector": "us", + "text": "us", + "type": "number" + } + ], + "filters": [], + "filterExpression": "month == \"${month}\"" + } + ] + }, + { + "id": 12, + "type": "stat", + "title": "India / China visitors ($month)", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "gridPos": { + "h": 5, + "w": 6, + "x": 18, + "y": 16 + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "reduceOptions": { + "values": false, + "calcs": [ + "lastNotNull" + ], + "fields": "/^anti$/" + }, + "textMode": "value", + "colorMode": "none", + "graphMode": "none" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "yesoreyeram-infinity-datasource", + "uid": "grafanacloud-infinity" + }, + "type": "json", + "source": "inline", + "format": "table", + "parser": "backend", + "root_selector": "", + "data": "[{\"month\":\"2026-03\",\"us\":65,\"team\":205,\"anti\":96,\"other\":207,\"icp_pct\":11.3,\"anti_pct\":16.8,\"total\":573},{\"month\":\"2026-04\",\"us\":68,\"team\":73,\"anti\":24,\"other\":83,\"icp_pct\":27.4,\"anti_pct\":9.7,\"total\":248},{\"month\":\"2026-05\",\"us\":113,\"team\":98,\"anti\":0,\"other\":55,\"icp_pct\":42.5,\"anti_pct\":0,\"total\":266},{\"month\":\"2026-06\",\"us\":326,\"team\":123,\"anti\":27,\"other\":136,\"icp_pct\":53.3,\"anti_pct\":4.4,\"total\":612},{\"month\":\"2026-07\",\"us\":75,\"team\":127,\"anti\":37,\"other\":154,\"icp_pct\":19.1,\"anti_pct\":9.4,\"total\":393},{\"month\":\"2026-08\",\"us\":0,\"team\":27,\"anti\":0,\"other\":82,\"icp_pct\":0,\"anti_pct\":0,\"total\":109}]", + "columns": [ + { + "selector": "month", + "text": "month", + "type": "string" + }, + { + "selector": "anti", + "text": "anti", + "type": "number" + } + ], + "filters": [], + "filterExpression": "month == \"${month}\"" + } + ] + }, + { + "id": 13, "type": "row", "title": "Company Page — monthly", "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 0 + "y": 21 }, "collapsed": false, "panels": [] }, { - "id": 2, + "id": 14, "type": "barchart", "title": "Page views & unique visitors", "datasource": { @@ -45,7 +800,7 @@ "h": 8, "w": 12, "x": 0, - "y": 1 + "y": 22 }, "fieldConfig": { "defaults": { @@ -102,7 +857,7 @@ ] }, { - "id": 3, + "id": 15, "type": "barchart", "title": "New followers", "datasource": { @@ -113,7 +868,7 @@ "h": 8, "w": 6, "x": 12, - "y": 1 + "y": 22 }, "fieldConfig": { "defaults": { @@ -165,7 +920,7 @@ ] }, { - "id": 4, + "id": 16, "type": "barchart", "title": "Post impressions", "datasource": { @@ -176,7 +931,7 @@ "h": 8, "w": 6, "x": 18, - "y": 1 + "y": 22 }, "fieldConfig": { "defaults": { @@ -228,7 +983,7 @@ ] }, { - "id": 5, + "id": 17, "type": "table", "title": "Monthly metrics", "datasource": { @@ -239,7 +994,7 @@ "h": 8, "w": 24, "x": 0, - "y": 9 + "y": 30 }, "fieldConfig": { "defaults": {}, @@ -313,20 +1068,20 @@ ] }, { - "id": 6, + "id": 18, "type": "row", "title": "Audience (12-month snapshot)", "gridPos": { "h": 1, "w": 24, "x": 0, - "y": 17 + "y": 38 }, "collapsed": false, "panels": [] }, { - "id": 7, + "id": 19, "type": "bargauge", "title": "Visitors by seniority", "datasource": { @@ -337,7 +1092,7 @@ "h": 8, "w": 12, "x": 0, - "y": 18 + "y": 39 }, "fieldConfig": { "defaults": { @@ -387,7 +1142,7 @@ ] }, { - "id": 8, + "id": 20, "type": "bargauge", "title": "Visitors by industry", "datasource": { @@ -398,7 +1153,7 @@ "h": 8, "w": 12, "x": 12, - "y": 18 + "y": 39 }, "fieldConfig": { "defaults": { @@ -448,7 +1203,7 @@ ] }, { - "id": 9, + "id": 21, "type": "bargauge", "title": "Followers by seniority", "datasource": { @@ -459,7 +1214,7 @@ "h": 8, "w": 12, "x": 0, - "y": 26 + "y": 47 }, "fieldConfig": { "defaults": { @@ -509,7 +1264,7 @@ ] }, { - "id": 10, + "id": 22, "type": "bargauge", "title": "Followers by industry", "datasource": { @@ -520,7 +1275,7 @@ "h": 8, "w": 12, "x": 12, - "y": 26 + "y": 47 }, "fieldConfig": { "defaults": { diff --git a/dashboards/li-stats/page/geo-monthly.json b/dashboards/li-stats/page/geo-monthly.json new file mode 100644 index 0000000..babc3b8 --- /dev/null +++ b/dashboards/li-stats/page/geo-monthly.json @@ -0,0 +1,58 @@ +{ + "months": { + "2026-03": { + "us": 65, + "team": 205, + "anti": 96, + "other": 207, + "icp_pct": 11.3, + "anti_pct": 16.8, + "total": 573 + }, + "2026-04": { + "us": 68, + "team": 73, + "anti": 24, + "other": 83, + "icp_pct": 27.4, + "anti_pct": 9.7, + "total": 248 + }, + "2026-05": { + "us": 113, + "team": 98, + "anti": 0, + "other": 55, + "icp_pct": 42.5, + "anti_pct": 0.0, + "total": 266 + }, + "2026-06": { + "us": 326, + "team": 123, + "anti": 27, + "other": 136, + "icp_pct": 53.3, + "anti_pct": 4.4, + "total": 612 + }, + "2026-07": { + "us": 75, + "team": 127, + "anti": 37, + "other": 154, + "icp_pct": 19.1, + "anti_pct": 9.4, + "total": 393 + }, + "2026-08": { + "us": 0, + "team": 27, + "anti": 0, + "other": 82, + "icp_pct": 0.0, + "anti_pct": 0.0, + "total": 109 + } + } +} \ No newline at end of file diff --git a/dashboards/li-stats/page/monthly.json b/dashboards/li-stats/page/monthly.json index 066cecb..f3728c1 100644 --- a/dashboards/li-stats/page/monthly.json +++ b/dashboards/li-stats/page/monthly.json @@ -1,6 +1,6 @@ { "source": "linkedin-page-admin-analytics-xls", - "generated_at": "2026-08-19T19:10:59Z", + "generated_at": "2026-08-20T17:44:42Z", "months": { "2026-03": { "page_views": 961, @@ -406,5 +406,103 @@ 15 ] ] + }, + "geography": { + "visitors": { + "buckets": { + "US": 1552, + "TEAM": 1681, + "ANTI": 560, + "OTHER": 2260 + }, + "total": 6053, + "icp_pct": 25.6, + "anti_pct": 9.3, + "countries": { + "Ukraine": 1681.0, + "United States": 1552.0, + "India": 560.0, + "Poland": 509.0, + "Canada": 252.0, + "Pakistan": 226.0, + "Spain": 160.0, + "United Kingdom": 129.0, + "France": 100.0, + "Texas Metropolitan Area": 98.0, + "Germany": 96.0, + "Portugal": 84.0, + "Montenegro": 59.0, + "Argentina": 52.0, + "Indonesia": 47.0, + "Czechia": 44.0, + "Philippines": 43.0, + "Bulgaria": 36.0, + "Netherlands": 33.0, + "Romania": 32.0, + "Vietnam": 32.0, + "Serbia": 29.0, + "North Macedonia": 28.0, + "Estonia": 26.0, + "Ireland": 22.0, + "Brazil": 18.0, + "Azerbaijan": 18.0, + "Georgia": 18.0, + "Israel": 17.0, + "Virginia Metropolitan Area": 14.0, + "Türkiye": 13.0, + "Austria": 13.0, + "Mexico": 12.0 + } + }, + "followers": { + "buckets": { + "US": 356, + "TEAM": 154, + "ANTI": 57, + "OTHER": 270 + }, + "total": 837, + "icp_pct": 42.5, + "anti_pct": 6.8, + "countries": { + "United States": 356.0, + "Ukraine": 154.0, + "India": 57.0, + "Poland": 42.0, + "Canada": 35.0, + "Pakistan": 28.0, + "United Kingdom": 13.0, + "Montenegro": 13.0, + "Serbia": 13.0, + "Germany": 10.0, + "Texas Metropolitan Area": 9.0, + "Ohio Metropolitan Area": 7.0, + "Portugal": 6.0, + "Australia": 6.0, + "South Africa": 6.0, + "Brazil": 5.0, + "France": 5.0, + "Georgia": 5.0, + "Egypt": 5.0, + "Ireland": 5.0, + "Sri Lanka": 4.0, + "Türkiye": 4.0, + "Netherlands": 4.0, + "Oregon Metropolitan Area": 4.0, + "Kenya": 4.0, + "Czechia": 4.0, + "Sweden": 4.0, + "Spain": 3.0, + "Denmark": 3.0, + "Bulgaria": 3.0, + "Azerbaijan": 3.0, + "Armenia": 3.0, + "Virginia Metropolitan Area": 3.0, + "Ethiopia": 3.0, + "Argentina": 3.0, + "Chile": 3.0, + "Tanzania": 2.0 + } + } } } \ No newline at end of file