Repository: metabase/metabase
Pull request #58072:  Closes VIZ-982 settings not always being propagated 
Author: login: lorem--ipsum
Labels: name: backport
description: Automatically create PR on current release branch on merge
name: .Team/DashViz
description: DEPRECATED  Please use .Team/UXWest instead
Created: 2025-05-16T09:36:00Z | Merged: 2025-05-19T08:56:33Z

<!-- Added by 'Add Issue References to PR' GitHub Action. To disable linking, add 'no-auto-issue-links' label to your PR. --> closes #57902
Closes [VIZ-982: Question viz settings aren't always propagated to "Visualize another way"](https://linear.app/metabase/issue/VIZ-982/question-viz-settings-arent-always-propagated-to-visualize-another-way)

### Description

Series specific settings weren't propagated when converting a question to a visualizer card, because it weren't migrated to use column refs' names.

### Demo

https://www.loom.com/share/1703c6de8bea44a089b310c586e4efe4

### Checklist

- [x] Tests have been added/updated to cover changes in this PR


Linked issue #57902: Question viz settings aren't always propagated to "Visualize another way"
### Describe the bug

[Repro loom](https://www.loom.com/share/314d9d8c6d3e454b964a230b863edae5?sid=cc05fe57-ccf8-43f6-816c-6bbde8431de6).

### To Reproduce

See above


### Expected behavior

_No response_

### Logs

_No response_

### Information about your Metabase installation

```JSON
Master
```

### Severity

P2

### Additional context

_No response_

Diff:
diff --git a/frontend/src/metabase/visualizer/utils/get-initial-state-for-card-data-source.ts b/frontend/src/metabase/visualizer/utils/get-initial-state-for-card-data-source.ts
index e49e36bca673..606c001e8ebc 100644
--- a/frontend/src/metabase/visualizer/utils/get-initial-state-for-card-data-source.ts
+++ b/frontend/src/metabase/visualizer/utils/get-initial-state-for-card-data-source.ts
@@ -31,6 +31,7 @@ import {
   isVisualizerSupportedVisualization,
 } from "./dashboard-card-supports-visualizer";
 import { createDataSource, createDataSourceNameRef } from "./data-source";
+import { updateVizSettingsWithRefs } from "./update-viz-settings-with-refs";
 import { getColumnVizSettings } from "./viz-settings";
 
 function pickColumnsFromTableToBarChart(
@@ -134,6 +135,7 @@ export function getInitialStateForCardDataSource(
     }
   }
 
+  const columnsToRefs: Record<string, string> = {};
   const columns = pickColumns(card.display, originalColumns);
 
   columns.forEach((column) => {
@@ -146,6 +148,7 @@ export function getInitialStateForCardDataSource(
       copyColumn(columnRef.name, column, dataSource.name, state.columns),
     );
     state.columnValuesMapping[columnRef.name] = [columnRef];
+    columnsToRefs[column.name] = columnRef.name;
   });
 
   const computedSettings: ComputedVisualizationSettings =
@@ -195,7 +198,7 @@ export function getInitialStateForCardDataSource(
     .filter(isNotNull);
 
   state.settings = {
-    ...card.visualization_settings,
+    ...updateVizSettingsWithRefs(card.visualization_settings, columnsToRefs),
     ...Object.fromEntries(entries),
     "card.title": card.name,
   };
diff --git a/frontend/src/metabase/visualizer/utils/get-initial-state-for-multiple-series.ts b/frontend/src/metabase/visualizer/utils/get-initial-state-for-multiple-series.ts
index 700b95591a70..2d84e2fd8235 100644
--- a/frontend/src/metabase/visualizer/utils/get-initial-state-for-multiple-series.ts
+++ b/frontend/src/metabase/visualizer/utils/get-initial-state-for-multiple-series.ts
@@ -14,6 +14,7 @@ import {
   extractReferencedColumns,
 } from "./column";
 import { createDataSource } from "./data-source";
+import { updateVizSettingsWithRefs } from "./update-viz-settings-with-refs";
 import { getColumnVizSettings } from "./viz-settings";
 
 type ColumnInfo = {
@@ -114,9 +115,29 @@ export function getInitialStateForMultipleSeries(rawSeries: RawSeries) {
     },
   );
 
-  const settingsFromAllCards = rawSeries.map((series, seriesIndex) =>
-    mapColumnVizSettings(series.card, allColumnInfos[seriesIndex]),
-  );
+  const columnsToRefs: Record<string, string> = {};
+  allColumnInfos.flat().forEach(({ columnRef }) => {
+    columnsToRefs[columnRef.originalName] = columnRef.name;
+  });
+
+  // Hack because settings use the original column display names for
+  // cards series except the first one ಠ_ಠ
+  rawSeries.forEach((series) => {
+    const metricName =
+      series.card.visualization_settings?.["graph.metrics"]?.[0];
+    if (metricName) {
+      const displayName = series.data.cols.find(
+        (c) => c.name === metricName,
+      )?.display_name;
+      if (displayName) {
+        columnsToRefs[series.card.name] = displayName;
+      }
+    }
+  });
+
+  const settingsFromAllCards = rawSeries.map((series, seriesIndex) => {
+    return mapColumnVizSettings(series.card, allColumnInfos[seriesIndex]);
+  });
 
   const mergedSettings = settingsFromAllCards.reduce<
     Record<string, string | string[]>
@@ -133,7 +154,10 @@ export function getInitialStateForMultipleSeries(rawSeries: RawSeries) {
   }, {});
 
   state.settings = {
-    ...mainCard.visualization_settings,
+    ...updateVizSettingsWithRefs(
+      mainCard.visualization_settings,
+      columnsToRefs,
+    ),
     ...mergedSettings,
     "card.title": mainCard.name,
   };
diff --git a/frontend/src/metabase/visualizer/utils/update-viz-settings-with-refs.ts b/frontend/src/metabase/visualizer/utils/update-viz-settings-with-refs.ts
new file mode 100644
index 000000000000..68ac729fb35f
--- /dev/null
+++ b/frontend/src/metabase/visualizer/utils/update-viz-settings-with-refs.ts
@@ -0,0 +1,51 @@
+import type { VisualizationSettings } from "metabase-types/api";
+
+/**
+ * Recursively converts visualization settings to use the new column references.
+ *
+ * If the settings contain the color for a series called, say, `avg` (`{colors: {avg: "#000"}}`),
+ * and the column reference for `avg` is `COLUMN_1`, this function will convert it
+ * to `{colors: {COLUMN_1: "#000"}}`.
+ *
+ *
+ * @param settings the settings to convert
+ * @param columnsToRefs the mapping of column names to their references
+ * @returns the converted settings
+ */
+export function updateVizSettingsWithRefs(
+  settings: VisualizationSettings,
+  columnsToRefs: Record<string, string>,
+): VisualizationSettings {
+  if (typeof settings !== "object" || settings === null) {
+    return settings;
+  }
+
+  if (Array.isArray(settings)) {
+    return settings.map((item) =>
+      updateVizSettingsWithRefs(item, columnsToRefs),
+    );
+  }
+
+  if (typeof settings === "object") {
+    const newSettings: VisualizationSettings = {};
+
+    for (const key in settings) {
+      // If the key exists in columnsToRefs, use the reference as the new key
+      const newKey = columnsToRefs[key] || key;
+
+      // Process the value recursively
+      const value = settings[key];
+      const newValue =
+        typeof value === "object" && value !== null
+          ? updateVizSettingsWithRefs(value, columnsToRefs)
+          : value;
+
+      // Add the entry with the new key and processed value
+      newSettings[newKey] = newValue;
+    }
+
+    return newSettings;
+  }
+
+  return settings;
+}
diff --git a/frontend/src/metabase/visualizer/utils/update-viz-settings-with-refs.unit.spec.ts b/frontend/src/metabase/visualizer/utils/update-viz-settings-with-refs.unit.spec.ts
new file mode 100644
index 000000000000..dfa506e021d9
--- /dev/null
+++ b/frontend/src/metabase/visualizer/utils/update-viz-settings-with-refs.unit.spec.ts
@@ -0,0 +1,164 @@
+import type { VisualizationSettings } from "metabase-types/api";
+
+import { updateVizSettingsWithRefs } from "./update-viz-settings-with-refs";
+
+describe("updateVizSettingsWithRefs", () => {
+  it("should handle empty objects", () => {
+    expect(updateVizSettingsWithRefs({}, {})).toEqual({});
+    expect(updateVizSettingsWithRefs({}, { avg: "COLUMN_1" })).toEqual({});
+  });
+
+  it("should convert column names to references in keys", () => {
+    const settings: VisualizationSettings = {
+      colors: {
+        avg: "#000",
+        sum: "#fff",
+      },
+    };
+    const columnsToRefs = { avg: "COLUMN_1", sum: "COLUMN_2" };
+
+    const result = updateVizSettingsWithRefs(settings, columnsToRefs);
+
+    expect(result).toEqual({
+      colors: {
+        COLUMN_1: "#000",
+        COLUMN_2: "#fff",
+      },
+    });
+  });
+
+  it("should handle nested objects", () => {
+    const settings: VisualizationSettings = {
+      series: {
+        avg: {
+          color: "#000",
+        },
+        sum: {
+          color: "#fff",
+        },
+      },
+    };
+    const columnsToRefs = { avg: "COLUMN_1", sum: "COLUMN_2" };
+
+    const result = updateVizSettingsWithRefs(settings, columnsToRefs);
+
+    expect(result).toEqual({
+      series: {
+        COLUMN_1: {
+          color: "#000",
+        },
+        COLUMN_2: {
+          color: "#fff",
+        },
+      },
+    });
+  });
+
+  it("should handle arrays", () => {
+    const settings: VisualizationSettings = {
+      dimensions: [
+        { name: "avg", color: "#000" },
+        { name: "sum", color: "#fff" },
+      ],
+    };
+    const columnsToRefs = { avg: "COLUMN_1", sum: "COLUMN_2" };
+
+    const result = updateVizSettingsWithRefs(settings, columnsToRefs);
+
+    expect(result).toEqual({
+      dimensions: [
+        { name: "avg", color: "#000" },
+        { name: "sum", color: "#fff" },
+      ],
+    });
+  });
+
+  it("should handle complex nested structure", () => {
+    const settings: VisualizationSettings = {
+      series_settings: {
+        avg: { color: "#000", "line.style": "solid" },
+        sum: { color: "#fff", "line.style": "dashed" },
+        count: { color: "#f00" },
+      },
+      column_settings: {
+        avg: { format: "number" },
+        ['["name","avg"]']: { format: "currency" },
+      },
+      data: [
+        { series: "avg", value: 10 },
+        { series: "sum", value: 20 },
+      ],
+    };
+    const columnsToRefs = {
+      avg: "COLUMN_1",
+      sum: "COLUMN_2",
+      ['["name","avg"]']: "COLUMN_3",
+    };
+
+    const result = updateVizSettingsWithRefs(settings, columnsToRefs);
+
+    expect(result).toEqual({
+      series_settings: {
+        COLUMN_1: { color: "#000", "line.style": "solid" },
+        COLUMN_2: { color: "#fff", "line.style": "dashed" },
+        count: { color: "#f00" },
+      },
+      column_settings: {
+        COLUMN_1: { format: "number" },
+        COLUMN_3: { format: "currency" },
+      },
+      data: [
+        { series: "avg", value: 10 },
+        { series: "sum", value: 20 },
+      ],
+    });
+  });
+
+  it("should handle column reference in a deeper object path", () => {
+    const settings: VisualizationSettings = {
+      deep: {
+        nested: {
+          avg: { show: true },
+          sum: { show: false },
+        },
+      },
+    };
+    const columnsToRefs = { avg: "COLUMN_1", sum: "COLUMN_2" };
+
+    const result = updateVizSettingsWithRefs(settings, columnsToRefs);
+
+    expect(result).toEqual({
+      deep: {
+        nested: {
+          COLUMN_1: { show: true },
+          COLUMN_2: { show: false },
+        },
+      },
+    });
+  });
+
+  it("should not modify original settings object", () => {
+    const settings: VisualizationSettings = {
+      colors: { avg: "#000" },
+    };
+    const columnsToRefs = { avg: "COLUMN_1" };
+
+    updateVizSettingsWithRefs(settings, columnsToRefs);
+
+    expect(settings).toEqual({
+      colors: { avg: "#000" },
+    });
+  });
+
+  it("should handle empty columnsToRefs", () => {
+    const settings: VisualizationSettings = {
+      colors: { avg: "#000", sum: "#fff" },
+    };
+
+    const result = updateVizSettingsWithRefs(settings, {});
+
+    expect(result).toEqual({
+      colors: { avg: "#000", sum: "#fff" },
+    });
+  });
+});


Reviews:
user: login: alxnddr
body: Works good! I left one minor comment, please have a look before merging
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-16T19:54:51Z
user: login: lorem--ipsum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T08:14:51Z

Review comments:
diff_hunk: @@ -0,0 +1,51 @@
+import type { VisualizationSettings } from "metabase-types/api";
+
+/**
+ * Recursively converts visualization settings to use the new column references.
+ *
+ * If the settings contain the color for a series called, say, `avg` (`{colors: {avg: "#000"}}`),
+ * and the column reference for `avg` is `COLUMN_1`, this function will convert it
+ * to `{colors: {COLUMN_1: "#000"}}`.
+ *
+ *
+ * @param settings the settings to convert
+ * @param columnsToRefs the mapping of column names to their references
+ * @returns the converted settings
+ */
+export function updateVizSettingsWithRefs(
path: frontend/src/metabase/visualizer/utils/update-viz-settings-with-refs.ts
user: login: alxnddr
body: Wdyt about changing this signature to `deepUpdateObjectKeys(object: unknown, mapping: Record<string, string>)` to make it more precise and remove type casts like `null as any` in specs? 
created_at: 2025-05-16T19:53:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,51 @@
+import type { VisualizationSettings } from "metabase-types/api";
+
+/**
+ * Recursively converts visualization settings to use the new column references.
+ *
+ * If the settings contain the color for a series called, say, `avg` (`{colors: {avg: "#000"}}`),
+ * and the column reference for `avg` is `COLUMN_1`, this function will convert it
+ * to `{colors: {COLUMN_1: "#000"}}`.
+ *
+ *
+ * @param settings the settings to convert
+ * @param columnsToRefs the mapping of column names to their references
+ * @returns the converted settings
+ */
+export function updateVizSettingsWithRefs(
path: frontend/src/metabase/visualizer/utils/update-viz-settings-with-refs.ts
user: login: lorem--ipsum
body: I don't want this function to be too generic, in fact I think the specs that test it against non-object values is meaningless (I'll remove it), TS would catch that.
created_at: 2025-05-19T08:14:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: trunk-io[bot]
created_at: 2025-05-16T10:36:02Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://raster.shields.io/badge/24643_tests-fd302c29-purple)](https://app.trunk.io/metabase/flaky-tests/pr/58072?repo=metabase/metabase&commitHash=fd302c292da1370c116b08953a1bb10d6f49ae2d&utm_source=gh-comment) &ensp; [![Static Badge](https://raster.shields.io/badge/0-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/58072?repo=metabase/metabase&commitHash=fd302c292da1370c116b08953a1bb10d6f49ae2d&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://raster.shields.io/badge/0-flaky-red)](https://app.trunk.io/metabase/flaky-tests/pr/58072?repo=metabase/metabase&commitHash=fd302c292da1370c116b08953a1bb10d6f49ae2d&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://raster.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/metabase/flaky-tests/pr/58072?repo=metabase/metabase&commitHash=fd302c292da1370c116b08953a1bb10d6f49ae2d&utm_source=gh-comment&runConclusion=QUARANTINED)


<sub>

[View Full Report ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/58072?repo=metabase/metabase&commitHash=fd302c292da1370c116b08953a1bb10d6f49ae2d&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-19T08:57:41Z
body: @lorem--ipsum Manual conflict resolution is required on #58146
author_association: CONTRIBUTOR