Repository: CARTAvis/carta-frontend
Pull request #2500: Yuhsuan/2492 channel map render config and animator
Author: login: YuHsuan-Hwang
Created: 2025-03-25T07:27:15Z | Merged: 2025-04-02T05:23:57Z

**Description**

This PR enhances the render config widget and the animator widget in channel map mode (#2492). The UI is redesigned for consistency with the behavior in non-channel map mode.

Code changes

* Render config
  * Removed `RenderConfigStore.channelMap[ScaleMin/ScaleMax/Histogram]`. The channel map mode and non-channel map mode share the same clip min/max states.
  * Update the render config histogram and clip min/max when the active channel is changed in channel map mode.
* Animator
  * Removed `ChannelMapStore.masterFrame` and `ChannelMapStore.masterImage`. The channel map mode and non-channel map mode share the same active image state.
  * Added all image options in channel map control image dropdown.
  * Added non ideal state for color blending images in channel map mode.
  * Added `reaction` to update the start/active channel when the active channel is out of range. Applied debouncing when updating the active channel so that it is more likely to be set to the top-left channel.
  * Clear all the tile cache and request for new tiles when the active polarization is changed.

Tested that

* Render config
  * The histogram updates when the active channel changes in channel map mode.
  * The clip min/max updates according to the selected percentile when the active channel changes in channel map mode (remains unchanged when custom is selected).
  * The clip min/max and the histogram remain unchanged when switching between channel map mode and non-channel map mode.
  * When “Per-cube” is selected, the clip min/max and histogram remain unchanged when the active channel changes in channel map mode.
* Animator
  * The image dropdown in the channel map control widget is synchronized to the active image (including color blending images).
  * When a color blending image is selected in channel map mode, a non-ideal state is shown. Color blending images integrated with channel map mode will be investigated in a separate PR.
  * The active channel is always visible in the image view. When the active channel is out of range, the start channel is set to the active channel. When the start/end channel is changed, the active channel is set to the top right corner if it is out of range.
  * The raster tiles update when the polarization changes in channel map mode. Note that when switching between the polarization, there are always new raster tile messages because we only cache the current polarization. When switching between the images, there can be no new raster tile messages because we cache for each image.

**Checklist**

For linked issues (if there are):
- [x] assignee and label added
- [x] GitHub Project estimate added

For the pull request:
- [x] reviewers and assignee added
- [x] GitHub Project estimate added
- [ ] changelog updated / no changelog update needed
- [x] unit test added (for functions with no dependenies)
- [x] API documentation added (for public variables and methods in stores)

For dependencies:
- [x] e2e test passing / corresponding fix added / new e2e test created
- [x] ~protobuf version bumped~ / no protobuf version bumped needed
- [x] ~protobuf updated to the latest dev commit~ / no protobuf update needed
- [x] ~corresponding ICD test fix added (`BackendService` changed)~ / no ICD test fix needed (`BackendService` unchanged)
- [ ] user manual prepared (for large new features)


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab04a62631..0c06a25f37 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [Unreleased]
 ### Fixed
 * Fixed inconsistent label offsets of ruler annotations ([#2472](https://github.com/CARTAvis/carta-frontend/issues/2472)).
-* Fixed the misplaced spectral line labels in the PNG export with the Retina display ([[#2423](https://github.com/CARTAvis/carta-frontend/issues/2423)])
+* Fixed the misplaced spectral line labels in the PNG export with the Retina display ([[#2423](https://github.com/CARTAvis/carta-frontend/issues/2423)]).
+### Added
+* Enhanced support for modifying the render configuration and the active channel/polarization in channel map mode ([[#2492
+](https://github.com/CARTAvis/carta-frontend/issues/2492)]).
 
 ## [5.0.0-beta.1c]
 
diff --git a/src/components/Animator/AnimatorComponent.tsx b/src/components/Animator/AnimatorComponent.tsx
index 7c75e25919..dbb652e766 100644
--- a/src/components/Animator/AnimatorComponent.tsx
+++ b/src/components/Animator/AnimatorComponent.tsx
@@ -219,36 +219,11 @@ export class AnimatorComponent extends React.Component<WidgetProps> {
             const imageTick = numImages > 10 ? imageTickPre : Array.from(Array(numImages).keys());
             imageSlider = (
                 <div className="animator-slider">
-                    <Radio
-                        value={AnimationMode.FRAME}
-                        disabled={appStore.animatorStore.animationActive || appStore.channelMapStore.channelMapEnabled}
-                        checked={appStore.animatorStore.animationMode === AnimationMode.FRAME}
-                        onChange={this.onAnimationModeChanged}
-                        label="Image"
-                    />
-                    {hideSliders && (
-                        <SafeNumericInput
-                            value={imageIndex}
-                            min={-1}
-                            max={numImages}
-                            stepSize={1}
-                            onValueChange={this.onImageChanged}
-                            fill={true}
-                            disabled={appStore.animatorStore.animationActive || appStore.channelMapStore.channelMapEnabled}
-                        />
-                    )}
+                    <Radio value={AnimationMode.FRAME} disabled={appStore.animatorStore.animationActive} checked={appStore.animatorStore.animationMode === AnimationMode.FRAME} onChange={this.onAnimationModeChanged} label="Image" />
+                    {hideSliders && <SafeNumericInput value={imageIndex} min={-1} max={numImages} stepSize={1} onValueChange={this.onImageChanged} fill={true} disabled={appStore.animatorStore.animationActive} />}
                     {!hideSliders && (
                         <React.Fragment>
-                            <Slider
-                                value={imageIndex}
-                                min={0}
-                                max={numImages - 1}
-                                showTrackFill={false}
-                                labelValues={imageTick}
-                                labelPrecision={0}
-                                onChange={this.onImageChanged}
-                                disabled={appStore.animatorStore.animationActive || appStore.channelMapStore.channelMapEnabled}
-                            />
+                            <Slider value={imageIndex} min={0} max={numImages - 1} showTrackFill={false} labelValues={imageTick} labelPrecision={0} onChange={this.onImageChanged} disabled={appStore.animatorStore.animationActive} />
                             <div className="slider-info">{appStore.activeImage.store.filename}</div>
                         </React.Fragment>
                     )}
@@ -267,22 +242,12 @@ export class AnimatorComponent extends React.Component<WidgetProps> {
                 <div className="animator-slider" data-testid="animator-slider">
                     <Radio
                         value={AnimationMode.CHANNEL}
-                        disabled={appStore.animatorStore.animationActive || appStore.channelMapStore.channelMapEnabled}
+                        disabled={appStore.animatorStore.animationActive}
                         checked={appStore.animatorStore.animationMode === AnimationMode.CHANNEL}
                         onChange={this.onAnimationModeChanged}
                         label={activeFrame.channelType}
                     />
-                    {hideSliders && (
-                        <SafeNumericInput
-                            value={activeFrame.requiredChannel}
-                            min={-1}
-                            max={numChannels}
-                            stepSize={1}
-                            onValueChange={this.onChannelChanged}
-                            fill={true}
-                            disabled={appStore.animatorStore.animationActive || appStore.channelMapStore.channelMapEnabled}
-                        />
-                    )}
+                    {hideSliders && <SafeNumericInput value={activeFrame.requiredChannel} min={-1} max={numChannels} stepSize={1} onValueChange={this.onChannelChanged} fill={true} disabled={appStore.animatorStore.animationActive} />}
                     {!hideSliders && (
                         <React.Fragment>
                             <Slider
@@ -294,7 +259,7 @@ export class AnimatorComponent extends React.Component<WidgetProps> {
                                 labelPrecision={0}
                                 showTrackFill={false}
                                 onChange={this.onChannelChanged}
-                                disabled={appStore.animatorStore.animationActive || appStore.channelMapStore.channelMapEnabled}
+                                disabled={appStore.animatorStore.animationActive}
                             />
                             <div className="slider-info" data-testid="animator-slider-info">
                                 <pre>{activeFrame.depthAxisInfo}</pre>
@@ -315,7 +280,7 @@ export class AnimatorComponent extends React.Component<WidgetProps> {
                                 labelStepSize={channelStep}
                                 labelPrecision={0}
                                 onChange={this.onRangeChanged}
-                                disabled={appStore.animatorStore.animationActive || appStore.channelMapStore.channelMapEnabled}
+                                disabled={appStore.animatorStore.animationActive}
                             />
                             <div className="slider-info" />
                         </React.Fragment>
@@ -328,13 +293,7 @@ export class AnimatorComponent extends React.Component<WidgetProps> {
         if (numStokes > 1) {
             stokesSlider = (
                 <div className={classNames("animator-slider", "stokes-slider", {"tiled-label": this.width < 750})} data-testid="animator-polarization-slider">
-                    <Radio
-                        value={AnimationMode.STOKES}
-                        disabled={appStore.animatorStore.animationActive || appStore.channelMapStore.channelMapEnabled}
-                        checked={appStore.animatorStore.animationMode === AnimationMode.STOKES}
-                        onChange={this.onAnimationModeChanged}
-                        label="Polarization"
-                    />
+                    <Radio value={AnimationMode.STOKES} disabled={appStore.animatorStore.animationActive} checked={appStore.animatorStore.animationMode === AnimationMode.STOKES} onChange={this.onAnimationModeChanged} label="Polarization" />
                     {hideSliders && (
                         <SafeNumericInput
                             value={activeFrame.requiredStokes}
@@ -342,7 +301,7 @@ export class AnimatorComponent extends React.Component<WidgetProps> {
                             max={activeFrame.frameInfo.fileInfoExtended.stokes}
                             stepSize={1}
                             onValueChange={this.onStokesChanged}
-                            disabled={appStore.animatorStore.animationActive || appStore.channelMapStore.channelMapEnabled}
+                            disabled={appStore.animatorStore.animationActive}
                             fill={true}
                         />
                     )}
@@ -357,7 +316,7 @@ export class AnimatorComponent extends React.Component<WidgetProps> {
                                     return isFinite(val) && val >= 0 && val < activeFrame?.polarizationInfo?.length ? activeFrame.polarizationInfo[val] : `${val}`;
                                 }}
                                 onChange={this.onStokesChanged}
-                                disabled={appStore.animatorStore.animationActive || appStore.channelMapStore.channelMapEnabled}
+                                disabled={appStore.animatorStore.animationActive}
                             />
                             <div className="slider-info" />
                         </React.Fragment>
@@ -393,10 +352,10 @@ export class AnimatorComponent extends React.Component<WidgetProps> {
 
         const playbackButtons = (
             <ButtonGroup fill={true} className="playback-buttons">
-                <Button icon={"chevron-backward"} onClick={this.onFirstClicked} disabled={appStore.channelMapStore.channelMapEnabled} data-testid="animator-first-button">
+                <Button icon={"chevron-backward"} onClick={this.onFirstClicked} data-testid="animator-first-button">
                     {!iconOnly && "First"}
                 </Button>
-                <Button icon={"step-backward"} onClick={this.onPrevClicked} disabled={appStore.channelMapStore.channelMapEnabled} data-testid="animator-previous-button">
+                <Button icon={"step-backward"} onClick={this.onPrevClicked} data-testid="animator-previous-button">
                     {!iconOnly && "Prev"}
                 </Button>
                 {appStore.animatorStore.animationActive && (
@@ -409,10 +368,10 @@ export class AnimatorComponent extends React.Component<WidgetProps> {
                         {!iconOnly && "Play"}
                     </Button>
                 )}
-                <Button icon={"step-forward"} onClick={this.onNextClicked} disabled={appStore.channelMapStore.channelMapEnabled} data-testid="animator-next-button">
+                <Button icon={"step-forward"} onClick={this.onNextClicked} data-testid="animator-next-button">
                     {!iconOnly && "Next"}
                 </Button>
-                <Button icon={"chevron-forward"} onClick={this.onLastClicked} disabled={appStore.channelMapStore.channelMapEnabled} data-testid="animator-last-button">
+                <Button icon={"chevron-forward"} onClick={this.onLastClicked} data-testid="animator-last-button">
                     {!iconOnly && "Last"}
                 </Button>
             </ButtonGroup>
@@ -420,7 +379,11 @@ export class AnimatorComponent extends React.Component<WidgetProps> {
 
         const numericControl = (
             <ControlGroup className="playback-numeric-control">
-                <HTMLSelect options={[NumericInputType.FrameRate, NumericInputType.Step]} onChange={ev => this.onNumericInputTypeChange(ev.currentTarget.value as NumericInputType)} />
+                <HTMLSelect
+                    disabled={appStore.animatorStore.animationActive || appStore.channelMapStore.channelMapEnabled}
+                    options={[NumericInputType.FrameRate, NumericInputType.Step]}
+                    onChange={ev => this.onNumericInputTypeChange(ev.currentTarget.value as NumericInputType)}
+                />
                 {this.numericInputType === NumericInputType.FrameRate ? (
                     <SafeNumericInput
                         value={appStore.animatorStore.frameRate}
diff --git a/src/components/ChannelMapControl/ChannelMapControlComponent.tsx b/src/components/ChannelMapControl/ChannelMapControlComponent.tsx
index 55a0e6d521..9bab5ac76b 100644
--- a/src/components/ChannelMapControl/ChannelMapControlComponent.tsx
+++ b/src/components/ChannelMapControl/ChannelMapControlComponent.tsx
@@ -38,10 +38,9 @@ export class ChannelMapControlComponent extends React.Component<WidgetProps> {
     };
 
     @action onChannelChanged = (val: number) => {
-        const channelMapSettings = AppStore.Instance.channelMapStore;
-        const frame = channelMapSettings.masterFrame;
+        const frame = AppStore.Instance.activeFrame;
         if (frame) {
-            channelMapSettings.setStartChannel(clamp(val, 0, frame.frameInfo.fileInfoExtended.depth - 1));
+            AppStore.Instance.channelMapStore.setStartChannel(clamp(val, 0, frame.frameInfo.fileInfoExtended.depth - 1));
         }
     };
 
@@ -49,7 +48,7 @@ export class ChannelMapControlComponent extends React.Component<WidgetProps> {
         const appStore = AppStore.Instance;
         const activeFrame = appStore.activeFrame;
         const channelMapSettings = appStore.channelMapStore;
-        const numChannels = channelMapSettings.masterFrame ? channelMapSettings.masterFrame.frameInfo.fileInfoExtended.depth : 10;
+        const numChannels = activeFrame ? activeFrame.frameInfo.fileInfoExtended.depth : 10;
         const iconOnly = this.width < 300;
 
         const channelMapControl = (
@@ -89,22 +88,19 @@ export class ChannelMapControlComponent extends React.Component<WidgetProps> {
                 <FormGroup className="channel-map-control-label" inline={true} label="Start channel" disabled={!channelMapSettings.channelMapEnabled}>
                     <Slider
                         min={0}
-                        max={appStore.channelMapStore.masterFrame?.frameInfo.fileInfoExtended.depth}
+                        max={activeFrame?.frameInfo.fileInfoExtended.depth}
                         stepSize={1}
-                        labelStepSize={Math.max(
-                            appStore.channelMapStore.masterFrame?.frameInfo.fileInfoExtended.depth / appStore.channelMapStore.numChannels,
-                            Math.ceil(appStore.channelMapStore.masterFrame?.frameInfo.fileInfoExtended.depth / 5)
-                        )}
+                        labelStepSize={Math.max(activeFrame?.frameInfo.fileInfoExtended.depth / appStore.channelMapStore.numChannels, Math.ceil(activeFrame?.frameInfo.fileInfoExtended.depth / 5))}
                         value={appStore.channelMapStore.startChannel}
                         onChange={channel => appStore.channelMapStore.setStartChannel(channel)}
-                        disabled={!channelMapSettings.channelMapEnabled || appStore.channelMapStore.masterFrame?.frameInfo.fileInfoExtended.depth <= 1}
+                        disabled={!channelMapSettings.channelMapEnabled || activeFrame?.frameInfo.fileInfoExtended.depth <= 1}
                     />
                 </FormGroup>
                 <FormGroup className="channel-map-control-label" inline={true} label="Image" disabled={!channelMapSettings.channelMapEnabled}>
                     <HTMLSelect
-                        value={appStore.channelMapStore.masterFrame?.frameInfo.fileId.toString()}
-                        options={[...(AppStore.Instance.frameNames ?? [])]}
-                        onChange={ev => appStore.setActiveImageByFileId(parseInt(ev.currentTarget.value))}
+                        value={appStore.activeImageIndex}
+                        options={AppStore.Instance.imageViewConfigStore.imageNames.map((name, index) => ({value: index, label: `${index}: ${name}`}))}
+                        onChange={ev => appStore.setActiveImageByIndex(parseInt(ev.currentTarget.value))}
                         disabled={!channelMapSettings.channelMapEnabled}
                         style={{width: "100px"}}
                         data-testid="image-dropdown"
diff --git a/src/components/ImageView/ChannelMapView/ChannelMapLabelComponent.tsx b/src/components/ImageView/ChannelMapView/ChannelMapLabelComponent.tsx
index 34ad5d7f68..e643cdd4da 100644
--- a/src/components/ImageView/ChannelMapView/ChannelMapLabelComponent.tsx
+++ b/src/components/ImageView/ChannelMapView/ChannelMapLabelComponent.tsx
@@ -39,7 +39,7 @@ export class ChannelMapLabelComponent extends React.Component<ChannelMapLabelCom
     }
 
     render() {
-        const frame = AppStore.Instance.channelMapStore.masterFrame;
+        const frame = AppStore.Instance.activeFrame;
         const channelMapStore = AppStore.Instance.channelMapStore;
         const {spectralString, velocityString} = frame.getFreqWithChannel(this.props.channel);
 
diff --git a/src/components/ImageView/ChannelMapView/ChannelMapViewComponent.tsx b/src/components/ImageView/ChannelMapView/ChannelMapViewComponent.tsx
index 31429eceb8..c0baf35301 100644
--- a/src/components/ImageView/ChannelMapView/ChannelMapViewComponent.tsx
+++ b/src/components/ImageView/ChannelMapView/ChannelMapViewComponent.tsx
@@ -24,8 +24,8 @@ export const ChannelMapViewComponent: React.FC<ChannelMapViewComponentProps> = o
     const regionViewRef = React.useRef<RegionViewComponent>();
     const appStore = AppStore.Instance;
     const channelMapStore = appStore.channelMapStore;
-    const frame = channelMapStore.masterFrame;
-    const image = channelMapStore.masterImage;
+    const frame = appStore.activeFrame;
+    const image = appStore.activeImage;
     const overlayStore = appStore.overlayStore;
     const colorBarSetting = overlayStore.colorbar;
     const colorbarOffset = overlayStore.colorbar.visible ? colorBarSetting.stageWidth + overlayStore?.colorbarHoverInfoHeight : 0;
@@ -69,6 +69,10 @@ export const ChannelMapViewComponent: React.FC<ChannelMapViewComponentProps> = o
         frame?.setCenter(cursorInfo.posImageSpace.x, cursorInfo.posImageSpace.y);
     };
 
+    if (image?.type === ImageType.COLOR_BLENDING) {
+        return <NonIdealState icon={"error"} title={"Not supported"} description={"Color blending images in channel map view is not supported"} />;
+    }
+
     const overlayComponents = channelMapStore.channelArray.map((channel, index) => {
         const appStore = AppStore.Instance;
         const overlayStore = appStore.overlayStore;
@@ -100,8 +104,8 @@ export const ChannelMapViewComponent: React.FC<ChannelMapViewComponentProps> = o
         const imageRenderHeight = frame?.renderHeight;
 
         return (
-            channel < channelMapStore.masterFrame?.frameInfo.fileInfoExtended.depth && (
-                <div key={index} onClick={() => channelMapStore.masterFrame.setChannel(channel)} style={{top: overlayComponentTop}}>
+            channel < frame?.frameInfo.fileInfoExtended.depth && (
+                <div key={index} onClick={() => frame.setChannel(channel)} style={{top: overlayComponentTop}}>
                     <ChannelMapInnerOverlayComponent
                         index={index}
                         frame={frame}
@@ -111,7 +115,7 @@ export const ChannelMapViewComponent: React.FC<ChannelMapViewComponentProps> = o
                         overlayComponentRef={overlayComponentRef}
                         setOverlayComponentRef={setOverlayComponentRef}
                     />
-                    {channelMapStore.masterFrame?.frameInfo.fileInfoExtended.depth > 1 && (
+                    {frame?.frameInfo.fileInfoExtended.depth > 1 && (
                         <ChannelMapLabelComponent
                             image={{
                                 type: ImageType.FRAME,
@@ -124,7 +128,7 @@ export const ChannelMapViewComponent: React.FC<ChannelMapViewComponentProps> = o
                             height={imageRenderHeight}
                             docked={props.docked}
                             channel={channel}
-                            highlighted={channel === channelMapStore.masterFrame.requiredChannel}
+                            highlighted={channel === frame.requiredChannel}
                         />
                     )}
                     <RegionViewComponent
@@ -172,11 +176,7 @@ export const ChannelMapViewComponent: React.FC<ChannelMapViewComponentProps> = o
                     cursorValue={frame.cursorInfo.isInsideImage ? frame.cursorValue.value : undefined}
                     isValueCurrent={frame.isCursorValueCurrent}
                     spectralInfo={frame.spectralInfo}
-                    width={
-                        channelMapStore.masterFrame?.frameInfo.fileInfoExtended.depth < channelMapStore.numColumns
-                            ? channelMapStore.masterFrame.renderWidth + overlayStore.paddingLeft + overlayStore.paddingRight
-                            : renderWidth - overlayStore.base
-                    }
+                    width={frame?.frameInfo.fileInfoExtended.depth < channelMapStore.numColumns ? frame.renderWidth + overlayStore.paddingLeft + overlayStore.paddingRight : renderWidth - overlayStore.base}
                     left={overlayStore.paddingLeft}
                     right={overlayStore.paddingRight}
                     docked={props.docked}
@@ -354,26 +354,26 @@ const ChannelMapInnerOverlayComponent = observer(
             height,
             channelMapStore.startChannel,
             channelMapStore.endChannel,
-            channelMapStore.masterFrame,
+            frame,
             channelMapStore.numColumns,
             channelMapStore.numRows,
-            channelMapStore.masterFrame?.center,
-            channelMapStore.masterFrame?.requiredFrameView,
-            channelMapStore.masterFrame?.requiredFrameView.xMin,
-            channelMapStore.masterFrame?.requiredFrameView.xMax,
-            channelMapStore.masterFrame?.requiredFrameView.yMin,
-            channelMapStore.masterFrame?.requiredFrameView.yMax,
-            channelMapStore.masterFrame?.zooming,
-            channelMapStore.masterFrame?.zoomLevel,
-            channelMapStore.masterFrame?.spatialReference,
-            channelMapStore.masterFrame?.channel,
-            channelMapStore.masterFrame?.isOffsetCoord,
-            channelMapStore.masterFrame?.wcsInfoShifted,
-            channelMapStore.masterFrame?.titleCustomText,
-            channelMapStore.masterFrame?.filename,
+            frame?.center,
+            frame?.requiredFrameView,
+            frame?.requiredFrameView.xMin,
+            frame?.requiredFrameView.xMax,
+            frame?.requiredFrameView.yMin,
+            frame?.requiredFrameView.yMax,
+            frame?.zooming,
+            frame?.zoomLevel,
+            frame?.spatialReference,
+            frame?.channel,
+            frame?.isOffsetCoord,
+            frame?.wcsInfoShifted,
+            frame?.titleCustomText,
+            frame?.filename,
             overlayStore.styleString,
             overlayStore.padding,
-            channelMapStore.masterFrame?.moving,
+            frame?.moving,
             overlayStore.global.system,
             overlayStore.global.color,
             overlayStore.title.color,
diff --git a/src/components/ImageView/ImageViewSettingsPanel/ImageViewSettingsPanelComponent.tsx b/src/components/ImageView/ImageViewSettingsPanel/ImageViewSettingsPanelComponent.tsx
index 3412a6a8e9..910d488241 100644
--- a/src/components/ImageView/ImageViewSettingsPanel/ImageViewSettingsPanelComponent.tsx
+++ b/src/components/ImageView/ImageViewSettingsPanel/ImageViewSettingsPanelComponent.tsx
@@ -123,7 +123,7 @@ export class ImageViewSettingsPanelComponent extends React.Component<WidgetProps
 
     public render() {
         const appStore = AppStore.Instance;
-        const frame = appStore.channelMapStore.channelMapEnabled && appStore.channelMapStore.masterFrame ? appStore.channelMapStore.masterFrame : appStore.activeFrame;
+        const frame = appStore.activeFrame;
         const overlayStore = appStore.overlayStore;
         const global = overlayStore.global;
         const title = overlayStore.title;
diff --git a/src/components/ImageView/Overlay/OverlayComponent.tsx b/src/components/ImageView/Overlay/OverlayComponent.tsx
index b05da56ef3..bc2951632c 100644
--- a/src/components/ImageView/Overlay/OverlayComponent.tsx
+++ b/src/components/ImageView/Overlay/OverlayComponent.tsx
@@ -215,7 +215,6 @@ export class OverlayComponent extends React.Component<OverlayComponentProps> {
         const channelMapStartChannel = AppStore.Instance.channelMapStore.startChannel;
         const channelMapNumColumns = AppStore.Instance.channelMapStore.numColumns;
         const channelMapNumRows = AppStore.Instance.channelMapStore.numRows;
-        const channelMapMasterFrame = AppStore.Instance.channelMapStore.masterFrame;
         const channelMapChannelNum = AppStore.Instance.channelMapStore.numChannels;
         const offsetCoord = frame.isOffsetCoord;
         const offsetWcs = frame.wcsInfoShifted;
diff --git a/src/components/ImageView/RasterView/RasterViewComponent.tsx b/src/components/ImageView/RasterView/RasterViewComponent.tsx
index 8efed900fb..4c64297500 100644
--- a/src/components/ImageView/RasterView/RasterViewComponent.tsx
+++ b/src/components/ImageView/RasterView/RasterViewComponent.tsx
@@ -127,7 +127,7 @@ export class RasterViewComponent extends React.Component<RasterViewComponentProp
                 const histStokesIndex = frame.renderConfig.stokesIndex;
                 const histChannel = frame.renderConfig.histChannel;
                 if (
-                    (frame.renderConfig.useCubeHistogram || frame.channel === histChannel || frame.isPreview || (AppStore.Instance.channelMapStore.channelMapEnabled && frame.renderConfig.channelMapHistogram)) &&
+                    (frame.renderConfig.useCubeHistogram || frame.channel === histChannel || frame.isPreview || AppStore.Instance.channelMapStore.channelMapEnabled) &&
                     (frame.stokes === histStokesIndex || frame.polarizations.indexOf(frame.stokes) === histStokesIndex)
                 ) {
                     this.updateUniforms(frame, Math.floor(frame.renderWidth), Math.floor(frame.renderHeight), this.props.pixelHighlightValue);
diff --git a/src/components/RenderConfig/RenderConfigComponent.tsx b/src/components/RenderConfig/RenderConfigComponent.tsx
index c76111f679..6fe47a0e16 100644
--- a/src/components/RenderConfig/RenderConfigComponent.tsx
+++ b/src/components/RenderConfig/RenderConfigComponent.tsx
@@ -61,7 +61,7 @@ export class RenderConfigComponent extends React.Component<WidgetProps> {
     @computed get plotData(): {values: Array<Point2D>; xMin: number; xMax: number; yMin: number; yMax: number} {
         const frame = AppStore.Instance.activeFrame;
         if (frame && frame.renderConfig.histogram && frame.renderConfig.histogram.bins && frame.renderConfig.histogram.bins.length) {
-            const histogram = AppStore.Instance.channelMapStore.channelMapEnabled && !frame.isPreview ? frame.renderConfig.channelMapHistogram : frame.renderConfig.histogram;
+            const histogram = frame.renderConfig.histogram;
             let minIndex = 0;
             let maxIndex = histogram.bins.length - 1;
 
@@ -414,14 +414,7 @@ export class RenderConfigComponent extends React.Component<WidgetProps> {
         let percentileButtonsDiv, percentileSelectDiv;
         if (displayRankButtons) {
             const percentileRankButtons = RenderConfigStore.PERCENTILE_RANKS.map(rank => (
-                <Button
-                    small={true}
-                    key={rank}
-                    onClick={() => this.handlePercentileRankClick(rank)}
-                    active={frame.renderConfig.selectedPercentileVal === rank}
-                    disabled={appStore.channelMapStore.channelMapEnabled}
-                    data-testid={"clip-button-" + rank}
-                >
+                <Button small={true} key={rank} onClick={() => this.handlePercentileRankClick(rank)} active={frame.renderConfig.selectedPercentileVal === rank} data-testid={"clip-button-" + rank}>
                     {`${rank}%`}
                 </Button>
             ));
diff --git a/src/services/TileService.ts b/src/services/TileService.ts
index da220f512b..dd26fab5c2 100644
--- a/src/services/TileService.ts
+++ b/src/services/TileService.ts
@@ -323,7 +323,7 @@ export class TileService {
         return result;
     }
 
-    requestChannelMapTiles(tiles: TileCoordinate[], frame: FrameStore, focusPoint: Point2D, compressionQuality: number, fullChannelRange: {min: number; max: number}) {
+    requestChannelMapTiles(tiles: TileCoordinate[], frame: FrameStore, focusPoint: Point2D, compressionQuality: number, fullChannelRange: {min: number; max: number}, polarizationChanged: boolean = false) {
         if (!frame) {
             return;
         }
@@ -332,7 +332,19 @@ export class TileService {
         const requiredChannel = frame.channel;
         const currentTiles = tiles.map(tile => tile.encode());
 
-        if (this.currentlyStreamingChannelRange && this.currentlyStreamingTileRange) this.clearQueueForChannelMap(this.pendingRequests, fileId, fullChannelRange, currentTiles, this.currentlyStreamingTileRange);
+        if (polarizationChanged) {
+            for (let i = fullChannelRange.min; i <= fullChannelRange.max; i++) {
+                const key = `${fileId}_${stokes}_${i}`;
+                this.pendingSynchronisedTiles.set(key, new Set(tiles.map(tile => tile.encode())));
+                this.receivedSynchronisedTiles.delete(key);
+            }
+            this.clearRequestQueue(fileId);
+            this.clearCompressedCache(fileId);
+        }
+
+        if (this.currentlyStreamingChannelRange && this.currentlyStreamingTileRange) {
+            this.clearQueueForChannelMap(this.pendingRequests, fileId, fullChannelRange, currentTiles, this.currentlyStreamingTileRange);
+        }
 
         const channelToTilesArray: {channel: number; tiles: TileCoordinate[]}[] = [];
 
@@ -371,13 +383,17 @@ export class TileService {
         }
     }
 
-    updateHiddenFileChannels(fileId: number, channel: number, stokes: number, channelMapEnabled?: boolean) {
-        if (!channelMapEnabled) {
-            this.clearCompressedCache(fileId);
-            this.clearGPUCache(fileId);
-        }
+    updateChannelMapActiveChannel(fileId: number, channel: number, stokes: number) {
+        this.channelMap.set(fileId, {channel, stokes});
+        this.backendService.setChannels(fileId, channel, stokes, {}, true);
+    }
+
+    updateHiddenFileChannels(fileId: number, channel: number, stokes: number) {
+        this.clearCompressedCache(fileId);
+        this.clearGPUCache(fileId);
+
         this.channelMap.set(fileId, {channel, stokes});
-        this.backendService.setChannels(fileId, channel, stokes, {}, channelMapEnabled);
+        this.backendService.setChannels(fileId, channel, stokes, {});
     }
 
     clearGPUCache(fileId: number | null | undefined) {
diff --git a/src/stores/AppStore/AppStore.ts b/src/stores/AppStore/AppStore.ts
index baeff4d215..7b3c42e170 100644
--- a/src/stores/AppStore/AppStore.ts
+++ b/src/stores/AppStore/AppStore.ts
@@ -65,7 +65,7 @@ import {
     WidgetsStore
 } from "stores";
 import {CompassAnnotationStore, CURSOR_REGION_ID, FrameInfo, FrameStore, PointAnnotationStore, RegionStore, RulerAnnotationStore, TextAnnotationStore} from "stores/Frame";
-import {HistogramWidgetStore, SpatialProfileWidgetStore, SpectralProfileWidgetStore, StatsWidgetStore, StokesAnalysisWidgetStore} from "stores/Widgets";
+import {HistogramWidgetStore, RegionId as RegionIdType, SpatialProfileWidgetStore, SpectralProfileWidgetStore, StatsWidgetStore, StokesAnalysisWidgetStore} from "stores/Widgets";
 import {distinct, exportScreenshot, getColorForTheme, GetRequiredTiles, getTimestamp, mapToObject, ProtobufProcessing} from "utilities";
 
 import GitCommit from "../../static/gitInfo";
@@ -565,9 +565,6 @@ export class AppStore {
         this.telemetryService.addFileOpenEntry(ack.fileId, ack.fileInfo.type, ack.fileInfoExtended.width, ack.fileInfoExtended.height, ack.fileInfoExtended.depth, ack.fileInfoExtended.stokes, generated);
 
         let newFrame = new FrameStore(frameInfo);
-        if (!newFrame.isPVImage && newFrame.frameInfo.fileInfoExtended.depth > 1) {
-            this.channelMapStore.setMasterFrame(newFrame);
-        }
 
         // Place frame in frame array (replace frame with the same ID if it exists)
         const existingFrameIndex = this.imageViewConfigStore.getImageListIndex(ImageType.FRAME, ack.fileId);
@@ -1006,13 +1003,6 @@ export class AppStore {
                     }
                 }
 
-                // Clean up if frame is used in channel map
-                if (this.channelMapStore.masterFrame?.frameInfo.fileId === fileId) {
-                    const firstImage = this.imageViewConfigStore.imageNum ? this.imageViewConfigStore.getImage(0) : null;
-                    const firstFrame = (firstImage?.store as FrameStore)?.frameInfo.fileInfoExtended.depth > 1 ? (firstImage.store as FrameStore) : null;
-                    this.channelMapStore.setMasterFrame(firstFrame);
-                }
-
                 if (removedFrameIsRasterScalingReference) {
                     const newReference = firstFrame;
                     if (newReference) {
@@ -1733,14 +1723,17 @@ export class AppStore {
 
             frame.channel = update.channel;
             frame.stokes = update.stokes;
-            if (this.imageViewConfigStore.visibleFrames.includes(frame) && !this.channelMapStore.channelMapEnabled) {
+
+            if (this.channelMapStore.channelMapEnabled) {
+                this.tileService.updateChannelMapActiveChannel(frame.frameInfo.fileId, frame.channel, frame.stokes);
+            } else if (this.imageViewConfigStore.visibleFrames.includes(frame)) {
                 const [tiles, midPointTileCoords] = frame.requiredTiles;
                 // If BUNIT = km/s, adopted compressionQuality is set to 32 regardless the preferences setup
                 const bunitVariant = ["km/s", "km s-1", "km s^-1", "km.s-1"];
                 const compressionQuality = bunitVariant.includes(frame.headerUnit) ? Math.max(this.preferenceStore.imageCompressionQuality, 32) : this.preferenceStore.imageCompressionQuality;
                 this.tileService.requestTiles(tiles, frame.frameInfo.fileId, frame.channel, frame.stokes, midPointTileCoords, compressionQuality, true);
             } else {
-                this.tileService.updateHiddenFileChannels(frame.frameInfo.fileId, frame.channel, frame.stokes, this.channelMapStore.channelMapEnabled);
+                this.tileService.updateHiddenFileChannels(frame.frameInfo.fileId, frame.channel, frame.stokes);
             }
         }
     };
@@ -2062,7 +2055,6 @@ export class AppStore {
                         if (this.syncContourToFrame) {
                             this.contourDataSource = frame;
                         }
-                        this.channelMapStore.setMasterFrame(frame);
                     }
                 } else {
                     this.widgetsStore.updateImageWidgetTitle(this.layoutStore.dockedLayout);
@@ -2218,12 +2210,9 @@ export class AppStore {
         // TODO: update histograms directly if the image is not active!
 
         // Add histogram to pending histogram list
-        if (regionHistogramData.regionId === -1 && !regionHistogramData.config.fixedNumBins && !regionHistogramData.config.fixedBounds) {
+        if (regionHistogramData.regionId === RegionIdType.IMAGE && !regionHistogramData.config.fixedNumBins && !regionHistogramData.config.fixedBounds) {
             const key = `${regionHistogramData.fileId}_${regionHistogramData.stokes}_${regionHistogramData.channel}`;
             this.pendingChannelHistograms.set(key, regionHistogramData);
-            if (this.channelMapStore.channelMapEnabled && regionHistogramData.channel === 0) {
-                this.updateHistogram(regionHistogramData.fileId, regionHistogramData.stokes, regionHistogramData.channel, true);
-            }
         } else if (regionHistogramData.regionId === -2) {
             // Update cube histogram if it is still required
             const updatedFrame = this.getFrame(regionHistogramData.fileId);
@@ -2235,6 +2224,11 @@ export class AppStore {
                 }
             }
         }
+
+        // update the render config widget histogram for channel map view mode
+        if (this.channelMapStore.channelMapEnabled && regionHistogramData.regionId === RegionIdType.IMAGE && regionHistogramData.stokes === this.activeFrame?.stokes) {
+            this.updateHistogram(regionHistogramData.fileId, regionHistogramData.stokes, regionHistogramData.channel);
+        }
     };
 
     @action handleTileStream = (tileStreamDetails: TileStreamDetails) => {
@@ -2270,7 +2264,7 @@ export class AppStore {
         this.updateHistogram(tileStreamDetails.fileId, tileStreamDetails.stokes, tileStreamDetails.channel);
     };
 
-    updateHistogram = (fileId: number, stokes: number, channel: number, updateChannelMapHistogram?: boolean) => {
+    updateHistogram = (fileId: number, stokes: number, channel: number) => {
         // Apply pending channel histogram
         const key = `${fileId}_${stokes}_${channel}`;
         const pendingHistogram = this.pendingChannelHistograms.get(key);
@@ -2284,14 +2278,9 @@ export class AppStore {
                 updatedFrame.renderConfig.updateChannelHistogram(channelHist);
                 updatedFrame.channel = channel;
                 updatedFrame.stokes = stokes;
-
-                if (updateChannelMapHistogram || !updatedFrame.renderConfig.channelMapHistogram) {
-                    updatedFrame.renderConfig.updateChannelMapHistogram(channelHist);
-                }
-            }
-            if (!updateChannelMapHistogram) {
-                this.pendingChannelHistograms.delete(key);
             }
+
+            this.pendingChannelHistograms.delete(key);
         }
     };
 
diff --git a/src/stores/ChannelMapStore/ChannelMapStore.test.ts b/src/stores/ChannelMapStore/ChannelMapStore.test.ts
new file mode 100644
index 0000000000..fadfb5228b
--- /dev/null
+++ b/src/stores/ChannelMapStore/ChannelMapStore.test.ts
@@ -0,0 +1,137 @@
+import {ChannelMapStore} from "./ChannelMapStore";
+
+jest.mock("stores", () => ({
+    AppStore: {
+        Instance: {
+            activeFrame: {
+                frameInfo: {
+                    fileInfoExtended: {depth: 12}
+                }
+            }
+        }
+    }
+}));
+
+describe("ChannelMapStore", () => {
+    const store = ChannelMapStore.Instance;
+
+    describe("setChannelMapEnabled", () => {
+        it("updates the channel map mode correctly", () => {
+            expect(store.channelMapEnabled).toBe(false);
+
+            store.setChannelMapEnabled(true);
+            expect(store.channelMapEnabled).toBe(true);
+        });
+    });
+
+    describe("setStartChannel", () => {
+        it("sets the displayed channels correctly", () => {
+            expect(store.startChannel).toBe(0);
+            expect(store.endChannel).toBe(3);
+            expect(store.channelArray).toEqual([0, 1, 2, 3]);
+
+            store.setStartChannel(1);
+            expect(store.startChannel).toBe(1);
+            expect(store.endChannel).toBe(4);
+            expect(store.channelArray).toEqual([1, 2, 3, 4]);
+        });
+
+        it("skips when the channel is out of range", () => {
+            store.setStartChannel(-1);
+            expect(store.startChannel).toBe(1);
+
+            store.setStartChannel(100);
+            expect(store.startChannel).toBe(1);
+        });
+    });
+
+    describe("setPrevChannel", () => {
+        it("sets the displayed channels correctly", () => {
+            store.setPrevChannel();
+            expect(store.startChannel).toBe(0);
+            expect(store.endChannel).toBe(3);
+            expect(store.channelArray).toEqual([0, 1, 2, 3]);
+        });
+    });
+
+    describe("setNextChannel", () => {
+        it("sets the displayed channels correctly", () => {
+            store.setNextChannel();
+            expect(store.startChannel).toBe(1);
+            expect(store.endChannel).toBe(4);
+            expect(store.channelArray).toEqual([1, 2, 3, 4]);
+        });
+    });
+
+    describe("setPrevPage", () => {
+        it("sets the displayed channels correctly", () => {
+            store.setStartChannel(5);
+            store.setPrevPage();
+            expect(store.startChannel).toBe(1);
+            expect(store.endChannel).toBe(4);
+            expect(store.channelArray).toEqual([1, 2, 3, 4]);
+        });
+
+        it("skips when the new start is out of range", () => {
+            store.setPrevPage();
+            expect(store.startChannel).toBe(1);
+        });
+    });
+
+    describe("setNextPage", () => {
+        it("sets the displayed channels correctly", () => {
+            store.setNextPage();
+            expect(store.startChannel).toBe(5);
+            expect(store.endChannel).toBe(8);
+            expect(store.channelArray).toEqual([5, 6, 7, 8]);
+
+            store.setNextPage();
+            expect(store.startChannel).toBe(9);
+            expect(store.endChannel).toBe(11);
+            expect(store.channelArray).toEqual([9, 10, 11]);
+        });
+
+        it("skips when the new start is out of range", () => {
+            store.setNextPage();
+            expect(store.startChannel).toBe(9);
+        });
+    });
+
+    describe("setNumColumns", () => {
+        it("sets the image view panel config correctly", () => {
+            store.setNumColumns(3);
+            expect(store.numColumns).toBe(3);
+            expect(store.numChannels).toBe(6);
+        });
+
+        it("skips when the number is invalid", () => {
+            store.setNumColumns(NaN);
+            expect(store.numColumns).toBe(3);
+
+            store.setNumColumns(0);
+            expect(store.numColumns).toBe(3);
+        });
+    });
+
+    describe("setNumRows", () => {
+        it("sets the image view panel config correctly", () => {
+            store.setNumRows(3);
+            expect(store.numRows).toBe(3);
+            expect(store.numChannels).toBe(9);
+        });
+
+        it("skips when the number is invalid", () => {
+            store.setNumRows(NaN);
+            expect(store.numRows).toBe(3);
+
+            store.setNumRows(0);
+            expect(store.numRows).toBe(3);
+        });
+    });
+
+    describe("totalChannelNum", () => {
+        it("returns number of channels of the active image", () => {
+            expect(store.totalChannelNum).toBe(12);
+        });
+    });
+});
diff --git a/src/stores/ChannelMapStore/ChannelMapStore.ts b/src/stores/ChannelMapStore/ChannelMapStore.ts
index 230c84c875..e959004d0b 100644
--- a/src/stores/ChannelMapStore/ChannelMapStore.ts
+++ b/src/stores/ChannelMapStore/ChannelMapStore.ts
@@ -1,7 +1,6 @@
-import {throttle} from "lodash";
-import {action, autorun, computed, makeObservable, observable} from "mobx";
+import {debounce, throttle} from "lodash";
+import {action, autorun, computed, makeObservable, observable, reaction} from "mobx";
 
-import {ImageItem, ImageType} from "models";
 import {TileService} from "services";
 import {AppStore, FrameStore} from "stores";
 
@@ -19,33 +18,53 @@ export class ChannelMapStore {
     constructor() {
         makeObservable(this);
         ChannelMapStore.staticInstance = this;
-        this.startChannel = 0;
-        this.numColumns = 2;
-        this.numRows = 2;
 
         autorun(() => {
-            if (this.masterFrame && this.masterFrame?.requiredFrameView && this.channelMapEnabled) {
+            const activeFrame = AppStore.Instance.activeFrame;
+            if (activeFrame?.requiredFrameView && this.channelMapEnabled) {
                 /* eslint-disable @typescript-eslint/no-unused-vars */
                 const startChannel = this.startChannel;
                 const numColumns = this.numColumns;
                 const numRows = this.numRows;
-                const channelRange = this.endChannel;
-                const center = this.masterFrame.center;
-                const requiredFrameView = this.masterFrame.requiredFrameView;
-                const requiredTiles = this.masterFrame.requiredTiles;
-                const zoomLevel = this.masterFrame.zoomLevel;
-                const spatialReference = this.masterFrame.spatialReference;
-                const channel = this.masterFrame.channel;
+                const endChannel = this.endChannel;
+                const center = activeFrame.center;
+                const requiredFrameView = activeFrame.requiredFrameView;
+                const requiredTiles = activeFrame.requiredTiles;
+                const zoomLevel = activeFrame.zoomLevel;
+                const spatialReference = activeFrame.spatialReference;
+                const channel = activeFrame.channel;
                 /* eslint-disable @typescript-eslint/no-unused-vars */
-                this.throttledRequestChannels(this.masterFrame);
+                this.throttledRequestChannels(activeFrame);
             }
         });
+
+        reaction(
+            () => this.channelArray,
+            channelArray => {
+                const channel = AppStore.Instance.activeFrame?.channel;
+                if (this.channelMapEnabled && Number.isFinite(channel) && !channelArray.includes(channel)) {
+                    this.debouncedSetAciveChannel(channelArray[0]);
+                }
+            }
+        );
+
+        reaction(
+            () => AppStore.Instance.activeFrame?.channel,
+            channel => {
+                if (Number.isFinite(channel) && !this.channelArray.includes(channel)) {
+                    this.setStartChannel(channel);
+                }
+            }
+        );
     }
 
-    @observable masterFrame: FrameStore;
+    /** The first channel at the top-left corner. */
     @observable startChannel: number = 0;
-    @observable numColumns: number;
-    @observable numRows: number;
+    /** The number of columns in the image view. */
+    @observable numColumns: number = 2;
+    /** The number of rows in the image view. */
+    @observable numRows: number = 2;
+    /** Indicates whether the channel map mode is enabled. */
     @observable channelMapEnabled: boolean = false;
 
     @observable showChannelString: boolean = false;
@@ -55,60 +74,85 @@ export class ChannelMapStore {
     @observable showSpectralStringLabel: boolean = false;
     @observable showVelocityStringLabel: boolean = false;
 
-    @action throttledRequestChannels = throttle((frame: FrameStore) => {
+    private throttledRequestChannels = throttle((frame: FrameStore) => this.requestChannels(frame), 100);
+    private debouncedSetAciveChannel = debounce((channel: number) => AppStore.Instance.activeFrame?.setChannel(channel), 200);
+
+    /**
+     * Clears the cache and requests new tiles when the polarization changes.
+     * @param frame - the frame to request tiles for.
+     */
+    handlePolarizationChanged = (frame: FrameStore) => this.requestChannels(frame, true);
+
+    private requestChannels = (frame: FrameStore, polarizationChanged: boolean = false) => {
         const [tiles, midPointTileCoords] = frame.requiredTiles;
         const preferenceStore = AppStore.Instance.preferenceStore;
         const bunitVariant = ["km/s", "km s-1", "km s^-1", "km.s-1"];
         const compressionQuality = frame.headerUnit && bunitVariant.includes(frame.headerUnit) ? Math.max(preferenceStore.imageCompressionQuality, 32) : preferenceStore.imageCompressionQuality;
-        TileService.Instance.requestChannelMapTiles(tiles, this.masterFrame, midPointTileCoords, compressionQuality, {min: this.startChannel, max: this.endChannel});
-    }, 100);
-
-    @action setMasterFrame(masterFrame: FrameStore) {
-        this.masterFrame = masterFrame;
-    }
+        TileService.Instance.requestChannelMapTiles(tiles, frame, midPointTileCoords, compressionQuality, {min: this.startChannel, max: this.endChannel}, polarizationChanged);
+    };
 
+    /**
+     * Enables or disables the channel map mode.
+     * @param enabled - Whether to enable the channel map mode.
+     */
     @action setChannelMapEnabled = (enabled: boolean) => {
         this.channelMapEnabled = enabled;
     };
 
+    /**
+     * Sets the first channel at the top-left corner. Skips when the channel is out of range.
+     * @param startChannel - The first channel at the top-left corner.
+     */
     @action setStartChannel(startChannel: number) {
         // Add checks for valid startChannel number for the masterFrame
-        if (startChannel < 0 || startChannel > this.masterFrame.frameInfo.fileInfoExtended.depth - 1) {
+        if (startChannel < 0 || startChannel > this.totalChannelNum - 1) {
             return;
         }
         this.startChannel = startChannel;
     }
 
+    /** Sets the first channel at the top-left corner to the previous channel. */
     @action setPrevChannel() {
         this.setStartChannel(this.startChannel - 1);
     }
 
+    /** Sets the first channel at the top-left corner to the next channel. */
     @action setNextChannel() {
         this.setStartChannel(this.startChannel + 1);
     }
 
+    /** Moves to the previous page of channels. */
     @action setPrevPage() {
-        const newStart = this.startChannel - this.numColumns * this.numRows;
+        const newStart = this.startChannel - this.numChannels;
 
         if (newStart >= 0) {
             this.setStartChannel(newStart);
         }
     }
 
+    /** Moves to the next page of channels. */
     @action setNextPage() {
-        const newStart = this.startChannel + this.numColumns * this.numRows;
+        const newStart = this.startChannel + this.numChannels;
 
         if (newStart >= 0) {
             this.setStartChannel(newStart);
         }
     }
 
+    /**
+     * Sets the number of columns in the image vew.
+     * @param numColumns - The number of columns in the image view.
+     */
     @action setNumColumns(numColumns: number) {
         if (isFinite(numColumns) && numColumns > 0) {
             this.numColumns = numColumns;
         }
     }
 
+    /**
+     * Sets the number of rows in the image view.
+     * @param numRows - The number of rows in the image view.
+     */
     @action setNumRows(numRows: number) {
         if (isFinite(numRows) && numRows > 0) {
             this.numRows = numRows;
@@ -145,29 +189,23 @@ export class ChannelMapStore {
         this.showVelocityStringLabel = show;
     };
 
+    /** The number of channels of the active image. Returns 1 if the information is unavailable. */
+    @computed private get totalChannelNum(): number {
+        return AppStore.Instance.activeFrame?.frameInfo?.fileInfoExtended?.depth ?? 1;
+    }
+
+    /** The number of panels in the image view. */
     @computed get numChannels(): number {
         return this.numColumns * this.numRows;
     }
 
+    /** The last channel in the image view. */
     @computed get endChannel(): number {
-        return Math.min(this.startChannel + this.numChannels - 1, this.masterFrame?.frameInfo?.fileInfoExtended?.depth - 1);
+        return Math.min(this.startChannel + this.numChannels - 1, this.totalChannelNum - 1);
     }
 
+    /** The displayed channels in the image view. */
     @computed get channelArray(): number[] {
-        const channelArray: number[] = [];
-        for (let i = this.startChannel; i < this.startChannel + this.numChannels; i += 1) {
-            if (i > this.masterFrame?.frameInfo?.fileInfoExtended.depth - 1) {
-                break;
-            }
-            channelArray.push(i);
-        }
-        return channelArray;
-    }
-
-    @computed get masterImage(): ImageItem {
-        return {
-            type: ImageType.FRAME,
-            store: this.masterFrame
-        };
+        return Array.from({length: this.endChannel - this.startChannel + 1}, (_, i) => this.startChannel + i);
     }
 }
diff --git a/src/stores/Frame/FrameStore.ts b/src/stores/Frame/FrameStore.ts
index 6a53fffe75..a23434b54e 100644
--- a/src/stores/Frame/FrameStore.ts
+++ b/src/stores/Frame/FrameStore.ts
@@ -1560,6 +1560,17 @@ export class FrameStore {
                 }
             }
         });
+
+        // Update the image view raster tiles in channel map mode
+        reaction(
+            () => this.stokes,
+            () => {
+                const channelMapStore = AppStore.Instance.channelMapStore;
+                if (this.requiredFrameView && channelMapStore.channelMapEnabled) {
+                    channelMapStore.handlePolarizationChanged(this);
+                }
+            }
+        );
     }
 
     updateWcsSystem = (formatStringX: string, formatStyingY: string, explicitSystem: SystemType) => {
diff --git a/src/stores/Frame/RenderConfigStore/RenderConfigStore.ts b/src/stores/Frame/RenderConfigStore/RenderConfigStore.ts
index 46304f2008..8718637770 100644
--- a/src/stores/Frame/RenderConfigStore/RenderConfigStore.ts
+++ b/src/stores/Frame/RenderConfigStore/RenderConfigStore.ts
@@ -185,7 +185,6 @@ export class RenderConfigStore {
     @observable alpha: number;
     @observable inverted: boolean;
     @observable channelHistogram: CARTA.IHistogram;
-    @observable channelMapHistogram: CARTA.IHistogram;
     @observable cubeHistogram: CARTA.IHistogram;
     @observable useCubeHistogram: boolean;
     @observable useCubeHistogramContours: boolean;
@@ -195,8 +194,6 @@ export class RenderConfigStore {
     @observable stokesIndex: number;
     @observable scaleMin: number[];
     @observable scaleMax: number[];
-    @observable channelMapScaleMin: number;
-    @observable channelMapScaleMax: number;
     @observable visible: boolean;
     @observable previewHistogramMax: number;
     @observable previewHistogramMin: number;
@@ -327,19 +324,11 @@ export class RenderConfigStore {
     }
 
     @computed get scaleMinVal() {
-        return AppStore.Instance.channelMapStore.channelMapEnabled && !AppStore.Instance.activeFrame?.isPreview
-            ? this.channelMapScaleMin
-            : this.previewHistogramMin
-              ? Math.max(this.previewHistogramMin, this.scaleMin[this.stokesIndex])
-              : this.scaleMin[this.stokesIndex];
+        return this.previewHistogramMin ? Math.max(this.previewHistogramMin, this.scaleMin[this.stokesIndex]) : this.scaleMin[this.stokesIndex];
     }
 
     @computed get scaleMaxVal() {
-        return AppStore.Instance.channelMapStore.channelMapEnabled && !AppStore.Instance.activeFrame?.isPreview
-            ? this.channelMapScaleMax
-            : this.previewHistogramMax
-              ? Math.min(this.previewHistogramMax, this.scaleMax[this.stokesIndex])
-              : this.scaleMax[this.stokesIndex];
+        return this.previewHistogramMax ? Math.min(this.previewHistogramMax, this.scaleMax[this.stokesIndex]) : this.scaleMax[this.stokesIndex];
     }
 
     @computed get selectedPercentileVal() {
@@ -440,16 +429,6 @@ export class RenderConfigStore {
         }
     };
 
-    @action updateChannelMapHistogram = (histogram: CARTA.IHistogram) => {
-        this.channelMapHistogram = histogram;
-
-        // Need change to 1 standard deviation.
-        if (this.selectedPercentile[this.stokesIndex] > 0 && !this.useCubeHistogram) {
-            this.channelMapScaleMin = -histogram.stdDev;
-            this.channelMapScaleMax = histogram.stdDev * 10;
-        }
-    };
-
     @action updateCubeHistogram = (histogram: CARTA.IHistogram, progress: number) => {
         this.cubeHistogram = histogram;
         this.cubeHistogramProgress = progress;
@@ -465,13 +444,9 @@ export class RenderConfigStore {
      * @param maxVal - The maximum scaling value.
      */
     @action setCustomScale = (minVal: number, maxVal: number) => {
-        if (AppStore.Instance.channelMapStore.channelMapEnabled) {
-            this.channelMapScaleMin = minVal;
-            this.channelMapScaleMax = maxVal;
-        } else {
-            this.scaleMin[this.stokesIndex] = minVal;
-            this.scaleMax[this.stokesIndex] = maxVal;
-        }
+        this.scaleMin[this.stokesIndex] = minVal;
+        this.scaleMax[this.stokesIndex] = maxVal;
+
         this.selectedPercentile[this.stokesIndex] = -1;
         this.updateSiblings();
     };


Reviews:
user: login: kswang1029
body: This looks good~ UX should improve substantially. 👍 No regression from e2e.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-01T01:12:34Z
user: login: loveluthien
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T05:50:56Z
user: login: YuHsuan-Hwang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T03:38:26Z
user: login: loveluthien
body: 😎
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-02T04:43:07Z

Review comments:
diff_hunk: @@ -19,33 +18,53 @@ export class ChannelMapStore {
     constructor() {
         makeObservable(this);
         ChannelMapStore.staticInstance = this;
-        this.startChannel = 0;
-        this.numColumns = 2;
-        this.numRows = 2;
 
         autorun(() => {
-            if (this.masterFrame && this.masterFrame?.requiredFrameView && this.channelMapEnabled) {
+            const activeFrame = AppStore.Instance.activeFrame;
+            if (activeFrame?.requiredFrameView && this.channelMapEnabled) {
                 /* eslint-disable @typescript-eslint/no-unused-vars */
                 const startChannel = this.startChannel;
                 const numColumns = this.numColumns;
                 const numRows = this.numRows;
-                const channelRange = this.endChannel;
-                const center = this.masterFrame.center;
-                const requiredFrameView = this.masterFrame.requiredFrameView;
-                const requiredTiles = this.masterFrame.requiredTiles;
-                const zoomLevel = this.masterFrame.zoomLevel;
-                const spatialReference = this.masterFrame.spatialReference;
-                const channel = this.masterFrame.channel;
+                const endChannel = this.endChannel;
+                const center = activeFrame.center;
+                const requiredFrameView = activeFrame.requiredFrameView;
+                const requiredTiles = activeFrame.requiredTiles;
+                const zoomLevel = activeFrame.zoomLevel;
+                const spatialReference = activeFrame.spatialReference;
+                const channel = activeFrame.channel;
                 /* eslint-disable @typescript-eslint/no-unused-vars */
-                this.throttledRequestChannels(this.masterFrame);
+                this.throttledRequestChannels(activeFrame);
             }
         });
+
+        reaction(
+            () => this.channelArray,
+            channelArray => {
+                const channel = AppStore.Instance.activeFrame?.channel;
+                if (this.channelMapEnabled && Number.isFinite(channel) && !channelArray.includes(channel)) {
+                    this.debouncedSetAciveChannel(channelArray[0]);
+                }
+            }
+        );
+
+        reaction(
+            () => AppStore.Instance.activeFrame?.channel,
+            channel => {
+                if (Number.isFinite(channel) && !this.channelArray.includes(channel)) {
+                    this.setStartChannel(channel);
+                }
+            }
+        );
     }
 
-    @observable masterFrame: FrameStore;
+    /** The first channel at the top-right corner. */
path: src/stores/ChannelMapStore/ChannelMapStore.ts
user: login: loveluthien
body: top-left?
created_at: 2025-04-01T05:50:54Z
author_association: COLLABORATOR
diff_hunk: @@ -19,33 +18,53 @@ export class ChannelMapStore {
     constructor() {
         makeObservable(this);
         ChannelMapStore.staticInstance = this;
-        this.startChannel = 0;
-        this.numColumns = 2;
-        this.numRows = 2;
 
         autorun(() => {
-            if (this.masterFrame && this.masterFrame?.requiredFrameView && this.channelMapEnabled) {
+            const activeFrame = AppStore.Instance.activeFrame;
+            if (activeFrame?.requiredFrameView && this.channelMapEnabled) {
                 /* eslint-disable @typescript-eslint/no-unused-vars */
                 const startChannel = this.startChannel;
                 const numColumns = this.numColumns;
                 const numRows = this.numRows;
-                const channelRange = this.endChannel;
-                const center = this.masterFrame.center;
-                const requiredFrameView = this.masterFrame.requiredFrameView;
-                const requiredTiles = this.masterFrame.requiredTiles;
-                const zoomLevel = this.masterFrame.zoomLevel;
-                const spatialReference = this.masterFrame.spatialReference;
-                const channel = this.masterFrame.channel;
+                const endChannel = this.endChannel;
+                const center = activeFrame.center;
+                const requiredFrameView = activeFrame.requiredFrameView;
+                const requiredTiles = activeFrame.requiredTiles;
+                const zoomLevel = activeFrame.zoomLevel;
+                const spatialReference = activeFrame.spatialReference;
+                const channel = activeFrame.channel;
                 /* eslint-disable @typescript-eslint/no-unused-vars */
-                this.throttledRequestChannels(this.masterFrame);
+                this.throttledRequestChannels(activeFrame);
             }
         });
+
+        reaction(
+            () => this.channelArray,
+            channelArray => {
+                const channel = AppStore.Instance.activeFrame?.channel;
+                if (this.channelMapEnabled && Number.isFinite(channel) && !channelArray.includes(channel)) {
+                    this.debouncedSetAciveChannel(channelArray[0]);
+                }
+            }
+        );
+
+        reaction(
+            () => AppStore.Instance.activeFrame?.channel,
+            channel => {
+                if (Number.isFinite(channel) && !this.channelArray.includes(channel)) {
+                    this.setStartChannel(channel);
+                }
+            }
+        );
     }
 
-    @observable masterFrame: FrameStore;
+    /** The first channel at the top-right corner. */
path: src/stores/ChannelMapStore/ChannelMapStore.ts
user: login: YuHsuan-Hwang
body: Fixed.
created_at: 2025-04-02T03:38:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: YuHsuan-Hwang
created_at: 2025-03-28T06:42:14Z
body: Updates:
* Refactor: added function `updateChannelMapActiveChannel` to show a more clear workflow.
* Fix: avoid updating the render config widget when changing the active polarization in the histogram widget.
* Fix: applied debouncing when updating the active channel in response to the new channel range.
author_association: CONTRIBUTOR
user: login: loveluthien
created_at: 2025-04-02T04:44:51Z
body: need to update the change log?
author_association: COLLABORATOR
user: login: YuHsuan-Hwang
created_at: 2025-04-02T05:04:11Z
body: Change log updated.
author_association: CONTRIBUTOR