You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ma...@apache.org on 2025/08/03 01:57:37 UTC

(superset) 08/08: no-verify-checkpoint

This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch matrixify
in repository https://gitbox.apache.org/repos/asf/superset.git

commit ed45606b24a2a468577fc7323243a02ecde245ed
Author: Maxime Beauchemin <ma...@gmail.com>
AuthorDate: Sat Aug 2 18:56:40 2025 -0700

    no-verify-checkpoint
---
 superset-frontend/package-lock.json                |   1 +
 superset-frontend/package.json                     |   1 +
 .../src/shared-controls/sharedControls.tsx         |  74 ++++
 .../superset-ui-core/src/utils/featureFlags.ts     |   1 +
 .../explore/components/ControlPanelsContainer.tsx  |  77 +++--
 .../explore/components/ExploreChartPanel/index.jsx |  58 +++-
 .../components/MatrixifyGrid/MatrixifyGridCell.tsx | 135 ++++++++
 .../MatrixifyGrid/MatrixifyGridGenerator.ts        |  37 +-
 .../MatrixifyGrid/MatrixifyGridRenderer.tsx        | 295 ++++++++--------
 .../components/MatrixifyGrid/templateUtils.ts      |  59 ++++
 .../src/explore/controlPanels/sections.tsx         |  50 ++-
 .../explore/controlUtils/getSectionsToRender.ts    |  12 +-
 superset-frontend/src/explore/types/matrixify.ts   |  15 +
 superset/config.py                                 |   2 +
 superset/models/helpers.py                         | 380 ++++++++++++++++-----
 15 files changed, 908 insertions(+), 289 deletions(-)

diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index e52430bcfc..91332ee77d 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -77,6 +77,7 @@
         "geostyler-style": "7.5.0",
         "geostyler-wfs-parser": "^2.0.3",
         "googleapis": "^130.0.0",
+        "handlebars": "^4.7.8",
         "immer": "^10.1.1",
         "interweave": "^13.1.0",
         "jquery": "^3.7.1",
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index 71397bcf14..5663532228 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -139,6 +139,7 @@
     "fuse.js": "^7.0.0",
     "geolib": "^2.0.24",
     "geostyler": "^14.1.3",
+    "handlebars": "^4.7.8",
     "geostyler-data": "^1.0.0",
     "geostyler-openlayers-parser": "^4.3.0",
     "geostyler-qgis-parser": "2.0.1",
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
index 379b0ca45c..d6b84fe0ff 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx
@@ -566,5 +566,79 @@ const sharedControls: Record<string, SharedControlConfig<any>> = {
       controls?.[`matrixify_dimension_selection_mode_${axis}`]?.value ===
         'topn',
   };
+  // Add grid layout controls
+  sharedControls.matrixify_row_height = {
+    type: 'TextControl',
+    label: t('Row Height'),
+    description: t('Height of each row in pixels'),
+    default: 300,
+    isInt: true,
+    validators: [],
+  };
+
+  sharedControls.matrixify_fit_columns_dynamically = {
+    type: 'CheckboxControl',
+    label: t('Fit Columns Dynamically'),
+    description: t(
+      'Automatically adjust column width based on available space',
+    ),
+    default: true,
+  };
+
+  sharedControls.matrixify_charts_per_row = {
+    type: 'SelectControl',
+    label: t('Charts Per Row'),
+    description: t('Number of charts to display per row'),
+    default: 4,
+    choices: [
+      [1, '1'],
+      [2, '2'],
+      [3, '3'],
+      [4, '4'],
+      [6, '6'],
+      [12, '12'],
+    ],
+    clearable: false,
+    visibility: ({ controls }) =>
+      !controls?.matrixify_fit_columns_dynamically?.value,
+  };
 });
+
+// Cell title control for Matrixify
+sharedControls.matrixify_cell_title_template = {
+  type: 'TextControl',
+  label: t('Title'),
+  description: t(
+    'Customize cell titles using Handlebars template syntax. Available variables: {{rowLabel}}, {{colLabel}}',
+  ),
+  default: '',
+  renderTrigger: true,
+  visibility: ({ controls }) =>
+    (controls?.matrixify_mode_rows?.value ||
+      controls?.matrixify_mode_columns?.value) !== undefined,
+};
+
+// Matrix display controls
+sharedControls.matrixify_show_row_labels = {
+  type: 'CheckboxControl',
+  label: t('Show row labels'),
+  description: t('Display labels for each row on the left side of the matrix'),
+  default: true,
+  renderTrigger: true,
+  visibility: ({ controls }) =>
+    (controls?.matrixify_mode_rows?.value ||
+      controls?.matrixify_mode_columns?.value) !== undefined,
+};
+
+sharedControls.matrixify_show_column_headers = {
+  type: 'CheckboxControl',
+  label: t('Show column headers'),
+  description: t('Display headers for each column at the top of the matrix'),
+  default: true,
+  renderTrigger: true,
+  visibility: ({ controls }) =>
+    (controls?.matrixify_mode_rows?.value ||
+      controls?.matrixify_mode_columns?.value) !== undefined,
+};
+
 export default sharedControls;
diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
index de2c66204f..2ac0540424 100644
--- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
+++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
@@ -49,6 +49,7 @@ export enum FeatureFlag {
   FilterBarClosedByDefault = 'FILTERBAR_CLOSED_BY_DEFAULT',
   GlobalAsyncQueries = 'GLOBAL_ASYNC_QUERIES',
   ListviewsDefaultCardView = 'LISTVIEWS_DEFAULT_CARD_VIEW',
+  Matrixify = 'MATRIXIFY',
   ScheduledQueries = 'SCHEDULED_QUERIES',
   SqllabBackendPersistence = 'SQLLAB_BACKEND_PERSISTENCE',
   SqlValidatorsByEngine = 'SQL_VALIDATORS_BY_ENGINE',
diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
index 90c1ff77d2..7431718fa3 100644
--- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
+++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx
@@ -41,6 +41,8 @@ import {
   JsonValue,
   NO_TIME_RANGE,
   usePrevious,
+  isFeatureEnabled,
+  FeatureFlag,
 } from '@superset-ui/core';
 import {
   ControlPanelSectionConfig,
@@ -60,7 +62,7 @@ import {
   Collapse,
   Modal,
   Loading,
-  Tag,
+  Label,
   Tooltip,
 } from '@superset-ui/core/components';
 import Tabs from '@superset-ui/core/components/Tabs';
@@ -737,6 +739,30 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
   }
 
   const showCustomizeTab = customizeSections.length > 0;
+  const showMatrixifyTab = isFeatureEnabled(FeatureFlag.Matrixify);
+
+  // Create Matrixify tab label with Beta tag
+  const matrixifyTabLabel = (
+    <span>
+      {t('Matrixify')}{' '}
+      <Tooltip
+        title={t(
+          'This feature is experimental and may change or have limitations',
+        )}
+        placement="top"
+      >
+        <Label
+          type="info"
+          css={css`
+            margin-left: ${theme.sizeUnit}px;
+            font-size: ${theme.fontSizeSM}px;
+          `}
+        >
+          {t('beta')}
+        </Label>
+      </Tooltip>
+    </span>
+  );
 
   return (
     <Styles ref={containerRef}>
@@ -780,36 +806,25 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
                 },
               ]
             : []),
-          {
-            key: TABS_KEYS.MATRIXIFY,
-            label: t('Matrixify'),
-            children: (
-              <>
-                {/* Beta tag with proper padding/margins */}
-                <div
-                  css={css`
-                    padding: ${theme.sizeUnit * 2}px ${theme.sizeUnit * 4}px;
-                  `}
-                >
-                  <Tooltip
-                    title={t(
-                      'This feature is experimental and may change or have limitations',
-                    )}
-                    placement="right"
-                  >
-                    <Tag color="warning">{t('Beta!')}</Tag>
-                  </Tooltip>
-                </div>
-                <Collapse
-                  defaultActiveKey={expandedMatrixifySections}
-                  expandIconPosition="right"
-                  ghost
-                  bordered
-                  items={[...matrixifySections.map(renderControlPanelSection)]}
-                />
-              </>
-            ),
-          },
+          ...(showMatrixifyTab
+            ? [
+                {
+                  key: TABS_KEYS.MATRIXIFY,
+                  label: matrixifyTabLabel,
+                  children: (
+                    <Collapse
+                      defaultActiveKey={expandedMatrixifySections}
+                      expandIconPosition="right"
+                      ghost
+                      bordered
+                      items={[
+                        ...matrixifySections.map(renderControlPanelSection),
+                      ]}
+                    />
+                  ),
+                },
+              ]
+            : []),
         ]}
       />
       <div css={actionButtonsContainerStyles}>
diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx b/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx
index 49d9b02a63..c19a70704e 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.jsx
@@ -242,20 +242,50 @@ const ExploreChartPanel = ({
         {chartPanelWidth &&
           chartPanelHeight &&
           (matrixifyGrid ? (
-            <MatrixifyGridRenderer
-              grid={matrixifyGrid}
-              chart={chart}
-              actions={actions}
-              datasource={datasource}
-              vizType={vizType}
-              onQuery={onQuery}
-              timeout={timeout}
-              ownState={ownState}
-              triggerRender={triggerRender}
-              force={force}
-              chartIsStale={chartIsStale}
-              errorMessage={errorMessage}
-            />
+            <>
+              {/* Hidden ChartContainer to maintain Redux state flow */}
+              <div style={{ display: 'none' }}>
+                <ChartContainer
+                  width={100}
+                  height={100}
+                  ownState={ownState}
+                  annotationData={chart.annotationData}
+                  chartAlert={chart.chartAlert}
+                  chartStackTrace={chart.chartStackTrace}
+                  chartId={chart.id}
+                  chartStatus={chart.chartStatus}
+                  triggerRender={triggerRender}
+                  force={force}
+                  datasource={datasource}
+                  errorMessage={errorMessage}
+                  formData={formData}
+                  latestQueryFormData={chart.latestQueryFormData}
+                  onQuery={onQuery}
+                  queriesResponse={chart.queriesResponse}
+                  chartIsStale={chartIsStale}
+                  setControlValue={actions.setControlValue}
+                  timeout={timeout}
+                  triggerQuery={chart.triggerQuery}
+                  vizType={vizType}
+                />
+              </div>
+              <MatrixifyGridRenderer
+                grid={matrixifyGrid}
+                chart={chart}
+                actions={actions}
+                datasource={datasource}
+                vizType={vizType}
+                onQuery={onQuery}
+                timeout={timeout}
+                ownState={ownState}
+                triggerRender={triggerRender}
+                force={force}
+                chartIsStale={chartIsStale}
+                errorMessage={errorMessage}
+                queriesData={chart.queriesResponse}
+                chartStatus={chart.chartStatus}
+              />
+            </>
           ) : (
             <ChartContainer
               width={Math.floor(chartPanelWidth)}
diff --git a/superset-frontend/src/explore/components/MatrixifyGrid/MatrixifyGridCell.tsx b/superset-frontend/src/explore/components/MatrixifyGrid/MatrixifyGridCell.tsx
new file mode 100644
index 0000000000..1f9161a224
--- /dev/null
+++ b/superset-frontend/src/explore/components/MatrixifyGrid/MatrixifyGridCell.tsx
@@ -0,0 +1,135 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { memo } from 'react';
+import { styled, SuperChart } from '@superset-ui/core';
+import { Card } from '@superset-ui/core/components';
+import { MatrixifyGridCell as GridCellData } from 'src/explore/types/matrixify';
+
+const StyledCard = styled(Card)`
+  height: 100%;
+
+  .ant-card-body {
+    padding: 0;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+  }
+
+  /* Remove any padding/margins that might be causing title height issues */
+  & .chart-container {
+    padding-top: 0 !important;
+  }
+
+  /* Target title elements inside the chart container */
+  & .superchart-container .header-title,
+  & .superchart-container [class*='title'] {
+    display: none !important;
+  }
+`;
+
+interface MatrixifyGridCellProps {
+  cell: GridCellData;
+  rowHeight: number;
+}
+
+/**
+ * Individual grid cell component - memoized to prevent unnecessary re-renders
+ */
+const MatrixifyGridCell = memo(
+  ({ cell, rowHeight }: MatrixifyGridCellProps) => {
+    // Use computed title from template (will be empty string if no template)
+    const cellLabel = cell.title || '';
+
+    // Only show label if it has content
+    const showLabel = cellLabel && cellLabel.trim() !== '';
+
+    const cardProps: any = {
+      className: 'matrixify-cell',
+      size: 'small',
+      'data-row': cell.row,
+      'data-col': cell.col,
+      'data-row-label': cell.rowLabel,
+      'data-col-label': cell.colLabel,
+      styles: {
+        body: {
+          padding: 0,
+          height: '100%',
+        },
+      },
+    };
+
+    // Only add title and header styles if we have a label
+    if (showLabel) {
+      cardProps.title = cellLabel;
+      cardProps.styles.header = {
+        minHeight: 'auto',
+        padding: '4px 8px',
+        fontSize: '12px',
+      };
+      cardProps.styles.body.height = 'calc(100% - 32px)';
+    }
+
+    return (
+      <StyledCard {...cardProps}>
+        <SuperChart
+          chartType={cell.formData.viz_type}
+          width="100%"
+          height="100%"
+          formData={cell.formData}
+          queriesData={[]}
+          enableNoResults={false}
+        />
+      </StyledCard>
+    );
+  },
+  // Custom comparison function to prevent unnecessary re-renders
+  // Returns true to skip re-render, false to re-render
+  (prevProps, nextProps) => {
+    // Always re-render if formData changes
+    if (
+      JSON.stringify(prevProps.cell.formData) !==
+      JSON.stringify(nextProps.cell.formData)
+    ) {
+      return false;
+    }
+
+    // Re-render if rowHeight changes
+    if (prevProps.rowHeight !== nextProps.rowHeight) {
+      return false;
+    }
+
+    // Re-render if cell position changes (shouldn't happen, but just in case)
+    if (prevProps.cell.id !== nextProps.cell.id) {
+      return false;
+    }
+
+    // Re-render if title changes
+    if (prevProps.cell.title !== nextProps.cell.title) {
+      return false;
+    }
+
+    // Skip re-render if nothing important changed
+    return true;
+  },
+);
+
+MatrixifyGridCell.displayName = 'MatrixifyGridCell';
+
+export default MatrixifyGridCell;
diff --git a/superset-frontend/src/explore/components/MatrixifyGrid/MatrixifyGridGenerator.ts b/superset-frontend/src/explore/components/MatrixifyGrid/MatrixifyGridGenerator.ts
index 196d309d20..fad92cdd19 100644
--- a/superset-frontend/src/explore/components/MatrixifyGrid/MatrixifyGridGenerator.ts
+++ b/superset-frontend/src/explore/components/MatrixifyGrid/MatrixifyGridGenerator.ts
@@ -25,6 +25,7 @@ import {
   getMatrixifyConfig,
   MatrixifyAxisConfig,
 } from 'src/explore/types/matrixify';
+import { generateCellTitle } from './templateUtils';
 
 /**
  * Extract label from a metric or dimension value
@@ -72,13 +73,35 @@ function generateCellFormData(
   // Clone base form data without matrixify fields
   const cellFormData: any = { ...baseFormData };
 
-  // Remove all matrixify-specific fields
+  // Remove all matrixify-specific fields and fields that cause title rendering
+  const fieldsToRemove = [
+    'slice_name',
+    'slice_id',
+    'annotation_layers',
+    'cache_timeout',
+    'url_params',
+  ];
   Object.keys(cellFormData).forEach(key => {
-    if (key.startsWith('matrixify_')) {
+    if (key.startsWith('matrixify_') || fieldsToRemove.includes(key)) {
       delete cellFormData[key];
     }
   });
 
+  // Remove any title/header related settings
+  const titleRelatedFields = [
+    'header_font_size',
+    'subheader',
+    'show_title',
+    'title',
+    'header_title_text_align',
+    'header_text',
+  ];
+  titleRelatedFields.forEach(field => {
+    if (field in cellFormData) {
+      delete cellFormData[field];
+    }
+  });
+
   // Add filters for dimension-based axes
   const additionalFilters: AdhocFilter[] = [];
 
@@ -116,6 +139,8 @@ function generateCellFormData(
       ...(cellFormData.adhoc_filters || []),
       ...additionalFilters,
     ];
+
+    // Debug: Log the filters being applied
   }
 
   // Set metrics based on row/column configuration
@@ -223,12 +248,20 @@ export function generateMatrixifyGrid(
         colCount > 1 ? col : null,
       );
 
+      // Generate title using template if provided
+      const title = generateCellTitle(
+        rowLabel,
+        colLabel,
+        formData.matrixify_cell_title_template,
+      );
+
       rowCells.push({
         id,
         row,
         col,
         rowLabel,
         colLabel,
+        title,
         formData: cellFormData,
       });
     }
diff --git a/superset-frontend/src/explore/components/MatrixifyGrid/MatrixifyGridRenderer.tsx b/superset-frontend/src/explore/components/MatrixifyGrid/MatrixifyGridRenderer.tsx
index 2b210200fa..6fa43996a4 100644
--- a/superset-frontend/src/explore/components/MatrixifyGrid/MatrixifyGridRenderer.tsx
+++ b/superset-frontend/src/explore/components/MatrixifyGrid/MatrixifyGridRenderer.tsx
@@ -19,33 +19,69 @@
 
 import { useMemo } from 'react';
 import { css, styled, t, useTheme } from '@superset-ui/core';
-import { Row, Col } from '@superset-ui/core/components';
-import ChartContainer from 'src/components/Chart/ChartContainer';
 import { MatrixifyGrid } from 'src/explore/types/matrixify';
+import MatrixifyGridCell from './MatrixifyGridCell';
 
 const GridContainer = styled.div`
   width: 100%;
   height: 100%;
-  overflow: auto;
+  display: flex;
+  flex-direction: column;
   padding: ${({ theme }) => theme.sizeUnit * 2}px;
+  box-sizing: border-box;
+  min-height: 0; /* Prevent flex item from forcing min height */
+`;
+
+const GridLayout = styled.div<{
+  columns: number;
+  hasRowHeaders: boolean;
+  rowHeight: number;
+  hasColumnHeaders: boolean;
+}>`
+  display: grid;
+  grid-template-columns: ${({ columns, hasRowHeaders }) =>
+    hasRowHeaders
+      ? `40px repeat(${columns}, minmax(0, 1fr))`
+      : `repeat(${columns}, minmax(0, 1fr))`};
+  ${({ hasColumnHeaders, rowHeight }) =>
+    hasColumnHeaders
+      ? `grid-template-rows: 40px; grid-auto-rows: ${rowHeight}px;`
+      : `grid-auto-rows: ${rowHeight}px;`}
+  gap: ${({ theme }) =>
+    theme.sizeUnit * 2}px; /* 16px to match dashboard grid gutter */
+  width: 100%;
+  overflow: auto;
+  flex: 1;
+  min-width: 0;
+  min-height: 0;
 `;
 
 const GridHeader = styled.div`
   background-color: ${({ theme }) => theme.colors.grayscale.light3};
-  padding: ${({ theme }) => theme.sizeUnit * 2}px;
+  padding: ${({ theme }) => theme.sizeUnit}px;
   font-weight: ${({ theme }) => theme.fontWeightStrong};
   text-align: center;
   border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-`;
+  font-size: ${({ theme }) => theme.fontSizeSM}px;
 
-const GridCell = styled.div`
-  border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
-  padding: ${({ theme }) => theme.sizeUnit}px;
-  min-height: 300px;
-  position: relative;
+  &.matrixify-row-header {
+    writing-mode: vertical-rl;
+    transform: rotate(-180deg);
+    padding: ${({ theme }) => theme.sizeUnit * 2}px
+      ${({ theme }) => theme.sizeUnit / 2}px;
+    min-width: 30px;
+    max-width: 40px;
+  }
+
+  &.matrixify-col-header {
+    height: 40px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
 `;
 
 interface MatrixifyGridRendererProps {
@@ -61,6 +97,8 @@ interface MatrixifyGridRendererProps {
   force?: boolean;
   chartIsStale?: boolean;
   errorMessage?: string;
+  queriesData?: any[]; // Query data from parent
+  chartStatus?: string; // Chart status from parent
 }
 
 export default function MatrixifyGridRenderer({
@@ -76,141 +114,106 @@ export default function MatrixifyGridRenderer({
   force,
   chartIsStale,
   errorMessage,
+  queriesData,
+  chartStatus,
 }: MatrixifyGridRendererProps) {
   const theme = useTheme();
 
-  // Calculate column width based on number of columns
-  const colSpan = useMemo(() => {
-    if (grid.colHeaders.length <= 1) return 24;
-    if (grid.colHeaders.length === 2) return 12;
-    if (grid.colHeaders.length === 3) return 8;
-    if (grid.colHeaders.length === 4) return 6;
-    return 6; // For 5+ columns, stick with span 6
-  }, [grid.colHeaders.length]);
-
-  // Render column headers
-  const columnHeaders = useMemo(() => {
-    if (grid.colHeaders.length === 1 && grid.colHeaders[0] === '') {
-      return null; // Don't show empty header for single column
+  // Extract layout settings from base form data
+  const rowHeight = grid.baseFormData.matrixify_row_height || 300;
+  const fitColumnsDynamically =
+    grid.baseFormData.matrixify_fit_columns_dynamically !== false;
+  const chartsPerRow = grid.baseFormData.matrixify_charts_per_row || 4;
+  const showRowLabels = grid.baseFormData.matrixify_show_row_labels !== false;
+  const showColumnHeaders =
+    grid.baseFormData.matrixify_show_column_headers !== false;
+
+  // Determine if we should display row headers
+  const hasRowData = grid.rowHeaders.length > 1 && grid.rowHeaders[0] !== '';
+  const hasRowHeaders = hasRowData && showRowLabels;
+
+  // Calculate number of columns for CSS Grid
+  const gridColumns = useMemo(() => {
+    if (!fitColumnsDynamically) {
+      return chartsPerRow;
     }
+    return grid.colHeaders.length || 1;
+  }, [grid.colHeaders.length, fitColumnsDynamically, chartsPerRow]);
+
+  // Render all grid cells in a single flat array for CSS Grid
+  const gridContent = useMemo(() => {
+    const cells: React.ReactNode[] = [];
 
+    // Add column headers if enabled
+    if (showColumnHeaders && grid.colHeaders.length > 0) {
+      // Add empty cell for row header column if needed
+      if (hasRowHeaders) {
+        cells.push(<div key="empty-corner" />);
+      }
+
+      // Add column headers
+      grid.colHeaders.forEach((header, colIdx) => {
+        cells.push(
+          <GridHeader
+            key={`col-header-${colIdx}`}
+            className="matrixify-col-header"
+            title={header}
+          >
+            {header}
+          </GridHeader>,
+        );
+      });
+    }
+
+    // Add rows with cells
+    grid.cells.forEach((row, rowIdx) => {
+      // Add row header if present
+      if (hasRowHeaders) {
+        cells.push(
+          <GridHeader
+            key={`row-header-${rowIdx}`}
+            className="matrixify-row-header"
+            title={grid.rowHeaders[rowIdx]}
+            css={css`
+              height: ${rowHeight}px;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+            `}
+          >
+            {grid.rowHeaders[rowIdx]}
+          </GridHeader>,
+        );
+      }
+
+      // Add cells
+      row.forEach(cell => {
+        if (!cell) return;
+        cells.push(
+          <MatrixifyGridCell key={cell.id} cell={cell} rowHeight={rowHeight} />,
+        );
+      });
+    });
+
+    return cells;
+  }, [grid, hasRowHeaders, rowHeight, showColumnHeaders]);
+
+  // Don't render the grid if we have no query data at all
+  if (!queriesData || queriesData.length === 0) {
     return (
-      <Row gutter={0}>
-        {/* Empty cell for row header column */}
-        {grid.rowHeaders.length > 1 && grid.rowHeaders[0] !== '' && (
-          <Col span={4}>
-            <div style={{ height: theme.sizeUnit * 8 }} />
-          </Col>
-        )}
-        {/* Column headers */}
-        {grid.colHeaders.map((header, idx) => (
-          <Col key={`col-header-${idx}`} span={colSpan}>
-            <GridHeader title={header}>{header}</GridHeader>
-          </Col>
-        ))}
-      </Row>
+      <div
+        css={css`
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          height: 100%;
+          color: ${theme.colors.grayscale.base};
+        `}
+      >
+        {t('No data to display')}
+      </div>
     );
-  }, [grid.colHeaders, grid.rowHeaders, colSpan, theme.sizeUnit]);
-
-  // Render grid rows
-  const gridRows = useMemo(
-    () =>
-      grid.cells.map((row, rowIdx) => (
-        <Row key={`row-${rowIdx}`} gutter={0}>
-          {/* Row header */}
-          {grid.rowHeaders.length > 1 && grid.rowHeaders[0] !== '' && (
-            <Col span={4}>
-              <GridHeader
-                title={grid.rowHeaders[rowIdx]}
-                css={css`
-                  height: 100%;
-                  display: flex;
-                  align-items: center;
-                  justify-content: center;
-                `}
-              >
-                {grid.rowHeaders[rowIdx]}
-              </GridHeader>
-            </Col>
-          )}
-          {/* Grid cells */}
-          {row.map((cell, colIdx) => {
-            if (!cell) return null;
-
-            return (
-              <Col key={cell.id} span={colSpan}>
-                <GridCell>
-                  <div
-                    css={css`
-                      position: absolute;
-                      top: ${theme.sizeUnit}px;
-                      left: ${theme.sizeUnit}px;
-                      right: ${theme.sizeUnit}px;
-                      font-size: ${theme.fontSize}px;
-                      color: ${theme.colors.grayscale.base};
-                      text-align: center;
-                      z-index: 1;
-                    `}
-                  >
-                    {cell.rowLabel &&
-                    cell.colLabel &&
-                    cell.rowLabel !== cell.colLabel
-                      ? `${cell.rowLabel} × ${cell.colLabel}`
-                      : cell.rowLabel || cell.colLabel}
-                  </div>
-                  <div
-                    css={css`
-                      width: 100%;
-                      height: 300px;
-                      margin-top: ${theme.sizeUnit * 4}px;
-                    `}
-                  >
-                    <ChartContainer
-                      width={300} // Will be responsive based on container
-                      height={250}
-                      ownState={ownState}
-                      annotationData={chart.annotationData}
-                      chartAlert={chart.chartAlert}
-                      chartStackTrace={chart.chartStackTrace}
-                      chartId={`${chart.id}-${cell.id}`}
-                      chartStatus={chart.chartStatus}
-                      triggerRender={triggerRender}
-                      force={force}
-                      datasource={datasource}
-                      errorMessage={errorMessage}
-                      formData={cell.formData}
-                      onQuery={onQuery}
-                      queriesResponse={chart.queriesResponse}
-                      chartIsStale={chartIsStale}
-                      setControlValue={actions.setControlValue}
-                      timeout={timeout}
-                      triggerQuery={chart.triggerQuery}
-                      vizType={vizType}
-                    />
-                  </div>
-                </GridCell>
-              </Col>
-            );
-          })}
-        </Row>
-      )),
-    [
-      grid,
-      chart,
-      actions,
-      datasource,
-      vizType,
-      onQuery,
-      timeout,
-      ownState,
-      triggerRender,
-      force,
-      chartIsStale,
-      errorMessage,
-      colSpan,
-      theme,
-    ],
-  );
+  }
 
   if (grid.cells.length === 0) {
     return (
@@ -229,9 +232,19 @@ export default function MatrixifyGridRenderer({
   }
 
   return (
-    <GridContainer>
-      {columnHeaders}
-      {gridRows}
+    <GridContainer className="matrixify-container">
+      <GridLayout
+        className="matrixify-grid"
+        columns={gridColumns}
+        hasRowHeaders={hasRowHeaders}
+        hasColumnHeaders={showColumnHeaders && grid.colHeaders.length > 0}
+        rowHeight={rowHeight}
+        data-columns={gridColumns}
+        data-has-row-headers={hasRowHeaders}
+        data-total-cells={grid.cells.flat().length}
+      >
+        {gridContent}
+      </GridLayout>
     </GridContainer>
   );
 }
diff --git a/superset-frontend/src/explore/components/MatrixifyGrid/templateUtils.ts b/superset-frontend/src/explore/components/MatrixifyGrid/templateUtils.ts
new file mode 100644
index 0000000000..ec7743dbd2
--- /dev/null
+++ b/superset-frontend/src/explore/components/MatrixifyGrid/templateUtils.ts
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import Handlebars from 'handlebars';
+
+/**
+ * Process a Handlebars template with the given context
+ * @param template The Handlebars template string
+ * @param context The context object with variables
+ * @returns The processed string or error message
+ */
+export function processHandlebarsTemplate(
+  template: string | undefined,
+  context: { rowLabel: string; colLabel: string },
+): string {
+  // If no template provided, return empty string (no title)
+  if (!template || template.trim() === '') {
+    return '';
+  }
+
+  try {
+    const compiledTemplate = Handlebars.compile(template);
+    return compiledTemplate(context);
+  } catch (error) {
+    // If template compilation fails, return empty string
+    return '';
+  }
+}
+
+/**
+ * Generate a cell title using the template or default formatting
+ * @param rowLabel The row label
+ * @param colLabel The column label
+ * @param template Optional Handlebars template
+ * @returns The formatted title
+ */
+export function generateCellTitle(
+  rowLabel: string,
+  colLabel: string,
+  template?: string,
+): string {
+  return processHandlebarsTemplate(template, { rowLabel, colLabel });
+}
diff --git a/superset-frontend/src/explore/controlPanels/sections.tsx b/superset-frontend/src/explore/controlPanels/sections.tsx
index c05f76fe7b..d426de8d31 100644
--- a/superset-frontend/src/explore/controlPanels/sections.tsx
+++ b/superset-frontend/src/explore/controlPanels/sections.tsx
@@ -277,21 +277,51 @@ export const NVD3TimeSeries: ControlPanelSectionConfig[] = [
 function buildMatrixifySection(
   axis: 'columns' | 'rows',
 ): ControlPanelSectionConfig {
+  const baseControls = [
+    [`matrixify_mode_${axis}`],
+    [`matrixify_${axis}`],
+    [`matrixify_dimension_selection_mode_${axis}`],
+    [`matrixify_dimension_${axis}`],
+    [`matrixify_topn_value_${axis}`],
+    [`matrixify_topn_metric_${axis}`],
+    [`matrixify_topn_order_${axis}`],
+  ];
+
+  // Add specific controls for each axis
+  if (axis === 'rows') {
+    // Add row height control at the beginning
+    baseControls.unshift(['matrixify_row_height']);
+  } else if (axis === 'columns') {
+    // Add fit columns control at the beginning
+    baseControls.unshift(['matrixify_fit_columns_dynamically']);
+    // Add charts per row after fit columns control (it will be second)
+    baseControls.splice(1, 0, ['matrixify_charts_per_row']);
+  }
+
   return {
-    label: axis === 'columns' ? t('Matrix Columns') : t('Matrix Rows'),
+    label: axis === 'columns' ? t('Columns') : t('Rows'),
     expanded: true,
     tabOverride: 'matrixify',
-    controlSetRows: [
-      [`matrixify_mode_${axis}`],
-      [`matrixify_${axis}`],
-      [`matrixify_dimension_selection_mode_${axis}`],
-      [`matrixify_dimension_${axis}`],
-      [`matrixify_topn_value_${axis}`],
-      [`matrixify_topn_metric_${axis}`],
-      [`matrixify_topn_order_${axis}`],
-    ],
+    controlSetRows: baseControls,
   };
 }
 
 export const matrixifyRows = buildMatrixifySection('rows');
 export const matrixifyColumns = buildMatrixifySection('columns');
+
+export const matrixifyCells: ControlPanelSectionConfig = {
+  label: t('Cells'),
+  expanded: true,
+  tabOverride: 'matrixify',
+  controlSetRows: [['matrixify_cell_title_template']],
+};
+
+export const matrixifyMatrix: ControlPanelSectionConfig = {
+  label: t('Matrix'),
+  expanded: true,
+  tabOverride: 'matrixify',
+  controlSetRows: [
+    ['matrixify_show_row_labels'],
+    ['matrixify_show_column_headers'],
+  ],
+};
diff --git a/superset-frontend/src/explore/controlUtils/getSectionsToRender.ts b/superset-frontend/src/explore/controlUtils/getSectionsToRender.ts
index 77221fc66d..2d7a5c3b44 100644
--- a/superset-frontend/src/explore/controlUtils/getSectionsToRender.ts
+++ b/superset-frontend/src/explore/controlUtils/getSectionsToRender.ts
@@ -58,7 +58,13 @@ const getMemoizedSectionsToRender = memoizeOne(
       }
     });
 
-    const { datasourceAndVizType, matrixifyRows, matrixifyColumns } = sections;
+    const {
+      datasourceAndVizType,
+      matrixifyRows,
+      matrixifyColumns,
+      matrixifyCells,
+      matrixifyMatrix,
+    } = sections;
 
     // list of datasource-specific controls that should be removed if the datasource is a specific type
     const filterControlsForTypes = [DatasourceType.Query, DatasourceType.Table];
@@ -68,8 +74,10 @@ const getMemoizedSectionsToRender = memoizeOne(
 
     return [
       datasourceAndVizType as ControlPanelSectionConfig,
-      matrixifyRows as ControlPanelSectionConfig,
+      matrixifyCells as ControlPanelSectionConfig,
+      matrixifyMatrix as ControlPanelSectionConfig,
       matrixifyColumns as ControlPanelSectionConfig,
+      matrixifyRows as ControlPanelSectionConfig,
     ]
       .concat(controlPanelSections.filter(isControlPanelSectionConfig))
       .map(section => {
diff --git a/superset-frontend/src/explore/types/matrixify.ts b/superset-frontend/src/explore/types/matrixify.ts
index d1bab8f1fa..20c3929636 100644
--- a/superset-frontend/src/explore/types/matrixify.ts
+++ b/superset-frontend/src/explore/types/matrixify.ts
@@ -89,6 +89,18 @@ export interface MatrixifyFormData {
   matrixify_topn_value_columns?: number;
   matrixify_topn_metric_columns?: AdhocMetric;
   matrixify_topn_order_columns?: MatrixifySortOrder;
+
+  // Grid layout configuration
+  matrixify_row_height?: number;
+  matrixify_fit_columns_dynamically?: boolean;
+  matrixify_charts_per_row?: number;
+
+  // Cell configuration
+  matrixify_cell_title_template?: string;
+
+  // Matrix display configuration
+  matrixify_show_row_labels?: boolean;
+  matrixify_show_column_headers?: boolean;
 }
 
 /**
@@ -179,6 +191,9 @@ export interface MatrixifyGridCell {
   /** Column label (metric name or dimension value) */
   colLabel: string;
 
+  /** Computed title for the cell (from template or default) */
+  title?: string;
+
   /** Form data for this specific cell's chart */
   formData: any; // This will be QueryFormData with appropriate filters/metrics
 }
diff --git a/superset/config.py b/superset/config.py
index 273f2232da..a83c6d5d98 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -615,6 +615,8 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
     "AG_GRID_TABLE_ENABLED": False,
     # Enable Table v2 time comparison feature
     "TABLE_V2_TIME_COMPARISON_ENABLED": False,
+    # Enable Matrixify feature for matrix-style chart layouts
+    "MATRIXIFY": True,
 }
 
 # ------------------------------
diff --git a/superset/models/helpers.py b/superset/models/helpers.py
index 400255dd07..6b3aeb18b8 100644
--- a/superset/models/helpers.py
+++ b/superset/models/helpers.py
@@ -1488,6 +1488,263 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
         col = self.make_sqla_column_compatible(col, label)
         return col
 
+    def _build_metric_expression(
+        self,
+        metric: Metric,
+        columns_by_name: dict[str, "TableColumn"],
+        metrics_by_name: dict[str, "SqlMetric"],
+        template_processor: Optional[BaseTemplateProcessor] = None,
+    ) -> ColumnElement:
+        """Convert a single metric (adhoc or predefined) to SQLAlchemy expression."""
+        if utils.is_adhoc_metric(metric):
+            assert isinstance(metric, dict)
+            return self.adhoc_metric_to_sqla(
+                metric=metric,
+                columns_by_name=columns_by_name,
+                template_processor=template_processor,
+            )
+        elif isinstance(metric, str) and metric in metrics_by_name:
+            return metrics_by_name[metric].get_sqla_col(
+                template_processor=template_processor
+            )
+        else:
+            raise QueryObjectValidationError(
+                _("Metric '%(metric)s' does not exist", metric=metric)
+            )
+
+    def _process_adhoc_sql_expression(
+        self,
+        expression: str,
+        template_processor: Optional[BaseTemplateProcessor] = None,
+    ) -> str:
+        """Process and validate an adhoc SQL expression with template processing."""
+        return self._process_sql_expression(
+            expression=expression,
+            database_id=self.database_id,
+            engine=self.database.backend,
+            schema=self.schema,
+            template_processor=template_processor,
+        )
+
+    def _normalize_column_labels(
+        self,
+        columns: Optional[list[Column]],
+        series_columns: Optional[list[Column]],
+    ) -> list[str]:
+        """Extract and normalize column labels for series columns."""
+        return [
+            self.db_engine_spec.make_label_compatible(column)
+            for column in utils.get_column_names(
+                columns=series_columns or [],
+            )
+        ]
+
+    def _build_top_groups_filter(
+        self,
+        df: pd.DataFrame,
+        groupby_series_columns: dict[str, Any],
+        columns_by_name: dict[str, "TableColumn"],
+    ) -> ColumnElement:
+        """Build SQL filter for top groups based on prequery results."""
+        dimensions = [
+            c
+            for c in df.columns
+            if c in groupby_series_columns
+        ]
+        return self._get_top_groups(
+            df, dimensions, groupby_series_columns, columns_by_name
+        )
+
+    def _get_series_orderby_expression(
+        self,
+        series_limit_metric: Optional[Metric],
+        main_metric_expr: ColumnElement,
+        metrics_by_name: dict[str, "SqlMetric"],
+        columns_by_name: dict[str, "TableColumn"],
+        template_processor: Optional[BaseTemplateProcessor] = None,
+    ) -> ColumnElement:
+        """Get the ORDER BY expression for series limit queries."""
+        if not series_limit_metric:
+            return main_metric_expr
+            
+        return self._get_series_orderby(
+            series_limit_metric=series_limit_metric,
+            metrics_by_name=metrics_by_name,
+            columns_by_name=columns_by_name,
+            template_processor=template_processor,
+        )
+
+    def _normalize_filter_value(
+        self,
+        value: FilterValue,
+        target_generic_type: GenericDataType,
+        operator: utils.FilterOperator,
+    ) -> Optional[FilterValue]:
+        """Normalize a single filter value based on type and operator."""
+        if operator == utils.FilterOperator.TEMPORAL_RANGE:
+            return value
+        if (
+            isinstance(value, (float, int))
+            and target_generic_type == utils.GenericDataType.TEMPORAL
+        ):
+            # Note: This would need db_engine_spec and target_native_type
+            # passed in for full implementation
+            return value
+        if isinstance(value, str):
+            value = value.strip("\t\n")
+            if (
+                target_generic_type == utils.GenericDataType.NUMERIC
+                and operator
+                not in {
+                    utils.FilterOperator.ILIKE,
+                    utils.FilterOperator.LIKE,
+                }
+            ):
+                return utils.cast_to_num(value)
+            if value == NULL_STRING:
+                return None
+            if value == EMPTY_STRING:
+                return ""
+        if target_generic_type == utils.GenericDataType.BOOLEAN:
+            return utils.cast_to_boolean(value)
+        return value
+
+    def _build_time_filter_expression(
+        self,
+        col: ColumnElement,
+        start_dttm: Optional[datetime],
+        end_dttm: Optional[datetime],
+        time_col: "TableColumn",
+    ) -> ColumnElement:
+        """Build time range filter expression."""
+        filters = []
+        if start_dttm:
+            filters.append(
+                col >= self.db_engine_spec.get_text_clause(
+                    self.dttm_sql_literal(start_dttm, time_col)
+                )
+            )
+        if end_dttm:
+            filters.append(
+                col < self.db_engine_spec.get_text_clause(
+                    self.dttm_sql_literal(end_dttm, time_col)
+                )
+            )
+        return and_(*filters) if filters else sa.true()
+
+    def _wrap_query_for_rowcount(
+        self,
+        qry: Select,
+    ) -> tuple[Select, list[str]]:
+        """Wrap a query in COUNT(*) for row count queries."""
+        if not self.db_engine_spec.allows_subqueries:
+            raise QueryObjectValidationError(
+                _("Database does not support subqueries")
+            )
+        label = "rowcount"
+        col = self.make_sqla_column_compatible(literal_column("COUNT(*)"), label)
+        wrapped_qry = sa.select([col]).select_from(qry.alias("rowcount_qry"))
+        labels_expected = [label]
+        return wrapped_qry, labels_expected
+
+    def _create_others_case_expression(
+        self,
+        expr: ColumnElement,
+        condition: Any,
+        label: Optional[str] = None,
+    ) -> ColumnElement:
+        """Create CASE expression for 'Others' grouping."""
+        case_expr = sa.case([(condition, expr)], else_=sa.literal("Others"))
+        if label:
+            case_expr = self.make_sqla_column_compatible(case_expr, label)
+        return case_expr
+
+    def _apply_advanced_data_type_filter(
+        self,
+        sqla_col: Column,
+        col_advanced_data_type: str,
+        operator: utils.FilterOperator,
+        values: FilterValues,
+    ) -> ColumnElement:
+        """Apply advanced data type filtering if enabled."""
+        if (
+            col_advanced_data_type != ""
+            and feature_flag_manager.is_feature_enabled(
+                "ENABLE_ADVANCED_DATA_TYPES"
+            )
+            and col_advanced_data_type in ADVANCED_DATA_TYPES
+        ):
+            is_list_target = operator in (
+                utils.FilterOperator.IN,
+                utils.FilterOperator.NOT_IN,
+            )
+            values_list = values if is_list_target else [values]  # type: ignore
+            bus_resp: AdvancedDataTypeResponse = ADVANCED_DATA_TYPES[
+                col_advanced_data_type
+            ].translate_type(
+                {
+                    "type": col_advanced_data_type,
+                    "values": values_list,
+                }
+            )
+            if bus_resp["error_message"]:
+                raise AdvancedDataTypeResponseError(
+                    _(bus_resp["error_message"])
+                )
+
+            return ADVANCED_DATA_TYPES[col_advanced_data_type].translate_filter(
+                sqla_col, operator, bus_resp["values"]
+            )
+        else:
+            raise ValueError("Advanced data type not supported or not enabled")
+
+    def _apply_orderby_direction(
+        self,
+        qry: Select,
+        orderby_exprs: list[ColumnElement],
+        orderby: list[OrderBy],
+        select_exprs: list[Union[Column, Label]],
+    ) -> Select:
+        """Apply ORDER BY with proper direction and column handling."""
+        for col, (_orig_col, ascending) in zip(orderby_exprs, orderby, strict=False):  # noqa: B007
+            if not self.db_engine_spec.allows_alias_in_orderby and isinstance(col, Label):
+                # if engine does not allow using SELECT alias in ORDER BY
+                # revert to the underlying column
+                col = col.element
+
+            if (
+                self.db_engine_spec.get_allows_alias_in_select(self.database)
+                and self.db_engine_spec.allows_hidden_cc_in_orderby
+                and col.name in [select_col.name for select_col in select_exprs]
+            ):
+                with self.database.get_sqla_engine() as engine:
+                    quote = engine.dialect.identifier_preparer.quote
+                    col = literal_column(quote(col.name))
+            direction = sa.asc if ascending else sa.desc
+            qry = qry.order_by(direction(col))
+        return qry
+
+    def _deduplicate_select_columns(
+        self,
+        select_exprs: list[Union[Column, Label]],
+        metrics_exprs: list[ColumnElement],
+        orderby_exprs: list[ColumnElement],
+    ) -> list[Union[Column, Label]]:
+        """Remove duplicate columns from SELECT clause."""
+        # Always remove duplicates by column name, as sometimes `metrics_exprs`
+        # can have the same name as a groupby column (e.g. when users use
+        # raw columns as custom SQL adhoc metric).
+        deduped = remove_duplicates(
+            select_exprs + metrics_exprs, key=lambda x: x.name
+        )
+        
+        # Order by columns are "hidden" columns, some databases require them
+        # always be present in SELECT if an aggregation function is used
+        if not self.db_engine_spec.allows_hidden_orderby_agg:
+            deduped = remove_duplicates(deduped + orderby_exprs)
+            
+        return deduped
+
     def get_sqla_query(  # pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements  # noqa: C901
         self,
         apply_fetch_values_predicate: bool = False,
@@ -1546,12 +1803,7 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
         rejected_adhoc_filters_columns: list[Union[str, ColumnTyping]] = []
         applied_adhoc_filters_columns: list[Union[str, ColumnTyping]] = []
         db_engine_spec = self.db_engine_spec
-        series_column_labels = [
-            db_engine_spec.make_label_compatible(column)
-            for column in utils.get_column_names(
-                columns=series_columns or [],
-            )
-        ]
+        series_column_labels = self._normalize_column_labels(columns, series_columns)
         # deprecated, to be removed in 2.0
         if is_timeseries and timeseries_limit:
             series_limit = timeseries_limit
@@ -1594,25 +1846,14 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
 
         metrics_exprs: list[ColumnElement] = []
         for metric in metrics:
-            if utils.is_adhoc_metric(metric):
-                assert isinstance(metric, dict)
-                metrics_exprs.append(
-                    self.adhoc_metric_to_sqla(
-                        metric=metric,
-                        columns_by_name=columns_by_name,
-                        template_processor=template_processor,
-                    )
-                )
-            elif isinstance(metric, str) and metric in metrics_by_name:
-                metrics_exprs.append(
-                    metrics_by_name[metric].get_sqla_col(
-                        template_processor=template_processor
-                    )
-                )
-            else:
-                raise QueryObjectValidationError(
-                    _("Metric '%(metric)s' does not exist", metric=metric)
+            metrics_exprs.append(
+                self._build_metric_expression(
+                    metric=metric,
+                    columns_by_name=columns_by_name,
+                    metrics_by_name=metrics_by_name,
+                    template_processor=template_processor,
                 )
+            )
 
         if metrics_exprs:
             main_metric_expr = metrics_exprs[0]
@@ -1633,11 +1874,8 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
             if isinstance(col, dict):
                 col = cast(AdhocMetric, col)
                 if col.get("sqlExpression"):
-                    col["sqlExpression"] = self._process_sql_expression(
+                    col["sqlExpression"] = self._process_adhoc_sql_expression(
                         expression=col["sqlExpression"],
-                        database_id=self.database_id,
-                        engine=self.database.backend,
-                        schema=self.schema,
                         template_processor=template_processor,
                     )
                 if utils.is_adhoc_metric(col):
@@ -1787,21 +2025,14 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
             )
             time_filters.append(time_filter_column)
 
-        # Always remove duplicates by column name, as sometimes `metrics_exprs`
-        # can have the same name as a groupby column (e.g. when users use
-        # raw columns as custom SQL adhoc metric).
-        select_exprs = remove_duplicates(
-            select_exprs + metrics_exprs, key=lambda x: x.name
+        # Deduplicate select columns
+        select_exprs = self._deduplicate_select_columns(
+            select_exprs, metrics_exprs, orderby_exprs
         )
 
         # Expected output columns
         labels_expected = [c.key for c in select_exprs]
 
-        # Order by columns are "hidden" columns, some databases require them
-        # always be present in SELECT if an aggregation function is used
-        if not db_engine_spec.allows_hidden_orderby_agg:
-            select_exprs = remove_duplicates(select_exprs + orderby_exprs)
-
         qry = sa.select(select_exprs)
 
         tbl, cte = self.get_from_clause(template_processor)
@@ -1877,25 +2108,15 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
                     )
                     and col_advanced_data_type in ADVANCED_DATA_TYPES
                 ):
-                    values = eq if is_list_target else [eq]  # type: ignore
-                    bus_resp: AdvancedDataTypeResponse = ADVANCED_DATA_TYPES[
-                        col_advanced_data_type
-                    ].translate_type(
-                        {
-                            "type": col_advanced_data_type,
-                            "values": values,
-                        }
-                    )
-                    if bus_resp["error_message"]:
-                        raise AdvancedDataTypeResponseError(
-                            _(bus_resp["error_message"])
-                        )
-
-                    where_clause_and.append(
-                        ADVANCED_DATA_TYPES[col_advanced_data_type].translate_filter(
-                            sqla_col, op, bus_resp["values"]
+                    try:
+                        where_clause_and.append(
+                            self._apply_advanced_data_type_filter(
+                                sqla_col, col_advanced_data_type, op, eq
+                            )
                         )
-                    )
+                    except ValueError:
+                        # Not an advanced data type, continue with regular processing
+                        pass
                 elif is_list_target:
                     assert isinstance(eq, (tuple, list))
                     if len(eq) == 0:
@@ -2031,20 +2252,8 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
 
         self.make_orderby_compatible(select_exprs, orderby_exprs)
 
-        for col, (_orig_col, ascending) in zip(orderby_exprs, orderby, strict=False):  # noqa: B007
-            if not db_engine_spec.allows_alias_in_orderby and isinstance(col, Label):
-                # if engine does not allow using SELECT alias in ORDER BY
-                # revert to the underlying column
-                col = col.element
-
-            if (
-                db_engine_spec.get_allows_alias_in_select(self.database)
-                and db_engine_spec.allows_hidden_cc_in_orderby
-                and col.name in [select_col.name for select_col in select_exprs]
-            ):
-                col = literal_column(quote(col.name))
-            direction = sa.asc if ascending else sa.desc
-            qry = qry.order_by(direction(col))
+        # Apply ORDER BY with proper direction
+        qry = self._apply_orderby_direction(qry, orderby_exprs, orderby, select_exprs)
 
         if row_limit:
             qry = qry.limit(row_limit)
@@ -2082,14 +2291,13 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
                 subq = subq.where(and_(*(where_clause_and + inner_time_filter)))
                 subq = subq.group_by(*inner_groupby_exprs)
 
-                ob = inner_main_metric_expr
-                if series_limit_metric:
-                    ob = self._get_series_orderby(
-                        series_limit_metric=series_limit_metric,
-                        metrics_by_name=metrics_by_name,
-                        columns_by_name=columns_by_name,
-                        template_processor=template_processor,
-                    )
+                ob = self._get_series_orderby_expression(
+                    series_limit_metric=series_limit_metric,
+                    main_metric_expr=inner_main_metric_expr,
+                    metrics_by_name=metrics_by_name,
+                    columns_by_name=columns_by_name,
+                    template_processor=template_processor,
+                )
                 direction = sa.desc if order_desc else sa.asc
                 subq = subq.order_by(direction(ob))
                 subq = subq.limit(series_limit)
@@ -2143,8 +2351,9 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
                 if series_limit_metric:
                     orderby = [
                         (
-                            self._get_series_orderby(
+                            self._get_series_orderby_expression(
                                 series_limit_metric=series_limit_metric,
+                                main_metric_expr=main_metric_expr,
                                 metrics_by_name=metrics_by_name,
                                 columns_by_name=columns_by_name,
                                 template_processor=template_processor,
@@ -2176,8 +2385,8 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
                     for c in result.df.columns
                     if c not in metrics and c in groupby_series_columns
                 ]
-                top_groups = self._get_top_groups(
-                    result.df, dimensions, groupby_series_columns, columns_by_name
+                top_groups = self._build_top_groups_filter(
+                    result.df, groupby_series_columns, columns_by_name
                 )
 
                 if group_others_when_limit_reached:
@@ -2205,14 +2414,7 @@ class ExploreMixin:  # pylint: disable=too-many-public-methods
         qry = qry.select_from(tbl)
 
         if is_rowcount:
-            if not db_engine_spec.allows_subqueries:
-                raise QueryObjectValidationError(
-                    _("Database does not support subqueries")
-                )
-            label = "rowcount"
-            col = self.make_sqla_column_compatible(literal_column("COUNT(*)"), label)
-            qry = sa.select([col]).select_from(qry.alias("rowcount_qry"))
-            labels_expected = [label]
+            qry, labels_expected = self._wrap_query_for_rowcount(qry)
 
         filter_columns = [flt.get("col") for flt in filter] if filter else []
         rejected_filter_columns = [