diff --git a/package-lock.json b/package-lock.json
index 500c90fe4079e3689f53f6ff0b836cd8e4847dc7..1fb0b91693fa9d26ef328518fef4f2249ffd670b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
         "@fortawesome/fontawesome-svg-core": "^6.4.0",
         "@fortawesome/free-solid-svg-icons": "^6.4.0",
         "@fortawesome/vue-fontawesome": "^3.0.3",
+        "@svgdotjs/svg.js": "^3.2.0",
         "@vueuse/core": "^10.4.1",
         "axios": "^1.4.0",
         "lodash-es": "^4.17.21",
@@ -927,6 +928,15 @@
       "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
       "dev": true
     },
+    "node_modules/@svgdotjs/svg.js": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.0.tgz",
+      "integrity": "sha512-Tr8p+QVP7y+QT1GBlq1Tt57IvedVH8zCPoYxdHLX0Oof3a/PqnC/tXAkVufv1JQJfsDHlH/UrjcDfgxSofqSNA==",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Fuzzyma"
+      }
+    },
     "node_modules/@tootallnate/once": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@@ -1020,9 +1030,9 @@
       "dev": true
     },
     "node_modules/@types/web-bluetooth": {
-      "version": "0.0.17",
-      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.17.tgz",
-      "integrity": "sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA=="
+      "version": "0.0.20",
+      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+      "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
     },
     "node_modules/@types/yauzl": {
       "version": "2.10.0",
@@ -1587,14 +1597,14 @@
       }
     },
     "node_modules/@vueuse/core": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.4.1.tgz",
-      "integrity": "sha512-DkHIfMIoSIBjMgRRvdIvxsyboRZQmImofLyOHADqiVbQVilP8VVHDhBX2ZqoItOgu7dWa8oXiNnScOdPLhdEXg==",
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.6.0.tgz",
+      "integrity": "sha512-+Yee+g9+9BEbvkyGdn4Bf4yZx9EfocAytpV2ZlrlP7xcz+qznLmZIDqDroTvc5vtMkWZicisgEv8dt3+jL+HQg==",
       "dependencies": {
-        "@types/web-bluetooth": "^0.0.17",
-        "@vueuse/metadata": "10.4.1",
-        "@vueuse/shared": "10.4.1",
-        "vue-demi": ">=0.14.5"
+        "@types/web-bluetooth": "^0.0.20",
+        "@vueuse/metadata": "10.6.0",
+        "@vueuse/shared": "10.6.0",
+        "vue-demi": ">=0.14.6"
       },
       "funding": {
         "url": "https://github.com/sponsors/antfu"
@@ -1626,19 +1636,19 @@
       }
     },
     "node_modules/@vueuse/metadata": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.4.1.tgz",
-      "integrity": "sha512-2Sc8X+iVzeuMGHr6O2j4gv/zxvQGGOYETYXEc41h0iZXIRnRbJZGmY/QP8dvzqUelf8vg0p/yEA5VpCEu+WpZg==",
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.6.0.tgz",
+      "integrity": "sha512-mzKHkHoiK6xVz01VzQjM2l6ofUanEaofgEGPgDHcAzlvOTccPRTIdEuzneOUTYxgfm1vkDikS6rtrEw/NYlaTQ==",
       "funding": {
         "url": "https://github.com/sponsors/antfu"
       }
     },
     "node_modules/@vueuse/shared": {
-      "version": "10.4.1",
-      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.4.1.tgz",
-      "integrity": "sha512-vz5hbAM4qA0lDKmcr2y3pPdU+2EVw/yzfRsBdu+6+USGa4PxqSQRYIUC9/NcT06y+ZgaTsyURw2I9qOFaaXHAg==",
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.6.0.tgz",
+      "integrity": "sha512-0t4MVE18sO+/4Gh0jfeOXBTjKeV4606N9kIrDOLPjFl8Rwnlodn+QC5A4LfJuysK7aOsTMjF3KnzNeueaI0xlQ==",
       "dependencies": {
-        "vue-demi": ">=0.14.5"
+        "vue-demi": ">=0.14.6"
       },
       "funding": {
         "url": "https://github.com/sponsors/antfu"
diff --git a/package.json b/package.json
index 7a616973b29c58ae17ef50588034709d497a0017..ed13830fb34eeae4c542f208b98c36b32a887d3f 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
     "@fortawesome/fontawesome-svg-core": "^6.4.0",
     "@fortawesome/free-solid-svg-icons": "^6.4.0",
     "@fortawesome/vue-fontawesome": "^3.0.3",
+    "@svgdotjs/svg.js": "^3.2.0",
     "@vueuse/core": "^10.4.1",
     "axios": "^1.4.0",
     "lodash-es": "^4.17.21",
diff --git a/src/components/BaseLinkListCard.vue b/src/components/BaseLinkListCard.vue
new file mode 100644
index 0000000000000000000000000000000000000000..619790104a3b47230f2410c62e822b34f4bdbd24
--- /dev/null
+++ b/src/components/BaseLinkListCard.vue
@@ -0,0 +1,147 @@
+<script setup lang="ts">
+/**
+ * Vue imports
+ */
+import { onMounted, ref } from 'vue'
+/**
+ * Components imports
+ */
+import Card from 'primevue/card'
+/**
+ * Other 3rd-party imports
+ */
+import { compact as _compact } from 'lodash-es'
+/**
+ * Types imports
+ */
+import type { TailwindBaseColorModel } from '@/typings/styleTypes'
+import type { RouteParamsRaw } from 'vue-router'
+
+export interface LinkListItemModel {
+  id: string
+  title: string
+  subtitle?: string
+  link: {
+    name: string
+    params?: RouteParamsRaw
+  }
+  details?: [string, string?, string?]
+}
+
+interface PropsModel {
+  listTitle?: string
+  linkItems: LinkListItemModel[]
+  color?: TailwindBaseColorModel
+  disabled?: boolean
+  hoverEvent?: boolean
+}
+
+const props = defineProps<PropsModel>()
+
+const emit = defineEmits<{
+  enterItem: [id: string]
+  leaveItem: [id: string]
+}>()
+
+const listContainer = ref<HTMLDivElement | null>(null)
+// const listContainerScroll = useScroll(listContainer, { behavior: 'smooth' })
+const linkItemWrappers = ref<HTMLDivElement[] | null>(null)
+
+onMounted(() => {
+  if (props.hoverEvent) {
+    linkItemWrappers.value?.forEach((linkItemWrapper) => {
+      linkItemWrapper.addEventListener('mouseenter', () => {
+        emit('enterItem', linkItemWrapper.dataset['linkItemId'] || '')
+      })
+      linkItemWrapper.addEventListener('mouseleave', () => {
+        emit('leaveItem', linkItemWrapper.dataset['linkItemId'] || '')
+      })
+    })
+  }
+})
+
+const scrollToListTop = () => {
+  // listContainerScroll.x.value = 0
+  setTimeout(() => {
+    listContainer.value?.scrollTo({ top: 0, behavior: 'smooth' })
+  }, 10)
+}
+
+defineExpose({ scrollToListTop })
+</script>
+
+<template>
+  <Card
+    :class="['!border', `!border-${color || 'slate'}-600`]"
+    :pt="{
+      body: { class: '!pr-0' },
+      content: {
+        class: '!py-0'
+      }
+    }"
+  >
+    <template v-if="listTitle" #title>
+      <h3 class="mb-4 text-center">{{ listTitle }}</h3>
+    </template>
+    <template v-if="!disabled" #content>
+      <div
+        ref="listContainer"
+        class="!mr-2 flex max-h-[65vh] flex-col gap-4 overflow-auto !pr-3"
+      >
+        <RouterLink
+          v-for="(linkItem, linkItemIndex) in linkItems"
+          :key="linkItemIndex"
+          :class="[
+            `hover:border-${color || 'slate'}-600`,
+            'rounded-md border-2 hover:shadow-md'
+          ]"
+          :to="{
+            name: linkItem.link.name,
+            params: linkItem.link.params
+          }"
+        >
+          <div
+            :ref="hoverEvent ? 'linkItemWrappers' : ''"
+            :data-link-item-id="linkItem.id"
+            :class="[
+              'grid p-2',
+              linkItem.details ? 'grid-cols-5' : 'grid-cols-1'
+            ]"
+          >
+            <div :class="['flex flex-col', linkItem.details && 'border-r-2']">
+              <h4 class="px-2 text-center text-lg font-semibold">
+                {{ linkItem.title }}
+              </h4>
+              <p
+                v-if="linkItem.subtitle"
+                class="text-center italic text-slate-400"
+              >
+                {{ linkItem.subtitle }}
+              </p>
+            </div>
+            <div
+              v-if="linkItem.details"
+              class="col-span-4 my-auto pl-4 text-xl text-slate-400"
+            >
+              {{ linkItem.details[0] }}
+              <template
+                v-for="(detail, detailIndex) in _compact(
+                  linkItem.details.slice(1)
+                )"
+                :key="detailIndex"
+              >
+                <span class="mx-2">•</span>
+                {{ detail + ' ' }}
+              </template>
+            </div>
+          </div>
+        </RouterLink>
+      </div>
+    </template>
+    <template v-else #content>
+      <span class="m-auto italic text-slate-400">
+        <slot name="disabled"> Link list is disabled. </slot>
+      </span>
+    </template>
+  </Card>
+</template>
diff --git a/src/components/BaseRadioFieldset.vue b/src/components/BaseRadioFieldset.vue
index 0ffae1bd9f1e1fa0c6ad72390e194273d0c5c1a8..b6dd0661b431a6271fdb5c0171634d21489c739b 100644
--- a/src/components/BaseRadioFieldset.vue
+++ b/src/components/BaseRadioFieldset.vue
@@ -19,10 +19,6 @@ interface PropsModel {
   labelFieldPath?: string
 }
 
-interface EmitModel {
-  change: [value: any]
-}
-
 interface SlotModel {
   /** Custom whole entry (input & label). */
   default(props: {
diff --git a/src/components/BaseTooltip.vue b/src/components/BaseTooltip.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a33f045505a57c88c8b6cd4568a9558179a863e9
--- /dev/null
+++ b/src/components/BaseTooltip.vue
@@ -0,0 +1,95 @@
+<script setup lang="ts">
+/**
+ * Vue imports
+ */
+import { computed } from 'vue'
+/**
+ * Types imports
+ */
+import type { StyleValue } from 'vue'
+
+interface PropsModel {
+  /** On which side to display the 'arrow' of the tooltip. */
+  side?: 'left' | 'right' | 'top' | 'bottom'
+  /** Whether to display or not the 'arrow' of the tooltip.  */
+  showArrow?: boolean
+}
+
+const props = withDefaults(defineProps<PropsModel>(), {
+  side: 'bottom',
+  showArrow: true
+})
+
+const tooltipStyle = computed<StyleValue>(() => {
+  const translateX =
+    props.side === 'bottom' || props.side === 'top'
+      ? '-50%'
+      : props.side === 'left'
+      ? '0.75rem'
+      : 'calc(-100% - 0.75rem)'
+  const translateY =
+    props.side === 'left' || props.side === 'right'
+      ? '50%'
+      : props.side === 'bottom'
+      ? '-0.75rem'
+      : 'calc(100% + 0.75rem)'
+  return {
+    transform: `translateX(${translateX}) translateY(${translateY})`
+  }
+})
+
+const tooltipAfterStyle = computed(() => {
+  const top =
+    props.side === 'left' || props.side === 'right'
+      ? '50%'
+      : props.side === 'bottom'
+      ? ''
+      : '0'
+  const bottom = props.side === 'bottom' && '0'
+  const left = props.side === 'left' && '0'
+  const right = props.side === 'right' && '0'
+  const translateX =
+    props.side === 'left' ? '-50%' : props.side === 'right' ? '50%' : '0'
+  const translateY = props.side === 'bottom' ? '50%' : '-50%'
+  const rotate =
+    props.side === 'bottom'
+      ? '45deg'
+      : props.side === 'left'
+      ? '135deg'
+      : props.side === 'top'
+      ? '225deg'
+      : '315deg'
+  return {
+    top,
+    bottom,
+    left,
+    right,
+    transform: `translateX(${translateX}) translateY(${translateY}) rotate(${rotate})`
+  }
+})
+</script>
+
+<template>
+  <div
+    :class="[
+      'tooltip fixed flex flex-col items-center rounded border bg-white p-2 transition-all duration-500',
+      showArrow &&
+        'after:absolute after:h-4 after:w-4 after:rounded-sm after:border-b after:border-r after:bg-white'
+    ]"
+    :style="tooltipStyle"
+  >
+    <slot></slot>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.tooltip {
+  &::after {
+    top: v-bind('tooltipAfterStyle.top');
+    bottom: v-bind('tooltipAfterStyle.bottom');
+    left: v-bind('tooltipAfterStyle.left');
+    right: v-bind('tooltipAfterStyle.right');
+    transform: v-bind('tooltipAfterStyle.transform');
+  }
+}
+</style>
diff --git a/src/components/ChromosomeMagnify.vue b/src/components/ChromosomeMagnify.vue
new file mode 100644
index 0000000000000000000000000000000000000000..aec0d7c92eb022ffc771d3bd0ff5d51b721590ec
--- /dev/null
+++ b/src/components/ChromosomeMagnify.vue
@@ -0,0 +1,478 @@
+<script setup lang="ts">
+/**
+ * Vue imports
+ */
+import { computed, onMounted, ref } from 'vue'
+/**
+ * Components imports
+ */
+import BaseTooltip from './BaseTooltip.vue'
+/**
+ * Other 3rd-party imports
+ */
+import {
+  onClickOutside,
+  useMouseInElement
+} from '@vueuse/core'
+import { G, Rect, SVG, type Svg } from '@svgdotjs/svg.js'
+/**
+ * Types imports
+ */
+import type { ChromosomeModel, GenomeObjectModel } from './KaryotypeBoard.vue'
+
+interface propsModel {
+  // The chromosome to display
+  chromosome: ChromosomeModel
+  // The DOM element corresponding to the chromosome
+  chromosomeElement: HTMLElement | SVGElement
+  // A list of objects to display on the chromosome
+  objects?: GenomeObjectModel[]
+  // The zoom factor to apply
+  zoomFactor?: number
+  // The orientation of the chromosome
+  orientation?: 'horizontal' | 'vertical'
+  // The radius of the 'magnifying glass'
+  radius?: number
+}
+
+const props = withDefaults(defineProps<propsModel>(), {
+  objects: () => [],
+  zoomFactor: 10,
+  orientation: 'horizontal',
+  radius: 50
+})
+
+/**
+ * Dimensions of the base chromosome element, position of the mouse relative to
+ * the top left of the client, and to the chromosome element
+ */
+const { chromosomeDimensions, mouseClient, mouseChromosome } = (() => {
+  const { elementHeight, elementWidth, elementX, elementY, isOutside, x, y } =
+    useMouseInElement(props.chromosomeElement, {
+      type: 'client'
+    })
+  return {
+    chromosomeDimensions: {
+      height: elementHeight,
+      width: elementWidth
+    },
+    mouseClient: {
+      x: x,
+      y: y
+    },
+    mouseChromosome: {
+      x: elementX,
+      y: elementY,
+      isOutside
+    }
+  }
+})()
+
+/**
+ * Ref to the div containing the chromosome SVG
+ */
+const magnifiedChromosome = ref<HTMLDivElement>()
+
+/**
+ * Position of the magnify div relative to the viewport
+ */
+const magnifyElementPositions = computed(() => ({
+  top: mouseClient.y.value,
+  left: mouseClient.x.value
+}))
+/**
+ * Location of the tooltip when locked in place, undefined if not locked
+ */
+const lockedTooltipPosition = ref<{ bottom: number; left: number }>()
+const lockOffset = ref(0)
+
+/**
+ * DOM elements of objects currently hovered
+ */
+const hoveredObjectsElements = ref<SVGElement[]>([])
+
+/**
+ * DOM elements corresponding to objects to display in the tooltip when locked
+ */
+const lockedTooltipObjectsElements = ref<SVGElement[]>([])
+
+/**
+ * DOM elements corresponding to objects to display in the tooltip
+ */
+const tooltipObjectsElements = computed(() =>
+  lockedTooltipPosition.value
+    ? lockedTooltipObjectsElements.value
+    : hoveredObjectsElements.value
+)
+
+/**
+ * Lock the tooltip in its place and add a scroll listener
+ */
+const lockTooltipIfUnlocked = () => {
+  if (lockedTooltipPosition.value) return
+  lockedTooltipObjectsElements.value = hoveredObjectsElements.value
+  // Set locked position
+  lockedTooltipPosition.value = {
+    bottom: tooltipComponentPositions.value.bottom,
+    left: tooltipComponentPositions.value.left
+  }
+  // Save lock offset
+  lockOffset.value = magnifiedChromosomeTranslate.value.x
+  // Add scroll event listener to unlock if scroll
+  document.addEventListener('scroll', unlockTooltipIfLocked)
+}
+
+/**
+ * Unlock the tooltip when clicking out of it
+ */
+const unlockTooltipIfLocked = () => {
+  if (!lockedTooltipPosition.value) return
+  // Reset position
+  lockedTooltipPosition.value = undefined
+  // Remove scroll event listener
+  document.removeEventListener('scroll', unlockTooltipIfLocked)
+}
+
+/**
+ * Ref to the tooltip component
+ */
+const tooltipComponent = ref()
+
+/**
+ * Position of the tooltip component relative to the viewport
+ */
+const tooltipComponentPositions = computed(() => ({
+  bottom:
+    lockedTooltipPosition.value?.bottom ||
+    window.innerHeight - mouseClient.y.value + 10,
+  // bottom: objectsGroupTop.value.value - mouseClient.y.value,
+  left: lockedTooltipPosition.value?.left || mouseClient.x.value
+}))
+
+/**
+ * Normalised position of the mouse on the chromosome (between 0 & 1)
+ */
+const mousePositionNormalised = computed(() => ({
+  x: mouseChromosome.x.value / chromosomeDimensions.width.value,
+  y: mouseChromosome.y.value / chromosomeDimensions.height.value
+}))
+
+/**
+ * Position of the mouse in terms of chromosome coordinate
+ */
+const mousePositionChromosomeCoordinate = computed(() =>
+  Math.round(mousePositionNormalised.value.x * props.chromosome.length)
+)
+
+/**
+ * Length (in bp) of the magnified portion, w/ respect to the zoom factor
+ */
+const magnifiedPortionLengthBP = computed(() =>
+  Math.round(
+    ((props.radius / chromosomeDimensions.width.value) *
+      props.chromosome.length) /
+      props.zoomFactor
+  )
+)
+
+/**
+ * Coordinates of the magnified chromosome portion
+ */
+const magnifiedPortionCoordinates = computed(() => ({
+  start:
+    mousePositionChromosomeCoordinate.value -
+    Math.floor(magnifiedPortionLengthBP.value / 2),
+  end:
+    mousePositionChromosomeCoordinate.value +
+    Math.floor(magnifiedPortionLengthBP.value / 2)
+}))
+
+/**
+ * Dimensions (in px) of the entire magnified chromosome
+ */
+const magnifiedChromosomeDimensionsPX = computed(() => ({
+  length: chromosomeDimensions.width.value * props.zoomFactor,
+  thickness: 1.5 * props.radius
+}))
+
+/**
+ * Translation applied to the magnified chromosome
+ */
+const magnifiedChromosomeTranslate = computed(() => ({
+  x:
+    -mousePositionNormalised.value.x *
+      magnifiedChromosomeDimensionsPX.value.length +
+    props.radius,
+  y:
+    (0.5 - mousePositionNormalised.value.y + 0.25) *
+    magnifiedChromosomeDimensionsPX.value.thickness
+}))
+
+/**
+ * Ref to the SVG representation of the chromosome
+ */
+const draw = ref<Svg>()
+
+/**
+ * Creates the elements forming the chromosome SVG
+ */
+const createChromosomeSVG = () => {
+  if (!magnifiedChromosome.value) {
+    console.log('Element not present')
+    return
+  }
+
+  draw.value = SVG().addTo(magnifiedChromosome.value)
+
+  // Create the body of the chromosome, made of the 2 arms
+  const mainGroup = draw.value
+    .group()
+    .fill(props.chromosome.color || '#84b650')
+    .addClass('zoom-chr-svg-main')
+  mainGroup.rect().addClass('zoom-chr-svg-main-short-arm')
+  mainGroup.rect().addClass('zoom-chr-svg-main-long-arm')
+
+  // Create and fill a group with the objects present on the chromosome
+  const objectsGroup = draw.value
+    .group()
+    .addClass('fill-[#abcf74]')
+    .addClass('zoom-chr-svg-objects')
+  props.objects.forEach((object) => {
+    const rect = objectsGroup.rect().data(object)
+    if (object.color) {
+      rect.fill(object.color)
+    }
+    rect.addClass('cursor-pointer')
+  })
+  objectsGroup.mousemove((e: MouseEvent) => {
+    // Get current hovered object elements
+    const hoveredObjectsElementsNew = document
+      .elementsFromPoint(e.clientX, e.clientY)
+      .filter((objectElement) =>
+        objectElement.parentElement?.classList.contains('zoom-chr-svg-objects')
+      ) as SVGElement[]
+
+    // Remove highlight on no longer hovered elements
+    hoveredObjectsElements.value.forEach((objectElement) => {
+      if (
+        !hoveredObjectsElementsNew.find(
+          (objectElementNew) =>
+            objectElementNew.dataset.id === objectElement.dataset.id
+        )
+      ) {
+        objectElement.setAttribute(
+          'class',
+          (objectElement.getAttribute('class') || '').replace(
+            ' brightness-90',
+            ''
+          )
+        )
+      }
+    })
+    // Add highlight on newly hovered elements
+    hoveredObjectsElementsNew.forEach((objectElementNew) => {
+      if (
+        !hoveredObjectsElements.value.find(
+          (objectElement) =>
+            objectElement.dataset.id === objectElementNew.dataset.id
+        )
+      ) {
+        objectElementNew.setAttribute(
+          'class',
+          `${objectElementNew.getAttribute('class') || ''} brightness-90`
+        )
+      }
+    })
+    hoveredObjectsElements.value = hoveredObjectsElementsNew
+  })
+  objectsGroup.mouseleave(() => {
+    hoveredObjectsElements.value.forEach((objectElement) => {
+      objectElement.setAttribute(
+        'class',
+        (objectElement.getAttribute('class') || '').replace(
+          ' brightness-90',
+          ''
+        )
+      )
+    })
+    hoveredObjectsElements.value = []
+  })
+
+  objectsGroup.click(lockTooltipIfUnlocked)
+
+  // Create the clip of the object's group, by cloning the chromosome body,
+  // and apply the clip
+  const clipGroup = draw.value.clip()
+  for (const path of mainGroup.clone().children()) {
+    clipGroup.add(path)
+  }
+  objectsGroup.clipWith(clipGroup)
+}
+
+/**
+ * Updates the size & position of the elements in the chromosome SVG
+ */
+const updateChromosomeSVG = () => {
+  if (!draw.value) {
+    return
+  }
+  // 1. Compute dimensions
+  const chromosomeDrawSize = {
+    height: magnifiedChromosomeDimensionsPX.value.length,
+    width: magnifiedChromosomeDimensionsPX.value.thickness
+  }
+  const ONE_BP_LENGTH_PX = chromosomeDrawSize.height / props.chromosome.length
+  // A tenth of the width of the chromosomes is added to arm's length to allow
+  // their superposition w/o losing the real length
+  const shortArmHeight =
+    ONE_BP_LENGTH_PX * (props.chromosome.centromereCenter || 0) +
+    chromosomeDrawSize.width / 10
+  const longArmHeight =
+    ONE_BP_LENGTH_PX *
+      (props.chromosome.length - (props.chromosome.centromereCenter || 0)) +
+    chromosomeDrawSize.width / 10
+  // If horizontal, chromosome will be rotated so the SVG height equals the
+  // chromosome width and vice-versa
+  const SVGSize =
+    props.orientation === 'vertical'
+      ? {
+          height: chromosomeDrawSize.height,
+          width: chromosomeDrawSize.width
+        }
+      : {
+          height: chromosomeDrawSize.width,
+          width: chromosomeDrawSize.height
+        }
+  // Resize the SVG
+  draw.value.size(SVGSize.width, SVGSize.height)
+
+  // 2. Retrieve SVG objects
+  // Casting is needed for Typescript to work, so the notation is a bit verbose
+  const chrMainGroup = draw.value.findOne('g.zoom-chr-svg-main') as G
+  const shortArmRect = draw.value.findOne(
+    'rect.zoom-chr-svg-main-short-arm'
+  ) as Rect
+  const longArmRect = draw.value.findOne(
+    'rect.zoom-chr-svg-main-long-arm'
+  ) as Rect
+  const objectsGroup = draw.value.findOne('g.zoom-chr-svg-objects') as G
+  const objectsGroupClipperShortArmRect = objectsGroup
+    .clipper()
+    .findOne('rect.zoom-chr-svg-main-short-arm') as Rect
+  const objectsGroupClipperLongArmRect = objectsGroup
+    .clipper()
+    .findOne('rect.zoom-chr-svg-main-long-arm') as Rect
+
+  // 3. Base operations (chromosome is vertical, centered, and text horizontal)
+  // Chromosome arms
+  shortArmRect
+    .size(chromosomeDrawSize.width, shortArmHeight)
+    .radius(chromosomeDrawSize.width / 2)
+    .cx(SVGSize.width / 2)
+  longArmRect
+    .size(chromosomeDrawSize.width, longArmHeight)
+    .radius(chromosomeDrawSize.width / 2)
+    .cx(SVGSize.width / 2)
+    .y(shortArmHeight - chromosomeDrawSize.width / 5)
+  // Chromosome objects
+  objectsGroup.children().forEach((objectRect) => {
+    const objectSide = objectRect.data('side') || 'left'
+    const objectDimensions = {
+      width: chromosomeDrawSize.width / 2,
+      height: Math.max(
+        (objectRect.data('end') - objectRect.data('start')) * ONE_BP_LENGTH_PX,
+        4 * props.zoomFactor
+      ),
+      x:
+        objectSide === 'left'
+          ? SVGSize.width / 2 -
+            chromosomeDrawSize.width / 2 -
+            chromosomeDrawSize.width / 10
+          : SVGSize.width / 2 + chromosomeDrawSize.width / 10,
+      y: objectRect.data('start') * ONE_BP_LENGTH_PX
+    }
+    ;(objectRect as Rect)
+      .move(objectDimensions.x, objectDimensions.y)
+      .size(objectDimensions.width, objectDimensions.height)
+      .radius(2)
+  })
+  // Clip path
+  objectsGroupClipperShortArmRect
+    .size(chromosomeDrawSize.width, shortArmHeight)
+    .radius(chromosomeDrawSize.width / 2)
+    .cx(SVGSize.width / 2)
+  objectsGroupClipperLongArmRect
+    .size(chromosomeDrawSize.width, longArmHeight)
+    .radius(chromosomeDrawSize.width / 2)
+    .cx(SVGSize.width / 2)
+    .y(shortArmHeight - chromosomeDrawSize.width / 5)
+
+  // 4. Transforms (orientation-specific)
+  const groupsTransforms =
+    props.orientation === 'vertical'
+      ? {
+          rotate: 0,
+          origin: { x: 0, y: 0 },
+          translateX: 0
+        }
+      : {
+          rotate: -90,
+          origin: { x: SVGSize.width / 2, y: SVGSize.height / 2 },
+          translateX: -SVGSize.width / 2 + SVGSize.height / 2
+        }
+  // Main group (chr arms)
+  chrMainGroup.transform(groupsTransforms)
+  // Objects group
+  objectsGroup.transform(groupsTransforms)
+}
+
+onMounted(() => {
+  createChromosomeSVG()
+  updateChromosomeSVG()
+})
+
+// Add a listener to unlock the tooltip when clicking outside of it
+onClickOutside(tooltipComponent, unlockTooltipIfLocked)
+</script>
+
+<template>
+  <div
+    v-show="!mouseChromosome.isOutside.value"
+    class="fixed -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-full border bg-white shadow-lg"
+    :style="{
+      height: `${radius * 2}px`,
+      width: `${radius * 2}px`,
+      top: `${magnifyElementPositions.top}px`,
+      left: `${magnifyElementPositions.left}px`
+    }"
+  >
+    <div
+      ref="magnifiedChromosome"
+      :style="{
+        transform: `translateY(${magnifiedChromosomeTranslate.y}px) translateX(${magnifiedChromosomeTranslate.x}px)`
+      }"
+    ></div>
+  </div>
+  <BaseTooltip
+    v-show="hoveredObjectsElements.length || lockedTooltipPosition"
+    ref="tooltipComponent"
+    :style="{
+      bottom: `${tooltipComponentPositions.bottom}px`,
+      left: `${tooltipComponentPositions.left}px`
+    }"
+  >
+    <div class="flex flex-col gap-2">
+      <RouterLink
+        v-for="objectElement in tooltipObjectsElements"
+        :key="objectElement.dataset.id"
+        :to="{
+          name: 'guideDetails',
+          params: { id: objectElement.dataset.id }
+        }"
+        class="font-bold underline hover:text-sky-600"
+      >
+        {{ objectElement.dataset.name }}
+      </RouterLink>
+    </div>
+  </BaseTooltip>
+</template>
diff --git a/src/components/KaryotypeBoard.vue b/src/components/KaryotypeBoard.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b63d4188d4eba7e8228315bb6303c2dddf37eea8
--- /dev/null
+++ b/src/components/KaryotypeBoard.vue
@@ -0,0 +1,499 @@
+<script setup lang="ts">
+/**
+ * Vue imports
+ */
+import { computed, onMounted, ref } from 'vue'
+/**
+ * Components imports
+ */
+import Button from 'primevue/button'
+import IconTablerCircleArrowLeft from '~icons/tabler/circle-arrow-left'
+import IconTablerCircleArrowLeftFilled from '~icons/tabler/circle-arrow-left-filled'
+/**
+ * Other 3rd-party imports
+ */
+import { SVG, Svg } from '@svgdotjs/svg.js'
+import { find as _find } from 'lodash-es'
+import { useElementBounding, useWindowScroll } from '@vueuse/core'
+/**
+ * Types imports
+ */
+import type { G } from '@svgdotjs/svg.js'
+import type { Rect } from '@svgdotjs/svg.js'
+import type { Text } from '@svgdotjs/svg.js'
+import type { HexColorCodeModel } from '@/typings/styleTypes'
+import ChromosomeMagnify from './ChromosomeMagnify.vue'
+
+export interface ChromosomeModel {
+  name: string
+  id: string
+  length: number
+  centromereCenter: number
+  color?: HexColorCodeModel
+  textLeft?: string
+  textRight?: string
+}
+
+export interface GenomeObjectModel {
+  name: string
+  id: string
+  chromosomeId: string
+  start: number
+  end: number
+  color?: HexColorCodeModel
+  highlightColor?: HexColorCodeModel
+  side?: 'left' | 'right'
+}
+
+interface PropsModel {
+  /** Chromosomes to be displayed */
+  chromosomes: ChromosomeModel[]
+  /** Objects to display on the chromosomes */
+  objects?: GenomeObjectModel[]
+}
+
+interface DrawSizesModel {
+  thickness: number
+  length: number
+}
+
+const props = defineProps<PropsModel>()
+
+const windowScroll = useWindowScroll({ behavior: 'smooth' })
+
+const boardContent = ref<HTMLDivElement>()
+const chromosomeElements = ref<{ [k: string]: SVGGElement }>({})
+const boardContentBounding = useElementBounding(boardContent)
+const remToPixelsRatio = computed(() =>
+  parseInt(window.getComputedStyle(boardContent.value!).fontSize)
+)
+
+const LONGEST_CHROMOSOME_LENGTH = 300
+const expandedChromosomeLength = computed(() =>
+  Math.min(2 * LONGEST_CHROMOSOME_LENGTH, boardContentBounding.width.value)
+)
+
+const longestChromosome = computed(() =>
+  props.chromosomes.reduce(
+    (acc, curr) => (curr.length > acc.length ? curr : acc),
+    props.chromosomes[0]
+  )
+)
+const objectsByChromosome = computed(() =>
+  props.chromosomes.reduce(
+    (acc: { [k: string]: GenomeObjectModel[] }, curr) => {
+      acc[curr.id] = (props.objects || []).filter(
+        (object) => object.chromosomeId === curr.id
+      )
+      return acc
+    },
+    {}
+  )
+)
+
+// Currently expanded chromosome, undefined if the full karyotype is displayed
+const expandedChromosome = ref<ChromosomeModel | undefined>(undefined)
+
+// Collection containing the Objects representing the chromosome's SVGs
+const draw = ref<{ [k: string]: Svg }>({})
+
+// Fills the `draw` ref by creating the SVG corresponding to every chromosome
+const createChromosomeSVGs = () => {
+  props.chromosomes.forEach((chromosome) => {
+    // Create the SVG
+    draw.value[chromosome.id] = SVG().addTo(`#${chromosome.id}-svg`)
+
+    // Create the body of the chromosome, made of the 2 arms
+    const mainGroup = draw.value[chromosome.id]
+      .group()
+      .fill(chromosome.color || '#84b650')
+      .addClass('chr-svg-main')
+    mainGroup.rect().addClass('chr-svg-main-short-arm')
+    mainGroup.rect().addClass('chr-svg-main-long-arm')
+    // Store DOM element corresponding to each group in a ref
+    chromosomeElements.value[chromosome.id] = mainGroup.node
+
+    // Create and fill a group with the objects present on the chromosome
+    const objectsGroup = draw.value[chromosome.id]
+      .group()
+      .addClass('fill-[#abcf74]')
+      .addClass('chr-svg-objects')
+    for (const object of objectsByChromosome.value[chromosome.id]) {
+      const rect = objectsGroup.rect().data(object)
+      if (object.color) {
+        rect.fill(object.color)
+      }
+    }
+
+    // Create the clip of the object's group, by cloning the chromosome body,
+    // and apply the clip
+    const clipGroup = draw.value[chromosome.id].clip()
+    for (const path of mainGroup.clone().children()) {
+      clipGroup.add(path)
+    }
+    objectsGroup.clipWith(clipGroup)
+
+    // Create a group for the texts
+    const textGroup = draw.value[chromosome.id]
+      .group()
+      .font({
+        style: 'italic'
+      })
+      .addClass('fill-slate-700 chr-svg-text')
+
+    // Create a group for side texts, and inserts those texts if provided
+    const sideTextGroup = textGroup
+      .group()
+      .font({
+        size: '1rem',
+        weight: '500',
+        anchor: 'middle'
+      })
+      .addClass('fill-slate-500 chr-svg-text-side')
+    if (chromosome.textLeft) {
+      sideTextGroup
+        .plain(chromosome.textLeft)
+        .addClass('chr-svg-text-side-left')
+    }
+    if (chromosome.textRight) {
+      sideTextGroup
+        .plain(chromosome.textRight)
+        .addClass('chr-svg-text-side-right')
+    }
+  })
+}
+
+// Update the SVG of a given chromosome
+const updateChromosomeSVG = (
+  chromosome: ChromosomeModel,
+  chromosomeDrawSize: DrawSizesModel,
+  orientation: 'vertical' | 'horizontal' = 'vertical'
+) => {
+  // 1. Compute dimensions
+  const ONE_BP_LENGTH_PX = chromosomeDrawSize.length / chromosome.length
+  // A tenth of the width of the chromosomes is added to arm's length to allow
+  // their superposition w/o losing the real length
+  const armsOverlap = chromosomeDrawSize.thickness / 5
+  const shortArmLength =
+    ONE_BP_LENGTH_PX * (chromosome.centromereCenter || 0) + armsOverlap / 2
+  const longArmLength =
+    ONE_BP_LENGTH_PX *
+      (chromosome.length - (chromosome.centromereCenter || 0)) +
+    armsOverlap / 2
+  const SVGSize =
+    orientation === 'vertical'
+      ? {
+          height: chromosomeDrawSize.length,
+          width: chromosomeDrawSize.thickness + 2 * remToPixelsRatio.value + 15
+        }
+      : {
+          height:
+            chromosomeDrawSize.thickness + 2 * remToPixelsRatio.value + 15,
+          width: chromosomeDrawSize.length
+        }
+  // Resize the SVG
+  draw.value[chromosome.id].size(SVGSize.width, SVGSize.height)
+
+  // 2. Retrieve SVG objects
+  // Casting is needed for Typescript to work, so the notation is a bit verbose
+  const chrMainGroup = draw.value[chromosome.id].findOne('g.chr-svg-main') as G
+  const shortArmRect = draw.value[chromosome.id].findOne(
+    'rect.chr-svg-main-short-arm'
+  ) as Rect
+  const longArmRect = draw.value[chromosome.id].findOne(
+    'rect.chr-svg-main-long-arm'
+  ) as Rect
+  const objectsGroup = draw.value[chromosome.id].findOne(
+    'g.chr-svg-objects'
+  ) as G
+  const objectsGroupClipperShortArmRect = objectsGroup
+    .clipper()
+    .findOne('rect.chr-svg-main-short-arm') as Rect
+  const objectsGroupClipperLongArmRect = objectsGroup
+    .clipper()
+    .findOne('rect.chr-svg-main-long-arm') as Rect
+  const sideTextLeftText = draw.value[chromosome.id].findOne(
+    'text.chr-svg-text-side-left'
+  ) as Text
+  const sideTextRightText = draw.value[chromosome.id].findOne(
+    'text.chr-svg-text-side-right'
+  ) as Text
+
+  // 3. Base operations (chromosome is vertical, centered, and text horizontal)
+  // Chromosome arms
+  shortArmRect
+    .size(chromosomeDrawSize.thickness, shortArmLength)
+    .radius(chromosomeDrawSize.thickness / 2)
+    .cx(SVGSize.width / 2)
+  longArmRect
+    .size(chromosomeDrawSize.thickness, longArmLength)
+    .radius(chromosomeDrawSize.thickness / 2)
+    .cx(SVGSize.width / 2)
+    .y(shortArmLength - armsOverlap)
+  // Chromosome objects
+  objectsGroup.children().forEach((objectRect) => {
+    const objectSide = objectRect.data('side') || 'left'
+    const objectDimensions = {
+      width: chromosomeDrawSize.thickness / 2,
+      height: Math.max(
+        (objectRect.data('end') - objectRect.data('start')) * ONE_BP_LENGTH_PX,
+        4
+      ),
+      x:
+        objectSide === 'left'
+          ? SVGSize.width / 2 -
+            chromosomeDrawSize.thickness / 2 -
+            chromosomeDrawSize.thickness / 10
+          : SVGSize.width / 2 + chromosomeDrawSize.thickness / 10,
+      y: objectRect.data('start') * ONE_BP_LENGTH_PX
+    }
+    ;(objectRect as Rect)
+      .move(objectDimensions.x, objectDimensions.y)
+      .size(objectDimensions.width, objectDimensions.height)
+      .radius(2)
+  })
+  // Clip path
+  objectsGroupClipperShortArmRect
+    .size(chromosomeDrawSize.thickness, shortArmLength)
+    .radius(chromosomeDrawSize.thickness / 2)
+    .cx(SVGSize.width / 2)
+  objectsGroupClipperLongArmRect
+    .size(chromosomeDrawSize.thickness, longArmLength)
+    .radius(chromosomeDrawSize.thickness / 2)
+    .cx(SVGSize.width / 2)
+    .y(shortArmLength - chromosomeDrawSize.thickness / 5)
+  // Texts
+  if (chromosome.textLeft) {
+    sideTextLeftText.center(SVGSize.width / 2, SVGSize.height / 2)
+  }
+  if (chromosome.textRight) {
+    sideTextRightText.center(SVGSize.width / 2, SVGSize.height / 2)
+  }
+
+  // 4. Transforms (orientation-specific)
+  const groupsTransforms =
+    orientation === 'vertical'
+      ? {
+          rotate: 0,
+          origin: { x: 0, y: 0 },
+          translateX: 0
+        }
+      : {
+          rotate: -90,
+          origin: { x: SVGSize.width / 2, y: SVGSize.height / 2 },
+          translateX: -SVGSize.width / 2 + SVGSize.height / 2
+        }
+  const textsTransforms =
+    orientation === 'vertical'
+      ? {
+          left: {
+            rotate: -90,
+            translate: { x: -(chromosomeDrawSize.thickness / 2 + 15), y: 0 }
+          },
+          right: {
+            rotate: 90,
+            translate: { x: chromosomeDrawSize.thickness / 2 + 15, y: 0 }
+          }
+        }
+      : {
+          left: {
+            rotate: 0,
+            translate: { x: 0, y: chromosomeDrawSize.thickness / 2 + 15 }
+          },
+          right: {
+            rotate: 0,
+            translate: { x: 0, y: -(chromosomeDrawSize.thickness / 2 + 15) }
+          }
+        }
+  // Main group (chr arms)
+  chrMainGroup.transform(groupsTransforms)
+  // Objects group
+  objectsGroup.transform(groupsTransforms)
+  // Texts
+  if (chromosome.textLeft) {
+    sideTextLeftText.transform(textsTransforms.left)
+  }
+  if (chromosome.textRight) {
+    sideTextRightText.transform(textsTransforms.right)
+  }
+}
+
+// Highlight a given object with its highlight color, is made available in slots
+const highlightObjectOnChromosome = (
+  chromosomeId: string,
+  objectId: string
+) => {
+  const objectsGroup = draw.value[chromosomeId].findOne(
+    'g.chr-svg-objects'
+  ) as G
+  const objectRect = Array.from(objectsGroup.children()).find(
+    (objectRect) => objectRect.node.dataset.id === objectId
+  )
+  if (!objectRect) return
+  const highlightColor = objectsByChromosome.value[chromosomeId].find(
+    (object) => object.id === objectId
+  )?.highlightColor
+  if (highlightColor) {
+    objectRect.fill(highlightColor)
+  }
+  const objectWidth = parseInt(objectRect.width().toString())
+  objectRect.width(objectWidth * 1.25).front()
+}
+
+// Remove highlight on a given object, is made available in slots
+const unhighlightObjectOnChromosome = (
+  chromosomeId: string,
+  objectId: string
+) => {
+  const objectsGroup = draw.value[chromosomeId].findOne(
+    'g.chr-svg-objects'
+  ) as G
+  const objectRect = Array.from(objectsGroup.children()).find(
+    (objectRect) => objectRect.node.dataset.id === objectId
+  )
+  if (!objectRect) return
+  const color = objectsByChromosome.value[chromosomeId].find(
+    (object) => object.id === objectId
+  )?.color
+  if (color) {
+    objectRect.fill(color)
+  }
+  const objectWidth = parseInt(objectRect.width().toString())
+  objectRect.width(objectWidth * 0.8)
+}
+
+// Callback to expand a given chromosome
+const expandChromosomeIfReduced = (chromosomeId: string) => {
+  if (expandedChromosome.value) return
+  // Update state
+  expandedChromosome.value = _find(props.chromosomes, ['id', chromosomeId])!
+
+  // Update UI
+  updateChromosomeSVG(
+    expandedChromosome.value,
+    {
+      length: expandedChromosomeLength.value,
+      thickness: 24
+    },
+    'horizontal'
+  )
+  windowScroll.y.value = boardContentBounding.top.value
+}
+
+// Callback to go back to full karyotype
+const reduceChromosome = () => {
+  // Use a size base shared between chromosomes for a representative relative sizing
+  const ONE_BP_LENGTH_PX =
+    LONGEST_CHROMOSOME_LENGTH / longestChromosome.value.length
+
+  if (expandedChromosome.value) {
+    // Update UI
+    updateChromosomeSVG(
+      expandedChromosome.value,
+      {
+        length: ONE_BP_LENGTH_PX * expandedChromosome.value.length,
+        thickness: 24
+      },
+      'vertical'
+    )
+  }
+  // setTimeout(() => {
+  windowScroll.y.value = boardContentBounding.top.value
+  // }, 100)
+
+  // Update state
+  expandedChromosome.value = undefined
+}
+
+onMounted(() => {
+  // Use a size base shared between chromosomes for a representative relative sizing
+  const ONE_BP_LENGTH_PX =
+    LONGEST_CHROMOSOME_LENGTH / longestChromosome.value.length
+
+  createChromosomeSVGs()
+
+  props.chromosomes.forEach((chromosome) => {
+    updateChromosomeSVG(chromosome, {
+      length: ONE_BP_LENGTH_PX * chromosome.length,
+      thickness: 24
+    })
+  })
+})
+</script>
+
+<template>
+  <Button
+    v-if="!!expandedChromosome"
+    class="group absolute cursor-pointer text-3xl text-slate-400 outline-none transition-colors duration-200 hover:text-slate-600 focus:text-slate-600"
+    unstyled
+  >
+    <icon-tabler-circle-arrow-left
+      class="absolute transition-all duration-200 group-hover:opacity-0 group-focus:opacity-0"
+    />
+    <icon-tabler-circle-arrow-left-filled
+      class="absolute opacity-0 transition-all duration-200 group-hover:opacity-100 group-focus:opacity-100"
+      @click="reduceChromosome()"
+    />
+  </Button>
+
+  <div
+    ref="boardContent"
+    class="flex flex-wrap justify-around gap-y-8 text-base"
+  >
+    <div
+      v-for="(chromosome, index) in chromosomes"
+      :key="index"
+      :class="[
+        'mx-auto flex flex-col justify-between',
+        expandedChromosome &&
+          expandedChromosome.id !== chromosome.id &&
+          'hidden'
+      ]"
+    >
+      <div
+        :class="[
+          'relative mb-2 flex flex-col rounded-xl border-2 border-dashed border-transparent p-4 px-16 text-center outline-none',
+          !expandedChromosome &&
+            'transition-all duration-200 before:absolute before:inset-0 before:rounded-xl before:mix-blend-soft-light before:transition-all before:duration-200 hover:border-slate-300 hover:before:bg-white focus:border-slate-300 focus:before:bg-black',
+          expandedChromosome && 'cursor-default'
+        ]"
+        role="button"
+        :tabindex="expandedChromosome ? '' : '0'"
+        @click="expandChromosomeIfReduced(chromosome.id)"
+      >
+        <div :class="['mb-8 text-2xl italic', expandedChromosome && 'mb-12']">
+          <h2 class="font-semibold">
+            {{
+              (expandedChromosome ? 'Chromosome ' : '') + chromosome.name ||
+              `chr${index}`
+            }}
+          </h2>
+          <h3 v-if="expandedChromosome" class="text-lg text-slate-400">
+            {{ chromosome.id }}
+          </h3>
+        </div>
+        <div :id="`${chromosome.id}-svg`" class="mx-auto"></div>
+        <ChromosomeMagnify
+          v-if="expandedChromosome"
+          :chromosome="chromosome"
+          :chromosome-element="chromosomeElements[chromosome.id]"
+          :objects="objectsByChromosome[chromosome.id]"
+        ></ChromosomeMagnify>
+      </div>
+      <slot
+        v-if="!expandedChromosome"
+        name="karyotypeChromosomeAppend"
+        :chromosome="chromosome"
+        :highlight-object="(objectId : string) => {highlightObjectOnChromosome(chromosome.id, objectId)}"
+        :unhighlight-object="(objectId : string) => {unhighlightObjectOnChromosome(chromosome.id, objectId)}"
+      ></slot>
+      <slot
+        v-else-if="expandedChromosome.id === chromosome.id"
+        name="expandedChromosomeAppend"
+        :chromosome="chromosome"
+        :highlight-object="(objectId : string) => {highlightObjectOnChromosome(chromosome.id, objectId)}"
+        :unhighlight-object="(objectId : string) => {unhighlightObjectOnChromosome(chromosome.id, objectId)}"
+      ></slot>
+    </div>
+  </div>
+</template>
diff --git a/src/stores/species.ts b/src/stores/species.ts
index 3e656d2ddd95201e3b5ebb616202fe286ad4cee8..92373b4237e0dfde214a17c336f08a6167a5c914 100644
--- a/src/stores/species.ts
+++ b/src/stores/species.ts
@@ -18,6 +18,7 @@ import type {
 } from '@/typings/snoboardApi'
 
 interface SpeciesModel {
+  id: number
   table_entries: RemoteTableEntryModel[]
   coverage: {
     guide: number
@@ -55,6 +56,7 @@ export const useSpeciesStore = defineStore('species', () => {
     )
 
     species.value = {
+      id: speciesId,
       table_entries: tableEntriesRes.data,
       coverage: {
         guide:
diff --git a/src/typings/styleTypes.ts b/src/typings/styleTypes.ts
index ae25563e5e9654197ed79264aaa9b568886eb3fd..449623855de96389ae73eee88200150a6f8c19c8 100644
--- a/src/typings/styleTypes.ts
+++ b/src/typings/styleTypes.ts
@@ -20,7 +20,8 @@ const TailwindBaseColors = [
   'purple',
   'fuschia',
   'pink',
-  'rose'
+  'rose',
+  'transparent'
 ] as const
 
 export type TailwindBaseColorModel = (typeof TailwindBaseColors)[number]
diff --git a/src/views/StatisticsView.vue b/src/views/StatisticsView.vue
index 083b68f583eaf224e1a60f06c926daf374415b84..3ab69b585e382ffb252580aaeecb4c535e5e029d 100644
--- a/src/views/StatisticsView.vue
+++ b/src/views/StatisticsView.vue
@@ -7,114 +7,249 @@ import { ref, computed, toRefs } from 'vue'
  * Components imports
  */
 import MainLayout from '@/layouts/MainLayout.vue'
-import Card from 'primevue/card'
+import KaryotypeBoard from '@/components/KaryotypeBoard.vue'
+import BaseLinkListCard from '@/components/BaseLinkListCard.vue'
+import Panel from 'primevue/panel'
 import IconTablerSquareRoundedChevronRightFilled from '~icons/tabler/square-rounded-chevron-right-filled'
 import IconTablerSquareRoundedChevronRight from '~icons/tabler/square-rounded-chevron-right'
+import IconFa6SolidDna from '~icons/fa6-solid/dna'
 /**
  * Other 3rd-party imports
  */
-import { uniqBy as _uniqBy, countBy as _countBy } from 'lodash-es'
+import {
+  uniqBy as _uniqBy,
+  countBy as _countBy,
+  sortBy as _sortBy
+} from 'lodash-es'
 /**
  * Stores imports
  */
 import { useSpeciesStore } from '@/stores/species'
+/**
+ * Types imports
+ */
+import type { LinkListItemModel } from '@/components/BaseLinkListCard.vue'
+import type { GenomeObjectModel } from '@/components/KaryotypeBoard.vue'
+import type { TailwindBaseColorModel } from '@/typings/styleTypes'
 /**
  * Utils imports
  */
 import { formatSpeciesName } from '@/utils/format'
 
-interface FieldDataModel {
-  name: string
-  id: string
-  type1: string
-  type2?: string
-  count: number
-}
-
-type FieldNameModel = 'modifications' | 'guides' | 'targets' | ''
+type FieldNameModel = 'modifications' | 'guides' | 'targets' | 'none'
 
 const { species } = toRefs(useSpeciesStore())
 
-const selectedField = ref<FieldNameModel>('')
+const selectedField = ref<FieldNameModel>('none')
+
+const listCardComponent = ref()
 
-const dataCardContent = ref()
+/**
+ * Table entries, duplicated modifications removed
+ */
+const modificationsTableEntries = computed(() =>
+  _uniqBy(species.value?.table_entries, 'modification.id')
+)
+/**
+ * Table entries, duplicated guides removed
+ */
+const guidesTableEntries = computed(() =>
+  _uniqBy(species.value?.table_entries, 'guide.id')
+)
+/**
+ * Table entries, duplicated targets removed
+ */
+const targetsTableEntries = computed(() =>
+  _uniqBy(species.value?.table_entries, 'target.id')
+)
 
+/**
+ * Select a field
+ * @param fieldName Name of the field to select
+ */
 const selectField = (fieldName: FieldNameModel) => {
   selectedField.value = fieldName
-  dataCardContent.value?.scrollTo({ top: 0, behavior: 'smooth' })
+  listCardComponent.value.scrollToListTop()
 }
 
-const selectedFieldColor = computed(() => {
-  switch (selectedField.value) {
-    case 'modifications':
-      return 'sky'
-    case 'guides':
-      return 'lime'
-    case 'targets':
-      return 'amber'
-    default:
-      return 'transparent'
-  }
-})
+/**
+ * The Tailwind color associated with a given field
+ */
+const colorByField: { [k: string]: TailwindBaseColorModel } = {
+  modifications: 'sky',
+  guides: 'lime',
+  targets: 'amber',
+  none: 'transparent'
+}
 
-const rowEntriesCountBy = computed(() => ({
+/**
+ * For each field, the number of entries associated with each ID of that field
+ */
+const rowEntriesCountByField = computed(() => ({
   modifications: _countBy(species.value?.table_entries, 'modification.id'),
   guides: _countBy(species.value?.table_entries, 'guide.id'),
-  targets: _countBy(
-    _uniqBy(species.value?.table_entries, 'modification.id'),
-    'target.id'
-  )
+  targets: _countBy(modificationsTableEntries.value, 'target.id'),
+  none: {}
 }))
 
-const selectedFieldData = computed<FieldDataModel[]>(() => {
-  switch (selectedField.value) {
-    case 'modifications':
-      return _uniqBy(species.value?.table_entries, 'modification.id').map(
-        (tableEntry) => ({
-          name: tableEntry.modification.name,
-          id: tableEntry.modification.id,
-          type1: "2'-O-me",
-          type2: tableEntry.modification.after,
-          count:
-            rowEntriesCountBy.value.modifications[tableEntry.modification.id]
-        })
-      )
-    case 'guides':
-      return _uniqBy(species.value?.table_entries, 'guide.id').map(
-        (tableEntry) => ({
-          name: tableEntry.guide.name,
-          id: tableEntry.guide.id,
-          type1: tableEntry.guide.family,
-          type2: tableEntry.guide.type,
-          count: rowEntriesCountBy.value.guides[tableEntry.guide.id]
-        })
-      )
-    case 'targets':
-      return _uniqBy(species.value?.table_entries, 'target.id').map(
-        (tableEntry) => ({
-          name: tableEntry.target.name,
-          id: tableEntry.target.id,
-          type1: tableEntry.target.type,
-          count: rowEntriesCountBy.value.targets[tableEntry.target.id]
-        })
-      )
-    default:
-      return []
-  }
-})
+/**
+ * For each field, a `LinkListItemModel` of the uniq entries of that field
+ */
+const itemsByField = computed<{ [k: string]: LinkListItemModel[] }>(() => ({
+  modifications: modificationsTableEntries.value.map((tableEntry) => ({
+    id: tableEntry.modification.id,
+    title: tableEntry.modification.name,
+    subtitle: tableEntry.modification.id,
+    link: {
+      name: `modificationDetails`,
+      params: { id: tableEntry.modification.id }
+    },
+    details: [
+      "2'-O-me",
+      tableEntry.modification.after,
+      `${
+        rowEntriesCountByField.value.modifications[tableEntry.modification.id]
+      } linked guide${
+        rowEntriesCountByField.value.modifications[tableEntry.modification.id] >
+        1
+          ? 's'
+          : ''
+      }`
+    ]
+  })),
+  guides: guidesTableEntries.value.map((tableEntry) => ({
+    id: tableEntry.guide.id,
+    title: tableEntry.guide.name,
+    subtitle: tableEntry.guide.id,
+    link: {
+      name: `guideDetails`,
+      params: { id: tableEntry.guide.id }
+    },
+    details: [
+      tableEntry.guide.family,
+      tableEntry.guide.type,
+      `${
+        rowEntriesCountByField.value.guides[tableEntry.guide.id]
+      } guided modification${
+        rowEntriesCountByField.value.guides[tableEntry.guide.id] > 1 ? 's' : ''
+      }`
+    ]
+  })),
+  targets: targetsTableEntries.value.map((tableEntry) => ({
+    id: tableEntry.target.id,
+    title: tableEntry.target.name,
+    subtitle: tableEntry.target.id,
+    link: {
+      name: `${selectedField.value.slice(0, -1)}Details`,
+      params: { id: tableEntry.target.id }
+    },
+    details: [
+      tableEntry.target.type,
+      `${
+        rowEntriesCountByField.value.targets[tableEntry.target.id]
+      } registered modification${
+        rowEntriesCountByField.value.targets[tableEntry.target.id] > 1
+          ? 's'
+          : ''
+      }`
+    ]
+  })),
+  none: []
+}))
 
-const selectedFieldCountInfo = () => {
-  switch (selectedField.value) {
-    case 'modifications':
-      return 'linked guide'
-    case 'guides':
-      return 'guided modification'
-    case 'targets':
-      return 'registered modification'
-    default:
-      return ''
-  }
-}
+/**
+ * A list of the main info of each chromosome of the species to display on
+ * the karyotype (i.e. len > 1e6)
+ */
+const karyotypeChromosomes = computed(
+  () =>
+    species.value?.chromosomes
+      .map((chr) => ({
+        name: chr.name,
+        id: chr.id,
+        length: chr.length,
+        centromereCenter: Math.round(chr.length / 2),
+
+        textLeft: 'C/D box',
+        textRight: 'H/ACA box'
+      }))
+      .filter((chr) => chr.length > 1e6) || []
+)
+
+/**
+ * A list of 'objects' to display on the karyotype, that is the species guides
+ */
+const karyotypeObjects = computed(
+  () =>
+    guidesTableEntries.value
+      .filter((tableEntry) => tableEntry.guide.type === 'CD box')
+      .map((tableEntry) => ({
+        name: tableEntry.guide.name,
+        id: tableEntry.guide.id,
+        start: tableEntry.guide.start,
+        end: tableEntry.guide.end,
+        chromosomeId: tableEntry.guide.chr_id,
+        side: 'left',
+        color: '#efcb58',
+        highlightColor: '#e87'
+      }))
+      .concat([
+        {
+          name: 'Test guide',
+          id: 'GD_TEST',
+          start: 1124873,
+          end: 1124988,
+          chromosomeId: 'CHR_00000',
+          side: 'right',
+          color: '#c8baff',
+          highlightColor: '#8bf'
+        }
+      ]) as GenomeObjectModel[]
+)
+
+/**
+ * The number of guide on each chromosome, as a {chrID: guideCount} map
+ */
+const guideCountByChromosome = computed(() =>
+  _countBy(guidesTableEntries.value, 'guide.chromosome.id')
+)
+
+/**
+ * For each chromosome, a `LinkListItemModel` of the guides present on that chromosome
+ */
+const guidesByKaryotypeChromosome = computed<{
+  [k: string]: LinkListItemModel[]
+}>(() =>
+  karyotypeChromosomes.value.reduce(
+    (
+      guidesByKaryotypeChromosome: { [k: string]: LinkListItemModel[] },
+      currKaryotypeChromosome
+    ) => {
+      guidesByKaryotypeChromosome[currKaryotypeChromosome.id] = _sortBy(
+        guidesTableEntries.value.filter(
+          (tableEntry) =>
+            tableEntry.guide.chromosome.id === currKaryotypeChromosome.id
+        ),
+        (el) => el.guide.start + el.guide.end
+      ).map((tableEntry) => ({
+        id: tableEntry.guide.id,
+        title: tableEntry.guide.name,
+        subtitle: tableEntry.guide.id,
+        link: {
+          name: 'guideDetails',
+          params: { id: tableEntry.guide.id }
+        },
+        details: [
+          tableEntry.guide.type,
+          `Start: ${tableEntry.guide.start}`,
+          `End: ${tableEntry.guide.end}`
+        ]
+      }))
+      return guidesByKaryotypeChromosome
+    },
+    {}
+  )
+)
 </script>
 
 <template>
@@ -126,7 +261,7 @@ const selectedFieldCountInfo = () => {
       Species statistics
     </h2>
 
-    <div class="mx-8 grid grid-cols-3 gap-8">
+    <div class="mx-8 mb-16 grid grid-cols-3 gap-8">
       <div class="left flex flex-col gap-4">
         <button
           :class="[
@@ -236,57 +371,69 @@ const selectedFieldCountInfo = () => {
         </div>
       </div>
       <div class="right col-span-2">
-        <Card
-          :class="['!border', `!border-${selectedFieldColor}-600`]"
-          :pt="{
-            body: { class: '!pr-0' },
-            content: {
-              class: '!py-0'
-            }
-          }"
+        <BaseLinkListCard
+          ref="listCardComponent"
+          :color="colorByField[selectedField]"
+          :link-items="itemsByField[selectedField]"
+          :disabled="selectedField === 'none'"
         >
-          <template v-if="selectedField" #content>
-            <div
-              ref="dataCardContent"
-              class="!mr-2 flex max-h-[65vh] flex-col gap-4 overflow-auto !pr-3"
-            >
-              <RouterLink
-                v-for="data in selectedFieldData"
-                :key="data.id"
-                :class="[
-                  `hover:border-${selectedFieldColor}-600`,
-                  'grid grid-cols-5 rounded-md border-2 p-2 hover:shadow-md'
-                ]"
-                :to="{
-                  name: `${selectedField.slice(0, -1)}Details`,
-                  params: { id: data.id }
-                }"
-              >
-                <div class="flex flex-col border-r-2">
-                  <h4 class="text-center text-lg font-semibold">
-                    {{ data.name }}
-                  </h4>
-                  <p class="text-center italic text-slate-400">{{ data.id }}</p>
-                </div>
-                <div class="col-span-4 my-auto pl-4 text-xl text-slate-400">
-                  {{ data.type1 }}
-                  <span class="mx-2">•</span>
-                  {{ data.type2 }}
-                  <span v-if="data.type2" class="mx-2">•</span>
-                  {{ data.count }}
-                  {{ selectedFieldCountInfo() + (data.count > 1 ? 's' : '') }}
-                </div>
-              </RouterLink>
-            </div>
+          <template #disabled>
+            <strong>&larr;</strong>
+            Chose a data type on the left to see its elements
           </template>
-          <template v-else #content>
-            <span class="m-auto italic text-slate-400">
-              <strong>&larr;</strong>
-              Chose a data type on the left to see its elements
-            </span></template
-          >
-        </Card>
+        </BaseLinkListCard>
       </div>
     </div>
+
+    <Panel
+      toggleable
+      class="mx-auto max-w-7xl break-words 2xl:max-w-[100rem]"
+      :pt="{
+        header: { class: '!bg-slate-50' },
+        toggler: { class: 'hover:!bg-slate-100' }
+      }"
+    >
+      <template #header="scope">
+        <icon-fa6-solid-dna :class="scope.class" />
+        <span :id="scope.id" class="p-panel-title">Chromosomes</span>
+      </template>
+
+      <KaryotypeBoard
+        v-if="species"
+        :chromosomes="karyotypeChromosomes"
+        :objects="karyotypeObjects"
+      >
+        <template #karyotypeChromosomeAppend="{ chromosome }">
+          <p class="text-center text-xl">
+            Guides:
+            <span class="font-bold italic">
+              {{ guideCountByChromosome[chromosome.id] }}
+            </span>
+          </p>
+        </template>
+        <template
+          #expandedChromosomeAppend="{
+            chromosome,
+            highlightObject,
+            unhighlightObject
+          }"
+        >
+          <div class="mb-8 text-center italic text-slate-400">
+            Length :
+            <em class="text-lg font-bold not-italic text-slate-700">{{
+              chromosome.length
+            }}</em>
+          </div>
+          <BaseLinkListCard
+            list-title="Guides list"
+            :link-items="guidesByKaryotypeChromosome[chromosome.id]"
+            color="sky"
+            :hover-event="true"
+            @enter-item="highlightObject"
+            @leave-item="unhighlightObject"
+          ></BaseLinkListCard>
+        </template>
+      </KaryotypeBoard>
+    </Panel>
   </MainLayout>
 </template>
diff --git a/tailwind.config.js b/tailwind.config.js
index 1b8dc0ad73334d62edc17bd6651f1678121b2708..d69f6465e0ea9f122df02cebdc28700955f00b14 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -36,9 +36,11 @@ export default {
       pattern: /^!?border-(lime|red|orange|yellow|violet|purple|slate)-600$/
     },
     {
-      pattern: /^border-(sky|lime|amber|transparent)-600$/,
+      pattern: /^border-(sky|lime|amber|transparent|slate)-600$/,
       variants: ['hover']
-    }
+    },
+    'brightness-90', // Zoomed chromosome objects hover
+    'cursor-pointer' // Zoomed chromosome objects
     // {
     //   pattern: /^!border-(sky|lime|amber|transparent)-600$/
     // }