Skip to content

Draft: feat(stats): add aggregated stats routes for articles, authors and crossed treemap

Simon APARTIS requested to merge refacto/dashboard-routes into main

refacto/dashboard-routes — Stats agrégées & treemap croisée

Contexte

Ajout des routes backend pour alimenter le dashboard de dataviz bibliométrique (ReviewPilot). Le dashboard expose trois colonnes : statistiques auteurs, statistiques articles, et statistiques croisées (treemap imbriquée à 2 ou 3 niveaux).


Fichiers ajoutés

routes/
  aggregatedStats.articles.route.ts   — stats agrégées de la collection articles
  aggregatedStats.authors.route.ts    — stats agrégées de la collection authors
  aggregatedStats.crossed.route.ts    — treemap croisée article × author

utils/
  aggregatedStats.fieldMap.ts         — whitelist et métadonnées des champs autorisés
  aggregatedStats.toTree.ts           — reshape du résultat MongoDB en arbre imbriqué
  aggregatedStats.SimpleCache.ts      — cache mémoire générique avec TTL

Routes exposées

Méthode Endpoint Description
GET /api/stats/articles Stats agrégées articles via $facet
GET /api/stats/authors Stats agrégées authors via $facet
GET /api/stats/crossed Treemap croisée avec jointure article↔️author
POST /api/stats/articles/refresh Invalide le cache articles du projet
POST /api/stats/authors/refresh Invalide le cache authors du projet
POST /api/stats/crossed/refresh Invalide tous les caches croisés du projet

Logique métier

Routes articles et authors

Chaque route exécute un pipeline MongoDB $match + $facet en une seule requête. Le $facet calcule toutes les répartitions en parallèle sur le même ensemble de documents filtrés par projectId.

Route crossed

Pipeline MongoDB dynamique construit à partir des paramètres de la requête :

  1. $match sur le projectId
  2. $lookup pour joindre le first author de chaque article — uniquement si au moins un des champs demandés appartient à la collection authors
  3. $match des filtres (valeurs sélectionnées par l'utilisateur sur les chips)
  4. Dénormalisation des champs multivalués avec pondération : un article avec N valeurs pour un champ tableau contribue 1/N à chacune — la somme des counts reste toujours égale au nombre d'articles
  5. $group dynamique selon les niveaux demandés
  6. Reshape du résultat plat en arbre imbriqué via toTree()

Pondération des champs multivalués

Les champs barriers, methodology, dataTypesDiscussed, positionOnDataOpenAccess sont des tableaux. Sans pondération, un article avec 3 barrières pèserait 3 fois plus qu'un article avec 1 barrière. La pondération normalise : chaque article pèse toujours 1, réparti équitablement entre ses valeurs.


Query params — route crossed

Param Requis Exemple Description
projectId oui abc123 ObjectId du projet
levels oui barriers,gender 2 ou 3 champs séparés par virgule
filters[field] non filters[language]=FR,EN Valeurs à inclure, séparées par virgule
refresh non true Force le recalcul en bypassant le cache

Champs disponibles pour la treemap croisée

Articleslanguage, openAccess, dataTypesDiscussed, methodology, barriers, pubyear, selectedSubfield, objectFocus, positionOnDataOpenAccess

Authorsgender, status

isArray est inféré directement depuis le schéma Mongoose — pas déclaré manuellement.


Cache

Cache mémoire in-process avec TTL d'1 heure, partagé entre les trois routes via SimpleCache<T>. Chaque combinaison projectId + levels + filters est cachée séparément pour la route crossed. Nettoyage automatique des entrées expirées toutes les 10 minutes.

⚠️ Cache in-process : non partagé entre instances. Si scaling horizontal → migrer vers Redis.


Réponse — route crossed

{
  totalCorpus:   number     // total articles du projet sans aucun filtre
  totalFiltered: number     // total articles après application des filters
  levels:        string[]   // champs dans l'ordre des niveaux
  tree:          TreeNode[] // arbre imbriqué pour le rendu de la treemap
}

type TreeNode = {
  label:    string | null   // null si la valeur était absente dans MongoDB
  count:    number          // arrondi à 2 décimales, peut être fractionnaire
  children: TreeNode | null // null aux feuilles (dernier niveau)
}

Merge request reports

Loading