Documentation

  1. Accueil
  2. Docs
  3. Documentation
  4. Principales fonctionnalit...
  5. Les formats
  6. Texte

Texte

Badge pour identifiant

Ce format permet d’afficher un identifiant pérenne (soit un DOI, soit un PMID), et d’y ajouter un lien vers la ressource.

=> Exemple

=> Affichage

Balises HTML

Le format HTML permet d’afficher dans l’application un bloc de HTML.

Attention : Depuis la version 15 de LODEX, des modifications ont été effectuées pour sécuriser les instances. Ces changements impliquent deux choses vis-à-vis de ce format :

  • éviter de définir des formats HTML dans un « loader »
  • utiliser des balises « englobantes » par exemple de type <div class = « xxxx »>…<\div> pour appliquer différents styles dans le même format HTML ou EJS (HTML + JavaScript)

Chiffres en gras

Routine utilisable

  • count-all

Ce format texte permet de mettre les chiffres du champ en gras et de choisir également une couleur.
Par exemple dans le cas où est affiché le nombre de documents présent dans le jeu de donnée avec comme « valeur arbitraire » dans « comment la valeur est créée » la routine  « /api/run/count-all »

Code

Colore syntaxiquement la valeur du champ en suivant la syntaxe du langage donné en paramètre (parmi XML, JSON, INI, Shell, SQL, Javascript).

=> Exemple

=> Affichage INI

CSS Styles

Le format CSS Styles permet de personnaliser l’apparence des données affichées dans une instance LODEX grâce au langage CSS (Cascading Style Sheets).

Contrairement aux formats HTML ou Markdown qui modifient la structure du contenu, le format CSS Styles agit uniquement sur sa présentation : couleurs, marges, bordures, polices, espacements, etc.

Le format peut être utilisé de deux manières :

  • Champ courant : les règles CSS sont appliquées uniquement au champ sur lequel le format est utilisé.
  • Ensemble de la page : les règles CSS sont injectées dans l’en-tête (<head>) de la page et peuvent être réutilisées par plusieurs champs HTML.

Option « Applique les règles CSS à l'ensemble de la page »

Appliquer des styles au champ courant

Par défaut, les règles CSS sont appliquées uniquement au champ courant.

En appliquant ensuite le format CSS Styles sur ce même champ, il est possible de modifier complètement son apparence.

Pour cela, il suffit de créer un champ contenant les données que l’on souhaite afficher (par exemple en sélectionnant une colonne existante), puis de lui appliquer le format CSS Styles.
On peut ensuite lui appliquer des règles CSS comme par exemple :

{
  background: #fff8e6;
  border: 2px solid #f2c94c;
  border-radius: 8px;
  padding: 1.5rem;
  color: #6b4f00;

  h3 {
    margin-top: 0;
    color: #d18b00;
    border-bottom: 2px solid #f2c94c;
    padding-bottom: .5rem;
  }

  p {
    margin-bottom: 0;
    line-height: 1.7;
  }
}

Le résultat est un encadré jaune appliquée uniquement à ce champ.
Option « Applique les règles CSS au champ courant»

Appliquer des styles à l’ensemble de la page

Pour utiliser des règles CSS globales, il est conseillé de créer un champ dédié uniquement au style.

Ce champ n’a pas vocation à afficher une donnée. Il doit donc rester très minimal :

  • ne pas lui donner de libellé visible
  • ne pas renseigner de valeur arbitraire
  • ne pas ajouter de routine
  • lui appliquer le format CSS Styles
  • activer l’option « Applique les règles CSS à l’ensemble de la page et non au champ courant ».

Les règles CSS définies dans ce champ sont alors injectées dans l’en-tête (<head>) de la page HTML. Elles peuvent ensuite s’appliquer aux balises utilisées dans d’autres champs HTML de l’instance.

Par exemple, dans ce champ dédié au CSS global, on peut définir le style qui sera appliqué à plusieurs champs de la page :

{
  blockquote {
    background: #eaf6ff;
    color: #6941eb;
    padding: 1rem 1.5rem;
    border-left: 5px solid #6941eb;
    border-radius: 6px;
    font-style: italic;
    margin: 1rem 0;
  }

  blockquote footer {
    margin-top: 1rem;
    text-align: right;
    font-style: normal;
    font-weight: bold;
  }
}

Il suffit ensuite de créer plusieurs champs avec le format Texte – Balises HTML contenant des balises <blockquote> :

<blockquote>
  Les données FAIR ne sont pas seulement ouvertes, elles sont aussi faciles à
  trouver, accessibles, interopérables et réutilisables.

  <footer>Principes FAIR</footer>
</blockquote>

Tout les champs utilisant cette balise seront automatiquement mises en forme par le CSS global, sans qu’il soit nécessaire de recopier les règles CSS dans chaque champ HTML.

Option « Applique les règles CSS a plusieurs champs»

Remarque : priorité des règles CSS

Dans certains cas, les éléments affichés par LODEX possèdent déjà leurs propres règles CSS définies par le thème de l’instance.

Par exemple, le titre principal du bandeau est déjà stylé par une règle similaire à :

#banner h1 {
  font-family: "Quicksand", sans-serif;
}

Si l’on souhaite utiliser une autre police, la règle suivante peut ne pas être prise en compte :

#banner h1 {
  font-family: "Anton", sans-serif;
}

En effet, le navigateur applique les règles CSS en tenant compte de leur spécificité et de leur ordre de chargement. Lorsque plusieurs règles concernent le même élément, celle définie par le thème peut rester prioritaire.

Il est alors possible de forcer l’application de la nouvelle règle avec le mot-clé !important :

#banner h1 {
  font-family: "Anton", sans-serif !important;
}

À noter : cette situation concerne principalement les éléments déjà présents dans l’interface de LODEX (bandeau, titres, boutons, etc.). Pour les éléments HTML que vous créez vous-même (par exemple <div>, <blockquote> ou <table>), l’utilisation de !important n’est généralement pas nécessaire.

Exemple d’une liste de valeurs en CSS

Exemple de personnalisation d'une liste de valeurs avec le format CSS Styles

Le format CSS Styles peut également être utilisé comme sous-format du format Texte – Liste de valeurs afin de personnaliser l’apparence de chaque valeur de la liste.

Dans cet exemple, le champ utilise le format Texte – Liste de valeurs avec le type Liste désordonnée (sans puce). Le format Texte – CSS Styles est ensuite appliqué comme sous-format.

Exemple de configuration d'une liste de valeurs avec le format CSS Styles

Les règles CSS suivantes permettent d’afficher chaque valeur sous la forme d’un encadré :

{
  display: block;
  margin: .35rem 0;
  padding: .7rem 1rem;
  background: #ecfdf5;
  border: 1px solid #b7f0d2;
  border-left: 6px solid #0f766e;
  border-radius: 8px;
  color: #2f2f2f;
  font-size: 1.05rem;
  font-weight: 700;
  box-shadow: 0 2px 6px rgba(0,0,0,.05);
}

Chaque valeur de la liste conserve ainsi son contenu d’origine, mais bénéficie d’une présentation personnalisée grâce au CSS.

Exemple de boutons de navigation avec CSS

Exemple de personnalisation de boutons avec le format CSS Styles

Le format CSS Styles peut être utilisé pour définir l’apparence d’éléments créés dans un champ HTML. Dans cet exemple, plusieurs classes CSS permettent de créer des boutons de navigation et de proposer différentes variantes de couleur.

Le même style de base est partagé par tous les boutons. Des classes supplémentaires permettent ensuite de modifier uniquement leur couleur. Les boutons peuvent pointer vers une section de la page ou vers une page externe.

Créer les boutons dans un champ HTML

Créer un champ utilisant un format HTML et ajouter le code suivant :

<nav class="lodex-anchor-nav">

  <a href="#methodologie"
     class="lodex-anchor-nav__link lodex-anchor-nav__link--green">
    Méthodologie
  </a>

  <a href="https://www.lodex.fr/docs/documentation/"
     class="lodex-anchor-nav__link lodex-anchor-nav__link--outline"
     target="_blank"
     rel="noopener">
    Documentation LODEX
  </a>

  <a href="https://www.lodex.fr/category/homepage/news/"
     class="lodex-anchor-nav__link lodex-anchor-nav__link--green"
     target="_blank"
     rel="noopener">
    Les actus
  </a>

  <a href="https://www.lodex.fr/contact/"
     class="lodex-anchor-nav__link lodex-anchor-nav__link--orange"
     target="_blank"
     rel="noopener">
    Contact
  </a>

</nav>

Définir l’apparence des boutons avec CSS Styles

Créer un champ utilisant le format CSS Styles et activer l’option permettant d’appliquer les règles à l’ensemble de la page. Ajouter ensuite le CSS suivant :

.lodex-anchor-nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 18px;
  margin: 1.2rem 0 1.5rem;
}

/* Style commun à tous les boutons */
.lodex-anchor-nav__link {
  display: inline-block;
  padding: 10px 20px;
  border-radius: 2px;
  font-weight: 600;
  font-size: .9rem;
  text-decoration: none !important;
  transition: all .2s ease;
}

/* Bouton vert */
.lodex-anchor-nav__link--green {
  background: #7ac143;
  border: 2px solid #7ac143;
  color: #ffffff !important;
}

/* Bouton blanc avec contour vert */
.lodex-anchor-nav__link--outline {
  background: #ffffff;
  border: 2px solid #7ac143;
  color: #222222 !important;
}

/* Bouton orange */
.lodex-anchor-nav__link--orange {
  background: #f58220;
  border: 2px solid #f58220;
  color: #ffffff !important;
}

/* Effet au survol */
.lodex-anchor-nav__link:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 10px rgba(0, 0, 0, .15);
  text-decoration: none !important;
}

/* Position d’arrivée du lien vers la section Méthodologie */
#methodologie {
  scroll-margin-top: 90px;
}

Comprendre le principe

Une classe commune pour tous les boutons.
La classe lodex-anchor-nav__link contient les propriétés partagées par tous les boutons : espacement, taille du texte, graisse, comportement au survol, etc.

Des classes pour créer différentes variantes.
Une seconde classe permet de choisir l’apparence du bouton. Dans cet exemple, trois variantes sont proposées : lodex-anchor-nav__link--green, lodex-anchor-nav__link--outline et lodex-anchor-nav__link--orange.

Un bouton peut ainsi utiliser simultanément la classe commune et une variante :

class="lodex-anchor-nav__link lodex-anchor-nav__link--orange"

La première classe fournit le style commun à tous les boutons. La seconde ajoute uniquement les propriétés propres à la variante orange.

Adapter les boutons

Modifier le texte.
Le texte situé entre les balises <a> et </a> correspond au libellé affiché dans le bouton.

<a ...>Documentation LODEX</a>

Modifier la destination.
L’attribut href définit la destination du bouton. Il peut contenir l’adresse d’une page externe :

href="https://www.lodex.fr/docs/documentation/"

Il peut également pointer vers un élément de la page grâce à son identifiant :

href="#methodologie"

Dans ce cas, un élément de la page doit posséder l’identifiant correspondant methodologie.

Ouvrir un lien externe dans un nouvel onglet.
Les attributs target="_blank" et rel="noopener" sont utilisés dans les exemples pour ouvrir les pages externes dans un nouvel onglet.

Choisir la variante du bouton.
Pour modifier l’apparence d’un bouton sans changer les autres, remplacer simplement sa classe de variante :

lodex-anchor-nav__link--green
lodex-anchor-nav__link--outline
lodex-anchor-nav__link--orange

Adapter l’apparence

Modifier les couleurs.
Les propriétés background, border et color permettent d’adapter chaque variante à l’identité visuelle de l’instance.

.lodex-anchor-nav__link--green {
  background: #7ac143;          /* À MODIFIER : couleur du fond */
  border: 2px solid #7ac143;    /* À MODIFIER : couleur de la bordure */
  color: #ffffff !important;    /* À MODIFIER : couleur du texte */
}

Modifier la forme et la taille.
Les principales propriétés à adapter dans la classe lodex-anchor-nav__link sont :

.lodex-anchor-nav__link {
  padding: 10px 20px;       /* À MODIFIER : taille du bouton */
  border-radius: 2px;        /* À MODIFIER : arrondi des angles */
  font-weight: 600;          /* À MODIFIER : graisse du texte */
  font-size: .9rem;          /* À MODIFIER : taille du texte */
}

Modifier l’espace entre les boutons.
La propriété gap de lodex-anchor-nav contrôle l’espace séparant les différents boutons.

.lodex-anchor-nav {
  gap: 18px; /* À MODIFIER : espace entre les boutons */
}

Adapter le défilement vers une section.
Lorsqu’un bouton pointe vers une section de la page, scroll-margin-top permet de conserver un espace au-dessus de la section ciblée, notamment lorsqu’un en-tête reste visible pendant le défilement.

#methodologie {
  scroll-margin-top: 90px; /* À MODIFIER selon la hauteur de l’en-tête */
}

JavaScript/HTML templating (modèle)

Le format JavaScript/HTML templating (modèle) permet de construire librement l’affichage d’un champ à l’aide d’un modèle associant HTML et JavaScript.

Ce format repose sur EJS (Embedded JavaScript), un langage de template qui permet d’intégrer des instructions JavaScript dans du contenu HTML.

Ce format permet notamment :

  • de construire une présentation HTML personnalisée
  • d’utiliser les données disponibles dans le champ
  • de parcourir des listes de valeurs
  • de combiner plusieurs informations dans un même affichage
  • d’effectuer des calculs ou des traitements avant l’affichage
  • d’adapter le contenu affiché en fonction de la structure ou de la valeur des données

Il est ainsi possible de ne pas simplement afficher une valeur telle qu’elle existe dans les données, mais de construire dynamiquement le contenu qui sera présenté à l’utilisateur.

Comprendre le principe d’un modèle EJS

Un modèle EJS associe du contenu HTML classique à des instructions JavaScript. Les balises EJS permettent de distinguer les parties qui doivent être exécutées de celles qui doivent être affichées.

Deux syntaxes sont particulièrement utiles pour commencer :

<% ... %>

Cette syntaxe permet d’exécuter du code JavaScript, par exemple pour créer une variable, effectuer un calcul, parcourir une liste ou appliquer une condition.

<%= ... %>

Cette syntaxe permet d’insérer le résultat d’une expression dans le contenu HTML affiché.

Accéder aux données du champ

Dans le format JavaScript/HTML templating (modèle), les données à afficher sont accessibles à partir de la variable root.values.

Lorsqu’une routine Lodex est utilisée pour alimenter le champ, root.values contient le résultat renvoyé par cette routine. Le modèle EJS peut alors exploiter directement ce résultat pour construire l’affichage, effectuer des calculs ou appliquer des traitements.

La structure de root.values dépend donc des données fournies au format. Il peut s’agir, par exemple, d’une valeur simple, d’une liste de valeurs ou d’une liste d’objets.

Par exemple, pour afficher directement une valeur simple :

<%= root.values %>

Le modèle peut également vérifier la structure des données afin d’adapter automatiquement l’affichage. Il est par exemple possible de déterminer si root.values contient une valeur simple ou une liste de valeurs.

<% if (!Array.isArray(root.values)) { %>
  <%= root.values %>
<% } %>

Dans cet exemple, le contenu est affiché uniquement si les données ne sont pas sous la forme d’un tableau.

Parcourir une liste de valeurs

Lorsque root.values contient un tableau, JavaScript permet de parcourir les différentes valeurs et de construire dynamiquement le contenu HTML correspondant.

<% for (let datum of root.values) { %>
  <p><%= datum.value %></p>
<% } %>

Une nouvelle portion de HTML est ainsi générée pour chaque élément présent dans la liste.

Adapter l’affichage à la structure des données

Le modèle peut également tester les propriétés présentes dans les données afin de choisir une présentation différente selon leur structure.

Par exemple, le modèle proposé par défaut dans Lodex vérifie si les données correspondent à une liste de paires _id / value :

<% if (Array.isArray(root.values) && root.values[0]._id !== undefined) { %>
  ...
<% } %>

Il peut également détecter une liste contenant des propriétés source, target et weight :

<% if (Array.isArray(root.values) && root.values[0].source !== undefined) { %>
  ...
<% } %>

Cette possibilité permet d’utiliser un même format pour plusieurs structures de données et de générer automatiquement une présentation adaptée.

Exemples de modèles EJS

Les exemples suivants présentent différents modèles EJS pouvant être adaptés et réutilisés dans une instance Lodex. Chaque modèle montre comment exploiter les données disponibles dans root.values pour effectuer des traitements et construire une présentation personnalisée.

Afficher le nombre de documents du corpus sous forme de bulle informative

Ce modèle permet d’afficher le nombre de documents présents dans le corpus sous la forme d’une bulle informative.

Affichage du nombre de publications du corpus avec un modèle EJS

Il utilise la routine count-all, qui compte dynamiquement le nombre de documents du corpus. Si des documents sont ajoutés ou supprimés, la valeur renvoyée par la routine est automatiquement mise à jour.

Le résultat de la routine est récupéré dans le modèle EJS à partir de root.values. Le modèle vérifie que cette valeur est bien un nombre, puis utilise JavaScript pour la formater avant son affichage.

<% if (typeof root.values === 'number') { %>
<%
    let formattedValue = root.values.toLocaleString('fr-FR');
%>

<div id="data-count-publicationNumber"><%= formattedValue %></div>

<% } %>

Dans cet extrait :

  • typeof root.values === 'number' vérifie que la routine a bien renvoyé une valeur numérique
  • root.values.toLocaleString('fr-FR') applique un formatage français au nombre, par exemple 4195 devient 4 195
  • <%= formattedValue %> insère la valeur formatée dans le contenu HTML

Le modèle complet ajoute ensuite des règles CSS afin d’afficher le résultat dans un cercle et prévoit également un affichage alternatif si aucune donnée numérique valide n’est disponible.

Code complet du modèle
<div class="ejs-wrapper">
    <% if (typeof root.values === 'number') { %>
    <%
        let formattedValue = root.values.toLocaleString('fr-FR');
    %>
    <style>
        #data-container-publicationNumber {
            background-color: #00284b;
            padding: 20px;
            border-radius: 50%;
            text-align: center;
            width: 250px;
            height: 250px;
            margin: 16px auto;
            font-family: Arial, sans-serif;
            border: 4px solid #00284b;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2);
        }

        #data-title-publicationNumber {
            font-size: 20px;
            color: #ffeb6e;
            font-weight: bold;
            text-align: center;
            max-width: 120px;
        }

        #data-count-publicationNumber {
            font-size: 24px;
            color: #ffeb6e;
            font-weight: bold;
            margin-top: 8px;
            display: flex;
            justify-content: center;
        }
    </style>

    <div id="data-container-publicationNumber">
        <div id="data-title-publicationNumber">Nombre de publications</div>
        <div id="data-count-publicationNumber"><%= formattedValue %></div>
    </div>

    <% } else { %>

    <style>
        #data-container-publicationNumber {
            background-color: #00284b;
            padding: 20px;
            border-radius: 50%;
            text-align: center;
            width: 150px;
            height: 150px;
            margin: 16px auto;
            font-family: Arial, sans-serif;
            border: 4px solid #00284b;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2);
        }

        #data-title-publicationNumber {
            font-size: 14px;
            color: #ffeb6e;
            font-weight: bold;
            text-align: center;
            max-width: 100px;
        }
    </style>

    <div id="data-container-publicationNumber">
        <div id="data-title-publicationNumber">Aucune donnée valide</div>
    </div>

    <% } %>
</div>

Afficher la part de publications en Open Access sous forme de bulle de progression

Ce modèle permet d’afficher la proportion de publications en Open Access sous la forme d’une bulle de progression circulaire.

Affichage de la part de publications en Open Access sous forme de bulle de progression avec un modèle EJS

Il utilise la routine distinct-by appliquée au champ indiquant le statut Open Access. La routine renvoie dans root.values une liste contenant les différentes valeurs du champ et leur nombre d’occurrences.

Contrairement à l’exemple précédent, la valeur à afficher n’est donc pas directement fournie par la routine. Le modèle EJS utilise JavaScript pour exploiter les résultats de distinct-by et calculer le pourcentage de publications en Open Access.

Le calcul est réalisé en trois étapes :

  • le nombre total de publications est calculé en additionnant les occurrences de toutes les valeurs avec reduce()
  • le nombre de publications ayant pour valeur Oui est récupéré avec find()
  • le pourcentage est calculé à partir de ces deux valeurs puis arrondi à une décimale avec toFixed(1)
<%
    let total = root.values.reduce((acc, item) => acc + (item.value || 0), 0);
    let yesCount = root.values.find(item => item._id === "Oui")?.value || 0;
    let percentage = total > 0 ? ((yesCount / total) * 100).toFixed(1) : 0;
%>

La valeur calculée est ensuite insérée dans le contenu HTML avec la syntaxe EJS <%= percentage %>.

<div id="data-count-openAccess"><%= percentage %> %</div>

Le modèle utilise également un élément SVG pour représenter graphiquement le pourcentage autour de la bulle. La longueur de la partie colorée de l’anneau est calculée dynamiquement à partir de la valeur de percentage.

stroke-dashoffset="<%= 722 - (722 * percentage / 100) %>"

Ainsi, le résultat de la routine est utilisé à la fois pour calculer une nouvelle information et pour construire dynamiquement sa représentation visuelle.

Code complet du modèle
<div class="ejs-wrapper">
<% if (Array.isArray(root.values) && root.values.length > 0) { %>
    <%
        let total = root.values.reduce((acc, item) => acc + (item.value || 0), 0);
        let yesCount = root.values.find(item => item._id === "Oui")?.value || 0;
        let percentage = total > 0 ? ((yesCount / total) * 100).toFixed(1) : 0;
    %>

    <style>
        #data-container-openAccess {
            background-color: #00284b;
            padding: 0;
            border-radius: 50%;
            width: 250px;
            height: 250px;
            margin: 16px auto;
            font-family: Arial, sans-serif;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2);
            position: relative;
        }

        #data-title-openAccess,
        #data-count-openAccess {
            position: relative;
            z-index: 2;
            color: #ffeb6e;
            font-weight: bold;
            text-align: center;
        }

        #data-title-openAccess {
            font-size: 18px;
            color: #ffeb6e;
            font-weight: bold;
            text-align: center;
            max-width: 160px;
            line-height: 1.2;
            word-wrap: break-word;
        }

        #data-count-openAccess {
            font-size: 22px;
            margin-top: 4px;
        }

        .progress-ring {
            position: absolute;
            top: 0;
            left: 0;
            z-index: 1;
            transform: rotate(-90deg);
        }
    </style>

    <div id="data-container-openAccess">
        <svg class="progress-ring" width="250" height="250">
            <circle
                cx="125"
                cy="125"
                r="115"
                stroke="#555555"
                stroke-width="10"
                fill="none"
            />
            <circle
                cx="125"
                cy="125"
                r="115"
                stroke="#ffeb6e"
                stroke-width="10"
                fill="none"
                stroke-dasharray="722"
                stroke-dashoffset="<%= 722 - (722 * percentage / 100) %>"
                style="transition: stroke-dashoffset 1s;"
            />
        </svg>

        <div id="data-title-openAccess">Publications en Open Access</div>
        <div id="data-count-openAccess"><%= percentage %> %</div>
    </div>

<% } else { %>

    <style>
        #data-container-openAccess {
            background-color: #00284b;
            border-radius: 50%;
            width: 250px;
            height: 250px;
            margin: 16px auto;
            font-family: Arial, sans-serif;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2);
        }

        #data-title-openAccess {
            font-size: 18px;
            color: #ffeb6e;
            font-weight: bold;
            text-align: center;
            max-width: 120px;
        }
    </style>

    <div id="data-container-openAccess">
        <div id="data-title-openAccess">Aucune donnée valide</div>
    </div>

<% } %>
</div>

Construire un tableau croisé à partir de données regroupées

Ce modèle montre comment utiliser EJS pour construire un tableau croisé à partir de données regroupées par une routine Lodex.

Dans cet exemple, on souhaite présenter des montants par revue et par année. La routine group-and-sum-with est utilisée avec un premier champ associant la revue et l’année et un second champ contenant les montants à additionner.

Exemple de tableau croisé construit à partir de données regroupées avec un modèle EJS

Les résultats de la routine sont récupérés dans root.values. Le modèle EJS utilise ensuite JavaScript pour séparer la revue et l’année, réorganiser les données et calculer les différents totaux avant de construire le tableau HTML.

Le tableau obtenu présente les revues en lignes et les années en colonnes. Il calcule également automatiquement le total pour chaque revue, le total pour chaque année ainsi que le total général.

La première étape consiste à parcourir les résultats de la routine et à séparer la revue et l’année contenues dans _id :

let [publisher, year] = item._id.split(';');

Les valeurs sont ensuite regroupées dans un objet intermédiaire afin de pouvoir retrouver, pour chaque revue, le montant correspondant à chaque année :

if (!dataByPublisher[publisher]) dataByPublisher[publisher] = {};
dataByPublisher[publisher][year] = value;

Le modèle détermine automatiquement les différentes années présentes dans les résultats. La liste est dédupliquée puis triée avant de servir à construire les colonnes du tableau :

let years = Object.values(dataByPublisher).reduce((acc, obj) => acc.concat(Object.keys(obj)), []);
years = [...new Set(years)].sort();

À partir de cette nouvelle structure, EJS génère dynamiquement :

  • une ligne pour chaque revue
  • une colonne pour chaque année présente dans les données
  • un total pour chaque revue
  • un total pour chaque année
  • un total général

Lorsqu’aucune valeur n’existe pour une revue et une année données, la cellule correspondante reste vide et est affichée avec un fond différent. Les valeurs numériques sont également arrondies et formatées avec toLocaleString('fr-FR') afin d’utiliser le format français.

Cet exemple montre qu’un modèle EJS peut aller au-delà d’un simple affichage : les données renvoyées par une routine peuvent être réorganisées, regroupées et recalculées avant d’être présentées sous la forme souhaitée.

Code complet du modèle
<%
    let dataByPublisher = {};

    root.values.forEach(item => {
        let [publisher, year] = item._id.split(';');
        let value = parseFloat(item.value);
        if (!isNaN(value)) {
            if (!dataByPublisher[publisher]) dataByPublisher[publisher] = {};
            dataByPublisher[publisher][year] = value;
        }
    });

    let years = Object.values(dataByPublisher).reduce((acc, obj) => acc.concat(Object.keys(obj)), []);
    years = [...new Set(years)].sort();

    let grandTotal = 0;
%>

<table style="width:100%;border-collapse:collapse;table-layout:fixed;margin:16px 0;">
    <thead>
        <tr style="background-color:#f2f2f2;">
            <th style="border:1px solid #444;padding:6px;text-align:left;width:150px;">Revue</th>
            <% years.forEach(year => { %>
                <th style="border:1px solid #444;padding:6px;text-align:right;"><%= year %></th>
            <% }); %>
            <th style="border:1px solid #444;padding:6px;text-align:right;font-weight:bold;">Total</th>
        </tr>
    </thead>

    <tbody>
        <%
            Object.keys(dataByPublisher).forEach((publisher, index) => {
                let total = 0;
                let rowColor = index % 2 === 0 ? '#ffffff' : '#deefd7';
        %>
            <tr style="background-color:<%= rowColor %>;">
                <td style="border:1px solid #444;padding:6px;text-align:left;"><%= publisher %></td>

                <% years.forEach(year => {
                    let value = dataByPublisher[publisher][year];
                    let isEmpty = value === undefined;
                    let bgColor = isEmpty ? '#e0e0e0' : 'transparent';
                    if (!isEmpty) total += Math.round(value);
                %>
                    <td style="border:1px solid #444;padding:6px;text-align:right;background-color:<%= bgColor %>;">
                        <%= !isEmpty ? Math.round(value).toLocaleString('fr-FR') : '' %>
                    </td>
                <% }); %>

                <td style="border:1px solid #444;padding:6px;text-align:right;font-weight:bold;">
                    <%= Math.round(total).toLocaleString('fr-FR') %>
                </td>

                <% grandTotal += total; %>
            </tr>
        <% }); %>
    </tbody>

    <tfoot>
        <tr style="background-color:#f7f9fc;font-weight:bold;">
            <td style="border:1px solid #444;padding:6px;text-align:left;">Total</td>

            <% years.forEach(year => {
                let yearTotal = 0;
                Object.keys(dataByPublisher).forEach(publisher => {
                    if (dataByPublisher[publisher][year] !== undefined) {
                        yearTotal += Math.round(dataByPublisher[publisher][year]);
                    }
                });
            %>
                <td style="border:1px solid #444;padding:6px;text-align:right;">
                    <%= Math.round(yearTotal).toLocaleString('fr-FR') %>
                </td>
            <% }); %>

            <td style="border:1px solid #444;padding:6px;text-align:right;font-weight:bold;">
                <%= Math.round(grandTotal).toLocaleString('fr-FR') %>
            </td>
        </tr>
    </tfoot>
</table>

Présenter les informations relatives à la constitution d’un corpus

Ce modèle permet de présenter les informations relatives à la constitution d’un corpus à partir d’une ou plusieurs requêtes API.

Dans cet exemple, on souhaite afficher la requête API utilisée pour constituer le corpus ainsi que la date à laquelle cette requête a été envoyée. Si plusieurs requêtes ont été utilisées, chacune d’elles est affichée avec sa date correspondante.

La routine cross-by est utilisée pour associer le champ contenant la requête API au champ contenant la date d’interrogation. Les résultats de cette routine sont accessibles dans root.values.

Le modèle EJS adapte ensuite automatiquement la présentation au nombre de requêtes disponibles : il affiche un contenu différent selon qu’aucune, une seule ou plusieurs requêtes sont présentes.

Le modèle commence par vérifier que root.values contient bien un tableau. Si ce n’est pas le cas, un tableau vide est utilisé :

let items = (root && Array.isArray(root.values)) ? root.values : [];

Le titre affiché est ensuite adapté automatiquement au nombre de requêtes :

let title = "Corpus réalisé à partir";
if (items.length === 1) {
  title = "Corpus réalisé à partir de la requête";
} else if (items.length > 1) {
  title = "Corpus réalisé à partir des requêtes";
}

Le modèle distingue ainsi trois situations :

  • si aucune requête n’est disponible, un message indique qu’aucune requête n’est disponible
  • si une seule requête est présente, la requête API et sa date d’interrogation sont affichées dans un bloc unique
  • si plusieurs requêtes sont présentes, le modèle génère automatiquement un bloc pour chacune d’elles

Voici le résultat obtenu lorsque le corpus a été constitué à partir d’une seule requête :

Présentation d'un corpus constitué à partir d'une seule requête API et de sa date d'interrogation

Lorsque plusieurs requêtes ont été utilisées pour constituer le corpus, le même modèle adapte automatiquement la présentation et crée un bloc pour chacune d’elles :

Présentation d'un corpus constitué à partir de deux requêtes API et de leurs dates d'interrogation

Dans ce second cas, une boucle JavaScript parcourt les résultats afin de générer autant de blocs que de requêtes :

for (let i = 0; i < items.length; i++) {
  let it = items[i];

  html += "<div>";
  html += "Requête " + (i + 1);
  html += "</div>";
}

Dans les résultats produits par la routine cross-by, la propriété source contient ici la requête API utilisée pour constituer le corpus, tandis que la propriété target contient la date à laquelle cette requête a été envoyée.

Le modèle utilise également _.escape() avant d’insérer ces informations dans le HTML afin d’échapper les caractères spéciaux contenus dans les données :

_.escape(String(it.source))
_.escape(String(it.target))

Contrairement aux exemples précédents, le contenu HTML est ici construit progressivement dans une variable html. Une fois la construction terminée, le contenu généré est injecté dans le rendu avec :

<%- html %>

La syntaxe <%- ... %> permet d’insérer le contenu HTML généré sans échapper les balises qui le composent.

Cet exemple montre ainsi qu’un même modèle EJS peut adapter automatiquement sa structure et son contenu aux données disponibles, sans qu’il soit nécessaire de créer un modèle différent selon le nombre de requêtes utilisées pour constituer le corpus.

Code complet du modèle
<%
  let items = (root && Array.isArray(root.values)) ? root.values : [];

  let title = "Corpus réalisé à partir";
  if (items.length === 1) {
    title = "Corpus réalisé à partir de la requête";
  } else if (items.length > 1) {
    title = "Corpus réalisé à partir des requêtes";
  }

  let html = "";

  html += "<div style='font-family:var(--font-texte) !important; text-align:left !important; margin:8px 0 14px 0; border-radius:8px; overflow:hidden; box-shadow:0 1px 3px rgba(0,0,0,0.1);'>";

  /* HEADER */
  html += "<div style='background:#00284b; color:#ffeb6e; padding:10px 14px; display:flex; align-items:center; gap:8px;'>";
  html += "<svg style='width:1.1em; height:1.1em; flex:0 0 auto;' viewBox='0 0 24 24' aria-hidden='true'>";
  html += "<path fill='currentColor' d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 3.5L18.5 8H14z'/>";
  html += "</svg>";
  html += "<span style='color:#ffeb6e; font-size:20px; font-weight:700; font-family:var(--font-texte); letter-spacing:0.02em;'>" + _.escape(title) + "</span>";
  html += "</div>";

  /* CONTENT */
  html += "<div style='background:#d9ecfb; padding:14px 16px; color:#00284b; font-size:1rem; line-height:1.5;'>";

  if (!items.length) {

    html += "<div style='color:#5b6470;'>Aucune requête disponible.</div>";

  } else if (items.length === 1) {

    let it = items[0];

    html += "<div style='background:#fff; border:1px solid #c7d6e6; border-radius:8px; padding:12px 14px;'>";
    html += "<div style='font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:.92rem; color:#1f2d3d; word-break:break-word; overflow-wrap:anywhere; margin-bottom:8px;'>" + _.escape(String(it.source)) + "</div>";
    html += "<div style='font-size:.92rem; color:#5b6470;'>Date d'interrogation : <strong>" + _.escape(String(it.target)) + "</strong></div>";
    html += "</div>";

  } else {

    html += "<div style='display:flex; flex-direction:column; gap:10px;'>";

    for (let i = 0; i < items.length; i++) {
      let it = items[i];

      html += "<div style='background:#fff; border:1px solid #c7d6e6; border-radius:8px; padding:12px 14px;'>";
      html += "<div style='font-size:.82rem; font-weight:800; text-transform:uppercase; letter-spacing:.04em; color:#6941eb; margin-bottom:6px;'>Requête " + (i + 1) + "</div>";
      html += "<div style='font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:.92rem; color:#1f2d3d; word-break:break-word; overflow-wrap:anywhere; margin-bottom:8px;'>" + _.escape(String(it.source)) + "</div>";
      html += "<div style='font-size:.92rem; color:#5b6470;'>Date d'interrogation : <strong>" + _.escape(String(it.target)) + "</strong></div>";
      html += "</div>";
    }

    html += "</div>";
  }

  html += "</div>";
  html += "</div>";
%>

<%- html %>

Construire une présentation synthétique à partir de plusieurs indicateurs

Ce modèle montre comment utiliser EJS pour construire une présentation synthétique à partir de plusieurs indicateurs présents dans le corpus.

Présentation synthétique des principaux indicateurs d'une instance Lodex construite avec un modèle EJS

Dans cet exemple, on souhaite afficher en un seul bloc plusieurs informations caractéristiques de l’instance : le nombre de documents, la part de publications en accès ouvert, le nombre d’auteurs, d’éditeurs, de pays, d’institutions et de sources, ainsi que l’année ou la période couverte par le corpus.

La routine ventilate-by est utilisée avec plusieurs champs du corpus. Cette routine compte, pour chaque champ sélectionné, le nombre d’occurrences de ses différentes valeurs. Les résultats sont renvoyés sous la forme d’éléments contenant notamment une source, une target et un weight.

Dans cet exemple, source correspond au nom du champ analysé, target à l’une des valeurs distinctes de ce champ et weight au nombre d’occurrences de cette valeur.

Les résultats de la routine sont accessibles dans root.values. Le modèle EJS les regroupe tout d’abord par champ :

const bySource = _.mapValues(
  _.groupBy(values, 'source'),
  items => _.uniq(items.map(v => v.target))
);

Cette structure permet ensuite de calculer simplement le nombre de valeurs distinctes pour plusieurs indicateurs :

const nbCountries    = (bySource['Pays'] || []).length;
const nbSources      = (bySource['Source originale (OpenAlex)'] || []).length;
const nbInstitutions = (bySource['Institution'] || []).length;
const nbPublishers   = (bySource['Éditeur original (OpenAlex)'] || []).length;
const nbAuthors      = (bySource['Auteur'] || []).length;

Le nombre total de documents est calculé à partir des poids associés au champ Accès ouvert :

const totalDocs = _.sumBy(
  values.filter(v => v.source === 'Accès ouvert'),
  'weight'
);

Le même ensemble de données est également utilisé pour calculer la part de publications en accès ouvert. Le modèle additionne le nombre de publications ayant pour valeur Oui, calcule le total des publications puis détermine le pourcentage correspondant :

const oaValues = values.filter(v => v.source === 'Accès ouvert');
const oaYes    = _.sumBy(oaValues.filter(v => v.target === 'Oui'), 'weight');
const oaTotal  = _.sumBy(oaValues, 'weight');
const oaPct    = oaTotal ? Math.round((oaYes / oaTotal) * 100) : 0;

Les années de publication sont également récupérées, converties en nombres puis triées :

const publicationYears = _.chain(bySource['Année de publication'] || [])
  .map(y => parseInt(y, 10))
  .filter(y => !isNaN(y))
  .sortBy()
  .value();

Le modèle adapte ensuite automatiquement le libellé affiché. Si une seule année est présente, il affiche une année interrogée. Si plusieurs années sont présentes, il affiche une période interrogée comprise entre la première et la dernière année :

const publicationYearLabel =
  publicationYears.length <= 1 ? 'année interrogée' : 'période interrogée';

const publicationYearValue =
  publicationYears.length === 0
    ? '—'
    : publicationYears.length === 1
      ? publicationYears[0]
      : publicationYears[0] + ' – ' + publicationYears[publicationYears.length - 1];

Une fois les différents indicateurs calculés, le modèle construit une série de cartes HTML permettant de les afficher dans une présentation unique.

Les valeurs calculées sont insérées dans les cartes à l’aide de la syntaxe EJS <%= ... %>. Par exemple :

<div class="number"><%= totalDocs.toLocaleString('fr-FR') %></div>
<div class="number"><%= oaPct %> %</div>
<div class="number"><%= nbAuthors.toLocaleString('fr-FR') %></div>

Le CSS intégré au modèle organise ensuite les différentes cartes dans une grille et définit leur apparence : couleurs, tailles, icônes, ombres, disposition et animations.

Le résultat obtenu constitue ainsi une vue synthétique des principaux indicateurs de l’instance, construite dynamiquement à partir des données du corpus.

Configuration de la routine

Dans cet exemple, la routine ventilate-by est configurée avec plusieurs champs correspondant aux indicateurs que l’on souhaite exploiter dans le modèle EJS.

Configuration de la routine ventilate-by utilisée pour construire plusieurs indicateurs dans un modèle EJS

Cet exemple montre qu’un modèle EJS peut exploiter simultanément plusieurs résultats provenant d’une même routine pour agréger, calculer, reformater et présenter plusieurs indicateurs dans une interface unique.

Code complet du modèle
<%
/* === AGRÉGATIONS === */
const values = root.values || [];

const bySource = _.mapValues(
  _.groupBy(values, 'source'),
  items => _.uniq(items.map(v => v.target))
);

const nbCountries    = (bySource['Pays'] || []).length;
const nbSources      = (bySource['Source originale (OpenAlex)'] || []).length;
const nbInstitutions = (bySource['Institution'] || []).length;
const nbPublishers   = (bySource['Éditeur original (OpenAlex)'] || []).length;
const nbAuthors      = (bySource['Auteur'] || []).length;

/* total documents */
const totalDocs = _.sumBy(
  values.filter(v => v.source === 'Accès ouvert'),
  'weight'
);

/* open access */
const oaValues = values.filter(v => v.source === 'Accès ouvert');
const oaYes    = _.sumBy(oaValues.filter(v => v.target === 'Oui'), 'weight');
const oaTotal  = _.sumBy(oaValues, 'weight');
const oaPct    = oaTotal ? Math.round((oaYes / oaTotal) * 100) : 0;

/* années */
const publicationYears = _.chain(bySource['Année de publication'] || [])
  .map(y => parseInt(y, 10))
  .filter(y => !isNaN(y))
  .sortBy()
  .value();

const publicationYearLabel =
  publicationYears.length <= 1 ? 'année interrogée' : 'période interrogée';

const publicationYearValue =
  publicationYears.length === 0
    ? '—'
    : publicationYears.length === 1
      ? publicationYears[0]
      : publicationYears[0] + ' – ' + publicationYears[publicationYears.length - 1];
%>

<div class="inist-stats inist-stats-v6">

  <h1 class="title">L’INSTANCE EN CHIFFRES</h1>

  <div class="stats-grid">

    <div class="card blue with-icon documents">
      <div class="number"><%= totalDocs.toLocaleString('fr-FR') %></div>
      <div class="text">documents</div>
      <div class="icon">
        <svg viewBox="0 0 24 24" aria-hidden="true">
          <path d="M7 2h7l5 5v15H7z"/>
          <path d="M14 2v5h5"/>
          <line x1="9" y1="13" x2="17" y2="13"/>
          <line x1="9" y1="17" x2="17" y2="17"/>
        </svg>
      </div>
    </div>

    <div class="card yellow with-icon oa">
      <div class="number"><%= oaPct %> %</div>
      <div class="text">d'accès ouvert</div>
      <div class="icon">
        <svg viewBox="0 0 24 24" aria-hidden="true">
          <rect x="5" y="10" width="14" height="10" rx="2"></rect>
          <path d="M9 10V7a3 3 0 0 1 6 0"></path>
        </svg>
      </div>
    </div>

    <div class="card violet with-icon authors">
      <div class="number"><%= nbAuthors.toLocaleString('fr-FR') %></div>
      <div class="text">auteurs</div>
      <div class="icon">
        <svg viewBox="0 0 24 24" aria-hidden="true">
          <circle cx="12" cy="8" r="3"></circle>
          <path d="M5 20c0-3.3 3.8-5 7-5s7 1.7 7 5"></path>
        </svg>
      </div>
    </div>

    <div class="card yellow publishers">
      <div class="number"><%= nbPublishers %></div>
      <div class="text">éditeurs</div>
    </div>

    <div class="card violet with-icon period">
      <div class="number"><%= publicationYearValue %></div>
      <div class="text"><%= publicationYearLabel %></div>
      <div class="icon">
        <svg viewBox="0 0 24 24" aria-hidden="true">
          <rect x="3" y="5" width="18" height="16" rx="2"/>
          <line x1="3" y1="9" x2="21" y2="9"/>
          <line x1="8" y1="3" x2="8" y2="7"/>
          <line x1="16" y1="3" x2="16" y2="7"/>
        </svg>
      </div>
    </div>

    <div class="card blue countries">
      <div class="number"><%= nbCountries %></div>
      <div class="text">pays représentés</div>
    </div>

    <div class="card yellow with-icon institutions">
      <div class="number"><%= nbInstitutions.toLocaleString('fr-FR') %></div>
      <div class="text">institutions</div>
      <div class="icon">
        <svg viewBox="0 0 24 24" aria-hidden="true">
          <path d="M3 10l9-5 9 5"></path>
          <path d="M4 10h16"></path>
          <line x1="6" y1="10" x2="6" y2="18"></line>
          <line x1="10" y1="10" x2="10" y2="18"></line>
          <line x1="14" y1="10" x2="14" y2="18"></line>
          <line x1="18" y1="10" x2="18" y2="18"></line>
          <line x1="4" y1="18" x2="20" y2="18"></line>
        </svg>
      </div>
    </div>

    <div class="card blue sources">
      <div class="number"><%= nbSources %></div>
      <div class="text">sources</div>
    </div>

  </div>
</div>

<style>
.inist-stats-v6{
  --blue:#00284b;
  --yellow:#ffeb6e;
  --violet:#6941eb;
  --violet-dark:#5a34d6;
  --shadow-soft:0 8px 22px rgba(0,0,0,.08);
  --shadow-hover:0 16px 30px rgba(0,0,0,.14);
  --radius:16px;
  font-family:"Helvetica Neue",Arial,sans-serif;
  color:#111;
  background:#fff;
  padding:1.25rem;
  max-width:1500px;
  margin:0 auto;
  overflow-x:auto;
}

.inist-stats-v6 .title{
  text-align:center;
  font-weight:900;
  letter-spacing:.05em;
  margin:0 0 1.2rem 0;
  font-size:1.65rem;
  color:var(--blue);
  text-transform:uppercase;
  position:relative;
}

.inist-stats-v6 .title::after{
  content:"";
  display:block;
  width:170px;
  height:4px;
  background:linear-gradient(90deg,var(--violet),#8c6cff);
  margin:.35rem auto 0;
  border-radius:999px;
}

.inist-stats-v6 .stats-grid{
  display:grid;
  grid-template-columns:repeat(6, 1fr);
  grid-template-rows:150px 150px;
  grid-template-areas:
    "documents documents oa authors authors publishers"
    "period period oa countries institutions sources";
  gap:16px;
  min-width:1320px;
  max-width:1420px;
  margin:0 auto;
}

.inist-stats-v6 .documents    { grid-area:documents; }
.inist-stats-v6 .oa           { grid-area:oa; }
.inist-stats-v6 .authors      { grid-area:authors; }
.inist-stats-v6 .publishers   { grid-area:publishers; }
.inist-stats-v6 .period       { grid-area:period; }
.inist-stats-v6 .countries    { grid-area:countries; }
.inist-stats-v6 .institutions { grid-area:institutions; }
.inist-stats-v6 .sources      { grid-area:sources; }

.inist-stats-v6 .card{
  position:relative;
  overflow:hidden;
  display:flex;
  flex-direction:column;
  justify-content:center;
  align-items:center;
  text-align:center;
  min-width:0;
  height:100%;
  padding:18px 16px;
  border-radius:var(--radius);
  box-shadow:var(--shadow-soft);
  transition:transform .28s ease, box-shadow .28s ease, filter .28s ease;
  opacity:0;
  transform:translateY(18px);
  animation:fadeUpPremium .7s ease forwards;
  isolation:isolate;
}

.inist-stats-v6 .card::before{
  content:"";
  position:absolute;
  inset:0;
  background:linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,0) 45%);
  pointer-events:none;
  z-index:0;
}

.inist-stats-v6 .card::after{
  content:"";
  position:absolute;
  inset:0;
  border-radius:inherit;
  border:1px solid rgba(255,255,255,.10);
  pointer-events:none;
  z-index:0;
}

.inist-stats-v6 .card > *{
  position:relative;
  z-index:1;
}

.inist-stats-v6 .card:hover{
  transform:translateY(-4px);
  box-shadow:var(--shadow-hover);
  filter:saturate(1.03);
}

.inist-stats-v6 .number{
  font-size:3rem;
  font-weight:800;
  margin-bottom:4px;
  line-height:1.05;
  letter-spacing:-.02em;
}

.inist-stats-v6 .text{
  font-size:1rem;
  line-height:1.25;
  font-weight:500;
  max-width:18ch;
}

.inist-stats-v6 .documents .number,
.inist-stats-v6 .authors .number,
.inist-stats-v6 .period .number{
  font-size:3.4rem;
}

.inist-stats-v6 .oa .number{
  font-size:4rem;
}

.inist-stats-v6 .oa .text{
  font-size:1rem;
}

.inist-stats-v6 .publishers .number,
.inist-stats-v6 .countries .number,
.inist-stats-v6 .institutions .number,
.inist-stats-v6 .sources .number{
  font-size:2.8rem;
}

.inist-stats-v6 .blue{
  background:linear-gradient(180deg,#00345f 0%, var(--blue) 100%);
  color:var(--yellow);
}

.inist-stats-v6 .yellow{
  background:linear-gradient(180deg,#ffef82 0%, var(--yellow) 100%);
  color:var(--blue);
}

.inist-stats-v6 .violet{
  background:linear-gradient(135deg,var(--violet) 0%, var(--violet-dark) 100%);
  color:#fff;
}

.inist-stats-v6 .with-icon .icon{
  position:absolute;
  right:-14px;
  bottom:-14px;
  width:150px;
  height:150px;
  opacity:.08;
  pointer-events:none;
  z-index:0;
}

.inist-stats-v6 .with-icon svg{
  width:100%;
  height:100%;
  stroke:currentColor;
  fill:none;
  stroke-width:1.6;
  stroke-linecap:round;
  stroke-linejoin:round;
}

.inist-stats-v6 .violet.with-icon .icon{
  opacity:.10;
}

@keyframes fadeUpPremium{
  to{
    opacity:1;
    transform:translateY(0);
  }
}

.inist-stats-v6 .card:nth-child(1){ animation-delay:.10s; }
.inist-stats-v6 .card:nth-child(2){ animation-delay:.30s; }
.inist-stats-v6 .card:nth-child(3){ animation-delay:.40s; }
.inist-stats-v6 .card:nth-child(4){ animation-delay:.50s; }
.inist-stats-v6 .card:nth-child(5){ animation-delay:.20s; }
.inist-stats-v6 .card:nth-child(6){ animation-delay:.60s; }
.inist-stats-v6 .card:nth-child(7){ animation-delay:.70s; }
.inist-stats-v6 .card:nth-child(8){ animation-delay:.80s; }
</style>

Construire une présentation synthétique en nid d’abeille à partir de plusieurs indicateurs

Un modèle EJS peut exploiter simultanément les résultats provenant de plusieurs champs afin de construire une présentation synthétique des principales caractéristiques d’un corpus.

Présentation synthétique L'INSTANCE EN CHIFFRES réalisée avec EJS

Dans cet exemple, plusieurs informations sont réunies dans une même visualisation intitulée « L’INSTANCE EN CHIFFRES » : nombre de ressources, part des publications en accès ouvert, auteurs, mots-clés, pays, période de publication, DOI, éditeurs, sources et institutions.

Configurer la routine

La routine ventilate-by est utilisée pour récupérer simultanément les valeurs de plusieurs champs.

Pour chaque valeur rencontrée, la routine fournit notamment trois informations :

  • source : le champ dont provient la valeur
  • target : la valeur rencontrée
  • weight : le nombre d’occurrences de cette valeur

Le modèle EJS peut ainsi distinguer les différents champs grâce à source, exploiter leurs valeurs avec target et utiliser weight lorsque le nombre d’occurrences doit être pris en compte.

Configuration de la routine ventilate-by utilisée pour construire la synthèse

Créer la présentation avec EJS

Les résultats de la routine sont disponibles dans le modèle EJS via root.values.

Le modèle commence par regrouper les valeurs selon leur source. Il peut ensuite effectuer différents traitements selon l’indicateur à produire : compter des valeurs distinctes, additionner des occurrences, calculer un pourcentage ou déterminer une période.

<%
/* ==========================================================
   RÉCUPÉRATION DES DONNÉES
   ========================================================== */

let values = Array.isArray(root.values) ? root.values : [];


/* ==========================================================
   REGROUPEMENT DES VALEURS PAR SOURCE

   On récupère ici les valeurs DISTINCTES de chaque champ.
   Ce regroupement est utilisé pour compter par exemple :
   - les pays ;
   - les sources ;
   - les institutions ;
   - les éditeurs ;
   - les auteurs ;
   - les mots-clés.
   ========================================================== */

let bySource = _.chain(values)
  .groupBy('source')
  .mapValues(items => _.uniq(_.map(items, 'target')))
  .value();


/* ==========================================================
   NOMBRE DE VALEURS DISTINCTES

   À MODIFIER :
   adapter les noms des sources aux champs utilisés
   dans votre propre routine.
   ========================================================== */

let nbCountries = (bySource['Pays'] || []).length;
let nbSources = (bySource['Source originale (OpenAlex)'] || []).length;
let nbInstitutions = (bySource['Institution'] || []).length;
let nbPublishers = (bySource['Éditeur original (OpenAlex)'] || []).length;
let nbAuthors = (bySource['Auteur'] || []).length;
let nbKeywords = (bySource['Mots-clés OpenAlex'] || []).length;


/* ==========================================================
   ACCÈS OUVERT ET NOMBRE TOTAL DE DOCUMENTS

   Dans cet exemple, chaque document possède une valeur
   dans le champ "Accès ouvert".

   La somme des poids de ce champ correspond donc au
   nombre total de documents.
   ========================================================== */

let oaValues = values.filter(v => v.source === 'Accès ouvert');

let totalDocs = _.sumBy(oaValues, v => Number(v.weight) || 0);


/* Nombre de documents en accès ouvert */

let oaYes = _.chain(oaValues)
  .filter(v => v.target === 'Oui')
  .sumBy(v => Number(v.weight) || 0)
  .value();


/* Pourcentage de documents en accès ouvert */

let oaPct = totalDocs ? Math.round((oaYes / totalDocs) * 100) : 0;


/* ==========================================================
   ANNÉE OU PÉRIODE DE PUBLICATION

   Si une seule année est présente, elle est affichée.
   Si plusieurs années sont présentes, le modèle affiche
   la première et la dernière année sous forme de période.
   ========================================================== */

let publicationYears = _.chain(bySource['Année de publication'] || [])
  .map(y => parseInt(y, 10))
  .filter(y => !isNaN(y))
  .sortBy()
  .value();

let publicationYearLabel = publicationYears.length <= 1 ? 'année interrogée' : 'période couverte';

let publicationYearValue = '—';

if (publicationYears.length === 1) {
  publicationYearValue = publicationYears[0];
}

if (publicationYears.length > 1) {
  publicationYearValue = publicationYears[0] + ' – ' + publicationYears[publicationYears.length - 1];
}


/* ==========================================================
   PUBLICATIONS POSSÉDANT UN DOI

   Dans les données utilisées dans cet exemple, les DOI
   absents ont préalablement été remplacés par "n/a".

   À MODIFIER :
   adapter cette valeur si les données utilisent une autre
   manière d'identifier l'absence de DOI.
   ========================================================== */

let nbWithDOI = _.chain(values)
  .filter(v =>
    v.source === 'doi' &&
    v.target != null &&
    String(v.target).trim() !== '' &&
    String(v.target).trim().toLowerCase() !== 'n/a'
  )
  .sumBy(v => Number(v.weight) || 0)
  .value();


/* ==========================================================
   CARTES À AFFICHER

   À MODIFIER :
   - value : valeur affichée ;
   - title : libellé de l'indicateur ;
   - color : couleur du cartouche ;
   - x / y : position du cartouche.
   ========================================================== */

let cards = [
  {value: totalDocs.toLocaleString('fr-FR'), title:'ressources dans l’instance', color:'blue', x:120, y:0},
  {value: oaPct + ' %', title:'publications en accès ouvert', color:'yellow', x:340, y:0},
  {value: nbAuthors.toLocaleString('fr-FR'), title:'auteur(e)s', color:'blue', x:560, y:0},
  {value: nbKeywords.toLocaleString('fr-FR'), title:'mots clés extraits', color:'yellow', x:10, y:180},
  {value: nbCountries.toLocaleString('fr-FR'), title:'pays représentés', color:'blue', x:230, y:180},
  {value: publicationYearValue, title:publicationYearLabel, color:'yellow', x:450, y:180},
  {value: nbWithDOI.toLocaleString('fr-FR'), title:'publications avec DOI', color:'blue', x:670, y:180},
  {value: nbPublishers.toLocaleString('fr-FR'), title:'éditeurs identifiés', color:'blue', x:120, y:360},
  {value: nbSources.toLocaleString('fr-FR'), title:'sources', color:'yellow', x:340, y:360},
  {value: nbInstitutions.toLocaleString('fr-FR'), title:'ENTITÉS CNRS', color:'blue', x:560, y:360}
];


/* ==========================================================
   STYLE GÉNÉRAL

   À MODIFIER :
   ces propriétés permettent d'adapter les dimensions
   générales et la typographie de la présentation.
   ========================================================== */

let containerStyle = 'font-family:Helvetica Neue,Arial,sans-serif;max-width:1100px;margin:0 auto;padding:20px;overflow:hidden;';

let titleStyle = 'text-align:center;font-weight:900;letter-spacing:0.05em;margin:0 0 40px 0;font-size:2em;color:#00284b;text-transform:uppercase;';


/* ==========================================================
   CONTENEUR DU NID D'ABEILLES
   ========================================================== */

let honeycombStyle = 'position:relative;width:880px;height:610px;margin:0 auto;';


/* ==========================================================
   STYLE DES CELLULES

   clip-path construit la forme hexagonale.

   À MODIFIER :
   width et height permettent notamment d'adapter
   la taille des cartouches.
   ========================================================== */

let cellBaseStyle = 'position:absolute;width:200px;height:230px;clip-path:polygon(50% 0%,100% 25%,100% 75%,50% 100%,0% 75%,0% 25%);display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:20px;box-sizing:border-box;';

let valueStyle = 'font-size:2em;font-weight:800;line-height:1.1;';

let labelStyle = 'font-size:0.7em;font-weight:400;margin-top:0.5rem;line-height:1.2;';
%>

<div style="<%= containerStyle %>">

  <div style="<%= titleStyle %>">
    L’INSTANCE EN CHIFFRES
  </div>

  <div style="<%= honeycombStyle %>">

    <% cards.forEach(card => {

      /*
       * Couleurs des hexagones.
       *
       * blue   = fond bleu / texte jaune
       * yellow = fond jaune / texte bleu
       *
       * À MODIFIER :
       * changer ici les couleurs utilisées.
       */

      let colorStyle = card.color === 'blue'
        ? 'background:#00284b;color:#ffeb6e;'
        : 'background:#ffeb6e;color:#00284b;';


      /*
       * Position de chaque hexagone.
       */

      let positionStyle = 'left:' + card.x + 'px;' + 'top:' + card.y + 'px;';

    %>

      <div style="<%= cellBaseStyle + colorStyle + positionStyle %>">

        <div style="<%= valueStyle %>">
          <%= card.value %>
        </div>

        <div style="<%= labelStyle %>">
          <%= card.title %>
        </div>

      </div>

    <% }) %>

  </div>

</div>
Comprendre le fonctionnement du modèle

Regrouper les informations par champ.
La propriété source permet d’identifier le champ dont provient chaque valeur. Le modèle construit ainsi un regroupement permettant d’exploiter séparément les pays, les auteurs, les institutions, les mots-clés ou encore les années de publication.

Compter des valeurs distinctes.
Pour certains indicateurs, comme le nombre d’auteurs, de pays ou de mots-clés, le modèle compte les valeurs distinctes présentes dans target.

Compter des occurrences.
Pour d’autres indicateurs, le nombre de valeurs distinctes ne suffit pas. Le modèle utilise alors weight, qui indique le nombre d’occurrences associé à une valeur.

Calculer de nouvelles informations.
EJS peut également effectuer des calculs qui ne sont pas directement fournis par la routine. Dans cet exemple, il calcule notamment le pourcentage de publications en accès ouvert et détermine automatiquement la période couverte par le corpus.

Construire la présentation.
Une fois les indicateurs calculés, ils sont réunis dans le tableau cards. Chaque élément définit la valeur, le libellé, la couleur et la position d’un cartouche.

Adapter le modèle à vos données

Adapter les noms des champs.
Les valeurs utilisées dans bySource, comme Pays, Auteur ou Mots-clés OpenAlex, doivent correspondre aux noms des sources produites par votre routine.

Choisir les indicateurs.
Le tableau cards détermine les informations affichées. Il est possible de supprimer un indicateur, d’en ajouter un nouveau ou de modifier son libellé.

Adapter les règles de calcul.
Les calculs doivent tenir compte de la structure des données utilisées. Dans cet exemple, les DOI absents ont préalablement été remplacés par n/a. Cette valeur est donc exclue lors du calcul du nombre de publications possédant un DOI.

Modifier les couleurs.
Les couleurs des cartouches sont définies dans colorStyle. Les couleurs #00284b et #ffeb6e peuvent être remplacées par celles correspondant à votre identité visuelle.

Modifier la disposition.
Les propriétés x et y de chaque élément du tableau cards déterminent la position des hexagones. Elles peuvent être adaptées pour modifier l’organisation du nid d’abeilles.

Construire un arbre hiérarchique à partir de données structurées

Ce modèle permet de représenter sous forme d’arbre des données organisées selon plusieurs niveaux hiérarchiques.

Dans cet exemple, l’arbre est affiché sur une page ressource et représente la classification thématique OpenAlex selon quatre niveaux :

  • Domain : domaine
  • Field : champ
  • Subfield : sous-champ
  • Topic : sujet

Le modèle EJS reconstruit automatiquement cette hiérarchie et génère les différents cartouches ainsi que les traits permettant de visualiser les relations entre les niveaux.

Bien que cet exemple utilise la classification thématique d’OpenAlex, le principe peut être réutilisé avec d’autres données organisées selon plusieurs niveaux hiérarchiques.


Arbre hiérarchique représentant des domaines, champs, sous-champs et sujets

Préparer les données sous forme de matrice

Avant de construire l’arbre, les données du champ doivent être organisées sous la forme d’une matrice.

Chaque ligne de cette matrice correspond à un chemin complet dans la hiérarchie. Dans cet exemple, chaque chemin contient successivement un domaine, un champ, un sous-champ et un sujet :

[
  ["Domain", "Field", "Subfield", "Topic"],
  ["Domain", "Field", "Subfield", "Topic"],
  ["Domain", "Field", "Subfield", "Topic"]
]

Plusieurs chemins peuvent partager certains niveaux. Par exemple, plusieurs sujets peuvent appartenir au même sous-champ, plusieurs sous-champs au même champ, etc.

C’est cette organisation sous forme de chemins qui permet ensuite au modèle EJS de retrouver les éléments communs et de reconstruire automatiquement les différentes ramifications de l’arbre.

Configurer la routine

L’arbre étant affiché sur une page ressource, les données nécessaires à sa construction sont récupérées à l’aide de la routine get-fields.

Configurez cette routine pour récupérer le champ contenant la matrice construite précédemment.

Le résultat de la routine est ensuite transmis au modèle EJS dans root.values. Dans le modèle présenté ci-dessous, les différents chemins hiérarchiques sont récupérés dans _id.

Le cheminement des données peut donc être résumé ainsi :

Champ contenant la matrice → routine get-fields → root.values → modèle EJS → arbre hiérarchique

Code complet du modèle

Le code est commenté afin de faciliter sa réutilisation. Les principales parties pouvant être adaptées sont signalées par la mention À MODIFIER. Les autres parties assurent la construction automatique de l’arbre et peuvent généralement être conservées telles quelles.

<%
/*
 * ==========================================================
 * ARBRE HIÉRARCHIQUE
 * ==========================================================
 *
 * Ce modèle transforme une matrice en arbre hiérarchique.
 *
 * Dans cet exemple :
 *
 * Domain > Field > Subfield > Topic
 *
 * Les données sont récupérées sur une page ressource
 * à l'aide d'une routine get-fields.
 *
 * Le résultat de la routine est disponible dans root.values.
 *
 *
 * ----------------------------------------------------------
 * POUR ADAPTER CE MODÈLE
 * ----------------------------------------------------------
 *
 * Les principales parties à modifier sont :
 *
 * 1. levelNames
 *    → noms affichés pour les différents niveaux
 *
 * 2. nodeStyle()
 *    → couleurs, taille et apparence des cartouches
 *
 * 3. levelTitleStyle()
 *    → couleur du nom de chaque niveau
 *
 * 4. branchWidth
 *    → espacement horizontal entre les branches
 *
 * 5. renderForest()
 *    → espacement entre les arbres principaux
 *
 * Le reste du code construit automatiquement l'arbre
 * à partir des données et peut généralement être conservé.
 */


/* ==========================================================
   RÉCUPÉRATION DES CHEMINS DEPUIS LA ROUTINE

   Chaque chemin est attendu sous la forme :
   [niveau 1, niveau 2, niveau 3, niveau 4]

   Aucune modification n'est normalement nécessaire ici,
   sauf si la structure des données fournie par la routine
   est différente.
   ========================================================== */

var paths = [];

(root.values || []).forEach(function(item) {

    if (!item || !Array.isArray(item._id)) {
        return;
    }

    item._id.forEach(function(path) {

        if (Array.isArray(path)) {
            paths.push(path);
        }

    });

});


/* ==========================================================
   CONSTRUCTION DE LA HIÉRARCHIE

   Cette partie transforme les différents chemins de la
   matrice en une structure arborescente.

   Les éléments communs à plusieurs chemins sont regroupés
   automatiquement.

   Aucune modification n'est normalement nécessaire.
   ========================================================== */

var tree = {};

paths.forEach(function(path) {

    var current = tree;

    path.forEach(function(label) {

        if (!label) {
            return;
        }

        if (!current[label]) {
            current[label] = {};
        }

        current = current[label];

    });

});


/* ==========================================================
   À MODIFIER : NOMS DES NIVEAUX

   Ces libellés sont affichés dans les cartouches.

   Remplacez-les pour adapter l'arbre à la structure
   de vos propres données.
   ========================================================== */

var levelNames = {
    1: "DOMAIN",
    2: "FIELD",
    3: "SUBFIELD",
    4: "TOPIC"
};


/* ==========================================================
   CALCUL DE L'ESPACE NÉCESSAIRE

   Cette fonction compte le nombre d'éléments terminaux
   présents dans une branche.

   Cette information permet ensuite de réserver davantage
   de largeur aux branches qui contiennent beaucoup
   de ramifications.

   Aucune modification n'est normalement nécessaire.
   ========================================================== */

function countLeaves(node) {

    var keys = Object.keys(node);

    if (keys.length === 0) {
        return 1;
    }

    var total = 0;

    keys.forEach(function(key) {
        total += countLeaves(node[key]);
    });

    return total;
}


/* ==========================================================
   À MODIFIER : APPARENCE DES CARTOUCHES

   La variable "common" contient les propriétés communes
   à tous les niveaux.

   Vous pouvez notamment modifier :

   width         → largeur des cartouches
   height        → hauteur des cartouches
   padding       → espace intérieur
   border-radius → arrondi des angles
   line-height   → espacement entre les lignes

   Chaque niveau possède ensuite ses propres couleurs :

   background → couleur de fond
   border     → couleur de la bordure
   color      → couleur du texte
   ========================================================== */

function nodeStyle(level) {

    var common =
        "box-sizing:border-box;" +
        "width:220px;" +
        "height:82px;" +
        "padding:10px 14px;" +
        "border-radius:8px;" +
        "display:flex;" +
        "flex-direction:column;" +
        "align-items:center;" +
        "justify-content:center;" +
        "text-align:center;" +
        "line-height:1.3;";


    /* ------------------------------------------------------
       À MODIFIER : DOMAIN
       Premier niveau de l'arbre
       ------------------------------------------------------ */

    if (level === 1) {

        return common +
            "background:#00284b;" +
            "border:2px solid #00284b;" +
            "color:#ffffff;";
    }


    /* ------------------------------------------------------
       À MODIFIER : FIELD
       Deuxième niveau de l'arbre
       ------------------------------------------------------ */

    if (level === 2) {

        return common +
            "background:#e8f1f8;" +
            "border:2px solid #6f9fbd;" +
            "color:#00284b;";
    }


    /* ------------------------------------------------------
       À MODIFIER : SUBFIELD
       Troisième niveau de l'arbre
       ------------------------------------------------------ */

    if (level === 3) {

        return common +
            "background:#fff7cc;" +
            "border:2px solid #e4c84f;" +
            "color:#3b3300;";
    }


    /* ------------------------------------------------------
       À MODIFIER : TOPIC
       Quatrième niveau de l'arbre
       ------------------------------------------------------ */

    return common +
        "background:#ecfdf5;" +
        "border:2px solid #7bc8a4;" +
        "color:#134e3a;";
}


/* ==========================================================
   À MODIFIER : COULEUR DU NOM DE CHAQUE NIVEAU

   Cette fonction définit la couleur des libellés
   DOMAIN, FIELD, SUBFIELD et TOPIC affichés
   au-dessus de chaque valeur.
   ========================================================== */

function levelTitleStyle(level) {

    if (level === 1) {
        return "color:#ffeb6e;";
    }

    if (level === 2) {
        return "color:#527d99;";
    }

    if (level === 3) {
        return "color:#8a7200;";
    }

    return "color:#34785d;";
}


/* ==========================================================
   GÉNÉRATION AUTOMATIQUE DES BRANCHES

   Cette fonction construit chaque partie de l'arbre :

   - le cartouche ;
   - son libellé ;
   - les traits de liaison ;
   - ses différents enfants.

   La fonction s'appelle ensuite elle-même pour construire
   progressivement tous les niveaux de la hiérarchie.

   Aucune modification n'est normalement nécessaire dans
   cette partie, à l'exception des paramètres explicitement
   signalés ci-dessous.
   ========================================================== */

function renderNode(label, children, level) {

    var childLabels = Object.keys(children);

    var leafCount = countLeaves(children);


    /* ------------------------------------------------------
       À MODIFIER SI NÉCESSAIRE : LARGEUR DES BRANCHES

       220 = largeur minimale réservée à une branche

       240 = espace réservé pour chaque branche terminale

       Augmentez 240 si les branches sont trop rapprochées.
       Diminuez cette valeur pour obtenir un arbre plus compact.
       ------------------------------------------------------ */

    var branchWidth = Math.max(
        220,
        leafCount * 240
    );

    var html = "";


    /* ======================================================
       CONTENEUR DE LA BRANCHE

       Sa largeur est calculée automatiquement à partir
       du nombre de ramifications présentes sous le nœud.
       ====================================================== */

    html +=
        '<div style="' +
        'display:flex;' +
        'flex-direction:column;' +
        'align-items:center;' +
        'position:relative;' +
        'box-sizing:border-box;' +
        'width:' + branchWidth + 'px;' +
        'flex:0 0 ' + branchWidth + 'px;' +
        '">';


    /* ======================================================
       CRÉATION DU CARTOUCHE
       ====================================================== */

    html += '<div style="' + nodeStyle(level) + '">';


    /* ------------------------------------------------------
       Nom du niveau : DOMAIN, FIELD, SUBFIELD ou TOPIC
       ------------------------------------------------------ */

    html +=
        '<div style="' +
        'font-size:10px;' +
        'font-weight:700;' +
        'letter-spacing:1.2px;' +
        'margin-bottom:5px;' +
        levelTitleStyle(level) +
        '">' +
        levelNames[level] +
        '</div>';


    /* ------------------------------------------------------
       Valeur correspondant au niveau
       ------------------------------------------------------ */

    html +=
        '<div style="' +
        'font-size:' + (level === 1 ? '16px' : '14px') + ';' +
        'font-weight:' + (level < 4 ? '600' : '500') + ';' +
        '">' +
        label +
        '</div>';


    html += '</div>';


    /* ======================================================
       CONSTRUCTION DES NIVEAUX SUIVANTS

       On continue uniquement si le nœud possède des enfants
       et si le quatrième niveau n'a pas encore été atteint.
       ====================================================== */

    if (childLabels.length > 0 && level < 4) {


        /* --------------------------------------------------
           PREMIER TRAIT VERTICAL

           Il relie le cartouche à la zone où commenceront
           les éventuelles ramifications.

           À MODIFIER :
           height:28px permet de modifier sa longueur.
           -------------------------------------------------- */

        html +=
            '<div style="' +
            'width:2px;' +
            'height:28px;' +
            'background:#94a3b8;' +
            'flex:0 0 28px;' +
            '"></div>';


        /* ==================================================
           CAS 1 : LE NŒUD POSSÈDE PLUSIEURS ENFANTS

           Une ligne horizontale est construite afin de
           répartir les différentes branches.
           ================================================== */

        if (childLabels.length > 1) {

            html +=
                '<div style="' +
                'display:grid;' +
                'grid-template-columns:repeat(' +
                    childLabels.length +
                    ', 1fr);' +
                'align-items:start;' +
                'justify-items:center;' +
                'position:relative;' +
                'width:100%;' +
                'box-sizing:border-box;' +
                '">';


            childLabels.forEach(function(childLabel, index) {

                var isFirst = index === 0;
                var isLast = index === childLabels.length - 1;


                html +=
                    '<div style="' +
                    'position:relative;' +
                    'display:flex;' +
                    'flex-direction:column;' +
                    'align-items:center;' +
                    'justify-content:flex-start;' +
                    'padding-top:28px;' +
                    'box-sizing:border-box;' +
                    'width:100%;' +
                    '">';


                /* ------------------------------------------
                   Trait horizontal vers la gauche

                   Il n'est pas nécessaire pour le premier
                   enfant de la branche.
                   ------------------------------------------ */

                if (!isFirst) {

                    html +=
                        '<div style="' +
                        'position:absolute;' +
                        'top:0;' +
                        'right:50%;' +
                        'width:50%;' +
                        'height:2px;' +
                        'background:#94a3b8;' +
                        '"></div>';

                }


                /* ------------------------------------------
                   Trait horizontal vers la droite

                   Il n'est pas nécessaire pour le dernier
                   enfant de la branche.
                   ------------------------------------------ */

                if (!isLast) {

                    html +=
                        '<div style="' +
                        'position:absolute;' +
                        'top:0;' +
                        'left:50%;' +
                        'width:50%;' +
                        'height:2px;' +
                        'background:#94a3b8;' +
                        '"></div>';

                }


                /* ------------------------------------------
                   Trait vertical vers l'enfant
                   ------------------------------------------ */

                html +=
                    '<div style="' +
                    'position:absolute;' +
                    'top:0;' +
                    'left:50%;' +
                    'width:2px;' +
                    'height:28px;' +
                    'background:#94a3b8;' +
                    'transform:translateX(-1px);' +
                    '"></div>';


                /* ------------------------------------------
                   Construction du sous-arbre

                   renderNode est appelée à nouveau pour
                   construire le niveau suivant.
                   ------------------------------------------ */

                html +=
                    '<div style="' +
                    'display:flex;' +
                    'justify-content:center;' +
                    'width:100%;' +
                    '">';


                html += renderNode(
                    childLabel,
                    children[childLabel],
                    level + 1
                );


                html += '</div>';

                html += '</div>';

            });


            html += '</div>';

        }


        /* ==================================================
           CAS 2 : LE NŒUD NE POSSÈDE QU'UN SEUL ENFANT

           Il n'est pas nécessaire de créer de ramification.
           On poursuit simplement l'arbre verticalement.
           ================================================== */

        else {

            var onlyChild = childLabels[0];


            /* ----------------------------------------------
               DEUXIÈME TRAIT VERTICAL

               Ce deuxième segment permet de conserver
               le même espacement vertical que lorsqu'un
               nœud possède plusieurs enfants.

               À MODIFIER :
               height:28px permet de modifier sa longueur.
               ---------------------------------------------- */

            html +=
                '<div style="' +
                'width:2px;' +
                'height:28px;' +
                'background:#94a3b8;' +
                'flex:0 0 28px;' +
                '"></div>';


            html += renderNode(
                onlyChild,
                children[onlyChild],
                level + 1
            );

        }

    }


    html += '</div>';

    return html;
}


/* ==========================================================
   AFFICHAGE FINAL DES ARBRES

   Chaque élément du premier niveau est affiché comme
   un arbre indépendant.

   Dans cet exemple, les différents DOMAIN sont donc
   disposés côte à côte.

   À MODIFIER :
   gap:60px permet d'augmenter ou de réduire l'espace
   entre ces arbres principaux.
   ========================================================== */

function renderForest() {

    var domains = Object.keys(tree);

    var html =
        '<div style="' +
        'display:flex;' +
        'flex-direction:row;' +
        'align-items:flex-start;' +
        'justify-content:center;' +
        'gap:60px;' +
        'width:max-content;' +
        'min-width:100%;' +
        '">';


    domains.forEach(function(domain) {

        html += renderNode(
            domain,
            tree[domain],
            1
        );

    });


    html += '</div>';

    return html;
}
%>


<div style="
    width:100%;
    overflow-x:auto;
    box-sizing:border-box;
    padding:30px 15px 50px 15px;
    font-family:Arial,sans-serif;
">

    <%- renderForest() %>

</div>
Comprendre le fonctionnement du modèle

Le modèle réalise plusieurs opérations successives à partir du résultat fourni par la routine get-fields.

Récupérer les chemins hiérarchiques. Les données disponibles dans root.values sont d’abord parcourues afin de récupérer les différents chemins présents dans _id. Chaque chemin correspond à une branche complète de la hiérarchie :

Domain → Field → Subfield → Topic

Regrouper les chemins communs. Le modèle transforme ensuite ces chemins en une structure arborescente. Lorsque plusieurs chemins possèdent les mêmes valeurs dans leurs premiers niveaux, celles-ci ne sont créées qu’une seule fois. Les ramifications apparaissent uniquement à partir du niveau où les chemins deviennent différents.

Adapter automatiquement l’espace disponible. Toutes les branches n’occupent pas nécessairement la même largeur. Un domaine comportant de nombreux sujets a besoin de davantage d’espace qu’un domaine ne contenant qu’un seul chemin. La fonction countLeaves compte donc le nombre d’éléments terminaux présents sous chaque branche afin de calculer automatiquement la largeur à lui réserver.

Construire les différents niveaux. La fonction renderNode construit un cartouche puis vérifie si celui-ci possède des enfants. Lorsqu’il possède plusieurs enfants, le modèle crée les ramifications nécessaires. Lorsqu’il n’en possède qu’un seul, l’arbre se poursuit verticalement. La même fonction est ensuite utilisée pour construire le niveau suivant jusqu’à atteindre le dernier niveau de la hiérarchie.

Adapter l’arbre à vos données

Le modèle peut être réutilisé pour représenter d’autres données hiérarchiques. Dans la plupart des cas, seules quelques parties du code doivent être modifiées.

Modifier le nom des niveaux. Les intitulés affichés dans les cartouches sont définis dans levelNames :

var levelNames = {
    1: "DOMAIN",
    2: "FIELD",
    3: "SUBFIELD",
    4: "TOPIC"
};

Remplacez ces valeurs par les noms correspondant aux niveaux de votre propre hiérarchie.

Modifier la taille des cartouches. La largeur et la hauteur communes aux différents cartouches sont définies dans la fonction nodeStyle :

"width:220px;" +
"height:82px;" +

Ces valeurs peuvent notamment être augmentées lorsque les libellés affichés sont particulièrement longs.

Modifier les couleurs des niveaux. Chaque niveau possède son propre style dans la fonction nodeStyle. Par exemple, le premier niveau utilise :

"background:#00284b;" +
"border:2px solid #00284b;" +
"color:#ffffff;"

Les propriétés background, border et color permettent respectivement de modifier la couleur de fond, la bordure et la couleur du texte.

La couleur du nom du niveau affiché dans le cartouche peut également être modifiée dans la fonction levelTitleStyle.

Modifier l’espacement horizontal des branches. La largeur réservée à une branche dépend du nombre d’éléments terminaux qu’elle contient :

var branchWidth = Math.max(
    220,
    leafCount * 240
);

Dans cet exemple, 220 correspond à la largeur minimale réservée à une branche et 240 à l’espace réservé pour chaque branche terminale.

Si les branches ou les cartouches sont trop rapprochés, augmentez la valeur 240. À l’inverse, vous pouvez la réduire pour obtenir un arbre plus compact.

Modifier l’espacement vertical entre les niveaux. Les traits verticaux utilisent une hauteur de 28px :

"height:28px;"

Cette valeur peut être augmentée ou réduite pour modifier l’espacement vertical entre les différents niveaux.

Dans le cas d’une branche ne comportant qu’un seul enfant, deux segments verticaux sont successivement utilisés afin de conserver le même espacement que dans les branches comportant plusieurs enfants.

Modifier l’espace entre les arbres principaux. Lorsque plusieurs éléments existent au premier niveau, ils sont affichés côte à côte. Leur espacement est défini dans la fonction renderForest :

"gap:60px;"

Augmentez ou réduisez cette valeur pour modifier l’espace entre les différents arbres principaux.

Adapter le nombre de niveaux. Le modèle présenté ici est construit pour une hiérarchie comportant quatre niveaux. La poursuite de la construction de l’arbre est notamment contrôlée par cette condition :

if (childLabels.length > 0 && level < 4)

Pour utiliser davantage de niveaux, cette limite doit être adaptée et les nouveaux intitulés doivent être ajoutés dans levelNames. Si vous souhaitez donner une apparence spécifique aux niveaux supplémentaires, il faudra également adapter les fonctions définissant le style des cartouches.

LaTeX

je n’arrive pas à tester, qu’est ce qu’on doit avoir ?? ?
Si veut récupérer des données en Latex dans un champ d’un article, sélectionner le séparateur.

Liste de valeurs

Le format List est un format de présentation des données en tableau ou sous forme de liste (à puces, ordonnée, désordonnée, liste à plat).

Il est possible de préciser le format des valeurs du tableau, en mettant une deuxième information.
=> Exemple : appliquer un format « Lodex-ressource », permettra d’afficher les ressources en renseignant les arks, dans « Comment la valeur est créée » et d’aller directement sur cette ressource en la sélectionnant.

Paragraphe

Le format paragraphe affiche un texte sur plusieurs lignes.
Sa particularité est qu’il soit éditable en paragraphe contrairement au format par défaut, qui met tout le texte sur une seule ligne.

Phrase

Ce format permet de mettre une phrase dans le champ.

SPARQL

En choisissant ce format la page à renseigner permet d’écrire des requêtes SPARQL et d’aligner nos données avec par exemple : Wikidata, IDRef, dataBNF, Mirabel…

Le format SPARQL texte dispose de plusieurs paramètres a renseigner : SPARQL endpoint, SPARQL request et max value permettent de définir, pour la ressource dédiée, les éléments à afficher et la quantité.

Sparql endpoint

-Le paramètre SPARQL endpoint permet de sélectionner la base de données SPARQL par exemple  « https://query.wikidata.org/sparql » (https://data.istex.fr/sparql/ est renseigné par défaut)
-Le SPARQL endpoint doit être une URL valide.

Sparql request

-Le paramètre SPARQL request contient la requête qui va être exécutée sur le endpoint.
-Pour indiquer la ressource qui va être incluse dans la requête, elle devra être signalée par « ?? ».

La requête est valable pour tous les cas les ?? correspondent à une variable qui va chercher les valeurs du champ concerné (crayon en admin).

=>Exemple : Pour la revue de sommaire et son alignement des titres avec wikidata les ?? correspondent aux titres : physiology, nature , lancet…etc

-Si la valeur de la ressource ou le corps de la requête est incorrect, la requête retournera une erreur et affichera « We are not able to display data ».
-Les valeurs ne seront prises en compte que lors du chargement de la page.

-Les variables présentes dans la requête seront affichées en séparant les mots : la majuscule faisant office de séparateur

=> Exemple :       SELECT ?IDwikidata ?nom ?nomFrancais ?image ?Classe ?coordonneesGeographiques ?nomPays ?departement ?NbHabitants ?capitale ?fuseauHoraire ?localisationSurLaCarte ?VIAF ?LC ?ISNI ?SUDOC ?BabelNet

-Un lien vers un validateur SPARQL est disponible à la fin  de la requête.

Le site ne fonctionne plus …mettre un autre site pour tester le SPARQL? ou on supprime cette possibilité

Max value

-Le paramètre max value ignore la clause LIMIT pour la remplacer par la valeur fixée.
-Augmenter le nombre de requêtes influe sur le temps de réponse du enpoint interrogé.
-Autrement dit, c’est le nombre de lignes (ou de résultats) affichées.

Hide resource field & separator

-Il est possible de masquer le champ indiquant la ressource ainsi que le SPARQL endpoint.

-En utilisant dans la requête une concaténation, il est possible d’afficher la valeur en forme de liste en indiquant le séparateur utilisé : ;; ou  &&

Syntaxe Markdown

Ce format texte permet de renseigner les données d’un champ en Markdown, qui est une syntaxe permettant d’écrire d’une manière simplifiée un texte qui sera transformé en HTML. (voir syntaxe)
Avec ce format il est possible d’insérer dans un même champ LODEX des paragraphes, des sections, du gras, de l’italique, des liens, …

Titre

Ce format permet de styler le texte, aux choix Tire 1, Titre 2, Titre 3 et Titre 4 et de choisir une couleur de police.

Comment pouvons-nous aider ?