diff --git a/package-lock.json b/package-lock.json
index d61b7297f78cbf13c0656db0554f629e1362436f..f5d700154ac7b2e64101b071c444d44034bbfeba 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -62,6 +62,7 @@
         "eslint-plugin-testing-library": "^6.0.1",
         "husky": "^8.0.0",
         "jest": "^29.7.0",
+        "jest-canvas-mock": "^2.5.2",
         "jest-environment-jsdom": "^29.7.0",
         "jest-junit": "^16.0.0",
         "jest-watch-typeahead": "^2.2.2",
@@ -4030,6 +4031,12 @@
         "node": ">=4"
       }
     },
+    "node_modules/cssfontparser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz",
+      "integrity": "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==",
+      "dev": true
+    },
     "node_modules/cssom": {
       "version": "0.5.0",
       "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
@@ -7674,6 +7681,16 @@
         }
       }
     },
+    "node_modules/jest-canvas-mock": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz",
+      "integrity": "sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A==",
+      "dev": true,
+      "dependencies": {
+        "cssfontparser": "^1.2.1",
+        "moo-color": "^1.0.2"
+      }
+    },
     "node_modules/jest-changed-files": {
       "version": "29.7.0",
       "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz",
@@ -9771,6 +9788,15 @@
         "node": ">=10"
       }
     },
+    "node_modules/moo-color": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz",
+      "integrity": "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==",
+      "dev": true,
+      "dependencies": {
+        "color-name": "^1.1.4"
+      }
+    },
     "node_modules/ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
diff --git a/package.json b/package.json
index 6e3f2e39d607187aa51823be6783b7581b30c9ed..6d2d85292d9f9ee9d0bb0f7bcd975fbddfcda2e2 100644
--- a/package.json
+++ b/package.json
@@ -76,6 +76,7 @@
     "eslint-plugin-testing-library": "^6.0.1",
     "husky": "^8.0.0",
     "jest": "^29.7.0",
+    "jest-canvas-mock": "^2.5.2",
     "jest-environment-jsdom": "^29.7.0",
     "jest-junit": "^16.0.0",
     "jest-watch-typeahead": "^2.2.2",
diff --git a/setupTests.ts b/setupTests.ts
index 11a7da9a6d761c10f3f1336c1c4c2c4a13c3e6fb..db87ae92bfff490a04acbd652192bbc003c28b03 100644
--- a/setupTests.ts
+++ b/setupTests.ts
@@ -1,4 +1,5 @@
 import '@testing-library/jest-dom';
+import 'jest-canvas-mock';
 
 // used by openlayers module
 global.ResizeObserver = jest.fn().mockImplementation(() => ({
diff --git a/src/components/Map/MapViewer/utils/config/getCanvasIcon.test.ts b/src/components/Map/MapViewer/utils/config/getCanvasIcon.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..aa5b864020822722db5351f870bb64595382ed2c
--- /dev/null
+++ b/src/components/Map/MapViewer/utils/config/getCanvasIcon.test.ts
@@ -0,0 +1,100 @@
+/* eslint-disable no-magic-numbers */
+import { DEFAULT_FONT_FAMILY } from '@/constants/font';
+import { createCanvas } from '@/utils/canvas/getCanvas';
+import {
+  drawNumberOnCanvas,
+  drawPinOnCanvas,
+  getTextPosition,
+  getTextWidth,
+} from './getCanvasIcon';
+
+const getContext = (): CanvasRenderingContext2D => {
+  const canvas = createCanvas({ width: 100, height: 100 });
+  return canvas.getContext('2d') as CanvasRenderingContext2D;
+};
+
+const ONCE = 1;
+
+describe('getCanvasIcon - util', () => {
+  beforeEach(() => {
+    jest.restoreAllMocks();
+  });
+
+  describe('getTextWidth - subUtil', () => {
+    const cases: [number, number][] = [
+      [1, 6.25],
+      [7, 8.333],
+      [43, 12.5],
+      [105, 16.666],
+    ];
+
+    it.each(cases)('on value=%s should return %s', (input, output) => {
+      expect(getTextWidth(input)).toBeCloseTo(output);
+    });
+  });
+
+  describe('getTextPosition - subUtil', () => {
+    const cases: [number, number, number, number][] = [
+      [100, 100, -37.5, -27.2],
+      [532, 443, -253.5, -164.4],
+      [10, 0, 7.5, 12.8],
+      [0, 10, 12.5, 8.8],
+      [0, 0, 12.5, 12.8],
+    ];
+
+    it.each(cases)(
+      'on textWidth=%s textHeight=%s should return x=%s y=%s',
+      (textWidth, textHeight, x, y) => {
+        expect(getTextPosition(textWidth, textHeight)).toMatchObject({
+          x,
+          y,
+        });
+      },
+    );
+  });
+
+  describe('drawPinOnCanvas - subUtil', () => {
+    const color = '#000000';
+
+    it('should run set fillStyle with color', () => {
+      const ctx = getContext();
+      drawPinOnCanvas({ color }, ctx);
+      expect(ctx.fillStyle).toBe(color);
+    });
+
+    it('should run fill method with valid arguments', () => {
+      const ctx = getContext();
+      const fillSpy = jest.spyOn(ctx, 'fill');
+      drawPinOnCanvas({ color }, ctx);
+
+      const call = fillSpy.mock.calls[0][0];
+      expect(call).toBeInstanceOf(Path2D);
+      expect(fillSpy).toBeCalledTimes(ONCE);
+    });
+  });
+
+  describe('drawNumberOnCanvas - subUtil', () => {
+    const ctx = getContext();
+    const fillTextSpy = jest.spyOn(ctx, 'fillText');
+    const value = 69;
+
+    beforeAll(() => {
+      drawNumberOnCanvas(
+        {
+          value,
+        },
+        ctx,
+      );
+    });
+    it('should set valid ctx fields', () => {
+      expect(ctx.fillStyle).toBe('#ffffff');
+      expect(ctx.textBaseline).toBe('top');
+      expect(ctx.font).toBe(`6.25px ${DEFAULT_FONT_FAMILY}`);
+    });
+
+    it('should run fillText once with valid args', () => {
+      expect(fillTextSpy).toBeCalledWith(`${value}`, 6.25, 12.8);
+      expect(fillTextSpy).toBeCalledTimes(ONCE);
+    });
+  });
+});
diff --git a/src/components/Map/MapViewer/utils/config/getCanvasIcon.ts b/src/components/Map/MapViewer/utils/config/getCanvasIcon.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c377b0efa124b12dc7ad8633e11d3f80cb5d0df0
--- /dev/null
+++ b/src/components/Map/MapViewer/utils/config/getCanvasIcon.ts
@@ -0,0 +1,73 @@
+import { PIN_PATH2D, PIN_SIZE } from '@/constants/canvas';
+import { HALF, ONE_AND_HALF, QUARTER, THIRD, TWO_AND_HALF } from '@/constants/dividers';
+import { DEFAULT_FONT_FAMILY } from '@/constants/font';
+import { Point } from '@/types/map';
+import { createCanvas } from '@/utils/canvas/getCanvas';
+import { getFontSizeToFit } from '@/utils/canvas/getFontSizeToFit';
+
+const SMALL_TEXT_VALUE = 1;
+const MEDIUM_TEXT_VALUE = 10;
+const BIG_TEXT_VALUE = 100;
+
+interface Args {
+  color: string;
+  value: number;
+}
+
+export const drawPinOnCanvas = (
+  { color }: Pick<Args, 'color'>,
+  ctx: CanvasRenderingContext2D,
+): void => {
+  const path = new Path2D(PIN_PATH2D);
+  ctx.fillStyle = color;
+  ctx.fill(path);
+};
+
+export const getTextWidth = (value: number): number => {
+  switch (true) {
+    case value === SMALL_TEXT_VALUE:
+      return PIN_SIZE.width / QUARTER;
+    case value < MEDIUM_TEXT_VALUE:
+      return PIN_SIZE.width / THIRD;
+    case value < BIG_TEXT_VALUE:
+      return PIN_SIZE.width / HALF;
+    default:
+      return PIN_SIZE.width / ONE_AND_HALF;
+  }
+};
+
+export const getTextPosition = (textWidth: number, textHeight: number): Point => ({
+  x: (PIN_SIZE.width - textWidth) / HALF,
+  y: (PIN_SIZE.height - textHeight) / TWO_AND_HALF,
+});
+
+export const drawNumberOnCanvas = (
+  { value }: Pick<Args, 'value'>,
+  ctx: CanvasRenderingContext2D,
+): void => {
+  const text = `${value}`;
+  const textMetrics = ctx.measureText(text);
+
+  const textWidth = getTextWidth(value);
+  const fontSize = getFontSizeToFit(ctx, text, DEFAULT_FONT_FAMILY, textWidth);
+  const textHeight = textMetrics.fontBoundingBoxAscent + textMetrics.fontBoundingBoxDescent;
+  const { x, y } = getTextPosition(textWidth, textHeight);
+
+  ctx.fillStyle = 'white';
+  ctx.textBaseline = 'top';
+  ctx.font = `${fontSize}px ${DEFAULT_FONT_FAMILY}`;
+  ctx.fillText(text, x, y);
+};
+
+export const getCanvasIcon = (args: Args): HTMLCanvasElement => {
+  const canvas = createCanvas(PIN_SIZE);
+  const ctx = canvas.getContext('2d');
+  if (!ctx) {
+    return canvas;
+  }
+
+  drawPinOnCanvas(args, ctx);
+  drawNumberOnCanvas(args, ctx);
+
+  return canvas;
+};
diff --git a/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts
index ac543476c5fb61ca762409d68eb586bf6f564a05..c240ae88a5017c0108be17e74e272dcf84abe855 100644
--- a/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts
+++ b/src/components/Map/MapViewer/utils/config/useOlMapLayers.test.ts
@@ -5,7 +5,9 @@ import { initialMapStateFixture } from '@/redux/map/map.fixtures';
 import { BACKGROUND_INITIAL_STATE_MOCK } from '@/redux/backgrounds/background.mock';
 import { renderHook, waitFor } from '@testing-library/react';
 import { Map } from 'ol';
+import BaseLayer from 'ol/layer/Base';
 import TileLayer from 'ol/layer/Tile';
+import VectorLayer from 'ol/layer/Vector';
 import React from 'react';
 import { useOlMapLayers } from './useOlMapLayers';
 import { useOlMap } from '../useOlMap';
@@ -50,7 +52,7 @@ describe('useOlMapLayers - util', () => {
     await waitFor(() => expect(setLayersSpy).toBeCalledTimes(CALLED_ONCE));
   });
 
-  it('should return valid View instance', async () => {
+  const getRenderedHookResults = (): BaseLayer[] => {
     const { Wrapper } = getReduxWrapperWithStore({
       map: {
         data: {
@@ -93,7 +95,20 @@ describe('useOlMapLayers - util', () => {
       },
     );
 
-    expect(result.current[0]).toBeInstanceOf(TileLayer);
-    expect(result.current[0].getSourceState()).toBe('ready');
+    return result.current;
+  };
+
+  it('should return valid TileLayer instance', () => {
+    const result = getRenderedHookResults();
+
+    expect(result[0]).toBeInstanceOf(TileLayer);
+    expect(result[0].getSourceState()).toBe('ready');
+  });
+
+  it('should return valid VectorLayer instance', () => {
+    const result = getRenderedHookResults();
+
+    expect(result[1]).toBeInstanceOf(VectorLayer);
+    expect(result[1].getSourceState()).toBe('ready');
   });
 });
diff --git a/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts b/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts
index 67d71d50060f40018ec5d515b0608a8b98bb4f92..c092a1465003e3c94414fd18582a6012dd74993a 100644
--- a/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts
+++ b/src/components/Map/MapViewer/utils/config/useOlMapLayers.ts
@@ -1,57 +1,24 @@
 /* eslint-disable no-magic-numbers */
-import { OPTIONS } from '@/constants/map';
-import { currentBackgroundImagePathSelector } from '@/redux/backgrounds/background.selectors';
-import { mapDataSizeSelector } from '@/redux/map/map.selectors';
-import { projectDataSelector } from '@/redux/project/project.selectors';
-import TileLayer from 'ol/layer/Tile';
-import { XYZ } from 'ol/source';
-import { useEffect, useMemo } from 'react';
-import { useSelector } from 'react-redux';
+import { useEffect } from 'react';
 import { MapConfig, MapInstance } from '../../MapViewer.types';
-import { getMapTileUrl } from './getMapTileUrl';
+import { useOlMapPinsLayer } from './useOlMapPinsLayer';
+import { useOlMapTileLayer } from './useOlMapTileLayer';
 
 interface UseOlMapLayersInput {
   mapInstance: MapInstance;
 }
 
 export const useOlMapLayers = ({ mapInstance }: UseOlMapLayersInput): MapConfig['layers'] => {
-  const mapSize = useSelector(mapDataSizeSelector);
-  const currentBackgroundImagePath = useSelector(currentBackgroundImagePathSelector);
-  const project = useSelector(projectDataSelector);
-
-  const sourceUrl = useMemo(
-    () => getMapTileUrl({ projectDirectory: project?.directory, currentBackgroundImagePath }),
-    [project?.directory, currentBackgroundImagePath],
-  );
-
-  const source = useMemo(
-    () =>
-      new XYZ({
-        url: sourceUrl,
-        maxZoom: mapSize.maxZoom,
-        minZoom: mapSize.minZoom,
-        tileSize: mapSize.tileSize,
-        wrapX: OPTIONS.wrapXInTileLayer,
-      }),
-    [sourceUrl, mapSize.maxZoom, mapSize.minZoom, mapSize.tileSize],
-  );
-
-  const tileLayer = useMemo(
-    (): TileLayer<XYZ> =>
-      new TileLayer({
-        visible: true,
-        source,
-      }),
-    [source],
-  );
+  const tileLayer = useOlMapTileLayer();
+  const pinsLayer = useOlMapPinsLayer();
 
   useEffect(() => {
     if (!mapInstance) {
       return;
     }
 
-    mapInstance.setLayers([tileLayer]);
-  }, [tileLayer, mapInstance]);
+    mapInstance.setLayers([tileLayer, pinsLayer]);
+  }, [tileLayer, pinsLayer, mapInstance]);
 
-  return [tileLayer];
+  return [tileLayer, pinsLayer];
 };
diff --git a/src/components/Map/MapViewer/utils/config/useOlMapPinsLayer.ts b/src/components/Map/MapViewer/utils/config/useOlMapPinsLayer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3244cf3a75e75a2a03fb23fc8dedb7eb43677bbf
--- /dev/null
+++ b/src/components/Map/MapViewer/utils/config/useOlMapPinsLayer.ts
@@ -0,0 +1,79 @@
+/* eslint-disable no-magic-numbers */
+import { PIN_SIZE } from '@/constants/canvas';
+import { allBioEntitesSelectorOfCurrentMap } from '@/redux/bioEntity/bioEntity.selectors';
+import { BioEntity } from '@/types/models';
+import { UsePointToProjectionResult, usePointToProjection } from '@/utils/map/usePointToProjection';
+import { Feature } from 'ol';
+import { Point as OlPoint } from 'ol/geom';
+import BaseLayer from 'ol/layer/Base';
+import VectorLayer from 'ol/layer/Vector';
+import VectorSource from 'ol/source/Vector';
+import Icon from 'ol/style/Icon';
+import Style from 'ol/style/Style';
+import { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+import { getCanvasIcon } from './getCanvasIcon';
+
+const getPinFeature = (
+  { x, y, width, height, name }: BioEntity,
+  pointToProjection: UsePointToProjectionResult,
+): Feature => {
+  const point = {
+    x: x + width / 2,
+    y: y + height / 2,
+  };
+
+  return new Feature({
+    geometry: new OlPoint(pointToProjection(point)),
+    name,
+  });
+};
+
+const getPinStyle = ({ value, color }: { value: number; color: string }): Style =>
+  new Style({
+    image: new Icon({
+      displacement: [0, PIN_SIZE.height],
+      anchorXUnits: 'fraction',
+      anchorYUnits: 'pixels',
+      img: getCanvasIcon({
+        color,
+        value,
+      }),
+    }),
+  });
+
+export const useOlMapPinsLayer = (): BaseLayer => {
+  const pointToProjection = usePointToProjection();
+  const bioEntites = useSelector(allBioEntitesSelectorOfCurrentMap);
+
+  const bioEntityFeatures = useMemo(
+    () =>
+      bioEntites.map(({ bioEntity }, index) => {
+        const feature = getPinFeature(bioEntity, pointToProjection);
+        const style = getPinStyle({
+          color: '#106AD7',
+          value: index + 1,
+        });
+
+        feature.setStyle(style);
+        return feature;
+      }),
+    [bioEntites, pointToProjection],
+  );
+
+  const vectorSource = useMemo(() => {
+    return new VectorSource({
+      features: [...bioEntityFeatures],
+    });
+  }, [bioEntityFeatures]);
+
+  const pinsLayer = useMemo(
+    () =>
+      new VectorLayer({
+        source: vectorSource,
+      }),
+    [vectorSource],
+  );
+
+  return pinsLayer;
+};
diff --git a/src/components/Map/MapViewer/utils/config/useOlMapTileLayer.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapTileLayer.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8c5321cbbb294676336ad9404a7fb7a4251f1fd5
--- /dev/null
+++ b/src/components/Map/MapViewer/utils/config/useOlMapTileLayer.test.ts
@@ -0,0 +1,77 @@
+/* eslint-disable no-magic-numbers */
+import { MAP_DATA_INITIAL_STATE, OPENED_MAPS_INITIAL_STATE } from '@/redux/map/map.constants';
+import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
+import { renderHook } from '@testing-library/react';
+import BaseLayer from 'ol/layer/Base';
+import TileLayer from 'ol/layer/Tile';
+import React from 'react';
+import { useOlMapTileLayer } from './useOlMapTileLayer';
+
+const useRefValue = {
+  current: null,
+};
+
+Object.defineProperty(useRefValue, 'current', {
+  get: jest.fn(() => ({
+    innerHTML: '',
+    appendChild: jest.fn(),
+    addEventListener: jest.fn(),
+    getRootNode: jest.fn(),
+  })),
+  set: jest.fn(() => ({
+    innerHTML: '',
+    appendChild: jest.fn(),
+    addEventListener: jest.fn(),
+    getRootNode: jest.fn(),
+  })),
+});
+
+jest.spyOn(React, 'useRef').mockReturnValue(useRefValue);
+
+describe('useOlMapTileLayer - util', () => {
+  const getRenderedHookResults = (): BaseLayer => {
+    const { Wrapper } = getReduxWrapperWithStore({
+      map: {
+        data: {
+          ...MAP_DATA_INITIAL_STATE,
+          size: {
+            width: 256,
+            height: 256,
+            tileSize: 256,
+            minZoom: 1,
+            maxZoom: 1,
+          },
+          position: {
+            initial: {
+              x: 256,
+              y: 256,
+            },
+            last: {
+              x: 256,
+              y: 256,
+            },
+          },
+        },
+        loading: 'idle',
+        error: {
+          name: '',
+          message: '',
+        },
+        openedMaps: OPENED_MAPS_INITIAL_STATE,
+      },
+    });
+
+    const { result } = renderHook(() => useOlMapTileLayer(), {
+      wrapper: Wrapper,
+    });
+
+    return result.current;
+  };
+
+  it('should return valid TileLayer instance', () => {
+    const result = getRenderedHookResults();
+
+    expect(result).toBeInstanceOf(TileLayer);
+    expect(result.getSourceState()).toBe('ready');
+  });
+});
diff --git a/src/components/Map/MapViewer/utils/config/useOlMapTileLayer.ts b/src/components/Map/MapViewer/utils/config/useOlMapTileLayer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b50d5c150dd2291feab71d5c5a5e0477f05e95aa
--- /dev/null
+++ b/src/components/Map/MapViewer/utils/config/useOlMapTileLayer.ts
@@ -0,0 +1,47 @@
+/* eslint-disable no-magic-numbers */
+import { OPTIONS } from '@/constants/map';
+import { currentBackgroundImagePathSelector } from '@/redux/backgrounds/background.selectors';
+import { mapDataSizeSelector } from '@/redux/map/map.selectors';
+import { projectDataSelector } from '@/redux/project/project.selectors';
+import BaseLayer from 'ol/layer/Base';
+import TileLayer from 'ol/layer/Tile';
+import { XYZ } from 'ol/source';
+import { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+import { getMapTileUrl } from './getMapTileUrl';
+
+// useOlMapTileLayer returns visual tile layer of the map
+// it makes it possible to view the map, scroll, zoom etc.
+export const useOlMapTileLayer = (): BaseLayer => {
+  const mapSize = useSelector(mapDataSizeSelector);
+  const currentBackgroundImagePath = useSelector(currentBackgroundImagePathSelector);
+  const project = useSelector(projectDataSelector);
+
+  const sourceUrl = useMemo(
+    () => getMapTileUrl({ projectDirectory: project?.directory, currentBackgroundImagePath }),
+    [project?.directory, currentBackgroundImagePath],
+  );
+
+  const source = useMemo(
+    () =>
+      new XYZ({
+        url: sourceUrl,
+        maxZoom: mapSize.maxZoom,
+        minZoom: mapSize.minZoom,
+        tileSize: mapSize.tileSize,
+        wrapX: OPTIONS.wrapXInTileLayer,
+      }),
+    [sourceUrl, mapSize.maxZoom, mapSize.minZoom, mapSize.tileSize],
+  );
+
+  const tileLayer = useMemo(
+    (): TileLayer<XYZ> =>
+      new TileLayer({
+        visible: true,
+        source,
+      }),
+    [source],
+  );
+
+  return tileLayer;
+};
diff --git a/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts b/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts
index 21d779d60483c0faff83938e1e6c96cd9133a615..2832e68da8c053073a8d9bf713a99251c4e42a6a 100644
--- a/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts
+++ b/src/components/Map/MapViewer/utils/config/useOlMapView.test.ts
@@ -1,13 +1,13 @@
 /* eslint-disable no-magic-numbers */
-import { setMapPosition } from '@/redux/map/map.slice';
-import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
-import { act, renderHook, waitFor } from '@testing-library/react';
-import { initialMapStateFixture } from '@/redux/map/map.fixtures';
 import {
   BACKGROUNDS_MOCK,
   BACKGROUND_INITIAL_STATE_MOCK,
 } from '@/redux/backgrounds/background.mock';
 import { MAP_DATA_INITIAL_STATE, OPENED_MAPS_INITIAL_STATE } from '@/redux/map/map.constants';
+import { initialMapStateFixture } from '@/redux/map/map.fixtures';
+import { setMapPosition } from '@/redux/map/map.slice';
+import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
+import { act, renderHook, waitFor } from '@testing-library/react';
 import { View } from 'ol';
 import Map from 'ol/Map';
 import React from 'react';
diff --git a/src/components/Map/MapViewer/utils/listeners/onMapSingleClick.ts b/src/components/Map/MapViewer/utils/listeners/onMapSingleClick.ts
index acf6e51664ffb871b4aab35e36ba3199bbfe9a29..4b39092bc28a0846ae4e4e2c7ca7eee3bc50233b 100644
--- a/src/components/Map/MapViewer/utils/listeners/onMapSingleClick.ts
+++ b/src/components/Map/MapViewer/utils/listeners/onMapSingleClick.ts
@@ -37,10 +37,11 @@ export const handleReactionResults =
         return;
       }
 
-      const { products, reactants } = payload[FIRST];
+      const { products, reactants, modifiers } = payload[FIRST];
       const productsIds = products.map(p => p.aliasId);
       const reactantsIds = reactants.map(r => r.aliasId);
-      const bioEntitiesIds = [...productsIds, ...reactantsIds].map(identifier => String(identifier));
+      const modifiersIds = modifiers.map(m => m.aliasId);
+      const bioEntitiesIds = [...productsIds, ...reactantsIds, ...modifiersIds].map(identifier => String(identifier));
 
       dispatch(setBioEntityContent([]));
       await dispatch(
diff --git a/src/components/SPA/MinervaSPA.component.tsx b/src/components/SPA/MinervaSPA.component.tsx
index 4de3ca1fb95c0e3bbf4b82dbc85e7afffb32d033..3376b1ca88b4dbbd938d828d5e69b95675af1aad 100644
--- a/src/components/SPA/MinervaSPA.component.tsx
+++ b/src/components/SPA/MinervaSPA.component.tsx
@@ -1,17 +1,10 @@
 import { FunctionalArea } from '@/components/FunctionalArea';
 import { Map } from '@/components/Map';
+import { manrope } from '@/constants/font';
 import { useReduxBusQueryManager } from '@/utils/query-manager/useReduxBusQueryManager';
-import { Manrope } from '@next/font/google';
 import { twMerge } from 'tailwind-merge';
 import { useInitializeStore } from '../../utils/initialize/useInitializeStore';
 
-const manrope = Manrope({
-  variable: '--font-manrope',
-  display: 'swap',
-  weight: ['400', '700'],
-  subsets: ['latin'],
-});
-
 export const MinervaSPA = (): JSX.Element => {
   useInitializeStore();
   useReduxBusQueryManager();
diff --git a/src/constants/canvas.ts b/src/constants/canvas.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b9741f3001cb3defe3931b799a208860b4110879
--- /dev/null
+++ b/src/constants/canvas.ts
@@ -0,0 +1,7 @@
+export const PIN_PATH2D =
+  'M12.3077 0C6.25641 0 0 4.61538 0 12.3077C0 19.5897 11.0769 30.9744 11.5897 31.4872C11.7949 31.6923 12 31.7949 12.3077 31.7949C12.6154 31.7949 12.8205 31.6923 13.0256 31.4872C13.5385 30.9744 24.6154 19.6923 24.6154 12.3077C24.6154 4.61538 18.359 0 12.3077 0Z';
+
+export const PIN_SIZE = {
+  width: 25,
+  height: 32,
+};
diff --git a/src/constants/dividers.ts b/src/constants/dividers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..402936ad0c276f9ff4b4ef36cc78ee3467752a59
--- /dev/null
+++ b/src/constants/dividers.ts
@@ -0,0 +1,5 @@
+export const ONE_AND_HALF = 1.5;
+export const HALF = 2;
+export const TWO_AND_HALF = 2.5;
+export const THIRD = 3;
+export const QUARTER = 4;
diff --git a/src/constants/font.ts b/src/constants/font.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a8ea3d4d6cb31c59c237ee51062294131e3ae497
--- /dev/null
+++ b/src/constants/font.ts
@@ -0,0 +1,10 @@
+import { Manrope } from '@next/font/google';
+
+export const manrope = Manrope({
+  variable: '--font-manrope',
+  display: 'swap',
+  weight: ['400', '700'],
+  subsets: ['latin'],
+});
+
+export const DEFAULT_FONT_FAMILY = manrope.style.fontFamily;
diff --git a/src/models/reaction.ts b/src/models/reaction.ts
index 7cc4e1add7f33494e45ba990b031c4a03a512642..51c7c51b014b35f76668730099639a929fb251cc 100644
--- a/src/models/reaction.ts
+++ b/src/models/reaction.ts
@@ -11,7 +11,7 @@ export const reactionSchema = z.object({
   kineticLaw: z.null(),
   lines: z.array(reactionLineSchema),
   modelId: z.number(),
-  modifiers: z.array(z.unknown()),
+  modifiers: z.array(productsSchema),
   name: z.string(),
   notes: z.string(),
   products: z.array(productsSchema),
diff --git a/src/redux/bioEntity/bioEntity.selectors.ts b/src/redux/bioEntity/bioEntity.selectors.ts
index 43c3002e30980c9f6c09076cb632b6d54fa74bb3..dac3b8c71cb696d8c9a2bd6289f35aa7df5dfccd 100644
--- a/src/redux/bioEntity/bioEntity.selectors.ts
+++ b/src/redux/bioEntity/bioEntity.selectors.ts
@@ -1,6 +1,8 @@
 import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
 import { rootSelector } from '@/redux/root/root.selectors';
+import { BioEntityContent } from '@/types/models';
 import { createSelector } from '@reduxjs/toolkit';
+import { currentModelIdSelector } from '../models/models.selectors';
 
 export const bioEntitySelector = createSelector(rootSelector, state => state.bioEntity);
 
@@ -9,6 +11,13 @@ export const loadingBioEntityStatusSelector = createSelector(
   state => state.loading,
 );
 
+export const allBioEntitesSelectorOfCurrentMap = createSelector(
+  bioEntitySelector,
+  currentModelIdSelector,
+  (state, currentModelId): BioEntityContent[] =>
+    (state?.data || []).filter(({ bioEntity }) => bioEntity.model === currentModelId),
+);
+
 export const numberOfBioEntitiesSelector = createSelector(bioEntitySelector, state =>
   state.data ? state.data.length : SIZE_OF_EMPTY_ARRAY,
 );
diff --git a/src/utils/canvas/getCanvas.test.ts b/src/utils/canvas/getCanvas.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a0f6f1adf1933477f52a1fcc6c1cc29a83d81e99
--- /dev/null
+++ b/src/utils/canvas/getCanvas.test.ts
@@ -0,0 +1,15 @@
+/* eslint-disable no-magic-numbers */
+import { createCanvas } from './getCanvas';
+
+describe('getCanvas', () => {
+  it('should return HTMLCanvasElement with valid size on positive params', () => {
+    const result = createCanvas({
+      width: 800,
+      height: 600,
+    });
+
+    expect(result).toBeInstanceOf(HTMLCanvasElement);
+    expect(result.width).toEqual(800);
+    expect(result.height).toEqual(600);
+  });
+});
diff --git a/src/utils/canvas/getCanvas.ts b/src/utils/canvas/getCanvas.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d380a465fefe0d70028ff2a7954df241885fe817
--- /dev/null
+++ b/src/utils/canvas/getCanvas.ts
@@ -0,0 +1,12 @@
+export const createCanvas = ({
+  width,
+  height,
+}: {
+  width: number;
+  height: number;
+}): HTMLCanvasElement => {
+  const canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  return canvas;
+};
diff --git a/src/utils/canvas/getFontSizeToFit.test.ts b/src/utils/canvas/getFontSizeToFit.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ce53c089ce648a8de13c4b3f885c250731d79161
--- /dev/null
+++ b/src/utils/canvas/getFontSizeToFit.test.ts
@@ -0,0 +1,25 @@
+/* eslint-disable no-magic-numbers */
+import { createCanvas } from './getCanvas';
+import { getFontSizeToFit } from './getFontSizeToFit';
+
+const getContext = (): CanvasRenderingContext2D => {
+  const canvas = createCanvas({ width: 100, height: 100 });
+  return canvas.getContext('2d') as CanvasRenderingContext2D;
+};
+
+describe('getFontSizeToFit', () => {
+  const cases: [string, string, number, number][] = [
+    ['Hello', 'Helvetica', 50, 10],
+    ['123', 'Arial', 48, 16],
+    ['1', '', 48, 48],
+    ['Text', '', 0, 0],
+    ['', '', 0, 0],
+  ];
+  it.each(cases)(
+    'on text=%s, fontFace=%s, maxWidth=%s it should return value %s',
+    (text, fontFace, maxWidth, result) => {
+      const ctx = getContext();
+      expect(getFontSizeToFit(ctx, text, fontFace, maxWidth)).toBeCloseTo(result);
+    },
+  );
+});
diff --git a/src/utils/canvas/getFontSizeToFit.ts b/src/utils/canvas/getFontSizeToFit.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dfad99cfec5d11e910ee7023a9abc57eaf278aaf
--- /dev/null
+++ b/src/utils/canvas/getFontSizeToFit.ts
@@ -0,0 +1,11 @@
+const DEFAULT_VALID_SIZE = 0;
+
+export const getFontSizeToFit = (
+  ctx: CanvasRenderingContext2D,
+  text: string,
+  fontFace: string,
+  maxWidth: number,
+): number => {
+  ctx.font = `1px ${fontFace}`;
+  return maxWidth / ctx.measureText(text).width || DEFAULT_VALID_SIZE;
+};
diff --git a/src/utils/map/usePointToProjection.ts b/src/utils/map/usePointToProjection.ts
index d0b0066b98d5f73abc92c4cca76dadf86e439ead..b6309fe612213490137220700c650021df38bfcb 100644
--- a/src/utils/map/usePointToProjection.ts
+++ b/src/utils/map/usePointToProjection.ts
@@ -7,7 +7,7 @@ import { useCallback } from 'react';
 import { useSelector } from 'react-redux';
 import { pointToLngLat } from './pointToLatLng';
 
-type UsePointToProjectionResult = (point: Point) => Coordinate;
+export type UsePointToProjectionResult = (point: Point) => Coordinate;
 
 type UsePointToProjection = () => UsePointToProjectionResult;
 
diff --git a/yarn.lock b/yarn.lock
index ef5cdfbf8c29f725a8559cf632cfd1d62d180e80..3ba2ee3e331785c9cd9c4f37c51d09a81789e163 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2149,7 +2149,7 @@
   dependencies:
     "color-name" "~1.1.4"
 
-"color-name@~1.1.4":
+"color-name@^1.1.4", "color-name@~1.1.4":
   "integrity" "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
   "resolved" "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
   "version" "1.1.4"
@@ -2325,6 +2325,11 @@
   "resolved" "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
   "version" "3.0.0"
 
+"cssfontparser@^1.2.1":
+  "integrity" "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg=="
+  "resolved" "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz"
+  "version" "1.2.1"
+
 "cssom@^0.5.0":
   "integrity" "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="
   "resolved" "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz"
@@ -4427,6 +4432,14 @@
     "reflect.getprototypeof" "^1.0.4"
     "set-function-name" "^2.0.1"
 
+"jest-canvas-mock@^2.5.2":
+  "integrity" "sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A=="
+  "resolved" "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz"
+  "version" "2.5.2"
+  dependencies:
+    "cssfontparser" "^1.2.1"
+    "moo-color" "^1.0.2"
+
 "jest-changed-files@^29.7.0":
   "integrity" "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w=="
   "resolved" "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz"
@@ -5353,6 +5366,13 @@
   "resolved" "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz"
   "version" "1.0.4"
 
+"moo-color@^1.0.2":
+  "integrity" "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ=="
+  "resolved" "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz"
+  "version" "1.0.3"
+  dependencies:
+    "color-name" "^1.1.4"
+
 "ms@^2.1.1", "ms@2.1.2":
   "integrity" "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
   "resolved" "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"