From 7e70b4a5239d38ce2f3f6e56df28af6470d16cba Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Thu, 3 Mar 2022 14:06:37 +0100
Subject: [PATCH] WIP: receptor POC: connectviity matrix viewer refactor: some
 stores

---
 .storybook/preview-head.html                  |   5 +
 .../sapi/core/sapiParcellation.ts             |  20 ++-
 src/atlasComponents/sapi/index.ts             |   3 +
 src/atlasComponents/sapi/sapi.service.ts      |  82 ++++++++-
 src/atlasComponents/sapi/schema.ts            |  59 +++++-
 src/atlasComponents/sapi/stories.base.ts      |   5 +
 src/atlasComponents/sapi/type.ts              |   8 +
 .../connectivityMatrix.component.ts           |  87 +++++++++
 .../connectivityMatrix.style.css              |   0
 .../connectivityMatrix.template.html          |   0
 .../exampleConnectivity.stories.ts            | 105 +++++++++++
 .../sapiViews/features/connectivity/index.ts  |   0
 .../sapiViews/features/connectivity/module.ts |  21 +++
 .../autoradiography/autoradiograph.stories.ts | 121 +++++++++++++
 .../autoradiography.component.ts              |  72 +++++++-
 .../autoradiography/autoradiography.style.css |   5 +
 .../sapiViews/features/receptors/base.ts      |  33 ++--
 .../fingerprint/fingerprint.component.ts      | 103 ++++++++++-
 .../fingerprint/fingerprint.stories.ts        |  48 +++--
 .../fingerprint/fingerprint.template.html     |   5 +-
 .../sapiViews/features/receptors/module.ts    |  20 ++-
 .../userAnnotations/tools/service.ts          |  11 +-
 .../atlasViewer.apiService.service.ts         |   6 -
 src/messaging/nmvSwc/index.ts                 |   8 +-
 src/services/state/userConfigState.store.ts   |  26 +--
 .../state/viewerConfig.store.helper.ts        |   8 +-
 src/services/state/viewerConfig.store.ts      |  10 --
 src/services/state/viewerState/actions.ts     |   5 -
 src/services/stateStore.service.ts            |   2 -
 src/state/userInteraction/actions.ts          |  13 +-
 src/state/userInteraction/selectors.ts        |   8 +-
 src/state/userInteraction/store.ts            |  24 ++-
 src/state/userInterface/actions.ts            |  12 +-
 src/state/userInterface/selectors.ts          |   4 +-
 src/state/userInterface/store.ts              |  20 +--
 src/ui/config/configCmp/config.component.ts   |  12 +-
 src/util/pureConstant.service.ts              |  12 +-
 src/viewerModule/nehuba/util.ts               |   4 +-
 .../viewerCmp/viewerCmp.component.ts          |   6 +-
 worker/worker-typedarray.js                   | 169 +++++++++++++++++-
 worker/worker.js                              |  52 +++++-
 41 files changed, 1051 insertions(+), 163 deletions(-)
 create mode 100644 .storybook/preview-head.html
 create mode 100644 src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.component.ts
 create mode 100644 src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.style.css
 create mode 100644 src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.template.html
 create mode 100644 src/atlasComponents/sapiViews/features/connectivity/exampleConnectivity.stories.ts
 create mode 100644 src/atlasComponents/sapiViews/features/connectivity/index.ts
 create mode 100644 src/atlasComponents/sapiViews/features/connectivity/module.ts
 create mode 100644 src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiograph.stories.ts

diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html
new file mode 100644
index 000000000..c581803c8
--- /dev/null
+++ b/.storybook/preview-head.html
@@ -0,0 +1,5 @@
+<script>
+  console.log("./storybook/preview-head.html working properly")
+</script>
+<script src="https://unpkg.com/kg-dataset-previewer@1.2.0/dist/kg-dataset-previewer/kg-dataset-previewer.js" defer></script>
+<script src="main.bundle.js" defer></script>
diff --git a/src/atlasComponents/sapi/core/sapiParcellation.ts b/src/atlasComponents/sapi/core/sapiParcellation.ts
index 05e367a54..7add33499 100644
--- a/src/atlasComponents/sapi/core/sapiParcellation.ts
+++ b/src/atlasComponents/sapi/core/sapiParcellation.ts
@@ -1,6 +1,6 @@
 import { SapiVolumeModel } from ".."
 import { SAPI } from "../sapi.service"
-import { SapiParcellationModel, SapiRegionModel } from "../type"
+import { SapiParcellationFeatureModel, SapiParcellationModel, SapiRegionModel } from "../type"
 
 export class SAPIParcellation{
   constructor(private sapi: SAPI, public atlasId: string, public id: string){
@@ -26,4 +26,22 @@ export class SAPIParcellation{
       `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/volumes`
     )
   }
+
+  getFeatures(): Promise<SapiParcellationFeatureModel[]> {
+    return this.sapi.http.get<SapiParcellationFeatureModel[]>(
+      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features`,
+      {
+        params: {
+          per_page: 5,
+          page: 0,
+        }
+      }
+    ).toPromise()
+  }
+
+  getFeatureInstance(instanceId: string): Promise<SapiParcellationFeatureModel> {
+    return this.sapi.http.get<SapiParcellationFeatureModel>(
+      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`,
+    ).toPromise()
+  }
 }
diff --git a/src/atlasComponents/sapi/index.ts b/src/atlasComponents/sapi/index.ts
index 092ebd6e9..17d2f7cb2 100644
--- a/src/atlasComponents/sapi/index.ts
+++ b/src/atlasComponents/sapi/index.ts
@@ -12,6 +12,9 @@ export {
   SapiSpatialFeatureModel
 } from "./type"
 
+import { SapiRegionalFeatureModel, SapiSpatialFeatureModel } from "./type"
+export type SapiFeatureModel = SapiRegionalFeatureModel | SapiSpatialFeatureModel
+
 export { SAPI } from "./sapi.service"
 export {
   SAPIAtlas,
diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts
index 5d1572ca4..07fb77d15 100644
--- a/src/atlasComponents/sapi/sapi.service.ts
+++ b/src/atlasComponents/sapi/sapi.service.ts
@@ -4,11 +4,12 @@ import { BS_ENDPOINT } from 'src/util/constants';
 import { map, shareReplay, take, tap } from "rxjs/operators";
 import { SAPIAtlas, SAPISpace } from './core'
 import { SapiAtlasModel, SapiParcellationModel, SapiRegionalFeatureModel, SapiRegionModel, SapiSpaceModel, SpyNpArrayDataModel } from "./type";
-import { CachedFunction } from "src/util/fn";
+import { CachedFunction, getExportNehuba } from "src/util/fn";
 import { SAPIParcellation } from "./core/sapiParcellation";
 import { SAPIRegion } from "./core/sapiRegion"
 import { MatSnackBar } from "@angular/material/snack-bar";
 import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service";
+import { EnumColorMapName } from "src/util/colorMaps";
 
 export const SIIBRA_API_VERSION_HEADER_KEY='x-siibra-api-version'
 export const SIIBRA_API_VERSION = '0.2.0'
@@ -133,8 +134,8 @@ export class SAPI{
       }
     })
   }
-
-  async processNpArrayData(input: SpyNpArrayDataModel) {
+  
+  async processNpArrayData<T extends keyof ProcessTypedArrayResult>(input: SpyNpArrayDataModel, method: PARSE_TYPEDARRAY = PARSE_TYPEDARRAY.RAW_ARRAY, params: ProcessTypedArrayResult[T]['input'] = null): Promise<ProcessTypedArrayResult[T]['output']> {
 
     const supportedDtype = [
       "uint8",
@@ -163,17 +164,84 @@ export class SAPI{
 
     try {
       const bin = atob(content)
-      const { pako } = (window as any).export_nehuba
+      const { pako } = getExportNehuba()
       const array = pako.inflate(bin)
-      this.workerSvc.sendMessage({
-        method: "",
+      let workerMsg: string
+      switch (method) {
+      case PARSE_TYPEDARRAY.CANVAS_FORTRAN_RGBA: {
+        workerMsg = "PROCESS_TYPED_ARRAY_F2RGBA"
+        break
+      }
+      case PARSE_TYPEDARRAY.CANVAS_COLORMAP_RGBA: {
+        workerMsg = "PROCESS_TYPED_ARRAY_CM2RGBA"
+        break
+      }
+      case PARSE_TYPEDARRAY.RAW_ARRAY: {
+        workerMsg = "PROCESS_TYPED_ARRAY_RAW"
+        break
+      }
+      default:{
+        throw new Error(`sapi.service#decodeNpArrayDataModel: method cannot be deciphered: ${method}`)
+      }
+      }
+      const { result } = await this.workerSvc.sendMessage({
+        method: workerMsg,
         param: {
-          
+          inputArray: array,
+          width,
+          height,
+          channel,
+          dtype,
+          processParams: params
         },
         transfers: [ array.buffer ]
       })
+      const { buffer, outputArray, min, max } = result
+      return {
+        type: method,
+        result: buffer,
+        rawArray: outputArray,
+        min,
+        max
+      }
     } catch (e) {
       throw new Error(`sapi.service#decodeNpArrayDataModel error: ${e}`)
     }
   }
 }
+
+export enum PARSE_TYPEDARRAY {
+  CANVAS_FORTRAN_RGBA="CANVAS_FORTRAN_RGBA",
+  CANVAS_COLORMAP_RGBA="CANVAS_COLORMAP_RGBA",
+  RAW_ARRAY="RAW_ARRAY",
+}
+
+type ProcessTypedArrayResult = {
+  [PARSE_TYPEDARRAY.CANVAS_FORTRAN_RGBA]: {
+    input: null
+    output: {
+      type: PARSE_TYPEDARRAY
+      result: Uint8ClampedArray
+    }
+  }
+  [PARSE_TYPEDARRAY.CANVAS_COLORMAP_RGBA]: {
+    input?: {
+      colormap?: EnumColorMapName
+      log?: boolean
+    }
+    output: {
+      type: PARSE_TYPEDARRAY
+      result: Uint8ClampedArray
+      max: number
+      min: number
+    }
+  }
+  [PARSE_TYPEDARRAY.RAW_ARRAY]: {
+    input: null
+    output: {
+      rawArray: number[][]
+      min: number
+      max: number
+    }
+  }
+}
diff --git a/src/atlasComponents/sapi/schema.ts b/src/atlasComponents/sapi/schema.ts
index c1f52d82e..9fb39e5ff 100644
--- a/src/atlasComponents/sapi/schema.ts
+++ b/src/atlasComponents/sapi/schema.ts
@@ -197,11 +197,11 @@ export interface components {
       "@id": string;
       /** Name */
       name: string;
-      matrix: components["schemas"]["NpArrayDataModel"];
-      /** Columns */
-      columns: string[];
       /** Parcellations */
       parcellations: { [key: string]: string }[];
+      matrix?: components["schemas"]["NpArrayDataModel"];
+      /** Columns */
+      columns?: string[];
     };
     /** DatasetJsonModel */
     DatasetJsonModel: {
@@ -330,6 +330,17 @@ export interface components {
         [key: string]: components["schemas"]["IEEGElectrodeModel"];
       };
     };
+    /** NeurotransmitterMarkupModel */
+    NeurotransmitterMarkupModel: {
+      /** Latex */
+      latex: string;
+      /** Markdown */
+      markdown: string;
+      /** Name */
+      name: string;
+      /** Label */
+      label: string;
+    };
     /** NiiMetadataModel */
     NiiMetadataModel: {
       /** Min */
@@ -428,6 +439,10 @@ export interface components {
       fingerprints: {
         [key: string]: components["schemas"]["FingerPrintDataModel"];
       };
+      /** Receptor Symbols */
+      receptor_symbols: {
+        [key: string]: components["schemas"]["SymbolMarkupClass"];
+      };
     };
     /** ReceptorDatasetModel */
     ReceptorDatasetModel: {
@@ -443,6 +458,15 @@ export interface components {
       urls: components["schemas"]["Url"][];
       data?: components["schemas"]["ReceptorDataModel"];
     };
+    /** ReceptorMarkupModel */
+    ReceptorMarkupModel: {
+      /** Latex */
+      latex: string;
+      /** Markdown */
+      markdown: string;
+      /** Name */
+      name: string;
+    };
     /** RelationAssessmentItem */
     RelationAssessmentItem: {
       /**
@@ -604,6 +628,16 @@ export interface components {
        */
       versionIdentifier: string;
     };
+    /** SerializationErrorModel */
+    SerializationErrorModel: {
+      /**
+       * Type
+       * @constant
+       */
+      type?: "spy/serialization-error";
+      /** Message */
+      message: string;
+    };
     /** SiibraAtIdModel */
     SiibraAtIdModel: {
       /** @Id */
@@ -661,6 +695,11 @@ export interface components {
       /** Kgv1Id */
       kgV1Id: string;
     };
+    /** SymbolMarkupClass */
+    SymbolMarkupClass: {
+      receptor: components["schemas"]["ReceptorMarkupModel"];
+      neurotransmitter: components["schemas"]["NeurotransmitterMarkupModel"];
+    };
     /** Url */
     Url: {
       /** Doi */
@@ -1448,7 +1487,10 @@ export interface operations {
       /** Successful Response */
       200: {
         content: {
-          "application/json": components["schemas"]["ConnectivityMatrixDataModel"];
+          "application/json": Partial<
+            components["schemas"]["ConnectivityMatrixDataModel"]
+          > &
+            Partial<components["schemas"]["SerializationErrorModel"]>;
         };
       };
       /** Validation Error */
@@ -1466,12 +1508,19 @@ export interface operations {
         atlas_id: string;
         parcellation_id: string;
       };
+      query: {
+        per_page?: number;
+        page?: number;
+      };
     };
     responses: {
       /** Successful Response */
       200: {
         content: {
-          "application/json": components["schemas"]["ConnectivityMatrixDataModel"][];
+          "application/json": (Partial<
+            components["schemas"]["ConnectivityMatrixDataModel"]
+          > &
+            Partial<components["schemas"]["SerializationErrorModel"]>)[];
         };
       };
       /** Validation Error */
diff --git a/src/atlasComponents/sapi/stories.base.ts b/src/atlasComponents/sapi/stories.base.ts
index a9501454d..56f0d88df 100644
--- a/src/atlasComponents/sapi/stories.base.ts
+++ b/src/atlasComponents/sapi/stories.base.ts
@@ -1,6 +1,7 @@
 import { forkJoin } from "rxjs"
 import { map, switchMap, withLatestFrom } from "rxjs/operators"
 import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionalFeatureModel, SapiRegionModel, SapiSpaceModel } from "."
+import { SapiParcellationFeatureModel } from "./type"
 
 /**
  * base class used to generate wrapper class for storybook
@@ -89,3 +90,7 @@ export async function getHoc1Left(): Promise<SapiRegionModel> {
 export async function getHoc1Features(): Promise<SapiRegionalFeatureModel[]> {
   return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20left/features`)).json()
 }
+
+export async function getJba29Features(): Promise<SapiParcellationFeatureModel[]> {
+  return await (await fetch(`${SAPI.bsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/features`)).json()
+}
diff --git a/src/atlasComponents/sapi/type.ts b/src/atlasComponents/sapi/type.ts
index dcb890b9a..87fddd2c6 100644
--- a/src/atlasComponents/sapi/type.ts
+++ b/src/atlasComponents/sapi/type.ts
@@ -39,12 +39,20 @@ export const guards = {
     && val.data.detail["neuroglancer/precomputed"]
 }
 
+/**
+ * serialization error type
+ */
+export type SapiSerializationErrorModel = components["schemas"]["SerializationErrorModel"]
+
 /**
  * datafeatures
  */
 export type SapiRegionalFeatureReceptorModel = components["schemas"]["ReceptorDatasetModel"]
 export type SapiRegionalFeatureModel = components["schemas"]["BaseDatasetJsonModel"] | SapiRegionalFeatureReceptorModel
 
+export type SapiParcellationFeatureMatrixModel = components["schemas"]["ConnectivityMatrixDataModel"]
+export type SapiParcellationFeatureModel = SapiParcellationFeatureMatrixModel | SapiSerializationErrorModel
+
 export function guardPipe<
   InputType,
   GuardType extends InputType
diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.component.ts b/src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.component.ts
new file mode 100644
index 000000000..69cd8b70d
--- /dev/null
+++ b/src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.component.ts
@@ -0,0 +1,87 @@
+import { AfterViewInit, Component, ElementRef, Input, OnChanges, SimpleChanges } from "@angular/core";
+import { SAPI, SapiAtlasModel, SapiParcellationModel } from "src/atlasComponents/sapi";
+import { PARSE_TYPEDARRAY } from "src/atlasComponents/sapi/sapi.service";
+import { SapiParcellationFeatureMatrixModel, SapiSerializationErrorModel } from "src/atlasComponents/sapi/type";
+import { EnumColorMapName } from "src/util/colorMaps";
+
+@Component({
+  selector: 'sxplr-sapiviews-features-connectivity-matrix',
+  templateUrl: './connectivityMatrix.template.html',
+  styleUrls: [
+    `./connectivityMatrix.style.css`
+  ]
+})
+
+export class ConnectivityMatrixView implements OnChanges, AfterViewInit{
+
+  @Input('sxplr-sapiviews-features-connectivity-matrix-atlas')
+  atlas: SapiAtlasModel
+  
+  @Input('sxplr-sapiviews-features-connectivity-matrix-parcellation')
+  parcellation: SapiParcellationModel
+
+  @Input('sxplr-sapiviews-features-connectivity-matrix-featureid')
+  featureId: string
+
+  matrixData: SapiParcellationFeatureMatrixModel
+  private pleaseRender = false
+  private renderBuffer: Uint8ClampedArray
+  width: number
+  height: number
+  
+  private async fetchMatrixData(){
+    this.matrixData = null
+    const matrix = await this.sapi.getParcellation(this.atlas["@id"], this.parcellation["@id"]).getFeatureInstance(this.featureId)
+    if ((matrix as SapiSerializationErrorModel)?.type === "spy/serialization-error") {
+      return
+    }
+    this.matrixData = matrix as SapiParcellationFeatureMatrixModel
+    this.width = this.matrixData.matrix["x-width"]
+    this.height = this.matrixData.matrix["x-height"]
+  }
+
+  ngAfterViewInit(): void {
+    if (this.pleaseRender) {
+      this.renderCanvas()
+    }
+  }
+
+  async ngOnChanges(changes: SimpleChanges) {
+    await this.fetchMatrixData()
+    const { result, max, min } = await this.sapi.processNpArrayData<PARSE_TYPEDARRAY.CANVAS_COLORMAP_RGBA>(this.matrixData.matrix, PARSE_TYPEDARRAY.CANVAS_COLORMAP_RGBA, { colormap: EnumColorMapName.JET })
+    const rawResult = await this.sapi.processNpArrayData<PARSE_TYPEDARRAY.RAW_ARRAY>(this.matrixData.matrix, PARSE_TYPEDARRAY.RAW_ARRAY)
+    console.log({
+      rawResult
+    })
+    this.renderBuffer = result
+    this.renderCanvas()
+  }
+
+  private renderCanvas(){
+
+    if (!this.el) {
+      this.pleaseRender = true
+      return
+    }
+
+    const arContainer = (this.el.nativeElement as HTMLElement)
+    while (arContainer.firstChild) {
+      arContainer.removeChild(arContainer.firstChild)
+    }
+
+    const canvas = document.createElement("canvas")
+    canvas.height = this.height
+    canvas.width = this.width
+    arContainer.appendChild(canvas)
+    const ctx = canvas.getContext("2d")
+    const imgData = ctx.createImageData(this.width, this.height)
+    imgData.data.set(this.renderBuffer)
+    ctx.putImageData(imgData, 0, 0)
+    this.pleaseRender = false
+  }
+
+  constructor(private sapi: SAPI, private el: ElementRef){
+
+  }
+
+}
\ No newline at end of file
diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.style.css b/src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.style.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.template.html b/src/atlasComponents/sapiViews/features/connectivity/connectivityMatrix/connectivityMatrix.template.html
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/atlasComponents/sapiViews/features/connectivity/exampleConnectivity.stories.ts b/src/atlasComponents/sapiViews/features/connectivity/exampleConnectivity.stories.ts
new file mode 100644
index 000000000..94bf2a984
--- /dev/null
+++ b/src/atlasComponents/sapiViews/features/connectivity/exampleConnectivity.stories.ts
@@ -0,0 +1,105 @@
+import { CommonModule } from "@angular/common"
+import { HttpClientModule } from "@angular/common/http"
+import { Component } from "@angular/core"
+import { FormsModule } from "@angular/forms"
+import { BrowserAnimationsModule } from "@angular/platform-browser/animations"
+import { Meta, moduleMetadata, Story } from "@storybook/angular"
+import { SAPI, SapiAtlasModel, SapiParcellationModel } from "src/atlasComponents/sapi"
+import { getJba29Features, getHumanAtlas, getJba29 } from "src/atlasComponents/sapi/stories.base"
+import { SapiParcellationFeatureModel } from "src/atlasComponents/sapi/type"
+import { AngularMaterialModule } from "src/sharedModules"
+import { ConnectivityMatrixView } from "./connectivityMatrix/connectivityMatrix.component"
+
+@Component({
+  selector: 'autoradiograph-wrapper-cmp',
+  template: `
+  <mat-form-field appearance="fill">
+    <mat-select [(ngModel)]="featureId">
+      <mat-option value="null" disabled>
+        --select--
+      </mat-option>
+
+      <mat-option [value]="feat['@id']"
+        *ngFor="let feat of features">
+        {{ feat.name }}
+      </mat-option>
+    </mat-select>
+  </mat-form-field>
+  <sxplr-sapiviews-features-connectivity-matrix
+    class="d-inline-block w-100 h-100"
+    *ngIf="featureId"
+    [sxplr-sapiviews-features-connectivity-matrix-atlas]="atlas"
+    [sxplr-sapiviews-features-connectivity-matrix-parcellation]="parcellation"
+    [sxplr-sapiviews-features-connectivity-matrix-featureid]="featureId"
+  >
+  </sxplr-sapiviews-features-connectivity-matrix>
+  `,
+  styles: [
+    `
+    :host
+    {
+      display: block;
+      max-width: 60rem;
+      max-height: 60rem;
+    }
+    `
+  ]
+})
+class ExampleConnectivityMatrixWrapper {
+  atlas: SapiAtlasModel
+  parcellation: SapiParcellationModel
+  features: SapiParcellationFeatureModel[] = []
+  featureId: string
+}
+
+export default {
+  component: ExampleConnectivityMatrixWrapper,
+  decorators: [
+    moduleMetadata({
+      imports: [
+        CommonModule,
+        AngularMaterialModule,
+        HttpClientModule,
+        BrowserAnimationsModule,
+        FormsModule,
+      ],
+      providers: [
+        SAPI
+      ],
+      declarations: [
+        ConnectivityMatrixView
+      ]
+    })
+  ],
+} as Meta
+
+const Template: Story<ExampleConnectivityMatrixWrapper> = (args: ExampleConnectivityMatrixWrapper, { loaded }) => {
+  const { atlas, parc, features } = loaded
+  return ({
+    props: {
+      ...args,
+      atlas: atlas,
+      parcellation: parc,
+      features
+    },
+  })
+}
+
+Template.loaders = [
+  async () => {
+    const atlas = await getHumanAtlas()
+    const parc = await getJba29()
+    const features = await getJba29Features()
+    return {
+      atlas, parc, features
+    }
+  }
+]
+
+export const Default = Template.bind({})
+Default.args = {
+
+}
+Default.loaders = [
+  ...Template.loaders
+]
\ No newline at end of file
diff --git a/src/atlasComponents/sapiViews/features/connectivity/index.ts b/src/atlasComponents/sapiViews/features/connectivity/index.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/atlasComponents/sapiViews/features/connectivity/module.ts b/src/atlasComponents/sapiViews/features/connectivity/module.ts
new file mode 100644
index 000000000..745df380c
--- /dev/null
+++ b/src/atlasComponents/sapiViews/features/connectivity/module.ts
@@ -0,0 +1,21 @@
+import { CommonModule } from "@angular/common";
+import { NgModule } from "@angular/core";
+import { SAPI } from "src/atlasComponents/sapi";
+import { ConnectivityMatrixView } from "./connectivityMatrix/connectivityMatrix.component";
+
+@NgModule({
+  imports: [
+    CommonModule,
+  ],
+  declarations: [
+    ConnectivityMatrixView,
+  ],
+  exports: [
+    ConnectivityMatrixView,
+  ],
+  providers: [
+    SAPI,
+  ]
+})
+
+export class SapiViewsFeatureConnectivityModule{}
\ No newline at end of file
diff --git a/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiograph.stories.ts b/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiograph.stories.ts
new file mode 100644
index 000000000..c8c33226c
--- /dev/null
+++ b/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiograph.stories.ts
@@ -0,0 +1,121 @@
+import { CommonModule } from "@angular/common"
+import { HttpClientModule } from "@angular/common/http"
+import { Component, ViewChild } from "@angular/core"
+import { FormsModule } from "@angular/forms"
+import { BrowserAnimationsModule } from "@angular/platform-browser/animations"
+import { Meta, moduleMetadata, Story } from "@storybook/angular"
+import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"
+import { getHoc1Features, getHoc1Left, getHumanAtlas, getJba29, getMni152, HumanHoc1StoryBase } from "src/atlasComponents/sapi/stories.base"
+import { AngularMaterialModule } from "src/sharedModules"
+import { Autoradiography } from "./autoradiography.component"
+
+@Component({
+  selector: 'autoradiograph-wrapper-cmp',
+  template: `
+  <mat-form-field appearance="fill">
+    <mat-select [(ngModel)]="selectedSymbol">
+      <mat-option value="" disabled>
+        --select--
+      </mat-option>
+
+      <mat-option [value]="option"
+        *ngFor="let option of options">
+        {{ option }}
+      </mat-option>
+    </mat-select>
+  </mat-form-field>
+  <sxplr-sapiviews-features-receptor-autoradiograph
+    class="d-inline-block w-100 h-100"
+    [sxplr-sapiviews-features-receptor-atlas]="atlas"
+    [sxplr-sapiviews-features-receptor-parcellation]="parcellation"
+    [sxplr-sapiviews-features-receptor-template]="template"
+    [sxplr-sapiviews-features-receptor-region]="region"
+    [sxplr-sapiviews-features-receptor-featureid]="featureId"
+    [sxplr-sapiviews-features-receptor-autoradiograph-selected-symbol]="selectedSymbol"
+  >
+  </sxplr-sapiviews-features-receptor-autoradiograph>
+  `,
+  styles: [
+    `
+    :host
+    {
+      display: block;
+      max-width: 24rem;
+      max-height: 24rem;
+    }
+    `
+  ]
+})
+class AutoRadiographWrapperCls {
+  atlas: SapiAtlasModel
+  parcellation: SapiParcellationModel
+  template: SapiSpaceModel
+  region: SapiRegionModel
+  featureId: string
+
+  @ViewChild(Autoradiography)
+  ar: Autoradiography
+
+  selectedSymbol: string
+
+  get options(){
+    return Object.keys(this.ar?.receptorData?.data?.autoradiographs || {})
+  }
+}
+
+export default {
+  component: AutoRadiographWrapperCls,
+  decorators: [
+    moduleMetadata({
+      imports: [
+        CommonModule,
+        AngularMaterialModule,
+        HttpClientModule,
+        BrowserAnimationsModule,
+        FormsModule,
+      ],
+      providers: [
+        SAPI
+      ],
+      declarations: [
+        Autoradiography
+      ]
+    })
+  ],
+} as Meta
+
+const Template: Story<AutoRadiographWrapperCls> = (args: AutoRadiographWrapperCls, { loaded }) => {
+  const { atlas, parc, space, region, receptorfeat } = loaded
+  return ({
+    props: {
+      ...args,
+      atlas: atlas,
+      parcellation: parc,
+      template: space,
+      region: region,
+      featureId: receptorfeat["@id"]
+    },
+  })
+}
+
+Template.loaders = [
+  async () => {
+    const atlas = await getHumanAtlas()
+    const parc = await getJba29()
+    const region = await getHoc1Left()
+    const space = await getMni152()
+    const features = await getHoc1Features()
+    const receptorfeat = features.find(f => f.type === "siibra/receptor")
+    return {
+      atlas, parc, space, region, receptorfeat
+    }
+  }
+]
+
+export const Default = Template.bind({})
+Default.args = {
+
+}
+Default.loaders = [
+  ...Template.loaders
+]
\ No newline at end of file
diff --git a/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.component.ts b/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.component.ts
index f7d764069..8e6c52293 100644
--- a/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.component.ts
+++ b/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.component.ts
@@ -1,11 +1,77 @@
-import { Component } from "@angular/core";
+import { AfterViewInit, Component, ElementRef, Input, OnChanges, SimpleChanges } from "@angular/core";
+import { SAPI } from "src/atlasComponents/sapi";
+import { PARSE_TYPEDARRAY } from "src/atlasComponents/sapi/sapi.service";
 import { BaseReceptor } from "../base";
 
 @Component({
+  selector: `sxplr-sapiviews-features-receptor-autoradiograph`,
   templateUrl: './autoradiography.template.html',
   styleUrls: [
     './autoradiography.style.css'
-  ]
+  ],
+  exportAs: 'sxplrSapiviewsFeaturesReceptorAR'
 })
 
-export class Autoradiography extends BaseReceptor{}
\ No newline at end of file
+export class Autoradiography extends BaseReceptor implements OnChanges, AfterViewInit{
+  
+  @Input('sxplr-sapiviews-features-receptor-autoradiograph-selected-symbol')
+  selectedSymbol: string
+
+  private pleaseRender = false
+
+  width: number
+  height: number
+  renderBuffer: Uint8ClampedArray
+
+  async ngOnChanges(simpleChanges: SimpleChanges) {
+    
+    if (this.baseInputChanged(simpleChanges)) {
+      await this.fetchReceptorData()
+    }
+
+    if (this.selectedSymbol) {
+      const fp = this.receptorData.data.autoradiographs[this.selectedSymbol]
+      if (!fp) {
+        this.error = `selectedSymbol ${this.selectedSymbol} cannot be found. Valid symbols are ${Object.keys(this.receptorData.data.autoradiographs)}`
+        return
+      }
+      const { "x-width": width, "x-height": height } = fp
+      
+      this.width = width
+      this.height = height
+
+      const { result } = await this.sapi.processNpArrayData<PARSE_TYPEDARRAY.CANVAS_FORTRAN_RGBA>(fp, PARSE_TYPEDARRAY.CANVAS_FORTRAN_RGBA)
+      this.renderBuffer = result
+      this.renderCanvas()
+    }
+  }
+  constructor(sapi: SAPI, private el: ElementRef){
+    super(sapi)
+  }
+
+  ngAfterViewInit(): void {
+    if (this.pleaseRender) this.renderCanvas()
+  }
+
+  private renderCanvas(){
+    if (!this.el) {
+      this.pleaseRender = true
+      return
+    }
+
+    const arContainer = (this.el.nativeElement as HTMLElement)
+    while (arContainer.firstChild) {
+      arContainer.removeChild(arContainer.firstChild)
+    }
+
+    const canvas = document.createElement("canvas")
+    canvas.height = this.height
+    canvas.width = this.width
+    arContainer.appendChild(canvas)
+    const ctx = canvas.getContext("2d")
+    const imgData = ctx.createImageData(this.width, this.height)
+    imgData.data.set(this.renderBuffer)
+    ctx.putImageData(imgData, 0, 0)
+    this.pleaseRender = false
+  }
+}
diff --git a/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.style.css b/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.style.css
index e69de29bb..833dd425a 100644
--- a/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.style.css
+++ b/src/atlasComponents/sapiViews/features/receptors/autoradiography/autoradiography.style.css
@@ -0,0 +1,5 @@
+:host >>> canvas
+{
+  max-width: 100%;
+  max-height: 100%;
+}
\ No newline at end of file
diff --git a/src/atlasComponents/sapiViews/features/receptors/base.ts b/src/atlasComponents/sapiViews/features/receptors/base.ts
index b627e844b..2d0a047b2 100644
--- a/src/atlasComponents/sapiViews/features/receptors/base.ts
+++ b/src/atlasComponents/sapiViews/features/receptors/base.ts
@@ -1,30 +1,44 @@
-import { Input } from "@angular/core";
+import { Input, SimpleChanges } from "@angular/core";
 import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi";
 import { SapiRegionalFeatureReceptorModel } from "src/atlasComponents/sapi/type";
 
 export class BaseReceptor{
   
-  @Input('sxplor-sapiviews-features-receptor-atlas')
+  @Input('sxplr-sapiviews-features-receptor-atlas')
   atlas: SapiAtlasModel
   
-  @Input('sxplor-sapiviews-features-receptor-parcellation')
+  @Input('sxplr-sapiviews-features-receptor-parcellation')
   parcellation: SapiParcellationModel
 
-  @Input('sxplor-sapiviews-features-receptor-template')
+  @Input('sxplr-sapiviews-features-receptor-template')
   template: SapiSpaceModel
   
-  @Input('sxplor-sapiviews-features-receptor-region')
+  @Input('sxplr-sapiviews-features-receptor-region')
   region: SapiRegionModel
 
-  @Input('sxplor-sapiviews-features-receptor-featureid')
+  @Input('sxplr-sapiviews-features-receptor-featureid')
   featureId: string
 
   receptorData: SapiRegionalFeatureReceptorModel
 
   error: string
 
-  async ngOnChanges(){
-    console.log("ngOnchanges called", this)
+  protected baseInputChanged(simpleChanges: SimpleChanges) {
+    const {
+      atlas,
+      parcellation,
+      template,
+      region,
+      featureId,
+    } = simpleChanges
+    return atlas
+      || parcellation
+      || template
+      || region
+      || featureId
+  }
+
+  protected async fetchReceptorData() {
     this.error = null
     if (!this.atlas) {
       this.error = `atlas needs to be defined, but is not`
@@ -42,18 +56,15 @@ export class BaseReceptor{
       this.error = `featureId needs to be defined, but is not`
       return
     }
-    console.log("fetching!")
     const result = await this.sapi.getRegion(this.atlas["@id"], this.parcellation["@id"], this.region.name).getFeatureInstance(this.featureId, this.template["@id"])
     if (result.type !== "siibra/receptor") {
       throw new Error(`BaseReceptor Error. Expected .type to be "siibra/receptor", but was "${result.type}"`)
     }
     this.receptorData = result
-    console.log(this.receptorData)
   }
 
   constructor(
     protected sapi: SAPI
   ){
-    console.log('constructor called')
   }
 }
diff --git a/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.component.ts b/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.component.ts
index f1788bcca..3fd3f1760 100644
--- a/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.component.ts
+++ b/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.component.ts
@@ -1,7 +1,44 @@
-import { Component, OnChanges } from "@angular/core";
+import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, OnChanges, OnDestroy, Output, SimpleChanges, ViewChild } from "@angular/core";
+import { fromEvent, Subscription } from "rxjs";
+import { distinctUntilChanged, map } from "rxjs/operators";
 import { SAPI } from "src/atlasComponents/sapi";
+import { SapiRegionalFeatureReceptorModel } from "src/atlasComponents/sapi/type";
 import { BaseReceptor } from "../base";
 
+/**
+ * kg-dataset-dumb-radar requires input to be a different shape
+ * once the the return signature
+ */
+type RequiredType = {
+  receptor: {
+    label: string
+  }
+  density: {
+    mean: number
+    sd: number
+    unit: string
+  }
+}
+
+function transformRadar(input: SapiRegionalFeatureReceptorModel['data']['fingerprints']): RequiredType[]{
+  const listRequired: RequiredType[] = []
+  for (const key in input) {
+    const item = input[key]
+    listRequired.push({
+      receptor: {
+        label: key
+      },
+      density: {
+        mean: item.mean,
+        sd: item.std,
+        unit: item.unit
+      }
+    })
+    
+  }
+  return listRequired
+}
+
 @Component({
   selector: 'sxplr-sapiviews-features-receptor-fingerprint',
   templateUrl: './fingerprint.template.html',
@@ -10,14 +47,66 @@ import { BaseReceptor } from "../base";
   ]
 })
 
-export class Fingerprint extends BaseReceptor implements OnChanges{
+export class Fingerprint extends BaseReceptor implements OnChanges, AfterViewInit, OnDestroy{
+
+  @Output('sxplr-sapiviews-features-receptor-fingerprint-receptor-selected')
+  selectReceptor = new EventEmitter<string>()
+
+  @HostListener('click')
+  onClick(){
+    if (this.mouseOverReceptor) {
+      this.selectReceptor.emit(this.mouseOverReceptor)
+    }
+  }
 
-  async ngOnChanges() {
-    console.log('ng on changes claled onc hild')
-    await super.ngOnChanges()
+  async ngOnChanges(simpleChanges: SimpleChanges) {
+    if (this.baseInputChanged(simpleChanges)) {
+      await this.fetchReceptorData()
+    }
+    if (this.receptorData) {
+      this.setDumbRadar()
+    }
   }
-  constructor(sapi: SAPI){
+  constructor(sapi: SAPI, private el: ElementRef){
     super(sapi)
-    console.log(this.atlas)
+  }
+
+  get dumbRadarCmp(){
+    return this.el?.nativeElement?.querySelector('kg-dataset-dumb-radar')
+  }
+
+  private setDumbRadarPlease = false
+  private sub: Subscription[] = []
+  private mouseOverReceptor: string
+
+  ngOnDestroy(){
+    while (this.sub.length > 0) this.sub.pop().unsubscribe()
+  }
+
+  ngAfterViewInit(){
+    if (this.setDumbRadarPlease) {
+      this.setDumbRadar()
+    }
+
+    this.sub.push(
+      fromEvent<CustomEvent>(this.el.nativeElement, 'kg-ds-prv-regional-feature-mouseover').pipe(
+        map(ev => ev.detail?.data?.receptor?.label),
+        distinctUntilChanged(),
+      ).subscribe(label => {
+        this.mouseOverReceptor = label
+      })
+    )
+  }
+  
+  setDumbRadar(){
+    if (!this.dumbRadarCmp) {
+      this.setDumbRadarPlease = true
+      return
+    }
+    
+    this.dumbRadarCmp.metaBs = this.receptorData.data.receptor_symbols
+    this.dumbRadarCmp.radar= transformRadar(this.receptorData.data.fingerprints)
+
+    this.setDumbRadarPlease = false
   }
 }
diff --git a/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.stories.ts b/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.stories.ts
index fbb8ffb74..014a06246 100644
--- a/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.stories.ts
+++ b/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.stories.ts
@@ -1,27 +1,36 @@
-import { CommonModule } from "@angular/common"
+import { CommonModule, DOCUMENT } from "@angular/common"
 import { HttpClientModule } from "@angular/common/http"
-import { Component, OnDestroy } from "@angular/core"
+import { Component, EventEmitter, Output } from "@angular/core"
 import { Meta, moduleMetadata, Story } from "@storybook/angular"
-import { forkJoin, Observable, Observer, Subscription } from "rxjs"
-import { map, switchMap } from "rxjs/operators"
 import { SAPI, SapiAtlasModel, SapiParcellationModel, SapiRegionModel, SapiSpaceModel } from "src/atlasComponents/sapi"
 import { getHoc1Features, getHoc1Left, getHumanAtlas, getJba29, getMni152, HumanHoc1StoryBase } from "src/atlasComponents/sapi/stories.base"
 import { AngularMaterialModule } from "src/sharedModules"
-import { BaseReceptor } from "../base"
-import { Fingerprint } from "./fingerprint.component"
+import { appendScriptFactory, APPEND_SCRIPT_TOKEN } from "src/util/constants"
+import { ReceptorViewModule } from ".."
 
 @Component({
   selector: 'fingerprint-wrapper-cmp',
   template: `
   <sxplr-sapiviews-features-receptor-fingerprint
-    [sxplor-sapiviews-features-receptor-atlas]="atlas"
-    [sxplor-sapiviews-features-receptor-parcellation]="parcellation"
-    [sxplor-sapiviews-features-receptor-template]="template"
-    [sxplor-sapiviews-features-receptor-region]="region"
-    [sxplor-sapiviews-features-receptor-featureid]="featureId"
+    [sxplr-sapiviews-features-receptor-atlas]="atlas"
+    [sxplr-sapiviews-features-receptor-parcellation]="parcellation"
+    [sxplr-sapiviews-features-receptor-template]="template"
+    [sxplr-sapiviews-features-receptor-region]="region"
+    [sxplr-sapiviews-features-receptor-featureid]="featureId"
+    (sxplr-sapiviews-features-receptor-fingerprint-receptor-selected)="selectReceptor.emit($event)"
   >
   </sxplr-sapiviews-features-receptor-fingerprint>
-  `
+  `,
+  styles: [
+    `
+    :host
+    {
+      display: block;
+      width: 20rem;
+      height: 20rem;
+    }
+    `
+  ]
 })
 class FingerprintWrapperCls {
   atlas: SapiAtlasModel
@@ -30,6 +39,8 @@ class FingerprintWrapperCls {
   region: SapiRegionModel
   featureId: string
 
+  @Output()
+  selectReceptor = new EventEmitter()
 }
 
 export default {
@@ -40,20 +51,23 @@ export default {
         CommonModule,
         AngularMaterialModule,
         HttpClientModule,
+        ReceptorViewModule,
       ],
       providers: [
-        SAPI
+        SAPI,
+        {
+          provide: APPEND_SCRIPT_TOKEN,
+          useFactory: appendScriptFactory,
+          deps: [ DOCUMENT ]
+        }
       ],
-      declarations: [
-        Fingerprint
-      ]
+      declarations: []
     })
   ],
 } as Meta
 
 const Template: Story<FingerprintWrapperCls> = (args: FingerprintWrapperCls, { loaded }) => {
   const { atlas, parc, space, region, receptorfeat } = loaded
-  console.log({ atlas, parc, space, region, receptorfeat })
   return ({
     props: {
       ...args,
diff --git a/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.template.html b/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.template.html
index 70cc01214..1588598e3 100644
--- a/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.template.html
+++ b/src/atlasComponents/sapiViews/features/receptors/fingerprint/fingerprint.template.html
@@ -1,5 +1,6 @@
 <div *ngIf="error">
   {{ error }}
 </div>
-<!-- <kg-dataset-dumb-radar [attr.kg-ds-prv-darkmode]="true">
-</kg-dataset-dumb-radar> -->
\ No newline at end of file
+
+<kg-dataset-dumb-radar>
+</kg-dataset-dumb-radar>
\ No newline at end of file
diff --git a/src/atlasComponents/sapiViews/features/receptors/module.ts b/src/atlasComponents/sapiViews/features/receptors/module.ts
index 4fdeaf55c..a1229981a 100644
--- a/src/atlasComponents/sapiViews/features/receptors/module.ts
+++ b/src/atlasComponents/sapiViews/features/receptors/module.ts
@@ -1,5 +1,6 @@
 import { CommonModule } from "@angular/common";
-import { NgModule } from "@angular/core";
+import { APP_INITIALIZER, NgModule } from "@angular/core";
+import { APPEND_SCRIPT_TOKEN } from "src/util/constants";
 import { Autoradiography } from "./autoradiography/autoradiography.component";
 import { Fingerprint } from "./fingerprint/fingerprint.component"
 import { Profile } from "./profile/profile.component"
@@ -17,7 +18,22 @@ import { Profile } from "./profile/profile.component"
     Autoradiography,
     Fingerprint,
     Profile,
-  ]
+  ],
+  providers: [{
+    provide: APP_INITIALIZER,
+    multi: true,
+    useFactory: (appendScriptFn: (url: string) => Promise<any>) => {
+
+      const libraries = [
+        'https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js',
+        'https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.1.2/es5/tex-svg.js'
+      ]
+      return () => Promise.all(libraries.map(appendScriptFn))
+    },
+    deps: [
+      APPEND_SCRIPT_TOKEN
+    ]
+  }]
 })
 
 export class ReceptorViewModule{}
diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts
index c6f075adc..546cd1238 100644
--- a/src/atlasComponents/userAnnotations/tools/service.ts
+++ b/src/atlasComponents/userAnnotations/tools/service.ts
@@ -7,7 +7,7 @@ import {map, switchMap, filter, shareReplay, pairwise } from "rxjs/operators";
 import { NehubaViewerUnit } from "src/viewerModule/nehuba";
 import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util";
 import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TCallbackFunction, TSands, TGeometryJson, TNgAnnotationLine, TCallback } from "./type";
-import { switchMapWaitFor } from "src/util/fn";
+import { getExportNehuba, switchMapWaitFor } from "src/util/fn";
 import { Polygon } from "./poly";
 import { Line } from "./line";
 import { Point } from "./point";
@@ -583,14 +583,14 @@ export class ModularUserAnnotationToolService implements OnDestroy{
     const bin = atob(encoded)
     
     await retry(() => {
-      if (!!(window as any).export_nehuba) return true
+      if (!!getExportNehuba()) return true
       else throw new Error(`export nehuba not yet ready`)
     }, {
       timeout: 1000,
       retries: 10
     })
     
-    const { pako } = (window as any).export_nehuba
+    const { pako } = getExportNehuba()
     const decoded = pako.inflate(bin, { to: 'string' })
     const arr = JSON.parse(decoded)
     const anns: IAnnotationGeometry[] = []
@@ -626,8 +626,9 @@ export class ModularUserAnnotationToolService implements OnDestroy{
       arr.push(json)
     }
     const stringifiedJSON = JSON.stringify(arr)
-    if (!(window as any).export_nehuba) return
-    const { pako } = (window as any).export_nehuba
+    const exportNehuba = getExportNehuba()
+    if (!exportNehuba) return
+    const { pako } = exportNehuba
     const compressed = pako.deflate(stringifiedJSON)
     let out = ''
     for (const num of compressed) {
diff --git a/src/atlasViewer/atlasViewer.apiService.service.ts b/src/atlasViewer/atlasViewer.apiService.service.ts
index 48906afed..f4529026d 100644
--- a/src/atlasViewer/atlasViewer.apiService.service.ts
+++ b/src/atlasViewer/atlasViewer.apiService.service.ts
@@ -231,11 +231,6 @@ export class AtlasViewerAPIServices implements OnDestroy{
 
         layersRegionLabelIndexMap: new Map(),
 
-        datasetsBSubject : this.store.pipe(
-          select('dataStore'),
-          select('fetchedDataEntries'),
-          startWith([])
-        ),
       },
       uiHandle : {
         getModalHandler : () => {
@@ -369,7 +364,6 @@ export interface IInteractiveViewerInterface {
     loadedTemplates: any[]
     regionsLabelIndexMap: Map<number, any> | null
     layersRegionLabelIndexMap: Map<string, Map<number, any>>
-    datasetsBSubject: Observable<any[]>
   }
 
   viewerHandle?: IVIewerHandle
diff --git a/src/messaging/nmvSwc/index.ts b/src/messaging/nmvSwc/index.ts
index 08b0ce699..2f4e75869 100644
--- a/src/messaging/nmvSwc/index.ts
+++ b/src/messaging/nmvSwc/index.ts
@@ -1,5 +1,5 @@
 import { Observable, Subject } from "rxjs"
-import { getUuid } from "src/util/fn"
+import { getExportNehuba, getUuid } from "src/util/fn"
 import { IMessagingActions, IMessagingActionTmpl, TVec4, TMat4 } from "../types"
 import { INmvTransform } from "./type"
 
@@ -110,7 +110,7 @@ export const processJsonLd = (json: { [key: string]: any }): Observable<IMessagi
       }
     })
 
-    await waitFor(() => !!(window as any).export_nehuba)
+    await waitFor(() => !!getExportNehuba())
 
     const b64Encoded = encoding.indexOf('base64') >= 0
     const isGzipped = encoding.indexOf('gzip') >= 0
@@ -119,7 +119,7 @@ export const processJsonLd = (json: { [key: string]: any }): Observable<IMessagi
       data = atob(data)
     }
     if (isGzipped) {
-      data = (window as any).export_nehuba.pako.inflate(data)
+      data = getExportNehuba().pako.inflate(data)
     }
     let output = ``
     for (let i = 0; i < data.length; i++) {
@@ -146,7 +146,7 @@ export const processJsonLd = (json: { [key: string]: any }): Observable<IMessagi
     ]
     // NG translation works on nm scale
     const scaleUmToNm = 1e3
-    const { mat3, vec3 } = (window as any).export_nehuba
+    const { mat3, vec3 } = getExportNehuba()
     const modA = mat3.fromValues(
       scaleUmToVoxelFixed[0], 0, 0,
       0, scaleUmToVoxelFixed[1], 0,
diff --git a/src/services/state/userConfigState.store.ts b/src/services/state/userConfigState.store.ts
index 4c869cd8f..f5906c02c 100644
--- a/src/services/state/userConfigState.store.ts
+++ b/src/services/state/userConfigState.store.ts
@@ -1,5 +1,5 @@
 import { Injectable, OnDestroy } from "@angular/core";
-import { Actions, Effect, ofType } from "@ngrx/effects";
+import { Actions, createEffect, Effect, ofType } from "@ngrx/effects";
 import { Action, createAction, createReducer, props, select, Store, on, createSelector } from "@ngrx/store";
 import { of, Subscription } from "rxjs";
 import { catchError, filter, map } from "rxjs/operators";
@@ -7,8 +7,8 @@ import { LOCAL_STORAGE_CONST } from "src/util//constants";
 // Get around the problem of importing duplicated string (ACTION_TYPES), even using ES6 alias seems to trip up the compiler
 // TODO file bug and reverse
 import { HttpClient } from "@angular/common/http";
-import { actionSetMobileUi } from "./viewerState/actions";
 import { PureContantService } from "src/util";
+import * as stateCtrl from "src/state"
 
 interface ICsp{
   'connect-src'?: string[]
@@ -94,6 +94,13 @@ export class UserConfigStateUseEffect implements OnDestroy {
 
   private subscriptions: Subscription[] = []
 
+  storeUseMobileInLocalStorage = createEffect(() => this.actions$.pipe(
+    ofType(stateCtrl.userInterface.actions.useModileUi),
+    map(({ flag }) => {
+      window.localStorage.setItem(LOCAL_STORAGE_CONST.MOBILE_UI, JSON.stringify(flag))
+    })
+  ), { dispatch: false })
+
   constructor(
     private actions$: Actions,
     private store$: Store<any>,
@@ -114,21 +121,6 @@ export class UserConfigStateUseEffect implements OnDestroy {
         }
       }),
     )
-
-    this.subscriptions.push(
-      this.actions$.pipe(
-        ofType(actionSetMobileUi.type),
-        map((action: any) => {
-          const { payload } = action
-          const { useMobileUI } = payload
-          return useMobileUI
-        }),
-        filter(bool => bool !== null),
-      ).subscribe((bool: boolean) => {
-        window.localStorage.setItem(LOCAL_STORAGE_CONST.MOBILE_UI, JSON.stringify(bool))
-      }),
-    )
-
   }
 
   public ngOnDestroy() {
diff --git a/src/services/state/viewerConfig.store.helper.ts b/src/services/state/viewerConfig.store.helper.ts
index 719b47459..7dc4a3c6c 100644
--- a/src/services/state/viewerConfig.store.helper.ts
+++ b/src/services/state/viewerConfig.store.helper.ts
@@ -7,7 +7,7 @@ export interface IViewerConfigState {
   useMobileUI: boolean
 }
 
-export const viewerConfigSelectorUseMobileUi = createSelector(
-  state => state[VIEWER_CONFIG_FEATURE_KEY],
-  viewerConfigState => viewerConfigState.useMobileUI
-)
+// export const viewerConfigSelectorUseMobileUi = createSelector(
+//   state => state[VIEWER_CONFIG_FEATURE_KEY],
+//   viewerConfigState => viewerConfigState.useMobileUI
+// )
diff --git a/src/services/state/viewerConfig.store.ts b/src/services/state/viewerConfig.store.ts
index 27850b946..650ba69c8 100644
--- a/src/services/state/viewerConfig.store.ts
+++ b/src/services/state/viewerConfig.store.ts
@@ -2,7 +2,6 @@ import { Action } from "@ngrx/store";
 import { LOCAL_STORAGE_CONST } from "src/util/constants";
 
 import { IViewerConfigState as StateInterface } from './viewerConfig.store.helper'
-import { actionSetMobileUi } from "./viewerState/actions";
 export { StateInterface }
 
 interface ViewerConfigurationAction extends Action {
@@ -23,7 +22,6 @@ export const CONFIG_CONSTANTS = {
 export const VIEWER_CONFIG_ACTION_TYPES = {
   SET_ANIMATION: `SET_ANIMATION`,
   UPDATE_CONFIG: `UPDATE_CONFIG`,
-  SET_MOBILE_UI: actionSetMobileUi.type,
 }
 
 // get gpu limit
@@ -58,14 +56,6 @@ export const defaultState: StateInterface = {
 
 export const getStateStore = ({ state = defaultState } = {}) => (prevState: StateInterface = state, action: ViewerConfigurationAction) => {
   switch (action.type) {
-  case VIEWER_CONFIG_ACTION_TYPES.SET_MOBILE_UI: {
-    const { payload } = action
-    const { useMobileUI } = payload
-    return {
-      ...prevState,
-      useMobileUI,
-    }
-  }
   case VIEWER_CONFIG_ACTION_TYPES.UPDATE_CONFIG:
     return {
       ...prevState,
diff --git a/src/services/state/viewerState/actions.ts b/src/services/state/viewerState/actions.ts
index 0157c15f8..aa5b5aad2 100644
--- a/src/services/state/viewerState/actions.ts
+++ b/src/services/state/viewerState/actions.ts
@@ -24,8 +24,3 @@ export const viewerStateMouseOverCustomLandmarkInPerspectiveView = createAction(
   `[viewerState] mouseOverCustomLandmarkInPerspectiveView`,
   props<{ payload: { label: string } }>()
 )
-
-export const actionSetMobileUi = createAction(
-  `[viewerState] setMobileUi`,
-  props<{ payload: { useMobileUI: boolean } }>()
-)
diff --git a/src/services/stateStore.service.ts b/src/services/stateStore.service.ts
index 558823d1b..ceef16525 100644
--- a/src/services/stateStore.service.ts
+++ b/src/services/stateStore.service.ts
@@ -119,13 +119,11 @@ export function isDefined(obj) {
 export interface IavRootStoreInterface {
   viewerConfigState: ViewerConfigStateInterface
   ngViewerState: NgViewerStateInterface
-  dataStore: any
   uiState: IUiState
   userConfigState: UserConfigStateInterface
 }
 
 export const defaultRootState: any = {
-  dataStore: {},
   ngViewerState: ngViewerDefaultState,
   uiState: uiDefaultState,
   userConfigState: userConfigDefaultState,
diff --git a/src/state/userInteraction/actions.ts b/src/state/userInteraction/actions.ts
index b7d30d2de..fd21b4fd0 100644
--- a/src/state/userInteraction/actions.ts
+++ b/src/state/userInteraction/actions.ts
@@ -1,7 +1,7 @@
 import { createAction, props } from "@ngrx/store"
 import { nameSpace } from "./const"
 import * as atlasSelection from "../atlasSelection"
-import { SapiRegionModel } from "src/atlasComponents/sapi"
+import { SapiRegionModel, SapiSpatialFeatureModel, SapiVolumeModel } from "src/atlasComponents/sapi"
 import * as userInterface from "../userInterface"
 
 export const {
@@ -31,3 +31,14 @@ export const mouseoverRegions = createAction(
     regions: SapiRegionModel[]
   }>()
 )
+
+export const showFeature = createAction(
+  `${nameSpace} showFeature`,
+  props<{
+    feature: SapiSpatialFeatureModel
+  }>()
+)
+
+export const clearShownFeature = createAction(
+  `${nameSpace} clearShownFeature`,
+)
diff --git a/src/state/userInteraction/selectors.ts b/src/state/userInteraction/selectors.ts
index 5b9b0bd79..b9aa84c61 100644
--- a/src/state/userInteraction/selectors.ts
+++ b/src/state/userInteraction/selectors.ts
@@ -1,5 +1,4 @@
-import { createAction, createSelector, props } from "@ngrx/store";
-import { SapiRegionModel } from "src/atlasComponents/sapi";
+import { createSelector } from "@ngrx/store";
 import { nameSpace } from "./const"
 import { UserInteraction } from "./store";
 
@@ -9,3 +8,8 @@ export const mousingOverRegions = createSelector(
   selectStore,
   state => state.mouseoverRegions
 )
+
+export const selectedFeature = createSelector(
+  selectStore,
+  state => state.selectedFeature
+)
diff --git a/src/state/userInteraction/store.ts b/src/state/userInteraction/store.ts
index 83bf75863..0ce97c2ff 100644
--- a/src/state/userInteraction/store.ts
+++ b/src/state/userInteraction/store.ts
@@ -1,12 +1,14 @@
 import { createReducer, on } from "@ngrx/store";
-import { SapiRegionModel } from "src/atlasComponents/sapi";
+import { SapiRegionModel, SapiFeatureModel } from "src/atlasComponents/sapi";
 import * as actions from "./actions"
 
 export type UserInteraction = {
   mouseoverRegions: SapiRegionModel[]
+  selectedFeature: SapiFeatureModel
 }
 
 const defaultState: UserInteraction = {
+  selectedFeature: null,
   mouseoverRegions: []
 }
 
@@ -20,5 +22,23 @@ export const reducer = createReducer(
         mouseoverRegions: regions
       }
     }
+  ),
+  on(
+    actions.showFeature,
+    (state, { feature }) => {
+      return {
+        ...state,
+        selectedFeature: feature
+      }
+    }
+  ),
+  on(
+    actions.clearShownFeature,
+    state => {
+      return {
+        ...state,
+        selectedFeature: null
+      }
+    }
   )
-)
\ No newline at end of file
+)
diff --git a/src/state/userInterface/actions.ts b/src/state/userInterface/actions.ts
index f229006bc..1e80ac94a 100644
--- a/src/state/userInterface/actions.ts
+++ b/src/state/userInterface/actions.ts
@@ -4,19 +4,13 @@ import { MatSnackBarConfig } from "@angular/material/snack-bar";
 import { createAction, props } from "@ngrx/store";
 import { nameSpace } from "./const"
 
-export const showFeature = createAction(
-  `${nameSpace} showFeature`,
+export const useModileUi = createAction(
+  `${nameSpace} useMobileUi`,
   props<{
-    feature: {
-      "@id": string
-    }
+    flag: boolean
   }>()
 )
 
-export const clearShownFeature = createAction(
-  `${nameSpace} clearShownFeature`,
-)
-
 export const openSidePanel = createAction(
   `${nameSpace} openSidePanel`
 )
diff --git a/src/state/userInterface/selectors.ts b/src/state/userInterface/selectors.ts
index fd36b44ec..15705846f 100644
--- a/src/state/userInterface/selectors.ts
+++ b/src/state/userInterface/selectors.ts
@@ -4,7 +4,7 @@ import { UiStore } from "./store"
 
 const selectStore = state => state[nameSpace] as UiStore
 
-export const selectedFeature = createSelector(
+export const useMobileUi = createSelector(
   selectStore,
-  state => state.selectedFeature
+  state => state.useMobileUi
 )
diff --git a/src/state/userInterface/store.ts b/src/state/userInterface/store.ts
index 400e7983b..aabe2f556 100644
--- a/src/state/userInterface/store.ts
+++ b/src/state/userInterface/store.ts
@@ -1,33 +1,23 @@
 import { createReducer, on } from "@ngrx/store";
-import { SapiVolumeModel } from "src/atlasComponents/sapi";
 import * as actions from "./actions"
 
 export type UiStore = {
-  selectedFeature: SapiVolumeModel
+  useMobileUi: boolean
 }
 
 const defaultStore: UiStore = {
-  selectedFeature: null
+  useMobileUi: false
 }
 
 export const reducer = createReducer(
   defaultStore,
   on(
-    actions.showFeature,
-    (state, { feature }) => {
+    actions.useModileUi,
+    (state, { flag }) => {
       return {
         ...state,
-        feature
+        useMobileUi: flag
       }
     }
   ),
-  on(
-    actions.clearShownFeature,
-    state => {
-      return {
-        ...state,
-        feature: null
-      }
-    }
-  )
 )
diff --git a/src/ui/config/configCmp/config.component.ts b/src/ui/config/configCmp/config.component.ts
index f34ca0eec..ffc01128f 100644
--- a/src/ui/config/configCmp/config.component.ts
+++ b/src/ui/config/configCmp/config.component.ts
@@ -13,6 +13,7 @@ import { PureContantService } from 'src/util';
 import { ngViewerActionSwitchPanelMode } from 'src/services/state/ngViewerState/actions';
 import { ngViewerSelectorPanelMode, ngViewerSelectorPanelOrder } from 'src/services/state/ngViewerState/selectors';
 import { atlasSelection } from 'src/state';
+import * as stateCtrl from "src/state"
 
 const GPU_TOOLTIP = `Higher GPU usage can cause crashes on lower end machines`
 const ANIMATION_TOOLTIP = `Animation can cause slowdowns in lower end machines`
@@ -115,12 +116,11 @@ export class ConfigComponent implements OnInit, OnDestroy {
 
   public toggleMobileUI(ev: MatSlideToggleChange) {
     const { checked } = ev
-    this.store.dispatch({
-      type: VIEWER_CONFIG_ACTION_TYPES.SET_MOBILE_UI,
-      payload: {
-        useMobileUI: checked,
-      },
-    })
+    this.store.dispatch(
+      stateCtrl.userInterface.actions.useModileUi({
+        flag: checked
+      })
+    )
   }
 
   public toggleAnimationFlag(ev: MatSlideToggleChange ) {
diff --git a/src/util/pureConstant.service.ts b/src/util/pureConstant.service.ts
index 9ffc4b9e9..34fa6e3bf 100644
--- a/src/util/pureConstant.service.ts
+++ b/src/util/pureConstant.service.ts
@@ -1,17 +1,15 @@
 import { Inject, Injectable, OnDestroy } from "@angular/core";
 import { Store, select } from "@ngrx/store";
-import { Observable, Subscription, of, forkJoin, combineLatest, from } from "rxjs";
-import { viewerConfigSelectorUseMobileUi } from "src/services/state/viewerConfig.store.helper";
-import { shareReplay, tap, scan, catchError, filter, switchMap, map, distinctUntilChanged, mapTo } from "rxjs/operators";
+import { Observable, Subscription, of, forkJoin, from } from "rxjs";
+import { shareReplay, switchMap, map } from "rxjs/operators";
 import { HttpClient } from "@angular/common/http";
 import { LoggingService } from "src/logging";
 import { BS_ENDPOINT, BACKENDURL } from "src/util/constants";
-import { TAtlas, TId, TParc, TRegionDetail, TRegionSummary, TSpaceFull, TSpaceSummary, TVolumeSrc } from "./siibraApiConstants/types";
+import { TId, TParc, TRegionDetail, TRegionSummary, TSpaceFull, TSpaceSummary } from "./siibraApiConstants/types";
 import { MultiDimMap, recursiveMutate, mutateDeepMerge } from "./fn";
 import { patchRegions } from './patchPureConstants'
-import { environment } from "src/environments/environment";
 import { MatSnackBar } from "@angular/material/snack-bar";
-import { atlasSelection } from "src/state";
+import { atlasSelection, userInterface } from "src/state";
 
 const validVolumeType = new Set([
   'neuroglancer/precomputed',
@@ -307,7 +305,7 @@ Raise/track issues at github repo: <a target = "_blank" href = "${this.repoUrl}"
     )
 
     this.useTouchUI$ = this.store.pipe(
-      select(viewerConfigSelectorUseMobileUi),
+      select(userInterface.selectors.useMobileUi),
       shareReplay(1)
     )
 
diff --git a/src/viewerModule/nehuba/util.ts b/src/viewerModule/nehuba/util.ts
index 9c4baa3b0..fcec9aa50 100644
--- a/src/viewerModule/nehuba/util.ts
+++ b/src/viewerModule/nehuba/util.ts
@@ -2,7 +2,7 @@ import { InjectionToken } from '@angular/core'
 import { Observable, pipe } from 'rxjs'
 import { filter, scan, take } from 'rxjs/operators'
 import { PANELS } from 'src/services/state/ngViewerState.store.helper'
-import { getViewer } from 'src/util/fn'
+import { getExportNehuba, getViewer } from 'src/util/fn'
 import { NehubaViewerUnit } from './nehubaViewer/nehubaViewer.component'
 import { NgConfigViewerState } from "./config.service"
 import { RecursivePartial } from './config.service/type'
@@ -213,7 +213,7 @@ export const userLmUnchanged = (oldlms, newlms) => {
 export const importNehubaFactory = appendSrc => {
   let pr: Promise<any>
   return () => {
-    if ((window as any).export_nehuba) return Promise.resolve()
+    if (getExportNehuba()) return Promise.resolve()
 
     if (pr) return pr
     pr = appendSrc('main.bundle.js')
diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts
index 00ac6d7e2..b19dec0a8 100644
--- a/src/viewerModule/viewerCmp/viewerCmp.component.ts
+++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts
@@ -166,7 +166,7 @@ export class ViewerCmp implements OnDestroy {
   public viewerCtx$ = this.ctxMenuSvc.context$
 
   public selectedFeature$ = this.store$.pipe(
-    select(userInterface.selectors.selectedFeature)
+    select(userInteraction.selectors.selectedFeature)
   )
 
   /**
@@ -375,7 +375,7 @@ export class ViewerCmp implements OnDestroy {
     )
 
     this.store$.dispatch(
-      userInterface.actions.showFeature({
+      userInteraction.actions.showFeature({
         feature
       })
     )
@@ -383,7 +383,7 @@ export class ViewerCmp implements OnDestroy {
 
   clearSelectedFeature(){
     this.store$.dispatch(
-      userInterface.actions.clearShownFeature()
+      userInteraction.actions.clearShownFeature()
     )
   }
 }
diff --git a/worker/worker-typedarray.js b/worker/worker-typedarray.js
index cec7f4260..c6d5933ce 100644
--- a/worker/worker-typedarray.js
+++ b/worker/worker-typedarray.js
@@ -1,13 +1,125 @@
 (function(exports){
-  const packNpArray = (inputArray, dtype, width, height) => {
-    if (dtype === "float32") {
-      
+  /**
+   * CM_CONST adopted from https://github.com/bpostlethwaite/colormap
+   * at commit hash 3406182
+   * 
+   * with MIT license
+   * 
+   * Copyright (c) <2012> ICRL
+   * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+   * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+   * 
+   * https://github.com/bpostlethwaite/colormap/blob/3406182/colorScale.js
+   */
+  const CM_CONST = {
+    "jet":[{"index":0,"rgb":[0,0,131]},{"index":0.125,"rgb":[0,60,170]},{"index":0.375,"rgb":[5,255,255]},{"index":0.625,"rgb":[255,255,0]},{"index":0.875,"rgb":[250,0,0]},{"index":1,"rgb":[128,0,0]}],
+    "viridis": [{"index":0,"rgb":[68,1,84]},{"index":0.13,"rgb":[71,44,122]},{"index":0.25,"rgb":[59,81,139]},{"index":0.38,"rgb":[44,113,142]},{"index":0.5,"rgb":[33,144,141]},{"index":0.63,"rgb":[39,173,129]},{"index":0.75,"rgb":[92,200,99]},{"index":0.88,"rgb":[170,220,50]},{"index":1,"rgb":[253,231,37]}],
+    "plasma": [{"index":0,"rgb":[13,8,135]},{"index":0.13,"rgb":[75,3,161]},{"index":0.25,"rgb":[125,3,168]},{"index":0.38,"rgb":[168,34,150]},{"index":0.5,"rgb":[203,70,121]},{"index":0.63,"rgb":[229,107,93]},{"index":0.75,"rgb":[248,148,65]},{"index":0.88,"rgb":[253,195,40]},{"index":1,"rgb":[240,249,33]}],
+    "magma": [{"index":0,"rgb":[0,0,4]},{"index":0.13,"rgb":[28,16,68]},{"index":0.25,"rgb":[79,18,123]},{"index":0.38,"rgb":[129,37,129]},{"index":0.5,"rgb":[181,54,122]},{"index":0.63,"rgb":[229,80,100]},{"index":0.75,"rgb":[251,135,97]},{"index":0.88,"rgb":[254,194,135]},{"index":1,"rgb":[252,253,191]}],
+    "inferno": [{"index":0,"rgb":[0,0,4]},{"index":0.13,"rgb":[31,12,72]},{"index":0.25,"rgb":[85,15,109]},{"index":0.38,"rgb":[136,34,106]},{"index":0.5,"rgb":[186,54,85]},{"index":0.63,"rgb":[227,89,51]},{"index":0.75,"rgb":[249,140,10]},{"index":0.88,"rgb":[249,201,50]},{"index":1,"rgb":[252,255,164]}],
+    "greyscale": [{"index":0,"rgb":[0,0,0]},{"index":1,"rgb":[255,255,255]}],
+  }
+
+  function lerp(min, max, val) {
+    const absDiff = max - min
+    const lowerVal = (val - min) / absDiff
+    return [lowerVal, 1 - lowerVal]
+  }
+
+  function getLerpToCm(colormap) {
+    if (!CM_CONST[colormap]) {
+      throw new Error(`colormap ${colormap} does not exist in CM_CONST`)
+    }
+    const cm = CM_CONST[colormap]
+
+    function check(nv, current) {
+      if (!cm[current + 1] || !cm[current]) {
+        debugger
+      }
+      const lower = cm[current].index <= nv
+      const higher = nv <= cm[current + 1].index
+      let returnVal = null
+      if (lower && higher) {
+        returnVal = {
+          index: nv,
+          rgb: [0, 0, 0]
+        }
+        const [ lowerPc, higherPc ] = lerp(cm[current].index, cm[current + 1].index, nv)
+        returnVal.rgb = returnVal.rgb.map((_, idx) => cm[current]['rgb'][idx] * lowerPc + cm[current + 1]['rgb'][idx] * higherPc)
+      }
+      return [ returnVal, { lower, higher } ]
+    }
+    return function lerpToCm(nv) {
+      let minLow = 0, maxHigh = cm.length, current = Math.floor((maxHigh + minLow) / 2), found
+      let iter = 0
+      while (true) {
+        iter ++
+        if (iter > 100) {
+          debugger
+          throw new Error(`iter > 1000, something we`)
+        }
+        const [val, { lower, higher }] = check(nv, current)
+        if (val) {
+          found = val
+          break
+        }
+        if (lower) {
+          minLow = current
+        }
+        if (higher) {
+          maxHigh = current
+        }
+        current = Math.floor((maxHigh + minLow) / 2)
+      }
+      return found
     }
-    if (dtype === "int32") {
+  }
+
 
+  function unpackToArray(inputArray, width, height, channel, dtype) {
+    /**
+     * NB: assuming C (row major) order!
+     */
+
+    if (channel !== 1) {
+      throw new Error(`cm2rgba channel must be 1, but is ${channel} instead`)
+    }
+    const depth = (() => {
+      if (dtype === "int32") return 4
+      if (dtype === "float32") return 4
+      if (dtype === "uint8") return 1
+      throw new Error(`unrecognised dtype: ${dtype}`)
+    })()
+    if (width * height * depth !== inputArray.length) {
+      throw new Error(`expecting width * height * depth === input.length, but ${width} * ${height} * ${depth} === ${width * height * depth} !== ${inputArray.length}`)
     }
-    if (dtype === "uint8") {
 
+    const UseConstructor = (() => {
+
+      if (dtype === "int32") return Int32Array
+      if (dtype === "float32") return Float32Array
+      if (dtype === "uint8") return Uint8Array
+      throw new Error(`unrecognised dtype: ${dtype}`)
+    })()
+    
+    const newArray = new UseConstructor(inputArray.buffer)
+    const outputArray = []
+    let min = null
+    let max = null
+    for (let y = 0; y < height; y ++) {
+      if (!outputArray[y]) outputArray[y] = []
+      for (let x = 0; x < width; x++) {
+        const num = newArray[y * width + x]
+        min = min === null ? num : Math.min(num, min)
+        max = max === null ? num : Math.max(num, max)
+        outputArray[y][x] = newArray[y * width + x]
+      }
+    }
+    return {
+      outputArray,
+      min,
+      max
     }
   }
 
@@ -40,6 +152,53 @@
         }
       }
       return buffer
+    },
+    cm2rgba(inputArray, width, height, channel, dtype, processParams) {
+      const { 
+        outputArray,
+        min,
+        max,
+       } = unpackToArray(inputArray, width, height, channel, dtype)
+      const {
+        colormap="jet"
+      } = processParams || {}
+
+      const _ = new ArrayBuffer(width * height * 4)
+      const buffer = new Uint8ClampedArray(_)
+
+      const absDiff = max - min
+      const lerpToCm = getLerpToCm(colormap)
+      
+      for (let row = 0; row < height; row ++) {
+        for (let col = 0; col < width; col ++){
+          const normalizedValue = (outputArray[row][col] - min) / absDiff
+          const { rgb } = lerpToCm(normalizedValue)
+
+          const toIdx = (row * width + col) * 4
+          buffer[toIdx] = rgb[0]
+          buffer[toIdx + 1] = rgb[1]
+          buffer[toIdx + 2] = rgb[2]
+          buffer[toIdx + 3] = 255
+        }
+      }
+      return {
+        buffer,
+        min,
+        max,
+      }
+    },
+    rawArray(inputArray, width, height, channel, dtype) {
+      const { 
+        outputArray,
+        min,
+        max,
+       } = unpackToArray(inputArray, width, height, channel, dtype)
+       console.log({ outputArray })
+       return {
+          outputArray,
+          min,
+          max,
+       }
     }
   }
 })(
diff --git a/worker/worker.js b/worker/worker.js
index 036dec5c8..ab8cc8371 100644
--- a/worker/worker.js
+++ b/worker/worker.js
@@ -26,6 +26,8 @@ const VALID_METHOD = {
   PROCESS_NIFTI: 'PROCESS_NIFTI',
   PROCESS_TYPED_ARRAY: `PROCESS_TYPED_ARRAY`,
   PROCESS_TYPED_ARRAY_F2RGBA: `PROCESS_TYPED_ARRAY_F2RGBA`,
+  PROCESS_TYPED_ARRAY_CM2RGBA: "PROCESS_TYPED_ARRAY_CM2RGBA",
+  PROCESS_TYPED_ARRAY_RAW: "PROCESS_TYPED_ARRAY_RAW",
 }
 
 const VALID_METHODS = [
@@ -33,6 +35,8 @@ const VALID_METHODS = [
   VALID_METHOD.PROCESS_NIFTI,
   VALID_METHOD.PROCESS_TYPED_ARRAY,
   VALID_METHOD.PROCESS_TYPED_ARRAY_F2RGBA,
+  VALID_METHOD.PROCESS_TYPED_ARRAY_CM2RGBA,
+  VALID_METHOD.PROCESS_TYPED_ARRAY_RAW,
 ]
 
 const validOutType = [
@@ -310,11 +314,57 @@ onmessage = (message) => {
         })
       }
     }
+    if (message.data.method === VALID_METHOD.PROCESS_TYPED_ARRAY_CM2RGBA) {
+      try {
+        const { inputArray, width, height, channel, dtype, processParams } = message.data.param
+        const { buffer, min, max } = self.typedArray.cm2rgba(inputArray, width, height, channel, dtype, processParams)
+
+        postMessage({
+          id,
+          result: {
+            buffer,
+            min,
+            max,
+          }
+        }, [ buffer.buffer ])
+      } catch (e) {
+        postMessage({
+          id,
+          error: {
+            code: 401,
+            message: `process typed array error: ${e.toString()}`
+          }
+        })
+      }
+    }
+    if (message.data.method === VALID_METHOD.PROCESS_TYPED_ARRAY_RAW) {
+      try {
+        const { inputArray, width, height, channel, dtype, processParams } = message.data.param
+        const { outputArray, min, max } = self.typedArray.rawArray(inputArray, width, height, channel, dtype, processParams)
+
+        postMessage({
+          id,
+          result: {
+            outputArray,
+            min,
+            max,
+          }
+        })
+      } catch (e) {
+        postMessage({
+          id,
+          error: {
+            code: 401,
+            message: `process typed array error: ${e.toString()}`
+          }
+        })
+      }
+    }
     postMessage({
       id,
       error: {
         code: 404,
-        message: `worker method not found`
+        message: `worker method not found. ${message.data.method}`
       }
     })
     return
-- 
GitLab