From 70f4aebc5fb005362652b0613b160b067281b3bd Mon Sep 17 00:00:00 2001 From: Clement Levallois <1244100+seinecle@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:10:55 +0200 Subject: [PATCH] Modernize Gephi 0.11.2 plugin development docs --- gephi-desktop/docs/Plugins/Appearance.md | 116 +++++ .../docs/Plugins/Desktop_UI_and_Events.md | 157 +++++++ .../Plugins/De\314\201marrage_rapide_(FR).md" | 312 ------------ gephi-desktop/docs/Plugins/Export.md | 265 +++++------ .../docs/Plugins/Extend_Data_Laboratory.md | 443 ++++++------------ gephi-desktop/docs/Plugins/Filter.md | 328 ++++--------- gephi-desktop/docs/Plugins/Generator.md | 169 ++++--- gephi-desktop/docs/Plugins/Getting_Started.md | 175 +++++++ .../docs/Plugins/Graph_API_and_Threads.md | 232 +++++++++ gephi-desktop/docs/Plugins/Import.md | 311 ++++++------ gephi-desktop/docs/Plugins/Layout.md | 196 ++++++-- .../docs/Plugins/Preview_renderer.md | 341 ++++---------- gephi-desktop/docs/Plugins/Statistics.md | 270 +++++------ gephi-desktop/docs/Plugins/index.md | 69 ++- 14 files changed, 1708 insertions(+), 1676 deletions(-) create mode 100644 gephi-desktop/docs/Plugins/Appearance.md create mode 100644 gephi-desktop/docs/Plugins/Desktop_UI_and_Events.md delete mode 100644 "gephi-desktop/docs/Plugins/De\314\201marrage_rapide_(FR).md" create mode 100644 gephi-desktop/docs/Plugins/Getting_Started.md create mode 100644 gephi-desktop/docs/Plugins/Graph_API_and_Threads.md diff --git a/gephi-desktop/docs/Plugins/Appearance.md b/gephi-desktop/docs/Plugins/Appearance.md new file mode 100644 index 0000000..81940ab --- /dev/null +++ b/gephi-desktop/docs/Plugins/Appearance.md @@ -0,0 +1,116 @@ +--- +id: Appearance +title: Appearance +sidebar_position: 12 +--- + +Appearance transformers add ways to map graph values to visual properties such as node size, element color, label size, or label color. They change graph element appearance; they are not Preview renderers. + +Use direct Graph API mutations when your plugin applies one particular style as part of its own command. Implement a transformer when users should choose a new reusable mapping from Gephi's Appearance panel. + +Add `graph-api` for direct styling. Add `appearance-api` and `org-openide-util-lookup` when contributing a transformer. + +## Apply an appearance from your own command + +For a fixed operation, change the visible elements under the appropriate graph lock: + +```java +Graph graph = graphModel.getGraphVisible(); +graph.writeLock(); +try { + for (Node node : graph.getNodes()) { + node.setColor(new Color(31, 119, 180)); + node.setSize(12f); + } +} finally { + graph.writeUnlock(); +} +``` + +Resolve `graphModel` for the current workspace when the operation starts. Use the visible graph when the command promises to style the filtered result, and the complete graph only when it explicitly promises to style all elements. This path applies a style; it does not add a new mapping to the Appearance panel. + +## Choose the mapping type + +- `SimpleTransformer` applies one configured style to every eligible element. +- `RankingTransformer` receives a numeric value and its normalized position from 0 to 1. +- `PartitionTransformer` receives a categorical value and its partition configuration. + +All three also implement `Transformer`, whose `isNode()` and `isEdge()` methods declare their target. + +## A ranking transformer + +This service maps a normalized number to node opacity: + +```java +@ServiceProvider(service = Transformer.class) +public final class RankingNodeOpacityTransformer + implements RankingTransformer { + private float minimum = 0.2f; + private float maximum = 1f; + + @Override + public void transform( + Node node, Ranking ranking, + Number value, float normalizedValue) { + float opacity = minimum + + normalizedValue * (maximum - minimum); + Color color = node.getColor(); + node.setColor(new Color( + color.getRed(), color.getGreen(), color.getBlue(), + Math.round(opacity * 255f))); + } + + @Override public boolean isNode() { return true; } + @Override public boolean isEdge() { return false; } + + public float getMinimum() { return minimum; } + public void setMinimum(float value) { minimum = clamp(value); } + public float getMaximum() { return maximum; } + public void setMaximum(float value) { maximum = clamp(value); } + private float clamp(float value) { + return Math.max(0f, Math.min(1f, value)); + } +} +``` + +The Appearance engine supplies the value and normalized value. Do not scan the graph inside `transform()`; it is called once per element. Decide how missing values behave in the associated function/UI and keep the transformer itself quick. + +## Make it visible in the Appearance panel + +A transformer service performs the operation. A matching singleton `TransformerUI` describes how it appears and configures the current `Function`: + +- `getTransformerClass()` returns the exact transformer class. +- `getCategory()` selects or defines the visual category. +- `getPanel(Function)` returns controls for the current ranking, partition, or simple function. +- Name, description, icon, and registration `position` control presentation. +- Optional control buttons add compact actions; `onApply(Function)` reacts after application. + +Register it separately: + +```java +@ServiceProvider(service = TransformerUI.class, position = 2000) +public final class RankingNodeOpacityUI + implements TransformerUI { + // Implement the TransformerUI contract and return + // RankingNodeOpacityTransformer.class from getTransformerClass(). +} +``` + +Because `TransformerUI` is a singleton, store no workspace-specific function in a field after the panel is done with it. Configure the transformer attached to the supplied `Function`, following the current built-in Appearance UIs for the exact function-to-transformer access pattern. + +## Ranking and partition design + +For ranking, validate the endpoints and define interpolation clearly. Normalized input can still encounter a zero-range column; do not divide by the raw range yourself when Gephi already supplies `normalizedValue`. + +For partitions, choose a deterministic value-to-style mapping and provide a stable fallback for null or newly appearing categories. Do not use `toString()` as a persistent category identity unless the underlying type guarantees it. + +## Appearance checklist + +- Node/edge applicability is correct. +- The transformer implements exactly the simple, ranking, or partition contract it needs. +- Per-element work is constant-time and has no graph-wide scans. +- Minimum/maximum and missing values have explicit behavior. +- A matching `TransformerUI` is registered and does not retain workspace state. +- Changes are tested in Overview, Preview, and export where those visual properties should carry through. + +Use Gephi 0.11.2's [AppearanceAPI](https://github.com/gephi/gephi/tree/v0.11.2/modules/AppearanceAPI) for contracts and [AppearancePlugin](https://github.com/gephi/gephi/tree/v0.11.2/modules/AppearancePlugin) for built-in color and size transformers. diff --git a/gephi-desktop/docs/Plugins/Desktop_UI_and_Events.md b/gephi-desktop/docs/Plugins/Desktop_UI_and_Events.md new file mode 100644 index 0000000..927c112 --- /dev/null +++ b/gephi-desktop/docs/Plugins/Desktop_UI_and_Events.md @@ -0,0 +1,157 @@ +--- +id: Desktop_UI_and_Events +title: Desktop UI and events +sidebar_position: 4 +--- + +Choose a desktop extension by how users should encounter it. A one-shot operation belongs in an action; direct graph-canvas interaction belongs in a tool; persistent controls or results belong in a `TopComponent`. Keep the calculation behind that UI in a separate class. + +## Pick the right surface + +| User interaction | Recommended mechanism | +| --- | --- | +| Click a menu or toolbar command | NetBeans Platform action annotations | +| Click or drag nodes in Overview | Gephi `Tool` and event-listener SPIs | +| Keep settings/results visible | NetBeans `TopComponent` | +| Act when a workspace changes | `WorkspaceListener` | +| Run once after the whole UI is ready | `ModuleInstall` plus `WindowManager.invokeWhenUIReady` | +| Change a Data Laboratory row, cell, or column | A Data Laboratory manipulator | + +Do not create a full window for a command that needs no persistent state. Conversely, do not hide an interactive workflow behind a chain of modal dialogs. + +## Add a menu action + +NetBeans annotations generate registration metadata at compile time: + +```java +@ActionID(category = "Tools", id = "org.example.plugin.CountNodesAction") +@ActionRegistration(displayName = "Count nodes") +@ActionReference(path = "Menu/Plugins", position = 100) +public final class CountNodesAction implements ActionListener { + @Override + public void actionPerformed(ActionEvent event) { + GraphModel model = Lookup.getDefault() + .lookup(GraphController.class) + .getGraphModel(); + + if (model == null) { + DialogDisplayer.getDefault().notify( + new NotifyDescriptor.Message("Open a workspace first.")); + return; + } + + int count = model.getGraphVisible().getNodeCount(); + DialogDisplayer.getDefault().notify( + new NotifyDescriptor.Message("Visible nodes: " + count)); + } +} +``` + +Add the NetBeans modules that own `org.openide.awt`, `org.openide.dialogs`, and `org.openide.util` packages. Use bundle keys for production display text. An action listener runs on the EDT: if the work is more than a quick lookup, launch a [long task](./Graph_API_and_Threads.md#keep-swing-responsive). + +## Add a persistent window + +A `TopComponent` is a dockable NetBeans window. The annotations below register it under **Window** and place it in an existing mode: + +```java +@TopComponent.Description( + preferredID = "MyResultsTopComponent", + persistenceType = TopComponent.PERSISTENCE_ALWAYS +) +@TopComponent.Registration(mode = "filtersmode", openAtStartup = false) +@ActionID(category = "Window", id = "org.example.plugin.MyResultsTopComponent") +@ActionReference(path = "Menu/Window") +@TopComponent.OpenActionRegistration( + displayName = "My results", + preferredID = "MyResultsTopComponent" +) +public final class MyResultsTopComponent extends TopComponent { + public MyResultsTopComponent() { + setName("My results"); + setLayout(new BorderLayout()); + add(buildContent(), BorderLayout.CENTER); + } +} +``` + +Treat the window as a view, not as the data model: + +- Resolve the current workspace when Run is clicked or when a workspace event arrives. +- Store results with the workspace that produced them; clear stale results on `disable()` or `close()`. +- Start observers or timers in `componentOpened()` and destroy them in `componentClosed()`. +- Do not start a graph-wide scan in the constructor. +- Use `SwingUtilities.invokeLater` for UI updates arriving from other threads. + +Generated `.form` files are optional. Hand-written Swing panels are often easier to review and merge; GUI-builder forms are useful for complex fixed layouts. + +## Add a graph interaction tool + +A tool appears in the Overview toolbar and can receive canvas or node events. Implement `org.gephi.tools.spi.Tool`, register it as a service, and provide a `ToolUI`: + +```java +@ServiceProvider(service = Tool.class) +public final class InspectTool implements Tool { + private final ToolUI ui = new InspectToolUI(); + + @Override public void select() { } + @Override public void unselect() { } + + @Override + public ToolEventListener[] getListeners() { + return new ToolEventListener[] { + (NodeClickEventListener) nodes -> { + inspect(nodes); + return false; + } + }; + } + + @Override public ToolUI getUI() { return ui; } + + @Override + public ToolSelectionType getSelectionType() { + return ToolSelectionType.SELECTION; + } +} +``` + +`ToolUI` supplies the name, description, icon, ordering position, and optional properties-bar panel. Event listeners should capture the event and return quickly. If inspection is expensive, copy stable node IDs and delegate the work; do not retain arbitrary node arrays indefinitely across graph changes. + +Use public `ToolsAPI` and `VisualizationAPI` types. Older examples sometimes call internal visualization controllers directly; prefer public services in 0.11.2 so the plugin is not tied to an implementation. + +## Capture lifecycle events + +Use `WorkspaceListener` for project context; its complete pattern is shown in [Graph access and long tasks](./Graph_API_and_Threads.md#react-to-workspace-changes). Register it with `@ServiceProvider(service = WorkspaceListener.class)`. + +For a single action after application startup, extend `ModuleInstall` and register the class in `manifest.mf`: + +```text +OpenIDE-Module-Install: org/example/plugin/Installer.class +``` + +```java +public final class Installer extends ModuleInstall { + @Override + public void restored() { + WindowManager.getDefault().invokeWhenUIReady(() -> { + // Initialize UI integration only; do not perform heavy work here. + }); + } +} +``` + +Startup hooks should be rare. Lazy initialization on first use makes Gephi start faster and avoids work for users who never open the plugin. + +## Internationalization and accessibility + +Place display text in `Bundle.properties` and retrieve it with `NbBundle`. Give controls accessible names and tooltips, associate each label with its input, preserve keyboard navigation, and never communicate state through color alone. User-visible errors should explain what happened and what the user can do next. + +## UI review checklist + +- The entry point is discoverable in the expected Gephi area. +- Controls are disabled when no workspace or valid selection exists. +- Run cannot be started twice; Cancel is available for long work. +- The window remains correct when users switch, close, or create workspaces. +- No internal implementation API is used where a public API exists. +- All listeners and workers have a clear owner and cleanup point. +- UI state is changed only on the EDT. diff --git "a/gephi-desktop/docs/Plugins/De\314\201marrage_rapide_(FR).md" "b/gephi-desktop/docs/Plugins/De\314\201marrage_rapide_(FR).md" deleted file mode 100644 index 26fcda0..0000000 --- "a/gephi-desktop/docs/Plugins/De\314\201marrage_rapide_(FR).md" +++ /dev/null @@ -1,312 +0,0 @@ ---- -id: démarrage-rapide-fr -title: Démarrage rapide (FR) -sidebar_position: 2 ---- - -# Tutoriel Gephi
Démarrage rapide -Bienvenue sur ce tutoriel. Vous apprendrez ici les étapes fondamentales pour visualiser et manipuler un réseau dans Gephi. - -Ce tutoriel se base sur la version 0.7alpha2 de Gephi. -[Télécharger Gephi](https://gephi.org/users/download/) - -# Import de fichiers -## Ouverture d’un fichier graphe -* Téléchargez le fichier [LesMiserables.gexf](https://gephi.org/datasets/LesMiserables.gexf) -* Dans la barre de menu, allez dans le menu **Fichier** et **Ouvrir...** - -~~ Image : tutorial_quickstart1_FR ~~ - -**Format des graphes** -- GEXF -- GraphML -- Pajek NET -- GDF -- GML -- Tulip TLP -- CSV -- ZIP compressé - -## Rapport d’import -
    -
  • Lorsque votre fichier s’ouvre, le rapport récapitule les données trouvées et les problèmes rencontrés.
  • -
      -
    • Nombre de noeuds
    • -
    • Nombre de liens
    • -
    • Type de graphe
    • -
    -
-* Cliquez sur **OK** pour valider et visualiser le graphe. - -~~ Image : tutorial_quickstart2_FR ~~ - -## Affichage du graphe -Nous avons importé le fichier de données LesMiserables.gexf1. Il s’agit du réseau pondéré des apparitions simultanées des personnages du roman _Les Misérables_ de Victor Hugo. -~~ Image : ? ~~ - -Les noeuds sont d’abord placés de manière aléatoire, la présentation que vous voyez peut donc différer légèrement. - -1D. E. Knuth, The Stanford GraphBase: A Platform for Combinatorial Computing, Addison-Wesley, Reading, MA (1993) - -# Visualisation -## Visualisation du graphe -
    -
  • Utilisez votre souris pour vous déplacer dans la visualisation et modifier son échelle.
  • -
      -
    • Zoom : Molette de la souris
    • -~~ Image : ? ~~ -
    • Panoramique : Glisser à droite
    • -~~ Image : ? ~~ -
    -
-* L’**échelle du poids des liens** se trouve en bas de la fenêtre. - -~~ Image : gephi_configure_labels__bisFR ~~ -* Si vous ne voyez plus votre graphe, réinitialisez l’échelle. - -~~ Image : ? ~~ - -# Spatialisation -## Spatialisation du graphe -La fonction principale des algorithmes de spatialisation est de définir la forme du graphe. -* Repérez le module **Spatialisation** dans le volet à gauche de l’écran. -* Sélectionnez **Force Atlas**. -Vous pouvez voir les propriétés de la spatialisation sous son nom. Conservez les valeurs. -* Pour lancer l’algorithme, cliquez sur **Exécuter**. - -**Algorithmes de spatialisation** -La spatialisation des graphes est généralement réalisée à partir d’algorithmes basés sur les forces. Ils suivent un principe simple : les noeuds reliés s’attirent et les noeuds non reliés se repoussent. - -## Contrôle de la spatialisation -Les propriétés de spatialisation vous permettent de contrôler l’algorithme afin de générer une représentation esthétique. -~~ Image : tutorial_quickstart9_FR ~~ -* Pour agrandir le graphe, réglez la **Force de répulsion** sur 10 000. -* Pour valider la valeur modifiée, appuyez sur la touche **Entrée** de votre clavier. -* Vous pouvez maintenant **Arrêter** l’algorithme. - -## Affichage du graphe spatialisé -~~ Image : ? ~~ - -# Classement (par couleur) -## Classement (par couleur) -Le module **Classement** vous permet de configurer la couleur et la taille d’un noeud. -* Cliquez sur le module **Classement** en haut à gauche de l’écran. -* Sélectionnez **Degré** comme paramètre de classement. - -~~ Image : tutorial_quickstart12_FR ~~ - -Le panneau de configuration suivant s’affiche : -~~ Image : tutorial_quickstart13_FR ~~ -* Cliquez sur **Appliquer** pour afficher le résultat. - -## Configuration des couleurs -~~ Image : tutorial_quickstart13_FR ~~ -* Déplacez la souris sur le curseur de dégradé. -~~ Image : ? ~~ -* Pour configurer la couleur, double-cliquez sur les triangles. -~~ Image : ? ~~ - -**Palette** -Pour utiliser la palette, double-cliquez sur le volet. -~~ Image : tutorial_quickstart17_FR + ? ~~ - -## Tableau des résultats du classement -Vous pouvez voir les valeurs du classement en affichant le tableau des résultats. Le noeud Valjean possède 36 liens et est le plus connecté du réseau. -* Activez l’affichage des résultats sous forme de tableau. -* Cliquez à nouveau sur le bouton **Appliquer**. - -~~ Image : tutorial_quickstart18_FR ~~ - -# Métriques -## Métriques -Nous allons calculer la longueur de chemin moyenne pour le réseau. Cela permet de calculer la longueur du chemin entre toutes les paires de noeuds et de fournir des informations sur la distance entre les noeuds. -* Dans le volet droit, cliquez sur le module **Statistiques**. -* Cliquez sur **Exécuter** en face de **Plus courts chemins**. - -~~ Image : tutorial_quickstart19_FR ~~ - -**Métriques disponibles** -- Diamètre -- Plus courts chemins -- Coefficient de Clustering -- PageRank -- HITS -- Betweeness Centrality -- Closeness Centrality -- Excentricité -- Détection de communauté (Modularité) - -## Configuration des métriques -Le panneau de configuration s’affiche. -~~ Image : tutorial_quickstart20_FR ~~ -Pour calculer la métrique, sélectionnez **Dirigé**, puis cliquez sur **OK**. - -## Résultat des métriques -À la fin du traitement, le résultat de la métrique est affiché dans un rapport. -~~ Image : tutorial_quickstart21 ~~ - -# Classement (par taille) -## Classement (par taille) -Les métriques génèrent des rapports généraux ainsi que les résultats pour chaque noeud. -Ainsi, trois nouvelles valeurs ont été créées par l’algorithme **Plus courts chemins**. -
    -
  • **Betweeness Centrality**
  • -
  • **Closeness Centrality**
  • -
  • **Excentricité**
  • -
- -* Retournez sur l’onglet **Classement**. -* Sélectionnez **Betweeness Centrality** dans la liste déroulante. -~~ Image : tutorial_quickstart22_FR ~~ -Cette métrique fait ressortir les noeuds les plus importants dont la fréquence est la plus élevée. - -À présent, paramétrez la taille des noeuds. L’indicateur de **Degree** (Degré) est toujours représenté par des couleurs. -* Pour définir la taille, sélectionnez l’icône en forme de diamant dans la barre d’outils. -* Définissez la taille minimale sur 10 et la taille maximale sur 50. -~~ Image : tutorial_quickstart23_FR ~~ -* Pour voir le résultat, cliquez sur **Appliquer**. - -## Graphe avec différenciation de couleurs et de tailles -~~ Image : ? ~~ -Couleur : Degree (Degré) -Taille : Métrique **Betweeness Centrality** - -# Nouvelle spatialisation -## Nouvelle spatialisation -La spatialisation n’est pas totalement satisfaisante. En effet, des noeuds de grande taille chevauchent parfois des noeuds plus petits. -L’algorithme **Force Atlas** dispose d’une option qui permet de prendre la taille des noeuds en considération lors de la spatialisation. -* Retournez sur le volet **Spatialisation**. -* Cochez l’option **Ajustement par taille**, puis exécutez à nouveau l’algorithme pendant quelques instants. -* Les noeuds ne se chevauchent plus désormais. - -~~ Image : tutorial_quickstart26_FR ~~ - -# Affichage des labels -## Affichage des labels -Explorons le réseau plus en détails maintenant que les couleurs et la taille mettent en valeur les noeuds importants. -~~ Image : tutorial_quickstart27_FR ~~ -* Affichez les labels des noeuds. -~~ Image : tutorial_quickstart ~~ -* Paramétrez l’affichage des labels afin que leur taille soit proportionnelle à celle des noeuds. -~~ Image : gephi_configure_labels__bisbisFR ~~ -* Pour régler la taille des labels, faites glisser le curseur de l’échelle. -~~ Image : gephi_configure_labels__bisbisbisFR ~~ - -# Détection de communauté -## Détection de communauté -La capacité à détecter et à étudier les communautés est essentielle à l’analyse du réseau. Dans notre exemple, nous souhaitons colorer les groupes de noeuds. -Gephi utilise la méthode Louvain1, disponible dans le volet **Statistiques**. -Cliquez sur **Exécuter** en regard de la ligne **Modularité**. -~~ Image : tutorial_quickstart28_FR ~~ - -* Sélectionnez **Aléatoire** dans le volet. -* Pour lancer la détection, cliquez sur **OK**. -~~ Image : tutorial_quickstart29_FR ~~ - -1Blondel V, Guillaume J, Lambiotte R, Mech E (2008) Fast unfolding of communities in large networks. J Stat Mech : Theory Exp 2008:P10008. (http://findcommunities.googlepages.com) - -# Partition -## Partition -L’algorithme de détection de communauté a créé une valeur de **Modularity Class** (classe de modularité) pour chaque noeud. -Le module **Partition** peut utiliser ces nouvelles données pour colorer les communautés. -* Dans le volet gauche, cliquez sur le module **Partition**. -* Pour remplir la liste de partitions, cliquez sur le bouton **Rafraîchir**. -~~ Image : tutorial_quickstart30_FR ~~ - -**Comment visualiser les colonnes affichant les noeuds et les liens ?** -Pour afficher les colonnes et les valeurs des noeuds et des liens, consultez le **Tableau de données**. -Pour actualiser le tableau, cliquez sur l’onglet **Laboratoire des données**, puis cliquez sur **Noeuds**. - -
-* Dans la liste de partitions, sélectionnez **Modularity Class** (classe de modularité). -Dans l’exemple ci-dessous, neuf communautés ont été trouvées. Le nombre de communautés est variable. Une couleur a été définie au hasard pour chaque identifiant de communauté. -~~ Image : tutorial_quickstart31_FR ~~ -* Pour afficher les noeuds en couleur, cliquez sur **Appliquer**. -Pour accéder à la fonction **Générer les couleurs aléatoirement**, cliquez sur le volet à l’aide du bouton droit de la souris. -~~ Image : tutorial_quickstart33_FR ~~ - -## Nouvelle apparence du réseau -~~ Image : ? ~~ - -# Filtrage -## Filtrage -Le filtrage constitue la dernière manipulation. Il est possible de créer des filtres permettant de masquer certains noeuds et liens du réseau. Dans cet exemple, nous allons créer un filtre afin de supprimer les feuilles, c’est-à-dire les noeuds à lien unique. -~~ Image : tutorial_quickstart34_FR ~~ -* Dans le volet droit, cliquez sur le module **Filtres**. -* Dans la catégorie **Topologie**, sélectionnez **Plage de degrés**. -~~ Image : tutorial_quickstart35_FR ~~ -* Sous **Requêtes**, faites glisser l’option sur **Glissez** le filtre ici. -~~ Image : tutorial_quickstart36_FR ~~ -* Pour activer le filtre, cliquez sur **Plage de degrés**. Le volet des paramètres s’affiche. -~~ Image : tutorial_quickstart37_FR ~~ -Ce volet comprend un curseur de réglage de la plage de degrés ainsi qu’un graphique représentant les données, ici la répartition par degrés. -~~ Image : tutorial_quickstart39_FR ~~ -* Déplacez le curseur de gauche afin de régler le degré minimum sur 2. -* Pour appliquer le filtre, cliquez sur le bouton **Filtrer**. -Les noeuds dont le degré est inférieur à 2 sont désormais masqués. - -**Astuce** -Pour modifier les limites des degrés, double-cliquez sur les valeurs correspondantes. -~~ Image : ? ~~ - -## Réseau filtré -~~ Image : ? ~~ -La manipulation est terminée. Nous allons à présent prévisualiser la modélisation et préparer l’export. - -# Prévisualisation -## Prévisualisation -
    -
  • Avant d’exporter le graphe au format SVG ou PDF, allez sur la **Prévisualisation** pour :
  • -
      -
    • obtenir un aperçu exact du graphe ;
    • -
    • y apporter la touche finale.
    • -
    -
-* Dans le ruban, sélectionnez l’onglet **Prévisualisation** : -~~ Image : tutorial_quickstart41_FR ~~ -* Pour afficher la prévisualisation, cliquez sur **Rafraîchir**. -~~ Image : tutorial_quickstart_preview_FR ~~ - -**Astuce** -Si le graphe est trop grand, vous pouvez afficher un graphe partiel en déplaçant le curseur du **Ratio** à 50 % ou à 25 %. - -* Dans la catégorie **Labels de noeud**, activez la case à cocher **Afficher les labels**. -* Cliquez sur **Prévisualiser**. -~~ Image : tutorial_quickstart44_FR ~~ -Dans les **Paramètres d’aperçu**, cliquez sur la liste de **Réglages** prédéfinis et essayez différentes configurations. - -## Prévisualisation du graphe -~~ Image : ~~ - -# Export -## Export au format SVG -À partir de la **Prévisualisation**, cliquez sur **SVG** en face de **Export**. -~~ Image : tutorial_quickstart43_FR ~~ - -Tout comme les PDF, les fichiers SVG sont des graphiques vectoriels. Les images peuvent facilement être redimensionnées. Il est donc possible de les imprimer ou de les intégrer à des présentations en haute résolution. - -Les fichiers SVG peuvent être modifiés à l’aide du logiciel Inkscape ou Adobe Illustrator. -~~ Image : ~~ - -**Captures d’écran en haute résolution ** -Si vous préférez uniquement les captures d’écran au format PNG en haute résolution, cliquez sur l’icône ~~ Image : ? ~~ située sous l’aperçu, dans la barre des paramètres d’aperçu. - -# Enregistrement -## Enregistrement du projet -L’enregistrement de votre projet regroupe l’ensemble des données et des résultats dans un fichier de session unique. -~~ Image : tutorial_quickstart48_FR ~~ -~~ Image : ? ~~ -Si vous n’avez pas suivi toutes les étapes, vous pouvez télécharger la session : -[https://gephi.org/datasets/lesmiserables.gml.zip](https://gephi.org/datasets/lesmiserables.gml.zip) - -# Conclusion -Ce tutoriel vous a présenté les manipulations de base permettant d’ouvrir, de visualiser, de manipuler et de générer un fichier de réseau avec Gephi. -~~ Images : ??? ~~ -Pour en savoir plus : -* [Site Web Gephi](http://gephi.org/) -* [Wiki Gephi](http://wiki.gephi.org/) -* [Forum Gephi](http://wiki.gephi.org/) - -Ces pages sont disponibles uniquement en anglais. - -Dernière mise à jour le 5 mars 2010 \ No newline at end of file diff --git a/gephi-desktop/docs/Plugins/Export.md b/gephi-desktop/docs/Plugins/Export.md index 7a2014b..aeb4b1f 100644 --- a/gephi-desktop/docs/Plugins/Export.md +++ b/gephi-desktop/docs/Plugins/Export.md @@ -1,193 +1,164 @@ --- id: Export title: Export -sidebar_position: 7 +sidebar_position: 8 --- -Exporters export data from Gephi to various targets, like files or streams. +An exporter serializes workspace data to a `Writer` or `OutputStream`. It should not choose the destination path itself: Gephi owns the file dialog or stream and injects the output object. -One can find file exporter examples in Gephi's `ExportPlugin` and `PreviewExport` [modules](https://github.com/gephi/gephi/tree/master/modules). +This tutorial sketches a `.pairs` graph exporter. Add `io-exporter-api`, `graph-api`, `project-api`, `utils-longtask`, and `org-openide-util-lookup`. -## Create a new Exporter +## Export through an installed exporter -### Set Dependencies - -Add `export-api`, `project-api`, and `org-openide-util-lookup` modules as dependencies for your plugin module *MyExport*. - -### Create Exporter Builder - -`ExporterBuilder` is a factory class for building the important instance, all Exporters should have their own builder. -Create a new builder *MyExporterBuilder* class, which implements one of the following interface: - -* [`GraphFileExporterBuilder`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/io/exporter/spi/GraphFileExporterBuilder.html) - For graph export (like GEXF, GraphML, CSV...) -* [`VectorFileExporterBuilder`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/io/exporter/spi/VectorFileExporterBuilder.html) - Vector graphics (like SVG, PDF, ...) -* [`ExportBuilder`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/io/exporter/spi/ExporterBuilder.html) - Anything else - -Let's say we create a exporter for a custom graph format with *.foo* extension, so we choose `GraphFileExporterBuilder`. - -Fill the `getFileTypes()` and `getName()` methods like below, for a single file format supported named *foo*. +Use `ExportController` when your plugin needs a format already provided by Gephi or another installed module: ```java -public String getName() { - return "foo"; -} - -public FileType[] getFileTypes() { - return new FileType[]{new FileType(".foo", "Foo files")}; +ExportController exports = Lookup.getDefault().lookup(ExportController.class); +Exporter exporter = exports.getExporter("gexf"); +if (exporter == null) { + throw new IllegalStateException("The requested exporter is not installed"); } -``` -Add `@ServiceProvider` annotation to your builder class to declare you are implementing an `Exporter` service. Add the following line before *MyExporterBuilder* class definition, as shown below: +if (exporter instanceof GraphExporter graphExporter) { + graphExporter.setWorkspace(workspace); + graphExporter.setExportVisible(true); +} -```java -@ServiceProvider(service = GraphFileExporterBuilder.class) -public class MyExporterBuilder implements GraphFileExporterBuilder{ -... +exports.exportFile(destinationFile, exporter); ``` -This annotation registers your implementation in the system, in order it can be discovered at runtime. - -Put `GraphFileExporterBuilder.class` as the annotation service parameter for graph files, `VectorFileExporterBuilder.class` for vector graphics and `ExportBuilder.class` for the rest. - -### Create Exporter +Choose `exportVisible` deliberately: `true` exports the filtered view and `false` the complete graph. Connect cancellation if the exporter implements `LongTask`. For plugin-controlled destinations, export to a temporary file in the destination directory and move it into place only after success when partial files would be misleading. Implement an exporter below only when adding a new output format. -Create a new exporter class, which implements `GraphExporter`, `VectorExporter` or simply `Exporter`, depending on what you set for the builder. +## Register a builder -The exporter is where the job is done, in its `execute()` method. The main input object the export needs is the Workspace. In Gephi, data are stored within workspaces. It is the place the exporter will find what to export. Before being executed by the export controller, the exporter will receive the workspace and other parameters through setters methods. +Choose the narrowest builder and exporter interfaces: -Implement also [`ByteExporter`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/io/exporter/spi/ByteExporter.html) interface for byte streams or [`CharacterExporter`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/io/exporter/spi/CharacterExporter.html) for texts. These are the two ways you can output data in a exporter, either text (`java.io.Writer`) or byte (`java.io.OutputStream`). Note that XML is text-based. So exporters are not specifically exporting to a file or a stream, they export to a Writer or an `OutputStream`. The controller later decides what to do with it. - -Add also `LongTask` interface to your class, in order you will be able to use progress and cancel management. Add `utils-longtask` as a dependency to profit from LongTask. - -Your exporter should now look like below: +- `GraphFileExporterBuilder` with `GraphExporter` for graph data. +- `VectorFileExporterBuilder` with `VectorExporter` for preview vector output. +- `ExporterBuilder`/`Exporter` for a non-file-specific target. +- `CharacterExporter` for text; `ByteExporter` for binary. ```java -public class MyExporter implements GraphExporter, CharacterExporter { - - private boolean exportVisible = false; - private Workspace workspace; - private Writer writer; - - @Override - public boolean execute() { - //Do the job - } - - @Override - public void setWorkspace(Workspace workspace) { - this.workspace = workspace; - } - - @Override - public Workspace getWorkspace() { - return workspace; - } - - @Override - public void setWriter(Writer writer) { - this.writer = writer; - } - - @Override - public void setExportVisible(boolean exportVisible) { - this.exportVisible = exportVisible; +@ServiceProvider(service = GraphFileExporterBuilder.class) +public final class PairsExporterBuilder implements GraphFileExporterBuilder { + @Override public String getName() { return "pairs"; } + @Override public FileType[] getFileTypes() { + return new FileType[] { + new FileType(".pairs", "Pairs edge-list files") + }; } - - @Override - public boolean isExportVisible() { - return exportVisible; + @Override public GraphExporter buildExporter() { + return new PairsExporter(); } } ``` -The `GraphExporter` interface has an additional parameter: `exportVisible`. It indicates if either the complete or only the visible graph should be exported. At any time the system keeps the complete graph in memory. When users use filtering, the visible graph is different, as some nodes/edges have been removed. Below is the way to retrieve the good graph with this parameter. +## Implement text export ```java -public boolean execute() { - GraphModel graphModel = workspace.getLookup().lookup(GraphModel.class); - Graph graph = null; - if (exportVisible) { - graph = graphModel.getGraphVisible(); - } else { - graph = graphModel.getGraph(); - } - //Do the job -} -``` +public final class PairsExporter + implements GraphExporter, CharacterExporter, LongTask { + private Workspace workspace; + private Writer writer; + private boolean exportVisible; + private volatile boolean cancelled; + private ProgressTicket progressTicket; -### Finish the builder + @Override public void setWorkspace(Workspace value) { workspace = value; } + @Override public Workspace getWorkspace() { return workspace; } + @Override public void setWriter(Writer value) { writer = value; } + @Override public void setExportVisible(boolean value) { exportVisible = value; } + @Override public boolean isExportVisible() { return exportVisible; } -In the builder, return a new instance of your exporter in the `buildExporter()` method. + @Override + public boolean execute() { + GraphModel model = workspace.getLookup().lookup(GraphModel.class); + Graph graph = exportVisible ? model.getGraphVisible() : model.getGraph(); + Progress.start(progressTicket, graph.getEdgeCount()); + + graph.readLock(); + try { + for (Edge edge : graph.getEdges()) { + if (cancelled) { + return false; + } + writer.write(csv(edge.getSource().getId().toString())); + writer.write(','); + writer.write(csv(edge.getTarget().getId().toString())); + writer.write(System.lineSeparator()); + Progress.progress(progressTicket); + } + writer.flush(); + return true; + } catch (IOException ex) { + throw new UncheckedIOException("Could not export pairs", ex); + } finally { + graph.readUnlock(); + Progress.finish(progressTicket); + } + } -```java -@Override -public GraphExporter buildExporter() { - return new MyExporter(); + private String csv(String value) { + return '"' + value.replace("\"", "\"\"") + '"'; + } + + @Override public boolean cancel() { cancelled = true; return true; } + @Override public void setProgressTicket(ProgressTicket ticket) { + progressTicket = ticket; + } } ``` -## With settings UI +The example quotes every field so commas, quotes, and line breaks in IDs cannot corrupt the format. A real exporter should define its encoding, line endings, direction, multigraph, dynamic data, and null-value policies in a format specification. -You can create an `ExporterUI` class for your exporter. It is not mandatory and the exporter will work normally with default settings. +`exportVisible` is significant: `true` means the filtered/visible view, while `false` means the complete graph. Never ignore it. If the format cannot represent a feature, warn through the appropriate UI or documentation rather than silently changing semantics. -### Create MyExporterUI +## Keep graph locks short -Create a new exporter UI class, for instance *MyExporterUI* that implements `ExporterUI`. +Holding a read lock while writing a slow disk or network stream can block graph changes for a long time. For moderate exports this provides a consistent snapshot. For very large or slow exports, copy the necessary primitive values in bounded batches under a lock and serialize after unlocking. Document whether concurrent edits can appear in the result. -Your UI class is responsible for providing the JPanel associated to your exporter and set settings value to your *MyExporter* instance. The system will ask for a JPanel, show a setting dialog and then call `unsetup()`. If users validate the settings panel by hitting OK, the `unsetup()` method is called with update set as true and ask the UI to write the setting values. -The sample below will help you: +## Add optional settings -```java -public class MyExporterUI implements ExporterUI { - - private JPanel panel; - private MyExporter exporter; - - public void setup(Exporter exporter) { - this.exporter= (MyExporter)exporter; - } - - public JPanel getPanel() { - panel = new JPanel(); - return panel; - } - - public void unsetup(boolean update) { - if(update) { - //The user clicked OK when closing settings - } else { - //Cancel was hit - } - panel = null; - exporter = null; - } - - public String getDisplayName() { - return "Exporter Foo"; - } - - public boolean isUIForExporter(Exporter exporter) { - return exporter instanceof MyExporter; - } -} -``` - -### Register the UI - -Add `@ServiceProvider` annotation to your UI class. Add the following line before *MyexporterUI* class definition, as shown below: +Register an `ExporterUI` service when users need format choices: ```java @ServiceProvider(service = ExporterUI.class) -public class MyExporterUI implements ExporterUI{ -... -``` +public final class PairsExporterUI implements ExporterUI { + private PairsExporter exporter; + private JCheckBox includeHeader; -Note that by doing this your class becomes a singleton. + @Override public void setup(Exporter value) { + exporter = (PairsExporter) value; + } + @Override public JPanel getPanel() { + includeHeader = new JCheckBox("Include header", exporter.isIncludeHeader()); + JPanel panel = new JPanel(); + panel.add(includeHeader); + return panel; + } + @Override public void unsetup(boolean update) { + if (update) { + exporter.setIncludeHeader(includeHeader.isSelected()); + } + exporter = null; + includeHeader = null; + } + @Override public boolean isUIForExporter(Exporter value) { + return value instanceof PairsExporter; + } + @Override public String getDisplayName() { return "Pairs options"; } +} +``` -### Remember settings +Each export creates a new exporter. If the UI should remember the user's last choice, store the preference in the UI/service layer and load it into the new exporter during `setup()`. -How to remember last settings set to the exporter, as each time a new export is made, a new instance of Exporter is created. +## Exporter checklist -It is the `ExporterUI`'s role to remember settings. The only thing to do is load settings at `setup()` and save settings at unsetup. Look at existing classes in the `ExporterPluginUI` module to have an example. +- The correct complete or visible graph is selected. +- Output is escaped according to a documented format, not by ad hoc concatenation. +- Writer/stream ownership is respected; flush it, but let the caller manage its lifecycle unless the SPI contract says otherwise. +- Cancellation returns `false` and leaves no misleading “successful” result. +- Locks and progress are finished in `finally`. +- Tests include Unicode, delimiters inside values, nulls, self-loops, parallel edges, and cancellation. -![image](/docs/Plugins/Export/00_image.png) +Production examples live in Gephi 0.11.2's [ExportPlugin](https://github.com/gephi/gephi/tree/v0.11.2/modules/ExportPlugin) and [PreviewExport](https://github.com/gephi/gephi/tree/v0.11.2/modules/PreviewExport) modules. diff --git a/gephi-desktop/docs/Plugins/Extend_Data_Laboratory.md b/gephi-desktop/docs/Plugins/Extend_Data_Laboratory.md index 4b1b36f..c283c02 100644 --- a/gephi-desktop/docs/Plugins/Extend_Data_Laboratory.md +++ b/gephi-desktop/docs/Plugins/Extend_Data_Laboratory.md @@ -1,375 +1,202 @@ --- id: Extend_Data_Laboratory title: Extend Data Laboratory -sidebar_position: 10 +sidebar_position: 11 --- -## Introduction +Data Laboratory **manipulators** add actions for selected nodes or edges, a cell value, a column, or the table as a whole. Implement one when introducing a new action into the Data Laboratory UI. When a plugin only needs to invoke an operation Data Laboratory already provides, use its public controllers instead. -Data Laboratory section of Gephi provides a good amount of basic and common features to manipulate the graph. But data laboratory can be extended with new features as plugins easily through Data Laboratory API/SPI. All Gephi's default data laboratory features are in **Data Laboratory Plugin**. **Simple and complex examples can be found in this module**. +Add `datalab-api`, `graph-api`, and `org-openide-util-lookup`. Add UI modules only when the implementation imports them. -For this tutorial, the **SPI** packages (`org.gephi.datalab.spi`) are the most interesting, while `org.gephi.datalab.api` only contains several controllers that expose general features to be used by plugins or other parts of gephi. +## Use existing Data Laboratory operations -Data Laboratory SPI may seem big, but all interfaces to implement are mostly the same with small changes necessary to define different types of features. **Once you know how to create some kind of feature, it is simple to create others**. - -**From now on, we will call data laboratory features 'manipulators' since they manipulate graph data/elements.** - -### Where each type of manipulator appears in Gephi desktop application - -![800px-data-lab-overview](/docs/Plugins/Extend_Data_Laboratory/00_800px-data-lab-overview.png) - -![merge-strategies](/docs/Plugins/Extend_Data_Laboratory/01_merge-strategies.png) - -### The Manipulator interface - -The [Manipulator](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/datalab/spi/Manipulator.html) interface is the part that most of the interfaces that you will implement share in common. Also, all kinds of manipulators can provide an optional GUI just by returning an implementation of the [ManipulatorUI](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/datalab/spi/ManipulatorUI.html) interface. - -The interfaces that extend Manipulator to provide specific types of manipulators are: - -- [NodesManipulator](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/datalab/spi/nodes/NodesManipulator.html) - Defines a context menu action to manipulate one or more selected nodes. -- [EdgesManipulator](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/datalab/spi/edges/EdgesManipulator.html) - Defines a context menu action to manipulate one or more selected edges. -- [GeneralActionsManipulator](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/datalab/spi/general/GeneralActionsManipulator.html) - Defines a general action not related to any specific graph element. [PluginGeneralActionsManipulator](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/datalab/spi/general/PluginGeneralActionsManipulator.html) is the same but appears in a plugin button that displays a list of general actions (use this when the tool bar does not have enough space for more general actions). -- [AttributeValueManipulator](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/datalab/spi/values/AttributeValueManipulator.html) - Defines a context menu action that manipulates a single cell data (pair of AttributeRow and AttributeColumn). -- [AttributeColumnsMergeStrategy](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/datalab/spi/columns/merge/AttributeColumnsMergeStrategy.html) - Defines an strategy --- [AttributeRowsMergeStrategy](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/datalab/spi/rows/merge/AttributeRowsMergeStrategy.html) - A very special type of manipulator that defines strategies for merging rows to be used by a nodes/edges manipulator or other parts (does not have a place in Data Laboratory GUI by itself but it is currently used by MergeNodes manipulator). - -Only [AttributeColumnsManipulator](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/datalab/spi/columns/AttributeColumnsManipulator.html) is a bit different from `Manipulator` because its workflow is not the same as all the previous manipulators, and therefore provides its unique interface plus a [AttributeColumnsManipulatorUI](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/datalab/spi/columns/AttributeColumnsManipulatorUI.html). - -## Create a new Manipulator - -Once we understand the interfaces that define our manipulators, we can create some sample features. - -### Setting up our data - -Here is where manipulators that need some **initial data** (like nodes, edges or columns) get it. This method **varies for each type of manipulator** but is always called before any other method. - -Code example for a nodes manipulator: +The controllers expose built-in operations and their capability checks. Ask whether an operation is valid before executing it: ```java -public void setup(Node[] nodes, Node clickedNode) { - this.nodes=nodes;//Keep the needed data for future operations. -} -``` - -In this case, setup receives an array of nodes (selected nodes at right click) and the specific clicked node. - -### Name and description - -Simply define a name an a description (optional) of what our manipulator does. +AttributeColumnsController columns = Lookup.getDefault() + .lookup(AttributeColumnsController.class); -```java -public String getName() { - return NbBundle.getMessage(Group.class, "Group.name");//Localized name for nodes grouping action -} - -public String getDescription() { - return null;//No description +if (columns.canClearColumnData(column)) { + columns.clearColumnData(table, column); } -``` -### Determining if our manipulator should be eligible to execute - -In determinate conditions we would not like our manipulator to be executed, so we declare it with a boolean. **The moment this is called, setup has already been called**. - -```java -public boolean canExecute() { - return nodes.length == 1;//This manipulator will only be executed with a single node selection +if (columns.canChangeColumnData(column)) { + columns.fillColumnWithValue(table, column, "Unknown"); } -``` - -### Provide an UI -Here we can return our GUI if desired. We will talk about ManipulatorUIs later. - -```java -public ManipulatorUI getUI(){ - return new MyUI();//It is not necessary to pass this class to the UI now because ManipulatorUI will be set up later. - //return null if no GUI +if (columns.canDeleteColumn(column)) { + columns.deleteAttributeColumn(table, column); } ``` -### Ordering and classifying manipulators +`GraphElementsController` similarly exposes supported node and edge operations. Prefer these controllers to reimplementing built-in behavior: their capability checks preserve property-column restrictions and other Data Laboratory rules. Resolve the table, column, and controller for the current workspace when the operation begins. Use the manipulator SPIs below only when adding a new user-visible action. -In order to declare the position in a group of manipulators that our manipulator has to appear, we have to provide a number identifying the category(type) and position. +## Choose a manipulator -Order of appearance is determined first by the type number and then by position number. Often consecutive elements are given numbers with considerable separation to be able to insert new categories between them. +| Context | SPI | Registration | +| --- | --- | --- | +| General toolbar action | `GeneralActionsManipulator` | Register the manipulator directly | +| Overflow **More actions** menu | `PluginGeneralActionsManipulator` | Register directly | +| Selected node rows | `NodesManipulator` | Register `NodesManipulatorBuilder` | +| Selected edge rows | `EdgesManipulator` | Register `EdgesManipulatorBuilder` | +| One cell | `AttributeValueManipulator` | Register its builder | +| One column | `AttributeColumnsManipulator` | Register directly | +| Merge columns/rows | Merge-strategy SPI | Register the matching builder | -```java -public int getType() { - return 300;//Will appear in the same category as other manipulators that also return 300 -} - -public int getPosition() { - return 0;//Will appear in its category before manipulators with a position of 1 or more -} -``` - -**These are the only methods that could be called before setup and therefore should not depend on context data**. +Builders matter because Lookup services are singletons while node, edge, cell, and merge manipulators hold per-invocation context. The builder must return a new instance. -### Optional icon +## A general action -Just return an icon for the feature or null. +This quick action is registered directly: ```java -public Icon getIcon() { - return ImageUtilities.loadImageIcon("org/gephi/datalab/plugin/manipulators/resources/group.png", true); -} -``` - -### Executing the final action - -**This is where our feature does the work** once it has been chosen and preferences (if any) have been chosen with or without a GUI. +@ServiceProvider(service = PluginGeneralActionsManipulator.class) +public final class RemoveSelfLoops implements PluginGeneralActionsManipulator { + @Override + public void execute() { + GraphModel model = Lookup.getDefault() + .lookup(GraphController.class) + .getGraphModel(); + if (model == null) { + return; + } -``` -public void execute() { - for(Node node:nodes){ - //Do something with the nodes + Graph graph = model.getGraph(); + graph.writeLock(); + try { + for (Edge edge : graph.getSelfLoops().toArray()) { + graph.removeEdge(edge); + } + } finally { + graph.writeUnlock(); + } } -} -``` - -### Complete code for Group nodes manipulator -```java -public class Group extends BasicNodesManipulator { - private Node[] nodes; - - public void setup(Node[] nodes, Node clickedNode) { - this.nodes=nodes; - } - - public void execute() { - GraphElementsController gec = Lookup.getDefault().lookup(GraphElementsController.class); - gec.groupNodes(nodes); - } - - public String getName() { - return NbBundle.getMessage(Group.class, "Group.name"); - } - - public String getDescription() { - return ""; - } - - public boolean canExecute() { - GraphElementsController gec = Lookup.getDefault().lookup(GraphElementsController.class); - return gec.canGroupNodes(nodes); + @Override public String getName() { return "Remove self-loops"; } + @Override public String getDescription() { + return "Removes every self-loop from the complete graph."; } - - public ManipulatorUI getUI() { - return null; - } - - public int getType() { - return 300; - } - - public int getPosition() { - return 0; - } - - public Icon getIcon() { - return ImageUtilities.loadImageIcon("org/gephi/datalab/plugin/manipulators/resources/group.png", true); + @Override public boolean canExecute() { + GraphModel model = Lookup.getDefault() + .lookup(GraphController.class).getGraphModel(); + return model != null && model.getGraph().getEdgeCount() > 0; } + @Override public ManipulatorUI getUI() { return null; } + @Override public int getType() { return 100; } + @Override public int getPosition() { return 100; } + @Override public Icon getIcon() { return null; } } ``` -## Nodes and edges manipulators +The action states that it changes the complete graph. If this could be slow on the supported graph sizes, ask for confirmation/settings through a UI and run the mutation through the long-task pattern rather than blocking the EDT. -Both of these types of manipulators are shown as context menu actions in a popup menu, and benefit from some special settings defined in the `ContextMenuItemManipulator` interface. Their principal extra feature is the possibility to have subitems, as can be observed in the next image. +## A selected-nodes action -![sub-items](/docs/Plugins/Extend_Data_Laboratory/02_sub-items.png) - -It defines 3 methods: +`setup` is called before context-dependent methods. Store only what the invocation needs: ```java -public ContextMenuItemManipulator[] getSubItems() { - return null;//Return subitems -} -``` - -Note that: - -- Most nodes/edges manipulators won't need to have subitems. -- Subitems can also return more subitems. -- Subitems will be set up with the proper data like their parents. -- If a item/subitem returns subitems, it will never be executed, their subitems will. -- `execute` method of the specific clicked subitem will be called. +public final class SetNodesRed implements NodesManipulator { + private Node[] nodes; -```java -public boolean isAvailable() { - return true;//Indicates if a subitem has to appear in the menu at all (enabled or not) -} - -public Integer getMnemonicKey() { - return null;//Shorcut associated key for this item + @Override + public void setup(Node[] selectedNodes, Node clickedNode) { + nodes = selectedNodes.clone(); + } + @Override public void execute() { + for (Node node : nodes) { + node.setColor(Color.RED); + } + } + @Override public boolean canExecute() { return nodes != null && nodes.length > 0; } + @Override public boolean isAvailable() { return true; } + @Override public ContextMenuItemManipulator[] getSubItems() { return null; } + @Override public Integer getMnemonicKey() { return null; } + @Override public String getName() { return "Set color to red"; } + @Override public String getDescription() { return "Colors the selected nodes red."; } + @Override public ManipulatorUI getUI() { return null; } + @Override public int getType() { return 100; } + @Override public int getPosition() { return 100; } + @Override public Icon getIcon() { return null; } } ``` -Most manipulators won't need this, so a simple class like the following can be extended by all your manipulators to avoid repeating code: +Register a factory, not this stateful class: ```java -public abstract class BasicNodesManipulator implements NodesManipulator{ - - public boolean isAvailable() { - return true; - } - - public ContextMenuItemManipulator[] getSubItems() { - return null; - } - - public Integer getMnemonicKey() { - return null; +@ServiceProvider(service = NodesManipulatorBuilder.class) +public final class SetNodesRedBuilder implements NodesManipulatorBuilder { + @Override public NodesManipulator getNodesManipulator() { + return new SetNodesRed(); } } ``` -*Visualization API has an SPI for adding context menu actions (GraphContextMenuItem) to nodes like DataLaboratory does with NodesManipulator. Note that they share the interface ContextMenuItemManipulator from Data Laboratory API, so they are compatible, being possible to reuse actions on nodes for Overview and Data Laboratory.* - -## AttributeColumnsManipulators +`getSubItems()` can create a submenu. If an item returns children, Gephi executes a child rather than the parent. Subitems receive the same selection context. -What makes different these manipulators is that, instead oh having data set up and then `canExecute` being called, eligibility is tested for every column and is later executed for the selected column (showing first the UI if necessary). +## Column actions -Here is a complete example of a real feature (columns duplication): +`AttributeColumnsManipulator` has a different lifecycle: eligibility and execution both receive the table and column. ```java -public class DuplicateColumn implements AttributeColumnsManipulator { - private String title; - private AttributeType columnType; - - public void execute(Table table, Column column) { - Lookup.getDefault().lookup(AttributeColumnsController.class).duplicateColumn(table, column, title, columnType); - } - - public String getName() { - return NbBundle.getMessage(DuplicateColumn.class, "DuplicateColumn.name"); - } - - public String getDescription() { - return ""; - } - +@ServiceProvider(service = AttributeColumnsManipulator.class) +public final class ClearStringColumn implements AttributeColumnsManipulator { + @Override public boolean canManipulateColumn(Table table, Column column) { - return true; - } - - public AttributeColumnsManipulatorUI getUI(Table table,Column column) { - return new DuplicateColumnUI(); - } - - public int getType() { - return 0; - } - - public int getPosition() { - return 400; + return String.class.equals(column.getTypeClass()) && !column.isProperty(); } - - public Image getIcon() { - return ImageUtilities.loadImage("org/gephi/datalab/plugin/manipulators/resources/table-duplicate-column.png"); - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public Class getColumnType() { - return columnType; - } - - public void setColumnType(Class columnType) { - this.columnType = columnType; + + @Override + public void execute(Table table, Column column) { + Graph graph = table.getGraph(); + if (table.isNodeTable()) { + for (Node node : graph.getNodes()) { + node.setAttribute(column, null); + } + } else { + for (Edge edge : graph.getEdges()) { + edge.setAttribute(column, null); + } + } + } + + @Override public AttributeColumnsManipulatorUI getUI(Table table, Column column) { + return null; } + @Override public String getName() { return "Clear text values"; } + @Override public String getDescription() { return "Sets every value in this text column to null."; } + @Override public int getType() { return 100; } + @Override public int getPosition() { return 100; } + @Override public Image getIcon() { return null; } } ``` -Column type and title are set through `DuplicateColumnUI`. +For destructive operations, provide a confirmation UI and describe the scope. Use `table.isNodeTable()`/`isEdgeTable()` rather than assuming which kind of row was supplied. -## Builders (how to get our plugins to appear in data laboratory perspective) +## Settings UI lifecycle -Like in other parts of Gephi, api/spi implementations are exposed with Netbeans Lookup API. +Most manipulators return a `ManipulatorUI`: -This is done with the `@ServiceProvider` annotation. Since this annotation returns a singleton of the service, in order to help the programmer avoid problems with previously set up data, some of the manipulators require a builder that returns instances of the manipulator as the service. +1. `setup(manipulator, dialogControls)` receives the operation and dialog controls. +2. `getSettingsPanel()` returns the Swing panel. +3. The user confirms or cancels. +4. `unSetup()` transfers validated values and clears references. -But other manipulators are directly exposed as the service with the annotation. +`isModal()` controls whether the dialog blocks other interaction. Use `DialogControls` to disable OK while values are invalid. The action itself must validate again. -For example, to declare a `GeneralActionsManipulator`, it is sufficient with: +## Ordering and availability -```java -@ServiceProvider(service = GeneralActionsManipulator.class) -public class AddEdgeToGraph implements GeneralActionsManipulator { -... -``` +`getType()` groups related actions; `getPosition()` orders them within that group. Leave numerical gaps so future actions can be inserted. `canExecute()` determines whether the current prepared action can run; `isAvailable()` determines whether a context-menu item appears at all. -But for a `NodesManipulator` you need to create a builder and give it the annotation (not to the manipulator): +Do not make `getName()`, ordering, or icons depend on selection context unless the interface lifecycle guarantees `setup()` was called first. -```java -@ServiceProvider(service=NodesManipulatorBuilder.class) -public class SetNodesSizeBuilder implements NodesManipulatorBuilder { - public NodesManipulator getNodesManipulator() { - return new SetNodesSize(); - } -} -``` - -Manipulator type | Needs a builder ------------------| ---------------- -AttributeColumnsManipulator | -GeneralActionsManipulator | -AttributeColumnsMergeStrategy | ✓ -EdgesManipulator | ✓ -NodesManipulator | ✓ -AttributeRowsMergeStrategy | ✓ -AttributeValueManipulator | ✓ - -## Providing a GUI - -When your feature needs a GUI you only need to return a [ManipulatorUI](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/datalab/spi/ManipulatorUI.html) implementation capable of configuring your feature extra data. Let's see what each method should do with an example: - -```java -public class MyManipulatorUI extends JPanel implements ManipulatorUI { - private MyManipulator manipulator; - private DialogControls dialogControls; - - void setup(Manipulator m, DialogControls dialogControls){ - //Receive our manipulator instance: - manipulator = (MyManipulator) m; //We know the type of manipulator we are going to receive so cast is safe - //And an object to control the dialog if necessary - //(for now it only is able to enable/disable the Ok button of the dialog for validation purposes) - this.dialogControls = dialogControls; - } - - void unSetup(){ - //Called when the dialog is closed, canceled or accepted. Pass necessary data to the manipulator: - manipulator.setSomeOption(someValue); - ... - } - - String getDisplayName(){ - //Provide title for the dialog: - return manipulator.getName();//For example, the manipulator name - } - - public JPanel getSettingsPanel(){ - //Provide the JPanel to create the UI dialog - //A good practice is to extend JPanel and just return this object - return this; - } - - /** - * Indicates if the created dialog has to be modal - */ - public boolean isModal(){ - return true; - } -} -``` +## Data Laboratory checklist -## Manipulators Summary +- The action appears in the narrowest correct context. +- Stateful manipulators are created by builders, not registered as singletons. +- Selection arrays are copied if retained and not kept after execution. +- Destructive scope is named and confirmed. +- Graph/table changes use the appropriate locking and threading strategy. +- `canExecute()` handles no project, empty selection, and incompatible columns. +- UI references are released in `unSetup()`. -![manipulators](/docs/Plugins/Extend_Data_Laboratory/03_manipulators.png) \ No newline at end of file +Use Gephi 0.11.2's [DataLaboratoryAPI](https://github.com/gephi/gephi/tree/v0.11.2/modules/DataLaboratoryAPI) for contracts and [DataLaboratoryPlugin](https://github.com/gephi/gephi/tree/v0.11.2/modules/DataLaboratoryPlugin) for current implementations. diff --git a/gephi-desktop/docs/Plugins/Filter.md b/gephi-desktop/docs/Plugins/Filter.md index 2bdff9e..3a21afd 100644 --- a/gephi-desktop/docs/Plugins/Filter.md +++ b/gephi-desktop/docs/Plugins/Filter.md @@ -1,284 +1,164 @@ --- id: Filter title: Filter -sidebar_position: 9 +sidebar_position: 10 --- -Filters are pruning the graph by keeping only nodes and edges that satisfies filters conditions. They are either predicates or functions that reduce the graph, predicates are easy and return only true or false, whereas functions input a graph and output a graph. +Filters derive a graph view without deleting elements from the main graph. The filter pipeline copies a view, applies nested queries, and can make the result visible. Changing a filter property reruns the query. -Create a new plugin module, that we will call *MyFilter*. +Add `filters-api`, `graph-api`, `project-api`, and `org-openide-util-lookup`. -One can find filters examples in the [FiltersPlugin module](https://github.com/gephi/gephi/tree/master/modules/FiltersPlugin/src/main). +## Apply an installed filter -## How filtering works in Gephi - -Gephi has a filter pipeline working with graph copies, where each filter can remove nodes and edges with settings. The Graph API in Gephi has the concept of View, a copy of the complete graph structure identified by an ID and where filters can work on it without disturbing other views. So, when a filter is removing nodes, it is removing them on a copy of the graph structure, the complete graph (named the main view) remains the same. This copy is eventually set as the visible view, the graph shown in the graph window. That is how users visualize a filtered graph. When filtering is disabled, the main view is simply set again as the visible view. - -The filter pipeline is executed on a separate Thread and therefore doesn't block the rest of the application. -Filters are wrapped into queries, which can be chained and combined. Similar as SQL nested queries concept, filter queries can have sub-queries and be represented as a tree, where the last executed query is the root. For example, the query **INDEGREE(3, EDGE_WEIGHT(2))** would be executed like below: - -* A complete copy of the graph structure is created in a new view -* It is passed to the **EDGE_WEIGHT** filter, which removes edges under weight 2 -* The sub-graph is passed to the **INDEGREE** filter, which removes nodes with in-degree less than 3 here -* The graph view is set as *visible view*, the graph windows automatically refreshes - -When a property is changed in the user interface, for instance the weight threshold, the complete process above is just re-executed and the former graph view is destroyed. - -## Create a new Filter - -### Set Dependencies - -Add `filters-api`, `graph-api`, and `org-openide-util-lookup` modules as dependencies for your plugin module. - -### Create FilterBuilder - -* `FilterBuilder` is a factory class for building the Filters. The builder is registered in the `FilterLibrary`. The library is the upper panel where users choose the filters they want to use. Another type of builders, named `CategoryBuilder` can create several builders at once and will be detailed later. -* Create a new builder *MyFilterBuilder* class, which implements [`FilterBuilder`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/filters/spi/FilterBuilder.html). -* Add `@ServiceProvider` annotation to your builder class, in order it is detected by the filter library. -Here is how it should look like: +Use `FilterController` when your plugin needs to run a filter that is already installed. Static builders are services; column-dependent builders are supplied by `CategoryBuilder` for the current workspace: ```java -@ServiceProvider(service = FilterBuilder.class) -public class MyFilterBuilder implements FilterBuilder { - - public Category getCategory() { - return FilterLibrary.NODE; - } - - public String getName() { - return "My Filter"; - } - - public Icon getIcon() { - return null; - } - - public String getDescription() { - return "A filter example"; - } - - public Filter getFilter() { - //TODO - } - - public JPanel getPanel(Filter filter) { - return null; - } - - public void destroy(Filter filter) { +Workspace workspace = projectController.getCurrentWorkspace(); +List builders = new ArrayList<>( + Lookup.getDefault().lookupAll(FilterBuilder.class)); + +for (CategoryBuilder category + : Lookup.getDefault().lookupAll(CategoryBuilder.class)) { + FilterBuilder[] dynamicBuilders = category.getBuilders(workspace); + if (dynamicBuilders != null) { + Collections.addAll(builders, dynamicBuilders); } } -``` -Notice the `getCategory()` method. As the `FilterLibrary` is a tree, this builder needs a parent. That is the category. Use default categories defined in `FilterLibrary` like above or simply return your own: +FilterBuilder builder = chooseBuilder(builders); +Filter filter = builder.getFilter(workspace); +// Set validated values through filter.getProperties() when needed. -```java -public Category getCategory() { - return new Category("Samples Filters"); -} +FilterController filters = Lookup.getDefault().lookup(FilterController.class); +Query query = filters.createQuery(filter); +filters.add(query); +filters.filterVisible(query); ``` -### Create Filter +A query can instead be exported with `exportToNewWorkspace(query)` or `exportToColumn(columnName, query)`. To restore the complete visible view, use `graphModel.setVisibleView(null)`. Discover by stable identity where the API supplies one; display names may be localized. Implement the SPI below only when contributing a genuinely new filter. -Before creating the filter class, you should decide which filter interface to implement: +## Choose the filter contract -* [`NodeFilter`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/filters/spi/NodeFilter.html): Basic filters for nodes, that works as predicates. For a given node the filter's role is to return true if the node is kept or false if it is removed. -* [`EdgeFilter`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/filters/spi/EdgeFilter.html): Basic filters for edges, that works as predicates. For a given edge the filter's role is to return true if the edge is kept or false if it is removed. -* [`ComplexFilter`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/filters/spi/ComplexFilter.html): Filter working with full graphs and returning a subgraph. +- `NodeFilter` is a predicate: return `true` to keep a node. +- `EdgeFilter` is the equivalent predicate for edges. +- `ComplexFilter` receives a graph view and can remove both nodes and edges. +- `CategoryBuilder` creates workspace-dependent families of filters, commonly one per attribute column. -The `ComplexFilter` interface is useful when both nodes and edges have to be filtered. -For instance, in the case of an edge weight filter (a filter that keeps edges only in a particular weight threshold) we only need `EdgeFilter`. But if I want to design a filter that prunes the graph until it's average degree is 5.5 I need to have control on both nodes and edges. The complex filter is a black box, where nodes and edges can be filtered (e.g. removed) with tricky or complex processes. +Prefer a predicate when it expresses the rule. It composes cleanly and lets the pipeline own view mutation. -#### NodeFilter +## Register a workspace-aware builder -Create a new *MyFilter* class, which implements `NodeFilter` interface. The main filter method is `evaluate()`. The `init()` asks if the filter is valid for the given graph. Only valid filters are executed by the system. -The example below removes isolated nodes (e.g. with degree equal to 0). The filter is said valid only if the graph have nodes. +In Gephi 0.11.2, `FilterBuilder.getFilter` receives the workspace: ```java -public class MyFilter implements NodeFilter { - - public boolean init(Graph graph) { - return graph.getNodeCount() > 0; - } - - public boolean evaluate(Graph graph, Node node) { - return graph.getDegree(node) > 0; - } - - public void finish() { +@ServiceProvider(service = FilterBuilder.class) +public final class MinimumDegreeBuilder implements FilterBuilder { + @Override public Category getCategory() { return FilterLibrary.TOPOLOGY; } + @Override public String getName() { return "Minimum degree"; } + @Override public String getDescription() { + return "Keeps nodes whose degree is at least the threshold."; } - - public String getName() { - return "My Filter"; + @Override public Icon getIcon() { return null; } + + @Override + public Filter getFilter(Workspace workspace) { + return new MinimumDegreeFilter(); } - - public FilterProperty[] getProperties() { - return new FilterProperty[0]; //Will be explained later + + @Override public JPanel getPanel(Filter filter) { + return new MinimumDegreePanel((MinimumDegreeFilter) filter); } + @Override public void destroy(Filter filter) { } } ``` -#### ComplexFilter - -The complex filter interface is very simple, the `filter()` method directly gives the [`Graph`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/graph/api/Graph.html). - -``public Graph filter(Graph graph);`` +Use the workspace argument when construction depends on its tables or model. Do not cache the resulting filter in the singleton builder. -Here is a example of a complex filter. It removes the 50% edges with the lowest weight. +## Implement a node predicate ```java -public Graph filter(Graph graph) { - int edgeCount = graph.getEdgeCount(); - edgeCount /= 2; - - Edge[] edges = graph.getEdges().toArray(); - Arrays.sort(edges, new Comparator() { - public int compare(Edge o1, Edge o2) { - return Float.compare(o1.getWeight(), o2.getWeight()); - } - }); - - for (int i = 0; i < edgeCount; i++) { - Edge edge = edges[i]; - graph.removeEdge(edge); - } - return graph; -} -``` +public final class MinimumDegreeFilter implements NodeFilter { + private Integer minimum = 1; -It uses the `removeEdge()` method from the `Graph` API and returns the graph it received. - -**Can a filter also add elements?** -Yes, as far as they are part of the main view as well. -As an example, here is how the NOT operator works: - -```java -Graph mainGraph = graph.getView().getGraphModel().getGraph(); -for (Node n : mainGraph.getNodes().toArray()) { - if (!graph.contains(n)) { - //The node n is not in graph - graph.addNode(n); - } else { - //The node n is in graph - graph.removeNode(n); + @Override public String getName() { return "Minimum degree"; } + @Override public boolean init(Graph graph) { return graph.getNodeCount() > 0; } + @Override public boolean evaluate(Graph graph, Node node) { + return graph.getDegree(node) >= minimum; } -} -``` - -### Finish the builder + @Override public void finish() { } -In the builder, return a new instance of your filter in the `getFilter()` method. + @Override + public FilterProperty[] getProperties() { + try { + return new FilterProperty[] { + FilterProperty.createProperty( + this, Integer.class, "minimum", "getMinimum", "setMinimum") + }; + } catch (NoSuchMethodException ex) { + throw new IllegalStateException(ex); + } + } -```java -@Override -public Filter getFilter() { - return new MyFilter(); + public Integer getMinimum() { return minimum; } + public void setMinimum(Integer value) { minimum = Math.max(0, value); } } ``` -### Properties - -It's very likely your filter will need to define properties, for instance a threshold or a pattern. In the case your filter has a user interface, the Filters API implementation has a system to automatically refresh the filter when a property value is changed. But for that it needs to be aware of these properties. That's why `Filter` interface has a `getProperties()` method. - -If your filter doesn't have any user interface, it is therefore not necessary to return properties. Otherwise, it is mandatory. -Below is how to define a simple `useRegex` boolean property: - -``FilterProperty p = FilterProperty.createProperty(this, Boolean.class, "useRegex");`` - -As it uses introspection to get and set the value, your class simply needs proper getters and setters. As a result that is how the method id filled: - -```java -public FilterProperty[] getProperties() { - return new FilterProperty[]{ - FilterProperty.createProperty(this, Boolean.class, "useRegex") - }; - } - - public boolean isUseRegex() { - return useRegex; - } - - public void setUseRegex(boolean useRegex) { - this.useRegex = useRegex; - } -``` - -When the `useRegex` value will be changed (through a checkbox for example), the system will be notified and re-execute the filter. +`init()` is called for the graph being filtered, `evaluate()` for each element, and `finish()` after evaluation. Keep `evaluate()` fast and free of UI work. The property uses JavaBean methods so the filter engine can observe updates. -### Provide a UI +## Update properties through the panel -The `getPanel()` method in *MyFilterBuilder* can return a user interface for the filter. -One can find existing filters user interface code in the `FiltersPluginUI` module. -The panel is needed to configure a `Filter` instance, already created. That's why the `getPanel()` method gives the `Filter`. Cast this filter to *MyFilter*, it cannot be anything else and configure settings. -When settings are modified, use properties to set the new value. Below is an example of a simple panel, with a checkbox to set the `useRegex` property which we defined earlier. +The settings panel must set the `FilterProperty`, not call the filter setter directly; that is how the engine learns that it must refresh: ```java -public class MyFilterPanel extends javax.swing.JPanel implements ItemListener { - - private MyFilter filter; - private javax.swing.JCheckBox regexCheckbox; - - public MyFilterPanel(MyFilter filter) { - this.filter = filter; - - regexCheckbox = new javax.swing.JCheckBox("Use Regex"); - add(regexCheckbox); - - regexCheckbox.addItemListener(this); - } - - public void itemStateChanged(ItemEvent e) { - FilterProperty useRegex = filter.getProperties()[0]; - useRegex.setValue(regexCheckbox.isSelected()); +public final class MinimumDegreePanel extends JPanel { + public MinimumDegreePanel(MinimumDegreeFilter filter) { + JSpinner spinner = new JSpinner(new SpinnerNumberModel( + filter.getMinimum(), 0, Integer.MAX_VALUE, 1)); + spinner.addChangeListener(event -> { + try { + filter.getProperties()[0].setValue(spinner.getValue()); + } catch (Exception ex) { + throw new IllegalStateException("Cannot update filter", ex); + } + }); + add(new JLabel("Minimum:")); + add(spinner); } } ``` -It is important to set the value to the `FilterProperty`, not to *MyFilter* directly. +In production, retain the property once instead of recreating the array on every event, localize labels, and validate values. -## Appendix +## Complex filters -### Create a CategoryBuilder - -Category builders are typically designed to build a filter that work on Attributes. For example, the `AttributeRangeFilter` works with all attribute columns. That means a `FilterBuilder` has to be created for each column, under a category (e.g. a folder). -The following example shows how several `AttributeRangeFilterBuilder` (which implements `FilterBuilder`) are created, one per attribute column. +A `ComplexFilter` may remove elements from the supplied **working view** and return it: ```java -@ServiceProvider(service = CategoryBuilder.class) -public class AttributeRangeBuilder implements CategoryBuilder { - - public Category getCategory() { - return new Category("Range", null, FilterLibrary.ATTRIBUTES); //The 'Range' folder will be in the 'Attributes' folder - } - - public FilterBuilder[] getBuilders(Workspace workspace) { - List builders = new ArrayList<>(); - GraphModel am = Lookup.getDefault().lookup(GraphController.class).getGraphModel(workspace); - List columns = new ArrayList<>(); - columns.addAll(am.getNodeTable().toList()); - columns.addAll(am.getEdgeTable().toList()); - for (Column c : columns) { - if (!c.isProperty() && !c.isArray()) { - if (AttributeUtils.isNumberType(c.getTypeClass())) { - AttributeRangeFilterBuilder b = new AttributeRangeFilterBuilder(c); - builders.add(b); - } - } +@Override +public Graph filter(Graph graph) { + for (Edge edge : graph.getEdges().toArray()) { + if (edge.getWeight() < minimumWeight) { + graph.removeEdge(edge); } - return builders.toArray(new FilterBuilder[0]); } + return graph; } ``` -### Provide an icon +Never modify `graphModel.getGraph()` from a complex filter. Work only on the graph passed to `filter()`. Snapshot an iterable before removing its elements. Be explicit about whether removing a node also removes its incident edges and how directed or parallel edges affect the rule. + +## Attribute-dependent filter families -Simply return an icon in the `FilterBuilder` implementation. +A `CategoryBuilder` receives a workspace and can inspect `GraphModel.getNodeTable()` and `getEdgeTable()` to return one `FilterBuilder` per compatible column. Exclude property columns and unsupported array/dynamic types unless the filter handles them. Rebuild for each workspace; a `Column` from one model is not context-free. -### Custom Properties +## Filter checklist -Properties natively supports all Java types, as well as Range and `Column`. For other types you may have to register a property editor to guarantee serialization to work. -Serialization is used to write and read properties values, when a Gephi project is saved. -Implement a property editor class and register it: +- `true` consistently means “keep,” not “remove.” +- The builder implements the 0.11.2 workspace-aware signature. +- Properties have matching getter/setter types and the UI changes them through `FilterProperty`. +- Predicate evaluation is quick, deterministic, and side-effect free. +- Complex filters mutate only the supplied working view. +- Empty graphs, missing values, direction, self-loops, and multigraphs are tested. -* Create a new class that extends `java.beans.PropertyEditorSupport` and fill `getAstext()` and `setAsText()` methods. -* Register this editor by doing `java.beans.PropertyEditorManager.registerEditor()` method. \ No newline at end of file +See Gephi 0.11.2's [FiltersPlugin](https://github.com/gephi/gephi/tree/v0.11.2/modules/FiltersPlugin) and [FiltersPluginUI](https://github.com/gephi/gephi/tree/v0.11.2/modules/FiltersPluginUI) for built-in implementations. diff --git a/gephi-desktop/docs/Plugins/Generator.md b/gephi-desktop/docs/Plugins/Generator.md index 99d984a..e5b9705 100644 --- a/gephi-desktop/docs/Plugins/Generator.md +++ b/gephi-desktop/docs/Plugins/Generator.md @@ -1,102 +1,117 @@ --- id: Generator title: Generator -sidebar_position: 8 +sidebar_position: 9 --- -Create a new plugin module, that we will call *MyGenerator*. +A generator creates a synthetic graph from parameters. Like an importer, it writes drafts into a `ContainerLoader`; Gephi validates and processes the container into a workspace. This is preferable to modifying the current `Graph` directly because it uses the normal import pipeline. -## Create a new Generator +Add `io-generator-api`, `io-importer-api`, `utils-longtask`, and `org-openide-util-lookup`. -### Set Dependencies +## Implement and register the generator -Add `org-openide-util-lookup`, `graph-api` and `utils-longtask` modules as dependencies for your plugin module *MyGenerator*. - -### Create Generator - -* Create a new class that implements [Generator](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/io/generator/spi/Generator.html). This is the place the code belongs. -* Fill getName() method by returning a display name like "My Generator". Fill `getUI()` method by returning `null`. Leaves other methods untouched for the moment. -* Add `@ServiceProvider` annotation to your builder class. Add the following line before *MyGenerator* class definition, as shown below: +Unlike several other extension points, `Generator` is registered directly: ```java @ServiceProvider(service = Generator.class) -public class MyGenerator implements Generator { -... -``` - -### Generate the graph +public final class PathGenerator implements Generator { + private int nodeCount = 100; + private volatile boolean cancelled; + private ProgressTicket progressTicket; + private final GeneratorUI ui = new PathGeneratorUI(); -Fill `generate()` method by creating `NodeDraft` and `EdgeDraft` elements to add to the `ContainerLoader`. + @Override public String getName() { return "Path graph"; } + @Override public GeneratorUI getUI() { return ui; } -### Set LongTask - -To let your graph creation task be canceled and its progress watched, implement `LongTask` interface on *MyGenerator*. -Add two fields: - -```java -private boolean cancel = false; -private ProgressTicket progressTicket; -``` + @Override + public void generate(ContainerLoader container) { + Progress.start(progressTicket, nodeCount); + NodeDraft previous = null; + try { + for (int i = 0; i < nodeCount && !cancelled; i++) { + NodeDraft node = container.factory().newNodeDraft("n" + i); + node.setLabel("Node " + i); + container.addNode(node); + + if (previous != null) { + EdgeDraft edge = container.factory().newEdgeDraft("e" + i); + edge.setSource(previous); + edge.setTarget(node); + container.addEdge(edge); + } + previous = node; + Progress.progress(progressTicket); + } + } finally { + Progress.finish(progressTicket); + } + } -and implement new methods: + @Override public boolean cancel() { cancelled = true; return true; } + @Override public void setProgressTicket(ProgressTicket ticket) { + progressTicket = ticket; + } -```java -public boolean cancel() { - cancel = true; - return true; -} - -public void setProgressTicket(ProgressTicket progressTicket) { - this.progressTicket = progressTicket; + public int getNodeCount() { return nodeCount; } + public void setNodeCount(int value) { + if (value < 1 || value > 1_000_000) { + throw new IllegalArgumentException("Node count is out of range"); + } + nodeCount = value; + } } ``` -Use the `cancel` field to terminate your algorithm execution properly and return from `generate()`. +Gephi's generator controller already runs generation through long-task infrastructure. Check cancellation inside the loop and finish progress in `finally`. Give generated elements stable, unique IDs when reproducibility or later lookup matters. + +## Configure parameters -### Sample +`GeneratorUI` is owned by the generator and copies values between the algorithm and a Swing panel: ```java -@ServiceProvider(service = Generator.class) -public class HelloWorld implements Generator { - - protected ProgressTicket progress; - protected boolean cancel = false; - - public void generate(ContainerLoader container) { - // create nodes - NodeDraft n1 = container.factory().newNodeDraft(); - NodeDraft n2 = container.factory().newNodeDraft(); - - // set node labels - n1.setLabel("Hello"); - n2.setLabel("World"); - - // create edge - EdgeDraft e = container.factory().newEdgeDraft(); - e.setSource(n1); - e.setTarget(n2); - - // fill in the graph - container.addNode(n1); - container.addNode(n2); - container.addEdge(e); - } - - public String getName() { - return "Hello World"; - } - - public GeneratorUI getUI() { - return null; +public final class PathGeneratorUI implements GeneratorUI { + private PathGenerator generator; + private JSpinner count; + + @Override + public void setup(Generator value) { + generator = (PathGenerator) value; } - - public boolean cancel() { - cancel = true; - return true; + + @Override + public JPanel getPanel() { + count = new JSpinner(new SpinnerNumberModel( + generator.getNodeCount(), 1, 1_000_000, 1)); + JPanel panel = new JPanel(); + panel.add(new JLabel("Nodes:")); + panel.add(count); + return panel; } - - public void setProgressTicket(ProgressTicket progressTicket) { - this.progress = progressTicket; + + @Override + public void unsetup() { + generator.setNodeCount((Integer) count.getValue()); + generator = null; + count = null; } } -``` \ No newline at end of file +``` + +Use a validation panel when parameters depend on one another. Validate again in setters because a generator can be called without the desktop UI. + +## Random and streaming generation + +For random graphs, offer an optional seed and use one `java.util.Random`/`SplittableRandom` instance. Put the chosen seed in graph metadata or the report so a result can be reproduced. + +A “streaming generator” from older examples may start its own indefinite thread. Treat that as a different product design in 0.11.2: define ownership, cancellation, rate limiting, workspace changes, and shutdown before starting any thread. Prefer bounded generation. If ongoing external data is the real requirement, consider an importer or dedicated controller with a visible connection lifecycle. + +## Generator checklist + +- Parameters have sensible bounds and deterministic defaults. +- Estimated node/edge counts are checked before allocating large structures. +- Cancellation leaves a valid container and stops promptly. +- Direction, weights, attributes, self-loops, and parallel-edge rules are explicit. +- A random seed can reproduce stochastic output. +- Tiny known cases and parameter boundaries have unit tests. + +See Gephi 0.11.2's [GeneratorPlugin](https://github.com/gephi/gephi/tree/v0.11.2/modules/GeneratorPlugin) for production generators. diff --git a/gephi-desktop/docs/Plugins/Getting_Started.md b/gephi-desktop/docs/Plugins/Getting_Started.md new file mode 100644 index 0000000..2c8e714 --- /dev/null +++ b/gephi-desktop/docs/Plugins/Getting_Started.md @@ -0,0 +1,175 @@ +--- +id: Getting_Started +title: Getting started +sidebar_position: 2 +--- + +This page takes you from an empty machine to a plugin running inside **Gephi 0.11.2**. The standard development environment is the [`gephi-plugins`](https://github.com/gephi/gephi-plugins/tree/master) repository: it provides the parent Maven configuration, downloads a matching Gephi distribution, builds NBM packages, and validates plugin metadata. + +## Prerequisites + +Install: + +- JDK 17 or later; verify with `java -version` and `javac -version`. +- Maven; verify with `mvn -version`. Maven must report the JDK you intend to use. +- Git. +- NetBeans or IntelliJ IDEA if you want an IDE. The command-line workflow remains the reference. + +Use a full JDK, not only a Java runtime. If several JDKs are installed, make sure Maven is not silently using an older one. + +## Create the development workspace + +Fork `gephi/gephi-plugins` on GitHub, then clone your fork: + +```bash +git clone https://github.com/YOUR-NAME/gephi-plugins.git +cd gephi-plugins +``` + +The repository's `master` branch is the development template. Treat the version used to **compile** the module separately from the version of Gephi used to **run and test** it. + +The root POM's `gephi.version` selects the Gephi distribution downloaded by the development build and launched by the `run` goal. It is also the distribution against which the repository's plugin tooling performs its compatibility work. To exercise the plugin in Gephi 0.11.2, use: + +```xml + + 0.11.2 + +``` + +Each generated module also has an explicit parent, for example: + +```xml + + org.gephi + gephi-plugin-parent + 0.11.1 + +``` + +That parent supplies the Gephi API dependency versions used at compilation. Changing only the root `gephi.version` does **not** change this module parent. Such a setup can deliberately compile against the 0.11.1 API baseline while launching and testing the result on Gephi 0.11.2. This demonstrates runtime compatibility with 0.11.2; it does not mean that the module was compiled against 0.11.2. + +Update the module parent only when a matching parent is published and you intentionally want that newer compilation baseline. Keep every module on the same parent version. Do not add versions to individual Gephi dependencies: the module parent manages them as a coherent set. + +Record these three facts independently in release notes and test records: + +| Question | Controlled or established by | +| --- | --- | +| Which Gephi API baseline compiled the module? | The module's `gephi-plugin-parent` version | +| Which Gephi distribution does the development tooling validate and launch? | The root `gephi.version` property | +| Which Gephi releases are known to work? | Installation and functional tests on those releases | + +Generate a module from the repository root: + +```bash +mvn org.gephi:gephi-maven-plugin:generate +``` + +Use a Java-style organization identifier such as `org.example`, a lowercase artifact such as `sample-plugin`, and a readable branding name such as `Sample Plugin`. The generator adds the module to the reactor and creates its metadata. + +## Understand the generated files + +The useful module structure is: + +```text +modules/SamplePlugin/ +├── pom.xml +└── src/main/ + ├── java/ Java sources + ├── resources/ Bundle.properties, icons, other resources + └── nbm/manifest.mf NetBeans module metadata +``` + +The root `pom.xml` lists all modules. The module POM contains plugin identity, license, author information, public packages, and dependencies. `manifest.mf` contains the display category and descriptions, or points to a localizing bundle. + +Use `Bundle.properties` for user-facing strings when practical. It avoids manifest line-length restrictions and makes later translation possible. Keep source code, identifiers, comments, metadata, and the default bundle in English. + +## Add dependencies deliberately + +Declare only the APIs your code imports. A module that reads the graph and registers a service might need: + +```xml + + + org.gephi + graph-api + + + org.gephi + project-api + + + org.netbeans.api + org-openide-util-lookup + + +``` + +The artifact names used by each tutorial are listed on that page. If Java reports that a package is not visible, add the module that owns that public package; do not solve it by copying Gephi JARs into the plugin. + +## Register an extension + +Gephi discovers implementations through Lookup. Most SPI implementations use `@ServiceProvider`: + +```java +import org.openide.util.lookup.ServiceProvider; + +@ServiceProvider(service = SomeGephiSPI.class) +public final class MyExtension implements SomeGephiSPI { + // Implement every method required by Gephi 0.11.2. +} +``` + +The value of `service` must be the exact SPI Gephi looks up, not merely an interface your class happens to implement. Builders should return a fresh stateful implementation when the SPI uses the builder pattern. + +## Build and run + +From the repository root: + +```bash +mvn clean package +mvn org.gephi:gephi-maven-plugin:run +``` + +`package` compiles tests, produces JAR and NBM artifacts, and runs plugin metadata checks. Run the Maven goal from the **root project**, not only from the module, so the temporary Gephi installation receives the rebuilt plugin. + +In the launched application, open **Tools > Plugins > Installed** and confirm that the module appears. Then exercise its actual entry point—Layout, Statistics, File, Data Laboratory, Preview, toolbar, or Window. + +### IDE workflow + +NetBeans recognizes the root as a Maven project and can run or debug it. In IntelliJ IDEA, import the root POM and create a Maven run configuration for `org.gephi:gephi-maven-plugin:run`. + +For remote debugging, use the run parameters documented in the development repository. Always rebuild the root reactor after a code change if the launched Gephi still seems to contain old classes. + +## A disciplined development loop + +1. Put calculations and parsing in plain Java classes with no Swing dependencies. +2. Unit-test those classes with small, deterministic fixtures. +3. Add the Gephi SPI adapter and test registration by launching Gephi. +4. Test with an empty project, a project with multiple workspaces, a filtered graph, and a graph large enough to expose UI freezes. +5. Test cancellation and error reporting, not only the successful path. +6. Run `mvn clean package` before every release. + +## Common first-run problems + +| Symptom | Likely cause | Fix | +| --- | --- | --- | +| The extension does not appear | Missing/wrong `@ServiceProvider`, wrong SPI, or stale build | Check the annotation and run the root `package` goal again | +| `UnsupportedClassVersionError` | Maven and Gephi use incompatible JDKs | Compare `mvn -version` with `java -version` | +| Package is not visible | Missing module dependency | Add the API module owning that package to the module POM | +| Changes are ignored | Only the child module was run | Rebuild and run from the repository root | +| The UI freezes | Work is executing on Swing's event dispatch thread | Follow [Graph access and long tasks](./Graph_API_and_Threads.md) | +| A plugin works in one workspace only | A cached `GraphModel` survived a workspace switch | Resolve context per operation or listen for workspace changes | + +## Prepare a release + +Before submitting or publishing: + +- Increment the module version. +- Record the compilation baseline, set the development distribution to Gephi 0.11.2, and test the NBM against a clean 0.11.2 installation. +- Complete the English display name, short and long descriptions, category, author, license, and homepage. +- Include license notices for bundled third-party libraries and check that their licenses permit redistribution. +- Avoid implementation-module dependencies unless unavoidable. +- Run all tests and `mvn clean package`. +- Install the generated NBM manually once and verify enable, disable, uninstall, and restart behavior. + +For the official marketplace workflow, push the plugin to your fork and open a pull request against the `master-forge` branch of [`gephi/gephi-plugins`](https://github.com/gephi/gephi-plugins). Enable maintainer edits when possible. Updates use the same process and must carry a higher module version. diff --git a/gephi-desktop/docs/Plugins/Graph_API_and_Threads.md b/gephi-desktop/docs/Plugins/Graph_API_and_Threads.md new file mode 100644 index 0000000..8d633eb --- /dev/null +++ b/gephi-desktop/docs/Plugins/Graph_API_and_Threads.md @@ -0,0 +1,232 @@ +--- +id: Graph_API_and_Threads +title: Graph access and long tasks +sidebar_position: 3 +--- + +Most substantial plugins eventually need the same foundations: find the active workspace, select the correct graph view, read or change attributes safely, react to changes, and keep work off Swing's event dispatch thread (EDT). Get these rules right before specializing in an importer, statistic, or custom panel. + +Add `graph-api`, `project-api`, `utils-longtask`, and `org-openide-util-lookup` as needed. + +## Resolve the current context + +Controllers are global services obtained from NetBeans Lookup. Models are workspace-specific. + +```java +ProjectController projects = Lookup.getDefault().lookup(ProjectController.class); +Workspace workspace = projects.getCurrentWorkspace(); +if (workspace == null) { + // No project/workspace is open: disable the action or show a clear message. + return; +} + +GraphController graphs = Lookup.getDefault().lookup(GraphController.class); +GraphModel graphModel = graphs.getGraphModel(workspace); +``` + +Resolve this context when an operation starts. A panel that remains open across project changes must not assume that the `GraphModel` captured by its constructor is still current. + +## Complete graph or visible graph? + +Gephi preserves the complete graph while filters expose a separate visible view. + +- `graphModel.getGraph()` returns the complete/main graph. +- `graphModel.getGraphVisible()` returns the currently visible view. +- Directed and undirected variants are available when an algorithm requires those semantics. + +Use the visible graph for an operation the user reasonably expects to follow the current filter, such as a layout or a “selected view” analysis. Use the complete graph for data maintenance or an operation explicitly described as applying to all data. State this choice in the UI and in exported metadata. + +## Read nodes, edges, and attributes + +Tables define columns; nodes and edges carry row values. + +```java +Graph graph = graphModel.getGraphVisible(); +Table nodeTable = graphModel.getNodeTable(); +Column score = nodeTable.getColumn("score"); + +if (score != null && Number.class.isAssignableFrom(score.getTypeClass())) { + graph.readLock(); + try { + for (Node node : graph.getNodes()) { + Number value = (Number) node.getAttribute(score); + // Copy only the values needed for later computation. + } + } finally { + graph.readUnlock(); + } +} +``` + +Check that a column exists and has the expected type. Attribute values may be `null`. Use column IDs for stable programmatic identity and titles for user-facing labels. + +### Snapshot, calculate, write back + +For expensive analysis, keep lock duration short: + +1. Under a read lock, copy the required IDs, adjacency, or values into plain Java structures. +2. Release the lock and perform the expensive calculation. +3. Reacquire the appropriate context, verify it is still valid, and write the results in a short phase. + +This avoids blocking other graph consumers for the full calculation. It also gives cancellation checks natural places to run. + +## Modify the graph + +Create elements with the model's factory and add them to the intended graph: + +```java +Graph graph = graphModel.getGraph(); +GraphFactory factory = graphModel.factory(); + +Node source = factory.newNode("source-id"); +source.setLabel("Source"); +Node target = factory.newNode("target-id"); +target.setLabel("Target"); +Edge edge = factory.newEdge(source, target); + +graph.writeLock(); +try { + graph.addNode(source); + graph.addNode(target); + graph.addEdge(edge); +} finally { + graph.writeUnlock(); +} +``` + +Never leave a lock open on an exception, cancellation, or early return. Always unlock in `finally`. Do not acquire a write lock while holding a read lock: lock upgrading can deadlock. + +To add a computed attribute: + +```java +Table table = graphModel.getNodeTable(); +Column column = table.getColumn("my_score"); +if (column == null) { + column = table.addColumn("my_score", "My score", Double.class, null); +} + +for (Node node : graph.getNodes()) { + node.setAttribute(column, scoresById.get(node.getId())); +} +``` + +Choose an ID unlikely to collide with another plugin. If an existing column has the wrong type, do not overwrite it silently; report the conflict or use a namespaced ID. + +## React to workspace changes + +A long-lived window can register a `WorkspaceListener` service: + +```java +@ServiceProvider(service = WorkspaceListener.class) +public final class WorkspaceTracker implements WorkspaceListener { + @Override public void initialize(Workspace workspace) { } + + @Override + public void select(Workspace workspace) { + SwingUtilities.invokeLater(() -> refreshFor(workspace)); + } + + @Override public void unselect(Workspace workspace) { } + @Override public void close(Workspace workspace) { } + @Override public void disable() { + SwingUtilities.invokeLater(this::clearUI); + } +} +``` + +Callbacks are application events, not permission to perform expensive work. Schedule Swing changes on the EDT and delegate analysis to a background task. + +## Observe graph changes + +Gephi 0.11.2's Graph API offers polling observers: + +```java +Graph graph = graphModel.getGraph(); +GraphObserver observer = graphModel.createGraphObserver(graph, true); + +if (observer.hasGraphChanged()) { + GraphDiff diff = observer.getDiff(); + Node[] added = diff.getAddedNodes().toArray(); + Edge[] removed = diff.getRemovedEdges().toArray(); + // Coalesce changes and refresh the UI on the EDT. +} + +observer.destroy(); +``` + +The second argument controls whether the observer collects a `GraphDiff`; pass `false` when a simple changed/not-changed flag is enough. Call `getDiff()` only after `hasGraphChanged()` returned true on an observer created with differences enabled. That status call resets the observer. A `TableObserver` similarly detects node/edge table schema changes and can optionally collect a `TableDiff`. + +Observers are not thread-safe. Poll one observer from one worker at a moderate rate, coalesce repeated updates, and destroy it when its window closes or workspace changes. + +For visualization selection and mouse interaction, use a documented Visualization or Tools API listener rather than repeatedly scanning selection state. See [Desktop UI and events](./Desktop_UI_and_Events.md). + +## Keep Swing responsive + +Swing components must be created and mutated on the EDT. Parsing, graph-wide iteration, network I/O, text analysis, and waiting must not run there. + +For a custom operation, implement `Runnable` and `LongTask`, then use `LongTaskExecutor`: + +```java +public final class ScoreTask implements Runnable, LongTask { + private volatile boolean cancelled; + private ProgressTicket progressTicket; + + @Override + public void run() { + Progress.start(progressTicket, totalWork()); + try { + for (WorkItem item : workItems()) { + if (cancelled || Thread.currentThread().isInterrupted()) { + return; + } + process(item); + Progress.progress(progressTicket); + } + } finally { + Progress.finish(progressTicket); + } + } + + @Override + public boolean cancel() { + cancelled = true; + return true; + } + + @Override + public void setProgressTicket(ProgressTicket ticket) { + this.progressTicket = ticket; + } +} +``` + +Launch it from a panel or action: + +```java +LongTaskExecutor executor = new LongTaskExecutor(true, "My plugin"); +ScoreTask task = new ScoreTask(); + +executor.setLongTaskListener(finishedTask -> + SwingUtilities.invokeLater(() -> { + runButton.setEnabled(true); + refreshResults(); + }) +); +executor.execute(task, task, "Calculating scores", throwable -> + SwingUtilities.invokeLater(() -> showError(throwable)) +); +``` + +Keep one executor per independent task stream; an executor runs only one task at a time. Disable duplicate Run actions while it is active. Wire Cancel to `executor.cancel()` and make `cancel()` fast and cooperative. + +`SwingWorker` is also suitable when a task is owned entirely by one Swing view. Publish immutable intermediate results and apply them in `process()` or `done()`; do not set labels or models from `doInBackground()`. + +## Threading checklist + +- No expensive work in action listeners, constructors, `componentOpened()`, or graph callbacks. +- No Swing mutation from worker threads. +- Cancellation state is `volatile` or otherwise thread-safe and checked inside loops. +- Every lock is released in `finally`; cancellation never bypasses cleanup. +- Background results are tied to the workspace and parameters that produced them. +- Listeners, observers, timers, and executors are stopped when no longer needed. +- Exceptions reach the user through a concise message and reach logs with enough context for diagnosis. diff --git a/gephi-desktop/docs/Plugins/Import.md b/gephi-desktop/docs/Plugins/Import.md index a5c988b..c520ddc 100644 --- a/gephi-desktop/docs/Plugins/Import.md +++ b/gephi-desktop/docs/Plugins/Import.md @@ -1,198 +1,199 @@ --- id: Import title: Import -sidebar_position: 5 +sidebar_position: 7 --- -Importers push data from files, databases or external datasources to a `Container`. The role of the container is to host all data collected by importers (i.e. nodes, edges and attributes). This documentation is focused on file importers, but the procedure is similar for databases. +An importer parses an external source into a neutral `ContainerLoader`. Gephi validates that container, shows an import report, and only then processes it into a workspace. Importers should not write directly to the current `GraphModel`. -Create a new plugin module, that we will call *MyImporter*. +This tutorial builds a text importer for `.pairs` files containing one `source,target` edge per line. Add `io-importer-api`, `utils-longtask`, `org-openide-filesystems`, and `org-openide-util-lookup`. -One can find file importer examples in the Gephi [source code](https://github.com/gephi/gephi/tree/master/modules/ImportPlugin/src/main). +## Import through an installed importer -## Create a new Importer - -### Set Dependencies - -Add `io-importer-api`, `utils-longtask`, `org-openide-filesystems` and `org-openide-util-lookup` modules as dependencies for your plugin module *MyImport*. - -### Create FileImporterBuilder - -* `ImporterBuilder` is a factory class for building the important instance, all Importers should have their own builder. -* Create a new builder class, for instance *MyImporterBuilder* that implements `FileImporterBuilder`. -* Fill the `getFileTypes()` and `getName()` methods like below, for a single file format supported named *foo*. +Use `ImportController` when your plugin needs to import a format Gephi already supports: ```java -public String getName() { - return "foo"; +ImportController imports = Lookup.getDefault().lookup(ImportController.class); +Container container = imports.importFile(file); +if (container == null) { + throw new IllegalArgumentException("No installed importer accepted the file"); } - -public FileType[] getFileTypes() { - return new FileType[]{new FileType(".foo", "Foo files")}; -} -``` -Add `@ServiceProvider` annotation to your builder class. Add the following line before *MyImporterBuilder* class definition, as shown below: +Processor processor = chooseProcessor( + Lookup.getDefault().lookupAll(Processor.class)); -```java -@ServiceProvider(service = FileImporterBuilder.class) -public class MyImporterBuilder implements FileImporterBuilder { -... +Workspace result = imports.process(container, processor, workspace); ``` -### Create FileImporter - -The importer is where the job will be done. Create a new importer class, for instance *MyImporter* that implements `FileImporter`. +Ensure that the intended project and workspace exist before processing. Importing creates a neutral container; processing is the distinct step that commits it to a workspace. Select the processor through an explicit policy—ordinary imports normally use Gephi's installed standard processor—rather than relying on collection order or a localized display name. Run file parsing and processing off the EDT, connect cancellation when the selected implementation supports `LongTask`, and present the import report when warnings matter. Implement an importer below only for a format that installed importers do not handle. -Notice the `setReader()` method. That means you will work with a reader instead directly with files. The system is responsible for setting the reader before the importer you're developing will be executed. - -Add also `LongTask` interface to your class, in order you will be able to use progress and cancel management. -Your *MyImporter* would look like this: +## Register the file format ```java -public class MyImporter implements FileImporter, LongTask { - - private Reader reader; - private ContainerLoader container; - private Report report; - private ProgressTicket progressTicket; - private boolean cancel = false; - - public void setReader(Reader reader) { - this.reader = reader; - } - - public boolean execute(ContainerLoader loader) { - this.container = loader; - this.report = new Report(); - //Import - return !cancel; - } - - public ContainerLoader getContainer() { - return container; - } - - public Report getReport() { - return report; - } - - public boolean cancel() { - cancel = true; - return true; - } - - public void setProgressTicket(ProgressTicket progressTicket) { - this.progressTicket = progressTicket; - } +@ServiceProvider(service = FileImporterBuilder.class) +public final class PairsImporterBuilder implements FileImporterBuilder { + @Override public String getName() { return "Pairs edge list"; } + + @Override + public FileType[] getFileTypes() { + return new FileType[] { + new FileType(".pairs", "Pairs edge-list files") + }; + } + + @Override + public boolean isMatchingImporter(FileObject fileObject) { + return "pairs".equalsIgnoreCase(fileObject.getExt()); + } + + @Override + public FileImporter buildImporter() { + return new PairsImporter(); + } } ``` -The infrastructure is set, the container for pushing data, the report for pushing logs and errors, and the progress management. +The builder is a singleton service; the importer it returns carries one operation's state and must be new each time. Match both the advertised extension and the actual file criteria. For ambiguous formats, inspect a small prefix safely rather than claiming every text file. -### Finish the builder +## Parse into drafts -Go back to the *MyImporterBuilder* and complete `buildImporter()` and `isMatchingImporter()` methods: +Gephi supplies the `Reader`, container, and progress ticket: ```java -public FileImporter buildImporter() { - return new MyImporter(); -} - -public boolean isMatchingImporter(FileObject fileObject) { - return fileObject.getExt().equalsIgnoreCase("foo"); +public final class PairsImporter implements FileImporter, LongTask { + private Reader reader; + private Report report; + private ProgressTicket progressTicket; + private volatile boolean cancelled; + + @Override public void setReader(Reader reader) { this.reader = reader; } + @Override public Report getReport() { return report; } + + @Override + public boolean execute(ContainerLoader container) { + report = new Report(); + Progress.start(progressTicket); + + try (BufferedReader lines = new BufferedReader(reader)) { + String line; + int lineNumber = 0; + while (!cancelled && (line = lines.readLine()) != null) { + lineNumber++; + if (line.isBlank() || line.startsWith("#")) { + continue; + } + + String[] fields = line.split(",", -1); + if (fields.length != 2 + || fields[0].isBlank() || fields[1].isBlank()) { + report.logIssue(new Issue( + "Line " + lineNumber + ": expected source,target", + Issue.Level.WARNING)); + continue; + } + + NodeDraft source = node(container, fields[0].trim()); + NodeDraft target = node(container, fields[1].trim()); + EdgeDraft edge = container.factory().newEdgeDraft(); + edge.setSource(source); + edge.setTarget(target); + container.addEdge(edge); + } + return !cancelled; + } catch (IOException ex) { + report.logIssue(new Issue( + "Could not read the file: " + ex.getMessage(), + Issue.Level.SEVERE)); + return false; + } finally { + Progress.finish(progressTicket); + } + } + + private NodeDraft node(ContainerLoader container, String id) { + NodeDraft node = container.getNode(id); + if (node == null) { + node = container.factory().newNodeDraft(id); + node.setLabel(id); + container.addNode(node); + } + return node; + } + + @Override public boolean cancel() { cancelled = true; return true; } + @Override public void setProgressTicket(ProgressTicket ticket) { + progressTicket = ticket; + } } ``` -## With settings UI - -You can create an `ImporterUI` class for your importer. It is not mandatory and the importer will work normally with default settings. +This example warns and skips malformed records. For a format invariant that makes the whole file unusable, log a severe or critical issue and return `false`. Never use one vague “invalid file” message when a line number, field, and expected value can guide the user. -### Create MyImporterUI +### Text, XML, and binary input -Create a new `ImporterUI` class, for instance *MyImporterUI* that implements [`ImporterUI`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/io/importer/spi/ImporterUI.html). +- Wrap text readers in `BufferedReader` or use `ImportUtils.getTextReader(reader)` when line numbers are useful. +- Prefer a streaming XML parser for large XML documents; `ImportUtils.getXMLReader(reader)` provides StAX support. +- Avoid DOM for unbounded files because it materializes the whole document. +- Implement the appropriate byte/file-aware SPI for genuinely binary formats rather than converting arbitrary bytes through a character reader. -Your UI class is responsible for providing the JPanel associated to your importer and set settings value to your *MyImporter* instance. The system will ask for a JPanel, show a setting dialog and then call `unsetup()`. If users validate the settings panel by hitting OK, the `unsetup()` method is called with update set as true and ask the UI to write the setting values. -The sample below will help you: +Treat all imported content as untrusted: bound sizes where the format allows it, validate numeric ranges, avoid entity expansion in XML, and never execute paths or commands found in data. -```java -public class MyImporterUI implements ImporterUI { - - private JPanel panel; - private JCheckBox option; - private MyImporter importer; - - public void setup(Importer importer) { - this.importer = (MyImporter)importer; - } - - public JPanel getPanel() { - panel = new JPanel(); - option = new JCheckBox("Option"); - panel.add(option); - return panel; - } - - public void unsetup(boolean update) { - if(update) { - importer.setOption(option.isSelected()); - } - panel = null; - importer = null; - option = null; - } - - public String getDisplayName() { - return "Importer Foo"; - } - - public boolean isUIForImporter(Importer importer) { - return importer instanceof MyImporter; - } -} -``` +## When a custom processor is appropriate -In the example below, notice the `importer.setOption(option.isSelected())` line. The `setOption()` method doesn't exist in *MyImporter* but it shows how to configure it easily. +The importer creates and validates drafts; a `Processor` decides how one or more `ContainerUnloader`s become workspace data. The standard processor already handles ordinary “append to current/new workspace” behavior. Write a custom processor only for a distinct merge or workspace-creation policy. It must implement `setContainers`, optional `setWorkspace`, `process`, progress, display name, and report; a matching `ProcessorUI` exposes it in the import workflow. Keep format parsing in the importer so the same container can still be processed in different ways. -### Register the UI +## Add settings UI only when needed -Add `@ServiceProvider` annotation to your UI class. Add the following line before *MyImporterUI* class definition, as shown below: +Register `ImporterUI` as a separate service. In Gephi 0.11.2, `setup` receives an **array** because one dialog can configure several importers: ```java @ServiceProvider(service = ImporterUI.class) -public class MyImporterUIimplements ImporterUI{ -... +public final class PairsImporterUI implements ImporterUI { + private PairsImporter[] importers; + private JCheckBox directed; + + @Override + public void setup(Importer[] values) { + importers = Arrays.stream(values) + .map(PairsImporter.class::cast) + .toArray(PairsImporter[]::new); + } + + @Override + public JPanel getPanel() { + directed = new JCheckBox("Create directed edges", true); + JPanel panel = new JPanel(); + panel.add(directed); + return panel; + } + + @Override + public void unsetup(boolean update) { + if (update) { + for (PairsImporter importer : importers) { + importer.setDirected(directed.isSelected()); + } + } + importers = null; + directed = null; + } + + @Override public String getDisplayName() { return "Pairs options"; } + @Override public boolean isUIForImporter(Importer value) { + return value instanceof PairsImporter; + } +} ``` -## Advices - -### How to write a text file importer - -The best way for text files is to use a `LineNumberReader`. -One can get it from the `ImportUtils` class: - -``LineNumberReader lineReader = ImportUtils.getTextReader(reader);`` - -### How to write an XML file importer - -XML files can be read with different methods (DOM, SAX, StAX) in Java. -[StAX](http://www.wikiwand.com/en/StAX) is combining simplicity and efficiency and should be favored to read XML files: - -``XMLStreamReader xmLReader = ImportUtils.getXMLReader(reader);`` - -Thought it is not recommended to use DOM as it's too much memory consuming, here is how to get a Document from the reader: - -``Document document = ImportUtils.getXMLDocument(reader);`` - -### How to handle errors and exceptions - -Warnings and errors are generated by importers to let users get feedback about the import process. Data is rarely clean and perfectly formated, thus it's necessary to give a specific error message for each problem. The report present in your importer class is here for that. - -Similar as classical logging framework, that is how to log a information message: +The example parser must then apply `directed` to each edge using the direction method defined by `EdgeDraft`. Copy settings back only when `update` is true. Validate them again in the importer. -``report.logIssue(new Issue("The attribute label has been found", Issue.Level.INFO));`` +## Importer checklist -There are four levels: **INFO**, **WARNING**, **SEVERE**, **CRITICAL**. Note that the critical level will stop the import process and throw an exception, so it is reserved if the file cannot be read or the XML is not valid for instance. -About exceptions, it is recommended that you throw `RuntimeException` from your execute method. They will be properly catched and displayed to users in a message box. +- Duplicate node IDs, missing endpoints, parallel edges, self-loops, encoding, and direction have explicit policies. +- Attribute columns are declared with correct types before values are assigned. +- Warnings identify the record; fatal problems return `false`. +- Parsing is streaming and cancellation is checked frequently. +- The supplied reader is not replaced with a hard-coded file path. +- Tests cover empty, malformed, Unicode, large, and cancelled input. -![image](/docs/Plugins/Import/00_image.png) +See the [Gephi 0.11.2 ImportPlugin](https://github.com/gephi/gephi/tree/v0.11.2/modules/ImportPlugin) for production parsers and the bootcamp's matrix importer for the overall builder/importer/UI shape only. diff --git a/gephi-desktop/docs/Plugins/Layout.md b/gephi-desktop/docs/Plugins/Layout.md index f728071..cd53eda 100644 --- a/gephi-desktop/docs/Plugins/Layout.md +++ b/gephi-desktop/docs/Plugins/Layout.md @@ -1,84 +1,178 @@ --- id: Layout title: Layout -sidebar_position: 3 +sidebar_position: 5 --- -## Create a new Layout +A layout repeatedly updates node coordinates. Gephi calls `initAlgo()` once, calls `goAlgo()` while `canAlgo()` is true, and finally calls `endAlgo()`. This iterative contract lets users see the graph move and change properties while the algorithm runs. -This HowTo shows how to create a new layout algorithm in Gephi. +Add `layout-api`, `graph-api`, and `org-openide-util-lookup`. -Please look at [How to add a new module](https://github.com/gephi/gephi-plugins?tab=readme-ov-file#how-to-add-a-new-module) first. When you have your plugin module, that we will call *MyLayout*, you can start this tutorial. +## Run an installed layout -### Set Dependencies - -Add `layout-api`, `graph-api` and `org-openide-util-lookup` modules as dependencies for your plugin module *MyLayout*. See [How to configure module dependencies](https://github.com/gephi/gephi-plugins?tab=readme-ov-file#where-are-dependencies-configured). - -### Create LayoutBuilder - -* Layout Builder provides information about the layout algorithm and is responsible for creating your Layout algorithm instances. All Layout algorithms should have their own builder. -* Create a new builder class, for instance *MyLayoutBuilder* that implements [`LayoutBuilder`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/layout/spi/LayoutBuilder.html). -* The builder interface is requesting a [`LayoutUI`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/layout/spi/LayoutUI.html) object. Create an inner or anonymous class that implements `LayoutUI`. -* Add `@ServiceProvider` annotation to your builder class. Add the following line before *MyLayoutBuilder* class definition, as shown below: +A plugin-owned workflow can discover and run an existing layout without registering another one: ```java -import org.openide.util.lookup.ServiceProvider; - -@ServiceProvider(service = LayoutBuilder.class) -public class MyLayoutBuilder implements LayoutBuilder { -... +LayoutBuilder builder = Lookup.getDefault().lookupAll(LayoutBuilder.class) + .stream() + .filter(candidate -> candidate.getName().equals(requestedName)) + .findFirst() + .orElseThrow(); + +Layout layout = builder.buildLayout(); +layout.setGraphModel(graphModel); +layout.resetPropertiesValues(); +applyValidatedProperties(layout, requestedProperties); + +layout.initAlgo(); +try { + for (int iteration = 0; + iteration < maximumIterations && layout.canAlgo() && !cancelled; + iteration++) { + layout.goAlgo(); + } +} finally { + layout.endAlgo(); +} ``` -### Create Layout - -Create a new class that implements [`Layout`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/layout/spi/Layout.html). This is the place the algorithm belongs. +Run this loop on a background executor, never on the EDT. Bound the iterations or elapsed time, propagate cancellation, and create a fresh layout for each run. If failure must not leave half-applied coordinates, snapshot positions before execution and restore them under a write lock. Implement the builder and layout contracts below only when adding a new algorithm to Gephi. -Methods `canAlgo()`, `initAlgo()`, `goAlgo()` and `endAlgo()` are the logic of the algorithm. But the algorithm is manipulating a graph, so where is this graph? The graph can be get from the `GraphModel`, which is injected through the `setGraphModel()` method (fill `setGraphModel()` with `this.graphModel = graphModel;`). The system always sets this graph model before `initAlgo()` is called. +## Register a builder -Add the graph model as a new private field of your class. -In your `goAlgo()` method, add the following code to get the current visible graph: +The registered builder is metadata plus a factory. It should return a new layout instance: ```java -graph = graphModel.getGraphVisible(); +@ServiceProvider(service = LayoutBuilder.class) +public final class GridLayoutBuilder implements LayoutBuilder { + @Override public String getName() { return "Simple grid"; } + + @Override + public Layout buildLayout() { + return new GridLayout(this); + } + + @Override + public LayoutUI getUI() { + return new LayoutUI() { + @Override public String getDescription() { + return "Moves visible nodes toward a regular grid."; + } + @Override public Icon getIcon() { return null; } + @Override public JPanel getSimplePanel(Layout layout) { return null; } + @Override public int getQualityRank() { return 1; } + @Override public int getSpeedRank() { return 5; } + }; + } +} ``` -### Properties +`LayoutUI` describes the algorithm and may supply a compact custom panel. Most settings should instead be `LayoutProperty` values so Gephi can display, edit, and persist them consistently. -Your algorithm may have settings and properties that users may control. To be visible and editable in the Layout UI, properties need to be properly defined. +## Implement the iteration lifecycle -Let's say you have a float speed parameter that you want to expose. That is how to create the appropriate LayoutPropery: +This compact implementation snapshots the visible nodes, computes target positions, and moves each node a fraction of the remaining distance per pass: ```java -LayoutProperty mySpeedProperty = LayoutProperty.createProperty( - this, - Float.class, - "Speed" - "Category", - "A short description what is this propery doing and how users may modify it", - "getSpeed", - "setSpeed"); +public final class GridLayout implements Layout { + private final LayoutBuilder builder; + private GraphModel graphModel; + private boolean running; + private Integer columns; + private Float spacing; + private Float speed; + + public GridLayout(LayoutBuilder builder) { + this.builder = builder; + resetPropertiesValues(); + } + + @Override public void setGraphModel(GraphModel model) { graphModel = model; } + @Override public LayoutBuilder getBuilder() { return builder; } + @Override public void initAlgo() { running = true; } + @Override public boolean canAlgo() { return running && graphModel != null; } + @Override public void endAlgo() { running = false; } + + @Override + public void goAlgo() { + Graph graph = graphModel.getGraphVisible(); + graph.readLock(); + try { + Node[] nodes = graph.getNodes().toArray(); + for (int i = 0; i < nodes.length; i++) { + float targetX = (i % columns) * spacing; + float targetY = (i / columns) * spacing; + Node node = nodes[i]; + node.setX(node.x() + (targetX - node.x()) * speed); + node.setY(node.y() + (targetY - node.y()) * speed); + } + } finally { + graph.readUnlock(); + } + } + + @Override + public void resetPropertiesValues() { + columns = 10; + spacing = 100f; + speed = 0.1f; + } + + // getProperties and JavaBean getters/setters follow. +} ``` -Note that you need to create proper getter and setter for each property you want to expose. - -## Advanced concepts +An algorithm that converges by itself can set `running = false` when its tolerance is reached. A continuous layout leaves it true until the user clicks Stop. `endAlgo()` must release algorithm-owned resources even if execution is interrupted. -### Layout Data +## Expose editable properties -A special mechanism is available if you need to store temporary objects in nodes. In the following example, we will create a new type of layout data which stores a dx and dy value. - -Create a new *MyLayoutData* class that implements [`LayoutData`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/graph/spi/LayoutData.html) and add two floats dx and dy. -How to init? Place the following code at the beginning of `goAlgo()`: +`LayoutProperty` uses JavaBean introspection, so the getter and setter names and boxed types must match exactly: ```java -for (Node n : nodes) { - MyLayoutData layoutData = n.getLayoutData(); - n.setLayoutData(layoutData); +@Override +public LayoutProperty[] getProperties() { + try { + return new LayoutProperty[] { + LayoutProperty.createProperty( + this, Integer.class, "Columns", "Grid", + "Number of nodes in each row.", + "getColumns", "setColumns"), + LayoutProperty.createProperty( + this, Float.class, "Spacing", "Grid", + "Distance between grid positions.", + "getSpacing", "setSpacing"), + LayoutProperty.createProperty( + this, Float.class, "Speed", "Movement", + "Fraction of the remaining distance moved per iteration.", + "getSpeed", "setSpeed") + }; + } catch (Exception ex) { + throw new IllegalStateException("Cannot create layout properties", ex); + } } + +public Integer getColumns() { return columns; } +public void setColumns(Integer value) { columns = Math.max(1, value); } +public Float getSpacing() { return spacing; } +public void setSpacing(Float value) { spacing = Math.max(1f, value); } +public Float getSpeed() { return speed; } +public void setSpeed(Float value) { speed = Math.max(0.001f, Math.min(1f, value)); } ``` -That ensure you have a layout data object for each of your nodes. +Validate values in setters because they can be changed while the algorithm runs. Avoid rebuilding large internal structures on every setter call; mark them dirty and rebuild at the start of the next iteration. + +## Per-node temporary state + +Some force algorithms need displacement, mass, or convergence state for every node. Use `Node.getLayoutData()` and `setLayoutData()` with a small object implementing `org.gephi.graph.spi.LayoutData`. Clear or replace data during `endAlgo()` if it should not survive the run. Do not put temporary algorithm state into user-visible attribute columns. + +## Performance and correctness -### Custom properties +- Operate on `getGraphVisible()` unless the UI explicitly promises to lay out hidden nodes too. +- Take one node snapshot per iteration; avoid repeated conversions inside inner loops. +- Keep allocations out of pairwise hot paths. +- Check `canAlgo()` and graph validity; tolerate an empty graph. +- Release locks in `finally`. +- Test deterministic geometry separately from the Gephi lifecycle. +- For large graphs, measure one iteration time and keep it short enough for responsive stop/property changes. -Properties can be basic types like Boolean, Float etc. If you want to expose custom types, for instance date, you can provide a `PropertyEditor` class when building the property. \ No newline at end of file +See the built-in [Layout API and plugins for Gephi 0.11.2](https://github.com/gephi/gephi/tree/v0.11.2/modules/LayoutPlugin) for production algorithms, then compare only concepts—not old signatures—with the bootcamp grid examples. diff --git a/gephi-desktop/docs/Plugins/Preview_renderer.md b/gephi-desktop/docs/Plugins/Preview_renderer.md index 26bd1fc..8545ddb 100644 --- a/gephi-desktop/docs/Plugins/Preview_renderer.md +++ b/gephi-desktop/docs/Plugins/Preview_renderer.md @@ -1,289 +1,148 @@ --- id: Preview_renderer title: Preview renderer -sidebar_position: 11 +sidebar_position: 13 --- -## Introduction +Preview renderers control the publication-oriented view used on screen and in SVG/PDF export. They do not change the Overview visualization engine. A renderer consumes preview `Item` objects and draws them to one or more `RenderTarget`s. -Preview is a highly customizable module and plug-ins can implement new renderers to display additional elements on screen, for instance hulls for groups. It's also possible to overwrite existing renderers and replace node or edge aspect. +Add `preview-api` and `org-openide-util-lookup`. Add `preview-plugin` only if you intentionally extend a built-in renderer or use its item implementation classes. -Create a new plugin module, that we will call **MyRenderer**, you can start this tutorial. +## Understand the rendering pipeline -**One can find renderers examples in the [PreviewPlugin](https://github.com/gephi/gephi/tree/master/modules/PreviewPlugin) module.** +For each refresh Gephi: -### How renderers work +1. Runs the required `ItemBuilder`s to create preview items from the graph. +2. Calls each renderer's `preProcess()` once. +3. Uses `isRendererForitem()` to select a renderer for each item. +4. Calls `render()` for every accepted item. +5. Calls `postProcess()` once. -Renderers describe how a particular item is rendered and has the code to dray on a rendering target (Java2D, SVG or PDF). Items are for example the node or edges of the graph and are given to renderers to be drawn. Each item (e.g. node, edge) should have its renderer. +`render()` can run thousands of times. Put global aggregation in `preProcess()`, per-item prepared values in `Item.setData()`, and global prepared values in `PreviewProperties`. Registered renderers are singleton services: do not store workspace/render mutable state in fields. -Rendering is a four-steps process: +## Register a renderer -1. First the `preProcess()` method is called on all renderers to let them initialize additional attributes for their items. The best example is the edge renderer which will initialize the source and target position in the `EdgeItem` object during this phase. In general the `preProcess()` method is the best for complex algorithms or gathering data from other items. Note that the `preProcess()` method is called only once per refresh, unlike `render()` which is called many times. -2. The `isRendererForitem()` is then used to determine which renderer should be used to render an item. The method provides access to the preview properties. For instance, if the properties says the edge display is disabled, the edge renderer should return false for every item. Note that nothing avoids several renderer to returns true for the same item. -3. The `render()` method is finally called for every item which the renderer returned `true` at `isRendererForitem()`. It receives the properties and the render target. It uses the item attributes and properties to determine item aspects and the render target to obtain the canvas. -4. Finally, the `postProcess()` method is called once for each renderer. It can be used to finalize the rendering, for instance to add legends or other global elements. - -If you want to create your own `Item` look at [[HowTo add a preview item]] - -## Create a new Renderer - -### Set Dependencies - -Add `preview-api` and `org-openide-util-lookup` modules as dependencies for your plugin module. See [[How To Set Module Dependencies]]. - -### Create new Renderer - -Create a new renderer **`MyRenderer`** class, which implements `Renderer`. - -Add `@ServiceProvider` annotation to your renderer class, so it is detected by the system. +```java +@ServiceProvider(service = Renderer.class, position = 500) +public final class NodeHaloRenderer implements Renderer { + private static final String ENABLED = "org.example.nodeHalo.enabled"; -Here is how it should look like: + @Override public String getDisplayName() { return "Node halo"; } -```java -@ServiceProvider(service = Renderer.class) -public class MyRenderer implements Renderer { - - public String getDisplayName(){ - //return user friendly name for the renderer - } - - public void preProcess(PreviewModel previewModel) { - //TODO - } - - public void render(Item item, RenderTarget target, PreviewProperties properties) { - //TODO - } - - public void postProcess(PreviewModel previewModel, RenderTarget target, PreviewProperties properties) { - //TODO - } - + @Override public PreviewProperty[] getProperties() { - //TODO + return new PreviewProperty[] { + PreviewProperty.createProperty( + this, ENABLED, Boolean.class, + "Show node halos", + "Draws a halo around every node.", + PreviewProperty.CATEGORY_NODES + ).setValue(Boolean.TRUE) + }; } - + + @Override public void preProcess(PreviewModel model) { } + + @Override public boolean isRendererForitem(Item item, PreviewProperties properties) { - //TODO + return Item.NODE.equals(item.getType()) + && properties.getBooleanValue(ENABLED); } - - public boolean needsItemBuilder(ItemBuilder itemBuilder, PreviewProperties properties) { - //TODO - } - - public CanvasSize getCanvasSize(Item item, PreviewProperties properties) { - //TODO - } - -} -``` -### Implement the `isRendererForItem` + @Override + public boolean needsItemBuilder( + ItemBuilder builder, PreviewProperties properties) { + return ItemBuilder.NODE_BUILDER.equals(builder.getType()) + && properties.getBooleanValue(ENABLED); + } -Each item returns a different value for its `getType()` method so it's easy to know if the item is a node, an edge or a label. + @Override + public void render( + Item item, RenderTarget target, PreviewProperties properties) { + if (target instanceof G2DTarget g2d) { + renderG2D(item, g2d); + } else if (target instanceof SVGTarget svg) { + renderSVG(item, svg); + } else if (target instanceof PDFTarget pdf) { + renderPDF(item, pdf); + } + } -The method also receives the current properties and can query values. + @Override + public CanvasSize getCanvasSize(Item item, PreviewProperties properties) { + float x = item.getData("x"); + float y = item.getData("y"); + float diameter = item.getData("size") + 8f; + return new CanvasSize( + x - diameter / 2f, y - diameter / 2f, + diameter, diameter); + } -Here is the code from the edge arrow renderer that only works when items are non-directed edges: + @Override + public void postProcess( + PreviewModel model, RenderTarget target, + PreviewProperties properties) { } -```java -public boolean isRendererForitem(Item item, PreviewProperties properties) { - return item.getType().equals(Item.EDGE) && properties.getBooleanValue(PreviewProperty.DIRECTED) - && (Boolean) item.getData(EdgeItem.DIRECTED); + // renderG2D, renderSVG, and renderPDF use each target's drawing API. } ``` -### Implement `getProperties` - -You can add your own properties attached to the renderer. Typically each renderer has its own properties. For instance the node renderer has border size or color as properties. Once defined a property will be shawn to the user who can change its value. - -Properties have a identifier, a display name, a description and a type. It's important to have a unique identifier to each property. Be sure to choose something not already taken. - -Properties are displayed in categories. You can either set an existing category or define a new one. Existing categories are `PreviewProperty.CATEGORY_NODES`, `PreviewProperty.CATEGORY_EDGES`, `PreviewProperty.CATEGORY_NODE_LABELS`, `PreviewProperty.CATEGORY_EDGE_LABELS` and `PreviewProperty.CATEGORY_EDGE_ARROWS`. +The `position` determines renderer order. Use a unique, namespaced property ID. `needsItemBuilder()` should return false when the renderer is disabled so Preview can avoid unnecessary item creation. -Here is how to create a property `"Border width"`: - -```java -public PreviewProperty[] getProperties() { - return new PreviewProperty[]{ - PreviewProperty.createProperty(this, "border_width", Float.class, - "Border width", - "", - PreviewProperty.CATEGORY_NODES).setValue(1f)}; -} -``` +The standard node builder stores `x`, `y`, `size`, and `color`; edge and label builders add their own data. If your renderer needs data not supplied by a standard item, write an `ItemBuilder` for a custom item type instead of overloading unrelated keys. -Properties should have a default value. It is set at the creation of the property simply by calling `setValue()`. +## Support every relevant target -### Implement render +The default targets expose different drawing backends: -The render method receives an item and draw it to a render target. There is three possible render targets: +- `G2DTarget.getGraphics()` returns `Graphics2D` for on-screen preview. +- `SVGTarget` creates DOM elements and exposes named top-level groups. +- `PDFTarget.getContentStream()` returns PDFBox's `PDPageContentStream`. --- [Processing](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/preview/api/G2DTarget.html) --- [SVG](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/preview/api/SVGTarget.html) --- [PDF](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/preview/api/PDFTarget.html) +If a visual feature appears on screen but vanishes from exported SVG/PDF, the renderer is incomplete. Share geometry and color calculations across the three drawing methods; only target-specific drawing should differ. Test each target with transparency, scaling, negative coordinates, and an empty graph. -Renderers should implement the drawing routines for the three targets. One might ask why the same code has to be duplicated in three different ways. Unfortunately there is no unified drawing toolkit flexible and efficient enough to support this task at this point. The good news is that renderers have total control on what is drawn. It's verbose but flexible. +`getCanvasSize()` must include the entire mark, including halo, stroke, arrow, or label. An underestimated box can crop output. Return `new CanvasSize()` only when the size genuinely cannot be computed. -When the target is Java2D, renderers obtain the `G2DTarget` object. For the SVG target, renderers obtain Batik's [Document](http://xmlgraphics.apache.org/batik/using/dom-api.html) instance. As the PDF target rely on the PDFBox library renderers obtain the [PDPageContentStream](https://pdfbox.apache.org/docs/2.0.8/javadocs/org/apache/pdfbox/pdmodel/PDPageContentStream.html) object. +## Preprocess without singleton state -Example how to handle the three targets in render: +For a global minimum and maximum: ```java -public void render(Item item, RenderTarget target, PreviewProperties properties) { - if (target instanceof G2DTarget) { - renderG2D(item, (G2DTarget) target, properties); - } else if (target instanceof SVGTarget) { - renderSVG(item, (SVGTarget) target, properties); - } else if (target instanceof PDFTarget) { - renderPDF(item, (PDFTarget) target, properties); +@Override +public void preProcess(PreviewModel model) { + float min = Float.POSITIVE_INFINITY; + float max = Float.NEGATIVE_INFINITY; + for (Item edge : model.getItems(Item.EDGE)) { + float weight = edge.getData("weight"); + min = Math.min(min, weight); + max = Math.max(max, weight); } -} - -public void renderG2D(Item item, G2DTarget target, PreviewProperties properties) { - Graphics2D graphics = target.getGraphics(); - ... -} - -public void renderPDF(Item item, PDFTarget target, PreviewProperties properties) { - PDPageContentStream cb = target.getContentStream(); - ... -} - -public void renderSVG(Item item, SVGTarget target, PreviewProperties properties) { - Element elem = target.createElement("circle"); - ... - target.getTopElement("nodes").appendChild(elem); -} -``` - -Look at renderers examples in the [PreviewPlugin](https://github.com/gephi/gephi/tree/master/modules/PreviewPlugin) module. - -## Overwrite an existing renderer - -The default renderers can be overridden or extended. -Extend or replace an existing renderer - -To extend or completely replace a default Renderer by your own implementation, create a new Renderer and set the annotation like below. In addition, add `preview-plugin` module as a dependency. - -`@ServiceProvider(service=Renderer.class, position=XXX) public class MyRenderer extends NodeRenderer` - -Being XXX the new position of the renderer Then you can reuse parts of the base class or just override them. - -Default renderers are: - -- `org.gephi.preview.plugin.renderers.NodeRenderer` -- `org.gephi.preview.plugin.renderers.EdgeRenderer` -- `org.gephi.preview.plugin.renderers.NodeLabelRenderer` -- `org.gephi.preview.plugin.renderers.EdgeLabelRenderer` -- `org.gephi.preview.plugin.renderers.ArrowRenderer` - -## Appendix - -### Custom properties - -Properties are usually default Java primitives like `Float` or `Color`. Custom property types can be used if a valid property editor is defined. Property editors define how a property value should be serialized and provide a custom UI to let the user modify the value. For instance to define a new property type `NumberRange` one could create a UI with two text fields to enter number ranges. - -Create the new type class and then create a new property editor: - -```java -public class NumberRangePropertyEditor extends java.beans.PropertyEditorSupport { - @Override - public Component getCustomEditor() { - //TODO returns custom JPanel - } - - @Override - public String getAsText() { - //TODO returns the value as string - } - - @Override - public void setAsText(String text) { - //TODO set value from the text - } - - @Override - public boolean supportsCustomEditor() { - return true; - } + model.getProperties().putValue("org.example.weight.min", min); + model.getProperties().putValue("org.example.weight.max", max); } ``` -One can find property editors examples in the [DesktopPreview](https://github.com/gephi/gephi/tree/master/modules/DesktopPreview) module. - -### Complex pre-process +Read these properties in `render()`. Do not assign `min` and `max` to fields: two previews or workspaces could otherwise overwrite the singleton renderer's state. -In each renderer the `preProcess()` method allows to access all preview data and execute complex algorithms. +## Extend or replace a built-in renderer -### How to read all items of a particular type +Extending `NodeRenderer`, `EdgeRenderer`, `NodeLabelRenderer`, `EdgeLabelRenderer`, or `ArrowRenderer` makes your renderer an alternative/replacement for that built-in type. This requires the `preview-plugin` dependency and couples the plugin more tightly to Gephi's implementation. Prefer a separate decorator renderer when it can draw the additional mark independently. -Simply by querying the preview model: +When replacement is necessary, register it with a deliberate position and test compatibility with the Renderers manager. Multiple plugins may extend the same built-in renderer. -```java -Item[] edgeItems = previewModel.getItems(Item.EDGE); -``` +## Custom properties -### How to use constants and data generated in pre-process +Basic property types are handled automatically. A custom value type needs a `PropertyEditor` that can parse/serialize values and optionally provide a custom editor. Register the editor and ensure malformed persisted settings fall back safely. For a simple range, two bounded numeric properties are often clearer than a custom type. -Renderers should remain **stateless**. That means one shouldn't define any variable or fields in the renderer class itself. Instead all data can be put in the property system. +## Renderer checklist -Here is an example how to calculate the min and max edge weight in the `preProcess()` and use it in `render()`: - -```java -public void preProcess(PreviewModel previewModel) { - PreviewProperties properties = previewModel.getProperties(); - Item[] edgeItems = previewModel.getItems(Item.EDGE); - - for (Item edge : edgeItems) { - minWeight = Math.min(minWeight, (Float) edge.getData(EdgeItem.WEIGHT)); - maxWeight = Math.max(maxWeight, (Float) edge.getData(EdgeItem.WEIGHT)); - } - properties.putValue("weight.min", minWeight); - properties.putValue("weight.max", maxWeight); -} - -public void render(Item item, RenderTarget target, PreviewProperties properties) { - float minWeight = properties.getFloatValue("weight.min"); - float maxWeight = properties.getFloatValue("weight.max"); - ... -} -``` +- The service has a stable position and display name. +- Property IDs are namespaced, typed, documented, and have defaults. +- `preProcess()` and `render()` keep state in items/properties, not fields. +- G2D, SVG, and PDF output agree. +- Canvas bounds include every painted pixel. +- Expensive aggregation runs once, not per item. +- Disabled renderers do not request unnecessary builders. +- SVG/PDF escaping and graphics-state save/restore are tested. -For additional data generated per item, simply call `item.setData()`. - -### Default item data - -**Node** -- x (float) -- y (float) -- size (float) -- color (color) - -**Edge** -- weight (float) -- directed (boolean) -- mutual (boolean) -- self_loop (boolean) -- meta_edge (boolean) -- color (color) - -**Node Label** -- label (string) -- color (color) -- size (float) -- width (float) -- height (float) -- visible (boolean) - -**Edge Labels** -- label (string) -- color (color) -- size (float) -- width (float) -- height (float) -- visible (boolean) - -**Edges have additional data set by the default edge renderer:** -- source (nodeitem) -- target (nodeitem) +Use Gephi 0.11.2's [Preview API](https://github.com/gephi/gephi/tree/v0.11.2/modules/PreviewAPI) and [built-in renderers](https://github.com/gephi/gephi/tree/v0.11.2/modules/PreviewPlugin/src/main/java/org/gephi/preview/plugin/renderers) as the exact reference. diff --git a/gephi-desktop/docs/Plugins/Statistics.md b/gephi-desktop/docs/Plugins/Statistics.md index 09dbe78..8ac8ecc 100644 --- a/gephi-desktop/docs/Plugins/Statistics.md +++ b/gephi-desktop/docs/Plugins/Statistics.md @@ -1,204 +1,164 @@ --- id: Statistics title: Statistics -sidebar_position: 4 +sidebar_position: 6 --- -## Create a new Metric +A statistics plugin computes a network result, may write node or edge columns, and returns an HTML report. The desktop Statistics panel discovers a `StatisticsBuilder` and, optionally, a matching `StatisticsUI`. -This HowTo shows how to create a new statistic/metric algorithm in Gephi. +Add `statistics-api`, `graph-api`, `utils-longtask`, and `org-openide-util-lookup`. -Create a new plugin module, that we will call *MyMetric*. +## Run an installed statistic -### Set Dependencies - -Add `statistics-api`, `graph-api` modules as dependencies for your plugin module *MyMetric*. See [[How To Set Module Dependencies]]. Also add `utils-longtask` and `org-openide-util-lookup`, which will be used. - -### Create StatisticsBuilder - -* Statistics Builder provides information about the metric algorithm and is responsible for creating your Statistics algorithm instances. All metric algorithms should have their own builder. -* Create a new builder class, for instance `MyMetricBuilder` that implements [`StatisticsBuilder`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/statistics/spi/StatisticsBuilder.html). -* Fill `getName()` method by returning a display name like *My Metric*. Leaves other methods untouched for the moment. -* Add `@ServiceProvider` annotation to your builder class. Add the following line before *MyMetricBuilder* class definition, as shown below: +Discover a `StatisticsBuilder` when your plugin needs an analysis already installed in Gephi: ```java -@ServiceProvider(service = StatisticsBuilder.class) -public class MyMetricBuilder implements StatisticsBuilder { -... +StatisticsBuilder builder = Lookup.getDefault() + .lookupAll(StatisticsBuilder.class) + .stream() + .filter(candidate -> candidate.getName().equals(requestedName)) + .findFirst() + .orElseThrow(); + +Statistics statistic = builder.getStatistics(); +applyValidatedParameters(statistic, requestedParameters); +statistic.execute(graphModel); +String report = statistic.getReport(); ``` -### Create Statistics +Execute expensive statistics on a background task. If the returned object implements `LongTask`, supply a progress ticket and connect cancellation before calling `execute`. Treat builder names as presentation text and prefer a stable identifier or an explicit supported-builder mapping when automating selection. Implement the contracts below only when contributing a new statistic. -Create a new class that implements [`Statistics`](https://javadoc.io/doc/org.gephi/gephi/latest/org/gephi/statistics/spi/Statistics.html) and name it *MyMetric*. This is the place the algorithm belongs. -Locate the `execute()` method, you will add your code here. The `getReport()` method should return plain text or HTML text that describe the algorithm execution. Create a new field +## Separate the three responsibilities -`` private String report = "";`` +- `StatisticsBuilder` creates fresh algorithm instances and identifies their class. +- `Statistics` contains calculation state and produces the report. +- `StatisticsUI` integrates settings and a short result value into the desktop panel. -and return report in `getReport()`. That done, go back to `StatisticsBuilder` and fill remaining methods like above: +Register builders and UIs, not the algorithm itself: ```java -public Statistics getStatistics() { - return new MyMetric(); -} - -public Class getStatisticsClass() { - return MyMetric.class; +@ServiceProvider(service = StatisticsBuilder.class) +public final class SelfLoopBuilder implements StatisticsBuilder { + @Override public String getName() { return "Self-loop count"; } + @Override public Statistics getStatistics() { return new SelfLoopStatistic(); } + @Override public Class getStatisticsClass() { + return SelfLoopStatistic.class; + } } ``` -Details about how to use `GraphModel` in the next section. - -### Set LongTask - -To let your algorithm task be cancelled and its progress watched, implement `LongTask` interface on *MyMetric*. -Add two fields: +## Implement a cancellable statistic ```java -private boolean cancel = false; -private ProgressTicket progressTicket; -``` - -and implement new methods: +public final class SelfLoopStatistic implements Statistics, LongTask { + private volatile boolean cancelled; + private ProgressTicket progressTicket; + private int edgeCount; + private int selfLoopCount; + + @Override + public void execute(GraphModel graphModel) { + Graph graph = graphModel.getGraphVisible(); + edgeCount = graph.getEdgeCount(); + selfLoopCount = 0; + Progress.start(progressTicket, edgeCount); + + graph.readLock(); + try { + for (Edge edge : graph.getEdges()) { + if (cancelled) { + return; + } + if (edge.isSelfLoop()) { + selfLoopCount++; + } + Progress.progress(progressTicket); + } + } finally { + graph.readUnlock(); + Progress.finish(progressTicket); + } + } -```java -public boolean cancel() { - cancel = true; - return true; -} + @Override + public String getReport() { + return "

Self-loop count

" + + "

Visible edges: " + edgeCount + "

" + + "

Self-loops: " + selfLoopCount + "

" + + ""; + } -public void setProgressTicket(ProgressTicket progressTicket) { - this.progressTicket = progressTicket; + @Override public boolean cancel() { cancelled = true; return true; } + @Override public void setProgressTicket(ProgressTicket ticket) { + progressTicket = ticket; + } + public int getSelfLoopCount() { return selfLoopCount; } } ``` -Use the cancel field to terminate your algorithm execution properly and return from `execute()`. -### Create StatisticsUI +The controller supplies the `GraphModel` and runs `LongTask` statistics in the long-task infrastructure. Use the visible graph if the measure should follow filtering. Say “visible graph” in the report so results are interpretable. -Create a new class that implements `StatisticsUI` and name it *MyMetricUI*. The user interfaces is defined here and allows to be automatically added to the `Statistics` module in Gephi. -Add `@ServiceProvider` annotation to your UI class. Add the following line before *MyMetricUI* class definition, as shown below: +For an expensive algorithm, snapshot the necessary structure under the read lock, release it, calculate, then write results in a short final phase. See [Graph access and long tasks](./Graph_API_and_Threads.md#snapshot-calculate-write-back). -```java -@ServiceProvider(service = StatisticsUI.class) -public class MyMetricUI implements StatisticsUI{ -... -``` +## Add result columns safely -First implement description method: +Use the table from `GraphModel`; the old `AttributeModel` parameter no longer exists in the 0.11.2 `Statistics.execute` signature. ```java -public String getDisplayName() { - return "My Metric"; -} - -public String getCategory() { - return StatisticsUI.CATEGORY_NETWORK_OVERVIEW; +Table nodeTable = graphModel.getNodeTable(); +Column column = nodeTable.getColumn("org.example.my_score"); +if (column == null) { + column = nodeTable.addColumn( + "org.example.my_score", "My score", Double.class, null); +} else if (!Double.class.equals(column.getTypeClass())) { + throw new IllegalStateException("My score column has an incompatible type"); } -public int getPosition() { - return 800; +for (Node node : graph.getNodes()) { + if (cancelled) { + return; + } + node.setAttribute(column, scores.get(node.getId())); } - - public Class getStatisticsClass() { - return MyMetric.class; - } ``` -The category is just where you want your metric to be displayed: **NODE**, **EDGE** or **NETWORK**. The position control the order the metric front-end are displayed. Returns a value between 1 and 1000, that indicates the position. Less means upper. - -Now create a new JPanel for your metric settings panel. Name it *MyMetricPanel*. Add setters and getters for all properties users can edit. Let's say the panel gives the choice on the graph type and has `isDirected()` and `setDirected()` methods. - -Now the `setup()` and `unsetup()` are filled. It follow the injection pattern, an instance of *MyMetric* is pushed to let the UI control it. +Namespace a computed column ID. Reuse a compatible existing column so rerunning the statistic updates rather than duplicates it. Decide what cancellation means before writing: either compute fully and commit, or document that partial values may remain. -First add fields to store the current metric: - -```java -private MyMetricPanel panel; -private MyMetric myMetric; -``` +## Integrate settings and summary UI -and fill `getSettingsPanel()`, `setup()` and `unsetup()`: +Register a `StatisticsUI` whose class exactly matches the builder: ```java -public JPanel getSettingsPanel() { - panel = new MyMetricPanel(); - return panel; -} +@ServiceProvider(service = StatisticsUI.class) +public final class SelfLoopUI implements StatisticsUI { + private SelfLoopStatistic statistic; -public void setup(Statistics statistics) { - this.myMetric = (MyMetric) statistics; - if(panel!=null) { - panel.setDirected(myMetric.isDirected()); + @Override public JPanel getSettingsPanel() { return null; } + @Override public void setup(Statistics value) { + statistic = (SelfLoopStatistic) value; } -} - -public void unsetup() { - if(panel!=null) { - myMetric.setDirected(panel.isDirected()); + @Override public void unsetup() { statistic = null; } + @Override public Class getStatisticsClass() { + return SelfLoopStatistic.class; } - panel = null; -} -``` - -The final step is the `getValue()`, which returns the result value on the front-end. If your metric doesn't have a single result value, return `null`. - -## Implementation help - -The `execute()` method gives both `GraphModel` for getting graph structure and AttributeModel to write results in new attribute columns. - -### Use Progress - -If you know how many steps your algorithm is doing, for instance if your algorithm just reads nodes: - -``Progress.start(progressTicket, graph.getNodeCount());`` - -and `Progress.progress(progressTicket)` within the loop. - -### Lock your algorithm - -It's preferable to execute your algorithm in a read lock, in order no other thread can modify the graph while execution. Never return `execute()` with a lock open. - -```java -graph.readLock(); -try{ - //Your algorithm - graph.readUnlock(); -} finally { - graph.readUnlock(); -} -``` - -### Write results for each node/edge - -It's easy, you create a new column and set row's value for each node. If you want to write an in-degree column, in `execute()`: - -```java -Table nodeTable = graphModel.getNodeTable(); -Column col = nodeTable.addColumn("my_column", "My Column", Integer.class); - -for (Node n : graph.getNodes()) { - n.setAttribute(col, ...); //Your value here + @Override public String getValue() { + return statistic == null ? null : Integer.toString(statistic.getSelfLoopCount()); + } + @Override public String getDisplayName() { return "Self-loop count"; } + @Override public String getShortDescription() { return "Counts visible self-loop edges."; } + @Override public String getCategory() { return StatisticsUI.CATEGORY_NETWORK_OVERVIEW; } + @Override public int getPosition() { return 800; } } ``` -### Sample - -```java -public void execute(GraphModel graphModel, AttributeModel attributeModel) { - report += "Algorithm started "; - Graph graph = graphModel.getGraphVisible(); - graph.readLock(); +If settings are needed, create the panel in `getSettingsPanel()`, load algorithm values in `setup()`, and copy validated values back in `unsetup()`. The algorithm must still validate settings because it can also be called programmatically. - try { - Progress.start(progressTicket, graph.getNodeCount()); +## Report quality checklist - for (Node n : graph.getNodes()) { - //do something - Progress.progress(progressTicket); - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - graph.readUnlock(); - } -} -``` \ No newline at end of file +- Define the graph view, directionality, weights, missing-value policy, and parameters used. +- Escape user-derived strings before placing them in HTML. +- Handle zero nodes/edges without division by zero. +- Start, update, and finish progress in all paths. +- Check cancellation in outer and expensive inner loops. +- Make repeated runs replace or deliberately version their output columns. +- Unit-test tiny graphs with known answers, including self-loops, parallel edges, and disconnected components as relevant. diff --git a/gephi-desktop/docs/Plugins/index.md b/gephi-desktop/docs/Plugins/index.md index 6299172..67beb55 100644 --- a/gephi-desktop/docs/Plugins/index.md +++ b/gephi-desktop/docs/Plugins/index.md @@ -1,15 +1,72 @@ --- id: index -title: Introduction +title: Plugin development sidebar_position: 1 --- -This section is designed to help developers write [Gephi Plugins](https://gephi.org/plugins/). +Gephi plugins are Java modules loaded by the Gephi Platform. A plugin can add an algorithm, a data source, a renderer, a desktop panel, a graph interaction tool, or a small command. This guide targets **Gephi 0.11.2** and **JDK 17 or later**. -Gephi can be extended in many ways and the major categories are Layout, Export, Import, Data Laboratory, Filter, Generator, Metric, Preview, Tool, and Appearance. +:::info Version scope -A good way to start is to look at examples with the [bootcamp](https://github.com/gephi/gephi-plugins-bootcamp). Another way is to look at existing plugins source code, which is publicly available on GitHub: [Gephi Plugins on GitHub](https://github.com/gephi/gephi-plugins/tree/master-forge/modules). +The examples and API signatures on these pages were checked against the Gephi `v0.11.2` source tag. Older examples remain useful for ideas, but code written for Gephi 0.9.x or 0.10.x must not be copied without checking its imports, method signatures, dependencies, and threading assumptions. -![image](https://camo.githubusercontent.com/9cb37e90a1eff225eaabe50a80adece6dad749798b8a1c7cfbe55d597c9adc0b/687474703a2f2f67657068692e6f72672f696d616765732f706c7567696e735f726962626f6e2e706e67) +::: -Official documentation about plugin development can be found directly on Github: [Gephi Plugin Development](https://github.com/gephi/gephi-plugins). We've also made a custom [Maven plugin](https://github.com/gephi/gephi-maven-plugin) to help get started and validate your plugin faster. \ No newline at end of file +## Use an API or add an extension? + +A plugin does not always need to contribute a new algorithm. Gephi exposes two complementary integration directions: + +- **Consume an API** when the plugin needs to run an installed layout, statistic, filter, importer, exporter, or Data Laboratory operation. Obtain the public controller or installed builders from `Lookup` and operate on the current workspace. +- **Implement an SPI** when the plugin introduces a new layout, statistic, file format, filter, transformer, renderer, or user-visible command that Gephi and other plugins should discover. + +Prefer consuming an existing public service when it already expresses the operation. Implementing a parallel extension duplicates behavior and may bypass capability checks, progress handling, import processing, or other semantics owned by Gephi. The specialized pages show both paths where both are useful. + +## Choose the smallest extension point + +Start from the user experience you want to add, then choose the corresponding service provider interface (SPI). + +| Goal | Extension point | Start here | +| --- | --- | --- | +| Arrange nodes | `Layout` and `LayoutBuilder` | [Layout](./Layout.md) | +| Calculate a graph measure | `Statistics`, `StatisticsBuilder`, and optionally `StatisticsUI` | [Statistics](./Statistics.md) | +| Read a file format | `FileImporter` and `FileImporterBuilder` | [Import](./Import.md) | +| Write a file format | `GraphExporter` and `GraphFileExporterBuilder` | [Export](./Export.md) | +| Create a synthetic network | `Generator` | [Generator](./Generator.md) | +| Build a reusable graph predicate | `NodeFilter`, `EdgeFilter`, or `ComplexFilter` | [Filter](./Filter.md) | +| Add table actions and transformations | Data Laboratory manipulators | [Extend Data Laboratory](./Extend_Data_Laboratory.md) | +| Add a new way to map values to color, size, or labels | Appearance `Transformer` | [Appearance](./Appearance.md) | +| Change Preview or exported graphics | `Renderer` and, when needed, `ItemBuilder` | [Preview renderer](./Preview_renderer.md) | +| Add a toolbar interaction, menu action, or window | NetBeans Platform actions, `Tool`, or `TopComponent` | [Desktop UI and events](./Desktop_UI_and_Events.md) | +| Read or modify the current network | Graph and Project APIs | [Graph access and long tasks](./Graph_API_and_Threads.md) | + +If one module needs several extension points, keep the data-processing code independent from Swing and put each registration in a small adapter class. This makes the core logic easier to test. + +## How a plugin fits into Gephi + +A typical interaction has four parts: + +1. **Registration:** a class annotated with `@ServiceProvider` advertises an implementation of a Gephi SPI. +2. **Discovery:** Gephi finds that service through NetBeans Lookup; you do not create a global registry yourself. +3. **Context:** Gephi supplies a `Workspace`, `GraphModel`, import container, preview model, or another object appropriate to the extension point. +4. **Execution:** your implementation does bounded work, reports progress when appropriate, and leaves the UI responsive. + +Most plugins therefore contain a small builder or UI adapter and a separate implementation class. Builders are usually singleton services; the objects they create are not. Never keep workspace-specific mutable state in a registered singleton unless it is deliberately keyed by workspace and cleaned up. + +Two less common hooks complete the panorama. An import `Processor` controls how validated containers are merged into one or more workspaces; most format plugins should use Gephi's standard processor. Project persistence providers serialize custom workspace state into `.gephi` files and require a stable, versioned schema. Use either only when the plugin truly owns those lifecycle semantics. + +## Recommended learning path + +1. Follow [Getting started](./Getting_Started.md) and run an empty generated plugin in Gephi. +2. Read [Graph access and long tasks](./Graph_API_and_Threads.md) before writing code that touches a graph or Swing component. +3. Follow the page for your chosen extension point. +4. Add tests for the non-UI logic, then exercise the plugin in a clean Gephi user directory. +5. Build the NBM and follow the submission checklist in [Getting started](./Getting_Started.md#prepare-a-release). + +## Authoritative references + +- [Gephi plugins development repository](https://github.com/gephi/gephi-plugins/tree/master) — project generator, build, run, packaging, and submission workflow. +- [Gephi 0.11.2 source](https://github.com/gephi/gephi/tree/v0.11.2/modules) — authoritative API interfaces and built-in implementations. +- [Gephi Javadoc](https://javadoc.io/doc/org.gephi/gephi/0.11.2/index.html) — API reference pinned to the target version. +- [Gephi plugins bootcamp](https://github.com/gephi/gephi-plugins-bootcamp) — broad catalogue of extension ideas. It targets 0.9.3, so use it as a design gallery, not as copy-ready 0.11.2 code. + +The public `org.gephi.*.api` and `org.gephi.*.spi` packages are the safest dependencies. Depending on implementation or UI classes from `*.impl` and `*.plugin` packages couples a plugin to internal details and should be exceptional and documented.