Skip to content

Conversation

romjeromealt
Copy link
Contributor

@romjeromealt romjeromealt commented Sep 11, 2025

@romjeromealt romjeromealt marked this pull request as ready for review September 12, 2025 17:36
@romjeromealt
Copy link
Contributor Author

romjeromealt commented Sep 12, 2025

Some cleanup, use of more recent modules and methods like time.perf_counter(), some features have been added with the help of Mistral AI (Codestral 25.08?). I asked agent to generate unit test (not tested) and documentation. Some additional improvements have been proposed (lambda, direct use, separate the UI and logic, etc.) or related to method like:

    for handle in self.filtered_list:
        yield self.dbstate.db.get_person_from_handle(handle)
...
self.relationship_cache = {}
def get_relationship(self, person):
    handle = person.get_handle()
    if handle not in self.relationship_cache:
        self.relationship_cache[handle] = t_one(
            self.dbstate, self.relationship, self.default_person, person
        )
    return self.relationship_cache[handle]

but not added because I am not comfortable with.

@romjeromealt
Copy link
Contributor Author

romjeromealt commented Sep 12, 2025

I did not include some extra extensions like : (sorry, in french)
Densité du réseau familial
Mesure la densité des liens familiaux dans un sous-arbre (ex: nombre de mariages ou de liens par individu).

def calculate_family_network_density(db, person, generations=3):
    """
    Calcule la densité du réseau familial pour un individu sur un nombre donné de générations.
    La densité est définie comme le ratio entre le nombre de liens et le nombre d'individus.

    Args:
        db: Base de données Gramps.
        person: Personne de référence.
        generations: Nombre de générations à considérer.

    Returns:
        float: Densité du réseau (entre 0 et 1).
    """
    # Collecter tous les individus dans les générations spécifiées
    individuals = set()
    stack = [(person, 0)]

    while stack:
        current_person, generation = stack.pop()
        if generation > generations:
            continue
        individuals.add(current_person.get_handle())

        # Ajouter les parents
        for family_handle in current_person.get_parent_family_handle_list():
            family = db.get_family_from_handle(family_handle)
            for parent_ref in [family.get_father_handle(), family.get_mother_handle()]:
                if parent_ref:
                    parent = db.get_person_from_handle(parent_ref)
                    stack.append((parent, generation + 1))

        # Ajouter les enfants
        for family_handle in current_person.get_family_handle_list():
            family = db.get_family_from_handle(family_handle)
            for child_ref in family.get_child_ref_list():
                child = db.get_person_from_handle(child_ref.get_reference_handle())
                stack.append((child, generation + 1))

    # Compter les liens (mariages et parent-enfant)
    num_links = 0
    for individual_handle in individuals:
        individual = db.get_person_from_handle(individual_handle)
        num_links += len(individual.get_family_handle_list())  # Mariages
        for family_handle in individual.get_parent_family_handle_list():
            family = db.get_family_from_handle(family_handle)
            num_links += len(family.get_child_ref_list())  # Liens parent-enfant

    num_individuals = len(individuals)
    if num_individuals <= 1:
        return 0.0

    # La densité est le ratio entre le nombre de liens et le nombre maximal possible de liens
    max_possible_links = num_individuals * (num_individuals - 1) / 2
    return num_links / max_possible_links

or Détection des communautés familiales
Identifie les sous-groupes (ou "communautés") fortement connectés dans un arbre généalogique (ex: familles élargies, branches distinctes).

def detect_family_communities(db, person, generations=4):
    """
    Détecte les communautés familiales dans un sous-arbre en utilisant un algorithme simple de clustering.
    Retourne une liste de sous-ensembles d'individus fortement connectés.

    Args:
        db: Base de données Gramps.
        person: Personne de référence.
        generations: Nombre de générations à considérer.

    Returns:
        list: Liste de sets, où chaque set représente une communauté.
    """
    from collections import defaultdict

    # Collecter tous les individus et leurs liens
    individuals = set()
    links = defaultdict(set)
    stack = [(person, 0)]

    while stack:
        current_person, generation = stack.pop()
        if generation > generations:
            continue
        current_handle = current_person.get_handle()
        individuals.add(current_handle)

        # Ajouter les liens avec les parents
        for family_handle in current_person.get_parent_family_handle_list():
            family = db.get_family_from_handle(family_handle)
            for parent_ref in [family.get_father_handle(), family.get_mother_handle()]:
                if parent_ref:
                    parent = db.get_person_from_handle(parent_ref)
                    parent_handle = parent.get_handle()
                    individuals.add(parent_handle)
                    links[current_handle].add(parent_handle)
                    links[parent_handle].add(current_handle)
                    stack.append((parent, generation + 1))

        # Ajouter les liens avec les enfants et conjoints
        for family_handle in current_person.get_family_handle_list():
            family = db.get_family_from_handle(family_handle)
            # Conjoints
            for spouse_ref in [family.get_father_handle(), family.get_mother_handle()]:
                if spouse_ref and spouse_ref != current_handle:
                    spouse = db.get_person_from_handle(spouse_ref)
                    spouse_handle = spouse.get_handle()
                    individuals.add(spouse_handle)
                    links[current_handle].add(spouse_handle)
                    links[spouse_handle].add(current_handle)
            # Enfants
            for child_ref in family.get_child_ref_list():
                child = db.get_person_from_handle(child_ref.get_reference_handle())
                child_handle = child.get_handle()
                individuals.add(child_handle)
                links[current_handle].add(child_handle)
                links[child_handle].add(current_handle)
                stack.append((child, generation + 1))

    # Algorithme simple de clustering (ex: composantes connexes)
    visited = set()
    communities = []

    for individual in individuals:
        if individual not in visited:
            stack = [individual]
            community = set()
            while stack:
                node = stack.pop()
                if node not in visited:
                    visited.add(node)
                    community.add(node)
                    for neighbor in links[node]:
                        if neighbor not in visited:
                            stack.append(neighbor)
            communities.append(community)

    return communities

it was too far (multiple prompts for debug) and maybe it increases too much the number of columns.

Here some lines (documentation) for helping to use these additionnal data or where to go with such improvements:

  • In-depth analysis: Allows for understanding the structure and dynamics of families.
  • Visualization: Can be used to generate family network graphs (e.g., with NetworkX or Gephi).
  • Pattern detection: Identifies extended families, isolated branches, or central individuals.
  • Customization: Adaptable to specific needs (e.g., research on consanguinity, migrations, etc.).

@romjeromealt
Copy link
Contributor Author

Some suggestions for additions to improve it:

  • Add Graphical Visualizations: Integrate libraries like Matplotlib or Plotly to create graphs of relationships and metrics.

  • Export to Other Formats: Add the ability to export data to formats like CSV, JSON, or PDF.

  • Advanced Filtering: Allow users to filter results based on specific criteria, such as gender, period, or number of generations.

  • Person Comparison: Add a feature to compare relationships and metrics between two specific individuals.

  • External Data Integration: Allow importing data from external sources like FamilySearch or Ancestry.

  • Enhanced User Interface: Add customization options for the interface, such as the ability to change the language or theme.

  • Performance Optimization: Improve performance by using techniques like multithreading or caching.

  • Enhanced Documentation: Add detailed comments and comprehensive documentation to facilitate understanding and usage of the script.

  • Unit Testing: Add unit tests to verify the proper functioning of different functions and methods.

  • Enhanced Logging: Improve logging to include more detailed information and different logging levels (DEBUG, INFO, WARNING, ERROR, CRITICAL).

Here's an example of how you could add a person comparison feature:

def compare_people(self, person1_handle, person2_handle):
    """
    Compare relationships and metrics between two people.
    Args:
        person1_handle: Handle of the first person.
        person2_handle: Handle of the second person.
    Returns:
        dict: A dictionary containing the differences between the two people.
    """
    person1 = self.dbstate.db.get_person_from_handle(person1_handle)
    person2 = self.dbstate.db.get_person_from_handle(person2_handle)

    comparison = {
        'name': {
            'person1': name_displayer.display(person1),
            'person2': name_displayer.display(person2)
        },
        'relationship': {
            'person1': get_relationship_between_people(self.dbstate, self.relationship, self.default_person, person1),
            'person2': get_relationship_between_people(self.dbstate, self.relationship, self.default_person, person2)
        },
        'surname_diversity': {
            'person1': FamilyPathMetrics.calculate_surname_diversity(self.dbstate.db, person1_handle, generations=5),
            'person2': FamilyPathMetrics.calculate_surname_diversity(self.dbstate.db, person2_handle, generations=5)
        },
        # Add other metrics to compare here
    }

    return comparison

This function compares names, relationships, and surname diversity between two people and returns a dictionary containing the differences. You can then use this dictionary to display the comparison results in the user interface. ~codestral Agent

@romjeromealt
Copy link
Contributor Author

even more suggestions for additions to improve the script:

  • Interactive Family Tree Visualization: Integrate an interactive family tree visualization tool like D3.js or Vis.js to allow users to explore the family tree graphically.

  • Data Validation and Cleaning: Add functions to validate and clean the data before processing, ensuring data integrity and consistency.

  • Batch Processing: Allow users to process multiple family trees or datasets in batch mode, saving time and effort.

  • Collaboration Features: Add features to collaborate with other users, such as sharing family trees, commenting on entries, or working on the same dataset simultaneously.

  • Mobile App Integration: Develop a mobile app version of the tool to allow users to access and manage their family trees on the go.

  • Machine Learning Integration: Use machine learning algorithms to predict relationships, suggest connections, or identify patterns in the family data.

  • Natural Language Processing (NLP): Integrate NLP to analyze and extract information from historical documents, newspapers, or other textual sources.

  • API Integration: Create an API to allow other applications or services to interact with the family tree data, enabling integration with other tools and platforms.

  • Privacy and Security Features: Add features to ensure the privacy and security of the family tree data, such as encryption, access controls, and data anonymization.

  • User Feedback and Analytics: Collect user feedback and analytics to understand usage patterns, identify areas for improvement, and enhance the user experience.

Here's an example of how you could add an interactive family tree visualization using D3.js:

    """
    Generate an interactive family tree visualization using D3.js.
    Args:
        person_handle: Handle of the person to center the visualization on.
    Returns:
        str: HTML content for the interactive family tree visualization.
    """
    person = self.dbstate.db.get_person_from_handle(person_handle)
    family_tree_data = self.get_family_tree_data(person)

    html_content = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <title>Family Tree Visualization</title>
        <script src="https://d3js.org/d3.v7.min.js"></script>
        <style>
            .node circle {{
                fill: #fff;
                stroke: steelblue;
                stroke-width: 3px;
            }}
            .node text {{
                font: 12px sans-serif;
            }}
            .link {{
                fill: none;
                stroke: #ccc;
                stroke-width: 2px;
            }}
        </style>
    </head>
    <body>
        <div id="family-tree"></div>
        <script>
            const data = {json.dumps(family_tree_data)};
            const width = 800;
            const height = 600;
            const svg = d3.select("#family-tree").append("svg")
                .attr("width", width)
                .attr("height", height);
            const g = svg.append("g")
                .attr("transform", `translate(${width / 2},${height / 2})`);
            const treeLayout = d3.tree().size([2 * Math.PI, 300]);
            const root = d3.hierarchy(data);
            const links = treeLayout(root).links();
            const link = g.selectAll(".link")
                .data(links)
                .enter().append("path")
                .attr("class", "link")
                .attr("d", d3.linkRadial()
                    .angle(d => d.x)
                    .radius(d => d.y));
            const node = g.selectAll(".node")
                .data(root.descendants())
                .enter().append("g")
                .attr("class", "node")
                .attr("transform", d => `
                    rotate(${d.x * 180 / Math.PI - 90})
                    translate(${d.y},0)
                `);
            node.append("circle")
                .attr("r", 10);
            node.append("text")
                .attr("dy", "0.31em")
                .attr("x", d => d.x < Math.PI ? 8 : -8)
                .style("text-anchor", d => d.x < Math.PI ? "start" : "end")
                .attr("transform", d => d.x >= Math.PI ? "rotate(180)" : null)
                .text(d => d.data.name);
        </script>
    </body>
    </html>
    """

    return html_content

def get_family_tree_data(self, person):
    """
    Get the family tree data for a person.
    Args:
        person: The person to get the family tree data for.
    Returns:
        dict: A dictionary containing the family tree data.
    """
    family_tree_data = {
        "name": name_displayer.display(person),
        "children": []
    }

    for family_handle in person.get_family_handle_list():
        family = self.dbstate.db.get_family_from_handle(family_handle)
        for child_ref in family.get_child_ref_list():
            child = self.dbstate.db.get_person_from_handle(child_ref.get_reference_handle())
            family_tree_data["children"].append(self.get_family_tree_data(child))

    return family_tree_data

This code generates an interactive family tree visualization using D3.js, centered on a specific person. The generate_family_tree_visualization function creates the HTML content for the visualization, while the get_family_tree_data function retrieves the family tree data for a person. You can then use this HTML content to display the interactive family tree visualization in a web browser or embed it in a web-based user interface.

etc.

version a.b.c => a.b+1.c
@romjeromealt
Copy link
Contributor Author

romjeromealt commented Sep 12, 2025

more experimental (rather an idea than a code for production):

gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
import time
import logging
import platform
import os
from uuid import uuid4
from gramps.gui.listmodel import ListModel, INTEGER
from gramps.gui.managedwindow import ManagedWindow
from gramps.gui.utils import ProgressMeter
from gramps.gui.plug import tool
from gramps.gui.dialog import WarningDialog
from gramps.gen.display.name import displayer as name_displayer
from gramps.gen.relationship import get_relationship_calculator
from gramps.gen.filters import GenericFilterFactory, rules
from gramps.gen.config import config
from gramps.gen.utils.docgen import ODSTab
from gramps.gen.utils.db import get_timeperiod
import number
from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gui.views.listview import ListView
from gramps.gui.views.treemodels import PersonTreeModel
from gramps.gui.views.treeview import TreeView
from gramps.gen.plug import MenuItem
from gramps.gen.plug.menu import BooleanOption, EnumeratedListOption, FilterOption, PersonOption, StringOption
from gramps.gen.const import URL_MANUAL_PAGE

try:
    _trans = glocale.get_addon_translator(__file__)
except ValueError:
    _trans = glocale.translation
_ = _trans.gettext
_LOG = logging.getLogger(__name__)
_LOG.info(platform.uname())
logging.basicConfig(filename='debug.log', level=logging.DEBUG)

# ... (rest of the existing code)

class FamilyTreeView(TreeView):
    """
    A view that displays a family tree using a TreeView widget.
    """
    def __init__(self, pdata, dbstate, uistate, track, name, model=None):
        TreeView.__init__(self, pdata, dbstate, uistate, track, name, model)
        self.dbstate = dbstate
        self.uistate = uistate
        self.relationship = get_relationship_calculator()
        self.stats_list = []

        # Create a TreeView to display family tree data
        self.tree_view = self.create_family_tree_view()

        # Add the TreeView to a ScrolledWindow
        scrolled_window = Gtk.ScrolledWindow()
        scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled_window.add(self.tree_view)

        # Add the ScrolledWindow to the main window
        self.pack_start(scrolled_window, True, True, 0)

        # Connect the "row-activated" signal to the on_row_activated function
        self.tree_view.connect("row-activated", self.on_row_activated)

    def create_family_tree_view(self):
        """
        Create a TreeView to display family tree data.
        Returns:
            Gtk.TreeView: A TreeView widget to display family tree data.
        """
        # Create a TreeStore to hold the family tree data
        tree_store = Gtk.TreeStore(int, str, str, int, int, int, int, str)  # Columns: Rel_id, Relation, Name, up, down, Common MRA, Rank, Period

        # Create a TreeView widget
        tree_view = Gtk.TreeView(model=tree_store)

        # Create and add columns to the TreeView
        rel_id_column = Gtk.TreeViewColumn("Rel_id", Gtk.CellRendererText(), text=0)
        relation_column = Gtk.TreeViewColumn("Relation", Gtk.CellRendererText(), text=1)
        name_column = Gtk.TreeViewColumn("Name", Gtk.CellRendererText(), text=2)
        up_column = Gtk.TreeViewColumn("up", Gtk.CellRendererText(), text=3)
        down_column = Gtk.TreeViewColumn("down", Gtk.CellRendererText(), text=4)
        common_mra_column = Gtk.TreeViewColumn("Common MRA", Gtk.CellRendererText(), text=5)
        rank_column = Gtk.TreeViewColumn("Rank", Gtk.CellRendererText(), text=6)
        period_column = Gtk.TreeViewColumn("Period", Gtk.CellRendererText(), text=7)

        tree_view.append_column(rel_id_column)
        tree_view.append_column(relation_column)
        tree_view.append_column(name_column)
        tree_view.append_column(up_column)
        tree_view.append_column(down_column)
        tree_view.append_column(common_mra_column)
        tree_view.append_column(rank_column)
        tree_view.append_column(period_column)

        # Populate the TreeView with family tree data
        self.populate_family_tree_view(tree_store)

        return tree_view

    def populate_family_tree_view(self, tree_store):
        """
        Populate the TreeView with family tree data.
        Args:
            tree_store: The TreeStore to populate with family tree data.
        """
        # Get the default person (root of the family tree)
        default_person = self.dbstate.db.get_default_person()
        if default_person:
            # Add the default person to the TreeView
            parent_iter = tree_store.append(None, [
                0,  # Rel_id
                "Self",  # Relation
                name_displayer.display(default_person),  # Name
                0,  # up
                0,  # down
                0,  # Common MRA
                0,  # Rank
                get_timeperiod(self.dbstate.db, default_person.get_handle())  # Period
            ])

            # Recursively add ancestors and descendants to the TreeView
            self.add_ancestors_to_tree_view(tree_store, default_person, parent_iter)
            self.add_descendants_to_tree_view(tree_store, default_person, parent_iter)

    def add_ancestors_to_tree_view(self, tree_store, person, parent_iter):
        """
        Add ancestors of a person to the TreeView.
        Args:
            tree_store: The TreeStore to add ancestors to.
            person: The person to add ancestors for.
            parent_iter: The parent iterator in the TreeStore.
        """
        for family_handle in person.get_parent_family_handle_list():
            family = self.dbstate.db.get_family_from_handle(family_handle)
            father_handle = family.get_father_handle()
            mother_handle = family.get_mother_handle()

            if father_handle:
                father = self.dbstate.db.get_person_from_handle(father_handle)
                dist = self.relationship.get_relationship_distance_new(
                    self.dbstate.db, self.dbstate.db.get_default_person(), father, only_birth=True)
                rank = dist[0][0]
                rel_a, rel_b = FamilyPathMetrics.extract_relationship_paths(dist)
                Ga, Gb = FamilyPathMetrics.calculate_relationship_path_lengths(rel_a, rel_b)
                mra = FamilyPathMetrics.calculate_mra(rel_a)
                kekule = FamilyPathMetrics.calculate_kekule_number(Ga, Gb, rel_a, rel_b)
                father_iter = tree_store.append(parent_iter, [
                    kekule,  # Rel_id
                    "Father",  # Relation
                    name_displayer.display(father),  # Name
                    Ga,  # up
                    Gb,  # down
                    mra,  # Common MRA
                    rank,  # Rank
                    get_timeperiod(self.dbstate.db, father.get_handle())  # Period
                ])
                self.add_ancestors_to_tree_view(tree_store, father, father_iter)

            if mother_handle:
                mother = self.dbstate.db.get_person_from_handle(mother_handle)
                dist = self.relationship.get_relationship_distance_new(
                    self.dbstate.db, self.dbstate.db.get_default_person(), mother, only_birth=True)
                rank = dist[0][0]
                rel_a, rel_b = FamilyPathMetrics.extract_relationship_paths(dist)
                Ga, Gb = FamilyPathMetrics.calculate_relationship_path_lengths(rel_a, rel_b)
                mra = FamilyPathMetrics.calculate_mra(rel_a)
                kekule = FamilyPathMetrics.calculate_kekule_number(Ga, Gb, rel_a, rel_b)
                mother_iter = tree_store.append(parent_iter, [
                    kekule,  # Rel_id
                    "Mother",  # Relation
                    name_displayer.display(mother),  # Name
                    Ga,  # up
                    Gb,  # down
                    mra,  # Common MRA
                    rank,  # Rank
                    get_timeperiod(self.dbstate.db, mother.get_handle())  # Period
                ])
                self.add_ancestors_to_tree_view(tree_store, mother, mother_iter)

    def add_descendants_to_tree_view(self, tree_store, person, parent_iter):
        """
        Add descendants of a person to the TreeView.
        Args:
            tree_store: The TreeStore to add descendants to.
            person: The person to add descendants for.
            parent_iter: The parent iterator in the TreeStore.
        """
        for family_handle in person.get_family_handle_list():
            family = self.dbstate.db.get_family_from_handle(family_handle)
            for child_ref in family.get_child_ref_list():
                child = self.dbstate.db.get_person_from_handle(child_ref.get_reference_handle())
                dist = self.relationship.get_relationship_distance_new(
                    self.dbstate.db, self.dbstate.db.get_default_person(), child, only_birth=True)
                rank = dist[0][0]
                rel_a, rel_b = FamilyPathMetrics.extract_relationship_paths(dist)
                Ga, Gb = FamilyPathMetrics.calculate_relationship_path_lengths(rel_a, rel_b)
                mra = FamilyPathMetrics.calculate_mra(rel_a)
                kekule = FamilyPathMetrics.calculate_kekule_number(Ga, Gb, rel_a, rel_b)
                child_iter = tree_store.append(parent_iter, [
                    kekule,  # Rel_id
                    "Child",  # Relation
                    name_displayer.display(child),  # Name
                    Ga,  # up
                    Gb,  # down
                    mra,  # Common MRA
                    rank,  # Rank
                    get_timeperiod(self.dbstate.db, child.get_handle())  # Period
                ])
                self.add_descendants_to_tree_view(tree_store, child, child_iter)

    def on_row_activated(self, tree_view, path, column):
        """
        Handle the "row-activated" signal for the TreeView.
        Args:
            tree_view: The TreeView widget.
            path: The path of the activated row.
            column: The column of the activated row.
        """
        # Get the selected row
        model = tree_view.get_model()
        iter = model.get_iter(path)
        handle = model.get_value(iter, 2)  # Get the handle from the third column

        # Get the person from the handle
        person = self.dbstate.db.get_person_from_handle(handle)

        # Open the person's edit dialog
        if person:
            self.uistate.set_active(person.handle, 'Person')

class FamilyTreeViewOptions(MenuItem):
    """
    Options for the FamilyTreeView.
    """
    def __init__(self, name, person_id=None):
        MenuItem.__init__(self, name, person_id)
        self.options_dict = {
            'enable_network_metrics': True,  # Option pour activer les métriques de réseau
        }
        self.options_help = {
            'enable_network_metrics': (
                _("Enable family network metrics"),
                "bool",
                _("Whether to calculate and display family network metrics."),
                None,
                True
            ),
        }

# Register the new view with Gramps
register_view(FamilyTreeView, FamilyTreeViewOptions)

# ... (rest of the existing code)
| Gramps Family Tree Viewer                                                                                           |
+---------------------------------------------------------------------------------------------------------------------+
| Rel_id | Relation       | Name            | up | down | Common MRA | Rank | Period      |
|--------+----------------+-----------------+----+------+-------------+------+-------------|
| 0      | Self           | John Doe        | 0  | 0    | 0           | 0    | 1900-2000   |
| 1      | Mother         | Jane Smith      | 1  | 2    | 15          | 1    | 1870-1950   |
| 2      | Mother         | Alice Johnson   | 2  | 3    | 30          | 2    | 1840-1920   |
| 3      | Father         | Bob Brown       | 2  | 3    | 30          | 2    | 1840-1920   |
| 4      | Father         | Michael Johnson | 1  | 2    | 15          | 1    | 1870-1950   |
| 5      | Mother         | Carol White     | 2  | 3    | 30          | 2    | 1840-1920   |
| 6      | Father         | David Black     | 2  | 3    | 30          | 2    | 1840-1920   |
+---------------------------------------------------------------------------------------------------------------------+


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant