hf-forum-dashboard / index.html
severo's picture
severo HF staff
Upload folder using huggingface_hub
8f9ed1f verified
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="generator" content="Observable Framework v1.11.0">
<title>Forum Dashboard | Observable Forum Dashboard</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="preload" as="style" href="./_observablehq/theme-air,near-midnight,alt,wide.f6ca92af.css">
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&amp;display=swap" crossorigin>
<link rel="stylesheet" type="text/css" href="./_observablehq/theme-air,near-midnight,alt,wide.f6ca92af.css">
<link rel="modulepreload" href="./_observablehq/client.4d3b1f74.js">
<link rel="modulepreload" href="./_observablehq/runtime.c4c9c1af.js">
<link rel="modulepreload" href="./_observablehq/stdlib.b89f0aee.js">
<link rel="modulepreload" href="./_node/[email protected]/index.f89e3560.js">
<link rel="modulepreload" href="./_npm/[email protected]/407f7a1f.js">
<link rel="modulepreload" href="./_npm/@observablehq/[email protected]/e828d8c8.js">
<link rel="modulepreload" href="./_node/[email protected]/index.4106013c.js">
<link rel="modulepreload" href="./_npm/[email protected]/7055d4c5.js">
<link rel="modulepreload" href="./_npm/[email protected]/c68fbd73.js">
<link rel="modulepreload" href="./_npm/[email protected]/a62ae5ce.js">
<link rel="modulepreload" href="./_npm/[email protected]/e95f898e.js">
<link rel="modulepreload" href="./_npm/[email protected]/d44feff9.js">
<link rel="modulepreload" href="./_npm/[email protected]/5830b12a.js">
<link rel="modulepreload" href="./_npm/[email protected]/84d7b8e9.js">
<link rel="modulepreload" href="./_npm/[email protected]/2c0cdfa2.js">
<link rel="modulepreload" href="./_npm/[email protected]/626bedc4.js">
<link rel="modulepreload" href="./_npm/[email protected]/00c41b5d.js">
<link rel="modulepreload" href="./_npm/[email protected]/b5f7cdc6.js">
<link rel="modulepreload" href="./_npm/[email protected]/b22c5864.js">
<link rel="modulepreload" href="./_npm/[email protected]/6f15f633.js">
<link rel="modulepreload" href="./_npm/[email protected]/ef1ec490.js">
<link rel="modulepreload" href="./_npm/[email protected]/5e1ff060.js">
<link rel="modulepreload" href="./_npm/[email protected]/5851d7ef.js">
<link rel="modulepreload" href="./_npm/[email protected]/dcd02767.js">
<link rel="modulepreload" href="./_npm/[email protected]/f1db2593.js">
<link rel="modulepreload" href="./_npm/[email protected]/034b7bcb.js">
<link rel="modulepreload" href="./_npm/[email protected]/4bb53638.js">
<link rel="modulepreload" href="./_npm/[email protected]/bbafde58.js">
<link rel="modulepreload" href="./_npm/[email protected]/aa5b35a8.js">
<link rel="modulepreload" href="./_npm/[email protected]/32c7fec2.js">
<link rel="modulepreload" href="./_npm/[email protected]/567840a0.js">
<link rel="modulepreload" href="./_npm/[email protected]/cf9b720b.js">
<link rel="modulepreload" href="./_npm/[email protected]/5dcd62f4.js">
<link rel="modulepreload" href="./_npm/[email protected]/f8e03c56.js">
<link rel="modulepreload" href="./_npm/[email protected]/5bc129e1.js">
<link rel="modulepreload" href="./_npm/[email protected]/19c92b44.js">
<link rel="modulepreload" href="./_npm/[email protected]/f31b5398.js">
<link rel="modulepreload" href="./_npm/[email protected]/8debb4ba.js">
<link rel="modulepreload" href="./_npm/[email protected]/4b0cc581.js">
<link rel="modulepreload" href="./_npm/[email protected]/1ee6c50d.js">
<link rel="modulepreload" href="./_npm/[email protected]/5eed35fd.js">
<link rel="modulepreload" href="./_npm/[email protected]/e67acb27.js">
<link rel="modulepreload" href="./_npm/[email protected]/8ac9039b.js">
<link rel="icon" href="./_file/observable.1af93621.png" type="image/png" sizes="32x32">
<script type="module">
import {define} from "./_observablehq/client.4d3b1f74.js";
import {registerFile} from "./_observablehq/stdlib.b89f0aee.js";
registerFile("./data/categories.csv", {"name":"./data/categories.csv","mimeType":"text/csv","path":"./_file/data/categories.f3fa0523.csv","lastModified":1727299665259,"size":662});
registerFile("./data/config.json", {"name":"./data/config.json","mimeType":"application/json","path":"./_file/data/config.05513390.json","lastModified":1727299086052,"size":50});
registerFile("./data/posts.csv", {"name":"./data/posts.csv","mimeType":"text/csv","path":"./_file/data/posts.36b9d40b.csv","lastModified":1727299664899,"size":6728921});
define({id: "e35ec5a1", inputs: ["FileAttachment"], outputs: ["d3","config","url","posts","categoriesRaw","topics","users","topicsByCategory","categories","tenTopUsers","tenTopAcceptedUsers","NUM_USERS","topAcceptedUsersPerYear","intervals","interval","intervalLabel","color","years"], body: async (FileAttachment) => {
const d3 = await import("./_node/[email protected]/index.f89e3560.js");
const config = await FileAttachment("./data/config.json").json();
const url = config.discourse_url;
const posts = await FileAttachment("./data/posts.csv").csv({ typed: true });
const categoriesRaw = await FileAttachment("./data/categories.csv").csv({
typed: true,
});
const topics = [
...d3
.rollup(
posts,
(v) => ({
topic_id: v[0].topic_id,
category_id: v[0].category_id,
posts: v,
users: new Set(v.map((d) => d.username)),
}),
(d) => d.topic_id
)
.values(),
];
const users = d3.rollup(
posts,
(v) => ({ username: v[0].username, avatar_template: v[0].avatar_template }),
(d) => d.username
);
const topicsByCategory = d3.rollup(
topics,
(v) => v.length,
(d) => d.category_id
);
const categories = categoriesRaw.map((d) => ({
...d,
topics: topicsByCategory.get(d.id) || 0,
}));
const tenTopUsers = d3
.rollups(
posts,
(v) => v.length,
(d) => d.username
)
.sort((a, b) => d3.descending(a[1], b[1]))
.slice(0, 10)
.map((d) => ({
username: d[0],
posts: d[1],
}));
const tenTopAcceptedUsers = d3
.rollups(
posts.filter((d) => d.accepted_answer),
(v) => v.length,
(d) => d.username
)
.sort((a, b) => d3.descending(a[1], b[1]))
.slice(0, 10)
.map((d) => ({
username: d[0],
posts: d[1],
}));
const NUM_USERS = 3;
const topAcceptedUsersPerYear = d3
.rollups(
posts.filter((d) => d.accepted_answer),
(v) => v.length,
(d) => d.created_at.getFullYear(),
(d) => d.username
)
.flatMap(([year, users_stats]) => {
const top_usernames = users_stats
.sort(([_, posts_count_a], [__, posts_count_b]) =>
d3.descending(posts_count_a, posts_count_b)
)
.slice(0, NUM_USERS)
.map(([username]) => username);
return top_usernames.map((username, i) => ({
rank: i + 1,
year,
username,
src: url + users.get(username).avatar_template.replace("{size}", "400"),
}));
});
const intervals = { month: "Month", year: "Year", day: "Day", week: "Week" };
const interval = "month";
const intervalLabel = intervals[interval];
const color = {
users: "#e36209",
posts: "#3b5fc0",
accepted: "green",
};
const years = d3.extent(posts, (d) => d.created_at.getFullYear());
return {d3,config,url,posts,categoriesRaw,topics,users,topicsByCategory,categories,tenTopUsers,tenTopAcceptedUsers,NUM_USERS,topAcceptedUsersPerYear,intervals,interval,intervalLabel,color,years};
}});
define({id: "28e5ebab", mode: "inline", inputs: ["url","display"], body: async (url,display) => {
display(await(
url
))
}});
define({id: "8404f401", mode: "inline", inputs: ["years","display"], body: async (years,display) => {
display(await(
years[0]
))
}});
define({id: "18528b79", mode: "inline", inputs: ["years","display"], body: async (years,display) => {
display(await(
years[1]
))
}});
define({id: "ebe544d8", mode: "inline", inputs: ["topics","display"], body: async (topics,display) => {
display(await(
topics.length.toLocaleString("en-US")
))
}});
define({id: "a9ac2ae4", mode: "inline", inputs: ["posts","display"], body: async (posts,display) => {
display(await(
posts.length.toLocaleString("en-US")
))
}});
define({id: "9f3c82c4", mode: "inline", inputs: ["users","display"], body: async (users,display) => {
display(await(
users.size.toLocaleString("en-US")
))
}});
define({id: "3939618f", inputs: ["Plot","color"], outputs: ["postsMAU"], body: (Plot,color) => {
function postsMAU(data, { width } = {}) {
return Plot.plot({
title: `Monthly active users`,
width,
height: 300,
y: { grid: true, label: `users` },
// color: {...color, legend: true},
marks: [
Plot.lineY(
data,
Plot.binX(
{ y: "distinct" },
{
x: "created_at",
y: "username",
stroke: color.users,
interval: "month",
tip: true,
}
)
),
Plot.ruleY([0]),
],
});
}
return {postsMAU};
}});
define({id: "694a2e8e", inputs: ["Plot","interval","color"], outputs: ["postsTimeline"], body: (Plot,interval,color) => {
function postsTimeline(data, { width } = {}) {
return Plot.plot({
title: `Posts created every ${interval}`,
width,
height: 300,
y: { grid: true, label: "posts" },
// color: {...color, legend: true},
marks: [
Plot.lineY(
data,
Plot.binX(
{ y: "count" },
{ x: "created_at", stroke: color.posts, interval, tip: true }
)
),
Plot.ruleY([0]),
],
});
}
return {postsTimeline};
}});
define({id: "df5d29c1", mode: "inline", inputs: ["resize","postsMAU","posts","display"], body: async (resize,postsMAU,posts,display) => {
display(await(
resize((width) => postsMAU(posts, {width}))
))
}});
define({id: "8db957c1", mode: "inline", inputs: ["resize","postsTimeline","posts","display"], body: async (resize,postsTimeline,posts,display) => {
display(await(
resize((width) => postsTimeline(posts, {width}))
))
}});
define({id: "35e85ade", inputs: ["Plot"], outputs: ["categoriesChart"], body: (Plot) => {
function categoriesChart(data, { width }) {
return Plot.plot({
title: "Most active categories",
width,
height: 300,
marginTop: 0,
marginLeft: 150,
x: { grid: true, label: "Topics" },
y: { label: null },
marks: [
Plot.barX(data, {
x: "topics",
y: "name",
fill: (d) => "#" + d.color,
tip: true,
sort: { y: "-x" },
}),
Plot.ruleX([0]),
],
});
}
return {categoriesChart};
}});
define({id: "4933adab", inputs: ["Plot","d3"], outputs: ["answersPerTopicChart"], body: (Plot,d3) => {
function answersPerTopicChart(data, { width }) {
return Plot.plot({
title: "Answers per topic",
width,
height: 300,
marginTop: 0,
marginLeft: 150,
x: { grid: true, label: "Proportion (%)", percent: true },
y: { label: "Answers", reverse: true },
marks: [
Plot.rectX(
data,
Plot.binY(
{ x: "proportion" },
{
y: {
value: (d) => d.posts.length - 1,
thresholds: d3.range(-0.5, 10.5),
},
fill: (d) => (d.posts.length === 1 ? "#AAA" : "#DDD"),
tip: true,
}
)
),
Plot.ruleX([0]),
],
});
}
return {answersPerTopicChart};
}});
define({id: "e3ec0bba", mode: "inline", inputs: ["resize","categoriesChart","categories","display"], body: async (resize,categoriesChart,categories,display) => {
display(await(
resize((width) => categoriesChart(categories, {width}))
))
}});
define({id: "4e86c617", mode: "inline", inputs: ["resize","answersPerTopicChart","topics","display"], body: async (resize,answersPerTopicChart,topics,display) => {
display(await(
resize((width) => answersPerTopicChart(topics, {width}))
))
}});
define({id: "1e4bc9aa", inputs: ["Plot","color"], outputs: ["topUsersChart","topAcceptedUsersChart","topAcceptedUsersPerYearChart"], body: (Plot,color) => {
function topUsersChart(data, { width }) {
return Plot.plot({
title: "Top posters",
width,
height: 300,
marginTop: 0,
marginLeft: 150,
x: { grid: true, label: "Posts" },
y: { label: null },
marks: [
Plot.barX(data, {
x: "posts",
y: "username",
fill: color.posts,
tip: true,
sort: { y: "-x" },
}),
Plot.ruleX([0]),
],
});
}
function topAcceptedUsersChart(data, { width }) {
return Plot.plot({
title: "Users with most accepted answers",
width,
height: 300,
marginTop: 0,
marginLeft: 150,
x: { grid: true, label: "Posts" },
y: { label: null },
marks: [
Plot.barX(data, {
x: "posts",
y: "username",
fill: color.accepted,
tip: true,
sort: { y: "-x" },
}),
Plot.ruleX([0]),
],
});
}
function topAcceptedUsersPerYearChart(data, { width }) {
return Plot.plot({
title: "User with most accepted answers per year",
width,
height: 300,
marginTop: 0,
marginLeft: 50,
marginRight: 50,
x: { grid: false, label: "Year" },
y: { grid: false, ticks: false, label: null, domain: [4, 0] },
color: { domain: [1, 2, 3], range: ["#FFD700", "#C0C0C0", "#CD7F32"] },
marks: [
Plot.image(data, {
x: (d) => new Date(d.year + "-01-01"),
y: "rank",
src: "src",
tip: true,
r: 20,
preserveAspectRatio: "xMidYMin slice",
title: (d) => `${d.username} - rank ${d.rank} (${d.year})`,
}),
Plot.dot(data, {
x: (d) => new Date(d.year + "-01-01"),
y: "rank",
r: 20,
stroke: "rank",
strokeWidth: 2,
}),
Plot.ruleY([4]),
],
});
}
return {topUsersChart,topAcceptedUsersChart,topAcceptedUsersPerYearChart};
}});
define({id: "0fa544ed", mode: "inline", inputs: ["resize","topUsersChart","tenTopUsers","display"], body: async (resize,topUsersChart,tenTopUsers,display) => {
display(await(
resize((width) => topUsersChart(tenTopUsers, {width}))
))
}});
define({id: "04e33544", mode: "inline", inputs: ["resize","topAcceptedUsersPerYearChart","topAcceptedUsersPerYear","display"], body: async (resize,topAcceptedUsersPerYearChart,topAcceptedUsersPerYear,display) => {
display(await(
resize((width) => topAcceptedUsersPerYearChart(topAcceptedUsersPerYear, {width}))
))
}});
define({id: "28e5ebab-1", mode: "inline", inputs: ["url","display"], body: async (url,display) => {
display(await(
url
))
}});
define({id: "78a1e913", mode: "inline", inputs: ["d3","posts","display"], body: async (d3,posts,display) => {
display(await(
d3.min(posts, d => d.created_at).getFullYear()
))
}});
define({id: "93beff2d", mode: "inline", inputs: ["d3","posts","display"], body: async (d3,posts,display) => {
display(await(
d3.max(posts, d => d.created_at).getFullYear()
))
}});
</script>
<div id="observablehq-center">
<main id="observablehq-main" class="observablehq">
<h1 id="forum-dashboard" tabindex="-1"><a class="observablehq-header-anchor" href="#forum-dashboard">Forum Dashboard</a></h1>
<!-- Load and transform the data -->
<div class="observablehq observablehq--block"><!--:e35ec5a1:--></div>
<p>Statistics for the <a href="./$%7Burl%7D"><observablehq-loading></observablehq-loading><!--:28e5ebab:--></a> forum.</p>
<h2 id="trends-over-time" tabindex="-1"><a class="observablehq-header-anchor" href="#trends-over-time">Trends over time</a></h2>
<!-- Cards with big numbers -->
<div class="grid grid-cols-4">
<div class="card">
<h2>Years</h2>
<span class="big"><observablehq-loading></observablehq-loading><!--:8404f401:--><observablehq-loading></observablehq-loading><!--:18528b79:--></span>
</div>
<div class="card">
<h2>Topics</h2>
<span class="big"><observablehq-loading></observablehq-loading><!--:ebe544d8:--></span>
<p>Topics are the forum's questions, or threads.</p>
</div>
<div class="card">
<h2>Posts</h2>
<span class="big"><observablehq-loading></observablehq-loading><!--:a9ac2ae4:--></span>
<p>The posts are comments in a thread, ie the answers to a question. The topics are not included.</p>
</div>
<!-- <div class="card">
<h2>Posts per topic</h2>
<span class="big">${(posts.length / topics.size).toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}</span>
</div>
<div class="card">
<h2>Users per topic</h2>
<span class="big">${(d3.sum(topics, d => d[1].users.size) / topics.size).toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}</span>
</div> -->
<!-- <div class="card">
<h2>Categories</h2>
<span class="big">${categories.length.toLocaleString("en-US")}</span>
</div> -->
<div class="card">
<h2>Users</h2>
<span class="big"><observablehq-loading></observablehq-loading><!--:9f3c82c4:--></span>
</div>
</div>
<!-- Plot of monthly active users -->
<div class="observablehq observablehq--block"><!--:3939618f:--></div>
<!-- Plot of posts history -->
<div class="observablehq observablehq--block"><!--:694a2e8e:--></div>
<div class="grid grid-cols-2">
<div class="card">
<observablehq-loading></observablehq-loading><!--:df5d29c1:-->
</div>
<div class="card">
<observablehq-loading></observablehq-loading><!--:8db957c1:-->
</div>
</div>
<h2 id="topics" tabindex="-1"><a class="observablehq-header-anchor" href="#topics">Topics</a></h2>
<!-- Plot of topics per category -->
<div class="observablehq observablehq--block"><!--:35e85ade:--></div>
<!-- Posts per topic -->
<div class="observablehq observablehq--block"><!--:4933adab:--></div>
<div class="grid grid-cols-2">
<div class="card">
<observablehq-loading></observablehq-loading><!--:e3ec0bba:-->
</div>
<div class="card">
<observablehq-loading></observablehq-loading><!--:4e86c617:-->
</div>
</div>
<h2 id="users" tabindex="-1"><a class="observablehq-header-anchor" href="#users">Users</a></h2>
<!-- Top users -->
<div class="observablehq observablehq--block"><!--:1e4bc9aa:--></div>
<div class="grid grid-cols-2">
<div class="card">
<observablehq-loading></observablehq-loading><!--:0fa544ed:-->
</div>
<!--
<div class="card">
${resize((width) => topAcceptedUsersChart(tenTopAcceptedUsers, {width}))}
</div>
-->
<div class="card">
<observablehq-loading></observablehq-loading><!--:04e33544:-->
</div>
</div>
<h2 id="data-and-code" tabindex="-1"><a class="observablehq-header-anchor" href="#data-and-code">Data and code</a></h2>
<p>The data of <a href="./$%7Burl%7D"><observablehq-loading></observablehq-loading><!--:28e5ebab-1:--></a> from <observablehq-loading></observablehq-loading><!--:78a1e913:--> to <observablehq-loading></observablehq-loading><!--:93beff2d:--> was downloaded using the <a href="https://docs.discourse.org/" target="_blank" rel="noopener noreferrer">Discourse API</a>.</p>
<p>The code is available on <a href="https://github.com/severo/discourse-dashboard" target="_blank" rel="noopener noreferrer">GitHub</a>.</p>
</main>
<footer id="observablehq-footer">
<div>Built with <a href="https://observablehq.com/" target="_blank" rel="noopener noreferrer">Observable</a> on <a title="2024-09-25T21:18:02">Sep 25, 2024</a>.</div>
</footer>
</div>