From b303750125c019c3919a2e63396b6399f4c23de0 Mon Sep 17 00:00:00 2001
From: Aleksandr Lesnenko <alxnddr@users.noreply.github.com>
Date: Thu, 17 Nov 2022 11:36:11 +0300
Subject: [PATCH] Fix invisible columns click behavior [ci nocache] (#26547)

---
 .../components/RowChart/RowChart.tsx          |   2 +-
 .../visualizations/shared/types/data.ts       |  15 ++-
 .../visualizations/shared/utils/data.ts       | 104 +++++++++---------
 .../shared/utils/data.unit.spec.ts            |  51 +++++----
 .../visualizations/shared/utils/series.ts     |   6 +
 .../visualizations/RowChart/RowChart.tsx      |   2 +-
 .../visualizations/RowChart/utils/events.ts   |  50 ++++++++-
 package.json                                  |   2 +-
 8 files changed, 149 insertions(+), 83 deletions(-)

diff --git a/frontend/src/metabase/static-viz/components/RowChart/RowChart.tsx b/frontend/src/metabase/static-viz/components/RowChart/RowChart.tsx
index f410cb38cdd..8cb7c73b03f 100644
--- a/frontend/src/metabase/static-viz/components/RowChart/RowChart.tsx
+++ b/frontend/src/metabase/static-viz/components/RowChart/RowChart.tsx
@@ -64,7 +64,7 @@ const StaticRowChart = ({ data, settings, getColor }: StaticRowChartProps) => {
     columnValueFormatter,
   );
   const groupedData = getGroupedDataset(
-    data,
+    data.rows,
     chartColumns,
     columnValueFormatter,
   );
diff --git a/frontend/src/metabase/visualizations/shared/types/data.ts b/frontend/src/metabase/visualizations/shared/types/data.ts
index 9bcdcb2e5c6..7c371e4b0ae 100644
--- a/frontend/src/metabase/visualizations/shared/types/data.ts
+++ b/frontend/src/metabase/visualizations/shared/types/data.ts
@@ -1,4 +1,9 @@
-import { DatasetColumn, DatasetData, RowValue } from "metabase-types/api";
+import {
+  DatasetColumn,
+  DatasetData,
+  RowValue,
+  RowValues,
+} from "metabase-types/api";
 
 export type TwoDimensionalChartData = Pick<DatasetData, "rows" | "cols">;
 
@@ -17,7 +22,13 @@ export type GroupedDatum = {
   dimensionValue: RowValue;
   metrics: MetricDatum;
   isClickable: boolean;
-  breakout?: { [key: BreakoutName]: MetricDatum };
+  breakout?: {
+    [key: BreakoutName]: {
+      metrics: MetricDatum;
+      rawRows: RowValues[];
+    };
+  };
+  rawRows: RowValues[];
 };
 
 export type GroupedDataset = GroupedDatum[];
diff --git a/frontend/src/metabase/visualizations/shared/utils/data.ts b/frontend/src/metabase/visualizations/shared/utils/data.ts
index c4c42b06759..3fb4a22e0da 100644
--- a/frontend/src/metabase/visualizations/shared/utils/data.ts
+++ b/frontend/src/metabase/visualizations/shared/utils/data.ts
@@ -4,7 +4,6 @@ import { RowValue, RowValues, SeriesOrderSetting } from "metabase-types/api";
 import {
   ChartColumns,
   ColumnDescriptor,
-  getColumnDescriptors,
 } from "metabase/visualizations/lib/graph/columns";
 import { ColumnFormatter } from "metabase/visualizations/shared/types/format";
 import {
@@ -17,7 +16,7 @@ import {
 } from "metabase/visualizations/shared/types/data";
 import { Series } from "metabase/visualizations/shared/components/RowChart/types";
 import { formatNullable } from "metabase/lib/formatting/nullable";
-import { isMetric } from "metabase-lib/types/utils/isa";
+import { getChartMetrics } from "./series";
 
 const getMetricValue = (value: RowValue): MetricValue => {
   if (typeof value === "number") {
@@ -27,26 +26,29 @@ const getMetricValue = (value: RowValue): MetricValue => {
   return null;
 };
 
+export const sumMetric = (left: RowValue, right: RowValue) => {
+  if (typeof left === "number" && typeof right === "number") {
+    return left + right;
+  } else if (typeof left === "number") {
+    return left;
+  } else if (typeof right === "number") {
+    return right;
+  }
+
+  return null;
+};
+
 const sumMetrics = (left: MetricDatum, right: MetricDatum): MetricDatum => {
   const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
   return Array.from(keys).reduce<MetricDatum>((datum, metricKey) => {
-    const leftValue = left[metricKey];
-    const rightValue = right[metricKey];
-
-    if (typeof leftValue === "number" || typeof rightValue === "number") {
-      datum[metricKey] = (leftValue ?? 0) + (rightValue ?? 0);
-    } else {
-      datum[metricKey] = null;
-    }
-
+    datum[metricKey] = sumMetric(left[metricKey], right[metricKey]);
     return datum;
   }, {});
 };
 
-const groupDataByDimensions = (
+export const getGroupedDataset = (
   rows: RowValues[],
   chartColumns: ChartColumns,
-  allMetrics: ColumnDescriptor[],
   columnFormatter: ColumnFormatter,
 ): GroupedDataset => {
   const { dimension } = chartColumns;
@@ -60,12 +62,16 @@ const groupDataByDimensions = (
       dimensionValue,
       metrics: {},
       isClickable: true,
+      rawRows: [],
     };
 
-    const rowMetrics = allMetrics.reduce<MetricDatum>((datum, metric) => {
-      datum[metric.column.name] = getMetricValue(row[metric.index]);
-      return datum;
-    }, {});
+    const rowMetrics = getChartMetrics(chartColumns).reduce<MetricDatum>(
+      (datum, metric) => {
+        datum[metric.column.name] = getMetricValue(row[metric.index]);
+        return datum;
+      },
+      {},
+    );
 
     datum.metrics = sumMetrics(rowMetrics, datum.metrics);
 
@@ -75,47 +81,31 @@ const groupDataByDimensions = (
         chartColumns.breakout.column,
       );
 
+      const breakoutRawRows = datum.breakout?.[breakoutName]?.rawRows ?? [];
+      breakoutRawRows.push(row);
+
+      const breakoutMetrics = sumMetrics(
+        rowMetrics,
+        datum.breakout?.[breakoutName]?.metrics ?? {},
+      );
+
       datum.breakout = {
         ...datum.breakout,
-        [breakoutName]: sumMetrics(
-          rowMetrics,
-          datum.breakout?.[breakoutName] ?? {},
-        ),
+        [breakoutName]: {
+          metrics: breakoutMetrics,
+          rawRows: breakoutRawRows,
+        },
       };
     }
 
+    datum.rawRows.push(row);
+
     groupedData.set(dimensionValue, datum);
   }
 
   return Array.from(groupedData.values());
 };
 
-export const getGroupedDataset = (
-  data: TwoDimensionalChartData,
-  chartColumns: ChartColumns,
-  columnFormatter: ColumnFormatter,
-): GroupedDataset => {
-  // We are grouping all metrics because they are used in chart tooltips
-  const allMetricColumns = data.cols
-    .filter(
-      (column, index) =>
-        isMetric(column) && index !== chartColumns.dimension.index,
-    )
-    .map(column => column.name);
-
-  const allMetricDescriptors = getColumnDescriptors(
-    allMetricColumns,
-    data.cols,
-  );
-
-  return groupDataByDimensions(
-    data.rows,
-    chartColumns,
-    allMetricDescriptors,
-    columnFormatter,
-  );
-};
-
 export const trimData = (
   dataset: GroupedDataset,
   valuesCountLimit: number,
@@ -142,19 +132,27 @@ export const trimData = (
 
       Object.keys(currentValue.breakout ?? {}).map(breakoutName => {
         groupedValue.breakout ??= {};
-
-        groupedValue.breakout[breakoutName] = sumMetrics(
-          groupedValue.breakout[breakoutName] ?? {},
-          currentValue.breakout?.[breakoutName] ?? {},
-        );
+        groupedValue.breakout[breakoutName] = {
+          metrics: sumMetrics(
+            groupedValue.breakout[breakoutName]?.metrics ?? {},
+            currentValue.breakout?.[breakoutName].metrics ?? {},
+          ),
+          rawRows: [
+            ...(groupedValue.breakout[breakoutName]?.rawRows ?? []),
+            ...(currentValue.breakout?.[breakoutName].rawRows ?? []),
+          ],
+        };
       });
 
+      groupedValue.rawRows.push(...currentValue.rawRows);
+
       return groupedValue;
     },
     {
       dimensionValue: groupedDatumDimensionValue,
       metrics: {},
       isClickable: false,
+      rawRows: [],
     },
   );
 
@@ -187,7 +185,7 @@ const getBreakoutSeries = (
       seriesName: breakoutName,
       yAccessor: (datum: GroupedDatum) => formatNullable(datum.dimensionValue),
       xAccessor: (datum: GroupedDatum) =>
-        datum.breakout?.[breakoutName]?.[metric.column.name] ?? null,
+        datum.breakout?.[breakoutName]?.metrics[metric.column.name] ?? null,
       seriesInfo: {
         metricColumn: metric.column,
         dimensionColumn: dimension.column,
diff --git a/frontend/src/metabase/visualizations/shared/utils/data.unit.spec.ts b/frontend/src/metabase/visualizations/shared/utils/data.unit.spec.ts
index 547bc6cd249..77d2f3336dc 100644
--- a/frontend/src/metabase/visualizations/shared/utils/data.unit.spec.ts
+++ b/frontend/src/metabase/visualizations/shared/utils/data.unit.spec.ts
@@ -1,4 +1,3 @@
-import { DatasetData } from "metabase-types/api";
 import { createMockColumn } from "metabase-types/api/mocks";
 import {
   BreakoutChartColumns,
@@ -20,16 +19,12 @@ const avgMetricColumn = createMockColumn({
   name: "avg",
 });
 
-const dataset: DatasetData = {
-  cols: [dimensionColumn, breakoutColumn, countMetricColumn, avgMetricColumn],
-  rows: [
-    [2020, "Doohickey", 400, 90],
-    [2020, "Gadget", 450, 100],
-    [2021, "Doohickey", 500, 110],
-    [2021, "Gadget", 550, 120],
-  ],
-  rows_truncated: 0,
-};
+const rows = [
+  [2020, "Doohickey", 400, 90],
+  [2020, "Gadget", 450, 100],
+  [2021, "Doohickey", 500, 110],
+  [2021, "Gadget", 550, 120],
+];
 
 const breakoutChartColumns: BreakoutChartColumns = {
   dimension: {
@@ -68,7 +63,7 @@ describe("data utils", () => {
     describe("chart with multiple metrics", () => {
       it("should group dataset by dimension values", () => {
         const groupedData = getGroupedDataset(
-          dataset,
+          rows,
           multipleMetricsChartColumns,
           columnFormatter,
         );
@@ -81,6 +76,7 @@ describe("data utils", () => {
               count: 850,
               avg: 190,
             },
+            rawRows: [rows[0], rows[1]],
           },
           {
             dimensionValue: 2021,
@@ -89,6 +85,7 @@ describe("data utils", () => {
               count: 1050,
               avg: 230,
             },
+            rawRows: [rows[2], rows[3]],
           },
         ]);
       });
@@ -98,7 +95,7 @@ describe("data utils", () => {
   describe("chart with a breakout", () => {
     it("should group dataset by dimension values and breakout", () => {
       const groupedData = getGroupedDataset(
-        dataset,
+        rows,
         breakoutChartColumns,
         columnFormatter,
       );
@@ -109,16 +106,20 @@ describe("data utils", () => {
           isClickable: true,
           metrics: {
             count: 850,
-            avg: 190,
           },
+          rawRows: [rows[0], rows[1]],
           breakout: {
             Doohickey: {
-              count: 400,
-              avg: 90,
+              metrics: {
+                count: 400,
+              },
+              rawRows: [rows[0]],
             },
             Gadget: {
-              count: 450,
-              avg: 100,
+              metrics: {
+                count: 450,
+              },
+              rawRows: [rows[1]],
             },
           },
         },
@@ -127,16 +128,20 @@ describe("data utils", () => {
           isClickable: true,
           metrics: {
             count: 1050,
-            avg: 230,
           },
+          rawRows: [rows[2], rows[3]],
           breakout: {
             Doohickey: {
-              count: 500,
-              avg: 110,
+              metrics: {
+                count: 500,
+              },
+              rawRows: [rows[2]],
             },
             Gadget: {
-              count: 550,
-              avg: 120,
+              metrics: {
+                count: 550,
+              },
+              rawRows: [rows[3]],
             },
           },
         },
diff --git a/frontend/src/metabase/visualizations/shared/utils/series.ts b/frontend/src/metabase/visualizations/shared/utils/series.ts
index b4d42d2dd15..729b2d90a40 100644
--- a/frontend/src/metabase/visualizations/shared/utils/series.ts
+++ b/frontend/src/metabase/visualizations/shared/utils/series.ts
@@ -33,3 +33,9 @@ export const getLabelsMetricColumn = (chartColumns: ChartColumns) => {
     ? chartColumns.metric
     : chartColumns.metrics[0];
 };
+
+export const getChartMetrics = (chartColumns: ChartColumns) => {
+  return "breakout" in chartColumns
+    ? [chartColumns.metric]
+    : chartColumns.metrics;
+};
diff --git a/frontend/src/metabase/visualizations/visualizations/RowChart/RowChart.tsx b/frontend/src/metabase/visualizations/visualizations/RowChart/RowChart.tsx
index 50fb845e4d7..f17c71679b4 100644
--- a/frontend/src/metabase/visualizations/visualizations/RowChart/RowChart.tsx
+++ b/frontend/src/metabase/visualizations/visualizations/RowChart/RowChart.tsx
@@ -132,7 +132,7 @@ const RowChartVisualization = ({
   );
 
   const groupedData = useMemo(
-    () => getGroupedDataset(data, chartColumns, formatColumnValue),
+    () => getGroupedDataset(data.rows, chartColumns, formatColumnValue),
     [chartColumns, data, formatColumnValue],
   );
   const goal = useMemo(() => getChartGoal(settings), [settings]);
diff --git a/frontend/src/metabase/visualizations/visualizations/RowChart/utils/events.ts b/frontend/src/metabase/visualizations/visualizations/RowChart/utils/events.ts
index b0eae2f1a77..fca7451ec27 100644
--- a/frontend/src/metabase/visualizations/visualizations/RowChart/utils/events.ts
+++ b/frontend/src/metabase/visualizations/visualizations/RowChart/utils/events.ts
@@ -3,8 +3,13 @@ import {
   RowValue,
   VisualizationSettings,
 } from "metabase-types/api";
+import { isNotNull } from "metabase/core/utils/types";
 import { formatNullable } from "metabase/lib/formatting/nullable";
-import { ChartColumns } from "metabase/visualizations/lib/graph/columns";
+import {
+  ChartColumns,
+  ColumnDescriptor,
+  getColumnDescriptors,
+} from "metabase/visualizations/lib/graph/columns";
 import {
   BarData,
   Series,
@@ -14,6 +19,8 @@ import {
   MetricDatum,
   SeriesInfo,
 } from "metabase/visualizations/shared/types/data";
+import { sumMetric } from "metabase/visualizations/shared/utils/data";
+import { isMetric } from "metabase-lib/types/utils/isa";
 
 const getMetricColumnData = (
   columns: DatasetColumn[],
@@ -31,6 +38,36 @@ const getMetricColumnData = (
   });
 };
 
+const getColumnData = (columns: ColumnDescriptor[], datum: GroupedDatum) => {
+  return columns
+    .map(columnDescriptor => {
+      const { column, index } = columnDescriptor;
+
+      let value = null;
+
+      if (isMetric(column)) {
+        const metricSum = datum.rawRows.reduce<number | null>(
+          (acc, currentRow) => sumMetric(acc, currentRow[index]),
+          null,
+        );
+
+        value = formatNullable(metricSum);
+      } else {
+        const distinctValues = new Set(datum.rawRows.map(row => row[index]));
+        value = distinctValues.size === 1 ? datum.rawRows[0][index] : null;
+      }
+
+      return value != null
+        ? {
+            key: column.display_name,
+            value: formatNullable(value),
+            col: column,
+          }
+        : null;
+    })
+    .filter(isNotNull);
+};
+
 const getColumnsData = (
   chartColumns: ChartColumns,
   series: Series<GroupedDatum, unknown>,
@@ -54,12 +91,21 @@ const getColumnsData = (
       col: chartColumns.breakout.column,
     });
 
-    metricDatum = datum.breakout[series.seriesKey];
+    metricDatum = datum.breakout[series.seriesKey].metrics;
   } else {
     metricDatum = datum.metrics;
   }
 
   data.push(...getMetricColumnData(datasetColumns, metricDatum));
+
+  const otherColumnsDescriptiors = getColumnDescriptors(
+    datasetColumns
+      .filter(column => !data.some(item => item.col === column))
+      .map(column => column.name),
+    datasetColumns,
+  );
+
+  data.push(...getColumnData(otherColumnsDescriptiors, datum));
   return data;
 };
 
diff --git a/package.json b/package.json
index 7832b258645..9a06d6df975 100644
--- a/package.json
+++ b/package.json
@@ -303,7 +303,7 @@
     "build-stats": "yarn && webpack --json > stats.json",
     "build-shared": "yarn && webpack --config webpack.shared.config.js",
     "build-static-viz": "yarn && yarn build:cljs && webpack --config webpack.static-viz.config.js",
-    "build-static-viz:watch": "yarn build-static-viz --watch",
+    "build-static-viz:watch": "webpack --config webpack.static-viz.config.js --watch",
     "precommit": "lint-staged",
     "preinstall": "echo $npm_execpath | grep -q yarn || echo '\\033[0;33mSorry, npm is not supported. Please use Yarn (https://yarnpkg.com/).\\033[0m'",
     "prettier": "prettier --write '{enterprise/,}frontend/**/*.{js,jsx,ts,tsx,css}'",
-- 
GitLab