Animals Lines/Family Trees Plugin or Visualisation

Hi
I have been painstakingly tracking down ancestors of my animal breeding lines, and putting them into FarmOS parents fields. Even for animals I never owned, I added the parents and grandparents as “archived” animals, so that the names, dates and images are available to me (is this a stupid move??)

Anyway, I would love to visualise/diagram/even text format print these relationships on a family tree type deal. Is there anyone else dabbling with this, or know of a plugin or trick?

As with many of the things in my FarmOS records, they are just records, and seeing visual forms of things is impossible otherwise. This makes it hard to do complicated tracking and tracing, like breeding lines

Thanks
Marlon

3 Likes

I agree it would be great to visualize this data @marlonv! I’ve never done something like that myself, but I bet there are existing libraries that could do most of the work.

D3.js might be worth exploring: Tree | D3 by Observable

3 Likes

That seems like a very nice lib and easy to pick up for hurried folks, like myself.
If I can’t find something else, I will have a look at it.
It’s true, getting even a dirty simple photo/name node based tree would be great.

3 Likes

I’ve wanted to find/make this also! Please keep us updated if you find a good solution or make progress…

1 Like

I was just playing around with loading a library for this in Asset Link…

Didn’t write the code to load actual asset relationships yet, but this might be a start:

FamilyTree.alink.vue

<script setup>
// Hacky way to expose Vue globally for this tree chart library
import * as Vue from 'vue';
window.Vue = Vue;

// Based on https://stackoverflow.com/a/9413807
function addLibraryTag(lib) {
  var po = document.createElement('script');
  po.type = 'text/javascript';
  po.async = true;
  po.src = lib;
  var s = document.getElementsByTagName('script')[0];
  s.parentNode.insertBefore(po, s);
}

addLibraryTag("https://cdn.jsdelivr.net/npm/vue-tree-chart-3@1.2.9/dist/TreeChart3.umd.min.js");

async function waitForLibrary() {
  function sleep(millis) {
    return new Promise(resolve => setTimeout(resolve, millis));
  }
  while (true) {
    if (window['TreeChart3']) {
      return window['TreeChart3'];
    }
    await sleep(500);
  }
}

const TreeChart = await waitForLibrary();

const chartOptions = {
    name: 'root',
    image_url: "https://cdn.quasar.dev/img/avatar1.jpg",
    class: ["rootNode"],
    children: [
      {
        name: 'children1',
        image_url: "https://cdn.quasar.dev/img/avatar2.jpg"
      },
      {
        name: 'children2',
        image_url: "https://cdn.quasar.dev/img/avatar3.jpg",
        mate: [{
          name: 'mate',
          image_url: "https://cdn.quasar.dev/img/avatar4.jpg"
        }],
        children: [
          {
            name: 'grandchild',
            image_url: "https://cdn.quasar.dev/img/avatar5.jpg"
          },
          {
            name: 'grandchild2',
            image_url: "https://cdn.quasar.dev/img/avatar6.jpg"
          },
          {
            name: 'grandchild3',
            image_url: "https://cdn.quasar.dev/img/avatar.png"
          }
        ]
      },
      {
        name: 'children3',
        image_url: "https://cdn.quasar.dev/img/boy-avatar.png"
      }
    ]
  };

const nodeClicked = (item) => {
  alert("Clicked: " + item.name);
};
</script>

<template alink-route[com.example.farmos_asset_link.routes.v0.family_tree_page]="/family-tree">
  <q-page padding class="text-left">
    <TreeChart :json="chartOptions" @click-node="nodeClicked" />
  </q-page>
</template>

Obviously, a production implementation wouldn’t load the library from a CDN, but for playing around that’s good enough.

4 Likes

It’s worth noting that Asset Link actually already has some similar functionality - just not in a chart format;

2 Likes

I will have a look at Asset Link.
Currently using exported CSV from the asset list view, and seeing if I can build a usable structure. Rendering the visuals will be difficult (trees and avoiding overlaps are blackmagic in itself).
Thanks for the replies

2 Likes

This makes me wonder what it would look like if we loaded animal assets into the same code that generates the /location page. :thinking:

That uses the Inspire Tree module + JS library.

https://www.drupal.org/project/inspire_tree

http://inspire-tree.com/

It may not be exactly what we would want for lineage rendering. But it might be interesting to see how it works out of the box.

3 Likes

Starting to get excited.
My parser got me down to reading my FarmOS animals for given type, and to where I have them in on-site and ‘terminated’ or ‘sold’ parents. Drawing the lines while arranging the nodes = galdalf already walked out on me out of frustration

Thanks for the info. Looking into every possible alley
for comedic purposes, a small segment of my output from one herd.

3 Likes

We refer to animals that we never owned but have parentage in our breeding lines “Reference Animals”. This term is often used to describe a Sire who AI thousands of calves but you never owned him. You simply bought his semen and potentially a bull/breed certificate.

2 Likes

This is an Asset type I’ve added to my own instance

image

4 Likes

Would this be “Safe” to use with future updates? I’d like to add this. I made a similar module that simply added a toggle field to mark an animal as reference, and hide it from the “list” of animal assets, but yours is better suited as a type of its own.

1 Like

As with all things there is a bit of at your own risk.

But I think it is safe enough as it just makes another Asset that is an exact copy of the Animal Asset in all but name. I can’t see any issue with updates in 2.x or 3.x but at some stage if there is a need for a migration similar to what was needed to move from 1.x to 2.x then it may need some additional steps but that shouldn’t be too hard either.

If you understand how the module works then the risk is much lower.

I haven’t used it in 3.x yet, it will need to have Drupal 10 added to the core requirements in farm_reference_animal.info.yml

1 Like

This is cool @Farmer-Ed - I hadn’t seen this.

I wonder if, instead of creating a separate asset type, it just added a “Reference Animal” flag that could be applied to Animal assets. :thinking:

Or maybe there was a good reason to have a separate asset type?

If you did want to move to a flag-based approach, the v3 release could be a good time to do that. It would just need to add the new flag, migrate Reference Animal assets to Animal assets with the flag set, and then remove the Reference Animal asset type to clean up.

2 Likes

At some point if the key stakeholders expect this product to grow into something more mainstream they need to insure that migration issues are minimalized. I haven’t looked to hard into 3x. I do notice that from 1x to 2x a major transformation occurred that set up what appears to be a more future proof structure.

I thought of just using the flag and do use some custom flags but…
I like that the animal Assets are my Assets and the Reference Animals are not. I even like that all of the archived animals are animals that have been on my farm at some point, which can be useful looking back (of course I could always filter by flag too).

Having said that none of this is as important to me now as I changed from a farm with breeding stock to a calf to beef farm this year.

2 Likes

I did create a module that adds just a flag (and defaultly hides them from the asset animals view), but I agree that it’s not entirely ideal.
I think the reference animal ‘type’ is a good idea.
Right now, I have mine flagged, and marked as archived, with a note on each one saying “Terminated, Sold or Absent” - and that opens up doors to human error.

2 Likes

This could be a good use case for the Farm asset type.

3 Likes

Yes! (Although now we’re considering adding a new “Organization” entity type, of which “Farm” would be a sub-type - instead of adding a “Farm asset” type. See: Organization-level data)

With “Farm” organizations, you would be able to assign certain assets to certain farms, and filter by farm when you’re looking at them. So a “Reference animal” flag would allow filtering to see ONLY reference animals, and the Farm organization would allow filtering to see ONLY your animals (active or archived).

I’m working on the code for the organization entity type as we speak… with any luck we’ll be able to include it with the next minor release of farmOS! :crossed_fingers: :rocket:

4 Likes

FamilyTree2.alink.vue

<script setup>
// Hacky way to expose Vue globally for this tree chart library
import * as Vue from 'vue';
window.Vue = Vue;

import { inject, ref, watchEffect } from 'vue';
import { useRouter } from 'vue-router'
import { createDrupalUrl } from "assetlink-plugin-api";

const router = useRouter();

const assetLink = inject('assetLink');

// Based on https://stackoverflow.com/a/9413807
function addLibraryTag(lib) {
  var po = document.createElement('script');
  po.type = 'text/javascript';
  po.async = true;
  po.src = lib;
  var s = document.getElementsByTagName('script')[0];
  s.parentNode.insertBefore(po, s);
}

addLibraryTag("https://cdn.jsdelivr.net/npm/vue-tree-chart-3@1.2.9/dist/TreeChart3.umd.min.js");

function sleep(millis) {
  return new Promise(resolve => setTimeout(resolve, millis));
}

async function waitForLibrary() {
  while (true) {
    if (window['TreeChart3']) {
      return window['TreeChart3'];
    }
    await sleep(500);
  }
}

const TreeChart = await waitForLibrary();

const props = defineProps({
  assetRef: {
    type: String,
    required: true,
  },
});

const chartOptions = ref(null);

// From https://pictogrammers.com/library/mdi/icon/help-box-outline/
const NO_IMAGE_PLACEHOLDER = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M11 18H13V16H11V18M12 6C9.8 6 8 7.8 8 10H10C10 8.9 10.9 8 12 8S14 8.9 14 10C14 12 11 11.8 11 15H13C13 12.8 16 12.5 16 10C16 7.8 14.2 6 12 6M19 5V19H5V5H19M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3Z' /%3E%3C/svg%3E";

const resolveImageUrl = async (imgRef, node) => {
  try {
    let entity = await assetLink.entitySource.cache.query(
        (q) => q.findRecord({ type: imgRef.type, id: imgRef.id }));

    if (entity) {
      node.image_url = createDrupalUrl(entity.attributes.uri.url).toString()
    }

    entity = await assetLink.entitySource.query(
        (q) => q.findRecord({ type: imgRef.type, id: imgRef.id }));

    node.image_url = createDrupalUrl(entity.attributes.uri.url).toString()
  } catch (e) {
    assetLink.vm.messages.push({text: `Failed to load asset image entity: ${e.message}`, type: "error"});
    console.log(e);
  }
};

const loadImage = (asset, node) => {
  node.image_url = NO_IMAGE_PLACEHOLDER;

  const imageRefs = asset.relationships?.image?.data || [];

  if (imageRefs.length) {
    resolveImageUrl(imageRefs[0], node);
  }

  return node;
};

const resolveParents = async (ofAsset) => {
  const results = await assetLink.entitySource.query(q =>
    q.findRelatedRecords({ type: ofAsset.type, id: ofAsset.id }, 'parent'));

  return results.flatMap(l => l);
};

const resolveChildren = async (ofAsset) => {
  const assetTypes = (await assetLink.getAssetTypes()).map(t => t.attributes.drupal_internal__id);

  const results = await assetLink.entitySource.query(q => assetTypes.map(assetType => {
    return q.findRecords(`asset--${assetType}`)
      .filter({
        relation: 'parent.id',
        op: 'some',
        records: [{ type: ofAsset.type, id: ofAsset.id }]
      })
      .sort('-created');
  }));

  return results.flatMap(l => l);
};
  
const createNode = (asset, otherAttrs) => loadImage(asset, {
  // The chart library doesn't seem to handle spaces in names very well.
  // For this demo, just replace them with dots
  name: asset.attributes.name.replace(' ', ''),
  // Include the drupal_internal__id so we can navigate between assets by
  // clicking on the chart
  drupal_internal__id: asset.attributes.drupal_internal__id,

  ...(otherAttrs || {}),
});

watchEffect(async () => {
  const thisAsset = await assetLink.resolveEntity('asset', props.assetRef);

  const parents = await resolveParents(thisAsset);

  const children = await resolveChildren(thisAsset);
  
  const thisAssetNode = createNode(thisAsset, {
    class: ["thisAssetNode"],
    children: children.map(child => createNode(child)),
  });
  
  // Make the root of the chart either the current asset or its parents
  if (!parents.length) {
    chartOptions.value = thisAssetNode;
  } else {
    const firstParent = parents[0];

    chartOptions.value = createNode(firstParent, {
      mate: parents.slice(1, parents.length).map(otherParent => createNode(otherParent)),
      children: [
		thisAssetNode,
      ]
    });
  }


});

const nodeClicked = (item) => {
  router.push(`/asset/${item.drupal_internal__id}/family-tree`);
};

// Scroll the chart to center it horizontally each time the chart contents changes
const page = ref(null);
const nodeObserver = ref(null);
watchEffect(() => {
  if (nodeObserver.value) {
    nodeObserver.value.disconnect();
  }

  if (!page.value || !page.value.$el) {
    return;
  }

  nodeObserver.value = new MutationObserver((mutationList, observer) => {
    const nodeToCenter = page.value.$el.querySelector('.thisAssetNode');

    if (nodeToCenter) {
      nodeToCenter.scrollIntoView({inline: 'center'});
    }
  });

  nodeObserver.value.observe(page.value.$el, { attributes: false, childList: true, subtree: true });
});
</script>

<template alink-route[com.example.farmos_asset_link.routes.v0.asset_family_tree_page]="/asset/:assetRef/family-tree">
  <q-page padding class="text-left column items-center" ref="page">
    <div class="col q-mt-xl" v-if="chartOptions">
      <TreeChart :json="chartOptions" @click-node="nodeClicked" />
    </div>
    <q-inner-loading :showing="!chartOptions">
        <q-spinner-ball
          color="primary"
          size="100px"
        />
    </q-inner-loading>
  </q-page>
</template>

Update: Made the chart scroll to center horizontally and did a bit of cleanup/commenting to make things clearer.

6 Likes