Repository: Singular-Genomics/G4X-viewer
Pull request #40: Fix/Scalebar issue
Author: login: pdabrowsky
Labels: name: bug
description: Something isn't working
name: needs review
description: Needs Review
Created: 2025-03-24T09:33:19Z | Merged: 2025-04-29T16:08:37Z

# Pull Request Template

## Issue 

Ticket: Closes #46 

## Description

Place the scale bar at the top and make sure it is not underneath the Transcript file

## Type of change

- [ ] New feature (non-breaking change which adds functionality)
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] Refactor (modifies existing functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update

## How to Test

1. Load Transcript file.
2. Observe Scalebar


Linked issue #46: Scalebar not visible when H&E loaded
---
Scalebar not visible when H&E loaded
---

## Issue Type

- [x] Bug
- [ ] Feature
- [ ] Documentation
- [ ] Other

## Description

The current scroll-bar layer is attached directly to the main OME-Tiff layer therefore all of our custom layers are rendered on top, therefore the scale bar is covered by any additional rendered layers 

Additionally, due to the current design, the scalebar does not have any background and loses all contrast when used when H&E images are loaded 

## Repo Steps

1. Environment setup:

  - G4X Viewer version: 2.4.0
  - OS: macOS 15.3.1
  - Browser: Chrome(133.0.6943.142)

2. Steps to reproduce (for bugs) or implement (for features):
   1. Upload any acceptable OME-Tiff file 
   2. Upload any data for a custom layer (transcripts, H&E Images, cell segmentation)
   3. Zoom in and observe how the scale bar is obscured by either the entire H&E Image, or rendered elements like the cell polygons or transcript points (increase the point size/cell opacity to view the issue more clearly)

![Image](https://github.com/user-attachments/assets/5e2aa88f-5c4b-4a3e-9b40-e1e3ad880e40)

## Expected Behavior

The scale bar should always remain a top layer and not be obscured by any data. Additionally, the scalebar should have it's own semi-transparent background (similar to other image info section available in the viewer) to ensure constant contrast 

## Current Behavior

The scalebar is obscured by custom layers and losses contrast depending on current background image 

Diff:
diff --git a/src/assets/img/scaleBarIcon.svg b/src/assets/img/scaleBarIcon.svg
new file mode 100644
index 00000000..09e4cec6
--- /dev/null
+++ b/src/assets/img/scaleBarIcon.svg
@@ -0,0 +1,5 @@
+<svg width="100" height="20" viewBox="0 0 100 20" xmlns="http://www.w3.org/2000/svg">
+  <line x1="10" y1="5" x2="10" y2="15" stroke="white" strokeWidth="2" />
+  <line x1="10" y1="10" x2="90" y2="10" stroke="white" strokeWidth="2" />
+  <line x1="90" y1="5" x2="90" y2="15" stroke="white" strokeWidth="2" />
+</svg> 
\ No newline at end of file
diff --git a/src/components/DetailView/DetailView.tsx b/src/components/DetailView/DetailView.tsx
new file mode 100644
index 00000000..7151902c
--- /dev/null
+++ b/src/components/DetailView/DetailView.tsx
@@ -0,0 +1,27 @@
+import { OVERVIEW_VIEW_ID, VivView } from '@hms-dbmi/viv';
+import { getImageLayer } from '../../utils/utils';
+import { DetailViewProps } from './DetailView.types';
+
+export const DETAIL_VIEW_ID = 'detail';
+
+export default class DetailView extends VivView {
+  constructor({ id, x = 0, y = 0, height, width }: DetailViewProps) {
+    super({ id, x, y, height, width } as any);
+  }
+
+  getLayers({ props }: any): any {
+    const { id } = this;
+    const layers = [getImageLayer(id, props)];
+    return layers;
+  }
+
+  filterViewState({ viewState, currentViewState }: any): any {
+    if (viewState.id === OVERVIEW_VIEW_ID) {
+      const { target } = viewState;
+      if (target) {
+        return { ...currentViewState, target };
+      }
+    }
+    return super.filterViewState({ viewState } as any);
+  }
+}
diff --git a/src/components/DetailView/DetailView.types.ts b/src/components/DetailView/DetailView.types.ts
new file mode 100644
index 00000000..94ee485c
--- /dev/null
+++ b/src/components/DetailView/DetailView.types.ts
@@ -0,0 +1,32 @@
+import { PixelSource } from '../PictureInPictureViewer/PictureInPictureViewer.types';
+
+export type DetailViewProps = {
+  id: string;
+  x?: number;
+  y?: number;
+  height: number;
+  width: number;
+};
+
+export type ImageLayerProps = {
+  loader: PixelSource[];
+  contrastLimits: number[][];
+  colors: number[][];
+  channelsVisible: boolean[];
+  selections: any[];
+  colormap?: string;
+  lensEnabled?: boolean;
+  lensSelection?: number;
+  lensRadius?: number;
+  lensBorderColor?: [number, number, number];
+  lensBorderRadius?: number;
+  extensions?: any[];
+  transparentColor?: [number, number, number];
+  onViewportLoad?: () => void;
+  [key: string]: any;
+};
+
+export type ViewStateProps = {
+  viewState: any;
+  currentViewState?: any;
+};
diff --git a/src/components/DetailView/index.ts b/src/components/DetailView/index.ts
new file mode 100644
index 00000000..75569723
--- /dev/null
+++ b/src/components/DetailView/index.ts
@@ -0,0 +1,5 @@
+import DetailView, { DETAIL_VIEW_ID } from './DetailView';
+
+export { DETAIL_VIEW_ID, DetailView };
+export * from './DetailView.types';
+export default DetailView;
diff --git a/src/components/ImageInfo/ImageInfo.tsx b/src/components/ImageInfo/ImageInfo.tsx
index 639bddd6..a9b893cf 100644
--- a/src/components/ImageInfo/ImageInfo.tsx
+++ b/src/components/ImageInfo/ImageInfo.tsx
@@ -2,7 +2,7 @@ import { Box, Theme, Typography, alpha, useTheme } from '@mui/material';
 import { useViewerStore } from '../../stores/ViewerStore';
 import { useShallow } from 'zustand/react/shallow';
 import { useChannelsStore } from '../../stores/ChannelsStore';
-
+import { ScaleBar } from '../ScaleBar';
 export const ImageInfo = () => {
   const theme = useTheme();
   const sx = styles(theme);
@@ -16,13 +16,16 @@ export const ImageInfo = () => {
   return (
     <>
       {level && (
-        <Box sx={sx.footerWrapper}>
-          <Typography sx={sx.footerText}>
-            {`Mouse Pos: [${hoverCoordinates.x || '--'}, ${hoverCoordinates.y || '--'}]`}
-          </Typography>
-          <Typography sx={sx.footerText}>{`Layer: ${pyramidResolution + 1}/${loader.length}`}</Typography>
-          <Typography sx={sx.footerText}>{`Shape: ${level.shape.join(', ')}`}</Typography>
-        </Box>
+        <>
+          <Box sx={sx.footerWrapper}>
+            <Typography sx={sx.footerText}>
+              {`Mouse Pos: [${hoverCoordinates.x || '--'}, ${hoverCoordinates.y || '--'}]`}
+            </Typography>
+            <Typography sx={sx.footerText}>{`Layer: ${pyramidResolution + 1}/${loader.length}`}</Typography>
+            <Typography sx={sx.footerText}>{`Shape: ${level.shape.join(', ')}`}</Typography>
+          </Box>
+          <ScaleBar />
+        </>
       )}
     </>
   );
diff --git a/src/components/PictureInPictureViewer/PictureInPictureViewer.tsx b/src/components/PictureInPictureViewer/PictureInPictureViewer.tsx
new file mode 100644
index 00000000..7d4ab43c
--- /dev/null
+++ b/src/components/PictureInPictureViewer/PictureInPictureViewer.tsx
@@ -0,0 +1,105 @@
+import { ColorPaletteExtension } from '@hms-dbmi/viv';
+import { DETAIL_VIEW_ID, OVERVIEW_VIEW_ID, OverviewView, getDefaultInitialViewState, VivViewer } from '@hms-dbmi/viv';
+import * as React from 'react';
+import { PictureInPictureViewerProps } from './PictureInPictureViewer.types';
+import DetailView from '../DetailView';
+
+export default function PictureInPictureViewer(props: PictureInPictureViewerProps) {
+  const {
+    loader,
+    contrastLimits,
+    colors,
+    channelsVisible,
+    viewStates: viewStatesProp,
+    colormap,
+    overview,
+    overviewOn,
+    selections,
+    hoverHooks = { handleValue: () => {}, handleCoordnate: () => {} },
+    height,
+    width,
+    lensEnabled = false,
+    lensSelection = 0,
+    lensRadius = 100,
+    lensBorderColor = [255, 255, 255],
+    lensBorderRadius = 0.02,
+    clickCenter = true,
+    transparentColor,
+    snapScaleBar = false,
+    onViewStateChange,
+    onHover,
+    onViewportLoad,
+    extensions = [new ColorPaletteExtension()],
+    deckProps
+  } = props;
+
+  const detailViewState = viewStatesProp?.find((v) => v.id === DETAIL_VIEW_ID);
+  // biome-ignore lint/correctness/useExhaustiveDependencies: Carried over from eslint, without explanation.
+  const baseViewState = React.useMemo(() => {
+    return detailViewState || getDefaultInitialViewState(loader, { height, width }, 0.5);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [loader, detailViewState]);
+
+  const detailView = new DetailView({
+    id: DETAIL_VIEW_ID,
+    height,
+    width,
+    snapScaleBar
+  } as any);
+
+  const layerConfig = {
+    loader,
+    contrastLimits,
+    colors,
+    channelsVisible,
+    selections,
+    onViewportLoad,
+    colormap,
+    lensEnabled,
+    lensSelection,
+    lensRadius,
+    lensBorderColor,
+    lensBorderRadius,
+    extensions,
+    transparentColor
+  };
+
+  const views = [detailView];
+  const layerProps = [layerConfig];
+  const viewStates = [{ ...baseViewState, id: DETAIL_VIEW_ID }];
+
+  if (overviewOn && loader) {
+    // It's unclear why this is needed because OverviewView.filterViewState sets "zoom" and "target".
+    const overviewViewState = viewStatesProp?.find((v) => v.id === OVERVIEW_VIEW_ID) || {
+      ...baseViewState,
+      id: OVERVIEW_VIEW_ID
+    };
+
+    const overviewView = new OverviewView({
+      id: OVERVIEW_VIEW_ID,
+      loader,
+      detailHeight: height,
+      detailWidth: width,
+      clickCenter,
+      ...overview
+    } as any);
+
+    views.push(overviewView as any);
+    layerProps.push({ ...layerConfig, lensEnabled: false });
+    viewStates.push(overviewViewState as any);
+  }
+
+  if (!loader) return null;
+
+  return (
+    <VivViewer
+      layerProps={layerProps as any}
+      views={views as any}
+      viewStates={viewStates as any}
+      hoverHooks={hoverHooks as any}
+      onViewStateChange={onViewStateChange as any}
+      onHover={onHover as any}
+      deckProps={deckProps}
+    />
+  );
+}
diff --git a/src/components/PictureInPictureViewer/PictureInPictureViewer.types.ts b/src/components/PictureInPictureViewer/PictureInPictureViewer.types.ts
new file mode 100644
index 00000000..dff3ffe4
--- /dev/null
+++ b/src/components/PictureInPictureViewer/PictureInPictureViewer.types.ts
@@ -0,0 +1,71 @@
+import { TiffPixelSource, ZarrPixelSource } from '@hms-dbmi/viv';
+
+// Define PixelSource type
+export type PixelSource = TiffPixelSource<string[]> | ZarrPixelSource<string[]>;
+
+export type SelectionsType = {
+  [x: string]: number | undefined;
+};
+
+export type OverviewSettings = {
+  scale?: number;
+  margin?: number;
+  position?: string;
+  minimumWidth?: number;
+  maximumWidth?: number;
+  boundingBoxColor?: number[];
+  boundingBoxOutlineWidth?: number;
+  viewportOutlineColor?: number[];
+  viewportOutlineWidth?: number;
+};
+
+export type HoverHooks = {
+  handleValue: (valueArray: any) => void;
+  handleCoordnate: (coordinate: any) => void;
+};
+
+export type ViewState = {
+  target: [number, number, number];
+  zoom: number;
+  id: string;
+  [key: string]: any;
+};
+
+export type ViewStateChangeParams = {
+  viewState: any;
+  viewId: string;
+  interactionState: any;
+  oldViewState: any;
+};
+
+export type ViewStateChange = (params: ViewStateChangeParams) => void;
+
+export type Hover = (info: any, event: any) => void;
+
+export type PictureInPictureViewerProps = {
+  contrastLimits: [number, number][];
+  colors: [number, number, number][];
+  channelsVisible: boolean[];
+  colormap?: string;
+  loader: PixelSource[];
+  selections: any[];
+  overview?: OverviewSettings;
+  overviewOn: boolean;
+  hoverHooks?: HoverHooks;
+  viewStates?: ViewState[];
+  height: number;
+  width: number;
+  extensions?: any[];
+  clickCenter?: boolean;
+  lensEnabled?: boolean;
+  lensSelection?: number;
+  lensRadius?: number;
+  lensBorderColor?: [number, number, number];
+  lensBorderRadius?: number;
+  transparentColor?: [number, number, number];
+  snapScaleBar?: boolean;
+  onViewStateChange?: ViewStateChange;
+  onHover?: Hover;
+  onViewportLoad?: () => void;
+  deckProps?: any;
+};
diff --git a/src/components/PictureInPictureViewer/index.ts b/src/components/PictureInPictureViewer/index.ts
new file mode 100644
index 00000000..0e6067d7
--- /dev/null
+++ b/src/components/PictureInPictureViewer/index.ts
@@ -0,0 +1,5 @@
+import PictureInPictureViewer from './PictureInPictureViewer';
+
+export { PictureInPictureViewer };
+export * from './PictureInPictureViewer.types';
+export default PictureInPictureViewer;
diff --git a/src/components/PictureInPictureViewerAdapter/PictureInPictureViewerAdapter.tsx b/src/components/PictureInPictureViewerAdapter/PictureInPictureViewerAdapter.tsx
index ca33bdb4..680c0d21 100644
--- a/src/components/PictureInPictureViewerAdapter/PictureInPictureViewerAdapter.tsx
+++ b/src/components/PictureInPictureViewerAdapter/PictureInPictureViewerAdapter.tsx
@@ -1,10 +1,4 @@
-import {
-  AdditiveColormapExtension,
-  DETAIL_VIEW_ID,
-  getDefaultInitialViewState,
-  LensExtension,
-  PictureInPictureViewer
-} from '@hms-dbmi/viv';
+import { AdditiveColormapExtension, DETAIL_VIEW_ID, getDefaultInitialViewState, LensExtension } from '@hms-dbmi/viv';
 import { useChannelsStore } from '../../stores/ChannelsStore/ChannelsStore';
 import { useShallow } from 'zustand/react/shallow';
 import { DEFAULT_OVERVIEW, FILL_PIXEL_VALUE } from '../../shared/constants';
@@ -22,6 +16,7 @@ import { debounce } from 'lodash';
 import { useBrightfieldImagesStore } from '../../stores/BrightfieldImagesStore';
 import PhotoCameraIcon from '@mui/icons-material/PhotoCamera';
 import { useSnackbar } from 'notistack';
+import PictureInPictureViewer from '../PictureInPictureViewer';
 
 export const PictureInPictureViewerAdapter = () => {
   const getLoader = useChannelsStore((store) => store.getLoader);
@@ -36,6 +31,8 @@ export const PictureInPictureViewerAdapter = () => {
   const deckGLRef = useRef<any>(null);
   const { enqueueSnackbar } = useSnackbar();
 
+  const debouncedUpdateRef = useRef<any>(null);
+
   const theme = useTheme();
   const sx = styles(theme);
 
@@ -63,13 +60,37 @@ export const PictureInPictureViewerAdapter = () => {
     ])
   );
 
+  useEffect(() => {
+    debouncedUpdateRef.current = debounce(
+      (zoom: number) => {
+        const z = Math.min(Math.max(Math.round(-zoom), 0), loader.length - 1);
+        useViewerStore.setState({
+          pyramidResolution: z
+        });
+      },
+      250,
+      { trailing: true }
+    );
+
+    return () => {
+      if (debouncedUpdateRef.current) {
+        debouncedUpdateRef.current.cancel();
+      }
+    };
+  }, [loader.length]);
+
   useEffect(() => {
     if (!viewState && containerSize.width && containerSize.height) {
       const width = containerSize.width;
       const height = containerSize.height;
       const defualtViewerState = getDefaultInitialViewState(loader, { width, height }, 0.5);
+
       useViewerStore.setState({
-        viewState: { ...defualtViewerState, id: DETAIL_VIEW_ID }
+        viewState: {
+          ...defualtViewerState,
+          id: DETAIL_VIEW_ID,
+          width
+        }
       });
     }
     // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -150,35 +171,35 @@ export const PictureInPictureViewerAdapter = () => {
             colormap={colormap}
             onViewportLoad={onViewportLoad}
             viewStates={viewState ? [viewState] : []}
-            onViewStateChange={debounce(
-              ({ viewState: newViewState, viewId }) => {
-                const z = Math.min(Math.max(Math.round(-(newViewState as any).zoom), 0), loader.length - 1);
-                useViewerStore.setState({
-                  pyramidResolution: z,
-                  viewState: { ...newViewState, id: viewId }
-                });
-              },
-              250,
-              { trailing: true }
-            )}
-            hoverHooks={{
-              handleValue: (values) =>
-                useViewerStore.setState({
-                  pixelValues: values.map((value) =>
-                    Number.isInteger(value) ? value.toFixed(1).toString() : FILL_PIXEL_VALUE
-                  )
-                }),
-              // @ts-expect-error Error in Viv jsDOC declaration.
-              // TODO: Fix when issue has beeen resolved and new version has been released.
-              handleCoordnate: (coords: number[]) =>
-                coords &&
-                useViewerStore.setState({
-                  hoverCoordinates: {
-                    x: coords[0].toFixed(0).toString(),
-                    y: coords[1].toFixed(0).toString()
-                  }
-                })
+            onViewStateChange={({ viewState: newViewState, viewId }: { viewState: any; viewId: any }) => {
+              // Update the viewState immediately
+              useViewerStore.setState({
+                viewState: { ...newViewState, id: viewId }
+              });
+
+              // Update the pyramidResolution with debounce
+              if (debouncedUpdateRef.current) {
+                debouncedUpdateRef.current((newViewState as any).zoom);
+              }
             }}
+            hoverHooks={
+              {
+                handleValue: (values: any) =>
+                  useViewerStore.setState({
+                    pixelValues: values.map((value: any) =>
+                      Number.isInteger(value) ? value.toFixed(1).toString() : FILL_PIXEL_VALUE
+                    )
+                  }),
+                handleCoordnate: (coords: number[]) =>
+                  coords &&
+                  useViewerStore.setState({
+                    hoverCoordinates: {
+                      x: coords[0].toFixed(0).toString(),
+                      y: coords[1].toFixed(0).toString()
+                    }
+                  })
+              } as any
+            }
           />
           <MuiTooltip title="Screenshot">
             <IconButton
diff --git a/src/components/PictureInPictureViewerAdapter/PictureInPictureViewerAdapter.types.ts b/src/components/PictureInPictureViewerAdapter/PictureInPictureViewerAdapter.types.ts
index 4966339d..fb14e90b 100644
--- a/src/components/PictureInPictureViewerAdapter/PictureInPictureViewerAdapter.types.ts
+++ b/src/components/PictureInPictureViewerAdapter/PictureInPictureViewerAdapter.types.ts
@@ -1,8 +1,8 @@
 import { TiffPixelSource, ZarrPixelSource } from '@hms-dbmi/viv';
 
 export type PictureInPictureViewerConfig = {
-  contrastLimits: number[][];
-  colors: number[][];
+  contrastLimits: [number, number][];
+  colors: [number, number, number][];
   channelsVisible: boolean[];
   loader: PixelSource[];
   selections: SelectionsType[];
diff --git a/src/components/ScaleBar/ScaleBar.tsx b/src/components/ScaleBar/ScaleBar.tsx
new file mode 100644
index 00000000..f6ca6b17
--- /dev/null
+++ b/src/components/ScaleBar/ScaleBar.tsx
@@ -0,0 +1,64 @@
+import { Box, Typography, alpha, useTheme, Theme } from '@mui/material';
+import { useShallow } from 'zustand/react/shallow';
+import { useViewerStore } from '../../stores/ViewerStore/ViewerStore';
+import { useChannelsStore } from '../../stores/ChannelsStore';
+import scaleBarIcon from '../../assets/img/scaleBarIcon.svg';
+
+export function ScaleBar() {
+  const theme = useTheme();
+  const sx = styles(theme);
+  const viewState = useViewerStore(useShallow((store) => store.viewState));
+  const getLoader = useChannelsStore((store) => store.getLoader);
+
+  const loader = getLoader();
+  const physicalSize = loader[0]?.meta?.physicalSizes?.x;
+
+  const { zoom, width } = viewState || { zoom: 0, width: 0 };
+  const scale = Math.pow(2, zoom);
+  const viewLength = width / scale;
+  const barLength = viewLength * 0.05;
+  const displayNumber = barLength * (physicalSize?.size || 1);
+
+  const formattedNumber = displayNumber < 1 ? displayNumber.toFixed(1) : Math.round(displayNumber);
+
+  if (!displayNumber) return null;
+
+  return (
+    <Box sx={sx.container}>
+      <Box sx={sx.iconContainer}>
+        <img
+          src={scaleBarIcon}
+          alt="scale bar"
+        />
+      </Box>
+      <Typography
+        sx={sx.text}
+        fontSize="14px"
+      >
+        {formattedNumber} {physicalSize?.unit || 'µm'}
+      </Typography>
+    </Box>
+  );
+}
+
+const styles = (theme: Theme) => ({
+  container: {
+    position: 'absolute',
+    right: 55,
+    bottom: 50,
+    backgroundColor: alpha(theme.palette.gx.primary.black, 0.5),
+    padding: '4px 8px',
+    borderRadius: '10px',
+    zIndex: 10,
+    display: 'flex',
+    flexDirection: 'column',
+    alignItems: 'center'
+  },
+  iconContainer: {
+    display: 'flex',
+    justifyContent: 'center'
+  },
+  text: {
+    color: theme.palette.gx.primary.white
+  }
+});
diff --git a/src/components/ScaleBar/index.ts b/src/components/ScaleBar/index.ts
new file mode 100644
index 00000000..bf4c4857
--- /dev/null
+++ b/src/components/ScaleBar/index.ts
@@ -0,0 +1 @@
+export * from './ScaleBar';
diff --git a/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/AddChannel/AddChannel.tsx b/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/AddChannel/AddChannel.tsx
index d517e6ce..65fcc059 100644
--- a/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/AddChannel/AddChannel.tsx
+++ b/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/AddChannel/AddChannel.tsx
@@ -41,7 +41,7 @@ export const AddChannel = () => {
     }).then(({ domain, contrastLimits }) => {
       setPropertiesForChannel(numSelectionsBeforeAdd, {
         domains: domain,
-        contrastLimits,
+        contrastLimits: contrastLimits as [number, number],
         channelsVisible: true
       });
       useViewerStore.setState({
diff --git a/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelController.tsx b/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelController.tsx
index 2442e3f5..3f94d859 100644
--- a/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelController.tsx
+++ b/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelController.tsx
@@ -57,7 +57,7 @@ export const ChannelController = ({
         </GxSelect>
         <Box>
           <ChannelOptions
-            handleColorSelect={handleColorSelect}
+            handleColorSelect={handleColorSelect as any}
             disabled={isLoading}
           />
           <Tooltip
diff --git a/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelController.types.ts b/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelController.types.ts
index d9b9edae..65518def 100644
--- a/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelController.types.ts
+++ b/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelController.types.ts
@@ -1,13 +1,13 @@
 export type ChannelControllerProps = {
-  color: number[];
+  color: [number, number, number];
   name: string;
   isLoading: boolean;
   pixelValue: string;
   channelVisible: boolean;
-  slider: number[];
+  slider: [number, number];
   toggleIsOn: () => void;
   onSelectionChange: (newValue: string) => void;
-  handleColorSelect: (newColor: number[]) => void;
-  handleSliderChange: (newValue: number[]) => void;
+  handleColorSelect: (newColor: [number, number, number]) => void;
+  handleSliderChange: (newValue: [number, number]) => void;
   handleRemoveChannel: () => void;
 };
diff --git a/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelRangeSlider/ChannelRangeSlider.tsx b/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelRangeSlider/ChannelRangeSlider.tsx
index 4dc7f45a..1a0ff4e6 100644
--- a/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelRangeSlider/ChannelRangeSlider.tsx
+++ b/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelRangeSlider/ChannelRangeSlider.tsx
@@ -39,7 +39,7 @@ export const ChannelRangeSlider = ({ color, slider, handleSliderChange, isLoadin
               ? CHANNEL_MIN
               : +currentValue;
         setMinInputValue(newValue.toString());
-        handleSliderChange([newValue, currentMaxValue]);
+        handleSliderChange([newValue, currentMaxValue] as [number, number]);
       }, DEBOUNCE_TIME_MS),
     [currentMaxValue, handleSliderChange]
   );
@@ -63,7 +63,7 @@ export const ChannelRangeSlider = ({ color, slider, handleSliderChange, isLoadin
               ? CHANNEL_MAX
               : +currentValue;
         setMaxInputValue(newValue.toString());
-        handleSliderChange([currentMinValue, newValue]);
+        handleSliderChange([currentMinValue, newValue] as [number, number]);
       }, DEBOUNCE_TIME_MS),
     [currentMinValue, handleSliderChange]
   );
@@ -100,7 +100,7 @@ export const ChannelRangeSlider = ({ color, slider, handleSliderChange, isLoadin
       <GxSlider
         disabled={isLoading}
         value={slider}
-        onChange={(_, newValue) => handleSliderChange(Array.isArray(newValue) ? newValue : [newValue])}
+        onChange={(_, newValue) => handleSliderChange(newValue as [number, number])}
         valueLabelFormat={(v) => truncateDecimalNumber(v, 5)}
         min={CHANNEL_MIN}
         max={CHANNEL_MAX}
diff --git a/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelRangeSlider/ChannelRangeSlider.types.ts b/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelRangeSlider/ChannelRangeSlider.types.ts
index 518eab60..f759ada1 100644
--- a/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelRangeSlider/ChannelRangeSlider.types.ts
+++ b/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelController/ChannelRangeSlider/ChannelRangeSlider.types.ts
@@ -1,6 +1,6 @@
 export type ChannelRangeSliderProps = {
-  slider: number[];
-  color: number[];
-  handleSliderChange: (newValue: number[]) => void;
+  slider: [number, number];
+  color: [number, number, number];
+  handleSliderChange: (newValue: [number, number]) => void;
   isLoading?: boolean;
 };
diff --git a/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelControllers.tsx b/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelControllers.tsx
index f5094ba6..14553e4f 100644
--- a/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelControllers.tsx
+++ b/src/components/ViewController/ChannelsSettingsSection/ChannelControllers/ChannelControllers.tsx
@@ -80,15 +80,15 @@ export const ChannelControllers = () => {
               channelsSettings[channelName].maxValue
             ) {
               const settings = channelsSettings[channelName];
-              newProps.contrastLimits = [settings.minValue, settings.maxValue] as number[];
+              newProps.contrastLimits = [settings.minValue, settings.maxValue] as [number, number];
             } else {
-              newProps.contrastLimits = newContrastLimit;
+              newProps.contrastLimits = newContrastLimit as [number, number];
             }
 
             if (channelName in channelsSettings && channelsSettings[channelName].color) {
-              newProps.colors = channelsSettings[channelName].color;
+              newProps.colors = channelsSettings[channelName].color as [number, number, number];
             } else if (Channels[c].Color) {
-              newProps.colors = Channels[c].Color.slice(0, -1);
+              newProps.colors = Channels[c].Color.slice(0, -1) as [number, number, number];
             }
 
             newProps.domains = domain;
@@ -109,14 +109,14 @@ export const ChannelControllers = () => {
           removeIsChannelLoading(index);
         };
 
-        const handleColorSelect = (color: number[]) => {
+        const handleColorSelect = (color: [number, number, number]) => {
           if (name in channelsSettings) {
             channelsSettings[name].color = color;
           }
           setPropertiesForChannel(index, { colors: color });
         };
 
-        const handleSliderChange = (newValue: number[]) => {
+        const handleSliderChange = (newValue: [number, number]) => {
           if (name in channelsSettings) {
             channelsSettings[name].minValue = newValue[0];
             channelsSettings[name].maxValue = newValue[1];
@@ -135,11 +135,11 @@ export const ChannelControllers = () => {
               channelVisible={channelsVisible[index]}
               pixelValue={pixelValues[index]}
               toggleIsOn={toggleIsOn}
-              color={colors[index]}
+              color={colors[index] as [number, number, number]}
               isLoading={isChannelLoading[index]}
               handleColorSelect={handleColorSelect}
               handleRemoveChannel={handleRemoveChannel}
-              slider={contrastLimits[index]}
+              slider={contrastLimits[index] as [number, number]}
               handleSliderChange={handleSliderChange}
             />
           </Box>
diff --git a/src/stores/ChannelsStore/ChannelsStore.types.ts b/src/stores/ChannelsStore/ChannelsStore.types.ts
index 0815bea9..4c7fb069 100644
--- a/src/stores/ChannelsStore/ChannelsStore.types.ts
+++ b/src/stores/ChannelsStore/ChannelsStore.types.ts
@@ -3,8 +3,8 @@ import { SelectionsType } from '../../components/PictureInPictureViewerAdapter/P
 export type ChannelsStore = ChannelsStoreValues & ChannelsStoreMethods;
 
 export type ChannelsStoreValues = {
-  contrastLimits: number[][];
-  colors: number[][];
+  contrastLimits: [number, number][];
+  colors: [number, number, number][];
   channelsVisible: boolean[];
   channelsSettings: ChannelsSettings;
   selections: SelectionsType[];
@@ -23,8 +23,8 @@ export type ChannelsStoreMethods = {
 };
 
 export type PropertiesUpdateType = {
-  contrastLimits?: number[];
-  colors?: number[];
+  contrastLimits?: [number, number];
+  colors?: [number, number, number];
   domains?: number[];
   selections?: SelectionsType;
   channelsVisible?: boolean;
@@ -40,7 +40,7 @@ export type ChannelsSettings = {
 };
 
 export type ChannelSettings = {
-  color?: number[];
+  color?: [number, number, number];
   maxValue?: number;
   minValue?: number;
 };
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index b613268e..f166a3fe 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -1,3 +1,5 @@
+import { ImageLayer, MultiscaleImageLayer } from '@vivjs/layers';
+
 export const getVivId = (id: string) => {
   return `-#${id}#`;
 };
@@ -19,3 +21,20 @@ export const parseJsonFromFile = (file: File) => {
     reader.readAsText(file);
   });
 };
+
+export function getImageLayer(id: string, props: any) {
+  const { loader } = props;
+  // Grab name of PixelSource if a class instance (works for Tiff & Zarr).
+  const sourceName = loader[0]?.constructor?.name;
+
+  // Create at least one layer even without selections so that the tests pass.
+  const Layer = loader.length > 1 ? MultiscaleImageLayer : ImageLayer;
+  const layerLoader = loader.length > 1 ? loader : loader[0];
+
+  return new Layer({
+    ...props,
+    id: `${sourceName}${getVivId(id)}`,
+    viewportId: id,
+    loader: layerLoader
+  });
+}


Reviews:
user: login: matzduniuk
body: Great job, u small update but not fully required
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-16T15:46:10Z
user: login: gouinK
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T13:07:03Z
user: login: matzduniuk
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-28T08:04:22Z

Review comments:
diff_hunk: @@ -0,0 +1,71 @@
+import { TiffPixelSource, ZarrPixelSource } from '@hms-dbmi/viv';
+
+// Define PixelSource type
+export type PixelSource = TiffPixelSource<string[]> | ZarrPixelSource<string[]>;
+
+export type SelectionsType = {
+  [x: string]: number | undefined;
+};
+
+export type OverviewSettings = {
+  scale?: number;
+  margin?: number;
+  position?: string;
+  minimumWidth?: number;
+  maximumWidth?: number;
+  boundingBoxColor?: number[];
+  boundingBoxOutlineWidth?: number;
+  viewportOutlineColor?: number[];
+  viewportOutlineWidth?: number;
+};
+
+export type HoverHooks = {
+  handleValue: (valueArray: any) => void;
+  handleCoordnate: (coordinate: any) => void;
+};
+
+export type ViewState = {
+  target: [number, number, number];
+  zoom: number;
+  id: string;
+  [key: string]: any;
+};
+
+export type ViewStateChangeParams = {
+  viewState: any;
+  viewId: string;
+  interactionState: any;
+  oldViewState: any;
+};
+
+export type ViewStateChange = (params: ViewStateChangeParams) => void;
+
+export type Hover = (info: any, event: any) => void;
+
+export type PictureInPictureViewerProps = {
+  contrastLimits: number[][] | [number, number][];
+  colors: number[][] | [number, number, number][];
path: src/components/PictureInPictureViewer/PictureInPictureViewer.types.ts
user: login: matzduniuk
body: Let's limit this definition to just  `[number, number, number][]` to enforce 3 values for RGB colors
created_at: 2025-04-16T15:37:05Z
author_association: COLLABORATOR
diff_hunk: @@ -134,8 +155,8 @@ export const PictureInPictureViewerAdapter = () => {
       {containerSize.width && containerSize.height && (
         <>
           <PictureInPictureViewer
-            contrastLimits={contrastLimits}
-            colors={colors}
+            contrastLimits={contrastLimits as any}
+            colors={colors as any}
path: src/components/PictureInPictureViewerAdapter/PictureInPictureViewerAdapter.tsx
user: login: matzduniuk
body: If we adjust the types, as described in the comment above, can we get rid of the `any` type assertion 
created_at: 2025-04-16T15:42:20Z
author_association: COLLABORATOR

PR comments:
user: login: gouinK
created_at: 2025-04-08T11:15:48Z
body: Is this supposed to fix the scalebar display when an H&E image is uploaded?
author_association: CONTRIBUTOR
user: login: pdabrowsky
created_at: 2025-04-08T12:58:27Z
body: @gouinK Yes, I am still working on implementing a different solution suggested by Mateusz - to move the entire PictureInPicture viewer from Viv into the project instead of importing it.
author_association: COLLABORATOR
user: login: gouinK
created_at: 2025-04-16T03:27:48Z
body: The scalebar is so smooth now! Looks great!
Can we adjust the size of the scalebar so it is does not take up so much space in the viewer? It that controllable?
author_association: CONTRIBUTOR
user: login: pdabrowsky
created_at: 2025-04-16T07:57:01Z
body: @gouinK Done, let me know if this is enough. Yes, we can do anything with the scalebar now :)
author_association: COLLABORATOR
user: login: gouinK
created_at: 2025-04-17T13:06:55Z
body: Looks good!
author_association: CONTRIBUTOR